Fix typos
[TortoiseGit.git] / src / Git / TGitPath.cpp
blob8684deae074f8acfc50b77f078532229e431552c
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2023 - TortoiseGit
4 // Copyright (C) 2003-2008 - 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 #include "stdafx.h"
21 #include "TGitPath.h"
22 #include "UnicodeUtils.h"
23 #include "GitAdminDir.h"
24 #include "PathUtils.h"
25 #include <regex>
26 #include "Git.h"
27 #include "../TortoiseShell/Globals.h"
28 #include "StringUtils.h"
29 #include "SmartHandle.h"
30 #include "../Resources/LoglistCommonResource.h"
31 #include <sys/stat.h>
33 #ifdef TGIT_LFS
34 #include "nlohmann/json.hpp"
35 using json = nlohmann::json;
36 #endif
38 extern CGit g_Git;
40 CTGitPath::CTGitPath()
44 CTGitPath::~CTGitPath()
47 // Create a TGitPath object from an unknown path type (same as using SetFromUnknown)
48 CTGitPath::CTGitPath(const CString& sUnknownPath) : CTGitPath()
50 SetFromUnknown(sUnknownPath);
53 CTGitPath::CTGitPath(const CString& sUnknownPath, bool bIsDirectory) : CTGitPath(sUnknownPath)
55 m_bDirectoryKnown = true;
56 m_bIsDirectory = bIsDirectory;
59 unsigned int CTGitPath::ParseStatus(const char status)
61 switch (status)
63 case 'M':
64 return LOGACTIONS_MODIFIED;
65 case 'R':
66 return LOGACTIONS_REPLACED;
67 case 'A':
68 return LOGACTIONS_ADDED;
69 case 'D':
70 return LOGACTIONS_DELETED;
71 case 'U':
72 return LOGACTIONS_UNMERGED;
73 case 'K':
74 return LOGACTIONS_DELETED;
75 case 'C':
76 return LOGACTIONS_COPY;
77 case 'T':
78 return LOGACTIONS_MODIFIED;
79 default:
80 return 0;
84 unsigned int CTGitPath::ParseAndUpdateStatus(git_delta_t status)
86 if (status == GIT_DELTA_MODIFIED)
87 m_Action |= LOGACTIONS_MODIFIED;
88 if (status == GIT_DELTA_RENAMED)
89 m_Action |= LOGACTIONS_REPLACED;
90 if (status == GIT_DELTA_ADDED)
91 m_Action |= LOGACTIONS_ADDED;
92 if (status == GIT_DELTA_DELETED)
93 m_Action |= LOGACTIONS_DELETED;
94 if (status == GIT_DELTA_UNMODIFIED)
95 m_Action |= LOGACTIONS_UNMERGED;
96 if (status == GIT_DELTA_COPIED)
97 m_Action |= LOGACTIONS_COPY;
98 if (status == GIT_DELTA_TYPECHANGE)
99 m_Action |= LOGACTIONS_MODIFIED;
101 return m_Action;
104 void CTGitPath::SetFromGit(const char* pPath)
106 Reset();
107 ATLASSERT(pPath);
108 if (!pPath)
109 return;
110 m_sFwdslashPath = CUnicodeUtils::GetUnicode(pPath);
111 SanitizeRootPath(m_sFwdslashPath, true);
114 void CTGitPath::SetFromGit(const char* pPath, bool bIsDirectory)
116 SetFromGit(pPath);
117 m_bDirectoryKnown = true;
118 m_bIsDirectory = bIsDirectory;
121 void CTGitPath::SetFromGit(const wchar_t* pPath, bool bIsDirectory)
123 Reset();
124 if (pPath)
126 m_sFwdslashPath = pPath;
127 SanitizeRootPath(m_sFwdslashPath, true);
129 m_bDirectoryKnown = true;
130 m_bIsDirectory = bIsDirectory;
133 void CTGitPath::SetFromGit(const CString& sPath, CString* oldpath, int* bIsDirectory)
135 Reset();
136 m_sFwdslashPath = sPath;
137 SanitizeRootPath(m_sFwdslashPath, true);
138 if (bIsDirectory)
140 m_bDirectoryKnown = true;
141 m_bIsDirectory = *bIsDirectory != FALSE;
143 if(oldpath)
144 m_sOldFwdslashPath = *oldpath;
147 void CTGitPath::SetFromWin(LPCWSTR pPath)
149 Reset();
150 m_sBackslashPath = pPath;
151 m_sBackslashPath.Replace(L"\\\\?\\", L"");
152 SanitizeRootPath(m_sBackslashPath, false);
153 ATLASSERT(m_sBackslashPath.Find('/')<0);
155 void CTGitPath::SetFromWin(const CString& sPath)
157 Reset();
158 m_sBackslashPath = sPath;
159 m_sBackslashPath.Replace(L"\\\\?\\", L"");
160 SanitizeRootPath(m_sBackslashPath, false);
162 void CTGitPath::SetFromWin(LPCWSTR pPath, bool bIsDirectory)
164 Reset();
165 m_sBackslashPath = pPath;
166 m_bIsDirectory = bIsDirectory;
167 m_bDirectoryKnown = true;
168 SanitizeRootPath(m_sBackslashPath, false);
170 void CTGitPath::SetFromWin(const CString& sPath, bool bIsDirectory)
172 Reset();
173 m_sBackslashPath = sPath;
174 m_bIsDirectory = bIsDirectory;
175 m_bDirectoryKnown = true;
176 SanitizeRootPath(m_sBackslashPath, false);
178 void CTGitPath::SetFromUnknown(const CString& sPath)
180 Reset();
181 // Just set whichever path we think is most likely to be used
182 // GitAdminDir admin;
183 // CString p;
184 // if(admin.HasAdminDir(sPath,&p))
185 // SetFwdslashPath(sPath.Right(sPath.GetLength()-p.GetLength()));
186 // else
187 SetFwdslashPath(sPath);
190 void CTGitPath::UpdateCase()
192 m_sBackslashPath = CPathUtils::GetLongPathname(GetWinPathString());
193 CPathUtils::TrimTrailingPathDelimiter(m_sBackslashPath);
194 SanitizeRootPath(m_sBackslashPath, false);
195 SetFwdslashPath(m_sBackslashPath);
198 LPCWSTR CTGitPath::GetWinPath() const
200 if(IsEmpty())
201 return L"";
202 if(m_sBackslashPath.IsEmpty())
203 SetBackslashPath(m_sFwdslashPath);
204 return m_sBackslashPath;
206 // This is a temporary function, to be used during the migration to
207 // the path class. Ultimately, functions consuming paths should take a CTGitPath&, not a CString
208 const CString& CTGitPath::GetWinPathString() const
210 if(m_sBackslashPath.IsEmpty())
211 SetBackslashPath(m_sFwdslashPath);
212 return m_sBackslashPath;
215 const CString& CTGitPath::GetGitPathString() const
217 if(m_sFwdslashPath.IsEmpty())
218 SetFwdslashPath(m_sBackslashPath);
219 return m_sFwdslashPath;
222 const CString &CTGitPath::GetGitOldPathString() const
224 return m_sOldFwdslashPath;
227 const CString& CTGitPath::GetUIPathString() const
229 if (m_sUIPath.IsEmpty())
230 m_sUIPath = GetWinPathString();
231 return m_sUIPath;
234 void CTGitPath::SetFwdslashPath(const CString& sPath) const
236 CString path = sPath;
237 path.Replace('\\', '/');
239 // We don't leave a trailing /
240 path.TrimRight('/');
241 path.Replace(L"//?/", L"");
243 SanitizeRootPath(path, true);
245 path.Replace(L"file:////", L"file://");
246 m_sFwdslashPath = path;
249 void CTGitPath::SetBackslashPath(const CString& sPath) const
251 CString path = sPath;
252 path.Replace('/', '\\');
253 path.TrimRight('\\');
254 SanitizeRootPath(path, false);
255 m_sBackslashPath = path;
258 void CTGitPath::SanitizeRootPath(CString& sPath, bool bIsForwardPath) const
260 // Make sure to add the trailing slash to root paths such as 'C:'
261 if (sPath.GetLength() == 2 && sPath[1] == ':')
262 sPath += (bIsForwardPath) ? L'/' : L'\\';
265 bool CTGitPath::IsDirectory() const
267 if(!m_bDirectoryKnown)
268 UpdateAttributes();
269 return m_bIsDirectory;
272 bool CTGitPath::Exists() const
274 if (!m_bExistsKnown)
275 UpdateAttributes();
276 return m_bExists;
279 bool CTGitPath::Delete(bool bTrash, bool bShowErrorUI) const
281 EnsureBackslashPathSet();
282 ::SetFileAttributes(m_sBackslashPath, FILE_ATTRIBUTE_NORMAL);
283 bool bRet = false;
284 if (Exists())
286 if ((bTrash)||(IsDirectory()))
288 auto buf = std::make_unique<wchar_t[]>(m_sBackslashPath.GetLength() + 2);
289 wcscpy_s(buf.get(), m_sBackslashPath.GetLength() + 2, m_sBackslashPath);
290 buf[m_sBackslashPath.GetLength()] = L'\0';
291 buf[m_sBackslashPath.GetLength() + 1] = L'\0';
292 bRet = CTGitPathList::DeleteViaShell(buf.get(), bTrash, bShowErrorUI);
294 else
295 bRet = !!::DeleteFile(m_sBackslashPath);
297 m_bExists = false;
298 m_bExistsKnown = true;
299 return bRet;
302 __int64 CTGitPath::GetLastWriteTime(bool force /* = false */) const
304 if (!m_bLastWriteTimeKnown || force)
305 UpdateAttributes();
306 return m_lastWriteTime;
309 __int64 CTGitPath::GetFileSize() const
311 if(!m_bDirectoryKnown)
312 UpdateAttributes();
313 return m_fileSize;
316 bool CTGitPath::IsReadOnly() const
318 if(!m_bLastWriteTimeKnown)
319 UpdateAttributes();
320 return m_bIsReadOnly;
323 void CTGitPath::UpdateAttributes() const
325 EnsureBackslashPathSet();
326 WIN32_FILE_ATTRIBUTE_DATA attribs;
327 if (m_sBackslashPath.IsEmpty())
328 m_sLongBackslashPath = L".";
329 else if (m_sBackslashPath.GetLength() >= 248)
331 if (!PathIsRelative(m_sBackslashPath))
332 m_sLongBackslashPath = L"\\\\?\\" + m_sBackslashPath;
333 else
334 m_sLongBackslashPath = L"\\\\?\\" + g_Git.CombinePath(m_sBackslashPath);
336 if (GetFileAttributesEx(m_sBackslashPath.IsEmpty() || m_sBackslashPath.GetLength() >= 248 ? m_sLongBackslashPath : m_sBackslashPath, GetFileExInfoStandard, &attribs))
338 m_bIsDirectory = !!(attribs.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
339 // don't cast directly to an __int64:
340 // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724284%28v=vs.85%29.aspx
341 // "Do not cast a pointer to a FILETIME structure to either a ULARGE_INTEGER* or __int64* value
342 // because it can cause alignment faults on 64-bit Windows."
343 m_lastWriteTime = static_cast<__int64>(attribs.ftLastWriteTime.dwHighDateTime) << 32 | attribs.ftLastWriteTime.dwLowDateTime;
344 if (m_bIsDirectory)
345 m_fileSize = 0;
346 else
347 m_fileSize = static_cast<__int64>(attribs.nFileSizeHigh) << 32 | attribs.nFileSizeLow;
348 m_bIsReadOnly = !!(attribs.dwFileAttributes & FILE_ATTRIBUTE_READONLY);
349 m_bExists = true;
351 else
353 m_bIsDirectory = false;
354 m_lastWriteTime = 0;
355 m_fileSize = 0;
356 DWORD err = GetLastError();
357 if ((err == ERROR_FILE_NOT_FOUND)||(err == ERROR_PATH_NOT_FOUND)||(err == ERROR_INVALID_NAME))
358 m_bExists = false;
359 else
361 m_bExists = true;
362 return;
365 m_bDirectoryKnown = true;
366 m_bLastWriteTimeKnown = true;
367 m_bExistsKnown = true;
370 CTGitPath CTGitPath::GetSubPath(const CTGitPath& root) const
372 CTGitPath path;
374 if (CStringUtils::StartsWith(GetWinPathString(), root.GetWinPathString()))
376 CString str=GetWinPathString();
377 path.SetFromWin(str.Right(str.GetLength()-root.GetWinPathString().GetLength()-1));
379 return path;
382 void CTGitPath::EnsureBackslashPathSet() const
384 if(m_sBackslashPath.IsEmpty())
386 SetBackslashPath(m_sFwdslashPath);
387 ATLASSERT(IsEmpty() || !m_sBackslashPath.IsEmpty());
390 void CTGitPath::EnsureFwdslashPathSet() const
392 if(m_sFwdslashPath.IsEmpty())
394 SetFwdslashPath(m_sBackslashPath);
395 ATLASSERT(IsEmpty() || !m_sFwdslashPath.IsEmpty());
400 // Reset all the caches
401 void CTGitPath::Reset()
403 m_bDirectoryKnown = false;
404 m_bLastWriteTimeKnown = false;
405 m_bHasAdminDirKnown = false;
406 m_bIsValidOnWindowsKnown = false;
407 m_bIsAdminDirKnown = false;
408 m_bExistsKnown = false;
410 m_sBackslashPath.Empty();
411 m_sLongBackslashPath.Empty();
412 m_sFwdslashPath.Empty();
413 m_sUIPath.Empty();
414 m_sProjectRoot.Empty();
415 m_sOldFwdslashPath.Empty();
417 this->m_Action=0;
418 this->m_StatAdd.Empty();
419 this->m_StatDel.Empty();
420 m_ParentNo=0;
421 m_stagingStatus = CTGitPath::StagingStatus::DontCare;
422 ATLASSERT(IsEmpty());
425 CTGitPath CTGitPath::GetDirectory() const
427 if ((IsDirectory())||(!Exists()))
428 return *this;
429 return GetContainingDirectory();
432 CTGitPath CTGitPath::GetContainingDirectory() const
434 EnsureBackslashPathSet();
436 CString sDirName = m_sBackslashPath.Left(m_sBackslashPath.ReverseFind('\\'));
437 if(sDirName.GetLength() == 2 && sDirName[1] == ':')
439 // This is a root directory, which needs a trailing slash
440 sDirName += '\\';
441 if(sDirName == m_sBackslashPath)
443 // We were clearly provided with a root path to start with - we should return nothing now
444 sDirName.Empty();
447 if(sDirName.GetLength() == 1 && sDirName[0] == '\\')
449 // We have an UNC path and we already are the root
450 sDirName.Empty();
452 CTGitPath retVal;
453 retVal.SetFromWin(sDirName);
454 return retVal;
457 CString CTGitPath::GetRootPathString() const
459 EnsureBackslashPathSet();
460 CString workingPath = m_sBackslashPath;
461 ATLVERIFY(::PathStripToRoot(CStrBuf(workingPath, MAX_PATH))); // MAX_PATH ok here.
462 return workingPath;
466 CString CTGitPath::GetFilename() const
468 //ATLASSERT(!IsDirectory());
469 return GetFileOrDirectoryName();
472 CString CTGitPath::GetFileOrDirectoryName() const
474 EnsureBackslashPathSet();
475 return m_sBackslashPath.Mid(m_sBackslashPath.ReverseFind('\\')+1);
478 CString CTGitPath::GetUIFileOrDirectoryName() const
480 GetUIPathString();
481 return m_sUIPath.Mid(m_sUIPath.ReverseFind('\\')+1);
484 CString CTGitPath::GetFileExtension() const
486 if(!IsDirectory())
488 EnsureBackslashPathSet();
489 const int dotPos = m_sBackslashPath.ReverseFind('.');
490 const int slashPos = m_sBackslashPath.ReverseFind('\\');
491 if (dotPos > slashPos)
492 return m_sBackslashPath.Mid(dotPos);
494 return CString();
496 CString CTGitPath::GetBaseFilename() const
498 CString filename=GetFilename();
499 const int dot = filename.ReverseFind(L'.');
500 if(dot>0)
501 filename.Truncate(dot);
502 return filename;
505 bool CTGitPath::IsEmpty() const
507 // Check the backward slash path first, since the chance that this
508 // one is set is higher. In case of a 'false' return value it's a little
509 // bit faster.
510 return m_sBackslashPath.IsEmpty() && m_sFwdslashPath.IsEmpty();
513 // Test if both paths refer to the same item
514 // Ignores case and slash direction
515 bool CTGitPath::IsEquivalentTo(const CTGitPath& rhs) const
517 // Try and find a slash direction which avoids having to convert
518 // both filenames
519 if(!m_sBackslashPath.IsEmpty())
521 // *We've* got a \ path - make sure that the RHS also has a \ path
522 rhs.EnsureBackslashPathSet();
523 return CPathUtils::ArePathStringsEqualWithCase(m_sBackslashPath, rhs.m_sBackslashPath);
525 else
527 // Assume we've got a fwdslash path and make sure that the RHS has one
528 rhs.EnsureFwdslashPathSet();
529 return CPathUtils::ArePathStringsEqualWithCase(m_sFwdslashPath, rhs.m_sFwdslashPath);
533 bool CTGitPath::IsEquivalentToWithoutCase(const CTGitPath& rhs) const
535 // Try and find a slash direction which avoids having to convert
536 // both filenames
537 if(!m_sBackslashPath.IsEmpty())
539 // *We've* got a \ path - make sure that the RHS also has a \ path
540 rhs.EnsureBackslashPathSet();
541 return CPathUtils::ArePathStringsEqual(m_sBackslashPath, rhs.m_sBackslashPath);
543 else
545 // Assume we've got a fwdslash path and make sure that the RHS has one
546 rhs.EnsureFwdslashPathSet();
547 return CPathUtils::ArePathStringsEqual(m_sFwdslashPath, rhs.m_sFwdslashPath);
551 bool CTGitPath::IsAncestorOf(const CTGitPath& possibleDescendant) const
553 possibleDescendant.EnsureBackslashPathSet();
554 EnsureBackslashPathSet();
556 if (m_sBackslashPath.IsEmpty() && PathIsRelative(possibleDescendant.m_sBackslashPath))
557 return true;
559 bool bPathStringsEqual = CPathUtils::ArePathStringsEqual(m_sBackslashPath, possibleDescendant.m_sBackslashPath.Left(m_sBackslashPath.GetLength()));
560 if (m_sBackslashPath.GetLength() >= possibleDescendant.GetWinPathString().GetLength())
562 return bPathStringsEqual;
565 return (bPathStringsEqual &&
566 ((possibleDescendant.m_sBackslashPath[m_sBackslashPath.GetLength()] == '\\')||
567 (m_sBackslashPath.GetLength()==3 && m_sBackslashPath[1]==':')));
570 // Get a string representing the file path, optionally with a base
571 // section stripped off the front.
572 CString CTGitPath::GetDisplayString(const CTGitPath* pOptionalBasePath /* = nullptr*/) const
574 EnsureFwdslashPathSet();
575 if (pOptionalBasePath)
577 // Find the length of the base-path without having to do an 'ensure' on it
578 int baseLength = max(pOptionalBasePath->m_sBackslashPath.GetLength(), pOptionalBasePath->m_sFwdslashPath.GetLength());
580 // Now, chop that baseLength of the front of the path
581 return m_sFwdslashPath.Mid(baseLength).TrimLeft('/');
583 return m_sFwdslashPath;
586 int CTGitPath::Compare(const CTGitPath& left, const CTGitPath& right)
588 left.EnsureBackslashPathSet();
589 right.EnsureBackslashPathSet();
590 return CStringUtils::FastCompareNoCase(left.m_sBackslashPath, right.m_sBackslashPath);
593 bool operator<(const CTGitPath& left, const CTGitPath& right)
595 return CTGitPath::Compare(left, right) < 0;
598 bool CTGitPath::PredLeftEquivalentToRight(const CTGitPath& left, const CTGitPath& right)
600 return left.IsEquivalentTo(right);
603 bool CTGitPath::PredLeftSameWCPathAsRight(const CTGitPath& left, const CTGitPath& right)
605 if (left.IsAdminDir() && right.IsAdminDir())
607 CTGitPath l = left;
608 CTGitPath r = right;
611 l = l.GetContainingDirectory();
612 } while(l.HasAdminDir());
615 r = r.GetContainingDirectory();
616 } while(r.HasAdminDir());
617 return l.GetContainingDirectory().IsEquivalentTo(r.GetContainingDirectory());
619 return left.GetDirectory().IsEquivalentTo(right.GetDirectory());
622 bool CTGitPath::CheckChild(const CTGitPath &parent, const CTGitPath& child)
624 return parent.IsAncestorOf(child);
627 void CTGitPath::AppendRawString(const CString& sAppend)
629 EnsureFwdslashPathSet();
630 CString strCopy = m_sFwdslashPath += sAppend;
631 SetFromUnknown(strCopy);
634 void CTGitPath::AppendPathString(const CString& sAppend)
636 EnsureBackslashPathSet();
637 CString cleanAppend(sAppend);
638 cleanAppend.Replace(L'/', L'\\');
639 cleanAppend.TrimLeft(L'\\');
640 m_sBackslashPath.TrimRight(L'\\');
641 CString strCopy = m_sBackslashPath;
642 strCopy += L'\\';
643 strCopy += cleanAppend;
644 SetFromWin(strCopy);
647 bool CTGitPath::IsWCRoot() const
649 if (m_bIsWCRootKnown)
650 return m_bIsWCRoot;
652 m_bIsWCRootKnown = true;
653 m_bIsWCRoot = false;
655 CString topDirectory;
656 if (!IsDirectory() || !HasAdminDir(&topDirectory))
657 return m_bIsWCRoot;
659 if (IsEquivalentToWithoutCase(topDirectory))
660 m_bIsWCRoot = true;
662 return m_bIsWCRoot;
665 bool CTGitPath::HasSubmodules() const
667 if (HasAdminDir())
669 CString path = m_sProjectRoot;
670 path += L"\\.gitmodules";
671 if( PathFileExists(path) )
672 return true;
674 return false;
677 int CTGitPath::GetAdminDirMask() const
679 int status = 0;
680 if (!HasAdminDir())
681 return status;
683 // ITEMIS_INGIT will be revoked if necessary in TortoiseShell/ContextMenu.cpp
684 status |= ITEMIS_INGIT|ITEMIS_INVERSIONEDFOLDER;
686 if (IsDirectory())
688 status |= ITEMIS_FOLDERINGIT;
689 if (IsWCRoot())
691 status |= ITEMIS_WCROOT;
693 if (IsRegisteredSubmoduleOfParentProject())
694 status |= ITEMIS_SUBMODULE;
698 CString dotGitPath;
699 bool isWorktree;
700 GitAdminDir::GetAdminDirPath(m_sProjectRoot, dotGitPath, &isWorktree);
701 if (HasStashDir(dotGitPath))
702 status |= ITEMIS_STASH;
704 if (PathFileExists(dotGitPath + L"svn\\.metadata"))
705 status |= ITEMIS_GITSVN;
707 if (isWorktree)
709 dotGitPath.Empty();
710 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot, dotGitPath);
713 if (PathFileExists(dotGitPath + L"BISECT_START"))
714 status |= ITEMIS_BISECT;
716 if (PathFileExists(dotGitPath + L"MERGE_HEAD"))
717 status |= ITEMIS_MERGEACTIVE;
719 if (PathFileExists(m_sProjectRoot + L"\\.gitmodules"))
720 status |= ITEMIS_SUBMODULECONTAINER;
722 return status;
725 bool CTGitPath::IsRegisteredSubmoduleOfParentProject(CString* parentProjectRoot /* nullptr */) const
727 CString topProjectDir;
728 if (!GitAdminDir::HasAdminDir(GetWinPathString(), false, &topProjectDir))
729 return false;
731 if (parentProjectRoot)
732 *parentProjectRoot = topProjectDir;
734 if (!PathFileExists(topProjectDir + L"\\.gitmodules"))
735 return false;
737 CAutoConfig config(true);
738 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(topProjectDir + L"\\.gitmodules"), GIT_CONFIG_LEVEL_APP, nullptr, FALSE);
739 CString relativePath = GetWinPathString().Mid(topProjectDir.GetLength());
740 relativePath.Replace(L'\\', L'/');
741 relativePath.Trim(L'/');
742 CStringA submodulePath = CUnicodeUtils::GetUTF8(relativePath);
743 if (git_config_foreach_match(config, "submodule\\..*\\.path", [](const git_config_entry* entry, void* data) { return static_cast<CStringA*>(data)->Compare(entry->value) == 0 ? GIT_EUSER : 0; }, &submodulePath) == GIT_EUSER)
744 return true;
745 return false;
748 bool CTGitPath::HasStashDir(const CString& dotGitPath) const
750 if (PathFileExists(dotGitPath + L"refs\\stash"))
751 return true;
753 CAutoFile hfile = CreateFile(dotGitPath + L"packed-refs", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
754 if (!hfile)
755 return false;
757 LARGE_INTEGER fileSize;
758 if (!::GetFileSizeEx(hfile, &fileSize) || fileSize.QuadPart == 0 || fileSize.QuadPart >= INT_MAX)
759 return false;
761 auto buff = std::unique_ptr<char[]>(new (std::nothrow) char[fileSize.LowPart + 1]); // prevent default initialization and throwing on allocation error
762 if (!buff)
763 return false;
764 DWORD size = 0;
765 if (!ReadFile(hfile, buff.get(), fileSize.LowPart, &size, nullptr))
766 return false;
767 buff[fileSize.LowPart] = '\0';
769 if (size != fileSize.LowPart)
770 return false;
772 for (DWORD i = 0; i < fileSize.LowPart;)
774 if (buff[i] == '#' || buff[i] == '^')
776 while (buff[i] != '\n')
778 ++i;
779 if (i == fileSize.LowPart)
780 break;
782 ++i;
785 if (i >= fileSize.LowPart)
786 break;
788 while (buff[i] != ' ')
790 ++i;
791 if (i == fileSize.LowPart)
792 break;
795 ++i;
796 if (i >= fileSize.LowPart)
797 break;
799 if (i <= fileSize.LowPart - strlen("refs/stash") && (buff[i + strlen("refs/stash")] == '\n' || buff[i + strlen("refs/stash")] == '\0') && !strncmp("refs/stash", buff.get() + i, strlen("refs/stash")))
800 return true;
801 while (buff[i] != '\n')
803 ++i;
804 if (i == fileSize.LowPart)
805 break;
808 while (buff[i] == '\n')
810 ++i;
811 if (i == fileSize.LowPart)
812 break;
815 return false;
818 bool CTGitPath::HasStashDir() const
820 if (!HasAdminDir())
821 return false;
823 CString dotGitPath;
824 GitAdminDir::GetAdminDirPath(m_sProjectRoot, dotGitPath);
826 return HasStashDir(dotGitPath);
829 bool CTGitPath::HasGitSVNDir() const
831 if (!HasAdminDir())
832 return false;
834 CString dotGitPath;
835 GitAdminDir::GetAdminDirPath(m_sProjectRoot, dotGitPath);
837 return PathFileExists(dotGitPath + L"svn\\.metadata") == TRUE;
839 bool CTGitPath::IsBisectActive() const
841 if (!HasAdminDir())
842 return false;
844 CString dotGitPath;
845 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot, dotGitPath);
847 return !!PathFileExists(dotGitPath + L"BISECT_START");
849 bool CTGitPath::IsRebaseActive() const
851 if (!HasAdminDir())
852 return false;
854 CString dotGitPath;
855 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot, dotGitPath);
857 return PathIsDirectory(dotGitPath + L"rebase-apply") || PathIsDirectory(dotGitPath + L"tgitrebase.active");
859 bool CTGitPath::IsCherryPickActive() const
861 if (!HasAdminDir())
862 return false;
864 CString dotGitPath;
865 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot, dotGitPath);
867 return !!PathFileExists(dotGitPath + L"CHERRY_PICK_HEAD");
869 bool CTGitPath::IsMergeActive() const
871 if (!HasAdminDir())
872 return false;
874 CString dotGitPath;
875 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot, dotGitPath);
877 return !!PathFileExists(dotGitPath + L"MERGE_HEAD");
879 bool CTGitPath::HasRebaseApply() const
881 if (!HasAdminDir())
882 return false;
884 CString dotGitPath;
885 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot, dotGitPath);
887 return !!PathFileExists(dotGitPath + L"rebase-apply");
889 bool CTGitPath::HasLFS() const
891 if (!HasAdminDir())
892 return false;
894 CString dotGitPath;
895 GitAdminDir::GetAdminDirPath(m_sProjectRoot, dotGitPath);
897 return PathFileExists(dotGitPath + L"lfs") == TRUE;
900 bool CTGitPath::HasAdminDir(CString* ProjectTopDir /* = nullptr */, bool force /* = false */) const
902 if (m_bHasAdminDirKnown && !force)
904 if (ProjectTopDir)
905 *ProjectTopDir = m_sProjectRoot;
906 return m_bHasAdminDir;
909 EnsureBackslashPathSet();
910 bool isAdminDir = false;
911 m_bHasAdminDir = GitAdminDir::HasAdminDir(m_sBackslashPath, IsDirectory(), &m_sProjectRoot, &isAdminDir);
912 m_bHasAdminDirKnown = true;
913 if ((m_bHasAdminDir || isAdminDir) && !m_bIsAdminDirKnown)
915 m_bIsAdminDir = isAdminDir;
916 m_bIsAdminDirKnown = true;
918 if (ProjectTopDir)
919 *ProjectTopDir = m_sProjectRoot;
920 return m_bHasAdminDir;
923 bool CTGitPath::IsAdminDir() const
925 if (m_bIsAdminDirKnown)
926 return m_bIsAdminDir;
928 EnsureBackslashPathSet();
929 m_bIsAdminDir = GitAdminDir::IsAdminDirPath(m_sBackslashPath);
930 m_bIsAdminDirKnown = true;
931 if (m_bIsAdminDir && !m_bHasAdminDirKnown)
933 m_bHasAdminDir = false;
934 m_bHasAdminDirKnown = true;
936 return m_bIsAdminDir;
939 bool CTGitPath::IsValidOnWindows() const
941 if (m_bIsValidOnWindowsKnown)
942 return m_bIsValidOnWindows;
944 m_bIsValidOnWindows = false;
945 EnsureBackslashPathSet();
946 CString sMatch = m_sBackslashPath + L"\r\n";
947 std::wstring sPattern;
948 // the 'file://' URL is just a normal windows path:
949 if (CStringUtils::StartsWithI(sMatch, L"file:\\\\"))
951 sMatch = sMatch.Mid(static_cast<int>(wcslen(L"file:\\\\")));
952 sMatch.TrimLeft(L'\\');
953 sPattern = L"^(\\\\\\\\\\?\\\\)?(([a-zA-Z]:|\\\\)\\\\)?(((\\.)|(\\.\\.)|([^\\\\/:\\*\\?\"\\|<> ](([^\\\\/:\\*\\?\"\\|<>\\. ])|([^\\\\/:\\*\\?\"\\|<>]*[^\\\\/:\\*\\?\"\\|<>\\. ]))?))\\\\)*[^\\\\/:\\*\\?\"\\|<> ](([^\\\\/:\\*\\?\"\\|<>\\. ])|([^\\\\/:\\*\\?\"\\|<>]*[^\\\\/:\\*\\?\"\\|<>\\. ]))?$";
955 else
956 sPattern = L"^(\\\\\\\\\\?\\\\)?(([a-zA-Z]:|\\\\)\\\\)?(((\\.)|(\\.\\.)|([^\\\\/:\\*\\?\"\\|<> ](([^\\\\/:\\*\\?\"\\|<>\\. ])|([^\\\\/:\\*\\?\"\\|<>]*[^\\\\/:\\*\\?\"\\|<>\\. ]))?))\\\\)*[^\\\\/:\\*\\?\"\\|<> ](([^\\\\/:\\*\\?\"\\|<>\\. ])|([^\\\\/:\\*\\?\"\\|<>]*[^\\\\/:\\*\\?\"\\|<>\\. ]))?$";
960 std::wregex rx(sPattern, std::regex_constants::icase | std::regex_constants::ECMAScript);
961 std::wsmatch match;
963 std::wstring rmatch = std::wstring(static_cast<LPCWSTR>(sMatch));
964 if (std::regex_match(rmatch, match, rx))
966 if (std::wstring(match[0]).compare(sMatch)==0)
967 m_bIsValidOnWindows = true;
969 if (m_bIsValidOnWindows)
971 // now check for illegal filenames
972 std::wregex rx2(L"\\\\(lpt\\d|com\\d|aux|nul|prn|con)(\\\\|$)", std::regex_constants::icase | std::regex_constants::ECMAScript);
973 rmatch = m_sBackslashPath;
974 if (std::regex_search(rmatch, rx2, std::regex_constants::match_default))
975 m_bIsValidOnWindows = false;
978 catch (std::exception&) {}
980 m_bIsValidOnWindowsKnown = true;
981 return m_bIsValidOnWindows;
984 //////////////////////////////////////////////////////////////////////////
986 CTGitPathList::CTGitPathList()
990 // A constructor which allows a path list to be easily built which one initial entry in
991 CTGitPathList::CTGitPathList(const CTGitPath& firstEntry)
993 AddPath(firstEntry);
996 int CTGitPathList::ParserFromLsFileSimple(BYTE_VECTOR& out, unsigned int action, bool clear /*= true*/)
998 size_t pos = 0;
999 const size_t end = out.size();
1000 CTGitPath path;
1001 CString pathstring;
1002 if (clear)
1003 Clear();
1004 while (pos < end)
1006 const size_t endOfLine = out.find('\0', pos);
1007 if (endOfLine == CGitByteArray::npos || endOfLine == pos || endOfLine - pos >= INT_MAX)
1008 return -1;
1010 pathstring.Empty();
1011 CGit::StringAppend(pathstring, &out[pos], CP_UTF8, static_cast<int>(endOfLine - pos));
1012 // SetFromGit resets the path
1013 if (CStringUtils::EndsWith(pathstring, L'/'))
1015 pathstring.Truncate(pathstring.GetLength() - 1);
1016 path.SetFromGit(pathstring, true);
1018 else
1019 path.SetFromGit(pathstring);
1021 path.m_Action = action;
1022 AddPath(path);
1024 pos = out.findNextString(endOfLine);
1026 return 0;
1029 // similar code in CGit::ParseConflictHashesFromLsFile
1030 int CTGitPathList::ParserFromLsFile(BYTE_VECTOR& out)
1032 size_t pos = 0;
1033 const size_t end = out.size();
1034 CTGitPath path;
1035 CString pathstring;
1036 this->Clear();
1037 while (pos < end)
1039 const size_t lineStart = pos;
1041 // m_Action is never used and propably never worked (needs to be set after path.SetFromGit)
1042 // also dropped LOGACTIONS_CACHE for 'H'
1043 // path.m_Action=path.ParserAction(out[pos]);
1044 pos = out.find(' ', pos); // advance to mode
1045 if (pos == CGitByteArray::npos)
1046 return -1;
1048 const size_t modestart = pos + 1;
1050 pos = out.find(' ', pos + 1); // advance to hash
1051 if (pos == CGitByteArray::npos)
1052 return -1;
1054 pos = out.find(' ', pos + 1); // advance to Stage
1055 if (pos == CGitByteArray::npos)
1056 return -1;
1058 const size_t stagestart = pos + 1;
1060 pos = out.find('\t', pos + 1); // advance to filename
1061 if (pos == CGitByteArray::npos)
1062 return -1;
1064 ++pos;
1065 const size_t fileNameEnd = out.find(0, pos);
1066 if (fileNameEnd == CGitByteArray::npos || fileNameEnd == pos || pos - lineStart != 52)
1067 return -1;
1068 pathstring.Empty();
1069 CGit::StringAppend(pathstring, &out[pos], CP_UTF8, static_cast<int>(fileNameEnd - pos));
1070 // SetFromGit resets the path
1071 path.SetFromGit(pathstring, (strtol(&out[modestart], nullptr, 8) & S_IFDIR) == S_IFDIR);
1072 if (strtol(&out[stagestart], nullptr, 10) != 0)
1074 if (!IsEmpty() && path == m_paths[m_paths.size() - 1])
1076 pos = out.findNextString(pos);
1077 continue;
1079 path.m_Action = CTGitPath::LOGACTIONS_UNMERGED;
1082 this->AddPath(path);
1084 pos=out.findNextString(pos);
1086 return 0;
1089 void CTGitPathList::UpdateStagingStatusFromPath(const CString& path, CTGitPath::StagingStatus status)
1091 for (int i = 0; i < this->GetCount(); ++i)
1093 if (CPathUtils::ArePathStringsEqualWithCase((*this)[i].GetGitPathString(), path))
1095 m_paths[i].m_stagingStatus = status;
1096 break;
1101 int CTGitPathList::FillUnRev(unsigned int action, const CTGitPathList* list, CString* err)
1103 this->Clear();
1104 CTGitPath path;
1106 const int count = [list]() {
1107 if (!list)
1108 return 1;
1109 else
1110 return list->GetCount();
1111 }();
1112 for (int i = 0; i < count; ++i)
1114 CString cmd;
1115 CString ignored;
1116 if(action & CTGitPath::LOGACTIONS_IGNORE)
1117 ignored = L" -i";
1119 if (!list)
1121 cmd = L"git.exe ls-files --exclude-standard --full-name --others -z";
1122 cmd+=ignored;
1125 else
1127 ATLASSERT(!(*list)[i].GetWinPathString().IsEmpty());
1128 cmd.Format(L"git.exe ls-files --exclude-standard --full-name --others -z%s -- \"%s\"",
1129 static_cast<LPCWSTR>(ignored),
1130 (*list)[i].GetWinPath());
1133 BYTE_VECTOR out, errb;
1134 if (g_Git.Run(cmd, &out, &errb))
1136 if (err)
1137 *err = errb;
1138 return -1;
1141 if (ParserFromLsFileSimple(out, action, false) < 0)
1142 return -1;
1144 return 0;
1147 #ifdef TGIT_LFS
1148 int CTGitPathList::FillLFSLocks(unsigned int action, CString* err)
1150 Clear();
1152 CString output;
1153 CString errCmd;
1154 if (g_Git.Run(L"git.exe lfs locks --json", &output, &errCmd, CP_UTF8) != 0)
1156 if (err)
1157 err->Append(errCmd);
1158 return -1;
1161 return ParserFromLFSLocks(action, output, err);
1164 int CTGitPathList::ParserFromLFSLocks(unsigned int action, const CString& output, CString* err)
1166 Clear();
1168 if (output.IsEmpty())
1169 return 0;
1173 auto result = json::parse(CUnicodeUtils::GetUTF8(output).GetString());
1174 for (auto& r : result)
1176 if (r["id"].get<std::string>().empty())
1177 continue;
1178 CTGitPath gitPath;
1179 gitPath.SetFromGit(CUnicodeUtils::GetUnicode(r["path"].get<std::string>().c_str()));
1180 gitPath.m_Action = action;
1181 gitPath.m_LFSLockOwner = CUnicodeUtils::GetUnicode(r["owner"]["name"].get<std::string>().c_str());
1182 AddPath(gitPath);
1185 catch (json::parse_error& ex)
1187 if (err)
1188 err->Append(CUnicodeUtils::GetUnicode(ex.what()));
1189 return -1;
1191 return 0;
1193 #endif
1195 int CTGitPathList::FillBasedOnIndexFlags(unsigned short flag, unsigned short flagextended, const CTGitPathList* list /*nullptr*/)
1197 Clear();
1198 CTGitPath path;
1200 CAutoRepository repository(g_Git.GetGitRepository());
1201 if (!repository)
1202 return -1;
1204 CAutoIndex index;
1205 if (git_repository_index(index.GetPointer(), repository))
1206 return -1;
1208 const int count = [list]() {
1209 if (!list)
1210 return 1;
1211 else
1212 return list->GetCount();
1213 }();
1214 for (int j = 0; j < count; ++j)
1216 for (size_t i = 0, ecount = git_index_entrycount(index); i < ecount; ++i)
1218 const git_index_entry *e = git_index_get_byindex(index, i);
1220 if (!e || !((e->flags & flag) || (e->flags_extended & flagextended)) || !e->path)
1221 continue;
1223 CString one = CUnicodeUtils::GetUnicode(e->path);
1225 if (!(!list || (*list)[j].GetWinPathString().IsEmpty() || one == (*list)[j].GetGitPathString() || (PathIsDirectory(g_Git.CombinePath((*list)[j].GetWinPathString())) && CStringUtils::StartsWith(one, (*list)[j].GetGitPathString() + L'/'))))
1226 continue;
1228 //SetFromGit will clear all status
1229 path.SetFromGit(one, (e->mode & S_IFDIR) == S_IFDIR);
1230 if (e->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE)
1231 path.m_Action = CTGitPath::LOGACTIONS_SKIPWORKTREE;
1232 else if (e->flags & GIT_INDEX_ENTRY_VALID)
1233 path.m_Action = CTGitPath::LOGACTIONS_ASSUMEVALID;
1234 AddPath(path);
1237 RemoveDuplicates();
1238 return 0;
1240 int CTGitPathList::ParserFromLog(BYTE_VECTOR& log)
1242 static bool mergeReplacedStatus = CRegDWORD(L"Software\\TortoiseGit\\MergeReplacedStatusKS", TRUE, false, HKEY_LOCAL_MACHINE) == TRUE; // TODO: remove kill-switch
1243 this->Clear();
1244 std::map<CString, size_t> duplicateMap;
1245 size_t pos = 0;
1246 CTGitPath path;
1247 m_Action=0;
1248 CString StatAdd;
1249 CString StatDel;
1250 CString pathname1;
1251 CString pathname2;
1253 const size_t logend = log.size();
1254 while (pos < logend)
1256 if(log[pos]==':')
1258 bool merged=false;
1259 if (pos + 1 >= logend)
1260 return -1;
1261 if (log[pos + 1] == ':')
1263 merged = true;
1264 ++pos;
1267 const size_t statusEnd = log.find('\0', pos);
1269 * There are at least two modes (each 6 characters) and two hashes (variable length [4, 40], cf. https://github.com/git/git/blob/master/environment.c#L18)
1270 * and the status (a char + optional score), each separated by space
1272 if (statusEnd == BYTE_VECTOR::npos || statusEnd - pos < ((6 + 1) + (6 + 1) + (4 + 1) + (4 + 1) + 1))
1273 return -1;
1275 const int modeOld = strtol(&log[pos + 1], nullptr, 8);
1276 const int modeNew = strtol(&log[pos + 7], nullptr, 8);
1277 // find start of status character
1278 size_t statusStart = log.find(' ', statusEnd - 6); // status: "A", "D", "U" or "C100" etc., 6 is chosen to find its start without interferring with the dst hash, see comment above
1279 if (statusStart == BYTE_VECTOR::npos)
1280 return -1;
1282 ++statusStart;
1283 pos = log.find('\0', statusStart); // advance to filename
1284 if (pos == BYTE_VECTOR::npos || statusStart == pos)
1285 return -1;
1286 ++pos;
1288 pathname2.Empty();
1289 if (log[statusStart] == 'C' || log[statusStart] == 'R')
1291 const size_t filenameEnd = log.find('\0', pos);
1292 if (filenameEnd == BYTE_VECTOR::npos || pos == filenameEnd || filenameEnd - pos >= INT_MAX)
1293 return -1;
1294 // old filename before rename
1295 CGit::StringAppend(pathname2, &log[pos], CP_UTF8, static_cast<int>(filenameEnd - pos));
1296 pos = filenameEnd + 1;
1298 const size_t filenameEnd = log.find('\0', pos);
1299 if (filenameEnd == BYTE_VECTOR::npos || pos == filenameEnd || filenameEnd - pos >= INT_MAX)
1300 return -1;
1301 pathname1.Empty();
1302 CGit::StringAppend(pathname1, &log[pos], CP_UTF8, static_cast<int>(filenameEnd - pos));
1303 pos = filenameEnd + 1;
1305 if (const auto existing = duplicateMap.find(pathname1); existing != duplicateMap.end())
1307 CTGitPath& p = m_paths[existing->second];
1308 if (!(mergeReplacedStatus && p.m_Action == CTGitPath::LOGACTIONS_REPLACED && (log[statusStart] == 'A' || log[statusStart] == 'D')))
1309 p.ParseAndUpdateStatus(log[statusStart]);
1311 // reset submodule/folder status if a staged entry is not a folder
1312 if (p.IsDirectory() && ((modeOld && !(modeOld & S_IFDIR)) || (modeNew && !(modeNew & S_IFDIR))))
1313 p.UnsetDirectoryStatus();
1314 else if (!p.IsDirectory() && (modeNew && (modeNew & S_IFDIR)))
1315 p.SetDirectoryStatus();
1317 if(merged)
1318 p.m_Action |= CTGitPath::LOGACTIONS_MERGED;
1319 m_Action |= p.m_Action;
1321 else
1323 unsigned int ac = CTGitPath::ParseStatus(log[statusStart]);
1324 ac |= merged?CTGitPath::LOGACTIONS_MERGED:0;
1326 int isSubmodule = FALSE;
1327 if (ac & (CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_UNMERGED))
1328 isSubmodule = (modeOld & S_IFDIR) == S_IFDIR;
1329 else
1330 isSubmodule = (modeNew & S_IFDIR) == S_IFDIR;
1332 // SetFromGit resets the path, hence action must be set afterwards
1333 path.SetFromGit(pathname1, &pathname2, &isSubmodule);
1334 path.m_Action=ac;
1335 this->m_Action|=ac;
1337 AddPath(path);
1338 duplicateMap.insert(std::pair<CString, size_t>(path.GetGitPathString(), m_paths.size() - 1));
1339 if (mergeReplacedStatus && !pathname2.IsEmpty())
1340 duplicateMap.insert(std::pair<CString, size_t>(path.GetGitOldPathString(), m_paths.size() - 1));
1343 else // numstat output
1345 size_t tabstart = log.find('\t', pos); // find end of first number (added lines)
1346 if (tabstart == BYTE_VECTOR::npos || tabstart - pos >= INT_MAX)
1347 return -1;
1349 StatAdd.Empty();
1350 CGit::StringAppend(StatAdd, &log[pos], CP_UTF8, static_cast<int>(tabstart - pos));
1351 pos = tabstart + 1;
1353 tabstart = log.find('\t', pos); // find end of second number (removed lines)
1354 if (tabstart == BYTE_VECTOR::npos || tabstart - pos >= INT_MAX)
1355 return -1;
1357 StatDel.Empty();
1358 CGit::StringAppend(StatDel, &log[pos], CP_UTF8, static_cast<int>(tabstart - pos));
1359 pos = tabstart + 1;
1361 if (pos >= logend)
1362 return -1;
1364 pathname2.Empty();
1365 if (log[pos] == '\0') // rename which holds an "old" pathname
1367 ++pos;
1368 const size_t endPathname = log.find('\0', pos);
1369 if (endPathname == BYTE_VECTOR::npos || pos == endPathname || endPathname - pos >= INT_MAX)
1370 return -1;
1371 CGit::StringAppend(pathname2, &log[pos], CP_UTF8, static_cast<int>(endPathname - pos));
1372 pos = endPathname + 1;
1374 const size_t endPathname = log.find('\0', pos);
1375 if (endPathname == BYTE_VECTOR::npos || pos == endPathname || endPathname - pos >= INT_MAX)
1376 return -1;
1377 pathname1.Empty();
1378 CGit::StringAppend(pathname1, &log[pos], CP_UTF8, static_cast<int>(endPathname - pos));
1379 pos = endPathname + 1;
1381 // SetFromGit resets the path
1382 int isSubmodule = FALSE;
1383 path.SetFromGit(pathname1, &pathname2, &isSubmodule);
1385 auto existing = duplicateMap.find(path.GetGitPathString());
1386 if (existing != duplicateMap.end())
1388 CTGitPath& p = m_paths[existing->second];
1389 p.m_StatAdd = StatAdd;
1390 p.m_StatDel = StatDel;
1392 else
1394 path.m_StatAdd = StatAdd;
1395 path.m_StatDel = StatDel;
1396 AddPath(path);
1397 duplicateMap.insert(std::pair<CString, size_t>(path.GetGitPathString(), m_paths.size() - 1));
1401 return 0;
1404 void CTGitPathList::AddPath(const CTGitPath& newPath)
1406 m_paths.push_back(newPath);
1407 m_commonBaseDirectory.Reset();
1409 int CTGitPathList::GetCount() const
1411 return static_cast<int>(m_paths.size());
1413 bool CTGitPathList::IsEmpty() const
1415 return m_paths.empty();
1417 void CTGitPathList::Clear()
1419 m_Action = 0;
1420 m_paths.clear();
1421 m_commonBaseDirectory.Reset();
1424 const CTGitPath& CTGitPathList::operator[](INT_PTR index) const
1426 ATLASSERT(index >= 0 && index < static_cast<INT_PTR>(m_paths.size()));
1427 return m_paths[index];
1430 bool CTGitPathList::AreAllPathsFiles() const
1432 // Look through the vector for any directories - if we find them, return false
1433 return std::none_of(m_paths.cbegin(), m_paths.cend(), std::mem_fn(&CTGitPath::IsDirectory));
1436 bool CTGitPathList::AreAllPathsDirectories() const
1438 // Look through the vector for directories - if we find none, return false
1439 return std::all_of(m_paths.cbegin(), m_paths.cend(), std::mem_fn(&CTGitPath::IsDirectory));
1442 bool CTGitPathList::IsAnyAncestorOf(const CTGitPath& possibleDescendant) const
1444 return std::any_of(m_paths.cbegin(), m_paths.cend(), [&possibleDescendant](auto& path) { return path.IsAncestorOf(possibleDescendant); });
1447 #if defined(_MFC_VER)
1449 bool CTGitPathList::LoadFromFile(const CTGitPath& filename)
1451 Clear();
1454 CString strLine;
1455 CStdioFile file(filename.GetWinPath(), CFile::typeBinary | CFile::modeRead | CFile::shareDenyWrite);
1457 // for every selected file/folder
1458 CTGitPath path;
1459 while (file.ReadString(strLine))
1461 if (strLine.IsEmpty())
1462 continue;
1463 path.SetFromUnknown(strLine);
1464 AddPath(path);
1466 file.Close();
1468 catch (CFileException* pE)
1470 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": CFileException loading target file list\n");
1471 //CString error;
1472 //pE->GetErrorMessage(CStrBuf(error, 8192), 8192);
1473 //MessageBox(nullptr, error, L"TortoiseGit", MB_ICONERROR);
1474 pE->Delete();
1475 return false;
1477 return true;
1480 bool CTGitPathList::WriteToFile(const CString& sFilename, bool bUTF8 /* = false */) const
1484 if (bUTF8)
1486 CStdioFile file(sFilename, CFile::typeText | CFile::modeReadWrite | CFile::modeCreate);
1487 for (const auto& path : m_paths)
1489 CStringA line = CStringA(path.GetGitPathString()) + '\n';
1490 file.Write(line, line.GetLength());
1492 file.Close();
1494 else
1496 CStdioFile file(sFilename, CFile::typeBinary | CFile::modeReadWrite | CFile::modeCreate);
1497 for (const auto& path : m_paths)
1498 file.WriteString(path.GetGitPathString() + L'\n');
1499 file.Close();
1502 catch (CFileException* pE)
1504 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": CFileException in writing temp file\n");
1505 pE->Delete();
1506 return false;
1508 return true;
1511 void CTGitPathList::LoadFromAsteriskSeparatedString(const CString& sPathString)
1513 int pos = 0;
1514 CString temp;
1515 for(;;)
1517 temp = sPathString.Tokenize(L"*", pos);
1518 if(temp.IsEmpty())
1519 break;
1520 AddPath(CTGitPath(CPathUtils::GetLongPathname(temp)));
1524 CString CTGitPathList::CreateAsteriskSeparatedString() const
1526 CString sRet;
1527 for (const auto& path : m_paths)
1529 if (!sRet.IsEmpty())
1530 sRet += L'*';
1531 sRet += path.GetWinPathString();
1533 return sRet;
1535 #endif // _MFC_VER
1537 bool CTGitPathList::WriteToPathSpecFile(const CString& sFilename) const
1539 CAutoFile hFile = ::CreateFile(sFilename, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, nullptr);
1540 if (!hFile)
1541 return false;
1543 constexpr char nullBuf[] = { '\0' };
1544 DWORD dwWritten = 0;
1545 for (const auto& path : m_paths)
1547 CStringA line = CUnicodeUtils::GetUTF8(path.GetGitPathString());
1548 if (!WriteFile(hFile, line.GetString(), static_cast<DWORD>(line.GetLength()), &dwWritten, nullptr))
1549 return false;
1550 if (!WriteFile(hFile, nullBuf, static_cast<DWORD>(sizeof(nullBuf)), &dwWritten, nullptr))
1551 return false;
1553 return true;
1556 bool
1557 CTGitPathList::AreAllPathsFilesInOneDirectory() const
1559 // Check if all the paths are files and in the same directory
1560 m_commonBaseDirectory.Reset();
1561 for (const auto& path : m_paths)
1563 if (path.IsDirectory())
1564 return false;
1565 const CTGitPath& baseDirectory = path.GetDirectory();
1566 if(m_commonBaseDirectory.IsEmpty())
1567 m_commonBaseDirectory = baseDirectory;
1568 else if(!m_commonBaseDirectory.IsEquivalentTo(baseDirectory))
1570 // Different path
1571 m_commonBaseDirectory.Reset();
1572 return false;
1575 return true;
1578 CTGitPath CTGitPathList::GetCommonDirectory() const
1580 if (m_commonBaseDirectory.IsEmpty())
1582 for (const auto& path : m_paths)
1584 const CTGitPath& baseDirectory = path.GetDirectory();
1585 if(m_commonBaseDirectory.IsEmpty())
1586 m_commonBaseDirectory = baseDirectory;
1587 else if(!m_commonBaseDirectory.IsEquivalentTo(baseDirectory))
1589 // Different path
1590 m_commonBaseDirectory.Reset();
1591 break;
1595 // since we only checked strings, not paths,
1596 // we have to make sure now that we really return a *path* here
1597 if (std::any_of(m_paths.cbegin(), m_paths.cend(), [&m_commonBaseDirectory = m_commonBaseDirectory](auto& path) { return !m_commonBaseDirectory.IsAncestorOf(path); }))
1598 m_commonBaseDirectory = m_commonBaseDirectory.GetContainingDirectory();
1599 return m_commonBaseDirectory;
1602 CTGitPath CTGitPathList::GetCommonRoot() const
1604 if (IsEmpty())
1605 return CTGitPath();
1607 if (GetCount() == 1)
1608 return m_paths[0];
1610 // first entry is common root for itself
1611 // (add trailing '\\' to detect partial matches of the last path element)
1612 CString root = m_paths[0].GetWinPathString() + L'\\';
1613 int rootLength = root.GetLength();
1615 // determine common path string prefix
1616 for (auto it = m_paths.cbegin() + 1; it != m_paths.cend(); ++it)
1618 CString path = it->GetWinPathString() + L'\\';
1620 int newLength = CStringUtils::GetMatchingLength(root, path);
1621 if (newLength != rootLength)
1623 root.Delete(newLength, rootLength);
1624 rootLength = newLength;
1628 // remove the last (partial) path element
1629 if (rootLength > 0)
1630 root.Delete(root.ReverseFind(L'\\'), rootLength);
1632 // done
1633 return CTGitPath(root);
1636 void CTGitPathList::SortByPathname(bool bReverse /*= false*/)
1638 std::sort(m_paths.begin(), m_paths.end());
1639 if (bReverse)
1640 std::reverse(m_paths.begin(), m_paths.end());
1643 void CTGitPathList::DeleteAllFiles(bool bTrash, bool bFilesOnly, bool bShowErrorUI)
1645 if (m_paths.empty())
1646 return;
1647 PathVector::const_iterator it;
1648 SortByPathname(true); // nested ones first
1650 CString sPaths;
1651 for (it = m_paths.cbegin(); it != m_paths.cend(); ++it)
1653 if ((it->Exists()) && ((it->IsDirectory() != bFilesOnly) || !bFilesOnly))
1655 if (!it->IsDirectory())
1656 ::SetFileAttributes(it->GetWinPath(), FILE_ATTRIBUTE_NORMAL);
1658 sPaths += it->GetWinPath();
1659 sPaths += '\0';
1662 if (sPaths.IsEmpty())
1663 return;
1664 sPaths += '\0';
1665 sPaths += '\0';
1666 DeleteViaShell(static_cast<LPCWSTR>(sPaths), bTrash, bShowErrorUI);
1667 Clear();
1670 bool CTGitPathList::DeleteViaShell(LPCWSTR path, bool bTrash, bool bShowErrorUI)
1672 SHFILEOPSTRUCT shop = {0};
1673 shop.wFunc = FO_DELETE;
1674 shop.pFrom = path;
1675 shop.fFlags = FOF_NOCONFIRMATION|FOF_NO_CONNECTED_ELEMENTS;
1676 if (!bShowErrorUI)
1677 shop.fFlags |= FOF_NOERRORUI | FOF_SILENT;
1678 if (bTrash)
1679 shop.fFlags |= FOF_ALLOWUNDO;
1680 const bool bRet = (SHFileOperation(&shop) == 0);
1681 return bRet;
1684 void CTGitPathList::RemoveDuplicates()
1686 SortByPathname();
1687 // Remove the duplicates
1688 // (Unique moves them to the end of the vector, then erase chops them off)
1689 m_paths.erase(std::unique(m_paths.begin(), m_paths.end(), &CTGitPath::PredLeftEquivalentToRight), m_paths.end());
1692 void CTGitPathList::RemoveAdminPaths()
1694 PathVector::iterator it;
1695 for(it = m_paths.begin(); it != m_paths.end(); )
1697 if (it->IsAdminDir())
1699 m_paths.erase(it);
1700 it = m_paths.begin();
1702 else
1703 ++it;
1707 void CTGitPathList::RemovePath(const CTGitPath& path)
1709 PathVector::iterator it;
1710 for(it = m_paths.begin(); it != m_paths.end(); ++it)
1712 if (it->IsEquivalentTo(path))
1714 m_paths.erase(it);
1715 return;
1720 void CTGitPathList::RemoveItem(const CTGitPath& path)
1722 PathVector::iterator it;
1723 for(it = m_paths.begin(); it != m_paths.end(); ++it)
1725 if (CPathUtils::ArePathStringsEqualWithCase(it->GetGitPathString(), path.GetGitPathString()))
1727 m_paths.erase(it);
1728 return;
1732 void CTGitPathList::RemoveChildren()
1734 SortByPathname();
1735 m_paths.erase(std::unique(m_paths.begin(), m_paths.end(), &CTGitPath::CheckChild), m_paths.end());
1738 bool CTGitPathList::IsEqual(const CTGitPathList& list)
1740 if (list.GetCount() != GetCount())
1741 return false;
1742 for (int i=0; i<list.GetCount(); ++i)
1744 if (!list[i].IsEquivalentTo(m_paths[i]))
1745 return false;
1747 return true;
1750 const CTGitPath* CTGitPathList::LookForGitPath(const CString& path) const
1752 for (int i = 0; i < this->GetCount(); ++i)
1754 if (CPathUtils::ArePathStringsEqualWithCase((*this)[i].GetGitPathString(), path))
1755 return &(*this)[i];
1757 return nullptr;
1760 CString CTGitPath::GetActionName(unsigned int action)
1762 if(action & CTGitPath::LOGACTIONS_UNMERGED)
1763 return MAKEINTRESOURCE(IDS_PATHACTIONS_CONFLICT);
1764 if(action & CTGitPath::LOGACTIONS_ADDED)
1765 return MAKEINTRESOURCE(IDS_PATHACTIONS_ADD);
1766 if (action & CTGitPath::LOGACTIONS_MISSING)
1767 return MAKEINTRESOURCE(IDS_PATHACTIONS_MISSING);
1768 if(action & CTGitPath::LOGACTIONS_DELETED)
1769 return MAKEINTRESOURCE(IDS_PATHACTIONS_DELETE);
1770 if(action & CTGitPath::LOGACTIONS_MERGED )
1771 return MAKEINTRESOURCE(IDS_PATHACTIONS_MERGED);
1773 if(action & CTGitPath::LOGACTIONS_MODIFIED)
1774 return MAKEINTRESOURCE(IDS_PATHACTIONS_MODIFIED);
1775 if(action & CTGitPath::LOGACTIONS_REPLACED)
1776 return MAKEINTRESOURCE(IDS_PATHACTIONS_RENAME);
1777 if(action & CTGitPath::LOGACTIONS_COPY)
1778 return MAKEINTRESOURCE(IDS_PATHACTIONS_COPY);
1780 if (action & CTGitPath::LOGACTIONS_ASSUMEVALID)
1781 return MAKEINTRESOURCE(IDS_PATHACTIONS_ASSUMEUNCHANGED);
1782 if (action & CTGitPath::LOGACTIONS_SKIPWORKTREE)
1783 return MAKEINTRESOURCE(IDS_PATHACTIONS_SKIPWORKTREE);
1785 if (action & CTGitPath::LOGACTIONS_IGNORE)
1786 return MAKEINTRESOURCE(IDS_PATHACTIONS_IGNORED);
1788 return MAKEINTRESOURCE(IDS_PATHACTIONS_UNKNOWN);
1791 CString CTGitPath::GetActionName() const
1793 return GetActionName(m_Action);
1796 unsigned int CTGitPathList::GetAction()
1798 return m_Action;
1801 CString CTGitPath::GetAbbreviatedRename() const
1803 if (GetGitOldPathString().IsEmpty())
1804 return GetFileOrDirectoryName();
1806 // Find common prefix which ends with a slash
1807 int prefix_length = 0;
1808 for (int i = 0, maxLength = min(m_sOldFwdslashPath.GetLength(), m_sFwdslashPath.GetLength()); i < maxLength; ++i)
1810 if (m_sOldFwdslashPath[i] != m_sFwdslashPath[i])
1811 break;
1812 if (m_sOldFwdslashPath[i] == L'/')
1813 prefix_length = i + 1;
1816 LPCWSTR oldName = static_cast<LPCWSTR>(m_sOldFwdslashPath) + m_sOldFwdslashPath.GetLength();
1817 LPCWSTR newName = static_cast<LPCWSTR>(m_sFwdslashPath) + m_sFwdslashPath.GetLength();
1819 int suffix_length = 0;
1820 int prefix_adjust_for_slash = (prefix_length ? 1 : 0);
1821 while (static_cast<LPCWSTR>(m_sOldFwdslashPath) + prefix_length - prefix_adjust_for_slash <= oldName &&
1822 static_cast<LPCWSTR>(m_sFwdslashPath) + prefix_length - prefix_adjust_for_slash <= newName &&
1823 *oldName == *newName)
1825 if (*oldName == L'/')
1826 suffix_length = m_sOldFwdslashPath.GetLength() - static_cast<int>(oldName - static_cast<LPCWSTR>(m_sOldFwdslashPath));
1827 --oldName;
1828 --newName;
1832 * pfx{old_midlen => new_midlen}sfx
1833 * {pfx-old => pfx-new}sfx
1834 * pfx{sfx-old => sfx-new}
1835 * name-old => name-new
1837 int old_midlen = m_sOldFwdslashPath.GetLength() - prefix_length - suffix_length;
1838 int new_midlen = m_sFwdslashPath.GetLength() - prefix_length - suffix_length;
1839 if (old_midlen < 0)
1840 old_midlen = 0;
1841 if (new_midlen < 0)
1842 new_midlen = 0;
1844 CString ret;
1845 if (prefix_length + suffix_length)
1847 ret = m_sOldFwdslashPath.Left(prefix_length);
1848 ret += L'{';
1850 ret += m_sOldFwdslashPath.Mid(prefix_length, old_midlen);
1851 ret += L" => ";
1852 ret += m_sFwdslashPath.Mid(prefix_length, new_midlen);
1853 if (prefix_length + suffix_length)
1855 ret += L'}';
1856 ret += m_sFwdslashPath.Mid(m_sFwdslashPath.GetLength() - suffix_length, suffix_length);
1858 return ret;