backend/plugins/tapd/tasks/shared.go (273 lines of code) (raw):
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tasks
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"github.com/apache/incubator-devlake/core/dal"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
"github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/plugins/tapd/models"
)
type Page struct {
Data Data `json:"data"`
}
type Data struct {
Count int `json:"count"`
}
var priorityMap = map[string]string{
"1": "Nice To Have",
"2": "Low",
"3": "Middle",
"4": "High",
}
var accountIdGen *didgen.DomainIdGenerator
var workspaceIdGen *didgen.DomainIdGenerator
var iterIdGen *didgen.DomainIdGenerator
func getAccountIdGen() *didgen.DomainIdGenerator {
if accountIdGen == nil {
accountIdGen = didgen.NewDomainIdGenerator(&models.TapdAccount{})
}
return accountIdGen
}
func getWorkspaceIdGen() *didgen.DomainIdGenerator {
if workspaceIdGen == nil {
workspaceIdGen = didgen.NewDomainIdGenerator(&models.TapdWorkspace{})
}
return workspaceIdGen
}
func getIterIdGen() *didgen.DomainIdGenerator {
if iterIdGen == nil {
iterIdGen = didgen.NewDomainIdGenerator(&models.TapdIteration{})
}
return iterIdGen
}
// res will not be used
func GetTotalPagesFromResponse(r *http.Response, args *api.ApiCollectorArgs) (int, errors.Error) {
data := args.Ctx.GetData().(*TapdTaskData)
apiClient, err := NewTapdApiClient(args.Ctx.TaskContext(), data.Connection)
if err != nil {
return 0, err
}
query := url.Values{}
query.Set("workspace_id", fmt.Sprintf("%v", data.Options.WorkspaceId))
res, err := apiClient.Get(fmt.Sprintf("%s/count", r.Request.URL.Path), query, nil)
if err != nil {
return 0, err
}
var page Page
err = api.UnmarshalResponse(res, &page)
count := page.Data.Count
totalPage := count/args.PageSize + 1
return totalPage, err
}
// parseIterationChangelog function is used to parse the iteration changelog
func parseIterationChangelog(taskCtx plugin.SubTaskContext, old string, new string) (iterationFromId int64, iterationToId int64, err errors.Error) {
data := taskCtx.GetData().(*TapdTaskData)
db := taskCtx.GetDal()
// Find the iteration with the old name
iterationFrom := &models.TapdIteration{}
clauses := []dal.Clause{
dal.From(&models.TapdIteration{}),
dal.Where("connection_id = ? and workspace_id = ? and name = ?",
data.Options.ConnectionId, data.Options.WorkspaceId, old),
}
err = db.First(iterationFrom, clauses...)
if err != nil && !db.IsErrorNotFound(err) {
return 0, 0, err
}
// Find the iteration with the new name
iterationTo := &models.TapdIteration{}
clauses = []dal.Clause{
dal.From(&models.TapdIteration{}),
dal.Where("connection_id = ? and workspace_id = ? and name = ?",
data.Options.ConnectionId, data.Options.WorkspaceId, new),
}
err = db.First(iterationTo, clauses...)
if err != nil && !db.IsErrorNotFound(err) {
return 0, 0, err
}
return iterationFrom.Id, iterationTo.Id, nil
}
// GetRawMessageDirectFromResponse extracts the raw message from an HTTP response
func GetRawMessageDirectFromResponse(res *http.Response) ([]json.RawMessage, errors.Error) {
// Read the response body
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
return nil, errors.Convert(err)
}
// Return the response body as a slice of json.RawMessage
return []json.RawMessage{body}, nil
}
func GetRawMessageArrayFromResponse(res *http.Response) ([]json.RawMessage, errors.Error) {
var data struct {
Data []json.RawMessage `json:"data"`
}
err := api.UnmarshalResponse(res, &data)
return data.Data, err
}
type TapdApiParams models.TapdApiParams
// CreateRawDataSubTaskArgs creates a new instance of api.RawDataSubTaskArgs based on the provided
// task context, raw table name, and a flag to determine if the company ID should be used.
// It returns a pointer to the created api.RawDataSubTaskArgs and a pointer to the filtered TapdTaskData.
func CreateRawDataSubTaskArgs(taskCtx plugin.SubTaskContext, rawTable string) (*api.RawDataSubTaskArgs, *TapdTaskData) {
// Retrieve task data from the provided task context and cast it to TapdTaskData
data := taskCtx.GetData().(*TapdTaskData)
// Create a filtered copy of the original data
filteredData := *data
filteredData.Options = &TapdOptions{}
*filteredData.Options = *data.Options
// Set up TapdApiParams based on the original data
var params = TapdApiParams{
ConnectionId: data.Options.ConnectionId,
WorkspaceId: data.Options.WorkspaceId,
}
// Create the RawDataSubTaskArgs with the task context, params, and raw table name
rawDataSubTaskArgs := &api.RawDataSubTaskArgs{
Ctx: taskCtx,
Params: params,
Table: rawTable,
}
// Return the created RawDataSubTaskArgs and the filtered TapdTaskData
return rawDataSubTaskArgs, &filteredData
}
// getTapdTypeMappings retrieves story types from _tool_tapd_workitem_types and maps them to
// typeMapping. It takes TapdTaskData, a Dal interface, and a system string as arguments.
// It returns a map of type ID to type name and an error, if any.
func getTapdTypeMappings(data *TapdTaskData, db dal.Dal, system string) (map[uint64]string, errors.Error) {
typeIdMapping := make(map[uint64]string)
issueTypes := make([]models.TapdWorkitemType, 0)
// Create clauses for querying the database
clauses := []dal.Clause{
dal.From(&models.TapdWorkitemType{}),
dal.Where("connection_id = ? and workspace_id = ? and entity_type = ?",
data.Options.ConnectionId, data.Options.WorkspaceId, system),
}
// Query the database for issue types
err := db.All(&issueTypes, clauses...)
if err != nil {
return nil, err
}
// Map the retrieved issue types
for _, issueType := range issueTypes {
typeIdMapping[issueType.Id] = issueType.Name
}
return typeIdMapping, nil
}
// getStdTypeMappings creates a map of user type to standard type based on the provided TapdTaskData.
// It returns the created map.
func getStdTypeMappings(data *TapdTaskData) map[string]string {
stdTypeMappings := make(map[string]string)
if data.Options.ScopeConfig == nil {
return stdTypeMappings
}
mapping := data.Options.ScopeConfig.TypeMappings
// Map user types to standard types
for userType, stdType := range mapping {
stdTypeMappings[userType] = strings.ToUpper(stdType)
}
return stdTypeMappings
}
// getStatusMapping creates a map of original status values to standard status values
// based on the provided TapdTaskData. It returns the created map.
func getStatusMapping(data *TapdTaskData) map[string]string {
stdStatusMappings := make(map[string]string)
if data.Options.ScopeConfig == nil {
return stdStatusMappings
}
mapping := data.Options.ScopeConfig.StatusMappings
// Map original status values to standard status values
for userStatus, stdStatus := range mapping {
stdStatusMappings[userStatus] = strings.ToUpper(stdStatus)
}
return stdStatusMappings
}
// getDefaultStdStatusMapping retrieves default standard status mappings for the given TapdTaskData and status list.
// It takes TapdTaskData, a Dal interface, and a statusList of type S (models.TapdStatus).
// It returns a map of English to Chinese status names, a function to get standard status from status key, and an error, if any.
func getDefaultStdStatusMapping[S models.TapdStatus](data *TapdTaskData, db dal.Dal, statusList []S) (map[string]string, func(statusKey string) string, errors.Error) {
// Create clauses for querying the database
clauses := []dal.Clause{
dal.Where("connection_id = ? and workspace_id = ?", data.Options.ConnectionId, data.Options.WorkspaceId),
}
// Query the database for status list
err := db.All(&statusList, clauses...)
if err != nil {
return nil, nil, err
}
// Create status language and last step maps
statusLanguageMap := make(map[string]string, len(statusList))
statusLastStepMap := make(map[string]bool, len(statusList))
// Populate status maps
for _, v := range statusList {
statusLanguageMap[v.GetEnglish()] = v.GetChinese()
statusLastStepMap[v.GetChinese()] = v.GetIsLastStep()
}
// Define function to get standard status from status key
getStdStatus := func(statusKey string) string {
if statusLastStepMap[statusKey] {
return ticket.DONE
} else if statusKey == "草稿" {
return ticket.TODO
} else {
return ticket.IN_PROGRESS
}
}
return statusLanguageMap, getStdStatus, nil
}
// unicodeToZh converts a string containing Unicode escape sequences to a Chinese string.
// It returns the converted string and an error if the conversion fails.
func unicodeToZh(s string) (string, error) {
str, err := strconv.Unquote(strings.Replace(strconv.Quote(s), `\\u`, `\u`, -1))
if err != nil {
return "", err
}
return str, nil
}
// convertUnicode converts the ValueAfterParsed and ValueBeforeParsed fields of a struct to Chinese text.
// It takes a pointer to a struct and returns an error if the conversion fails.
func convertUnicode(p interface{}) errors.Error {
var err errors.Error
pType := reflect.TypeOf(p)
if pType.Kind() != reflect.Ptr {
panic("expected a pointer to a struct")
}
pValue := reflect.ValueOf(p).Elem()
if pValue.Kind() != reflect.Struct {
panic("expected a pointer to a struct")
}
after, err := errors.Convert01(unicodeToZh(pValue.FieldByName("ValueAfterParsed").String()))
if err != nil {
return err
}
before, err := errors.Convert01(unicodeToZh(pValue.FieldByName("ValueBeforeParsed").String()))
if err != nil {
return err
}
if after == "--" {
after = ""
}
if before == "--" {
before = ""
}
// Set ValueAfterParsed and ValueBeforeParsed fields
valueAfterField := pValue.FieldByName("ValueAfterParsed")
valueAfterField.SetString(after)
valueBeforeField := pValue.FieldByName("ValueBeforeParsed")
valueBeforeField.SetString(before)
return nil
}
// replaceSemicolonWithComma replaces all semicolons with commas in the given string
// and trims any trailing commas. It returns the modified string.
func replaceSemicolonWithComma(str string) string {
res := strings.ReplaceAll(str, ";", ",")
return strings.TrimRight(res, ",")
}
// generateDomainAccountIdForUsers generates domain account IDs for a list of users.
// The input 'param' is a string with format "user1,user2,user3".
// The function takes a string containing a list of users separated by commas and a connectionId.
// It returns a string containing the generated domain account IDs for each user, separated by commas.
func generateDomainAccountIdForUsers(param string, connectionId uint64) string {
if param == "" {
return ""
}
param = replaceSemicolonWithComma(param)
users := strings.Split(param, ",")
var res []string
for _, user := range users {
res = append(res, getAccountIdGen().Generate(connectionId, user))
}
return strings.Join(res, ",")
}
// extractStatus extracts the status from the given blob and returns a map of status names to status values.
func extractStatus(blob []byte) (map[string]string, errors.Error) {
var statusRes struct {
Data interface{} `json:"data"`
}
err := errors.Convert(json.Unmarshal(blob, &statusRes))
if err != nil {
return nil, err
}
data, ok := statusRes.Data.(map[string]interface{})
if !ok {
return nil, nil
}
results := make(map[string]string)
for k, v := range data {
if value, ok := v.(string); ok {
results[k] = value
}
}
return results, nil
}
// getRepoNamespaceFromUrlPath
// returns the namespace of a repository from the given URL path,
// which is the url path without the last segment, and without leading and trailing slashes.
func getRepoNamespaceFromUrlPath(path string) string {
// Remove leading and trailing slashes
path = strings.Trim(path, "/")
// Remove last segment
path = path[:strings.LastIndex(path, "/")]
return path
}
// getRepoNameFromUrlPath
// returns the last segment of url path as repo name, without .git suffix.
func getRepoNameFromUrlPath(path string) string {
// Remove leading and trailing slashes
path = strings.Trim(path, "/")
// Remove .git suffix
path = strings.TrimSuffix(path, ".git")
// Get last segment
return path[strings.LastIndex(path, "/")+1:]
}