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.
22 #include "UnicodeUtils.h"
23 #include "GitAdminDir.h"
24 #include "PathUtils.h"
27 #include "../TortoiseShell/Globals.h"
28 #include "StringUtils.h"
29 #include "SmartHandle.h"
30 #include "../Resources/LoglistCommonResource.h"
34 #include "nlohmann/json.hpp"
35 using json
= nlohmann::json
;
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
)
64 return LOGACTIONS_MODIFIED
;
66 return LOGACTIONS_REPLACED
;
68 return LOGACTIONS_ADDED
;
70 return LOGACTIONS_DELETED
;
72 return LOGACTIONS_UNMERGED
;
74 return LOGACTIONS_DELETED
;
76 return LOGACTIONS_COPY
;
78 return LOGACTIONS_MODIFIED
;
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
;
104 void CTGitPath::SetFromGit(const char* pPath
)
110 m_sFwdslashPath
= CUnicodeUtils::GetUnicode(pPath
);
111 SanitizeRootPath(m_sFwdslashPath
, true);
114 void CTGitPath::SetFromGit(const char* pPath
, bool bIsDirectory
)
117 m_bDirectoryKnown
= true;
118 m_bIsDirectory
= bIsDirectory
;
121 void CTGitPath::SetFromGit(const wchar_t* pPath
, bool bIsDirectory
)
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
)
136 m_sFwdslashPath
= sPath
;
137 SanitizeRootPath(m_sFwdslashPath
, true);
140 m_bDirectoryKnown
= true;
141 m_bIsDirectory
= *bIsDirectory
!= FALSE
;
144 m_sOldFwdslashPath
= *oldpath
;
147 void CTGitPath::SetFromWin(LPCWSTR pPath
)
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
)
158 m_sBackslashPath
= sPath
;
159 m_sBackslashPath
.Replace(L
"\\\\?\\", L
"");
160 SanitizeRootPath(m_sBackslashPath
, false);
162 void CTGitPath::SetFromWin(LPCWSTR pPath
, bool bIsDirectory
)
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
)
173 m_sBackslashPath
= sPath
;
174 m_bIsDirectory
= bIsDirectory
;
175 m_bDirectoryKnown
= true;
176 SanitizeRootPath(m_sBackslashPath
, false);
178 void CTGitPath::SetFromUnknown(const CString
& sPath
)
181 // Just set whichever path we think is most likely to be used
182 // GitAdminDir admin;
184 // if(admin.HasAdminDir(sPath,&p))
185 // SetFwdslashPath(sPath.Right(sPath.GetLength()-p.GetLength()));
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
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();
234 void CTGitPath::SetFwdslashPath(const CString
& sPath
) const
236 CString path
= sPath
;
237 path
.Replace('\\', '/');
239 // We don't leave a trailing /
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
)
269 return m_bIsDirectory
;
272 bool CTGitPath::Exists() const
279 bool CTGitPath::Delete(bool bTrash
, bool bShowErrorUI
) const
281 EnsureBackslashPathSet();
282 ::SetFileAttributes(m_sBackslashPath
, FILE_ATTRIBUTE_NORMAL
);
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
);
295 bRet
= !!::DeleteFile(m_sBackslashPath
);
298 m_bExistsKnown
= true;
302 __int64
CTGitPath::GetLastWriteTime(bool force
/* = false */) const
304 if (!m_bLastWriteTimeKnown
|| force
)
306 return m_lastWriteTime
;
309 __int64
CTGitPath::GetFileSize() const
311 if(!m_bDirectoryKnown
)
316 bool CTGitPath::IsReadOnly() const
318 if(!m_bLastWriteTimeKnown
)
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
;
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
;
347 m_fileSize
= static_cast<__int64
>(attribs
.nFileSizeHigh
) << 32 | attribs
.nFileSizeLow
;
348 m_bIsReadOnly
= !!(attribs
.dwFileAttributes
& FILE_ATTRIBUTE_READONLY
);
353 m_bIsDirectory
= false;
356 DWORD err
= GetLastError();
357 if ((err
== ERROR_FILE_NOT_FOUND
)||(err
== ERROR_PATH_NOT_FOUND
)||(err
== ERROR_INVALID_NAME
))
365 m_bDirectoryKnown
= true;
366 m_bLastWriteTimeKnown
= true;
367 m_bExistsKnown
= true;
370 CTGitPath
CTGitPath::GetSubPath(const CTGitPath
& root
) const
374 if (CStringUtils::StartsWith(GetWinPathString(), root
.GetWinPathString()))
376 CString str
=GetWinPathString();
377 path
.SetFromWin(str
.Right(str
.GetLength()-root
.GetWinPathString().GetLength()-1));
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();
414 m_sProjectRoot
.Empty();
415 m_sOldFwdslashPath
.Empty();
418 this->m_StatAdd
.Empty();
419 this->m_StatDel
.Empty();
421 m_stagingStatus
= CTGitPath::StagingStatus::DontCare
;
422 ATLASSERT(IsEmpty());
425 CTGitPath
CTGitPath::GetDirectory() const
427 if ((IsDirectory())||(!Exists()))
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
441 if(sDirName
== m_sBackslashPath
)
443 // We were clearly provided with a root path to start with - we should return nothing now
447 if(sDirName
.GetLength() == 1 && sDirName
[0] == '\\')
449 // We have an UNC path and we already are the root
453 retVal
.SetFromWin(sDirName
);
457 CString
CTGitPath::GetRootPathString() const
459 EnsureBackslashPathSet();
460 CString workingPath
= m_sBackslashPath
;
461 ATLVERIFY(::PathStripToRoot(CStrBuf(workingPath
, MAX_PATH
))); // MAX_PATH ok here.
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
481 return m_sUIPath
.Mid(m_sUIPath
.ReverseFind('\\')+1);
484 CString
CTGitPath::GetFileExtension() const
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
);
496 CString
CTGitPath::GetBaseFilename() const
498 CString filename
=GetFilename();
499 const int dot
= filename
.ReverseFind(L
'.');
501 filename
.Truncate(dot
);
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
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
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
);
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
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
);
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
))
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())
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
;
643 strCopy
+= cleanAppend
;
647 bool CTGitPath::IsWCRoot() const
649 if (m_bIsWCRootKnown
)
652 m_bIsWCRootKnown
= true;
655 CString topDirectory
;
656 if (!IsDirectory() || !HasAdminDir(&topDirectory
))
659 if (IsEquivalentToWithoutCase(topDirectory
))
665 bool CTGitPath::HasSubmodules() const
669 CString path
= m_sProjectRoot
;
670 path
+= L
"\\.gitmodules";
671 if( PathFileExists(path
) )
677 int CTGitPath::GetAdminDirMask() const
683 // ITEMIS_INGIT will be revoked if necessary in TortoiseShell/ContextMenu.cpp
684 status
|= ITEMIS_INGIT
|ITEMIS_INVERSIONEDFOLDER
;
688 status
|= ITEMIS_FOLDERINGIT
;
691 status
|= ITEMIS_WCROOT
;
693 if (IsRegisteredSubmoduleOfParentProject())
694 status
|= ITEMIS_SUBMODULE
;
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
;
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
;
725 bool CTGitPath::IsRegisteredSubmoduleOfParentProject(CString
* parentProjectRoot
/* nullptr */) const
727 CString topProjectDir
;
728 if (!GitAdminDir::HasAdminDir(GetWinPathString(), false, &topProjectDir
))
731 if (parentProjectRoot
)
732 *parentProjectRoot
= topProjectDir
;
734 if (!PathFileExists(topProjectDir
+ L
"\\.gitmodules"))
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
)
748 bool CTGitPath::HasStashDir(const CString
& dotGitPath
) const
750 if (PathFileExists(dotGitPath
+ L
"refs\\stash"))
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);
757 LARGE_INTEGER fileSize
;
758 if (!::GetFileSizeEx(hfile
, &fileSize
) || fileSize
.QuadPart
== 0 || fileSize
.QuadPart
>= INT_MAX
)
761 auto buff
= std::unique_ptr
<char[]>(new (std::nothrow
) char[fileSize
.LowPart
+ 1]); // prevent default initialization and throwing on allocation error
765 if (!ReadFile(hfile
, buff
.get(), fileSize
.LowPart
, &size
, nullptr))
767 buff
[fileSize
.LowPart
] = '\0';
769 if (size
!= fileSize
.LowPart
)
772 for (DWORD i
= 0; i
< fileSize
.LowPart
;)
774 if (buff
[i
] == '#' || buff
[i
] == '^')
776 while (buff
[i
] != '\n')
779 if (i
== fileSize
.LowPart
)
785 if (i
>= fileSize
.LowPart
)
788 while (buff
[i
] != ' ')
791 if (i
== fileSize
.LowPart
)
796 if (i
>= fileSize
.LowPart
)
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")))
801 while (buff
[i
] != '\n')
804 if (i
== fileSize
.LowPart
)
808 while (buff
[i
] == '\n')
811 if (i
== fileSize
.LowPart
)
818 bool CTGitPath::HasStashDir() const
824 GitAdminDir::GetAdminDirPath(m_sProjectRoot
, dotGitPath
);
826 return HasStashDir(dotGitPath
);
829 bool CTGitPath::HasGitSVNDir() const
835 GitAdminDir::GetAdminDirPath(m_sProjectRoot
, dotGitPath
);
837 return PathFileExists(dotGitPath
+ L
"svn\\.metadata") == TRUE
;
839 bool CTGitPath::IsBisectActive() const
845 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot
, dotGitPath
);
847 return !!PathFileExists(dotGitPath
+ L
"BISECT_START");
849 bool CTGitPath::IsRebaseActive() const
855 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot
, dotGitPath
);
857 return PathIsDirectory(dotGitPath
+ L
"rebase-apply") || PathIsDirectory(dotGitPath
+ L
"tgitrebase.active");
859 bool CTGitPath::IsCherryPickActive() const
865 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot
, dotGitPath
);
867 return !!PathFileExists(dotGitPath
+ L
"CHERRY_PICK_HEAD");
869 bool CTGitPath::IsMergeActive() const
875 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot
, dotGitPath
);
877 return !!PathFileExists(dotGitPath
+ L
"MERGE_HEAD");
879 bool CTGitPath::HasRebaseApply() const
885 GitAdminDir::GetWorktreeAdminDirPath(m_sProjectRoot
, dotGitPath
);
887 return !!PathFileExists(dotGitPath
+ L
"rebase-apply");
889 bool CTGitPath::HasLFS() const
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
)
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;
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]:|\\\\)\\\\)?(((\\.)|(\\.\\.)|([^\\\\/:\\*\\?\"\\|<> ](([^\\\\/:\\*\\?\"\\|<>\\. ])|([^\\\\/:\\*\\?\"\\|<>]*[^\\\\/:\\*\\?\"\\|<>\\. ]))?))\\\\)*[^\\\\/:\\*\\?\"\\|<> ](([^\\\\/:\\*\\?\"\\|<>\\. ])|([^\\\\/:\\*\\?\"\\|<>]*[^\\\\/:\\*\\?\"\\|<>\\. ]))?$";
956 sPattern
= L
"^(\\\\\\\\\\?\\\\)?(([a-zA-Z]:|\\\\)\\\\)?(((\\.)|(\\.\\.)|([^\\\\/:\\*\\?\"\\|<> ](([^\\\\/:\\*\\?\"\\|<>\\. ])|([^\\\\/:\\*\\?\"\\|<>]*[^\\\\/:\\*\\?\"\\|<>\\. ]))?))\\\\)*[^\\\\/:\\*\\?\"\\|<> ](([^\\\\/:\\*\\?\"\\|<>\\. ])|([^\\\\/:\\*\\?\"\\|<>]*[^\\\\/:\\*\\?\"\\|<>\\. ]))?$";
960 std::wregex
rx(sPattern
, std::regex_constants::icase
| std::regex_constants::ECMAScript
);
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
)
996 int CTGitPathList::ParserFromLsFileSimple(BYTE_VECTOR
& out
, unsigned int action
, bool clear
/*= true*/)
999 const size_t end
= out
.size();
1006 const size_t endOfLine
= out
.find('\0', pos
);
1007 if (endOfLine
== CGitByteArray::npos
|| endOfLine
== pos
|| endOfLine
- pos
>= INT_MAX
)
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);
1019 path
.SetFromGit(pathstring
);
1021 path
.m_Action
= action
;
1024 pos
= out
.findNextString(endOfLine
);
1029 // similar code in CGit::ParseConflictHashesFromLsFile
1030 int CTGitPathList::ParserFromLsFile(BYTE_VECTOR
& out
)
1033 const size_t end
= out
.size();
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
)
1048 const size_t modestart
= pos
+ 1;
1050 pos
= out
.find(' ', pos
+ 1); // advance to hash
1051 if (pos
== CGitByteArray::npos
)
1054 pos
= out
.find(' ', pos
+ 1); // advance to Stage
1055 if (pos
== CGitByteArray::npos
)
1058 const size_t stagestart
= pos
+ 1;
1060 pos
= out
.find('\t', pos
+ 1); // advance to filename
1061 if (pos
== CGitByteArray::npos
)
1065 const size_t fileNameEnd
= out
.find(0, pos
);
1066 if (fileNameEnd
== CGitByteArray::npos
|| fileNameEnd
== pos
|| pos
- lineStart
!= 52)
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
);
1079 path
.m_Action
= CTGitPath::LOGACTIONS_UNMERGED
;
1082 this->AddPath(path
);
1084 pos
=out
.findNextString(pos
);
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
;
1101 int CTGitPathList::FillUnRev(unsigned int action
, const CTGitPathList
* list
, CString
* err
)
1106 const int count
= [list
]() {
1110 return list
->GetCount();
1112 for (int i
= 0; i
< count
; ++i
)
1116 if(action
& CTGitPath::LOGACTIONS_IGNORE
)
1121 cmd
= L
"git.exe ls-files --exclude-standard --full-name --others -z";
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
))
1141 if (ParserFromLsFileSimple(out
, action
, false) < 0)
1148 int CTGitPathList::FillLFSLocks(unsigned int action
, CString
* err
)
1154 if (g_Git
.Run(L
"git.exe lfs locks --json", &output
, &errCmd
, CP_UTF8
) != 0)
1157 err
->Append(errCmd
);
1161 return ParserFromLFSLocks(action
, output
, err
);
1164 int CTGitPathList::ParserFromLFSLocks(unsigned int action
, const CString
& output
, CString
* err
)
1168 if (output
.IsEmpty())
1173 auto result
= json::parse(CUnicodeUtils::GetUTF8(output
).GetString());
1174 for (auto& r
: result
)
1176 if (r
["id"].get
<std::string
>().empty())
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());
1185 catch (json::parse_error
& ex
)
1188 err
->Append(CUnicodeUtils::GetUnicode(ex
.what()));
1195 int CTGitPathList::FillBasedOnIndexFlags(unsigned short flag
, unsigned short flagextended
, const CTGitPathList
* list
/*nullptr*/)
1200 CAutoRepository
repository(g_Git
.GetGitRepository());
1205 if (git_repository_index(index
.GetPointer(), repository
))
1208 const int count
= [list
]() {
1212 return list
->GetCount();
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
)
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
'/'))))
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
;
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
1244 std::map
<CString
, size_t> duplicateMap
;
1253 const size_t logend
= log
.size();
1254 while (pos
< logend
)
1259 if (pos
+ 1 >= logend
)
1261 if (log
[pos
+ 1] == ':')
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))
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
)
1283 pos
= log
.find('\0', statusStart
); // advance to filename
1284 if (pos
== BYTE_VECTOR::npos
|| statusStart
== pos
)
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
)
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
)
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();
1318 p
.m_Action
|= CTGitPath::LOGACTIONS_MERGED
;
1319 m_Action
|= p
.m_Action
;
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
;
1330 isSubmodule
= (modeNew
& S_IFDIR
) == S_IFDIR
;
1332 // SetFromGit resets the path, hence action must be set afterwards
1333 path
.SetFromGit(pathname1
, &pathname2
, &isSubmodule
);
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
)
1350 CGit::StringAppend(StatAdd
, &log
[pos
], CP_UTF8
, static_cast<int>(tabstart
- pos
));
1353 tabstart
= log
.find('\t', pos
); // find end of second number (removed lines)
1354 if (tabstart
== BYTE_VECTOR::npos
|| tabstart
- pos
>= INT_MAX
)
1358 CGit::StringAppend(StatDel
, &log
[pos
], CP_UTF8
, static_cast<int>(tabstart
- pos
));
1365 if (log
[pos
] == '\0') // rename which holds an "old" pathname
1368 const size_t endPathname
= log
.find('\0', pos
);
1369 if (endPathname
== BYTE_VECTOR::npos
|| pos
== endPathname
|| endPathname
- pos
>= INT_MAX
)
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
)
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
;
1394 path
.m_StatAdd
= StatAdd
;
1395 path
.m_StatDel
= StatDel
;
1397 duplicateMap
.insert(std::pair
<CString
, size_t>(path
.GetGitPathString(), m_paths
.size() - 1));
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()
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
)
1455 CStdioFile
file(filename
.GetWinPath(), CFile::typeBinary
| CFile::modeRead
| CFile::shareDenyWrite
);
1457 // for every selected file/folder
1459 while (file
.ReadString(strLine
))
1461 if (strLine
.IsEmpty())
1463 path
.SetFromUnknown(strLine
);
1468 catch (CFileException
* pE
)
1470 CTraceToOutputDebugString::Instance()(__FUNCTION__
": CFileException loading target file list\n");
1472 //pE->GetErrorMessage(CStrBuf(error, 8192), 8192);
1473 //MessageBox(nullptr, error, L"TortoiseGit", MB_ICONERROR);
1480 bool CTGitPathList::WriteToFile(const CString
& sFilename
, bool bUTF8
/* = false */) const
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());
1496 CStdioFile
file(sFilename
, CFile::typeBinary
| CFile::modeReadWrite
| CFile::modeCreate
);
1497 for (const auto& path
: m_paths
)
1498 file
.WriteString(path
.GetGitPathString() + L
'\n');
1502 catch (CFileException
* pE
)
1504 CTraceToOutputDebugString::Instance()(__FUNCTION__
": CFileException in writing temp file\n");
1511 void CTGitPathList::LoadFromAsteriskSeparatedString(const CString
& sPathString
)
1517 temp
= sPathString
.Tokenize(L
"*", pos
);
1520 AddPath(CTGitPath(CPathUtils::GetLongPathname(temp
)));
1524 CString
CTGitPathList::CreateAsteriskSeparatedString() const
1527 for (const auto& path
: m_paths
)
1529 if (!sRet
.IsEmpty())
1531 sRet
+= path
.GetWinPathString();
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);
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))
1550 if (!WriteFile(hFile
, nullBuf
, static_cast<DWORD
>(sizeof(nullBuf
)), &dwWritten
, nullptr))
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())
1565 const CTGitPath
& baseDirectory
= path
.GetDirectory();
1566 if(m_commonBaseDirectory
.IsEmpty())
1567 m_commonBaseDirectory
= baseDirectory
;
1568 else if(!m_commonBaseDirectory
.IsEquivalentTo(baseDirectory
))
1571 m_commonBaseDirectory
.Reset();
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
))
1590 m_commonBaseDirectory
.Reset();
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
1607 if (GetCount() == 1)
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
1630 root
.Delete(root
.ReverseFind(L
'\\'), rootLength
);
1633 return CTGitPath(root
);
1636 void CTGitPathList::SortByPathname(bool bReverse
/*= false*/)
1638 std::sort(m_paths
.begin(), m_paths
.end());
1640 std::reverse(m_paths
.begin(), m_paths
.end());
1643 void CTGitPathList::DeleteAllFiles(bool bTrash
, bool bFilesOnly
, bool bShowErrorUI
)
1645 if (m_paths
.empty())
1647 PathVector::const_iterator it
;
1648 SortByPathname(true); // nested ones first
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();
1662 if (sPaths
.IsEmpty())
1666 DeleteViaShell(static_cast<LPCWSTR
>(sPaths
), bTrash
, bShowErrorUI
);
1670 bool CTGitPathList::DeleteViaShell(LPCWSTR path
, bool bTrash
, bool bShowErrorUI
)
1672 SHFILEOPSTRUCT shop
= {0};
1673 shop
.wFunc
= FO_DELETE
;
1675 shop
.fFlags
= FOF_NOCONFIRMATION
|FOF_NO_CONNECTED_ELEMENTS
;
1677 shop
.fFlags
|= FOF_NOERRORUI
| FOF_SILENT
;
1679 shop
.fFlags
|= FOF_ALLOWUNDO
;
1680 const bool bRet
= (SHFileOperation(&shop
) == 0);
1684 void CTGitPathList::RemoveDuplicates()
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())
1700 it
= m_paths
.begin();
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
))
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()))
1732 void CTGitPathList::RemoveChildren()
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())
1742 for (int i
=0; i
<list
.GetCount(); ++i
)
1744 if (!list
[i
].IsEquivalentTo(m_paths
[i
]))
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
))
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()
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
])
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
));
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
;
1845 if (prefix_length
+ suffix_length
)
1847 ret
= m_sOldFwdslashPath
.Left(prefix_length
);
1850 ret
+= m_sOldFwdslashPath
.Mid(prefix_length
, old_midlen
);
1852 ret
+= m_sFwdslashPath
.Mid(prefix_length
, new_midlen
);
1853 if (prefix_length
+ suffix_length
)
1856 ret
+= m_sFwdslashPath
.Mid(m_sFwdslashPath
.GetLength() - suffix_length
, suffix_length
);