Files
gitea/routers/api/v1/repo/mirror.go
T
a4d1711777 fix(api): remove git remote when deleting a push mirror (#40049)
Fixes https://github.com/go-gitea/gitea/issues/40037

`DELETE /repos/{owner}/{repo}/push_mirrors/{name}` only deleted the
`push_mirror` row and left the `remote_mirror_*` git remotes, which
embed the mirror credentials, in the repository and wiki config. Each
create/delete cycle through the API left more credentials behind.

- Remove the git remotes before deleting the row and record it in the
audit log, like the web settings handler.
- Return 404 for an unknown name, as documented, instead of 204.
- Ignore an already missing code remote, so the wiki remote is still
removed and such mirrors can be deleted through both the API and the web
settings.

---

AI disclosure: I used an AI coding assistant to help write this change
and the PR description. I reviewed the code and ran the tests myself.

---------

Signed-off-by: sarankumar1325 <120080287+sarankumar1325@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-31 17:47:36 +00:00

395 lines
10 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/migrations"
mirror_service "gitea.dev/services/mirror"
)
// MirrorSync adds a mirrored repository to the sync queue
func MirrorSync(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
// ---
// summary: Sync a mirrored repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo to sync
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to sync
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/empty"
// default:
// "$ref": "#/responses/error"
repo := ctx.Repo.Repository
if !ctx.Repo.Permission.CanWrite(unit.TypeCode) {
ctx.APIError(http.StatusForbidden, "Must have write access")
}
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
if _, err := repo_model.GetMirrorByRepoID(ctx, repo.ID); err != nil {
if errors.Is(err, repo_model.ErrMirrorNotExist) {
ctx.APIError(http.StatusBadRequest, "Repository is not a mirror")
return
}
ctx.APIErrorInternal(err)
return
}
mirror_service.AddPullMirrorToQueue(repo.ID)
ctx.Status(http.StatusOK)
}
// PushMirrorSync adds all push mirrored repositories to the sync queue
func PushMirrorSync(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/push_mirrors-sync repository repoPushMirrorSync
// ---
// summary: Sync all push mirrored repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo to sync
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to sync
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/empty"
// default:
// "$ref": "#/responses/error"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
// Get All push mirrors of a specific repo
pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
if err != nil {
ctx.APIError(http.StatusNotFound, err.Error())
return
}
failedPushMirrors := make([]string, 0)
for _, mirror := range pushMirrors {
ok := mirror_service.SyncPushMirror(ctx, mirror.ID)
if !ok {
failedPushMirrors = append(failedPushMirrors, mirror.RemoteName)
}
}
if len(failedPushMirrors) != 0 {
ctx.APIError(http.StatusUnprocessableEntity, "error occurred when syncing push mirrors: "+strings.Join(failedPushMirrors, ", "))
return
}
ctx.Status(http.StatusOK)
}
// ListPushMirrors get list of push mirrors of a repository
func ListPushMirrors(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/push_mirrors repository repoListPushMirrors
// ---
// summary: Get all push mirrors of the repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/PushMirrorList"
// default:
// "$ref": "#/responses/error"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
repo := ctx.Repo.Repository
// Get all push mirrors for the specified repository.
pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.APIError(http.StatusNotFound, err.Error())
return
}
responsePushMirrors := make([]*api.PushMirror, 0, len(pushMirrors))
for _, mirror := range pushMirrors {
m, err := convert.ToPushMirror(ctx, mirror)
if err == nil {
responsePushMirrors = append(responsePushMirrors, m)
}
}
ctx.SetLinkHeader(int64(len(responsePushMirrors)), utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, responsePushMirrors)
}
// GetPushMirrorByName get push mirror of a repository by name
func GetPushMirrorByName(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/push_mirrors/{name} repository repoGetPushMirrorByRemoteName
// ---
// summary: Get push mirror of the repository by remoteName
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: name
// in: path
// description: remote name of push mirror
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/PushMirror"
// default:
// "$ref": "#/responses/error"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
mirrorName := ctx.PathParam("name")
pushMirror, err := repo_model.GetRepoPushMirrorByRemoteName(ctx, ctx.Repo.Repository.ID, mirrorName)
if err != nil {
ctx.APIErrorAuto(err)
return
}
m, err := convert.ToPushMirror(ctx, pushMirror)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, m)
}
// AddPushMirror adds a push mirror to a repository
func AddPushMirror(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/push_mirrors repository repoAddPushMirror
// ---
// summary: add a push mirror to the repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreatePushMirrorOption"
// responses:
// "200":
// "$ref": "#/responses/PushMirror"
// default:
// "$ref": "#/responses/error"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
if setting.Mirror.DisableNewPush {
ctx.APIError(http.StatusForbidden, "the site administrator has disabled the creation of new push mirrors")
return
}
pushMirror := web.GetForm[*api.CreatePushMirrorOption](ctx)
CreatePushMirror(ctx, pushMirror)
}
// DeletePushMirrorByRemoteName deletes a push mirror from a repository by remoteName
func DeletePushMirrorByRemoteName(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/push_mirrors/{name} repository repoDeletePushMirror
// ---
// summary: deletes a push mirror from a repository by remoteName
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: name
// in: path
// description: remote name of the pushMirror
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// default:
// "$ref": "#/responses/error"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
pushMirror, err := repo_model.GetRepoPushMirrorByRemoteName(ctx, ctx.Repo.Repository.ID, ctx.PathParam("name"))
if err != nil {
ctx.APIErrorAuto(err)
return
}
err = mirror_service.DeleteRepoPushMirrorByID(ctx, ctx.Repo.Repository.ID, pushMirror.ID)
if err != nil {
ctx.APIErrorAuto(err)
return
}
ctx.Status(http.StatusNoContent)
}
func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirrorOption) {
repo := ctx.Repo.Repository
interval, err := time.ParseDuration(mirrorOption.Interval)
if err != nil {
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("invalid interval: %v", err))
return
}
if interval != 0 && interval < setting.Mirror.MinInterval {
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("interval is shorter than minimum %v", setting.Mirror.MinInterval.String()))
return
}
address, err := git.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
if err == nil {
err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
}
if err != nil {
HandleRemoteAddressError(ctx, err)
return
}
remoteSuffix := util.CryptoRandomString(10)
remoteAddress, err := util.SanitizeURL(mirrorOption.RemoteAddress)
if err != nil {
ctx.APIErrorInternal(err)
return
}
pushMirror := &repo_model.PushMirror{
RepoID: repo.ID,
Repo: repo,
RemoteName: "remote_mirror_" + remoteSuffix,
Interval: interval,
SyncOnCommit: mirrorOption.SyncOnCommit,
RemoteAddress: remoteAddress,
}
if err = db.Insert(ctx, pushMirror); err != nil {
ctx.APIErrorInternal(err)
return
}
// if the registration of the push mirrorOption fails remove it from the database
if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil {
ctx.APIErrorInternal(err)
return
}
m, err := convert.ToPushMirror(ctx, pushMirror)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, m)
}
func HandleRemoteAddressError(ctx *context.APIContext, err error) {
if addrErr, ok := err.(*git.ErrInvalidCloneAddr); ok {
switch {
case addrErr.IsProtocolInvalid:
ctx.APIError(http.StatusBadRequest, "Invalid mirror protocol")
case addrErr.IsURLError:
ctx.APIError(http.StatusBadRequest, "Invalid Url ")
case addrErr.IsPermissionDenied:
ctx.APIError(http.StatusUnauthorized, "Permission denied")
default:
ctx.APIError(http.StatusBadRequest, "Unknown error")
}
return
}
}