ToroiseShell Git tab shows last modified commit info again using libgit2
[TortoiseGit.git] / src / TortoiseShell / GITPropertyPage.cpp
blob178ed4f5a1d15cdbd19132ffc22d03fd4ebbe527
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2008 - TortoiseSVN
4 // Copyright (C) 2008-2012 - TortoiseGit
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 #include "stdafx.h"
22 #include "ShellExt.h"
23 #include "gitpropertypage.h"
24 #include "UnicodeUtils.h"
25 #include "PathUtils.h"
26 #include "UnicodeUtils.h"
27 #include "CreateProcessHelper.h"
28 #include "FormatMessageWrapper.h"
30 #define MAX_STRING_LENGTH 4096 //should be big enough
32 // Nonmember function prototypes
33 BOOL CALLBACK PageProc (HWND, UINT, WPARAM, LPARAM);
34 UINT CALLBACK PropPageCallbackProc ( HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp );
36 /////////////////////////////////////////////////////////////////////////////
37 // Dialog procedures and other callback functions
39 BOOL CALLBACK PageProc (HWND hwnd, UINT uMessage, WPARAM wParam, LPARAM lParam)
41 CGitPropertyPage * sheetpage;
43 if (uMessage == WM_INITDIALOG)
45 sheetpage = (CGitPropertyPage*) ((LPPROPSHEETPAGE) lParam)->lParam;
46 SetWindowLongPtr (hwnd, GWLP_USERDATA, (LONG_PTR) sheetpage);
47 sheetpage->SetHwnd(hwnd);
49 else
51 sheetpage = (CGitPropertyPage*) GetWindowLongPtr (hwnd, GWLP_USERDATA);
54 if (sheetpage != 0L)
55 return sheetpage->PageProc(hwnd, uMessage, wParam, lParam);
56 else
57 return FALSE;
60 UINT CALLBACK PropPageCallbackProc ( HWND /*hwnd*/, UINT uMsg, LPPROPSHEETPAGE ppsp )
62 // Delete the page before closing.
63 if (PSPCB_RELEASE == uMsg)
65 CGitPropertyPage* sheetpage = (CGitPropertyPage*) ppsp->lParam;
66 if (sheetpage != NULL)
67 delete sheetpage;
69 return 1;
72 // *********************** CGitPropertyPage *************************
74 CGitPropertyPage::CGitPropertyPage(const std::vector<stdstring> &newFilenames)
75 :filenames(newFilenames)
76 ,m_bChanged(false)
80 CGitPropertyPage::~CGitPropertyPage(void)
84 void CGitPropertyPage::SetHwnd(HWND newHwnd)
86 m_hwnd = newHwnd;
89 BOOL CGitPropertyPage::PageProc (HWND /*hwnd*/, UINT uMessage, WPARAM wParam, LPARAM lParam)
91 switch (uMessage)
93 case WM_INITDIALOG:
95 InitWorkfileView();
96 return TRUE;
98 case WM_NOTIFY:
100 LPNMHDR point = (LPNMHDR)lParam;
101 int code = point->code;
103 // Respond to notifications.
105 if (code == PSN_APPLY && filenames.size() == 1 && m_bChanged)
109 CTGitPath path(filenames.front().c_str());
110 CString projectTopDir;
111 if(!path.HasAdminDir(&projectTopDir) || path.IsDirectory())
112 break;
114 CTGitPath relatepath;
115 CString strpath=path.GetWinPathString();
117 if(projectTopDir[projectTopDir.GetLength() - 1] == _T('\\'))
118 relatepath.SetFromWin(strpath.Right(strpath.GetLength() - projectTopDir.GetLength()));
119 else
120 relatepath.SetFromWin(strpath.Right(strpath.GetLength() - projectTopDir.GetLength() - 1));
122 CStringA gitdir = CUnicodeUtils::GetMulti(projectTopDir, CP_UTF8);
123 git_repository *repository = NULL;
124 git_index *index = NULL;
126 int ret = git_repository_open(&repository, gitdir.GetBuffer());
127 gitdir.ReleaseBuffer();
128 if (ret)
129 break;
131 if (git_repository_index(&index, repository))
133 git_repository_free(repository);
134 break;
137 bool changed = false;
139 CStringA pathA = CUnicodeUtils::GetMulti(relatepath.GetGitPathString(), CP_UTF8);
140 int idx = git_index_find(index, pathA);
141 if (idx >= 0) {
142 git_index_entry *e = git_index_get(index, idx);
144 if (SendMessage(GetDlgItem(m_hwnd, IDC_ASSUMEVALID), BM_GETCHECK, 0, 0) == BST_CHECKED)
146 if (!(e->flags & GIT_IDXENTRY_VALID))
148 e->flags |= GIT_IDXENTRY_VALID;
149 changed = true;
152 else
154 if (e->flags & GIT_IDXENTRY_VALID)
156 e->flags &= ~GIT_IDXENTRY_VALID;
157 changed = true;
160 if (SendMessage(GetDlgItem(m_hwnd, IDC_SKIPWORKTREE), BM_GETCHECK, 0, 0) == BST_CHECKED)
162 if (!(e->flags_extended & GIT_IDXENTRY_SKIP_WORKTREE))
164 e->flags_extended |= GIT_IDXENTRY_SKIP_WORKTREE;
165 changed = true;
168 else
170 if (e->flags_extended & GIT_IDXENTRY_SKIP_WORKTREE)
172 e->flags_extended &= ~GIT_IDXENTRY_SKIP_WORKTREE;
173 changed = true;
176 if (SendMessage(GetDlgItem(m_hwnd, IDC_EXECUTABLE), BM_GETCHECK, 0, 0) == BST_CHECKED)
178 if (!(e->mode & 0111))
180 e->mode |= 0111;
181 changed = true;
184 else
186 if (e->mode & 0111)
188 e->mode &= ~0111;
189 changed = true;
192 if (changed)
193 git_index_add2(index, e);
196 if (changed)
198 if (!git_index_write(index))
199 m_bChanged = false;
202 git_index_free(index);
203 } while (0);
205 SetWindowLongPtr (m_hwnd, DWLP_MSGRESULT, FALSE);
206 return TRUE;
209 case WM_DESTROY:
210 return TRUE;
212 case WM_COMMAND:
213 PageProcOnCommand(wParam);
214 break;
215 } // switch (uMessage)
216 return FALSE;
218 void CGitPropertyPage::PageProcOnCommand(WPARAM wParam)
220 if(HIWORD(wParam) != BN_CLICKED)
221 return;
223 switch (LOWORD(wParam))
225 case IDC_SHOWLOG:
227 tstring gitCmd = _T(" /command:");
228 gitCmd += _T("log /path:\"");
229 gitCmd += filenames.front().c_str();
230 gitCmd += _T("\"");
231 RunCommand(gitCmd);
233 break;
234 case IDC_SHOWSETTINGS:
236 CTGitPath path(filenames.front().c_str());
237 CString projectTopDir;
238 if(!path.HasAdminDir(&projectTopDir))
239 return;
241 tstring gitCmd = _T(" /command:");
242 gitCmd += _T("settings /path:\"");
243 gitCmd += projectTopDir;
244 gitCmd += _T("\"");
245 RunCommand(gitCmd);
247 break;
248 case IDC_ASSUMEVALID:
249 case IDC_SKIPWORKTREE:
250 case IDC_EXECUTABLE:
251 m_bChanged = true;
252 SendMessage(GetParent(m_hwnd), PSM_CHANGED, (WPARAM)m_hwnd, 0);
253 break;
257 void CGitPropertyPage::RunCommand(const tstring& command)
259 tstring tortoiseProcPath = CPathUtils::GetAppDirectory(g_hmodThisDll) + _T("TortoiseProc.exe");
260 if (CCreateProcessHelper::CreateProcessDetached(tortoiseProcPath.c_str(), const_cast<TCHAR*>(command.c_str())))
262 // process started - exit
263 return;
266 MessageBox(NULL, CFormatMessageWrapper(), _T("TortoiseProc launch failed"), MB_OK | MB_ICONINFORMATION);
269 void CGitPropertyPage::Time64ToTimeString(__time64_t time, TCHAR * buf, size_t buflen)
271 struct tm newtime;
272 SYSTEMTIME systime;
273 TCHAR timebuf[MAX_STRING_LENGTH];
274 TCHAR datebuf[MAX_STRING_LENGTH];
276 LCID locale = (WORD)CRegStdDWORD(_T("Software\\TortoiseGit\\LanguageID"), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT));
277 locale = MAKELCID(locale, SORT_DEFAULT);
279 *buf = '\0';
280 if (time)
282 _localtime64_s(&newtime, &time);
284 systime.wDay = (WORD)newtime.tm_mday;
285 systime.wDayOfWeek = (WORD)newtime.tm_wday;
286 systime.wHour = (WORD)newtime.tm_hour;
287 systime.wMilliseconds = 0;
288 systime.wMinute = (WORD)newtime.tm_min;
289 systime.wMonth = (WORD)newtime.tm_mon+1;
290 systime.wSecond = (WORD)newtime.tm_sec;
291 systime.wYear = (WORD)newtime.tm_year+1900;
292 if (CRegStdDWORD(_T("Software\\TortoiseGit\\LogDateFormat")) == 1)
293 GetDateFormat(locale, DATE_SHORTDATE, &systime, NULL, datebuf, MAX_STRING_LENGTH);
294 else
295 GetDateFormat(locale, DATE_LONGDATE, &systime, NULL, datebuf, MAX_STRING_LENGTH);
296 GetTimeFormat(locale, 0, &systime, NULL, timebuf, MAX_STRING_LENGTH);
297 *buf = '\0';
298 _tcsncat_s(buf, buflen, timebuf, MAX_STRING_LENGTH-1);
299 _tcsncat_s(buf, buflen, _T(", "), MAX_STRING_LENGTH-1);
300 _tcsncat_s(buf, buflen, datebuf, MAX_STRING_LENGTH-1);
304 struct TreewalkStruct
306 const char *folder;
307 const char *name;
308 git_oid oid;
311 static int TreewalkCB_FindFileRecentCommit(const char *root, const git_tree_entry *entry, void *payload)
313 TreewalkStruct *treewalkstruct = (TreewalkStruct *)payload;
314 char folder[MAX_PATH];
315 strcpy(folder, root);
316 strcat(folder, git_tree_entry_name(entry));
317 strcat(folder, "/");
318 if (strstr(treewalkstruct->folder, folder))
319 return 0;
321 if (!strcmp(treewalkstruct->folder, root))
323 if (!strcmp(git_tree_entry_name(entry), treewalkstruct->name))
325 git_oid_cpy(&treewalkstruct->oid, git_tree_entry_id(entry));
326 return -1;
329 return 0;
332 return 1;
335 static git_commit * FindFileRecentCommit(git_repository *repository, CString path)
337 git_commit *commit = NULL, *commit2 = NULL;
338 git_revwalk *walk;
339 if (git_revwalk_new(&walk, repository))
340 return NULL;
342 CStringA pathA = CUnicodeUtils::GetUTF8(path);
343 const char *pathC = pathA.GetBuffer();
344 char folder[MAX_PATH], file[MAX_PATH];
345 const char *slash = strrchr(pathC, '/');
346 if (slash)
348 strncpy(folder, pathC, slash - pathC + 1);
349 folder[slash - pathC + 1] = '\0';
350 strcpy(file, slash + 1);
352 else
354 folder[0] = '\0';
355 strcpy(file, pathC);
357 pathA.ReleaseBuffer();
359 TreewalkStruct treewalkstruct = { folder, file };
360 TreewalkStruct treewalkstruct2 = { folder, file };
364 if (git_revwalk_push_head(walk))
365 throw "git_revwalk_push_head";
367 git_oid oid;
368 while (!git_revwalk_next(&oid, walk))
370 if (commit != NULL)
372 git_commit_free(commit);
373 commit = NULL;
376 if (git_commit_lookup(&commit, repository, &oid))
378 commit = NULL;
379 throw "git_commit_lookup 1";
382 git_tree *tree;
383 if (git_commit_tree(&tree, commit))
384 throw "git_commit_tree 1";
386 memset(&treewalkstruct.oid.id, 0, sizeof(treewalkstruct.oid.id));
387 int ret = git_tree_walk(tree, TreewalkCB_FindFileRecentCommit, GIT_TREEWALK_PRE, &treewalkstruct);
388 git_tree_free(tree);
389 if (ret < 0 && ret != GIT_EUSER)
390 throw "git_tree_walk 1";
392 bool diff = true;
393 // for merge point, check if it is different to all parents, if yes then there are real change in the merge point.
394 // if no parent then of course it is different
395 for (unsigned int i = 0; i < git_commit_parentcount(commit); i++)
397 if (git_commit_parent(&commit2, commit, i))
399 commit2 = NULL;
400 throw "git_commit_parent";
403 if (git_commit_tree(&tree, commit2))
404 throw "git_commit_tree 2";
406 git_commit_free(commit2);
407 memset(&treewalkstruct2.oid.id, 0, sizeof(treewalkstruct2.oid.id));
408 int ret = git_tree_walk(tree, TreewalkCB_FindFileRecentCommit, GIT_TREEWALK_PRE, &treewalkstruct2);
409 git_tree_free(tree);
410 if (ret < 0 && ret != GIT_EUSER)
411 throw "git_tree_walk 2";
413 if (!git_oid_cmp(&treewalkstruct.oid, &treewalkstruct2.oid))
414 diff = false;
415 else if (git_revwalk_hide(walk, git_commit_parent_oid(commit, i)))
416 throw "git_revwalk_hide";
419 if (diff)
420 break;
423 catch (...)
425 if (commit != NULL)
427 git_commit_free(commit);
428 commit = NULL;
430 if (commit2 != NULL)
432 git_commit_free(commit2);
433 commit2 = NULL;
437 git_revwalk_free(walk);
438 return commit;
441 void CGitPropertyPage::DisplayCommit(git_commit * commit, UINT hashLabel, UINT subjectLabel, UINT authorLabel, UINT dateLabel)
443 int encode = CP_UTF8;
444 const char * encodingString = git_commit_message_encoding(commit);
445 if (encodingString != NULL)
447 CString str;
448 g_Git.StringAppend(&str, (BYTE*)encodingString, CP_UTF8);
449 encode = CUnicodeUtils::GetCPCode(str);
452 const git_signature * author = git_commit_author(commit);
453 CString authorName;
454 g_Git.StringAppend(&authorName, (BYTE*)author->name, encode);
456 CString message;
457 g_Git.StringAppend(&message, (BYTE*)git_commit_message(commit), encode);
459 int start = 0;
460 message = message.Tokenize(L"\n", start);
462 SetDlgItemText(m_hwnd, hashLabel, CGitHash((char*)(git_commit_id(commit)->id)).ToString());
463 SetDlgItemText(m_hwnd, subjectLabel, message);
464 SetDlgItemText(m_hwnd, authorLabel, authorName);
466 CString authorDate;
467 Time64ToTimeString(author->when.time, authorDate.GetBufferSetLength(200), 200);
468 SetDlgItemText(m_hwnd, dateLabel, authorDate);
471 void CGitPropertyPage::InitWorkfileView()
473 if (filenames.empty())
474 return;
476 CTGitPath path(filenames.front().c_str());
478 CString ProjectTopDir;
479 if(!path.HasAdminDir(&ProjectTopDir))
480 return;
482 CStringA gitdir = CUnicodeUtils::GetMulti(ProjectTopDir, CP_UTF8);
483 git_repository *repository = NULL;
485 int ret = git_repository_open(&repository, gitdir.GetBuffer());
486 gitdir.ReleaseBuffer();
487 if (ret)
488 return;
490 CGit git;
492 git.SetCurrentDir(ProjectTopDir);
493 CString username;
494 git.Run(_T("git.exe config user.name"), &username, CP_UTF8);
495 CString useremail;
496 git.Run(_T("git.exe config user.email"), &useremail, CP_UTF8);
497 CString autocrlf;
498 git.Run(_T("git.exe config core.autocrlf"), &autocrlf, CP_UTF8);
499 CString safecrlf;
500 git.Run(_T("git.exe config core.safecrlf"), &safecrlf, CP_UTF8);
502 CString branch;
503 CString remotebranch;
505 if (!g_Git.GetCurrentBranchFromFile(ProjectTopDir, branch))
507 CString cmd, remote;
508 cmd.Format(_T("git.exe config branch.%s.merge"), branch.Trim());
509 git.Run(cmd, &remotebranch, CP_UTF8);
510 cmd.Format(_T("git.exe config branch.%s.remote"), branch.Trim());
511 git.Run(cmd, &remote, CP_UTF8);
512 remote.Trim();
513 remotebranch.Trim();
514 if((!remote.IsEmpty()) && (!remotebranch.IsEmpty()))
516 remotebranch = remotebranch.Mid(11);
517 remotebranch = remote + _T("/") + remotebranch;
520 else
521 branch = _T("detached HEAD");
523 if (autocrlf.Trim().IsEmpty())
524 autocrlf = _T("false");
525 if (safecrlf.Trim().IsEmpty())
526 safecrlf = _T("false");
528 SetDlgItemText(m_hwnd,IDC_CONFIG_USERNAME,username.Trim());
529 SetDlgItemText(m_hwnd,IDC_CONFIG_USEREMAIL,useremail.Trim());
530 SetDlgItemText(m_hwnd,IDC_CONFIG_AUTOCRLF,autocrlf.Trim());
531 SetDlgItemText(m_hwnd,IDC_CONFIG_SAFECRLF,safecrlf.Trim());
533 SetDlgItemText(m_hwnd,IDC_SHELL_CURRENT_BRANCH,branch.Trim());
534 remotebranch.Trim().Replace(_T("\n"), _T("; "));
535 SetDlgItemText(m_hwnd,IDC_SHELL_REMOTE_BRANCH, remotebranch);
537 git_oid oid;
538 git_commit *HEADcommit = NULL;
539 if (!git_reference_name_to_oid(&oid, repository, "HEAD") && !git_commit_lookup(&HEADcommit, repository, &oid) && HEADcommit != NULL)
541 DisplayCommit(HEADcommit, IDC_HEAD_HASH, IDC_HEAD_SUBJECT, IDC_HEAD_AUTHOR, IDC_HEAD_DATE);
542 git_commit_free(HEADcommit);
546 if (filenames.size() == 1)
548 CTGitPath relatepath;
549 CString strpath=path.GetWinPathString();
551 if(ProjectTopDir[ProjectTopDir.GetLength()-1] == _T('\\'))
553 relatepath.SetFromWin( strpath.Right(strpath.GetLength()-ProjectTopDir.GetLength()));
555 else
557 relatepath.SetFromWin( strpath.Right(strpath.GetLength()-ProjectTopDir.GetLength()-1));
560 git_commit *commit = FindFileRecentCommit(repository, relatepath.GetGitPathString());
561 if (commit != NULL)
563 DisplayCommit(commit, IDC_LAST_HASH, IDC_LAST_SUBJECT, IDC_LAST_AUTHOR, IDC_LAST_DATE);
564 git_commit_free(commit);
567 if (!path.IsDirectory())
569 // get assume valid flag
570 bool assumevalid = false;
571 bool skipworktree = false;
572 bool executable = false;
575 git_index *index = NULL;
577 if (git_repository_index(&index, repository))
578 break;
580 CStringA pathA = CUnicodeUtils::GetMulti(relatepath.GetGitPathString(), CP_UTF8);
581 int idx = git_index_find(index, pathA);
582 if (idx >= 0) {
583 git_index_entry *e = git_index_get(index, idx);
585 if (e->flags & GIT_IDXENTRY_VALID)
586 assumevalid = true;
588 if (e->flags_extended & GIT_IDXENTRY_SKIP_WORKTREE)
589 skipworktree = true;
591 if (e->mode & 0111)
592 executable = true;
594 else
596 // do not show checkboxes for unversioned files
597 ShowWindow(GetDlgItem(m_hwnd, IDC_ASSUMEVALID), SW_HIDE);
598 ShowWindow(GetDlgItem(m_hwnd, IDC_SKIPWORKTREE), SW_HIDE);
599 ShowWindow(GetDlgItem(m_hwnd, IDC_EXECUTABLE), SW_HIDE);
602 git_index_free(index);
603 } while (0);
604 SendMessage(GetDlgItem(m_hwnd, IDC_ASSUMEVALID), BM_SETCHECK, assumevalid ? BST_CHECKED : BST_UNCHECKED, 0);
605 SendMessage(GetDlgItem(m_hwnd, IDC_SKIPWORKTREE), BM_SETCHECK, skipworktree ? BST_CHECKED : BST_UNCHECKED, 0);
606 SendMessage(GetDlgItem(m_hwnd, IDC_EXECUTABLE), BM_SETCHECK, executable ? BST_CHECKED : BST_UNCHECKED, 0);
608 else
610 ShowWindow(GetDlgItem(m_hwnd, IDC_ASSUMEVALID), SW_HIDE);
611 ShowWindow(GetDlgItem(m_hwnd, IDC_SKIPWORKTREE), SW_HIDE);
612 ShowWindow(GetDlgItem(m_hwnd, IDC_EXECUTABLE), SW_HIDE);
615 else
617 ShowWindow(GetDlgItem(m_hwnd, IDC_ASSUMEVALID), SW_HIDE);
618 ShowWindow(GetDlgItem(m_hwnd, IDC_SKIPWORKTREE), SW_HIDE);
619 ShowWindow(GetDlgItem(m_hwnd, IDC_EXECUTABLE), SW_HIDE);
622 git_repository_free(repository);
626 // CShellExt member functions (needed for IShellPropSheetExt)
627 STDMETHODIMP CShellExt::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
629 __try
631 return AddPages_Wrap(lpfnAddPage, lParam);
633 __except(CCrashReport::Instance().SendReport(GetExceptionInformation()))
636 return E_FAIL;
639 STDMETHODIMP CShellExt::AddPages_Wrap(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
641 CString ProjectTopDir;
643 for (std::vector<stdstring>::iterator I = files_.begin(); I != files_.end(); ++I)
646 GitStatus svn = GitStatus();
647 if (svn.GetStatus(CTGitPath(I->c_str())) == (-2))
648 return S_OK; // file/directory not under version control
650 if (svn.status->entry == NULL)
651 return S_OK;
653 if( CTGitPath(I->c_str()).HasAdminDir(&ProjectTopDir))
654 break;
655 else
656 return S_OK;
659 if (files_.empty())
660 return S_OK;
662 LoadLangDll();
663 PROPSHEETPAGE psp;
664 SecureZeroMemory(&psp, sizeof(PROPSHEETPAGE));
665 HPROPSHEETPAGE hPage;
666 CGitPropertyPage *sheetpage = new (std::nothrow) CGitPropertyPage(files_);
668 psp.dwSize = sizeof (psp);
669 psp.dwFlags = PSP_USEREFPARENT | PSP_USETITLE | PSP_USEICONID | PSP_USECALLBACK;
670 psp.hInstance = g_hResInst;
671 psp.pszTemplate = MAKEINTRESOURCE(IDD_PROPPAGE);
672 psp.pszIcon = MAKEINTRESOURCE(IDI_APPSMALL);
673 psp.pszTitle = _T("Git");
674 psp.pfnDlgProc = (DLGPROC) PageProc;
675 psp.lParam = (LPARAM) sheetpage;
676 psp.pfnCallback = PropPageCallbackProc;
677 psp.pcRefParent = (UINT*)&g_cRefThisDll;
679 hPage = CreatePropertySheetPage (&psp);
681 if (hPage != NULL)
683 if (!lpfnAddPage (hPage, lParam))
685 delete sheetpage;
686 DestroyPropertySheetPage (hPage);
690 return S_OK;
693 STDMETHODIMP CShellExt::ReplacePage (UINT /*uPageID*/, LPFNADDPROPSHEETPAGE /*lpfnReplaceWith*/, LPARAM /*lParam*/)
695 return E_FAIL;