UE 版本控制逻辑分析

发布于 2023-04-15  177 次阅读


UE 版本控制逻辑分析

笔者在 自定义树形结构控件 中使用到了版本控制的相关逻辑修改文件角标的显示,并提供了一些功能,但未展开相关逻辑,因此记录本文详细拓展一下在自定义编辑器中如何连接并进行版本控制的方法。

SourceControlHelpers

系统提供了版本控制的相关工具函数,位置在 Engine\Source\Developer\SourceControl\Public\SourceControlHelpers.h,其中的方法可用,并且操作前基本均会强制刷新文件版本控制状态,保证操作正确性,笔者测试,每次状态的同步或操作大概要耗时 500ms 左右。

注:其中的 CheckOutOrAddFiles 方法存在一些问题(UE 5.2),笔者已提 PR 修复相关代码,已批准,待 UnrealBot 合入。

流程梳理

eg. 我们这里以内容浏览器的 CheckOut 为例,跟踪系统中 CheckOut 的调用逻辑如下:

  • FAssetFileContextMenu::ExecuteSCCCheckOut();
    • FEditorFileUtils::CheckoutPackages(PackagesToCheckOut);
    • FEditorFileUtils::CheckoutPackages(PackagesToCheckOut);
      • SourceControlProvider.Execute(ISourceControlOperation::Create(), PkgsToCheckOut);
      • SourceControlProvider.Execute(ISourceControlOperation::Create(), FinalPackageCheckoutList);

不难看出,核心逻辑在 ISourceControlProvider 的 Execute 方法,看起来通过入参就可以执行不同的版本控制命令。

    virtual ECommandResult::Type Execute( const FSourceControlOperationRef& InOperation, FSourceControlChangelistPtr InChangelist, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete() ) = 0;

接口中提供了 Execute 方法,核心逻辑主要是通过入参的 InOperation 来执行不同的操作,InFiles 控制要操作的文件,InConcurrency 控制是否异步,以及 InOperationCompleteDelegate 绑定回调代理。

FSourceControlOperationRef 是一个 ISourceControlOperation 实例的引用,系统中实现了诸多命令,如:

命 令 作用
FCheckIn 用于将文件提交到源控制的操作
FCheckOut 用于将文件从源码控制中迁出的操作
FConnect 用于连接(或测试连接)到源控制的操作
FCopy 用于将一个文件或目录从一个位置复制到另一个位置的操作
FDelete 用于在源控制中标记文件以便删除的操作
FMarkForAdd 在源码控制中用于标记文件的添加的操作
FResolve 用于解决处于冲突状态的文件的操作
FRevert 用于将所做的更改恢复到它们在源控制中的状态的操作

其他读者可自行查阅 Engine\Source\Developer\SourceControl\Public\SourceControlOperations.h 。

通过这个参数,各个版本控制的实现中可以通过此参数识别出操作类型,执行相应的命令。这里的设计方式对上层业务来说是很友好的,通过不同的操作结构体,可以代表和执行各种不同的源代码控制操作,使得 UE 的源代码控制系统可以轻易地扩展到支持更多功能,只需要新增对应操作结构体即可,并且抽象掉了复杂的多种多样的版本控制系统,使得调用逻辑统一,接口与内部逻辑解耦,内部实现可以灵活改变而不影响外部调用。

这里在执行 FCheckOut 之前,还以 FUpdateStatus 为入参进行了一次调用,FUpdateStatus 中提供了 bForceUpdate 参数,控制此参数可以控制是否需要强制从版本控制处获取状态,因为在各个版本控制类的实现中,默认会使用缓存的文件状态处理相应逻辑(因为每次操作耗时较久)。比如在 FPerforceSourceControlProvider 中,存在成员变量 TMap<FString, TSharedRef<class FPerforceSourceControlState, ESPMode::ThreadSafe> > StateCache; ,大多数情况下会读取此 Map 中的状态直接作为版本控制状态,而不会去真正获取 Perforce 端的状态,当此 TMap 中不存在要寻找的文件时,也不会去获取 Perforce 端的属性,代码如下:

    TSharedRef<FPerforceSourceControlState, ESPMode::ThreadSafe>* State = StateCache.Find(Filename);
    if(State != NULL)
    {
        // found cached item
        return (*State);
    }
    else
    {
        // cache an unknown state for this item
        TSharedRef<FPerforceSourceControlState, ESPMode::ThreadSafe> NewState = MakeShared<FPerforceSourceControlState>(Filename);
        StateCache.Add(Filename, NewState);
        return NewState;
    }

这里会直接默认构造一个 FPerforceSourceControlState 类型的状态枚举,作为未被缓存文件的状态,而其默认值为 DontCare,枚举类型及功能如下:

namespace EPerforceState
{
    enum Type
    {
        /** Don't know or don't care. */
        DontCare        = 0,

        /** File is checked out to current user. */
        CheckedOut      = 1,

        /** File is not checked out (but IS controlled by the source control system). */
        ReadOnly        = 2,

        /** File is new and not in the depot - needs to be added. */
        NotInDepot      = 4,

        /** File is checked out by another user and cannot be checked out locally. */
        CheckedOutOther = 5,

        /** Certain packages are best ignored by the SCC system (MyLevel, Transient, etc). */
        Ignore          = 6,

        /** File is marked for add */
        OpenForAdd      = 7,

        /** File is marked for delete */
        MarkedForDelete = 8,

        /** Not under client root */
        NotUnderClientRoot  = 9,

        /** Opened for branch */
        Branched = 10,
    };
}

也正是因为这个缓存机制,以及默认状态下未被缓存的文件会被按 DontCare 处理,所以当我们直接对非工程目录下的文件执行 Perforce 相关操作时,大概率会失败(DontCare 文件操作均会失败,这里描述大概率是因为有些外部调用会触发状态强制状态同步),因此对于非工程目录下的文件,我们需要在合适的时机强制同步文件版本控制状态,即上文中提到的 FUpdateStatus 参数,代码如下:

    ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
    TSharedPtr<FUpdateStatus, ESPMode::ThreadSafe> UpdateStatusOp = ISourceControlOperation::Create<FUpdateStatus>();
    UpdateStatusOp->SetForceUpdate(true);
    SourceControlProvider.Execute(UpdateStatusOp.ToSharedRef(), FilesAddToSCC, EConcurrency::Asynchronous);

如果能够找到合适的时机执行命令,推荐使用异步方式提前执行,相关操作较为耗时,提前执行缓存后后续操作使用 Cache State 也能有较高效率 。系统中的内容浏览器方案是会在特定的时机进行状态的同步,例如右键时在 Source Control Menu 中会提供 Refresh 方法供刷新版本控制文件状态,以及当进行版本控制操作、切换项目、重启引擎、执行保存等时机强制同步,如果读者需要进行相关操作推荐也采用如上方案。

当我们对多个文件操作时,切勿迭代然后针对每个文件调用 Execute 或 SourceControlHelpers 提供的相应方法,每次与版本控制系统通信都会耗费大约 500ms,推荐组织好文件,以命令为单位使用其提供的批量方法处理。

具体执行逻辑

ISourceControlProvider 抽象了各个版本控制系统的逻辑,以供上层调用,下层的逻辑各自比较特殊,笔者这里只搭建了 Perforce 和 GitLfs,因此便不再仔细研究,这里简单跟踪一下 Perforce 的实现逻辑。

这里还是以核心方法 Execute 示例:

  • FPerforceSourceControlProvider::Execute()
  • FPerforceSourceControlProvider::CreateWorker(InOperation->GetName())
    • IPerforceSourceControlWorker::CreateWorker(InOperationName, *this);
    • IPerforceSourceControlWorker::Execute(InCommand)
      • FPerforceConnection::RunCommand()
      • P4Client.Run(FROM_TCHAR(*InCommand, bIsUnicode), &User);

PerforceSourceControlOperations.cpp 文件中定义了一个 TMap<FName, FGetPerforceSourceControlWorker> 类型的静态变量,在 IPerforceSourceControlWorker 的 RegisterWorkers 中,注册了各个 P4 操作命令对应的 Worker,RegisterWorkers 会在 FPerforceSourceControlModule StartupModule 时调用,这些具体的 Worker 中,完成了具体的版本控制操作,最终会调用到第三方库的 P4Api,完成对应的版本控制命令。
这里的 Worker 再次进行了抽象,但这里只是剥离了版本控制的细节,提供了标准的源代码操作接口,此外,异步逻辑在外层维护,相应接口回调处理即可。

如堕五里雾中
最后更新于 2023-07-28