1
// TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2009-2024 - TortoiseGit
4 // Copyright (C) 2003-2013 - 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.
20 // RepositoryBrowser.cpp : implementation file
24 #include "TortoiseProc.h"
25 #include "RepositoryBrowser.h"
29 #include "UnicodeUtils.h"
30 #include "SysImageList.h"
33 #include "PathUtils.h"
34 #include "StringUtils.h"
36 #include "DragDropImpl.h"
37 #include "GitDataObject.h"
40 #include "MessageBox.h"
42 #define OVERLAY_EXTERNAL 1
43 #define OVERLAY_EXECUTABLE 2
44 #define OVERLAY_SYMLINK 3
46 void SetSortArrowA(CListCtrl
* control
, int nColumn
, bool bAscending
)
52 CHeaderCtrl
* pHeader
= control
->GetHeaderCtrl();
53 HDITEM HeaderItem
= {0};
54 HeaderItem
.mask
= HDI_FORMAT
;
55 for (int i
= 0; i
< pHeader
->GetItemCount(); ++i
)
57 pHeader
->GetItem(i
, &HeaderItem
);
58 HeaderItem
.fmt
&= ~(HDF_SORTDOWN
| HDF_SORTUP
);
59 pHeader
->SetItem(i
, &HeaderItem
);
63 pHeader
->GetItem(nColumn
, &HeaderItem
);
64 HeaderItem
.fmt
|= (bAscending
? HDF_SORTUP
: HDF_SORTDOWN
);
65 pHeader
->SetItem(nColumn
, &HeaderItem
);
69 class CRepoListCompareFunc
72 CRepoListCompareFunc(CListCtrl
* pList
, int col
, bool desc
)
78 static int CALLBACK
StaticCompare(LPARAM lParam1
, LPARAM lParam2
, LPARAM lParamSort
)
80 return reinterpret_cast<CRepoListCompareFunc
*>(lParamSort
)->Compare(lParam1
, lParam2
);
83 int Compare(LPARAM lParam1
, LPARAM lParam2
)
85 auto pLeft
= reinterpret_cast<CShadowFilesTree
*>(m_pList
->GetItemData(static_cast<int>(lParam1
)));
86 auto pRight
= reinterpret_cast<CShadowFilesTree
*>(m_pList
->GetItemData(static_cast<int>(lParam2
)));
91 case CRepositoryBrowser::eCol_Name
:
92 result
= SortStrCmp(pLeft
->m_sName
, pRight
->m_sName
);
96 case CRepositoryBrowser::eCol_Extension
:
97 result
= m_pList
->GetItemText(static_cast<int>(lParam1
), 1).CompareNoCase(m_pList
->GetItemText(static_cast<int>(lParam2
), 1));
98 if (result
== 0) // if extensions are the same, use the filename to sort
99 result
= SortStrCmp(pRight
->m_sName
, pRight
->m_sName
);
103 case CRepositoryBrowser::eCol_FileSize
:
104 if (pLeft
->m_iSize
> pRight
->m_iSize
)
106 else if (pLeft
->m_iSize
< pRight
->m_iSize
)
109 result
= SortStrCmp(pLeft
->m_sName
, pRight
->m_sName
);
115 if (pLeft
->m_bFolder
!= pRight
->m_bFolder
)
117 if (pRight
->m_bFolder
)
125 int SortStrCmp(const CString
&left
, const CString
&right
)
127 if (CRepositoryBrowser::s_bSortLogical
)
128 return StrCmpLogicalW(left
, right
);
129 return StrCmpI(left
, right
);
132 int m_col
= CRepositoryBrowser::eCol_Name
;
134 CListCtrl
* m_pList
= nullptr;
137 // CRepositoryBrowser dialog
139 bool CRepositoryBrowser::s_bSortLogical
= true;
141 IMPLEMENT_DYNAMIC(CRepositoryBrowser
, CResizableStandAloneDialog
)
143 CRepositoryBrowser::CRepositoryBrowser(CString rev
, CWnd
* pParent
/*=nullptr*/)
144 : CResizableStandAloneDialog(CRepositoryBrowser::IDD
, pParent
)
146 , m_ColumnManager(&m_RepoList
)
150 CRepositoryBrowser::~CRepositoryBrowser()
154 void CRepositoryBrowser::DoDataExchange(CDataExchange
* pDX
)
156 CDialog::DoDataExchange(pDX
);
157 DDX_Control(pDX
, IDC_REPOTREE
, m_RepoTree
);
158 DDX_Control(pDX
, IDC_REPOLIST
, m_RepoList
);
162 BEGIN_MESSAGE_MAP(CRepositoryBrowser
, CResizableStandAloneDialog
)
163 ON_NOTIFY(TVN_SELCHANGED
, IDC_REPOTREE
, &CRepositoryBrowser::OnTvnSelchangedRepoTree
)
164 ON_NOTIFY(TVN_ITEMEXPANDING
, IDC_REPOTREE
, &CRepositoryBrowser::OnTvnItemExpandingRepoTree
)
166 ON_NOTIFY(LVN_COLUMNCLICK
, IDC_REPOLIST
, &CRepositoryBrowser::OnLvnColumnclickRepoList
)
167 ON_NOTIFY(LVN_ITEMCHANGED
, IDC_REPOLIST
, &CRepositoryBrowser::OnLvnItemchangedRepolist
)
168 ON_NOTIFY(NM_DBLCLK
, IDC_REPOLIST
, &CRepositoryBrowser::OnNMDblclk_RepoList
)
169 ON_BN_CLICKED(IDC_BUTTON_REVISION
, &CRepositoryBrowser::OnBnClickedButtonRevision
)
175 ON_NOTIFY(LVN_BEGINDRAG
, IDC_REPOLIST
, &CRepositoryBrowser::OnLvnBegindragRepolist
)
176 ON_NOTIFY(TVN_BEGINDRAG
, IDC_REPOTREE
, &CRepositoryBrowser::OnTvnBegindragRepotree
)
180 // CRepositoryBrowser message handlers
182 BOOL
CRepositoryBrowser::OnInitDialog()
184 CResizableStandAloneDialog::OnInitDialog();
185 CAppUtils::MarkWindowAsUnpinnable(m_hWnd
);
187 AddAnchor(IDC_STATIC_REPOURL
, TOP_LEFT
);
188 AddAnchor(IDC_REPOBROWSER_URL
, TOP_LEFT
, TOP_RIGHT
);
189 AddAnchor(IDC_STATIC_REF
, TOP_RIGHT
);
190 AddAnchor(IDC_BUTTON_REVISION
, TOP_RIGHT
);
191 AddAnchor(IDC_REPOTREE
, TOP_LEFT
, BOTTOM_LEFT
);
192 AddAnchor(IDC_REPOLIST
, TOP_LEFT
, BOTTOM_RIGHT
);
193 AddAnchor(IDHELP
, BOTTOM_RIGHT
);
194 AddAnchor(IDC_INFOLABEL
, BOTTOM_LEFT
, BOTTOM_RIGHT
);
195 AddAnchor(IDOK
, BOTTOM_RIGHT
);
196 AddAnchor(IDCANCEL
, BOTTOM_RIGHT
);
198 CRepositoryBrowser::s_bSortLogical
= !CRegDWORD(L
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\NoStrCmpLogical", 0, false, HKEY_CURRENT_USER
);
199 if (CRepositoryBrowser::s_bSortLogical
)
200 CRepositoryBrowser::s_bSortLogical
= !CRegDWORD(L
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\NoStrCmpLogical", 0, false, HKEY_LOCAL_MACHINE
);
202 static UINT columnNames
[] = { IDS_STATUSLIST_COLFILENAME
, IDS_STATUSLIST_COLEXT
, IDS_LOG_SIZE
};
203 static int columnWidths
[] = { CDPIAware::Instance().ScaleX(GetSafeHwnd(), 150), CDPIAware::Instance().ScaleX(GetSafeHwnd(), 100), CDPIAware::Instance().ScaleX(GetSafeHwnd(), 100) };
204 static_assert(_countof(columnNames
) == _countof(columnWidths
));
205 DWORD dwDefaultColumns
= (1 << eCol_Name
) | (1 << eCol_Extension
) | (1 << eCol_FileSize
);
206 m_ColumnManager
.SetNames(columnNames
, _countof(columnNames
));
207 constexpr int columnVersion
= 6; // adjust when changing number/names/etc. of columns
208 m_ColumnManager
.ReadSettings(dwDefaultColumns
, 0, L
"RepoBrowser", columnVersion
, _countof(columnNames
), columnWidths
);
209 m_ColumnManager
.SetRightAlign(m_ColumnManager
.GetColumnByName(IDS_LOG_SIZE
));
211 // set up the list control
212 // set the extended style of the list control
213 // the style LVS_EX_FULLROWSELECT interferes with the background watermark image but it's more important to be able to select in the whole row.
214 CRegDWORD
regFullRowSelect(L
"Software\\TortoiseGit\\FullRowSelect", TRUE
);
215 DWORD exStyle
= LVS_EX_HEADERDRAGDROP
| LVS_EX_DOUBLEBUFFER
| LVS_EX_INFOTIP
| LVS_EX_SUBITEMIMAGES
;
216 if (DWORD(regFullRowSelect
))
217 exStyle
|= LVS_EX_FULLROWSELECT
;
218 m_RepoList
.SetExtendedStyle(exStyle
);
219 m_RepoList
.SetImageList(&SYS_IMAGE_LIST(), LVSIL_SMALL
);
220 CAppUtils::SetListCtrlBackgroundImage(m_RepoList
.GetSafeHwnd(), IDI_REPOBROWSER_BKG
);
222 m_RepoTree
.SetImageList(&SYS_IMAGE_LIST(), TVSIL_NORMAL
);
223 exStyle
= TVS_EX_FADEINOUTEXPANDOS
| TVS_EX_AUTOHSCROLL
| TVS_EX_DOUBLEBUFFER
;
224 m_RepoTree
.SetExtendedStyle(exStyle
, exStyle
);
226 m_nExternalOvl
= SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_EXTERNALOVL
, 0, 0));
227 m_nExecutableOvl
= SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_EXECUTABLEOVL
, 0, 0));
228 m_nSymlinkOvl
= SYS_IMAGE_LIST().AddIcon(CCommonAppUtils::LoadIconEx(IDI_SYMLINKOVL
, 0, 0));
229 // set externaloverlay in SYS_IMAGE_LIST() in Refresh method, so that it is updated after every launch of the logdialog
231 SetWindowTheme(m_RepoTree
.GetSafeHwnd(), L
"Explorer", nullptr);
232 SetWindowTheme(m_RepoList
.GetSafeHwnd(), L
"Explorer", nullptr);
237 CAutoThemeData hTheme
= OpenThemeData(m_RepoTree
, L
"TREEVIEW");
238 GetThemeMetric(hTheme
, NULL
, TVP_TREEITEM
, TREIS_NORMAL
, TMT_BORDERSIZE
, &borderWidth
);
241 borderWidth
= GetSystemMetrics(SM_CYBORDER
);
242 m_RepoTree
.SetItemHeight(static_cast<SHORT
>(m_RepoTree
.GetItemHeight() + 2 * borderWidth
));
244 m_nIconFolder
= SYS_IMAGE_LIST().GetDirIconIndex();
245 m_nOpenIconFolder
= SYS_IMAGE_LIST().GetDirOpenIconIndex();
247 EnableSaveRestore(L
"Reposbrowser");
249 DWORD xPos
= CRegDWORD(L
"Software\\TortoiseGit\\TortoiseProc\\ResizableState\\RepobrowserDivider", 0);
253 GetDlgItem(IDC_REPOTREE
)->GetClientRect(&rc
);
254 xPos
= rc
.right
- rc
.left
;
256 HandleDividerMove(CPoint(CDPIAware::Instance().ScaleX(GetSafeHwnd(), xPos
+ 20), CDPIAware::Instance().ScaleY(GetSafeHwnd(), 10)), false);
258 CAppUtils::SetWindowTitle(*this, g_Git
.m_CurrentDir
);
260 m_bHasWC
= !GitAdminDir::IsBareRepo(g_Git
.m_CurrentDir
);
264 UpdateDiffWithFileFromReg();
266 m_RepoList
.SetFocus();
271 void CRepositoryBrowser::UpdateDiffWithFileFromReg()
273 static CString lastDiffLaterFile
;
274 if (CString diffLaterFile
= CRegString(L
"Software\\TortoiseGit\\DiffLater", L
""); !diffLaterFile
.IsEmpty() && lastDiffLaterFile
!= diffLaterFile
)
276 lastDiffLaterFile
= diffLaterFile
;
277 m_sMarkForDiffFilename
= diffLaterFile
;
278 m_sMarkForDiffVersion
.Empty();
282 void CRepositoryBrowser::OnDestroy()
284 const int maxcol
= m_ColumnManager
.GetColumnCount();
285 for (int col
= 0; col
< maxcol
; ++col
)
286 if (m_ColumnManager
.IsVisible(col
))
287 m_ColumnManager
.ColumnResized(col
);
288 m_ColumnManager
.WriteSettings();
290 CResizableStandAloneDialog::OnDestroy();
293 void CRepositoryBrowser::OnOK()
295 if (GetFocus() == &m_RepoList
&& (GetKeyState(VK_MENU
) & 0x8000) == 0)
297 // list control has focus: 'enter' the folder
298 if (m_RepoList
.GetSelectedCount() != 1)
301 POSITION pos
= m_RepoList
.GetFirstSelectedItemPosition();
304 auto item
= GetListEntry(m_RepoList
.GetNextSelectedItem(pos
));
307 FillListCtrlForShadowTree(item
);
308 m_RepoTree
.SelectItem(item
->m_hTree
);
311 OpenFile(item
->GetFullName(), OPEN
, item
->m_bSubmodule
, item
->m_hash
);
316 SaveDividerPosition();
317 CResizableStandAloneDialog::OnOK();
320 void CRepositoryBrowser::OnCancel()
322 SaveDividerPosition();
323 CResizableStandAloneDialog::OnCancel();
326 void CRepositoryBrowser::OnNMDblclk_RepoList(NMHDR
*pNMHDR
, LRESULT
*pResult
)
330 LPNMITEMACTIVATE pNmItemActivate
= reinterpret_cast<LPNMITEMACTIVATE
>(pNMHDR
);
331 if (pNmItemActivate
->iItem
< 0)
334 auto pItem
= GetListEntry(pNmItemActivate
->iItem
);
338 if (!pItem
->m_bFolder
)
340 OpenFile(pItem
->GetFullName(), OPEN
, pItem
->m_bSubmodule
, pItem
->m_hash
);
345 FillListCtrlForShadowTree(pItem
);
346 m_RepoTree
.SelectItem(pItem
->m_hTree
);
350 void CRepositoryBrowser::Refresh()
353 if (m_nExternalOvl
>= 0)
354 SYS_IMAGE_LIST().SetOverlayImage(m_nExternalOvl
, OVERLAY_EXTERNAL
);
355 if (m_nExecutableOvl
>= 0)
356 SYS_IMAGE_LIST().SetOverlayImage(m_nExecutableOvl
, OVERLAY_EXECUTABLE
);
357 if (m_nSymlinkOvl
>= 0)
358 SYS_IMAGE_LIST().SetOverlayImage(m_nSymlinkOvl
, OVERLAY_SYMLINK
);
360 m_RepoTree
.DeleteAllItems();
361 m_RepoList
.DeleteAllItems();
362 m_TreeRoot
.m_ShadowTree
.clear();
363 m_TreeRoot
.m_sName
.Empty();
364 m_TreeRoot
.m_bFolder
= true;
366 TVINSERTSTRUCT tvinsert
= {0};
367 tvinsert
.hParent
= TVI_ROOT
;
368 tvinsert
.hInsertAfter
= TVI_ROOT
;
369 tvinsert
.itemex
.mask
= TVIF_DI_SETITEM
| TVIF_PARAM
| TVIF_TEXT
| TVIF_IMAGE
| TVIF_SELECTEDIMAGE
| TVIF_STATE
;
370 tvinsert
.itemex
.pszText
= const_cast<LPWSTR
>(L
"/");
371 tvinsert
.itemex
.lParam
= reinterpret_cast<LPARAM
>(&m_TreeRoot
);
372 tvinsert
.itemex
.iImage
= m_nIconFolder
;
373 tvinsert
.itemex
.iSelectedImage
= m_nOpenIconFolder
;
374 m_TreeRoot
.m_hTree
= m_RepoTree
.InsertItem(&tvinsert
);
376 ReadTree(&m_TreeRoot
);
377 m_RepoTree
.Expand(m_TreeRoot
.m_hTree
, TVE_EXPAND
);
378 FillListCtrlForShadowTree(&m_TreeRoot
);
379 m_RepoTree
.SelectItem(m_TreeRoot
.m_hTree
);
383 int CRepositoryBrowser::ReadTreeRecursive(git_repository
& repo
, const git_tree
* tree
, CShadowFilesTree
* treeroot
, bool recursive
)
385 const size_t count
= git_tree_entrycount(tree
);
386 bool hasSubfolders
= false;
387 treeroot
->m_bLoaded
= true;
389 for (size_t i
= 0; i
< count
; ++i
)
391 const git_tree_entry
*entry
= git_tree_entry_byindex(tree
, i
);
395 const int mode
= git_tree_entry_filemode(entry
);
397 CString base
= CUnicodeUtils::GetUnicode(git_tree_entry_name(entry
));
399 const git_oid
*oid
= git_tree_entry_id(entry
);
400 CShadowFilesTree
* pNextTree
= &treeroot
->m_ShadowTree
[base
];
401 pNextTree
->m_sName
= base
;
402 pNextTree
->m_pParent
= treeroot
;
403 pNextTree
->m_hash
= oid
;
405 if (mode
== GIT_FILEMODE_COMMIT
)
406 pNextTree
->m_bSubmodule
= true;
407 else if (mode
& S_IFDIR
)
409 hasSubfolders
= true;
410 pNextTree
->m_bFolder
= true;
411 pNextTree
->m_bLoaded
= false;
413 TVINSERTSTRUCT tvinsert
= {0};
414 tvinsert
.hParent
= treeroot
->m_hTree
;
415 tvinsert
.hInsertAfter
= TVI_SORT
;
416 tvinsert
.itemex
.mask
= TVIF_DI_SETITEM
| TVIF_PARAM
| TVIF_TEXT
| TVIF_IMAGE
| TVIF_SELECTEDIMAGE
| TVIF_STATE
| TVIF_CHILDREN
;
417 tvinsert
.itemex
.pszText
= base
.GetBuffer(base
.GetLength());
418 tvinsert
.itemex
.cChildren
= 1;
419 tvinsert
.itemex
.lParam
= reinterpret_cast<LPARAM
>(pNextTree
);
420 tvinsert
.itemex
.iImage
= m_nIconFolder
;
421 tvinsert
.itemex
.iSelectedImage
= m_nOpenIconFolder
;
422 pNextTree
->m_hTree
= m_RepoTree
.InsertItem(&tvinsert
);
423 base
.ReleaseBuffer();
427 if (git_tree_lookup(subtree
.GetPointer(), &repo
, oid
))
429 MessageBox(CGit::GetLibGit2LastErr(L
"Could not lookup path."), L
"TortoiseGit", MB_ICONERROR
);
433 ReadTreeRecursive(repo
, subtree
, pNextTree
, recursive
);
438 if (mode
== GIT_FILEMODE_BLOB_EXECUTABLE
)
439 pNextTree
->m_bExecutable
= true;
440 if (mode
== GIT_FILEMODE_LINK
)
441 pNextTree
->m_bSymlink
= true;
443 git_blob_lookup(blob
.GetPointer(), &repo
, oid
);
447 pNextTree
->m_iSize
= git_blob_rawsize(blob
);
453 TVITEM tvitem
= { 0 };
454 tvitem
.hItem
= treeroot
->m_hTree
;
455 tvitem
.mask
= TVIF_CHILDREN
;
456 tvitem
.cChildren
= 0;
457 m_RepoTree
.SetItem(&tvitem
);
463 int CRepositoryBrowser::ReadTree(CShadowFilesTree
* treeroot
, const CString
& root
, bool recursive
)
466 CAutoRepository
repository(g_Git
.GetGitRepository());
469 MessageBox(CGit::GetLibGit2LastErr(L
"Could not open repository."), L
"TortoiseGit", MB_ICONERROR
);
473 if (m_sRevision
== L
"HEAD")
475 int ret
= git_repository_head_unborn(repository
);
476 if (ret
== 1) // is orphan
480 MessageBox(g_Git
.GetLibGit2LastErr(L
"Could not check HEAD."), L
"TortoiseGit", MB_ICONERROR
);
486 if (CGit::GetHash(repository
, hash
, m_sRevision
+ L
"^{}")) // add ^{} in order to dereference signed tags
488 MessageBox(CGit::GetLibGit2LastErr(L
"Could not get hash of \"" + m_sRevision
+ L
"^{}\"."), L
"TortoiseGit", MB_ICONERROR
);
493 if (git_commit_lookup(commit
.GetPointer(), repository
, hash
))
495 MessageBox(CGit::GetLibGit2LastErr(L
"Could not lookup commit."), L
"TortoiseGit", MB_ICONERROR
);
500 if (git_commit_tree(tree
.GetPointer(), commit
))
502 MessageBox(CGit::GetLibGit2LastErr(L
"Could not get tree of commit."), L
"TortoiseGit", MB_ICONERROR
);
508 CAutoTreeEntry treeEntry
;
509 if (git_tree_entry_bypath(treeEntry
.GetPointer(), tree
, CUnicodeUtils::GetUTF8(root
)))
511 MessageBox(CGit::GetLibGit2LastErr(L
"Could not lookup path."), L
"TortoiseGit", MB_ICONERROR
);
514 if (git_tree_entry_type(treeEntry
) != GIT_OBJECT_TREE
)
516 MessageBox(CGit::GetLibGit2LastErr(L
"Could not lookup path."), L
"TortoiseGit", MB_ICONERROR
);
521 if (git_tree_entry_to_object(object
.GetPointer(), repository
, treeEntry
))
523 MessageBox(CGit::GetLibGit2LastErr(L
"Could not lookup path."), L
"TortoiseGit", MB_ICONERROR
);
527 tree
.ConvertFrom(std::move(object
));
530 treeroot
->m_hash
= git_tree_id(tree
);
531 ReadTreeRecursive(*repository
, tree
, treeroot
, recursive
);
533 // try to resolve hash to a branch name
534 if (m_sRevision
== hash
.ToString())
537 if (CGit::GetMapHashToFriendName(repository
, map
))
538 MessageBox(g_Git
.GetLibGit2LastErr(L
"Could not get all refs."), L
"TortoiseGit", MB_ICONERROR
);
539 if (!map
[hash
].empty())
540 m_sRevision
= map
[hash
].at(0);
542 this->GetDlgItem(IDC_BUTTON_REVISION
)->SetWindowText(CStringUtils::EscapeAccellerators(m_sRevision
));
547 void CRepositoryBrowser::OnTvnSelchangedRepoTree(NMHDR
*pNMHDR
, LRESULT
*pResult
)
549 LPNMTREEVIEW pNMTreeView
= reinterpret_cast<LPNMTREEVIEW
>(pNMHDR
);
552 FillListCtrlForTreeNode(pNMTreeView
->itemNew
.hItem
);
555 void CRepositoryBrowser::OnTvnItemExpandingRepoTree(NMHDR
*pNMHDR
, LRESULT
*pResult
)
557 LPNMTREEVIEW pNMTreeView
= reinterpret_cast<LPNMTREEVIEW
>(pNMHDR
);
560 auto pTree
= GetTreeEntry(pNMTreeView
->itemNew
.hItem
);
564 if (!pTree
->m_bLoaded
)
565 ReadTree(pTree
, pTree
->GetFullName());
568 void CRepositoryBrowser::FillListCtrlForTreeNode(HTREEITEM treeNode
)
570 m_RepoList
.DeleteAllItems();
572 auto pTree
= GetTreeEntry(treeNode
);
576 CString url
= L
'/' + pTree
->GetFullName();
577 GetDlgItem(IDC_REPOBROWSER_URL
)->SetWindowText(url
);
579 if (!pTree
->m_bLoaded
)
580 ReadTree(pTree
, pTree
->GetFullName());
582 FillListCtrlForShadowTree(pTree
);
585 void CRepositoryBrowser::FillListCtrlForShadowTree(CShadowFilesTree
* pTree
)
587 for (auto itShadowTree
= pTree
->m_ShadowTree
.cbegin(); itShadowTree
!= pTree
->m_ShadowTree
.cend(); ++itShadowTree
)
589 int icon
= m_nIconFolder
;
590 if (!(*itShadowTree
).second
.m_bFolder
&& !(*itShadowTree
).second
.m_bSubmodule
)
592 icon
= SYS_IMAGE_LIST().GetPathIconIndex((*itShadowTree
).second
.m_sName
);
595 const int indexItem
= m_RepoList
.InsertItem(m_RepoList
.GetItemCount(), (*itShadowTree
).second
.m_sName
, icon
);
597 if ((*itShadowTree
).second
.m_bSubmodule
)
599 m_RepoList
.SetItemState(indexItem
, INDEXTOOVERLAYMASK(OVERLAY_EXTERNAL
), LVIS_OVERLAYMASK
);
601 if ((*itShadowTree
).second
.m_bExecutable
)
602 m_RepoList
.SetItemState(indexItem
, INDEXTOOVERLAYMASK(OVERLAY_EXECUTABLE
), LVIS_OVERLAYMASK
);
603 if ((*itShadowTree
).second
.m_bSymlink
)
604 m_RepoList
.SetItemState(indexItem
, INDEXTOOVERLAYMASK(OVERLAY_SYMLINK
), LVIS_OVERLAYMASK
);
605 m_RepoList
.SetItemData(indexItem
, reinterpret_cast<DWORD_PTR
>(&(*itShadowTree
).second
));
606 if (!(*itShadowTree
).second
.m_bFolder
&& !(*itShadowTree
).second
.m_bSubmodule
)
610 temp
= CPathUtils::GetFileExtFromPath((*itShadowTree
).second
.m_sName
);
611 m_RepoList
.SetItemText(indexItem
, eCol_Extension
, temp
);
613 StrFormatByteSize64((*itShadowTree
).second
.m_iSize
, CStrBuf(temp
, 20), 20);
614 m_RepoList
.SetItemText(indexItem
, eCol_FileSize
, temp
);
618 CRepoListCompareFunc
compareFunc(&m_RepoList
, m_currSortCol
, m_currSortDesc
);
619 m_RepoList
.SortItemsEx(&CRepoListCompareFunc::StaticCompare
, reinterpret_cast<DWORD_PTR
>(&compareFunc
));
621 SetSortArrowA(&m_RepoList
, m_currSortCol
, !m_currSortDesc
);
626 void CRepositoryBrowser::UpdateInfoLabel()
629 POSITION pos
= m_RepoList
.GetFirstSelectedItemPosition();
632 if (m_RepoList
.GetSelectedCount() > 1)
634 temp
.FormatMessage(IDS_REPOBROWSE_INFOMULTI
, m_RepoList
.GetSelectedCount());
638 int index
= m_RepoList
.GetNextSelectedItem(pos
);
639 auto item
= GetListEntry(index
);
640 if (item
->m_bSubmodule
)
641 temp
.FormatMessage(IDS_REPOBROWSE_INFOEXT
, static_cast<LPCWSTR
>(m_RepoList
.GetItemText(index
, eCol_Name
)), static_cast<LPCWSTR
>(item
->m_hash
.ToString()));
642 else if (item
->m_bFolder
)
643 temp
= m_RepoList
.GetItemText(index
, eCol_Name
);
645 temp
.FormatMessage(IDS_REPOBROWSE_INFOFILE
, static_cast<LPCWSTR
>(m_RepoList
.GetItemText(index
, eCol_Name
)), static_cast<LPCWSTR
>(m_RepoList
.GetItemText(index
, eCol_FileSize
)));
650 HTREEITEM hTreeItem
= m_RepoTree
.GetSelectedItem();
651 if (hTreeItem
!= nullptr)
653 auto pTree
= GetTreeEntry(hTreeItem
);
654 if (pTree
!= nullptr)
656 size_t files
= 0, submodules
= 0;
657 for (auto itShadowTree
= pTree
->m_ShadowTree
.cbegin(); itShadowTree
!= pTree
->m_ShadowTree
.cend(); ++itShadowTree
)
659 if (!(*itShadowTree
).second
.m_bFolder
&& !(*itShadowTree
).second
.m_bSubmodule
)
661 if ((*itShadowTree
).second
.m_bSubmodule
)
664 temp
.FormatMessage(IDS_REPOBROWSE_INFO
, static_cast<LPCWSTR
>(pTree
->m_sName
), files
, submodules
, pTree
->m_ShadowTree
.size() - files
- submodules
, pTree
->m_ShadowTree
.size());
668 SetDlgItemText(IDC_INFOLABEL
, temp
);
671 void CRepositoryBrowser::OnLvnItemchangedRepolist(NMHDR
* /* pNMHDR */, LRESULT
*pResult
)
677 void CRepositoryBrowser::OnContextMenu(CWnd
* pWndFrom
, CPoint point
)
679 if (pWndFrom
== &m_RepoList
)
681 CRect headerPosition
;
682 m_RepoList
.GetHeaderCtrl()->GetWindowRect(headerPosition
);
683 if (!headerPosition
.PtInRect(point
))
684 OnContextMenu_RepoList(point
);
686 else if (pWndFrom
== &m_RepoTree
)
687 OnContextMenu_RepoTree(point
);
690 void CRepositoryBrowser::OnContextMenu_RepoTree(CPoint point
)
692 CPoint clientPoint
= point
;
693 m_RepoTree
.ScreenToClient(&clientPoint
);
695 HTREEITEM hTreeItem
= m_RepoTree
.HitTest(clientPoint
);
699 TShadowFilesTreeList selectedLeafs
;
700 selectedLeafs
.push_back(GetTreeEntry(hTreeItem
));
702 ShowContextMenu(point
, selectedLeafs
, ONLY_FOLDERS
);
705 void CRepositoryBrowser::OnContextMenu_RepoList(CPoint point
)
707 TShadowFilesTreeList selectedLeafs
;
708 selectedLeafs
.reserve(m_RepoList
.GetSelectedCount());
710 bool folderSelected
= false;
711 bool filesSelected
= false;
712 bool submodulesSelected
= false;
714 POSITION pos
= m_RepoList
.GetFirstSelectedItemPosition();
717 auto item
= GetListEntry(m_RepoList
.GetNextSelectedItem(pos
));
718 if (item
->m_bSubmodule
)
719 submodulesSelected
= true;
721 folderSelected
= true;
723 filesSelected
= true;
724 selectedLeafs
.push_back(item
);
727 eSelectionType selType
= ONLY_FILES
;
728 if (folderSelected
&& filesSelected
)
729 selType
= MIXED_FOLDERS_FILES
;
730 else if (folderSelected
)
731 selType
= ONLY_FOLDERS
;
732 else if (submodulesSelected
)
733 selType
= ONLY_FILESSUBMODULES
;
734 ShowContextMenu(point
, selectedLeafs
, selType
);
737 void CRepositoryBrowser::ShowContextMenu(CPoint point
, TShadowFilesTreeList
&selectedLeafs
, eSelectionType selType
)
740 popupMenu
.CreatePopupMenu();
742 bool bAddSeparator
= false;
744 if (selectedLeafs
.size() == 1)
746 popupMenu
.AppendMenuIcon(eCmd_Open
, IDS_REPOBROWSE_OPEN
, IDI_OPEN
);
747 popupMenu
.SetDefaultItem(eCmd_Open
, FALSE
);
748 if (selType
== ONLY_FILES
|| selType
== ONLY_FILESSUBMODULES
)
750 popupMenu
.AppendMenuIcon(eCmd_OpenWith
, IDS_LOG_POPUP_OPENWITH
, IDI_OPEN
);
751 popupMenu
.AppendMenuIcon(eCmd_OpenWithAlternativeEditor
, IDS_LOG_POPUP_VIEWREV
, IDI_NOTEPAD
);
754 popupMenu
.AppendMenu(MF_SEPARATOR
);
756 if (m_bHasWC
&& (selType
== ONLY_FILES
|| selType
== ONLY_FILESSUBMODULES
))
758 popupMenu
.AppendMenuIcon(eCmd_CompareWC
, IDS_LOG_POPUP_COMPARE
, IDI_DIFF
);
759 bAddSeparator
= true;
763 popupMenu
.AppendMenu(MF_SEPARATOR
);
764 bAddSeparator
= false;
767 temp
.LoadString(IDS_MENULOG
);
768 popupMenu
.AppendMenuIcon(eCmd_ViewLog
, temp
, IDI_LOG
);
769 if (selectedLeafs
[0]->m_bSubmodule
)
771 temp
.LoadString(IDS_LOG_SUBMODULE
);
772 popupMenu
.AppendMenuIcon(eCmd_ViewLogSubmodule
, temp
, IDI_LOG
);
775 if (selType
== ONLY_FILES
)
778 popupMenu
.AppendMenuIcon(eCmd_Blame
, IDS_LOG_POPUP_BLAME
, IDI_BLAME
);
780 popupMenu
.AppendMenu(MF_SEPARATOR
);
781 temp
.LoadString(IDS_LOG_POPUP_SAVE
);
782 popupMenu
.AppendMenuIcon(eCmd_SaveAs
, temp
, IDI_SAVEAS
);
785 bAddSeparator
= true;
788 if (!selectedLeafs
.empty() && selType
== ONLY_FILES
&& m_bHasWC
)
790 popupMenu
.AppendMenuIcon(eCmd_Revert
, IDS_LOG_POPUP_REVERTTOREV
, IDI_REVERT
);
791 bAddSeparator
= true;
795 popupMenu
.AppendMenu(MF_SEPARATOR
);
797 if (selectedLeafs
.size() == 1 && selType
== ONLY_FILES
)
799 popupMenu
.AppendMenuIcon(eCmd_PrepareDiff
, IDS_PREPAREDIFF
, IDI_DIFF
);
800 UpdateDiffWithFileFromReg();
801 if (!m_sMarkForDiffFilename
.IsEmpty())
804 if (selectedLeafs
.at(0)->GetFullName() == m_sMarkForDiffFilename
)
805 diffWith
= m_sMarkForDiffVersion
.ToString();
808 PathCompactPathEx(CStrBuf(diffWith
, 2 * GIT_HASH_SIZE
), m_sMarkForDiffFilename
, 2 * GIT_HASH_SIZE
, 0);
809 if (!m_sMarkForDiffVersion
.IsEmpty() || PathIsRelative(m_sMarkForDiffFilename
))
810 diffWith
+= L
':' + m_sMarkForDiffVersion
.ToString(g_Git
.GetShortHASHLength());
813 menuEntry
.Format(IDS_MENUDIFFNOW
, static_cast<LPCWSTR
>(diffWith
));
814 popupMenu
.AppendMenuIcon(eCmd_PrepareDiff_Compare
, menuEntry
, IDI_DIFF
);
816 popupMenu
.AppendMenu(MF_SEPARATOR
);
819 if (!selectedLeafs
.empty())
821 popupMenu
.AppendMenuIcon(eCmd_CopyPath
, IDS_STATUSLIST_CONTEXT_COPY
, IDI_COPYCLIP
);
822 popupMenu
.AppendMenuIcon(eCmd_CopyHash
, IDS_COPY_COMMIT_HASH
, IDI_COPYCLIP
);
825 const eCmd cmd
= static_cast<eCmd
>(popupMenu
.TrackPopupMenuEx(TPM_LEFTALIGN
| TPM_RETURNCMD
, point
.x
, point
.y
, this, nullptr));
829 case eCmd_ViewLogSubmodule
:
832 sCmd
.Format(L
"/command:log /path:\"%s\"", static_cast<LPCWSTR
>(g_Git
.CombinePath(selectedLeafs
.at(0)->GetFullName())));
833 if (cmd
== eCmd_ViewLog
&& selectedLeafs
.at(0)->m_bSubmodule
)
834 sCmd
+= L
" /submodule";
835 if (cmd
== eCmd_ViewLog
)
836 sCmd
+= L
" /endrev:" + m_sRevision
;
837 CAppUtils::RunTortoiseGitProc(sCmd
);
842 CAppUtils::LaunchTortoiseBlame(g_Git
.CombinePath(selectedLeafs
.at(0)->GetFullName()), m_sRevision
);
846 if (!selectedLeafs
.at(0)->m_bSubmodule
&& selectedLeafs
.at(0)->m_bFolder
)
848 FillListCtrlForTreeNode(selectedLeafs
.at(0)->m_hTree
);
849 m_RepoTree
.SelectItem(selectedLeafs
.at(0)->m_hTree
);
852 OpenFile(selectedLeafs
.at(0)->GetFullName(), OPEN
, selectedLeafs
.at(0)->m_bSubmodule
, selectedLeafs
.at(0)->m_hash
);
855 OpenFile(selectedLeafs
.at(0)->GetFullName(), OPEN_WITH
, selectedLeafs
.at(0)->m_bSubmodule
, selectedLeafs
.at(0)->m_hash
);
857 case eCmd_OpenWithAlternativeEditor
:
858 OpenFile(selectedLeafs
.at(0)->GetFullName(), ALTERNATIVEEDITOR
, selectedLeafs
.at(0)->m_bSubmodule
, selectedLeafs
.at(0)->m_hash
);
862 CTGitPath
file(selectedLeafs
.at(0)->GetFullName());
863 CGitDiff::Diff(GetSafeHwnd(), &file
, &file
, GIT_REV_ZERO
, m_sRevision
);
869 for (auto itShadowTree
= selectedLeafs
.cbegin(); itShadowTree
!= selectedLeafs
.cend(); ++itShadowTree
)
871 if (RevertItemToVersion((*itShadowTree
)->GetFullName()))
877 msg
.FormatMessage(IDS_STATUSLIST_FILESREVERTED
, count
, static_cast<LPCWSTR
>(m_sRevision
));
878 MessageBox(msg
, L
"TortoiseGit", MB_OK
);
882 FileSaveAs(selectedLeafs
.at(0)->GetFullName());
887 for (auto itShadowTree
= selectedLeafs
.cbegin(); itShadowTree
!= selectedLeafs
.cend(); ++itShadowTree
)
889 sClipboard
+= (*itShadowTree
)->m_sName
+ L
"\r\n";
891 CStringUtils::WriteAsciiStringToClipboard(sClipboard
);
896 CopyHashToClipboard(selectedLeafs
);
899 case eCmd_PrepareDiff
:
900 m_sMarkForDiffFilename
= selectedLeafs
.at(0)->GetFullName();
901 if (g_Git
.GetHash(m_sMarkForDiffVersion
, m_sRevision
+ L
"^{}")) // add ^{} in order to dereference signed tags
903 m_sMarkForDiffFilename
.Empty();
904 MessageBox(g_Git
.GetGitLastErr(L
"Could not get SHA-1 for \"" + m_sRevision
+ L
"^{}\"."), L
"TortoiseGit", MB_ICONERROR
);
907 case eCmd_PrepareDiff_Compare
:
909 if (auto reg
= CRegString(L
"Software\\TortoiseGit\\DiffLater", L
""); m_sMarkForDiffFilename
== reg
)
911 CTGitPath
savedFile(m_sMarkForDiffFilename
);
912 CTGitPath
selectedFile(selectedLeafs
.at(0)->GetFullName());
913 CGitHash currentHash
;
914 if (g_Git
.GetHash(currentHash
, m_sRevision
+ L
"^{}")) // add ^{} in order to dereference signed tags
916 MessageBox(g_Git
.GetGitLastErr(L
"Could not get SHA-1 for \"" + m_sRevision
+ L
"^{}\"."), L
"TortoiseGit", MB_ICONERROR
);
919 CGitDiff::Diff(GetSafeHwnd(), &selectedFile
, &savedFile
, currentHash
.ToString(), m_sMarkForDiffVersion
.ToString());
925 BOOL
CRepositoryBrowser::PreTranslateMessage(MSG
* pMsg
)
927 if (pMsg
->message
== WM_KEYDOWN
)
929 switch (pMsg
->wParam
)
937 if (pMsg
->hwnd
== m_RepoList
.m_hWnd
&& (GetAsyncKeyState(VK_CONTROL
) & 0x8000))
939 // select all entries
940 for (int i
= 0; i
< m_RepoList
.GetItemCount(); ++i
)
941 m_RepoList
.SetItemState(i
, LVIS_SELECTED
, LVIS_SELECTED
);
948 return CResizableStandAloneDialog::PreTranslateMessage(pMsg
);
951 void CRepositoryBrowser::OnLvnColumnclickRepoList(NMHDR
*pNMHDR
, LRESULT
*pResult
)
953 LPNMLISTVIEW pNMLV
= reinterpret_cast<LPNMLISTVIEW
>(pNMHDR
);
957 if (m_currSortCol
== pNMLV
->iSubItem
)
958 m_currSortDesc
= !m_currSortDesc
;
961 m_currSortCol
= pNMLV
->iSubItem
;
962 m_currSortDesc
= false;
965 CRepoListCompareFunc
compareFunc(&m_RepoList
, m_currSortCol
, m_currSortDesc
);
966 m_RepoList
.SortItemsEx(&CRepoListCompareFunc::StaticCompare
, reinterpret_cast<DWORD_PTR
>(&compareFunc
));
968 SetSortArrowA(&m_RepoList
, m_currSortCol
, !m_currSortDesc
);
971 void CRepositoryBrowser::OnBnClickedButtonRevision()
973 // use the git log to allow selection of a version
975 if (dlg
.IsThreadRunning())
977 CMessageBox::Show(GetSafeHwnd(), IDS_PROC_LOG_ONLYONCE
, IDS_APPNAME
, MB_ICONEXCLAMATION
);
980 dlg
.SetParams(CTGitPath(), CTGitPath(), m_sRevision
, m_sRevision
, 0);
981 // tell the dialog to use mode for selecting revisions
983 dlg
.ShowWorkingTreeChanges(false);
984 // only one revision must be selected however
985 dlg
.SingleSelection(true);
986 if (dlg
.DoModal() == IDOK
&& !dlg
.GetSelectedHash().empty())
988 m_sRevision
= dlg
.GetSelectedHash().at(0).ToString();
991 BringWindowToTop(); /* cf. issue #3493 */
994 void CRepositoryBrowser::SaveDividerPosition()
997 GetDlgItem(IDC_REPOTREE
)->GetClientRect(&rc
);
998 CRegDWORD
xPos(L
"Software\\TortoiseGit\\TortoiseProc\\ResizableState\\RepobrowserDivider");
999 xPos
= CDPIAware::Instance().UnscaleX(GetSafeHwnd(), rc
.right
- rc
.left
);
1002 void CRepositoryBrowser::HandleDividerMove(CPoint point
, bool bDraw
)
1004 RECT rect
, tree
, list
, treelist
, treelistclient
;
1006 // create an union of the tree and list control rectangle
1007 GetDlgItem(IDC_REPOLIST
)->GetWindowRect(&list
);
1008 GetDlgItem(IDC_REPOTREE
)->GetWindowRect(&tree
);
1009 UnionRect(&treelist
, &tree
, &list
);
1010 treelistclient
= treelist
;
1011 ScreenToClient(&treelistclient
);
1013 ClientToScreen(&point
);
1014 GetClientRect(&rect
);
1015 ClientToScreen(&rect
);
1017 auto minWidth
= CDPIAware::Instance().ScaleX(GetSafeHwnd(), REPOBROWSER_CTRL_MIN_WIDTH
);
1018 CPoint point2
= point
;
1019 if (point2
.x
< treelist
.left
+ minWidth
)
1020 point2
.x
= treelist
.left
+ minWidth
;
1021 if (point2
.x
> treelist
.right
- minWidth
)
1022 point2
.x
= treelist
.right
- minWidth
;
1024 point
.x
-= rect
.left
;
1025 point
.y
-= treelist
.top
;
1027 OffsetRect(&treelist
, -treelist
.left
, -treelist
.top
);
1029 if (point
.x
< treelist
.left
+ minWidth
)
1030 point
.x
= treelist
.left
+ minWidth
;
1031 if (point
.x
> treelist
.right
- minWidth
)
1032 point
.x
= treelist
.right
- minWidth
;
1034 const auto divWidth
= CDPIAware::Instance().ScaleX(GetSafeHwnd(), 2);
1038 CDC
* pDC
= GetDC();
1039 DrawXorBar(pDC
, m_oldSliderXPos
- divWidth
, treelistclient
.top
, 2 * divWidth
, treelistclient
.bottom
- treelistclient
.top
- CDPIAware::Instance().ScaleY(GetSafeHwnd(), 2));
1043 m_oldSliderXPos
= point
.x
;
1045 //position the child controls
1046 GetDlgItem(IDC_REPOTREE
)->GetWindowRect(&treelist
);
1047 treelist
.right
= point2
.x
- divWidth
;
1048 ScreenToClient(&treelist
);
1049 RemoveAnchor(IDC_REPOTREE
);
1050 GetDlgItem(IDC_REPOTREE
)->MoveWindow(&treelist
);
1051 GetDlgItem(IDC_REPOLIST
)->GetWindowRect(&treelist
);
1052 treelist
.left
= point2
.x
+ divWidth
;
1053 ScreenToClient(&treelist
);
1054 RemoveAnchor(IDC_REPOLIST
);
1055 GetDlgItem(IDC_REPOLIST
)->MoveWindow(&treelist
);
1057 AddAnchor(IDC_REPOTREE
, TOP_LEFT
, BOTTOM_LEFT
);
1058 AddAnchor(IDC_REPOLIST
, TOP_LEFT
, BOTTOM_RIGHT
);
1061 void CRepositoryBrowser::OnMouseMove(UINT nFlags
, CPoint point
)
1063 if (bDragMode
== FALSE
)
1066 RECT rect
, tree
, list
, treelist
, treelistclient
;
1067 // create an union of the tree and list control rectangle
1068 GetDlgItem(IDC_REPOLIST
)->GetWindowRect(&list
);
1069 GetDlgItem(IDC_REPOTREE
)->GetWindowRect(&tree
);
1070 UnionRect(&treelist
, &tree
, &list
);
1071 treelistclient
= treelist
;
1072 ScreenToClient(&treelistclient
);
1074 //convert the mouse coordinates relative to the top-left of
1076 ClientToScreen(&point
);
1077 GetClientRect(&rect
);
1078 ClientToScreen(&rect
);
1079 point
.x
-= rect
.left
;
1080 point
.y
-= treelist
.top
;
1082 //same for the window coordinates - make them relative to 0,0
1083 OffsetRect(&treelist
, -treelist
.left
, -treelist
.top
);
1085 auto minWidth
= CDPIAware::Instance().ScaleX(GetSafeHwnd(), REPOBROWSER_CTRL_MIN_WIDTH
);
1086 if (point
.x
< treelist
.left
+ minWidth
)
1087 point
.x
= treelist
.left
+ minWidth
;
1088 if (point
.x
> treelist
.right
- minWidth
)
1089 point
.x
= treelist
.right
- minWidth
;
1091 if ((nFlags
& MK_LBUTTON
) && (point
.x
!= m_oldSliderXPos
))
1093 CDC
* pDC
= GetDC();
1097 const auto divWidth
= CDPIAware::Instance().ScaleX(GetSafeHwnd(), 2);
1098 const auto divHeight
= CDPIAware::Instance().ScaleY(GetSafeHwnd(), 2);
1099 DrawXorBar(pDC
, m_oldSliderXPos
- divWidth
, treelistclient
.top
, 2 * divWidth
, treelistclient
.bottom
- treelistclient
.top
- divHeight
);
1100 DrawXorBar(pDC
, point
.x
- divWidth
, treelistclient
.top
, 2 * divWidth
, treelistclient
.bottom
- treelistclient
.top
- divHeight
);
1105 m_oldSliderXPos
= point
.x
;
1108 CStandAloneDialogTmpl
<CResizableDialog
>::OnMouseMove(nFlags
, point
);
1111 void CRepositoryBrowser::OnLButtonDown(UINT nFlags
, CPoint point
)
1113 RECT rect
, tree
, list
, treelist
, treelistclient
;
1115 // create an union of the tree and list control rectangle
1116 GetDlgItem(IDC_REPOLIST
)->GetWindowRect(&list
);
1117 GetDlgItem(IDC_REPOTREE
)->GetWindowRect(&tree
);
1118 UnionRect(&treelist
, &tree
, &list
);
1119 treelistclient
= treelist
;
1120 ScreenToClient(&treelistclient
);
1122 //convert the mouse coordinates relative to the top-left of
1124 ClientToScreen(&point
);
1125 GetClientRect(&rect
);
1126 ClientToScreen(&rect
);
1127 point
.x
-= rect
.left
;
1128 point
.y
-= treelist
.top
;
1130 //same for the window coordinates - make them relative to 0,0
1131 OffsetRect(&treelist
, -treelist
.left
, -treelist
.top
);
1133 const auto minWidth
= CDPIAware::Instance().ScaleX(GetSafeHwnd(), REPOBROWSER_CTRL_MIN_WIDTH
);
1135 if (point
.x
< treelist
.left
+ minWidth
)
1136 return CStandAloneDialogTmpl
< CResizableDialog
>::OnLButtonDown(nFlags
, point
);
1137 if (point
.x
> treelist
.right
- CDPIAware::Instance().ScaleX(GetSafeHwnd(), 3))
1138 return CStandAloneDialogTmpl
< CResizableDialog
>::OnLButtonDown(nFlags
, point
);
1139 if (point
.x
> treelist
.right
- minWidth
)
1140 point
.x
= treelist
.right
- minWidth
;
1142 const auto divHeight
= CDPIAware::Instance().ScaleY(GetSafeHwnd(), 3);
1143 if ((point
.y
< treelist
.top
+ divHeight
) || (point
.y
> treelist
.bottom
- divHeight
))
1144 return CStandAloneDialogTmpl
<CResizableDialog
>::OnLButtonDown(nFlags
, point
);
1150 const auto divWidth
= CDPIAware::Instance().ScaleX(GetSafeHwnd(), 2);
1151 CDC
* pDC
= GetDC();
1152 DrawXorBar(pDC
, point
.x
- divWidth
, treelistclient
.top
, 2 * divWidth
, treelistclient
.bottom
- treelistclient
.top
- CDPIAware::Instance().ScaleY(GetSafeHwnd(), 2));
1155 m_oldSliderXPos
= point
.x
;
1157 CStandAloneDialogTmpl
<CResizableDialog
>::OnLButtonDown(nFlags
, point
);
1160 void CRepositoryBrowser::OnLButtonUp(UINT nFlags
, CPoint point
)
1162 if (bDragMode
== FALSE
)
1165 HandleDividerMove(point
, true);
1170 CStandAloneDialogTmpl
<CResizableDialog
>::OnLButtonUp(nFlags
, point
);
1173 void CRepositoryBrowser::OnCaptureChanged(CWnd
*pWnd
)
1177 __super::OnCaptureChanged(pWnd
);
1180 void CRepositoryBrowser::DrawXorBar(CDC
* pDC
, int x1
, int y1
, int width
, int height
)
1182 static WORD _dotPatternBmp
[8] =
1184 0x0055, 0x00aa, 0x0055, 0x00aa,
1185 0x0055, 0x00aa, 0x0055, 0x00aa
1189 HBRUSH hbr
, hbrushOld
;
1191 hbm
= CreateBitmap(8, 8, 1, 1, _dotPatternBmp
);
1192 hbr
= CreatePatternBrush(hbm
);
1194 pDC
->SetBrushOrg(x1
, y1
);
1195 hbrushOld
= static_cast<HBRUSH
>(pDC
->SelectObject(hbr
));
1197 PatBlt(pDC
->GetSafeHdc(), x1
, y1
, width
, height
, PATINVERT
);
1199 pDC
->SelectObject(hbrushOld
);
1205 BOOL
CRepositoryBrowser::OnSetCursor(CWnd
* pWnd
, UINT nHitTest
, UINT message
)
1211 GetClientRect(&rect
);
1213 ScreenToClient(&pt
);
1214 if (PtInRect(&rect
, pt
))
1216 ClientToScreen(&pt
);
1217 // are we right of the tree control?
1218 GetDlgItem(IDC_REPOTREE
)->GetWindowRect(&rect
);
1219 const auto divHeight
= CDPIAware::Instance().ScaleY(GetSafeHwnd(), 3);
1220 if ((pt
.x
> rect
.right
) && (pt
.y
>= rect
.top
+ divHeight
) && (pt
.y
<= rect
.bottom
- divHeight
))
1222 // but left of the list control?
1223 GetDlgItem(IDC_REPOLIST
)->GetWindowRect(&rect
);
1224 if (pt
.x
< rect
.left
)
1226 HCURSOR hCur
= LoadCursor(nullptr, IDC_SIZEWE
);
1233 return CStandAloneDialogTmpl
<CResizableDialog
>::OnSetCursor(pWnd
, nHitTest
, message
);
1236 void CRepositoryBrowser::FileSaveAs(const CString path
)
1238 CTGitPath
gitPath(path
);
1241 if (g_Git
.GetHash(hash
, m_sRevision
+ L
"^{}")) // add ^{} in order to dereference signed tags
1243 MessageBox(g_Git
.GetGitLastErr(L
"Could not get hash of \"" + m_sRevision
+ L
"^{}\"."), L
"TortoiseGit", MB_ICONERROR
);
1248 filename
.Format(L
"%s\\%s-%s%s", static_cast<LPCWSTR
>(g_Git
.CombinePath(gitPath
.GetContainingDirectory())), static_cast<LPCWSTR
>(gitPath
.GetBaseFilename()), static_cast<LPCWSTR
>(hash
.ToString(g_Git
.GetShortHASHLength())), static_cast<LPCWSTR
>(gitPath
.GetFileExtension()));
1249 if (!CAppUtils::FileOpenSave(filename
, nullptr, 0, 0, false, GetSafeHwnd()))
1252 if (g_Git
.GetOneFile(m_sRevision
, gitPath
, filename
))
1255 out
.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED
, static_cast<LPCWSTR
>(gitPath
.GetGitPathString()), static_cast<LPCWSTR
>(m_sRevision
), static_cast<LPCWSTR
>(filename
));
1256 MessageBox(g_Git
.GetGitLastErr(out
, CGit::GIT_CMD_GETONEFILE
), L
"TortoiseGit", MB_ICONERROR
);
1260 void CRepositoryBrowser::OpenFile(const CString path
, eOpenType mode
, bool isSubmodule
, const CGitHash
& itemHash
)
1262 CTGitPath
gitPath(path
);
1265 if (g_Git
.GetHash(hash
, m_sRevision
+ L
"^{}")) // add ^{} in order to dereference signed tags
1267 MessageBox(g_Git
.GetGitLastErr(L
"Could not get hash of \"" + m_sRevision
+ L
"^{}\"."), L
"TortoiseGit", MB_ICONERROR
);
1271 CString file
= CTempFiles::Instance().GetTempFilePath(false, gitPath
, hash
).GetWinPathString();
1274 if (mode
== OPEN
&& !GitAdminDir::IsBareRepo(g_Git
.m_CurrentDir
))
1276 CTGitPath subPath
= CTGitPath(g_Git
.m_CurrentDir
);
1277 subPath
.AppendPathString(gitPath
.GetWinPathString());
1278 CAutoRepository
repo(subPath
.GetGitPathString());
1280 if (!repo
|| git_commit_lookup(commit
.GetPointer(), repo
, itemHash
))
1283 out
.FormatMessage(IDS_REPOBROWSEASKSUBMODULEUPDATE
, static_cast<LPCWSTR
>(itemHash
.ToString()), static_cast<LPCWSTR
>(gitPath
.GetGitPathString()));
1284 if (MessageBox(out
, L
"TortoiseGit", MB_YESNO
| MB_ICONQUESTION
) != IDYES
)
1288 sCmd
.Format(L
"/command:subupdate /bkpath:\"%s\" /selectedpath:\"%s\"", static_cast<LPCWSTR
>(g_Git
.m_CurrentDir
), static_cast<LPCWSTR
>(gitPath
.GetGitPathString()));
1289 CAppUtils::RunTortoiseGitProc(sCmd
);
1294 cmd
.Format(L
"/command:repobrowser /path:\"%s\" /rev:%s", static_cast<LPCWSTR
>(g_Git
.CombinePath(path
)), static_cast<LPCWSTR
>(itemHash
.ToString()));
1295 CAppUtils::RunTortoiseGitProc(cmd
);
1300 CFile
submoduleCommit(file
, CFile::modeCreate
| CFile::modeWrite
);
1301 CStringA commitInfo
= "Subproject commit " + CStringA(itemHash
.ToString());
1302 submoduleCommit
.Write(commitInfo
, commitInfo
.GetLength());
1304 else if (g_Git
.GetOneFile(m_sRevision
, gitPath
, file
))
1307 out
.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED
, static_cast<LPCWSTR
>(gitPath
.GetGitPathString()), static_cast<LPCWSTR
>(m_sRevision
), static_cast<LPCWSTR
>(file
));
1308 MessageBox(g_Git
.GetGitLastErr(out
, CGit::GIT_CMD_GETONEFILE
), L
"TortoiseGit", MB_ICONERROR
);
1312 if (mode
== ALTERNATIVEEDITOR
)
1314 CAppUtils::LaunchAlternativeEditor(file
);
1317 else if (mode
== OPEN
)
1319 CAppUtils::ShellOpen(file
);
1323 CAppUtils::ShowOpenWithDialog(file
);
1325 bool CRepositoryBrowser::RevertItemToVersion(const CString
&path
)
1327 CString endOfOptions
;
1328 if (CGit::ms_LastMsysGitVersion
>= ConvertVersionToInt(2, 43, 1))
1329 endOfOptions
= L
" --end-of-options";
1331 cmd
.Format(L
"git.exe checkout%s %s -- \"%s\"", static_cast<LPCWSTR
>(endOfOptions
), static_cast<LPCWSTR
>(m_sRevision
), static_cast<LPCWSTR
>(path
));
1332 if (g_Git
.Run(cmd
, &out
, CP_UTF8
))
1334 if (MessageBox(out
, L
"TortoiseGit", MB_ICONEXCLAMATION
| MB_OKCANCEL
) == IDCANCEL
)
1341 void CRepositoryBrowser::CopyHashToClipboard(TShadowFilesTreeList
&selectedLeafs
)
1343 if (!selectedLeafs
.empty())
1347 for (size_t i
= 0; i
< selectedLeafs
.size(); ++i
)
1350 sClipdata
+= L
"\r\n";
1351 sClipdata
+= selectedLeafs
[i
]->m_hash
.ToString();
1354 CStringUtils::WriteAsciiStringToClipboard(sClipdata
, GetSafeHwnd());
1358 void CRepositoryBrowser::OnLvnBegindragRepolist(NMHDR
* pNMHDR
, LRESULT
* pResult
)
1362 // get selected paths
1363 POSITION pos
= m_RepoList
.GetFirstSelectedItemPosition();
1367 HTREEITEM hTreeItem
= m_RepoTree
.GetSelectedItem();
1373 auto pTree
= GetTreeEntry(hTreeItem
);
1377 CTGitPathList toExport
;
1379 while ((index
= m_RepoList
.GetNextSelectedItem(pos
)) >= 0)
1381 auto item
= GetListEntry(index
);
1382 if (item
->m_bFolder
)
1384 RecursivelyAdd(toExport
, item
);
1389 path
.SetFromGit(item
->GetFullName(), item
->m_bSubmodule
);
1390 toExport
.AddPath(path
);
1393 LPNMLISTVIEW pNMLV
= reinterpret_cast<LPNMLISTVIEW
>(pNMHDR
);
1394 BeginDrag(m_RepoList
, toExport
, pTree
->GetFullName(), pNMLV
->ptAction
);
1397 void CRepositoryBrowser::RecursivelyAdd(CTGitPathList
& toExport
, CShadowFilesTree
* pTree
)
1399 if (!pTree
->m_bLoaded
)
1400 ReadTree(pTree
, pTree
->GetFullName(), true);
1402 for (auto itShadowTree
= pTree
->m_ShadowTree
.begin(); itShadowTree
!= pTree
->m_ShadowTree
.end(); ++itShadowTree
)
1404 if ((*itShadowTree
).second
.m_bFolder
)
1406 RecursivelyAdd(toExport
, &(*itShadowTree
).second
);
1410 path
.SetFromGit((*itShadowTree
).second
.GetFullName(), (*itShadowTree
).second
.m_bSubmodule
);
1411 toExport
.AddPath(path
);
1415 void CRepositoryBrowser::OnTvnBegindragRepotree(NMHDR
* pNMHDR
, LRESULT
* pResult
)
1419 LPNMTREEVIEW pNMTreeView
= reinterpret_cast<LPNMTREEVIEW
>(pNMHDR
);
1421 auto pTree
= GetTreeEntry(pNMTreeView
->itemNew
.hItem
);
1425 CTGitPathList toExport
;
1426 RecursivelyAdd(toExport
, pTree
);
1428 BeginDrag(m_RepoTree
, toExport
, pTree
->m_pParent
? pTree
->m_pParent
->GetFullName() : CString(), pNMTreeView
->ptDrag
);
1431 void CRepositoryBrowser::BeginDrag(const CWnd
& window
, CTGitPathList
& files
, const CString
& root
, POINT
& point
)
1434 if (g_Git
.GetHash(hash
, m_sRevision
+ L
"^{}")) // add ^{} in order to dereference signed tags
1436 MessageBox(g_Git
.GetGitLastErr(L
"Could not get hash of \"" + m_sRevision
+ L
"^{}\"."), L
"TortoiseGit", MB_ICONERROR
);
1440 // build copy source / content
1441 auto pdsrc
= std::make_unique
<CIDropSource
>();
1447 GitDataObject
* pdobj
= new GitDataObject(files
, hash
, root
.GetLength());
1451 pdobj
->SetAsyncMode(TRUE
);
1452 CDragSourceHelper dragsrchelper
;
1453 dragsrchelper
.InitializeFromWindow(window
.GetSafeHwnd(), point
, pdobj
);
1454 pdsrc
->m_pIDataObj
= pdobj
;
1455 pdsrc
->m_pIDataObj
->AddRef();
1457 // Initiate the Drag & Drop
1459 ::DoDragDrop(pdobj
, pdsrc
.get(), DROPEFFECT_MOVE
| DROPEFFECT_COPY
, &dwEffect
);
1466 CShadowFilesTree
* CRepositoryBrowser::GetListEntry(int index
)
1468 auto entry
= reinterpret_cast<CShadowFilesTree
*>(m_RepoList
.GetItemData(index
));
1473 CShadowFilesTree
* CRepositoryBrowser::GetTreeEntry(HTREEITEM treeItem
)
1475 auto entry
= reinterpret_cast<CShadowFilesTree
*>(m_RepoTree
.GetItemData(treeItem
));