0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-19 21:28:33 +02:00

Merge branch 'main' into lunny/reafctor_setdefaultbranch

This commit is contained in:
Lunny Xiao 2025-03-28 19:07:44 -07:00
commit 95e2224841
33 changed files with 1640 additions and 1172 deletions

View File

@ -378,6 +378,7 @@ func prepareMigrationTasks() []*migration {
newMigration(315, "Add Ephemeral to ActionRunner", v1_24.AddEphemeralToActionRunner),
newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
}
return preparedMigrations
}

View File

@ -0,0 +1,17 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_24 //nolint
import (
"code.gitea.io/gitea/models/perm"
"xorm.io/xorm"
)
func AddRepoUnitAnonymousAccessMode(x *xorm.Engine) error {
type RepoUnit struct { //revive:disable-line:exported
AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
}
return x.Sync(&RepoUnit{})
}

View File

@ -25,7 +25,8 @@ type Permission struct {
units []*repo_model.RepoUnit
unitsMode map[unit.Type]perm_model.AccessMode
everyoneAccessMode map[unit.Type]perm_model.AccessMode
everyoneAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user
anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user
}
// IsOwner returns true if current user is the owner of repository.
@ -39,7 +40,7 @@ func (p *Permission) IsAdmin() bool {
}
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
// It doesn't count the "everyone access mode".
// It doesn't count the "public(anonymous/everyone) access mode".
func (p *Permission) HasAnyUnitAccess() bool {
for _, v := range p.unitsMode {
if v >= perm_model.AccessModeRead {
@ -49,7 +50,12 @@ func (p *Permission) HasAnyUnitAccess() bool {
return p.AccessMode >= perm_model.AccessModeRead
}
func (p *Permission) HasAnyUnitAccessOrEveryoneAccess() bool {
func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool {
for _, v := range p.anonymousAccessMode {
if v >= perm_model.AccessModeRead {
return true
}
}
for _, v := range p.everyoneAccessMode {
if v >= perm_model.AccessModeRead {
return true
@ -73,14 +79,16 @@ func (p *Permission) GetFirstUnitRepoID() int64 {
}
// UnitAccessMode returns current user access mode to the specify unit of the repository
// It also considers "everyone access mode"
// It also considers "public (anonymous/everyone) access mode"
func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode {
// if the units map contains the access mode, use it, but admin/owner mode could override it
if m, ok := p.unitsMode[unitType]; ok {
return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m)
}
// if the units map does not contain the access mode, return the default access mode if the unit exists
unitDefaultAccessMode := max(p.AccessMode, p.everyoneAccessMode[unitType])
unitDefaultAccessMode := p.AccessMode
unitDefaultAccessMode = max(unitDefaultAccessMode, p.anonymousAccessMode[unitType])
unitDefaultAccessMode = max(unitDefaultAccessMode, p.everyoneAccessMode[unitType])
hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType })
return util.Iif(hasUnit, unitDefaultAccessMode, perm_model.AccessModeNone)
}
@ -171,27 +179,38 @@ func (p *Permission) LogString() string {
format += "\n\tunitsMode[%-v]: %-v"
args = append(args, key.LogString(), value.LogString())
}
format += "\n\tanonymousAccessMode: %-v"
args = append(args, p.anonymousAccessMode)
format += "\n\teveryoneAccessMode: %-v"
args = append(args, p.everyoneAccessMode)
format += "\n\t]>"
return fmt.Sprintf(format, args...)
}
func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) {
if accessMode >= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] {
if *modeMap == nil {
*modeMap = make(map[unit.Type]perm_model.AccessMode)
}
(*modeMap)[unitType] = accessMode
}
}
func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
// apply public (anonymous) access permissions
for _, u := range perm.units {
applyPublicAccessPermission(u.Type, u.AnonymousAccessMode, &perm.anonymousAccessMode)
}
if user == nil || user.ID <= 0 {
// for anonymous access, it could be:
// AccessMode is None or Read, units has repo units, unitModes is nil
return
}
// apply everyone access permissions
// apply public (everyone) access permissions
for _, u := range perm.units {
if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] {
if perm.everyoneAccessMode == nil {
perm.everyoneAccessMode = make(map[unit.Type]perm_model.AccessMode)
}
perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode
}
applyPublicAccessPermission(u.Type, u.EveryoneAccessMode, &perm.everyoneAccessMode)
}
if perm.unitsMode == nil {
@ -209,6 +228,11 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
break
}
}
for t := range perm.anonymousAccessMode {
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
break
}
}
for t := range perm.everyoneAccessMode {
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
break

View File

@ -22,14 +22,21 @@ func TestHasAnyUnitAccess(t *testing.T) {
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
}
assert.False(t, perm.HasAnyUnitAccess())
assert.False(t, perm.HasAnyUnitAccessOrEveryoneAccess())
assert.False(t, perm.HasAnyUnitAccessOrPublicAccess())
perm = Permission{
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
everyoneAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead},
}
assert.False(t, perm.HasAnyUnitAccess())
assert.True(t, perm.HasAnyUnitAccessOrEveryoneAccess())
assert.True(t, perm.HasAnyUnitAccessOrPublicAccess())
perm = Permission{
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
anonymousAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead},
}
assert.False(t, perm.HasAnyUnitAccess())
assert.True(t, perm.HasAnyUnitAccessOrPublicAccess())
perm = Permission{
AccessMode: perm_model.AccessModeRead,
@ -43,7 +50,7 @@ func TestHasAnyUnitAccess(t *testing.T) {
assert.True(t, perm.HasAnyUnitAccess())
}
func TestApplyEveryoneRepoPermission(t *testing.T) {
func TestApplyPublicAccessRepoPermission(t *testing.T) {
perm := Permission{
AccessMode: perm_model.AccessModeNone,
units: []*repo_model.RepoUnit{
@ -53,6 +60,15 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
finalProcessRepoUnitPermission(nil, &perm)
assert.False(t, perm.CanRead(unit.TypeWiki))
perm = Permission{
AccessMode: perm_model.AccessModeNone,
units: []*repo_model.RepoUnit{
{Type: unit.TypeWiki, AnonymousAccessMode: perm_model.AccessModeRead},
},
}
finalProcessRepoUnitPermission(nil, &perm)
assert.True(t, perm.CanRead(unit.TypeWiki))
perm = Permission{
AccessMode: perm_model.AccessModeNone,
units: []*repo_model.RepoUnit{

View File

@ -42,12 +42,13 @@ func (err ErrUnitTypeNotExist) Unwrap() error {
// RepoUnit describes all units of a repository
type RepoUnit struct { //revive:disable-line:exported
ID int64
RepoID int64 `xorm:"INDEX(s)"`
Type unit.Type `xorm:"INDEX(s)"`
Config convert.Conversion `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
ID int64
RepoID int64 `xorm:"INDEX(s)"`
Type unit.Type `xorm:"INDEX(s)"`
Config convert.Conversion `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
}
func init() {

View File

@ -4,6 +4,9 @@
package badge
import (
"strings"
"unicode"
actions_model "code.gitea.io/gitea/models/actions"
)
@ -11,54 +14,35 @@ import (
// We use 10x scale to calculate more precisely
// Then scale down to normal size in tmpl file
type Label struct {
text string
width int
}
func (l Label) Text() string {
return l.text
}
func (l Label) Width() int {
return l.width
}
func (l Label) TextLength() int {
return int(float64(l.width-defaultOffset) * 9.5)
}
func (l Label) X() int {
return l.width*5 + 10
}
type Message struct {
type Text struct {
text string
width int
x int
}
func (m Message) Text() string {
return m.text
func (t Text) Text() string {
return t.text
}
func (m Message) Width() int {
return m.width
func (t Text) Width() int {
return t.width
}
func (m Message) X() int {
return m.x
func (t Text) X() int {
return t.x
}
func (m Message) TextLength() int {
return int(float64(m.width-defaultOffset) * 9.5)
func (t Text) TextLength() int {
return int(float64(t.width-defaultOffset) * 10)
}
type Badge struct {
Color string
FontSize int
Label Label
Message Message
IDPrefix string
FontFamily string
Color string
FontSize int
Label Text
Message Text
}
func (b Badge) Width() int {
@ -66,10 +50,10 @@ func (b Badge) Width() int {
}
const (
defaultOffset = 9
defaultFontSize = 11
DefaultColor = "#9f9f9f" // Grey
defaultFontWidth = 7 // approximate speculation
defaultOffset = 10
defaultFontSize = 11
DefaultColor = "#9f9f9f" // Grey
DefaultFontFamily = "DejaVu Sans,Verdana,Geneva,sans-serif"
)
var StatusColorMap = map[actions_model.Status]string{
@ -85,20 +69,43 @@ var StatusColorMap = map[actions_model.Status]string{
// GenerateBadge generates badge with given template
func GenerateBadge(label, message, color string) Badge {
lw := defaultFontWidth*len(label) + defaultOffset
mw := defaultFontWidth*len(message) + defaultOffset
x := lw*10 + mw*5 - 10
lw := calculateTextWidth(label) + defaultOffset
mw := calculateTextWidth(message) + defaultOffset
lx := lw * 5
mx := lw*10 + mw*5 - 10
return Badge{
Label: Label{
FontFamily: DefaultFontFamily,
Label: Text{
text: label,
width: lw,
x: lx,
},
Message: Message{
Message: Text{
text: message,
width: mw,
x: x,
x: mx,
},
FontSize: defaultFontSize * 10,
Color: color,
}
}
func calculateTextWidth(text string) int {
width := 0
widthData := DejaVuGlyphWidthData()
for _, char := range strings.TrimSpace(text) {
charWidth, ok := widthData[char]
if !ok {
// use the width of 'm' in case of missing glyph width data for a printable character
if unicode.IsPrint(char) {
charWidth = widthData['m']
} else {
charWidth = 0
}
}
width += int(charWidth)
}
return width
}

View File

@ -0,0 +1,208 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package badge
import "sync"
// DejaVuGlyphWidthData is generated by `sfnt.Face.GlyphAdvance(nil, <rune>, 11, font.HintingNone)` with DejaVu Sans
// v2.37 (https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-sans-ttf-2.37.zip).
//
// Fonts defined in "DefaultFontFamily" all have similar widths (including "DejaVu Sans"),
// and these widths are fixed and don't seem to change.
//
// A devtest page "/devtest/badge-actions-svg" could be used to check the rendered images.
var DejaVuGlyphWidthData = sync.OnceValue(func() map[rune]uint8 {
return map[rune]uint8{
32: 3,
33: 4,
34: 5,
35: 9,
36: 7,
37: 10,
38: 9,
39: 3,
40: 4,
41: 4,
42: 6,
43: 9,
44: 3,
45: 4,
46: 3,
47: 4,
48: 7,
49: 7,
50: 7,
51: 7,
52: 7,
53: 7,
54: 7,
55: 7,
56: 7,
57: 7,
58: 4,
59: 4,
60: 9,
61: 9,
62: 9,
63: 6,
64: 11,
65: 8,
66: 8,
67: 8,
68: 8,
69: 7,
70: 6,
71: 9,
72: 8,
73: 3,
74: 3,
75: 7,
76: 6,
77: 9,
78: 8,
79: 9,
80: 7,
81: 9,
82: 8,
83: 7,
84: 7,
85: 8,
86: 8,
87: 11,
88: 8,
89: 7,
90: 8,
91: 4,
92: 4,
93: 4,
94: 9,
95: 6,
96: 6,
97: 7,
98: 7,
99: 6,
100: 7,
101: 7,
102: 4,
103: 7,
104: 7,
105: 3,
106: 3,
107: 6,
108: 3,
109: 11,
110: 7,
111: 7,
112: 7,
113: 7,
114: 5,
115: 6,
116: 4,
117: 7,
118: 7,
119: 9,
120: 7,
121: 7,
122: 6,
123: 7,
124: 4,
125: 7,
126: 9,
161: 4,
162: 7,
163: 7,
164: 7,
165: 7,
166: 4,
167: 6,
168: 6,
169: 11,
170: 5,
171: 7,
172: 9,
174: 11,
175: 6,
176: 6,
177: 9,
178: 4,
179: 4,
180: 6,
181: 7,
182: 7,
183: 3,
184: 6,
185: 4,
186: 5,
187: 7,
188: 11,
189: 11,
190: 11,
191: 6,
192: 8,
193: 8,
194: 8,
195: 8,
196: 8,
197: 8,
198: 11,
199: 8,
200: 7,
201: 7,
202: 7,
203: 7,
204: 3,
205: 3,
206: 3,
207: 3,
208: 9,
209: 8,
210: 9,
211: 9,
212: 9,
213: 9,
214: 9,
215: 9,
216: 9,
217: 8,
218: 8,
219: 8,
220: 8,
221: 7,
222: 7,
223: 7,
224: 7,
225: 7,
226: 7,
227: 7,
228: 7,
229: 7,
230: 11,
231: 6,
232: 7,
233: 7,
234: 7,
235: 7,
236: 3,
237: 3,
238: 3,
239: 3,
240: 7,
241: 7,
242: 7,
243: 7,
244: 7,
245: 7,
246: 7,
247: 9,
248: 7,
249: 7,
250: 7,
251: 7,
252: 7,
253: 7,
254: 7,
255: 7,
}
})

View File

@ -14,25 +14,26 @@ type Batch struct {
Writer WriteCloserError
}
func (repo *Repository) NewBatch(ctx context.Context) (*Batch, error) {
// NewBatch creates a new batch for the given repository, the Close must be invoked before release the batch
func NewBatch(ctx context.Context, repoPath string) (*Batch, error) {
// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first!
if err := ensureValidGitRepository(ctx, repo.Path); err != nil {
if err := ensureValidGitRepository(ctx, repoPath); err != nil {
return nil, err
}
var batch Batch
batch.Writer, batch.Reader, batch.cancel = catFileBatch(ctx, repo.Path)
batch.Writer, batch.Reader, batch.cancel = catFileBatch(ctx, repoPath)
return &batch, nil
}
func (repo *Repository) NewBatchCheck(ctx context.Context) (*Batch, error) {
func NewBatchCheck(ctx context.Context, repoPath string) (*Batch, error) {
// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first!
if err := ensureValidGitRepository(ctx, repo.Path); err != nil {
if err := ensureValidGitRepository(ctx, repoPath); err != nil {
return nil, err
}
var check Batch
check.Writer, check.Reader, check.cancel = catFileBatchCheck(ctx, repo.Path)
check.Writer, check.Reader, check.cancel = catFileBatchCheck(ctx, repoPath)
return &check, nil
}

View File

@ -350,9 +350,10 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error {
// We need to check if the context is canceled by the program on Windows.
// This is because Windows does not have signal checking when terminating the process.
// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
// `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled.
if runtime.GOOS == "windows" &&
err != nil &&
err.Error() == "" &&
(err.Error() == "" || err.Error() == "exit status 1") &&
cmd.ProcessState.ExitCode() == 1 &&
ctx.Err() == context.Canceled {
return ctx.Err()

View File

@ -67,7 +67,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func(), error) {
if repo.batch == nil {
var err error
repo.batch, err = repo.NewBatch(ctx)
repo.batch, err = NewBatch(ctx, repo.Path)
if err != nil {
return nil, nil, nil, err
}
@ -81,7 +81,7 @@ func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bu
}
log.Debug("Opening temporary cat file batch for: %s", repo.Path)
tempBatch, err := repo.NewBatch(ctx)
tempBatch, err := NewBatch(ctx, repo.Path)
if err != nil {
return nil, nil, nil, err
}
@ -92,7 +92,7 @@ func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bu
func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func(), error) {
if repo.check == nil {
var err error
repo.check, err = repo.NewBatchCheck(ctx)
repo.check, err = NewBatchCheck(ctx, repo.Path)
if err != nil {
return nil, nil, nil, err
}
@ -106,7 +106,7 @@ func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError
}
log.Debug("Opening temporary cat file batch-check for: %s", repo.Path)
tempBatchCheck, err := repo.NewBatchCheck(ctx)
tempBatchCheck, err := NewBatchCheck(ctx, repo.Path)
if err != nil {
return nil, nil, nil, err
}

View File

@ -16,7 +16,6 @@ import (
"code.gitea.io/gitea/modules/analyze"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/indexer"
path_filter "code.gitea.io/gitea/modules/indexer/code/bleve/token/path"
"code.gitea.io/gitea/modules/indexer/code/internal"
@ -217,12 +216,7 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository, batch
func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error {
batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize)
if len(changes.Updates) > 0 {
r, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
return err
}
defer r.Close()
gitBatch, err := r.NewBatch(ctx)
gitBatch, err := git.NewBatch(ctx, repo.RepoPath())
if err != nil {
return err
}

View File

@ -15,7 +15,6 @@ import (
"code.gitea.io/gitea/modules/analyze"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/indexer"
"code.gitea.io/gitea/modules/indexer/code/internal"
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
@ -209,12 +208,7 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository) elasti
func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error {
reqs := make([]elastic.BulkableRequest, 0)
if len(changes.Updates) > 0 {
r, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
return err
}
defer r.Close()
batch, err := r.NewBatch(ctx)
batch, err := git.NewBatch(ctx, repo.RepoPath())
if err != nil {
return err
}

View File

@ -22,7 +22,7 @@ func DownloadActionsRunJobLogsWithIndex(ctx *context.Base, ctxRepo *repo_model.R
if err = runJobs.LoadRepos(ctx); err != nil {
return fmt.Errorf("LoadRepos: %w", err)
}
if 0 < jobIndex || jobIndex >= int64(len(runJobs)) {
if jobIndex < 0 || jobIndex >= int64(len(runJobs)) {
return util.NewNotExistErrorf("job index is out of range: %d", jobIndex)
}
return DownloadActionsRunJobLogs(ctx, ctxRepo, runJobs[jobIndex])

View File

@ -4,16 +4,21 @@
package devtest
import (
"html/template"
"net/http"
"path"
"strconv"
"strings"
"time"
"unicode"
"code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/badge"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
@ -45,84 +50,121 @@ func FetchActionTest(ctx *context.Context) {
ctx.JSONRedirect("")
}
func prepareMockData(ctx *context.Context) {
if ctx.Req.URL.Path == "/devtest/gitea-ui" {
now := time.Now()
ctx.Data["TimeNow"] = now
ctx.Data["TimePast5s"] = now.Add(-5 * time.Second)
ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second)
ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute)
ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute)
ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second)
ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second)
func prepareMockDataGiteaUI(ctx *context.Context) {
now := time.Now()
ctx.Data["TimeNow"] = now
ctx.Data["TimePast5s"] = now.Add(-5 * time.Second)
ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second)
ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute)
ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute)
ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second)
ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second)
}
func prepareMockDataBadgeCommitSign(ctx *context.Context) {
var commits []*asymkey.SignCommit
mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}})
mockUser := mockUsers[0]
commits = append(commits, &asymkey.SignCommit{
Verification: &asymkey.CommitVerification{},
UserCommit: &user_model.UserCommit{
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
},
})
commits = append(commits, &asymkey.SignCommit{
Verification: &asymkey.CommitVerification{
Verified: true,
Reason: "name / key-id",
SigningUser: mockUser,
SigningKey: &asymkey.GPGKey{KeyID: "12345678"},
TrustStatus: "trusted",
},
UserCommit: &user_model.UserCommit{
User: mockUser,
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
},
})
commits = append(commits, &asymkey.SignCommit{
Verification: &asymkey.CommitVerification{
Verified: true,
Reason: "name / key-id",
SigningUser: mockUser,
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
TrustStatus: "untrusted",
},
UserCommit: &user_model.UserCommit{
User: mockUser,
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
},
})
commits = append(commits, &asymkey.SignCommit{
Verification: &asymkey.CommitVerification{
Verified: true,
Reason: "name / key-id",
SigningUser: mockUser,
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
TrustStatus: "other(unmatch)",
},
UserCommit: &user_model.UserCommit{
User: mockUser,
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
},
})
commits = append(commits, &asymkey.SignCommit{
Verification: &asymkey.CommitVerification{
Warning: true,
Reason: "gpg.error",
SigningEmail: "test@example.com",
},
UserCommit: &user_model.UserCommit{
User: mockUser,
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
},
})
ctx.Data["MockCommits"] = commits
}
func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
fontFamilyNames := strings.Split(badge.DefaultFontFamily, ",")
selectedFontFamilyName := ctx.FormString("font", fontFamilyNames[0])
var badges []badge.Badge
badges = append(badges, badge.GenerateBadge("啊啊啊啊啊啊啊啊啊啊啊啊", "🌞🌞🌞🌞🌞", "green"))
for r := rune(0); r < 256; r++ {
if unicode.IsPrint(r) {
s := strings.Repeat(string(r), 15)
badges = append(badges, badge.GenerateBadge(s, util.TruncateRunes(s, 7), "green"))
}
}
if ctx.Req.URL.Path == "/devtest/commit-sign-badge" {
var commits []*asymkey.SignCommit
mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}})
mockUser := mockUsers[0]
commits = append(commits, &asymkey.SignCommit{
Verification: &asymkey.CommitVerification{},
UserCommit: &user_model.UserCommit{
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
},
})
commits = append(commits, &asymkey.SignCommit{
Verification: &asymkey.CommitVerification{
Verified: true,
Reason: "name / key-id",
SigningUser: mockUser,
SigningKey: &asymkey.GPGKey{KeyID: "12345678"},
TrustStatus: "trusted",
},
UserCommit: &user_model.UserCommit{
User: mockUser,
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
},
})
commits = append(commits, &asymkey.SignCommit{
Verification: &asymkey.CommitVerification{
Verified: true,
Reason: "name / key-id",
SigningUser: mockUser,
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
TrustStatus: "untrusted",
},
UserCommit: &user_model.UserCommit{
User: mockUser,
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
},
})
commits = append(commits, &asymkey.SignCommit{
Verification: &asymkey.CommitVerification{
Verified: true,
Reason: "name / key-id",
SigningUser: mockUser,
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
TrustStatus: "other(unmatch)",
},
UserCommit: &user_model.UserCommit{
User: mockUser,
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
},
})
commits = append(commits, &asymkey.SignCommit{
Verification: &asymkey.CommitVerification{
Warning: true,
Reason: "gpg.error",
SigningEmail: "test@example.com",
},
UserCommit: &user_model.UserCommit{
User: mockUser,
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
},
})
var badgeSVGs []template.HTML
for i, b := range badges {
b.IDPrefix = "devtest-" + strconv.FormatInt(int64(i), 10) + "-"
b.FontFamily = selectedFontFamilyName
h, err := ctx.RenderToHTML("shared/actions/runner_badge", map[string]any{"Badge": b})
if err != nil {
ctx.ServerError("RenderToHTML", err)
return
}
badgeSVGs = append(badgeSVGs, h)
}
ctx.Data["BadgeSVGs"] = badgeSVGs
ctx.Data["BadgeFontFamilyNames"] = fontFamilyNames
ctx.Data["SelectedFontFamilyName"] = selectedFontFamilyName
}
ctx.Data["MockCommits"] = commits
func prepareMockData(ctx *context.Context) {
switch ctx.Req.URL.Path {
case "/devtest/gitea-ui":
prepareMockDataGiteaUI(ctx)
case "/devtest/badge-commit-sign":
prepareMockDataBadgeCommitSign(ctx)
case "/devtest/badge-actions-svg":
prepareMockDataBadgeActionsSvg(ctx)
}
}
func Tmpl(ctx *context.Context) {
func TmplCommon(ctx *context.Context) {
prepareMockData(ctx)
if ctx.Req.Method == "POST" {
_ = ctx.Req.ParseForm()

View File

@ -4,26 +4,12 @@
package web
import (
"net/http"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/context"
)
func addOwnerRepoGitHTTPRouters(m *web.Router) {
reqGitSignIn := func(ctx *context.Context) {
if !setting.Service.RequireSignInView {
return
}
// rely on the results of Contexter
if !ctx.IsSigned {
// TODO: support digit auth - which would be Authorization header with digit
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
ctx.HTTPError(http.StatusUnauthorized)
}
}
m.Group("/{username}/{reponame}", func() {
m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack)
@ -36,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) {
m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
}, optSignInIgnoreCsrf, reqGitSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
}, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
}

View File

@ -154,8 +154,8 @@ func createCommon(ctx *context.Context) {
ctx.Data["Licenses"] = repo_module.Licenses
ctx.Data["Readmes"] = repo_module.Readmes
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
ctx.Data["CanCreateRepoInDoer"] = ctx.Doer.CanCreateRepo()
ctx.Data["MaxCreationLimitOfDoer"] = ctx.Doer.MaxCreationLimit()
ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats
ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat
}

File diff suppressed because it is too large Load Diff

View File

@ -1639,7 +1639,7 @@ func registerRoutes(m *web.Router) {
m.Group("/devtest", func() {
m.Any("", devtest.List)
m.Any("/fetch-action-test", devtest.FetchActionTest)
m.Any("/{sub}", devtest.Tmpl)
m.Any("/{sub}", devtest.TmplCommon)
m.Get("/repo-action-view/{run}/{job}", devtest.MockActionsView)
m.Post("/actions-mock/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs)
})

View File

@ -346,7 +346,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
return
}
if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() && !canWriteAsMaintainer(ctx) {
if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) {
if ctx.FormString("go-get") == "1" {
EarlyResponseForGoGetMeta(ctx)
return

View File

@ -0,0 +1,18 @@
{{template "devtest/devtest-header"}}
<div class="page-content devtest ui container">
<div>
<h1>Actions SVG</h1>
<form class="tw-my-3">
{{range $fontName := .BadgeFontFamilyNames}}
<label><input name="font" type="radio" value="{{$fontName}}" {{Iif (eq $.SelectedFontFamilyName $fontName) "checked"}}>{{$fontName}}</label>
{{end}}
<button>submit</button>
</form>
<div class="flex-text-block tw-flex-wrap">
{{range $badgeSVG := .BadgeSVGs}}
<div>{{$badgeSVG}}</div>
{{end}}
</div>
</div>
</div>
{{template "devtest/devtest-footer"}}

View File

@ -1,25 +1,22 @@
{{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
<div class="ui container tw-max-w-full">
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4 tw-gap-3">
<h2 class="tw-mb-0 tw-flex-1 tw-break-anywhere">{{.Project.Title}}</h2>
<div class="project-toolbar-right">
<div class="ui secondary filter menu labels">
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
{{template "repo/issue/filter_item_user_assign" dict
"QueryParamKey" "assignee"
"QueryLink" $queryLink
"UserSearchList" $.Assignees
"SelectedUserId" $.AssigneeID
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee")
"TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
"TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
}}
</div>
</div>
<div class="flex-text-block tw-flex-wrap tw-mb-4">
<h2 class="tw-mb-0">{{.Project.Title}}</h2>
<div class="tw-flex-1"></div>
<div class="ui secondary menu tw-m-0">
{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
{{template "repo/issue/filter_item_user_assign" dict
"QueryParamKey" "assignee"
"QueryLink" $queryLink
"UserSearchList" $.Assignees
"SelectedUserId" $.AssigneeID
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee")
"TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
"TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
}}
</div>
{{if $canWriteProject}}
<div class="ui compact mini menu">
<a class="item" href="{{.Link}}/edit?redirect=project">

View File

@ -7,25 +7,21 @@
<div class="ui attached segment">
{{template "base/alert" .}}
{{template "repo/create_helper" .}}
{{if not .CanCreateRepo}}
<div class="ui negative message">
<p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p>
</div>
{{end}}
<form class="ui form left-right-form new-repo-form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
<div class="inline required field {{if .Err_Owner}}error{{end}}">
<label>{{ctx.Locale.Tr "repo.owner"}}</label>
<div class="ui selection owner dropdown">
<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required>
<span class="text truncated-item-container" title="{{.ContextUser.Name}}">
{{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}}
<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span>
</span>
<div class="ui selection dropdown" id="repo_owner_dropdown">
<input type="hidden" name="uid" value="{{.ContextUser.ID}}">
<span class="text truncated-item-name"></span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}">
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"
{{if not .CanCreateRepoInDoer}}
data-create-repo-disallowed-prompt="{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimitOfDoer}}"
{{end}}
>
{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
</div>
@ -212,7 +208,7 @@
<br>
<div class="inline field">
<label></label>
<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}">
<button class="ui primary button">
{{ctx.Locale.Tr "repo.create_repo"}}
</button>
</div>

View File

@ -14,9 +14,10 @@
</div>
{{end}}
<div class="list-header">
{{template "repo/issue/navbar" .}}
<div class="list-header flex-text-block">
{{template "repo/issue/search" .}}
<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
{{if not .Repository.IsArchived}}
{{if .PageIsIssueList}}
<a class="ui small primary button issue-list-new" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>

View File

@ -2,8 +2,9 @@
<div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
{{template "repo/header" .}}
<div class="ui container padded">
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
{{template "repo/issue/navbar" .}}
<div class="flex-text-block tw-justify-end tw-mb-4">
<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
</div>
</div>

View File

@ -1,25 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="18"
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="20"
role="img" aria-label="{{.Badge.Label.Text}}: {{.Badge.Message.Text}}">
<title>{{.Badge.Label.Text}}: {{.Badge.Message.Text}}</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#fff" stop-opacity=".7" />
<stop offset=".1" stop-color="#aaa" stop-opacity=".1" />
<stop offset=".9" stop-color="#000" stop-opacity=".3" />
<stop offset="1" stop-color="#000" stop-opacity=".5" />
<linearGradient id="{{.Badge.IDPrefix}}s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1" />
<stop offset="1" stop-opacity=".1" />
</linearGradient>
<clipPath id="r">
<rect width="{{.Badge.Width}}" height="18" rx="4" fill="#fff" />
<clipPath id="{{.Badge.IDPrefix}}r">
<rect width="{{.Badge.Width}}" height="20" rx="3" fill="#fff" />
</clipPath>
<g clip-path="url(#r)">
<rect width="{{.Badge.Label.Width}}" height="18" fill="#555" />
<rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="18" fill="{{.Badge.Color}}" />
<rect width="{{.Badge.Width}}" height="18" fill="url(#s)" />
<g clip-path="url(#{{.Badge.IDPrefix}}r)">
<rect width="{{.Badge.Label.Width}}" height="20" fill="#555" />
<rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="20" fill="{{.Badge.Color}}" />
<rect width="{{.Badge.Width}}" height="20" fill="url(#{{.Badge.IDPrefix}}s)" />
</g>
<g fill="#fff" text-anchor="middle" font-family="{{.Badge.FontFamily}}"
text-rendering="geometricPrecision" font-size="{{.Badge.FontSize}}">
<text aria-hidden="true" x="{{.Badge.Label.X}}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)"
textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text>
<text x="{{.Badge.Label.X}}" y="140"
transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text>
<text aria-hidden="true" x="{{.Badge.Message.X}}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)"
textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text>
<text x="{{.Badge.Message.X}}" y="140" transform="scale(.1)" fill="#fff"
textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text>
</g>
<g fill="#fff" text-anchor="middle" font-family="Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision"
font-size="{{.Badge.FontSize}}"><text aria-hidden="true" x="{{.Badge.Label.X}}" y="140" fill="#010101" fill-opacity=".3"
transform="scale(.1)" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text x="{{.Badge.Label.X}}" y="130"
transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text aria-hidden="true"
x="{{.Badge.Message.X}}" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)"
textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text><text x="{{.Badge.Message.X}}" y="130" transform="scale(.1)"
fill="#fff" textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text></g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -31,7 +31,7 @@ func TestDownloadTaskLogs(t *testing.T) {
testCases := []struct {
treePath string
fileContent string
outcome *mockTaskOutcome
outcome []*mockTaskOutcome
zstdEnabled bool
}{
{
@ -46,21 +46,44 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: echo job1 with zstd enabled
job2:
runs-on: ubuntu-latest
steps:
- run: echo job2 with zstd enabled
`,
outcome: &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(1 * time.Second)),
Content: " \U0001F433 docker create image",
outcome: []*mockTaskOutcome{
{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(1 * time.Second)),
Content: " \U0001F433 docker create image",
},
{
Time: timestamppb.New(now.Add(2 * time.Second)),
Content: "job1 zstd enabled",
},
{
Time: timestamppb.New(now.Add(3 * time.Second)),
Content: "\U0001F3C1 Job succeeded",
},
},
{
Time: timestamppb.New(now.Add(2 * time.Second)),
Content: "job1 zstd enabled",
},
{
Time: timestamppb.New(now.Add(3 * time.Second)),
Content: "\U0001F3C1 Job succeeded",
},
{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(1 * time.Second)),
Content: " \U0001F433 docker create image",
},
{
Time: timestamppb.New(now.Add(2 * time.Second)),
Content: "job2 zstd enabled",
},
{
Time: timestamppb.New(now.Add(3 * time.Second)),
Content: "\U0001F3C1 Job succeeded",
},
},
},
},
@ -78,21 +101,44 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: echo job1 with zstd disabled
job2:
runs-on: ubuntu-latest
steps:
- run: echo job2 with zstd disabled
`,
outcome: &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(4 * time.Second)),
Content: " \U0001F433 docker create image",
outcome: []*mockTaskOutcome{
{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(4 * time.Second)),
Content: " \U0001F433 docker create image",
},
{
Time: timestamppb.New(now.Add(5 * time.Second)),
Content: "job1 zstd disabled",
},
{
Time: timestamppb.New(now.Add(6 * time.Second)),
Content: "\U0001F3C1 Job succeeded",
},
},
{
Time: timestamppb.New(now.Add(5 * time.Second)),
Content: "job1 zstd disabled",
},
{
Time: timestamppb.New(now.Add(6 * time.Second)),
Content: "\U0001F3C1 Job succeeded",
},
{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(4 * time.Second)),
Content: " \U0001F433 docker create image",
},
{
Time: timestamppb.New(now.Add(5 * time.Second)),
Content: "job2 zstd disabled",
},
{
Time: timestamppb.New(now.Add(6 * time.Second)),
Content: "\U0001F3C1 Job succeeded",
},
},
},
},
@ -124,54 +170,55 @@ jobs:
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
// fetch and execute task
task := runner.fetchTask(t)
runner.execTask(t, task, tc.outcome)
// fetch and execute tasks
for jobIndex, outcome := range tc.outcome {
task := runner.fetchTask(t)
runner.execTask(t, task, outcome)
// check whether the log file exists
logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id)
if setting.Actions.LogCompression.IsZstd() {
logFileName += ".zst"
// check whether the log file exists
logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id)
if setting.Actions.LogCompression.IsZstd() {
logFileName += ".zst"
}
_, err := storage.Actions.Stat(logFileName)
assert.NoError(t, err)
// download task logs and check content
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, repo.Name, runIndex, jobIndex)).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
assert.Len(t, logTextLines, len(outcome.logRows))
for idx, lr := range outcome.logRows {
assert.Equal(
t,
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
logTextLines[idx],
)
}
runID, _ := strconv.ParseInt(task.Context.GetFields()["run_id"].GetStringValue(), 10, 64)
jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID)
assert.NoError(t, err)
assert.Len(t, jobs, len(tc.outcome))
jobID := jobs[jobIndex].ID
// download task logs from API and check content
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, jobID)).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
assert.Len(t, logTextLines, len(outcome.logRows))
for idx, lr := range outcome.logRows {
assert.Equal(
t,
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
logTextLines[idx],
)
}
}
_, err := storage.Actions.Stat(logFileName)
assert.NoError(t, err)
// download task logs and check content
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/0/logs", user2.Name, repo.Name, runIndex)).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
assert.Len(t, logTextLines, len(tc.outcome.logRows))
for idx, lr := range tc.outcome.logRows {
assert.Equal(
t,
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
logTextLines[idx],
)
}
runID, _ := strconv.ParseInt(task.Context.GetFields()["run_id"].GetStringValue(), 10, 64)
jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID)
assert.NoError(t, err)
assert.Len(t, jobs, 1)
jobID := jobs[0].ID
// download task logs from API and check content
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, jobID)).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
assert.Len(t, logTextLines, len(tc.outcome.logRows))
for idx, lr := range tc.outcome.logRows {
assert.Equal(
t,
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
logTextLines[idx],
)
}
resetFunc()
})
}

View File

@ -9,6 +9,8 @@ import (
"net/url"
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
@ -16,7 +18,10 @@ import (
)
func TestGitSmartHTTP(t *testing.T) {
onGiteaRun(t, testGitSmartHTTP)
onGiteaRun(t, func(t *testing.T, u *url.URL) {
testGitSmartHTTP(t, u)
testRenamedRepoRedirect(t)
})
}
func testGitSmartHTTP(t *testing.T, u *url.URL) {
@ -73,3 +78,21 @@ func testGitSmartHTTP(t *testing.T, u *url.URL) {
})
}
}
func testRenamedRepoRedirect(t *testing.T) {
defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
// git client requires to get a 301 redirect response before 401 unauthorized response
req := NewRequest(t, "GET", "/user2/oldrepo1/info/refs")
resp := MakeRequest(t, req, http.StatusMovedPermanently)
redirect := resp.Header().Get("Location")
assert.Equal(t, "/user2/repo1/info/refs", redirect)
req = NewRequest(t, "GET", redirect)
resp = MakeRequest(t, req, http.StatusUnauthorized)
assert.Equal(t, "Unauthorized\n", resp.Body.String())
req = NewRequest(t, "GET", redirect).AddBasicAuth("user2")
resp = MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "65f1bf27bc3bf70f64657658635e66094edbcb4d\trefs/tags/v1.1")
}

View File

@ -31,16 +31,16 @@ func testRepoGenerate(t *testing.T, session *TestSession, templateID, templateOw
// Step2: click the "Use this template" button
htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/create\"]").Attr("href")
link, exists := htmlDoc.doc.Find(`a.ui.button[href^="/repo/create"]`).Attr("href")
assert.True(t, exists, "The template has changed")
req = NewRequest(t, "GET", link)
resp = session.MakeRequest(t, req, http.StatusOK)
// Step3: fill the form of the create
// Step3: fill the form on the "create" page
htmlDoc = NewHTMLParser(t, resp.Body)
link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/create\"]").Attr("action")
link, exists = htmlDoc.doc.Find(`form.ui.form[action^="/repo/create"]`).Attr("action")
assert.True(t, exists, "The template has changed")
_, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", generateOwner.ID)).Attr("data-value")
_, exists = htmlDoc.doc.Find(fmt.Sprintf(`#repo_owner_dropdown .item[data-value="%d"]`, generateOwner.ID)).Attr("data-value")
assert.True(t, exists, "Generate owner '%s' is not present in select box", generateOwnerName)
req = NewRequestWithValues(t, "POST", link, map[string]string{
"_csrf": htmlDoc.GetCSRF(),

View File

@ -989,14 +989,7 @@ table th[data-sortt-desc] .svg {
box-shadow: 0 0 0 1px var(--color-secondary) inset;
}
.emoji {
font-size: 1.25em;
line-height: var(--line-height-default);
font-style: normal !important;
font-weight: var(--font-weight-normal) !important;
vertical-align: -0.075em;
}
/* for "image" emojis like ":git:" ":gitea:" and ":github:" (see CUSTOM_EMOJIS config option) */
.emoji img {
border-width: 0 !important;
margin: 0 !important;
@ -1155,6 +1148,11 @@ table th[data-sortt-desc] .svg {
min-width: 0;
}
.flex-text-block > .ui.button,
.flex-text-inline > .ui.button {
margin: 0; /* fomantic buttons have default margin, when we use them in a flex container with gap, we do not need these margins */
}
/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content
the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden */
.ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) {

View File

@ -8,18 +8,6 @@
margin: 0 0.5em;
}
.project-toolbar-right .filter.menu {
flex-direction: row;
flex-wrap: wrap;
}
@media (max-width: 767.98px) {
.project-toolbar-right .dropdown .menu {
left: auto !important;
right: auto !important;
}
}
.project-column {
background-color: var(--color-project-column-bg) !important;
border: 1px solid var(--color-secondary) !important;

View File

@ -336,11 +336,6 @@
padding-right: 28px;
}
.markup .emoji {
max-width: none;
vertical-align: text-top;
}
.markup span.frame {
display: block;
overflow: hidden;

View File

@ -1,4 +1,4 @@
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
import {hideElem, querySingleVisibleElem, showElem, toggleElem} from '../utils/dom.ts';
import {htmlEscape} from 'escape-goat';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {sanitizeRepoName} from './repo-common.ts';
@ -6,7 +6,9 @@ import {sanitizeRepoName} from './repo-common.ts';
const {appSubUrl} = window.config;
function initRepoNewTemplateSearch(form: HTMLFormElement) {
const inputRepoOwnerUid = form.querySelector<HTMLInputElement>('#uid');
const elSubmitButton = querySingleVisibleElem<HTMLInputElement>(form, '.ui.primary.button');
const elCreateRepoErrorMessage = form.querySelector('#create-repo-error-message');
const elRepoOwnerDropdown = form.querySelector('#repo_owner_dropdown');
const elRepoTemplateDropdown = form.querySelector<HTMLInputElement>('#repo_template_search');
const inputRepoTemplate = form.querySelector<HTMLInputElement>('#repo_template');
const elTemplateUnits = form.querySelector('#template_units');
@ -19,11 +21,23 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) {
inputRepoTemplate.addEventListener('change', checkTemplate);
checkTemplate();
const $dropdown = fomanticQuery(elRepoTemplateDropdown);
const $repoOwnerDropdown = fomanticQuery(elRepoOwnerDropdown);
const $repoTemplateDropdown = fomanticQuery(elRepoTemplateDropdown);
const onChangeOwner = function () {
$dropdown.dropdown('setting', {
const ownerId = $repoOwnerDropdown.dropdown('get value');
const $ownerItem = $repoOwnerDropdown.dropdown('get item', ownerId);
hideElem(elCreateRepoErrorMessage);
elSubmitButton.disabled = false;
if ($ownerItem?.length) {
const elOwnerItem = $ownerItem[0];
elCreateRepoErrorMessage.textContent = elOwnerItem.getAttribute('data-create-repo-disallowed-prompt') ?? '';
const hasError = Boolean(elCreateRepoErrorMessage.textContent);
toggleElem(elCreateRepoErrorMessage, hasError);
elSubmitButton.disabled = hasError;
}
$repoTemplateDropdown.dropdown('setting', {
apiSettings: {
url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${inputRepoOwnerUid.value}`,
url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${ownerId}`,
onResponse(response: any) {
const results = [];
results.push({name: '', value: ''}); // empty item means not using template
@ -33,14 +47,14 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) {
value: String(tmplRepo.repository.id),
});
}
$dropdown.fomanticExt.onResponseKeepSelectedItem($dropdown, inputRepoTemplate.value);
$repoTemplateDropdown.fomanticExt.onResponseKeepSelectedItem($repoTemplateDropdown, inputRepoTemplate.value);
return {results};
},
cache: false,
},
});
};
inputRepoOwnerUid.addEventListener('change', onChangeOwner);
$repoOwnerDropdown.dropdown('setting', 'onChange', onChangeOwner);
onChangeOwner();
}