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