// Copyright Epic Games, Inc. All Rights Reserved. #include "SwarmService.h" #include "ISourceControlModule.h" #include "ISourceControlProvider.h" #include "SourceControlOperations.h" #include "Interfaces/IHttpResponse.h" #include "Logic/Services/SubmitToolServiceProvider.h" #include "Logic/Services/Interfaces/ISTSourceControlService.h" #include "Logic/TagService.h" #include "Logic/ChangelistService.h" #include "Serialization/JsonSerializer.h" #include "JsonObjectConverter.h" #include "Logging/SubmitToolLog.h" FSwarmService::FSwarmService(TWeakPtr InServiceProvider) : ServiceProvider(InServiceProvider) { GetSwarmURL(); ServiceProvider.Pin()->GetService()->GetUsersAndGroups(FOnUsersAndGroupsGet::FDelegate::CreateLambda([this](TArray>& InUsers, TArray>& InGroups) { Users = &InUsers; Groups = &InGroups; })); } void FSwarmService::FetchReview(const OnGetReviewComplete& OnComplete) { if (!bCanConnect) { return; } FString Changelist = ServiceProvider.Pin()->GetService()->GetCLID(); if (Changelist.IsEmpty()) { return; } const TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json; charset=utf-8")); HttpRequest->SetHeader(TEXT("Authorization"), *ServiceProvider.Pin()->GetService()->GetAuthTicket().ToString()); HttpRequest->SetURL(FString::Format(TEXT("{0}?change={1}&max={2}"), { ReviewsURL(), Changelist, // get the review for a specific CL 1 // we only want a single review. })); HttpRequest->SetVerb(TEXT("GET")); HttpRequest->OnProcessRequestComplete().BindLambda([OnComplete, this](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully) { if (!bConnectedSuccessfully) { OnComplete.ExecuteIfBound(Review, TEXT("Connection Failed")); return; } UE_LOG(LogSubmitToolDebug, Log, TEXT("Fetch review Response: %s"), *Response->GetContentAsString()); if(EHttpResponseCodes::IsOk(Response->GetResponseCode())) { FSwarmReviewCollection ReviewCollection; if(FJsonObjectConverter::JsonObjectStringToUStruct(Response->GetContentAsString(), &ReviewCollection, 0, 0)) { if(ReviewCollection.Reviews.Num() > 0) { Review = MakeUnique(ReviewCollection.Reviews[0]); OnComplete.ExecuteIfBound(Review, {}); } else { OnComplete.ExecuteIfBound(Review, TEXT("No available reviews.")); } } else { OnComplete.ExecuteIfBound(Review, TEXT("Could not parse the response json.")); } } else { UE_LOG(LogSubmitTool, Error, TEXT("Could not communicate with swarm due to error %d.\n%s"), Response->GetResponseCode(), *Response->GetContentAsString()); OnComplete.ExecuteIfBound(Review, FString::Printf(TEXT("Error code %d."), Response->GetResponseCode())); } }); HttpRequest->ProcessRequest(); } void FSwarmService::CreateReview(const TArray& InReviewers, const OnCreateReviewComplete& OnComplete) { if(!bCanConnect) { OnComplete.ExecuteIfBound(false, FString()); return; } FString Changelist = ServiceProvider.Pin()->GetService()->GetCLID(); if(Changelist.IsEmpty()) { return; } if(CreateSwarmRequest.IsValid()) { CreateSwarmRequest->CancelRequest(); } CreateSwarmRequest = FHttpModule::Get().CreateRequest(); CreateSwarmRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json; charset=utf-8")); CreateSwarmRequest->SetHeader(TEXT("Authorization"), *ServiceProvider.Pin()->GetService()->GetAuthTicket().ToString()); CreateSwarmRequest->SetURL(ReviewsURL()); CreateSwarmRequest->SetVerb(TEXT("POST")); TSharedRef RequestJson = MakeShared(); RequestJson->SetNumberField(TEXT("change"), FCString::Atoi(*Changelist)); TArray> ReviewersObject; TArray> GroupsObject; for (const FString& value : InReviewers) { if (Users != nullptr) { TSharedPtr* FoundUser = Users->FindByPredicate([value = value.TrimChar(TCHAR('@'))](TSharedPtr& InUserData) { return InUserData->Username == value; }); if (FoundUser != nullptr) { ReviewersObject.Add(MakeShared((*FoundUser)->Username)); } continue; } if (Groups != nullptr) { TSharedPtr* FoundGroup = Groups->FindByPredicate([&value](TSharedPtr& InGroupData) { return *InGroupData == value; }); if (FoundGroup != nullptr) { TSharedPtr GroupObject = MakeShared(); TSharedPtr GroupDetails = MakeShared(); GroupDetails->SetStringField(TEXT("required"), TEXT("false")); GroupObject->SetObjectField(*(*FoundGroup), GroupDetails); GroupsObject.Add(MakeShared(GroupObject)); } } } if (!ReviewersObject.IsEmpty()) { RequestJson->SetArrayField(TEXT("reviewers"), ReviewersObject); } if(!GroupsObject.IsEmpty()) { RequestJson->SetArrayField(TEXT("reviewerGroups"), GroupsObject); } RequestJson->SetStringField(TEXT("description"), ServiceProvider.Pin()->GetService()->GetCLDescription()); FString BodyString; TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&BodyString); FJsonSerializer::Serialize(RequestJson, JsonWriter); UE_LOG(LogSubmitToolDebug, Log, TEXT("Create Swarm request body:\n%s"), *BodyString); CreateSwarmRequest->SetContentAsString(BodyString); CreateSwarmRequest->OnProcessRequestComplete().BindLambda([OnComplete, this](FHttpRequestPtr Request, FHttpResponsePtr HttpResponse, bool bConnectedSuccessfully) { if(!bConnectedSuccessfully) { if(HttpResponse.IsValid()) { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to create swarm review. Connection error %d - %s."), HttpResponse->GetResponseCode(), *EHttpResponseCodes::GetDescription(static_cast(HttpResponse->GetResponseCode())).ToString()); UE_LOG(LogSubmitToolDebug, Warning, TEXT("Unable to create swarm review. Connection error\nResponse: %s"), *HttpResponse->GetContentAsString()); } else { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to create swarm review. Connection error, no response.")); } OnComplete.ExecuteIfBound(false, FString()); return; } if(HttpResponse.IsValid()) { UE_LOG(LogSubmitToolDebug, Log, TEXT("Create review Response: %s"), *HttpResponse->GetContentAsString()); if(EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) { TSharedRef> Reader = TJsonReaderFactory::Create(HttpResponse->GetContentAsString()); TSharedPtr JsonObj; if(!FJsonSerializer::Deserialize(Reader, JsonObj)) { UE_LOG(LogSubmitTool, Error, TEXT("Unable to deserialize swarm create response")); return; } FSwarmReview ReviewObj; if (FJsonObjectConverter::JsonObjectToUStruct(JsonObj->GetObjectField(TEXT("review")).ToSharedRef(), &ReviewObj)) { Review = MakeUnique(ReviewObj); } FString ReviewId = JsonObj->GetObjectField(TEXT("review"))->GetStringField(TEXT("id")); FString ReviewURL = BuildReviewURL(ReviewId); OnComplete.ExecuteIfBound(true, ReviewURL); } else { UE_LOG(LogSubmitTool, Error, TEXT("Could not create a swarm review due to error %d - %s."), HttpResponse->GetResponseCode(), *EHttpResponseCodes::GetDescription(static_cast(HttpResponse->GetResponseCode())).ToString()); OnComplete.ExecuteIfBound(false, FString()); } } else { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to create swarm review. Failed with unknown connection error")); OnComplete.ExecuteIfBound(false, FString()); } }); UE_LOG(LogSubmitToolP4, Log, TEXT("Creating swarm review")); CreateSwarmRequest->ProcessRequest(); } void FSwarmService::UpdateReviewDescription(const TDelegate& OnComplete, const FString& InDescription) { if(!bCanConnect || !Review.IsValid()) { UE_LOG(LogSubmitTool, Warning, TEXT("Tried to update swarm review but Swarm API is not available or there is no review for this CL")); OnComplete.ExecuteIfBound(false); return; } if(UpdateSwarmRequest.IsValid()) { UpdateSwarmRequest->CancelRequest(); } UpdateSwarmRequest = FHttpModule::Get().CreateRequest(); UpdateSwarmRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json; charset=utf-8")); UpdateSwarmRequest->SetHeader(TEXT("Authorization"), *ServiceProvider.Pin()->GetService()->GetAuthTicket().ToString()); FString URL = ReviewsURL() / FString::FromInt(Review->Id); UpdateSwarmRequest->SetURL(URL); UpdateSwarmRequest->SetVerb(TEXT("PATCH")); TSharedRef RequestJson = MakeShared(); RequestJson->SetStringField(TEXT("description"), InDescription); FString BodyString; TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&BodyString); FJsonSerializer::Serialize(RequestJson, JsonWriter); UE_LOG(LogSubmitToolDebug, Log, TEXT("Update Swarm request body:\n%s"), *BodyString); UpdateSwarmRequest->SetContentAsString(BodyString); UpdateSwarmRequest->OnProcessRequestComplete().BindLambda([OnComplete, this](FHttpRequestPtr Request, FHttpResponsePtr HttpResponse, bool bConnectedSuccessfully) { if(!bConnectedSuccessfully || !HttpResponse.IsValid()) { if(HttpResponse.IsValid()) { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to update swarm review. Connection error %d - %s."), HttpResponse->GetResponseCode(), *EHttpResponseCodes::GetDescription(static_cast(HttpResponse->GetResponseCode())).ToString()); UE_LOG(LogSubmitToolDebug, Warning, TEXT("Unable to update swarm review. Connection error %d\nResponse: %s"), HttpResponse->GetResponseCode(), *HttpResponse->GetContentAsString()); } else { UE_LOG(LogSubmitTool, Warning, TEXT("Unable to update swarm review. Connection error, no response.")); } OnComplete.ExecuteIfBound(false); return; } else { UE_LOG(LogSubmitToolDebug, Log, TEXT("Update review response: %s"), *HttpResponse->GetContentAsString()); if(EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) { UE_LOG(LogSubmitTool, Log, TEXT("Swarm description updated successfully")); OnComplete.ExecuteIfBound(true); } else { UE_LOG(LogSubmitTool, Error, TEXT("Could not update swarm description due to error %d - %s."), HttpResponse->GetResponseCode(), *EHttpResponseCodes::GetDescription(static_cast(HttpResponse->GetResponseCode())).ToString()); OnComplete.ExecuteIfBound(false); } } }); UE_LOG(LogSubmitToolP4, Log, TEXT("Updating swarm review description")); UpdateSwarmRequest->ProcessRequest(); } const FString& FSwarmService::GetSwarmURL() { if(SwarmURL.IsEmpty()) { ServiceProvider.Pin()->GetService()->RunCommand(TEXT("property"), { "-l", "-n", "P4.Swarm.URL" }, FOnSCCCommandComplete::CreateLambda( [this](bool bSuccess, const FSCCRecordSet& InResultValues, const FSourceControlResultInfo& InResultsInfo) { if(bSuccess && InResultValues.Num() > 0 && InResultValues[0].Contains(TEXT("value"))) { SwarmURL = InResultValues[0][TEXT("value")]; } })).Wait(); } return SwarmURL; } FString FSwarmService::ReviewsURL() const { return SwarmURL.IsEmpty() ? FString{} : SwarmURL / TEXT("api/v9/reviews"); } const TArray FSwarmService::GetUsersInSwarmTag() const { TArray Reviewers; for (const FTag* Tag : ServiceProvider.Pin()->GetService()->GetTagsArray()) { if (Tag->Definition.InputSubType.Equals(TEXT("Swarm"), ESearchCase::IgnoreCase)) { return Tag->GetValues(); } } return TArray(); }