2025-04-14 14:47:22 +03:00

462 lines
13 KiB
C++

// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com)
//
// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt
// or copy at http://opensource.org/licenses/MIT)
#include "GitSourceControlState.h"
#if ENGINE_MAJOR_VERSION >= 5
#include "Textures/SlateIcon.h"
#if ENGINE_MINOR_VERSION >= 2
#include "RevisionControlStyle/RevisionControlStyle.h"
#endif
#endif
#define LOCTEXT_NAMESPACE "GitSourceControl.State"
int32 FGitSourceControlState::GetHistorySize() const
{
return History.Num();
}
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::GetHistoryItem( int32 HistoryIndex ) const
{
check(History.IsValidIndex(HistoryIndex));
return History[HistoryIndex];
}
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::FindHistoryRevision(int32 RevisionNumber) const
{
for (auto Iter(History.CreateConstIterator()); Iter; Iter++)
{
if ((*Iter)->GetRevisionNumber() == RevisionNumber)
{
return *Iter;
}
}
return nullptr;
}
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::FindHistoryRevision(const FString& InRevision) const
{
for (const auto& Revision : History)
{
if (Revision->GetRevision() == InRevision)
{
return Revision;
}
}
return nullptr;
}
#if ENGINE_MAJOR_VERSION < 5 || ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 3
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::GetBaseRevForMerge() const
{
for(const auto& Revision : History)
{
// look for the the SHA1 id of the file, not the commit id (revision)
if (Revision->FileHash == PendingMergeBaseFileHash)
{
return Revision;
}
}
return nullptr;
}
#endif
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::GetCurrentRevision() const
{
return nullptr;
}
#endif
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3
ISourceControlState::FResolveInfo FGitSourceControlState::GetResolveInfo() const
{
return PendingResolveInfo;
}
#endif
// @todo add Slate icons for git specific states (NotAtHead vs Conflicted...)
#if ENGINE_MAJOR_VERSION < 5
#define GET_ICON_RETURN( NAME ) FName( "ContentBrowser.SCC_" #NAME )
FName FGitSourceControlState::GetIconName() const
{
#else
#if ENGINE_MINOR_VERSION >= 2
#define GET_ICON_RETURN( NAME ) FSlateIcon(FRevisionControlStyleManager::GetStyleSetName(), "RevisionControl." #NAME )
#else
#define GET_ICON_RETURN( NAME ) FSlateIcon(FAppStyle::GetAppStyleSetName(), "Perforce." #NAME )
#endif
FSlateIcon FGitSourceControlState::GetIcon() const
{
#endif
switch (GetGitState())
{
case EGitState::NotAtHead:
return GET_ICON_RETURN(NotAtHeadRevision);
case EGitState::LockedOther:
return GET_ICON_RETURN(CheckedOutByOtherUser);
case EGitState::NotLatest:
return GET_ICON_RETURN(ModifiedOtherBranch);
case EGitState::Unmerged:
return GET_ICON_RETURN(Branched);
case EGitState::Added:
return GET_ICON_RETURN(OpenForAdd);
case EGitState::Untracked:
return GET_ICON_RETURN(NotInDepot);
case EGitState::Deleted:
return GET_ICON_RETURN(MarkedForDelete);
case EGitState::Modified:
case EGitState::CheckedOut:
return GET_ICON_RETURN(CheckedOut);
case EGitState::Ignored:
return GET_ICON_RETURN(NotInDepot);
default:
#if ENGINE_MAJOR_VERSION < 5
return NAME_None;
#else
return FSlateIcon();
#endif
}
}
#if ENGINE_MAJOR_VERSION < 5
FName FGitSourceControlState::GetSmallIconName() const
{
switch (GetGitState()) {
case EGitState::NotAtHead:
return FName("ContentBrowser.SCC_NotAtHeadRevision_Small");
case EGitState::LockedOther:
return FName("ContentBrowser.SCC_CheckedOutByOtherUser_Small");
case EGitState::NotLatest:
return FName("ContentBrowser.SCC_ModifiedOtherBranch_Small");
case EGitState::Unmerged:
return FName("ContentBrowser.SCC_Branched_Small");
case EGitState::Added:
return FName("ContentBrowser.SCC_OpenForAdd_Small");
case EGitState::Untracked:
return FName("ContentBrowser.SCC_NotInDepot_Small");
case EGitState::Deleted:
return FName("ContentBrowser.SCC_MarkedForDelete_Small");
case EGitState::Modified:
case EGitState::CheckedOut:
return FName("ContentBrowser.SCC_CheckedOut_Small");
case EGitState::Ignored:
return FName("ContentBrowser.SCC_NotInDepot_Small");
default:
return NAME_None;
}
}
#endif
FText FGitSourceControlState::GetDisplayName() const
{
switch (GetGitState())
{
case EGitState::NotAtHead:
return LOCTEXT("NotCurrent", "Not current");
case EGitState::LockedOther:
return FText::Format(LOCTEXT("CheckedOutOther", "Checked out by: {0}"), FText::FromString(State.LockUser));
case EGitState::NotLatest:
return FText::Format(LOCTEXT("ModifiedOtherBranch", "Modified in branch: {0}"), FText::FromString(State.HeadBranch));
case EGitState::Unmerged:
return LOCTEXT("Conflicted", "Conflicted");
case EGitState::Added:
return LOCTEXT("OpenedForAdd", "Opened for add");
case EGitState::Untracked:
return LOCTEXT("NotControlled", "Not Under Revision Control");
case EGitState::Deleted:
return LOCTEXT("MarkedForDelete", "Marked for delete");
case EGitState::Modified:
case EGitState::CheckedOut:
return LOCTEXT("CheckedOut", "Checked out");
case EGitState::Ignored:
return LOCTEXT("Ignore", "Ignore");
case EGitState::Lockable:
return LOCTEXT("ReadOnly", "Read only");
case EGitState::None:
return LOCTEXT("Unknown", "Unknown");
default:
return FText();
}
}
FText FGitSourceControlState::GetDisplayTooltip() const
{
switch (GetGitState())
{
case EGitState::NotAtHead:
return LOCTEXT("NotCurrent_Tooltip", "The file(s) are not at the head revision");
case EGitState::LockedOther:
return FText::Format(LOCTEXT("CheckedOutOther_Tooltip", "Checked out by: {0}"), FText::FromString(State.LockUser));
case EGitState::NotLatest:
return FText::Format(LOCTEXT("ModifiedOtherBranch_Tooltip", "Modified in branch: {0} CL:{1} ({2})"), FText::FromString(State.HeadBranch), FText::FromString(HeadCommit), FText::FromString(HeadAction));
case EGitState::Unmerged:
return LOCTEXT("ContentsConflict_Tooltip", "The contents of the item conflict with updates received from the repository.");
case EGitState::Added:
return LOCTEXT("OpenedForAdd_Tooltip", "The file(s) are opened for add");
case EGitState::Untracked:
return LOCTEXT("NotControlled_Tooltip", "Item is not under revision control.");
case EGitState::Deleted:
return LOCTEXT("MarkedForDelete_Tooltip", "The file(s) are marked for delete");
case EGitState::Modified:
case EGitState::CheckedOut:
return LOCTEXT("CheckedOut_Tooltip", "The file(s) are checked out");
case EGitState::Ignored:
return LOCTEXT("Ignored_Tooltip", "Item is being ignored.");
case EGitState::Lockable:
return LOCTEXT("ReadOnly_Tooltip", "The file(s) are marked locally as read-only");
case EGitState::None:
return LOCTEXT("Unknown_Tooltip", "Unknown revision control state");
default:
return FText();
}
}
const FString& FGitSourceControlState::GetFilename() const
{
return LocalFilename;
}
const FDateTime& FGitSourceControlState::GetTimeStamp() const
{
return TimeStamp;
}
// Deleted and Missing assets cannot appear in the Content Browser, but they do in the Submit files to Revision Control window!
bool FGitSourceControlState::CanCheckIn() const
{
// We can check in if this is new content
if (IsAdded())
{
return true;
}
// Cannot check back in if conflicted or not current
if (!IsCurrent() || IsConflicted())
{
return false;
}
// We can check back in if we're locked.
if (State.LockState == ELockState::Locked)
{
return true;
}
// We can check in any file that has been modified, unless someone else locked it.
if (State.LockState != ELockState::LockedOther && IsModified() && IsSourceControlled())
{
return true;
}
return false;
}
bool FGitSourceControlState::CanCheckout() const
{
if (State.LockState == ELockState::Unlockable)
{
// Everything is already available for check in (checked out).
return false;
}
else
{
// We don't want to allow checkout if the file is out-of-date, as modifying an out-of-date binary file will most likely result in a merge conflict
return State.LockState == ELockState::NotLocked && IsCurrent();
}
}
bool FGitSourceControlState::IsCheckedOut() const
{
if (State.LockState == ELockState::Unlockable)
{
return IsSourceControlled(); // TODO: try modified instead? might block editing the file with a holding pattern
}
else
{
// We check for modified here too, because sometimes you don't lock a file but still want to push it. CanCheckout still true, so that you can lock it later...
return State.LockState == ELockState::Locked || (State.FileState == EFileState::Modified && State.LockState != ELockState::LockedOther);
}
}
bool FGitSourceControlState::IsCheckedOutOther(FString* Who) const
{
if (Who != nullptr)
{
// The packages dialog uses our lock user regardless if it was locked by other or us.
// But, if there is no lock user, it shows information about modification in other branches, which is important.
// So, only show our own lock user if it hasn't been modified in another branch.
// This is a very, very rare state (maybe impossible), but one that should be displayed properly.
if (State.LockState == ELockState::LockedOther || (State.LockState == ELockState::Locked && !IsModifiedInOtherBranch()))
{
*Who = State.LockUser;
}
}
return State.LockState == ELockState::LockedOther;
}
bool FGitSourceControlState::IsCheckedOutInOtherBranch(const FString& CurrentBranch) const
{
// You can't check out separately per branch
return false;
}
bool FGitSourceControlState::IsModifiedInOtherBranch(const FString& CurrentBranch) const
{
return State.RemoteState == ERemoteState::NotLatest;
}
bool FGitSourceControlState::GetOtherBranchHeadModification(FString& HeadBranchOut, FString& ActionOut, int32& HeadChangeListOut) const
{
if (!IsModifiedInOtherBranch())
{
return false;
}
HeadBranchOut = State.HeadBranch;
ActionOut = HeadAction; // TODO: from ERemoteState
HeadChangeListOut = 0; // TODO: get head commit
return true;
}
bool FGitSourceControlState::IsCurrent() const
{
return State.RemoteState != ERemoteState::NotAtHead && State.RemoteState != ERemoteState::NotLatest;
}
bool FGitSourceControlState::IsSourceControlled() const
{
return State.TreeState != ETreeState::Untracked && State.TreeState != ETreeState::Ignored && State.TreeState != ETreeState::NotInRepo;
}
bool FGitSourceControlState::IsAdded() const
{
// Added is when a file was untracked and is now added.
return State.FileState == EFileState::Added;
}
bool FGitSourceControlState::IsDeleted() const
{
return State.FileState == EFileState::Deleted;
}
bool FGitSourceControlState::IsIgnored() const
{
return State.TreeState == ETreeState::Ignored;
}
bool FGitSourceControlState::CanEdit() const
{
// Perforce does not care about it being current
return IsCheckedOut() || IsAdded();
}
bool FGitSourceControlState::CanDelete() const
{
// Perforce enforces that a deleted file must be current.
if (!IsCurrent())
{
return false;
}
// If someone else hasn't checked it out, we can delete revision controlled files.
return !IsCheckedOutOther() && IsSourceControlled();
}
bool FGitSourceControlState::IsUnknown() const
{
return State.FileState == EFileState::Unknown && State.TreeState == ETreeState::NotInRepo;
}
bool FGitSourceControlState::IsModified() const
{
return State.TreeState == ETreeState::Working ||
State.TreeState == ETreeState::Staged;
}
bool FGitSourceControlState::CanAdd() const
{
return State.TreeState == ETreeState::Untracked;
}
bool FGitSourceControlState::IsConflicted() const
{
return State.FileState == EFileState::Unmerged;
}
bool FGitSourceControlState::CanRevert() const
{
// Can revert the file state if we modified, even if it was locked by someone else.
// Useful for when someone locked a file, and you just wanna play around with it locallly, and then revert it.
return CanCheckIn() || IsModified();
}
EGitState::Type FGitSourceControlState::GetGitState() const
{
// No matter what, we must pull from remote, even if we have locked or if we have modified.
switch (State.RemoteState)
{
case ERemoteState::NotAtHead:
return EGitState::NotAtHead;
default:
break;
}
/** Someone else locked this file across branches. */
// We cannot push under any circumstance, if someone else has locked.
if (State.LockState == ELockState::LockedOther)
{
return EGitState::LockedOther;
}
// We could theoretically push, but we shouldn't.
if (State.RemoteState == ERemoteState::NotLatest)
{
return EGitState::NotLatest;
}
switch (State.FileState)
{
case EFileState::Unmerged:
return EGitState::Unmerged;
case EFileState::Added:
return EGitState::Added;
case EFileState::Deleted:
return EGitState::Deleted;
case EFileState::Modified:
return EGitState::Modified;
default:
break;
}
if (State.TreeState == ETreeState::Untracked)
{
return EGitState::Untracked;
}
if (State.LockState == ELockState::Locked)
{
return EGitState::CheckedOut;
}
if (IsSourceControlled())
{
if (CanCheckout())
{
return EGitState::Lockable;
}
return EGitState::Unmodified;
}
return EGitState::None;
}
#undef LOCTEXT_NAMESPACE