mirror of
https://github.com/go-gitea/gitea
synced 2025-12-07 13:28:25 +00:00
Merge branch 'main' into api-repo-actions
This commit is contained in:
@@ -22,8 +22,48 @@ const (
|
||||
GithubEventRelease = "release"
|
||||
GithubEventPullRequestComment = "pull_request_comment"
|
||||
GithubEventGollum = "gollum"
|
||||
GithubEventSchedule = "schedule"
|
||||
)
|
||||
|
||||
// IsDefaultBranchWorkflow returns true if the event only triggers workflows on the default branch
|
||||
func IsDefaultBranchWorkflow(triggedEvent webhook_module.HookEventType) bool {
|
||||
switch triggedEvent {
|
||||
case webhook_module.HookEventDelete:
|
||||
// GitHub "delete" event
|
||||
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#delete
|
||||
return true
|
||||
case webhook_module.HookEventFork:
|
||||
// GitHub "fork" event
|
||||
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#fork
|
||||
return true
|
||||
case webhook_module.HookEventIssueComment:
|
||||
// GitHub "issue_comment" event
|
||||
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment
|
||||
return true
|
||||
case webhook_module.HookEventPullRequestComment:
|
||||
// GitHub "pull_request_comment" event
|
||||
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
|
||||
return true
|
||||
case webhook_module.HookEventWiki:
|
||||
// GitHub "gollum" event
|
||||
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum
|
||||
return true
|
||||
case webhook_module.HookEventSchedule:
|
||||
// GitHub "schedule" event
|
||||
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
|
||||
return true
|
||||
case webhook_module.HookEventIssues,
|
||||
webhook_module.HookEventIssueAssign,
|
||||
webhook_module.HookEventIssueLabel,
|
||||
webhook_module.HookEventIssueMilestone:
|
||||
// Github "issues" event
|
||||
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// canGithubEventMatch check if the input Github event can match any Gitea event.
|
||||
func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEventType) bool {
|
||||
switch eventName {
|
||||
@@ -51,7 +91,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
|
||||
case webhook_module.HookEventPullRequest,
|
||||
webhook_module.HookEventPullRequestSync,
|
||||
webhook_module.HookEventPullRequestAssign,
|
||||
webhook_module.HookEventPullRequestLabel:
|
||||
webhook_module.HookEventPullRequestLabel,
|
||||
webhook_module.HookEventPullRequestReviewRequest,
|
||||
webhook_module.HookEventPullRequestMilestone:
|
||||
return true
|
||||
|
||||
default:
|
||||
@@ -69,6 +111,14 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
|
||||
return false
|
||||
}
|
||||
|
||||
case GithubEventSchedule:
|
||||
return triggedEvent == webhook_module.HookEventSchedule
|
||||
|
||||
case GithubEventIssueComment:
|
||||
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
|
||||
return triggedEvent == webhook_module.HookEventIssueComment ||
|
||||
triggedEvent == webhook_module.HookEventPullRequestComment
|
||||
|
||||
default:
|
||||
return eventName == string(triggedEvent)
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@ func TestCanGithubEventMatch(t *testing.T) {
|
||||
webhook_module.HookEventCreate,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"create pull request comment",
|
||||
GithubEventIssueComment,
|
||||
webhook_module.HookEventPullRequestComment,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -100,7 +100,7 @@ func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limi
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("scan: %w", err)
|
||||
return nil, fmt.Errorf("ReadLogs scan: %w", err)
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
|
||||
@@ -35,9 +35,18 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
|
||||
} else if task.Status.IsDone() {
|
||||
preStep.Stopped = task.Stopped
|
||||
preStep.Status = actions_model.StatusFailure
|
||||
if task.Status.IsSkipped() {
|
||||
preStep.Status = actions_model.StatusSkipped
|
||||
}
|
||||
}
|
||||
logIndex += preStep.LogLength
|
||||
|
||||
// lastHasRunStep is the last step that has run.
|
||||
// For example,
|
||||
// 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1.
|
||||
// 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3.
|
||||
// 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2.
|
||||
// So its Stopped is the Started of postStep when there are no more steps to run.
|
||||
var lastHasRunStep *actions_model.ActionTaskStep
|
||||
for _, step := range task.Steps {
|
||||
if step.Status.HasRun() {
|
||||
@@ -53,11 +62,15 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
|
||||
Name: postStepName,
|
||||
Status: actions_model.StatusWaiting,
|
||||
}
|
||||
if task.Status.IsDone() {
|
||||
// If the lastHasRunStep is the last step, or it has failed, postStep has started.
|
||||
if lastHasRunStep.Status.IsFailure() || lastHasRunStep == task.Steps[len(task.Steps)-1] {
|
||||
postStep.LogIndex = logIndex
|
||||
postStep.LogLength = task.LogLength - postStep.LogIndex
|
||||
postStep.Status = task.Status
|
||||
postStep.Started = lastHasRunStep.Stopped
|
||||
postStep.Status = actions_model.StatusRunning
|
||||
}
|
||||
if task.Status.IsDone() {
|
||||
postStep.Status = task.Status
|
||||
postStep.Stopped = task.Stopped
|
||||
}
|
||||
ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2)
|
||||
|
||||
@@ -103,6 +103,40 @@ func TestFullSteps(t *testing.T) {
|
||||
{Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 100, LogLength: 0, Started: 10100, Stopped: 10100},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all steps finished but task is running",
|
||||
task: &actions_model.ActionTask{
|
||||
Steps: []*actions_model.ActionTaskStep{
|
||||
{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090},
|
||||
},
|
||||
Status: actions_model.StatusRunning,
|
||||
Started: 10000,
|
||||
Stopped: 0,
|
||||
LogLength: 100,
|
||||
},
|
||||
want: []*actions_model.ActionTaskStep{
|
||||
{Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010},
|
||||
{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090},
|
||||
{Name: postStepName, Status: actions_model.StatusRunning, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skipped task",
|
||||
task: &actions_model.ActionTask{
|
||||
Steps: []*actions_model.ActionTaskStep{
|
||||
{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
|
||||
},
|
||||
Status: actions_model.StatusSkipped,
|
||||
Started: 0,
|
||||
Stopped: 0,
|
||||
LogLength: 0,
|
||||
},
|
||||
want: []*actions_model.ActionTaskStep{
|
||||
{Name: preStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
|
||||
{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
|
||||
{Name: postStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
|
||||
type DetectedWorkflow struct {
|
||||
EntryName string
|
||||
TriggerEvent string
|
||||
TriggerEvent *jobparser.Event
|
||||
Content []byte
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ func DetectWorkflows(
|
||||
commit *git.Commit,
|
||||
triggedEvent webhook_module.HookEventType,
|
||||
payload api.Payloader,
|
||||
detectSchedule bool,
|
||||
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
|
||||
entries, err := ListWorkflows(commit)
|
||||
if err != nil {
|
||||
@@ -114,6 +115,7 @@ func DetectWorkflows(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// one workflow may have multiple events
|
||||
events, err := GetEventsFromContent(content)
|
||||
if err != nil {
|
||||
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
|
||||
@@ -122,17 +124,18 @@ func DetectWorkflows(
|
||||
for _, evt := range events {
|
||||
log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent)
|
||||
if evt.IsSchedule() {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt.Name,
|
||||
Content: content,
|
||||
if detectSchedule {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt,
|
||||
Content: content,
|
||||
}
|
||||
schedules = append(schedules, dwf)
|
||||
}
|
||||
schedules = append(schedules, dwf)
|
||||
}
|
||||
if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
||||
} else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt.Name,
|
||||
TriggerEvent: evt,
|
||||
Content: content,
|
||||
}
|
||||
workflows = append(workflows, dwf)
|
||||
@@ -143,6 +146,41 @@ func DetectWorkflows(
|
||||
return workflows, schedules, nil
|
||||
}
|
||||
|
||||
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
|
||||
entries, err := ListWorkflows(commit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wfs := make([]*DetectedWorkflow, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
content, err := GetContentFromEntry(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// one workflow may have multiple events
|
||||
events, err := GetEventsFromContent(content)
|
||||
if err != nil {
|
||||
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
for _, evt := range events {
|
||||
if evt.IsSchedule() {
|
||||
log.Trace("detect scheduled workflow: %q", entry.Name())
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt,
|
||||
Content: content,
|
||||
}
|
||||
wfs = append(wfs, dwf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wfs, nil
|
||||
}
|
||||
|
||||
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
|
||||
if !canGithubEventMatch(evt.Name, triggedEvent) {
|
||||
return false
|
||||
@@ -153,7 +191,8 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
||||
webhook_module.HookEventCreate,
|
||||
webhook_module.HookEventDelete,
|
||||
webhook_module.HookEventFork,
|
||||
webhook_module.HookEventWiki:
|
||||
webhook_module.HookEventWiki,
|
||||
webhook_module.HookEventSchedule:
|
||||
if len(evt.Acts()) != 0 {
|
||||
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
|
||||
}
|
||||
@@ -182,7 +221,9 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
||||
webhook_module.HookEventPullRequest,
|
||||
webhook_module.HookEventPullRequestSync,
|
||||
webhook_module.HookEventPullRequestAssign,
|
||||
webhook_module.HookEventPullRequestLabel:
|
||||
webhook_module.HookEventPullRequestLabel,
|
||||
webhook_module.HookEventPullRequestReviewRequest,
|
||||
webhook_module.HookEventPullRequestMilestone:
|
||||
return matchPullRequestEvent(gitRepo, commit, payload.(*api.PullRequestPayload), evt)
|
||||
|
||||
case // pull_request_review
|
||||
@@ -358,13 +399,13 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
} else {
|
||||
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
// Actions with the same name:
|
||||
// opened, edited, closed, reopened, assigned, unassigned
|
||||
// opened, edited, closed, reopened, assigned, unassigned, review_requested, review_request_removed, milestoned, demilestoned
|
||||
// Actions need to be converted:
|
||||
// synchronized -> synchronize
|
||||
// label_updated -> labeled
|
||||
// label_cleared -> unlabeled
|
||||
// Unsupported activity types:
|
||||
// converted_to_draft, ready_for_review, locked, unlocked, review_requested, review_request_removed, auto_merge_enabled, auto_merge_disabled
|
||||
// converted_to_draft, ready_for_review, locked, unlocked, auto_merge_enabled, auto_merge_disabled, enqueued, dequeued
|
||||
|
||||
action := prPayload.Action
|
||||
switch action {
|
||||
@@ -400,6 +441,9 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
// all acts conditions should be satisfied
|
||||
for cond, vals := range acts {
|
||||
switch cond {
|
||||
case "types":
|
||||
// types have been checked
|
||||
continue
|
||||
case "branches":
|
||||
refName := git.RefName(prPayload.PullRequest.Base.Ref)
|
||||
patterns, err := workflowpattern.CompilePatterns(vals...)
|
||||
|
||||
@@ -118,6 +118,13 @@ func TestDetectMatched(t *testing.T) {
|
||||
yamlOn: "on: gollum",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "HookEventSchedue(schedule) matches GithubEventSchedule(schedule)",
|
||||
triggedEvent: webhook_module.HookEventSchedule,
|
||||
payload: nil,
|
||||
yamlOn: "on: schedule",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -29,11 +29,10 @@ func GetKeyPair(ctx context.Context, user *user_model.User) (pub, priv string, e
|
||||
return pub, priv, err
|
||||
}
|
||||
return pub, priv, err
|
||||
} else {
|
||||
priv = settings[user_model.UserActivityPubPrivPem].SettingValue
|
||||
pub = settings[user_model.UserActivityPubPubPem].SettingValue
|
||||
return pub, priv, err
|
||||
}
|
||||
priv = settings[user_model.UserActivityPubPrivPem].SettingValue
|
||||
pub = settings[user_model.UserActivityPubPubPem].SettingValue
|
||||
return pub, priv, err
|
||||
}
|
||||
|
||||
// GetPublicKey function returns a user's public key
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ package password
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
goContext "context"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"html/template"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -15,6 +17,11 @@ import (
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrComplexity = errors.New("password not complex enough")
|
||||
ErrMinLength = errors.New("password not long enough")
|
||||
)
|
||||
|
||||
// complexity contains information about a particular kind of password complexity
|
||||
type complexity struct {
|
||||
ValidChars string
|
||||
@@ -101,26 +108,29 @@ func Generate(n int) (string, error) {
|
||||
}
|
||||
buffer[j] = validChars[rnd.Int64()]
|
||||
}
|
||||
pwned, err := IsPwned(goContext.Background(), string(buffer))
|
||||
if err != nil {
|
||||
|
||||
if err := IsPwned(context.Background(), string(buffer)); err != nil {
|
||||
if errors.Is(err, ErrIsPwned) {
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if IsComplexEnough(string(buffer)) && !pwned && string(buffer[0]) != " " && string(buffer[n-1]) != " " {
|
||||
if IsComplexEnough(string(buffer)) && string(buffer[0]) != " " && string(buffer[n-1]) != " " {
|
||||
return string(buffer), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BuildComplexityError builds the error message when password complexity checks fail
|
||||
func BuildComplexityError(locale translation.Locale) string {
|
||||
func BuildComplexityError(locale translation.Locale) template.HTML {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(locale.Tr("form.password_complexity"))
|
||||
buffer.WriteString(locale.TrString("form.password_complexity"))
|
||||
buffer.WriteString("<ul>")
|
||||
for _, c := range requiredList {
|
||||
buffer.WriteString("<li>")
|
||||
buffer.WriteString(locale.Tr(c.TrNameOne))
|
||||
buffer.WriteString(locale.TrString(c.TrNameOne))
|
||||
buffer.WriteString("</li>")
|
||||
}
|
||||
buffer.WriteString("</ul>")
|
||||
return buffer.String()
|
||||
return template.HTML(buffer.String())
|
||||
}
|
||||
|
||||
@@ -5,24 +5,48 @@ package password
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/auth/password/pwn"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
var ErrIsPwned = errors.New("password has been pwned")
|
||||
|
||||
type ErrIsPwnedRequest struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func IsErrIsPwnedRequest(err error) bool {
|
||||
_, ok := err.(ErrIsPwnedRequest)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrIsPwnedRequest) Error() string {
|
||||
return fmt.Sprintf("using Have-I-Been-Pwned service failed: %v", err.err)
|
||||
}
|
||||
|
||||
func (err ErrIsPwnedRequest) Unwrap() error {
|
||||
return err.err
|
||||
}
|
||||
|
||||
// IsPwned checks whether a password has been pwned
|
||||
// NOTE: This func returns true if it encounters an error under the assumption that you ALWAYS want to check against
|
||||
// HIBP, so not getting a response should block a password until it can be verified.
|
||||
func IsPwned(ctx context.Context, password string) (bool, error) {
|
||||
// If a password has not been pwned, no error is returned.
|
||||
func IsPwned(ctx context.Context, password string) error {
|
||||
if !setting.PasswordCheckPwn {
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
client := pwn.New(pwn.WithContext(ctx))
|
||||
count, err := client.CheckPassword(password, true)
|
||||
if err != nil {
|
||||
return true, err
|
||||
return ErrIsPwnedRequest{err}
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
if count > 0 {
|
||||
return ErrIsPwned
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func newRequest(ctx context.Context, method, url string, body io.ReadCloser) (*h
|
||||
// because artificial responses will be added to the response
|
||||
// For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
|
||||
func (c *Client) CheckPassword(pw string, padding bool) (int, error) {
|
||||
if strings.TrimSpace(pw) == "" {
|
||||
if pw == "" {
|
||||
return -1, ErrEmptyPassword
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
package pwn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var client = New(WithHTTP(&http.Client{
|
||||
@@ -25,78 +26,44 @@ func TestMain(m *testing.M) {
|
||||
func TestPassword(t *testing.T) {
|
||||
// Check input error
|
||||
_, err := client.CheckPassword("", false)
|
||||
if err == nil {
|
||||
t.Log("blank input should return an error")
|
||||
t.Fail()
|
||||
}
|
||||
if !errors.Is(err, ErrEmptyPassword) {
|
||||
t.Log("blank input should return ErrEmptyPassword")
|
||||
t.Fail()
|
||||
}
|
||||
assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword")
|
||||
|
||||
// Should fail
|
||||
fail := "password1234"
|
||||
count, err := client.CheckPassword(fail, false)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
}
|
||||
if count == 0 {
|
||||
t.Logf("%s should fail as a password\n", fail)
|
||||
t.Fail()
|
||||
}
|
||||
assert.NotEmpty(t, count, "%s should fail as a password", fail)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Should fail (with padding)
|
||||
failPad := "administrator"
|
||||
count, err = client.CheckPassword(failPad, true)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
}
|
||||
if count == 0 {
|
||||
t.Logf("%s should fail as a password\n", failPad)
|
||||
t.Fail()
|
||||
}
|
||||
assert.NotEmpty(t, count, "%s should fail as a password", failPad)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Checking for a "good" password isn't going to be perfect, but we can give it a good try
|
||||
// with hopefully minimal error. Try five times?
|
||||
var good bool
|
||||
var pw string
|
||||
for idx := 0; idx <= 5; idx++ {
|
||||
pw = testPassword()
|
||||
count, err = client.CheckPassword(pw, false)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
assert.Condition(t, func() bool {
|
||||
for i := 0; i <= 5; i++ {
|
||||
count, err = client.CheckPassword(testPassword(), false)
|
||||
assert.NoError(t, err)
|
||||
if count == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
good = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !good {
|
||||
t.Log("no generated passwords passed. there is a chance this is a fluke")
|
||||
t.Fail()
|
||||
}
|
||||
return false
|
||||
}, "no generated passwords passed. there is a chance this is a fluke")
|
||||
|
||||
// Again, but with padded responses
|
||||
good = false
|
||||
for idx := 0; idx <= 5; idx++ {
|
||||
pw = testPassword()
|
||||
count, err = client.CheckPassword(pw, true)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fail()
|
||||
assert.Condition(t, func() bool {
|
||||
for i := 0; i <= 5; i++ {
|
||||
count, err = client.CheckPassword(testPassword(), true)
|
||||
assert.NoError(t, err)
|
||||
if count == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
good = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !good {
|
||||
t.Log("no generated passwords passed. there is a chance this is a fluke")
|
||||
t.Fail()
|
||||
}
|
||||
return false
|
||||
}, "no generated passwords passed. there is a chance this is a fluke")
|
||||
}
|
||||
|
||||
// Credit to https://golangbyexample.com/generate-random-password-golang/
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
package avatar
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
// HashAvatar will generate a unique string, which ensures that when there's a
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
package identicon
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const minImageSize = 16
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package badge
|
||||
|
||||
import (
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
)
|
||||
|
||||
// The Badge layout: |offset|label|message|
|
||||
// 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 {
|
||||
text string
|
||||
width int
|
||||
x int
|
||||
}
|
||||
|
||||
func (m Message) Text() string {
|
||||
return m.text
|
||||
}
|
||||
|
||||
func (m Message) Width() int {
|
||||
return m.width
|
||||
}
|
||||
|
||||
func (m Message) X() int {
|
||||
return m.x
|
||||
}
|
||||
|
||||
func (m Message) TextLength() int {
|
||||
return int(float64(m.width-defaultOffset) * 9.5)
|
||||
}
|
||||
|
||||
type Badge struct {
|
||||
Color string
|
||||
FontSize int
|
||||
Label Label
|
||||
Message Message
|
||||
}
|
||||
|
||||
func (b Badge) Width() int {
|
||||
return b.Label.width + b.Message.width
|
||||
}
|
||||
|
||||
const (
|
||||
defaultOffset = 9
|
||||
defaultFontSize = 11
|
||||
DefaultColor = "#9f9f9f" // Grey
|
||||
defaultFontWidth = 7 // approximate speculation
|
||||
)
|
||||
|
||||
var StatusColorMap = map[actions_model.Status]string{
|
||||
actions_model.StatusSuccess: "#4c1", // Green
|
||||
actions_model.StatusSkipped: "#dfb317", // Yellow
|
||||
actions_model.StatusUnknown: "#97ca00", // Light Green
|
||||
actions_model.StatusFailure: "#e05d44", // Red
|
||||
actions_model.StatusCancelled: "#fe7d37", // Orange
|
||||
actions_model.StatusWaiting: "#dfb317", // Yellow
|
||||
actions_model.StatusRunning: "#dfb317", // Yellow
|
||||
actions_model.StatusBlocked: "#dfb317", // Yellow
|
||||
}
|
||||
|
||||
// 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
|
||||
return Badge{
|
||||
Label: Label{
|
||||
text: label,
|
||||
width: lw,
|
||||
},
|
||||
Message: Message{
|
||||
text: message,
|
||||
width: mw,
|
||||
x: x,
|
||||
},
|
||||
FontSize: defaultFontSize * 10,
|
||||
Color: color,
|
||||
}
|
||||
}
|
||||
@@ -4,85 +4,12 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"unicode/utf8"
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// NaturalSortLess compares two strings so that they could be sorted in natural order
|
||||
func NaturalSortLess(s1, s2 string) bool {
|
||||
var i1, i2 int
|
||||
for {
|
||||
rune1, j1, end1 := getNextRune(s1, i1)
|
||||
rune2, j2, end2 := getNextRune(s2, i2)
|
||||
if end1 || end2 {
|
||||
return end1 != end2 && end1
|
||||
}
|
||||
dec1 := isDecimal(rune1)
|
||||
dec2 := isDecimal(rune2)
|
||||
var less, equal bool
|
||||
if dec1 && dec2 {
|
||||
i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2)
|
||||
} else if !dec1 && !dec2 {
|
||||
equal = rune1 == rune2
|
||||
less = rune1 < rune2
|
||||
i1 = j1
|
||||
i2 = j2
|
||||
} else {
|
||||
return rune1 < rune2
|
||||
}
|
||||
if !equal {
|
||||
return less
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getNextRune(str string, pos int) (rune, int, bool) {
|
||||
if pos < len(str) {
|
||||
r, w := utf8.DecodeRuneInString(str[pos:])
|
||||
// Fallback to ascii
|
||||
if r == utf8.RuneError {
|
||||
r = rune(str[pos])
|
||||
w = 1
|
||||
}
|
||||
return r, pos + w, false
|
||||
}
|
||||
return 0, pos, true
|
||||
}
|
||||
|
||||
func isDecimal(r rune) bool {
|
||||
return '0' <= r && r <= '9'
|
||||
}
|
||||
|
||||
func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) {
|
||||
d1, d2 := true, true
|
||||
var dec1, dec2 string
|
||||
for d1 || d2 {
|
||||
if d1 {
|
||||
r, j, end := getNextRune(str1, pos1)
|
||||
if !end && isDecimal(r) {
|
||||
dec1 += string(r)
|
||||
pos1 = j
|
||||
} else {
|
||||
d1 = false
|
||||
}
|
||||
}
|
||||
if d2 {
|
||||
r, j, end := getNextRune(str2, pos2)
|
||||
if !end && isDecimal(r) {
|
||||
dec2 += string(r)
|
||||
pos2 = j
|
||||
} else {
|
||||
d2 = false
|
||||
}
|
||||
}
|
||||
}
|
||||
less, equal = compareBigNumbers(dec1, dec2)
|
||||
return pos1, pos2, less, equal
|
||||
}
|
||||
|
||||
func compareBigNumbers(dec1, dec2 string) (less, equal bool) {
|
||||
d1, _ := big.NewInt(0).SetString(dec1, 10)
|
||||
d2, _ := big.NewInt(0).SetString(dec2, 10)
|
||||
cmp := d1.Cmp(d2)
|
||||
return cmp < 0, cmp == 0
|
||||
c := collate.New(language.English, collate.Numeric)
|
||||
return c.CompareString(s1, s2) < 0
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func TestNaturalSortLess(t *testing.T) {
|
||||
test := func(s1, s2 string, less bool) {
|
||||
assert.Equal(t, less, NaturalSortLess(s1, s2))
|
||||
assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2)
|
||||
}
|
||||
test("v1.20.0", "v1.2.0", false)
|
||||
test("v1.20.0", "v1.29.0", true)
|
||||
@@ -20,4 +20,11 @@ func TestNaturalSortLess(t *testing.T) {
|
||||
test("a-1-a", "a-1-b", true)
|
||||
test("2", "12", true)
|
||||
test("a", "ab", true)
|
||||
|
||||
test("A", "b", true)
|
||||
test("a", "B", true)
|
||||
|
||||
test("cafe", "café", true)
|
||||
test("café", "cafe", false)
|
||||
test("caff", "café", false)
|
||||
}
|
||||
|
||||
+10
-37
@@ -4,8 +4,8 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@@ -24,16 +23,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
// EncodeMD5 encodes string to md5 hex value.
|
||||
func EncodeMD5(str string) string {
|
||||
m := md5.New()
|
||||
_, _ = m.Write([]byte(str))
|
||||
return hex.EncodeToString(m.Sum(nil))
|
||||
}
|
||||
|
||||
// EncodeSha1 string to sha1 hex value.
|
||||
func EncodeSha1(str string) string {
|
||||
h := sha1.New()
|
||||
@@ -70,11 +61,6 @@ func BasicAuthDecode(encoded string) (string, string, error) {
|
||||
return auth[0], auth[1], nil
|
||||
}
|
||||
|
||||
// BasicAuthEncode encode basic auth string
|
||||
func BasicAuthEncode(username, password string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
}
|
||||
|
||||
// VerifyTimeLimitCode verify time limit code
|
||||
func VerifyTimeLimitCode(data string, minutes int, code string) bool {
|
||||
if len(code) <= 18 {
|
||||
@@ -129,7 +115,7 @@ func CreateTimeLimitCode(data string, minutes int, startInf any) string {
|
||||
|
||||
// create sha1 encode string
|
||||
sh := sha1.New()
|
||||
_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, setting.SecretKey, startStr, endStr, minutes)))
|
||||
_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, endStr, minutes)))
|
||||
encoded := hex.EncodeToString(sh.Sum(nil))
|
||||
|
||||
code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
|
||||
@@ -164,13 +150,16 @@ func TruncateString(str string, limit int) string {
|
||||
|
||||
// StringsToInt64s converts a slice of string to a slice of int64.
|
||||
func StringsToInt64s(strs []string) ([]int64, error) {
|
||||
ints := make([]int64, len(strs))
|
||||
for i := range strs {
|
||||
n, err := strconv.ParseInt(strs[i], 10, 64)
|
||||
if strs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
ints := make([]int64, 0, len(strs))
|
||||
for _, s := range strs {
|
||||
n, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return ints, err
|
||||
return nil, err
|
||||
}
|
||||
ints[i] = n
|
||||
ints = append(ints, n)
|
||||
}
|
||||
return ints, nil
|
||||
}
|
||||
@@ -184,22 +173,6 @@ func Int64sToStrings(ints []int64) []string {
|
||||
return strs
|
||||
}
|
||||
|
||||
// Int64sContains returns if a int64 in a slice of int64
|
||||
func Int64sContains(intsSlice []int64, a int64) bool {
|
||||
for _, c := range intsSlice {
|
||||
if c == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsLetter reports whether the rune is a letter (category L).
|
||||
// https://github.com/golang/go/blob/c3b4918/src/go/scanner/scanner.go#L342
|
||||
func IsLetter(ch rune) bool {
|
||||
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch)
|
||||
}
|
||||
|
||||
// EntryIcon returns the octicon class for displaying files/directories
|
||||
func EntryIcon(entry *git.TreeEntry) string {
|
||||
switch {
|
||||
|
||||
@@ -11,13 +11,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEncodeMD5(t *testing.T) {
|
||||
assert.Equal(t,
|
||||
"3858f62230ac3c915f300c664312c63f",
|
||||
EncodeMD5("foobar"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestEncodeSha1(t *testing.T) {
|
||||
assert.Equal(t,
|
||||
"8843d7f92416211de9ebb963ff4ce28125932878",
|
||||
@@ -52,11 +45,6 @@ func TestBasicAuthDecode(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBasicAuthEncode(t *testing.T) {
|
||||
assert.Equal(t, "Zm9vOmJhcg==", BasicAuthEncode("foo", "bar"))
|
||||
assert.Equal(t, "MjM6IjotLS0t", BasicAuthEncode("23:\"", "----"))
|
||||
}
|
||||
|
||||
func TestVerifyTimeLimitCode(t *testing.T) {
|
||||
tc := []struct {
|
||||
data string
|
||||
@@ -150,12 +138,13 @@ func TestStringsToInt64s(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
testSuccess(nil, nil)
|
||||
testSuccess([]string{}, []int64{})
|
||||
testSuccess([]string{"-1234"}, []int64{-1234})
|
||||
testSuccess([]string{"1", "4", "16", "64", "256"},
|
||||
[]int64{1, 4, 16, 64, 256})
|
||||
testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256})
|
||||
|
||||
_, err := StringsToInt64s([]string{"-1", "a", "$"})
|
||||
ints, err := StringsToInt64s([]string{"-1", "a"})
|
||||
assert.Len(t, ints, 0)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -167,29 +156,6 @@ func TestInt64sToStrings(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
func TestInt64sContains(t *testing.T) {
|
||||
assert.True(t, Int64sContains([]int64{6, 44324, 4324, 32, 1, 2323}, 1))
|
||||
assert.True(t, Int64sContains([]int64{2323}, 2323))
|
||||
assert.False(t, Int64sContains([]int64{6, 44324, 4324, 32, 1, 2323}, 232))
|
||||
}
|
||||
|
||||
func TestIsLetter(t *testing.T) {
|
||||
assert.True(t, IsLetter('a'))
|
||||
assert.True(t, IsLetter('e'))
|
||||
assert.True(t, IsLetter('q'))
|
||||
assert.True(t, IsLetter('z'))
|
||||
assert.True(t, IsLetter('A'))
|
||||
assert.True(t, IsLetter('E'))
|
||||
assert.True(t, IsLetter('Q'))
|
||||
assert.True(t, IsLetter('Z'))
|
||||
assert.True(t, IsLetter('_'))
|
||||
assert.False(t, IsLetter('-'))
|
||||
assert.False(t, IsLetter('1'))
|
||||
assert.False(t, IsLetter('$'))
|
||||
assert.False(t, IsLetter(0x00))
|
||||
assert.False(t, IsLetter(0x93))
|
||||
}
|
||||
|
||||
// TODO: Test EntryIcon
|
||||
|
||||
func TestSetupGiteaRoot(t *testing.T) {
|
||||
|
||||
Vendored
+3
-3
@@ -24,11 +24,11 @@ func newCache(cacheConfig setting.Cache) (mc.Cache, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// NewContext start cache service
|
||||
func NewContext() error {
|
||||
// Init start cache service
|
||||
func Init() error {
|
||||
var err error
|
||||
|
||||
if conn == nil && setting.CacheService.Enabled {
|
||||
if conn == nil {
|
||||
if conn, err = newCache(setting.CacheService.Cache); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -22,9 +22,9 @@ func createTestCache() {
|
||||
}
|
||||
|
||||
func TestNewContext(t *testing.T) {
|
||||
assert.NoError(t, NewContext())
|
||||
assert.NoError(t, Init())
|
||||
|
||||
setting.CacheService.Cache = setting.Cache{Enabled: true, Adapter: "redis", Conn: "some random string"}
|
||||
setting.CacheService.Cache = setting.Cache{Adapter: "redis", Conn: "some random string"}
|
||||
con, err := newCache(setting.Cache{
|
||||
Adapter: "rand",
|
||||
Conn: "false conf",
|
||||
|
||||
+22
-21
@@ -22,17 +22,21 @@ import (
|
||||
// UTF8BOM is the utf-8 byte-order marker
|
||||
var UTF8BOM = []byte{'\xef', '\xbb', '\xbf'}
|
||||
|
||||
type ConvertOpts struct {
|
||||
KeepBOM bool
|
||||
}
|
||||
|
||||
// ToUTF8WithFallbackReader detects the encoding of content and converts to UTF-8 reader if possible
|
||||
func ToUTF8WithFallbackReader(rd io.Reader) io.Reader {
|
||||
func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
|
||||
buf := make([]byte, 2048)
|
||||
n, err := util.ReadAtMost(rd, buf)
|
||||
if err != nil {
|
||||
return io.MultiReader(bytes.NewReader(RemoveBOMIfPresent(buf[:n])), rd)
|
||||
return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd)
|
||||
}
|
||||
|
||||
charsetLabel, err := DetectEncoding(buf[:n])
|
||||
if err != nil || charsetLabel == "UTF-8" {
|
||||
return io.MultiReader(bytes.NewReader(RemoveBOMIfPresent(buf[:n])), rd)
|
||||
return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd)
|
||||
}
|
||||
|
||||
encoding, _ := charset.Lookup(charsetLabel)
|
||||
@@ -42,20 +46,20 @@ func ToUTF8WithFallbackReader(rd io.Reader) io.Reader {
|
||||
|
||||
return transform.NewReader(
|
||||
io.MultiReader(
|
||||
bytes.NewReader(RemoveBOMIfPresent(buf[:n])),
|
||||
bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)),
|
||||
rd,
|
||||
),
|
||||
encoding.NewDecoder(),
|
||||
)
|
||||
}
|
||||
|
||||
// ToUTF8WithErr converts content to UTF8 encoding
|
||||
func ToUTF8WithErr(content []byte) (string, error) {
|
||||
// ToUTF8 converts content to UTF8 encoding
|
||||
func ToUTF8(content []byte, opts ConvertOpts) (string, error) {
|
||||
charsetLabel, err := DetectEncoding(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if charsetLabel == "UTF-8" {
|
||||
return string(RemoveBOMIfPresent(content)), nil
|
||||
return string(MaybeRemoveBOM(content, opts)), nil
|
||||
}
|
||||
|
||||
encoding, _ := charset.Lookup(charsetLabel)
|
||||
@@ -70,28 +74,22 @@ func ToUTF8WithErr(content []byte) (string, error) {
|
||||
result = append(result, content[n:]...)
|
||||
}
|
||||
|
||||
result = RemoveBOMIfPresent(result)
|
||||
result = MaybeRemoveBOM(result, opts)
|
||||
|
||||
return string(result), err
|
||||
}
|
||||
|
||||
// ToUTF8WithFallback detects the encoding of content and converts to UTF-8 if possible
|
||||
func ToUTF8WithFallback(content []byte) []byte {
|
||||
bs, _ := io.ReadAll(ToUTF8WithFallbackReader(bytes.NewReader(content)))
|
||||
func ToUTF8WithFallback(content []byte, opts ConvertOpts) []byte {
|
||||
bs, _ := io.ReadAll(ToUTF8WithFallbackReader(bytes.NewReader(content), opts))
|
||||
return bs
|
||||
}
|
||||
|
||||
// ToUTF8 converts content to UTF8 encoding and ignore error
|
||||
func ToUTF8(content string) string {
|
||||
res, _ := ToUTF8WithErr([]byte(content))
|
||||
return res
|
||||
}
|
||||
|
||||
// ToUTF8DropErrors makes sure the return string is valid utf-8; attempts conversion if possible
|
||||
func ToUTF8DropErrors(content []byte) []byte {
|
||||
func ToUTF8DropErrors(content []byte, opts ConvertOpts) []byte {
|
||||
charsetLabel, err := DetectEncoding(content)
|
||||
if err != nil || charsetLabel == "UTF-8" {
|
||||
return RemoveBOMIfPresent(content)
|
||||
return MaybeRemoveBOM(content, opts)
|
||||
}
|
||||
|
||||
encoding, _ := charset.Lookup(charsetLabel)
|
||||
@@ -117,11 +115,14 @@ func ToUTF8DropErrors(content []byte) []byte {
|
||||
}
|
||||
}
|
||||
|
||||
return RemoveBOMIfPresent(decoded)
|
||||
return MaybeRemoveBOM(decoded, opts)
|
||||
}
|
||||
|
||||
// RemoveBOMIfPresent removes a UTF-8 BOM from a []byte
|
||||
func RemoveBOMIfPresent(content []byte) []byte {
|
||||
// MaybeRemoveBOM removes a UTF-8 BOM from a []byte when opts.KeepBOM is false
|
||||
func MaybeRemoveBOM(content []byte, opts ConvertOpts) []byte {
|
||||
if opts.KeepBOM {
|
||||
return content
|
||||
}
|
||||
if len(content) > 2 && bytes.Equal(content[0:3], UTF8BOM) {
|
||||
return content[3:]
|
||||
}
|
||||
|
||||
@@ -30,15 +30,15 @@ func resetDefaultCharsetsOrder() {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveBOMIfPresent(t *testing.T) {
|
||||
res := RemoveBOMIfPresent([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba})
|
||||
func TestMaybeRemoveBOM(t *testing.T) {
|
||||
res := MaybeRemoveBOM([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
|
||||
|
||||
res = RemoveBOMIfPresent([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba})
|
||||
res = MaybeRemoveBOM([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
|
||||
}
|
||||
|
||||
func TestToUTF8WithErr(t *testing.T) {
|
||||
func TestToUTF8(t *testing.T) {
|
||||
resetDefaultCharsetsOrder()
|
||||
var res string
|
||||
var err error
|
||||
@@ -47,53 +47,53 @@ func TestToUTF8WithErr(t *testing.T) {
|
||||
// locale, so some conversions might behave differently. For that reason, we don't
|
||||
// depend on particular conversions but in expected behaviors.
|
||||
|
||||
res, err = ToUTF8WithErr([]byte{0x41, 0x42, 0x43})
|
||||
res, err = ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ABC", res)
|
||||
|
||||
// "áéíóú"
|
||||
res, err = ToUTF8WithErr([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba})
|
||||
res, err = ToUTF8([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res))
|
||||
|
||||
// "áéíóú"
|
||||
res, err = ToUTF8WithErr([]byte{
|
||||
res, err = ToUTF8([]byte{
|
||||
0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3,
|
||||
0xc3, 0xba,
|
||||
})
|
||||
}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res))
|
||||
|
||||
res, err = ToUTF8WithErr([]byte{
|
||||
res, err = ToUTF8([]byte{
|
||||
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
|
||||
0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73, 0x41, 0x41, 0x41, 0x2e,
|
||||
})
|
||||
}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
stringMustStartWith(t, "Hola,", res)
|
||||
stringMustEndWith(t, "AAA.", res)
|
||||
|
||||
res, err = ToUTF8WithErr([]byte{
|
||||
res, err = ToUTF8([]byte{
|
||||
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
|
||||
0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73, 0x41, 0x41, 0x41, 0x2e,
|
||||
})
|
||||
}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
stringMustStartWith(t, "Hola,", res)
|
||||
stringMustEndWith(t, "AAA.", res)
|
||||
|
||||
res, err = ToUTF8WithErr([]byte{
|
||||
res, err = ToUTF8([]byte{
|
||||
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
|
||||
0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73, 0x41, 0x41, 0x41, 0x2e,
|
||||
})
|
||||
}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
stringMustStartWith(t, "Hola,", res)
|
||||
stringMustEndWith(t, "AAA.", res)
|
||||
|
||||
// Japanese (Shift-JIS)
|
||||
// 日属秘ぞしちゅ。
|
||||
res, err = ToUTF8WithErr([]byte{
|
||||
res, err = ToUTF8([]byte{
|
||||
0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82,
|
||||
0xBF, 0x82, 0xE3, 0x81, 0x42,
|
||||
})
|
||||
}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte{
|
||||
0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3,
|
||||
@@ -101,7 +101,7 @@ func TestToUTF8WithErr(t *testing.T) {
|
||||
},
|
||||
[]byte(res))
|
||||
|
||||
res, err = ToUTF8WithErr([]byte{0x00, 0x00, 0x00, 0x00})
|
||||
res, err = ToUTF8([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, []byte(res))
|
||||
}
|
||||
@@ -109,22 +109,22 @@ func TestToUTF8WithErr(t *testing.T) {
|
||||
func TestToUTF8WithFallback(t *testing.T) {
|
||||
resetDefaultCharsetsOrder()
|
||||
// "ABC"
|
||||
res := ToUTF8WithFallback([]byte{0x41, 0x42, 0x43})
|
||||
res := ToUTF8WithFallback([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0x41, 0x42, 0x43}, res)
|
||||
|
||||
// "áéíóú"
|
||||
res = ToUTF8WithFallback([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba})
|
||||
res = ToUTF8WithFallback([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
|
||||
|
||||
// UTF8 BOM + "áéíóú"
|
||||
res = ToUTF8WithFallback([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba})
|
||||
res = ToUTF8WithFallback([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
|
||||
|
||||
// "Hola, así cómo ños"
|
||||
res = ToUTF8WithFallback([]byte{
|
||||
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
|
||||
0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73,
|
||||
})
|
||||
}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{
|
||||
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xC3, 0xAD, 0x20, 0x63,
|
||||
0xC3, 0xB3, 0x6D, 0x6F, 0x20, 0xC3, 0xB1, 0x6F, 0x73,
|
||||
@@ -133,126 +133,65 @@ func TestToUTF8WithFallback(t *testing.T) {
|
||||
// "Hola, así cómo "
|
||||
minmatch := []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xC3, 0xAD, 0x20, 0x63, 0xC3, 0xB3, 0x6D, 0x6F, 0x20}
|
||||
|
||||
res = ToUTF8WithFallback([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73})
|
||||
res = ToUTF8WithFallback([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73}, ConvertOpts{})
|
||||
// Do not fail for differences in invalid cases, as the library might change the conversion criteria for those
|
||||
assert.Equal(t, minmatch, res[0:len(minmatch)])
|
||||
|
||||
res = ToUTF8WithFallback([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73})
|
||||
res = ToUTF8WithFallback([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73}, ConvertOpts{})
|
||||
// Do not fail for differences in invalid cases, as the library might change the conversion criteria for those
|
||||
assert.Equal(t, minmatch, res[0:len(minmatch)])
|
||||
|
||||
// Japanese (Shift-JIS)
|
||||
// "日属秘ぞしちゅ。"
|
||||
res = ToUTF8WithFallback([]byte{0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82, 0xBF, 0x82, 0xE3, 0x81, 0x42})
|
||||
res = ToUTF8WithFallback([]byte{0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82, 0xBF, 0x82, 0xE3, 0x81, 0x42}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{
|
||||
0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3,
|
||||
0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82,
|
||||
}, res)
|
||||
|
||||
res = ToUTF8WithFallback([]byte{0x00, 0x00, 0x00, 0x00})
|
||||
res = ToUTF8WithFallback([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res)
|
||||
}
|
||||
|
||||
func TestToUTF8(t *testing.T) {
|
||||
resetDefaultCharsetsOrder()
|
||||
// Note: golang compiler seems so behave differently depending on the current
|
||||
// locale, so some conversions might behave differently. For that reason, we don't
|
||||
// depend on particular conversions but in expected behaviors.
|
||||
|
||||
res := ToUTF8(string([]byte{0x41, 0x42, 0x43}))
|
||||
assert.Equal(t, "ABC", res)
|
||||
|
||||
// "áéíóú"
|
||||
res = ToUTF8(string([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}))
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res))
|
||||
|
||||
// BOM + "áéíóú"
|
||||
res = ToUTF8(string([]byte{
|
||||
0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3,
|
||||
0xc3, 0xba,
|
||||
}))
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res))
|
||||
|
||||
// Latin1
|
||||
// Hola, así cómo ños
|
||||
res = ToUTF8(string([]byte{
|
||||
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
|
||||
0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73,
|
||||
}))
|
||||
assert.Equal(t, []byte{
|
||||
0x48, 0x6f, 0x6c, 0x61, 0x2c, 0x20, 0x61, 0x73, 0xc3, 0xad, 0x20, 0x63,
|
||||
0xc3, 0xb3, 0x6d, 0x6f, 0x20, 0xc3, 0xb1, 0x6f, 0x73,
|
||||
}, []byte(res))
|
||||
|
||||
// Latin1
|
||||
// Hola, así cómo \x07ños
|
||||
res = ToUTF8(string([]byte{
|
||||
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
|
||||
0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73,
|
||||
}))
|
||||
// Hola,
|
||||
bytesMustStartWith(t, []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C}, []byte(res))
|
||||
|
||||
// This test FAILS
|
||||
// res = ToUTF8("Hola, así cómo \x81ños")
|
||||
// Do not fail for differences in invalid cases, as the library might change the conversion criteria for those
|
||||
// assert.Regexp(t, "^Hola, así cómo", res)
|
||||
|
||||
// Japanese (Shift-JIS)
|
||||
// 日属秘ぞしちゅ。
|
||||
res = ToUTF8(string([]byte{
|
||||
0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82,
|
||||
0xBF, 0x82, 0xE3, 0x81, 0x42,
|
||||
}))
|
||||
assert.Equal(t, []byte{
|
||||
0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3,
|
||||
0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82,
|
||||
},
|
||||
[]byte(res))
|
||||
|
||||
res = ToUTF8("\x00\x00\x00\x00")
|
||||
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, []byte(res))
|
||||
}
|
||||
|
||||
func TestToUTF8DropErrors(t *testing.T) {
|
||||
resetDefaultCharsetsOrder()
|
||||
// "ABC"
|
||||
res := ToUTF8DropErrors([]byte{0x41, 0x42, 0x43})
|
||||
res := ToUTF8DropErrors([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0x41, 0x42, 0x43}, res)
|
||||
|
||||
// "áéíóú"
|
||||
res = ToUTF8DropErrors([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba})
|
||||
res = ToUTF8DropErrors([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
|
||||
|
||||
// UTF8 BOM + "áéíóú"
|
||||
res = ToUTF8DropErrors([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba})
|
||||
res = ToUTF8DropErrors([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
|
||||
|
||||
// "Hola, así cómo ños"
|
||||
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73})
|
||||
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73}, res[:8])
|
||||
assert.Equal(t, []byte{0x73}, res[len(res)-1:])
|
||||
|
||||
// "Hola, así cómo "
|
||||
minmatch := []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xC3, 0xAD, 0x20, 0x63, 0xC3, 0xB3, 0x6D, 0x6F, 0x20}
|
||||
|
||||
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73})
|
||||
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73}, ConvertOpts{})
|
||||
// Do not fail for differences in invalid cases, as the library might change the conversion criteria for those
|
||||
assert.Equal(t, minmatch, res[0:len(minmatch)])
|
||||
|
||||
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73})
|
||||
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73}, ConvertOpts{})
|
||||
// Do not fail for differences in invalid cases, as the library might change the conversion criteria for those
|
||||
assert.Equal(t, minmatch, res[0:len(minmatch)])
|
||||
|
||||
// Japanese (Shift-JIS)
|
||||
// "日属秘ぞしちゅ。"
|
||||
res = ToUTF8DropErrors([]byte{0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82, 0xBF, 0x82, 0xE3, 0x81, 0x42})
|
||||
res = ToUTF8DropErrors([]byte{0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82, 0xBF, 0x82, 0xE3, 0x81, 0x42}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{
|
||||
0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3,
|
||||
0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82,
|
||||
}, res)
|
||||
|
||||
res = ToUTF8DropErrors([]byte{0x00, 0x00, 0x00, 0x00})
|
||||
res = ToUTF8DropErrors([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{})
|
||||
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res)
|
||||
}
|
||||
|
||||
@@ -302,10 +241,6 @@ func stringMustEndWith(t *testing.T, expected, value string) {
|
||||
assert.Equal(t, expected, value[len(value)-len(expected):])
|
||||
}
|
||||
|
||||
func bytesMustStartWith(t *testing.T, expected, value []byte) {
|
||||
assert.Equal(t, expected, value[:len(expected)])
|
||||
}
|
||||
|
||||
func TestToUTF8WithFallbackReader(t *testing.T) {
|
||||
resetDefaultCharsetsOrder()
|
||||
|
||||
@@ -317,7 +252,7 @@ func TestToUTF8WithFallbackReader(t *testing.T) {
|
||||
}
|
||||
input = input[:testLen]
|
||||
input += "// Выключаем"
|
||||
rd := ToUTF8WithFallbackReader(bytes.NewReader([]byte(input)))
|
||||
rd := ToUTF8WithFallbackReader(bytes.NewReader([]byte(input)), ConvertOpts{})
|
||||
r, _ := io.ReadAll(rd)
|
||||
assert.EqualValuesf(t, input, string(r), "testing string len=%d", testLen)
|
||||
}
|
||||
|
||||
+10
-49
@@ -8,11 +8,12 @@
|
||||
package charset
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
)
|
||||
|
||||
@@ -20,20 +21,18 @@ import (
|
||||
const RuneNBSP = 0xa0
|
||||
|
||||
// EscapeControlHTML escapes the unicode control sequences in a provided html document
|
||||
func EscapeControlHTML(text string, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output string) {
|
||||
func EscapeControlHTML(html template.HTML, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output template.HTML) {
|
||||
sb := &strings.Builder{}
|
||||
outputStream := &HTMLStreamerWriter{Writer: sb}
|
||||
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
||||
|
||||
if err := StreamHTML(strings.NewReader(text), streamer); err != nil {
|
||||
streamer.escaped.HasError = true
|
||||
log.Error("Error whilst escaping: %v", err)
|
||||
}
|
||||
return streamer.escaped, sb.String()
|
||||
escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, allowed...) // err has been handled in EscapeControlReader
|
||||
return escaped, template.HTML(sb.String())
|
||||
}
|
||||
|
||||
// EscapeControlReaders escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte
|
||||
// EscapeControlReader escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus
|
||||
func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
|
||||
if !setting.UI.AmbiguousUnicodeDetection {
|
||||
_, err = io.Copy(writer, reader)
|
||||
return &EscapeStatus{}, err
|
||||
}
|
||||
outputStream := &HTMLStreamerWriter{Writer: writer}
|
||||
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
||||
|
||||
@@ -43,41 +42,3 @@ func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.
|
||||
}
|
||||
return streamer.escaped, err
|
||||
}
|
||||
|
||||
// EscapeControlStringReader escapes the unicode control sequences in a provided reader of string content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte. HTML line breaks are not inserted after every newline by this method.
|
||||
func EscapeControlStringReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
|
||||
bufRd := bufio.NewReader(reader)
|
||||
outputStream := &HTMLStreamerWriter{Writer: writer}
|
||||
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
||||
|
||||
for {
|
||||
line, rdErr := bufRd.ReadString('\n')
|
||||
if len(line) > 0 {
|
||||
if err := streamer.Text(line); err != nil {
|
||||
streamer.escaped.HasError = true
|
||||
log.Error("Error whilst escaping: %v", err)
|
||||
return streamer.escaped, err
|
||||
}
|
||||
}
|
||||
if rdErr != nil {
|
||||
if rdErr != io.EOF {
|
||||
err = rdErr
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return streamer.escaped, err
|
||||
}
|
||||
|
||||
// EscapeControlString escapes the unicode control sequences in a provided string and returns the findings as an EscapeStatus and the escaped string
|
||||
func EscapeControlString(text string, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output string) {
|
||||
sb := &strings.Builder{}
|
||||
outputStream := &HTMLStreamerWriter{Writer: sb}
|
||||
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
||||
|
||||
if err := streamer.Text(text); err != nil {
|
||||
streamer.escaped.HasError = true
|
||||
log.Error("Error whilst escaping: %v", err)
|
||||
}
|
||||
return streamer.escaped, sb.String()
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func (e *escapeStreamer) Text(data string) error {
|
||||
until, next = nextIdxs[0]+pos, nextIdxs[1]+pos
|
||||
}
|
||||
|
||||
// from pos until until we know that the runes are not \r\t\n or even ' '
|
||||
// from pos until we know that the runes are not \r\t\n or even ' '
|
||||
runes := make([]rune, 0, next-until)
|
||||
positions := make([]int, 0, next-until+1)
|
||||
|
||||
@@ -173,7 +173,7 @@ func (e *escapeStreamer) ambiguousRune(r, c rune) error {
|
||||
Val: "ambiguous-code-point",
|
||||
}, html.Attribute{
|
||||
Key: "data-tooltip-content",
|
||||
Val: e.locale.Tr("repo.ambiguous_character", r, c),
|
||||
Val: e.locale.TrString("repo.ambiguous_character", r, c),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
package charset
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type escapeControlTest struct {
|
||||
@@ -132,22 +136,8 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestEscapeControlString(t *testing.T) {
|
||||
for _, tt := range escapeControlTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
status, result := EscapeControlString(tt.text, &translation.MockLocale{})
|
||||
if !reflect.DeepEqual(*status, tt.status) {
|
||||
t.Errorf("EscapeControlString() status = %v, wanted= %v", status, tt.status)
|
||||
}
|
||||
if result != tt.result {
|
||||
t.Errorf("EscapeControlString()\nresult= %v,\nwanted= %v", result, tt.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeControlReader(t *testing.T) {
|
||||
// lets add some control characters to the tests
|
||||
// add some control characters to the tests
|
||||
tests := make([]escapeControlTest, 0, len(escapeControlTests)*3)
|
||||
copy(tests, escapeControlTests)
|
||||
|
||||
@@ -167,31 +157,25 @@ func TestEscapeControlReader(t *testing.T) {
|
||||
tests = append(tests, test)
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`repo.ambiguous_character:\d+,\d+`) // simplify the output for the tests, remove the translation variants
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := strings.NewReader(tt.text)
|
||||
output := &strings.Builder{}
|
||||
status, err := EscapeControlReader(input, output, &translation.MockLocale{})
|
||||
result := output.String()
|
||||
if err != nil {
|
||||
t.Errorf("EscapeControlReader(): err = %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(*status, tt.status) {
|
||||
t.Errorf("EscapeControlReader() status = %v, wanted= %v", status, tt.status)
|
||||
}
|
||||
if result != tt.result {
|
||||
t.Errorf("EscapeControlReader()\nresult= %v,\nwanted= %v", result, tt.result)
|
||||
}
|
||||
status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.status, *status)
|
||||
outStr := output.String()
|
||||
outStr = re.ReplaceAllString(outStr, "repo.ambiguous_character")
|
||||
assert.Equal(t, tt.result, outStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeControlReader_panic(t *testing.T) {
|
||||
bs := make([]byte, 0, 20479)
|
||||
bs = append(bs, 'A')
|
||||
for i := 0; i < 6826; i++ {
|
||||
bs = append(bs, []byte("—")...)
|
||||
}
|
||||
_, _ = EscapeControlString(string(bs), &translation.MockLocale{})
|
||||
func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
|
||||
_, out := EscapeControlHTML("a test", &translation.MockLocale{})
|
||||
assert.EqualValues(t, `a<span class="escaped-code-point" data-escaped="[U+00A0]"><span class="char"> </span></span>test`, out)
|
||||
setting.UI.AmbiguousUnicodeDetection = false
|
||||
_, out = EscapeControlHTML("a test", &translation.MockLocale{})
|
||||
assert.EqualValues(t, `a test`, out)
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
type routerLoggerOptions struct {
|
||||
req *http.Request
|
||||
Identity *string
|
||||
Start *time.Time
|
||||
ResponseWriter http.ResponseWriter
|
||||
Ctx map[string]any
|
||||
RequestID *string
|
||||
}
|
||||
|
||||
const keyOfRequestIDInTemplate = ".RequestID"
|
||||
|
||||
// According to:
|
||||
// TraceId: A valid trace identifier is a 16-byte array with at least one non-zero byte
|
||||
// MD5 output is 16 or 32 bytes: md5-bytes is 16, md5-hex is 32
|
||||
// SHA1: similar, SHA1-bytes is 20, SHA1-hex is 40.
|
||||
// UUID is 128-bit, 32 hex chars, 36 ASCII chars with 4 dashes
|
||||
// So, we accept a Request ID with a maximum character length of 40
|
||||
const maxRequestIDByteLength = 40
|
||||
|
||||
func parseRequestIDFromRequestHeader(req *http.Request) string {
|
||||
requestID := "-"
|
||||
for _, key := range setting.Log.RequestIDHeaders {
|
||||
if req.Header.Get(key) != "" {
|
||||
requestID = req.Header.Get(key)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(requestID) > maxRequestIDByteLength {
|
||||
requestID = fmt.Sprintf("%s...", requestID[:maxRequestIDByteLength])
|
||||
}
|
||||
return requestID
|
||||
}
|
||||
|
||||
// AccessLogger returns a middleware to log access logger
|
||||
func AccessLogger() func(http.Handler) http.Handler {
|
||||
logger := log.GetLogger("access")
|
||||
needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate)
|
||||
logTemplate, _ := template.New("log").Parse(setting.Log.AccessLogTemplate)
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
var requestID string
|
||||
if needRequestID {
|
||||
requestID = parseRequestIDFromRequestHeader(req)
|
||||
}
|
||||
|
||||
reqHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
reqHost = req.RemoteAddr
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, req)
|
||||
rw := w.(ResponseWriter)
|
||||
|
||||
identity := "-"
|
||||
data := middleware.GetContextData(req.Context())
|
||||
if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
|
||||
identity = signedUser.Name
|
||||
}
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err = logTemplate.Execute(buf, routerLoggerOptions{
|
||||
req: req,
|
||||
Identity: &identity,
|
||||
Start: &start,
|
||||
ResponseWriter: rw,
|
||||
Ctx: map[string]any{
|
||||
"RemoteAddr": req.RemoteAddr,
|
||||
"RemoteHost": reqHost,
|
||||
"Req": req,
|
||||
},
|
||||
RequestID: &requestID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("Could not execute access logger template: %v", err.Error())
|
||||
}
|
||||
|
||||
logger.Info("%s", buf.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
mc "code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
web_types "code.gitea.io/gitea/modules/web/types"
|
||||
|
||||
"gitea.com/go-chi/cache"
|
||||
)
|
||||
|
||||
// APIContext is a specific context for API service
|
||||
type APIContext struct {
|
||||
*Base
|
||||
|
||||
Cache cache.Cache
|
||||
|
||||
Doer *user_model.User // current signed-in user
|
||||
IsSigned bool
|
||||
IsBasicAuth bool
|
||||
|
||||
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
|
||||
|
||||
Repo *Repository
|
||||
Org *APIOrganization
|
||||
Package *Package
|
||||
}
|
||||
|
||||
func init() {
|
||||
web.RegisterResponseStatusProvider[*APIContext](func(req *http.Request) web_types.ResponseStatusProvider {
|
||||
return req.Context().Value(apiContextKey).(*APIContext)
|
||||
})
|
||||
}
|
||||
|
||||
// Currently, we have the following common fields in error response:
|
||||
// * message: the message for end users (it shouldn't be used for error type detection)
|
||||
// if we need to indicate some errors, we should introduce some new fields like ErrorCode or ErrorType
|
||||
// * url: the swagger document URL
|
||||
|
||||
// APIError is error format response
|
||||
// swagger:response error
|
||||
type APIError struct {
|
||||
Message string `json:"message"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// APIValidationError is error format response related to input validation
|
||||
// swagger:response validationError
|
||||
type APIValidationError struct {
|
||||
Message string `json:"message"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// APIInvalidTopicsError is error format response to invalid topics
|
||||
// swagger:response invalidTopicsError
|
||||
type APIInvalidTopicsError struct {
|
||||
Message string `json:"message"`
|
||||
InvalidTopics []string `json:"invalidTopics"`
|
||||
}
|
||||
|
||||
// APIEmpty is an empty response
|
||||
// swagger:response empty
|
||||
type APIEmpty struct{}
|
||||
|
||||
// APIForbiddenError is a forbidden error response
|
||||
// swagger:response forbidden
|
||||
type APIForbiddenError struct {
|
||||
APIError
|
||||
}
|
||||
|
||||
// APINotFound is a not found empty response
|
||||
// swagger:response notFound
|
||||
type APINotFound struct{}
|
||||
|
||||
// APIConflict is a conflict empty response
|
||||
// swagger:response conflict
|
||||
type APIConflict struct{}
|
||||
|
||||
// APIRedirect is a redirect response
|
||||
// swagger:response redirect
|
||||
type APIRedirect struct{}
|
||||
|
||||
// APIString is a string response
|
||||
// swagger:response string
|
||||
type APIString string
|
||||
|
||||
// APIRepoArchivedError is an error that is raised when an archived repo should be modified
|
||||
// swagger:response repoArchivedError
|
||||
type APIRepoArchivedError struct {
|
||||
APIError
|
||||
}
|
||||
|
||||
// ServerError responds with error message, status is 500
|
||||
func (ctx *APIContext) ServerError(title string, err error) {
|
||||
ctx.Error(http.StatusInternalServerError, title, err)
|
||||
}
|
||||
|
||||
// Error responds with an error message to client with given obj as the message.
|
||||
// If status is 500, also it prints error to log.
|
||||
func (ctx *APIContext) Error(status int, title string, obj any) {
|
||||
var message string
|
||||
if err, ok := obj.(error); ok {
|
||||
message = err.Error()
|
||||
} else {
|
||||
message = fmt.Sprintf("%s", obj)
|
||||
}
|
||||
|
||||
if status == http.StatusInternalServerError {
|
||||
log.ErrorWithSkip(1, "%s: %s", title, message)
|
||||
|
||||
if setting.IsProd && !(ctx.Doer != nil && ctx.Doer.IsAdmin) {
|
||||
message = ""
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(status, APIError{
|
||||
Message: message,
|
||||
URL: setting.API.SwaggerURL,
|
||||
})
|
||||
}
|
||||
|
||||
// InternalServerError responds with an error message to the client with the error as a message
|
||||
// and the file and line of the caller.
|
||||
func (ctx *APIContext) InternalServerError(err error) {
|
||||
log.ErrorWithSkip(1, "InternalServerError: %v", err)
|
||||
|
||||
var message string
|
||||
if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
|
||||
message = err.Error()
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusInternalServerError, APIError{
|
||||
Message: message,
|
||||
URL: setting.API.SwaggerURL,
|
||||
})
|
||||
}
|
||||
|
||||
type apiContextKeyType struct{}
|
||||
|
||||
var apiContextKey = apiContextKeyType{}
|
||||
|
||||
// GetAPIContext returns a context for API routes
|
||||
func GetAPIContext(req *http.Request) *APIContext {
|
||||
return req.Context().Value(apiContextKey).(*APIContext)
|
||||
}
|
||||
|
||||
func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string {
|
||||
page := NewPagination(total, pageSize, curPage, 0)
|
||||
paginater := page.Paginater
|
||||
links := make([]string, 0, 4)
|
||||
|
||||
if paginater.HasNext() {
|
||||
u := *curURL
|
||||
queries := u.Query()
|
||||
queries.Set("page", fmt.Sprintf("%d", paginater.Next()))
|
||||
u.RawQuery = queries.Encode()
|
||||
|
||||
links = append(links, fmt.Sprintf("<%s%s>; rel=\"next\"", setting.AppURL, u.RequestURI()[1:]))
|
||||
}
|
||||
if !paginater.IsLast() {
|
||||
u := *curURL
|
||||
queries := u.Query()
|
||||
queries.Set("page", fmt.Sprintf("%d", paginater.TotalPages()))
|
||||
u.RawQuery = queries.Encode()
|
||||
|
||||
links = append(links, fmt.Sprintf("<%s%s>; rel=\"last\"", setting.AppURL, u.RequestURI()[1:]))
|
||||
}
|
||||
if !paginater.IsFirst() {
|
||||
u := *curURL
|
||||
queries := u.Query()
|
||||
queries.Set("page", "1")
|
||||
u.RawQuery = queries.Encode()
|
||||
|
||||
links = append(links, fmt.Sprintf("<%s%s>; rel=\"first\"", setting.AppURL, u.RequestURI()[1:]))
|
||||
}
|
||||
if paginater.HasPrevious() {
|
||||
u := *curURL
|
||||
queries := u.Query()
|
||||
queries.Set("page", fmt.Sprintf("%d", paginater.Previous()))
|
||||
u.RawQuery = queries.Encode()
|
||||
|
||||
links = append(links, fmt.Sprintf("<%s%s>; rel=\"prev\"", setting.AppURL, u.RequestURI()[1:]))
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
// SetLinkHeader sets pagination link header by given total number and page size.
|
||||
func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
|
||||
links := genAPILinks(ctx.Req.URL, total, pageSize, ctx.FormInt("page"))
|
||||
|
||||
if len(links) > 0 {
|
||||
ctx.RespHeader().Set("Link", strings.Join(links, ","))
|
||||
ctx.AppendAccessControlExposeHeaders("Link")
|
||||
}
|
||||
}
|
||||
|
||||
// CheckForOTP validates OTP
|
||||
func (ctx *APIContext) CheckForOTP() {
|
||||
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
|
||||
return // Skip 2FA
|
||||
}
|
||||
|
||||
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
|
||||
twofa, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
if auth.IsErrTwoFactorNotEnrolled(err) {
|
||||
return // No 2FA enrollment for this user
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err)
|
||||
return
|
||||
}
|
||||
ok, err := twofa.ValidateTOTP(otpHeader)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
ctx.Error(http.StatusUnauthorized, "", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// APIContexter returns apicontext as middleware
|
||||
func APIContexter() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
base, baseCleanUp := NewBaseContext(w, req)
|
||||
ctx := &APIContext{
|
||||
Base: base,
|
||||
Cache: mc.GetCache(),
|
||||
Repo: &Repository{PullRequest: &PullRequest{}},
|
||||
Org: &APIOrganization{},
|
||||
}
|
||||
defer baseCleanUp()
|
||||
|
||||
ctx.Base.AppendContextValue(apiContextKey, ctx)
|
||||
ctx.Base.AppendContextValueFunc(git.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
|
||||
|
||||
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
|
||||
if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
|
||||
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||
|
||||
next.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound handles 404s for APIContext
|
||||
// String will replace message, errors will be added to a slice
|
||||
func (ctx *APIContext) NotFound(objs ...any) {
|
||||
message := ctx.Tr("error.not_found")
|
||||
var errors []string
|
||||
for _, obj := range objs {
|
||||
// Ignore nil
|
||||
if obj == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err, ok := obj.(error); ok {
|
||||
errors = append(errors, err.Error())
|
||||
} else {
|
||||
message = obj.(string)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusNotFound, map[string]any{
|
||||
"message": message,
|
||||
"url": setting.API.SwaggerURL,
|
||||
"errors": errors,
|
||||
})
|
||||
}
|
||||
|
||||
// ReferencesGitRepo injects the GitRepo into the Context
|
||||
// you can optional skip the IsEmpty check
|
||||
func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context.CancelFunc) {
|
||||
return func(ctx *APIContext) (cancel context.CancelFunc) {
|
||||
// Empty repository does not have reference information.
|
||||
if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For API calls.
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
repoPath := repo_model.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
|
||||
gitRepo, err := git.OpenRepository(ctx, repoPath)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RepoRef Invalid repo "+repoPath, err)
|
||||
return cancel
|
||||
}
|
||||
ctx.Repo.GitRepo = gitRepo
|
||||
// We opened it, we should close it
|
||||
return func() {
|
||||
// If it's been set to nil then assume someone else has closed it.
|
||||
if ctx.Repo.GitRepo != nil {
|
||||
_ = ctx.Repo.GitRepo.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cancel
|
||||
}
|
||||
}
|
||||
|
||||
// RepoRefForAPI handles repository reference names when the ref name is not explicitly given
|
||||
func RepoRefForAPI(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := GetAPIContext(req)
|
||||
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
ctx.InternalServerError(fmt.Errorf("no open git repo"))
|
||||
return
|
||||
}
|
||||
|
||||
if ref := ctx.FormTrim("ref"); len(ref) > 0 {
|
||||
commit, err := ctx.Repo.GitRepo.GetCommit(ref)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Repo.Commit = commit
|
||||
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
||||
ctx.Repo.TreePath = ctx.Params("*")
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
|
||||
|
||||
if ctx.Repo.GitRepo.IsBranchExist(refName) {
|
||||
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
||||
} else if ctx.Repo.GitRepo.IsTagExist(refName) {
|
||||
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refName)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
||||
} else if len(refName) == git.SHAFullLength {
|
||||
ctx.Repo.CommitID = refName
|
||||
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
|
||||
if err != nil {
|
||||
ctx.NotFound("GetCommit", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ctx.NotFound(fmt.Errorf("not exist: '%s'", ctx.Params("*")))
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
// HasAPIError returns true if error occurs in form validation.
|
||||
func (ctx *APIContext) HasAPIError() bool {
|
||||
hasErr, ok := ctx.Data["HasError"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hasErr.(bool)
|
||||
}
|
||||
|
||||
// GetErrMsg returns error message in form validation.
|
||||
func (ctx *APIContext) GetErrMsg() string {
|
||||
msg, _ := ctx.Data["ErrorMsg"].(string)
|
||||
if msg == "" {
|
||||
msg = "invalid form data"
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// NotFoundOrServerError use error check function to determine if the error
|
||||
// is about not found. It responds with 404 status code for not found error,
|
||||
// or error context description for logging purpose of 500 server error.
|
||||
func (ctx *APIContext) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
|
||||
if errCheck(logErr) {
|
||||
ctx.JSON(http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "NotFoundOrServerError", logMsg)
|
||||
}
|
||||
|
||||
// IsUserSiteAdmin returns true if current user is a site admin
|
||||
func (ctx *APIContext) IsUserSiteAdmin() bool {
|
||||
return ctx.IsSigned && ctx.Doer.IsAdmin
|
||||
}
|
||||
|
||||
// IsUserRepoAdmin returns true if current user is admin in current repo
|
||||
func (ctx *APIContext) IsUserRepoAdmin() bool {
|
||||
return ctx.Repo.IsAdmin()
|
||||
}
|
||||
|
||||
// IsUserRepoWriter returns true if current user has write privilege in current repo
|
||||
func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool {
|
||||
for _, unitType := range unitTypes {
|
||||
if ctx.Repo.CanWrite(unitType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import "code.gitea.io/gitea/models/organization"
|
||||
|
||||
// APIOrganization contains organization and team
|
||||
type APIOrganization struct {
|
||||
Organization *organization.Organization
|
||||
Team *organization.Team
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenAPILinks(t *testing.T) {
|
||||
setting.AppURL = "http://localhost:3000/"
|
||||
kases := map[string][]string{
|
||||
"api/v1/repos/jerrykan/example-repo/issues?state=all": {
|
||||
`<http://localhost:3000/api/v1/repos/jerrykan/example-repo/issues?page=2&state=all>; rel="next"`,
|
||||
`<http://localhost:3000/api/v1/repos/jerrykan/example-repo/issues?page=5&state=all>; rel="last"`,
|
||||
},
|
||||
"api/v1/repos/jerrykan/example-repo/issues?state=all&page=1": {
|
||||
`<http://localhost:3000/api/v1/repos/jerrykan/example-repo/issues?page=2&state=all>; rel="next"`,
|
||||
`<http://localhost:3000/api/v1/repos/jerrykan/example-repo/issues?page=5&state=all>; rel="last"`,
|
||||
},
|
||||
"api/v1/repos/jerrykan/example-repo/issues?state=all&page=2": {
|
||||
`<http://localhost:3000/api/v1/repos/jerrykan/example-repo/issues?page=3&state=all>; rel="next"`,
|
||||
`<http://localhost:3000/api/v1/repos/jerrykan/example-repo/issues?page=5&state=all>; rel="last"`,
|
||||
`<http://localhost:3000/api/v1/repos/jerrykan/example-repo/issues?page=1&state=all>; rel="first"`,
|
||||
`<http://localhost:3000/api/v1/repos/jerrykan/example-repo/issues?page=1&state=all>; rel="prev"`,
|
||||
},
|
||||
"api/v1/repos/jerrykan/example-repo/issues?state=all&page=5": {
|
||||
`<http://localhost:3000/api/v1/repos/jerrykan/example-repo/issues?page=1&state=all>; rel="first"`,
|
||||
`<http://localhost:3000/api/v1/repos/jerrykan/example-repo/issues?page=4&state=all>; rel="prev"`,
|
||||
},
|
||||
}
|
||||
|
||||
for req, response := range kases {
|
||||
u, err := url.Parse(setting.AppURL + req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
p := u.Query().Get("page")
|
||||
curPage, _ := strconv.Atoi(p)
|
||||
|
||||
links := genAPILinks(u, 100, 20, curPage)
|
||||
|
||||
assert.EqualValues(t, links, response)
|
||||
}
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type contextValuePair struct {
|
||||
key any
|
||||
valueFn func() any
|
||||
}
|
||||
|
||||
type Base struct {
|
||||
originCtx context.Context
|
||||
contextValues []contextValuePair
|
||||
|
||||
Resp ResponseWriter
|
||||
Req *http.Request
|
||||
|
||||
// Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData.
|
||||
// Although it's mainly used for MVC templates, sometimes it's also used to pass data between middlewares/handler
|
||||
Data middleware.ContextData
|
||||
|
||||
// Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation
|
||||
Locale translation.Locale
|
||||
}
|
||||
|
||||
func (b *Base) Deadline() (deadline time.Time, ok bool) {
|
||||
return b.originCtx.Deadline()
|
||||
}
|
||||
|
||||
func (b *Base) Done() <-chan struct{} {
|
||||
return b.originCtx.Done()
|
||||
}
|
||||
|
||||
func (b *Base) Err() error {
|
||||
return b.originCtx.Err()
|
||||
}
|
||||
|
||||
func (b *Base) Value(key any) any {
|
||||
for _, pair := range b.contextValues {
|
||||
if pair.key == key {
|
||||
return pair.valueFn()
|
||||
}
|
||||
}
|
||||
return b.originCtx.Value(key)
|
||||
}
|
||||
|
||||
func (b *Base) AppendContextValueFunc(key any, valueFn func() any) any {
|
||||
b.contextValues = append(b.contextValues, contextValuePair{key, valueFn})
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Base) AppendContextValue(key, value any) any {
|
||||
b.contextValues = append(b.contextValues, contextValuePair{key, func() any { return value }})
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Base) GetData() middleware.ContextData {
|
||||
return b.Data
|
||||
}
|
||||
|
||||
// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
|
||||
func (b *Base) AppendAccessControlExposeHeaders(names ...string) {
|
||||
val := b.RespHeader().Get("Access-Control-Expose-Headers")
|
||||
if len(val) != 0 {
|
||||
b.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", ")))
|
||||
} else {
|
||||
b.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// SetTotalCountHeader set "X-Total-Count" header
|
||||
func (b *Base) SetTotalCountHeader(total int64) {
|
||||
b.RespHeader().Set("X-Total-Count", fmt.Sprint(total))
|
||||
b.AppendAccessControlExposeHeaders("X-Total-Count")
|
||||
}
|
||||
|
||||
// Written returns true if there are something sent to web browser
|
||||
func (b *Base) Written() bool {
|
||||
return b.Resp.WrittenStatus() != 0
|
||||
}
|
||||
|
||||
func (b *Base) WrittenStatus() int {
|
||||
return b.Resp.WrittenStatus()
|
||||
}
|
||||
|
||||
// Status writes status code
|
||||
func (b *Base) Status(status int) {
|
||||
b.Resp.WriteHeader(status)
|
||||
}
|
||||
|
||||
// Write writes data to web browser
|
||||
func (b *Base) Write(bs []byte) (int, error) {
|
||||
return b.Resp.Write(bs)
|
||||
}
|
||||
|
||||
// RespHeader returns the response header
|
||||
func (b *Base) RespHeader() http.Header {
|
||||
return b.Resp.Header()
|
||||
}
|
||||
|
||||
// Error returned an error to web browser
|
||||
func (b *Base) Error(status int, contents ...string) {
|
||||
v := http.StatusText(status)
|
||||
if len(contents) > 0 {
|
||||
v = contents[0]
|
||||
}
|
||||
http.Error(b.Resp, v, status)
|
||||
}
|
||||
|
||||
// JSON render content as JSON
|
||||
func (b *Base) JSON(status int, content any) {
|
||||
b.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
|
||||
b.Resp.WriteHeader(status)
|
||||
if err := json.NewEncoder(b.Resp).Encode(content); err != nil {
|
||||
log.Error("Render JSON failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoteAddr returns the client machine ip address
|
||||
func (b *Base) RemoteAddr() string {
|
||||
return b.Req.RemoteAddr
|
||||
}
|
||||
|
||||
// Params returns the param on route
|
||||
func (b *Base) Params(p string) string {
|
||||
s, _ := url.PathUnescape(chi.URLParam(b.Req, strings.TrimPrefix(p, ":")))
|
||||
return s
|
||||
}
|
||||
|
||||
func (b *Base) PathParamRaw(p string) string {
|
||||
return chi.URLParam(b.Req, strings.TrimPrefix(p, ":"))
|
||||
}
|
||||
|
||||
// ParamsInt64 returns the param on route as int64
|
||||
func (b *Base) ParamsInt64(p string) int64 {
|
||||
v, _ := strconv.ParseInt(b.Params(p), 10, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
// SetParams set params into routes
|
||||
func (b *Base) SetParams(k, v string) {
|
||||
chiCtx := chi.RouteContext(b)
|
||||
chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
|
||||
}
|
||||
|
||||
// FormString returns the first value matching the provided key in the form as a string
|
||||
func (b *Base) FormString(key string) string {
|
||||
return b.Req.FormValue(key)
|
||||
}
|
||||
|
||||
// FormStrings returns a string slice for the provided key from the form
|
||||
func (b *Base) FormStrings(key string) []string {
|
||||
if b.Req.Form == nil {
|
||||
if err := b.Req.ParseMultipartForm(32 << 20); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if v, ok := b.Req.Form[key]; ok {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormTrim returns the first value for the provided key in the form as a space trimmed string
|
||||
func (b *Base) FormTrim(key string) string {
|
||||
return strings.TrimSpace(b.Req.FormValue(key))
|
||||
}
|
||||
|
||||
// FormInt returns the first value for the provided key in the form as an int
|
||||
func (b *Base) FormInt(key string) int {
|
||||
v, _ := strconv.Atoi(b.Req.FormValue(key))
|
||||
return v
|
||||
}
|
||||
|
||||
// FormInt64 returns the first value for the provided key in the form as an int64
|
||||
func (b *Base) FormInt64(key string) int64 {
|
||||
v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
// FormBool returns true if the value for the provided key in the form is "1", "true" or "on"
|
||||
func (b *Base) FormBool(key string) bool {
|
||||
s := b.Req.FormValue(key)
|
||||
v, _ := strconv.ParseBool(s)
|
||||
v = v || strings.EqualFold(s, "on")
|
||||
return v
|
||||
}
|
||||
|
||||
// FormOptionalBool returns an OptionalBoolTrue or OptionalBoolFalse if the value
|
||||
// for the provided key exists in the form else it returns OptionalBoolNone
|
||||
func (b *Base) FormOptionalBool(key string) util.OptionalBool {
|
||||
value := b.Req.FormValue(key)
|
||||
if len(value) == 0 {
|
||||
return util.OptionalBoolNone
|
||||
}
|
||||
s := b.Req.FormValue(key)
|
||||
v, _ := strconv.ParseBool(s)
|
||||
v = v || strings.EqualFold(s, "on")
|
||||
return util.OptionalBoolOf(v)
|
||||
}
|
||||
|
||||
func (b *Base) SetFormString(key, value string) {
|
||||
_ = b.Req.FormValue(key) // force parse form
|
||||
b.Req.Form.Set(key, value)
|
||||
}
|
||||
|
||||
// PlainTextBytes renders bytes as plain text
|
||||
func (b *Base) plainTextInternal(skip, status int, bs []byte) {
|
||||
statusPrefix := status / 100
|
||||
if statusPrefix == 4 || statusPrefix == 5 {
|
||||
log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs))
|
||||
}
|
||||
b.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
||||
b.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
b.Resp.WriteHeader(status)
|
||||
if _, err := b.Resp.Write(bs); err != nil {
|
||||
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
// PlainTextBytes renders bytes as plain text
|
||||
func (b *Base) PlainTextBytes(status int, bs []byte) {
|
||||
b.plainTextInternal(2, status, bs)
|
||||
}
|
||||
|
||||
// PlainText renders content as plain text
|
||||
func (b *Base) PlainText(status int, text string) {
|
||||
b.plainTextInternal(2, status, []byte(text))
|
||||
}
|
||||
|
||||
// Redirect redirects the request
|
||||
func (b *Base) Redirect(location string, status ...int) {
|
||||
code := http.StatusSeeOther
|
||||
if len(status) == 1 {
|
||||
code = status[0]
|
||||
}
|
||||
|
||||
if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
|
||||
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
|
||||
// 1. the first request to "/my-path" contains cookie
|
||||
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
|
||||
// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser
|
||||
// 4. then the browser accepts the empty session, then the user is logged out
|
||||
// So in this case, we should remove the session cookie from the response header
|
||||
removeSessionCookieHeader(b.Resp)
|
||||
}
|
||||
http.Redirect(b.Resp, b.Req, location, code)
|
||||
}
|
||||
|
||||
type ServeHeaderOptions httplib.ServeHeaderOptions
|
||||
|
||||
func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) {
|
||||
httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opt))
|
||||
}
|
||||
|
||||
// ServeContent serves content to http request
|
||||
func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
|
||||
httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opts))
|
||||
http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r)
|
||||
}
|
||||
|
||||
// Close frees all resources hold by Context
|
||||
func (b *Base) cleanUp() {
|
||||
if b.Req != nil && b.Req.MultipartForm != nil {
|
||||
_ = b.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Base) Tr(msg string, args ...any) string {
|
||||
return b.Locale.Tr(msg, args...)
|
||||
}
|
||||
|
||||
func (b *Base) TrN(cnt any, key1, keyN string, args ...any) string {
|
||||
return b.Locale.TrN(cnt, key1, keyN, args...)
|
||||
}
|
||||
|
||||
func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, closeFunc func()) {
|
||||
b = &Base{
|
||||
originCtx: req.Context(),
|
||||
Req: req,
|
||||
Resp: WrapResponseWriter(resp),
|
||||
Locale: middleware.Locale(resp, req),
|
||||
Data: middleware.GetContextData(req.Context()),
|
||||
}
|
||||
b.AppendContextValue(translation.ContextKey, b.Locale)
|
||||
b.Req = b.Req.WithContext(b)
|
||||
return b, b.cleanUp
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/hcaptcha"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/mcaptcha"
|
||||
"code.gitea.io/gitea/modules/recaptcha"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/turnstile"
|
||||
|
||||
"gitea.com/go-chi/captcha"
|
||||
)
|
||||
|
||||
var (
|
||||
imageCaptchaOnce sync.Once
|
||||
cpt *captcha.Captcha
|
||||
)
|
||||
|
||||
// GetImageCaptcha returns global image captcha
|
||||
func GetImageCaptcha() *captcha.Captcha {
|
||||
imageCaptchaOnce.Do(func() {
|
||||
cpt = captcha.NewCaptcha(captcha.Options{
|
||||
SubURL: setting.AppSubURL,
|
||||
})
|
||||
cpt.Store = cache.GetCache()
|
||||
})
|
||||
return cpt
|
||||
}
|
||||
|
||||
// SetCaptchaData sets common captcha data
|
||||
func SetCaptchaData(ctx *Context) {
|
||||
if !setting.Service.EnableCaptcha {
|
||||
return
|
||||
}
|
||||
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
|
||||
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
|
||||
ctx.Data["Captcha"] = GetImageCaptcha()
|
||||
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
|
||||
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
|
||||
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
|
||||
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
|
||||
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
|
||||
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
|
||||
}
|
||||
|
||||
const (
|
||||
gRecaptchaResponseField = "g-recaptcha-response"
|
||||
hCaptchaResponseField = "h-captcha-response"
|
||||
mCaptchaResponseField = "m-captcha-response"
|
||||
cfTurnstileResponseField = "cf-turnstile-response"
|
||||
)
|
||||
|
||||
// VerifyCaptcha verifies Captcha data
|
||||
// No-op if captchas are not enabled
|
||||
func VerifyCaptcha(ctx *Context, tpl base.TplName, form any) {
|
||||
if !setting.Service.EnableCaptcha {
|
||||
return
|
||||
}
|
||||
|
||||
var valid bool
|
||||
var err error
|
||||
switch setting.Service.CaptchaType {
|
||||
case setting.ImageCaptcha:
|
||||
valid = GetImageCaptcha().VerifyReq(ctx.Req)
|
||||
case setting.ReCaptcha:
|
||||
valid, err = recaptcha.Verify(ctx, ctx.Req.Form.Get(gRecaptchaResponseField))
|
||||
case setting.HCaptcha:
|
||||
valid, err = hcaptcha.Verify(ctx, ctx.Req.Form.Get(hCaptchaResponseField))
|
||||
case setting.MCaptcha:
|
||||
valid, err = mcaptcha.Verify(ctx, ctx.Req.Form.Get(mCaptchaResponseField))
|
||||
case setting.CfTurnstile:
|
||||
valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField))
|
||||
default:
|
||||
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Debug("%v", err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
ctx.Data["Err_Captcha"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tpl, form)
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
mc "code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
web_types "code.gitea.io/gitea/modules/web/types"
|
||||
|
||||
"gitea.com/go-chi/cache"
|
||||
"gitea.com/go-chi/session"
|
||||
)
|
||||
|
||||
// Render represents a template render
|
||||
type Render interface {
|
||||
TemplateLookup(tmpl string, templateCtx context.Context) (templates.TemplateExecutor, error)
|
||||
HTML(w io.Writer, status int, name string, data any, templateCtx context.Context) error
|
||||
}
|
||||
|
||||
// Context represents context of a request.
|
||||
type Context struct {
|
||||
*Base
|
||||
|
||||
TemplateContext TemplateContext
|
||||
|
||||
Render Render
|
||||
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
||||
|
||||
Cache cache.Cache
|
||||
Csrf CSRFProtector
|
||||
Flash *middleware.Flash
|
||||
Session session.Store
|
||||
|
||||
Link string // current request URL (without query string)
|
||||
|
||||
Doer *user_model.User // current signed-in user
|
||||
IsSigned bool
|
||||
IsBasicAuth bool
|
||||
|
||||
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
|
||||
|
||||
Repo *Repository
|
||||
Org *Organization
|
||||
Package *Package
|
||||
}
|
||||
|
||||
type TemplateContext map[string]any
|
||||
|
||||
func init() {
|
||||
web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider {
|
||||
return req.Context().Value(WebContextKey).(*Context)
|
||||
})
|
||||
}
|
||||
|
||||
// TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString.
|
||||
// This is useful if the locale message is intended to only produce HTML content.
|
||||
func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
|
||||
trArgs := make([]any, len(args))
|
||||
for i, arg := range args {
|
||||
trArgs[i] = html.EscapeString(arg)
|
||||
}
|
||||
return ctx.Locale.Tr(msg, trArgs...)
|
||||
}
|
||||
|
||||
type webContextKeyType struct{}
|
||||
|
||||
var WebContextKey = webContextKeyType{}
|
||||
|
||||
func GetWebContext(req *http.Request) *Context {
|
||||
ctx, _ := req.Context().Value(WebContextKey).(*Context)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// ValidateContext is a special context for form validation middleware. It may be different from other contexts.
|
||||
type ValidateContext struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
// GetValidateContext gets a context for middleware form validation
|
||||
func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
|
||||
if ctxAPI, ok := req.Context().Value(apiContextKey).(*APIContext); ok {
|
||||
ctx = &ValidateContext{Base: ctxAPI.Base}
|
||||
} else if ctxWeb, ok := req.Context().Value(WebContextKey).(*Context); ok {
|
||||
ctx = &ValidateContext{Base: ctxWeb.Base}
|
||||
} else {
|
||||
panic("invalid context, expect either APIContext or Context")
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func NewTemplateContextForWeb(ctx *Context) TemplateContext {
|
||||
tmplCtx := NewTemplateContext(ctx)
|
||||
tmplCtx["Locale"] = ctx.Base.Locale
|
||||
tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx)
|
||||
return tmplCtx
|
||||
}
|
||||
|
||||
func NewWebContext(base *Base, render Render, session session.Store) *Context {
|
||||
ctx := &Context{
|
||||
Base: base,
|
||||
Render: render,
|
||||
Session: session,
|
||||
|
||||
Cache: mc.GetCache(),
|
||||
Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"),
|
||||
Repo: &Repository{PullRequest: &PullRequest{}},
|
||||
Org: &Organization{},
|
||||
}
|
||||
ctx.TemplateContext = NewTemplateContextForWeb(ctx)
|
||||
ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Contexter initializes a classic context for a request.
|
||||
func Contexter() func(next http.Handler) http.Handler {
|
||||
rnd := templates.HTMLRenderer()
|
||||
csrfOpts := CsrfOptions{
|
||||
Secret: setting.SecretKey,
|
||||
Cookie: setting.CSRFCookieName,
|
||||
SetCookie: true,
|
||||
Secure: setting.SessionConfig.Secure,
|
||||
CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
|
||||
Header: "X-Csrf-Token",
|
||||
CookieDomain: setting.SessionConfig.Domain,
|
||||
CookiePath: setting.SessionConfig.CookiePath,
|
||||
SameSite: setting.SessionConfig.SameSite,
|
||||
}
|
||||
if !setting.IsProd {
|
||||
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
base, baseCleanUp := NewBaseContext(resp, req)
|
||||
defer baseCleanUp()
|
||||
ctx := NewWebContext(base, rnd, session.GetSession(req))
|
||||
|
||||
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
|
||||
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this
|
||||
ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
|
||||
ctx.Data["Link"] = ctx.Link
|
||||
ctx.Data["locale"] = ctx.Locale
|
||||
|
||||
// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules
|
||||
ctx.PageData = map[string]any{}
|
||||
ctx.Data["PageData"] = ctx.PageData
|
||||
|
||||
ctx.Base.AppendContextValue(WebContextKey, ctx)
|
||||
ctx.Base.AppendContextValueFunc(git.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
|
||||
|
||||
ctx.Csrf = PrepareCSRFProtector(csrfOpts, ctx)
|
||||
|
||||
// Get the last flash message from cookie
|
||||
lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash)
|
||||
if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 {
|
||||
// store last Flash message into the template data, to render it
|
||||
ctx.Data["Flash"] = &middleware.Flash{
|
||||
DataStore: ctx,
|
||||
Values: vals,
|
||||
ErrorMsg: vals.Get("error"),
|
||||
SuccessMsg: vals.Get("success"),
|
||||
InfoMsg: vals.Get("info"),
|
||||
WarningMsg: vals.Get("warning"),
|
||||
}
|
||||
}
|
||||
|
||||
// if there are new messages in the ctx.Flash, write them into cookie
|
||||
ctx.Resp.Before(func(resp ResponseWriter) {
|
||||
if val := ctx.Flash.Encode(); val != "" {
|
||||
middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, val, 0)
|
||||
} else if lastFlashCookie != "" {
|
||||
middleware.SetSiteCookie(ctx.Resp, CookieNameFlash, "", -1)
|
||||
}
|
||||
})
|
||||
|
||||
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
|
||||
if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
|
||||
ctx.ServerError("ParseMultipartForm", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
|
||||
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||
|
||||
ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
|
||||
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`)
|
||||
|
||||
// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
|
||||
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
|
||||
ctx.Data["DisableStars"] = setting.Repository.DisableStars
|
||||
ctx.Data["EnableActions"] = setting.Actions.Enabled
|
||||
|
||||
ctx.Data["ManifestData"] = setting.ManifestData
|
||||
|
||||
ctx.Data["UnitWikiGlobalDisabled"] = unit.TypeWiki.UnitGlobalDisabled()
|
||||
ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled()
|
||||
ctx.Data["UnitPullsGlobalDisabled"] = unit.TypePullRequests.UnitGlobalDisabled()
|
||||
ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled()
|
||||
ctx.Data["UnitActionsGlobalDisabled"] = unit.TypeActions.UnitGlobalDisabled()
|
||||
|
||||
ctx.Data["AllLangs"] = translation.AllLangs()
|
||||
|
||||
next.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HasError returns true if error occurs in form validation.
|
||||
// Attention: this function changes ctx.Data and ctx.Flash
|
||||
func (ctx *Context) HasError() bool {
|
||||
hasErr, ok := ctx.Data["HasError"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
|
||||
ctx.Data["Flash"] = ctx.Flash
|
||||
return hasErr.(bool)
|
||||
}
|
||||
|
||||
// GetErrMsg returns error message in form validation.
|
||||
func (ctx *Context) GetErrMsg() string {
|
||||
msg, _ := ctx.Data["ErrorMsg"].(string)
|
||||
if msg == "" {
|
||||
msg = "invalid form data"
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (ctx *Context) JSONRedirect(redirect string) {
|
||||
ctx.JSON(http.StatusOK, map[string]any{"redirect": redirect})
|
||||
}
|
||||
|
||||
func (ctx *Context) JSONOK() {
|
||||
ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it
|
||||
}
|
||||
|
||||
func (ctx *Context) JSONError(msg string) {
|
||||
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg})
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
const CookieNameFlash = "gitea_flash"
|
||||
|
||||
func removeSessionCookieHeader(w http.ResponseWriter) {
|
||||
cookies := w.Header()["Set-Cookie"]
|
||||
w.Header().Del("Set-Cookie")
|
||||
for _, cookie := range cookies {
|
||||
if strings.HasPrefix(cookie, setting.SessionConfig.CookieName+"=") {
|
||||
continue
|
||||
}
|
||||
w.Header().Add("Set-Cookie", cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSiteCookie convenience function to set most cookies consistently
|
||||
// CSRF and a few others are the exception here
|
||||
func (ctx *Context) SetSiteCookie(name, value string, maxAge int) {
|
||||
middleware.SetSiteCookie(ctx.Resp, name, value, maxAge)
|
||||
}
|
||||
|
||||
// DeleteSiteCookie convenience function to delete most cookies consistently
|
||||
// CSRF and a few others are the exception here
|
||||
func (ctx *Context) DeleteSiteCookie(name string) {
|
||||
middleware.SetSiteCookie(ctx.Resp, name, "", -1)
|
||||
}
|
||||
|
||||
// GetSiteCookie returns given cookie value from request header.
|
||||
func (ctx *Context) GetSiteCookie(name string) string {
|
||||
return middleware.GetSiteCookie(ctx.Req, name)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
)
|
||||
|
||||
// IsUserSiteAdmin returns true if current user is a site admin
|
||||
func (ctx *Context) IsUserSiteAdmin() bool {
|
||||
return ctx.IsSigned && ctx.Doer.IsAdmin
|
||||
}
|
||||
|
||||
// IsUserRepoAdmin returns true if current user is admin in current repo
|
||||
func (ctx *Context) IsUserRepoAdmin() bool {
|
||||
return ctx.Repo.IsAdmin()
|
||||
}
|
||||
|
||||
// IsUserRepoWriter returns true if current user has write privilege in current repo
|
||||
func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool {
|
||||
for _, unitType := range unitTypes {
|
||||
if ctx.Repo.CanWrite(unitType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UploadStream returns the request body or the first form file
|
||||
// Only form files need to get closed.
|
||||
func (ctx *Context) UploadStream() (rd io.ReadCloser, needToClose bool, err error) {
|
||||
contentType := strings.ToLower(ctx.Req.Header.Get("Content-Type"))
|
||||
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if ctx.Req.MultipartForm.File == nil {
|
||||
return nil, false, http.ErrMissingFile
|
||||
}
|
||||
for _, files := range ctx.Req.MultipartForm.File {
|
||||
if len(files) > 0 {
|
||||
r, err := files[0].Open()
|
||||
return r, true, err
|
||||
}
|
||||
}
|
||||
return nil, false, http.ErrMissingFile
|
||||
}
|
||||
return ctx.Req.Body, false, nil
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
// RedirectToUser redirect to a differently-named user
|
||||
func RedirectToUser(ctx *Base, userName string, redirectUserID int64) {
|
||||
user, err := user_model.GetUserByID(ctx, redirectUserID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "unable to get user")
|
||||
return
|
||||
}
|
||||
|
||||
redirectPath := strings.Replace(
|
||||
ctx.Req.URL.EscapedPath(),
|
||||
url.PathEscape(userName),
|
||||
url.PathEscape(user.Name),
|
||||
1,
|
||||
)
|
||||
if ctx.Req.URL.RawQuery != "" {
|
||||
redirectPath += "?" + ctx.Req.URL.RawQuery
|
||||
}
|
||||
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
// RedirectToFirst redirects to first not empty URL
|
||||
func (ctx *Context) RedirectToFirst(location ...string) {
|
||||
for _, loc := range location {
|
||||
if len(loc) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if httplib.IsRiskyRedirectURL(loc) {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx.Redirect(loc)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
}
|
||||
|
||||
const tplStatus500 base.TplName = "status/500"
|
||||
|
||||
// HTML calls Context.HTML and renders the template to HTTP response
|
||||
func (ctx *Context) HTML(status int, name base.TplName) {
|
||||
log.Debug("Template: %s", name)
|
||||
|
||||
tmplStartTime := time.Now()
|
||||
if !setting.IsProd {
|
||||
ctx.Data["TemplateName"] = name
|
||||
}
|
||||
ctx.Data["TemplateLoadTimes"] = func() string {
|
||||
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
|
||||
}
|
||||
|
||||
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if rendering fails, show error page
|
||||
if name != tplStatus500 {
|
||||
err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err))
|
||||
ctx.ServerError("Render failed", err) // show the 500 error page
|
||||
} else {
|
||||
ctx.PlainText(http.StatusInternalServerError, "Unable to render status/500 page, the template system is broken, or Gitea can't find your template files.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RenderToString renders the template content to a string
|
||||
func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) {
|
||||
var buf strings.Builder
|
||||
err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data, ctx.TemplateContext)
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
// RenderWithErr used for page has form validation but need to prompt error to users.
|
||||
func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form any) {
|
||||
if form != nil {
|
||||
middleware.AssignForm(form, ctx.Data)
|
||||
}
|
||||
ctx.Flash.ErrorMsg = msg
|
||||
ctx.Data["Flash"] = ctx.Flash
|
||||
ctx.HTML(http.StatusOK, tpl)
|
||||
}
|
||||
|
||||
// NotFound displays a 404 (Not Found) page and prints the given error, if any.
|
||||
func (ctx *Context) NotFound(logMsg string, logErr error) {
|
||||
ctx.notFoundInternal(logMsg, logErr)
|
||||
}
|
||||
|
||||
func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
||||
if logErr != nil {
|
||||
log.Log(2, log.DEBUG, "%s: %v", logMsg, logErr)
|
||||
if !setting.IsProd {
|
||||
ctx.Data["ErrorMsg"] = logErr
|
||||
}
|
||||
}
|
||||
|
||||
// response simple message if Accept isn't text/html
|
||||
showHTML := false
|
||||
for _, part := range ctx.Req.Header["Accept"] {
|
||||
if strings.Contains(part, "text/html") {
|
||||
showHTML = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !showHTML {
|
||||
ctx.plainTextInternal(3, http.StatusNotFound, []byte("Not found.\n"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||||
ctx.Data["Title"] = "Page Not Found"
|
||||
ctx.HTML(http.StatusNotFound, base.TplName("status/404"))
|
||||
}
|
||||
|
||||
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
|
||||
func (ctx *Context) ServerError(logMsg string, logErr error) {
|
||||
ctx.serverErrorInternal(logMsg, logErr)
|
||||
}
|
||||
|
||||
func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
|
||||
if logErr != nil {
|
||||
log.ErrorWithSkip(2, "%s: %v", logMsg, logErr)
|
||||
if _, ok := logErr.(*net.OpError); ok || errors.Is(logErr, &net.OpError{}) {
|
||||
// This is an error within the underlying connection
|
||||
// and further rendering will not work so just return
|
||||
return
|
||||
}
|
||||
|
||||
// it's safe to show internal error to admin users, and it helps
|
||||
if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
|
||||
ctx.Data["ErrorMsg"] = fmt.Sprintf("%s, %s", logMsg, logErr)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = "Internal Server Error"
|
||||
ctx.HTML(http.StatusInternalServerError, tplStatus500)
|
||||
}
|
||||
|
||||
// NotFoundOrServerError use error check function to determine if the error
|
||||
// is about not found. It responds with 404 status code for not found error,
|
||||
// or error context description for logging purpose of 500 server error.
|
||||
// TODO: remove the "errCheck" and use util.ErrNotFound to check
|
||||
func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
|
||||
if errCheck(logErr) {
|
||||
ctx.notFoundInternal(logMsg, logErr)
|
||||
return
|
||||
}
|
||||
ctx.serverErrorInternal(logMsg, logErr)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
var _ context.Context = TemplateContext(nil)
|
||||
|
||||
func NewTemplateContext(ctx context.Context) TemplateContext {
|
||||
return TemplateContext{"_ctx": ctx}
|
||||
}
|
||||
|
||||
func (c TemplateContext) parentContext() context.Context {
|
||||
return c["_ctx"].(context.Context)
|
||||
}
|
||||
|
||||
func (c TemplateContext) Deadline() (deadline time.Time, ok bool) {
|
||||
return c.parentContext().Deadline()
|
||||
}
|
||||
|
||||
func (c TemplateContext) Done() <-chan struct{} {
|
||||
return c.parentContext().Done()
|
||||
}
|
||||
|
||||
func (c TemplateContext) Err() error {
|
||||
return c.parentContext().Err()
|
||||
}
|
||||
|
||||
func (c TemplateContext) Value(key any) any {
|
||||
return c.parentContext().Value(key)
|
||||
}
|
||||
|
||||
// DataRaceCheck checks whether the template context function "ctx()" returns the consistent context
|
||||
// as the current template's rendering context (request context), to help to find data race issues as early as possible.
|
||||
// When the code is proven to be correct and stable, this function should be removed.
|
||||
func (c TemplateContext) DataRaceCheck(dataCtx context.Context) (string, error) {
|
||||
if c.parentContext() != dataCtx {
|
||||
log.Error("TemplateContext.DataRaceCheck: parent context mismatch\n%s", log.Stack(2))
|
||||
return "", errors.New("parent context mismatch")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRemoveSessionCookieHeader(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
w.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
|
||||
w.Header().Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
|
||||
assert.Len(t, w.Header().Values("Set-Cookie"), 2)
|
||||
removeSessionCookieHeader(w)
|
||||
assert.Len(t, w.Header().Values("Set-Cookie"), 1)
|
||||
assert.Contains(t, "other=bar", w.Header().Get("Set-Cookie"))
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2021 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// a middleware that generates and validates CSRF tokens.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
|
||||
type CSRFProtector interface {
|
||||
// GetHeaderName returns HTTP header to search for token.
|
||||
GetHeaderName() string
|
||||
// GetFormName returns form value to search for token.
|
||||
GetFormName() string
|
||||
// GetToken returns the token.
|
||||
GetToken() string
|
||||
// Validate validates the token in http context.
|
||||
Validate(ctx *Context)
|
||||
// DeleteCookie deletes the cookie
|
||||
DeleteCookie(ctx *Context)
|
||||
}
|
||||
|
||||
type csrfProtector struct {
|
||||
opt CsrfOptions
|
||||
// Token generated to pass via header, cookie, or hidden form value.
|
||||
Token string
|
||||
// This value must be unique per user.
|
||||
ID string
|
||||
}
|
||||
|
||||
// GetHeaderName returns the name of the HTTP header for csrf token.
|
||||
func (c *csrfProtector) GetHeaderName() string {
|
||||
return c.opt.Header
|
||||
}
|
||||
|
||||
// GetFormName returns the name of the form value for csrf token.
|
||||
func (c *csrfProtector) GetFormName() string {
|
||||
return c.opt.Form
|
||||
}
|
||||
|
||||
// GetToken returns the current token. This is typically used
|
||||
// to populate a hidden form in an HTML template.
|
||||
func (c *csrfProtector) GetToken() string {
|
||||
return c.Token
|
||||
}
|
||||
|
||||
// CsrfOptions maintains options to manage behavior of Generate.
|
||||
type CsrfOptions struct {
|
||||
// The global secret value used to generate Tokens.
|
||||
Secret string
|
||||
// HTTP header used to set and get token.
|
||||
Header string
|
||||
// Form value used to set and get token.
|
||||
Form string
|
||||
// Cookie value used to set and get token.
|
||||
Cookie string
|
||||
// Cookie domain.
|
||||
CookieDomain string
|
||||
// Cookie path.
|
||||
CookiePath string
|
||||
CookieHTTPOnly bool
|
||||
// SameSite set the cookie SameSite type
|
||||
SameSite http.SameSite
|
||||
// Key used for getting the unique ID per user.
|
||||
SessionKey string
|
||||
// oldSessionKey saves old value corresponding to SessionKey.
|
||||
oldSessionKey string
|
||||
// If true, send token via X-Csrf-Token header.
|
||||
SetHeader bool
|
||||
// If true, send token via _csrf cookie.
|
||||
SetCookie bool
|
||||
// Set the Secure flag to true on the cookie.
|
||||
Secure bool
|
||||
// Disallow Origin appear in request header.
|
||||
Origin bool
|
||||
// Cookie lifetime. Default is 0
|
||||
CookieLifeTime int
|
||||
}
|
||||
|
||||
func prepareDefaultCsrfOptions(opt CsrfOptions) CsrfOptions {
|
||||
if opt.Secret == "" {
|
||||
randBytes, err := util.CryptoRandomBytes(8)
|
||||
if err != nil {
|
||||
// this panic can be handled by the recover() in http handlers
|
||||
panic(fmt.Errorf("failed to generate random bytes: %w", err))
|
||||
}
|
||||
opt.Secret = base32.StdEncoding.EncodeToString(randBytes)
|
||||
}
|
||||
if opt.Header == "" {
|
||||
opt.Header = "X-Csrf-Token"
|
||||
}
|
||||
if opt.Form == "" {
|
||||
opt.Form = "_csrf"
|
||||
}
|
||||
if opt.Cookie == "" {
|
||||
opt.Cookie = "_csrf"
|
||||
}
|
||||
if opt.CookiePath == "" {
|
||||
opt.CookiePath = "/"
|
||||
}
|
||||
if opt.SessionKey == "" {
|
||||
opt.SessionKey = "uid"
|
||||
}
|
||||
if opt.CookieLifeTime == 0 {
|
||||
opt.CookieLifeTime = int(CsrfTokenTimeout.Seconds())
|
||||
}
|
||||
|
||||
opt.oldSessionKey = "_old_" + opt.SessionKey
|
||||
return opt
|
||||
}
|
||||
|
||||
func newCsrfCookie(c *csrfProtector, value string) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: c.opt.Cookie,
|
||||
Value: value,
|
||||
Path: c.opt.CookiePath,
|
||||
Domain: c.opt.CookieDomain,
|
||||
MaxAge: c.opt.CookieLifeTime,
|
||||
Secure: c.opt.Secure,
|
||||
HttpOnly: c.opt.CookieHTTPOnly,
|
||||
SameSite: c.opt.SameSite,
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareCSRFProtector returns a CSRFProtector to be used for every request.
|
||||
// Additionally, depending on options set, generated tokens will be sent via Header and/or Cookie.
|
||||
func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector {
|
||||
opt = prepareDefaultCsrfOptions(opt)
|
||||
x := &csrfProtector{opt: opt}
|
||||
|
||||
if opt.Origin && len(ctx.Req.Header.Get("Origin")) > 0 {
|
||||
return x
|
||||
}
|
||||
|
||||
x.ID = "0"
|
||||
uidAny := ctx.Session.Get(opt.SessionKey)
|
||||
if uidAny != nil {
|
||||
switch uidVal := uidAny.(type) {
|
||||
case string:
|
||||
x.ID = uidVal
|
||||
case int64:
|
||||
x.ID = strconv.FormatInt(uidVal, 10)
|
||||
default:
|
||||
log.Error("invalid uid type in session: %T", uidAny)
|
||||
}
|
||||
}
|
||||
|
||||
oldUID := ctx.Session.Get(opt.oldSessionKey)
|
||||
uidChanged := oldUID == nil || oldUID.(string) != x.ID
|
||||
cookieToken := ctx.GetSiteCookie(opt.Cookie)
|
||||
|
||||
needsNew := true
|
||||
if uidChanged {
|
||||
_ = ctx.Session.Set(opt.oldSessionKey, x.ID)
|
||||
} else if cookieToken != "" {
|
||||
// If cookie token presents, re-use existing unexpired token, else generate a new one.
|
||||
if issueTime, ok := ParseCsrfToken(cookieToken); ok {
|
||||
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
|
||||
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
|
||||
x.Token = cookieToken
|
||||
needsNew = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needsNew {
|
||||
// FIXME: actionId.
|
||||
x.Token = GenerateCsrfToken(x.opt.Secret, x.ID, "POST", time.Now())
|
||||
if opt.SetCookie {
|
||||
cookie := newCsrfCookie(x, x.Token)
|
||||
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
|
||||
}
|
||||
}
|
||||
|
||||
if opt.SetHeader {
|
||||
ctx.Resp.Header().Add(opt.Header, x.Token)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func (c *csrfProtector) validateToken(ctx *Context, token string) {
|
||||
if !ValidCsrfToken(token, c.opt.Secret, c.ID, "POST", time.Now()) {
|
||||
c.DeleteCookie(ctx)
|
||||
if middleware.IsAPIPath(ctx.Req) {
|
||||
// currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints.
|
||||
http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest)
|
||||
} else {
|
||||
ctx.Flash.Error(ctx.Tr("error.invalid_csrf"))
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
|
||||
// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated.
|
||||
// If this validation fails, custom Error is sent in the reply.
|
||||
// If neither a header nor form value is found, http.StatusBadRequest is sent.
|
||||
func (c *csrfProtector) Validate(ctx *Context) {
|
||||
if token := ctx.Req.Header.Get(c.GetHeaderName()); token != "" {
|
||||
c.validateToken(ctx, token)
|
||||
return
|
||||
}
|
||||
if token := ctx.Req.FormValue(c.GetFormName()); token != "" {
|
||||
c.validateToken(ctx, token)
|
||||
return
|
||||
}
|
||||
c.validateToken(ctx, "") // no csrf token, use an empty token to respond error
|
||||
}
|
||||
|
||||
func (c *csrfProtector) DeleteCookie(ctx *Context) {
|
||||
if c.opt.SetCookie {
|
||||
cookie := newCsrfCookie(c, "")
|
||||
cookie.MaxAge = -1
|
||||
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
// Organization contains organization context
|
||||
type Organization struct {
|
||||
IsOwner bool
|
||||
IsMember bool
|
||||
IsTeamMember bool // Is member of team.
|
||||
IsTeamAdmin bool // In owner team or team that has admin permission level.
|
||||
Organization *organization.Organization
|
||||
OrgLink string
|
||||
CanCreateOrgRepo bool
|
||||
PublicMemberOnly bool // Only display public members
|
||||
|
||||
Team *organization.Team
|
||||
Teams []*organization.Team
|
||||
}
|
||||
|
||||
func (org *Organization) CanWriteUnit(ctx *Context, unitType unit.Type) bool {
|
||||
return org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeWrite
|
||||
}
|
||||
|
||||
func (org *Organization) CanReadUnit(ctx *Context, unitType unit.Type) bool {
|
||||
return org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeRead
|
||||
}
|
||||
|
||||
func GetOrganizationByParams(ctx *Context) {
|
||||
orgName := ctx.Params(":org")
|
||||
|
||||
var err error
|
||||
|
||||
ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
redirectUserID, err := user_model.LookupUserRedirect(ctx, orgName)
|
||||
if err == nil {
|
||||
RedirectToUser(ctx.Base, orgName, redirectUserID)
|
||||
} else if user_model.IsErrUserRedirectNotExist(err) {
|
||||
ctx.NotFound("GetUserByName", err)
|
||||
} else {
|
||||
ctx.ServerError("LookupUserRedirect", err)
|
||||
}
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// HandleOrgAssignment handles organization assignment
|
||||
func HandleOrgAssignment(ctx *Context, args ...bool) {
|
||||
var (
|
||||
requireMember bool
|
||||
requireOwner bool
|
||||
requireTeamMember bool
|
||||
requireTeamAdmin bool
|
||||
)
|
||||
if len(args) >= 1 {
|
||||
requireMember = args[0]
|
||||
}
|
||||
if len(args) >= 2 {
|
||||
requireOwner = args[1]
|
||||
}
|
||||
if len(args) >= 3 {
|
||||
requireTeamMember = args[2]
|
||||
}
|
||||
if len(args) >= 4 {
|
||||
requireTeamAdmin = args[3]
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
if ctx.ContextUser == nil {
|
||||
// if Organization is not defined, get it from params
|
||||
if ctx.Org.Organization == nil {
|
||||
GetOrganizationByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if ctx.ContextUser.IsOrganization() {
|
||||
if ctx.Org == nil {
|
||||
ctx.Org = &Organization{}
|
||||
}
|
||||
ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser)
|
||||
} else {
|
||||
// ContextUser is an individual User
|
||||
return
|
||||
}
|
||||
|
||||
org := ctx.Org.Organization
|
||||
|
||||
// Handle Visibility
|
||||
if org.Visibility != structs.VisibleTypePublic && !ctx.IsSigned {
|
||||
// We must be signed in to see limited or private organizations
|
||||
ctx.NotFound("OrgAssignment", err)
|
||||
return
|
||||
}
|
||||
|
||||
if org.Visibility == structs.VisibleTypePrivate {
|
||||
requireMember = true
|
||||
} else if ctx.IsSigned && ctx.Doer.IsRestricted {
|
||||
requireMember = true
|
||||
}
|
||||
|
||||
ctx.ContextUser = org.AsUser()
|
||||
ctx.Data["Org"] = org
|
||||
|
||||
// Admin has super access.
|
||||
if ctx.IsSigned && ctx.Doer.IsAdmin {
|
||||
ctx.Org.IsOwner = true
|
||||
ctx.Org.IsMember = true
|
||||
ctx.Org.IsTeamMember = true
|
||||
ctx.Org.IsTeamAdmin = true
|
||||
ctx.Org.CanCreateOrgRepo = true
|
||||
} else if ctx.IsSigned {
|
||||
ctx.Org.IsOwner, err = org.IsOwnedBy(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsOwnedBy", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Org.IsOwner {
|
||||
ctx.Org.IsMember = true
|
||||
ctx.Org.IsTeamMember = true
|
||||
ctx.Org.IsTeamAdmin = true
|
||||
ctx.Org.CanCreateOrgRepo = true
|
||||
} else {
|
||||
ctx.Org.IsMember, err = org.IsOrgMember(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsOrgMember", err)
|
||||
return
|
||||
}
|
||||
ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("CanCreateOrgRepo", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fake data.
|
||||
ctx.Data["SignedUser"] = &user_model.User{}
|
||||
}
|
||||
if (requireMember && !ctx.Org.IsMember) ||
|
||||
(requireOwner && !ctx.Org.IsOwner) {
|
||||
ctx.NotFound("OrgAssignment", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
|
||||
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
|
||||
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
ctx.Data["IsPublicMember"] = func(uid int64) bool {
|
||||
is, _ := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, uid)
|
||||
return is
|
||||
}
|
||||
ctx.Data["CanCreateOrgRepo"] = ctx.Org.CanCreateOrgRepo
|
||||
|
||||
ctx.Org.OrgLink = org.AsUser().OrganisationLink()
|
||||
ctx.Data["OrgLink"] = ctx.Org.OrgLink
|
||||
|
||||
// Member
|
||||
ctx.Org.PublicMemberOnly = ctx.Doer == nil || !ctx.Org.IsMember && !ctx.Doer.IsAdmin
|
||||
opts := &organization.FindOrgMembersOpts{
|
||||
OrgID: org.ID,
|
||||
PublicOnly: ctx.Org.PublicMemberOnly,
|
||||
}
|
||||
ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountOrgMembers", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Team.
|
||||
if ctx.Org.IsMember {
|
||||
shouldSeeAllTeams := false
|
||||
if ctx.Org.IsOwner {
|
||||
shouldSeeAllTeams = true
|
||||
} else {
|
||||
teams, err := org.GetUserTeams(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserTeams", err)
|
||||
return
|
||||
}
|
||||
for _, team := range teams {
|
||||
if team.IncludesAllRepositories && team.AccessMode >= perm.AccessModeAdmin {
|
||||
shouldSeeAllTeams = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if shouldSeeAllTeams {
|
||||
ctx.Org.Teams, err = org.LoadTeams(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadTeams", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ctx.Org.Teams, err = org.GetUserTeams(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserTeams", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Data["NumTeams"] = len(ctx.Org.Teams)
|
||||
}
|
||||
|
||||
teamName := ctx.Params(":team")
|
||||
if len(teamName) > 0 {
|
||||
teamExists := false
|
||||
for _, team := range ctx.Org.Teams {
|
||||
if team.LowerName == strings.ToLower(teamName) {
|
||||
teamExists = true
|
||||
ctx.Org.Team = team
|
||||
ctx.Org.IsTeamMember = true
|
||||
ctx.Data["Team"] = ctx.Org.Team
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !teamExists {
|
||||
ctx.NotFound("OrgAssignment", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["IsTeamMember"] = ctx.Org.IsTeamMember
|
||||
if requireTeamMember && !ctx.Org.IsTeamMember {
|
||||
ctx.NotFound("OrgAssignment", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.AccessMode >= perm.AccessModeAdmin
|
||||
ctx.Data["IsTeamAdmin"] = ctx.Org.IsTeamAdmin
|
||||
if requireTeamAdmin && !ctx.Org.IsTeamAdmin {
|
||||
ctx.NotFound("OrgAssignment", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Data["ContextUser"] = ctx.ContextUser
|
||||
|
||||
ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
|
||||
ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages)
|
||||
ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode)
|
||||
}
|
||||
|
||||
// OrgAssignment returns a middleware to handle organization assignment
|
||||
func OrgAssignment(args ...bool) func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
HandleOrgAssignment(ctx, args...)
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
)
|
||||
|
||||
// Package contains owner, access mode and optional the package descriptor
|
||||
type Package struct {
|
||||
Owner *user_model.User
|
||||
AccessMode perm.AccessMode
|
||||
Descriptor *packages_model.PackageDescriptor
|
||||
}
|
||||
|
||||
type packageAssignmentCtx struct {
|
||||
*Base
|
||||
Doer *user_model.User
|
||||
ContextUser *user_model.User
|
||||
}
|
||||
|
||||
// PackageAssignment returns a middleware to handle Context.Package assignment
|
||||
func PackageAssignment() func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
errorFn := func(status int, title string, obj any) {
|
||||
err, ok := obj.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("%s", obj)
|
||||
}
|
||||
if status == http.StatusNotFound {
|
||||
ctx.NotFound(title, err)
|
||||
} else {
|
||||
ctx.ServerError(title, err)
|
||||
}
|
||||
}
|
||||
paCtx := &packageAssignmentCtx{Base: ctx.Base, Doer: ctx.Doer, ContextUser: ctx.ContextUser}
|
||||
ctx.Package = packageAssignment(paCtx, errorFn)
|
||||
}
|
||||
}
|
||||
|
||||
// PackageAssignmentAPI returns a middleware to handle Context.Package assignment
|
||||
func PackageAssignmentAPI() func(ctx *APIContext) {
|
||||
return func(ctx *APIContext) {
|
||||
paCtx := &packageAssignmentCtx{Base: ctx.Base, Doer: ctx.Doer, ContextUser: ctx.ContextUser}
|
||||
ctx.Package = packageAssignment(paCtx, ctx.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any)) *Package {
|
||||
pkg := &Package{
|
||||
Owner: ctx.ContextUser,
|
||||
}
|
||||
var err error
|
||||
pkg.AccessMode, err = determineAccessMode(ctx.Base, pkg, ctx.Doer)
|
||||
if err != nil {
|
||||
errCb(http.StatusInternalServerError, "determineAccessMode", err)
|
||||
return pkg
|
||||
}
|
||||
|
||||
packageType := ctx.Params("type")
|
||||
name := ctx.Params("name")
|
||||
version := ctx.Params("version")
|
||||
if packageType != "" && name != "" && version != "" {
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
errCb(http.StatusNotFound, "GetVersionByNameAndVersion", err)
|
||||
} else {
|
||||
errCb(http.StatusInternalServerError, "GetVersionByNameAndVersion", err)
|
||||
}
|
||||
return pkg
|
||||
}
|
||||
|
||||
pkg.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv)
|
||||
if err != nil {
|
||||
errCb(http.StatusInternalServerError, "GetPackageDescriptor", err)
|
||||
return pkg
|
||||
}
|
||||
}
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
|
||||
if setting.Service.RequireSignInView && doer == nil {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
if doer != nil && !doer.IsGhost() && (!doer.IsActive || doer.ProhibitLogin) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
// TODO: ActionUser permission check
|
||||
accessMode := perm.AccessModeNone
|
||||
if pkg.Owner.IsOrganization() {
|
||||
org := organization.OrgFromUser(pkg.Owner)
|
||||
|
||||
if doer != nil && !doer.IsGhost() {
|
||||
// 1. If user is logged in, check all team packages permissions
|
||||
var err error
|
||||
accessMode, err = org.GetOrgUserMaxAuthorizeLevel(ctx, doer.ID)
|
||||
if err != nil {
|
||||
return accessMode, err
|
||||
}
|
||||
// If access mode is less than write check every team for more permissions
|
||||
// The minimum possible access mode is read for org members
|
||||
if accessMode < perm.AccessModeWrite {
|
||||
teams, err := organization.GetUserOrgTeams(ctx, org.ID, doer.ID)
|
||||
if err != nil {
|
||||
return accessMode, err
|
||||
}
|
||||
for _, t := range teams {
|
||||
perm := t.UnitAccessMode(ctx, unit.TypePackages)
|
||||
if accessMode < perm {
|
||||
accessMode = perm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if accessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, pkg.Owner, doer) {
|
||||
// 2. If user is unauthorized or no org member, check if org is visible
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
} else {
|
||||
if doer != nil && !doer.IsGhost() {
|
||||
// 1. Check if user is package owner
|
||||
if doer.ID == pkg.Owner.ID {
|
||||
accessMode = perm.AccessModeOwner
|
||||
} else if pkg.Owner.Visibility == structs.VisibleTypePublic || pkg.Owner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
} else if pkg.Owner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
}
|
||||
|
||||
return accessMode, nil
|
||||
}
|
||||
|
||||
// PackageContexter initializes a package context for a request.
|
||||
func PackageContexter() func(next http.Handler) http.Handler {
|
||||
renderer := templates.HTMLRenderer()
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
base, baseCleanUp := NewBaseContext(resp, req)
|
||||
defer baseCleanUp()
|
||||
|
||||
// it is still needed when rendering 500 page in a package handler
|
||||
ctx := NewWebContext(base, renderer, nil)
|
||||
ctx.Base.AppendContextValue(WebContextKey, ctx)
|
||||
next.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/paginator"
|
||||
)
|
||||
|
||||
// Pagination provides a pagination via paginator.Paginator and additional configurations for the link params used in rendering
|
||||
type Pagination struct {
|
||||
Paginater *paginator.Paginator
|
||||
urlParams []string
|
||||
}
|
||||
|
||||
// NewPagination creates a new instance of the Pagination struct.
|
||||
// "pagingNum" is "page size" or "limit", "current" is "page"
|
||||
func NewPagination(total, pagingNum, current, numPages int) *Pagination {
|
||||
p := &Pagination{}
|
||||
p.Paginater = paginator.New(total, pagingNum, current, numPages)
|
||||
return p
|
||||
}
|
||||
|
||||
// AddParam adds a value from context identified by ctxKey as link param under a given paramKey
|
||||
func (p *Pagination) AddParam(ctx *Context, paramKey, ctxKey string) {
|
||||
_, exists := ctx.Data[ctxKey]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
paramData := fmt.Sprintf("%v", ctx.Data[ctxKey]) // cast any to string
|
||||
urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(paramKey), url.QueryEscape(paramData))
|
||||
p.urlParams = append(p.urlParams, urlParam)
|
||||
}
|
||||
|
||||
// AddParamString adds a string parameter directly
|
||||
func (p *Pagination) AddParamString(key, value string) {
|
||||
urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value))
|
||||
p.urlParams = append(p.urlParams, urlParam)
|
||||
}
|
||||
|
||||
// GetParams returns the configured URL params
|
||||
func (p *Pagination) GetParams() template.URL {
|
||||
return template.URL(strings.Join(p.urlParams, "&"))
|
||||
}
|
||||
|
||||
// SetDefaultParams sets common pagination params that are often used
|
||||
func (p *Pagination) SetDefaultParams(ctx *Context) {
|
||||
p.AddParam(ctx, "sort", "SortType")
|
||||
p.AddParam(ctx, "q", "Keyword")
|
||||
// do not add any more uncommon params here!
|
||||
p.AddParam(ctx, "t", "queryType")
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// RequireRepoAdmin returns a middleware for requiring repository admin permission
|
||||
func RequireRepoAdmin() func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
if !ctx.IsSigned || !ctx.Repo.IsAdmin() {
|
||||
ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRepoWriter returns a middleware for requiring repository write to the specify unitType
|
||||
func RequireRepoWriter(unitType unit.Type) func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
if !ctx.Repo.CanWrite(unitType) {
|
||||
ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CanEnableEditor checks if the user is allowed to write to the branch of the repo
|
||||
func CanEnableEditor() func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
|
||||
ctx.NotFound("CanWriteToBranch denies permission", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRepoWriterOr returns a middleware for requiring repository write to one of the unit permission
|
||||
func RequireRepoWriterOr(unitTypes ...unit.Type) func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
for _, unitType := range unitTypes {
|
||||
if ctx.Repo.CanWrite(unitType) {
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRepoReader returns a middleware for requiring repository read to the specify unitType
|
||||
func RequireRepoReader(unitType unit.Type) func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
if !ctx.Repo.CanRead(unitType) {
|
||||
if log.IsTrace() {
|
||||
if ctx.IsSigned {
|
||||
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
unitType,
|
||||
ctx.Repo.Repository,
|
||||
ctx.Repo.Permission)
|
||||
} else {
|
||||
log.Trace("Permission Denied: Anonymous user cannot read %-v in Repo %-v\n"+
|
||||
"Anonymous user in Repo has Permissions: %-+v",
|
||||
unitType,
|
||||
ctx.Repo.Repository,
|
||||
ctx.Repo.Permission)
|
||||
}
|
||||
}
|
||||
ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRepoReaderOr returns a middleware for requiring repository write to one of the unit permission
|
||||
func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
for _, unitType := range unitTypes {
|
||||
if ctx.Repo.CanRead(unitType) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if log.IsTrace() {
|
||||
var format string
|
||||
var args []any
|
||||
if ctx.IsSigned {
|
||||
format = "Permission Denied: User %-v cannot read ["
|
||||
args = append(args, ctx.Doer)
|
||||
} else {
|
||||
format = "Permission Denied: Anonymous user cannot read ["
|
||||
}
|
||||
for _, unit := range unitTypes {
|
||||
format += "%-v, "
|
||||
args = append(args, unit)
|
||||
}
|
||||
|
||||
format = format[:len(format)-2] + "] in Repo %-v\n" +
|
||||
"User in Repo has Permissions: %-+v"
|
||||
args = append(args, ctx.Repo.Repository, ctx.Repo.Permission)
|
||||
log.Trace(format, args...)
|
||||
}
|
||||
ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckRepoScopedToken check whether personal access token has repo scope
|
||||
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
|
||||
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
|
||||
return
|
||||
}
|
||||
|
||||
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if ok { // it's a personal access token but not oauth2 token
|
||||
var scopeMatched bool
|
||||
|
||||
requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)
|
||||
|
||||
// check if scope only applies to public resources
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
}
|
||||
|
||||
if publicOnly && repo.IsPrivate {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
scopeMatched, err = scope.HasScope(requiredScopes...)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !scopeMatched {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
web_types "code.gitea.io/gitea/modules/web/types"
|
||||
)
|
||||
|
||||
// PrivateContext represents a context for private routes
|
||||
type PrivateContext struct {
|
||||
*Base
|
||||
Override context.Context
|
||||
|
||||
Repo *Repository
|
||||
}
|
||||
|
||||
func init() {
|
||||
web.RegisterResponseStatusProvider[*PrivateContext](func(req *http.Request) web_types.ResponseStatusProvider {
|
||||
return req.Context().Value(privateContextKey).(*PrivateContext)
|
||||
})
|
||||
}
|
||||
|
||||
// Deadline is part of the interface for context.Context and we pass this to the request context
|
||||
func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) {
|
||||
if ctx.Override != nil {
|
||||
return ctx.Override.Deadline()
|
||||
}
|
||||
return ctx.Base.Deadline()
|
||||
}
|
||||
|
||||
// Done is part of the interface for context.Context and we pass this to the request context
|
||||
func (ctx *PrivateContext) Done() <-chan struct{} {
|
||||
if ctx.Override != nil {
|
||||
return ctx.Override.Done()
|
||||
}
|
||||
return ctx.Base.Done()
|
||||
}
|
||||
|
||||
// Err is part of the interface for context.Context and we pass this to the request context
|
||||
func (ctx *PrivateContext) Err() error {
|
||||
if ctx.Override != nil {
|
||||
return ctx.Override.Err()
|
||||
}
|
||||
return ctx.Base.Err()
|
||||
}
|
||||
|
||||
var privateContextKey any = "default_private_context"
|
||||
|
||||
// GetPrivateContext returns a context for Private routes
|
||||
func GetPrivateContext(req *http.Request) *PrivateContext {
|
||||
return req.Context().Value(privateContextKey).(*PrivateContext)
|
||||
}
|
||||
|
||||
// PrivateContexter returns apicontext as middleware
|
||||
func PrivateContexter() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
base, baseCleanUp := NewBaseContext(w, req)
|
||||
ctx := &PrivateContext{Base: base}
|
||||
defer baseCleanUp()
|
||||
ctx.Base.AppendContextValue(privateContextKey, ctx)
|
||||
|
||||
next.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OverrideContext overrides the underlying request context for Done() etc.
|
||||
// This function should be used when there is a need for work to continue even if the request has been cancelled.
|
||||
// Primarily this affects hook/post-receive and hook/proc-receive both of which need to continue working even if
|
||||
// the underlying request has timed out from the ssh/http push
|
||||
func OverrideContext(ctx *PrivateContext) (cancel context.CancelFunc) {
|
||||
// We now need to override the request context as the base for our work because even if the request is cancelled we have to continue this work
|
||||
ctx.Override, _, cancel = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PrivateContext: %s", ctx.Req.RequestURI), process.RequestProcessType, true)
|
||||
return cancel
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,103 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
web_types "code.gitea.io/gitea/modules/web/types"
|
||||
)
|
||||
|
||||
// ResponseWriter represents a response writer for HTTP
|
||||
type ResponseWriter interface {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
web_types.ResponseStatusProvider
|
||||
|
||||
Before(func(ResponseWriter))
|
||||
|
||||
Status() int // used by access logger template
|
||||
Size() int // used by access logger template
|
||||
}
|
||||
|
||||
var _ ResponseWriter = &Response{}
|
||||
|
||||
// Response represents a response
|
||||
type Response struct {
|
||||
http.ResponseWriter
|
||||
written int
|
||||
status int
|
||||
befores []func(ResponseWriter)
|
||||
beforeExecuted bool
|
||||
}
|
||||
|
||||
// Write writes bytes to HTTP endpoint
|
||||
func (r *Response) Write(bs []byte) (int, error) {
|
||||
if !r.beforeExecuted {
|
||||
for _, before := range r.befores {
|
||||
before(r)
|
||||
}
|
||||
r.beforeExecuted = true
|
||||
}
|
||||
size, err := r.ResponseWriter.Write(bs)
|
||||
r.written += size
|
||||
if err != nil {
|
||||
return size, err
|
||||
}
|
||||
if r.status == 0 {
|
||||
r.status = http.StatusOK
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (r *Response) Status() int {
|
||||
return r.status
|
||||
}
|
||||
|
||||
func (r *Response) Size() int {
|
||||
return r.written
|
||||
}
|
||||
|
||||
// WriteHeader write status code
|
||||
func (r *Response) WriteHeader(statusCode int) {
|
||||
if !r.beforeExecuted {
|
||||
for _, before := range r.befores {
|
||||
before(r)
|
||||
}
|
||||
r.beforeExecuted = true
|
||||
}
|
||||
if r.status == 0 {
|
||||
r.status = statusCode
|
||||
r.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush flushes cached data
|
||||
func (r *Response) Flush() {
|
||||
if f, ok := r.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// WrittenStatus returned status code written
|
||||
func (r *Response) WrittenStatus() int {
|
||||
return r.status
|
||||
}
|
||||
|
||||
// Before allows for a function to be called before the ResponseWriter has been written to. This is
|
||||
// useful for setting headers or any other operations that must happen before a response has been written.
|
||||
func (r *Response) Before(f func(ResponseWriter)) {
|
||||
r.befores = append(r.befores, f)
|
||||
}
|
||||
|
||||
func WrapResponseWriter(resp http.ResponseWriter) *Response {
|
||||
if v, ok := resp.(*Response); ok {
|
||||
return v
|
||||
}
|
||||
return &Response{
|
||||
ResponseWriter: resp,
|
||||
status: 0,
|
||||
befores: make([]func(ResponseWriter), 0),
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetQueryBeforeSince return parsed time (unix format) from URL query's before and since
|
||||
func GetQueryBeforeSince(ctx *Base) (before, since int64, err error) {
|
||||
before, err = parseFormTime(ctx, "before")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
since, err = parseFormTime(ctx, "since")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return before, since, nil
|
||||
}
|
||||
|
||||
// parseTime parse time and return unix timestamp
|
||||
func parseFormTime(ctx *Base, name string) (int64, error) {
|
||||
value := strings.TrimSpace(ctx.FormString(name))
|
||||
if len(value) != 0 {
|
||||
t, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !t.IsZero() {
|
||||
return t.Unix(), nil
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright 2012 Google Inc. All Rights Reserved.
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2020 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CsrfTokenTimeout represents the duration that XSRF tokens are valid.
|
||||
// It is exported so clients may set cookie timeouts that match generated tokens.
|
||||
const CsrfTokenTimeout = 24 * time.Hour
|
||||
|
||||
// CsrfTokenRegenerationInterval is the interval between token generations, old tokens are still valid before CsrfTokenTimeout
|
||||
var CsrfTokenRegenerationInterval = 10 * time.Minute
|
||||
|
||||
var csrfTokenSep = []byte(":")
|
||||
|
||||
// GenerateCsrfToken returns a URL-safe secure XSRF token that expires in CsrfTokenTimeout hours.
|
||||
// key is a secret key for your application.
|
||||
// userID is a unique identifier for the user.
|
||||
// actionID is the action the user is taking (e.g. POSTing to a particular path).
|
||||
func GenerateCsrfToken(key, userID, actionID string, now time.Time) string {
|
||||
nowUnixNano := now.UnixNano()
|
||||
nowUnixNanoStr := strconv.FormatInt(nowUnixNano, 10)
|
||||
h := hmac.New(sha1.New, []byte(key))
|
||||
h.Write([]byte(strings.ReplaceAll(userID, ":", "_")))
|
||||
h.Write(csrfTokenSep)
|
||||
h.Write([]byte(strings.ReplaceAll(actionID, ":", "_")))
|
||||
h.Write(csrfTokenSep)
|
||||
h.Write([]byte(nowUnixNanoStr))
|
||||
tok := fmt.Sprintf("%s:%s", h.Sum(nil), nowUnixNanoStr)
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(tok))
|
||||
}
|
||||
|
||||
func ParseCsrfToken(token string) (issueTime time.Time, ok bool) {
|
||||
data, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
pos := bytes.LastIndex(data, csrfTokenSep)
|
||||
if pos == -1 {
|
||||
return time.Time{}, false
|
||||
}
|
||||
nanos, err := strconv.ParseInt(string(data[pos+1:]), 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return time.Unix(0, nanos), true
|
||||
}
|
||||
|
||||
// ValidCsrfToken returns true if token is a valid and unexpired token returned by Generate.
|
||||
func ValidCsrfToken(token, key, userID, actionID string, now time.Time) bool {
|
||||
issueTime, ok := ParseCsrfToken(token)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the token is not expired.
|
||||
if now.Sub(issueTime) >= CsrfTokenTimeout {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the token is not from the future.
|
||||
// Allow 1-minute grace period in case the token is being verified on a
|
||||
// machine whose clock is behind the machine that issued the token.
|
||||
if issueTime.After(now.Add(1 * time.Minute)) {
|
||||
return false
|
||||
}
|
||||
|
||||
expected := GenerateCsrfToken(key, userID, actionID, issueTime)
|
||||
|
||||
// Check that the token matches the expected value.
|
||||
// Use constant time comparison to avoid timing attacks.
|
||||
return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright 2012 Google Inc. All Rights Reserved.
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2020 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
key = "quay"
|
||||
userID = "12345678"
|
||||
actionID = "POST /form"
|
||||
)
|
||||
|
||||
var (
|
||||
now = time.Now()
|
||||
oneMinuteFromNow = now.Add(1 * time.Minute)
|
||||
)
|
||||
|
||||
func Test_ValidToken(t *testing.T) {
|
||||
t.Run("Validate token", func(t *testing.T) {
|
||||
tok := GenerateCsrfToken(key, userID, actionID, now)
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, oneMinuteFromNow))
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(CsrfTokenTimeout-1*time.Nanosecond)))
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(-1*time.Minute)))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_SeparatorReplacement tests that separators are being correctly substituted
|
||||
func Test_SeparatorReplacement(t *testing.T) {
|
||||
t.Run("Test two separator replacements", func(t *testing.T) {
|
||||
assert.NotEqual(t, GenerateCsrfToken("foo:bar", "baz", "wah", now),
|
||||
GenerateCsrfToken("foo", "bar:baz", "wah", now))
|
||||
})
|
||||
}
|
||||
|
||||
func Test_InvalidToken(t *testing.T) {
|
||||
t.Run("Test invalid tokens", func(t *testing.T) {
|
||||
invalidTokenTests := []struct {
|
||||
name, key, userID, actionID string
|
||||
t time.Time
|
||||
}{
|
||||
{"Bad key", "foobar", userID, actionID, oneMinuteFromNow},
|
||||
{"Bad userID", key, "foobar", actionID, oneMinuteFromNow},
|
||||
{"Bad actionID", key, userID, "foobar", oneMinuteFromNow},
|
||||
{"Expired", key, userID, actionID, now.Add(CsrfTokenTimeout)},
|
||||
{"More than 1 minute from the future", key, userID, actionID, now.Add(-1*time.Nanosecond - 1*time.Minute)},
|
||||
}
|
||||
|
||||
tok := GenerateCsrfToken(key, userID, actionID, now)
|
||||
for _, itt := range invalidTokenTests {
|
||||
assert.False(t, ValidCsrfToken(tok, itt.key, itt.userID, itt.actionID, itt.t))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_ValidateBadData primarily tests that no unexpected panics are triggered during parsing
|
||||
func Test_ValidateBadData(t *testing.T) {
|
||||
t.Run("Validate bad data", func(t *testing.T) {
|
||||
badDataTests := []struct {
|
||||
name, tok string
|
||||
}{
|
||||
{"Invalid Base64", "ASDab24(@)$*=="},
|
||||
{"No delimiter", base64.URLEncoding.EncodeToString([]byte("foobar12345678"))},
|
||||
{"Invalid time", base64.URLEncoding.EncodeToString([]byte("foobar:foobar"))},
|
||||
}
|
||||
|
||||
for _, bdt := range badDataTests {
|
||||
assert.False(t, ValidCsrfToken(bdt.tok, key, userID, actionID, oneMinuteFromNow))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package contexttest provides utilities for testing Web/API contexts with models.
|
||||
package contexttest
|
||||
|
||||
import (
|
||||
gocontext "context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func mockRequest(t *testing.T, reqPath string) *http.Request {
|
||||
method, path, found := strings.Cut(reqPath, " ")
|
||||
if !found {
|
||||
method = "GET"
|
||||
path = reqPath
|
||||
}
|
||||
requestURL, err := url.Parse(path)
|
||||
assert.NoError(t, err)
|
||||
req := &http.Request{Method: method, URL: requestURL, Form: url.Values{}}
|
||||
req = req.WithContext(middleware.WithContextData(req.Context()))
|
||||
return req
|
||||
}
|
||||
|
||||
// MockContext mock context for unit tests
|
||||
func MockContext(t *testing.T, reqPath string) (*context.Context, *httptest.ResponseRecorder) {
|
||||
resp := httptest.NewRecorder()
|
||||
req := mockRequest(t, reqPath)
|
||||
base, baseCleanUp := context.NewBaseContext(resp, req)
|
||||
_ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
|
||||
base.Data = middleware.GetContextData(req.Context())
|
||||
base.Locale = &translation.MockLocale{}
|
||||
|
||||
ctx := context.NewWebContext(base, &MockRender{}, nil)
|
||||
|
||||
chiCtx := chi.NewRouteContext()
|
||||
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
|
||||
return ctx, resp
|
||||
}
|
||||
|
||||
// MockAPIContext mock context for unit tests
|
||||
func MockAPIContext(t *testing.T, reqPath string) (*context.APIContext, *httptest.ResponseRecorder) {
|
||||
resp := httptest.NewRecorder()
|
||||
req := mockRequest(t, reqPath)
|
||||
base, baseCleanUp := context.NewBaseContext(resp, req)
|
||||
base.Data = middleware.GetContextData(req.Context())
|
||||
base.Locale = &translation.MockLocale{}
|
||||
ctx := &context.APIContext{Base: base}
|
||||
_ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
|
||||
|
||||
chiCtx := chi.NewRouteContext()
|
||||
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
|
||||
return ctx, resp
|
||||
}
|
||||
|
||||
// LoadRepo load a repo into a test context.
|
||||
func LoadRepo(t *testing.T, ctx gocontext.Context, repoID int64) {
|
||||
var doer *user_model.User
|
||||
repo := &context.Repository{}
|
||||
switch ctx := ctx.(type) {
|
||||
case *context.Context:
|
||||
ctx.Repo = repo
|
||||
doer = ctx.Doer
|
||||
case *context.APIContext:
|
||||
ctx.Repo = repo
|
||||
doer = ctx.Doer
|
||||
default:
|
||||
assert.FailNow(t, "context is not *context.Context or *context.APIContext")
|
||||
}
|
||||
|
||||
repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
var err error
|
||||
repo.Owner, err = user_model.GetUserByID(ctx, repo.Repository.OwnerID)
|
||||
assert.NoError(t, err)
|
||||
repo.RepoLink = repo.Repository.Link()
|
||||
repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo.Repository, doer)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// LoadRepoCommit loads a repo's commit into a test context.
|
||||
func LoadRepoCommit(t *testing.T, ctx gocontext.Context) {
|
||||
var repo *context.Repository
|
||||
switch ctx := ctx.(type) {
|
||||
case *context.Context:
|
||||
repo = ctx.Repo
|
||||
case *context.APIContext:
|
||||
repo = ctx.Repo
|
||||
default:
|
||||
assert.FailNow(t, "context is not *context.Context or *context.APIContext")
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, repo.Repository.RepoPath())
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
branch, err := gitRepo.GetHEADBranch()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, branch)
|
||||
if branch != nil {
|
||||
repo.Commit, err = gitRepo.GetBranchCommit(branch.Name)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadUser load a user into a test context
|
||||
func LoadUser(t *testing.T, ctx gocontext.Context, userID int64) {
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
|
||||
switch ctx := ctx.(type) {
|
||||
case *context.Context:
|
||||
ctx.Doer = doer
|
||||
case *context.APIContext:
|
||||
ctx.Doer = doer
|
||||
default:
|
||||
assert.FailNow(t, "context is not *context.Context or *context.APIContext")
|
||||
}
|
||||
}
|
||||
|
||||
// LoadGitRepo load a git repo into a test context. Requires that ctx.Repo has
|
||||
// already been populated.
|
||||
func LoadGitRepo(t *testing.T, ctx *context.Context) {
|
||||
assert.NoError(t, ctx.Repo.Repository.LoadOwner(ctx))
|
||||
var err error
|
||||
ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
type MockRender struct{}
|
||||
|
||||
func (tr *MockRender) TemplateLookup(tmpl string, _ gocontext.Context) (templates.TemplateExecutor, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (tr *MockRender) HTML(w io.Writer, status int, _ string, _ any, _ gocontext.Context) error {
|
||||
if resp, ok := w.(http.ResponseWriter); ok {
|
||||
resp.WriteHeader(status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+2
-2
@@ -123,9 +123,9 @@ func guessDelimiter(data []byte) rune {
|
||||
func FormatError(err error, locale translation.Locale) (string, error) {
|
||||
if perr, ok := err.(*stdcsv.ParseError); ok {
|
||||
if perr.Err == stdcsv.ErrFieldCount {
|
||||
return locale.Tr("repo.error.csv.invalid_field_count", perr.Line), nil
|
||||
return locale.TrString("repo.error.csv.invalid_field_count", perr.Line), nil
|
||||
}
|
||||
return locale.Tr("repo.error.csv.unexpected", perr.Line, perr.Column), nil
|
||||
return locale.TrString("repo.error.csv.unexpected", perr.Line, perr.Column), nil
|
||||
}
|
||||
|
||||
return "", err
|
||||
|
||||
@@ -561,14 +561,14 @@ func TestFormatError(t *testing.T) {
|
||||
err: &csv.ParseError{
|
||||
Err: csv.ErrFieldCount,
|
||||
},
|
||||
expectedMessage: "repo.error.csv.invalid_field_count",
|
||||
expectedMessage: "repo.error.csv.invalid_field_count:0",
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
err: &csv.ParseError{
|
||||
Err: csv.ErrBareQuote,
|
||||
},
|
||||
expectedMessage: "repo.error.csv.unexpected",
|
||||
expectedMessage: "repo.error.csv.unexpected:0,0",
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
const tplCommentPrefix = `# gitea public key`
|
||||
|
||||
func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
|
||||
f, err := os.Open(fPath)
|
||||
if err != nil {
|
||||
if !autofix {
|
||||
logger.Critical("Unable to open authorized_keys file. ERROR: %v", err)
|
||||
return fmt.Errorf("Unable to open authorized_keys file. ERROR: %w", err)
|
||||
}
|
||||
logger.Warn("Unable to open authorized_keys. (ERROR: %v). Attempting to rewrite...", err)
|
||||
if err = asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
|
||||
logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err)
|
||||
return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %w", err)
|
||||
}
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
linesInAuthorizedKeys := make(container.Set[string])
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
||||
continue
|
||||
}
|
||||
linesInAuthorizedKeys.Add(line)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
// now we regenerate and check if there are any lines missing
|
||||
regenerated := &bytes.Buffer{}
|
||||
if err := asymkey_model.RegeneratePublicKeys(ctx, regenerated); err != nil {
|
||||
logger.Critical("Unable to regenerate authorized_keys file. ERROR: %v", err)
|
||||
return fmt.Errorf("Unable to regenerate authorized_keys file. ERROR: %w", err)
|
||||
}
|
||||
scanner = bufio.NewScanner(regenerated)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
||||
continue
|
||||
}
|
||||
if linesInAuthorizedKeys.Contains(line) {
|
||||
continue
|
||||
}
|
||||
if !autofix {
|
||||
logger.Critical(
|
||||
"authorized_keys file %q is out of date.\nRegenerate it with:\n\t\"%s\"\nor\n\t\"%s\"",
|
||||
fPath,
|
||||
"gitea admin regenerate keys",
|
||||
"gitea doctor --run authorized-keys --fix")
|
||||
return fmt.Errorf(`authorized_keys is out of date and should be regenerated with "gitea admin regenerate keys" or "gitea doctor --run authorized-keys --fix"`)
|
||||
}
|
||||
logger.Warn("authorized_keys is out of date. Attempting rewrite...")
|
||||
err = asymkey_model.RewriteAllPublicKeys(ctx)
|
||||
if err != nil {
|
||||
logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err)
|
||||
return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check if OpenSSH authorized_keys file is up-to-date",
|
||||
Name: "authorized-keys",
|
||||
IsDefault: true,
|
||||
Run: checkAuthorizedKeys,
|
||||
Priority: 4,
|
||||
})
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func iterateUserAccounts(ctx context.Context, each func(*user.User) error) error {
|
||||
err := db.Iterate(
|
||||
ctx,
|
||||
builder.Gt{"id": 0},
|
||||
func(ctx context.Context, bean *user.User) error {
|
||||
return each(bean)
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Since 1.16.4 new restrictions has been set on email addresses. However users with invalid email
|
||||
// addresses would be currently facing a error due to their invalid email address.
|
||||
// Ref: https://github.com/go-gitea/gitea/pull/19085 & https://github.com/go-gitea/gitea/pull/17688
|
||||
func checkUserEmail(ctx context.Context, logger log.Logger, _ bool) error {
|
||||
// We could use quirky SQL to get all users that start without a [a-zA-Z0-9], but that would mean
|
||||
// DB provider-specific SQL and only works _now_. So instead we iterate through all user accounts
|
||||
// and use the user.ValidateEmail function to be future-proof.
|
||||
var invalidUserCount int64
|
||||
if err := iterateUserAccounts(ctx, func(u *user.User) error {
|
||||
// Only check for users, skip
|
||||
if u.Type != user.UserTypeIndividual {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := user.ValidateEmail(u.Email); err != nil {
|
||||
invalidUserCount++
|
||||
logger.Warn("User[id=%d name=%q] have not a valid e-mail: %v", u.ID, u.Name, err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("iterateUserAccounts: %w", err)
|
||||
}
|
||||
|
||||
if invalidUserCount == 0 {
|
||||
logger.Info("All users have a valid e-mail.")
|
||||
} else {
|
||||
logger.Warn("%d user(s) have a non-valid e-mail.", invalidUserCount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// From time to time Gitea makes changes to the reserved usernames and which symbols
|
||||
// are allowed for various reasons. This check helps with detecting users that, according
|
||||
// to our reserved names, don't have a valid username.
|
||||
func checkUserName(ctx context.Context, logger log.Logger, _ bool) error {
|
||||
var invalidUserCount int64
|
||||
if err := iterateUserAccounts(ctx, func(u *user.User) error {
|
||||
if err := user.IsUsableUsername(u.Name); err != nil {
|
||||
invalidUserCount++
|
||||
logger.Warn("User[id=%d] does not have a valid username: %v", u.ID, err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("iterateUserAccounts: %w", err)
|
||||
}
|
||||
|
||||
if invalidUserCount == 0 {
|
||||
logger.Info("All users have a valid username.")
|
||||
} else {
|
||||
logger.Warn("%d user(s) have a non-valid username.", invalidUserCount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check if users has an valid email address",
|
||||
Name: "check-user-email",
|
||||
IsDefault: false,
|
||||
Run: checkUserEmail,
|
||||
Priority: 9,
|
||||
})
|
||||
Register(&Check{
|
||||
Title: "Check if users have a valid username",
|
||||
Name: "check-user-names",
|
||||
IsDefault: false,
|
||||
Run: checkUserName,
|
||||
Priority: 9,
|
||||
})
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func checkOldArchives(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
numRepos := 0
|
||||
numReposUpdated := 0
|
||||
err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
|
||||
if repo.IsEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
p := filepath.Join(repo.RepoPath(), "archives")
|
||||
isDir, err := util.IsDir(p)
|
||||
if err != nil {
|
||||
log.Warn("check if %s is directory failed: %v", p, err)
|
||||
}
|
||||
if isDir {
|
||||
numRepos++
|
||||
if autofix {
|
||||
if err := os.RemoveAll(p); err == nil {
|
||||
numReposUpdated++
|
||||
} else {
|
||||
log.Warn("remove %s failed: %v", p, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if autofix {
|
||||
logger.Info("%d / %d old archives in repository deleted", numReposUpdated, numRepos)
|
||||
} else {
|
||||
logger.Info("%d old archives in repository need to be deleted", numRepos)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check old archives",
|
||||
Name: "check-old-archives",
|
||||
IsDefault: false,
|
||||
Run: checkOldArchives,
|
||||
Priority: 7,
|
||||
})
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/migrations"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
type consistencyCheck struct {
|
||||
Name string
|
||||
Counter func(context.Context) (int64, error)
|
||||
Fixer func(context.Context) (int64, error)
|
||||
FixedMessage string
|
||||
}
|
||||
|
||||
func (c *consistencyCheck) Run(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
count, err := c.Counter(ctx)
|
||||
if err != nil {
|
||||
logger.Critical("Error: %v whilst counting %s", err, c.Name)
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
if autofix {
|
||||
var fixed int64
|
||||
if fixed, err = c.Fixer(ctx); err != nil {
|
||||
logger.Critical("Error: %v whilst fixing %s", err, c.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
prompt := "Deleted"
|
||||
if c.FixedMessage != "" {
|
||||
prompt = c.FixedMessage
|
||||
}
|
||||
|
||||
if fixed < 0 {
|
||||
logger.Info(prompt+" %d %s", count, c.Name)
|
||||
} else {
|
||||
logger.Info(prompt+" %d/%d %s", fixed, count, c.Name)
|
||||
}
|
||||
} else {
|
||||
logger.Warn("Found %d %s", count, c.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func asFixer(fn func(ctx context.Context) error) func(ctx context.Context) (int64, error) {
|
||||
return func(ctx context.Context) (int64, error) {
|
||||
err := fn(ctx)
|
||||
return -1, err
|
||||
}
|
||||
}
|
||||
|
||||
func genericOrphanCheck(name, subject, refobject, joincond string) consistencyCheck {
|
||||
return consistencyCheck{
|
||||
Name: name,
|
||||
Counter: func(ctx context.Context) (int64, error) {
|
||||
return db.CountOrphanedObjects(ctx, subject, refobject, joincond)
|
||||
},
|
||||
Fixer: func(ctx context.Context) (int64, error) {
|
||||
err := db.DeleteOrphanedObjects(ctx, subject, refobject, joincond)
|
||||
return -1, err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
// make sure DB version is uptodate
|
||||
if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
|
||||
logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
|
||||
return err
|
||||
}
|
||||
|
||||
consistencyChecks := []consistencyCheck{
|
||||
{
|
||||
// find labels without existing repo or org
|
||||
Name: "Orphaned Labels without existing repository or organisation",
|
||||
Counter: issues_model.CountOrphanedLabels,
|
||||
Fixer: asFixer(issues_model.DeleteOrphanedLabels),
|
||||
},
|
||||
{
|
||||
// find IssueLabels without existing label
|
||||
Name: "Orphaned Issue Labels without existing label",
|
||||
Counter: issues_model.CountOrphanedIssueLabels,
|
||||
Fixer: asFixer(issues_model.DeleteOrphanedIssueLabels),
|
||||
},
|
||||
{
|
||||
// find issues without existing repository
|
||||
Name: "Orphaned Issues without existing repository",
|
||||
Counter: issues_model.CountOrphanedIssues,
|
||||
Fixer: asFixer(issues_model.DeleteOrphanedIssues),
|
||||
},
|
||||
// find releases without existing repository
|
||||
genericOrphanCheck("Orphaned Releases without existing repository",
|
||||
"release", "repository", "`release`.repo_id=repository.id"),
|
||||
// find pulls without existing issues
|
||||
genericOrphanCheck("Orphaned PullRequests without existing issue",
|
||||
"pull_request", "issue", "pull_request.issue_id=issue.id"),
|
||||
// find pull requests without base repository
|
||||
genericOrphanCheck("Pull request entries without existing base repository",
|
||||
"pull_request", "repository", "pull_request.base_repo_id=repository.id"),
|
||||
// find tracked times without existing issues/pulls
|
||||
genericOrphanCheck("Orphaned TrackedTimes without existing issue",
|
||||
"tracked_time", "issue", "tracked_time.issue_id=issue.id"),
|
||||
// find attachments without existing issues or releases
|
||||
{
|
||||
Name: "Orphaned Attachments without existing issues or releases",
|
||||
Counter: repo_model.CountOrphanedAttachments,
|
||||
Fixer: asFixer(repo_model.DeleteOrphanedAttachments),
|
||||
},
|
||||
// find null archived repositories
|
||||
{
|
||||
Name: "Repositories with is_archived IS NULL",
|
||||
Counter: repo_model.CountNullArchivedRepository,
|
||||
Fixer: repo_model.FixNullArchivedRepository,
|
||||
FixedMessage: "Fixed",
|
||||
},
|
||||
// find label comments with empty labels
|
||||
{
|
||||
Name: "Label comments with empty labels",
|
||||
Counter: issues_model.CountCommentTypeLabelWithEmptyLabel,
|
||||
Fixer: issues_model.FixCommentTypeLabelWithEmptyLabel,
|
||||
FixedMessage: "Fixed",
|
||||
},
|
||||
// find label comments with labels from outside the repository
|
||||
{
|
||||
Name: "Label comments with labels from outside the repository",
|
||||
Counter: issues_model.CountCommentTypeLabelWithOutsideLabels,
|
||||
Fixer: issues_model.FixCommentTypeLabelWithOutsideLabels,
|
||||
FixedMessage: "Removed",
|
||||
},
|
||||
// find issue_label with labels from outside the repository
|
||||
{
|
||||
Name: "IssueLabels with Labels from outside the repository",
|
||||
Counter: issues_model.CountIssueLabelWithOutsideLabels,
|
||||
Fixer: issues_model.FixIssueLabelWithOutsideLabels,
|
||||
FixedMessage: "Removed",
|
||||
},
|
||||
{
|
||||
Name: "Action with created_unix set as an empty string",
|
||||
Counter: activities_model.CountActionCreatedUnixString,
|
||||
Fixer: activities_model.FixActionCreatedUnixString,
|
||||
FixedMessage: "Set to zero",
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: function to recalc all counters
|
||||
|
||||
if setting.Database.Type.IsPostgreSQL() {
|
||||
consistencyChecks = append(consistencyChecks, consistencyCheck{
|
||||
Name: "Sequence values",
|
||||
Counter: db.CountBadSequences,
|
||||
Fixer: asFixer(db.FixBadSequences),
|
||||
FixedMessage: "Updated",
|
||||
})
|
||||
}
|
||||
|
||||
consistencyChecks = append(consistencyChecks,
|
||||
// find protected branches without existing repository
|
||||
genericOrphanCheck("Protected Branches without existing repository",
|
||||
"protected_branch", "repository", "protected_branch.repo_id=repository.id"),
|
||||
// find branches without existing repository
|
||||
genericOrphanCheck("Branches without existing repository",
|
||||
"branch", "repository", "branch.repo_id=repository.id"),
|
||||
// find LFS locks without existing repository
|
||||
genericOrphanCheck("LFS locks without existing repository",
|
||||
"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),
|
||||
// find collaborations without users
|
||||
genericOrphanCheck("Collaborations without existing user",
|
||||
"collaboration", "user", "collaboration.user_id=`user`.id"),
|
||||
// find collaborations without repository
|
||||
genericOrphanCheck("Collaborations without existing repository",
|
||||
"collaboration", "repository", "collaboration.repo_id=repository.id"),
|
||||
// find access without users
|
||||
genericOrphanCheck("Access entries without existing user",
|
||||
"access", "user", "access.user_id=`user`.id"),
|
||||
// find access without repository
|
||||
genericOrphanCheck("Access entries without existing repository",
|
||||
"access", "repository", "access.repo_id=repository.id"),
|
||||
// find action without repository
|
||||
genericOrphanCheck("Action entries without existing repository",
|
||||
"action", "repository", "action.repo_id=repository.id"),
|
||||
// find action without user
|
||||
genericOrphanCheck("Action entries without existing user",
|
||||
"action", "user", "action.act_user_id=`user`.id"),
|
||||
// find OAuth2Grant without existing user
|
||||
genericOrphanCheck("Orphaned OAuth2Grant without existing User",
|
||||
"oauth2_grant", "user", "oauth2_grant.user_id=`user`.id"),
|
||||
// find OAuth2Application without existing user
|
||||
genericOrphanCheck("Orphaned OAuth2Application without existing User",
|
||||
"oauth2_application", "user", "oauth2_application.uid=`user`.id"),
|
||||
// find OAuth2AuthorizationCode without existing OAuth2Grant
|
||||
genericOrphanCheck("Orphaned OAuth2AuthorizationCode without existing OAuth2Grant",
|
||||
"oauth2_authorization_code", "oauth2_grant", "oauth2_authorization_code.grant_id=oauth2_grant.id"),
|
||||
// find stopwatches without existing user
|
||||
genericOrphanCheck("Orphaned Stopwatches without existing User",
|
||||
"stopwatch", "user", "stopwatch.user_id=`user`.id"),
|
||||
// find stopwatches without existing issue
|
||||
genericOrphanCheck("Orphaned Stopwatches without existing Issue",
|
||||
"stopwatch", "issue", "stopwatch.issue_id=`issue`.id"),
|
||||
// find redirects without existing user.
|
||||
genericOrphanCheck("Orphaned Redirects without existing redirect user",
|
||||
"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
|
||||
)
|
||||
|
||||
for _, c := range consistencyChecks {
|
||||
if err := c.Run(ctx, logger, autofix); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check consistency of database",
|
||||
Name: "check-db-consistency",
|
||||
IsDefault: false,
|
||||
Run: checkDBConsistency,
|
||||
Priority: 3,
|
||||
})
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/migrations"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
func checkDBVersion(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
logger.Info("Expected database version: %d", migrations.ExpectedVersion())
|
||||
if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
|
||||
if !autofix {
|
||||
logger.Critical("Error: %v during ensure up to date", err)
|
||||
return err
|
||||
}
|
||||
logger.Warn("Got Error: %v during ensure up to date", err)
|
||||
logger.Warn("Attempting to migrate to the latest DB version to fix this.")
|
||||
|
||||
err = db.InitEngineWithMigration(ctx, migrations.Migrate)
|
||||
if err != nil {
|
||||
logger.Critical("Error: %v during migration", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check Database Version",
|
||||
Name: "check-db-version",
|
||||
IsDefault: true,
|
||||
Run: checkDBVersion,
|
||||
AbortIfFailed: false,
|
||||
Priority: 2,
|
||||
})
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// Check represents a Doctor check
|
||||
type Check struct {
|
||||
Title string
|
||||
Name string
|
||||
IsDefault bool
|
||||
Run func(ctx context.Context, logger log.Logger, autofix bool) error
|
||||
AbortIfFailed bool
|
||||
SkipDatabaseInitialization bool
|
||||
Priority int
|
||||
}
|
||||
|
||||
func initDBSkipLogger(ctx context.Context) error {
|
||||
setting.MustInstalled()
|
||||
setting.LoadDBSetting()
|
||||
if err := db.InitEngine(ctx); err != nil {
|
||||
return fmt.Errorf("db.InitEngine: %w", err)
|
||||
}
|
||||
// some doctor sub-commands need to use git command
|
||||
if err := git.InitFull(ctx); err != nil {
|
||||
return fmt.Errorf("git.InitFull: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type doctorCheckLogger struct {
|
||||
colorize bool
|
||||
}
|
||||
|
||||
var _ log.BaseLogger = (*doctorCheckLogger)(nil)
|
||||
|
||||
func (d *doctorCheckLogger) Log(skip int, level log.Level, format string, v ...any) {
|
||||
_, _ = fmt.Fprintf(os.Stdout, format+"\n", v...)
|
||||
}
|
||||
|
||||
func (d *doctorCheckLogger) GetLevel() log.Level {
|
||||
return log.TRACE
|
||||
}
|
||||
|
||||
type doctorCheckStepLogger struct {
|
||||
colorize bool
|
||||
}
|
||||
|
||||
var _ log.BaseLogger = (*doctorCheckStepLogger)(nil)
|
||||
|
||||
func (d *doctorCheckStepLogger) Log(skip int, level log.Level, format string, v ...any) {
|
||||
levelChar := fmt.Sprintf("[%s]", strings.ToUpper(level.String()[0:1]))
|
||||
var levelArg any = levelChar
|
||||
if d.colorize {
|
||||
levelArg = log.NewColoredValue(levelChar, level.ColorAttributes()...)
|
||||
}
|
||||
args := append([]any{levelArg}, v...)
|
||||
_, _ = fmt.Fprintf(os.Stdout, " - %s "+format+"\n", args...)
|
||||
}
|
||||
|
||||
func (d *doctorCheckStepLogger) GetLevel() log.Level {
|
||||
return log.TRACE
|
||||
}
|
||||
|
||||
// Checks is the list of available commands
|
||||
var Checks []*Check
|
||||
|
||||
// RunChecks runs the doctor checks for the provided list
|
||||
func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) error {
|
||||
// the checks output logs by a special logger, they do not use the default logger
|
||||
logger := log.BaseLoggerToGeneralLogger(&doctorCheckLogger{colorize: colorize})
|
||||
loggerStep := log.BaseLoggerToGeneralLogger(&doctorCheckStepLogger{colorize: colorize})
|
||||
dbIsInit := false
|
||||
for i, check := range checks {
|
||||
if !dbIsInit && !check.SkipDatabaseInitialization {
|
||||
// Only open database after the most basic configuration check
|
||||
if err := initDBSkipLogger(ctx); err != nil {
|
||||
logger.Error("Error whilst initializing the database: %v", err)
|
||||
logger.Error("Check if you are using the right config file. You can use a --config directive to specify one.")
|
||||
return nil
|
||||
}
|
||||
dbIsInit = true
|
||||
}
|
||||
logger.Info("\n[%d] %s", i+1, check.Title)
|
||||
if err := check.Run(ctx, loggerStep, autofix); err != nil {
|
||||
if check.AbortIfFailed {
|
||||
logger.Critical("FAIL")
|
||||
return err
|
||||
}
|
||||
logger.Error("ERROR")
|
||||
} else {
|
||||
logger.Info("OK")
|
||||
}
|
||||
}
|
||||
logger.Info("\nAll done.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register registers a command with the list
|
||||
func Register(command *Check) {
|
||||
Checks = append(Checks, command)
|
||||
sort.SliceStable(Checks, func(i, j int) bool {
|
||||
if Checks[i].Priority == Checks[j].Priority {
|
||||
return Checks[i].Name < Checks[j].Name
|
||||
}
|
||||
if Checks[i].Priority == 0 {
|
||||
return false
|
||||
}
|
||||
return Checks[i].Priority < Checks[j].Priority
|
||||
})
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// #16831 revealed that the dump command that was broken in 1.14.3-1.14.6 and 1.15.0 (#15885).
|
||||
// This led to repo_unit and login_source cfg not being converted to JSON in the dump
|
||||
// Unfortunately although it was hoped that there were only a few users affected it
|
||||
// appears that many users are affected.
|
||||
|
||||
// We therefore need to provide a doctor command to fix this repeated issue #16961
|
||||
|
||||
func parseBool16961(bs []byte) (bool, error) {
|
||||
if bytes.EqualFold(bs, []byte("%!s(bool=false)")) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if bytes.EqualFold(bs, []byte("%!s(bool=true)")) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("unexpected bool format: %s", string(bs))
|
||||
}
|
||||
|
||||
func fixUnitConfig16961(bs []byte, cfg *repo_model.UnitConfig) (fixed bool, err error) {
|
||||
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Handle #16961
|
||||
if string(bs) != "&{}" && len(bs) != 0 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func fixExternalWikiConfig16961(bs []byte, cfg *repo_model.ExternalWikiConfig) (fixed bool, err error) {
|
||||
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(bs) < 3 {
|
||||
return false, err
|
||||
}
|
||||
if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
|
||||
return false, err
|
||||
}
|
||||
cfg.ExternalWikiURL = string(bs[2 : len(bs)-1])
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func fixExternalTrackerConfig16961(bs []byte, cfg *repo_model.ExternalTrackerConfig) (fixed bool, err error) {
|
||||
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
// Handle #16961
|
||||
if len(bs) < 3 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
|
||||
return false, err
|
||||
}
|
||||
|
||||
parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
|
||||
if len(parts) != 3 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
cfg.ExternalTrackerURL = string(bytes.Join(parts[:len(parts)-2], []byte{' '}))
|
||||
cfg.ExternalTrackerFormat = string(parts[len(parts)-2])
|
||||
cfg.ExternalTrackerStyle = string(parts[len(parts)-1])
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func fixPullRequestsConfig16961(bs []byte, cfg *repo_model.PullRequestsConfig) (fixed bool, err error) {
|
||||
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Handle #16961
|
||||
if len(bs) < 3 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// PullRequestsConfig was the following in 1.14
|
||||
// type PullRequestsConfig struct {
|
||||
// IgnoreWhitespaceConflicts bool
|
||||
// AllowMerge bool
|
||||
// AllowRebase bool
|
||||
// AllowRebaseMerge bool
|
||||
// AllowSquash bool
|
||||
// AllowManualMerge bool
|
||||
// AutodetectManualMerge bool
|
||||
// }
|
||||
//
|
||||
// 1.15 added in addition:
|
||||
// DefaultDeleteBranchAfterMerge bool
|
||||
// DefaultMergeStyle MergeStyle
|
||||
parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
|
||||
if len(parts) < 7 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var parseErr error
|
||||
cfg.IgnoreWhitespaceConflicts, parseErr = parseBool16961(parts[0])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
cfg.AllowMerge, parseErr = parseBool16961(parts[1])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
cfg.AllowRebase, parseErr = parseBool16961(parts[2])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
cfg.AllowRebaseMerge, parseErr = parseBool16961(parts[3])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
cfg.AllowSquash, parseErr = parseBool16961(parts[4])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
cfg.AllowManualMerge, parseErr = parseBool16961(parts[5])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
cfg.AutodetectManualMerge, parseErr = parseBool16961(parts[6])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
|
||||
// 1.14 unit
|
||||
if len(parts) == 7 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(parts) < 9 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
cfg.DefaultDeleteBranchAfterMerge, parseErr = parseBool16961(parts[7])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
|
||||
cfg.DefaultMergeStyle = repo_model.MergeStyle(string(bytes.Join(parts[8:], []byte{' '})))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func fixIssuesConfig16961(bs []byte, cfg *repo_model.IssuesConfig) (fixed bool, err error) {
|
||||
err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Handle #16961
|
||||
if len(bs) < 3 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' {
|
||||
return false, err
|
||||
}
|
||||
|
||||
parts := bytes.Split(bs[2:len(bs)-1], []byte{' '})
|
||||
if len(parts) != 3 {
|
||||
return false, err
|
||||
}
|
||||
var parseErr error
|
||||
cfg.EnableTimetracker, parseErr = parseBool16961(parts[0])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
cfg.AllowOnlyContributorsToTrackTime, parseErr = parseBool16961(parts[1])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
cfg.EnableDependencies, parseErr = parseBool16961(parts[2])
|
||||
if parseErr != nil {
|
||||
return false, errors.Join(err, parseErr)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func fixBrokenRepoUnit16961(repoUnit *repo_model.RepoUnit, bs []byte) (fixed bool, err error) {
|
||||
// Shortcut empty or null values
|
||||
if len(bs) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
switch repoUnit.Type {
|
||||
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects:
|
||||
cfg := &repo_model.UnitConfig{}
|
||||
repoUnit.Config = cfg
|
||||
if fixed, err := fixUnitConfig16961(bs, cfg); !fixed {
|
||||
return false, err
|
||||
}
|
||||
case unit.TypeExternalWiki:
|
||||
cfg := &repo_model.ExternalWikiConfig{}
|
||||
repoUnit.Config = cfg
|
||||
|
||||
if fixed, err := fixExternalWikiConfig16961(bs, cfg); !fixed {
|
||||
return false, err
|
||||
}
|
||||
case unit.TypeExternalTracker:
|
||||
cfg := &repo_model.ExternalTrackerConfig{}
|
||||
repoUnit.Config = cfg
|
||||
if fixed, err := fixExternalTrackerConfig16961(bs, cfg); !fixed {
|
||||
return false, err
|
||||
}
|
||||
case unit.TypePullRequests:
|
||||
cfg := &repo_model.PullRequestsConfig{}
|
||||
repoUnit.Config = cfg
|
||||
|
||||
if fixed, err := fixPullRequestsConfig16961(bs, cfg); !fixed {
|
||||
return false, err
|
||||
}
|
||||
case unit.TypeIssues:
|
||||
cfg := &repo_model.IssuesConfig{}
|
||||
repoUnit.Config = cfg
|
||||
if fixed, err := fixIssuesConfig16961(bs, cfg); !fixed {
|
||||
return false, err
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized repo unit type: %v", repoUnit.Type))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func fixBrokenRepoUnits16961(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
// RepoUnit describes all units of a repository
|
||||
type RepoUnit struct {
|
||||
ID int64
|
||||
RepoID int64
|
||||
Type unit.Type
|
||||
Config []byte
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
}
|
||||
|
||||
count := 0
|
||||
|
||||
err := db.Iterate(
|
||||
ctx,
|
||||
builder.Gt{
|
||||
"id": 0,
|
||||
},
|
||||
func(ctx context.Context, unit *RepoUnit) error {
|
||||
bs := unit.Config
|
||||
repoUnit := &repo_model.RepoUnit{
|
||||
ID: unit.ID,
|
||||
RepoID: unit.RepoID,
|
||||
Type: unit.Type,
|
||||
CreatedUnix: unit.CreatedUnix,
|
||||
}
|
||||
|
||||
if fixed, err := fixBrokenRepoUnit16961(repoUnit, bs); !fixed {
|
||||
return err
|
||||
}
|
||||
|
||||
count++
|
||||
if !autofix {
|
||||
return nil
|
||||
}
|
||||
|
||||
return repo_model.UpdateRepoUnit(ctx, repoUnit)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.Critical("Unable to iterate across repounits to fix the broken units: Error %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !autofix {
|
||||
if count == 0 {
|
||||
logger.Info("Found no broken repo_units")
|
||||
} else {
|
||||
logger.Warn("Found %d broken repo_units", count)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
logger.Info("Fixed %d broken repo_units", count)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check for incorrectly dumped repo_units (See #16961)",
|
||||
Name: "fix-broken-repo-units",
|
||||
IsDefault: false,
|
||||
Run: fixBrokenRepoUnits16961,
|
||||
Priority: 7,
|
||||
})
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_fixUnitConfig_16961(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bs string
|
||||
wantFixed bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
bs: "",
|
||||
wantFixed: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "normal: {}",
|
||||
bs: "{}",
|
||||
wantFixed: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "broken but fixable: &{}",
|
||||
bs: "&{}",
|
||||
wantFixed: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "broken but unfixable: &{asdasd}",
|
||||
bs: "&{asdasd}",
|
||||
wantFixed: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotFixed, err := fixUnitConfig16961([]byte(tt.bs), &repo_model.UnitConfig{})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("fixUnitConfig_16961() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotFixed != tt.wantFixed {
|
||||
t.Errorf("fixUnitConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fixExternalWikiConfig_16961(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bs string
|
||||
expected string
|
||||
wantFixed bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "normal: {\"ExternalWikiURL\":\"http://someurl\"}",
|
||||
bs: "{\"ExternalWikiURL\":\"http://someurl\"}",
|
||||
expected: "http://someurl",
|
||||
wantFixed: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "broken: &{http://someurl}",
|
||||
bs: "&{http://someurl}",
|
||||
expected: "http://someurl",
|
||||
wantFixed: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "broken but unfixable: http://someurl",
|
||||
bs: "http://someurl",
|
||||
wantFixed: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &repo_model.ExternalWikiConfig{}
|
||||
gotFixed, err := fixExternalWikiConfig16961([]byte(tt.bs), cfg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("fixExternalWikiConfig_16961() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotFixed != tt.wantFixed {
|
||||
t.Errorf("fixExternalWikiConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
|
||||
}
|
||||
if cfg.ExternalWikiURL != tt.expected {
|
||||
t.Errorf("fixExternalWikiConfig_16961().ExternalWikiURL = %v, want %v", cfg.ExternalWikiURL, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fixExternalTrackerConfig_16961(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bs string
|
||||
expected repo_model.ExternalTrackerConfig
|
||||
wantFixed bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "normal",
|
||||
bs: `{"ExternalTrackerURL":"a","ExternalTrackerFormat":"b","ExternalTrackerStyle":"c"}`,
|
||||
expected: repo_model.ExternalTrackerConfig{
|
||||
ExternalTrackerURL: "a",
|
||||
ExternalTrackerFormat: "b",
|
||||
ExternalTrackerStyle: "c",
|
||||
},
|
||||
wantFixed: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "broken",
|
||||
bs: "&{a b c}",
|
||||
expected: repo_model.ExternalTrackerConfig{
|
||||
ExternalTrackerURL: "a",
|
||||
ExternalTrackerFormat: "b",
|
||||
ExternalTrackerStyle: "c",
|
||||
},
|
||||
wantFixed: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "broken - too many fields",
|
||||
bs: "&{a b c d}",
|
||||
wantFixed: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "broken - wrong format",
|
||||
bs: "a b c d}",
|
||||
wantFixed: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &repo_model.ExternalTrackerConfig{}
|
||||
gotFixed, err := fixExternalTrackerConfig16961([]byte(tt.bs), cfg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("fixExternalTrackerConfig_16961() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotFixed != tt.wantFixed {
|
||||
t.Errorf("fixExternalTrackerConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
|
||||
}
|
||||
if cfg.ExternalTrackerFormat != tt.expected.ExternalTrackerFormat {
|
||||
t.Errorf("fixExternalTrackerConfig_16961().ExternalTrackerFormat = %v, want %v", tt.expected.ExternalTrackerFormat, cfg.ExternalTrackerFormat)
|
||||
}
|
||||
if cfg.ExternalTrackerStyle != tt.expected.ExternalTrackerStyle {
|
||||
t.Errorf("fixExternalTrackerConfig_16961().ExternalTrackerStyle = %v, want %v", tt.expected.ExternalTrackerStyle, cfg.ExternalTrackerStyle)
|
||||
}
|
||||
if cfg.ExternalTrackerURL != tt.expected.ExternalTrackerURL {
|
||||
t.Errorf("fixExternalTrackerConfig_16961().ExternalTrackerURL = %v, want %v", tt.expected.ExternalTrackerURL, cfg.ExternalTrackerURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fixPullRequestsConfig_16961(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bs string
|
||||
expected repo_model.PullRequestsConfig
|
||||
wantFixed bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "normal",
|
||||
bs: `{"IgnoreWhitespaceConflicts":false,"AllowMerge":false,"AllowRebase":false,"AllowRebaseMerge":false,"AllowSquash":false,"AllowManualMerge":false,"AutodetectManualMerge":false,"DefaultDeleteBranchAfterMerge":false,"DefaultMergeStyle":""}`,
|
||||
},
|
||||
{
|
||||
name: "broken - 1.14",
|
||||
bs: `&{%!s(bool=false) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=false) %!s(bool=false)}`,
|
||||
expected: repo_model.PullRequestsConfig{
|
||||
IgnoreWhitespaceConflicts: false,
|
||||
AllowMerge: true,
|
||||
AllowRebase: true,
|
||||
AllowRebaseMerge: true,
|
||||
AllowSquash: true,
|
||||
AllowManualMerge: false,
|
||||
AutodetectManualMerge: false,
|
||||
},
|
||||
wantFixed: true,
|
||||
},
|
||||
{
|
||||
name: "broken - 1.15",
|
||||
bs: `&{%!s(bool=false) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=false) %!s(bool=false) %!s(bool=false) merge}`,
|
||||
expected: repo_model.PullRequestsConfig{
|
||||
AllowMerge: true,
|
||||
AllowRebase: true,
|
||||
AllowRebaseMerge: true,
|
||||
AllowSquash: true,
|
||||
DefaultMergeStyle: repo_model.MergeStyleMerge,
|
||||
},
|
||||
wantFixed: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &repo_model.PullRequestsConfig{}
|
||||
gotFixed, err := fixPullRequestsConfig16961([]byte(tt.bs), cfg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("fixPullRequestsConfig_16961() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotFixed != tt.wantFixed {
|
||||
t.Errorf("fixPullRequestsConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
|
||||
}
|
||||
assert.EqualValues(t, &tt.expected, cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fixIssuesConfig_16961(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bs string
|
||||
expected repo_model.IssuesConfig
|
||||
wantFixed bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "normal",
|
||||
bs: `{"EnableTimetracker":true,"AllowOnlyContributorsToTrackTime":true,"EnableDependencies":true}`,
|
||||
expected: repo_model.IssuesConfig{
|
||||
EnableTimetracker: true,
|
||||
AllowOnlyContributorsToTrackTime: true,
|
||||
EnableDependencies: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "broken",
|
||||
bs: `&{%!s(bool=true) %!s(bool=true) %!s(bool=true)}`,
|
||||
expected: repo_model.IssuesConfig{
|
||||
EnableTimetracker: true,
|
||||
AllowOnlyContributorsToTrackTime: true,
|
||||
EnableDependencies: true,
|
||||
},
|
||||
wantFixed: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &repo_model.IssuesConfig{}
|
||||
gotFixed, err := fixIssuesConfig16961([]byte(tt.bs), cfg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("fixIssuesConfig_16961() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotFixed != tt.wantFixed {
|
||||
t.Errorf("fixIssuesConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
|
||||
}
|
||||
assert.EqualValues(t, &tt.expected, cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func fixOwnerTeamCreateOrgRepo(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
count := 0
|
||||
|
||||
err := db.Iterate(
|
||||
ctx,
|
||||
builder.Eq{"authorize": perm.AccessModeOwner, "can_create_org_repo": false},
|
||||
func(ctx context.Context, team *org_model.Team) error {
|
||||
team.CanCreateOrgRepo = true
|
||||
count++
|
||||
|
||||
if !autofix {
|
||||
return nil
|
||||
}
|
||||
|
||||
return models.UpdateTeam(ctx, team, false, false)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.Critical("Unable to iterate across repounits to fix incorrect can_create_org_repo: Error %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !autofix {
|
||||
if count == 0 {
|
||||
logger.Info("Found no team with incorrect can_create_org_repo")
|
||||
} else {
|
||||
logger.Warn("Found %d teams with incorrect can_create_org_repo", count)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
logger.Info("Fixed %d teams with incorrect can_create_org_repo", count)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check for incorrect can_create_org_repo for org owner teams",
|
||||
Name: "fix-owner-team-create-org-repo",
|
||||
IsDefault: false,
|
||||
Run: fixOwnerTeamCreateOrgRepo,
|
||||
Priority: 7,
|
||||
})
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
numRepos := 0
|
||||
numHeadsBroken := 0
|
||||
numDefaultBranchesBroken := 0
|
||||
numReposUpdated := 0
|
||||
err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
|
||||
numRepos++
|
||||
_, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse").AddDashesAndList(repo.DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
|
||||
|
||||
head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
|
||||
|
||||
// what we expect: default branch is valid, and HEAD points to it
|
||||
if headErr == nil && defaultBranchErr == nil && head == repo.DefaultBranch {
|
||||
return nil
|
||||
}
|
||||
|
||||
if headErr != nil {
|
||||
numHeadsBroken++
|
||||
}
|
||||
if defaultBranchErr != nil {
|
||||
numDefaultBranchesBroken++
|
||||
}
|
||||
|
||||
// if default branch is broken, let the user fix that in the UI
|
||||
if defaultBranchErr != nil {
|
||||
logger.Warn("Default branch for %s/%s doesn't point to a valid commit", repo.OwnerName, repo.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we're not autofixing, that's all we can do
|
||||
if !autofix {
|
||||
return nil
|
||||
}
|
||||
|
||||
// otherwise, let's try fixing HEAD
|
||||
err := git.NewCommand(ctx, "symbolic-ref").AddDashesAndList("HEAD", git.BranchPrefix+repo.DefaultBranch).Run(&git.RunOpts{Dir: repo.RepoPath()})
|
||||
if err != nil {
|
||||
logger.Warn("Failed to fix HEAD for %s/%s: %v", repo.OwnerName, repo.Name, err)
|
||||
return nil
|
||||
}
|
||||
numReposUpdated++
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Critical("Error when fixing repo HEADs: %v", err)
|
||||
}
|
||||
|
||||
if autofix {
|
||||
logger.Info("Out of %d repos, HEADs for %d are now fixed and HEADS for %d are still broken", numRepos, numReposUpdated, numDefaultBranchesBroken+numHeadsBroken-numReposUpdated)
|
||||
} else {
|
||||
if numHeadsBroken == 0 && numDefaultBranchesBroken == 0 {
|
||||
logger.Info("All %d repos have their HEADs in the correct state", numRepos)
|
||||
} else {
|
||||
if numHeadsBroken == 0 && numDefaultBranchesBroken != 0 {
|
||||
logger.Critical("Default branches are broken for %d/%d repos", numDefaultBranchesBroken, numRepos)
|
||||
} else if numHeadsBroken != 0 && numDefaultBranchesBroken == 0 {
|
||||
logger.Warn("HEADs are broken for %d/%d repos", numHeadsBroken, numRepos)
|
||||
} else {
|
||||
logger.Critical("Out of %d repos, HEADS are broken for %d and default branches are broken for %d", numRepos, numHeadsBroken, numDefaultBranchesBroken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Synchronize repo HEADs",
|
||||
Name: "synchronize-repo-heads",
|
||||
IsDefault: true,
|
||||
Run: synchronizeRepoHeads,
|
||||
Priority: 7,
|
||||
})
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Garbage collect LFS",
|
||||
Name: "gc-lfs",
|
||||
IsDefault: false,
|
||||
Run: garbageCollectLFSCheck,
|
||||
AbortIfFailed: false,
|
||||
SkipDatabaseInitialization: false,
|
||||
Priority: 1,
|
||||
})
|
||||
}
|
||||
|
||||
func garbageCollectLFSCheck(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
if !setting.LFS.StartServer {
|
||||
return fmt.Errorf("LFS support is disabled")
|
||||
}
|
||||
|
||||
if err := repository.GarbageCollectLFSMetaObjects(ctx, repository.GarbageCollectLFSMetaObjectsOptions{
|
||||
LogDetail: logger.Info,
|
||||
AutoFix: autofix,
|
||||
// Only attempt to garbage collect lfs meta objects older than a week as the order of git lfs upload
|
||||
// and git object upload is not necessarily guaranteed. It's possible to imagine a situation whereby
|
||||
// an LFS object is uploaded but the git branch is not uploaded immediately, or there are some rapid
|
||||
// changes in new branches that might lead to lfs objects becoming temporarily unassociated with git
|
||||
// objects.
|
||||
//
|
||||
// It is likely that a week is potentially excessive but it should definitely be enough that any
|
||||
// unassociated LFS object is genuinely unassociated.
|
||||
OlderThan: time.Now().Add(-24 * time.Hour * 7),
|
||||
// We don't set the UpdatedLessRecentlyThan because we want to do a full GC
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return checkStorage(&checkStorageOptions{LFS: true})(ctx, logger, autofix)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func iteratePRs(ctx context.Context, repo *repo_model.Repository, each func(*repo_model.Repository, *issues_model.PullRequest) error) error {
|
||||
return db.Iterate(
|
||||
ctx,
|
||||
builder.Eq{"base_repo_id": repo.ID},
|
||||
func(ctx context.Context, bean *issues_model.PullRequest) error {
|
||||
return each(repo, bean)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func checkPRMergeBase(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
numRepos := 0
|
||||
numPRs := 0
|
||||
numPRsUpdated := 0
|
||||
err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
|
||||
numRepos++
|
||||
return iteratePRs(ctx, repo, func(repo *repo_model.Repository, pr *issues_model.PullRequest) error {
|
||||
numPRs++
|
||||
pr.BaseRepo = repo
|
||||
repoPath := repo.RepoPath()
|
||||
|
||||
oldMergeBase := pr.MergeBase
|
||||
|
||||
if !pr.HasMerged {
|
||||
var err error
|
||||
pr.MergeBase, _, err = git.NewCommand(ctx, "merge-base").AddDashesAndList(pr.BaseBranch, pr.GetGitRefName()).RunStdString(&git.RunOpts{Dir: repoPath})
|
||||
if err != nil {
|
||||
var err2 error
|
||||
pr.MergeBase, _, err2 = git.NewCommand(ctx, "rev-parse").AddDynamicArguments(git.BranchPrefix + pr.BaseBranch).RunStdString(&git.RunOpts{Dir: repoPath})
|
||||
if err2 != nil {
|
||||
logger.Warn("Unable to get merge base for PR ID %d, #%d onto %s in %s/%s. Error: %v & %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err, err2)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parentsString, _, err := git.NewCommand(ctx, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath})
|
||||
if err != nil {
|
||||
logger.Warn("Unable to get parents for merged PR ID %d, #%d onto %s in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err)
|
||||
return nil
|
||||
}
|
||||
parents := strings.Split(strings.TrimSpace(parentsString), " ")
|
||||
if len(parents) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
refs := append([]string{}, parents[1:]...)
|
||||
refs = append(refs, pr.GetGitRefName())
|
||||
cmd := git.NewCommand(ctx, "merge-base").AddDashesAndList(refs...)
|
||||
pr.MergeBase, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath})
|
||||
if err != nil {
|
||||
logger.Warn("Unable to get merge base for merged PR ID %d, #%d onto %s in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
pr.MergeBase = strings.TrimSpace(pr.MergeBase)
|
||||
if pr.MergeBase != oldMergeBase {
|
||||
if autofix {
|
||||
if err := pr.UpdateCols(ctx, "merge_base"); err != nil {
|
||||
logger.Critical("Failed to update merge_base. ERROR: %v", err)
|
||||
return fmt.Errorf("Failed to update merge_base. ERROR: %w", err)
|
||||
}
|
||||
} else {
|
||||
logger.Info("#%d onto %s in %s/%s: MergeBase should be %s but is %s", pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, oldMergeBase, pr.MergeBase)
|
||||
}
|
||||
numPRsUpdated++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
if autofix {
|
||||
logger.Info("%d PR mergebases updated of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
|
||||
} else {
|
||||
if numPRsUpdated == 0 {
|
||||
logger.Info("All %d PRs in %d repos have a correct mergebase", numPRs, numRepos)
|
||||
} else if err == nil {
|
||||
logger.Critical("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
|
||||
return fmt.Errorf("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
|
||||
} else {
|
||||
logger.Warn("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Recalculate merge bases",
|
||||
Name: "recalculate-merge-bases",
|
||||
IsDefault: false,
|
||||
Run: checkPRMergeBase,
|
||||
Priority: 7,
|
||||
})
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func iterateRepositories(ctx context.Context, each func(*repo_model.Repository) error) error {
|
||||
err := db.Iterate(
|
||||
ctx,
|
||||
builder.Gt{"id": 0},
|
||||
func(ctx context.Context, bean *repo_model.Repository) error {
|
||||
return each(bean)
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkScriptType(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
path, err := exec.LookPath(setting.ScriptType)
|
||||
if err != nil {
|
||||
logger.Critical("ScriptType \"%q\" is not on the current PATH. Error: %v", setting.ScriptType, err)
|
||||
return fmt.Errorf("ScriptType \"%q\" is not on the current PATH. Error: %w", setting.ScriptType, err)
|
||||
}
|
||||
logger.Info("ScriptType %s is on the current PATH at %s", setting.ScriptType, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkHooks(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
|
||||
results, err := repository.CheckDelegateHooks(repo.RepoPath())
|
||||
if err != nil {
|
||||
logger.Critical("Unable to check delegate hooks for repo %-v. ERROR: %v", repo, err)
|
||||
return fmt.Errorf("Unable to check delegate hooks for repo %-v. ERROR: %w", repo, err)
|
||||
}
|
||||
if len(results) > 0 && autofix {
|
||||
logger.Warn("Regenerated hooks for %s", repo.FullName())
|
||||
if err := repository.CreateDelegateHooks(repo.RepoPath()); err != nil {
|
||||
logger.Critical("Unable to recreate delegate hooks for %-v. ERROR: %v", repo, err)
|
||||
return fmt.Errorf("Unable to recreate delegate hooks for %-v. ERROR: %w", repo, err)
|
||||
}
|
||||
}
|
||||
for _, result := range results {
|
||||
logger.Warn(result)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Critical("Errors noted whilst checking delegate hooks.")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkUserStarNum(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
if autofix {
|
||||
if err := models.DoctorUserStarNum(ctx); err != nil {
|
||||
logger.Critical("Unable update User Stars numbers")
|
||||
return err
|
||||
}
|
||||
logger.Info("Updated User Stars numbers.")
|
||||
} else {
|
||||
logger.Info("No check available for User Stars numbers (skipped)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEnablePushOptions(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
numRepos := 0
|
||||
numNeedUpdate := 0
|
||||
|
||||
if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
|
||||
numRepos++
|
||||
r, err := git.OpenRepository(ctx, repo.RepoPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if autofix {
|
||||
_, _, err := git.NewCommand(ctx, "config", "receive.advertisePushOptions", "true").RunStdString(&git.RunOpts{Dir: r.Path})
|
||||
return err
|
||||
}
|
||||
|
||||
value, _, err := git.NewCommand(ctx, "config", "receive.advertisePushOptions").RunStdString(&git.RunOpts{Dir: r.Path})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, valid := git.ParseBool(strings.TrimSpace(value))
|
||||
if !result || !valid {
|
||||
numNeedUpdate++
|
||||
logger.Info("%s: does not have receive.advertisePushOptions set correctly: %q", repo.FullName(), value)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Critical("Unable to EnablePushOptions: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if autofix {
|
||||
logger.Info("Enabled push options for %d repositories.", numRepos)
|
||||
} else {
|
||||
logger.Info("Checked %d repositories, %d need updates.", numRepos, numNeedUpdate)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
numRepos := 0
|
||||
numNeedUpdate := 0
|
||||
cache, err := lru.New[int64, any](512)
|
||||
if err != nil {
|
||||
logger.Critical("Unable to create cache: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
|
||||
numRepos++
|
||||
|
||||
if owner, has := cache.Get(repo.OwnerID); has {
|
||||
repo.Owner = owner.(*user_model.User)
|
||||
} else {
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
cache.Add(repo.OwnerID, repo.Owner)
|
||||
}
|
||||
|
||||
// Create/Remove git-daemon-export-ok for git-daemon...
|
||||
daemonExportFile := path.Join(repo.RepoPath(), `git-daemon-export-ok`)
|
||||
isExist, err := util.IsExist(daemonExportFile)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
|
||||
return err
|
||||
}
|
||||
isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic
|
||||
|
||||
if isPublic != isExist {
|
||||
numNeedUpdate++
|
||||
if autofix {
|
||||
if !isPublic && isExist {
|
||||
if err = util.Remove(daemonExportFile); err != nil {
|
||||
log.Error("Failed to remove %s: %v", daemonExportFile, err)
|
||||
}
|
||||
} else if isPublic && !isExist {
|
||||
if f, err := os.Create(daemonExportFile); err != nil {
|
||||
log.Error("Failed to create %s: %v", daemonExportFile, err)
|
||||
} else {
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Critical("Unable to checkDaemonExport: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if autofix {
|
||||
logger.Info("Updated git-daemon-export-ok files for %d of %d repositories.", numNeedUpdate, numRepos)
|
||||
} else {
|
||||
logger.Info("Checked %d repositories, %d need updates.", numRepos, numNeedUpdate)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
numRepos := 0
|
||||
numNeedUpdate := 0
|
||||
numWritten := 0
|
||||
if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
|
||||
numRepos++
|
||||
|
||||
commitGraphExists := func() (bool, error) {
|
||||
// Check commit-graph exists
|
||||
commitGraphFile := path.Join(repo.RepoPath(), `objects/info/commit-graph`)
|
||||
isExist, err := util.IsExist(commitGraphFile)
|
||||
if err != nil {
|
||||
logger.Error("Unable to check if %s exists. Error: %v", commitGraphFile, err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !isExist {
|
||||
commitGraphsDir := path.Join(repo.RepoPath(), `objects/info/commit-graphs`)
|
||||
isExist, err = util.IsExist(commitGraphsDir)
|
||||
if err != nil {
|
||||
logger.Error("Unable to check if %s exists. Error: %v", commitGraphsDir, err)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return isExist, nil
|
||||
}
|
||||
|
||||
isExist, err := commitGraphExists()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isExist {
|
||||
numNeedUpdate++
|
||||
if autofix {
|
||||
if err := git.WriteCommitGraph(ctx, repo.RepoPath()); err != nil {
|
||||
logger.Error("Unable to write commit-graph in %s. Error: %v", repo.FullName(), err)
|
||||
return err
|
||||
}
|
||||
isExist, err := commitGraphExists()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isExist {
|
||||
numWritten++
|
||||
logger.Info("Commit-graph written: %s", repo.FullName())
|
||||
} else {
|
||||
logger.Warn("No commit-graph written: %s", repo.FullName())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Critical("Unable to checkCommitGraph: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if autofix {
|
||||
logger.Info("Wrote commit-graph files for %d of %d repositories.", numWritten, numRepos)
|
||||
} else {
|
||||
logger.Info("Checked %d repositories, %d without commit-graphs.", numRepos, numNeedUpdate)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check if SCRIPT_TYPE is available",
|
||||
Name: "script-type",
|
||||
IsDefault: false,
|
||||
Run: checkScriptType,
|
||||
Priority: 5,
|
||||
})
|
||||
Register(&Check{
|
||||
Title: "Check if hook files are up-to-date and executable",
|
||||
Name: "hooks",
|
||||
IsDefault: false,
|
||||
Run: checkHooks,
|
||||
Priority: 6,
|
||||
})
|
||||
Register(&Check{
|
||||
Title: "Recalculate Stars number for all user",
|
||||
Name: "recalculate-stars-number",
|
||||
IsDefault: false,
|
||||
Run: checkUserStarNum,
|
||||
Priority: 6,
|
||||
})
|
||||
Register(&Check{
|
||||
Title: "Check that all git repositories have receive.advertisePushOptions set to true",
|
||||
Name: "enable-push-options",
|
||||
IsDefault: false,
|
||||
Run: checkEnablePushOptions,
|
||||
Priority: 7,
|
||||
})
|
||||
Register(&Check{
|
||||
Title: "Check git-daemon-export-ok files",
|
||||
Name: "check-git-daemon-export-ok",
|
||||
IsDefault: false,
|
||||
Run: checkDaemonExport,
|
||||
Priority: 8,
|
||||
})
|
||||
Register(&Check{
|
||||
Title: "Check commit-graphs",
|
||||
Name: "check-commit-graphs",
|
||||
IsDefault: false,
|
||||
Run: checkCommitGraph,
|
||||
Priority: 9,
|
||||
})
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
type configurationFile struct {
|
||||
Name string
|
||||
Path string
|
||||
IsDirectory bool
|
||||
Required bool
|
||||
Writable bool
|
||||
}
|
||||
|
||||
func checkConfigurationFile(logger log.Logger, autofix bool, fileOpts configurationFile) error {
|
||||
logger.Info(`%-26s %q`, log.NewColoredValue(fileOpts.Name+":", log.Reset), fileOpts.Path)
|
||||
fi, err := os.Stat(fileOpts.Path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) && autofix && fileOpts.IsDirectory {
|
||||
if err := os.MkdirAll(fileOpts.Path, 0o777); err != nil {
|
||||
logger.Error(" Directory does not exist and could not be created. ERROR: %v", err)
|
||||
return fmt.Errorf("Configuration directory: \"%q\" does not exist and could not be created. ERROR: %w", fileOpts.Path, err)
|
||||
}
|
||||
fi, err = os.Stat(fileOpts.Path)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if fileOpts.Required {
|
||||
logger.Error(" Is REQUIRED but is not accessible. ERROR: %v", err)
|
||||
return fmt.Errorf("Configuration file \"%q\" is not accessible but is required. Error: %w", fileOpts.Path, err)
|
||||
}
|
||||
logger.Warn(" NOTICE: is not accessible (Error: %v)", err)
|
||||
// this is a non-critical error
|
||||
return nil
|
||||
}
|
||||
|
||||
if fileOpts.IsDirectory && !fi.IsDir() {
|
||||
logger.Error(" ERROR: not a directory")
|
||||
return fmt.Errorf("Configuration directory \"%q\" is not a directory. Error: %w", fileOpts.Path, err)
|
||||
} else if !fileOpts.IsDirectory && !fi.Mode().IsRegular() {
|
||||
logger.Error(" ERROR: not a regular file")
|
||||
return fmt.Errorf("Configuration file \"%q\" is not a regular file. Error: %w", fileOpts.Path, err)
|
||||
} else if fileOpts.Writable {
|
||||
if err := isWritableDir(fileOpts.Path); err != nil {
|
||||
logger.Error(" ERROR: is required to be writable but is not writable: %v", err)
|
||||
return fmt.Errorf("Configuration file \"%q\" is required to be writable but is not. Error: %w", fileOpts.Path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
if fi, err := os.Stat(setting.CustomConf); err != nil || !fi.Mode().IsRegular() {
|
||||
logger.Error("Failed to find configuration file at '%s'.", setting.CustomConf)
|
||||
logger.Error("If you've never ran Gitea yet, this is normal and '%s' will be created for you on first run.", setting.CustomConf)
|
||||
logger.Error("Otherwise check that you are running this command from the correct path and/or provide a `--config` parameter.")
|
||||
logger.Critical("Cannot proceed without a configuration file")
|
||||
return err
|
||||
}
|
||||
|
||||
setting.MustInstalled()
|
||||
|
||||
configurationFiles := []configurationFile{
|
||||
{"Configuration File Path", setting.CustomConf, false, true, false},
|
||||
{"Repository Root Path", setting.RepoRootPath, true, true, true},
|
||||
{"Data Root Path", setting.AppDataPath, true, true, true},
|
||||
{"Custom File Root Path", setting.CustomPath, true, false, false},
|
||||
{"Work directory", setting.AppWorkPath, true, true, false},
|
||||
{"Log Root Path", setting.Log.RootPath, true, true, true},
|
||||
}
|
||||
|
||||
if !setting.HasBuiltinBindata {
|
||||
configurationFiles = append(configurationFiles, configurationFile{"Static File Root Path", setting.StaticRootPath, true, true, false})
|
||||
}
|
||||
|
||||
numberOfErrors := 0
|
||||
for _, configurationFile := range configurationFiles {
|
||||
if err := checkConfigurationFile(logger, autofix, configurationFile); err != nil {
|
||||
numberOfErrors++
|
||||
}
|
||||
}
|
||||
|
||||
if numberOfErrors > 0 {
|
||||
logger.Critical("Please check your configuration files and try again.")
|
||||
return fmt.Errorf("%d configuration files with errors", numberOfErrors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isWritableDir(path string) error {
|
||||
// There's no platform-independent way of checking if a directory is writable
|
||||
// https://stackoverflow.com/questions/20026320/how-to-tell-if-folder-exists-and-is-writable
|
||||
|
||||
tmpFile, err := os.CreateTemp(path, "doctors-order")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(tmpFile.Name()); err != nil {
|
||||
fmt.Printf("Warning: can't remove temporary file: '%s'\n", tmpFile.Name()) //nolint:forbidigo
|
||||
}
|
||||
tmpFile.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check paths and basic configuration",
|
||||
Name: "paths",
|
||||
IsDefault: true,
|
||||
Run: checkConfigurationFiles,
|
||||
AbortIfFailed: true,
|
||||
SkipDatabaseInitialization: true,
|
||||
Priority: 1,
|
||||
})
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func handleDeleteOrphanedRepos(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
test := &consistencyCheck{
|
||||
Name: "Repos with no existing owner",
|
||||
Counter: countOrphanedRepos,
|
||||
Fixer: deleteOrphanedRepos,
|
||||
FixedMessage: "Deleted all content related to orphaned repos",
|
||||
}
|
||||
return test.Run(ctx, logger, autofix)
|
||||
}
|
||||
|
||||
// countOrphanedRepos count repository where user of owner_id do not exist
|
||||
func countOrphanedRepos(ctx context.Context) (int64, error) {
|
||||
return db.CountOrphanedObjects(ctx, "repository", "user", "repository.owner_id=user.id")
|
||||
}
|
||||
|
||||
// deleteOrphanedRepos delete repository where user of owner_id do not exist
|
||||
func deleteOrphanedRepos(ctx context.Context) (int64, error) {
|
||||
batchSize := db.MaxBatchInsertSize("repository")
|
||||
e := db.GetEngine(ctx)
|
||||
var deleted int64
|
||||
adminUser := &user_model.User{IsAdmin: true}
|
||||
|
||||
for {
|
||||
var ids []int64
|
||||
if err := e.Table("`repository`").
|
||||
Join("LEFT", "`user`", "repository.owner_id=user.id").
|
||||
Where(builder.IsNull{"`user`.id"}).
|
||||
Select("`repository`.id").Limit(batchSize).Find(&ids); err != nil {
|
||||
return deleted, err
|
||||
}
|
||||
|
||||
// if we don't get ids we have deleted them all
|
||||
if len(ids) == 0 {
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if err := repo_service.DeleteRepositoryDirectly(ctx, adminUser, id, true); err != nil {
|
||||
return deleted, err
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Deleted all content related to orphaned repos",
|
||||
Name: "delete-orphaned-repos",
|
||||
IsDefault: false,
|
||||
Run: handleDeleteOrphanedRepos,
|
||||
Priority: 4,
|
||||
})
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type commonStorageCheckOptions struct {
|
||||
storer storage.ObjectStorage
|
||||
isOrphaned func(path string, obj storage.Object, stat fs.FileInfo) (bool, error)
|
||||
name string
|
||||
}
|
||||
|
||||
func commonCheckStorage(ctx context.Context, logger log.Logger, autofix bool, opts *commonStorageCheckOptions) error {
|
||||
totalCount, orphanedCount := 0, 0
|
||||
totalSize, orphanedSize := int64(0), int64(0)
|
||||
|
||||
var pathsToDelete []string
|
||||
if err := opts.storer.IterateObjects("", func(p string, obj storage.Object) error {
|
||||
defer obj.Close()
|
||||
|
||||
totalCount++
|
||||
stat, err := obj.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
totalSize += stat.Size()
|
||||
|
||||
orphaned, err := opts.isOrphaned(p, obj, stat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if orphaned {
|
||||
orphanedCount++
|
||||
orphanedSize += stat.Size()
|
||||
if autofix {
|
||||
pathsToDelete = append(pathsToDelete, p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Error("Error whilst iterating %s storage: %v", opts.name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if orphanedCount > 0 {
|
||||
if autofix {
|
||||
var deletedNum int
|
||||
for _, p := range pathsToDelete {
|
||||
if err := opts.storer.Delete(p); err != nil {
|
||||
log.Error("Error whilst deleting %s from %s storage: %v", p, opts.name, err)
|
||||
} else {
|
||||
deletedNum++
|
||||
}
|
||||
}
|
||||
logger.Info("Deleted %d/%d orphaned %s(s)", deletedNum, orphanedCount, opts.name)
|
||||
} else {
|
||||
logger.Warn("Found %d/%d (%s/%s) orphaned %s(s)", orphanedCount, totalCount, base.FileSize(orphanedSize), base.FileSize(totalSize), opts.name)
|
||||
}
|
||||
} else {
|
||||
logger.Info("Found %d (%s) %s(s)", totalCount, base.FileSize(totalSize), opts.name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type checkStorageOptions struct {
|
||||
All bool
|
||||
Attachments bool
|
||||
LFS bool
|
||||
Avatars bool
|
||||
RepoAvatars bool
|
||||
RepoArchives bool
|
||||
Packages bool
|
||||
}
|
||||
|
||||
// checkStorage will return a doctor check function to check the requested storage types for "orphaned" stored object/files and optionally delete them
|
||||
func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
return func(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
if err := storage.Init(); err != nil {
|
||||
logger.Error("storage.Init failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Attachments || opts.All {
|
||||
if err := commonCheckStorage(ctx, logger, autofix,
|
||||
&commonStorageCheckOptions{
|
||||
storer: storage.Attachments,
|
||||
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
|
||||
exists, err := repo.ExistAttachmentsByUUID(ctx, stat.Name())
|
||||
return !exists, err
|
||||
},
|
||||
name: "attachment",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.LFS || opts.All {
|
||||
if !setting.LFS.StartServer {
|
||||
logger.Info("LFS isn't enabled (skipped)")
|
||||
return nil
|
||||
}
|
||||
if err := commonCheckStorage(ctx, logger, autofix,
|
||||
&commonStorageCheckOptions{
|
||||
storer: storage.LFS,
|
||||
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
|
||||
// The oid of an LFS stored object is the name but with all the path.Separators removed
|
||||
oid := strings.ReplaceAll(path, "/", "")
|
||||
exists, err := git.ExistsLFSObject(ctx, oid)
|
||||
return !exists, err
|
||||
},
|
||||
name: "LFS file",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Avatars || opts.All {
|
||||
if err := commonCheckStorage(ctx, logger, autofix,
|
||||
&commonStorageCheckOptions{
|
||||
storer: storage.Avatars,
|
||||
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
|
||||
exists, err := user.ExistsWithAvatarAtStoragePath(ctx, path)
|
||||
return !exists, err
|
||||
},
|
||||
name: "avatar",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.RepoAvatars || opts.All {
|
||||
if err := commonCheckStorage(ctx, logger, autofix,
|
||||
&commonStorageCheckOptions{
|
||||
storer: storage.RepoAvatars,
|
||||
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
|
||||
exists, err := repo.ExistsWithAvatarAtStoragePath(ctx, path)
|
||||
return !exists, err
|
||||
},
|
||||
name: "repo avatar",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.RepoArchives || opts.All {
|
||||
if err := commonCheckStorage(ctx, logger, autofix,
|
||||
&commonStorageCheckOptions{
|
||||
storer: storage.RepoAvatars,
|
||||
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
|
||||
exists, err := repo.ExistsRepoArchiverWithStoragePath(ctx, path)
|
||||
if err == nil || errors.Is(err, util.ErrInvalidArgument) {
|
||||
// invalid arguments mean that the object is not a valid repo archiver and it should be removed
|
||||
return !exists, nil
|
||||
}
|
||||
return !exists, err
|
||||
},
|
||||
name: "repo archive",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Packages || opts.All {
|
||||
if !setting.Packages.Enabled {
|
||||
logger.Info("Packages isn't enabled (skipped)")
|
||||
return nil
|
||||
}
|
||||
if err := commonCheckStorage(ctx, logger, autofix,
|
||||
&commonStorageCheckOptions{
|
||||
storer: storage.Packages,
|
||||
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
|
||||
key, err := packages_module.RelativePathToKey(path)
|
||||
if err != nil {
|
||||
// If there is an error here then the relative path does not match a valid package
|
||||
// Therefore it is orphaned by default
|
||||
return true, nil
|
||||
}
|
||||
|
||||
exists, err := packages.ExistPackageBlobWithSHA(ctx, string(key))
|
||||
|
||||
return !exists, err
|
||||
},
|
||||
name: "package blob",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check if there are orphaned storage files",
|
||||
Name: "storages",
|
||||
IsDefault: false,
|
||||
Run: checkStorage(&checkStorageOptions{All: true}),
|
||||
AbortIfFailed: false,
|
||||
SkipDatabaseInitialization: false,
|
||||
Priority: 1,
|
||||
})
|
||||
|
||||
Register(&Check{
|
||||
Title: "Check if there are orphaned attachments in storage",
|
||||
Name: "storage-attachments",
|
||||
IsDefault: false,
|
||||
Run: checkStorage(&checkStorageOptions{Attachments: true}),
|
||||
AbortIfFailed: false,
|
||||
SkipDatabaseInitialization: false,
|
||||
Priority: 1,
|
||||
})
|
||||
|
||||
Register(&Check{
|
||||
Title: "Check if there are orphaned lfs files in storage",
|
||||
Name: "storage-lfs",
|
||||
IsDefault: false,
|
||||
Run: checkStorage(&checkStorageOptions{LFS: true}),
|
||||
AbortIfFailed: false,
|
||||
SkipDatabaseInitialization: false,
|
||||
Priority: 1,
|
||||
})
|
||||
|
||||
Register(&Check{
|
||||
Title: "Check if there are orphaned avatars in storage",
|
||||
Name: "storage-avatars",
|
||||
IsDefault: false,
|
||||
Run: checkStorage(&checkStorageOptions{Avatars: true, RepoAvatars: true}),
|
||||
AbortIfFailed: false,
|
||||
SkipDatabaseInitialization: false,
|
||||
Priority: 1,
|
||||
})
|
||||
|
||||
Register(&Check{
|
||||
Title: "Check if there are orphaned archives in storage",
|
||||
Name: "storage-archives",
|
||||
IsDefault: false,
|
||||
Run: checkStorage(&checkStorageOptions{RepoArchives: true}),
|
||||
AbortIfFailed: false,
|
||||
SkipDatabaseInitialization: false,
|
||||
Priority: 1,
|
||||
})
|
||||
|
||||
Register(&Check{
|
||||
Title: "Check if there are orphaned package blobs in storage",
|
||||
Name: "storage-packages",
|
||||
IsDefault: false,
|
||||
Run: checkStorage(&checkStorageOptions{Packages: true}),
|
||||
AbortIfFailed: false,
|
||||
SkipDatabaseInitialization: false,
|
||||
Priority: 1,
|
||||
})
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
func checkUserType(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
count, err := user_model.CountWrongUserType(ctx)
|
||||
if err != nil {
|
||||
logger.Critical("Error: %v whilst counting wrong user types")
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
if autofix {
|
||||
if count, err = user_model.FixWrongUserType(ctx); err != nil {
|
||||
logger.Critical("Error: %v whilst fixing wrong user types")
|
||||
return err
|
||||
}
|
||||
logger.Info("%d users with wrong type fixed", count)
|
||||
} else {
|
||||
logger.Warn("%d users with wrong type exist", count)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check if user with wrong type exist",
|
||||
Name: "check-user-type",
|
||||
IsDefault: true,
|
||||
Run: checkUserType,
|
||||
Priority: 3,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package dump
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
)
|
||||
|
||||
var SupportedOutputTypes = []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}
|
||||
|
||||
// PrepareFileNameAndType prepares the output file name and type, if the type is not supported, it returns an empty "outType"
|
||||
func PrepareFileNameAndType(argFile, argType string) (outFileName, outType string) {
|
||||
if argFile == "" && argType == "" {
|
||||
outType = SupportedOutputTypes[0]
|
||||
outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
|
||||
} else if argFile == "" {
|
||||
outType = argType
|
||||
outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
|
||||
} else if argType == "" {
|
||||
if filepath.Ext(outFileName) == "" {
|
||||
outType = SupportedOutputTypes[0]
|
||||
outFileName = argFile
|
||||
} else {
|
||||
for _, t := range SupportedOutputTypes {
|
||||
if strings.HasSuffix(argFile, "."+t) {
|
||||
outFileName = argFile
|
||||
outType = t
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
outFileName, outType = argFile, argType
|
||||
}
|
||||
if !slices.Contains(SupportedOutputTypes, outType) {
|
||||
return "", ""
|
||||
}
|
||||
return outFileName, outType
|
||||
}
|
||||
|
||||
func IsSubdir(upper, lower string) (bool, error) {
|
||||
if relPath, err := filepath.Rel(upper, lower); err != nil {
|
||||
return false, err
|
||||
} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type Dumper struct {
|
||||
Writer archiver.Writer
|
||||
Verbose bool
|
||||
|
||||
globalExcludeAbsPaths []string
|
||||
}
|
||||
|
||||
func (dumper *Dumper) AddReader(r io.ReadCloser, info os.FileInfo, customName string) error {
|
||||
if dumper.Verbose {
|
||||
log.Info("Adding file %s", customName)
|
||||
}
|
||||
|
||||
return dumper.Writer.Write(archiver.File{
|
||||
FileInfo: archiver.FileInfo{
|
||||
FileInfo: info,
|
||||
CustomName: customName,
|
||||
},
|
||||
ReadCloser: r,
|
||||
})
|
||||
}
|
||||
|
||||
func (dumper *Dumper) AddFile(filePath, absPath string) error {
|
||||
file, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dumper.AddReader(file, fileInfo, filePath)
|
||||
}
|
||||
|
||||
func (dumper *Dumper) normalizeFilePath(absPath string) string {
|
||||
absPath = filepath.Clean(absPath)
|
||||
if setting.IsWindows {
|
||||
absPath = strings.ToLower(absPath)
|
||||
}
|
||||
return absPath
|
||||
}
|
||||
|
||||
func (dumper *Dumper) GlobalExcludeAbsPath(absPaths ...string) {
|
||||
for _, absPath := range absPaths {
|
||||
dumper.globalExcludeAbsPaths = append(dumper.globalExcludeAbsPaths, dumper.normalizeFilePath(absPath))
|
||||
}
|
||||
}
|
||||
|
||||
func (dumper *Dumper) shouldExclude(absPath string, excludes []string) bool {
|
||||
norm := dumper.normalizeFilePath(absPath)
|
||||
return slices.Contains(dumper.globalExcludeAbsPaths, norm) || slices.Contains(excludes, norm)
|
||||
}
|
||||
|
||||
func (dumper *Dumper) AddRecursiveExclude(insidePath, absPath string, excludes []string) error {
|
||||
excludes = slices.Clone(excludes)
|
||||
for i := range excludes {
|
||||
excludes[i] = dumper.normalizeFilePath(excludes[i])
|
||||
}
|
||||
return dumper.addFileOrDir(insidePath, absPath, excludes)
|
||||
}
|
||||
|
||||
func (dumper *Dumper) addFileOrDir(insidePath, absPath string, excludes []string) error {
|
||||
absPath, err := filepath.Abs(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
files, err := dir.Readdir(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
currentAbsPath := filepath.Join(absPath, file.Name())
|
||||
if dumper.shouldExclude(currentAbsPath, excludes) {
|
||||
continue
|
||||
}
|
||||
|
||||
currentInsidePath := path.Join(insidePath, file.Name())
|
||||
if file.IsDir() {
|
||||
if err := dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = dumper.addFileOrDir(currentInsidePath, currentAbsPath, excludes); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
|
||||
shouldAdd := file.Mode().IsRegular()
|
||||
if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
target, err := filepath.EvalSymlinks(currentAbsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetStat, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shouldAdd = targetStat.Mode().IsRegular()
|
||||
}
|
||||
if shouldAdd {
|
||||
if err = dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package dump
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrepareFileNameAndType(t *testing.T) {
|
||||
defer timeutil.MockSet(time.Unix(1234, 0))()
|
||||
test := func(argFile, argType, expFile, expType string) {
|
||||
outFile, outType := PrepareFileNameAndType(argFile, argType)
|
||||
assert.Equal(t,
|
||||
fmt.Sprintf("outFile=%s, outType=%s", expFile, expType),
|
||||
fmt.Sprintf("outFile=%s, outType=%s", outFile, outType),
|
||||
fmt.Sprintf("argFile=%s, argType=%s", argFile, argType),
|
||||
)
|
||||
}
|
||||
|
||||
test("", "", "gitea-dump-1234.zip", "zip")
|
||||
test("", "tar.gz", "gitea-dump-1234.tar.gz", "tar.gz")
|
||||
test("", "no-such", "", "")
|
||||
|
||||
test("-", "", "-", "zip")
|
||||
test("-", "tar.gz", "-", "tar.gz")
|
||||
test("-", "no-such", "", "")
|
||||
|
||||
test("a", "", "a", "zip")
|
||||
test("a", "tar.gz", "a", "tar.gz")
|
||||
test("a", "no-such", "", "")
|
||||
|
||||
test("a.zip", "", "a.zip", "zip")
|
||||
test("a.zip", "tar.gz", "a.zip", "tar.gz")
|
||||
test("a.zip", "no-such", "", "")
|
||||
|
||||
test("a.tar.gz", "", "a.tar.gz", "zip")
|
||||
test("a.tar.gz", "tar.gz", "a.tar.gz", "tar.gz")
|
||||
test("a.tar.gz", "no-such", "", "")
|
||||
}
|
||||
|
||||
func TestIsSubDir(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
|
||||
|
||||
isSub, err := IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include"))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isSub)
|
||||
|
||||
isSub, err = IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include/sub"))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isSub)
|
||||
|
||||
isSub, err = IsSubdir(filepath.Join(tmpDir, "include/sub"), filepath.Join(tmpDir, "include"))
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isSub)
|
||||
}
|
||||
|
||||
type testWriter struct {
|
||||
added []string
|
||||
}
|
||||
|
||||
func (t *testWriter) Create(out io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testWriter) Write(f archiver.File) error {
|
||||
t.added = append(t.added, f.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDumper(t *testing.T) {
|
||||
sortStrings := func(s []string) []string {
|
||||
sort.Strings(s)
|
||||
return s
|
||||
}
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude1"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude2"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/a"), nil, 0o644)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/sub/b"), nil, 0o644)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude1/a-1"), nil, 0o644)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude2/a-2"), nil, 0o644)
|
||||
|
||||
tw := &testWriter{}
|
||||
d := &Dumper{Writer: tw}
|
||||
d.GlobalExcludeAbsPath(filepath.Join(tmpDir, "include/exclude1"))
|
||||
err := d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), []string{filepath.Join(tmpDir, "include/exclude2")})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, sortStrings([]string{"include/a", "include/sub", "include/sub/b"}), sortStrings(tw.added))
|
||||
|
||||
tw = &testWriter{}
|
||||
d = &Dumper{Writer: tw}
|
||||
err = d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), nil)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, sortStrings([]string{"include/exclude2", "include/exclude2/a-2", "include/a", "include/sub", "include/sub/b", "include/exclude1", "include/exclude1/a-1"}), sortStrings(tw.added))
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package generate
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
@@ -38,19 +39,24 @@ func NewInternalToken() (string, error) {
|
||||
return internalToken, nil
|
||||
}
|
||||
|
||||
// NewJwtSecret generates a new value intended to be used for JWT secrets.
|
||||
func NewJwtSecret() ([]byte, error) {
|
||||
bytes := make([]byte, 32)
|
||||
_, err := io.ReadFull(rand.Reader, bytes)
|
||||
if err != nil {
|
||||
const defaultJwtSecretLen = 32
|
||||
|
||||
// DecodeJwtSecretBase64 decodes a base64 encoded jwt secret into bytes, and check its length
|
||||
func DecodeJwtSecretBase64(src string) ([]byte, error) {
|
||||
encoding := base64.RawURLEncoding
|
||||
decoded := make([]byte, encoding.DecodedLen(len(src))+3)
|
||||
if n, err := encoding.Decode(decoded, []byte(src)); err != nil {
|
||||
return nil, err
|
||||
} else if n != defaultJwtSecretLen {
|
||||
return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, defaultJwtSecretLen)
|
||||
}
|
||||
return bytes, nil
|
||||
return decoded[:defaultJwtSecretLen], nil
|
||||
}
|
||||
|
||||
// NewJwtSecretBase64 generates a new base64 encoded value intended to be used for JWT secrets.
|
||||
func NewJwtSecretBase64() ([]byte, string, error) {
|
||||
bytes, err := NewJwtSecret()
|
||||
// NewJwtSecretWithBase64 generates a jwt secret with its base64 encoded value intended to be used for saving into config file
|
||||
func NewJwtSecretWithBase64() ([]byte, string, error) {
|
||||
bytes := make([]byte, defaultJwtSecretLen)
|
||||
_, err := io.ReadFull(rand.Reader, bytes)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package generate
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDecodeJwtSecretBase64(t *testing.T) {
|
||||
_, err := DecodeJwtSecretBase64("abcd")
|
||||
assert.ErrorContains(t, err, "invalid base64 decoded length")
|
||||
_, err = DecodeJwtSecretBase64(strings.Repeat("a", 64))
|
||||
assert.ErrorContains(t, err, "invalid base64 decoded length")
|
||||
|
||||
str32 := strings.Repeat("x", 32)
|
||||
encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32))
|
||||
decoded32, err := DecodeJwtSecretBase64(encoded32)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str32, string(decoded32))
|
||||
}
|
||||
|
||||
func TestNewJwtSecretWithBase64(t *testing.T) {
|
||||
secret, encoded, err := NewJwtSecretWithBase64()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, secret, 32)
|
||||
decoded, err := DecodeJwtSecretBase64(encoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, secret, decoded)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
)
|
||||
|
||||
const (
|
||||
AttributeLinguistVendored = "linguist-vendored"
|
||||
AttributeLinguistGenerated = "linguist-generated"
|
||||
AttributeLinguistDocumentation = "linguist-documentation"
|
||||
AttributeLinguistDetectable = "linguist-detectable"
|
||||
AttributeLinguistLanguage = "linguist-language"
|
||||
AttributeGitlabLanguage = "gitlab-language"
|
||||
)
|
||||
|
||||
// true if "set"/"true", false if "unset"/"false", none otherwise
|
||||
func AttributeToBool(attr map[string]string, name string) optional.Option[bool] {
|
||||
switch attr[name] {
|
||||
case "set", "true":
|
||||
return optional.Some(true)
|
||||
case "unset", "false":
|
||||
return optional.Some(false)
|
||||
}
|
||||
return optional.None[bool]()
|
||||
}
|
||||
|
||||
func AttributeToString(attr map[string]string, name string) optional.Option[string] {
|
||||
if value, has := attr[name]; has && value != "unspecified" {
|
||||
return optional.Some(value)
|
||||
}
|
||||
return optional.None[string]()
|
||||
}
|
||||
+35
-35
@@ -148,7 +148,7 @@ func CatFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi
|
||||
// ReadBatchLine reads the header line from cat-file --batch
|
||||
// We expect:
|
||||
// <sha> SP <type> SP <size> LF
|
||||
// sha is a 40byte not 20byte here
|
||||
// sha is a hex encoded here
|
||||
func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
|
||||
typ, err = rd.ReadString('\n')
|
||||
if err != nil {
|
||||
@@ -203,16 +203,7 @@ headerLoop:
|
||||
}
|
||||
|
||||
// Discard the rest of the tag
|
||||
discard := size - n + 1
|
||||
for discard > math.MaxInt32 {
|
||||
_, err := rd.Discard(math.MaxInt32)
|
||||
if err != nil {
|
||||
return id, err
|
||||
}
|
||||
discard -= math.MaxInt32
|
||||
}
|
||||
_, err := rd.Discard(int(discard))
|
||||
return id, err
|
||||
return id, DiscardFull(rd, size-n+1)
|
||||
}
|
||||
|
||||
// ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream.
|
||||
@@ -238,33 +229,23 @@ headerLoop:
|
||||
}
|
||||
|
||||
// Discard the rest of the commit
|
||||
discard := size - n + 1
|
||||
for discard > math.MaxInt32 {
|
||||
_, err := rd.Discard(math.MaxInt32)
|
||||
if err != nil {
|
||||
return id, err
|
||||
}
|
||||
discard -= math.MaxInt32
|
||||
}
|
||||
_, err := rd.Discard(int(discard))
|
||||
return id, err
|
||||
return id, DiscardFull(rd, size-n+1)
|
||||
}
|
||||
|
||||
// git tree files are a list:
|
||||
// <mode-in-ascii> SP <fname> NUL <20-byte SHA>
|
||||
// <mode-in-ascii> SP <fname> NUL <binary Hash>
|
||||
//
|
||||
// Unfortunately this 20-byte notation is somewhat in conflict to all other git tools
|
||||
// Therefore we need some method to convert these 20-byte SHAs to a 40-byte SHA
|
||||
// Therefore we need some method to convert these binary hashes to hex hashes
|
||||
|
||||
// constant hextable to help quickly convert between 20byte and 40byte hashes
|
||||
// constant hextable to help quickly convert between binary and hex representation
|
||||
const hextable = "0123456789abcdef"
|
||||
|
||||
// To40ByteSHA converts a 20-byte SHA into a 40-byte sha. Input and output can be the
|
||||
// same 40 byte slice to support in place conversion without allocations.
|
||||
// BinToHexHeash converts a binary Hash into a hex encoded one. Input and output can be the
|
||||
// same byte slice to support in place conversion without allocations.
|
||||
// This is at least 100x quicker that hex.EncodeToString
|
||||
// NB This requires that out is a 40-byte slice
|
||||
func To40ByteSHA(sha, out []byte) []byte {
|
||||
for i := 19; i >= 0; i-- {
|
||||
func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
|
||||
for i := objectFormat.FullLength()/2 - 1; i >= 0; i-- {
|
||||
v := sha[i]
|
||||
vhi, vlo := v>>4, v&0x0f
|
||||
shi, slo := hextable[vhi], hextable[vlo]
|
||||
@@ -278,10 +259,10 @@ func To40ByteSHA(sha, out []byte) []byte {
|
||||
// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
|
||||
//
|
||||
// Each line is composed of:
|
||||
// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <20-byte SHA>
|
||||
// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <binary HASH>
|
||||
//
|
||||
// We don't attempt to convert the 20-byte SHA to 40-byte SHA to save a lot of time
|
||||
func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
||||
// We don't attempt to convert the raw HASH to save a lot of time
|
||||
func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
||||
var readBytes []byte
|
||||
|
||||
// Read the Mode & fname
|
||||
@@ -324,11 +305,12 @@ func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fn
|
||||
fnameBuf = fnameBuf[:len(fnameBuf)-1]
|
||||
fname = fnameBuf
|
||||
|
||||
// Deal with the 20-byte SHA
|
||||
// Deal with the binary hash
|
||||
idx = 0
|
||||
for idx < 20 {
|
||||
len := objectFormat.FullLength() / 2
|
||||
for idx < len {
|
||||
var read int
|
||||
read, err = rd.Read(shaBuf[idx:20])
|
||||
read, err = rd.Read(shaBuf[idx:len])
|
||||
n += read
|
||||
if err != nil {
|
||||
return mode, fname, sha, n, err
|
||||
@@ -345,3 +327,21 @@ func init() {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
callerPrefix = strings.TrimSuffix(filename, "modules/git/batch_reader.go")
|
||||
}
|
||||
|
||||
func DiscardFull(rd *bufio.Reader, discard int64) error {
|
||||
if discard > math.MaxInt32 {
|
||||
n, err := rd.Discard(math.MaxInt32)
|
||||
discard -= int64(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for discard > 0 {
|
||||
n, err := rd.Discard(int(discard))
|
||||
discard -= int64(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+38
-20
@@ -10,7 +10,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -18,8 +17,10 @@ import (
|
||||
|
||||
// BlamePart represents block of blame - continuous lines with one sha
|
||||
type BlamePart struct {
|
||||
Sha string
|
||||
Lines []string
|
||||
Sha string
|
||||
Lines []string
|
||||
PreviousSha string
|
||||
PreviousPath string
|
||||
}
|
||||
|
||||
// BlameReader returns part of file blame one by one
|
||||
@@ -30,47 +31,56 @@ type BlameReader struct {
|
||||
done chan error
|
||||
lastSha *string
|
||||
ignoreRevsFile *string
|
||||
objectFormat ObjectFormat
|
||||
}
|
||||
|
||||
func (r *BlameReader) UsesIgnoreRevs() bool {
|
||||
return r.ignoreRevsFile != nil
|
||||
}
|
||||
|
||||
var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
|
||||
|
||||
// NextPart returns next part of blame (sequential code lines with the same commit)
|
||||
func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
var blamePart *BlamePart
|
||||
|
||||
if r.lastSha != nil {
|
||||
blamePart = &BlamePart{*r.lastSha, make([]string, 0)}
|
||||
blamePart = &BlamePart{
|
||||
Sha: *r.lastSha,
|
||||
Lines: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
var line []byte
|
||||
const previousHeader = "previous "
|
||||
var lineBytes []byte
|
||||
var isPrefix bool
|
||||
var err error
|
||||
|
||||
for err != io.EOF {
|
||||
line, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
lineBytes, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return blamePart, err
|
||||
}
|
||||
|
||||
if len(line) == 0 {
|
||||
if len(lineBytes) == 0 {
|
||||
// isPrefix will be false
|
||||
continue
|
||||
}
|
||||
|
||||
lines := shaLineRegex.FindSubmatch(line)
|
||||
if lines != nil {
|
||||
sha1 := string(lines[1])
|
||||
var objectID string
|
||||
objectFormatLength := r.objectFormat.FullLength()
|
||||
|
||||
if len(lineBytes) > objectFormatLength && lineBytes[objectFormatLength] == ' ' && r.objectFormat.IsValid(string(lineBytes[0:objectFormatLength])) {
|
||||
objectID = string(lineBytes[0:objectFormatLength])
|
||||
}
|
||||
if len(objectID) > 0 {
|
||||
if blamePart == nil {
|
||||
blamePart = &BlamePart{sha1, make([]string, 0)}
|
||||
blamePart = &BlamePart{
|
||||
Sha: objectID,
|
||||
Lines: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
if blamePart.Sha != sha1 {
|
||||
r.lastSha = &sha1
|
||||
if blamePart.Sha != objectID {
|
||||
r.lastSha = &objectID
|
||||
// need to munch to end of line...
|
||||
for isPrefix {
|
||||
_, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
@@ -80,10 +90,13 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
}
|
||||
return blamePart, nil
|
||||
}
|
||||
} else if line[0] == '\t' {
|
||||
code := line[1:]
|
||||
|
||||
blamePart.Lines = append(blamePart.Lines, string(code))
|
||||
} else if lineBytes[0] == '\t' {
|
||||
blamePart.Lines = append(blamePart.Lines, string(lineBytes[1:]))
|
||||
} else if bytes.HasPrefix(lineBytes, []byte(previousHeader)) {
|
||||
offset := len(previousHeader) // already includes a space
|
||||
blamePart.PreviousSha = string(lineBytes[offset : offset+objectFormatLength])
|
||||
offset += objectFormatLength + 1 // +1 for space
|
||||
blamePart.PreviousPath = string(lineBytes[offset:])
|
||||
}
|
||||
|
||||
// need to munch to end of line...
|
||||
@@ -102,6 +115,10 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
|
||||
// Close BlameReader - don't run NextPart after invoking that
|
||||
func (r *BlameReader) Close() error {
|
||||
if r.bufferedReader == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := <-r.done
|
||||
r.bufferedReader = nil
|
||||
_ = r.reader.Close()
|
||||
@@ -113,7 +130,7 @@ func (r *BlameReader) Close() error {
|
||||
}
|
||||
|
||||
// CreateBlameReader creates reader for given repository, commit and file
|
||||
func CreateBlameReader(ctx context.Context, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
|
||||
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
|
||||
var ignoreRevsFile *string
|
||||
if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore {
|
||||
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
|
||||
@@ -162,6 +179,7 @@ func CreateBlameReader(ctx context.Context, repoPath string, commit *Commit, fil
|
||||
bufferedReader: bufferedReader,
|
||||
done: done,
|
||||
ignoreRevsFile: ignoreRevsFile,
|
||||
objectFormat: objectFormat,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReadingBlameOutputSha256(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
t.Run("Without .git-blame-ignore-revs", func(t *testing.T) {
|
||||
repo, err := OpenRepository(ctx, "./tests/repos/repo5_pulls_sha256")
|
||||
assert.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
commit, err := repo.GetCommit("0b69b7bb649b5d46e14cabb6468685e5dd721290acc7ffe604d37cde57927345")
|
||||
assert.NoError(t, err)
|
||||
|
||||
parts := []*BlamePart{
|
||||
{
|
||||
Sha: "1e35a51dc00fd7de730344c07061acfe80e8117e075ac979b6a29a3a045190ca",
|
||||
Lines: []string{
|
||||
"# test_repo",
|
||||
"Test repository for testing migration from github to gitea",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "0b69b7bb649b5d46e14cabb6468685e5dd721290acc7ffe604d37cde57927345",
|
||||
Lines: []string{"", "Do not make any changes to this repo it is used for unit testing"},
|
||||
PreviousSha: "1e35a51dc00fd7de730344c07061acfe80e8117e075ac979b6a29a3a045190ca",
|
||||
PreviousPath: "README.md",
|
||||
},
|
||||
}
|
||||
|
||||
for _, bypass := range []bool{false, true} {
|
||||
blameReader, err := CreateBlameReader(ctx, Sha256ObjectFormat, "./tests/repos/repo5_pulls_sha256", commit, "README.md", bypass)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, blameReader)
|
||||
defer blameReader.Close()
|
||||
|
||||
assert.False(t, blameReader.UsesIgnoreRevs())
|
||||
|
||||
for _, part := range parts {
|
||||
actualPart, err := blameReader.NextPart()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, part, actualPart)
|
||||
}
|
||||
|
||||
// make sure all parts have been read
|
||||
actualPart, err := blameReader.NextPart()
|
||||
assert.Nil(t, actualPart)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("With .git-blame-ignore-revs", func(t *testing.T) {
|
||||
repo, err := OpenRepository(ctx, "./tests/repos/repo6_blame_sha256")
|
||||
assert.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
full := []*BlamePart{
|
||||
{
|
||||
Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc",
|
||||
Lines: []string{"line", "line"},
|
||||
},
|
||||
{
|
||||
Sha: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe",
|
||||
Lines: []string{"changed line"},
|
||||
PreviousSha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc",
|
||||
PreviousPath: "blame.txt",
|
||||
},
|
||||
{
|
||||
Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc",
|
||||
Lines: []string{"line", "line", ""},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
CommitID string
|
||||
UsesIgnoreRevs bool
|
||||
Bypass bool
|
||||
Parts []*BlamePart
|
||||
}{
|
||||
{
|
||||
CommitID: "e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3",
|
||||
UsesIgnoreRevs: true,
|
||||
Bypass: false,
|
||||
Parts: []*BlamePart{
|
||||
{
|
||||
Sha: "ab2b57a4fa476fb2edb74dafa577caf918561abbaa8fba0c8dc63c412e17a7cc",
|
||||
Lines: []string{"line", "line", "changed line", "line", "line", ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
CommitID: "e2f5660e15159082902960af0ed74fc144921d2b0c80e069361853b3ece29ba3",
|
||||
UsesIgnoreRevs: false,
|
||||
Bypass: true,
|
||||
Parts: full,
|
||||
},
|
||||
{
|
||||
CommitID: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe",
|
||||
UsesIgnoreRevs: false,
|
||||
Bypass: false,
|
||||
Parts: full,
|
||||
},
|
||||
{
|
||||
CommitID: "9347b0198cd1f25017579b79d0938fa89dba34ad2514f0dd92f6bc975ed1a2fe",
|
||||
UsesIgnoreRevs: false,
|
||||
Bypass: false,
|
||||
Parts: full,
|
||||
},
|
||||
}
|
||||
|
||||
objectFormat, err := repo.GetObjectFormat()
|
||||
assert.NoError(t, err)
|
||||
for _, c := range cases {
|
||||
commit, err := repo.GetCommit(c.CommitID)
|
||||
assert.NoError(t, err)
|
||||
blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, blameReader)
|
||||
defer blameReader.Close()
|
||||
|
||||
assert.Equal(t, c.UsesIgnoreRevs, blameReader.UsesIgnoreRevs())
|
||||
|
||||
for _, part := range c.Parts {
|
||||
actualPart, err := blameReader.NextPart()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, part, actualPart)
|
||||
}
|
||||
|
||||
// make sure all parts have been read
|
||||
actualPart, err := blameReader.NextPart()
|
||||
assert.Nil(t, actualPart)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
+20
-14
@@ -24,20 +24,22 @@ func TestReadingBlameOutput(t *testing.T) {
|
||||
|
||||
parts := []*BlamePart{
|
||||
{
|
||||
"72866af952e98d02a73003501836074b286a78f6",
|
||||
[]string{
|
||||
Sha: "72866af952e98d02a73003501836074b286a78f6",
|
||||
Lines: []string{
|
||||
"# test_repo",
|
||||
"Test repository for testing migration from github to gitea",
|
||||
},
|
||||
},
|
||||
{
|
||||
"f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
||||
[]string{"", "Do not make any changes to this repo it is used for unit testing"},
|
||||
Sha: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
||||
Lines: []string{"", "Do not make any changes to this repo it is used for unit testing"},
|
||||
PreviousSha: "72866af952e98d02a73003501836074b286a78f6",
|
||||
PreviousPath: "README.md",
|
||||
},
|
||||
}
|
||||
|
||||
for _, bypass := range []bool{false, true} {
|
||||
blameReader, err := CreateBlameReader(ctx, "./tests/repos/repo5_pulls", commit, "README.md", bypass)
|
||||
blameReader, err := CreateBlameReader(ctx, Sha1ObjectFormat, "./tests/repos/repo5_pulls", commit, "README.md", bypass)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, blameReader)
|
||||
defer blameReader.Close()
|
||||
@@ -64,16 +66,18 @@ func TestReadingBlameOutput(t *testing.T) {
|
||||
|
||||
full := []*BlamePart{
|
||||
{
|
||||
"af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
[]string{"line", "line"},
|
||||
Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
Lines: []string{"line", "line"},
|
||||
},
|
||||
{
|
||||
"45fb6cbc12f970b04eacd5cd4165edd11c8d7376",
|
||||
[]string{"changed line"},
|
||||
Sha: "45fb6cbc12f970b04eacd5cd4165edd11c8d7376",
|
||||
Lines: []string{"changed line"},
|
||||
PreviousSha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
PreviousPath: "blame.txt",
|
||||
},
|
||||
{
|
||||
"af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
[]string{"line", "line", ""},
|
||||
Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
Lines: []string{"line", "line", ""},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -89,8 +93,8 @@ func TestReadingBlameOutput(t *testing.T) {
|
||||
Bypass: false,
|
||||
Parts: []*BlamePart{
|
||||
{
|
||||
"af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
[]string{"line", "line", "changed line", "line", "line", ""},
|
||||
Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
Lines: []string{"line", "line", "changed line", "line", "line", ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -114,11 +118,13 @@ func TestReadingBlameOutput(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
objectFormat, err := repo.GetObjectFormat()
|
||||
assert.NoError(t, err)
|
||||
for _, c := range cases {
|
||||
commit, err := repo.GetCommit(c.CommitID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
blameReader, err := CreateBlameReader(ctx, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
|
||||
blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, blameReader)
|
||||
defer blameReader.Close()
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID SHA1
|
||||
ID ObjectID
|
||||
|
||||
gogitEncodedObj plumbing.EncodedObject
|
||||
name string
|
||||
|
||||
+10
-20
@@ -9,14 +9,13 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID SHA1
|
||||
ID ObjectID
|
||||
|
||||
gotSize bool
|
||||
size int64
|
||||
@@ -103,26 +102,17 @@ func (b *blobReader) Read(p []byte) (n int, err error) {
|
||||
|
||||
// Close implements io.Closer
|
||||
func (b *blobReader) Close() error {
|
||||
defer b.cancel()
|
||||
if b.n > 0 {
|
||||
for b.n > math.MaxInt32 {
|
||||
n, err := b.rd.Discard(math.MaxInt32)
|
||||
b.n -= int64(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.n -= math.MaxInt32
|
||||
}
|
||||
n, err := b.rd.Discard(int(b.n))
|
||||
b.n -= int64(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if b.rd == nil {
|
||||
return nil
|
||||
}
|
||||
if b.n == 0 {
|
||||
_, err := b.rd.Discard(1)
|
||||
b.n--
|
||||
|
||||
defer b.cancel()
|
||||
|
||||
if err := DiscardFull(b.rd, b.n+1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.rd = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+17
-11
@@ -12,9 +12,9 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -345,6 +345,17 @@ func (c *Command) Run(opts *RunOpts) error {
|
||||
log.Debug("slow git.Command.Run: %s (%s)", c, elapsed)
|
||||
}
|
||||
|
||||
// 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.
|
||||
if runtime.GOOS == "windows" &&
|
||||
err != nil &&
|
||||
err.Error() == "" &&
|
||||
cmd.ProcessState.ExitCode() == 1 &&
|
||||
ctx.Err() == context.Canceled {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
if err != nil && ctx.Err() != context.DeadlineExceeded {
|
||||
return err
|
||||
}
|
||||
@@ -356,7 +367,6 @@ type RunStdError interface {
|
||||
error
|
||||
Unwrap() error
|
||||
Stderr() string
|
||||
IsExitCode(code int) bool
|
||||
}
|
||||
|
||||
type runStdError struct {
|
||||
@@ -381,23 +391,19 @@ func (r *runStdError) Stderr() string {
|
||||
return r.stderr
|
||||
}
|
||||
|
||||
func (r *runStdError) IsExitCode(code int) bool {
|
||||
func IsErrorExitCode(err error, code int) bool {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(r.err, &exitError) {
|
||||
if errors.As(err, &exitError) {
|
||||
return exitError.ExitCode() == code
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func bytesToString(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b)) // that's what Golang's strings.Builder.String() does (go/src/strings/builder.go)
|
||||
}
|
||||
|
||||
// RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr).
|
||||
func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) {
|
||||
stdoutBytes, stderrBytes, err := c.RunStdBytes(opts)
|
||||
stdout = bytesToString(stdoutBytes)
|
||||
stderr = bytesToString(stderrBytes)
|
||||
stdout = util.UnsafeBytesToString(stdoutBytes)
|
||||
stderr = util.UnsafeBytesToString(stderrBytes)
|
||||
if err != nil {
|
||||
return stdout, stderr, &runStdError{err: err, stderr: stderr}
|
||||
}
|
||||
@@ -432,7 +438,7 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS
|
||||
err := c.Run(newOpts)
|
||||
stderr = stderrBuf.Bytes()
|
||||
if err != nil {
|
||||
return nil, stderr, &runStdError{err: err, stderr: bytesToString(stderr)}
|
||||
return nil, stderr, &runStdError{err: err, stderr: util.UnsafeBytesToString(stderr)}
|
||||
}
|
||||
// even if there is no err, there could still be some stderr output
|
||||
return stdoutBuf.Bytes(), stderr, nil
|
||||
|
||||
+22
-12
@@ -9,6 +9,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
@@ -21,18 +22,18 @@ import (
|
||||
// Commit represents a git commit.
|
||||
type Commit struct {
|
||||
Tree
|
||||
ID SHA1 // The ID of this commit object
|
||||
ID ObjectID // The ID of this commit object
|
||||
Author *Signature
|
||||
Committer *Signature
|
||||
CommitMessage string
|
||||
Signature *CommitGPGSignature
|
||||
Signature *CommitSignature
|
||||
|
||||
Parents []SHA1 // SHA1 strings
|
||||
Parents []ObjectID // ID strings
|
||||
submoduleCache *ObjectCache
|
||||
}
|
||||
|
||||
// CommitGPGSignature represents a git commit signature part.
|
||||
type CommitGPGSignature struct {
|
||||
// CommitSignature represents a git commit signature part.
|
||||
type CommitSignature struct {
|
||||
Signature string
|
||||
Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
|
||||
}
|
||||
@@ -43,15 +44,16 @@ func (c *Commit) Message() string {
|
||||
}
|
||||
|
||||
// Summary returns first line of commit message.
|
||||
// The string is forced to be valid UTF8
|
||||
func (c *Commit) Summary() string {
|
||||
return strings.Split(strings.TrimSpace(c.CommitMessage), "\n")[0]
|
||||
return strings.ToValidUTF8(strings.Split(strings.TrimSpace(c.CommitMessage), "\n")[0], "?")
|
||||
}
|
||||
|
||||
// ParentID returns oid of n-th parent (0-based index).
|
||||
// It returns nil if no such parent exists.
|
||||
func (c *Commit) ParentID(n int) (SHA1, error) {
|
||||
func (c *Commit) ParentID(n int) (ObjectID, error) {
|
||||
if n >= len(c.Parents) {
|
||||
return SHA1{}, ErrNotExist{"", ""}
|
||||
return nil, ErrNotExist{"", ""}
|
||||
}
|
||||
return c.Parents[n], nil
|
||||
}
|
||||
@@ -208,9 +210,9 @@ func (c *Commit) CommitsBefore() ([]*Commit, error) {
|
||||
}
|
||||
|
||||
// HasPreviousCommit returns true if a given commitHash is contained in commit's parents
|
||||
func (c *Commit) HasPreviousCommit(commitHash SHA1) (bool, error) {
|
||||
func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) {
|
||||
this := c.ID.String()
|
||||
that := commitHash.String()
|
||||
that := objectID.String()
|
||||
|
||||
if this == that {
|
||||
return false, nil
|
||||
@@ -231,9 +233,14 @@ func (c *Commit) HasPreviousCommit(commitHash SHA1) (bool, error) {
|
||||
|
||||
// IsForcePush returns true if a push from oldCommitHash to this is a force push
|
||||
func (c *Commit) IsForcePush(oldCommitID string) (bool, error) {
|
||||
if oldCommitID == EmptySHA {
|
||||
objectFormat, err := c.repo.GetObjectFormat()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if oldCommitID == objectFormat.EmptyObjectID().String() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
oldCommit, err := c.repo.GetCommit(oldCommitID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -305,7 +312,7 @@ func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error)
|
||||
return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String())
|
||||
}
|
||||
|
||||
// FileChangedSinceCommit Returns true if the file given has changed since the the past commit
|
||||
// FileChangedSinceCommit Returns true if the file given has changed since the past commit
|
||||
// YOU MUST ENSURE THAT pastCommit is a valid commit ID.
|
||||
func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) {
|
||||
return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
|
||||
@@ -390,6 +397,9 @@ func (c *Commit) GetSubModules() (*ObjectCache, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("GetSubModules scan: %w", err)
|
||||
}
|
||||
|
||||
return c.submoduleCache, nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
||||
func convertPGPSignature(c *object.Commit) *CommitSignature {
|
||||
if c.PGPSignature == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -47,11 +47,17 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Encoding != "" && c.Encoding != "UTF-8" {
|
||||
if _, err = fmt.Fprintf(&w, "\nencoding %s\n", c.Encoding); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &CommitGPGSignature{
|
||||
return &CommitSignature{
|
||||
Signature: c.PGPSignature,
|
||||
Payload: w.String(),
|
||||
}
|
||||
@@ -59,11 +65,11 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
||||
|
||||
func convertCommit(c *object.Commit) *Commit {
|
||||
return &Commit{
|
||||
ID: c.Hash,
|
||||
ID: ParseGogitHash(c.Hash),
|
||||
CommitMessage: c.Message,
|
||||
Committer: &c.Committer,
|
||||
Author: &c.Author,
|
||||
Signature: convertPGPSignature(c),
|
||||
Parents: c.ParentHashes,
|
||||
Parents: ParseGogitHashArray(c.ParentHashes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
|
||||
defer commitGraphFile.Close()
|
||||
}
|
||||
|
||||
c, err := commitNodeIndex.Get(commit.ID)
|
||||
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -151,6 +151,9 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string,
|
||||
return nil, err
|
||||
}
|
||||
if typ != "commit" {
|
||||
if err := DiscardFull(batchReader, size+1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID)
|
||||
}
|
||||
c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size))
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
// We need this to interpret commits from cat-file or cat-file --batch
|
||||
//
|
||||
// If used as part of a cat-file --batch stream you need to limit the reader to the correct size
|
||||
func CommitFromReader(gitRepo *Repository, sha SHA1, reader io.Reader) (*Commit, error) {
|
||||
func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader) (*Commit, error) {
|
||||
commit := &Commit{
|
||||
ID: sha,
|
||||
ID: objectID,
|
||||
Author: &Signature{},
|
||||
Committer: &Signature{},
|
||||
}
|
||||
@@ -84,7 +84,11 @@ readLoop:
|
||||
commit.Committer = &Signature{}
|
||||
commit.Committer.Decode(data)
|
||||
_, _ = payloadSB.Write(line)
|
||||
case "encoding":
|
||||
_, _ = payloadSB.Write(line)
|
||||
case "gpgsig":
|
||||
fallthrough
|
||||
case "gpgsig-sha256": // FIXME: no intertop, so only 1 exists at present.
|
||||
_, _ = signatureSB.Write(data)
|
||||
_ = signatureSB.WriteByte('\n')
|
||||
pgpsig = true
|
||||
@@ -95,7 +99,7 @@ readLoop:
|
||||
}
|
||||
}
|
||||
commit.CommitMessage = messageSB.String()
|
||||
commit.Signature = &CommitGPGSignature{
|
||||
commit.Signature = &CommitSignature{
|
||||
Signature: signatureSB.String(),
|
||||
Payload: payloadSB.String(),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommitsCountSha256(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256")
|
||||
|
||||
commitsCount, err := CommitsCount(DefaultContext,
|
||||
CommitsCountOptions{
|
||||
RepoPath: bareRepo1Path,
|
||||
Revision: []string{"f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc"},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), commitsCount)
|
||||
}
|
||||
|
||||
func TestCommitsCountWithoutBaseSha256(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256")
|
||||
|
||||
commitsCount, err := CommitsCount(DefaultContext,
|
||||
CommitsCountOptions{
|
||||
RepoPath: bareRepo1Path,
|
||||
Not: "main",
|
||||
Revision: []string{"branch1"},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), commitsCount)
|
||||
}
|
||||
|
||||
func TestGetFullCommitIDSha256(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256")
|
||||
|
||||
id, err := GetFullCommitID(DefaultContext, bareRepo1Path, "f004f4")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc", id)
|
||||
}
|
||||
|
||||
func TestGetFullCommitIDErrorSha256(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256")
|
||||
|
||||
id, err := GetFullCommitID(DefaultContext, bareRepo1Path, "unknown")
|
||||
assert.Empty(t, id)
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, "object does not exist [id: unknown, rel_path: ]")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitFromReaderSha256(t *testing.T) {
|
||||
commitString := `9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 commit 1114
|
||||
tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e
|
||||
parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8
|
||||
author Adam Majer <amajer@suse.de> 1698676906 +0100
|
||||
committer Adam Majer <amajer@suse.de> 1698676906 +0100
|
||||
gpgsig-sha256 -----BEGIN PGP SIGNATURE-----
|
||||
` + " " + `
|
||||
iQIrBAABCgAtFiEES+fB08xlgTrzSdQvhkUIsBsmec8FAmU/wKoPHGFtYWplckBz
|
||||
dXNlLmRlAAoJEIZFCLAbJnnP4s4PQIJATa++WPzR6/H4etT7bsOGoMyguEJYyWOd
|
||||
aTybplzT7QAL7h2to0QszGabtzMJPIA39xSFZNYNN30voK5YyyYibXluPKgjemfK
|
||||
WNXwF+gkwgZI38gSvKf+vlqI+EYyIFe19wOhiju0m8SIlB5NEPiWHa17q2mqmqqx
|
||||
1FWa2JdqLPYjAtSLFXeSZegrY5V1FxdemyMUONkg8YO9OSIMZiE0GsnnOXQ3xcT4
|
||||
JTCnmlUxIKw689UiEY80JopUIq+Wl7+qq9507IYYSUCyB6JazL42AKMzVCbD+qBP
|
||||
oOzh/hafYgk9H9qCQXaLbmvs17zXRpicig1bAzqgAy1FDelvpERyRTydEajSLIG6
|
||||
U1cRCkgXCZ0NfsYNPPmBa8b3+rnstypXYTbyMwTln7FfUAaGo6o9JYiPMkzxlmsy
|
||||
zfp/tcaY8+LlBL9aOJjtv+a0p+HrpCGd6CCa4ARfphTLq8QRSSh8uzlB9N+6HnRI
|
||||
VAEUo6ecdDxSpyt2naeg9pKus/BRi7P6g4B1hkk/zZstUX/QP4IQuAJbXjkvsC+X
|
||||
HKRr3NlRM/DygzTyj0gN74uoa0goCIbyAQhiT42nm0cuhM7uN/W0ayrlZjGF1cbR
|
||||
8NCJUL2Nwj0ywKIavC99Ipkb8AsFwpVT6U6effs6
|
||||
=xybZ
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
signed commit`
|
||||
|
||||
sha := &Sha256Hash{
|
||||
0x94, 0x33, 0xb2, 0xa6, 0x2b, 0x96, 0x4c, 0x17, 0xa4, 0x48, 0x5a, 0xe1, 0x80, 0xf4, 0x5f, 0x59,
|
||||
0x5d, 0x3e, 0x69, 0xd3, 0x1b, 0x78, 0x60, 0x87, 0x77, 0x5e, 0x28, 0xc6, 0xb6, 0x39, 0x9d, 0xf0,
|
||||
}
|
||||
gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare_sha256"))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, gitRepo)
|
||||
defer gitRepo.Close()
|
||||
|
||||
commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString))
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, commitFromReader) {
|
||||
return
|
||||
}
|
||||
assert.EqualValues(t, sha, commitFromReader.ID)
|
||||
assert.EqualValues(t, `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIrBAABCgAtFiEES+fB08xlgTrzSdQvhkUIsBsmec8FAmU/wKoPHGFtYWplckBz
|
||||
dXNlLmRlAAoJEIZFCLAbJnnP4s4PQIJATa++WPzR6/H4etT7bsOGoMyguEJYyWOd
|
||||
aTybplzT7QAL7h2to0QszGabtzMJPIA39xSFZNYNN30voK5YyyYibXluPKgjemfK
|
||||
WNXwF+gkwgZI38gSvKf+vlqI+EYyIFe19wOhiju0m8SIlB5NEPiWHa17q2mqmqqx
|
||||
1FWa2JdqLPYjAtSLFXeSZegrY5V1FxdemyMUONkg8YO9OSIMZiE0GsnnOXQ3xcT4
|
||||
JTCnmlUxIKw689UiEY80JopUIq+Wl7+qq9507IYYSUCyB6JazL42AKMzVCbD+qBP
|
||||
oOzh/hafYgk9H9qCQXaLbmvs17zXRpicig1bAzqgAy1FDelvpERyRTydEajSLIG6
|
||||
U1cRCkgXCZ0NfsYNPPmBa8b3+rnstypXYTbyMwTln7FfUAaGo6o9JYiPMkzxlmsy
|
||||
zfp/tcaY8+LlBL9aOJjtv+a0p+HrpCGd6CCa4ARfphTLq8QRSSh8uzlB9N+6HnRI
|
||||
VAEUo6ecdDxSpyt2naeg9pKus/BRi7P6g4B1hkk/zZstUX/QP4IQuAJbXjkvsC+X
|
||||
HKRr3NlRM/DygzTyj0gN74uoa0goCIbyAQhiT42nm0cuhM7uN/W0ayrlZjGF1cbR
|
||||
8NCJUL2Nwj0ywKIavC99Ipkb8AsFwpVT6U6effs6
|
||||
=xybZ
|
||||
-----END PGP SIGNATURE-----
|
||||
`, commitFromReader.Signature.Signature)
|
||||
assert.EqualValues(t, `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e
|
||||
parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8
|
||||
author Adam Majer <amajer@suse.de> 1698676906 +0100
|
||||
committer Adam Majer <amajer@suse.de> 1698676906 +0100
|
||||
|
||||
signed commit`, commitFromReader.Signature.Payload)
|
||||
assert.EqualValues(t, "Adam Majer <amajer@suse.de>", commitFromReader.Author.String())
|
||||
|
||||
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
|
||||
assert.NoError(t, err)
|
||||
commitFromReader.CommitMessage += "\n\n"
|
||||
commitFromReader.Signature.Payload += "\n\n"
|
||||
assert.EqualValues(t, commitFromReader, commitFromReader2)
|
||||
}
|
||||
|
||||
func TestHasPreviousCommitSha256(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256")
|
||||
|
||||
repo, err := openRepositoryWithDefaultContext(bareRepo1Path)
|
||||
assert.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
commit, err := repo.GetCommit("f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc")
|
||||
assert.NoError(t, err)
|
||||
|
||||
objectFormat, err := repo.GetObjectFormat()
|
||||
assert.NoError(t, err)
|
||||
|
||||
parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c")
|
||||
notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236")
|
||||
assert.Equal(t, objectFormat, parentSHA.Type())
|
||||
assert.Equal(t, objectFormat.Name(), "sha256")
|
||||
|
||||
haz, err := commit.HasPreviousCommit(parentSHA)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, haz)
|
||||
|
||||
hazNot, err := commit.HasPreviousCommit(notParentSHA)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hazNot)
|
||||
|
||||
selfNot, err := commit.HasPreviousCommit(commit.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, selfNot)
|
||||
}
|
||||
|
||||
func TestGetCommitFileStatusMergesSha256(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo6_merge_sha256")
|
||||
|
||||
commitFileStatus, err := GetCommitFileStatus(DefaultContext, bareRepo1Path, "d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := CommitFileStatus{
|
||||
[]string{
|
||||
"add_file.txt",
|
||||
},
|
||||
[]string{},
|
||||
[]string{
|
||||
"to_modify.txt",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected.Added, commitFileStatus.Added)
|
||||
assert.Equal(t, expected.Removed, commitFileStatus.Removed)
|
||||
assert.Equal(t, expected.Modified, commitFileStatus.Modified)
|
||||
|
||||
expected = CommitFileStatus{
|
||||
[]string{},
|
||||
[]string{
|
||||
"to_remove.txt",
|
||||
},
|
||||
[]string{},
|
||||
}
|
||||
|
||||
commitFileStatus, err = GetCommitFileStatus(DefaultContext, bareRepo1Path, "da1ded40dc8e5b7c564171f4bf2fc8370487decfb1cb6a99ef28f3ed73d09172")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected.Added, commitFileStatus.Added)
|
||||
assert.Equal(t, expected.Removed, commitFileStatus.Removed)
|
||||
assert.Equal(t, expected.Modified, commitFileStatus.Modified)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ gpgsig -----BEGIN PGP SIGNATURE-----
|
||||
|
||||
empty commit`
|
||||
|
||||
sha := SHA1{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2}
|
||||
sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2}
|
||||
gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, gitRepo)
|
||||
@@ -125,6 +125,73 @@ empty commit`, commitFromReader.Signature.Payload)
|
||||
assert.EqualValues(t, commitFromReader, commitFromReader2)
|
||||
}
|
||||
|
||||
func TestCommitWithEncodingFromReader(t *testing.T) {
|
||||
commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074
|
||||
tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
|
||||
parent 47b24e7ab977ed31c5a39989d570847d6d0052af
|
||||
author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
||||
committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
||||
encoding ISO-8859-1
|
||||
gpgsig -----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow
|
||||
Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR
|
||||
gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq
|
||||
zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr
|
||||
frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j
|
||||
FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd
|
||||
G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn
|
||||
SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F
|
||||
yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
|
||||
jw4YcO5u
|
||||
=r3UU
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
ISO-8859-1`
|
||||
|
||||
sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2}
|
||||
gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, gitRepo)
|
||||
defer gitRepo.Close()
|
||||
|
||||
commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString))
|
||||
assert.NoError(t, err)
|
||||
if !assert.NotNil(t, commitFromReader) {
|
||||
return
|
||||
}
|
||||
assert.EqualValues(t, sha, commitFromReader.ID)
|
||||
assert.EqualValues(t, `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow
|
||||
Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR
|
||||
gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq
|
||||
zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr
|
||||
frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j
|
||||
FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd
|
||||
G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn
|
||||
SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F
|
||||
yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
|
||||
jw4YcO5u
|
||||
=r3UU
|
||||
-----END PGP SIGNATURE-----
|
||||
`, commitFromReader.Signature.Signature)
|
||||
assert.EqualValues(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
|
||||
parent 47b24e7ab977ed31c5a39989d570847d6d0052af
|
||||
author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
||||
committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
||||
encoding ISO-8859-1
|
||||
|
||||
ISO-8859-1`, commitFromReader.Signature.Payload)
|
||||
assert.EqualValues(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String())
|
||||
|
||||
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n"))
|
||||
assert.NoError(t, err)
|
||||
commitFromReader.CommitMessage += "\n\n"
|
||||
commitFromReader.Signature.Payload += "\n\n"
|
||||
assert.EqualValues(t, commitFromReader, commitFromReader2)
|
||||
}
|
||||
|
||||
func TestHasPreviousCommit(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ func (err ErrBranchNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrPushOutOfDate represents an error if merging fails due to unrelated histories
|
||||
// ErrPushOutOfDate represents an error if merging fails due to the base branch being updated
|
||||
type ErrPushOutOfDate struct {
|
||||
StdOut string
|
||||
StdErr string
|
||||
|
||||
@@ -49,9 +49,9 @@ func TestFormat_Flag(t *testing.T) {
|
||||
{
|
||||
name: "multiple fields",
|
||||
|
||||
givenFormat: foreachref.NewFormat("refname:short", "objecttype", "objectname"),
|
||||
givenFormat: foreachref.NewFormat("refname:lstrip=2", "objecttype", "objectname"),
|
||||
|
||||
wantFlag: "refname:short %(refname:short)%00objecttype %(objecttype)%00objectname %(objectname)%00%00",
|
||||
wantFlag: "refname:lstrip=2 %(refname:lstrip=2)%00objecttype %(objecttype)%00objectname %(objectname)%00%00",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+71
-50
@@ -33,42 +33,45 @@ var (
|
||||
// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx
|
||||
DefaultContext context.Context
|
||||
|
||||
// SupportProcReceive version >= 2.29.0
|
||||
SupportProcReceive bool
|
||||
DefaultFeatures struct {
|
||||
GitVersion *version.Version
|
||||
|
||||
gitVersion *version.Version
|
||||
SupportProcReceive bool // >= 2.29
|
||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||
}
|
||||
)
|
||||
|
||||
// loadGitVersion returns current Git version from shell. Internal usage only.
|
||||
func loadGitVersion() (*version.Version, error) {
|
||||
// loadGitVersion tries to get the current git version and stores it into a global variable
|
||||
func loadGitVersion() error {
|
||||
// doesn't need RWMutex because it's executed by Init()
|
||||
if gitVersion != nil {
|
||||
return gitVersion, nil
|
||||
if DefaultFeatures.GitVersion != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil)
|
||||
if runErr != nil {
|
||||
return nil, runErr
|
||||
return runErr
|
||||
}
|
||||
|
||||
fields := strings.Fields(stdout)
|
||||
ver, err := parseGitVersionLine(strings.TrimSpace(stdout))
|
||||
if err == nil {
|
||||
DefaultFeatures.GitVersion = ver
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func parseGitVersionLine(s string) (*version.Version, error) {
|
||||
fields := strings.Fields(s)
|
||||
if len(fields) < 3 {
|
||||
return nil, fmt.Errorf("invalid git version output: %s", stdout)
|
||||
return nil, fmt.Errorf("invalid git version: %q", s)
|
||||
}
|
||||
|
||||
var versionString string
|
||||
|
||||
// Handle special case on Windows.
|
||||
i := strings.Index(fields[2], "windows")
|
||||
if i >= 1 {
|
||||
versionString = fields[2][:i-1]
|
||||
} else {
|
||||
versionString = fields[2]
|
||||
// version string is like: "git version 2.29.3" or "git version 2.29.3.windows.1"
|
||||
versionString := fields[2]
|
||||
if pos := strings.Index(versionString, "windows"); pos >= 1 {
|
||||
versionString = versionString[:pos-1]
|
||||
}
|
||||
|
||||
var err error
|
||||
gitVersion, err = version.NewVersion(versionString)
|
||||
return gitVersion, err
|
||||
return version.NewVersion(versionString)
|
||||
}
|
||||
|
||||
// SetExecutablePath changes the path of git executable and checks the file permission and version.
|
||||
@@ -83,8 +86,7 @@ func SetExecutablePath(path string) error {
|
||||
}
|
||||
GitExecutable = absPath
|
||||
|
||||
_, err = loadGitVersion()
|
||||
if err != nil {
|
||||
if err = loadGitVersion(); err != nil {
|
||||
return fmt.Errorf("unable to load git version: %w", err)
|
||||
}
|
||||
|
||||
@@ -93,7 +95,7 @@ func SetExecutablePath(path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if gitVersion.LessThan(versionRequired) {
|
||||
if DefaultFeatures.GitVersion.LessThan(versionRequired) {
|
||||
moreHint := "get git: https://git-scm.com/download/"
|
||||
if runtime.GOOS == "linux" {
|
||||
// there are a lot of CentOS/RHEL users using old git, so we add a special hint for them
|
||||
@@ -102,19 +104,22 @@ func SetExecutablePath(path string) error {
|
||||
moreHint = "get git: https://git-scm.com/download/linux and https://ius.io"
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", gitVersion.Original(), RequiredVersion, moreHint)
|
||||
return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", DefaultFeatures.GitVersion.Original(), RequiredVersion, moreHint)
|
||||
}
|
||||
|
||||
if err = checkGitVersionCompatibility(DefaultFeatures.GitVersion); err != nil {
|
||||
return fmt.Errorf("installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git", DefaultFeatures.GitVersion.String(), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VersionInfo returns git version information
|
||||
func VersionInfo() string {
|
||||
if gitVersion == nil {
|
||||
if DefaultFeatures.GitVersion == nil {
|
||||
return "(git not found)"
|
||||
}
|
||||
format := "%s"
|
||||
args := []any{gitVersion.Original()}
|
||||
args := []any{DefaultFeatures.GitVersion.Original()}
|
||||
// Since git wire protocol has been released from git v2.18
|
||||
if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
|
||||
format += ", Wire Protocol %s Enabled"
|
||||
@@ -166,10 +171,6 @@ func InitSimple(ctx context.Context) error {
|
||||
// InitFull initializes git module with version check and change global variables, sync gitconfig.
|
||||
// It should only be called once at the beginning of the program initialization (TestMain/GlobalInitInstalled) as this code makes unsynchronized changes to variables.
|
||||
func InitFull(ctx context.Context) (err error) {
|
||||
if err = checkInit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = InitSimple(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -188,7 +189,13 @@ func InitFull(ctx context.Context) (err error) {
|
||||
if CheckGitVersionAtLeast("2.9") == nil {
|
||||
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
|
||||
}
|
||||
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
|
||||
DefaultFeatures.SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
|
||||
DefaultFeatures.SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit
|
||||
if DefaultFeatures.SupportHashSha256 {
|
||||
SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat)
|
||||
} else {
|
||||
log.Warn("sha256 hash support is disabled - requires Git >= 2.42. Gogit is currently unsupported")
|
||||
}
|
||||
|
||||
if setting.LFS.StartServer {
|
||||
if CheckGitVersionAtLeast("2.1.2") != nil {
|
||||
@@ -249,7 +256,7 @@ func syncGitConfig() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if SupportProcReceive {
|
||||
if DefaultFeatures.SupportProcReceive {
|
||||
// set support for AGit flow
|
||||
if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
@@ -260,19 +267,18 @@ func syncGitConfig() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user
|
||||
// however, some docker users and samba users find it difficult to configure their systems so that Gitea's git repositories are owned by the Gitea user. (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
|
||||
// see issue: https://github.com/go-gitea/gitea/issues/19455
|
||||
// Fundamentally the problem lies with the uid-gid-mapping mechanism for filesystems in docker on windows (and to a lesser extent samba).
|
||||
// Docker's configuration mechanism for local filesystems provides no way of setting this mapping and although there is a mechanism for setting this uid through using cifs mounting it is complicated and essentially undocumented
|
||||
// Thus the owner uid/gid for files on these filesystems will be marked as root.
|
||||
// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user.
|
||||
// However, some docker users and samba users find it difficult to configure their systems correctly,
|
||||
// so that Gitea's git repositories are owned by the Gitea user.
|
||||
// (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
|
||||
// See issue: https://github.com/go-gitea/gitea/issues/19455
|
||||
// As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea,
|
||||
// it is now safe to set "safe.directory=*" for internal usage only.
|
||||
// Please note: the wildcard "*" is only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later
|
||||
// Although only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later - this setting is tolerated by earlier versions
|
||||
// Although this setting is only supported by some new git versions, it is also tolerated by earlier versions
|
||||
if err := configAddNonExist("safe.directory", "*"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := configSet("core.longpaths", "true"); err != nil {
|
||||
return err
|
||||
@@ -305,22 +311,37 @@ func syncGitConfig() (err error) {
|
||||
|
||||
// CheckGitVersionAtLeast check git version is at least the constraint version
|
||||
func CheckGitVersionAtLeast(atLeast string) error {
|
||||
if _, err := loadGitVersion(); err != nil {
|
||||
return err
|
||||
if DefaultFeatures.GitVersion == nil {
|
||||
panic("git module is not initialized") // it shouldn't happen
|
||||
}
|
||||
atLeastVersion, err := version.NewVersion(atLeast)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if gitVersion.Compare(atLeastVersion) < 0 {
|
||||
return fmt.Errorf("installed git binary version %s is not at least %s", gitVersion.Original(), atLeast)
|
||||
if DefaultFeatures.GitVersion.Compare(atLeastVersion) < 0 {
|
||||
return fmt.Errorf("installed git binary version %s is not at least %s", DefaultFeatures.GitVersion.Original(), atLeast)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkGitVersionCompatibility(gitVer *version.Version) error {
|
||||
badVersions := []struct {
|
||||
Version *version.Version
|
||||
Reason string
|
||||
}{
|
||||
{version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"},
|
||||
}
|
||||
for _, bad := range badVersions {
|
||||
if gitVer.Equal(bad.Version) {
|
||||
return errors.New(bad.Reason)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configSet(key, value string) error {
|
||||
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
if err != nil && !err.IsExitCode(1) {
|
||||
if err != nil && !IsErrorExitCode(err, 1) {
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
}
|
||||
|
||||
@@ -343,7 +364,7 @@ func configSetNonExist(key, value string) error {
|
||||
// already exist
|
||||
return nil
|
||||
}
|
||||
if err.IsExitCode(1) {
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist, set new config
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
|
||||
if err != nil {
|
||||
@@ -361,7 +382,7 @@ func configAddNonExist(key, value string) error {
|
||||
// already exist
|
||||
return nil
|
||||
}
|
||||
if err.IsExitCode(1) {
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist, add new config
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil)
|
||||
if err != nil {
|
||||
@@ -382,7 +403,7 @@ func configUnsetAll(key, value string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err.IsExitCode(1) {
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -93,3 +94,25 @@ func TestSyncConfig(t *testing.T) {
|
||||
assert.True(t, gitConfigContains("[sync-test]"))
|
||||
assert.True(t, gitConfigContains("cfg-key-a = CfgValA"))
|
||||
}
|
||||
|
||||
func TestParseGitVersion(t *testing.T) {
|
||||
v, err := parseGitVersionLine("git version 2.29.3")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2.29.3", v.String())
|
||||
|
||||
v, err = parseGitVersionLine("git version 2.29.3.windows.1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2.29.3", v.String())
|
||||
|
||||
_, err = parseGitVersionLine("git version")
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = parseGitVersionLine("git version windows")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCheckGitVersionCompatibility(t *testing.T) {
|
||||
assert.NoError(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.0"))))
|
||||
assert.ErrorContains(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.1"))), "regression bug of GIT_FLUSH")
|
||||
assert.NoError(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.2"))))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type GrepResult struct {
|
||||
Filename string
|
||||
LineNumbers []int
|
||||
LineCodes []string
|
||||
}
|
||||
|
||||
type GrepOptions struct {
|
||||
RefName string
|
||||
MaxResultLimit int
|
||||
ContextLineNumber int
|
||||
IsFuzzy bool
|
||||
}
|
||||
|
||||
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
|
||||
stdoutReader, stdoutWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create os pipe to grep: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
|
||||
/*
|
||||
The output is like this ( "^@" means \x00):
|
||||
|
||||
HEAD:.air.toml
|
||||
6^@bin = "gitea"
|
||||
|
||||
HEAD:.changelog.yml
|
||||
2^@repo: go-gitea/gitea
|
||||
*/
|
||||
var results []*GrepResult
|
||||
cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name")
|
||||
cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber))
|
||||
if opts.IsFuzzy {
|
||||
words := strings.Fields(search)
|
||||
for _, word := range words {
|
||||
cmd.AddOptionValues("-e", strings.TrimLeft(word, "-"))
|
||||
}
|
||||
} else {
|
||||
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
|
||||
}
|
||||
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
|
||||
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
|
||||
stderr := bytes.Buffer{}
|
||||
err = cmd.Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: stdoutWriter,
|
||||
Stderr: &stderr,
|
||||
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
|
||||
_ = stdoutWriter.Close()
|
||||
defer stdoutReader.Close()
|
||||
|
||||
isInBlock := false
|
||||
scanner := bufio.NewScanner(stdoutReader)
|
||||
var res *GrepResult
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !isInBlock {
|
||||
if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok {
|
||||
isInBlock = true
|
||||
res = &GrepResult{Filename: filename}
|
||||
results = append(results, res)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if line == "" {
|
||||
if len(results) >= opts.MaxResultLimit {
|
||||
cancel()
|
||||
break
|
||||
}
|
||||
isInBlock = false
|
||||
continue
|
||||
}
|
||||
if line == "--" {
|
||||
continue
|
||||
}
|
||||
if lineNum, lineCode, ok := strings.Cut(line, "\x00"); ok {
|
||||
lineNumInt, _ := strconv.Atoi(lineNum)
|
||||
res.LineNumbers = append(res.LineNumbers, lineNumInt)
|
||||
res.LineCodes = append(res.LineCodes, lineCode)
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
},
|
||||
})
|
||||
// git grep exits by cancel (killed), usually it is caused by the limit of results
|
||||
if IsErrorExitCode(err, -1) && stderr.Len() == 0 {
|
||||
return results, nil
|
||||
}
|
||||
// git grep exits with 1 if no results are found
|
||||
if IsErrorExitCode(err, 1) && stderr.Len() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String())
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGrepSearch(t *testing.T) {
|
||||
repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "language_stats_repo"))
|
||||
assert.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
res, err := GrepSearch(context.Background(), repo, "void", GrepOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*GrepResult{
|
||||
{
|
||||
Filename: "java-hello/main.java",
|
||||
LineNumbers: []int{3},
|
||||
LineCodes: []string{" public static void main(String[] args)"},
|
||||
},
|
||||
{
|
||||
Filename: "main.vendor.java",
|
||||
LineNumbers: []int{3},
|
||||
LineCodes: []string{" public static void main(String[] args)"},
|
||||
},
|
||||
}, res)
|
||||
|
||||
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*GrepResult{
|
||||
{
|
||||
Filename: "java-hello/main.java",
|
||||
LineNumbers: []int{3},
|
||||
LineCodes: []string{" public static void main(String[] args)"},
|
||||
},
|
||||
}, res)
|
||||
|
||||
res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 0)
|
||||
|
||||
res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{})
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, res, 0)
|
||||
}
|
||||
@@ -4,12 +4,11 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
// Cache represents a caching interface
|
||||
@@ -39,7 +38,7 @@ func NewLastCommitCache(count int64, repoPath string, gitRepo *Repository, cache
|
||||
if cache == nil {
|
||||
return nil
|
||||
}
|
||||
if !setting.CacheService.LastCommit.Enabled || count < setting.CacheService.LastCommit.CommitsCount {
|
||||
if count < setting.CacheService.LastCommit.CommitsCount {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -92,17 +91,17 @@ func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) {
|
||||
|
||||
// GetCommitByPath gets the last commit for the entry in the provided commit
|
||||
func (c *LastCommitCache) GetCommitByPath(commitID, entryPath string) (*Commit, error) {
|
||||
sha1, err := NewIDFromString(commitID)
|
||||
sha, err := NewIDFromString(commitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lastCommit, err := c.Get(sha1.String(), entryPath)
|
||||
lastCommit, err := c.Get(sha.String(), entryPath)
|
||||
if err != nil || lastCommit != nil {
|
||||
return lastCommit, err
|
||||
}
|
||||
|
||||
lastCommit, err = c.repo.getCommitByPathWithID(sha1, entryPath)
|
||||
lastCommit, err = c.repo.getCommitByPathWithID(sha, entryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ package git
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
||||
)
|
||||
|
||||
@@ -18,7 +19,7 @@ func (c *Commit) CacheCommit(ctx context.Context) error {
|
||||
}
|
||||
commitNodeIndex, _ := c.repo.CommitNodeIndex()
|
||||
|
||||
index, err := commitNodeIndex.Get(c.ID)
|
||||
index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -143,17 +143,20 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int
|
||||
}
|
||||
|
||||
// Our "line" must look like: <commitid> SP (<parent> SP) * NUL
|
||||
ret.CommitID = string(g.next[0:40])
|
||||
parents := string(g.next[41:])
|
||||
commitIDs := string(g.next)
|
||||
if g.buffull {
|
||||
more, err := g.rd.ReadString('\x00')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parents += more
|
||||
commitIDs += more
|
||||
}
|
||||
commitIDs = commitIDs[:len(commitIDs)-1]
|
||||
splitIDs := strings.Split(commitIDs, " ")
|
||||
ret.CommitID = splitIDs[0]
|
||||
if len(splitIDs) > 1 {
|
||||
ret.ParentIDs = splitIDs[1:]
|
||||
}
|
||||
parents = parents[:len(parents)-1]
|
||||
ret.ParentIDs = strings.Split(parents, " ")
|
||||
|
||||
// now read the next "line"
|
||||
g.buffull = false
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
@@ -72,7 +73,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
|
||||
defer commitGraphFile.Close()
|
||||
}
|
||||
|
||||
commitNode, err := commitNodeIndex.Get(notes.ID)
|
||||
commitNode, err := commitNodeIndex.Get(plumbing.Hash(notes.ID.RawValue()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user