Fix compare two files on LogDlg compared wrong revision
[TortoiseGit.git] / src / Git / GitStatusListCtrl.cpp
blobee0f2940935f1406f1e3c43c66e81ee5784e4374
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)
276 m_critSec.Init();
277 m_bNoAutoselectMissing = CRegDWORD(L"Software\\TortoiseGit\\AutoselectMissingFiles", FALSE) == TRUE;
280 CGitStatusListCtrl::~CGitStatusListCtrl()
282 ClearStatusArray();
285 void CGitStatusListCtrl::ClearStatusArray()
287 #if 0
288 CAutoWriteLock locker(m_guard);
289 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
291 delete m_arStatusArray[i];
293 m_arStatusArray.clear();
294 #endif
297 void CGitStatusListCtrl::Init(DWORD dwColumns, const CString& sColumnInfoContainer, unsigned __int64 dwContextMenus /* = GitSLC_POPALL */, bool bHasCheckboxes /* = true */, bool bHasWC /* = true */, DWORD allowedColumns /* = 0xffffffff */)
299 CAutoWriteLock locker(m_guard);
301 m_dwDefaultColumns = dwColumns | 1;
302 m_dwContextMenus = dwContextMenus;
303 m_bHasCheckboxes = bHasCheckboxes;
304 m_bHasWC = bHasWC;
305 m_bWaitCursor = true;
307 // set the extended style of the listcontrol
308 DWORD exStyle = LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP | LVS_EX_SUBITEMIMAGES;
309 exStyle |= (bHasCheckboxes ? LVS_EX_CHECKBOXES : 0);
310 SetRedraw(false);
311 SetExtendedStyle(exStyle);
312 CResizableColumnsListCtrl::Init();
314 SetWindowTheme(m_hWnd, L"Explorer", nullptr);
316 m_nIconFolder = SYS_IMAGE_LIST().GetDirIconIndex();
317 m_nRestoreOvl = SYS_IMAGE_LIST().AddIcon((HICON)LoadImage(AfxGetResourceHandle(), MAKEINTRESOURCE(IDI_RESTOREOVL), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
318 SYS_IMAGE_LIST().SetOverlayImage(m_nRestoreOvl, OVL_RESTORE);
319 SetImageList(&SYS_IMAGE_LIST(), LVSIL_SMALL);
321 // keep CSorter::operator() in sync!!
322 static UINT standardColumnNames[GITSLC_NUMCOLUMNS]
323 = { IDS_STATUSLIST_COLFILE
324 , IDS_STATUSLIST_COLFILENAME
325 , IDS_STATUSLIST_COLEXT
326 , IDS_STATUSLIST_COLSTATUS
327 , IDS_STATUSLIST_COLADD
328 , IDS_STATUSLIST_COLDEL
329 , IDS_STATUSLIST_COLLASTMODIFIED
330 , IDS_STATUSLIST_COLSIZE
333 m_ColumnManager.SetNames(standardColumnNames,GITSLC_NUMCOLUMNS);
334 m_ColumnManager.ReadSettings(m_dwDefaultColumns, 0xffffffff & ~(allowedColumns | m_dwDefaultColumns), sColumnInfoContainer, GITSLC_NUMCOLUMNS);
335 m_ColumnManager.SetRightAlign(4);
336 m_ColumnManager.SetRightAlign(5);
337 m_ColumnManager.SetRightAlign(7);
339 // enable file drops
340 if (!m_pDropTarget)
342 m_pDropTarget = std::make_unique<CGitStatusListCtrlDropTarget>(this);
343 RegisterDragDrop(m_hWnd, m_pDropTarget.get());
344 // create the supported formats:
345 FORMATETC ftetc = { 0 };
346 ftetc.dwAspect = DVASPECT_CONTENT;
347 ftetc.lindex = -1;
348 ftetc.tymed = TYMED_HGLOBAL;
349 ftetc.cfFormat = CF_HDROP;
350 m_pDropTarget->AddSuportedFormat(ftetc);
353 SetRedraw(true);
354 m_bWaitCursor = false;
357 bool CGitStatusListCtrl::SetBackgroundImage(UINT nID)
359 m_nBackgroundImageID = nID;
360 return CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), nID);
363 BOOL CGitStatusListCtrl::GetStatus ( const CTGitPathList* pathList
364 , bool bUpdate /* = FALSE */
365 , bool bShowIgnores /* = false */
366 , bool bShowUnRev /* = false */
367 , bool bShowLocalChangesIgnored /* = false */)
369 CAutoWriteLock locker(m_guard);
371 m_bEmpty = false;
372 m_bBusy = true;
373 m_bWaitCursor = true;
374 Invalidate();
376 m_bIsRevertTheirMy = g_Git.IsRebaseRunning() > 0;
378 int mask= CGitStatusListCtrl::FILELIST_MODIFY;
379 if(bShowIgnores)
380 mask|= CGitStatusListCtrl::FILELIST_IGNORE;
381 if(bShowUnRev)
382 mask|= CGitStatusListCtrl::FILELIST_UNVER;
383 if (bShowLocalChangesIgnored)
384 mask |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
385 this->UpdateFileList(mask, bUpdate, pathList);
387 if (pathList && m_setDirectFiles.empty())
389 // remember files which are selected by users so that those can be preselected
390 for (int i = 0; i < pathList->GetCount(); ++i)
391 if (!(*pathList)[i].IsDirectory())
392 m_setDirectFiles.insert((*pathList)[i].GetGitPathString());
395 #if 0
396 int refetchcounter = 0;
397 BOOL bRet = TRUE;
398 Invalidate();
399 // force the cursor to change
400 POINT pt;
401 GetCursorPos(&pt);
402 SetCursorPos(pt.x, pt.y);
404 m_mapFilenameToChecked.clear();
405 //m_StatusUrlList.Clear();
406 bool bHasChangelists = (m_changelists.size() > 1 || (!m_changelists.empty() && !m_bHasIgnoreGroup));
407 m_changelists.clear();
408 for (size_t i=0; i < m_arStatusArray.size(); i++)
410 FileEntry * entry = m_arStatusArray[i];
411 if ( bHasChangelists && entry->checked)
413 // If change lists are present, remember all checked entries
414 CString path = entry->GetPath().GetGitPathString();
415 m_mapFilenameToChecked[path] = true;
417 if ( (entry->status==git_wc_status_unversioned || entry->status==git_wc_status_missing ) && entry->checked )
419 // The user manually selected an unversioned or missing file. We remember
420 // this so that the selection can be restored when refreshing.
421 CString path = entry->GetPath().GetGitPathString();
422 m_mapFilenameToChecked[path] = true;
424 else if ( entry->status > git_wc_status_normal && !entry->checked )
426 // The user manually deselected a versioned file. We remember
427 // this so that the deselection can be restored when refreshing.
428 CString path = entry->GetPath().GetGitPathString();
429 m_mapFilenameToChecked[path] = false;
433 // use a sorted path list to make sure we fetch the status of
434 // parent items first, *then* the child items (if any)
435 CTGitPathList sortedPathList = pathList;
436 sortedPathList.SortByPathname();
439 bRet = TRUE;
440 m_nTargetCount = 0;
441 m_bHasExternalsFromDifferentRepos = FALSE;
442 m_bHasExternals = FALSE;
443 m_bHasUnversionedItems = FALSE;
444 m_bHasChangeLists = false;
445 m_bShowIgnores = bShowIgnores;
446 m_nSortedColumn = 0;
447 m_bBlock = TRUE;
448 m_bBusy = true;
449 m_bEmpty = false;
450 Invalidate();
452 // first clear possible status data left from
453 // previous GetStatus() calls
454 ClearStatusArray();
456 m_StatusFileList = sortedPathList;
458 // Since Git_client_status() returns all files, even those in
459 // folders included with Git:externals we need to check if all
460 // files we get here belongs to the same repository.
461 // It is possible to commit changes in an external folder, as long
462 // as the folder belongs to the same repository (but another path),
463 // but it is not possible to commit all files if the externals are
464 // from a different repository.
466 // To check if all files belong to the same repository, we compare the
467 // UUID's - if they're identical then the files belong to the same
468 // repository and can be committed. But if they're different, then
469 // tell the user to commit all changes in the external folders
470 // first and exit.
471 CStringA sUUID; // holds the repo UUID
472 CTGitPathList arExtPaths; // list of Git:external paths
474 GitConfig config;
476 m_sURL.Empty();
478 m_nTargetCount = sortedPathList.GetCount();
480 GitStatus status(m_pbCanceled);
481 if(m_nTargetCount > 1 && sortedPathList.AreAllPathsFilesInOneDirectory())
483 // This is a special case, where we've been given a list of files
484 // all from one folder
485 // We handle them by setting a status filter, then requesting the Git status of
486 // all the files in the directory, filtering for the ones we're interested in
487 status.SetFilter(sortedPathList);
489 // if all selected entries are files, we don't do a recursive status
490 // fetching. But if only one is a directory, we have to recurse.
491 git_depth_t depth = git_depth_files;
492 // We have set a filter. If all selected items were files or we fetch
493 // the status not recursively, then we have to include
494 // ignored items too - the user has selected them
495 bool bShowIgnoresRight = true;
496 for (int fcindex=0; fcindex<sortedPathList.GetCount(); ++fcindex)
498 if (sortedPathList[fcindex].IsDirectory())
500 depth = git_depth_infinity;
501 bShowIgnoresRight = false;
502 break;
505 if(!FetchStatusForSingleTarget(config, status, sortedPathList.GetCommonDirectory(), bUpdate, sUUID, arExtPaths, true, depth, bShowIgnoresRight))
506 bRet = FALSE;
508 else
510 for(int nTarget = 0; nTarget < m_nTargetCount; nTarget++)
512 // check whether the path we want the status for is already fetched due to status-fetching
513 // of a parent path.
514 // this check is only done for file paths, because folder paths could be included already
515 // but not recursively
516 if (sortedPathList[nTarget].IsDirectory() || !GetListEntry(sortedPathList[nTarget]))
518 if(!FetchStatusForSingleTarget(config, status, sortedPathList[nTarget], bUpdate, sUUID,
519 arExtPaths, false, git_depth_infinity, bShowIgnores))
521 bRet = FALSE;
527 // remove the 'helper' files of conflicted items from the list.
528 // otherwise they would appear as unversioned files.
529 for (INT_PTR cind = 0; cind < m_ConflictFileList.GetCount(); ++cind)
531 for (size_t i=0; i < m_arStatusArray.size(); i++)
533 if (m_arStatusArray[i]->GetPath().IsEquivalentTo(m_ConflictFileList[cind]))
535 delete m_arStatusArray[i];
536 m_arStatusArray.erase(m_arStatusArray.cbegin() + i);
537 break;
541 refetchcounter++;
542 } while(!BuildStatistics() && (refetchcounter < 2) && (*m_pbCanceled==false));
544 m_bBlock = FALSE;
545 m_bBusy = false;
546 GetCursorPos(&pt);
547 SetCursorPos(pt.x, pt.y);
548 return bRet;
549 #endif
550 m_bBusy = false;
551 m_bWaitCursor = false;
552 BuildStatistics();
553 return TRUE;
556 // Get the show-flags bitmap value which corresponds to a particular Git status
557 DWORD CGitStatusListCtrl::GetShowFlagsFromGitStatus(git_wc_status_kind status)
559 switch (status)
561 case git_wc_status_none:
562 case git_wc_status_unversioned:
563 return GITSLC_SHOWUNVERSIONED;
564 case git_wc_status_ignored:
565 if (!m_bShowIgnores)
566 return GITSLC_SHOWDIRECTS;
567 return GITSLC_SHOWDIRECTS|GITSLC_SHOWIGNORED;
568 case git_wc_status_normal:
569 return GITSLC_SHOWNORMAL;
570 case git_wc_status_added:
571 return GITSLC_SHOWADDED;
572 case git_wc_status_deleted:
573 return GITSLC_SHOWREMOVED;
574 case git_wc_status_modified:
575 return GITSLC_SHOWMODIFIED;
576 case git_wc_status_conflicted:
577 return GITSLC_SHOWCONFLICTED;
578 default:
579 // we should NEVER get here!
580 ASSERT(FALSE);
581 break;
583 return 0;
586 void CGitStatusListCtrl::Show(unsigned int dwShow, unsigned int dwCheck /*=0*/, bool /*bShowFolders*/ /* = true */,BOOL UpdateStatusList,bool UseStoredCheckStatus)
588 m_bWaitCursor = true;
589 m_bBusy = true;
590 m_bEmpty = false;
591 Invalidate();
593 WORD langID = (WORD)CRegStdDWORD(L"Software\\TortoiseGit\\LanguageID", GetUserDefaultLangID());
594 SetRedraw(FALSE);
596 CAutoWriteLock locker(m_guard);
597 DeleteAllItems();
598 m_nSelected = 0;
600 m_nShownUnversioned = 0;
601 m_nShownModified = 0;
602 m_nShownAdded = 0;
603 m_nShownDeleted = 0;
604 m_nShownConflicted = 0;
605 m_nShownFiles = 0;
606 m_nShownSubmodules = 0;
608 m_dwShow = dwShow;
610 if (UpdateStatusList)
612 m_arStatusArray.clear();
613 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
614 m_arStatusArray.push_back(&m_StatusFileList[i]);
616 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
617 m_arStatusArray.push_back(&m_UnRevFileList[i]);
619 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
620 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
622 PrepareGroups();
623 m_arListArray.clear();
624 m_arListArray.reserve(m_arStatusArray.size());
625 if (m_nSortedColumn >= 0)
627 CSorter predicate(&m_ColumnManager, m_nSortedColumn, m_bAscending);
628 std::stable_sort(m_arStatusArray.begin(), m_arStatusArray.end(), predicate);
631 int index = 0;
632 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
634 //set default checkbox status
635 auto entry = const_cast<CTGitPath*>(m_arStatusArray[i]);
636 CString path = entry->GetGitPathString();
637 if (!m_mapFilenameToChecked.empty() && m_mapFilenameToChecked.find(path) != m_mapFilenameToChecked.end())
638 entry->m_Checked = m_mapFilenameToChecked[path];
639 else if (!UseStoredCheckStatus)
641 bool autoSelectSubmodules = !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules);
642 if (((entry->m_Action & dwCheck) && !(m_bNoAutoselectMissing && entry->m_Action & CTGitPath::LOGACTIONS_MISSING) || dwShow & GITSLC_SHOWDIRECTFILES && m_setDirectFiles.find(path) != m_setDirectFiles.end()) && autoSelectSubmodules)
643 entry->m_Checked = true;
644 else
645 entry->m_Checked = false;
646 m_mapFilenameToChecked[path] = entry->m_Checked;
649 if (entry->m_Action & dwShow)
651 AddEntry(entry, langID, index);
652 index++;
657 AdjustColumnWidths();
659 SetRedraw(TRUE);
660 GetStatisticsString();
662 CHeaderCtrl * pHeader = GetHeaderCtrl();
663 HDITEM HeaderItem = {0};
664 HeaderItem.mask = HDI_FORMAT;
665 for (int i=0; i<pHeader->GetItemCount(); ++i)
667 pHeader->GetItem(i, &HeaderItem);
668 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
669 pHeader->SetItem(i, &HeaderItem);
671 if (m_nSortedColumn >= 0)
673 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
674 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
675 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
678 RestoreScrollPos();
680 m_bWaitCursor = false;
681 m_bBusy = false;
682 m_bEmpty = (GetItemCount() == 0);
683 Invalidate();
685 this->BuildStatistics();
687 #if 0
689 m_bShowFolders = bShowFolders;
691 int nTopIndex = GetTopIndex();
692 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
693 int nSelectedEntry = 0;
694 if (posSelectedEntry)
695 nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
696 SetRedraw(FALSE);
697 DeleteAllItems();
699 PrepareGroups();
701 m_arListArray.clear();
703 m_arListArray.reserve(m_arStatusArray.size());
704 SetItemCount (static_cast<int>(m_arStatusArray.size()));
706 int listIndex = 0;
707 for (size_t i=0; i < m_arStatusArray.size(); ++i)
709 FileEntry * entry = m_arStatusArray[i];
710 if ((entry->inexternal) && (!(dwShow & SVNSLC_SHOWINEXTERNALS)))
711 continue;
712 if ((entry->differentrepo || entry->isNested) && (! (dwShow & SVNSLC_SHOWEXTERNALFROMDIFFERENTREPO)))
713 continue;
714 if (entry->IsFolder() && (!bShowFolders))
715 continue; // don't show folders if they're not wanted.
717 #if 0
718 git_wc_status_kind status = GitStatus::GetMoreImportant(entry->status, entry->remotestatus);
719 DWORD showFlags = GetShowFlagsFromGitStatus(status);
720 if (entry->switched)
721 showFlags |= SVNSLC_SHOWSWITCHED;
722 if (!entry->changelist.IsEmpty())
723 showFlags |= SVNSLC_SHOWINCHANGELIST;
724 #endif
725 bool bAllowCheck = ((entry->changelist.Compare(GITSLC_IGNORECHANGELIST) != 0)
726 && (m_bCheckIfGroupsExist || (m_changelists.empty() || (m_changelists.size() == 1 && m_bHasIgnoreGroup))));
728 // status_ignored is a special case - we must have the 'direct' flag set to add a status_ignored item
729 #if 0
730 if (status != Git_wc_status_ignored || (entry->direct) || (dwShow & GitSLC_SHOWIGNORED))
732 if ((!entry->IsFolder()) && (status == Git_wc_status_deleted) && (dwShow & SVNSLC_SHOWREMOVEDANDPRESENT))
734 if (PathFileExists(entry->GetPath().GetWinPath()))
736 m_arListArray.push_back(i);
737 if ((dwCheck & SVNSLC_SHOWREMOVEDANDPRESENT)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
739 if (bAllowCheck)
740 entry->checked = true;
742 AddEntry(entry, langID, listIndex++);
745 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFILES)&&(entry->direct)&&(!entry->IsFolder())))
747 m_arListArray.push_back(i);
748 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
750 if (bAllowCheck)
751 entry->checked = true;
753 AddEntry(entry, langID, listIndex++);
755 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFOLDER)&&(entry->direct)&&entry->IsFolder()))
757 m_arListArray.push_back(i);
758 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
760 if (bAllowCheck)
761 entry->checked = true;
763 AddEntry(entry, langID, listIndex++);
766 #endif
769 SetItemCount(listIndex);
771 m_ColumnManager.UpdateRelevance (m_arStatusArray, m_arListArray);
773 AdjustColumnWidths();
775 SetRedraw(TRUE);
776 GetStatisticsString();
778 CHeaderCtrl * pHeader = GetHeaderCtrl();
779 HDITEM HeaderItem = {0};
780 HeaderItem.mask = HDI_FORMAT;
781 for (int i=0; i<pHeader->GetItemCount(); ++i)
783 pHeader->GetItem(i, &HeaderItem);
784 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
785 pHeader->SetItem(i, &HeaderItem);
787 if (m_nSortedColumn)
789 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
790 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
791 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
794 if (nSelectedEntry)
796 SetItemState(nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
797 EnsureVisible(nSelectedEntry, false);
799 else
801 // Restore the item at the top of the list.
802 for (int i=0;GetTopIndex() != nTopIndex;i++)
804 if ( !EnsureVisible(nTopIndex+i,false) )
805 break;
809 m_bEmpty = (GetItemCount() == 0);
810 Invalidate();
811 #endif
814 void CGitStatusListCtrl::StoreScrollPos()
816 m_sScrollPos.enabled = true;
817 m_sScrollPos.nTopIndex = GetTopIndex();
818 m_sScrollPos.selMark = GetSelectionMark();
819 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
820 m_sScrollPos.nSelectedEntry = 0;
821 if (posSelectedEntry)
822 m_sScrollPos.nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
825 void CGitStatusListCtrl::RestoreScrollPos()
827 if (!m_sScrollPos.enabled || CRegDWORD(L"Software\\TortoiseGit\\RememberFileListPosition", TRUE) != TRUE)
828 return;
830 if (m_sScrollPos.nSelectedEntry)
832 SetItemState(m_sScrollPos.nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
833 EnsureVisible(m_sScrollPos.nSelectedEntry, false);
835 else
837 // Restore the item at the top of the list.
838 for (int i = 0; GetTopIndex() != m_sScrollPos.nTopIndex; ++i)
840 if (!EnsureVisible(m_sScrollPos.nTopIndex + i, false))
841 break;
844 if (m_sScrollPos.selMark >= 0)
846 SetSelectionMark(m_sScrollPos.selMark);
847 SetItemState(m_sScrollPos.selMark, LVIS_FOCUSED, LVIS_FOCUSED);
849 m_sScrollPos.enabled = false;
852 int CGitStatusListCtrl::GetColumnIndex(int mask)
854 for (int i = 0; i < 32; ++i)
855 if(mask&0x1)
856 return i;
857 else
858 mask=mask>>1;
859 return -1;
862 CString CGitStatusListCtrl::GetCellText(int listIndex, int column)
864 static CString from(MAKEINTRESOURCE(IDS_STATUSLIST_FROM));
865 static bool abbreviateRenamings(((DWORD)CRegDWORD(L"Software\\TortoiseGit\\AbbreviateRenamings", FALSE)) == TRUE);
866 static bool relativeTimes = (CRegDWORD(L"Software\\TortoiseGit\\RelativeTimes", FALSE) != FALSE);
867 static const CString empty;
869 CAutoReadLock locker(m_guard);
870 const auto* entry = GetListEntry(listIndex);
871 if (!entry)
872 return empty;
874 switch (column)
876 case 0: // relative path
877 if (!(entry->m_Action & (CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_COPY) && !entry->GetGitOldPathString().IsEmpty()))
878 return entry->GetGitPathString();
880 if (!abbreviateRenamings)
882 CString entryname = entry->GetGitPathString();
883 entryname += L' ';
884 // relative path
885 entryname.AppendFormat(from, (LPCTSTR)entry->GetGitOldPathString());
886 return entryname;
889 return entry->GetAbbreviatedRename();
891 case 1: // GITSLC_COLFILENAME
892 return entry->GetFileOrDirectoryName();
894 case 2: // GITSLC_COLEXT
895 return entry->GetFileExtension();
897 case 3: // GITSLC_COLSTATUS
898 return entry->GetActionName();
900 case 4: // GITSLC_COLADD
901 return entry->m_StatAdd;
903 case 5: // GITSLC_COLDEL
904 return entry->m_StatDel;
906 case 6: // GITSLC_COLMODIFICATIONDATE
907 if (!(entry->m_Action & CTGitPath::LOGACTIONS_DELETED) && m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLMODIFICATIONDATE)))
909 CString modificationDate;
910 __int64 filetime = entry->GetLastWriteTime();
911 if (filetime)
913 FILETIME* f = (FILETIME*)(__int64*)&filetime;
914 modificationDate = CLoglistUtils::FormatDateAndTime(CTime(CGit::filetime_to_time_t(f)), DATE_SHORTDATE, true, relativeTimes);
916 return modificationDate;
918 return empty;
920 case 7: // GITSLC_COLSIZE
921 if (!(entry->IsDirectory() || !m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLSIZE))))
923 TCHAR buf[100] = { 0 };
924 StrFormatByteSize64(entry->GetFileSize(), buf, 100);
925 return buf;
927 return empty;
929 #if 0
930 default: // user-defined properties
931 if (column < m_ColumnManager.GetColumnCount())
933 assert(m_ColumnManager.IsUserProp(column));
935 const CString& name = m_ColumnManager.GetName(column);
936 auto propEntry = m_PropertyMap.find(entry->GetPath());
937 if (propEntry != m_PropertyMap.end())
939 if (propEntry->second.HasProperty(name))
941 const CString& propVal = propEntry->second[name];
942 return propVal.IsEmpty()
943 ? m_sNoPropValueText
944 : propVal;
948 #endif
950 return empty;
953 void CGitStatusListCtrl::AddEntry(CTGitPath * GitPath, WORD /*langID*/, int listIndex)
955 CAutoWriteLock locker(m_guard);
956 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
957 CString path = GitPath->GetGitPathString();
959 int index = listIndex;
960 // Load the icons *now* so the icons are cached when showing them later in the
961 // WM_PAINT handler.
962 // Problem is that (at least on Win10), loading the icons in the WM_PAINT
963 // handler triggers an OLE operation, which should not happen in WM_PAINT at all
964 // (see ..\VC\atlmfc\src\mfc\olemsgf.cpp, COleMessageFilter::OnMessagePending() for details about this)
965 // By loading the icons here, they get cached and the OLE operation won't happen
966 // later in the WM_PAINT handler.
967 // This solves the 'hang' which happens in the commit dialog if images are
968 // shown in the file list.
969 int icon_idx = 0;
970 if (GitPath->IsDirectory())
972 icon_idx = m_nIconFolder;
973 m_nShownSubmodules++;
975 else
977 icon_idx = SYS_IMAGE_LIST().GetPathIconIndex(*GitPath);
978 m_nShownFiles++;
980 switch (GitPath->m_Action)
982 case CTGitPath::LOGACTIONS_ADDED:
983 case CTGitPath::LOGACTIONS_COPY:
984 m_nShownAdded++;
985 break;
986 case CTGitPath::LOGACTIONS_DELETED:
987 case CTGitPath::LOGACTIONS_MISSING:
988 case CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING:
989 m_nShownDeleted++;
990 break;
991 case CTGitPath::LOGACTIONS_REPLACED:
992 case CTGitPath::LOGACTIONS_MODIFIED:
993 case CTGitPath::LOGACTIONS_MERGED:
994 m_nShownModified++;
995 break;
996 case CTGitPath::LOGACTIONS_UNMERGED:
997 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_ADDED:
998 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_MODIFIED:
999 m_nShownConflicted++;
1000 break;
1001 case CTGitPath::LOGACTIONS_UNVER:
1002 m_nShownUnversioned++;
1003 break;
1004 default:
1005 m_nShownUnversioned++;
1006 break;
1009 LVITEM lvItem = { 0 };
1010 lvItem.iItem = listIndex;
1011 lvItem.lParam = (LPARAM)GitPath;
1012 lvItem.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_STATE | LVIF_PARAM;
1013 lvItem.pszText = LPSTR_TEXTCALLBACK;
1014 lvItem.stateMask = LVIS_OVERLAYMASK;
1015 if (m_restorepaths.find(GitPath->GetWinPathString()) != m_restorepaths.end())
1016 lvItem.state = INDEXTOOVERLAYMASK(OVL_RESTORE);
1017 lvItem.iImage = icon_idx;
1018 InsertItem(&lvItem);
1020 m_arListArray.push_back(index);
1022 SetCheck(index, GitPath->m_Checked);
1023 if (GitPath->m_Checked)
1024 m_nSelected++;
1026 if ((GitPath->m_Action & CTGitPath::LOGACTIONS_SKIPWORKTREE) || (GitPath->m_Action & CTGitPath::LOGACTIONS_ASSUMEVALID))
1027 SetItemGroup(index, 3);
1028 else if (GitPath->m_Action & CTGitPath::LOGACTIONS_IGNORE)
1029 SetItemGroup(index, 2);
1030 else if( GitPath->m_Action & CTGitPath::LOGACTIONS_UNVER)
1031 SetItemGroup(index,1);
1032 else
1033 SetItemGroup(index, GitPath->m_ParentNo&(PARENT_MASK|MERGE_MASK));
1036 bool CGitStatusListCtrl::SetItemGroup(int item, int groupindex)
1038 CAutoWriteLock locker(m_guard);
1039 // if (!(m_dwContextMenus & SVNSLC_POPCHANGELISTS))
1040 // return false;
1041 if (groupindex < 0)
1042 return false;
1043 LVITEM i = {0};
1044 i.mask = LVIF_GROUPID;
1045 i.iItem = item;
1046 i.iSubItem = 0;
1047 i.iGroupId = groupindex;
1049 return !!SetItem(&i);
1052 void CGitStatusListCtrl::OnHdnItemclick(NMHDR *pNMHDR, LRESULT *pResult)
1054 LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
1055 *pResult = 0;
1057 CAutoReadWeakLock lock(m_guard);
1058 if (!lock.IsAcquired())
1059 return;
1061 if (m_arStatusArray.empty())
1062 return;
1064 if (m_nSortedColumn == phdr->iItem)
1065 m_bAscending = !m_bAscending;
1066 else
1067 m_bAscending = TRUE;
1068 m_nSortedColumn = phdr->iItem;
1069 Show(m_dwShow, 0, m_bShowFolders,false,true);
1072 BOOL CGitStatusListCtrl::OnLvnItemchanged(NMHDR *pNMHDR, LRESULT *pResult)
1074 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
1075 *pResult = 0;
1076 CWnd* pParent = GetLogicalParent();
1077 if (pParent && pParent->GetSafeHwnd())
1078 pParent->SendMessage(GITSLNM_ITEMCHANGED, pNMLV->iItem);
1080 if (m_nBlockItemChangeHandler)
1081 return FALSE;
1083 if ((pNMLV->uNewState==0)||(pNMLV->uNewState & LVIS_SELECTED)||(pNMLV->uNewState & LVIS_FOCUSED))
1084 return FALSE;
1086 CAutoWriteWeakLock writeLock(m_guard);
1087 if (!writeLock.IsAcquired())
1089 NotifyCheck();
1090 return FALSE;
1093 bool bSelected = !!(ListView_GetItemState(m_hWnd, pNMLV->iItem, LVIS_SELECTED) & LVIS_SELECTED);
1094 int nListItems = GetItemCount();
1096 // was the item checked?
1098 if (GetCheck(pNMLV->iItem))
1101 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1102 CheckEntry(pNMLV->iItem, nListItems);
1104 if (bSelected)
1106 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1107 POSITION pos = GetFirstSelectedItemPosition();
1108 int index;
1109 while ((index = GetNextSelectedItem(pos)) >= 0)
1111 if (index != pNMLV->iItem)
1112 CheckEntry(index, nListItems);
1116 else
1119 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1120 UncheckEntry(pNMLV->iItem, nListItems);
1122 if (bSelected)
1124 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
1125 POSITION pos = GetFirstSelectedItemPosition();
1126 int index;
1127 while ((index = GetNextSelectedItem(pos)) >= 0)
1129 if (index != pNMLV->iItem)
1130 UncheckEntry(index, nListItems);
1135 GetStatisticsString();
1136 NotifyCheck();
1138 return FALSE;
1141 void CGitStatusListCtrl::CheckEntry(int index, int /*nListItems*/)
1143 CAutoWriteLock locker(m_guard);
1144 auto path = GetListEntry(index);
1145 if (!path)
1146 return;
1147 m_mapFilenameToChecked[path->GetGitPathString()] = true;
1148 SetCheck(index, TRUE);
1149 // if an unversioned item was checked, then we need to check if
1150 // the parent folders are unversioned too. If the parent folders actually
1151 // are unversioned, then check those too.
1152 #if 0
1153 if (entry->status == git_wc_status_unversioned)
1155 // we need to check the parent folder too
1156 const CTGitPath& folderpath = entry->path;
1157 for (int i=0; i< nListItems; ++i)
1159 FileEntry * testEntry = GetListEntry(i);
1160 ASSERT(testEntry);
1161 if (!testEntry)
1162 continue;
1163 if (!testEntry->checked)
1165 if (testEntry->path.IsAncestorOf(folderpath) && (!testEntry->path.IsEquivalentTo(folderpath)))
1167 SetEntryCheck(testEntry,i,true);
1168 m_nSelected++;
1173 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1174 if ( (entry->status == git_wc_status_deleted) || (m_bCheckChildrenWithParent) || (bShift) )
1176 // if a deleted folder gets checked, we have to check all
1177 // children of that folder too.
1178 if (entry->path.IsDirectory())
1179 SetCheckOnAllDescendentsOf(entry, true);
1181 // if a deleted file or folder gets checked, we have to
1182 // check all parents of this item too.
1183 for (int i=0; i<nListItems; ++i)
1185 FileEntry * testEntry = GetListEntry(i);
1186 ASSERT(testEntry);
1187 if (!testEntry)
1188 continue;
1189 if (!testEntry->checked)
1191 if (testEntry->path.IsAncestorOf(entry->path) && (!testEntry->path.IsEquivalentTo(entry->path)))
1193 if ((testEntry->status == git_wc_status_deleted)||(m_bCheckChildrenWithParent))
1195 SetEntryCheck(testEntry,i,true);
1196 m_nSelected++;
1197 // now we need to check all children of this parent folder
1198 SetCheckOnAllDescendentsOf(testEntry, true);
1204 #endif
1205 if ( !path->m_Checked )
1207 path->m_Checked = TRUE;
1208 m_nSelected++;
1212 void CGitStatusListCtrl::UncheckEntry(int index, int /*nListItems*/)
1214 CAutoWriteLock locker(m_guard);
1215 auto path = GetListEntry(index);
1216 if (!path)
1217 return;
1218 SetCheck(index, FALSE);
1219 m_mapFilenameToChecked[path->GetGitPathString()] = false;
1220 // item was unchecked
1221 #if 0
1222 if (entry->path.IsDirectory())
1224 // disable all files within an unselected folder, except when unchecking a folder with property changes
1225 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1226 if ( (entry->status != git_wc_status_modified) || (bShift) )
1228 SetCheckOnAllDescendentsOf(entry, false);
1231 else if (entry->status == git_wc_status_deleted)
1233 // a "deleted" file was unchecked, so uncheck all parent folders
1234 // and all children of those parents
1235 for (int i=0; i<nListItems; i++)
1237 FileEntry * testEntry = GetListEntry(i);
1238 ASSERT(testEntry);
1239 if (!testEntry)
1240 continue;
1241 if (testEntry->checked)
1243 if (testEntry->path.IsAncestorOf(entry->path))
1245 if (testEntry->status == git_wc_status_deleted)
1247 SetEntryCheck(testEntry,i,false);
1248 m_nSelected--;
1250 SetCheckOnAllDescendentsOf(testEntry, false);
1256 #endif
1257 if ( path->m_Checked )
1259 path->m_Checked = FALSE;
1260 m_nSelected--;
1263 void CGitStatusListCtrl::BuildStatistics()
1265 CAutoReadLock locker(m_guard);
1267 // now gather some statistics
1268 m_nUnversioned = 0;
1269 m_nNormal = 0;
1270 m_nModified = 0;
1271 m_nAdded = 0;
1272 m_nDeleted = 0;
1273 m_nConflicted = 0;
1274 m_nTotal = 0;
1275 m_nSelected = 0;
1276 m_nLineAdded = 0;
1277 m_nLineDeleted = 0;
1278 m_nRenamed = 0;
1280 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
1282 int status = m_arStatusArray[i]->m_Action;
1284 m_nLineAdded += _wtol(m_arStatusArray[i]->m_StatAdd);
1285 m_nLineDeleted += _wtol(m_arStatusArray[i]->m_StatDel);
1287 if(status&(CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
1288 m_nAdded++;
1290 if(status&CTGitPath::LOGACTIONS_DELETED)
1291 m_nDeleted++;
1293 if(status&(CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_MODIFIED))
1294 m_nModified++;
1296 if(status&CTGitPath::LOGACTIONS_UNMERGED)
1297 m_nConflicted++;
1299 if(status&(CTGitPath::LOGACTIONS_IGNORE|CTGitPath::LOGACTIONS_UNVER))
1300 m_nUnversioned++;
1302 if(status&(CTGitPath::LOGACTIONS_REPLACED))
1303 m_nRenamed++;
1305 if (m_arStatusArray[i]->m_Checked)
1306 m_nSelected++;
1310 int CGitStatusListCtrl::GetGroupFromPoint(POINT * ppt)
1312 // the point must be relative to the upper left corner of the control
1314 if (!ppt)
1315 return -1;
1316 if (!IsGroupViewEnabled())
1317 return -1;
1319 POINT pt = *ppt;
1320 pt.x = 10;
1321 UINT flags = 0;
1322 int nItem = -1;
1323 RECT rc;
1324 GetWindowRect(&rc);
1325 while (((flags & LVHT_BELOW) == 0)&&(pt.y < rc.bottom))
1327 nItem = HitTest(pt, &flags);
1328 if ((flags & LVHT_ONITEM)||(flags & LVHT_EX_GROUP_HEADER))
1330 // the first item below the point
1332 // check if the point is too much right (i.e. if the point
1333 // is farther to the right than the width of the item)
1334 RECT r;
1335 GetItemRect(nItem, &r, LVIR_LABEL);
1336 if (ppt->x > r.right)
1337 return -1;
1339 LVITEM lv = {0};
1340 lv.mask = LVIF_GROUPID;
1341 lv.iItem = nItem;
1342 GetItem(&lv);
1343 int groupID = lv.iGroupId;
1344 // now we search upwards and check if the item above this one
1345 // belongs to another group. If it belongs to the same group,
1346 // we're not over a group header
1347 while (pt.y >= 0)
1349 pt.y -= 2;
1350 nItem = HitTest(pt, &flags);
1351 if ((flags & LVHT_ONITEM)&&(nItem >= 0))
1353 // the first item below the point
1354 LVITEM lv2 = {0};
1355 lv2.mask = LVIF_GROUPID;
1356 lv2.iItem = nItem;
1357 GetItem(&lv2);
1358 if (lv2.iGroupId != groupID)
1359 return groupID;
1360 else
1361 return -1;
1364 if (pt.y < 0)
1365 return groupID;
1366 return -1;
1368 pt.y += 2;
1370 return -1;
1373 void CGitStatusListCtrl::OnContextMenuGroup(CWnd * /*pWnd*/, CPoint point)
1375 POINT clientpoint = point;
1376 ScreenToClient(&clientpoint);
1377 if ((IsGroupViewEnabled())&&(GetGroupFromPoint(&clientpoint) >= 0))
1379 CAutoReadWeakLock readLock(m_guard);
1380 if (!readLock.IsAcquired())
1381 return;
1383 CMenu popup;
1384 if (popup.CreatePopupMenu())
1386 CString temp;
1387 temp.LoadString(IDS_STATUSLIST_CHECKGROUP);
1388 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_CHECKGROUP, temp);
1389 temp.LoadString(IDS_STATUSLIST_UNCHECKGROUP);
1390 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_UNCHECKGROUP, temp);
1391 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
1392 bool bCheck = false;
1393 switch (cmd)
1395 case IDGITLC_CHECKGROUP:
1396 bCheck = true;
1397 // fall through here
1398 case IDGITLC_UNCHECKGROUP:
1400 int group = GetGroupFromPoint(&clientpoint);
1401 // go through all items and check/uncheck those assigned to the group
1402 // but block the OnLvnItemChanged handler
1403 for (int i=0; i<GetItemCount(); ++i)
1405 LVITEM lv = { 0 };
1406 lv.mask = LVIF_GROUPID;
1407 lv.iItem = i;
1408 GetItem(&lv);
1410 if (lv.iGroupId == group)
1412 auto entry = GetListEntry(i);
1413 if (entry)
1415 bool bOldCheck = entry->m_Checked;
1416 SetEntryCheck(entry, i, bCheck);
1417 if (bCheck != bOldCheck)
1419 if (bCheck)
1420 m_nSelected++;
1421 else
1422 m_nSelected--;
1428 GetStatisticsString();
1429 NotifyCheck();
1431 break;
1437 void CGitStatusListCtrl::OnContextMenuList(CWnd * pWnd, CPoint point)
1439 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1441 CAutoWriteWeakLock writeLock(m_guard);
1442 if (!writeLock.IsAcquired())
1443 return;
1445 auto selectedCount = GetSelectedCount();
1446 int selIndex = GetSelectionMark();
1447 int selSubitem = -1;
1448 if (selectedCount > 0)
1450 CPoint pt = point;
1451 ScreenToClient(&pt);
1452 LVHITTESTINFO hittest = { 0 };
1453 hittest.flags = LVHT_ONITEM;
1454 hittest.pt = pt;
1455 if (this->SubItemHitTest(&hittest) >= 0)
1456 selSubitem = hittest.iSubItem;
1458 if ((point.x == -1) && (point.y == -1))
1460 CRect rect;
1461 GetItemRect(selIndex, &rect, LVIR_LABEL);
1462 ClientToScreen(&rect);
1463 point = rect.CenterPoint();
1465 if (selectedCount == 0 && m_bHasCheckboxes)
1467 // nothing selected could mean the context menu is requested for
1468 // a group header
1469 OnContextMenuGroup(pWnd, point);
1471 else if (selIndex >= 0)
1473 auto filepath = GetListEntry(selIndex);
1474 if (!filepath)
1475 return;
1477 //const CTGitPath& filepath = entry->path;
1478 int wcStatus = filepath->m_Action;
1479 // entry is selected, now show the popup menu
1480 CIconMenu popup;
1481 CMenu changelistSubMenu;
1482 CMenu ignoreSubMenu;
1483 CIconMenu clipSubMenu;
1484 CMenu shellMenu;
1485 if (popup.CreatePopupMenu())
1487 //Add Menu for version controlled file
1489 if (selectedCount > 0 && (wcStatus & CTGitPath::LOGACTIONS_UNMERGED))
1491 if (selectedCount == 1 && (m_dwContextMenus & GITSLC_POPCONFLICT))
1493 popup.AppendMenuIcon(IDGITLC_EDITCONFLICT, IDS_MENUCONFLICT, IDI_CONFLICT);
1494 popup.SetDefaultItem(IDGITLC_EDITCONFLICT, FALSE);
1496 if (m_dwContextMenus & GITSLC_POPRESOLVE)
1498 popup.AppendMenuIcon(IDGITLC_RESOLVECONFLICT, IDS_STATUSLIST_CONTEXT_RESOLVED, IDI_RESOLVE);
1499 CString tmp, mineTitle, theirsTitle;
1500 CAppUtils::GetConflictTitles(nullptr, mineTitle, theirsTitle, m_bIsRevertTheirMy);
1501 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)theirsTitle);
1502 if (m_bIsRevertTheirMy)
1504 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, tmp, IDI_RESOLVE);
1505 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)mineTitle);
1506 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, tmp, IDI_RESOLVE);
1508 else
1510 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, tmp, IDI_RESOLVE);
1511 tmp.Format(IDS_SVNPROGRESS_MENURESOLVEUSING, (LPCTSTR)mineTitle);
1512 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, tmp, IDI_RESOLVE);
1515 if ((m_dwContextMenus & GITSLC_POPCONFLICT)||(m_dwContextMenus & GITSLC_POPRESOLVE))
1516 popup.AppendMenu(MF_SEPARATOR);
1519 if (selectedCount > 0)
1521 if (wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
1523 if (m_dwContextMenus & GITSLC_POPADD)
1524 popup.AppendMenuIcon(IDGITLC_ADD, IDS_STATUSLIST_CONTEXT_ADD, IDI_ADD);
1525 if (m_dwContextMenus & GITSLC_POPCOMMIT)
1526 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1530 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)
1532 bool bEntryAdded = false;
1533 if (m_dwContextMenus & GITSLC_POPCOMPAREWITHBASE)
1535 if(filepath->m_ParentNo & MERGE_MASK)
1536 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_TREE_DIFF, IDI_DIFF);
1537 else
1538 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_LOG_COMPAREWITHBASE, IDI_DIFF);
1540 if (!(wcStatus & (CTGitPath::LOGACTIONS_UNMERGED)) || selectedCount != 1)
1541 popup.SetDefaultItem(IDGITLC_COMPARE, FALSE);
1542 bEntryAdded = true;
1545 if (!g_Git.IsInitRepos() && (m_dwContextMenus & GITSLC_POPGNUDIFF))
1547 popup.AppendMenuIcon(IDGITLC_GNUDIFF1, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1548 bEntryAdded = true;
1551 if ((m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPAREWC)) && m_bHasWC)
1553 if ((!m_CurrentVersion.IsEmpty()) && m_CurrentVersion != GIT_REV_ZERO)
1555 popup.AppendMenuIcon(IDGITLC_COMPAREWC, IDS_LOG_POPUP_COMPARE, IDI_DIFF);
1556 bEntryAdded = true;
1560 if (bEntryAdded)
1561 popup.AppendMenu(MF_SEPARATOR);
1564 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
1566 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPARETWOREVISIONS))
1568 popup.AppendMenuIcon(IDGITLC_COMPARETWOREVISIONS, IDS_LOG_POPUP_COMPARETWO, IDI_DIFF);
1569 popup.SetDefaultItem(IDGITLC_COMPARETWOREVISIONS, FALSE);
1571 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_GNUDIFF2REVISIONS))
1572 popup.AppendMenuIcon(IDGITLC_GNUDIFF2REVISIONS, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1575 //Select Multi item
1576 if (selectedCount > 0)
1578 if (selectedCount == 2 && (m_dwContextMenus & GITSLC_POPCOMPARETWOFILES))
1580 POSITION pos = GetFirstSelectedItemPosition();
1581 int index = GetNextSelectedItem(pos);
1582 if (index >= 0)
1584 auto entry2 = GetListEntry(index);
1585 bool firstEntryExistsAndIsFile = entry2 && !entry2->IsDirectory();
1586 index = GetNextSelectedItem(pos);
1587 if (index >= 0)
1589 entry2 = GetListEntry(index);
1590 if (firstEntryExistsAndIsFile && entry2 && !entry2->IsDirectory())
1591 popup.AppendMenuIcon(IDGITLC_COMPARETWOFILES, IDS_STATUSLIST_CONTEXT_COMPARETWOFILES, IDI_DIFF);
1597 if (selectedCount > 0 && (!(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))) && m_bHasWC)
1599 if ((m_dwContextMenus & GITSLC_POPCOMMIT) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && !(wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1600 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1602 if ((m_dwContextMenus & GITSLC_POPREVERT) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO))
1603 popup.AppendMenuIcon(IDGITLC_REVERT, IDS_MENUREVERT, IDI_REVERT);
1605 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)))
1606 popup.AppendMenuIcon(IDGITLC_SKIPWORKTREE, IDS_STATUSLIST_SKIPWORKTREE);
1608 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)))
1609 popup.AppendMenuIcon(IDGITLC_ASSUMEVALID, IDS_MENUASSUMEVALID);
1611 if ((m_dwContextMenus & GITLC_POPUNSETIGNORELOCALCHANGES) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && (wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1612 popup.AppendMenuIcon(IDGITLC_UNSETIGNORELOCALCHANGES, IDS_STATUSLIST_UNSETIGNORELOCALCHANGES);
1614 if (m_dwContextMenus & GITSLC_POPRESTORE && !filepath->IsDirectory())
1616 if (m_restorepaths.find(filepath->GetWinPathString()) == m_restorepaths.end())
1617 popup.AppendMenuIcon(IDGITLC_CREATERESTORE, IDS_MENUCREATERESTORE, IDI_RESTORE);
1618 else
1619 popup.AppendMenuIcon(IDGITLC_RESTOREPATH, IDS_MENURESTORE, IDI_RESTORE);
1622 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOREV)) && ( !this->m_CurrentVersion.IsEmpty() )
1623 && this->m_CurrentVersion != GIT_REV_ZERO && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1625 popup.AppendMenuIcon(IDGITLC_REVERTTOREV, IDS_LOG_POPUP_REVERTTOREV, IDI_REVERT);
1628 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOPARENT)) && ( !this->m_CurrentVersion.IsEmpty() )
1629 && this->m_CurrentVersion != GIT_REV_ZERO && !(wcStatus & CTGitPath::LOGACTIONS_ADDED))
1631 popup.AppendMenuIcon(IDGITLC_REVERTTOPARENT, IDS_LOG_POPUP_REVERTTOPARENT, IDI_REVERT);
1635 if (selectedCount == 1 && !(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
1637 if (m_dwContextMenus & GITSLC_POPSHOWLOG)
1638 popup.AppendMenuIcon(IDGITLC_LOG, IDS_REPOBROWSE_SHOWLOG, IDI_LOG);
1639 if (m_dwContextMenus & GITSLC_POPSHOWLOGSUBMODULE && filepath->IsDirectory())
1640 popup.AppendMenuIcon(IDGITLC_LOGSUBMODULE, IDS_LOG_SUBMODULE, IDI_LOG);
1641 if (m_dwContextMenus & GITSLC_POPSHOWLOGOLDNAME && (wcStatus & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY) && !filepath->GetGitOldPathString().IsEmpty()))
1642 popup.AppendMenuIcon(IDGITLC_LOGOLDNAME, IDS_STATUSLIST_SHOWLOGOLDNAME, IDI_LOG);
1643 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)
1644 popup.AppendMenuIcon(IDGITLC_BLAME, IDS_MENUBLAME, IDI_BLAME);
1647 if (selectedCount > 0)
1649 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_EXPORT)) && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1650 popup.AppendMenuIcon(IDGITLC_EXPORT, IDS_LOG_POPUP_EXPORT, IDI_EXPORT);
1653 if (selectedCount == 1)
1655 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_SAVEAS) && ! filepath->IsDirectory() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1656 popup.AppendMenuIcon(IDGITLC_SAVEAS, IDS_LOG_POPUP_SAVE, IDI_SAVEAS);
1658 if (m_dwContextMenus & GITSLC_POPOPEN && !filepath->IsDirectory() && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1660 popup.AppendMenuIcon(IDGITLC_VIEWREV, IDS_LOG_POPUP_VIEWREV);
1661 popup.AppendMenuIcon(IDGITLC_OPEN, IDS_REPOBROWSE_OPEN, IDI_OPEN);
1662 popup.AppendMenuIcon(IDGITLC_OPENWITH, IDS_LOG_POPUP_OPENWITH, IDI_OPEN);
1665 if (m_dwContextMenus & GITSLC_POPEXPLORE && !(wcStatus & CTGitPath::LOGACTIONS_DELETED) && m_bHasWC)
1666 popup.AppendMenuIcon(IDGITLC_EXPLORE, IDS_STATUSLIST_CONTEXT_EXPLORE, IDI_EXPLORER);
1668 if (m_dwContextMenus & GITSLC_PREPAREDIFF && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1670 popup.AppendMenu(MF_SEPARATOR);
1671 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF, IDS_PREPAREDIFF, IDI_DIFF);
1672 if (!m_sMarkForDiffFilename.IsEmpty())
1674 CString diffWith;
1675 if (filepath->GetGitPathString() == m_sMarkForDiffFilename)
1676 diffWith = m_sMarkForDiffVersion;
1677 else
1679 PathCompactPathEx(CStrBuf(diffWith, 2 * GIT_HASH_SIZE), m_sMarkForDiffFilename, 2 * GIT_HASH_SIZE, 0);
1680 diffWith += L':' + m_sMarkForDiffVersion.Left(g_Git.GetShortHASHLength());
1682 CString menuEntry;
1683 menuEntry.Format(IDS_MENUDIFFNOW, (LPCTSTR)diffWith);
1684 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF_COMPARE, menuEntry, IDI_DIFF);
1688 if (selectedCount > 0)
1690 // if (((wcStatus == git_wc_status_unversioned)||(wcStatus == git_wc_status_ignored))&&(m_dwContextMenus & SVNSLC_POPDELETE))
1691 // popup.AppendMenuIcon(IDSVNLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1692 // 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))
1693 // {
1694 // if (bShift)
1695 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVEKEEP, IDI_DELETE);
1696 // else
1697 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVE, IDI_DELETE);
1698 // }
1699 if ((wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE | CTGitPath::LOGACTIONS_MISSING))/*||(wcStatus == git_wc_status_deleted)*/)
1701 if (m_dwContextMenus & GITSLC_POPDELETE)
1702 popup.AppendMenuIcon(IDGITLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1704 if ( (wcStatus & CTGitPath::LOGACTIONS_UNVER || wcStatus & CTGitPath::LOGACTIONS_DELETED) )
1706 if (m_dwContextMenus & GITSLC_POPIGNORE)
1708 CTGitPathList ignorelist;
1709 FillListOfSelectedItemPaths(ignorelist);
1710 //check if all selected entries have the same extension
1711 bool bSameExt = true;
1712 CString sExt;
1713 for (int i=0; i<ignorelist.GetCount(); ++i)
1715 if (sExt.IsEmpty() && (i==0))
1716 sExt = ignorelist[i].GetFileExtension();
1717 else if (sExt.CompareNoCase(ignorelist[i].GetFileExtension())!=0)
1718 bSameExt = false;
1720 if (bSameExt)
1722 if (ignoreSubMenu.CreateMenu())
1724 CString ignorepath;
1725 if (ignorelist.GetCount()==1)
1726 ignorepath = ignorelist[0].GetFileOrDirectoryName();
1727 else
1728 ignorepath.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1729 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNORE, ignorepath);
1730 ignorepath = L'*' + sExt;
1731 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREMASK, ignorepath);
1732 if (ignorelist.GetCount() == 1 && !ignorelist[0].GetContainingDirectory().GetGitPathString().IsEmpty())
1733 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREFOLDER, ignorelist[0].GetContainingDirectory().GetGitPathString());
1734 CString temp;
1735 temp.LoadString(IDS_MENUIGNORE);
1736 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)ignoreSubMenu.m_hMenu, temp);
1739 else
1741 CString temp;
1742 if (ignorelist.GetCount()==1)
1743 temp.LoadString(IDS_MENUIGNORE);
1744 else
1745 temp.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1746 popup.AppendMenuIcon(IDGITLC_IGNORE, temp, IDI_IGNORE);
1747 temp.Format(IDS_MENUIGNOREMULTIPLEMASK, ignorelist.GetCount());
1748 popup.AppendMenuIcon(IDGITLC_IGNOREMASK, temp, IDI_IGNORE);
1756 if (selectedCount > 0)
1758 popup.AppendMenu(MF_SEPARATOR);
1760 if (clipSubMenu.CreatePopupMenu())
1762 CString temp;
1763 clipSubMenu.AppendMenuIcon(IDGITLC_COPYFULL, IDS_STATUSLIST_CONTEXT_COPYFULLPATHS, IDI_COPYCLIP);
1764 clipSubMenu.AppendMenuIcon(IDGITLC_COPYRELPATHS, IDS_STATUSLIST_CONTEXT_COPYRELPATHS, IDI_COPYCLIP);
1765 clipSubMenu.AppendMenuIcon(IDGITLC_COPYFILENAMES, IDS_STATUSLIST_CONTEXT_COPYFILENAMES, IDI_COPYCLIP);
1766 clipSubMenu.AppendMenuIcon(IDGITLC_COPYEXT, IDS_STATUSLIST_CONTEXT_COPYEXT, IDI_COPYCLIP);
1767 if (selSubitem >= 0)
1769 temp.Format(IDS_STATUSLIST_CONTEXT_COPYCOL, (LPCWSTR)m_ColumnManager.GetName(selSubitem));
1770 clipSubMenu.AppendMenuIcon(IDGITLC_COPYCOL, temp, IDI_COPYCLIP);
1772 temp.LoadString(IDS_LOG_POPUP_COPYTOCLIPBOARD);
1773 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)clipSubMenu.m_hMenu, temp);
1776 #if 0
1777 if ((m_dwContextMenus & SVNSLC_POPCHANGELISTS))
1778 &&(wcStatus != git_wc_status_unversioned)&&(wcStatus != git_wc_status_none))
1780 popup.AppendMenu(MF_SEPARATOR);
1781 // changelist commands
1782 size_t numChangelists = GetNumberOfChangelistsInSelection();
1783 if (numChangelists > 0)
1784 popup.AppendMenuIcon(IDSVNLC_REMOVEFROMCS, IDS_STATUSLIST_CONTEXT_REMOVEFROMCS);
1785 if ((!entry->IsFolder())&&(changelistSubMenu.CreateMenu()))
1787 CString temp;
1788 temp.LoadString(IDS_STATUSLIST_CONTEXT_CREATECS);
1789 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDSVNLC_CREATECS, temp);
1791 if (entry->changelist.Compare(SVNSLC_IGNORECHANGELIST))
1793 changelistSubMenu.AppendMenu(MF_SEPARATOR);
1794 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDSVNLC_CREATEIGNORECS, SVNSLC_IGNORECHANGELIST);
1797 if (!m_changelists.empty())
1799 // find the changelist names
1800 bool bNeedSeparator = true;
1801 int cmdID = IDSVNLC_MOVETOCS;
1802 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
1804 if ((entry->changelist.Compare(it->first))&&(it->first.Compare(SVNSLC_IGNORECHANGELIST)))
1806 if (bNeedSeparator)
1808 changelistSubMenu.AppendMenu(MF_SEPARATOR);
1809 bNeedSeparator = false;
1811 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, cmdID, it->first);
1812 cmdID++;
1816 temp.LoadString(IDS_STATUSLIST_CONTEXT_MOVETOCS);
1817 popup.AppendMenu(MF_POPUP|MF_STRING, (UINT_PTR)changelistSubMenu.GetSafeHmenu(), temp);
1820 #endif
1823 m_hShellMenu = nullptr;
1824 if (selectedCount > 0 && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)) && m_bHasWC && (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO) && shellMenu.CreatePopupMenu())
1826 // insert the shell context menu
1827 popup.AppendMenu(MF_SEPARATOR);
1828 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)shellMenu.m_hMenu, CString(MAKEINTRESOURCE(IDS_STATUSLIST_CONTEXT_SHELL)));
1829 m_hShellMenu = shellMenu.m_hMenu;
1832 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
1833 g_IContext2 = nullptr;
1834 g_IContext3 = nullptr;
1835 if (m_pContextMenu)
1837 if (cmd >= SHELL_MIN_CMD && cmd <= SHELL_MAX_CMD) // see if returned idCommand belongs to shell menu entries)
1839 CMINVOKECOMMANDINFOEX cmi = { 0 };
1840 cmi.cbSize = sizeof(CMINVOKECOMMANDINFOEX);
1841 cmi.fMask = CMIC_MASK_UNICODE | CMIC_MASK_PTINVOKE;
1842 if (GetKeyState(VK_CONTROL) < 0)
1843 cmi.fMask |= CMIC_MASK_CONTROL_DOWN;
1844 if (bShift)
1845 cmi.fMask |= CMIC_MASK_SHIFT_DOWN;
1846 cmi.hwnd = m_hWnd;
1847 cmi.lpVerb = MAKEINTRESOURCEA(cmd - SHELL_MIN_CMD);
1848 cmi.lpVerbW = MAKEINTRESOURCEW(cmd - SHELL_MIN_CMD);
1849 cmi.nShow = SW_SHOWNORMAL;
1850 cmi.ptInvoke = point;
1852 m_pContextMenu->InvokeCommand((LPCMINVOKECOMMANDINFO)&cmi);
1854 cmd = 0;
1856 m_pContextMenu->Release();
1857 m_pContextMenu = nullptr;
1859 if (g_pFolderhook)
1861 delete g_pFolderhook;
1862 g_pFolderhook = nullptr;
1864 if (g_psfDesktopFolder)
1866 g_psfDesktopFolder->Release();
1867 g_psfDesktopFolder = nullptr;
1869 for (int i = 0; i < g_pidlArrayItems; i++)
1871 if (g_pidlArray[i])
1872 CoTaskMemFree(g_pidlArray[i]);
1874 if (g_pidlArray)
1875 CoTaskMemFree(g_pidlArray);
1876 g_pidlArray = nullptr;
1877 g_pidlArrayItems = 0;
1879 m_bWaitCursor = true;
1880 bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1881 //int iItemCountBeforeMenuCmd = GetItemCount();
1882 //bool bForce = false;
1883 switch (cmd)
1885 case IDGITLC_VIEWREV:
1886 OpenFile(filepath, ALTERNATIVEEDITOR);
1887 break;
1889 case IDGITLC_OPEN:
1890 OpenFile(filepath,OPEN);
1891 break;
1893 case IDGITLC_OPENWITH:
1894 OpenFile(filepath,OPEN_WITH);
1895 break;
1897 case IDGITLC_EXPLORE:
1898 CAppUtils::ExploreTo(GetSafeHwnd(), g_Git.CombinePath(filepath));
1899 break;
1901 case IDGITLC_PREPAREDIFF:
1902 m_sMarkForDiffFilename = filepath->GetGitPathString();
1903 m_sMarkForDiffVersion = m_CurrentVersion;
1904 break;
1906 case IDGITLC_PREPAREDIFF_COMPARE:
1908 CTGitPath savedFile(m_sMarkForDiffFilename);
1909 CGitDiff::Diff(filepath, &savedFile, m_CurrentVersion, m_sMarkForDiffVersion, false, false, 0, bShift);
1911 break;
1913 case IDGITLC_CREATERESTORE:
1915 POSITION pos = GetFirstSelectedItemPosition();
1916 while (pos)
1918 int index = GetNextSelectedItem(pos);
1919 auto entry2 = GetListEntry(index);
1920 if (!entry2 || entry2->IsDirectory())
1921 continue;
1922 if (m_restorepaths.find(entry2->GetWinPathString()) != m_restorepaths.end())
1923 continue;
1924 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
1925 // delete the temp file: the temp file has the FILE_ATTRIBUTE_TEMPORARY flag set
1926 // and copying the real file over it would leave that temp flag.
1927 DeleteFile(tempFile.GetWinPath());
1928 if (CopyFile(g_Git.CombinePath(entry2), tempFile.GetWinPath(), FALSE))
1930 m_restorepaths[entry2->GetWinPathString()] = tempFile.GetWinPathString();
1931 SetItemState(index, INDEXTOOVERLAYMASK(OVL_RESTORE), LVIS_OVERLAYMASK);
1934 Invalidate();
1936 break;
1938 case IDGITLC_RESTOREPATH:
1940 if (CMessageBox::Show(m_hWnd, IDS_STATUSLIST_RESTOREPATH, IDS_APPNAME, 2, IDI_QUESTION, IDS_RESTOREBUTTON, IDS_ABORTBUTTON) == 2)
1941 break;
1942 POSITION pos = GetFirstSelectedItemPosition();
1943 while (pos)
1945 int index = GetNextSelectedItem(pos);
1946 auto entry2 = GetListEntry(index);
1947 if (!entry2)
1948 continue;
1949 if (m_restorepaths.find(entry2->GetWinPathString()) == m_restorepaths.end())
1950 continue;
1951 if (CopyFile(m_restorepaths[entry2->GetWinPathString()], g_Git.CombinePath(entry2), FALSE))
1953 m_restorepaths.erase(entry2->GetWinPathString());
1954 SetItemState(index, 0, LVIS_OVERLAYMASK);
1957 Invalidate();
1959 break;
1961 // Compare current version and work copy.
1962 case IDGITLC_COMPAREWC:
1964 if (!CheckMultipleDiffs())
1965 break;
1966 POSITION pos = GetFirstSelectedItemPosition();
1967 while ( pos )
1969 int index = GetNextSelectedItem(pos);
1970 StartDiffWC(index);
1973 break;
1975 // Compare with base version. when current version is zero, compare workcopy and HEAD.
1976 case IDGITLC_COMPARE:
1978 if (!CheckMultipleDiffs())
1979 break;
1980 POSITION pos = GetFirstSelectedItemPosition();
1981 while ( pos )
1983 int index = GetNextSelectedItem(pos);
1984 StartDiff(index);
1987 break;
1989 case IDGITLC_COMPARETWOREVISIONS:
1991 if (!CheckMultipleDiffs())
1992 break;
1993 POSITION pos = GetFirstSelectedItemPosition();
1994 while ( pos )
1996 int index = GetNextSelectedItem(pos);
1997 StartDiffTwo(index);
2000 break;
2002 case IDGITLC_COMPARETWOFILES:
2004 POSITION pos = GetFirstSelectedItemPosition();
2005 if (pos)
2007 auto firstfilepath = GetListEntry(GetNextSelectedItem(pos));
2008 if (!firstfilepath)
2009 break;
2011 auto secondfilepath = GetListEntry(GetNextSelectedItem(pos));
2012 if (!secondfilepath)
2013 break;
2015 CString sCmd;
2016 if (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
2017 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);
2018 else
2019 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);
2020 if (bShift)
2021 sCmd += L" /alternative";
2022 CAppUtils::RunTortoiseGitProc(sCmd);
2025 break;
2027 case IDGITLC_GNUDIFF1:
2029 if (!CheckMultipleDiffs())
2030 break;
2031 POSITION pos = GetFirstSelectedItemPosition();
2032 while (pos)
2034 auto selectedFilepath = GetListEntry(GetNextSelectedItem(pos));
2035 if (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
2037 CString fromwhere;
2038 if (m_amend)
2039 fromwhere = L"~1";
2040 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, GitRev::GetHead() + fromwhere, *selectedFilepath, GitRev::GetWorkingCopy(), bShift);
2042 else
2044 if ((selectedFilepath->m_ParentNo & (PARENT_MASK | MERGE_MASK)) == 0)
2045 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, m_CurrentVersion + L"~1", *selectedFilepath, m_CurrentVersion, bShift);
2046 else
2048 CString str;
2049 if (!(selectedFilepath->m_ParentNo & MERGE_MASK))
2050 str.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, (selectedFilepath->m_ParentNo & PARENT_MASK) + 1);
2052 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, str, *selectedFilepath, m_CurrentVersion, bShift, false, false, false, !!(selectedFilepath->m_ParentNo & MERGE_MASK));
2057 break;
2059 case IDGITLC_GNUDIFF2REVISIONS:
2061 if (!CheckMultipleDiffs())
2062 break;
2063 POSITION pos = GetFirstSelectedItemPosition();
2064 while (pos)
2066 auto entry = GetListEntry(GetNextSelectedItem(pos));
2067 CAppUtils::StartShowUnifiedDiff(m_hWnd, *entry, m_Rev2, *entry, m_Rev1, bShift);
2070 break;
2072 case IDGITLC_ADD:
2074 CTGitPathList paths;
2075 FillListOfSelectedItemPaths(paths, true);
2077 CGitProgressDlg progDlg;
2078 AddProgressCommand addCommand;
2079 progDlg.SetCommand(&addCommand);
2080 addCommand.SetShowCommitButtonAfterAdd((m_dwContextMenus & GITSLC_POPCOMMIT) != 0);
2081 addCommand.SetPathList(paths);
2082 progDlg.SetItemCount(paths.GetCount());
2083 progDlg.DoModal();
2085 // reset unchecked status
2086 POSITION pos = GetFirstSelectedItemPosition();
2087 int index;
2088 while ((index = GetNextSelectedItem(pos)) >= 0)
2089 m_mapFilenameToChecked.erase(GetListEntry(index)->GetGitPathString());
2091 if (GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2092 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2094 SetRedraw(TRUE);
2096 break;
2098 case IDGITLC_DELETE:
2099 DeleteSelectedFiles();
2100 break;
2102 case IDGITLC_BLAME:
2104 CAppUtils::LaunchTortoiseBlame(g_Git.CombinePath(filepath), m_CurrentVersion);
2106 break;
2108 case IDGITLC_LOG:
2109 case IDGITLC_LOGSUBMODULE:
2111 CString sCmd;
2112 sCmd.Format(L"/command:log /path:\"%s\"", (LPCTSTR)g_Git.CombinePath(filepath));
2113 if (cmd == IDGITLC_LOG && filepath->IsDirectory())
2114 sCmd += L" /submodule";
2115 if (!m_sDisplayedBranch.IsEmpty())
2116 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2117 CAppUtils::RunTortoiseGitProc(sCmd, false, !(cmd == IDGITLC_LOGSUBMODULE));
2119 break;
2121 case IDGITLC_LOGOLDNAME:
2123 CTGitPath oldName(filepath->GetGitOldPathString());
2124 CString sCmd;
2125 sCmd.Format(L"/command:log /path:\"%s\"", (LPCTSTR)g_Git.CombinePath(oldName));
2126 if (!m_sDisplayedBranch.IsEmpty())
2127 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2128 CAppUtils::RunTortoiseGitProc(sCmd);
2130 break;
2132 case IDGITLC_EDITCONFLICT:
2134 if (CAppUtils::ConflictEdit(*filepath, bShift, m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2136 CString conflictedFile = g_Git.CombinePath(filepath);
2137 if (!PathFileExists(conflictedFile) && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2139 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2140 break;
2142 StoreScrollPos();
2143 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2146 break;
2148 case IDGITLC_RESOLVETHEIRS: //follow up
2149 case IDGITLC_RESOLVEMINE: //follow up
2150 case IDGITLC_RESOLVECONFLICT:
2152 if (CMessageBox::Show(m_hWnd, IDS_PROC_RESOLVE, IDS_APPNAME, MB_ICONQUESTION | MB_YESNO)==IDYES)
2154 bool needsFullRefresh = false;
2155 POSITION pos = GetFirstSelectedItemPosition();
2156 while (pos != 0)
2158 int index;
2159 index = GetNextSelectedItem(pos);
2160 auto fentry = GetListEntry(index);
2161 if (!fentry)
2162 continue;
2164 CAppUtils::resolve_with resolveWith = CAppUtils::RESOLVE_WITH_CURRENT;
2165 if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE))
2166 resolveWith = CAppUtils::RESOLVE_WITH_THEIRS;
2167 else if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS))
2168 resolveWith = CAppUtils::RESOLVE_WITH_MINE;
2169 if (CAppUtils::ResolveConflict(*fentry, resolveWith) == 0 && fentry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
2170 needsFullRefresh = true;
2172 if (needsFullRefresh && CRegDWORD(L"Software\\TortoiseGit\\RefreshFileListAfterResolvingConflict", TRUE) == TRUE)
2174 CWnd* pParent = GetLogicalParent();
2175 if (pParent && pParent->GetSafeHwnd())
2176 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2177 SetRedraw(TRUE);
2178 break;
2180 StoreScrollPos();
2181 Show(m_dwShow, 0, m_bShowFolders,0,true);
2184 break;
2186 case IDGITLC_IGNORE:
2188 CTGitPathList ignorelist;
2189 //std::vector<CString> toremove;
2190 FillListOfSelectedItemPaths(ignorelist, true);
2192 if(!CAppUtils::IgnoreFile(ignorelist,false))
2193 break;
2195 SetRedraw(FALSE);
2196 CWnd* pParent = GetLogicalParent();
2197 if (pParent && pParent->GetSafeHwnd())
2199 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2201 SetRedraw(TRUE);
2203 break;
2205 case IDGITLC_IGNOREMASK:
2207 CString common;
2208 CString ext=filepath->GetFileExtension();
2209 CTGitPathList ignorelist;
2210 FillListOfSelectedItemPaths(ignorelist, true);
2212 if (!CAppUtils::IgnoreFile(ignorelist,true))
2213 break;
2215 SetRedraw(FALSE);
2216 CWnd* pParent = GetLogicalParent();
2217 if (pParent && pParent->GetSafeHwnd())
2219 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2222 SetRedraw(TRUE);
2224 break;
2226 case IDGITLC_IGNOREFOLDER:
2228 CTGitPathList ignorelist;
2229 ignorelist.AddPath(filepath->GetContainingDirectory());
2231 if (!CAppUtils::IgnoreFile(ignorelist, false))
2232 break;
2234 SetRedraw(FALSE);
2235 CWnd *pParent = GetLogicalParent();
2236 if (pParent && pParent->GetSafeHwnd())
2237 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2239 SetRedraw(TRUE);
2241 break;
2242 case IDGITLC_COMMIT:
2244 CTGitPathList targetList;
2245 FillListOfSelectedItemPaths(targetList);
2246 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2247 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2248 CString commandline = L"/command:commit /pathfile:\"";
2249 commandline += tempFile.GetWinPathString();
2250 commandline += L'"';
2251 commandline += L" /deletepathfile";
2252 CAppUtils::RunTortoiseGitProc(commandline);
2254 break;
2255 case IDGITLC_REVERT:
2257 // If at least one item is not in the status "added"
2258 // we ask for a confirmation
2259 BOOL bConfirm = FALSE;
2260 POSITION pos = GetFirstSelectedItemPosition();
2261 int index;
2262 while ((index = GetNextSelectedItem(pos)) >= 0)
2264 auto fentry = GetListEntry(index);
2265 if(fentry && fentry->m_Action &CTGitPath::LOGACTIONS_MODIFIED && !fentry->IsDirectory())
2267 bConfirm = TRUE;
2268 break;
2272 CString str;
2273 str.Format(IDS_PROC_WARNREVERT, selectedCount);
2275 if (!bConfirm || MessageBox(str, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES)
2277 CTGitPathList targetList;
2278 FillListOfSelectedItemPaths(targetList);
2280 // make sure that the list is reverse sorted, so that
2281 // children are removed before any parents
2282 targetList.SortByPathname(true);
2284 // put all reverted files in the trashbin, except the ones with 'added'
2285 // status because they are not restored by the revert.
2286 CTGitPathList delList;
2287 POSITION pos2 = GetFirstSelectedItemPosition();
2288 int index2;
2289 while ((index2 = GetNextSelectedItem(pos2)) >= 0)
2291 auto entry = GetListEntry(index2);
2292 if (entry&&(!(entry->m_Action& CTGitPath::LOGACTIONS_ADDED))
2293 && (!(entry->m_Action& CTGitPath::LOGACTIONS_REPLACED)) && !entry->IsDirectory())
2295 CTGitPath fullpath;
2296 fullpath.SetFromWin(g_Git.CombinePath(entry));
2297 delList.AddPath(fullpath);
2300 if (DWORD(CRegDWORD(L"Software\\TortoiseGit\\RevertWithRecycleBin", TRUE)))
2301 delList.DeleteAllFiles(true);
2303 CString revertToCommit = L"HEAD";
2304 if (m_amend)
2305 revertToCommit = L"HEAD~1";
2306 CString err;
2307 if (g_Git.Revert(revertToCommit, targetList, err))
2308 MessageBox(L"Revert failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
2309 else
2311 bool updateStatusList = false;
2312 for (int i = 0 ; i < targetList.GetCount(); ++i)
2314 int nListboxEntries = GetItemCount();
2315 for (int nItem=0; nItem<nListboxEntries; ++nItem)
2317 auto path = GetListEntry(nItem);
2318 if (path->GetGitPathString()==targetList[i].GetGitPathString() && !path->IsDirectory())
2320 if(path->m_Action & CTGitPath::LOGACTIONS_ADDED)
2322 path->m_Action = CTGitPath::LOGACTIONS_UNVER;
2323 SetEntryCheck(path,nItem,false);
2324 updateStatusList = true;
2325 #if 0 // revert an added file and some entry will be cloned (part 1/2)
2326 SetItemGroup(nItem,1);
2327 this->m_StatusFileList.RemoveItem(*path);
2328 this->m_UnRevFileList.AddPath(*path);
2329 //this->m_IgnoreFileList.RemoveItem(*path);
2330 #endif
2332 else
2334 if (GetCheck(nItem))
2335 m_nSelected--;
2336 RemoveListEntry(nItem);
2338 break;
2340 else if (path->GetGitPathString()==targetList[i].GetGitPathString() && path->IsDirectory() && path->IsWCRoot())
2342 CString sCmd;
2343 sCmd.Format(L"/command:revert /path:\"%s\"", (LPCTSTR)path->GetGitPathString());
2344 CCommonAppUtils::RunTortoiseGitProc(sCmd);
2348 SetRedraw(TRUE);
2349 #if 0 // revert an added file and some entry will be cloned (part 2/2)
2350 Show(m_dwShow, 0, m_bShowFolders,updateStatusList,true);
2351 NotifyCheck();
2352 #else
2353 if (updateStatusList && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2354 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2355 #endif
2359 break;
2361 case IDGITLC_ASSUMEVALID:
2362 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_ASSUMEVALID, BST_CHECKED, BST_INDETERMINATE);
2363 break;
2364 case IDGITLC_SKIPWORKTREE:
2365 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_SKIPWORKTREE, BST_INDETERMINATE, BST_CHECKED);
2366 break;
2367 case IDGITLC_UNSETIGNORELOCALCHANGES:
2368 SetGitIndexFlagsForSelectedFiles(IDS_PROC_UNSET_IGNORELOCALCHANGES, BST_UNCHECKED, BST_UNCHECKED);
2369 break;
2370 case IDGITLC_COPYFULL:
2371 case IDGITLC_COPYRELPATHS:
2372 case IDGITLC_COPYFILENAMES:
2373 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME, cmd);
2374 break;
2375 case IDGITLC_COPYEXT:
2376 CopySelectedEntriesToClipboard((DWORD)-1, 0);
2377 break;
2378 case IDGITLC_COPYCOL:
2379 CopySelectedEntriesToClipboard((DWORD)1 << selSubitem, 0);
2380 break;
2381 case IDGITLC_EXPORT:
2382 FilesExport();
2383 break;
2384 case IDGITLC_SAVEAS:
2385 FileSaveAs(filepath);
2386 break;
2388 case IDGITLC_REVERTTOREV:
2389 RevertSelectedItemToVersion();
2390 break;
2391 case IDGITLC_REVERTTOPARENT:
2392 RevertSelectedItemToVersion(true);
2393 break;
2394 #if 0
2395 case IDSVNLC_COMMIT:
2397 CTSVNPathList targetList;
2398 FillListOfSelectedItemPaths(targetList);
2399 CTSVNPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2400 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2401 CString commandline = CPathUtils::GetAppDirectory();
2402 commandline += L"TortoiseGitProc.exe /command:commit /pathfile:\"";
2403 commandline += tempFile.GetWinPathString();
2404 commandline += L'"';
2405 commandline += L" /deletepathfile";
2406 CAppUtils::LaunchApplication(commandline, nullptr, false);
2408 break;
2409 case IDSVNLC_CREATEIGNORECS:
2410 CreateChangeList(SVNSLC_IGNORECHANGELIST);
2411 break;
2412 case IDSVNLC_CREATECS:
2414 CCreateChangelistDlg dlg;
2415 if (dlg.DoModal() == IDOK)
2416 CreateChangeList(dlg.m_sName);
2418 break;
2419 default:
2421 if (cmd < IDSVNLC_MOVETOCS)
2422 break;
2423 CTSVNPathList changelistItems;
2424 FillListOfSelectedItemPaths(changelistItems);
2426 // find the changelist name
2427 CString sChangelist;
2428 int cmdID = IDSVNLC_MOVETOCS;
2429 SetRedraw(FALSE);
2430 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
2432 if ((it->first.Compare(SVNSLC_IGNORECHANGELIST))&&(entry->changelist.Compare(it->first)))
2434 if (cmd == cmdID)
2435 sChangelist = it->first;
2436 cmdID++;
2439 if (!sChangelist.IsEmpty())
2441 SVN git;
2442 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
2444 // The changelists were moved, but we now need to run through the selected items again
2445 // and update their changelist
2446 POSITION pos = GetFirstSelectedItemPosition();
2447 int index;
2448 while ((index = GetNextSelectedItem(pos)) >= 0)
2450 FileEntry * e = GetListEntry(index);
2451 e->changelist = sChangelist;
2452 if (!e->IsFolder())
2454 if (m_changelists.find(e->changelist)!=m_changelists.end())
2455 SetItemGroup(index, m_changelists[e->changelist]);
2456 else
2457 SetItemGroup(index, 0);
2461 else
2462 MessageBox(git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
2464 SetRedraw(TRUE);
2466 break;
2467 #endif
2469 } // switch (cmd)
2470 m_bWaitCursor = false;
2471 GetStatisticsString();
2472 //int iItemCountAfterMenuCmd = GetItemCount();
2473 //if (iItemCountAfterMenuCmd != iItemCountBeforeMenuCmd)
2475 // CWnd* pParent = GetParent();
2476 // if (pParent && pParent->GetSafeHwnd())
2477 // {
2478 // pParent->SendMessage(SVNSLNM_ITEMCOUNTCHANGED);
2479 // }
2481 } // if (popup.CreatePopupMenu())
2482 } // if (selIndex >= 0)
2485 void CGitStatusListCtrl::SetGitIndexFlagsForSelectedFiles(UINT message, BOOL assumevalid, BOOL skipworktree)
2487 if (CMessageBox::Show(GetSafeHwnd(), message, IDS_APPNAME, MB_YESNO | MB_DEFBUTTON2 | MB_ICONQUESTION) != IDYES)
2488 return;
2490 CAutoReadLock locker(m_guard);
2492 CAutoRepository repository(g_Git.GetGitRepository());
2493 if (!repository)
2495 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2496 return;
2499 CAutoIndex gitindex;
2500 if (git_repository_index(gitindex.GetPointer(), repository))
2502 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2503 return;
2506 POSITION pos = GetFirstSelectedItemPosition();
2507 int index = -1;
2508 while ((index = GetNextSelectedItem(pos)) >= 0)
2510 auto path = GetListEntry(index);
2511 if (path == nullptr)
2512 continue;
2514 size_t idx;
2515 if (!git_index_find(&idx, gitindex, CUnicodeUtils::GetMulti(path->GetGitPathString(), CP_UTF8)))
2517 git_index_entry *e = const_cast<git_index_entry *>(git_index_get_byindex(gitindex, idx)); // HACK
2518 if (assumevalid == BST_UNCHECKED)
2519 e->flags &= ~GIT_IDXENTRY_VALID;
2520 else if (assumevalid == BST_CHECKED)
2521 e->flags |= GIT_IDXENTRY_VALID;
2522 if (skipworktree == BST_UNCHECKED)
2523 e->flags_extended &= ~GIT_IDXENTRY_SKIP_WORKTREE;
2524 else if (skipworktree == BST_CHECKED)
2525 e->flags_extended |= GIT_IDXENTRY_SKIP_WORKTREE;
2526 git_index_add(gitindex, e);
2528 else
2529 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2532 if (git_index_write(gitindex))
2534 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2535 return;
2538 if (nullptr != GetLogicalParent() && nullptr != GetLogicalParent()->GetSafeHwnd())
2539 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2541 SetRedraw(TRUE);
2544 void CGitStatusListCtrl::OnContextMenu(CWnd* pWnd, CPoint point)
2546 __super::OnContextMenu(pWnd, point);
2547 if (pWnd == this)
2548 OnContextMenuList(pWnd, point);
2551 void CGitStatusListCtrl::OnNMDblclk(NMHDR *pNMHDR, LRESULT *pResult)
2553 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
2554 *pResult = 0;
2556 CAutoReadWeakLock readLock(m_guard);
2557 if (!readLock.IsAcquired())
2558 return;
2560 if (pNMLV->iItem < 0)
2561 return;
2563 auto file = GetListEntry(pNMLV->iItem);
2565 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)) {
2566 StartDiffWC(pNMLV->iItem);
2567 return;
2569 if( file->m_Action&CTGitPath::LOGACTIONS_UNMERGED )
2571 if (CAppUtils::ConflictEdit(*file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2573 CString conflictedFile = g_Git.CombinePath(file);
2574 if (!PathFileExists(conflictedFile) && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2576 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2577 return;
2579 StoreScrollPos();
2580 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2583 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))
2584 return;
2585 else
2587 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
2588 StartDiffTwo(pNMLV->iItem);
2589 else
2590 StartDiff(pNMLV->iItem);
2593 void CGitStatusListCtrl::StartDiffTwo(int fileindex)
2595 if(fileindex<0)
2596 return;
2598 CAutoReadLock locker(m_guard);
2599 auto ptr = GetListEntry(fileindex);
2600 if (!ptr)
2601 return;
2602 CTGitPath file1 = *ptr;
2604 if (file1.m_Action & CTGitPath::LOGACTIONS_ADDED)
2605 CGitDiff::DiffNull(&file1, m_Rev1, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2606 else if (file1.m_Action & CTGitPath::LOGACTIONS_DELETED)
2607 CGitDiff::DiffNull(&file1, m_Rev2, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2608 else
2609 CGitDiff::Diff(&file1, &file1, m_Rev1, m_Rev2, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2612 void CGitStatusListCtrl::StartDiffWC(int fileindex)
2614 if(fileindex<0)
2615 return;
2617 CAutoReadLock locker(m_guard);
2618 if (m_CurrentVersion.IsEmpty())
2619 m_CurrentVersion == GIT_REV_ZERO;
2621 auto ptr = GetListEntry(fileindex);
2622 if (!ptr)
2623 return;
2624 CTGitPath file1 = *ptr;
2625 file1.m_Action = 0; // reset action, so that diff is not started as added/deleted file; see issue #1757
2627 CGitDiff::Diff(&file1, &file1, GIT_REV_ZERO, m_CurrentVersion, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2630 void CGitStatusListCtrl::StartDiff(int fileindex)
2632 if(fileindex<0)
2633 return;
2635 CAutoReadLock locker(m_guard);
2636 auto ptr = GetListEntry(fileindex);
2637 if (!ptr)
2638 return;
2639 CTGitPath file1 = *ptr;
2640 CTGitPath file2;
2641 if(file1.m_Action & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY))
2642 file2.SetFromGit(file1.GetGitOldPathString());
2643 else
2644 file2=file1;
2646 if(this->m_CurrentVersion.IsEmpty() || m_CurrentVersion== GIT_REV_ZERO)
2648 CString fromwhere;
2649 if(m_amend && (file1.m_Action & CTGitPath::LOGACTIONS_ADDED) == 0)
2650 fromwhere = L"~1";
2651 if( g_Git.IsInitRepos())
2652 CGitDiff::DiffNull(GetListEntry(fileindex),
2653 GIT_REV_ZERO, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2654 else if( file1.m_Action&CTGitPath::LOGACTIONS_ADDED )
2655 CGitDiff::DiffNull(GetListEntry(fileindex),
2656 m_CurrentVersion + fromwhere, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2657 else if( file1.m_Action&CTGitPath::LOGACTIONS_DELETED )
2658 CGitDiff::DiffNull(GetListEntry(fileindex),
2659 GitRev::GetHead() + fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2660 else
2661 CGitDiff::Diff(&file1,&file2,
2662 CString(GIT_REV_ZERO),
2663 GitRev::GetHead() + fromwhere, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2665 else
2667 CGitHash hash;
2668 CString fromwhere = m_CurrentVersion + L"~1";
2669 if(m_amend)
2670 fromwhere = m_CurrentVersion + L"~2";
2671 bool revfail = !!g_Git.GetHash(hash, fromwhere);
2672 if (revfail || (file1.m_Action & file1.LOGACTIONS_ADDED))
2673 CGitDiff::DiffNull(&file1, m_CurrentVersion, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2674 else if (file1.m_Action & file1.LOGACTIONS_DELETED)
2676 if (file1.m_ParentNo > 0)
2677 fromwhere.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, file1.m_ParentNo + 1);
2679 CGitDiff::DiffNull(&file1, fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2681 else
2683 if( file1.m_ParentNo & MERGE_MASK)
2685 CTGitPath base, theirs, mine, merge;
2687 CString temppath;
2688 GetTempPath(temppath);
2689 temppath.TrimRight(L'\\');
2691 mine.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".LOCAL" + file1.GetFileExtension());
2692 theirs.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".REMOTE" + file1.GetFileExtension());
2693 base.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".BASE" + file1.GetFileExtension());
2695 CFile tempfile;
2696 //create a empty file, incase stage is not three
2697 tempfile.Open(mine.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2698 tempfile.Close();
2699 tempfile.Open(theirs.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2700 tempfile.Close();
2701 tempfile.Open(base.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2702 tempfile.Close();
2704 merge.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".Merged" + file1.GetFileExtension());
2706 int parent1=-1, parent2 =-1;
2707 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
2709 if(m_arStatusArray[i]->GetGitPathString() == file1.GetGitPathString())
2711 if(m_arStatusArray[i]->m_ParentNo & MERGE_MASK)
2714 else
2716 if(parent1<0)
2717 parent1 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
2718 else if (parent2 <0)
2719 parent2 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
2724 if(g_Git.GetOneFile(m_CurrentVersion, file1, (CString&)merge.GetWinPathString()))
2725 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2727 if(parent1>=0)
2729 CString str;
2730 str.Format(L"%s^%d", (LPCTSTR)this->m_CurrentVersion, parent1 + 1);
2732 if(g_Git.GetOneFile(str, file1, (CString&)mine.GetWinPathString()))
2733 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2736 if(parent2>=0)
2738 CString str;
2739 str.Format(L"%s^%d", (LPCTSTR)this->m_CurrentVersion, parent2 + 1);
2741 if(g_Git.GetOneFile(str, file1, (CString&)theirs.GetWinPathString()))
2742 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2745 if(parent1>=0 && parent2>=0)
2747 CString cmd, output;
2748 cmd.Format(L"git.exe merge-base %s^%d %s^%d", (LPCTSTR)this->m_CurrentVersion, parent1 + 1,
2749 (LPCTSTR)m_CurrentVersion, parent2 + 1);
2751 if (!g_Git.Run(cmd, &output, nullptr, CP_UTF8))
2753 if (g_Git.GetOneFile(output.Left(2 * GIT_HASH_SIZE), file1, (CString&)base.GetWinPathString()))
2754 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETBASEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2757 CAppUtils::StartExtMerge(!!(GetAsyncKeyState(VK_SHIFT) & 0x8000), base, theirs, mine, merge, L"BASE", L"REMOTE", L"LOCAL");
2759 else
2761 CString str;
2762 if( (file1.m_ParentNo&PARENT_MASK) == 0)
2763 str = L"~1";
2764 else
2765 str.Format(L"^%d", (file1.m_ParentNo & PARENT_MASK) + 1);
2766 CGitDiff::Diff(&file1,&file2,
2767 m_CurrentVersion,
2768 m_CurrentVersion + str, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2774 CString CGitStatusListCtrl::GetStatisticsString(bool simple)
2776 CString sNormal = CString(MAKEINTRESOURCE(IDS_STATUSNORMAL));
2777 CString sAdded = CString(MAKEINTRESOURCE(IDS_STATUSADDED));
2778 CString sDeleted = CString(MAKEINTRESOURCE(IDS_STATUSDELETED));
2779 CString sModified = CString(MAKEINTRESOURCE(IDS_STATUSMODIFIED));
2780 CString sConflicted = CString(MAKEINTRESOURCE(IDS_STATUSCONFLICTED));
2781 CString sUnversioned = CString(MAKEINTRESOURCE(IDS_STATUSUNVERSIONED));
2782 CString sRenamed = CString(MAKEINTRESOURCE(IDS_STATUSREPLACED));
2783 CString sToolTip;
2784 if(simple)
2786 sToolTip.Format(IDS_STATUSLIST_STATUSLINE1,
2787 this->m_nLineAdded,this->m_nLineDeleted,
2788 (LPCTSTR)sModified, m_nModified,
2789 (LPCTSTR)sAdded, m_nAdded,
2790 (LPCTSTR)sDeleted, m_nDeleted,
2791 (LPCTSTR)sRenamed, m_nRenamed
2794 else
2796 sToolTip.Format(IDS_STATUSLIST_STATUSLINE2,
2797 this->m_nLineAdded,this->m_nLineDeleted,
2798 (LPCTSTR)sNormal, m_nNormal,
2799 (LPCTSTR)sUnversioned, m_nUnversioned,
2800 (LPCTSTR)sModified, m_nModified,
2801 (LPCTSTR)sAdded, m_nAdded,
2802 (LPCTSTR)sDeleted, m_nDeleted,
2803 (LPCTSTR)sConflicted, m_nConflicted
2806 CString sStats;
2807 sStats.Format(IDS_COMMITDLG_STATISTICSFORMAT, m_nSelected, GetItemCount());
2808 if (m_pStatLabel)
2809 m_pStatLabel->SetWindowText(sStats);
2811 if (m_pSelectButton)
2813 if (m_nSelected == 0)
2814 m_pSelectButton->SetCheck(BST_UNCHECKED);
2815 else if (m_nSelected != GetItemCount())
2816 m_pSelectButton->SetCheck(BST_INDETERMINATE);
2817 else
2818 m_pSelectButton->SetCheck(BST_CHECKED);
2821 if (m_pConfirmButton)
2822 m_pConfirmButton->EnableWindow(m_nSelected>0);
2824 return sToolTip;
2827 CString CGitStatusListCtrl::GetCommonDirectory(bool bStrict)
2829 CAutoReadLock locker(m_guard);
2830 if (!bStrict)
2832 // not strict means that the selected folder has priority
2833 if (!m_StatusFileList.GetCommonDirectory().IsEmpty())
2834 return m_StatusFileList.GetCommonDirectory().GetWinPath();
2837 CTGitPathList list;
2838 int nListItems = GetItemCount();
2839 for (int i=0; i<nListItems; ++i)
2841 auto* entry = GetListEntry(i);
2842 if (entry->IsEmpty())
2843 continue;
2844 list.AddPath(*entry);
2846 return list.GetCommonRoot().GetWinPath();
2849 void CGitStatusListCtrl::SelectAll(bool bSelect, bool /*bIncludeNoCommits*/)
2851 CWaitCursor waitCursor;
2852 // block here so the LVN_ITEMCHANGED messages
2853 // get ignored
2854 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
2857 CAutoWriteLock locker(m_guard);
2858 SetRedraw(FALSE);
2860 int nListItems = GetItemCount();
2861 if (bSelect)
2862 m_nSelected = nListItems;
2863 else
2864 m_nSelected = 0;
2866 for (int i=0; i<nListItems; ++i)
2868 auto path = GetListEntry(i);
2869 if (!path)
2870 continue;
2871 //if ((bIncludeNoCommits)||(entry->GetChangeList().Compare(SVNSLC_IGNORECHANGELIST)))
2872 SetEntryCheck(path,i,bSelect);
2875 SetRedraw(TRUE);
2876 GetStatisticsString();
2877 NotifyCheck();
2880 void CGitStatusListCtrl::Check(DWORD dwCheck, bool check)
2882 CWaitCursor waitCursor;
2883 // block here so the LVN_ITEMCHANGED messages
2884 // get ignored
2886 CAutoWriteLock locker(m_guard);
2887 SetRedraw(FALSE);
2888 ScopedInDecrement blocker(m_nBlockItemChangeHandler);
2890 int nListItems = GetItemCount();
2892 for (int i = 0; i < nListItems; ++i)
2894 auto entry = GetListEntry(i);
2895 if (!entry)
2896 continue;
2898 DWORD showFlags = entry->m_Action;
2899 if (entry->IsDirectory())
2900 showFlags |= GITSLC_SHOWSUBMODULES;
2901 else
2902 showFlags |= GITSLC_SHOWFILES;
2904 if (check && (showFlags & dwCheck) && !GetCheck(i) && !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules && !(dwCheck & GITSLC_SHOWSUBMODULES)))
2906 SetEntryCheck(entry, i, true);
2907 m_nSelected++;
2909 else if (!check && (showFlags & dwCheck) && GetCheck(i))
2911 SetEntryCheck(entry, i, false);
2912 m_nSelected--;
2916 SetRedraw(TRUE);
2917 GetStatisticsString();
2918 NotifyCheck();
2921 void CGitStatusListCtrl::OnLvnGetInfoTip(NMHDR *pNMHDR, LRESULT *pResult)
2923 LPNMLVGETINFOTIP pGetInfoTip = reinterpret_cast<LPNMLVGETINFOTIP>(pNMHDR);
2924 *pResult = 0;
2925 if (CRegDWORD(L"Software\\TortoiseGit\\ShowListFullPathTooltip", TRUE) != TRUE)
2926 return;
2928 CAutoReadWeakLock readLock(m_guard);
2929 if (!readLock.IsAcquired())
2930 return;
2932 auto entry = GetListEntry(pGetInfoTip->iItem);
2934 if (entry)
2935 if (pGetInfoTip->cchTextMax > entry->GetGitPathString().GetLength() + g_Git.m_CurrentDir.GetLength())
2937 CString str = g_Git.CombinePath(entry->GetWinPathString());
2938 wcsncpy_s(pGetInfoTip->pszText, pGetInfoTip->cchTextMax, str.GetBuffer(), pGetInfoTip->cchTextMax - 1);
2942 void CGitStatusListCtrl::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult)
2944 NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
2946 // Take the default processing unless we set this to something else below.
2947 *pResult = CDRF_DODEFAULT;
2949 // First thing - check the draw stage. If it's the control's prepaint
2950 // stage, then tell Windows we want messages for every item.
2952 switch (pLVCD->nmcd.dwDrawStage)
2954 case CDDS_PREPAINT:
2955 *pResult = CDRF_NOTIFYITEMDRAW;
2956 break;
2957 case CDDS_ITEMPREPAINT:
2959 // This is the prepaint stage for an item. Here's where we set the
2960 // item's text color. Our return value will tell Windows to draw the
2961 // item itself, but it will use the new color we set here.
2963 // Tell Windows to paint the control itself.
2964 *pResult = CDRF_DODEFAULT;
2965 CAutoReadWeakLock readLock(m_guard, 0);
2966 if (!readLock.IsAcquired())
2967 return;
2969 COLORREF crText = GetSysColor(COLOR_WINDOWTEXT);
2971 if (m_arStatusArray.size() > (DWORD_PTR)pLVCD->nmcd.dwItemSpec)
2973 auto entry = GetListEntry((int)pLVCD->nmcd.dwItemSpec);
2974 if (!entry)
2975 return;
2977 // coloring
2978 // ========
2979 // black : unversioned, normal
2980 // purple : added
2981 // blue : modified
2982 // brown : missing, deleted, replaced
2983 // green : merged (or potential merges)
2984 // red : conflicts or sure conflicts
2985 if(entry->m_Action & CTGitPath::LOGACTIONS_GRAY)
2986 crText = RGB(128,128,128);
2987 else if(entry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
2988 crText = m_Colors.GetColor(CColors::Conflict);
2989 else if(entry->m_Action & (CTGitPath::LOGACTIONS_MODIFIED))
2990 crText = m_Colors.GetColor(CColors::Modified);
2991 else if(entry->m_Action & (CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
2992 crText = m_Colors.GetColor(CColors::Added);
2993 else if(entry->m_Action & CTGitPath::LOGACTIONS_DELETED)
2994 crText = m_Colors.GetColor(CColors::Deleted);
2995 else if(entry->m_Action & CTGitPath::LOGACTIONS_REPLACED)
2996 crText = m_Colors.GetColor(CColors::Renamed);
2997 else if(entry->m_Action & CTGitPath::LOGACTIONS_MERGED)
2998 crText = m_Colors.GetColor(CColors::Merged);
2999 else
3000 crText = GetSysColor(COLOR_WINDOWTEXT);
3001 // Store the color back in the NMLVCUSTOMDRAW struct.
3002 pLVCD->clrText = crText;
3005 break;
3009 void CGitStatusListCtrl::OnLvnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult)
3011 auto pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);
3012 *pResult = 0;
3014 // Create a pointer to the item
3015 LV_ITEM* pItem = &(pDispInfo)->item;
3017 CAutoReadWeakLock readLock(m_guard, 0);
3018 if (readLock.IsAcquired())
3020 if (pItem->mask & LVIF_TEXT)
3022 CString text = GetCellText(pItem->iItem, pItem->iSubItem);
3023 lstrcpyn(pItem->pszText, text, pItem->cchTextMax - 1);
3026 else
3027 pItem->mask = 0;
3030 BOOL CGitStatusListCtrl::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
3032 if (pWnd != this)
3033 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3034 if (!m_bWaitCursor && !m_bBusy)
3036 HCURSOR hCur = LoadCursor(nullptr, IDC_ARROW);
3037 SetCursor(hCur);
3038 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3040 HCURSOR hCur = LoadCursor(nullptr, IDC_WAIT);
3041 SetCursor(hCur);
3042 return TRUE;
3045 void CGitStatusListCtrl::RemoveListEntry(int index)
3047 CAutoWriteLock locker(m_guard);
3048 DeleteItem(index);
3050 m_arStatusArray.erase(m_arStatusArray.cbegin() + index);
3052 #if 0
3053 delete m_arStatusArray[m_arListArray[index]];
3054 m_arStatusArray.erase(m_arStatusArray.begin()+m_arListArray[index]);
3055 m_arListArray.erase(m_arListArray.begin()+index);
3056 for (int i=index; i< (int)m_arListArray.size(); ++i)
3058 m_arListArray[i]--;
3060 #endif
3063 ///< Set a checkbox on an entry in the listbox
3064 // NEVER, EVER call SetCheck directly, because you'll end-up with the checkboxes and the 'checked' flag getting out of sync
3065 void CGitStatusListCtrl::SetEntryCheck(CTGitPath* pEntry, int listboxIndex, bool bCheck)
3067 CAutoWriteLock locker(m_guard);
3068 pEntry->m_Checked = bCheck;
3069 m_mapFilenameToChecked[pEntry->GetGitPathString()] = bCheck;
3070 SetCheck(listboxIndex, bCheck);
3073 void CGitStatusListCtrl::ResetChecked(const CTGitPath& entry)
3075 CAutoWriteLock locker(m_guard);
3076 CTGitPath adjustedEntry;
3077 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3078 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3079 else
3080 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3081 if (entry.IsDirectory())
3083 STRING_VECTOR toDelete;
3084 for (auto it = m_mapFilenameToChecked.begin(); it != m_mapFilenameToChecked.end(); ++it)
3086 if (adjustedEntry.IsAncestorOf(it->first))
3087 toDelete.emplace_back(it->first);
3089 for (const auto& file : toDelete)
3090 m_mapFilenameToChecked.erase(file);
3091 return;
3093 m_mapFilenameToChecked.erase(adjustedEntry.GetGitPathString());
3096 #if 0
3097 void CGitStatusListCtrl::SetCheckOnAllDescendentsOf(const FileEntry* parentEntry, bool bCheck)
3099 CAutoWriteLock locker(m_guard);
3100 int nListItems = GetItemCount();
3101 for (int j=0; j< nListItems ; ++j)
3103 FileEntry * childEntry = GetListEntry(j);
3104 ASSERT(childEntry);
3105 if (!childEntry || childEntry == parentEntry)
3106 continue;
3107 if (childEntry->checked != bCheck)
3109 if (parentEntry->path.IsAncestorOf(childEntry->path))
3111 SetEntryCheck(childEntry,j,bCheck);
3112 if(bCheck)
3113 m_nSelected++;
3114 else
3115 m_nSelected--;
3120 #endif
3122 void CGitStatusListCtrl::WriteCheckedNamesToPathList(CTGitPathList& pathList)
3124 pathList.Clear();
3125 CAutoReadLock locker(m_guard);
3126 int nListItems = GetItemCount();
3127 for (int i = 0; i< nListItems; ++i)
3129 auto entry = GetListEntry(i);
3130 if (entry->m_Checked)
3131 pathList.AddPath(*entry);
3133 pathList.SortByPathname();
3137 /// Build a path list of all the selected items in the list (NOTE - SELECTED, not CHECKED)
3138 void CGitStatusListCtrl::FillListOfSelectedItemPaths(CTGitPathList& pathList, bool /*bNoIgnored*/)
3140 pathList.Clear();
3142 CAutoReadLock locker(m_guard);
3143 POSITION pos = GetFirstSelectedItemPosition();
3144 int index;
3145 while ((index = GetNextSelectedItem(pos)) >= 0)
3147 auto entry = GetListEntry(index);
3148 //if ((bNoIgnored)&&(entry->status == git_wc_status_ignored))
3149 // continue;
3150 pathList.AddPath(*entry);
3154 UINT CGitStatusListCtrl::OnGetDlgCode()
3156 // we want to process the return key and not have that one
3157 // routed to the default pushbutton
3158 return CListCtrl::OnGetDlgCode() | DLGC_WANTALLKEYS;
3161 void CGitStatusListCtrl::OnNMReturn(NMHDR * /*pNMHDR*/, LRESULT *pResult)
3163 *pResult = 0;
3164 CAutoReadWeakLock readLock(m_guard);
3165 if (!readLock.IsAcquired())
3166 return;
3167 if (!CheckMultipleDiffs())
3168 return;
3169 bool needsRefresh = false;
3170 bool resolvedTreeConfict = false;
3171 POSITION pos = GetFirstSelectedItemPosition();
3172 while ( pos )
3174 int index = GetNextSelectedItem(pos);
3175 if (index < 0)
3176 return;
3177 auto file = GetListEntry(index);
3178 if (file == nullptr)
3179 return;
3180 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
3181 StartDiffWC(index);
3182 else if ((file->m_Action & CTGitPath::LOGACTIONS_UNMERGED))
3184 if (CAppUtils::ConflictEdit(*file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
3186 CString conflictedFile = g_Git.CombinePath(file);
3187 needsRefresh = needsRefresh || !PathFileExists(conflictedFile);
3188 resolvedTreeConfict = resolvedTreeConfict || (file->m_Action & CTGitPath::LOGACTIONS_UNMERGED) == 0;
3191 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))
3192 continue;
3193 else
3195 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
3196 StartDiffTwo(index);
3197 else
3198 StartDiff(index);
3201 if (needsRefresh && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
3202 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
3203 else if (resolvedTreeConfict)
3205 StoreScrollPos();
3206 Show(m_dwShow, 0, m_bShowFolders, 0, true);
3210 void CGitStatusListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
3212 // Since we catch all keystrokes (to have the enter key processed here instead
3213 // of routed to the default pushbutton) we have to make sure that other
3214 // keys like Tab and Esc still do what they're supposed to do
3215 // Tab = change focus to next/previous control
3216 // Esc = quit the dialog
3217 switch (nChar)
3219 case (VK_TAB):
3221 ::PostMessage(GetLogicalParent()->GetSafeHwnd(), WM_NEXTDLGCTL, GetKeyState(VK_SHIFT)&0x8000, 0);
3222 return;
3224 break;
3225 case (VK_ESCAPE):
3227 ::SendMessage(GetLogicalParent()->GetSafeHwnd(), WM_CLOSE, 0, 0);
3229 break;
3232 CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
3235 void CGitStatusListCtrl::PreSubclassWindow()
3237 __super::PreSubclassWindow();
3238 EnableToolTips(TRUE);
3239 SetWindowTheme(GetSafeHwnd(), L"Explorer", nullptr);
3242 void CGitStatusListCtrl::OnPaint()
3244 LRESULT defres = Default();
3245 if ((m_bBusy) || (m_bEmpty))
3247 CString str;
3248 if (m_bBusy)
3250 if (m_sBusy.IsEmpty())
3251 str.LoadString(IDS_STATUSLIST_BUSYMSG);
3252 else
3253 str = m_sBusy;
3255 else
3257 if (m_sEmpty.IsEmpty())
3258 str.LoadString(IDS_STATUSLIST_EMPTYMSG);
3259 else
3260 str = m_sEmpty;
3262 COLORREF clrText = ::GetSysColor(COLOR_WINDOWTEXT);
3263 COLORREF clrTextBk;
3264 if (IsWindowEnabled())
3265 clrTextBk = ::GetSysColor(COLOR_WINDOW);
3266 else
3267 clrTextBk = ::GetSysColor(COLOR_3DFACE);
3269 CRect rc;
3270 GetClientRect(&rc);
3271 CHeaderCtrl* pHC = GetHeaderCtrl();
3272 if (pHC)
3274 CRect rcH;
3275 pHC->GetItemRect(0, &rcH);
3276 rc.top += rcH.bottom;
3278 CDC* pDC = GetDC();
3280 CMyMemDC memDC(pDC, &rc);
3282 memDC.SetTextColor(clrText);
3283 memDC.SetBkColor(clrTextBk);
3284 memDC.BitBlt(rc.left, rc.top, rc.Width(), rc.Height(), pDC, rc.left, rc.top, SRCCOPY);
3285 rc.top += 10;
3286 CGdiObject * oldfont = memDC.SelectStockObject(DEFAULT_GUI_FONT);
3287 memDC.DrawText(str, rc, DT_CENTER | DT_VCENTER |
3288 DT_WORDBREAK | DT_NOPREFIX | DT_NOCLIP);
3289 memDC.SelectObject(oldfont);
3291 ReleaseDC(pDC);
3293 if (defres)
3295 // the Default() call did not process the WM_PAINT message!
3296 // Validate the update region ourselves to avoid
3297 // an endless loop repainting
3298 CRect rc;
3299 GetUpdateRect(&rc, FALSE);
3300 if (!rc.IsRectEmpty())
3301 ValidateRect(rc);
3305 void CGitStatusListCtrl::OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult)
3307 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
3309 CAutoReadLock locker(m_guard);
3311 CTGitPathList pathList;
3312 FillListOfSelectedItemPaths(pathList);
3313 if (pathList.IsEmpty())
3314 return;
3316 auto pdsrc = std::make_unique<CIDropSource>();
3317 if (!pdsrc)
3318 return;
3319 pdsrc->AddRef();
3321 GitDataObject* pdobj = new GitDataObject(pathList, m_Rev2.IsEmpty() ? m_CurrentVersion : m_Rev2);
3322 if (!pdobj)
3323 return;
3324 pdobj->AddRef();
3326 CDragSourceHelper dragsrchelper;
3328 SetRedraw(false);
3329 dragsrchelper.InitializeFromWindow(m_hWnd, pNMLV->ptAction, pdobj);
3330 SetRedraw(true);
3331 //dragsrchelper.InitializeFromBitmap()
3332 pdsrc->m_pIDataObj = pdobj;
3333 pdsrc->m_pIDataObj->AddRef();
3335 // Initiate the Drag & Drop
3336 DWORD dwEffect;
3337 m_bOwnDrag = true;
3338 ::DoDragDrop(pdobj, pdsrc.get(), DROPEFFECT_MOVE | DROPEFFECT_COPY, &dwEffect);
3339 m_bOwnDrag = false;
3340 pdsrc->Release();
3341 pdsrc.release();
3342 pdobj->Release();
3344 *pResult = 0;
3347 bool CGitStatusListCtrl::EnableFileDrop()
3349 m_bFileDropsEnabled = true;
3350 return true;
3353 bool CGitStatusListCtrl::HasPath(const CTGitPath& path)
3355 CAutoReadLock locker(m_guard);
3356 CTGitPath adjustedEntry;
3357 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3358 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3359 else
3360 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3361 for (size_t i=0; i < m_arStatusArray.size(); ++i)
3363 if (m_arStatusArray[i]->IsEquivalentTo(adjustedEntry))
3364 return true;
3367 return false;
3370 BOOL CGitStatusListCtrl::PreTranslateMessage(MSG* pMsg)
3372 if (pMsg->message == WM_KEYDOWN)
3374 switch (pMsg->wParam)
3376 case 'A':
3378 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3380 // select all entries
3381 for (int i=0; i<GetItemCount(); ++i)
3382 SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
3383 return TRUE;
3386 break;
3387 case 'C':
3388 case VK_INSERT:
3390 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3392 // copy all selected paths to the clipboard
3393 if (GetAsyncKeyState(VK_SHIFT)&0x8000)
3394 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME | GITSLC_COLSTATUS, IDGITLC_COPYRELPATHS);
3395 else
3396 CopySelectedEntriesToClipboard(GITSLC_COLFILENAME, IDGITLC_COPYRELPATHS);
3397 return TRUE;
3400 break;
3401 case VK_DELETE:
3403 if ((GetSelectedCount() > 0) && (m_dwContextMenus & GITSLC_POPDELETE))
3405 CAutoReadLock locker(m_guard);
3406 auto filepath = GetListEntry(GetSelectionMark());
3407 if (filepath != nullptr && (filepath->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
3408 DeleteSelectedFiles();
3411 break;
3415 return __super::PreTranslateMessage(pMsg);
3418 bool CGitStatusListCtrl::CopySelectedEntriesToClipboard(DWORD dwCols, int cmd)
3420 if (GetSelectedCount() == 0)
3421 return false;
3423 CString sClipboard;
3425 bool bMultipleColumnSelected = ((dwCols & dwCols - 1) != 0); // multiple columns are selected (clear least signifient bit and check for zero)
3427 #define ADDTOCLIPBOARDSTRING(x) sClipboard += (sClipboard.IsEmpty() || (sClipboard.Right(1)==L"\n")) ? (x) : ('\t' + x)
3428 #define ADDNEWLINETOCLIPBOARDSTRING() sClipboard += (sClipboard.IsEmpty()) ? L"" : L"\r\n"
3430 // first add the column titles as the first line
3431 DWORD selection = 0;
3432 int count = m_ColumnManager.GetColumnCount();
3433 for (int column = 0; column < count; ++column)
3435 if ((dwCols == -1 && m_ColumnManager.IsVisible(column)) || (column < GITSLC_NUMCOLUMNS && (dwCols & (1 << column))))
3437 if (bMultipleColumnSelected)
3438 ADDTOCLIPBOARDSTRING(m_ColumnManager.GetName(column));
3440 selection |= 1 << column;
3444 if (bMultipleColumnSelected)
3445 ADDNEWLINETOCLIPBOARDSTRING();
3447 // maybe clear first line when only one column is selected (btw by select not by dwCols) is simpler(not faster) way
3448 // but why no title on single column output ?
3449 // if (selection & selection-1) == 0 ) sClipboard = "";
3451 CAutoReadLock locker(m_guard);
3453 POSITION pos = GetFirstSelectedItemPosition();
3454 while (pos)
3456 int index = GetNextSelectedItem(pos);
3457 // we selected only cols we want, so not other then select test needed
3458 for (int column = 0; column < count; ++column)
3460 if (cmd && (GITSLC_COLFILENAME & (1 << column)))
3462 auto* entry = GetListEntry(index);
3463 if (entry)
3465 CString sPath;
3466 switch (cmd)
3468 case IDGITLC_COPYFULL:
3469 sPath = g_Git.CombinePath(entry);
3470 break;
3471 case IDGITLC_COPYRELPATHS:
3472 sPath = entry->GetGitPathString();
3473 break;
3474 case IDGITLC_COPYFILENAMES:
3475 sPath = entry->GetFileOrDirectoryName();
3476 break;
3478 ADDTOCLIPBOARDSTRING(sPath);
3481 else if (selection & (1 << column))
3482 ADDTOCLIPBOARDSTRING(GetCellText(index, column));
3485 ADDNEWLINETOCLIPBOARDSTRING();
3488 return CStringUtils::WriteAsciiStringToClipboard(sClipboard);
3491 size_t CGitStatusListCtrl::GetNumberOfChangelistsInSelection()
3493 #if 0
3494 CAutoReadLock locker(m_guard);
3495 std::set<CString> changelists;
3496 POSITION pos = GetFirstSelectedItemPosition();
3497 int index;
3498 while ((index = GetNextSelectedItem(pos)) >= 0)
3500 FileEntry * entry = GetListEntry(index);
3501 if (!entry->changelist.IsEmpty())
3502 changelists.insert(entry->changelist);
3504 return changelists.size();
3505 #endif
3506 return 0;
3509 bool CGitStatusListCtrl::PrepareGroups(bool bForce /* = false */)
3511 CAutoWriteLock locker(m_guard);
3512 bool bHasGroups=false;
3513 int max =0;
3515 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
3517 int ParentNo = m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3518 if( ParentNo > max)
3519 max=m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3522 if (((m_dwShow & GITSLC_SHOWUNVERSIONED) && !m_UnRevFileList.IsEmpty()) ||
3523 ((m_dwShow & GITSLC_SHOWIGNORED) && !m_IgnoreFileList.IsEmpty()) ||
3524 (m_dwShow & (GITSLC_SHOWASSUMEVALID | GITSLC_SHOWSKIPWORKTREE) && !m_LocalChangesIgnoredFileList.IsEmpty()) ||
3525 max>0 || bForce)
3527 bHasGroups = true;
3530 RemoveAllGroups();
3531 EnableGroupView(bHasGroups);
3533 TCHAR groupname[1024] = { 0 };
3534 int groupindex = 0;
3536 if(bHasGroups)
3538 LVGROUP grp = {0};
3539 grp.cbSize = sizeof(LVGROUP);
3540 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3541 groupindex=0;
3543 //if(m_UnRevFileList.GetCount()>0)
3544 if(max >0)
3546 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MERGEDFILES)), 1023);
3547 grp.pszHeader = groupname;
3548 grp.iGroupId = MERGE_MASK;
3549 grp.uAlign = LVGA_HEADER_LEFT;
3550 InsertGroup(0, &grp);
3552 CAutoRepository repository(g_Git.GetGitRepository());
3553 if (!repository)
3554 MessageBox(CGit::GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK | MB_ICONERROR);
3555 for (int i = 0; i <= max; ++i)
3557 CString str;
3558 str.Format(IDS_STATUSLIST_GROUP_DIFFWITHPARENT, i+1);
3559 if (repository)
3561 CString rev;
3562 rev.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, i + 1);
3563 CGitHash hash;
3564 if (!CGit::GetHash(repository, hash, rev))
3565 str += L": " + hash.ToString().Left(g_Git.GetShortHASHLength());
3567 grp.pszHeader = str.GetBuffer();
3568 str.ReleaseBuffer();
3569 grp.iGroupId = i;
3570 grp.uAlign = LVGA_HEADER_LEFT;
3571 InsertGroup(i+1, &grp);
3574 else
3576 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MODIFIEDFILES)), 1023);
3577 grp.pszHeader = groupname;
3578 grp.iGroupId = groupindex;
3579 grp.uAlign = LVGA_HEADER_LEFT;
3580 InsertGroup(groupindex++, &grp);
3583 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_NOTVERSIONEDFILES)), 1023);
3584 grp.pszHeader = groupname;
3585 grp.iGroupId = groupindex;
3586 grp.uAlign = LVGA_HEADER_LEFT;
3587 InsertGroup(groupindex++, &grp);
3590 //if(m_IgnoreFileList.GetCount()>0)
3592 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNOREDFILES)), 1023);
3593 grp.pszHeader = groupname;
3594 grp.iGroupId = groupindex;
3595 grp.uAlign = LVGA_HEADER_LEFT;
3596 InsertGroup(groupindex++, &grp);
3600 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNORELOCALCHANGES)), 1023);
3601 grp.pszHeader = groupname;
3602 grp.iGroupId = groupindex;
3603 grp.uAlign = LVGA_HEADER_LEFT;
3604 InsertGroup(groupindex++, &grp);
3609 #if 0
3610 m_bHasIgnoreGroup = false;
3612 // now add the items which don't belong to a group
3613 LVGROUP grp = {0};
3614 grp.cbSize = sizeof(LVGROUP);
3615 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3616 CString sUnassignedName(MAKEINTRESOURCE(IDS_STATUSLIST_UNASSIGNED_CHANGESET));
3617 wcsncpy_s(groupname, 1024, (LPCTSTR)sUnassignedName, 1023);
3618 grp.pszHeader = groupname;
3619 grp.iGroupId = groupindex;
3620 grp.uAlign = LVGA_HEADER_LEFT;
3621 InsertGroup(groupindex++, &grp);
3623 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
3625 if (it->first.Compare(SVNSLC_IGNORECHANGELIST)!=0)
3627 LVGROUP grp = {0};
3628 grp.cbSize = sizeof(LVGROUP);
3629 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3630 wcsncpy_s(groupname, 1024, it->first, 1023);
3631 grp.pszHeader = groupname;
3632 grp.iGroupId = groupindex;
3633 grp.uAlign = LVGA_HEADER_LEFT;
3634 it->second = InsertGroup(groupindex++, &grp);
3636 else
3637 m_bHasIgnoreGroup = true;
3640 if (m_bHasIgnoreGroup)
3642 // and now add the group 'ignore-on-commit'
3643 std::map<CString,int>::iterator it = m_changelists.find(SVNSLC_IGNORECHANGELIST);
3644 if (it != m_changelists.end())
3646 grp.cbSize = sizeof(LVGROUP);
3647 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3648 wcsncpy_s(groupname, 1024, SVNSLC_IGNORECHANGELIST, 1023);
3649 grp.pszHeader = groupname;
3650 grp.iGroupId = groupindex;
3651 grp.uAlign = LVGA_HEADER_LEFT;
3652 it->second = InsertGroup(groupindex, &grp);
3655 #endif
3656 return bHasGroups;
3659 void CGitStatusListCtrl::NotifyCheck()
3661 CWnd* pParent = GetLogicalParent();
3662 if (pParent && pParent->GetSafeHwnd())
3664 pParent->SendMessage(GITSLNM_CHECKCHANGED, m_nSelected);
3668 int CGitStatusListCtrl::UpdateFileList(const CTGitPathList* list)
3670 CAutoWriteLock locker(m_guard);
3671 m_CurrentVersion = GIT_REV_ZERO;
3673 g_Git.GetWorkingTreeChanges(m_StatusFileList, m_amend, list);
3675 BOOL bDeleteChecked = FALSE;
3676 int deleteFromIndex = 0;
3677 bool needsRefresh = false;
3678 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
3680 auto gitpatch = const_cast<CTGitPath*>(&m_StatusFileList[i]);
3681 gitpatch->m_Checked = TRUE;
3683 if ((gitpatch->m_Action & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_MODIFIED)) && !gitpatch->Exists())
3685 if (!bDeleteChecked)
3687 CString message;
3688 message.Format(IDS_ASK_REMOVE_FROM_INDEX, gitpatch->GetWinPath());
3689 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);
3691 if (deleteFromIndex == 1)
3693 CString err;
3694 if (g_Git.Run(L"git.exe checkout -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
3695 MessageBox(L"Restoring from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
3696 else
3697 needsRefresh = true;
3699 else if (deleteFromIndex == 2)
3701 CString err;
3702 if (g_Git.Run(L"git.exe rm -f --cache -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
3703 MessageBox(L"Removing from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
3704 else
3705 needsRefresh = true;
3709 m_arStatusArray.push_back(&m_StatusFileList[i]);
3712 if (needsRefresh)
3713 MessageBox(L"Due to changes to the index, please refresh the dialog (e.g., by pressing F5).", L"TortoiseGit", MB_ICONINFORMATION);
3715 return 0;
3718 int CGitStatusListCtrl::UpdateWithGitPathList(CTGitPathList &list)
3720 CAutoWriteLock locker(m_guard);
3721 m_arStatusArray.clear();
3722 for (int i = 0; i < list.GetCount(); ++i)
3724 auto gitpath = const_cast<CTGitPath*>(&list[i]);
3726 if(gitpath ->m_Action & CTGitPath::LOGACTIONS_HIDE)
3727 continue;
3729 gitpath->m_Checked = TRUE;
3730 m_arStatusArray.push_back(&list[i]);
3732 return 0;
3735 int CGitStatusListCtrl::UpdateUnRevFileList(CTGitPathList &list)
3737 CAutoWriteLock locker(m_guard);
3738 m_UnRevFileList = list;
3739 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
3741 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
3742 gitpatch->m_Checked = FALSE;
3743 m_arStatusArray.push_back(&m_UnRevFileList[i]);
3745 return 0;
3748 int CGitStatusListCtrl::UpdateUnRevFileList(const CTGitPathList* List)
3750 CAutoWriteLock locker(m_guard);
3751 CString err;
3752 if (m_UnRevFileList.FillUnRev(CTGitPath::LOGACTIONS_UNVER, List, &err))
3754 MessageBox(L"Failed to get UnRev file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
3755 return -1;
3758 if (m_StatusFileList.m_Action & CTGitPath::LOGACTIONS_DELETED)
3760 int unrev = 0;
3761 int status = 0;
3762 while (unrev < m_UnRevFileList.GetCount() && status < m_StatusFileList.GetCount())
3764 auto cmp = CTGitPath::Compare(m_UnRevFileList[unrev], m_StatusFileList[status]);
3765 if (cmp < 1)
3767 ++unrev;
3768 continue;
3770 if (cmp == 1)
3771 m_UnRevFileList.RemovePath(m_StatusFileList[status]);
3772 ++status;
3776 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
3778 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
3779 gitpatch->m_Checked = FALSE;
3780 m_arStatusArray.push_back(&m_UnRevFileList[i]);
3782 return 0;
3785 int CGitStatusListCtrl::UpdateIgnoreFileList(const CTGitPathList* List)
3787 CAutoWriteLock locker(m_guard);
3788 CString err;
3789 if (m_IgnoreFileList.FillUnRev(CTGitPath::LOGACTIONS_IGNORE, List, &err))
3791 MessageBox(L"Failed to get Ignore file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
3792 return -1;
3795 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
3797 auto gitpatch = const_cast<CTGitPath*>(&m_IgnoreFileList[i]);
3798 gitpatch->m_Checked = FALSE;
3799 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
3801 return 0;
3804 int CGitStatusListCtrl::UpdateLocalChangesIgnoredFileList(const CTGitPathList* list)
3806 CAutoWriteLock locker(m_guard);
3807 m_LocalChangesIgnoredFileList.FillBasedOnIndexFlags(GIT_IDXENTRY_VALID, GIT_IDXENTRY_SKIP_WORKTREE, list);
3808 for (int i = 0; i < m_LocalChangesIgnoredFileList.GetCount(); ++i)
3810 auto gitpatch = const_cast<CTGitPath*>(&m_LocalChangesIgnoredFileList[i]);
3811 gitpatch->m_Checked = FALSE;
3812 m_arStatusArray.push_back(&m_LocalChangesIgnoredFileList[i]);
3814 return 0;
3817 int CGitStatusListCtrl::UpdateFileList(int mask, bool once, const CTGitPathList* List)
3819 CAutoWriteLock locker(m_guard);
3820 if(mask&CGitStatusListCtrl::FILELIST_MODIFY)
3822 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_MODIFY)))
3824 UpdateFileList(List);
3825 m_FileLoaded|=CGitStatusListCtrl::FILELIST_MODIFY;
3828 if (mask & CGitStatusListCtrl::FILELIST_UNVER || mask & CGitStatusListCtrl::FILELIST_IGNORE)
3830 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_UNVER)))
3832 UpdateUnRevFileList(List);
3833 m_FileLoaded|=CGitStatusListCtrl::FILELIST_UNVER;
3835 if(mask&CGitStatusListCtrl::FILELIST_IGNORE && (once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_IGNORE))))
3837 UpdateIgnoreFileList(List);
3838 m_FileLoaded |= CGitStatusListCtrl::FILELIST_IGNORE;
3841 if (mask & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED && (once || (!(m_FileLoaded & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED))))
3843 UpdateLocalChangesIgnoredFileList(List);
3844 m_FileLoaded |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
3846 return 0;
3849 void CGitStatusListCtrl::Clear()
3851 CAutoWriteLock locker(m_guard);
3852 m_FileLoaded=0;
3853 this->DeleteAllItems();
3854 this->m_arListArray.clear();
3855 this->m_arStatusArray.clear();
3856 this->m_changelists.clear();
3859 bool CGitStatusListCtrl::CheckMultipleDiffs()
3861 UINT selCount = GetSelectedCount();
3862 if (selCount > max(3, (DWORD)CRegDWORD(L"Software\\TortoiseGit\\NumDiffWarning", 10)))
3864 CString message;
3865 message.Format(IDS_STATUSLIST_WARN_MAXDIFF, selCount);
3866 return MessageBox(message, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES;
3868 return true;
3871 //////////////////////////////////////////////////////////////////////////
3872 bool CGitStatusListCtrlDropTarget::OnDrop(FORMATETC* pFmtEtc, STGMEDIUM& medium, DWORD* /*pdwEffect*/, POINTL pt)
3874 if (pFmtEtc->cfFormat == CF_HDROP && medium.tymed == TYMED_HGLOBAL)
3876 HDROP hDrop = (HDROP)GlobalLock(medium.hGlobal);
3877 if (hDrop)
3879 TCHAR szFileName[MAX_PATH] = {0};
3881 UINT cFiles = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
3883 POINT clientpoint;
3884 clientpoint.x = pt.x;
3885 clientpoint.y = pt.y;
3886 ScreenToClient(m_hTargetWnd, &clientpoint);
3887 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
3889 #if 0
3890 CTGitPathList changelistItems;
3891 for (UINT i = 0; i < cFiles; ++i)
3893 if (DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
3894 changelistItems.AddPath(CTGitPath(szFileName));
3896 // find the changelist name
3897 CString sChangelist;
3898 LONG_PTR nGroup = m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint);
3899 for (std::map<CString, int>::iterator it = m_pGitStatusListCtrl->m_changelists.begin(); it != m_pGitStatusListCtrl->m_changelists.end(); ++it)
3900 if (it->second == nGroup)
3901 sChangelist = it->first;
3903 if (!sChangelist.IsEmpty())
3905 CGit git;
3906 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
3908 for (int l=0; l<changelistItems.GetCount(); ++l)
3910 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
3911 if (index >= 0)
3913 auto e = m_pGitStatusListCtrl->GetListEntry(index);
3914 if (e)
3916 e->changelist = sChangelist;
3917 if (!e->IsFolder())
3919 if (m_pGitStatusListCtrl->m_changelists.find(e->changelist) != m_pGitStatusListCtrl->m_changelists.end())
3920 m_pGitStatusListCtrl->SetItemGroup(index, m_pGitStatusListCtrl->m_changelists[e->changelist]);
3921 else
3922 m_pGitStatusListCtrl->SetItemGroup(index, 0);
3926 else
3928 HWND hParentWnd = GetParent(m_hTargetWnd);
3929 if (hParentWnd)
3930 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)changelistItems[l].GetWinPath());
3934 else
3935 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
3937 else
3939 SVN git;
3940 if (git.RemoveFromChangeList(changelistItems, CStringArray(), git_depth_empty))
3942 for (int l=0; l<changelistItems.GetCount(); ++l)
3944 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
3945 if (index >= 0)
3947 auto e = m_pGitStatusListCtrl->GetListEntry(index);
3948 if (e)
3950 e->changelist = sChangelist;
3951 m_pGitStatusListCtrl->SetItemGroup(index, 0);
3954 else
3956 HWND hParentWnd = GetParent(m_hTargetWnd);
3957 if (hParentWnd)
3958 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)changelistItems[l].GetWinPath());
3962 else
3963 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
3965 #endif
3967 else
3969 for (UINT i = 0; i < cFiles; ++i)
3971 if (!DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
3972 continue;
3974 HWND hParentWnd = GetParent(m_hTargetWnd);
3975 if (hParentWnd)
3976 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)szFileName);
3980 GlobalUnlock(medium.hGlobal);
3982 return true; //let base free the medium
3985 HRESULT STDMETHODCALLTYPE CGitStatusListCtrlDropTarget::DragOver(DWORD grfKeyState, POINTL pt, DWORD __RPC_FAR* pdwEffect)
3987 CIDropTarget::DragOver(grfKeyState, pt, pdwEffect);
3988 *pdwEffect = DROPEFFECT_COPY;
3989 if (m_pGitStatusListCtrl)
3991 POINT clientpoint;
3992 clientpoint.x = pt.x;
3993 clientpoint.y = pt.y;
3994 ScreenToClient(m_hTargetWnd, &clientpoint);
3995 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
3996 *pdwEffect = DROPEFFECT_NONE;
3997 else if ((!m_pGitStatusListCtrl->m_bFileDropsEnabled) || (m_pGitStatusListCtrl->m_bOwnDrag))
3998 *pdwEffect = DROPEFFECT_NONE;
4000 return S_OK;
4003 void CGitStatusListCtrl::FilesExport()
4005 CAutoReadLock locker(m_guard);
4006 CString exportDir;
4007 // export all changed files to a folder
4008 CBrowseFolder browseFolder;
4009 browseFolder.m_style = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNFSANCESTORS | BIF_RETURNONLYFSDIRS;
4010 if (browseFolder.Show(GetSafeHwnd(), exportDir) != CBrowseFolder::OK)
4011 return;
4013 POSITION pos = GetFirstSelectedItemPosition();
4014 int index;
4015 while ((index = GetNextSelectedItem(pos)) >= 0)
4017 auto fd = GetListEntry(index);
4018 // we cannot export directories or folders
4019 if ((fd->m_Action & CTGitPath::LOGACTIONS_DELETED) || fd->IsDirectory())
4020 continue;
4022 CPathUtils::MakeSureDirectoryPathExists(exportDir + L'\\' + fd->GetContainingDirectory().GetWinPathString());
4023 CString filename = exportDir + L'\\' + fd->GetWinPathString();
4024 if (m_CurrentVersion == GIT_REV_ZERO)
4026 if (!CopyFile(g_Git.CombinePath(fd), filename, false))
4028 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4029 return;
4032 else
4034 if (g_Git.GetOneFile(m_CurrentVersion, *fd, filename))
4036 CString out;
4037 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)fd->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)filename);
4038 if (CMessageBox::Show(GetSafeHwnd(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", 2, IDI_WARNING, CString(MAKEINTRESOURCE(IDS_IGNOREBUTTON)), CString(MAKEINTRESOURCE(IDS_ABORTBUTTON))) == 2)
4039 return;
4045 void CGitStatusListCtrl::FileSaveAs(CTGitPath *path)
4047 CAutoReadLock locker(m_guard);
4048 CString filename;
4049 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());
4050 if (!CAppUtils::FileOpenSave(filename, nullptr, 0, 0, false, GetSafeHwnd()))
4051 return;
4052 if (m_CurrentVersion == GIT_REV_ZERO)
4054 if (!CopyFile(g_Git.CombinePath(path), filename, false))
4056 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4057 return;
4060 else
4062 if (g_Git.GetOneFile(m_CurrentVersion, *path, filename))
4064 CString out;
4065 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)path->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)filename);
4066 CMessageBox::Show(GetSafeHwnd(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4067 return;
4072 int CGitStatusListCtrl::RevertSelectedItemToVersion(bool parent)
4074 CAutoReadLock locker(m_guard);
4075 if(this->m_CurrentVersion.IsEmpty())
4076 return 0;
4077 if(this->m_CurrentVersion == GIT_REV_ZERO)
4078 return 0;
4080 POSITION pos = GetFirstSelectedItemPosition();
4081 int index;
4082 CString cmd,out;
4083 std::map<CString, int> versionMap;
4084 while ((index = GetNextSelectedItem(pos)) >= 0)
4086 auto fentry = GetListEntry(index);
4087 CString version;
4088 if (parent)
4090 int parentNo = fentry->m_ParentNo & PARENT_MASK;
4091 CString ref;
4092 ref.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, parentNo + 1);
4093 CGitHash hash;
4094 if (g_Git.GetHash(hash, ref))
4096 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of ref \"" + ref + L"\"."), L"TortoiseGit", MB_ICONERROR);
4097 continue;
4100 version = hash.ToString();
4102 else
4103 version = m_CurrentVersion;
4105 CString filename = fentry->GetGitPathString();
4106 if (!fentry->GetGitOldPathString().IsEmpty())
4107 filename = fentry->GetGitOldPathString();
4108 cmd.Format(L"git.exe checkout %s -- \"%s\"", (LPCTSTR)version, (LPCTSTR)filename);
4109 out.Empty();
4110 if (g_Git.Run(cmd, &out, CP_UTF8))
4112 if (MessageBox(out, L"TortoiseGit", MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL)
4113 continue;
4115 else
4116 versionMap[version]++;
4119 out.Empty();
4120 for (auto it = versionMap.cbegin(); it != versionMap.cend(); ++it)
4122 CString versionEntry;
4123 versionEntry.Format(IDS_STATUSLIST_FILESREVERTED, it->second, (LPCTSTR)it->first);
4124 out += versionEntry + L"\r\n";
4126 if (!out.IsEmpty())
4128 if (GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
4129 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
4130 CMessageBox::Show(GetSafeHwnd(), out, L"TortoiseGit", MB_OK);
4132 return 0;
4135 void CGitStatusListCtrl::OpenFile(CTGitPath*filepath,int mode)
4137 CString file;
4138 if(this->m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
4139 file = g_Git.CombinePath(filepath);
4140 else
4142 file = CTempFiles::Instance().GetTempFilePath(false, *filepath, m_CurrentVersion).GetWinPathString();
4143 CString cmd,out;
4144 if(g_Git.GetOneFile(m_CurrentVersion, *filepath, file))
4146 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)filepath->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)file);
4147 CMessageBox::Show(GetSafeHwnd(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4148 return;
4150 SetFileAttributes(file, FILE_ATTRIBUTE_READONLY);
4152 if(mode == ALTERNATIVEEDITOR)
4154 CAppUtils::LaunchAlternativeEditor(file);
4155 return;
4158 if (mode == OPEN)
4159 CAppUtils::ShellOpen(file, GetSafeHwnd());
4160 else
4161 CAppUtils::ShowOpenWithDialog(file, GetSafeHwnd());
4164 void CGitStatusListCtrl::DeleteSelectedFiles()
4166 CAutoWriteLock locker(m_guard);
4167 //Collect paths
4168 std::vector<int> selectIndex;
4170 POSITION pos = GetFirstSelectedItemPosition();
4171 int index;
4172 while ((index = GetNextSelectedItem(pos)) >= 0)
4173 selectIndex.push_back(index);
4175 CAutoRepository repo = g_Git.GetGitRepository();
4176 if (!repo)
4178 MessageBox(g_Git.GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK);
4179 return;
4181 CAutoIndex gitIndex;
4182 if (git_repository_index(gitIndex.GetPointer(), repo))
4184 g_Git.GetLibGit2LastErr(L"Could not open index.");
4185 return;
4187 int needWriteIndex = 0;
4189 //Create file-list ('\0' separated) for SHFileOperation
4190 CString filelist;
4191 for (size_t i = 0; i < selectIndex.size(); ++i)
4193 index = selectIndex[i];
4195 auto path = GetListEntry(index);
4196 if (path == nullptr)
4197 continue;
4199 // do not report errors as we could remove an unversioned file
4200 needWriteIndex += git_index_remove_bypath(gitIndex, CUnicodeUtils::GetUTF8(path->GetGitPathString())) == 0 ? 1 : 0;
4202 if (!path->Exists())
4203 continue;
4205 filelist += path->GetWinPathString();
4206 filelist += L'|';
4208 filelist += L'|';
4209 int len = filelist.GetLength();
4210 auto buf = std::make_unique<TCHAR[]>(len + 2);
4211 wcscpy_s(buf.get(), len + 2, filelist);
4212 CStringUtils::PipesToNulls(buf.get(), len + 2);
4213 SHFILEOPSTRUCT fileop;
4214 fileop.hwnd = this->m_hWnd;
4215 fileop.wFunc = FO_DELETE;
4216 fileop.pFrom = buf.get();
4217 fileop.pTo = nullptr;
4218 fileop.fFlags = FOF_NO_CONNECTED_ELEMENTS | ((GetAsyncKeyState(VK_SHIFT) & 0x8000) ? 0 : FOF_ALLOWUNDO);
4219 fileop.lpszProgressTitle = L"deleting file";
4220 int result = SHFileOperation(&fileop);
4222 if ((result == 0 || len == 1) && (!fileop.fAnyOperationsAborted))
4224 if (needWriteIndex && git_index_write(gitIndex))
4225 MessageBox(g_Git.GetLibGit2LastErr(L"Could not write index."), L"TortoiseGit", MB_OK);
4227 if (needWriteIndex)
4229 CWnd* pParent = GetLogicalParent();
4230 if (pParent && pParent->GetSafeHwnd())
4231 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
4232 SetRedraw(TRUE);
4233 return;
4236 SetRedraw(FALSE);
4237 POSITION pos2 = nullptr;
4238 while ((pos2 = GetFirstSelectedItemPosition()) != nullptr)
4240 int index2 = GetNextSelectedItem(pos2);
4241 if (GetCheck(index2))
4242 m_nSelected--;
4243 m_nTotal--;
4245 RemoveListEntry(index2);
4247 SetRedraw(TRUE);
4251 BOOL CGitStatusListCtrl::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
4253 switch (message)
4255 case WM_MENUCHAR: // only supported by IContextMenu3
4256 if (g_IContext3)
4258 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4259 return TRUE;
4261 break;
4263 case WM_DRAWITEM:
4264 case WM_MEASUREITEM:
4265 if (wParam)
4266 break; // if wParam != 0 then the message is not menu-related
4268 case WM_INITMENU:
4269 case WM_INITMENUPOPUP:
4271 HMENU hMenu = (HMENU)wParam;
4272 if ((hMenu == m_hShellMenu) && (GetMenuItemCount(hMenu) == 0))
4274 // the shell submenu is populated only on request, i.e. right
4275 // before the submenu is shown
4276 if (g_pFolderhook)
4278 delete g_pFolderhook;
4279 g_pFolderhook = nullptr;
4281 CTGitPathList targetList;
4282 FillListOfSelectedItemPaths(targetList);
4283 if (!targetList.IsEmpty())
4285 // get IShellFolder interface of Desktop (root of shell namespace)
4286 if (g_psfDesktopFolder)
4287 g_psfDesktopFolder->Release();
4288 SHGetDesktopFolder(&g_psfDesktopFolder); // needed to obtain full qualified pidl
4290 // ParseDisplayName creates a PIDL from a file system path relative to the IShellFolder interface
4291 // but since we use the Desktop as our interface and the Desktop is the namespace root
4292 // that means that it's a fully qualified PIDL, which is what we need
4294 if (g_pidlArray)
4296 for (int i = 0; i < g_pidlArrayItems; i++)
4298 if (g_pidlArray[i])
4299 CoTaskMemFree(g_pidlArray[i]);
4301 CoTaskMemFree(g_pidlArray);
4302 g_pidlArray = nullptr;
4303 g_pidlArrayItems = 0;
4305 int nItems = targetList.GetCount();
4306 g_pidlArray = (LPITEMIDLIST *)CoTaskMemAlloc((nItems + 10) * sizeof(LPITEMIDLIST));
4307 SecureZeroMemory(g_pidlArray, (nItems + 10) * sizeof(LPITEMIDLIST));
4308 int succeededItems = 0;
4309 PIDLIST_RELATIVE pidl = nullptr;
4311 int bufsize = 1024;
4312 auto filepath = std::make_unique<WCHAR[]>(bufsize);
4313 for (int i = 0; i < nItems; i++)
4315 CString fullPath = g_Git.CombinePath(targetList[i].GetWinPath());
4316 if (bufsize < fullPath.GetLength())
4318 bufsize = fullPath.GetLength() + 3;
4319 filepath = std::make_unique<WCHAR[]>(bufsize);
4321 wcscpy_s(filepath.get(), bufsize, fullPath);
4322 if (SUCCEEDED(g_psfDesktopFolder->ParseDisplayName(nullptr, 0, filepath.get(), nullptr, &pidl, nullptr)))
4323 g_pidlArray[succeededItems++] = pidl; // copy pidl to pidlArray
4325 if (succeededItems == 0)
4327 CoTaskMemFree(g_pidlArray);
4328 g_pidlArray = nullptr;
4331 g_pidlArrayItems = succeededItems;
4333 if (g_pidlArrayItems)
4335 CString ext = targetList[0].GetFileExtension();
4337 ASSOCIATIONELEMENT const rgAssocItem[] =
4339 { ASSOCCLASS_PROGID_STR, nullptr, ext },
4340 { ASSOCCLASS_SYSTEM_STR, nullptr, ext },
4341 { ASSOCCLASS_APP_STR, nullptr, ext },
4342 { ASSOCCLASS_STAR, nullptr, nullptr },
4343 { ASSOCCLASS_FOLDER, nullptr, nullptr },
4345 IQueryAssociations* pIQueryAssociations = nullptr;
4346 if (FAILED(AssocCreateForClasses(rgAssocItem, ARRAYSIZE(rgAssocItem), IID_IQueryAssociations, (void**)&pIQueryAssociations)))
4347 pIQueryAssociations = nullptr; // not a problem, it works without this
4349 g_pFolderhook = new CIShellFolderHook(g_psfDesktopFolder, targetList);
4350 LPCONTEXTMENU icm1 = nullptr;
4352 DEFCONTEXTMENU dcm = { 0 };
4353 dcm.hwnd = m_hWnd;
4354 dcm.psf = g_pFolderhook;
4355 dcm.cidl = g_pidlArrayItems;
4356 dcm.apidl = (PCUITEMID_CHILD_ARRAY)g_pidlArray;
4357 dcm.punkAssociationInfo = pIQueryAssociations;
4358 if (SUCCEEDED(SHCreateDefaultContextMenu(&dcm, IID_IContextMenu, (void**)&icm1)))
4360 int iMenuType = 0; // to know which version of IContextMenu is supported
4361 if (icm1)
4362 { // since we got an IContextMenu interface we can now obtain the higher version interfaces via that
4363 if (icm1->QueryInterface(IID_IContextMenu3, (void**)&m_pContextMenu) == S_OK)
4364 iMenuType = 3;
4365 else if (icm1->QueryInterface(IID_IContextMenu2, (void**)&m_pContextMenu) == S_OK)
4366 iMenuType = 2;
4368 if (m_pContextMenu)
4369 icm1->Release(); // we can now release version 1 interface, cause we got a higher one
4370 else
4372 // since no higher versions were found
4373 // redirect ppContextMenu to version 1 interface
4374 iMenuType = 1;
4375 m_pContextMenu = icm1;
4378 if (m_pContextMenu)
4380 // lets fill the our popup menu
4381 UINT flags = CMF_NORMAL;
4382 flags |= (GetKeyState(VK_SHIFT) & 0x8000) != 0 ? CMF_EXTENDEDVERBS : 0;
4383 m_pContextMenu->QueryContextMenu(hMenu, 0, SHELL_MIN_CMD, SHELL_MAX_CMD, flags);
4386 // subclass window to handle menu related messages in CShellContextMenu
4387 if (iMenuType > 1) // only subclass if its version 2 or 3
4389 if (iMenuType == 2)
4390 g_IContext2 = (LPCONTEXTMENU2)m_pContextMenu;
4391 else // version 3
4392 g_IContext3 = (LPCONTEXTMENU3)m_pContextMenu;
4396 if (pIQueryAssociations)
4397 pIQueryAssociations->Release();
4400 if (g_IContext3)
4401 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4402 else if (g_IContext2)
4403 g_IContext2->HandleMenuMsg(message, wParam, lParam);
4404 return TRUE;
4408 break;
4409 default:
4410 break;
4413 return __super::OnWndMsg(message, wParam, lParam, pResult);
4416 CTGitPath* CGitStatusListCtrl::GetListEntry(int index)
4418 ATLASSERT(m_guard.GetCurrentThreadStatus());
4419 if ((size_t)index >= m_arListArray.size())
4421 ATLASSERT(FALSE);
4422 return nullptr;
4424 if (m_arListArray[index] >= m_arStatusArray.size())
4426 ATLASSERT(FALSE);
4427 return nullptr;
4429 return const_cast<CTGitPath*>(m_arStatusArray[m_arListArray[index]]);
4432 void CGitStatusListCtrl::OnSysColorChange()
4434 __super::OnSysColorChange();
4435 if (m_nBackgroundImageID)
4436 CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), m_nBackgroundImageID);
4439 ULONG CGitStatusListCtrl::GetGestureStatus(CPoint /*ptTouch*/)
4441 return 0;