CGitStatusListCtrl: Use stable sort
[TortoiseGit.git] / src / Git / GitStatusListCtrl.cpp
blobfad49cf6c9aa99f0009610cdcc9f6fed5f703d55
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2017 - TortoiseGit
4 // Copyright (C) 2003-2008, 2013-2015 - TortoiseSVN
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 #include "stdafx.h"
22 #include "resource.h"
23 #include "..\\TortoiseShell\resource.h"
24 #include "GitStatusListCtrl.h"
25 #include "MessageBox.h"
26 #include "MyMemDC.h"
27 #include "UnicodeUtils.h"
28 #include "AppUtils.h"
29 #include "PathUtils.h"
30 #include "TempFile.h"
31 #include "StringUtils.h"
32 #include "LoglistUtils.h"
33 #include "Git.h"
34 #include "GitRev.h"
35 #include "GitDiff.h"
36 #include "GitProgressDlg.h"
37 #include "SysImageList.h"
38 #include "TGitPath.h"
39 #include "registry.h"
40 #include "InputDlg.h"
41 #include "GitAdminDir.h"
42 #include "GitDataObject.h"
43 #include "ProgressCommands/AddProgressCommand.h"
44 #include "IconMenu.h"
45 #include "FormatMessageWrapper.h"
46 #include "BrowseFolder.h"
47 #include "SysInfo.h"
49 const UINT CGitStatusListCtrl::GITSLNM_ITEMCOUNTCHANGED
50 = ::RegisterWindowMessage(L"GITSLNM_ITEMCOUNTCHANGED");
51 const UINT CGitStatusListCtrl::GITSLNM_NEEDSREFRESH
52 = ::RegisterWindowMessage(L"GITSLNM_NEEDSREFRESH");
53 const UINT CGitStatusListCtrl::GITSLNM_ADDFILE
54 = ::RegisterWindowMessage(L"GITSLNM_ADDFILE");
55 const UINT CGitStatusListCtrl::GITSLNM_CHECKCHANGED
56 = ::RegisterWindowMessage(L"GITSLNM_CHECKCHANGED");
57 const UINT CGitStatusListCtrl::GITSLNM_ITEMCHANGED
58 = ::RegisterWindowMessage(L"GITSLNM_ITEMCHANGED");
60 struct icompare
62 bool operator() (const std::wstring& lhs, const std::wstring& rhs) const
64 // no logical comparison here: we need this sorted strictly
65 return _wcsicmp(lhs.c_str(), rhs.c_str()) < 0;
69 class CIShellFolderHook : public IShellFolder
71 public:
72 CIShellFolderHook(LPSHELLFOLDER sf, const CTGitPathList& pathlist)
74 sf->AddRef();
75 m_iSF = sf;
76 // it seems the paths in the HDROP need to be sorted, otherwise
77 // it might not work properly or even crash.
78 // to get the items sorted, we just add them to a set
79 for (int i = 0; i < pathlist.GetCount(); ++i)
80 sortedpaths.insert((LPCTSTR)g_Git.CombinePath(pathlist[i].GetWinPath()));
83 ~CIShellFolderHook() { m_iSF->Release(); }
85 // IUnknown methods --------
86 virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, __RPC__deref_out void **ppvObject) { return m_iSF->QueryInterface(riid, ppvObject); }
87 virtual ULONG STDMETHODCALLTYPE AddRef(void) { return m_iSF->AddRef(); }
88 virtual ULONG STDMETHODCALLTYPE Release(void) { 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);
93 virtual HRESULT STDMETHODCALLTYPE CompareIDs(LPARAM lParam, __RPC__in PCUIDLIST_RELATIVE pidl1, __RPC__in PCUIDLIST_RELATIVE pidl2) { return m_iSF->CompareIDs(lParam, pidl1, pidl2); }
94 virtual HRESULT STDMETHODCALLTYPE GetDisplayNameOf(__RPC__in_opt PCUITEMID_CHILD pidl, SHGDNF uFlags, __RPC__out STRRET *pName) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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_WM_SETCURSOR()
206 ON_WM_GETDLGCODE()
207 ON_NOTIFY_REFLECT(NM_RETURN, OnNMReturn)
208 ON_WM_KEYDOWN()
209 ON_WM_PAINT()
210 ON_WM_SYSCOLORCHANGE()
211 ON_NOTIFY_REFLECT(LVN_BEGINDRAG, OnBeginDrag)
212 ON_NOTIFY_REFLECT(LVN_ITEMCHANGING, &CGitStatusListCtrl::OnLvnItemchanging)
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_bEmpty(false)
222 , m_bShowIgnores(false)
223 , m_bIgnoreRemoveOnly(false)
224 , m_bCheckChildrenWithParent(false)
225 , m_bUnversionedLast(true)
226 , m_bHasChangeLists(false)
227 , m_bBlock(false)
228 , m_bBlockUI(false)
229 , m_bHasCheckboxes(false)
230 , m_bCheckIfGroupsExist(true)
231 , m_bFileDropsEnabled(false)
232 , m_bOwnDrag(false)
233 , m_dwDefaultColumns(0)
234 , m_bAscending(false)
235 , m_nSortedColumn(-1)
236 , m_bHasExternalsFromDifferentRepos(false)
237 , m_amend(false)
238 , m_bDoNotAutoselectSubmodules(false)
239 , m_bNoAutoselectMissing(false)
240 , m_bHasWC(true)
241 , m_hwndLogicalParent(nullptr)
242 , m_bHasUnversionedItems(FALSE)
243 , m_nTargetCount(0)
244 , m_bHasExternals(false)
245 , m_bHasIgnoreGroup(false)
246 , m_nUnversioned(0)
247 , m_nNormal(0)
248 , m_nModified(0)
249 , m_nAdded(0)
250 , m_nDeleted(0)
251 , m_nConflicted(0)
252 , m_nTotal(0)
253 , m_nSelected(0)
254 , m_nRenamed(0)
255 , m_nShownUnversioned(0)
256 , m_nShownModified(0)
257 , m_nShownAdded(0)
258 , m_nShownDeleted(0)
259 , m_nShownConflicted(0)
260 , m_nShownFiles(0)
261 , m_nShownSubmodules(0)
262 , m_dwShow(0)
263 , m_bShowFolders(false)
264 , m_bUpdate(false)
265 , m_dwContextMenus(0)
266 , m_nIconFolder(0)
267 , m_nRestoreOvl(0)
268 , m_pContextMenu(nullptr)
269 , m_hShellMenu(nullptr)
270 , m_nBackgroundImageID(0)
271 , m_FileLoaded(0)
272 , m_bIsRevertTheirMy(false)
273 , m_nLineAdded(0)
274 , m_nLineDeleted(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 Locker lock(m_critSec);
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 Locker lock(m_critSec);
301 m_dwDefaultColumns = dwColumns | 1;
302 m_dwContextMenus = dwContextMenus;
303 m_bHasCheckboxes = bHasCheckboxes;
304 m_bHasWC = bHasWC;
306 // set the extended style of the listcontrol
307 DWORD exStyle = LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP | LVS_EX_SUBITEMIMAGES;
308 exStyle |= (bHasCheckboxes ? LVS_EX_CHECKBOXES : 0);
309 SetRedraw(false);
310 SetExtendedStyle(exStyle);
311 CResizableColumnsListCtrl::Init();
313 SetWindowTheme(m_hWnd, L"Explorer", nullptr);
315 m_nIconFolder = SYS_IMAGE_LIST().GetDirIconIndex();
316 m_nRestoreOvl = SYS_IMAGE_LIST().AddIcon((HICON)LoadImage(AfxGetResourceHandle(), MAKEINTRESOURCE(IDI_RESTOREOVL), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
317 SYS_IMAGE_LIST().SetOverlayImage(m_nRestoreOvl, OVL_RESTORE);
318 SetImageList(&SYS_IMAGE_LIST(), LVSIL_SMALL);
320 // keep CSorter::operator() in sync!!
321 static UINT standardColumnNames[GITSLC_NUMCOLUMNS]
322 = { IDS_STATUSLIST_COLFILE
323 , IDS_STATUSLIST_COLFILENAME
324 , IDS_STATUSLIST_COLEXT
325 , IDS_STATUSLIST_COLSTATUS
326 , IDS_STATUSLIST_COLADD
327 , IDS_STATUSLIST_COLDEL
328 , IDS_STATUSLIST_COLLASTMODIFIED
329 , IDS_STATUSLIST_COLSIZE
332 m_ColumnManager.SetNames(standardColumnNames,GITSLC_NUMCOLUMNS);
333 m_ColumnManager.ReadSettings(m_dwDefaultColumns, 0xffffffff & ~(allowedColumns | m_dwDefaultColumns), sColumnInfoContainer, GITSLC_NUMCOLUMNS);
334 m_ColumnManager.SetRightAlign(4);
335 m_ColumnManager.SetRightAlign(5);
336 m_ColumnManager.SetRightAlign(7);
338 // enable file drops
339 if (!m_pDropTarget)
341 m_pDropTarget = std::make_unique<CGitStatusListCtrlDropTarget>(this);
342 RegisterDragDrop(m_hWnd, m_pDropTarget.get());
343 // create the supported formats:
344 FORMATETC ftetc = { 0 };
345 ftetc.dwAspect = DVASPECT_CONTENT;
346 ftetc.lindex = -1;
347 ftetc.tymed = TYMED_HGLOBAL;
348 ftetc.cfFormat = CF_HDROP;
349 m_pDropTarget->AddSuportedFormat(ftetc);
352 SetRedraw(true);
355 bool CGitStatusListCtrl::SetBackgroundImage(UINT nID)
357 m_nBackgroundImageID = nID;
358 return CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), nID);
361 BOOL CGitStatusListCtrl::GetStatus ( const CTGitPathList* pathList
362 , bool bUpdate /* = FALSE */
363 , bool bShowIgnores /* = false */
364 , bool bShowUnRev /* = false */
365 , bool bShowLocalChangesIgnored /* = false */)
367 m_bBusy = true;
368 m_bEmpty = false;
369 Invalidate();
371 m_bIsRevertTheirMy = g_Git.IsRebaseRunning() > 0;
373 Locker lock(m_critSec);
374 int mask= CGitStatusListCtrl::FILELIST_MODIFY;
375 if(bShowIgnores)
376 mask|= CGitStatusListCtrl::FILELIST_IGNORE;
377 if(bShowUnRev)
378 mask|= CGitStatusListCtrl::FILELIST_UNVER;
379 if (bShowLocalChangesIgnored)
380 mask |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
381 this->UpdateFileList(mask, bUpdate, pathList);
383 if (pathList && m_setDirectFiles.empty())
385 // remember files which are selected by users so that those can be preselected
386 for (int i = 0; i < pathList->GetCount(); ++i)
387 if (!(*pathList)[i].IsDirectory())
388 m_setDirectFiles.insert((*pathList)[i].GetGitPathString());
391 #if 0
392 int refetchcounter = 0;
393 BOOL bRet = TRUE;
394 Invalidate();
395 // force the cursor to change
396 POINT pt;
397 GetCursorPos(&pt);
398 SetCursorPos(pt.x, pt.y);
400 m_mapFilenameToChecked.clear();
401 //m_StatusUrlList.Clear();
402 bool bHasChangelists = (m_changelists.size() > 1 || (!m_changelists.empty() && !m_bHasIgnoreGroup));
403 m_changelists.clear();
404 for (size_t i=0; i < m_arStatusArray.size(); i++)
406 FileEntry * entry = m_arStatusArray[i];
407 if ( bHasChangelists && entry->checked)
409 // If change lists are present, remember all checked entries
410 CString path = entry->GetPath().GetGitPathString();
411 m_mapFilenameToChecked[path] = true;
413 if ( (entry->status==git_wc_status_unversioned || entry->status==git_wc_status_missing ) && entry->checked )
415 // The user manually selected an unversioned or missing file. We remember
416 // this so that the selection can be restored when refreshing.
417 CString path = entry->GetPath().GetGitPathString();
418 m_mapFilenameToChecked[path] = true;
420 else if ( entry->status > git_wc_status_normal && !entry->checked )
422 // The user manually deselected a versioned file. We remember
423 // this so that the deselection can be restored when refreshing.
424 CString path = entry->GetPath().GetGitPathString();
425 m_mapFilenameToChecked[path] = false;
429 // use a sorted path list to make sure we fetch the status of
430 // parent items first, *then* the child items (if any)
431 CTGitPathList sortedPathList = pathList;
432 sortedPathList.SortByPathname();
435 bRet = TRUE;
436 m_nTargetCount = 0;
437 m_bHasExternalsFromDifferentRepos = FALSE;
438 m_bHasExternals = FALSE;
439 m_bHasUnversionedItems = FALSE;
440 m_bHasChangeLists = false;
441 m_bShowIgnores = bShowIgnores;
442 m_nSortedColumn = 0;
443 m_bBlock = TRUE;
444 m_bBusy = true;
445 m_bEmpty = false;
446 Invalidate();
448 // first clear possible status data left from
449 // previous GetStatus() calls
450 ClearStatusArray();
452 m_StatusFileList = sortedPathList;
454 // Since Git_client_status() returns all files, even those in
455 // folders included with Git:externals we need to check if all
456 // files we get here belongs to the same repository.
457 // It is possible to commit changes in an external folder, as long
458 // as the folder belongs to the same repository (but another path),
459 // but it is not possible to commit all files if the externals are
460 // from a different repository.
462 // To check if all files belong to the same repository, we compare the
463 // UUID's - if they're identical then the files belong to the same
464 // repository and can be committed. But if they're different, then
465 // tell the user to commit all changes in the external folders
466 // first and exit.
467 CStringA sUUID; // holds the repo UUID
468 CTGitPathList arExtPaths; // list of Git:external paths
470 GitConfig config;
472 m_sURL.Empty();
474 m_nTargetCount = sortedPathList.GetCount();
476 GitStatus status(m_pbCanceled);
477 if(m_nTargetCount > 1 && sortedPathList.AreAllPathsFilesInOneDirectory())
479 // This is a special case, where we've been given a list of files
480 // all from one folder
481 // We handle them by setting a status filter, then requesting the Git status of
482 // all the files in the directory, filtering for the ones we're interested in
483 status.SetFilter(sortedPathList);
485 // if all selected entries are files, we don't do a recursive status
486 // fetching. But if only one is a directory, we have to recurse.
487 git_depth_t depth = git_depth_files;
488 // We have set a filter. If all selected items were files or we fetch
489 // the status not recursively, then we have to include
490 // ignored items too - the user has selected them
491 bool bShowIgnoresRight = true;
492 for (int fcindex=0; fcindex<sortedPathList.GetCount(); ++fcindex)
494 if (sortedPathList[fcindex].IsDirectory())
496 depth = git_depth_infinity;
497 bShowIgnoresRight = false;
498 break;
501 if(!FetchStatusForSingleTarget(config, status, sortedPathList.GetCommonDirectory(), bUpdate, sUUID, arExtPaths, true, depth, bShowIgnoresRight))
502 bRet = FALSE;
504 else
506 for(int nTarget = 0; nTarget < m_nTargetCount; nTarget++)
508 // check whether the path we want the status for is already fetched due to status-fetching
509 // of a parent path.
510 // this check is only done for file paths, because folder paths could be included already
511 // but not recursively
512 if (sortedPathList[nTarget].IsDirectory() || !GetListEntry(sortedPathList[nTarget]))
514 if(!FetchStatusForSingleTarget(config, status, sortedPathList[nTarget], bUpdate, sUUID,
515 arExtPaths, false, git_depth_infinity, bShowIgnores))
517 bRet = FALSE;
523 // remove the 'helper' files of conflicted items from the list.
524 // otherwise they would appear as unversioned files.
525 for (INT_PTR cind = 0; cind < m_ConflictFileList.GetCount(); ++cind)
527 for (size_t i=0; i < m_arStatusArray.size(); i++)
529 if (m_arStatusArray[i]->GetPath().IsEquivalentTo(m_ConflictFileList[cind]))
531 delete m_arStatusArray[i];
532 m_arStatusArray.erase(m_arStatusArray.cbegin() + i);
533 break;
537 refetchcounter++;
538 } while(!BuildStatistics() && (refetchcounter < 2) && (*m_pbCanceled==false));
540 m_bBlock = FALSE;
541 m_bBusy = false;
542 GetCursorPos(&pt);
543 SetCursorPos(pt.x, pt.y);
544 return bRet;
545 #endif
546 BuildStatistics();
547 return TRUE;
550 // Get the show-flags bitmap value which corresponds to a particular Git status
551 DWORD CGitStatusListCtrl::GetShowFlagsFromGitStatus(git_wc_status_kind status)
553 switch (status)
555 case git_wc_status_none:
556 case git_wc_status_unversioned:
557 return GITSLC_SHOWUNVERSIONED;
558 case git_wc_status_ignored:
559 if (!m_bShowIgnores)
560 return GITSLC_SHOWDIRECTS;
561 return GITSLC_SHOWDIRECTS|GITSLC_SHOWIGNORED;
562 case git_wc_status_normal:
563 return GITSLC_SHOWNORMAL;
564 case git_wc_status_added:
565 return GITSLC_SHOWADDED;
566 case git_wc_status_deleted:
567 return GITSLC_SHOWREMOVED;
568 case git_wc_status_modified:
569 return GITSLC_SHOWMODIFIED;
570 case git_wc_status_conflicted:
571 return GITSLC_SHOWCONFLICTED;
572 default:
573 // we should NEVER get here!
574 ASSERT(FALSE);
575 break;
577 return 0;
580 void CGitStatusListCtrl::Show(unsigned int dwShow, unsigned int dwCheck /*=0*/, bool /*bShowFolders*/ /* = true */,BOOL UpdateStatusList,bool UseStoredCheckStatus)
582 m_bBusy = true;
583 m_bEmpty = false;
584 Invalidate();
586 CWinApp * pApp = AfxGetApp();
587 if (pApp)
588 pApp->DoWaitCursor(1);
590 Locker lock(m_critSec);
591 WORD langID = (WORD)CRegStdDWORD(L"Software\\TortoiseGit\\LanguageID", GetUserDefaultLangID());
593 //SetItemCount(listIndex);
594 SetRedraw(FALSE);
595 DeleteAllItems();
596 m_nSelected = 0;
598 m_nShownUnversioned = 0;
599 m_nShownModified = 0;
600 m_nShownAdded = 0;
601 m_nShownDeleted = 0;
602 m_nShownConflicted = 0;
603 m_nShownFiles = 0;
604 m_nShownSubmodules = 0;
606 m_dwShow = dwShow;
608 if(UpdateStatusList)
610 m_arStatusArray.clear();
611 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
612 m_arStatusArray.push_back(&m_StatusFileList[i]);
614 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
615 m_arStatusArray.push_back(&m_UnRevFileList[i]);
617 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
618 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
620 PrepareGroups();
621 if (m_nSortedColumn >= 0)
623 CSorter predicate (&m_ColumnManager, m_nSortedColumn, m_bAscending);
624 std::stable_sort(m_arStatusArray.begin(), m_arStatusArray.end(), predicate);
627 int index =0;
628 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
630 //set default checkbox status
631 auto entry = const_cast<CTGitPath*>(m_arStatusArray[i]);
632 CString path = entry->GetGitPathString();
633 if (!m_mapFilenameToChecked.empty() && m_mapFilenameToChecked.find(path) != m_mapFilenameToChecked.end())
634 entry->m_Checked=m_mapFilenameToChecked[path];
635 else if (!UseStoredCheckStatus)
637 bool autoSelectSubmodules = !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules);
638 if (((entry->m_Action & dwCheck) && !(m_bNoAutoselectMissing && entry->m_Action & CTGitPath::LOGACTIONS_MISSING) || dwShow & GITSLC_SHOWDIRECTFILES && m_setDirectFiles.find(path) != m_setDirectFiles.end()) && autoSelectSubmodules)
639 entry->m_Checked=true;
640 else
641 entry->m_Checked=false;
642 m_mapFilenameToChecked[path] = entry->m_Checked;
645 if(entry->m_Action & dwShow)
647 AddEntry(entry,langID,index);
648 index++;
652 AdjustColumnWidths();
654 SetRedraw(TRUE);
655 GetStatisticsString();
657 CHeaderCtrl * pHeader = GetHeaderCtrl();
658 HDITEM HeaderItem = {0};
659 HeaderItem.mask = HDI_FORMAT;
660 for (int i=0; i<pHeader->GetItemCount(); ++i)
662 pHeader->GetItem(i, &HeaderItem);
663 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
664 pHeader->SetItem(i, &HeaderItem);
666 if (m_nSortedColumn >= 0)
668 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
669 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
670 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
673 RestoreScrollPos();
675 if (pApp)
676 pApp->DoWaitCursor(-1);
678 m_bBusy = false;
679 m_bEmpty = (GetItemCount() == 0);
680 Invalidate();
682 this->BuildStatistics();
684 #if 0
686 CWinApp * pApp = AfxGetApp();
687 if (pApp)
688 pApp->DoWaitCursor(1);
690 m_bShowFolders = bShowFolders;
692 int nTopIndex = GetTopIndex();
693 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
694 int nSelectedEntry = 0;
695 if (posSelectedEntry)
696 nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
697 SetRedraw(FALSE);
698 DeleteAllItems();
700 PrepareGroups();
702 m_arListArray.clear();
704 m_arListArray.reserve(m_arStatusArray.size());
705 SetItemCount (static_cast<int>(m_arStatusArray.size()));
707 int listIndex = 0;
708 for (size_t i=0; i < m_arStatusArray.size(); ++i)
710 FileEntry * entry = m_arStatusArray[i];
711 if ((entry->inexternal) && (!(dwShow & SVNSLC_SHOWINEXTERNALS)))
712 continue;
713 if ((entry->differentrepo || entry->isNested) && (! (dwShow & SVNSLC_SHOWEXTERNALFROMDIFFERENTREPO)))
714 continue;
715 if (entry->IsFolder() && (!bShowFolders))
716 continue; // don't show folders if they're not wanted.
718 #if 0
719 git_wc_status_kind status = GitStatus::GetMoreImportant(entry->status, entry->remotestatus);
720 DWORD showFlags = GetShowFlagsFromGitStatus(status);
721 if (entry->switched)
722 showFlags |= SVNSLC_SHOWSWITCHED;
723 if (!entry->changelist.IsEmpty())
724 showFlags |= SVNSLC_SHOWINCHANGELIST;
725 #endif
726 bool bAllowCheck = ((entry->changelist.Compare(GITSLC_IGNORECHANGELIST) != 0)
727 && (m_bCheckIfGroupsExist || (m_changelists.empty() || (m_changelists.size() == 1 && m_bHasIgnoreGroup))));
729 // status_ignored is a special case - we must have the 'direct' flag set to add a status_ignored item
730 #if 0
731 if (status != Git_wc_status_ignored || (entry->direct) || (dwShow & GitSLC_SHOWIGNORED))
733 if ((!entry->IsFolder()) && (status == Git_wc_status_deleted) && (dwShow & SVNSLC_SHOWREMOVEDANDPRESENT))
735 if (PathFileExists(entry->GetPath().GetWinPath()))
737 m_arListArray.push_back(i);
738 if ((dwCheck & SVNSLC_SHOWREMOVEDANDPRESENT)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
740 if (bAllowCheck)
741 entry->checked = true;
743 AddEntry(entry, langID, listIndex++);
746 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFILES)&&(entry->direct)&&(!entry->IsFolder())))
748 m_arListArray.push_back(i);
749 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
751 if (bAllowCheck)
752 entry->checked = true;
754 AddEntry(entry, langID, listIndex++);
756 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFOLDER)&&(entry->direct)&&entry->IsFolder()))
758 m_arListArray.push_back(i);
759 if ((dwCheck & showFlags)||((dwCheck & SVNSLC_SHOWDIRECTS)&&(entry->direct)))
761 if (bAllowCheck)
762 entry->checked = true;
764 AddEntry(entry, langID, listIndex++);
767 #endif
770 SetItemCount(listIndex);
772 m_ColumnManager.UpdateRelevance (m_arStatusArray, m_arListArray);
774 AdjustColumnWidths();
776 SetRedraw(TRUE);
777 GetStatisticsString();
779 CHeaderCtrl * pHeader = GetHeaderCtrl();
780 HDITEM HeaderItem = {0};
781 HeaderItem.mask = HDI_FORMAT;
782 for (int i=0; i<pHeader->GetItemCount(); ++i)
784 pHeader->GetItem(i, &HeaderItem);
785 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
786 pHeader->SetItem(i, &HeaderItem);
788 if (m_nSortedColumn)
790 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
791 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
792 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
795 if (nSelectedEntry)
797 SetItemState(nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
798 EnsureVisible(nSelectedEntry, false);
800 else
802 // Restore the item at the top of the list.
803 for (int i=0;GetTopIndex() != nTopIndex;i++)
805 if ( !EnsureVisible(nTopIndex+i,false) )
806 break;
810 if (pApp)
811 pApp->DoWaitCursor(-1);
813 m_bEmpty = (GetItemCount() == 0);
814 Invalidate();
815 #endif
818 void CGitStatusListCtrl::Show(unsigned int /*dwShow*/, const CTGitPathList& checkedList, bool /*bShowFolders*/ /* = true */)
820 m_dwShow = GITSLC_SHOWALL;
821 DeleteAllItems();
822 m_arStatusArray.clear();
823 for (int i = 0; i < checkedList.GetCount(); ++i)
825 this->AddEntry((CTGitPath *)&checkedList[i],0,i);
826 m_arStatusArray.push_back(&checkedList[i]);
829 AdjustColumnWidths();
831 RestoreScrollPos();
833 return ;
834 #if 0
836 Locker lock(m_critSec);
837 WORD langID = (WORD)CRegStdDWORD(L"Software\\TortoiseGit\\LanguageID", GetUserDefaultLangID());
839 CWinApp * pApp = AfxGetApp();
840 if (pApp)
841 pApp->DoWaitCursor(1);
842 m_dwShow = dwShow;
843 m_bShowFolders = bShowFolders;
844 m_nSelected = 0;
845 int nTopIndex = GetTopIndex();
846 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
847 int nSelectedEntry = 0;
848 if (posSelectedEntry)
849 nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
850 SetRedraw(FALSE);
851 DeleteAllItems();
853 PrepareGroups();
855 m_arListArray.clear();
857 m_arListArray.reserve(m_arStatusArray.size());
858 SetItemCount (static_cast<int>(m_arStatusArray.size()));
860 int listIndex = 0;
861 for (size_t i=0; i < m_arStatusArray.size(); ++i)
863 FileEntry * entry = m_arStatusArray[i];
864 if ((entry->inexternal) && (!(dwShow & SVNSLC_SHOWINEXTERNALS)))
865 continue;
866 if ((entry->differentrepo || entry->isNested) && (! (dwShow & SVNSLC_SHOWEXTERNALFROMDIFFERENTREPO)))
867 continue;
868 if (entry->IsFolder() && (!bShowFolders))
869 continue; // don't show folders if they're not wanted.
870 #if 0
871 git_wc_status_kind status = SVNStatus::GetMoreImportant(entry->status, entry->remotestatus);
872 DWORD showFlags = GetShowFlagsFromSVNStatus(status);
873 if (!entry->changelist.IsEmpty())
874 showFlags |= SVNSLC_SHOWINCHANGELIST;
876 // status_ignored is a special case - we must have the 'direct' flag set to add a status_ignored item
877 if (status != git_wc_status_ignored || (entry->direct) || (dwShow & SVNSLC_SHOWIGNORED))
879 for (int npath = 0; npath < checkedList.GetCount(); ++npath)
881 if (entry->GetPath().IsEquivalentTo(checkedList[npath]))
883 entry->checked = true;
884 break;
887 if ((!entry->IsFolder()) && (status == git_wc_status_deleted) && (dwShow & SVNSLC_SHOWREMOVEDANDPRESENT))
889 if (PathFileExists(entry->GetPath().GetWinPath()))
891 m_arListArray.push_back(i);
892 AddEntry(entry, langID, listIndex++);
895 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFILES)&&(entry->direct)&&(!entry->IsFolder())))
897 m_arListArray.push_back(i);
898 AddEntry(entry, langID, listIndex++);
900 else if ((dwShow & showFlags)||((dwShow & SVNSLC_SHOWDIRECTFOLDER)&&(entry->direct)&&entry->IsFolder()))
902 m_arListArray.push_back(i);
903 AddEntry(entry, langID, listIndex++);
905 else if (entry->switched)
907 m_arListArray.push_back(i);
908 AddEntry(entry, langID, listIndex++);
911 #endif
914 SetItemCount(listIndex);
916 AdjustColumnWidths();
918 SetRedraw(TRUE);
919 GetStatisticsString();
921 CHeaderCtrl * pHeader = GetHeaderCtrl();
922 HDITEM HeaderItem = {0};
923 HeaderItem.mask = HDI_FORMAT;
924 for (int i=0; i<pHeader->GetItemCount(); ++i)
926 pHeader->GetItem(i, &HeaderItem);
927 HeaderItem.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
928 pHeader->SetItem(i, &HeaderItem);
930 if (m_nSortedColumn)
932 pHeader->GetItem(m_nSortedColumn, &HeaderItem);
933 HeaderItem.fmt |= (m_bAscending ? HDF_SORTUP : HDF_SORTDOWN);
934 pHeader->SetItem(m_nSortedColumn, &HeaderItem);
937 if (nSelectedEntry)
939 SetItemState(nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
940 EnsureVisible(nSelectedEntry, false);
942 else
944 // Restore the item at the top of the list.
945 for (int i=0;GetTopIndex() != nTopIndex;i++)
947 if ( !EnsureVisible(nTopIndex+i,false) )
948 break;
952 if (pApp)
953 pApp->DoWaitCursor(-1);
955 m_bEmpty = (GetItemCount() == 0);
956 Invalidate();
957 #endif
960 void CGitStatusListCtrl::StoreScrollPos()
962 m_sScrollPos.enabled = true;
963 m_sScrollPos.nTopIndex = GetTopIndex();
964 m_sScrollPos.selMark = GetSelectionMark();
965 POSITION posSelectedEntry = GetFirstSelectedItemPosition();
966 m_sScrollPos.nSelectedEntry = 0;
967 if (posSelectedEntry)
968 m_sScrollPos.nSelectedEntry = GetNextSelectedItem(posSelectedEntry);
971 void CGitStatusListCtrl::RestoreScrollPos()
973 if (!m_sScrollPos.enabled || CRegDWORD(L"Software\\TortoiseGit\\RememberFileListPosition", TRUE) != TRUE)
974 return;
976 if (m_sScrollPos.nSelectedEntry)
978 SetItemState(m_sScrollPos.nSelectedEntry, LVIS_SELECTED, LVIS_SELECTED);
979 EnsureVisible(m_sScrollPos.nSelectedEntry, false);
981 else
983 // Restore the item at the top of the list.
984 for (int i = 0; GetTopIndex() != m_sScrollPos.nTopIndex; ++i)
986 if (!EnsureVisible(m_sScrollPos.nTopIndex + i, false))
987 break;
990 if (m_sScrollPos.selMark >= 0)
992 SetSelectionMark(m_sScrollPos.selMark);
993 SetItemState(m_sScrollPos.selMark, LVIS_FOCUSED, LVIS_FOCUSED);
995 m_sScrollPos.enabled = false;
998 int CGitStatusListCtrl::GetColumnIndex(int mask)
1000 for (int i = 0; i < 32; ++i)
1001 if(mask&0x1)
1002 return i;
1003 else
1004 mask=mask>>1;
1005 return -1;
1007 void CGitStatusListCtrl::AddEntry(CTGitPath * GitPath, WORD /*langID*/, int listIndex)
1009 static CString from(MAKEINTRESOURCE(IDS_STATUSLIST_FROM));
1010 static HINSTANCE hResourceHandle(AfxGetResourceHandle());
1011 static bool abbreviateRenamings(((DWORD)CRegDWORD(L"Software\\TortoiseGit\\AbbreviateRenamings", FALSE)) == TRUE);
1012 static bool relativeTimes = (CRegDWORD(L"Software\\TortoiseGit\\RelativeTimes", FALSE) != FALSE);
1014 CString path = GitPath->GetGitPathString();
1016 m_bBlock = TRUE;
1017 int index = listIndex;
1018 int nCol = 1;
1019 CString entryname = GitPath->GetGitPathString();
1020 int icon_idx = 0;
1021 if (GitPath->IsDirectory())
1023 icon_idx = m_nIconFolder;
1024 m_nShownSubmodules++;
1026 else
1028 icon_idx = SYS_IMAGE_LIST().GetPathIconIndex(*GitPath);
1029 m_nShownFiles++;
1031 switch (GitPath->m_Action)
1033 case CTGitPath::LOGACTIONS_ADDED:
1034 case CTGitPath::LOGACTIONS_COPY:
1035 m_nShownAdded++;
1036 break;
1037 case CTGitPath::LOGACTIONS_DELETED:
1038 case CTGitPath::LOGACTIONS_MISSING:
1039 case CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING:
1040 m_nShownDeleted++;
1041 break;
1042 case CTGitPath::LOGACTIONS_REPLACED:
1043 case CTGitPath::LOGACTIONS_MODIFIED:
1044 case CTGitPath::LOGACTIONS_MERGED:
1045 m_nShownModified++;
1046 break;
1047 case CTGitPath::LOGACTIONS_UNMERGED:
1048 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_ADDED:
1049 case CTGitPath::LOGACTIONS_UNMERGED | CTGitPath::LOGACTIONS_MODIFIED:
1050 m_nShownConflicted++;
1051 break;
1052 case CTGitPath::LOGACTIONS_UNVER:
1053 m_nShownUnversioned++;
1054 break;
1055 default:
1056 m_nShownUnversioned++;
1057 break;
1059 if(GitPath->m_Action & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY) && !GitPath->GetGitOldPathString().IsEmpty())
1061 if (!abbreviateRenamings)
1063 entryname += L' ';
1064 // relative path
1065 entryname.AppendFormat(from, (LPCTSTR)GitPath->GetGitOldPathString());
1067 else
1068 entryname = GitPath->GetAbbreviatedRename();
1071 InsertItem(index, entryname, icon_idx);
1072 if (m_restorepaths.find(GitPath->GetWinPathString()) != m_restorepaths.end())
1073 SetItemState(index, INDEXTOOVERLAYMASK(OVL_RESTORE), TVIS_OVERLAYMASK);
1075 this->SetItemData(index, (DWORD_PTR)GitPath);
1076 // SVNSLC_COLFILENAME
1077 SetItemText(index, nCol++, GitPath->GetFileOrDirectoryName());
1078 // SVNSLC_COLEXT
1079 SetItemText(index, nCol++, GitPath->GetFileExtension());
1080 // SVNSLC_COLSTATUS
1081 SetItemText(index, nCol++, GitPath->GetActionName());
1083 SetItemText(index, GetColumnIndex(GITSLC_COLADD),GitPath->m_StatAdd);
1084 SetItemText(index, GetColumnIndex(GITSLC_COLDEL),GitPath->m_StatDel);
1086 if (!(GitPath->m_Action & CTGitPath::LOGACTIONS_DELETED) && m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLMODIFICATIONDATE)))
1088 CString modificationDate;
1089 __int64 filetime = GitPath->GetLastWriteTime();
1090 if (filetime)
1092 FILETIME* f = (FILETIME*)(__int64*)&filetime;
1093 modificationDate = CLoglistUtils::FormatDateAndTime(CTime(CGit::filetime_to_time_t(f)), DATE_SHORTDATE, true, relativeTimes);
1095 SetItemText(index, GetColumnIndex(GITSLC_COLMODIFICATIONDATE), modificationDate);
1097 // SVNSLC_COLSIZE
1098 if (GitPath->IsDirectory() || !m_ColumnManager.IsRelevant(GetColumnIndex(GITSLC_COLSIZE)))
1099 SetItemText(index, GetColumnIndex(GITSLC_COLSIZE), L"");
1100 else
1102 TCHAR buf[100] = { 0 };
1103 StrFormatByteSize64(GitPath->GetFileSize(), buf, 100);
1104 SetItemText(index, GetColumnIndex(GITSLC_COLSIZE), buf);
1107 SetCheck(index, GitPath->m_Checked);
1108 if (GitPath->m_Checked)
1109 m_nSelected++;
1111 if ((GitPath->m_Action & CTGitPath::LOGACTIONS_SKIPWORKTREE) || (GitPath->m_Action & CTGitPath::LOGACTIONS_ASSUMEVALID))
1112 SetItemGroup(index, 3);
1113 else if (GitPath->m_Action & CTGitPath::LOGACTIONS_IGNORE)
1114 SetItemGroup(index, 2);
1115 else if( GitPath->m_Action & CTGitPath::LOGACTIONS_UNVER)
1116 SetItemGroup(index,1);
1117 else
1118 SetItemGroup(index, GitPath->m_ParentNo&(PARENT_MASK|MERGE_MASK));
1120 m_bBlock = FALSE;
1123 bool CGitStatusListCtrl::SetItemGroup(int item, int groupindex)
1125 // if (!(m_dwContextMenus & SVNSLC_POPCHANGELISTS))
1126 // return false;
1127 if (groupindex < 0)
1128 return false;
1129 LVITEM i = {0};
1130 i.mask = LVIF_GROUPID;
1131 i.iItem = item;
1132 i.iSubItem = 0;
1133 i.iGroupId = groupindex;
1135 return !!SetItem(&i);
1138 void CGitStatusListCtrl::OnHdnItemclick(NMHDR *pNMHDR, LRESULT *pResult)
1140 LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
1141 *pResult = 0;
1142 if (m_bBlock || m_arStatusArray.empty())
1143 return;
1144 m_bBlock = TRUE;
1145 if (m_nSortedColumn == phdr->iItem)
1146 m_bAscending = !m_bAscending;
1147 else
1148 m_bAscending = TRUE;
1149 m_nSortedColumn = phdr->iItem;
1150 Show(m_dwShow, 0, m_bShowFolders,false,true);
1152 m_bBlock = FALSE;
1155 void CGitStatusListCtrl::OnLvnItemchanging(NMHDR *pNMHDR, LRESULT *pResult)
1157 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
1158 *pResult = 0;
1160 #define ISCHECKED(x) ((x) ? ((((x)&LVIS_STATEIMAGEMASK)>>12)-1) : FALSE)
1161 if ((m_bBlock)&&(m_bBlockUI))
1163 // if we're blocked, prevent changing of the check state
1164 if ((!ISCHECKED(pNMLV->uOldState) && ISCHECKED(pNMLV->uNewState))||
1165 (ISCHECKED(pNMLV->uOldState) && !ISCHECKED(pNMLV->uNewState)))
1166 *pResult = TRUE;
1170 BOOL CGitStatusListCtrl::OnLvnItemchanged(NMHDR *pNMHDR, LRESULT *pResult)
1172 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
1173 *pResult = 0;
1174 CWnd* pParent = GetLogicalParent();
1175 if (pParent && pParent->GetSafeHwnd())
1176 pParent->SendMessage(GITSLNM_ITEMCHANGED, pNMLV->iItem);
1178 if ((pNMLV->uNewState==0)||(pNMLV->uNewState & LVIS_SELECTED)||(pNMLV->uNewState & LVIS_FOCUSED))
1179 return FALSE;
1181 if (m_bBlock)
1182 return FALSE;
1184 bool bSelected = !!(ListView_GetItemState(m_hWnd, pNMLV->iItem, LVIS_SELECTED) & LVIS_SELECTED);
1185 int nListItems = GetItemCount();
1187 m_bBlock = TRUE;
1188 // was the item checked?
1190 //CTGitPath *gitpath=(CTGitPath*)GetItemData(pNMLV->iItem);
1191 //gitpath->m_Checked=GetCheck(pNMLV->iItem);
1193 if (GetCheck(pNMLV->iItem))
1195 CheckEntry(pNMLV->iItem, nListItems);
1196 if (bSelected)
1198 POSITION pos = GetFirstSelectedItemPosition();
1199 int index;
1200 while ((index = GetNextSelectedItem(pos)) >= 0)
1202 if (index != pNMLV->iItem)
1203 CheckEntry(index, nListItems);
1207 else
1209 UncheckEntry(pNMLV->iItem, nListItems);
1210 if (bSelected)
1212 POSITION pos = GetFirstSelectedItemPosition();
1213 int index;
1214 while ((index = GetNextSelectedItem(pos)) >= 0)
1216 if (index != pNMLV->iItem)
1217 UncheckEntry(index, nListItems);
1222 GetStatisticsString();
1223 m_bBlock = FALSE;
1224 NotifyCheck();
1226 return FALSE;
1229 void CGitStatusListCtrl::CheckEntry(int index, int /*nListItems*/)
1231 Locker lock(m_critSec);
1232 //FileEntry * entry = GetListEntry(index);
1233 auto path = GetListEntry(index);
1234 if (!path)
1235 return;
1236 m_mapFilenameToChecked[path->GetGitPathString()] = true;
1237 SetCheck(index, TRUE);
1238 //entry = GetListEntry(index);
1239 // if an unversioned item was checked, then we need to check if
1240 // the parent folders are unversioned too. If the parent folders actually
1241 // are unversioned, then check those too.
1242 #if 0
1243 if (entry->status == git_wc_status_unversioned)
1245 // we need to check the parent folder too
1246 const CTGitPath& folderpath = entry->path;
1247 for (int i=0; i< nListItems; ++i)
1249 FileEntry * testEntry = GetListEntry(i);
1250 ASSERT(testEntry);
1251 if (!testEntry)
1252 continue;
1253 if (!testEntry->checked)
1255 if (testEntry->path.IsAncestorOf(folderpath) && (!testEntry->path.IsEquivalentTo(folderpath)))
1257 SetEntryCheck(testEntry,i,true);
1258 m_nSelected++;
1263 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1264 if ( (entry->status == git_wc_status_deleted) || (m_bCheckChildrenWithParent) || (bShift) )
1266 // if a deleted folder gets checked, we have to check all
1267 // children of that folder too.
1268 if (entry->path.IsDirectory())
1269 SetCheckOnAllDescendentsOf(entry, true);
1271 // if a deleted file or folder gets checked, we have to
1272 // check all parents of this item too.
1273 for (int i=0; i<nListItems; ++i)
1275 FileEntry * testEntry = GetListEntry(i);
1276 ASSERT(testEntry);
1277 if (!testEntry)
1278 continue;
1279 if (!testEntry->checked)
1281 if (testEntry->path.IsAncestorOf(entry->path) && (!testEntry->path.IsEquivalentTo(entry->path)))
1283 if ((testEntry->status == git_wc_status_deleted)||(m_bCheckChildrenWithParent))
1285 SetEntryCheck(testEntry,i,true);
1286 m_nSelected++;
1287 // now we need to check all children of this parent folder
1288 SetCheckOnAllDescendentsOf(testEntry, true);
1294 #endif
1295 if ( !path->m_Checked )
1297 path->m_Checked = TRUE;
1298 m_nSelected++;
1302 void CGitStatusListCtrl::UncheckEntry(int index, int /*nListItems*/)
1304 Locker lock(m_critSec);
1305 auto path = GetListEntry(index);
1306 if (!path)
1307 return;
1308 SetCheck(index, FALSE);
1309 m_mapFilenameToChecked[path->GetGitPathString()] = false;
1310 //entry = GetListEntry(index);
1311 // item was unchecked
1312 #if 0
1313 if (entry->path.IsDirectory())
1315 // disable all files within an unselected folder, except when unchecking a folder with property changes
1316 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1317 if ( (entry->status != git_wc_status_modified) || (bShift) )
1319 SetCheckOnAllDescendentsOf(entry, false);
1322 else if (entry->status == git_wc_status_deleted)
1324 // a "deleted" file was unchecked, so uncheck all parent folders
1325 // and all children of those parents
1326 for (int i=0; i<nListItems; i++)
1328 FileEntry * testEntry = GetListEntry(i);
1329 ASSERT(testEntry);
1330 if (!testEntry)
1331 continue;
1332 if (testEntry->checked)
1334 if (testEntry->path.IsAncestorOf(entry->path))
1336 if (testEntry->status == git_wc_status_deleted)
1338 SetEntryCheck(testEntry,i,false);
1339 m_nSelected--;
1341 SetCheckOnAllDescendentsOf(testEntry, false);
1347 #endif
1348 if ( path->m_Checked )
1350 path->m_Checked = FALSE;
1351 m_nSelected--;
1354 bool CGitStatusListCtrl::BuildStatistics()
1356 bool bRefetchStatus = false;
1358 // now gather some statistics
1359 m_nUnversioned = 0;
1360 m_nNormal = 0;
1361 m_nModified = 0;
1362 m_nAdded = 0;
1363 m_nDeleted = 0;
1364 m_nConflicted = 0;
1365 m_nTotal = 0;
1366 m_nSelected = 0;
1367 m_nLineAdded = 0;
1368 m_nLineDeleted = 0;
1369 m_nRenamed = 0;
1371 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
1373 int status = m_arStatusArray[i]->m_Action;
1375 m_nLineAdded += _wtol(m_arStatusArray[i]->m_StatAdd);
1376 m_nLineDeleted += _wtol(m_arStatusArray[i]->m_StatDel);
1378 if(status&(CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
1379 m_nAdded++;
1381 if(status&CTGitPath::LOGACTIONS_DELETED)
1382 m_nDeleted++;
1384 if(status&(CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_MODIFIED))
1385 m_nModified++;
1387 if(status&CTGitPath::LOGACTIONS_UNMERGED)
1388 m_nConflicted++;
1390 if(status&(CTGitPath::LOGACTIONS_IGNORE|CTGitPath::LOGACTIONS_UNVER))
1391 m_nUnversioned++;
1393 if(status&(CTGitPath::LOGACTIONS_REPLACED))
1394 m_nRenamed++;
1396 if (m_arStatusArray[i]->m_Checked)
1397 m_nSelected++;
1399 return !bRefetchStatus;
1403 int CGitStatusListCtrl::GetGroupFromPoint(POINT * ppt)
1405 // the point must be relative to the upper left corner of the control
1407 if (!ppt)
1408 return -1;
1409 if (!IsGroupViewEnabled())
1410 return -1;
1412 POINT pt = *ppt;
1413 pt.x = 10;
1414 UINT flags = 0;
1415 int nItem = -1;
1416 RECT rc;
1417 GetWindowRect(&rc);
1418 while (((flags & LVHT_BELOW) == 0)&&(pt.y < rc.bottom))
1420 nItem = HitTest(pt, &flags);
1421 if ((flags & LVHT_ONITEM)||(flags & LVHT_EX_GROUP_HEADER))
1423 // the first item below the point
1425 // check if the point is too much right (i.e. if the point
1426 // is farther to the right than the width of the item)
1427 RECT r;
1428 GetItemRect(nItem, &r, LVIR_LABEL);
1429 if (ppt->x > r.right)
1430 return -1;
1432 LVITEM lv = {0};
1433 lv.mask = LVIF_GROUPID;
1434 lv.iItem = nItem;
1435 GetItem(&lv);
1436 int groupID = lv.iGroupId;
1437 // now we search upwards and check if the item above this one
1438 // belongs to another group. If it belongs to the same group,
1439 // we're not over a group header
1440 while (pt.y >= 0)
1442 pt.y -= 2;
1443 nItem = HitTest(pt, &flags);
1444 if ((flags & LVHT_ONITEM)&&(nItem >= 0))
1446 // the first item below the point
1447 LVITEM lv2 = {0};
1448 lv2.mask = LVIF_GROUPID;
1449 lv2.iItem = nItem;
1450 GetItem(&lv2);
1451 if (lv2.iGroupId != groupID)
1452 return groupID;
1453 else
1454 return -1;
1457 if (pt.y < 0)
1458 return groupID;
1459 return -1;
1461 pt.y += 2;
1463 return -1;
1466 void CGitStatusListCtrl::OnContextMenuGroup(CWnd * /*pWnd*/, CPoint point)
1468 POINT clientpoint = point;
1469 ScreenToClient(&clientpoint);
1470 if ((IsGroupViewEnabled())&&(GetGroupFromPoint(&clientpoint) >= 0))
1472 CMenu popup;
1473 if (popup.CreatePopupMenu())
1475 CString temp;
1476 temp.LoadString(IDS_STATUSLIST_CHECKGROUP);
1477 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_CHECKGROUP, temp);
1478 temp.LoadString(IDS_STATUSLIST_UNCHECKGROUP);
1479 popup.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_UNCHECKGROUP, temp);
1480 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
1481 bool bCheck = false;
1482 switch (cmd)
1484 case IDGITLC_CHECKGROUP:
1485 bCheck = true;
1486 // fall through here
1487 case IDGITLC_UNCHECKGROUP:
1489 int group = GetGroupFromPoint(&clientpoint);
1490 // go through all items and check/uncheck those assigned to the group
1491 // but block the OnLvnItemChanged handler
1492 m_bBlock = true;
1493 for (int i=0; i<GetItemCount(); ++i)
1495 LVITEM lv = { 0 };
1496 lv.mask = LVIF_GROUPID;
1497 lv.iItem = i;
1498 GetItem(&lv);
1500 if (lv.iGroupId == group)
1502 auto entry = GetListEntry(i);
1503 if (entry)
1505 bool bOldCheck = entry->m_Checked;
1506 SetEntryCheck(entry, i, bCheck);
1507 if (bCheck != bOldCheck)
1509 if (bCheck)
1510 m_nSelected++;
1511 else
1512 m_nSelected--;
1518 GetStatisticsString();
1519 NotifyCheck();
1520 m_bBlock = false;
1522 break;
1528 void CGitStatusListCtrl::OnContextMenuList(CWnd * pWnd, CPoint point)
1530 //WORD langID = (WORD)CRegStdDWORD(L"Software\\TortoiseGit\\LanguageID", GetUserDefaultLangID());
1532 bool bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1533 CTGitPath * filepath;
1535 auto selectedCount = GetSelectedCount();
1536 int selIndex = GetSelectionMark();
1537 if ((point.x == -1) && (point.y == -1))
1539 CRect rect;
1540 GetItemRect(selIndex, &rect, LVIR_LABEL);
1541 ClientToScreen(&rect);
1542 point = rect.CenterPoint();
1544 if (selectedCount == 0 && m_bHasCheckboxes)
1546 // nothing selected could mean the context menu is requested for
1547 // a group header
1548 OnContextMenuGroup(pWnd, point);
1550 else if (selIndex >= 0)
1552 filepath = GetListEntry(selIndex);
1553 if (!filepath)
1554 return;
1556 //const CTGitPath& filepath = entry->path;
1557 int wcStatus = filepath->m_Action;
1558 // entry is selected, now show the popup menu
1559 Locker lock(m_critSec);
1560 CIconMenu popup;
1561 CMenu changelistSubMenu;
1562 CMenu ignoreSubMenu;
1563 CMenu shellMenu;
1564 if (popup.CreatePopupMenu())
1566 //Add Menu for version controlled file
1568 if (selectedCount > 0 && (wcStatus & CTGitPath::LOGACTIONS_UNMERGED))
1570 if (selectedCount == 1 && (m_dwContextMenus & GITSLC_POPCONFLICT))
1572 popup.AppendMenuIcon(IDGITLC_EDITCONFLICT, IDS_MENUCONFLICT, IDI_CONFLICT);
1573 popup.SetDefaultItem(IDGITLC_EDITCONFLICT, FALSE);
1575 if (m_dwContextMenus & GITSLC_POPRESOLVE)
1576 popup.AppendMenuIcon(IDGITLC_RESOLVECONFLICT, IDS_STATUSLIST_CONTEXT_RESOLVED, IDI_RESOLVE);
1577 if ((m_dwContextMenus & GITSLC_POPRESOLVE)/*&&(entry->textstatus == git_wc_status_conflicted)*/)
1579 popup.AppendMenuIcon(IDGITLC_RESOLVETHEIRS, IDS_SVNPROGRESS_MENUUSETHEIRS, IDI_RESOLVE);
1580 popup.AppendMenuIcon(IDGITLC_RESOLVEMINE, IDS_SVNPROGRESS_MENUUSEMINE, IDI_RESOLVE);
1582 if ((m_dwContextMenus & GITSLC_POPCONFLICT)||(m_dwContextMenus & GITSLC_POPRESOLVE))
1583 popup.AppendMenu(MF_SEPARATOR);
1586 if (selectedCount > 0)
1588 if (wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
1590 if (m_dwContextMenus & GITSLC_POPADD)
1592 //if ( entry->IsFolder() )
1594 // popup.AppendMenuIcon(IDSVNLC_ADD_RECURSIVE, IDS_STATUSLIST_CONTEXT_ADD_RECURSIVE, IDI_ADD);
1596 //else
1598 popup.AppendMenuIcon(IDGITLC_ADD, IDS_STATUSLIST_CONTEXT_ADD, IDI_ADD);
1601 if (m_dwContextMenus & GITSLC_POPCOMMIT)
1602 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1606 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)
1608 bool bEntryAdded = false;
1609 if (m_dwContextMenus & GITSLC_POPCOMPAREWITHBASE)
1611 if(filepath->m_ParentNo & MERGE_MASK)
1612 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_TREE_DIFF, IDI_DIFF);
1613 else
1614 popup.AppendMenuIcon(IDGITLC_COMPARE, IDS_LOG_COMPAREWITHBASE, IDI_DIFF);
1616 if (!(wcStatus & (CTGitPath::LOGACTIONS_UNMERGED)) || selectedCount != 1)
1617 popup.SetDefaultItem(IDGITLC_COMPARE, FALSE);
1618 bEntryAdded = true;
1621 if (!g_Git.IsInitRepos() && (m_dwContextMenus & GITSLC_POPGNUDIFF))
1623 popup.AppendMenuIcon(IDGITLC_GNUDIFF1, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1624 bEntryAdded = true;
1627 if ((m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPAREWC)) && m_bHasWC)
1629 if ((!m_CurrentVersion.IsEmpty()) && m_CurrentVersion != GIT_REV_ZERO)
1631 popup.AppendMenuIcon(IDGITLC_COMPAREWC, IDS_LOG_POPUP_COMPARE, IDI_DIFF);
1632 bEntryAdded = true;
1636 if (bEntryAdded)
1637 popup.AppendMenu(MF_SEPARATOR);
1640 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
1642 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_COMPARETWOREVISIONS))
1644 popup.AppendMenuIcon(IDGITLC_COMPARETWOREVISIONS, IDS_LOG_POPUP_COMPARETWO, IDI_DIFF);
1645 popup.SetDefaultItem(IDGITLC_COMPARETWOREVISIONS, FALSE);
1647 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_GNUDIFF2REVISIONS))
1648 popup.AppendMenuIcon(IDGITLC_GNUDIFF2REVISIONS, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1651 //Select Multi item
1652 if (selectedCount > 0)
1654 if (selectedCount == 2 && (m_dwContextMenus & GITSLC_POPCOMPARETWOFILES) && (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO))
1656 POSITION pos = GetFirstSelectedItemPosition();
1657 int index = GetNextSelectedItem(pos);
1658 if (index >= 0)
1660 auto entry2 = GetListEntry(index);
1661 bool firstEntryExistsAndIsFile = entry2 && !entry2->IsDirectory();
1662 index = GetNextSelectedItem(pos);
1663 if (index >= 0)
1665 entry2 = GetListEntry(index);
1666 if (firstEntryExistsAndIsFile && entry2 && !entry2->IsDirectory())
1667 popup.AppendMenuIcon(IDGITLC_COMPARETWOFILES, IDS_STATUSLIST_CONTEXT_COMPARETWOFILES, IDI_DIFF);
1673 if (selectedCount > 0 && (!(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))) && m_bHasWC)
1675 if ((m_dwContextMenus & GITSLC_POPCOMMIT) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && !(wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1676 popup.AppendMenuIcon(IDGITLC_COMMIT, IDS_STATUSLIST_CONTEXT_COMMIT, IDI_COMMIT);
1678 if ((m_dwContextMenus & GITSLC_POPREVERT) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO))
1679 popup.AppendMenuIcon(IDGITLC_REVERT, IDS_MENUREVERT, IDI_REVERT);
1681 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)))
1682 popup.AppendMenuIcon(IDGITLC_SKIPWORKTREE, IDS_STATUSLIST_SKIPWORKTREE);
1684 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)))
1685 popup.AppendMenuIcon(IDGITLC_ASSUMEVALID, IDS_MENUASSUMEVALID);
1687 if ((m_dwContextMenus & GITLC_POPUNSETIGNORELOCALCHANGES) && (this->m_CurrentVersion.IsEmpty() || this->m_CurrentVersion == GIT_REV_ZERO) && (wcStatus & (CTGitPath::LOGACTIONS_SKIPWORKTREE | CTGitPath::LOGACTIONS_ASSUMEVALID)))
1688 popup.AppendMenuIcon(IDGITLC_UNSETIGNORELOCALCHANGES, IDS_STATUSLIST_UNSETIGNORELOCALCHANGES);
1690 if (m_dwContextMenus & GITSLC_POPRESTORE && !filepath->IsDirectory())
1692 if (m_restorepaths.find(filepath->GetWinPathString()) == m_restorepaths.end())
1693 popup.AppendMenuIcon(IDGITLC_CREATERESTORE, IDS_MENUCREATERESTORE, IDI_RESTORE);
1694 else
1695 popup.AppendMenuIcon(IDGITLC_RESTOREPATH, IDS_MENURESTORE, IDI_RESTORE);
1698 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOREV)) && ( !this->m_CurrentVersion.IsEmpty() )
1699 && this->m_CurrentVersion != GIT_REV_ZERO && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1701 popup.AppendMenuIcon(IDGITLC_REVERTTOREV, IDS_LOG_POPUP_REVERTTOREV, IDI_REVERT);
1704 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_REVERTTOPARENT)) && ( !this->m_CurrentVersion.IsEmpty() )
1705 && this->m_CurrentVersion != GIT_REV_ZERO && !(wcStatus & CTGitPath::LOGACTIONS_ADDED))
1707 popup.AppendMenuIcon(IDGITLC_REVERTTOPARENT, IDS_LOG_POPUP_REVERTTOPARENT, IDI_REVERT);
1711 if (selectedCount == 1 && !(wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
1713 if (m_dwContextMenus & GITSLC_POPSHOWLOG)
1714 popup.AppendMenuIcon(IDGITLC_LOG, IDS_REPOBROWSE_SHOWLOG, IDI_LOG);
1715 if (m_dwContextMenus & GITSLC_POPSHOWLOGSUBMODULE && filepath->IsDirectory())
1716 popup.AppendMenuIcon(IDGITLC_LOGSUBMODULE, IDS_LOG_SUBMODULE, IDI_LOG);
1717 if (m_dwContextMenus & GITSLC_POPSHOWLOGOLDNAME && (wcStatus & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY) && !filepath->GetGitOldPathString().IsEmpty()))
1718 popup.AppendMenuIcon(IDGITLC_LOGOLDNAME, IDS_STATUSLIST_SHOWLOGOLDNAME, IDI_LOG);
1719 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)
1720 popup.AppendMenuIcon(IDGITLC_BLAME, IDS_MENUBLAME, IDI_BLAME);
1723 if (selectedCount > 0)
1725 if ((m_dwContextMenus & GetContextMenuBit(IDGITLC_EXPORT)) && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1726 popup.AppendMenuIcon(IDGITLC_EXPORT, IDS_LOG_POPUP_EXPORT, IDI_EXPORT);
1729 if (selectedCount == 1)
1731 if (m_dwContextMenus & this->GetContextMenuBit(IDGITLC_SAVEAS) && ! filepath->IsDirectory() && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1732 popup.AppendMenuIcon(IDGITLC_SAVEAS, IDS_LOG_POPUP_SAVE, IDI_SAVEAS);
1734 if (m_dwContextMenus & GITSLC_POPOPEN && !filepath->IsDirectory() && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)))
1736 popup.AppendMenuIcon(IDGITLC_VIEWREV, IDS_LOG_POPUP_VIEWREV);
1737 popup.AppendMenuIcon(IDGITLC_OPEN, IDS_REPOBROWSE_OPEN, IDI_OPEN);
1738 popup.AppendMenuIcon(IDGITLC_OPENWITH, IDS_LOG_POPUP_OPENWITH, IDI_OPEN);
1741 if (m_dwContextMenus & GITSLC_POPEXPLORE && !(wcStatus & CTGitPath::LOGACTIONS_DELETED) && m_bHasWC)
1742 popup.AppendMenuIcon(IDGITLC_EXPLORE, IDS_STATUSLIST_CONTEXT_EXPLORE, IDI_EXPLORER);
1744 if (m_dwContextMenus & GITSLC_PREPAREDIFF && !(wcStatus & CTGitPath::LOGACTIONS_DELETED))
1746 popup.AppendMenu(MF_SEPARATOR);
1747 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF, IDS_PREPAREDIFF, IDI_DIFF);
1748 if (!m_sMarkForDiffFilename.IsEmpty())
1750 CString diffWith;
1751 if (filepath->GetGitPathString() == m_sMarkForDiffFilename)
1752 diffWith = m_sMarkForDiffVersion;
1753 else
1755 PathCompactPathEx(CStrBuf(diffWith, 2 * GIT_HASH_SIZE), m_sMarkForDiffFilename, 2 * GIT_HASH_SIZE, 0);
1756 diffWith += L':' + m_sMarkForDiffVersion.Left(g_Git.GetShortHASHLength());
1758 CString menuEntry;
1759 menuEntry.Format(IDS_MENUDIFFNOW, (LPCTSTR)diffWith);
1760 popup.AppendMenuIcon(IDGITLC_PREPAREDIFF_COMPARE, menuEntry, IDI_DIFF);
1764 if (selectedCount > 0)
1766 // if (((wcStatus == git_wc_status_unversioned)||(wcStatus == git_wc_status_ignored))&&(m_dwContextMenus & SVNSLC_POPDELETE))
1767 // popup.AppendMenuIcon(IDSVNLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1768 // 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))
1769 // {
1770 // if (bShift)
1771 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVEKEEP, IDI_DELETE);
1772 // else
1773 // popup.AppendMenuIcon(IDGitLC_REMOVE, IDS_MENUREMOVE, IDI_DELETE);
1774 // }
1775 if ((wcStatus & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE | CTGitPath::LOGACTIONS_MISSING))/*||(wcStatus == git_wc_status_deleted)*/)
1777 if (m_dwContextMenus & GITSLC_POPDELETE)
1778 popup.AppendMenuIcon(IDGITLC_DELETE, IDS_MENUREMOVE, IDI_DELETE);
1780 if ( (wcStatus & CTGitPath::LOGACTIONS_UNVER || wcStatus & CTGitPath::LOGACTIONS_DELETED) )
1782 if (m_dwContextMenus & GITSLC_POPIGNORE)
1784 CTGitPathList ignorelist;
1785 FillListOfSelectedItemPaths(ignorelist);
1786 //check if all selected entries have the same extension
1787 bool bSameExt = true;
1788 CString sExt;
1789 for (int i=0; i<ignorelist.GetCount(); ++i)
1791 if (sExt.IsEmpty() && (i==0))
1792 sExt = ignorelist[i].GetFileExtension();
1793 else if (sExt.CompareNoCase(ignorelist[i].GetFileExtension())!=0)
1794 bSameExt = false;
1796 if (bSameExt)
1798 if (ignoreSubMenu.CreateMenu())
1800 CString ignorepath;
1801 if (ignorelist.GetCount()==1)
1802 ignorepath = ignorelist[0].GetFileOrDirectoryName();
1803 else
1804 ignorepath.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1805 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNORE, ignorepath);
1806 ignorepath = L'*' + sExt;
1807 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREMASK, ignorepath);
1808 if (ignorelist.GetCount() == 1 && !ignorelist[0].GetContainingDirectory().GetGitPathString().IsEmpty())
1809 ignoreSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDGITLC_IGNOREFOLDER, ignorelist[0].GetContainingDirectory().GetGitPathString());
1810 CString temp;
1811 temp.LoadString(IDS_MENUIGNORE);
1812 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)ignoreSubMenu.m_hMenu, temp);
1815 else
1817 CString temp;
1818 if (ignorelist.GetCount()==1)
1819 temp.LoadString(IDS_MENUIGNORE);
1820 else
1821 temp.Format(IDS_MENUIGNOREMULTIPLE, ignorelist.GetCount());
1822 popup.AppendMenuIcon(IDGITLC_IGNORE, temp, IDI_IGNORE);
1823 temp.Format(IDS_MENUIGNOREMULTIPLEMASK, ignorelist.GetCount());
1824 popup.AppendMenuIcon(IDGITLC_IGNOREMASK, temp, IDI_IGNORE);
1832 if (selectedCount > 0)
1834 popup.AppendMenu(MF_SEPARATOR);
1835 popup.AppendMenuIcon(IDGITLC_COPY, IDS_STATUSLIST_CONTEXT_COPY, IDI_COPYCLIP);
1836 popup.AppendMenuIcon(IDGITLC_COPYEXT, IDS_STATUSLIST_CONTEXT_COPYEXT, IDI_COPYCLIP);
1837 #if 0
1838 if ((m_dwContextMenus & SVNSLC_POPCHANGELISTS))
1839 &&(wcStatus != git_wc_status_unversioned)&&(wcStatus != git_wc_status_none))
1841 popup.AppendMenu(MF_SEPARATOR);
1842 // changelist commands
1843 size_t numChangelists = GetNumberOfChangelistsInSelection();
1844 if (numChangelists > 0)
1845 popup.AppendMenuIcon(IDSVNLC_REMOVEFROMCS, IDS_STATUSLIST_CONTEXT_REMOVEFROMCS);
1846 if ((!entry->IsFolder())&&(changelistSubMenu.CreateMenu()))
1848 CString temp;
1849 temp.LoadString(IDS_STATUSLIST_CONTEXT_CREATECS);
1850 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDSVNLC_CREATECS, temp);
1852 if (entry->changelist.Compare(SVNSLC_IGNORECHANGELIST))
1854 changelistSubMenu.AppendMenu(MF_SEPARATOR);
1855 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, IDSVNLC_CREATEIGNORECS, SVNSLC_IGNORECHANGELIST);
1858 if (!m_changelists.empty())
1860 // find the changelist names
1861 bool bNeedSeparator = true;
1862 int cmdID = IDSVNLC_MOVETOCS;
1863 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
1865 if ((entry->changelist.Compare(it->first))&&(it->first.Compare(SVNSLC_IGNORECHANGELIST)))
1867 if (bNeedSeparator)
1869 changelistSubMenu.AppendMenu(MF_SEPARATOR);
1870 bNeedSeparator = false;
1872 changelistSubMenu.AppendMenu(MF_STRING | MF_ENABLED, cmdID, it->first);
1873 cmdID++;
1877 temp.LoadString(IDS_STATUSLIST_CONTEXT_MOVETOCS);
1878 popup.AppendMenu(MF_POPUP|MF_STRING, (UINT_PTR)changelistSubMenu.GetSafeHmenu(), temp);
1881 #endif
1884 m_hShellMenu = nullptr;
1885 if (selectedCount > 0 && !(wcStatus & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING)) && m_bHasWC && (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO) && shellMenu.CreatePopupMenu())
1887 // insert the shell context menu
1888 popup.AppendMenu(MF_SEPARATOR);
1889 popup.InsertMenu((UINT)-1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)shellMenu.m_hMenu, CString(MAKEINTRESOURCE(IDS_STATUSLIST_CONTEXT_SHELL)));
1890 m_hShellMenu = shellMenu.m_hMenu;
1893 int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
1894 g_IContext2 = nullptr;
1895 g_IContext3 = nullptr;
1896 if (m_pContextMenu)
1898 if (cmd >= SHELL_MIN_CMD && cmd <= SHELL_MAX_CMD) // see if returned idCommand belongs to shell menu entries)
1900 CMINVOKECOMMANDINFOEX cmi = { 0 };
1901 cmi.cbSize = sizeof(CMINVOKECOMMANDINFOEX);
1902 cmi.fMask = CMIC_MASK_UNICODE | CMIC_MASK_PTINVOKE;
1903 if (GetKeyState(VK_CONTROL) < 0)
1904 cmi.fMask |= CMIC_MASK_CONTROL_DOWN;
1905 if (bShift)
1906 cmi.fMask |= CMIC_MASK_SHIFT_DOWN;
1907 cmi.hwnd = m_hWnd;
1908 cmi.lpVerb = MAKEINTRESOURCEA(cmd - SHELL_MIN_CMD);
1909 cmi.lpVerbW = MAKEINTRESOURCEW(cmd - SHELL_MIN_CMD);
1910 cmi.nShow = SW_SHOWNORMAL;
1911 cmi.ptInvoke = point;
1913 m_pContextMenu->InvokeCommand((LPCMINVOKECOMMANDINFO)&cmi);
1915 cmd = 0;
1917 m_pContextMenu->Release();
1918 m_pContextMenu = nullptr;
1920 if (g_pFolderhook)
1922 delete g_pFolderhook;
1923 g_pFolderhook = nullptr;
1925 if (g_psfDesktopFolder)
1927 g_psfDesktopFolder->Release();
1928 g_psfDesktopFolder = nullptr;
1930 for (int i = 0; i < g_pidlArrayItems; i++)
1932 if (g_pidlArray[i])
1933 CoTaskMemFree(g_pidlArray[i]);
1935 if (g_pidlArray)
1936 CoTaskMemFree(g_pidlArray);
1937 g_pidlArray = nullptr;
1938 g_pidlArrayItems = 0;
1940 m_bBlock = TRUE;
1941 AfxGetApp()->DoWaitCursor(1);
1942 bShift = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
1943 //int iItemCountBeforeMenuCmd = GetItemCount();
1944 //bool bForce = false;
1945 switch (cmd)
1947 case IDGITLC_VIEWREV:
1948 OpenFile(filepath, ALTERNATIVEEDITOR);
1949 break;
1951 case IDGITLC_OPEN:
1952 OpenFile(filepath,OPEN);
1953 break;
1955 case IDGITLC_OPENWITH:
1956 OpenFile(filepath,OPEN_WITH);
1957 break;
1959 case IDGITLC_EXPLORE:
1960 CAppUtils::ExploreTo(GetSafeHwnd(), g_Git.CombinePath(filepath));
1961 break;
1963 case IDGITLC_PREPAREDIFF:
1964 m_sMarkForDiffFilename = filepath->GetGitPathString();
1965 m_sMarkForDiffVersion = m_CurrentVersion;
1966 break;
1968 case IDGITLC_PREPAREDIFF_COMPARE:
1970 CTGitPath savedFile(m_sMarkForDiffFilename);
1971 CGitDiff::Diff(filepath, &savedFile, m_CurrentVersion, m_sMarkForDiffVersion, false, false, 0, bShift);
1973 break;
1975 case IDGITLC_CREATERESTORE:
1977 POSITION pos = GetFirstSelectedItemPosition();
1978 while (pos)
1980 int index = GetNextSelectedItem(pos);
1981 auto entry2 = GetListEntry(index);
1982 if (!entry2 || entry2->IsDirectory())
1983 continue;
1984 if (m_restorepaths.find(entry2->GetWinPathString()) != m_restorepaths.end())
1985 continue;
1986 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
1987 // delete the temp file: the temp file has the FILE_ATTRIBUTE_TEMPORARY flag set
1988 // and copying the real file over it would leave that temp flag.
1989 DeleteFile(tempFile.GetWinPath());
1990 if (CopyFile(g_Git.CombinePath(entry2), tempFile.GetWinPath(), FALSE))
1992 m_restorepaths[entry2->GetWinPathString()] = tempFile.GetWinPathString();
1993 SetItemState(index, INDEXTOOVERLAYMASK(OVL_RESTORE), LVIS_OVERLAYMASK);
1996 Invalidate();
1998 break;
2000 case IDGITLC_RESTOREPATH:
2002 if (CMessageBox::Show(m_hWnd, IDS_STATUSLIST_RESTOREPATH, IDS_APPNAME, 2, IDI_QUESTION, IDS_RESTOREBUTTON, IDS_ABORTBUTTON) == 2)
2003 break;
2004 POSITION pos = GetFirstSelectedItemPosition();
2005 while (pos)
2007 int index = GetNextSelectedItem(pos);
2008 auto entry2 = GetListEntry(index);
2009 if (!entry2)
2010 continue;
2011 if (m_restorepaths.find(entry2->GetWinPathString()) == m_restorepaths.end())
2012 continue;
2013 if (CopyFile(m_restorepaths[entry2->GetWinPathString()], g_Git.CombinePath(entry2), FALSE))
2015 m_restorepaths.erase(entry2->GetWinPathString());
2016 SetItemState(index, 0, LVIS_OVERLAYMASK);
2019 Invalidate();
2021 break;
2023 // Compare current version and work copy.
2024 case IDGITLC_COMPAREWC:
2026 if (!CheckMultipleDiffs())
2027 break;
2028 POSITION pos = GetFirstSelectedItemPosition();
2029 while ( pos )
2031 int index = GetNextSelectedItem(pos);
2032 StartDiffWC(index);
2035 break;
2037 // Compare with base version. when current version is zero, compare workcopy and HEAD.
2038 case IDGITLC_COMPARE:
2040 if (!CheckMultipleDiffs())
2041 break;
2042 POSITION pos = GetFirstSelectedItemPosition();
2043 while ( pos )
2045 int index = GetNextSelectedItem(pos);
2046 StartDiff(index);
2049 break;
2051 case IDGITLC_COMPARETWOREVISIONS:
2053 if (!CheckMultipleDiffs())
2054 break;
2055 POSITION pos = GetFirstSelectedItemPosition();
2056 while ( pos )
2058 int index = GetNextSelectedItem(pos);
2059 StartDiffTwo(index);
2062 break;
2064 case IDGITLC_COMPARETWOFILES:
2066 POSITION pos = GetFirstSelectedItemPosition();
2067 if (pos)
2069 auto firstfilepath = GetListEntry(GetNextSelectedItem(pos));
2070 if (!firstfilepath)
2071 break;
2073 auto secondfilepath = GetListEntry(GetNextSelectedItem(pos));
2074 if (!secondfilepath)
2075 break;
2077 CString sCmd;
2078 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);
2079 if (bShift)
2080 sCmd += L" /alternative";
2081 CAppUtils::RunTortoiseGitProc(sCmd);
2084 break;
2086 case IDGITLC_GNUDIFF1:
2088 if (!CheckMultipleDiffs())
2089 break;
2090 POSITION pos = GetFirstSelectedItemPosition();
2091 while (pos)
2093 auto selectedFilepath = GetListEntry(GetNextSelectedItem(pos));
2094 if (m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
2096 CString fromwhere;
2097 if (m_amend)
2098 fromwhere = L"~1";
2099 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, GitRev::GetHead() + fromwhere, *selectedFilepath, GitRev::GetWorkingCopy(), bShift);
2101 else
2103 if ((selectedFilepath->m_ParentNo & (PARENT_MASK | MERGE_MASK)) == 0)
2104 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, m_CurrentVersion + L"~1", *selectedFilepath, m_CurrentVersion, bShift);
2105 else
2107 CString str;
2108 if (!(selectedFilepath->m_ParentNo & MERGE_MASK))
2109 str.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, (selectedFilepath->m_ParentNo & PARENT_MASK) + 1);
2111 CAppUtils::StartShowUnifiedDiff(m_hWnd, *selectedFilepath, str, *selectedFilepath, m_CurrentVersion, bShift, false, false, false, !!(selectedFilepath->m_ParentNo & MERGE_MASK));
2116 break;
2118 case IDGITLC_GNUDIFF2REVISIONS:
2120 if (!CheckMultipleDiffs())
2121 break;
2122 POSITION pos = GetFirstSelectedItemPosition();
2123 while (pos)
2125 auto entry = GetListEntry(GetNextSelectedItem(pos));
2126 CAppUtils::StartShowUnifiedDiff(m_hWnd, *entry, m_Rev2, *entry, m_Rev1, bShift);
2129 break;
2131 case IDGITLC_ADD:
2133 CTGitPathList paths;
2134 FillListOfSelectedItemPaths(paths, true);
2136 CGitProgressDlg progDlg;
2137 AddProgressCommand addCommand;
2138 progDlg.SetCommand(&addCommand);
2139 addCommand.SetShowCommitButtonAfterAdd((m_dwContextMenus & GITSLC_POPCOMMIT) != 0);
2140 addCommand.SetPathList(paths);
2141 progDlg.SetItemCount(paths.GetCount());
2142 progDlg.DoModal();
2144 // reset unchecked status
2145 POSITION pos = GetFirstSelectedItemPosition();
2146 int index;
2147 while ((index = GetNextSelectedItem(pos)) >= 0)
2148 m_mapFilenameToChecked.erase(GetListEntry(index)->GetGitPathString());
2150 if (GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2151 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2153 SetRedraw(TRUE);
2155 break;
2157 case IDGITLC_DELETE:
2158 DeleteSelectedFiles();
2159 break;
2161 case IDGITLC_BLAME:
2163 CAppUtils::LaunchTortoiseBlame(g_Git.CombinePath(filepath), m_CurrentVersion);
2165 break;
2167 case IDGITLC_LOG:
2168 case IDGITLC_LOGSUBMODULE:
2170 CString sCmd;
2171 sCmd.Format(L"/command:log /path:\"%s\"", (LPCTSTR)g_Git.CombinePath(filepath));
2172 if (cmd == IDGITLC_LOG && filepath->IsDirectory())
2173 sCmd += L" /submodule";
2174 if (!m_sDisplayedBranch.IsEmpty())
2175 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2176 CAppUtils::RunTortoiseGitProc(sCmd, false, !(cmd == IDGITLC_LOGSUBMODULE));
2178 break;
2180 case IDGITLC_LOGOLDNAME:
2182 CTGitPath oldName(filepath->GetGitOldPathString());
2183 CString sCmd;
2184 sCmd.Format(L"/command:log /path:\"%s\"", (LPCTSTR)g_Git.CombinePath(oldName));
2185 if (!m_sDisplayedBranch.IsEmpty())
2186 sCmd += L" /range:\"" + m_sDisplayedBranch + L'"';
2187 CAppUtils::RunTortoiseGitProc(sCmd);
2189 break;
2191 case IDGITLC_EDITCONFLICT:
2193 if (CAppUtils::ConflictEdit(*filepath, bShift, m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2195 CString conflictedFile = g_Git.CombinePath(filepath);
2196 if (!PathFileExists(conflictedFile) && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2198 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2199 break;
2201 StoreScrollPos();
2202 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2205 break;
2207 case IDGITLC_RESOLVETHEIRS: //follow up
2208 case IDGITLC_RESOLVEMINE: //follow up
2209 case IDGITLC_RESOLVECONFLICT:
2211 if (CMessageBox::Show(m_hWnd, IDS_PROC_RESOLVE, IDS_APPNAME, MB_ICONQUESTION | MB_YESNO)==IDYES)
2213 bool needsFullRefresh = false;
2214 POSITION pos = GetFirstSelectedItemPosition();
2215 while (pos != 0)
2217 int index;
2218 index = GetNextSelectedItem(pos);
2219 auto fentry = GetListEntry(index);
2220 if (!fentry)
2221 continue;
2223 CAppUtils::resolve_with resolveWith = CAppUtils::RESOLVE_WITH_CURRENT;
2224 if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE))
2225 resolveWith = CAppUtils::RESOLVE_WITH_THEIRS;
2226 else if (((!this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVEMINE) || ((this->m_bIsRevertTheirMy) && cmd == IDGITLC_RESOLVETHEIRS))
2227 resolveWith = CAppUtils::RESOLVE_WITH_MINE;
2228 if (CAppUtils::ResolveConflict(*fentry, resolveWith) == 0 && fentry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
2229 needsFullRefresh = true;
2231 if (needsFullRefresh && CRegDWORD(L"Software\\TortoiseGit\\RefreshFileListAfterResolvingConflict", TRUE) == TRUE)
2233 CWnd* pParent = GetLogicalParent();
2234 if (pParent && pParent->GetSafeHwnd())
2235 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2236 SetRedraw(TRUE);
2237 break;
2239 StoreScrollPos();
2240 Show(m_dwShow, 0, m_bShowFolders,0,true);
2243 break;
2245 case IDGITLC_IGNORE:
2247 CTGitPathList ignorelist;
2248 //std::vector<CString> toremove;
2249 FillListOfSelectedItemPaths(ignorelist, true);
2251 if(!CAppUtils::IgnoreFile(ignorelist,false))
2252 break;
2254 SetRedraw(FALSE);
2255 CWnd* pParent = GetLogicalParent();
2256 if (pParent && pParent->GetSafeHwnd())
2258 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2260 SetRedraw(TRUE);
2262 break;
2264 case IDGITLC_IGNOREMASK:
2266 CString common;
2267 CString ext=filepath->GetFileExtension();
2268 CTGitPathList ignorelist;
2269 FillListOfSelectedItemPaths(ignorelist, true);
2271 if (!CAppUtils::IgnoreFile(ignorelist,true))
2272 break;
2274 SetRedraw(FALSE);
2275 CWnd* pParent = GetLogicalParent();
2276 if (pParent && pParent->GetSafeHwnd())
2278 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2281 SetRedraw(TRUE);
2283 break;
2285 case IDGITLC_IGNOREFOLDER:
2287 CTGitPathList ignorelist;
2288 ignorelist.AddPath(filepath->GetContainingDirectory());
2290 if (!CAppUtils::IgnoreFile(ignorelist, false))
2291 break;
2293 SetRedraw(FALSE);
2294 CWnd *pParent = GetLogicalParent();
2295 if (pParent && pParent->GetSafeHwnd())
2296 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
2298 SetRedraw(TRUE);
2300 break;
2301 case IDGITLC_COMMIT:
2303 CTGitPathList targetList;
2304 FillListOfSelectedItemPaths(targetList);
2305 CTGitPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2306 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2307 CString commandline = L"/command:commit /pathfile:\"";
2308 commandline += tempFile.GetWinPathString();
2309 commandline += L'"';
2310 commandline += L" /deletepathfile";
2311 CAppUtils::RunTortoiseGitProc(commandline);
2313 break;
2314 case IDGITLC_REVERT:
2316 // If at least one item is not in the status "added"
2317 // we ask for a confirmation
2318 BOOL bConfirm = FALSE;
2319 POSITION pos = GetFirstSelectedItemPosition();
2320 int index;
2321 while ((index = GetNextSelectedItem(pos)) >= 0)
2323 auto fentry = GetListEntry(index);
2324 if(fentry && fentry->m_Action &CTGitPath::LOGACTIONS_MODIFIED && !fentry->IsDirectory())
2326 bConfirm = TRUE;
2327 break;
2331 CString str;
2332 str.Format(IDS_PROC_WARNREVERT, selectedCount);
2334 if (!bConfirm || MessageBox(str, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES)
2336 CTGitPathList targetList;
2337 FillListOfSelectedItemPaths(targetList);
2339 // make sure that the list is reverse sorted, so that
2340 // children are removed before any parents
2341 targetList.SortByPathname(true);
2343 // put all reverted files in the trashbin, except the ones with 'added'
2344 // status because they are not restored by the revert.
2345 CTGitPathList delList;
2346 POSITION pos2 = GetFirstSelectedItemPosition();
2347 int index2;
2348 while ((index2 = GetNextSelectedItem(pos2)) >= 0)
2350 auto entry = GetListEntry(index2);
2351 if (entry&&(!(entry->m_Action& CTGitPath::LOGACTIONS_ADDED))
2352 && (!(entry->m_Action& CTGitPath::LOGACTIONS_REPLACED)) && !entry->IsDirectory())
2354 CTGitPath fullpath;
2355 fullpath.SetFromWin(g_Git.CombinePath(entry));
2356 delList.AddPath(fullpath);
2359 if (DWORD(CRegDWORD(L"Software\\TortoiseGit\\RevertWithRecycleBin", TRUE)))
2360 delList.DeleteAllFiles(true);
2362 CString revertToCommit = L"HEAD";
2363 if (m_amend)
2364 revertToCommit = L"HEAD~1";
2365 CString err;
2366 if (g_Git.Revert(revertToCommit, targetList, err))
2367 MessageBox(L"Revert failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
2368 else
2370 bool updateStatusList = false;
2371 for (int i = 0 ; i < targetList.GetCount(); ++i)
2373 int nListboxEntries = GetItemCount();
2374 for (int nItem=0; nItem<nListboxEntries; ++nItem)
2376 auto path = GetListEntry(nItem);
2377 if (path->GetGitPathString()==targetList[i].GetGitPathString() && !path->IsDirectory())
2379 if(path->m_Action & CTGitPath::LOGACTIONS_ADDED)
2381 path->m_Action = CTGitPath::LOGACTIONS_UNVER;
2382 SetEntryCheck(path,nItem,false);
2383 updateStatusList = true;
2384 #if 0 // revert an added file and some entry will be cloned (part 1/2)
2385 SetItemGroup(nItem,1);
2386 this->m_StatusFileList.RemoveItem(*path);
2387 this->m_UnRevFileList.AddPath(*path);
2388 //this->m_IgnoreFileList.RemoveItem(*path);
2389 #endif
2391 else
2393 if (GetCheck(nItem))
2394 m_nSelected--;
2395 RemoveListEntry(nItem);
2397 break;
2399 else if (path->GetGitPathString()==targetList[i].GetGitPathString() && path->IsDirectory() && path->IsWCRoot())
2401 CString sCmd;
2402 sCmd.Format(L"/command:revert /path:\"%s\"", (LPCTSTR)path->GetGitPathString());
2403 CCommonAppUtils::RunTortoiseGitProc(sCmd);
2407 SetRedraw(TRUE);
2408 #if 0 // revert an added file and some entry will be cloned (part 2/2)
2409 Show(m_dwShow, 0, m_bShowFolders,updateStatusList,true);
2410 NotifyCheck();
2411 #else
2412 if (updateStatusList && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2413 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2414 #endif
2418 break;
2420 case IDGITLC_ASSUMEVALID:
2421 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_ASSUMEVALID, BST_CHECKED, BST_INDETERMINATE);
2422 break;
2423 case IDGITLC_SKIPWORKTREE:
2424 SetGitIndexFlagsForSelectedFiles(IDS_PROC_MARK_SKIPWORKTREE, BST_INDETERMINATE, BST_CHECKED);
2425 break;
2426 case IDGITLC_UNSETIGNORELOCALCHANGES:
2427 SetGitIndexFlagsForSelectedFiles(IDS_PROC_UNSET_IGNORELOCALCHANGES, BST_UNCHECKED, BST_UNCHECKED);
2428 break;
2429 case IDGITLC_COPY:
2430 CopySelectedEntriesToClipboard(0);
2431 break;
2432 case IDGITLC_COPYEXT:
2433 CopySelectedEntriesToClipboard((DWORD)-1);
2434 break;
2435 case IDGITLC_EXPORT:
2436 FilesExport();
2437 break;
2438 case IDGITLC_SAVEAS:
2439 FileSaveAs(filepath);
2440 break;
2442 case IDGITLC_REVERTTOREV:
2443 RevertSelectedItemToVersion();
2444 break;
2445 case IDGITLC_REVERTTOPARENT:
2446 RevertSelectedItemToVersion(true);
2447 break;
2448 #if 0
2449 case IDSVNLC_COMMIT:
2451 CTSVNPathList targetList;
2452 FillListOfSelectedItemPaths(targetList);
2453 CTSVNPath tempFile = CTempFiles::Instance().GetTempFilePath(false);
2454 VERIFY(targetList.WriteToFile(tempFile.GetWinPathString()));
2455 CString commandline = CPathUtils::GetAppDirectory();
2456 commandline += L"TortoiseGitProc.exe /command:commit /pathfile:\"";
2457 commandline += tempFile.GetWinPathString();
2458 commandline += L'"';
2459 commandline += L" /deletepathfile";
2460 CAppUtils::LaunchApplication(commandline, nullptr, false);
2462 break;
2463 case IDSVNLC_CREATEIGNORECS:
2464 CreateChangeList(SVNSLC_IGNORECHANGELIST);
2465 break;
2466 case IDSVNLC_CREATECS:
2468 CCreateChangelistDlg dlg;
2469 if (dlg.DoModal() == IDOK)
2470 CreateChangeList(dlg.m_sName);
2472 break;
2473 default:
2475 if (cmd < IDSVNLC_MOVETOCS)
2476 break;
2477 CTSVNPathList changelistItems;
2478 FillListOfSelectedItemPaths(changelistItems);
2480 // find the changelist name
2481 CString sChangelist;
2482 int cmdID = IDSVNLC_MOVETOCS;
2483 SetRedraw(FALSE);
2484 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
2486 if ((it->first.Compare(SVNSLC_IGNORECHANGELIST))&&(entry->changelist.Compare(it->first)))
2488 if (cmd == cmdID)
2489 sChangelist = it->first;
2490 cmdID++;
2493 if (!sChangelist.IsEmpty())
2495 SVN git;
2496 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
2498 // The changelists were moved, but we now need to run through the selected items again
2499 // and update their changelist
2500 POSITION pos = GetFirstSelectedItemPosition();
2501 int index;
2502 while ((index = GetNextSelectedItem(pos)) >= 0)
2504 FileEntry * e = GetListEntry(index);
2505 e->changelist = sChangelist;
2506 if (!e->IsFolder())
2508 if (m_changelists.find(e->changelist)!=m_changelists.end())
2509 SetItemGroup(index, m_changelists[e->changelist]);
2510 else
2511 SetItemGroup(index, 0);
2515 else
2516 MessageBox(git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
2518 SetRedraw(TRUE);
2520 break;
2521 #endif
2523 } // switch (cmd)
2524 m_bBlock = FALSE;
2525 AfxGetApp()->DoWaitCursor(-1);
2526 GetStatisticsString();
2527 //int iItemCountAfterMenuCmd = GetItemCount();
2528 //if (iItemCountAfterMenuCmd != iItemCountBeforeMenuCmd)
2530 // CWnd* pParent = GetParent();
2531 // if (pParent && pParent->GetSafeHwnd())
2532 // {
2533 // pParent->SendMessage(SVNSLNM_ITEMCOUNTCHANGED);
2534 // }
2536 } // if (popup.CreatePopupMenu())
2537 } // if (selIndex >= 0)
2540 void CGitStatusListCtrl::SetGitIndexFlagsForSelectedFiles(UINT message, BOOL assumevalid, BOOL skipworktree)
2542 if (CMessageBox::Show(GetSafeHwnd(), message, IDS_APPNAME, MB_YESNO | MB_DEFBUTTON2 | MB_ICONQUESTION) != IDYES)
2543 return;
2545 CAutoRepository repository(g_Git.GetGitRepository());
2546 if (!repository)
2548 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2549 return;
2552 CAutoIndex gitindex;
2553 if (git_repository_index(gitindex.GetPointer(), repository))
2555 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2556 return;
2559 POSITION pos = GetFirstSelectedItemPosition();
2560 int index = -1;
2561 while ((index = GetNextSelectedItem(pos)) >= 0)
2563 auto path = GetListEntry(index);
2564 if (path == nullptr)
2565 continue;
2567 size_t idx;
2568 if (!git_index_find(&idx, gitindex, CUnicodeUtils::GetMulti(path->GetGitPathString(), CP_UTF8)))
2570 git_index_entry *e = const_cast<git_index_entry *>(git_index_get_byindex(gitindex, idx)); // HACK
2571 if (assumevalid == BST_UNCHECKED)
2572 e->flags &= ~GIT_IDXENTRY_VALID;
2573 else if (assumevalid == BST_CHECKED)
2574 e->flags |= GIT_IDXENTRY_VALID;
2575 if (skipworktree == BST_UNCHECKED)
2576 e->flags_extended &= ~GIT_IDXENTRY_SKIP_WORKTREE;
2577 else if (skipworktree == BST_CHECKED)
2578 e->flags_extended |= GIT_IDXENTRY_SKIP_WORKTREE;
2579 git_index_add(gitindex, e);
2581 else
2582 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2585 if (git_index_write(gitindex))
2587 MessageBox(g_Git.GetLibGit2LastErr(), L"TortoiseGit", MB_ICONERROR);
2588 return;
2591 if (nullptr != GetLogicalParent() && nullptr != GetLogicalParent()->GetSafeHwnd())
2592 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2594 SetRedraw(TRUE);
2597 void CGitStatusListCtrl::OnContextMenu(CWnd* pWnd, CPoint point)
2599 __super::OnContextMenu(pWnd, point);
2600 if (pWnd == this)
2601 OnContextMenuList(pWnd, point);
2604 void CGitStatusListCtrl::OnNMDblclk(NMHDR *pNMHDR, LRESULT *pResult)
2606 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
2607 *pResult = 0;
2608 if (m_bBlock || m_bBusy)
2609 return;
2611 if (pNMLV->iItem < 0)
2612 return;
2614 Locker lock(m_critSec);
2616 auto file = GetListEntry(pNMLV->iItem);
2618 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)) {
2619 StartDiffWC(pNMLV->iItem);
2620 return;
2622 if( file->m_Action&CTGitPath::LOGACTIONS_UNMERGED )
2624 if (CAppUtils::ConflictEdit(*file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
2626 CString conflictedFile = g_Git.CombinePath(file);
2627 if (!PathFileExists(conflictedFile) && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
2629 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
2630 return;
2632 StoreScrollPos();
2633 Show(m_dwShow, 0, m_bShowFolders, 0, true);
2636 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))
2637 return;
2638 else
2640 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
2641 StartDiffTwo(pNMLV->iItem);
2642 else
2643 StartDiff(pNMLV->iItem);
2646 void CGitStatusListCtrl::StartDiffTwo(int fileindex)
2648 if(fileindex<0)
2649 return;
2651 auto ptr = GetListEntry(fileindex);
2652 if (!ptr)
2653 return;
2654 CTGitPath file1 = *ptr;
2656 if (file1.m_Action & CTGitPath::LOGACTIONS_ADDED)
2657 CGitDiff::DiffNull(&file1, m_Rev1, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2658 else if (file1.m_Action & CTGitPath::LOGACTIONS_DELETED)
2659 CGitDiff::DiffNull(&file1, m_Rev2, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2660 else
2661 CGitDiff::Diff(&file1, &file1, m_Rev1, m_Rev2, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2664 void CGitStatusListCtrl::StartDiffWC(int fileindex)
2666 if(fileindex<0)
2667 return;
2669 CString Ver;
2670 if (m_CurrentVersion.IsEmpty())
2671 m_CurrentVersion == GIT_REV_ZERO;
2673 auto ptr = GetListEntry(fileindex);
2674 if (!ptr)
2675 return;
2676 CTGitPath file1 = *ptr;
2677 file1.m_Action = 0; // reset action, so that diff is not started as added/deleted file; see issue #1757
2679 CGitDiff::Diff(&file1, &file1, GIT_REV_ZERO, m_CurrentVersion, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2682 void CGitStatusListCtrl::StartDiff(int fileindex)
2684 if(fileindex<0)
2685 return;
2687 auto ptr = GetListEntry(fileindex);
2688 if (!ptr)
2689 return;
2690 CTGitPath file1 = *ptr;
2691 CTGitPath file2;
2692 if(file1.m_Action & (CTGitPath::LOGACTIONS_REPLACED|CTGitPath::LOGACTIONS_COPY))
2693 file2.SetFromGit(file1.GetGitOldPathString());
2694 else
2695 file2=file1;
2697 if(this->m_CurrentVersion.IsEmpty() || m_CurrentVersion== GIT_REV_ZERO)
2699 CString fromwhere;
2700 if(m_amend && (file1.m_Action & CTGitPath::LOGACTIONS_ADDED) == 0)
2701 fromwhere = L"~1";
2702 if( g_Git.IsInitRepos())
2703 CGitDiff::DiffNull(GetListEntry(fileindex),
2704 GIT_REV_ZERO, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2705 else if( file1.m_Action&CTGitPath::LOGACTIONS_ADDED )
2706 CGitDiff::DiffNull(GetListEntry(fileindex),
2707 m_CurrentVersion + fromwhere, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2708 else if( file1.m_Action&CTGitPath::LOGACTIONS_DELETED )
2709 CGitDiff::DiffNull(GetListEntry(fileindex),
2710 GitRev::GetHead() + fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2711 else
2712 CGitDiff::Diff(&file1,&file2,
2713 CString(GIT_REV_ZERO),
2714 GitRev::GetHead() + fromwhere, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2716 else
2718 CGitHash hash;
2719 CString fromwhere = m_CurrentVersion + L"~1";
2720 if(m_amend)
2721 fromwhere = m_CurrentVersion + L"~2";
2722 bool revfail = !!g_Git.GetHash(hash, fromwhere);
2723 if (revfail || (file1.m_Action & file1.LOGACTIONS_ADDED))
2724 CGitDiff::DiffNull(&file1, m_CurrentVersion, true, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2725 else if (file1.m_Action & file1.LOGACTIONS_DELETED)
2727 if (file1.m_ParentNo > 0)
2728 fromwhere.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, file1.m_ParentNo + 1);
2730 CGitDiff::DiffNull(&file1, fromwhere, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2732 else
2734 if( file1.m_ParentNo & MERGE_MASK)
2736 CTGitPath base, theirs, mine, merge;
2738 CString temppath;
2739 GetTempPath(temppath);
2740 temppath.TrimRight(L'\\');
2742 mine.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".LOCAL" + file1.GetFileExtension());
2743 theirs.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".REMOTE" + file1.GetFileExtension());
2744 base.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".BASE" + file1.GetFileExtension());
2746 CFile tempfile;
2747 //create a empty file, incase stage is not three
2748 tempfile.Open(mine.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2749 tempfile.Close();
2750 tempfile.Open(theirs.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2751 tempfile.Close();
2752 tempfile.Open(base.GetWinPathString(),CFile::modeCreate|CFile::modeReadWrite);
2753 tempfile.Close();
2755 merge.SetFromGit(temppath + L'\\' + file1.GetFileOrDirectoryName() + L".Merged" + file1.GetFileExtension());
2757 int parent1=-1, parent2 =-1;
2758 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
2760 if(m_arStatusArray[i]->GetGitPathString() == file1.GetGitPathString())
2762 if(m_arStatusArray[i]->m_ParentNo & MERGE_MASK)
2765 else
2767 if(parent1<0)
2768 parent1 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
2769 else if (parent2 <0)
2770 parent2 = m_arStatusArray[i]->m_ParentNo & PARENT_MASK;
2775 if(g_Git.GetOneFile(m_CurrentVersion, file1, (CString&)merge.GetWinPathString()))
2776 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2778 if(parent1>=0)
2780 CString str;
2781 str.Format(L"%s^%d", (LPCTSTR)this->m_CurrentVersion, parent1 + 1);
2783 if(g_Git.GetOneFile(str, file1, (CString&)mine.GetWinPathString()))
2784 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2787 if(parent2>=0)
2789 CString str;
2790 str.Format(L"%s^%d", (LPCTSTR)this->m_CurrentVersion, parent2 + 1);
2792 if(g_Git.GetOneFile(str, file1, (CString&)theirs.GetWinPathString()))
2793 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETMERGEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2796 if(parent1>=0 && parent2>=0)
2798 CString cmd, output;
2799 cmd.Format(L"git.exe merge-base %s^%d %s^%d", (LPCTSTR)this->m_CurrentVersion, parent1 + 1,
2800 (LPCTSTR)m_CurrentVersion, parent2 + 1);
2802 if (!g_Git.Run(cmd, &output, nullptr, CP_UTF8))
2804 if (g_Git.GetOneFile(output.Left(2 * GIT_HASH_SIZE), file1, (CString&)base.GetWinPathString()))
2805 CMessageBox::Show(GetSafeHwnd(), IDS_STATUSLIST_FAILEDGETBASEFILE, IDS_APPNAME, MB_OK | MB_ICONERROR);
2808 CAppUtils::StartExtMerge(!!(GetAsyncKeyState(VK_SHIFT) & 0x8000), base, theirs, mine, merge, L"BASE", L"REMOTE", L"LOCAL");
2810 else
2812 CString str;
2813 if( (file1.m_ParentNo&PARENT_MASK) == 0)
2814 str = L"~1";
2815 else
2816 str.Format(L"^%d", (file1.m_ParentNo & PARENT_MASK) + 1);
2817 CGitDiff::Diff(&file1,&file2,
2818 m_CurrentVersion,
2819 m_CurrentVersion + str, false, false, 0, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
2825 CString CGitStatusListCtrl::GetStatisticsString(bool simple)
2827 CString sNormal = CString(MAKEINTRESOURCE(IDS_STATUSNORMAL));
2828 CString sAdded = CString(MAKEINTRESOURCE(IDS_STATUSADDED));
2829 CString sDeleted = CString(MAKEINTRESOURCE(IDS_STATUSDELETED));
2830 CString sModified = CString(MAKEINTRESOURCE(IDS_STATUSMODIFIED));
2831 CString sConflicted = CString(MAKEINTRESOURCE(IDS_STATUSCONFLICTED));
2832 CString sUnversioned = CString(MAKEINTRESOURCE(IDS_STATUSUNVERSIONED));
2833 CString sRenamed = CString(MAKEINTRESOURCE(IDS_STATUSREPLACED));
2834 CString sToolTip;
2835 if(simple)
2837 sToolTip.Format(IDS_STATUSLIST_STATUSLINE1,
2838 this->m_nLineAdded,this->m_nLineDeleted,
2839 (LPCTSTR)sModified, m_nModified,
2840 (LPCTSTR)sAdded, m_nAdded,
2841 (LPCTSTR)sDeleted, m_nDeleted,
2842 (LPCTSTR)sRenamed, m_nRenamed
2845 else
2847 sToolTip.Format(IDS_STATUSLIST_STATUSLINE2,
2848 this->m_nLineAdded,this->m_nLineDeleted,
2849 (LPCTSTR)sNormal, m_nNormal,
2850 (LPCTSTR)sUnversioned, m_nUnversioned,
2851 (LPCTSTR)sModified, m_nModified,
2852 (LPCTSTR)sAdded, m_nAdded,
2853 (LPCTSTR)sDeleted, m_nDeleted,
2854 (LPCTSTR)sConflicted, m_nConflicted
2857 CString sStats;
2858 sStats.Format(IDS_COMMITDLG_STATISTICSFORMAT, m_nSelected, GetItemCount());
2859 if (m_pStatLabel)
2860 m_pStatLabel->SetWindowText(sStats);
2862 if (m_pSelectButton)
2864 if (m_nSelected == 0)
2865 m_pSelectButton->SetCheck(BST_UNCHECKED);
2866 else if (m_nSelected != GetItemCount())
2867 m_pSelectButton->SetCheck(BST_INDETERMINATE);
2868 else
2869 m_pSelectButton->SetCheck(BST_CHECKED);
2872 if (m_pConfirmButton)
2873 m_pConfirmButton->EnableWindow(m_nSelected>0);
2875 return sToolTip;
2878 CString CGitStatusListCtrl::GetCommonDirectory(bool bStrict)
2880 if (!bStrict)
2882 // not strict means that the selected folder has priority
2883 if (!m_StatusFileList.GetCommonDirectory().IsEmpty())
2884 return m_StatusFileList.GetCommonDirectory().GetWinPath();
2887 CTGitPathList list;
2888 int nListItems = GetItemCount();
2889 for (int i=0; i<nListItems; ++i)
2891 auto* entry = GetListEntry(i);
2892 if (entry->IsEmpty())
2893 continue;
2894 list.AddPath(*entry);
2896 return list.GetCommonRoot().GetWinPath();
2899 void CGitStatusListCtrl::SelectAll(bool bSelect, bool /*bIncludeNoCommits*/)
2901 CWaitCursor waitCursor;
2902 // block here so the LVN_ITEMCHANGED messages
2903 // get ignored
2904 m_bBlock = TRUE;
2905 SetRedraw(FALSE);
2907 int nListItems = GetItemCount();
2908 if (bSelect)
2909 m_nSelected = nListItems;
2910 else
2911 m_nSelected = 0;
2913 for (int i=0; i<nListItems; ++i)
2915 auto path = GetListEntry(i);
2916 if (!path)
2917 continue;
2918 //if ((bIncludeNoCommits)||(entry->GetChangeList().Compare(SVNSLC_IGNORECHANGELIST)))
2919 SetEntryCheck(path,i,bSelect);
2922 // unblock before redrawing
2923 m_bBlock = FALSE;
2924 SetRedraw(TRUE);
2925 GetStatisticsString();
2926 NotifyCheck();
2929 void CGitStatusListCtrl::Check(DWORD dwCheck, bool check)
2931 CWaitCursor waitCursor;
2932 // block here so the LVN_ITEMCHANGED messages
2933 // get ignored
2934 m_bBlock = TRUE;
2935 SetRedraw(FALSE);
2937 int nListItems = GetItemCount();
2939 for (int i = 0; i < nListItems; ++i)
2941 auto entry = GetListEntry(i);
2942 if (!entry)
2943 continue;
2945 DWORD showFlags = entry->m_Action;
2946 if (entry->IsDirectory())
2947 showFlags |= GITSLC_SHOWSUBMODULES;
2948 else
2949 showFlags |= GITSLC_SHOWFILES;
2951 if (check && (showFlags & dwCheck) && !GetCheck(i) && !(entry->IsDirectory() && m_bDoNotAutoselectSubmodules && !(dwCheck & GITSLC_SHOWSUBMODULES)))
2953 SetEntryCheck(entry, i, true);
2954 m_nSelected++;
2956 else if (!check && (showFlags & dwCheck) && GetCheck(i))
2958 SetEntryCheck(entry, i, false);
2959 m_nSelected--;
2962 // unblock before redrawing
2963 m_bBlock = FALSE;
2964 SetRedraw(TRUE);
2965 GetStatisticsString();
2966 NotifyCheck();
2969 void CGitStatusListCtrl::OnLvnGetInfoTip(NMHDR *pNMHDR, LRESULT *pResult)
2971 LPNMLVGETINFOTIP pGetInfoTip = reinterpret_cast<LPNMLVGETINFOTIP>(pNMHDR);
2972 *pResult = 0;
2973 if (m_bBlock || CRegDWORD(L"Software\\TortoiseGit\\ShowListFullPathTooltip", TRUE) != TRUE)
2974 return;
2976 auto entry = GetListEntry(pGetInfoTip->iItem);
2978 if (entry)
2979 if (pGetInfoTip->cchTextMax > entry->GetGitPathString().GetLength() + g_Git.m_CurrentDir.GetLength())
2981 CString str = g_Git.CombinePath(entry->GetWinPathString());
2982 wcsncpy_s(pGetInfoTip->pszText, pGetInfoTip->cchTextMax, str.GetBuffer(), pGetInfoTip->cchTextMax - 1);
2986 void CGitStatusListCtrl::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult)
2988 NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
2990 // Take the default processing unless we set this to something else below.
2991 *pResult = CDRF_DODEFAULT;
2993 // First thing - check the draw stage. If it's the control's prepaint
2994 // stage, then tell Windows we want messages for every item.
2996 switch (pLVCD->nmcd.dwDrawStage)
2998 case CDDS_PREPAINT:
2999 *pResult = CDRF_NOTIFYITEMDRAW;
3000 break;
3001 case CDDS_ITEMPREPAINT:
3003 // This is the prepaint stage for an item. Here's where we set the
3004 // item's text color. Our return value will tell Windows to draw the
3005 // item itself, but it will use the new color we set here.
3007 // Tell Windows to paint the control itself.
3008 *pResult = CDRF_DODEFAULT;
3009 if (m_bBlock)
3010 return;
3012 COLORREF crText = GetSysColor(COLOR_WINDOWTEXT);
3014 if (m_arStatusArray.size() > (DWORD_PTR)pLVCD->nmcd.dwItemSpec)
3016 auto entry = GetListEntry((int)pLVCD->nmcd.dwItemSpec);
3017 if (!entry)
3018 return;
3020 // coloring
3021 // ========
3022 // black : unversioned, normal
3023 // purple : added
3024 // blue : modified
3025 // brown : missing, deleted, replaced
3026 // green : merged (or potential merges)
3027 // red : conflicts or sure conflicts
3028 if(entry->m_Action & CTGitPath::LOGACTIONS_GRAY)
3029 crText = RGB(128,128,128);
3030 else if(entry->m_Action & CTGitPath::LOGACTIONS_UNMERGED)
3031 crText = m_Colors.GetColor(CColors::Conflict);
3032 else if(entry->m_Action & (CTGitPath::LOGACTIONS_MODIFIED))
3033 crText = m_Colors.GetColor(CColors::Modified);
3034 else if(entry->m_Action & (CTGitPath::LOGACTIONS_ADDED|CTGitPath::LOGACTIONS_COPY))
3035 crText = m_Colors.GetColor(CColors::Added);
3036 else if(entry->m_Action & CTGitPath::LOGACTIONS_DELETED)
3037 crText = m_Colors.GetColor(CColors::Deleted);
3038 else if(entry->m_Action & CTGitPath::LOGACTIONS_REPLACED)
3039 crText = m_Colors.GetColor(CColors::Renamed);
3040 else if(entry->m_Action & CTGitPath::LOGACTIONS_MERGED)
3041 crText = m_Colors.GetColor(CColors::Merged);
3042 else
3043 crText = GetSysColor(COLOR_WINDOWTEXT);
3044 // Store the color back in the NMLVCUSTOMDRAW struct.
3045 pLVCD->clrText = crText;
3048 break;
3052 BOOL CGitStatusListCtrl::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
3054 if (pWnd != this)
3055 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3056 if (!m_bBlock)
3058 HCURSOR hCur = LoadCursor(nullptr, IDC_ARROW);
3059 SetCursor(hCur);
3060 return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
3062 HCURSOR hCur = LoadCursor(nullptr, IDC_WAIT);
3063 SetCursor(hCur);
3064 return TRUE;
3067 void CGitStatusListCtrl::RemoveListEntry(int index)
3069 Locker lock(m_critSec);
3070 DeleteItem(index);
3072 m_arStatusArray.erase(m_arStatusArray.cbegin() + index);
3074 #if 0
3075 delete m_arStatusArray[m_arListArray[index]];
3076 m_arStatusArray.erase(m_arStatusArray.begin()+m_arListArray[index]);
3077 m_arListArray.erase(m_arListArray.begin()+index);
3078 for (int i=index; i< (int)m_arListArray.size(); ++i)
3080 m_arListArray[i]--;
3082 #endif
3085 ///< Set a checkbox on an entry in the listbox
3086 // NEVER, EVER call SetCheck directly, because you'll end-up with the checkboxes and the 'checked' flag getting out of sync
3087 void CGitStatusListCtrl::SetEntryCheck(CTGitPath* pEntry, int listboxIndex, bool bCheck)
3089 pEntry->m_Checked = bCheck;
3090 m_mapFilenameToChecked[pEntry->GetGitPathString()] = bCheck;
3091 SetCheck(listboxIndex, bCheck);
3094 void CGitStatusListCtrl::ResetChecked(const CTGitPath& entry)
3096 CTGitPath adjustedEntry;
3097 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3098 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3099 else
3100 adjustedEntry.SetFromWin(entry.GetWinPathString().Right(entry.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3101 if (entry.IsDirectory())
3103 STRING_VECTOR toDelete;
3104 for (auto it = m_mapFilenameToChecked.begin(); it != m_mapFilenameToChecked.end(); ++it)
3106 if (adjustedEntry.IsAncestorOf(it->first))
3107 toDelete.emplace_back(it->first);
3109 for (const auto& file : toDelete)
3110 m_mapFilenameToChecked.erase(file);
3111 return;
3113 m_mapFilenameToChecked.erase(adjustedEntry.GetGitPathString());
3116 #if 0
3117 void CGitStatusListCtrl::SetCheckOnAllDescendentsOf(const FileEntry* parentEntry, bool bCheck)
3119 int nListItems = GetItemCount();
3120 for (int j=0; j< nListItems ; ++j)
3122 FileEntry * childEntry = GetListEntry(j);
3123 ASSERT(childEntry);
3124 if (!childEntry || childEntry == parentEntry)
3125 continue;
3126 if (childEntry->checked != bCheck)
3128 if (parentEntry->path.IsAncestorOf(childEntry->path))
3130 SetEntryCheck(childEntry,j,bCheck);
3131 if(bCheck)
3132 m_nSelected++;
3133 else
3134 m_nSelected--;
3139 #endif
3141 void CGitStatusListCtrl::WriteCheckedNamesToPathList(CTGitPathList& pathList)
3143 pathList.Clear();
3144 int nListItems = GetItemCount();
3145 for (int i = 0; i< nListItems; ++i)
3147 auto entry = GetListEntry(i);
3148 if (entry->m_Checked)
3149 pathList.AddPath(*entry);
3151 pathList.SortByPathname();
3155 /// Build a path list of all the selected items in the list (NOTE - SELECTED, not CHECKED)
3156 void CGitStatusListCtrl::FillListOfSelectedItemPaths(CTGitPathList& pathList, bool /*bNoIgnored*/)
3158 pathList.Clear();
3160 POSITION pos = GetFirstSelectedItemPosition();
3161 int index;
3162 while ((index = GetNextSelectedItem(pos)) >= 0)
3164 auto entry = GetListEntry(index);
3165 //if ((bNoIgnored)&&(entry->status == git_wc_status_ignored))
3166 // continue;
3167 pathList.AddPath(*entry);
3171 UINT CGitStatusListCtrl::OnGetDlgCode()
3173 // we want to process the return key and not have that one
3174 // routed to the default pushbutton
3175 return CListCtrl::OnGetDlgCode() | DLGC_WANTALLKEYS;
3178 void CGitStatusListCtrl::OnNMReturn(NMHDR * /*pNMHDR*/, LRESULT *pResult)
3180 *pResult = 0;
3181 if (m_bBlock)
3182 return;
3183 if (!CheckMultipleDiffs())
3184 return;
3185 bool needsRefresh = false;
3186 bool resolvedTreeConfict = false;
3187 POSITION pos = GetFirstSelectedItemPosition();
3188 while ( pos )
3190 int index = GetNextSelectedItem(pos);
3191 if (index < 0)
3192 return;
3193 auto file = GetListEntry(index);
3194 if (file == nullptr)
3195 return;
3196 if (file->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE))
3197 StartDiffWC(index);
3198 else if ((file->m_Action & CTGitPath::LOGACTIONS_UNMERGED))
3200 if (CAppUtils::ConflictEdit(*file, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000), m_bIsRevertTheirMy, GetLogicalParent() ? GetLogicalParent()->GetSafeHwnd() : nullptr))
3202 CString conflictedFile = g_Git.CombinePath(file);
3203 needsRefresh = needsRefresh || !PathFileExists(conflictedFile);
3204 resolvedTreeConfict = resolvedTreeConfict || (file->m_Action & CTGitPath::LOGACTIONS_UNMERGED) == 0;
3207 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))
3208 continue;
3209 else
3211 if (!m_Rev1.IsEmpty() && !m_Rev2.IsEmpty())
3212 StartDiffTwo(index);
3213 else
3214 StartDiff(index);
3217 if (needsRefresh && GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
3218 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
3219 else if (resolvedTreeConfict)
3221 StoreScrollPos();
3222 Show(m_dwShow, 0, m_bShowFolders, 0, true);
3226 void CGitStatusListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
3228 // Since we catch all keystrokes (to have the enter key processed here instead
3229 // of routed to the default pushbutton) we have to make sure that other
3230 // keys like Tab and Esc still do what they're supposed to do
3231 // Tab = change focus to next/previous control
3232 // Esc = quit the dialog
3233 switch (nChar)
3235 case (VK_TAB):
3237 ::PostMessage(GetLogicalParent()->GetSafeHwnd(), WM_NEXTDLGCTL, GetKeyState(VK_SHIFT)&0x8000, 0);
3238 return;
3240 break;
3241 case (VK_ESCAPE):
3243 ::SendMessage(GetLogicalParent()->GetSafeHwnd(), WM_CLOSE, 0, 0);
3245 break;
3248 CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
3251 void CGitStatusListCtrl::PreSubclassWindow()
3253 __super::PreSubclassWindow();
3254 EnableToolTips(TRUE);
3255 SetWindowTheme(GetSafeHwnd(), L"Explorer", nullptr);
3258 void CGitStatusListCtrl::OnPaint()
3260 LRESULT defres = Default();
3261 if ((m_bBusy) || (m_bEmpty))
3263 CString str;
3264 if (m_bBusy)
3266 if (m_sBusy.IsEmpty())
3267 str.LoadString(IDS_STATUSLIST_BUSYMSG);
3268 else
3269 str = m_sBusy;
3271 else
3273 if (m_sEmpty.IsEmpty())
3274 str.LoadString(IDS_STATUSLIST_EMPTYMSG);
3275 else
3276 str = m_sEmpty;
3278 COLORREF clrText = ::GetSysColor(COLOR_WINDOWTEXT);
3279 COLORREF clrTextBk;
3280 if (IsWindowEnabled())
3281 clrTextBk = ::GetSysColor(COLOR_WINDOW);
3282 else
3283 clrTextBk = ::GetSysColor(COLOR_3DFACE);
3285 CRect rc;
3286 GetClientRect(&rc);
3287 CHeaderCtrl* pHC = GetHeaderCtrl();
3288 if (pHC)
3290 CRect rcH;
3291 pHC->GetItemRect(0, &rcH);
3292 rc.top += rcH.bottom;
3294 CDC* pDC = GetDC();
3296 CMyMemDC memDC(pDC, &rc);
3298 memDC.SetTextColor(clrText);
3299 memDC.SetBkColor(clrTextBk);
3300 memDC.BitBlt(rc.left, rc.top, rc.Width(), rc.Height(), pDC, rc.left, rc.top, SRCCOPY);
3301 rc.top += 10;
3302 CGdiObject * oldfont = memDC.SelectStockObject(DEFAULT_GUI_FONT);
3303 memDC.DrawText(str, rc, DT_CENTER | DT_VCENTER |
3304 DT_WORDBREAK | DT_NOPREFIX | DT_NOCLIP);
3305 memDC.SelectObject(oldfont);
3307 ReleaseDC(pDC);
3309 if (defres)
3311 // the Default() call did not process the WM_PAINT message!
3312 // Validate the update region ourselves to avoid
3313 // an endless loop repainting
3314 CRect rc;
3315 GetUpdateRect(&rc, FALSE);
3316 if (!rc.IsRectEmpty())
3317 ValidateRect(rc);
3321 void CGitStatusListCtrl::OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult)
3323 LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
3325 Locker lock(m_critSec);
3327 CTGitPathList pathList;
3328 FillListOfSelectedItemPaths(pathList);
3329 if (pathList.IsEmpty())
3330 return;
3332 auto pdsrc = std::make_unique<CIDropSource>();
3333 if (!pdsrc)
3334 return;
3335 pdsrc->AddRef();
3337 GitDataObject* pdobj = new GitDataObject(pathList, m_Rev2.IsEmpty() ? m_CurrentVersion : m_Rev2);
3338 if (!pdobj)
3339 return;
3340 pdobj->AddRef();
3342 CDragSourceHelper dragsrchelper;
3344 SetRedraw(false);
3345 dragsrchelper.InitializeFromWindow(m_hWnd, pNMLV->ptAction, pdobj);
3346 SetRedraw(true);
3347 //dragsrchelper.InitializeFromBitmap()
3348 pdsrc->m_pIDataObj = pdobj;
3349 pdsrc->m_pIDataObj->AddRef();
3351 // Initiate the Drag & Drop
3352 DWORD dwEffect;
3353 m_bOwnDrag = true;
3354 ::DoDragDrop(pdobj, pdsrc.get(), DROPEFFECT_MOVE | DROPEFFECT_COPY, &dwEffect);
3355 m_bOwnDrag = false;
3356 pdsrc->Release();
3357 pdsrc.release();
3358 pdobj->Release();
3360 *pResult = 0;
3363 bool CGitStatusListCtrl::EnableFileDrop()
3365 m_bFileDropsEnabled = true;
3366 return true;
3369 bool CGitStatusListCtrl::HasPath(const CTGitPath& path)
3371 CTGitPath adjustedEntry;
3372 if (g_Git.m_CurrentDir[g_Git.m_CurrentDir.GetLength() - 1] == L'\\')
3373 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength()));
3374 else
3375 adjustedEntry.SetFromWin(path.GetWinPathString().Right(path.GetWinPathString().GetLength() - g_Git.m_CurrentDir.GetLength() - 1));
3376 for (size_t i=0; i < m_arStatusArray.size(); ++i)
3378 if (m_arStatusArray[i]->IsEquivalentTo(adjustedEntry))
3379 return true;
3382 return false;
3385 BOOL CGitStatusListCtrl::PreTranslateMessage(MSG* pMsg)
3387 if (pMsg->message == WM_KEYDOWN)
3389 switch (pMsg->wParam)
3391 case 'A':
3393 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3395 // select all entries
3396 for (int i=0; i<GetItemCount(); ++i)
3397 SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
3398 return TRUE;
3401 break;
3402 case 'C':
3403 case VK_INSERT:
3405 if (GetAsyncKeyState(VK_CONTROL)&0x8000)
3407 // copy all selected paths to the clipboard
3408 if (GetAsyncKeyState(VK_SHIFT)&0x8000)
3409 CopySelectedEntriesToClipboard(GITSLC_COLSTATUS);
3410 else
3411 CopySelectedEntriesToClipboard(0);
3412 return TRUE;
3415 break;
3416 case VK_DELETE:
3418 if ((GetSelectedCount() > 0) && (m_dwContextMenus & GITSLC_POPDELETE))
3420 m_bBlock = TRUE;
3421 auto filepath = GetListEntry(GetSelectionMark());
3422 if (filepath != nullptr && (filepath->m_Action & (CTGitPath::LOGACTIONS_UNVER | CTGitPath::LOGACTIONS_IGNORE)))
3423 DeleteSelectedFiles();
3424 m_bBlock = FALSE;
3427 break;
3431 return __super::PreTranslateMessage(pMsg);
3434 bool CGitStatusListCtrl::CopySelectedEntriesToClipboard(DWORD dwCols)
3436 static HINSTANCE hResourceHandle(AfxGetResourceHandle());
3437 // WORD langID = (WORD)CRegStdDWORD(L"Software\\TortoiseGit\\LanguageID", GetUserDefaultLangID());
3439 CString sClipboard;
3440 CString temp;
3441 //TCHAR buf[100];
3442 if (GetSelectedCount() == 0)
3443 return false;
3445 // first add the column titles as the first line
3446 // We needn't head when only path copy
3447 //temp.LoadString(IDS_STATUSLIST_COLFILE);
3448 //sClipboard = temp;
3450 DWORD selection = 0;
3451 for (int i = 0, count = m_ColumnManager.GetColumnCount(); i < count; ++i)
3452 if ( ((dwCols == -1) && m_ColumnManager.IsVisible (i))
3453 || ((dwCols != 1) && (i < 32) && ((dwCols & (1 << i)) != 0)))
3455 sClipboard += L'\t' + m_ColumnManager.GetName(i);
3457 if (i < 32)
3458 selection += 1 << i;
3461 if(dwCols)
3462 sClipboard += L"\r\n";
3464 POSITION pos = GetFirstSelectedItemPosition();
3465 int index;
3466 while ((index = GetNextSelectedItem(pos)) >= 0)
3468 auto entry = GetListEntry(index);
3469 if (!entry)
3470 continue;
3472 sClipboard += entry->GetWinPathString();
3473 if (selection & GITSLC_COLFILENAME)
3474 sClipboard += L'\t' + entry->GetFileOrDirectoryName();
3475 if (selection & GITSLC_COLEXT)
3476 sClipboard += L'\t' + entry->GetFileExtension();
3478 if (selection & GITSLC_COLSTATUS)
3479 sClipboard += L'\t' + entry->GetActionName();
3480 #if 0
3481 if (selection & SVNSLC_COLDATE)
3483 TCHAR datebuf[SVN_DATE_BUFFER];
3484 apr_time_t date = entry->last_commit_date;
3485 SVN::formatDate(datebuf, date, true);
3486 if (date)
3487 temp = datebuf;
3488 else
3489 temp.Empty();
3490 sClipboard += L'\t' + temp;
3492 for ( int i = SVNSLC_NUMCOLUMNS, count = m_ColumnManager.GetColumnCount()
3493 ; i < count
3494 ; ++i)
3496 if ((dwCols == -1) && m_ColumnManager.IsVisible (i))
3498 CString value
3499 = entry->present_props[m_ColumnManager.GetName(i)];
3500 sClipboard += L'\t' + value;
3503 #endif
3504 if (selection & GITSLC_COLADD)
3505 sClipboard += L'\t' + entry->m_StatAdd;
3506 if (selection & GITSLC_COLDEL)
3507 sClipboard += L'\t' + entry->m_StatDel;
3509 sClipboard += L"\r\n";
3512 return CStringUtils::WriteAsciiStringToClipboard(sClipboard);
3515 size_t CGitStatusListCtrl::GetNumberOfChangelistsInSelection()
3517 #if 0
3518 std::set<CString> changelists;
3519 POSITION pos = GetFirstSelectedItemPosition();
3520 int index;
3521 while ((index = GetNextSelectedItem(pos)) >= 0)
3523 FileEntry * entry = GetListEntry(index);
3524 if (!entry->changelist.IsEmpty())
3525 changelists.insert(entry->changelist);
3527 return changelists.size();
3528 #endif
3529 return 0;
3532 bool CGitStatusListCtrl::PrepareGroups(bool bForce /* = false */)
3534 bool bHasGroups=false;
3535 int max =0;
3537 for (size_t i = 0; i < m_arStatusArray.size(); ++i)
3539 int ParentNo = m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3540 if( ParentNo > max)
3541 max=m_arStatusArray[i]->m_ParentNo&PARENT_MASK;
3544 if (((m_dwShow & GITSLC_SHOWUNVERSIONED) && !m_UnRevFileList.IsEmpty()) ||
3545 ((m_dwShow & GITSLC_SHOWIGNORED) && !m_IgnoreFileList.IsEmpty()) ||
3546 (m_dwShow & (GITSLC_SHOWASSUMEVALID | GITSLC_SHOWSKIPWORKTREE) && !m_LocalChangesIgnoredFileList.IsEmpty()) ||
3547 max>0 || bForce)
3549 bHasGroups = true;
3552 RemoveAllGroups();
3553 EnableGroupView(bHasGroups);
3555 TCHAR groupname[1024] = { 0 };
3556 int groupindex = 0;
3558 if(bHasGroups)
3560 LVGROUP grp = {0};
3561 grp.cbSize = sizeof(LVGROUP);
3562 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3563 groupindex=0;
3565 //if(m_UnRevFileList.GetCount()>0)
3566 if(max >0)
3568 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MERGEDFILES)), 1023);
3569 grp.pszHeader = groupname;
3570 grp.iGroupId = MERGE_MASK;
3571 grp.uAlign = LVGA_HEADER_LEFT;
3572 InsertGroup(0, &grp);
3574 CAutoRepository repository(g_Git.GetGitRepository());
3575 if (!repository)
3576 MessageBox(CGit::GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK | MB_ICONERROR);
3577 for (int i = 0; i <= max; ++i)
3579 CString str;
3580 str.Format(IDS_STATUSLIST_GROUP_DIFFWITHPARENT, i+1);
3581 if (repository)
3583 CString rev;
3584 rev.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, i + 1);
3585 CGitHash hash;
3586 if (!CGit::GetHash(repository, hash, rev))
3587 str += L": " + hash.ToString().Left(g_Git.GetShortHASHLength());
3589 grp.pszHeader = str.GetBuffer();
3590 str.ReleaseBuffer();
3591 grp.iGroupId = i;
3592 grp.uAlign = LVGA_HEADER_LEFT;
3593 InsertGroup(i+1, &grp);
3596 else
3598 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_MODIFIEDFILES)), 1023);
3599 grp.pszHeader = groupname;
3600 grp.iGroupId = groupindex;
3601 grp.uAlign = LVGA_HEADER_LEFT;
3602 InsertGroup(groupindex++, &grp);
3605 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_NOTVERSIONEDFILES)), 1023);
3606 grp.pszHeader = groupname;
3607 grp.iGroupId = groupindex;
3608 grp.uAlign = LVGA_HEADER_LEFT;
3609 InsertGroup(groupindex++, &grp);
3612 //if(m_IgnoreFileList.GetCount()>0)
3614 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNOREDFILES)), 1023);
3615 grp.pszHeader = groupname;
3616 grp.iGroupId = groupindex;
3617 grp.uAlign = LVGA_HEADER_LEFT;
3618 InsertGroup(groupindex++, &grp);
3622 wcsncpy_s(groupname, 1024, (LPCTSTR)CString(MAKEINTRESOURCE(IDS_STATUSLIST_GROUP_IGNORELOCALCHANGES)), 1023);
3623 grp.pszHeader = groupname;
3624 grp.iGroupId = groupindex;
3625 grp.uAlign = LVGA_HEADER_LEFT;
3626 InsertGroup(groupindex++, &grp);
3631 #if 0
3632 m_bHasIgnoreGroup = false;
3634 // now add the items which don't belong to a group
3635 LVGROUP grp = {0};
3636 grp.cbSize = sizeof(LVGROUP);
3637 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3638 CString sUnassignedName(MAKEINTRESOURCE(IDS_STATUSLIST_UNASSIGNED_CHANGESET));
3639 wcsncpy_s(groupname, 1024, (LPCTSTR)sUnassignedName, 1023);
3640 grp.pszHeader = groupname;
3641 grp.iGroupId = groupindex;
3642 grp.uAlign = LVGA_HEADER_LEFT;
3643 InsertGroup(groupindex++, &grp);
3645 for (auto it = m_changelists.cbegin(); it != m_changelists.cend(); ++it)
3647 if (it->first.Compare(SVNSLC_IGNORECHANGELIST)!=0)
3649 LVGROUP grp = {0};
3650 grp.cbSize = sizeof(LVGROUP);
3651 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3652 wcsncpy_s(groupname, 1024, it->first, 1023);
3653 grp.pszHeader = groupname;
3654 grp.iGroupId = groupindex;
3655 grp.uAlign = LVGA_HEADER_LEFT;
3656 it->second = InsertGroup(groupindex++, &grp);
3658 else
3659 m_bHasIgnoreGroup = true;
3662 if (m_bHasIgnoreGroup)
3664 // and now add the group 'ignore-on-commit'
3665 std::map<CString,int>::iterator it = m_changelists.find(SVNSLC_IGNORECHANGELIST);
3666 if (it != m_changelists.end())
3668 grp.cbSize = sizeof(LVGROUP);
3669 grp.mask = LVGF_ALIGN | LVGF_GROUPID | LVGF_HEADER;
3670 wcsncpy_s(groupname, 1024, SVNSLC_IGNORECHANGELIST, 1023);
3671 grp.pszHeader = groupname;
3672 grp.iGroupId = groupindex;
3673 grp.uAlign = LVGA_HEADER_LEFT;
3674 it->second = InsertGroup(groupindex, &grp);
3677 #endif
3678 return bHasGroups;
3681 void CGitStatusListCtrl::NotifyCheck()
3683 CWnd* pParent = GetLogicalParent();
3684 if (pParent && pParent->GetSafeHwnd())
3686 pParent->SendMessage(GITSLNM_CHECKCHANGED, m_nSelected);
3690 int CGitStatusListCtrl::UpdateFileList(const CTGitPathList* list)
3692 m_CurrentVersion = GIT_REV_ZERO;
3694 g_Git.GetWorkingTreeChanges(m_StatusFileList, m_amend, list);
3696 BOOL bDeleteChecked = FALSE;
3697 int deleteFromIndex = 0;
3698 bool needsRefresh = false;
3699 for (int i = 0; i < m_StatusFileList.GetCount(); ++i)
3701 auto gitpatch = const_cast<CTGitPath*>(&m_StatusFileList[i]);
3702 gitpatch->m_Checked = TRUE;
3704 if ((gitpatch->m_Action & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_MODIFIED)) && !gitpatch->Exists())
3706 if (!bDeleteChecked)
3708 CString message;
3709 message.Format(IDS_ASK_REMOVE_FROM_INDEX, gitpatch->GetWinPath());
3710 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);
3712 if (deleteFromIndex == 1)
3714 CString err;
3715 if (g_Git.Run(L"git.exe checkout -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
3716 MessageBox(L"Restoring from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
3717 else
3718 needsRefresh = true;
3720 else if (deleteFromIndex == 2)
3722 CString err;
3723 if (g_Git.Run(L"git.exe rm -f --cache -- \"" + gitpatch->GetWinPathString() + L'"', &err, CP_UTF8))
3724 MessageBox(L"Removing from index failed:\n" + err, L"TortoiseGit", MB_ICONERROR);
3725 else
3726 needsRefresh = true;
3730 m_arStatusArray.push_back(&m_StatusFileList[i]);
3733 if (needsRefresh)
3734 MessageBox(L"Due to changes to the index, please refresh the dialog (e.g., by pressing F5).", L"TortoiseGit", MB_ICONINFORMATION);
3736 return 0;
3739 int CGitStatusListCtrl::UpdateWithGitPathList(CTGitPathList &list)
3741 m_arStatusArray.clear();
3742 for (int i = 0; i < list.GetCount(); ++i)
3744 auto gitpath = const_cast<CTGitPath*>(&list[i]);
3746 if(gitpath ->m_Action & CTGitPath::LOGACTIONS_HIDE)
3747 continue;
3749 gitpath->m_Checked = TRUE;
3750 m_arStatusArray.push_back(&list[i]);
3752 return 0;
3755 int CGitStatusListCtrl::UpdateUnRevFileList(CTGitPathList &list)
3757 m_UnRevFileList = list;
3758 for (int i = 0; i < m_UnRevFileList.GetCount(); ++i)
3760 auto gitpatch = const_cast<CTGitPath*>(&m_UnRevFileList[i]);
3761 gitpatch->m_Checked = FALSE;
3762 m_arStatusArray.push_back(&m_UnRevFileList[i]);
3764 return 0;
3767 int CGitStatusListCtrl::UpdateUnRevFileList(const CTGitPathList* List)
3769 CString err;
3770 if (m_UnRevFileList.FillUnRev(CTGitPath::LOGACTIONS_UNVER, List, &err))
3772 MessageBox(L"Failed to get UnRev file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
3773 return -1;
3776 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 CString err;
3788 if (m_IgnoreFileList.FillUnRev(CTGitPath::LOGACTIONS_IGNORE, List, &err))
3790 MessageBox(L"Failed to get Ignore file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
3791 return -1;
3794 for (int i = 0; i < m_IgnoreFileList.GetCount(); ++i)
3796 auto gitpatch = const_cast<CTGitPath*>(&m_IgnoreFileList[i]);
3797 gitpatch->m_Checked = FALSE;
3798 m_arStatusArray.push_back(&m_IgnoreFileList[i]);
3800 return 0;
3803 int CGitStatusListCtrl::UpdateLocalChangesIgnoredFileList(const CTGitPathList* list)
3805 m_LocalChangesIgnoredFileList.FillBasedOnIndexFlags(GIT_IDXENTRY_VALID, GIT_IDXENTRY_SKIP_WORKTREE, list);
3806 for (int i = 0; i < m_LocalChangesIgnoredFileList.GetCount(); ++i)
3808 auto gitpatch = const_cast<CTGitPath*>(&m_LocalChangesIgnoredFileList[i]);
3809 gitpatch->m_Checked = FALSE;
3810 m_arStatusArray.push_back(&m_LocalChangesIgnoredFileList[i]);
3812 return 0;
3815 int CGitStatusListCtrl::UpdateFileList(int mask, bool once, const CTGitPathList* List)
3817 if(mask&CGitStatusListCtrl::FILELIST_MODIFY)
3819 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_MODIFY)))
3821 UpdateFileList(List);
3822 m_FileLoaded|=CGitStatusListCtrl::FILELIST_MODIFY;
3825 if (mask & CGitStatusListCtrl::FILELIST_UNVER || mask & CGitStatusListCtrl::FILELIST_IGNORE)
3827 if(once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_UNVER)))
3829 UpdateUnRevFileList(List);
3830 m_FileLoaded|=CGitStatusListCtrl::FILELIST_UNVER;
3832 if(mask&CGitStatusListCtrl::FILELIST_IGNORE && (once || (!(m_FileLoaded&CGitStatusListCtrl::FILELIST_IGNORE))))
3834 UpdateIgnoreFileList(List);
3835 m_FileLoaded |= CGitStatusListCtrl::FILELIST_IGNORE;
3838 if (mask & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED && (once || (!(m_FileLoaded & CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED))))
3840 UpdateLocalChangesIgnoredFileList(List);
3841 m_FileLoaded |= CGitStatusListCtrl::FILELIST_LOCALCHANGESIGNORED;
3843 return 0;
3846 void CGitStatusListCtrl::Clear()
3848 m_FileLoaded=0;
3849 this->DeleteAllItems();
3850 this->m_arListArray.clear();
3851 this->m_arStatusArray.clear();
3852 this->m_changelists.clear();
3855 bool CGitStatusListCtrl::CheckMultipleDiffs()
3857 UINT selCount = GetSelectedCount();
3858 if (selCount > max(3, (DWORD)CRegDWORD(L"Software\\TortoiseGit\\NumDiffWarning", 10)))
3860 CString message;
3861 message.Format(IDS_STATUSLIST_WARN_MAXDIFF, selCount);
3862 return MessageBox(message, L"TortoiseGit", MB_YESNO | MB_ICONQUESTION) == IDYES;
3864 return true;
3867 //////////////////////////////////////////////////////////////////////////
3868 bool CGitStatusListCtrlDropTarget::OnDrop(FORMATETC* pFmtEtc, STGMEDIUM& medium, DWORD* /*pdwEffect*/, POINTL pt)
3870 if (pFmtEtc->cfFormat == CF_HDROP && medium.tymed == TYMED_HGLOBAL)
3872 HDROP hDrop = (HDROP)GlobalLock(medium.hGlobal);
3873 if (hDrop)
3875 TCHAR szFileName[MAX_PATH] = {0};
3877 UINT cFiles = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
3879 POINT clientpoint;
3880 clientpoint.x = pt.x;
3881 clientpoint.y = pt.y;
3882 ScreenToClient(m_hTargetWnd, &clientpoint);
3883 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
3885 #if 0
3886 CTGitPathList changelistItems;
3887 for (UINT i = 0; i < cFiles; ++i)
3889 if (DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
3890 changelistItems.AddPath(CTGitPath(szFileName));
3892 // find the changelist name
3893 CString sChangelist;
3894 LONG_PTR nGroup = m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint);
3895 for (std::map<CString, int>::iterator it = m_pGitStatusListCtrl->m_changelists.begin(); it != m_pGitStatusListCtrl->m_changelists.end(); ++it)
3896 if (it->second == nGroup)
3897 sChangelist = it->first;
3899 if (!sChangelist.IsEmpty())
3901 CGit git;
3902 if (git.AddToChangeList(changelistItems, sChangelist, git_depth_empty))
3904 for (int l=0; l<changelistItems.GetCount(); ++l)
3906 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
3907 if (index >= 0)
3909 auto e = m_pGitStatusListCtrl->GetListEntry(index);
3910 if (e)
3912 e->changelist = sChangelist;
3913 if (!e->IsFolder())
3915 if (m_pGitStatusListCtrl->m_changelists.find(e->changelist) != m_pGitStatusListCtrl->m_changelists.end())
3916 m_pGitStatusListCtrl->SetItemGroup(index, m_pGitStatusListCtrl->m_changelists[e->changelist]);
3917 else
3918 m_pGitStatusListCtrl->SetItemGroup(index, 0);
3922 else
3924 HWND hParentWnd = GetParent(m_hTargetWnd);
3925 if (hParentWnd)
3926 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)changelistItems[l].GetWinPath());
3930 else
3931 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
3933 else
3935 SVN git;
3936 if (git.RemoveFromChangeList(changelistItems, CStringArray(), git_depth_empty))
3938 for (int l=0; l<changelistItems.GetCount(); ++l)
3940 int index = m_pGitStatusListCtrl->GetIndex(changelistItems[l]);
3941 if (index >= 0)
3943 auto e = m_pGitStatusListCtrl->GetListEntry(index);
3944 if (e)
3946 e->changelist = sChangelist;
3947 m_pGitStatusListCtrl->SetItemGroup(index, 0);
3950 else
3952 HWND hParentWnd = GetParent(m_hTargetWnd);
3953 if (hParentWnd)
3954 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)changelistItems[l].GetWinPath());
3958 else
3959 CMessageBox::Show(m_pGitStatusListCtrl->m_hWnd, git.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
3961 #endif
3963 else
3965 for (UINT i = 0; i < cFiles; ++i)
3967 if (!DragQueryFile(hDrop, i, szFileName, _countof(szFileName)))
3968 continue;
3970 HWND hParentWnd = GetParent(m_hTargetWnd);
3971 if (hParentWnd)
3972 ::SendMessage(hParentWnd, CGitStatusListCtrl::GITSLNM_ADDFILE, 0, (LPARAM)szFileName);
3976 GlobalUnlock(medium.hGlobal);
3978 return true; //let base free the medium
3981 HRESULT STDMETHODCALLTYPE CGitStatusListCtrlDropTarget::DragOver(DWORD grfKeyState, POINTL pt, DWORD __RPC_FAR* pdwEffect)
3983 CIDropTarget::DragOver(grfKeyState, pt, pdwEffect);
3984 *pdwEffect = DROPEFFECT_COPY;
3985 if (m_pGitStatusListCtrl)
3987 POINT clientpoint;
3988 clientpoint.x = pt.x;
3989 clientpoint.y = pt.y;
3990 ScreenToClient(m_hTargetWnd, &clientpoint);
3991 if ((m_pGitStatusListCtrl->IsGroupViewEnabled()) && (m_pGitStatusListCtrl->GetGroupFromPoint(&clientpoint) >= 0))
3992 *pdwEffect = DROPEFFECT_NONE;
3993 else if ((!m_pGitStatusListCtrl->m_bFileDropsEnabled) || (m_pGitStatusListCtrl->m_bOwnDrag))
3994 *pdwEffect = DROPEFFECT_NONE;
3996 return S_OK;
3999 void CGitStatusListCtrl::FilesExport()
4001 CString exportDir;
4002 // export all changed files to a folder
4003 CBrowseFolder browseFolder;
4004 browseFolder.m_style = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNFSANCESTORS | BIF_RETURNONLYFSDIRS;
4005 if (browseFolder.Show(GetSafeHwnd(), exportDir) != CBrowseFolder::OK)
4006 return;
4008 POSITION pos = GetFirstSelectedItemPosition();
4009 int index;
4010 while ((index = GetNextSelectedItem(pos)) >= 0)
4012 auto fd = GetListEntry(index);
4013 // we cannot export directories or folders
4014 if ((fd->m_Action & CTGitPath::LOGACTIONS_DELETED) || fd->IsDirectory())
4015 continue;
4017 CPathUtils::MakeSureDirectoryPathExists(exportDir + L'\\' + fd->GetContainingDirectory().GetWinPathString());
4018 CString filename = exportDir + L'\\' + fd->GetWinPathString();
4019 if (m_CurrentVersion == GIT_REV_ZERO)
4021 if (!CopyFile(g_Git.CombinePath(fd), filename, false))
4023 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4024 return;
4027 else
4029 if (g_Git.GetOneFile(m_CurrentVersion, *fd, filename))
4031 CString out;
4032 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)fd->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)filename);
4033 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)
4034 return;
4040 void CGitStatusListCtrl::FileSaveAs(CTGitPath *path)
4042 CString filename;
4043 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());
4044 if (!CAppUtils::FileOpenSave(filename, nullptr, 0, 0, false, GetSafeHwnd()))
4045 return;
4046 if (m_CurrentVersion == GIT_REV_ZERO)
4048 if (!CopyFile(g_Git.CombinePath(path), filename, false))
4050 MessageBox(CFormatMessageWrapper(), L"TortoiseGit", MB_OK | MB_ICONERROR);
4051 return;
4054 else
4056 if (g_Git.GetOneFile(m_CurrentVersion, *path, filename))
4058 CString out;
4059 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)path->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)filename);
4060 CMessageBox::Show(GetSafeHwnd(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4061 return;
4066 int CGitStatusListCtrl::RevertSelectedItemToVersion(bool parent)
4068 if(this->m_CurrentVersion.IsEmpty())
4069 return 0;
4070 if(this->m_CurrentVersion == GIT_REV_ZERO)
4071 return 0;
4073 POSITION pos = GetFirstSelectedItemPosition();
4074 int index;
4075 CString cmd,out;
4076 std::map<CString, int> versionMap;
4077 while ((index = GetNextSelectedItem(pos)) >= 0)
4079 auto fentry = GetListEntry(index);
4080 CString version;
4081 if (parent)
4083 int parentNo = fentry->m_ParentNo & PARENT_MASK;
4084 CString ref;
4085 ref.Format(L"%s^%d", (LPCTSTR)m_CurrentVersion, parentNo + 1);
4086 CGitHash hash;
4087 if (g_Git.GetHash(hash, ref))
4089 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of ref \"" + ref + L"\"."), L"TortoiseGit", MB_ICONERROR);
4090 continue;
4093 version = hash.ToString();
4095 else
4096 version = m_CurrentVersion;
4098 CString filename = fentry->GetGitPathString();
4099 if (!fentry->GetGitOldPathString().IsEmpty())
4100 filename = fentry->GetGitOldPathString();
4101 cmd.Format(L"git.exe checkout %s -- \"%s\"", (LPCTSTR)version, (LPCTSTR)filename);
4102 out.Empty();
4103 if (g_Git.Run(cmd, &out, CP_UTF8))
4105 if (MessageBox(out, L"TortoiseGit", MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL)
4106 continue;
4108 else
4109 versionMap[version]++;
4112 out.Empty();
4113 for (auto it = versionMap.cbegin(); it != versionMap.cend(); ++it)
4115 CString versionEntry;
4116 versionEntry.Format(IDS_STATUSLIST_FILESREVERTED, it->second, (LPCTSTR)it->first);
4117 out += versionEntry + L"\r\n";
4119 if (!out.IsEmpty())
4121 if (GetLogicalParent() && GetLogicalParent()->GetSafeHwnd())
4122 GetLogicalParent()->SendMessage(GITSLNM_NEEDSREFRESH);
4123 CMessageBox::Show(GetSafeHwnd(), out, L"TortoiseGit", MB_OK);
4125 return 0;
4128 void CGitStatusListCtrl::OpenFile(CTGitPath*filepath,int mode)
4130 CString file;
4131 if(this->m_CurrentVersion.IsEmpty() || m_CurrentVersion == GIT_REV_ZERO)
4132 file = g_Git.CombinePath(filepath);
4133 else
4135 file = CTempFiles::Instance().GetTempFilePath(false, *filepath, m_CurrentVersion).GetWinPathString();
4136 CString cmd,out;
4137 if(g_Git.GetOneFile(m_CurrentVersion, *filepath, file))
4139 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)filepath->GetGitPathString(), (LPCTSTR)m_CurrentVersion, (LPCTSTR)file);
4140 CMessageBox::Show(GetSafeHwnd(), g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
4141 return;
4143 SetFileAttributes(file, FILE_ATTRIBUTE_READONLY);
4145 if(mode == ALTERNATIVEEDITOR)
4147 CAppUtils::LaunchAlternativeEditor(file);
4148 return;
4151 if (mode == OPEN)
4152 CAppUtils::ShellOpen(file, GetSafeHwnd());
4153 else
4154 CAppUtils::ShowOpenWithDialog(file, GetSafeHwnd());
4157 void CGitStatusListCtrl::DeleteSelectedFiles()
4159 //Collect paths
4160 std::vector<int> selectIndex;
4162 POSITION pos = GetFirstSelectedItemPosition();
4163 int index;
4164 while ((index = GetNextSelectedItem(pos)) >= 0)
4165 selectIndex.push_back(index);
4167 CAutoRepository repo = g_Git.GetGitRepository();
4168 if (!repo)
4170 MessageBox(g_Git.GetLibGit2LastErr(L"Could not open repository."), L"TortoiseGit", MB_OK);
4171 return;
4173 CAutoIndex gitIndex;
4174 if (git_repository_index(gitIndex.GetPointer(), repo))
4176 g_Git.GetLibGit2LastErr(L"Could not open index.");
4177 return;
4179 int needWriteIndex = 0;
4181 //Create file-list ('\0' separated) for SHFileOperation
4182 CString filelist;
4183 for (size_t i = 0; i < selectIndex.size(); ++i)
4185 index = selectIndex[i];
4187 auto path = GetListEntry(index);
4188 if (path == nullptr)
4189 continue;
4191 // do not report errors as we could remove an unversioned file
4192 needWriteIndex += git_index_remove_bypath(gitIndex, CUnicodeUtils::GetUTF8(path->GetGitPathString())) == 0 ? 1 : 0;
4194 if (!path->Exists())
4195 continue;
4197 filelist += path->GetWinPathString();
4198 filelist += L'|';
4200 filelist += L'|';
4201 int len = filelist.GetLength();
4202 auto buf = std::make_unique<TCHAR[]>(len + 2);
4203 wcscpy_s(buf.get(), len + 2, filelist);
4204 CStringUtils::PipesToNulls(buf.get(), len + 2);
4205 SHFILEOPSTRUCT fileop;
4206 fileop.hwnd = this->m_hWnd;
4207 fileop.wFunc = FO_DELETE;
4208 fileop.pFrom = buf.get();
4209 fileop.pTo = nullptr;
4210 fileop.fFlags = FOF_NO_CONNECTED_ELEMENTS | ((GetAsyncKeyState(VK_SHIFT) & 0x8000) ? 0 : FOF_ALLOWUNDO);
4211 fileop.lpszProgressTitle = L"deleting file";
4212 int result = SHFileOperation(&fileop);
4214 if ((result == 0 || len == 1) && (!fileop.fAnyOperationsAborted))
4216 if (needWriteIndex && git_index_write(gitIndex))
4217 MessageBox(g_Git.GetLibGit2LastErr(L"Could not write index."), L"TortoiseGit", MB_OK);
4219 if (needWriteIndex)
4221 CWnd* pParent = GetLogicalParent();
4222 if (pParent && pParent->GetSafeHwnd())
4223 pParent->SendMessage(GITSLNM_NEEDSREFRESH);
4224 SetRedraw(TRUE);
4225 return;
4228 SetRedraw(FALSE);
4229 POSITION pos2 = nullptr;
4230 while ((pos2 = GetFirstSelectedItemPosition()) != nullptr)
4232 int index2 = GetNextSelectedItem(pos2);
4233 if (GetCheck(index2))
4234 m_nSelected--;
4235 m_nTotal--;
4237 RemoveListEntry(index2);
4239 SetRedraw(TRUE);
4243 BOOL CGitStatusListCtrl::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
4245 switch (message)
4247 case WM_MENUCHAR: // only supported by IContextMenu3
4248 if (g_IContext3)
4250 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4251 return TRUE;
4253 break;
4255 case WM_DRAWITEM:
4256 case WM_MEASUREITEM:
4257 if (wParam)
4258 break; // if wParam != 0 then the message is not menu-related
4260 case WM_INITMENU:
4261 case WM_INITMENUPOPUP:
4263 HMENU hMenu = (HMENU)wParam;
4264 if ((hMenu == m_hShellMenu) && (GetMenuItemCount(hMenu) == 0))
4266 // the shell submenu is populated only on request, i.e. right
4267 // before the submenu is shown
4268 if (g_pFolderhook)
4270 delete g_pFolderhook;
4271 g_pFolderhook = nullptr;
4273 CTGitPathList targetList;
4274 FillListOfSelectedItemPaths(targetList);
4275 if (!targetList.IsEmpty())
4277 // get IShellFolder interface of Desktop (root of shell namespace)
4278 if (g_psfDesktopFolder)
4279 g_psfDesktopFolder->Release();
4280 SHGetDesktopFolder(&g_psfDesktopFolder); // needed to obtain full qualified pidl
4282 // ParseDisplayName creates a PIDL from a file system path relative to the IShellFolder interface
4283 // but since we use the Desktop as our interface and the Desktop is the namespace root
4284 // that means that it's a fully qualified PIDL, which is what we need
4286 if (g_pidlArray)
4288 for (int i = 0; i < g_pidlArrayItems; i++)
4290 if (g_pidlArray[i])
4291 CoTaskMemFree(g_pidlArray[i]);
4293 CoTaskMemFree(g_pidlArray);
4294 g_pidlArray = nullptr;
4295 g_pidlArrayItems = 0;
4297 int nItems = targetList.GetCount();
4298 g_pidlArray = (LPITEMIDLIST *)CoTaskMemAlloc((nItems + 10) * sizeof(LPITEMIDLIST));
4299 SecureZeroMemory(g_pidlArray, (nItems + 10) * sizeof(LPITEMIDLIST));
4300 int succeededItems = 0;
4301 PIDLIST_RELATIVE pidl = nullptr;
4303 int bufsize = 1024;
4304 auto filepath = std::make_unique<WCHAR[]>(bufsize);
4305 for (int i = 0; i < nItems; i++)
4307 CString fullPath = g_Git.CombinePath(targetList[i].GetWinPath());
4308 if (bufsize < fullPath.GetLength())
4310 bufsize = fullPath.GetLength() + 3;
4311 filepath = std::make_unique<WCHAR[]>(bufsize);
4313 wcscpy_s(filepath.get(), bufsize, fullPath);
4314 if (SUCCEEDED(g_psfDesktopFolder->ParseDisplayName(nullptr, 0, filepath.get(), nullptr, &pidl, nullptr)))
4315 g_pidlArray[succeededItems++] = pidl; // copy pidl to pidlArray
4317 if (succeededItems == 0)
4319 CoTaskMemFree(g_pidlArray);
4320 g_pidlArray = nullptr;
4323 g_pidlArrayItems = succeededItems;
4325 if (g_pidlArrayItems)
4327 CString ext = targetList[0].GetFileExtension();
4329 ASSOCIATIONELEMENT const rgAssocItem[] =
4331 { ASSOCCLASS_PROGID_STR, nullptr, ext },
4332 { ASSOCCLASS_SYSTEM_STR, nullptr, ext },
4333 { ASSOCCLASS_APP_STR, nullptr, ext },
4334 { ASSOCCLASS_STAR, nullptr, nullptr },
4335 { ASSOCCLASS_FOLDER, nullptr, nullptr },
4337 IQueryAssociations* pIQueryAssociations = nullptr;
4338 if (FAILED(AssocCreateForClasses(rgAssocItem, ARRAYSIZE(rgAssocItem), IID_IQueryAssociations, (void**)&pIQueryAssociations)))
4339 pIQueryAssociations = nullptr; // not a problem, it works without this
4341 g_pFolderhook = new CIShellFolderHook(g_psfDesktopFolder, targetList);
4342 LPCONTEXTMENU icm1 = nullptr;
4344 DEFCONTEXTMENU dcm = { 0 };
4345 dcm.hwnd = m_hWnd;
4346 dcm.psf = g_pFolderhook;
4347 dcm.cidl = g_pidlArrayItems;
4348 dcm.apidl = (PCUITEMID_CHILD_ARRAY)g_pidlArray;
4349 dcm.punkAssociationInfo = pIQueryAssociations;
4350 if (SUCCEEDED(SHCreateDefaultContextMenu(&dcm, IID_IContextMenu, (void**)&icm1)))
4352 int iMenuType = 0; // to know which version of IContextMenu is supported
4353 if (icm1)
4354 { // since we got an IContextMenu interface we can now obtain the higher version interfaces via that
4355 if (icm1->QueryInterface(IID_IContextMenu3, (void**)&m_pContextMenu) == S_OK)
4356 iMenuType = 3;
4357 else if (icm1->QueryInterface(IID_IContextMenu2, (void**)&m_pContextMenu) == S_OK)
4358 iMenuType = 2;
4360 if (m_pContextMenu)
4361 icm1->Release(); // we can now release version 1 interface, cause we got a higher one
4362 else
4364 // since no higher versions were found
4365 // redirect ppContextMenu to version 1 interface
4366 iMenuType = 1;
4367 m_pContextMenu = icm1;
4370 if (m_pContextMenu)
4372 // lets fill the our popup menu
4373 UINT flags = CMF_NORMAL;
4374 flags |= (GetKeyState(VK_SHIFT) & 0x8000) != 0 ? CMF_EXTENDEDVERBS : 0;
4375 m_pContextMenu->QueryContextMenu(hMenu, 0, SHELL_MIN_CMD, SHELL_MAX_CMD, flags);
4378 // subclass window to handle menu related messages in CShellContextMenu
4379 if (iMenuType > 1) // only subclass if its version 2 or 3
4381 if (iMenuType == 2)
4382 g_IContext2 = (LPCONTEXTMENU2)m_pContextMenu;
4383 else // version 3
4384 g_IContext3 = (LPCONTEXTMENU3)m_pContextMenu;
4388 if (pIQueryAssociations)
4389 pIQueryAssociations->Release();
4392 if (g_IContext3)
4393 g_IContext3->HandleMenuMsg2(message, wParam, lParam, pResult);
4394 else if (g_IContext2)
4395 g_IContext2->HandleMenuMsg(message, wParam, lParam);
4396 return TRUE;
4400 break;
4401 default:
4402 break;
4405 return __super::OnWndMsg(message, wParam, lParam, pResult);
4408 CTGitPath* CGitStatusListCtrl::GetListEntry(int index)
4410 auto entry = reinterpret_cast<CTGitPath*>(GetItemData(index));
4411 ASSERT(entry);
4412 return entry;
4415 void CGitStatusListCtrl::OnSysColorChange()
4417 __super::OnSysColorChange();
4418 if (m_nBackgroundImageID)
4419 CAppUtils::SetListCtrlBackgroundImage(GetSafeHwnd(), m_nBackgroundImageID);