Merge branch 'main' into feature/remove-signin-tabs

This commit is contained in:
Joshua Nazareth 2024-04-12 18:03:02 +05:30 committed by GitHub
commit 4190d26a4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 644 additions and 341 deletions

View File

@ -297,6 +297,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
sess := db.GetEngine(ctx)
// check whether from branch exist
var branch Branch
exist, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, from).Get(&branch)
if err != nil {
@ -308,6 +309,24 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
}
}
// check whether to branch exist or is_deleted
var dstBranch Branch
exist, err = db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, to).Get(&dstBranch)
if err != nil {
return err
}
if exist {
if !dstBranch.IsDeleted {
return ErrBranchAlreadyExists{
BranchName: to,
}
}
if _, err := db.GetEngine(ctx).ID(dstBranch.ID).NoAutoCondition().Delete(&dstBranch); err != nil {
return err
}
}
// 1. update branch in database
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
Name: to,
@ -362,12 +381,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
return err
}
// 5. do git action
if err = gitAction(ctx, isDefault); err != nil {
return err
}
// 6. insert renamed branch record
// 5. insert renamed branch record
renamedBranch := &RenamedBranch{
RepoID: repo.ID,
From: from,
@ -378,6 +392,11 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
return err
}
// 6. do git action
if err = gitAction(ctx, isDefault); err != nil {
return err
}
return committer.Commit()
}

View File

@ -292,30 +292,27 @@ func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOp
}
// GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs
func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHAs map[int64]string, listOptions db.ListOptions) (map[int64][]*CommitStatus, error) {
func GetLatestCommitStatusForPairs(ctx context.Context, repoSHAs []RepoSHA) (map[int64][]*CommitStatus, error) {
type result struct {
Index int64
RepoID int64
SHA string
}
results := make([]result, 0, len(repoIDsToLatestCommitSHAs))
results := make([]result, 0, len(repoSHAs))
getBase := func() *xorm.Session {
return db.GetEngine(ctx).Table(&CommitStatus{})
}
// Create a disjunction of conditions for each repoID and SHA pair
conds := make([]builder.Cond, 0, len(repoIDsToLatestCommitSHAs))
for repoID, sha := range repoIDsToLatestCommitSHAs {
conds = append(conds, builder.Eq{"repo_id": repoID, "sha": sha})
conds := make([]builder.Cond, 0, len(repoSHAs))
for _, repoSHA := range repoSHAs {
conds = append(conds, builder.Eq{"repo_id": repoSHA.RepoID, "sha": repoSHA.SHA})
}
sess := getBase().Where(builder.Or(conds...)).
Select("max( `index` ) as `index`, repo_id").
GroupBy("context_hash, repo_id").OrderBy("max( `index` ) desc")
if !listOptions.IsListAll() {
sess = db.SetSessionPagination(sess, &listOptions)
}
Select("max( `index` ) as `index`, repo_id, sha").
GroupBy("context_hash, repo_id, sha").OrderBy("max( `index` ) desc")
err := sess.Find(&results)
if err != nil {
@ -332,7 +329,7 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA
cond := builder.Eq{
"`index`": result.Index,
"repo_id": result.RepoID,
"sha": repoIDsToLatestCommitSHAs[result.RepoID],
"sha": result.SHA,
}
conds = append(conds, cond)
}

View File

@ -0,0 +1,84 @@
// Copyright 2024 Gitea. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"xorm.io/builder"
)
// CommitStatusSummary holds the latest commit Status of a single Commit
type CommitStatusSummary struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"`
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"`
State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"`
}
func init() {
db.RegisterModel(new(CommitStatusSummary))
}
type RepoSHA struct {
RepoID int64
SHA string
}
func GetLatestCommitStatusForRepoAndSHAs(ctx context.Context, repoSHAs []RepoSHA) ([]*CommitStatus, error) {
cond := builder.NewCond()
for _, rs := range repoSHAs {
cond = cond.Or(builder.Eq{"repo_id": rs.RepoID, "sha": rs.SHA})
}
var summaries []CommitStatusSummary
if err := db.GetEngine(ctx).Where(cond).Find(&summaries); err != nil {
return nil, err
}
commitStatuses := make([]*CommitStatus, 0, len(repoSHAs))
for _, summary := range summaries {
commitStatuses = append(commitStatuses, &CommitStatus{
RepoID: summary.RepoID,
SHA: summary.SHA,
State: summary.State,
})
}
return commitStatuses, nil
}
func UpdateCommitStatusSummary(ctx context.Context, repoID int64, sha string) error {
commitStatuses, _, err := GetLatestCommitStatus(ctx, repoID, sha, db.ListOptionsAll)
if err != nil {
return err
}
state := CalcCommitStatus(commitStatuses)
// mysql will return 0 when update a record which state hasn't been changed which behaviour is different from other database,
// so we need to use insert in on duplicate
if setting.Database.Type.IsMySQL() {
_, err := db.GetEngine(ctx).Exec("INSERT INTO commit_status_summary (repo_id,sha,state) VALUES (?,?,?) ON DUPLICATE KEY UPDATE state=?",
repoID, sha, state.State, state.State)
return err
}
if cnt, err := db.GetEngine(ctx).Where("repo_id=? AND sha=?", repoID, sha).
Cols("state").
Update(&CommitStatusSummary{
State: state.State,
}); err != nil {
return err
} else if cnt == 0 {
_, err = db.GetEngine(ctx).Insert(&CommitStatusSummary{
RepoID: repoID,
SHA: sha,
State: state.State,
})
return err
}
return nil
}

View File

@ -576,7 +576,10 @@ var migrations = []Migration{
// Gitea 1.22.0 ends at 294
// v294 -> v295
NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue),
// v295 -> v296
NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary),
}
// GetCurrentDBVersion returns the current db version

View File

@ -0,0 +1,18 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import "xorm.io/xorm"
func AddCommitStatusSummary(x *xorm.Engine) error {
type CommitStatusSummary struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"`
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"`
State string `xorm:"VARCHAR(7) NOT NULL"`
}
// there is no migrations because if there is no data on this table, it will fall back to get data
// from commit status
return x.Sync2(new(CommitStatusSummary))
}

View File

@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"os"
"slices"
"strconv"
"strings"
@ -27,6 +28,7 @@ type GrepOptions struct {
MaxResultLimit int
ContextLineNumber int
IsFuzzy bool
MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated
}
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
@ -71,10 +73,20 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
defer stdoutReader.Close()
isInBlock := false
scanner := bufio.NewScanner(stdoutReader)
rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024))
var res *GrepResult
for scanner.Scan() {
line := scanner.Text()
for {
lineBytes, isPrefix, err := rd.ReadLine()
if isPrefix {
lineBytes = slices.Clone(lineBytes)
for isPrefix && err == nil {
_, isPrefix, err = rd.ReadLine()
}
}
if len(lineBytes) == 0 && err != nil {
break
}
line := string(lineBytes) // the memory of lineBytes is mutable
if !isInBlock {
if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok {
isInBlock = true
@ -100,7 +112,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
res.LineCodes = append(res.LineCodes, lineCode)
}
}
return scanner.Err()
return nil
},
})
// git grep exits by cancel (killed), usually it is caused by the limit of results

View File

@ -41,6 +41,16 @@ func TestGrepSearch(t *testing.T) {
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1, MaxLineLength: 39})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "java-hello/main.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] arg"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{})
assert.NoError(t, err)
assert.Len(t, res, 0)

View File

@ -313,7 +313,13 @@ func RenameBranchPost(ctx *context.Context) {
msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
if err != nil {
ctx.ServerError("RenameBranch", err)
switch {
case git_model.IsErrBranchAlreadyExists(err):
ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To))
ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
default:
ctx.ServerError("RenameBranch", err)
}
return
}

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
"github.com/nektos/act/pkg/jobparser"
)
@ -122,18 +123,13 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
if err != nil {
return fmt.Errorf("HashTypeInterfaceFromHashString: %w", err)
}
if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
Repo: repo,
SHA: commitID,
Creator: creator,
CommitStatus: &git_model.CommitStatus{
SHA: sha,
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
Description: description,
Context: ctxname,
CreatorID: creator.ID,
State: state,
},
if err := commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &git_model.CommitStatus{
SHA: sha,
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
Description: description,
Context: ctxname,
CreatorID: creator.ID,
State: state,
}); err != nil {
return fmt.Errorf("NewCommitStatus: %w", err)
}

View File

@ -72,6 +72,11 @@ func (g *GitBucketDownloader) LogString() string {
// NewGitBucketDownloader creates a GitBucket downloader
func NewGitBucketDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GitBucketDownloader {
githubDownloader := NewGithubDownloaderV3(ctx, baseURL, userName, password, token, repoOwner, repoName)
// Gitbucket 4.40 uses different internal hard-coded perPage values.
// Issues, PRs, and other major parts use 25. Release page uses 10.
// Some API doesn't support paging yet. Sounds difficult, but using
// minimum number among them worked out very well.
githubDownloader.maxPerPage = 10
githubDownloader.SkipReactions = true
githubDownloader.SkipReviews = true
return &GitBucketDownloader{

View File

@ -7,6 +7,7 @@ import (
"context"
"crypto/sha256"
"fmt"
"slices"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
@ -15,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/automerge"
@ -25,12 +27,41 @@ func getCacheKey(repoID int64, brancheName string) string {
return fmt.Sprintf("commit_status:%x", hashBytes)
}
func updateCommitStatusCache(ctx context.Context, repoID int64, branchName string, status api.CommitStatusState) error {
c := cache.GetCache()
return c.Put(getCacheKey(repoID, branchName), string(status), 3*24*60)
type commitStatusCacheValue struct {
State string `json:"state"`
TargetURL string `json:"target_url"`
}
func deleteCommitStatusCache(ctx context.Context, repoID int64, branchName string) error {
func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheValue {
c := cache.GetCache()
statusStr, ok := c.Get(getCacheKey(repoID, branchName)).(string)
if ok && statusStr != "" {
var cv commitStatusCacheValue
err := json.Unmarshal([]byte(statusStr), &cv)
if err == nil && cv.State != "" {
return &cv
}
if err != nil {
log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
}
}
return nil
}
func updateCommitStatusCache(repoID int64, branchName string, state api.CommitStatusState, targetURL string) error {
c := cache.GetCache()
bs, err := json.Marshal(commitStatusCacheValue{
State: state.String(),
TargetURL: targetURL,
})
if err != nil {
log.Warn("updateCommitStatusCache: json.Marshal failed: %v", err)
return nil
}
return c.Put(getCacheKey(repoID, branchName), string(bs), 3*24*60)
}
func deleteCommitStatusCache(repoID int64, branchName string) error {
c := cache.GetCache()
return c.Delete(getCacheKey(repoID, branchName))
}
@ -59,13 +90,19 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
sha = commit.ID.String()
}
if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
Repo: repo,
Creator: creator,
SHA: commit.ID,
CommitStatus: status,
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
Repo: repo,
Creator: creator,
SHA: commit.ID,
CommitStatus: status,
}); err != nil {
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
}
return git_model.UpdateCommitStatusSummary(ctx, repo.ID, commit.ID.String())
}); err != nil {
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
return err
}
defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
@ -74,7 +111,7 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
}
if commit.ID.String() == defaultBranchCommit.ID.String() { // since one commit status updated, the combined commit status should be invalid
if err := deleteCommitStatusCache(ctx, repo.ID, repo.DefaultBranch); err != nil {
if err := deleteCommitStatusCache(repo.ID, repo.DefaultBranch); err != nil {
log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
}
}
@ -91,12 +128,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
// FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache
func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) {
results := make([]*git_model.CommitStatus, len(repos))
c := cache.GetCache()
for i, repo := range repos {
status, ok := c.Get(getCacheKey(repo.ID, repo.DefaultBranch)).(string)
if ok && status != "" {
results[i] = &git_model.CommitStatus{State: api.CommitStatusState(status)}
if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil {
results[i] = &git_model.CommitStatus{
State: api.CommitStatusState(cv.State),
TargetURL: cv.TargetURL,
}
}
}
@ -114,8 +151,35 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
return nil, fmt.Errorf("FindBranchesByRepoAndBranchName: %v", err)
}
var repoSHAs []git_model.RepoSHA
for id, sha := range repoIDsToLatestCommitSHAs {
repoSHAs = append(repoSHAs, git_model.RepoSHA{RepoID: id, SHA: sha})
}
summaryResults, err := git_model.GetLatestCommitStatusForRepoAndSHAs(ctx, repoSHAs)
if err != nil {
return nil, fmt.Errorf("GetLatestCommitStatusForRepoAndSHAs: %v", err)
}
for _, summary := range summaryResults {
for i, repo := range repos {
if repo.ID == summary.RepoID {
results[i] = summary
_ = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool {
return repoSHA.RepoID == repo.ID
})
if results[i].State != "" {
if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
}
}
break
}
}
}
// call the database O(1) times to get the commit statuses for all repos
repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptionsAll)
repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs)
if err != nil {
return nil, fmt.Errorf("GetLatestCommitStatusForPairs: %v", err)
}
@ -124,7 +188,7 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
if results[i] == nil {
results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
if results[i].State != "" {
if err := updateCommitStatusCache(ctx, repo.ID, repo.DefaultBranch, results[i].State); err != nil {
if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
}
}

View File

@ -13,11 +13,9 @@
<div class="ui four wide column">
{{template "shared/user/profile_big_avatar" .}}
</div>
<div class="ui twelve wide column">
<div class="tw-mb-4">
<div class="ui twelve wide column tw-mb-4">
{{template "user/overview/header" .}}
</div>
{{template "projects/list" .}}
{{template "projects/list" .}}
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
{{template "shared/user/org_profile_avatar" .}}
<div class="ui container">
<div class="ui container tw-mb-4">
{{template "user/overview/header" .}}
</div>
<div class="ui container fluid padded">

View File

@ -13,11 +13,9 @@
<div class="ui four wide column">
{{template "shared/user/profile_big_avatar" .}}
</div>
<div class="ui twelve wide column">
<div class="tw-mb-4">
<div class="ui twelve wide column tw-mb-4">
{{template "user/overview/header" .}}
</div>
{{template "package/shared/versionlist" .}}
{{template "package/shared/versionlist" .}}
</div>
</div>
</div>

View File

@ -13,11 +13,9 @@
<div class="ui four wide column">
{{template "shared/user/profile_big_avatar" .}}
</div>
<div class="ui twelve wide column">
<div class="tw-mb-4">
<div class="ui twelve wide column tw-mb-4">
{{template "user/overview/header" .}}
</div>
{{template "package/shared/list" .}}
{{template "package/shared/list" .}}
</div>
</div>
</div>

View File

@ -5,11 +5,8 @@
<div class="ui four wide column">
{{template "shared/user/profile_big_avatar" .}}
</div>
<div class="ui twelve wide column">
<div class="tw-mb-4">
{{template "user/overview/header" .}}
</div>
<div class="ui twelve wide column tw-mb-4">
{{template "user/overview/header" .}}
{{if eq .TabName "activity"}}
{{if .ContextUser.KeepActivityPrivate}}
<div class="ui info message">

View File

@ -12,6 +12,9 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
api "code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert"
@ -90,6 +93,10 @@ func TestPullCreate_CommitStatus(t *testing.T) {
assert.True(t, ok)
assert.Contains(t, cls, statesIcons[status])
}
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
css := unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: repo1.ID, SHA: commitID})
assert.EqualValues(t, api.CommitStatusWarning, css.State)
})
}

View File

@ -5,17 +5,23 @@ package integration
import (
"net/http"
"net/url"
"testing"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
gitea_context "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestRenameBranch(t *testing.T) {
onGiteaRun(t, testRenameBranch)
}
func testRenameBranch(t *testing.T, u *url.URL) {
defer tests.PrepareTestEnv(t)()
unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: 1, Name: "master"})
@ -26,20 +32,19 @@ func TestRenameBranch(t *testing.T) {
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
postData := map[string]string{
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"from": "master",
"to": "main",
}
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", postData)
})
session.MakeRequest(t, req, http.StatusSeeOther)
// check new branch link
req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", postData)
req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", nil)
session.MakeRequest(t, req, http.StatusOK)
// check old branch link
req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", postData)
req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", nil)
resp = session.MakeRequest(t, req, http.StatusSeeOther)
location := resp.Header().Get("Location")
assert.Equal(t, "/user2/repo1/src/branch/main/README.md", location)
@ -47,4 +52,69 @@ func TestRenameBranch(t *testing.T) {
// check db
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.Equal(t, "main", repo1.DefaultBranch)
// create branch1
csrf := GetCSRF(t, session, "/user2/repo1/src/branch/main")
req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{
"_csrf": csrf,
"new_branch_name": "branch1",
})
session.MakeRequest(t, req, http.StatusSeeOther)
branch1 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
assert.Equal(t, "branch1", branch1.Name)
// create branch2
req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{
"_csrf": csrf,
"new_branch_name": "branch2",
})
session.MakeRequest(t, req, http.StatusSeeOther)
branch2 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
assert.Equal(t, "branch2", branch2.Name)
// rename branch2 to branch1
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"from": "branch2",
"to": "branch1",
})
session.MakeRequest(t, req, http.StatusSeeOther)
flashCookie := session.GetCookie(gitea_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Contains(t, flashCookie.Value, "error")
branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
assert.Equal(t, "branch2", branch2.Name)
branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
assert.Equal(t, "branch1", branch1.Name)
// delete branch1
req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete", map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"name": "branch1",
})
session.MakeRequest(t, req, http.StatusOK)
branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
assert.Equal(t, "branch2", branch2.Name)
branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
assert.True(t, branch1.IsDeleted) // virtual deletion
// rename branch2 to branch1 again
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"from": "branch2",
"to": "branch1",
})
session.MakeRequest(t, req, http.StatusSeeOther)
flashCookie = session.GetCookie(gitea_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Contains(t, flashCookie.Value, "success")
unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"})
branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"})
assert.Equal(t, "branch1", branch1.Name)
}

View File

@ -1,14 +1,16 @@
import $ from 'jquery';
import {hideElem, showElem} from '../utils/dom.js';
import {GET} from '../modules/fetch.js';
export function initRepoGraphGit() {
const graphContainer = document.getElementById('git-graph-container');
if (!graphContainer) return;
$('#flow-color-monochrome').on('click', () => {
$('#flow-color-monochrome').addClass('active');
$('#flow-color-colored').removeClass('active');
$('#git-graph-container').removeClass('colored').addClass('monochrome');
document.getElementById('flow-color-monochrome')?.addEventListener('click', () => {
document.getElementById('flow-color-monochrome').classList.add('active');
document.getElementById('flow-color-colored')?.classList.remove('active');
graphContainer.classList.remove('colored');
graphContainer.classList.add('monochrome');
const params = new URLSearchParams(window.location.search);
params.set('mode', 'monochrome');
const queryString = params.toString();
@ -17,29 +19,31 @@ export function initRepoGraphGit() {
} else {
window.history.replaceState({}, '', window.location.pathname);
}
$('.pagination a').each((_, that) => {
const href = that.getAttribute('href');
if (!href) return;
for (const link of document.querySelectorAll('.pagination a')) {
const href = link.getAttribute('href');
if (!href) continue;
const url = new URL(href, window.location);
const params = url.searchParams;
params.set('mode', 'monochrome');
url.search = `?${params.toString()}`;
that.setAttribute('href', url.href);
});
link.setAttribute('href', url.href);
}
});
$('#flow-color-colored').on('click', () => {
$('#flow-color-colored').addClass('active');
$('#flow-color-monochrome').removeClass('active');
$('#git-graph-container').addClass('colored').removeClass('monochrome');
$('.pagination a').each((_, that) => {
const href = that.getAttribute('href');
if (!href) return;
document.getElementById('flow-color-colored')?.addEventListener('click', () => {
document.getElementById('flow-color-colored').classList.add('active');
document.getElementById('flow-color-monochrome')?.classList.remove('active');
graphContainer.classList.add('colored');
graphContainer.classList.remove('monochrome');
for (const link of document.querySelectorAll('.pagination a')) {
const href = link.getAttribute('href');
if (!href) continue;
const url = new URL(href, window.location);
const params = url.searchParams;
params.delete('mode');
url.search = `?${params.toString()}`;
that.setAttribute('href', url.href);
});
link.setAttribute('href', url.href);
}
const params = new URLSearchParams(window.location.search);
params.delete('mode');
const queryString = params.toString();
@ -56,20 +60,21 @@ export function initRepoGraphGit() {
const ajaxUrl = new URL(url);
ajaxUrl.searchParams.set('div-only', 'true');
window.history.replaceState({}, '', queryString ? `?${queryString}` : window.location.pathname);
$('#pagination').empty();
$('#rel-container').addClass('tw-hidden');
$('#rev-container').addClass('tw-hidden');
$('#loading-indicator').removeClass('tw-hidden');
document.getElementById('pagination').innerHTML = '';
hideElem('#rel-container');
hideElem('#rev-container');
showElem('#loading-indicator');
(async () => {
const response = await GET(String(ajaxUrl));
const html = await response.text();
const $div = $(html);
$('#pagination').html($div.find('#pagination').html());
$('#rel-container').html($div.find('#rel-container').html());
$('#rev-container').html($div.find('#rev-container').html());
$('#loading-indicator').addClass('tw-hidden');
$('#rel-container').removeClass('tw-hidden');
$('#rev-container').removeClass('tw-hidden');
const div = document.createElement('div');
div.innerHTML = html;
document.getElementById('pagination').innerHTML = div.getElementById('pagination').innerHTML;
document.getElementById('rel-container').innerHTML = div.getElementById('rel-container').innerHTML;
document.getElementById('rev-container').innerHTML = div.getElementById('rev-container').innerHTML;
hideElem('#loading-indicator');
showElem('#rel-container');
showElem('#rev-container');
})();
};
const dropdownSelected = params.getAll('branch');
@ -77,8 +82,9 @@ export function initRepoGraphGit() {
dropdownSelected.splice(0, 0, '...flow-hide-pr-refs');
}
$('#flow-select-refs-dropdown').dropdown('set selected', dropdownSelected);
$('#flow-select-refs-dropdown').dropdown({
const flowSelectRefsDropdown = document.getElementById('flow-select-refs-dropdown');
$(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected);
$(flowSelectRefsDropdown).dropdown({
clearable: true,
fullTextSeach: 'exact',
onRemove(toRemove) {
@ -104,36 +110,46 @@ export function initRepoGraphGit() {
updateGraph();
},
});
$('#git-graph-container').on('mouseenter', '#rev-list li', (e) => {
const flow = $(e.currentTarget).data('flow');
if (flow === 0) return;
$(`#flow-${flow}`).addClass('highlight');
$(e.currentTarget).addClass('hover');
$(`#rev-list li[data-flow='${flow}']`).addClass('highlight');
graphContainer.addEventListener('mouseenter', (e) => {
if (e.target.matches('#rev-list li')) {
const flow = e.target.getAttribute('data-flow');
if (flow === '0') return;
document.getElementById(`flow-${flow}`)?.classList.add('highlight');
e.target.classList.add('hover');
for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
item.classList.add('highlight');
}
} else if (e.target.matches('#rel-container .flow-group')) {
e.target.classList.add('highlight');
const flow = e.target.getAttribute('data-flow');
for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
item.classList.add('highlight');
}
} else if (e.target.matches('#rel-container .flow-commit')) {
const rev = e.target.getAttribute('data-rev');
document.querySelector(`#rev-list li#commit-${rev}`)?.classList.add('hover');
}
});
$('#git-graph-container').on('mouseleave', '#rev-list li', (e) => {
const flow = $(e.currentTarget).data('flow');
if (flow === 0) return;
$(`#flow-${flow}`).removeClass('highlight');
$(e.currentTarget).removeClass('hover');
$(`#rev-list li[data-flow='${flow}']`).removeClass('highlight');
});
$('#git-graph-container').on('mouseenter', '#rel-container .flow-group', (e) => {
$(e.currentTarget).addClass('highlight');
const flow = $(e.currentTarget).data('flow');
$(`#rev-list li[data-flow='${flow}']`).addClass('highlight');
});
$('#git-graph-container').on('mouseleave', '#rel-container .flow-group', (e) => {
$(e.currentTarget).removeClass('highlight');
const flow = $(e.currentTarget).data('flow');
$(`#rev-list li[data-flow='${flow}']`).removeClass('highlight');
});
$('#git-graph-container').on('mouseenter', '#rel-container .flow-commit', (e) => {
const rev = $(e.currentTarget).data('rev');
$(`#rev-list li#commit-${rev}`).addClass('hover');
});
$('#git-graph-container').on('mouseleave', '#rel-container .flow-commit', (e) => {
const rev = $(e.currentTarget).data('rev');
$(`#rev-list li#commit-${rev}`).removeClass('hover');
graphContainer.addEventListener('mouseleave', (e) => {
if (e.target.matches('#rev-list li')) {
const flow = e.target.getAttribute('data-flow');
if (flow === '0') return;
document.getElementById(`flow-${flow}`)?.classList.remove('highlight');
e.target.classList.remove('hover');
for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
item.classList.remove('highlight');
}
} else if (e.target.matches('#rel-container .flow-group')) {
e.target.classList.remove('highlight');
const flow = e.target.getAttribute('data-flow');
for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
item.classList.remove('highlight');
}
} else if (e.target.matches('#rel-container .flow-commit')) {
const rev = e.target.getAttribute('data-rev');
document.querySelector(`#rev-list li#commit-${rev}`)?.classList.remove('hover');
}
});
}

View File

@ -0,0 +1,206 @@
import $ from 'jquery';
import {handleReply} from './repo-issue.js';
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
import {createDropzone} from './dropzone.js';
import {GET, POST} from '../modules/fetch.js';
import {hideElem, showElem} from '../utils/dom.js';
import {attachRefIssueContextPopup} from './contextpopup.js';
import {initCommentContent, initMarkupContent} from '../markup/content.js';
const {csrfToken} = window.config;
async function onEditContent(event) {
event.preventDefault();
const segment = this.closest('.header').nextElementSibling;
const editContentZone = segment.querySelector('.edit-content-zone');
const renderContent = segment.querySelector('.render-content');
const rawContent = segment.querySelector('.raw-content');
let comboMarkdownEditor;
/**
* @param {HTMLElement} dropzone
*/
const setupDropzone = async (dropzone) => {
if (!dropzone) return null;
let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
const dz = await createDropzone(dropzone, {
url: dropzone.getAttribute('data-upload-url'),
headers: {'X-Csrf-Token': csrfToken},
maxFiles: dropzone.getAttribute('data-max-file'),
maxFilesize: dropzone.getAttribute('data-max-size'),
acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'),
addRemoveLinks: true,
dictDefaultMessage: dropzone.getAttribute('data-default-message'),
dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'),
dictFileTooBig: dropzone.getAttribute('data-file-too-big'),
dictRemoveFile: dropzone.getAttribute('data-remove-file'),
timeout: 0,
thumbnailMethod: 'contain',
thumbnailWidth: 480,
thumbnailHeight: 480,
init() {
this.on('success', (file, data) => {
file.uuid = data.uuid;
fileUuidDict[file.uuid] = {submitted: false};
const input = document.createElement('input');
input.id = data.uuid;
input.name = 'files';
input.type = 'hidden';
input.value = data.uuid;
dropzone.querySelector('.files').append(input);
});
this.on('removedfile', async (file) => {
document.getElementById(file.uuid)?.remove();
if (disableRemovedfileEvent) return;
if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
try {
await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
} catch (error) {
console.error(error);
}
}
});
this.on('submit', () => {
for (const fileUuid of Object.keys(fileUuidDict)) {
fileUuidDict[fileUuid].submitted = true;
}
});
this.on('reload', async () => {
try {
const response = await GET(editContentZone.getAttribute('data-attachment-url'));
const data = await response.json();
// do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
disableRemovedfileEvent = true;
dz.removeAllFiles(true);
dropzone.querySelector('.files').innerHTML = '';
for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove();
fileUuidDict = {};
disableRemovedfileEvent = false;
for (const attachment of data) {
const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`;
dz.emit('addedfile', attachment);
dz.emit('thumbnail', attachment, imgSrc);
dz.emit('complete', attachment);
fileUuidDict[attachment.uuid] = {submitted: true};
dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
const input = document.createElement('input');
input.id = attachment.uuid;
input.name = 'files';
input.type = 'hidden';
input.value = attachment.uuid;
dropzone.querySelector('.files').append(input);
}
if (!dropzone.querySelector('.dz-preview')) {
dropzone.classList.remove('dz-started');
}
} catch (error) {
console.error(error);
}
});
},
});
dz.emit('reload');
return dz;
};
const cancelAndReset = (e) => {
e.preventDefault();
showElem(renderContent);
hideElem(editContentZone);
comboMarkdownEditor.attachedDropzoneInst?.emit('reload');
};
const saveAndRefresh = async (e) => {
e.preventDefault();
showElem(renderContent);
hideElem(editContentZone);
const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst;
try {
const params = new URLSearchParams({
content: comboMarkdownEditor.value(),
context: editContentZone.getAttribute('data-context'),
});
for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value);
const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
const data = await response.json();
if (!data.content) {
renderContent.innerHTML = document.getElementById('no-content').innerHTML;
rawContent.textContent = '';
} else {
renderContent.innerHTML = data.content;
rawContent.textContent = comboMarkdownEditor.value();
const refIssues = renderContent.querySelectorAll('p .ref-issue');
attachRefIssueContextPopup(refIssues);
}
const content = segment;
if (!content.querySelector('.dropzone-attachments')) {
if (data.attachments !== '') {
content.insertAdjacentHTML('beforeend', data.attachments);
}
} else if (data.attachments === '') {
content.querySelector('.dropzone-attachments').remove();
} else {
content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
}
dropzoneInst?.emit('submit');
dropzoneInst?.emit('reload');
initMarkupContent();
initCommentContent();
} catch (error) {
console.error(error);
}
};
comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
if (!comboMarkdownEditor) {
editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone'));
editContentZone.querySelector('.cancel.button').addEventListener('click', cancelAndReset);
editContentZone.querySelector('.save.button').addEventListener('click', saveAndRefresh);
}
// Show write/preview tab and copy raw content as needed
showElem(editContentZone);
hideElem(renderContent);
if (!comboMarkdownEditor.value()) {
comboMarkdownEditor.value(rawContent.textContent);
}
comboMarkdownEditor.focus();
}
export function initRepoIssueCommentEdit() {
// Edit issue or comment content
$(document).on('click', '.edit-content', onEditContent);
// Quote reply
$(document).on('click', '.quote-reply', async function (event) {
event.preventDefault();
const target = $(this).data('target');
const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
const content = `> ${quote}\n\n`;
let editor;
if ($(this).hasClass('quote-reply-diff')) {
const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
editor = await handleReply($replyBtn);
} else {
// for normal issue/comment page
editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
}
if (editor) {
if (editor.value()) {
editor.value(`${editor.value()}\n\n${content}`);
} else {
editor.value(content);
}
editor.focus();
editor.moveCursorToEnd();
}
});
}

View File

@ -3,7 +3,7 @@ import {
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
initRepoIssueTitleEdit, initRepoIssueWipToggle,
initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
initRepoPullRequestUpdate, updateIssuesMeta, initIssueTemplateCommentEditors, initSingleCommentEditor,
} from './repo-issue.js';
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
import {svg} from '../svg.js';
@ -15,18 +15,13 @@ import {
import {initCitationFileCopyContent} from './citation.js';
import {initCompLabelEdit} from './comp/LabelEdit.js';
import {initRepoDiffConversationNav} from './repo-diff.js';
import {createDropzone} from './dropzone.js';
import {initCommentContent, initMarkupContent} from '../markup/content.js';
import {initCompReactionSelector} from './comp/ReactionSelector.js';
import {initRepoSettingBranches} from './repo-settings.js';
import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
import {hideElem, showElem} from '../utils/dom.js';
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
import {attachRefIssueContextPopup} from './contextpopup.js';
import {POST, GET} from '../modules/fetch.js';
const {csrfToken} = window.config;
import {POST} from '../modules/fetch.js';
import {initRepoIssueCommentEdit} from './repo-issue-edit.js';
// if there are draft comments, confirm before reloading, to avoid losing comments
function reloadConfirmDraftComment() {
@ -316,172 +311,6 @@ export function initRepoCommentForm() {
selectItem('.select-assignee', '#assignee_id');
}
async function onEditContent(event) {
event.preventDefault();
const segment = this.closest('.header').nextElementSibling;
const editContentZone = segment.querySelector('.edit-content-zone');
const renderContent = segment.querySelector('.render-content');
const rawContent = segment.querySelector('.raw-content');
let comboMarkdownEditor;
/**
* @param {HTMLElement} dropzone
*/
const setupDropzone = async (dropzone) => {
if (!dropzone) return null;
let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
const dz = await createDropzone(dropzone, {
url: dropzone.getAttribute('data-upload-url'),
headers: {'X-Csrf-Token': csrfToken},
maxFiles: dropzone.getAttribute('data-max-file'),
maxFilesize: dropzone.getAttribute('data-max-size'),
acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'),
addRemoveLinks: true,
dictDefaultMessage: dropzone.getAttribute('data-default-message'),
dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'),
dictFileTooBig: dropzone.getAttribute('data-file-too-big'),
dictRemoveFile: dropzone.getAttribute('data-remove-file'),
timeout: 0,
thumbnailMethod: 'contain',
thumbnailWidth: 480,
thumbnailHeight: 480,
init() {
this.on('success', (file, data) => {
file.uuid = data.uuid;
fileUuidDict[file.uuid] = {submitted: false};
const input = document.createElement('input');
input.id = data.uuid;
input.name = 'files';
input.type = 'hidden';
input.value = data.uuid;
dropzone.querySelector('.files').append(input);
});
this.on('removedfile', async (file) => {
document.getElementById(file.uuid)?.remove();
if (disableRemovedfileEvent) return;
if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
try {
await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
} catch (error) {
console.error(error);
}
}
});
this.on('submit', () => {
for (const fileUuid of Object.keys(fileUuidDict)) {
fileUuidDict[fileUuid].submitted = true;
}
});
this.on('reload', async () => {
try {
const response = await GET(editContentZone.getAttribute('data-attachment-url'));
const data = await response.json();
// do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
disableRemovedfileEvent = true;
dz.removeAllFiles(true);
dropzone.querySelector('.files').innerHTML = '';
for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove();
fileUuidDict = {};
disableRemovedfileEvent = false;
for (const attachment of data) {
const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`;
dz.emit('addedfile', attachment);
dz.emit('thumbnail', attachment, imgSrc);
dz.emit('complete', attachment);
fileUuidDict[attachment.uuid] = {submitted: true};
dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
const input = document.createElement('input');
input.id = attachment.uuid;
input.name = 'files';
input.type = 'hidden';
input.value = attachment.uuid;
dropzone.querySelector('.files').append(input);
}
if (!dropzone.querySelector('.dz-preview')) {
dropzone.classList.remove('dz-started');
}
} catch (error) {
console.error(error);
}
});
},
});
dz.emit('reload');
return dz;
};
const cancelAndReset = (e) => {
e.preventDefault();
showElem(renderContent);
hideElem(editContentZone);
comboMarkdownEditor.attachedDropzoneInst?.emit('reload');
};
const saveAndRefresh = async (e) => {
e.preventDefault();
showElem(renderContent);
hideElem(editContentZone);
const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst;
try {
const params = new URLSearchParams({
content: comboMarkdownEditor.value(),
context: editContentZone.getAttribute('data-context'),
});
for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value);
const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
const data = await response.json();
if (!data.content) {
renderContent.innerHTML = document.getElementById('no-content').innerHTML;
rawContent.textContent = '';
} else {
renderContent.innerHTML = data.content;
rawContent.textContent = comboMarkdownEditor.value();
const refIssues = renderContent.querySelectorAll('p .ref-issue');
attachRefIssueContextPopup(refIssues);
}
const content = segment;
if (!content.querySelector('.dropzone-attachments')) {
if (data.attachments !== '') {
content.insertAdjacentHTML('beforeend', data.attachments);
}
} else if (data.attachments === '') {
content.querySelector('.dropzone-attachments').remove();
} else {
content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
}
dropzoneInst?.emit('submit');
dropzoneInst?.emit('reload');
initMarkupContent();
initCommentContent();
} catch (error) {
console.error(error);
}
};
comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
if (!comboMarkdownEditor) {
editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone'));
editContentZone.querySelector('.cancel.button').addEventListener('click', cancelAndReset);
editContentZone.querySelector('.save.button').addEventListener('click', saveAndRefresh);
}
// Show write/preview tab and copy raw content as needed
showElem(editContentZone);
hideElem(renderContent);
if (!comboMarkdownEditor.value()) {
comboMarkdownEditor.value(rawContent.textContent);
}
comboMarkdownEditor.focus();
}
export function initRepository() {
if (!$('.page-content.repository').length) return;
@ -585,33 +414,3 @@ export function initRepository() {
initUnicodeEscapeButton();
}
function initRepoIssueCommentEdit() {
// Edit issue or comment content
$(document).on('click', '.edit-content', onEditContent);
// Quote reply
$(document).on('click', '.quote-reply', async function (event) {
event.preventDefault();
const target = $(this).data('target');
const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
const content = `> ${quote}\n\n`;
let editor;
if ($(this).hasClass('quote-reply-diff')) {
const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
editor = await handleReply($replyBtn);
} else {
// for normal issue/comment page
editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
}
if (editor) {
if (editor.value()) {
editor.value(`${editor.value()}\n\n${content}`);
} else {
editor.value(content);
}
editor.focus();
editor.moveCursorToEnd();
}
});
}