blob: e3cc2312b0930d4422a4ed6dbf2a475f56f45cbd [file]
/*
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 api
import (
"net/http"
"strconv"
"strings"
"github.com/apache/incubator-devlake/core/dal"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
)
// ScopeDuplicateConnection is a connection that shares a repository scope.
type ScopeDuplicateConnection struct {
ConnectionId uint64 `json:"connectionId"`
ConnectionName string `json:"connectionName"`
}
// ScopeDuplicateGroup is one repository that appears under multiple connections
// (diagnostics) or already exists under another connection (pre-add check).
type ScopeDuplicateGroup struct {
GithubId int `json:"githubId"`
HTMLUrl string `json:"htmlUrl"`
FullName string `json:"fullName"`
Connections []ScopeDuplicateConnection `json:"connections"`
}
// ScopeDuplicatesOutput is the response body for GetScopeDuplicates.
type ScopeDuplicatesOutput struct {
Duplicates []ScopeDuplicateGroup `json:"duplicates"`
}
// scopeDuplicateRow is one joined row from the scoped SQL query.
type scopeDuplicateRow struct {
GithubId int `gorm:"column:github_id"`
HTMLUrl string `gorm:"column:html_url"`
FullName string `gorm:"column:full_name"`
ConnectionId uint64 `gorm:"column:connection_id"`
ConnectionName string `gorm:"column:connection_name"`
}
// GetScopeDuplicates returns GitHub repositories registered under more than one
// connection, or (with connectionId + githubIds) candidates already present on
// other connections.
// @Summary Find GitHub scopes duplicated across connections
// @Description Diagnostics: groups where the same githubId appears on more than one connection.
// @Description Pre-add check: pass connectionId and githubIds to find candidates already registered elsewhere.
// @Tags plugins/github
// @Param connectionId query int false "Current connection id (pre-add check)"
// @Param githubIds query string false "Comma-separated GitHub repo ids to check (pre-add check)"
// @Success 200 {object} ScopeDuplicatesOutput
// @Failure 400 {object} shared.ApiBody "Bad Request"
// @Failure 500 {object} shared.ApiBody "Internal Error"
// @Router /plugins/github/scope-duplicates [GET]
func GetScopeDuplicates(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
connectionId, githubIds, err := parseScopeDuplicateQuery(input)
if err != nil {
return nil, err
}
// Pre-add check with an empty selection: nothing to warn about.
if connectionId != nil && len(githubIds) == 0 {
return &plugin.ApiResourceOutput{
Body: ScopeDuplicatesOutput{Duplicates: []ScopeDuplicateGroup{}},
Status: http.StatusOK,
}, nil
}
rows, err := queryScopeDuplicateRows(basicRes.GetDal(), connectionId, githubIds)
if err != nil {
return nil, err
}
return &plugin.ApiResourceOutput{
Body: ScopeDuplicatesOutput{Duplicates: groupScopeDuplicateRows(rows)},
Status: http.StatusOK,
}, nil
}
func parseScopeDuplicateQuery(input *plugin.ApiResourceInput) (*uint64, []int, errors.Error) {
var connectionId *uint64
if v := input.Query.Get("connectionId"); v != "" {
id, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return nil, nil, errors.BadInput.Wrap(err, "invalid connectionId")
}
connectionId = &id
}
var githubIds []int
if v := input.Query.Get("githubIds"); v != "" {
for _, part := range strings.Split(v, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
id, err := strconv.Atoi(part)
if err != nil {
return nil, nil, errors.BadInput.Wrap(err, "invalid githubIds")
}
githubIds = append(githubIds, id)
}
}
if len(githubIds) > 0 && connectionId == nil {
return nil, nil, errors.BadInput.New("connectionId is required when githubIds is provided")
}
return connectionId, githubIds, nil
}
// queryScopeDuplicateRows loads only the rows needed for the requested mode.
// Check mode: selected githubIds on any connection other than connectionId.
// Diagnostics: githubIds that already appear on more than one connection.
func queryScopeDuplicateRows(db dal.Dal, connectionId *uint64, githubIds []int) ([]scopeDuplicateRow, errors.Error) {
clauses := []dal.Clause{
dal.Select("r.github_id, r.html_url, r.full_name, r.connection_id, c.name AS connection_name"),
dal.From("_tool_github_repos r"),
dal.Join("INNER JOIN _tool_github_connections c ON c.id = r.connection_id"),
dal.Orderby("r.github_id ASC, r.connection_id ASC"),
}
if connectionId != nil {
clauses = append(clauses, dal.Where(
"r.github_id IN ? AND r.connection_id != ?",
githubIds,
*connectionId,
))
} else {
clauses = append(clauses, dal.Where(`r.github_id IN (
SELECT github_id FROM _tool_github_repos
GROUP BY github_id
HAVING COUNT(DISTINCT connection_id) > 1
)`))
}
var rows []scopeDuplicateRow
if err := db.All(&rows, clauses...); err != nil {
return nil, err
}
return rows, nil
}
// groupScopeDuplicateRows collapses already-filtered SQL rows into API groups.
func groupScopeDuplicateRows(rows []scopeDuplicateRow) []ScopeDuplicateGroup {
if len(rows) == 0 {
return []ScopeDuplicateGroup{}
}
result := make([]ScopeDuplicateGroup, 0)
var current *ScopeDuplicateGroup
seenConns := make(map[uint64]struct{})
flush := func() {
if current != nil {
result = append(result, *current)
}
}
for _, row := range rows {
if current == nil || current.GithubId != row.GithubId {
flush()
current = &ScopeDuplicateGroup{
GithubId: row.GithubId,
HTMLUrl: row.HTMLUrl,
FullName: row.FullName,
Connections: make([]ScopeDuplicateConnection, 0, 2),
}
seenConns = make(map[uint64]struct{})
}
if current.HTMLUrl == "" && row.HTMLUrl != "" {
current.HTMLUrl = row.HTMLUrl
}
if current.FullName == "" && row.FullName != "" {
current.FullName = row.FullName
}
if _, ok := seenConns[row.ConnectionId]; ok {
continue
}
seenConns[row.ConnectionId] = struct{}{}
current.Connections = append(current.Connections, ScopeDuplicateConnection{
ConnectionId: row.ConnectionId,
ConnectionName: row.ConnectionName,
})
}
flush()
return result
}