1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2015-2017 - TortoiseGit
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License
7 // as published by the Free Software Foundation; either version 2
8 // of the License, or (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software Foundation,
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 // GitRefCompareList.cpp : implementation file
24 #include "GitTagCompareList.h"
25 #include "GitRefCompareList.h"
27 #include "UnicodeUtils.h"
30 #include "..\TortoiseShell\resource.h"
31 #include "LoglistCommonResource.h"
32 #include "SysProgressDlg.h"
33 #include "ProgressDlg.h"
35 IMPLEMENT_DYNAMIC(CGitTagCompareList
, CHintCtrl
<CListCtrl
>)
37 BEGIN_MESSAGE_MAP(CGitTagCompareList
, CHintCtrl
<CListCtrl
>)
39 ON_NOTIFY(HDN_ITEMCLICKA
, 0, OnHdnItemclick
)
40 ON_NOTIFY(HDN_ITEMCLICKW
, 0, OnHdnItemclick
)
43 BOOL
CGitTagCompareList::m_bSortLogical
= FALSE
;
51 IDGITRCL_DELETEREMOTE
,
58 IDGITRCLH_HIDEUNCHANGED
= 1,
61 inline static bool SortPredicate(bool sortLogical
, const CString
& e1
, const CString
& e2
)
64 return StrCmpLogicalW(e1
, e2
) < 0;
65 return e1
.Compare(e2
) < 0;
68 CGitTagCompareList::CGitTagCompareList()
69 : CHintCtrl
<CListCtrl
>()
79 m_bSortLogical
= !CRegDWORD(L
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\NoStrCmpLogical", 0, false, HKEY_CURRENT_USER
);
81 m_bSortLogical
= !CRegDWORD(L
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\NoStrCmpLogical", 0, false, HKEY_LOCAL_MACHINE
);
82 m_bHideEqual
= CRegDWORD(L
"Software\\TortoiseGit\\TagCompareHideEqual", FALSE
);
85 void CGitTagCompareList::Init()
88 colTag
= InsertColumn(index
++, CString(MAKEINTRESOURCE(IDS_PROC_TAG
)));
89 colDiff
= InsertColumn(index
++, CString(MAKEINTRESOURCE(IDS_STATUSLIST_COLSTATUS
)));
90 colMyHash
= InsertColumn(index
++, CString(MAKEINTRESOURCE(IDS_TAGCOMPARE_LOCALHASH
)));
91 colMyMessage
= InsertColumn(index
++, CString(MAKEINTRESOURCE(IDS_TAGCOMPARE_LOCALMESSAGE
)));
92 colTheirHash
= InsertColumn(index
++, CString(MAKEINTRESOURCE(IDS_TAGCOMPARE_REMOTEHASH
)));
93 colTheirMessage
= InsertColumn(index
++, CString(MAKEINTRESOURCE(IDS_TAGCOMPARE_REMOTEMESSAGE
)));
95 SetWindowTheme(m_hWnd
, L
"Explorer", nullptr);
97 if (!!CRegDWORD(L
"Software\\TortoiseGit\\SortTagsReversed", 0, false, HKEY_LOCAL_MACHINE
) || !!CRegDWORD(L
"Software\\TortoiseGit\\SortTagsReversed", 0, false, HKEY_CURRENT_USER
))
104 int CGitTagCompareList::Fill(const CString
& remote
, CString
& err
)
111 pleaseWait
.LoadString(IDS_PROGRESSWAIT
);
112 ShowText(pleaseWait
, true);
114 if (!g_Git
.m_IsUseLibGit2
)
116 err
= L
"Only available with libgit2 enabled";
120 REF_VECTOR remoteTags
;
121 if (g_Git
.GetRemoteTags(remote
, remoteTags
))
123 err
= g_Git
.GetGitLastErr(L
"Could not retrieve remote tags.", CGit::GIT_CMD_FETCH
);
127 CAutoRepository
repo(g_Git
.GetGitRepository());
130 err
= CGit::GetLibGit2LastErr(L
"Could not open repository.");
134 MAP_HASH_NAME hashMap
;
135 if (CGit::GetMapHashToFriendName(repo
, hashMap
))
137 err
= CGit::GetLibGit2LastErr(L
"Could not get all refs.");
141 REF_VECTOR localTags
;
142 for (auto it
= hashMap
.cbegin(); it
!= hashMap
.cend(); ++it
)
144 const auto& hash
= it
->first
;
145 const auto& refs
= it
->second
;
146 std::for_each(refs
.cbegin(), refs
.cend(), [&](const auto& ref
)
148 if (CStringUtils::StartsWith(ref
, L
"refs/tags/"))
150 auto tagname
= ref
.Mid((int)wcslen(L
"refs/tags/"));
151 localTags
.emplace_back(TGitRef
{ tagname
, hash
});
152 if (CStringUtils::EndsWith(tagname
, L
"^{}"))
154 tagname
.Replace(L
"^{}", L
"");
155 CAutoObject gitObject
;
156 if (git_revparse_single(gitObject
.GetPointer(), repo
, CUnicodeUtils::GetUTF8(tagname
)))
159 git_oid_cpy((git_oid
*)tagHash
.m_hash
, git_object_id(gitObject
));
160 localTags
.emplace_back(TGitRef
{ tagname
, tagHash
});
165 std::sort(remoteTags
.begin(), remoteTags
.end(), std::bind(SortPredicate
, m_bSortLogical
, std::placeholders::_1
, std::placeholders::_2
));
166 std::sort(localTags
.begin(), localTags
.end(), std::bind(SortPredicate
, m_bSortLogical
, std::placeholders::_1
, std::placeholders::_2
));
168 auto remoteIt
= remoteTags
.cbegin();
169 auto localIt
= localTags
.cbegin();
171 while (remoteIt
!= remoteTags
.cend() && localIt
!= localTags
.cend())
173 if (SortPredicate(m_bSortLogical
, remoteIt
->name
, localIt
->name
))
175 AddEntry(repo
, remoteIt
->name
, nullptr, &remoteIt
->hash
);
180 if (remoteIt
->name
== localIt
->name
)
182 AddEntry(repo
, remoteIt
->name
, &localIt
->hash
, &remoteIt
->hash
);
186 AddEntry(repo
, localIt
->name
, &localIt
->hash
, nullptr);
191 while (remoteIt
!= remoteTags
.cend())
193 AddEntry(repo
, remoteIt
->name
, nullptr, &remoteIt
->hash
);
197 while (localIt
!= localTags
.cend())
199 AddEntry(repo
, localIt
->name
, &localIt
->hash
, nullptr);
205 CHeaderCtrl
* pHeader
= GetHeaderCtrl();
206 HDITEM HeaderItem
= { 0 };
207 HeaderItem
.mask
= HDI_FORMAT
;
208 for (int i
= 0; i
< pHeader
->GetItemCount(); ++i
)
210 pHeader
->GetItem(i
, &HeaderItem
);
211 HeaderItem
.fmt
&= ~(HDF_SORTDOWN
| HDF_SORTUP
);
212 pHeader
->SetItem(i
, &HeaderItem
);
214 if (m_nSortedColumn
>= 0)
216 pHeader
->GetItem(m_nSortedColumn
, &HeaderItem
);
217 HeaderItem
.fmt
|= (m_bAscending
? HDF_SORTUP
: HDF_SORTDOWN
);
218 pHeader
->SetItem(m_nSortedColumn
, &HeaderItem
);
224 void CGitTagCompareList::AddEntry(git_repository
* repo
, const CString
& tag
, const CGitHash
* myHash
, const CGitHash
* theirHash
)
229 entry
.myHash
= *myHash
;
231 entry
.theirHash
= *theirHash
;
233 CAutoCommit oldCommit
;
236 if (!git_commit_lookup(oldCommit
.GetPointer(), repo
, (const git_oid
*)&myHash
->m_hash
))
237 entry
.myMessage
= CGitRefCompareList::GetCommitMessage(oldCommit
);
240 CAutoCommit newCommit
;
243 if (!git_commit_lookup(newCommit
.GetPointer(), repo
, (const git_oid
*)&theirHash
->m_hash
))
244 entry
.theirMessage
= CGitRefCompareList::GetCommitMessage(newCommit
);
247 if (myHash
&& theirHash
)
249 if (*myHash
== *theirHash
)
250 entry
.diffstate
.LoadString(IDS_TAGCOMPARE_SAME
);
252 entry
.diffstate
.LoadString(IDS_TAGCOMPARE_DIFFER
);
255 entry
.diffstate
.LoadString(IDS_TAGCOMPARE_ONLYLOCAL
);
257 entry
.diffstate
.LoadString(IDS_TAGCOMPARE_ONLYREMOTE
);
258 m_TagList
.emplace_back(entry
);
261 void CGitTagCompareList::Show()
265 pleaseWait
.LoadString(IDS_PROGRESSWAIT
);
266 ShowText(pleaseWait
, true);
271 if (m_nSortedColumn
>= 0)
273 auto predicate
= [](bool sortLogical
, int sortColumn
, const TagEntry
& e1
, const TagEntry
& e2
)
278 return SortPredicate(sortLogical
, e1
.name
, e2
.name
);
281 return SortPredicate(false, e1
.diffstate
, e2
.diffstate
);
284 return e1
.myHash
< e2
.myHash
;
287 return SortPredicate(sortLogical
, e1
.myMessage
, e2
.myMessage
);
290 return e1
.theirHash
< e2
.theirHash
;
293 return SortPredicate(sortLogical
, e1
.theirMessage
, e2
.theirMessage
);
300 std::stable_sort(m_TagList
.begin(), m_TagList
.end(), std::bind(predicate
, m_bSortLogical
, m_nSortedColumn
, std::placeholders::_1
, std::placeholders::_2
));
302 std::stable_sort(m_TagList
.begin(), m_TagList
.end(), std::bind(predicate
, m_bSortLogical
, m_nSortedColumn
, std::placeholders::_2
, std::placeholders::_1
));
306 for (const auto& entry
: m_TagList
)
308 if (entry
.myHash
== entry
.theirHash
&& m_bHideEqual
)
311 InsertItem(index
, entry
.name
);
312 SetItemText(index
, colDiff
, entry
.diffstate
);
313 if (!entry
.myHash
.IsEmpty())
314 SetItemText(index
, colMyHash
, entry
.myHash
.ToString().Left(g_Git
.GetShortHASHLength()));
315 SetItemText(index
, colMyMessage
, entry
.myMessage
);
316 if (!entry
.theirHash
.IsEmpty())
317 SetItemText(index
, colTheirHash
, entry
.theirHash
.ToString().Left(g_Git
.GetShortHASHLength()));
318 SetItemText(index
, colTheirMessage
, entry
.theirMessage
);
321 for (int i
= 0; i
< GetHeaderCtrl()->GetItemCount(); ++i
)
322 SetColumnWidth(i
, LVSCW_AUTOSIZE_USEHEADER
);
324 auto pHeader
= GetHeaderCtrl();
325 HDITEM HeaderItem
= { 0 };
326 HeaderItem
.mask
= HDI_FORMAT
;
327 for (int i
= 0; i
< pHeader
->GetItemCount(); ++i
)
329 pHeader
->GetItem(i
, &HeaderItem
);
330 HeaderItem
.fmt
&= ~(HDF_SORTDOWN
| HDF_SORTUP
);
331 pHeader
->SetItem(i
, &HeaderItem
);
333 if (m_nSortedColumn
>= 0)
335 pHeader
->GetItem(m_nSortedColumn
, &HeaderItem
);
336 HeaderItem
.fmt
|= (m_bAscending
? HDF_SORTUP
: HDF_SORTDOWN
);
337 pHeader
->SetItem(m_nSortedColumn
, &HeaderItem
);
344 empty
.LoadString(IDS_COMPAREREV_NODIFF
);
345 ShowText(empty
, true);
351 void CGitTagCompareList::OnHdnItemclick(NMHDR
*pNMHDR
, LRESULT
*pResult
)
353 auto phdr
= reinterpret_cast<LPNMHEADER
>(pNMHDR
);
356 if (m_TagList
.empty())
359 if (m_nSortedColumn
== phdr
->iItem
)
360 m_bAscending
= !m_bAscending
;
363 m_nSortedColumn
= phdr
->iItem
;
368 void CGitTagCompareList::OnContextMenu(CWnd
*pWnd
, CPoint point
)
372 OnContextMenuList(pWnd
, point
);
374 else if (pWnd
== GetHeaderCtrl())
376 OnContextMenuHeader(pWnd
, point
);
380 void CGitTagCompareList::OnContextMenuList(CWnd
* /*pWnd*/, CPoint point
)
382 int selIndex
= GetSelectionMark();
386 CString tag
= GetItemText(selIndex
, colTag
);
387 tag
.Replace(L
"^{}", L
"");
388 CString myHash
= GetItemText(selIndex
, colMyHash
);
389 CString theirHash
= GetItemText(selIndex
, colTheirHash
);
391 popup
.CreatePopupMenu();
393 if (!myHash
.IsEmpty())
395 logStr
.Format(IDS_SHOWLOG_OF
, (LPCTSTR
)myHash
);
396 popup
.AppendMenuIcon(IDGITRCL_MYLOG
, logStr
, IDI_LOG
);
399 if (myHash
!= theirHash
)
401 if (!theirHash
.IsEmpty())
403 logStr
.Format(IDS_SHOWLOG_OF
, (LPCTSTR
)theirHash
);
404 popup
.AppendMenuIcon(IDGITRCL_THEIRLOG
, logStr
, IDI_LOG
);
407 if (!myHash
.IsEmpty() && !theirHash
.IsEmpty())
408 popup
.AppendMenuIcon(IDGITRCL_COMPARE
, IDS_LOG_POPUP_COMPAREWITHPREVIOUS
, IDI_DIFF
);
410 popup
.AppendMenu(MF_SEPARATOR
);
412 if (!theirHash
.IsEmpty())
413 popup
.AppendMenuIcon(IDGITRCL_FETCH
, IDS_MENUFETCH
, IDI_UPDATE
);
415 if (!myHash
.IsEmpty())
416 popup
.AppendMenuIcon(IDGITRCL_PUSH
, IDS_MENUPUSH
, IDI_COMMIT
);
419 popup
.AppendMenu(MF_SEPARATOR
);
421 if (!myHash
.IsEmpty())
422 popup
.AppendMenuIcon(IDGITRCL_DELETELOCAL
, IDS_DELETE_LOCALTAG
, IDI_DELETE
);
424 if (!theirHash
.IsEmpty())
425 popup
.AppendMenuIcon(IDGITRCL_DELETEREMOTE
, IDS_DELETE_REMOTETAG
, IDI_DELETE
);
427 int cmd
= popup
.TrackPopupMenu(TPM_RETURNCMD
| TPM_LEFTALIGN
| TPM_NONOTIFY
, point
.x
, point
.y
, this);
431 case IDGITRCL_THEIRLOG
:
434 sCmd
.Format(L
"/command:log /path:\"%s\" /endrev:\"%s\"", (LPCTSTR
)g_Git
.m_CurrentDir
, cmd
== IDGITRCL_MYLOG
? (LPCTSTR
)myHash
: (LPCTSTR
)theirHash
);
435 CAppUtils::RunTortoiseGitProc(sCmd
);
438 case IDGITRCL_COMPARE
:
441 sCmd
.Format(L
"/command:showcompare /path:\"%s\" /revision1:\"%s\" /revision2:\"%s\"", (LPCTSTR
)g_Git
.m_CurrentDir
, (LPCTSTR
)myHash
, (LPCTSTR
)theirHash
);
442 if (!!(GetAsyncKeyState(VK_SHIFT
) & 0x8000))
443 sCmd
+= L
" /alternative";
444 CAppUtils::RunTortoiseGitProc(sCmd
);
447 case IDGITRCL_DELETELOCAL
:
450 csMessage
.Format(IDS_PROC_DELETEBRANCHTAG
, (LPCTSTR
)tag
);
451 if (MessageBox(csMessage
, L
"TortoiseGit", MB_YESNO
| MB_ICONQUESTION
) != IDYES
)
454 g_Git
.DeleteRef(L
"refs/tags/" + tag
);
457 if (Fill(m_remote
, err
))
458 MessageBox(err
, L
"TortoiseGit", MB_ICONERROR
);
461 case IDGITRCL_DELETEREMOTE
:
464 csMessage
.Format(IDS_PROC_DELETEBRANCHTAG
, (LPCTSTR
)tag
);
465 if (MessageBox(csMessage
, L
"TortoiseGit", MB_YESNO
| MB_ICONQUESTION
) != IDYES
)
467 CSysProgressDlg sysProgressDlg
;
468 sysProgressDlg
.SetTitle(CString(MAKEINTRESOURCE(IDS_APPNAME
)));
469 sysProgressDlg
.SetLine(1, CString(MAKEINTRESOURCE(IDS_DELETING_REMOTE_REFS
)));
470 sysProgressDlg
.SetLine(2, CString(MAKEINTRESOURCE(IDS_PROGRESSWAIT
)));
471 sysProgressDlg
.SetShowProgressBar(false);
472 sysProgressDlg
.ShowModal(this, true);
475 list
.push_back(L
"refs/tags/" + tag
);
476 if (g_Git
.DeleteRemoteRefs(m_remote
, list
))
478 MessageBox(g_Git
.GetGitLastErr(L
"Could not delete remote tag.", CGit::GIT_CMD_PUSH
), L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
479 sysProgressDlg
.Stop();
485 auto ret
= Fill(m_remote
, err
);
486 sysProgressDlg
.Stop();
488 MessageBox(err
, L
"TortoiseGit", MB_ICONERROR
);
496 dlg
.m_GitCmd
.Format(L
"git.exe push --force \"%s\" refs/tags/%s", (LPCTSTR
)m_remote
, (LPCTSTR
)tag
);
500 if (Fill(m_remote
, err
))
501 MessageBox(err
, L
"TortoiseGit", MB_ICONERROR
);
508 dlg
.m_GitCmd
.Format(L
"git.exe fetch \"%s\" refs/tags/%s:refs/tags/%s", (LPCTSTR
)m_remote
, (LPCTSTR
)tag
, (LPCTSTR
)tag
);
512 if (Fill(m_remote
, err
))
513 MessageBox(err
, L
"TortoiseGit", MB_ICONERROR
);
520 static void AppendMenuChecked(CMenu
&menu
, UINT nTextID
, UINT_PTR nItemID
, BOOL checked
= FALSE
, BOOL enabled
= TRUE
)
523 text
.LoadString(nTextID
);
524 menu
.AppendMenu(MF_STRING
| (enabled
? MF_ENABLED
: MF_DISABLED
) | (checked
? MF_CHECKED
: MF_UNCHECKED
), nItemID
, text
);
527 void CGitTagCompareList::OnContextMenuHeader(CWnd
* /*pWnd*/, CPoint point
)
530 if (popup
.CreatePopupMenu())
532 AppendMenuChecked(popup
, IDS_HIDEUNCHANGED
, IDGITRCLH_HIDEUNCHANGED
, m_bHideEqual
);
534 int selection
= popup
.TrackPopupMenu(TPM_RETURNCMD
| TPM_LEFTALIGN
| TPM_NONOTIFY
, point
.x
, point
.y
, this);
537 case IDGITRCLH_HIDEUNCHANGED
:
538 m_bHideEqual
= !m_bHideEqual
;