Merge branch 'main' into feat-version-arch

pull/33262/head
Exploding Dragon 2 weeks ago committed by GitHub
commit 220f23a96e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      models/git/branch.go
  2. 2
      options/locale/locale_en-US.ini
  3. 1
      options/locale/locale_pt-PT.ini
  4. 12
      routers/common/middleware.go
  5. 2
      routers/web/repo/code_frequency.go
  6. 2
      routers/web/repo/contributors.go
  7. 2
      routers/web/repo/recent_commits.go
  8. 10
      routers/web/repo/search.go
  9. 2
      routers/web/repo/setting/webhook.go
  10. 4
      routers/web/web.go
  11. 103
      services/context/access_log.go
  12. 75
      services/context/access_log_test.go
  13. 37
      services/context/response.go
  14. 17
      services/repository/branch.go
  15. 48
      services/repository/merge_upstream.go
  16. 14
      templates/repo/code/upstream_diverging_info.tmpl
  17. 51
      tests/integration/repo_merge_upstream_test.go

@ -167,6 +167,9 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
BranchName: branchName,
}
}
// FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again.
// It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs
// In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted`
return &branch, nil
}

@ -1953,7 +1953,7 @@ pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[
pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s
pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes
pulls.upstream_diverging_merge = Sync fork
pulls.upstream_diverging_merge_confirm = Would you like to merge base repository's default branch onto this repository's branch %s?
pulls.upstream_diverging_merge_confirm = Would you like to merge "%[1]s" onto "%[2]s"?
pull.deleted_branch = (deleted):%s
pull.agit_documentation = Review documentation about AGit

@ -1952,7 +1952,6 @@ pulls.upstream_diverging_prompt_behind_1=Este ramo está %[1]d cometimento atrá
pulls.upstream_diverging_prompt_behind_n=Este ramo está %[1]d cometimentos atrás de %[2]s
pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações
pulls.upstream_diverging_merge=Sincronizar derivação
pulls.upstream_diverging_merge_confirm=Gostaria de integrar o ramo principal do repositório base no ramo %s deste repositório?
pull.deleted_branch=(eliminado):%s
pull.agit_documentation=Rever a documentação sobre o AGit

@ -43,14 +43,18 @@ func ProtocolMiddlewares() (handlers []any) {
func RequestContextHandler() func(h http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
profDesc := fmt.Sprintf("%s: %s", req.Method, req.RequestURI)
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
// this response writer might not be the same as the one in context.Base.Resp
// because there might be a "gzip writer" in the middle, so the "written size" here is the compressed size
respWriter := context.WrapResponseWriter(respOrig)
profDesc := fmt.Sprintf("HTTP: %s %s", req.Method, req.RequestURI)
ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc)
defer finished()
defer func() {
if err := recover(); err != nil {
RenderPanicErrorPage(resp, req, err) // it should never panic
RenderPanicErrorPage(respWriter, req, err) // it should never panic
}
}()
@ -62,7 +66,7 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
_ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
}
})
next.ServeHTTP(context.WrapResponseWriter(resp), req)
next.ServeHTTP(respWriter, req)
})
}
}

@ -29,7 +29,7 @@ func CodeFrequency(ctx *context.Context) {
// CodeFrequencyData returns JSON of code frequency data
func CodeFrequencyData(ctx *context.Context) {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return

@ -26,7 +26,7 @@ func Contributors(ctx *context.Context) {
// ContributorsData renders JSON of contributors along with their weekly commit statistics
func ContributorsData(ctx *context.Context) {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return

@ -29,7 +29,7 @@ func RecentCommits(ctx *context.Context) {
// RecentCommitsData returns JSON of recent commits data
func RecentCommitsData(ctx *context.Context) {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return

@ -67,10 +67,11 @@ func Search(ctx *context.Context) {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
}
} else {
searchRefName := git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch) // BranchName should be default branch or the first existing branch
res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, prepareSearch.Keyword, git.GrepOptions{
ContextLineNumber: 1,
IsFuzzy: prepareSearch.IsFuzzy,
RefName: git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch).String(), // BranchName should be default branch or the first existing branch
RefName: searchRefName.String(),
PathspecList: indexSettingToGitGrepPathspecList(),
})
if err != nil {
@ -78,6 +79,11 @@ func Search(ctx *context.Context) {
ctx.ServerError("GrepSearch", err)
return
}
commitID, err := ctx.Repo.GitRepo.GetRefCommitID(searchRefName.String())
if err != nil {
ctx.ServerError("GetRefCommitID", err)
return
}
total = len(res)
pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res))
pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res))
@ -86,7 +92,7 @@ func Search(ctx *context.Context) {
searchResults = append(searchResults, &code_indexer.Result{
RepoID: ctx.Repo.Repository.ID,
Filename: r.Filename,
CommitID: ctx.Repo.CommitID,
CommitID: commitID,
// UpdatedUnix: not supported yet
// Language: not supported yet
// Color: not supported yet

@ -654,6 +654,8 @@ func TestWebhook(ctx *context.Context) {
}
// Grab latest commit or fake one if it's empty repository.
// Note: in old code, the "ctx.Repo.Commit" is the last commit of the default branch.
// New code doesn't set that commit, so it always uses the fake commit to test webhook.
commit := ctx.Repo.Commit
if commit == nil {
ghost := user_model.NewGhostUser()

@ -1146,7 +1146,7 @@ func registerRoutes(m *web.Router) {
m.Post("/cancel", repo.MigrateCancelPost)
})
},
reqSignIn, context.RepoAssignment, reqRepoAdmin, context.RepoRef(),
reqSignIn, context.RepoAssignment, reqRepoAdmin,
ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer),
)
// end "/{username}/{reponame}/settings"
@ -1513,7 +1513,7 @@ func registerRoutes(m *web.Router) {
m.Group("/activity_author_data", func() {
m.Get("", repo.ActivityAuthors)
m.Get("/{period}", repo.ActivityAuthors)
}, context.RepoRef(), repo.MustBeNotEmpty)
}, repo.MustBeNotEmpty)
m.Group("/archive", func() {
m.Get("/*", repo.Download)

@ -18,13 +18,14 @@ import (
"code.gitea.io/gitea/modules/web/middleware"
)
type routerLoggerOptions struct {
req *http.Request
type accessLoggerTmplData struct {
Identity *string
Start *time.Time
ResponseWriter http.ResponseWriter
Ctx map[string]any
RequestID *string
ResponseWriter struct {
Status, Size int
}
Ctx map[string]any
RequestID *string
}
const keyOfRequestIDInTemplate = ".RequestID"
@ -51,51 +52,65 @@ func parseRequestIDFromRequestHeader(req *http.Request) string {
return requestID
}
type accessLogRecorder struct {
logger log.BaseLogger
logTemplate *template.Template
needRequestID bool
}
func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter, req *http.Request) {
var requestID string
if lr.needRequestID {
requestID = parseRequestIDFromRequestHeader(req)
}
reqHost, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
reqHost = req.RemoteAddr
}
identity := "-"
data := middleware.GetContextData(req.Context())
if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
identity = signedUser.Name
}
buf := bytes.NewBuffer([]byte{})
tmplData := accessLoggerTmplData{
Identity: &identity,
Start: &start,
Ctx: map[string]any{
"RemoteAddr": req.RemoteAddr,
"RemoteHost": reqHost,
"Req": req,
},
RequestID: &requestID,
}
tmplData.ResponseWriter.Status = respWriter.Status()
tmplData.ResponseWriter.Size = respWriter.WrittenSize()
err = lr.logTemplate.Execute(buf, tmplData)
if err != nil {
log.Error("Could not execute access logger template: %v", err.Error())
}
lr.logger.Log(1, log.INFO, "%s", buf.String())
}
func newAccessLogRecorder() *accessLogRecorder {
return &accessLogRecorder{
logger: log.GetLogger("access"),
logTemplate: template.Must(template.New("log").Parse(setting.Log.AccessLogTemplate)),
needRequestID: len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate),
}
}
// AccessLogger returns a middleware to log access logger
func AccessLogger() func(http.Handler) http.Handler {
logger := log.GetLogger("access")
needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate)
logTemplate, _ := template.New("log").Parse(setting.Log.AccessLogTemplate)
recorder := newAccessLogRecorder()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
var requestID string
if needRequestID {
requestID = parseRequestIDFromRequestHeader(req)
}
reqHost, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
reqHost = req.RemoteAddr
}
next.ServeHTTP(w, req)
rw := w.(ResponseWriter)
identity := "-"
data := middleware.GetContextData(req.Context())
if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
identity = signedUser.Name
}
buf := bytes.NewBuffer([]byte{})
err = logTemplate.Execute(buf, routerLoggerOptions{
req: req,
Identity: &identity,
Start: &start,
ResponseWriter: rw,
Ctx: map[string]any{
"RemoteAddr": req.RemoteAddr,
"RemoteHost": reqHost,
"Req": req,
},
RequestID: &requestID,
})
if err != nil {
log.Error("Could not execute access logger template: %v", err.Error())
}
logger.Info("%s", buf.String())
recorder.record(start, w.(ResponseWriter), req)
})
}
}

@ -0,0 +1,75 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"fmt"
"net/http"
"net/url"
"testing"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
type testAccessLoggerMock struct {
logs []string
}
func (t *testAccessLoggerMock) Log(skip int, level log.Level, format string, v ...any) {
t.logs = append(t.logs, fmt.Sprintf(format, v...))
}
func (t *testAccessLoggerMock) GetLevel() log.Level {
return log.INFO
}
type testAccessLoggerResponseWriterMock struct{}
func (t testAccessLoggerResponseWriterMock) Header() http.Header {
return nil
}
func (t testAccessLoggerResponseWriterMock) Before(f func(ResponseWriter)) {}
func (t testAccessLoggerResponseWriterMock) WriteHeader(statusCode int) {}
func (t testAccessLoggerResponseWriterMock) Write(bytes []byte) (int, error) {
return 0, nil
}
func (t testAccessLoggerResponseWriterMock) Flush() {}
func (t testAccessLoggerResponseWriterMock) WrittenStatus() int {
return http.StatusOK
}
func (t testAccessLoggerResponseWriterMock) Status() int {
return t.WrittenStatus()
}
func (t testAccessLoggerResponseWriterMock) WrittenSize() int {
return 123123
}
func TestAccessLogger(t *testing.T) {
setting.Log.AccessLogTemplate = `{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"`
recorder := newAccessLogRecorder()
mockLogger := &testAccessLoggerMock{}
recorder.logger = mockLogger
req := &http.Request{
RemoteAddr: "remote-addr",
Method: "GET",
Proto: "https",
URL: &url.URL{Path: "/path"},
}
req.Header = http.Header{}
req.Header.Add("Referer", "referer")
req.Header.Add("User-Agent", "user-agent")
recorder.record(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), &testAccessLoggerResponseWriterMock{}, req)
assert.Equal(t, []string{`remote-addr - - [02/Jan/2000:03:04:05 +0000] "GET /path https" 200 123123 "referer" "user-agent"`}, mockLogger.logs)
}

@ -15,27 +15,26 @@ type ResponseWriter interface {
http.Flusher
web_types.ResponseStatusProvider
Before(func(ResponseWriter))
Status() int // used by access logger template
Size() int // used by access logger template
Before(fn func(ResponseWriter))
Status() int
WrittenSize() int
}
var _ ResponseWriter = &Response{}
var _ ResponseWriter = (*Response)(nil)
// Response represents a response
type Response struct {
http.ResponseWriter
written int
status int
befores []func(ResponseWriter)
beforeFuncs []func(ResponseWriter)
beforeExecuted bool
}
// Write writes bytes to HTTP endpoint
func (r *Response) Write(bs []byte) (int, error) {
if !r.beforeExecuted {
for _, before := range r.befores {
for _, before := range r.beforeFuncs {
before(r)
}
r.beforeExecuted = true
@ -51,18 +50,14 @@ func (r *Response) Write(bs []byte) (int, error) {
return size, nil
}
func (r *Response) Status() int {
return r.status
}
func (r *Response) Size() int {
func (r *Response) WrittenSize() int {
return r.written
}
// WriteHeader write status code
func (r *Response) WriteHeader(statusCode int) {
if !r.beforeExecuted {
for _, before := range r.befores {
for _, before := range r.beforeFuncs {
before(r)
}
r.beforeExecuted = true
@ -80,6 +75,12 @@ func (r *Response) Flush() {
}
}
// Status returns status code written
// TODO: use WrittenStatus instead
func (r *Response) Status() int {
return r.status
}
// WrittenStatus returned status code written
func (r *Response) WrittenStatus() int {
return r.status
@ -87,17 +88,13 @@ func (r *Response) WrittenStatus() int {
// Before allows for a function to be called before the ResponseWriter has been written to. This is
// useful for setting headers or any other operations that must happen before a response has been written.
func (r *Response) Before(f func(ResponseWriter)) {
r.befores = append(r.befores, f)
func (r *Response) Before(fn func(ResponseWriter)) {
r.beforeFuncs = append(r.beforeFuncs, fn)
}
func WrapResponseWriter(resp http.ResponseWriter) *Response {
if v, ok := resp.(*Response); ok {
return v
}
return &Response{
ResponseWriter: resp,
status: 0,
befores: make([]func(ResponseWriter), 0),
}
return &Response{ResponseWriter: resp}
}

@ -668,9 +668,12 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
}
// BranchDivergingInfo contains the information about the divergence of a head branch to the base branch.
// This struct is also used in templates, so it needs to search for all references before changing it.
type BranchDivergingInfo struct {
// whether the base branch contains new commits which are not in the head branch
BaseHasNewCommits bool
// behind/after are number of commits that the head branch is behind/after the base branch, it's 0 if it's unable to calculate.
// there could be a case that BaseHasNewCommits=true while the behind/after are both 0 (unable to calculate).
HeadCommitsBehind int
HeadCommitsAhead int
}
@ -681,11 +684,20 @@ func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repo
if err != nil {
return nil, err
}
if headGitBranch.IsDeleted {
return nil, git_model.ErrBranchNotExist{
BranchName: headBranch,
}
}
baseGitBranch, err := git_model.GetBranch(ctx, baseRepo.ID, baseBranch)
if err != nil {
return nil, err
}
if baseGitBranch.IsDeleted {
return nil, git_model.ErrBranchNotExist{
BranchName: baseBranch,
}
}
info := &BranchDivergingInfo{}
if headGitBranch.CommitID == baseGitBranch.CommitID {
@ -720,5 +732,6 @@ func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repo
}
info.HeadCommitsBehind, info.HeadCommitsAhead = diff.Behind, diff.Ahead
info.BaseHasNewCommits = info.HeadCommitsBehind > 0
return info, nil
}

@ -4,7 +4,7 @@
package repository
import (
"context"
"errors"
"fmt"
issue_model "code.gitea.io/gitea/models/issues"
@ -18,16 +18,24 @@ import (
)
// MergeUpstream merges the base repository's default branch into the fork repository's current branch.
func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
if err = repo.MustNotBeArchived(); err != nil {
return "", err
}
if err = repo.GetBaseRepo(ctx); err != nil {
return "", err
}
divergingInfo, err := GetUpstreamDivergingInfo(ctx, repo, branch)
if err != nil {
return "", err
}
if !divergingInfo.BaseBranchHasNewCommits {
return "up-to-date", nil
}
err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{
Remote: repo.RepoPath(),
Branch: fmt.Sprintf("%s:%s", repo.BaseRepo.DefaultBranch, branch),
Branch: fmt.Sprintf("%s:%s", divergingInfo.BaseBranchName, branch),
Env: repo_module.PushingEnvironment(doer, repo),
})
if err == nil {
@ -59,7 +67,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
BaseRepoID: repo.BaseRepo.ID,
BaseRepo: repo.BaseRepo,
HeadBranch: branch, // maybe HeadCommitID is not needed
BaseBranch: repo.BaseRepo.DefaultBranch,
BaseBranch: divergingInfo.BaseBranchName,
}
fakeIssue.PullRequest = fakePR
err = pull.Update(ctx, fakePR, doer, "merge upstream", false)
@ -69,8 +77,15 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
return "merge", nil
}
// UpstreamDivergingInfo is also used in templates, so it needs to search for all references before changing it.
type UpstreamDivergingInfo struct {
BaseBranchName string
BaseBranchHasNewCommits bool
HeadBranchCommitsBehind int
}
// GetUpstreamDivergingInfo returns the information about the divergence between the fork repository's branch and the base repository's default branch.
func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, forkRepo *repo_model.Repository, forkBranch string) (*BranchDivergingInfo, error) {
func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, forkRepo *repo_model.Repository, forkBranch string) (*UpstreamDivergingInfo, error) {
if !forkRepo.IsFork {
return nil, util.NewInvalidArgumentErrorf("repo is not a fork")
}
@ -83,5 +98,26 @@ func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, forkRepo *repo_model.Re
return nil, err
}
return GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch)
// Do the best to follow the GitHub's behavior, suppose there is a `branch-a` in fork repo:
// * if `branch-a` exists in base repo: try to sync `base:branch-a` to `fork:branch-a`
// * if `branch-a` doesn't exist in base repo: try to sync `base:main` to `fork:branch-a`
info, err := GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkBranch, forkRepo, forkBranch)
if err == nil {
return &UpstreamDivergingInfo{
BaseBranchName: forkBranch,
BaseBranchHasNewCommits: info.BaseHasNewCommits,
HeadBranchCommitsBehind: info.HeadCommitsBehind,
}, nil
}
if errors.Is(err, util.ErrNotExist) {
info, err = GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch)
if err == nil {
return &UpstreamDivergingInfo{
BaseBranchName: forkRepo.BaseRepo.DefaultBranch,
BaseBranchHasNewCommits: info.BaseHasNewCommits,
HeadBranchCommitsBehind: info.HeadCommitsBehind,
}, nil
}
}
return nil, err
}

@ -1,10 +1,12 @@
{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseHasNewCommits .UpstreamDivergingInfo.HeadCommitsBehind)}}
{{if and .UpstreamDivergingInfo .UpstreamDivergingInfo.BaseBranchHasNewCommits}}
<div class="ui message flex-text-block">
<div class="tw-flex-1">
{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.Repository.BaseRepo.DefaultBranch|PathEscapeSegments)}}
{{$upstreamHtml := HTMLFormat `<a href="%s">%s:%s</a>` $upstreamLink .Repository.BaseRepo.FullName .Repository.BaseRepo.DefaultBranch}}
{{if .UpstreamDivergingInfo.HeadCommitsBehind}}
{{ctx.Locale.TrN .UpstreamDivergingInfo.HeadCommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.HeadCommitsBehind $upstreamHtml}}
{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.UpstreamDivergingInfo.BaseBranchName|PathEscapeSegments)}}
{{$upstreamRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.BaseRepo.FullName .UpstreamDivergingInfo.BaseBranchName}}
{{$thisRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.FullName .BranchName}}
{{$upstreamHtml := HTMLFormat `<a href="%s">%s</a>` $upstreamLink $upstreamRepoBranchDisplay}}
{{if .UpstreamDivergingInfo.HeadBranchCommitsBehind}}
{{ctx.Locale.TrN .UpstreamDivergingInfo.HeadBranchCommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.HeadBranchCommitsBehind $upstreamHtml}}
{{else}}
{{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}}
{{end}}
@ -12,7 +14,7 @@
{{if .CanWriteCode}}
<button class="ui compact primary button tw-m-0 link-action"
data-modal-confirm-header="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}"
data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" .BranchName}}"
data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" $upstreamRepoBranchDisplay $thisRepoBranchDisplay}}"
data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}
</button>

@ -60,25 +60,54 @@ func TestRepoMergeUpstream(t *testing.T) {
t.Run("HeadBeforeBase", func(t *testing.T) {
// add a file in base repo
sessionBaseUser := loginUser(t, baseUser.Name)
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-1"))
// the repo shows a prompt to "sync fork"
var mergeUpstreamLink string
require.Eventually(t, func() bool {
resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
if mergeUpstreamLink == "" {
return false
}
respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`)
}, 5*time.Second, 100*time.Millisecond)
t.Run("DetectDefaultBranch", func(t *testing.T) {
// the repo shows a prompt to "sync fork" (defaults to the default branch)
require.Eventually(t, func() bool {
resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
if mergeUpstreamLink == "" {
return false
}
respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`)
}, 5*time.Second, 100*time.Millisecond)
})
t.Run("DetectSameBranch", func(t *testing.T) {
// if the fork-branch name also exists in the base repo, then use that branch instead
req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/master", map[string]string{
"_csrf": GetUserCSRFToken(t, sessionBaseUser),
"new_branch_name": "fork-branch",
})
sessionBaseUser.MakeRequest(t, req, http.StatusSeeOther)
require.Eventually(t, func() bool {
resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
if mergeUpstreamLink == "" {
return false
}
respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/fork-branch">user2/repo1:fork-branch</a>`)
}, 5*time.Second, 100*time.Millisecond)
})
// click the "sync fork" button
req = NewRequestWithValues(t, "POST", mergeUpstreamLink, map[string]string{"_csrf": GetUserCSRFToken(t, session)})
session.MakeRequest(t, req, http.StatusOK)
checkFileContent("fork-branch", "test-content-1")
// delete the "fork-branch" from the base repo
req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete?name=fork-branch", map[string]string{
"_csrf": GetUserCSRFToken(t, sessionBaseUser),
})
sessionBaseUser.MakeRequest(t, req, http.StatusOK)
})
t.Run("BaseChangeAfterHeadChange", func(t *testing.T) {

Loading…
Cancel
Save