mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-04-23 07:29:39 +00:00
feat: sync forks (#2364)
Some checks failed
/ release (push) Waiting to run
testing / backend-checks (push) Has been skipped
testing / frontend-checks (push) Has been skipped
testing / test-unit (push) Has been skipped
testing / test-e2e (push) Has been skipped
testing / test-mysql (push) Has been skipped
testing / test-pgsql (push) Has been skipped
testing / test-sqlite (push) Has been skipped
testing / test-remote-cacher (redis) (push) Has been skipped
testing / test-remote-cacher (valkey) (push) Has been skipped
testing / test-remote-cacher (garnet) (push) Has been skipped
testing / test-remote-cacher (redict) (push) Has been skipped
testing / security-check (push) Has been skipped
Integration tests for the release process / release-simulation (push) Has been cancelled
Some checks failed
/ release (push) Waiting to run
testing / backend-checks (push) Has been skipped
testing / frontend-checks (push) Has been skipped
testing / test-unit (push) Has been skipped
testing / test-e2e (push) Has been skipped
testing / test-mysql (push) Has been skipped
testing / test-pgsql (push) Has been skipped
testing / test-sqlite (push) Has been skipped
testing / test-remote-cacher (redis) (push) Has been skipped
testing / test-remote-cacher (valkey) (push) Has been skipped
testing / test-remote-cacher (garnet) (push) Has been skipped
testing / test-remote-cacher (redict) (push) Has been skipped
testing / security-check (push) Has been skipped
Integration tests for the release process / release-simulation (push) Has been cancelled
This allows syncing a branch of a fork with a branch of the base repo. It looks like this:  This is only possible, if the fork don't have commits that are not in the main repo. The feature is already working, but it is missing Finetuning, a better API, translations and tests, so this is currently WIP. It is also not tested with go-git. <!--start release-notes-assistant--> ## Release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Features - [PR](https://codeberg.org/forgejo/forgejo/pulls/2364): <!--number 2364 --><!--line 0 --><!--description c3luYyBmb3Jrcw==-->sync forks<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2364 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: JakobDev <jakobdev@gmx.de> Co-committed-by: JakobDev <jakobdev@gmx.de>
This commit is contained in:
parent
3272e3588a
commit
8296a23d79
15 changed files with 723 additions and 4 deletions
|
@ -432,6 +432,11 @@ func (c *Commit) GetBranchName() (string, error) {
|
|||
return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], nil
|
||||
}
|
||||
|
||||
// GetAllBranches returns a slice with all branches that contains this commit
|
||||
func (c *Commit) GetAllBranches() ([]string, error) {
|
||||
return c.repo.getBranches(c, -1)
|
||||
}
|
||||
|
||||
// CommitFileStatus represents status of files in a commit.
|
||||
type CommitFileStatus struct {
|
||||
Added []string
|
||||
|
|
|
@ -5,6 +5,7 @@ package git
|
|||
|
||||
import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
@ -370,6 +371,23 @@ func TestParseCommitRenames(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetAllBranches(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||
|
||||
bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path)
|
||||
require.NoError(t, err)
|
||||
|
||||
commit, err := bareRepo1.GetCommit("95bb4d39648ee7e325106df01a621c530863a653")
|
||||
require.NoError(t, err)
|
||||
|
||||
branches, err := commit.GetAllBranches()
|
||||
require.NoError(t, err)
|
||||
|
||||
slices.Sort(branches)
|
||||
|
||||
assert.Equal(t, []string{"branch1", "branch2", "master"}, branches)
|
||||
}
|
||||
|
||||
func Test_parseSubmoduleContent(t *testing.T) {
|
||||
submoduleFiles := []struct {
|
||||
fileContent string
|
||||
|
|
|
@ -444,10 +444,13 @@ func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit,
|
|||
|
||||
func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) {
|
||||
if CheckGitVersionAtLeast("2.7.0") == nil {
|
||||
stdout, _, err := NewCommand(repo.Ctx, "for-each-ref", "--format=%(refname:strip=2)").
|
||||
AddOptionFormat("--count=%d", limit).
|
||||
AddOptionValues("--contains", commit.ID.String(), BranchPrefix).
|
||||
RunStdString(&RunOpts{Dir: repo.Path})
|
||||
command := NewCommand(repo.Ctx, "for-each-ref", "--format=%(refname:strip=2)").AddOptionValues("--contains", commit.ID.String(), BranchPrefix)
|
||||
|
||||
if limit != -1 {
|
||||
command = command.AddOptionFormat("--count=%d", limit)
|
||||
}
|
||||
|
||||
stdout, _, err := command.RunStdString(&RunOpts{Dir: repo.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -10,3 +10,11 @@ type CreateForkOption struct {
|
|||
// name of the forked repository
|
||||
Name *string `json:"name"`
|
||||
}
|
||||
|
||||
// SyncForkInfo information about syncing a fork
|
||||
type SyncForkInfo struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
ForkCommit string `json:"fork_commit"`
|
||||
BaseCommit string `json:"base_commit"`
|
||||
CommitsBehind int `json:"commits_behind"`
|
||||
}
|
||||
|
|
|
@ -1220,6 +1220,10 @@ archive.title_date = This repository has been archived on %s. You can view files
|
|||
archive.nocomment = Commenting is not possible because the repository is archived.
|
||||
archive.pull.noreview = This repository is archived. You cannot review pull requests.
|
||||
|
||||
sync_fork.branch_behind_one = This branch is %d commit behind %s
|
||||
sync_fork.branch_behind_few = This branch is %d commits behind %s
|
||||
sync_fork.button = Sync
|
||||
|
||||
form.reach_limit_of_creation_1 = The owner has already reached the limit of %d repository.
|
||||
form.reach_limit_of_creation_n = The owner has already reached the limit of %d repositories.
|
||||
form.name_reserved = The repository name "%s" is reserved.
|
||||
|
|
|
@ -1355,6 +1355,12 @@ func Routes() *web.Route {
|
|||
m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar)
|
||||
m.Delete("", repo.DeleteAvatar)
|
||||
}, reqAdmin(), reqToken())
|
||||
m.Group("/sync_fork", func() {
|
||||
m.Get("", reqRepoReader(unit.TypeCode), repo.SyncForkDefaultInfo)
|
||||
m.Post("", mustNotBeArchived, reqRepoWriter(unit.TypeCode), repo.SyncForkDefault)
|
||||
m.Get("/{branch}", reqRepoReader(unit.TypeCode), repo.SyncForkBranchInfo)
|
||||
m.Post("/{branch}", mustNotBeArchived, reqRepoWriter(unit.TypeCode), repo.SyncForkBranch)
|
||||
})
|
||||
|
||||
m.Get("/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive)
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
|
|
185
routers/api/v1/repo/sync_fork.go
Normal file
185
routers/api/v1/repo/sync_fork.go
Normal file
|
@ -0,0 +1,185 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
git_model "forgejo.org/models/git"
|
||||
"forgejo.org/services/context"
|
||||
repo_service "forgejo.org/services/repository"
|
||||
)
|
||||
|
||||
func getSyncForkInfo(ctx *context.APIContext, branch string) {
|
||||
if !ctx.Repo.Repository.IsFork {
|
||||
ctx.Error(http.StatusBadRequest, "NoFork", "The Repo must be a fork")
|
||||
return
|
||||
}
|
||||
|
||||
syncForkInfo, err := repo_service.GetSyncForkInfo(ctx, ctx.Repo.Repository, branch)
|
||||
if err != nil {
|
||||
if git_model.IsErrBranchNotExist(err) {
|
||||
ctx.NotFound(err, branch)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Error(http.StatusInternalServerError, "GetSyncForkInfo", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, syncForkInfo)
|
||||
}
|
||||
|
||||
// SyncForkBranchInfo returns information about syncing the default fork branch with the base branch
|
||||
func SyncForkDefaultInfo(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/sync_fork repository repoSyncForkDefaultInfo
|
||||
// ---
|
||||
// summary: Gets information about syncing the fork default branch with the base branch
|
||||
// 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
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/SyncForkInfo"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
getSyncForkInfo(ctx, ctx.Repo.Repository.DefaultBranch)
|
||||
}
|
||||
|
||||
// SyncForkBranchInfo returns information about syncing a fork branch with the base branch
|
||||
func SyncForkBranchInfo(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/sync_fork/{branch} repository repoSyncForkBranchInfo
|
||||
// ---
|
||||
// summary: Gets information about syncing a fork branch with the base branch
|
||||
// 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: branch
|
||||
// in: path
|
||||
// description: The branch
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/SyncForkInfo"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
getSyncForkInfo(ctx, ctx.Params("branch"))
|
||||
}
|
||||
|
||||
func syncForkBranch(ctx *context.APIContext, branch string) {
|
||||
if !ctx.Repo.Repository.IsFork {
|
||||
ctx.Error(http.StatusBadRequest, "NoFork", "The Repo must be a fork")
|
||||
return
|
||||
}
|
||||
|
||||
syncForkInfo, err := repo_service.GetSyncForkInfo(ctx, ctx.Repo.Repository, branch)
|
||||
if err != nil {
|
||||
if git_model.IsErrBranchNotExist(err) {
|
||||
ctx.NotFound(err, branch)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Error(http.StatusInternalServerError, "GetSyncForkInfo", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !syncForkInfo.Allowed {
|
||||
ctx.Error(http.StatusBadRequest, "NotAllowed", "You can't sync this branch")
|
||||
return
|
||||
}
|
||||
|
||||
err = repo_service.SyncFork(ctx, ctx.Doer, ctx.Repo.Repository, branch)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "SyncFork", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// SyncForkBranch syncs the default of a fork with the base branch
|
||||
func SyncForkDefault(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/sync_fork repository repoSyncForkDefault
|
||||
// ---
|
||||
// summary: Syncs the default branch of a fork with the base branch
|
||||
// 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
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
syncForkBranch(ctx, ctx.Repo.Repository.DefaultBranch)
|
||||
}
|
||||
|
||||
// SyncForkBranch syncs a fork branch with the base branch
|
||||
func SyncForkBranch(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/sync_fork/{branch} repository repoSyncForkBranch
|
||||
// ---
|
||||
// summary: Syncs a fork branch with the base branch
|
||||
// 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: branch
|
||||
// in: path
|
||||
// description: The branch
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
syncForkBranch(ctx, ctx.Params("branch"))
|
||||
}
|
|
@ -448,3 +448,10 @@ type swaggerCompare struct {
|
|||
// in:body
|
||||
Body api.Compare `json:"body"`
|
||||
}
|
||||
|
||||
// SyncForkInfo
|
||||
// swagger:response SyncForkInfo
|
||||
type swaggerSyncForkInfo struct {
|
||||
// in:body
|
||||
Body []api.SyncForkInfo `json:"body"`
|
||||
}
|
||||
|
|
|
@ -782,3 +782,27 @@ func PrepareBranchList(ctx *context.Context) {
|
|||
}
|
||||
ctx.Data["Branches"] = brs
|
||||
}
|
||||
|
||||
func SyncFork(ctx *context.Context) {
|
||||
redirectURL := fmt.Sprintf("%s/src/branch/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(ctx.Repo.BranchName))
|
||||
branch := ctx.Params("branch")
|
||||
|
||||
syncForkInfo, err := repo_service.GetSyncForkInfo(ctx, ctx.Repo.Repository, branch)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetSyncForkInfo", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !syncForkInfo.Allowed {
|
||||
ctx.Redirect(redirectURL)
|
||||
return
|
||||
}
|
||||
|
||||
err = repo_service.SyncFork(ctx, ctx.Doer, ctx.Repo.Repository, branch)
|
||||
if err != nil {
|
||||
ctx.ServerError("SyncFork", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(redirectURL)
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ import (
|
|||
"forgejo.org/routers/web/feed"
|
||||
"forgejo.org/services/context"
|
||||
issue_service "forgejo.org/services/issue"
|
||||
repo_service "forgejo.org/services/repository"
|
||||
files_service "forgejo.org/services/repository/files"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
|
@ -1154,6 +1155,21 @@ PostRecentBranchCheck:
|
|||
}
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository.IsFork && ctx.Repo.IsViewBranch && len(ctx.Repo.TreePath) == 0 && ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
|
||||
syncForkInfo, err := repo_service.GetSyncForkInfo(ctx, ctx.Repo.Repository, ctx.Repo.BranchName)
|
||||
if err != nil {
|
||||
ctx.ServerError("CanSync", err)
|
||||
return
|
||||
}
|
||||
|
||||
if syncForkInfo.Allowed {
|
||||
ctx.Data["CanSyncFork"] = true
|
||||
ctx.Data["ForkCommitsBehind"] = syncForkInfo.CommitsBehind
|
||||
ctx.Data["SyncForkLink"] = fmt.Sprintf("%s/sync_fork/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(ctx.Repo.BranchName))
|
||||
ctx.Data["BaseBranchLink"] = fmt.Sprintf("%s/src/branch/%s", ctx.Repo.Repository.BaseRepo.HTMLURL(), util.PathEscapeSegments(ctx.Repo.BranchName))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Paths"] = paths
|
||||
|
||||
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
||||
|
|
|
@ -1592,6 +1592,8 @@ func registerRoutes(m *web.Route) {
|
|||
}, context.RepoRef(), reqRepoCodeReader)
|
||||
}
|
||||
m.Get("/commit/{sha:([a-f0-9]{4,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
|
||||
|
||||
m.Get("/sync_fork/{branch}", context.RepoMustNotBeArchived(), repo.MustBeNotEmpty, reqRepoCodeWriter, repo.SyncFork)
|
||||
}, ignSignIn, context.RepoAssignment, context.UnitTypes())
|
||||
|
||||
m.Post("/{username}/{reponame}/lastcommit/*", ignSignInAndCsrf, context.RepoAssignment, context.UnitTypes(), context.RepoRefByType(context.RepoRefCommit), reqRepoCodeReader, repo.LastCommit)
|
||||
|
|
113
services/repository/sync_fork.go
Normal file
113
services/repository/sync_fork.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
git_model "forgejo.org/models/git"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/git"
|
||||
repo_module "forgejo.org/modules/repository"
|
||||
api "forgejo.org/modules/structs"
|
||||
)
|
||||
|
||||
// SyncFork syncs a branch of a fork with the base repo
|
||||
func SyncFork(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) error {
|
||||
err := repo.MustNotBeArchived()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = repo.GetBaseRepo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{
|
||||
Remote: repo.RepoPath(),
|
||||
Branch: fmt.Sprintf("%s:%s", branch, branch),
|
||||
Env: repo_module.PushingEnvironment(doer, repo),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// CanSyncFork returns information about syncing a fork
|
||||
func GetSyncForkInfo(ctx context.Context, repo *repo_model.Repository, branch string) (*api.SyncForkInfo, error) {
|
||||
info := new(api.SyncForkInfo)
|
||||
|
||||
if !repo.IsFork {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
if repo.IsArchived {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
err := repo.GetBaseRepo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.ForkCommit = forkBranch.CommitID
|
||||
|
||||
baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, branch)
|
||||
if err != nil {
|
||||
if git_model.IsErrBranchNotExist(err) {
|
||||
// If the base repo don't have the branch, we don't need to continue
|
||||
return info, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.BaseCommit = baseBranch.CommitID
|
||||
|
||||
// If both branches has the same latest commit, we don't need to sync
|
||||
if forkBranch.CommitID == baseBranch.CommitID {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Check if the latest commit of the fork is also in the base
|
||||
gitRepo, err := git.OpenRepository(ctx, repo.BaseRepo.RepoPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetCommit(forkBranch.CommitID)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
return info, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
branchList, err := commit.GetAllBranches()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !slices.Contains(branchList, branch) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.Allowed = true
|
||||
info.CommitsBehind = diff.Behind
|
||||
|
||||
return info, nil
|
||||
}
|
|
@ -158,6 +158,18 @@
|
|||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .CanSyncFork}}
|
||||
<div class="ui positive message tw-flex tw-items-center">
|
||||
<div class="tw-flex-1">
|
||||
{{ctx.Locale.TrN .ForkCommitsBehind "repo.sync_fork.branch_behind_one" "repo.sync_fork.branch_behind_few" .ForkCommitsBehind (printf "<a href='%s'>%s:%s</a>" .BaseBranchLink .Repository.BaseRepo.FullName .BranchName | SafeHTML)}}
|
||||
</div>
|
||||
<a role="button" class="ui compact positive button tw-m-0" href="{{.SyncForkLink}}">
|
||||
{{ctx.Locale.Tr "repo.sync_fork.button"}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .IsViewFile}}
|
||||
{{template "repo/view_file" .}}
|
||||
{{else if .IsBlame}}
|
||||
|
|
199
templates/swagger/v1_json.tmpl
generated
199
templates/swagger/v1_json.tmpl
generated
|
@ -15630,6 +15630,172 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/sync_fork": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Gets information about syncing the fork default branch with the base branch",
|
||||
"operationId": "repoSyncForkDefaultInfo",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/SyncForkInfo"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/error"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Syncs the default branch of a fork with the base branch",
|
||||
"operationId": "repoSyncForkDefault",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/error"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/sync_fork/{branch}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Gets information about syncing a fork branch with the base branch",
|
||||
"operationId": "repoSyncForkBranchInfo",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The branch",
|
||||
"name": "branch",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/SyncForkInfo"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/error"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Syncs a fork branch with the base branch",
|
||||
"operationId": "repoSyncForkBranch",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The branch",
|
||||
"name": "branch",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/error"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/tag_protections": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
@ -27432,6 +27598,30 @@
|
|||
},
|
||||
"x-go-package": "forgejo.org/modules/structs"
|
||||
},
|
||||
"SyncForkInfo": {
|
||||
"description": "SyncForkInfo information about syncing a fork",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowed": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "Allowed"
|
||||
},
|
||||
"base_commit": {
|
||||
"type": "string",
|
||||
"x-go-name": "BaseCommit"
|
||||
},
|
||||
"commits_behind": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "CommitsBehind"
|
||||
},
|
||||
"fork_commit": {
|
||||
"type": "string",
|
||||
"x-go-name": "ForkCommit"
|
||||
}
|
||||
},
|
||||
"x-go-package": "forgejo.org/modules/structs"
|
||||
},
|
||||
"Tag": {
|
||||
"description": "Tag represents a repository tag",
|
||||
"type": "object",
|
||||
|
@ -29211,6 +29401,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"SyncForkInfo": {
|
||||
"description": "SyncForkInfo",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/SyncForkInfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Tag": {
|
||||
"description": "Tag",
|
||||
"schema": {
|
||||
|
|
117
tests/integration/repo_sync_fork_test.go
Normal file
117
tests/integration/repo_sync_fork_test.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
api "forgejo.org/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func syncForkTest(t *testing.T, forkName, urlPart string, webSync bool) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20})
|
||||
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
baseUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: baseRepo.OwnerID})
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
/// Create a new fork
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.LowerName), &api.CreateForkOption{Name: &forkName}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusAccepted)
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/%s", user.Name, forkName, urlPart).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var syncForkInfo *api.SyncForkInfo
|
||||
DecodeJSON(t, resp, &syncForkInfo)
|
||||
|
||||
// This is a new fork, so the commits in both branches should be the same
|
||||
assert.False(t, syncForkInfo.Allowed)
|
||||
assert.Equal(t, syncForkInfo.BaseCommit, syncForkInfo.ForkCommit)
|
||||
|
||||
// Make a commit on the base branch
|
||||
err := createOrReplaceFileInBranch(baseUser, baseRepo, "sync_fork.txt", "master", "Hello")
|
||||
require.NoError(t, err)
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/%s", user.Name, forkName, urlPart).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
DecodeJSON(t, resp, &syncForkInfo)
|
||||
|
||||
// The commits should no longer be the same and we can sync
|
||||
assert.True(t, syncForkInfo.Allowed)
|
||||
assert.NotEqual(t, syncForkInfo.BaseCommit, syncForkInfo.ForkCommit)
|
||||
|
||||
// Sync the fork
|
||||
if webSync {
|
||||
session.MakeRequest(t, NewRequestf(t, "GET", "/%s/%s/sync_fork/master", user.Name, forkName), http.StatusSeeOther)
|
||||
} else {
|
||||
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/%s", user.Name, forkName, urlPart).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/%s", user.Name, forkName, urlPart).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
DecodeJSON(t, resp, &syncForkInfo)
|
||||
|
||||
// After the sync both commits should be the same again
|
||||
assert.False(t, syncForkInfo.Allowed)
|
||||
assert.Equal(t, syncForkInfo.BaseCommit, syncForkInfo.ForkCommit)
|
||||
}
|
||||
|
||||
func TestAPIRepoSyncForkDefault(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
syncForkTest(t, "SyncForkDefault", "sync_fork", false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIRepoSyncForkBranch(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
syncForkTest(t, "SyncForkBranch", "sync_fork/master", false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWebRepoSyncForkBranch(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
syncForkTest(t, "SyncForkBranch", "sync_fork/master", true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWebRepoSyncForkHomepage(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
forkName := "SyncForkHomepage"
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20})
|
||||
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
baseUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: baseRepo.OwnerID})
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
/// Create a new fork
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.LowerName), &api.CreateForkOption{Name: &forkName}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusAccepted)
|
||||
|
||||
// Make a commit on the base branch
|
||||
err := createOrReplaceFileInBranch(baseUser, baseRepo, "sync_fork.txt", "master", "Hello")
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/%s", user.Name, forkName), http.StatusOK)
|
||||
|
||||
assert.Contains(t, resp.Body.String(), fmt.Sprintf("This branch is 1 commit behind <a href='http://localhost:%s/user2/repo1/src/branch/master'>user2/repo1:master</a>", u.Port()))
|
||||
})
|
||||
}
|
Loading…
Add table
Reference in a new issue