Merge branch 'main' into main

This commit is contained in:
Tomeamis 2023-12-19 12:57:18 +01:00 committed by GitHub
commit 0421028556
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
298 changed files with 2558 additions and 2571 deletions

54
.github/stale.yml vendored
View File

@ -1,54 +0,0 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 60
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 14
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- status/blocked
- kind/security
- lgtm/done
- reviewed/confirmed
- priority/critical
- kind/proposal
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had recent activity.
I am here to help clear issues left open even if solved or waiting for more insight.
This issue will be closed if no further activity occurs during the next 2 weeks.
If the issue is still valid just add a comment to keep it alive.
Thank you for your contributions.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue has been automatically closed because of inactivity.
You can re-open it if needed.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 1
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
pulls:
daysUntilStale: 60
daysUntilClose: 60
markComment: >
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs during the next 2 months. Thank you
for your contributions.
closeComment: >
This pull request has been automatically closed because of inactivity.
You can re-open it if needed.

View File

@ -78,6 +78,8 @@ jobs:
id: meta id: meta
with: with:
images: gitea/gitea images: gitea/gitea
flavor: |
latest=false
# 1.2.3-rc0 # 1.2.3-rc0
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
@ -109,6 +111,7 @@ jobs:
images: gitea/gitea images: gitea/gitea
# each tag below will have the suffix of -rootless # each tag below will have the suffix of -rootless
flavor: | flavor: |
latest=false
suffix=-rootless suffix=-rootless
# 1.2.3-rc0 # 1.2.3-rc0
tags: | tags: |

View File

@ -86,7 +86,6 @@ jobs:
# 1.2 # 1.2
# 1.2.3 # 1.2.3
tags: | tags: |
type=raw,value=latest
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}} type=semver,pattern={{version}}
@ -118,14 +117,13 @@ jobs:
images: gitea/gitea images: gitea/gitea
# each tag below will have the suffix of -rootless # each tag below will have the suffix of -rootless
flavor: | flavor: |
suffix=-rootless suffix=-rootless,onlatest=true
# this will generate tags in the following format (with -rootless suffix added): # this will generate tags in the following format (with -rootless suffix added):
# latest # latest
# 1 # 1
# 1.2 # 1.2
# 1.2.3 # 1.2.3
tags: | tags: |
type=raw,value=latest
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}} type=semver,pattern={{version}}

File diff suppressed because one or more lines are too long

View File

@ -15,9 +15,8 @@ import (
var ( var (
// CmdActions represents the available actions sub-commands. // CmdActions represents the available actions sub-commands.
CmdActions = &cli.Command{ CmdActions = &cli.Command{
Name: "actions", Name: "actions",
Usage: "", Usage: "Manage Gitea Actions",
Description: "Commands for managing Gitea Actions",
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
subcmdActionsGenRunnerToken, subcmdActionsGenRunnerToken,
}, },

View File

@ -21,7 +21,7 @@ var (
// CmdAdmin represents the available admin sub-command. // CmdAdmin represents the available admin sub-command.
CmdAdmin = &cli.Command{ CmdAdmin = &cli.Command{
Name: "admin", Name: "admin",
Usage: "Command line interface to perform common administrative operations", Usage: "Perform common administrative operations",
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
subcmdUser, subcmdUser,
subcmdRepoSyncReleases, subcmdRepoSyncReleases,

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/migrations" "code.gitea.io/gitea/models/migrations"
migrate_base "code.gitea.io/gitea/models/migrations/base" migrate_base "code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/doctor" "code.gitea.io/gitea/modules/doctor"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -22,6 +23,19 @@ import (
"xorm.io/xorm" "xorm.io/xorm"
) )
// CmdDoctor represents the available doctor sub-command.
var CmdDoctor = &cli.Command{
Name: "doctor",
Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
Subcommands: []*cli.Command{
cmdDoctorCheck,
cmdRecreateTable,
cmdDoctorConvert,
},
}
var cmdDoctorCheck = &cli.Command{ var cmdDoctorCheck = &cli.Command{
Name: "check", Name: "check",
Usage: "Diagnose and optionally fix problems", Usage: "Diagnose and optionally fix problems",
@ -60,19 +74,6 @@ var cmdDoctorCheck = &cli.Command{
}, },
} }
// CmdDoctor represents the available doctor sub-command.
var CmdDoctor = &cli.Command{
Name: "doctor",
Usage: "Diagnose and optionally fix problems",
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
Subcommands: []*cli.Command{
cmdDoctorCheck,
cmdRecreateTable,
cmdDoctorConvert,
},
}
var cmdRecreateTable = &cli.Command{ var cmdRecreateTable = &cli.Command{
Name: "recreate-table", Name: "recreate-table",
Usage: "Recreate tables from XORM definitions and copy the data.", Usage: "Recreate tables from XORM definitions and copy the data.",
@ -177,6 +178,7 @@ func runDoctorCheck(ctx *cli.Context) error {
if ctx.IsSet("list") { if ctx.IsSet("list") {
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
_, _ = w.Write([]byte("Default\tName\tTitle\n")) _, _ = w.Write([]byte("Default\tName\tTitle\n"))
doctor.SortChecks(doctor.Checks)
for _, check := range doctor.Checks { for _, check := range doctor.Checks {
if check.IsDefault { if check.IsDefault {
_, _ = w.Write([]byte{'*'}) _, _ = w.Write([]byte{'*'})
@ -192,26 +194,20 @@ func runDoctorCheck(ctx *cli.Context) error {
var checks []*doctor.Check var checks []*doctor.Check
if ctx.Bool("all") { if ctx.Bool("all") {
checks = doctor.Checks checks = make([]*doctor.Check, len(doctor.Checks))
copy(checks, doctor.Checks)
} else if ctx.IsSet("run") { } else if ctx.IsSet("run") {
addDefault := ctx.Bool("default") addDefault := ctx.Bool("default")
names := ctx.StringSlice("run") runNamesSet := container.SetOf(ctx.StringSlice("run")...)
for i, name := range names {
names[i] = strings.ToLower(strings.TrimSpace(name))
}
for _, check := range doctor.Checks { for _, check := range doctor.Checks {
if addDefault && check.IsDefault { if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
checks = append(checks, check) checks = append(checks, check)
continue runNamesSet.Remove(check.Name)
}
for _, name := range names {
if name == check.Name {
checks = append(checks, check)
break
}
} }
} }
if len(runNamesSet) > 0 {
return fmt.Errorf("unknown checks: %q", strings.Join(runNamesSet.Values(), ","))
}
} else { } else {
for _, check := range doctor.Checks { for _, check := range doctor.Checks {
if check.IsDefault { if check.IsDefault {
@ -219,6 +215,5 @@ func runDoctorCheck(ctx *cli.Context) error {
} }
} }
} }
return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks) return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
} }

33
cmd/doctor_test.go Normal file
View File

@ -0,0 +1,33 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"testing"
"code.gitea.io/gitea/modules/doctor"
"code.gitea.io/gitea/modules/log"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
)
func TestDoctorRun(t *testing.T) {
doctor.Register(&doctor.Check{
Title: "Test Check",
Name: "test-check",
Run: func(ctx context.Context, logger log.Logger, autofix bool) error { return nil },
SkipDatabaseInitialization: true,
})
app := cli.NewApp()
app.Commands = []*cli.Command{cmdDoctorCheck}
err := app.Run([]string{"./gitea", "check", "--run", "test-check"})
assert.NoError(t, err)
err = app.Run([]string{"./gitea", "check", "--run", "no-such"})
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"})
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
}

View File

@ -18,7 +18,7 @@ var (
// CmdGenerate represents the available generate sub-command. // CmdGenerate represents the available generate sub-command.
CmdGenerate = &cli.Command{ CmdGenerate = &cli.Command{
Name: "generate", Name: "generate",
Usage: "Command line interface for running generators", Usage: "Generate Gitea's secrets/keys/tokens",
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
subcmdSecret, subcmdSecret,
}, },

View File

@ -31,8 +31,8 @@ var (
// CmdHook represents the available hooks sub-command. // CmdHook represents the available hooks sub-command.
CmdHook = &cli.Command{ CmdHook = &cli.Command{
Name: "hook", Name: "hook",
Usage: "Delegate commands to corresponding Git hooks", Usage: "(internal) Should only be called by Git",
Description: "This should only be called by Git", Description: "Delegate commands to corresponding Git hooks",
Before: PrepareConsoleLoggerLevel(log.FATAL), Before: PrepareConsoleLoggerLevel(log.FATAL),
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
subcmdHookPreReceive, subcmdHookPreReceive,
@ -376,7 +376,9 @@ Gitea or set your environment appropriately.`, "")
oldCommitIDs[count] = string(fields[0]) oldCommitIDs[count] = string(fields[0])
newCommitIDs[count] = string(fields[1]) newCommitIDs[count] = string(fields[1])
refFullNames[count] = git.RefName(fields[2]) refFullNames[count] = git.RefName(fields[2])
if refFullNames[count] == git.BranchPrefix+"master" && newCommitIDs[count] != git.EmptySHA && count == total {
commitID, _ := git.NewIDFromString(newCommitIDs[count])
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
masterPushed = true masterPushed = true
} }
count++ count++
@ -669,7 +671,8 @@ Gitea or set your environment appropriately.`, "")
if err != nil { if err != nil {
return err return err
} }
if rs.OldOID != git.EmptySHA { commitID, _ := git.NewIDFromString(rs.OldOID)
if !commitID.IsZero() {
err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID)) err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID))
if err != nil { if err != nil {
return err return err

View File

@ -16,10 +16,11 @@ import (
// CmdKeys represents the available keys sub-command // CmdKeys represents the available keys sub-command
var CmdKeys = &cli.Command{ var CmdKeys = &cli.Command{
Name: "keys", Name: "keys",
Usage: "This command queries the Gitea database to get the authorized command for a given ssh key fingerprint", Usage: "(internal) Should only be called by SSH server",
Before: PrepareConsoleLoggerLevel(log.FATAL), Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint",
Action: runKeys, Before: PrepareConsoleLoggerLevel(log.FATAL),
Action: runKeys,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "expected", Name: "expected",

View File

@ -10,12 +10,12 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// cmdHelp is our own help subcommand with more information // cmdHelp is our own help subcommand with more information
// Keep in mind that the "./gitea help"(subcommand) is different from "./gitea --help"(flag), the flag doesn't parse the config or output "DEFAULT CONFIGURATION:" information
func cmdHelp() *cli.Command { func cmdHelp() *cli.Command {
c := &cli.Command{ c := &cli.Command{
Name: "help", Name: "help",
@ -47,16 +47,10 @@ DEFAULT CONFIGURATION:
return c return c
} }
var helpFlag = cli.HelpFlag
func init() {
// cli.HelpFlag = nil TODO: after https://github.com/urfave/cli/issues/1794 we can use this
}
func appGlobalFlags() []cli.Flag { func appGlobalFlags() []cli.Flag {
return []cli.Flag{ return []cli.Flag{
// make the builtin flags at the top // make the builtin flags at the top
helpFlag, cli.HelpFlag,
// shared configuration flags, they are for global and for each sub-command at the same time // shared configuration flags, they are for global and for each sub-command at the same time
// eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed // eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed
@ -121,20 +115,22 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context)
func NewMainApp(version, versionExtra string) *cli.App { func NewMainApp(version, versionExtra string) *cli.App {
app := cli.NewApp() app := cli.NewApp()
app.Name = "Gitea" app.Name = "Gitea"
app.HelpName = "gitea"
app.Usage = "A painless self-hosted Git service" app.Usage = "A painless self-hosted Git service"
app.Description = `By default, Gitea will start serving using the web-server with no argument, which can alternatively be run by running the subcommand "web".` app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
app.Version = version + versionExtra app.Version = version + versionExtra
app.EnableBashCompletion = true app.EnableBashCompletion = true
// these sub-commands need to use config file // these sub-commands need to use config file
subCmdWithConfig := []*cli.Command{ subCmdWithConfig := []*cli.Command{
cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config"
CmdWeb, CmdWeb,
CmdServ, CmdServ,
CmdHook, CmdHook,
CmdKeys,
CmdDump, CmdDump,
CmdAdmin, CmdAdmin,
CmdMigrate, CmdMigrate,
CmdKeys,
CmdDoctor, CmdDoctor,
CmdManager, CmdManager,
CmdEmbedded, CmdEmbedded,
@ -142,13 +138,8 @@ func NewMainApp(version, versionExtra string) *cli.App {
CmdDumpRepository, CmdDumpRepository,
CmdRestoreRepository, CmdRestoreRepository,
CmdActions, CmdActions,
cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config"
} }
cmdConvert := util.ToPointer(*cmdDoctorConvert)
cmdConvert.Hidden = true // still support the legacy "./gitea doctor" by the hidden sub-command, remove it in next release
subCmdWithConfig = append(subCmdWithConfig, cmdConvert)
// these sub-commands do not need the config file, and they do not depend on any path or environment variable. // these sub-commands do not need the config file, and they do not depend on any path or environment variable.
subCmdStandalone := []*cli.Command{ subCmdStandalone := []*cli.Command{
CmdCert, CmdCert,

View File

@ -42,7 +42,7 @@ const (
// CmdServ represents the available serv sub-command. // CmdServ represents the available serv sub-command.
var CmdServ = &cli.Command{ var CmdServ = &cli.Command{
Name: "serv", Name: "serv",
Usage: "This command should only be called by SSH shell", Usage: "(internal) Should only be called by SSH shell",
Description: "Serv provides access auth for repositories", Description: "Serv provides access auth for repositories",
Before: PrepareConsoleLoggerLevel(log.FATAL), Before: PrepareConsoleLoggerLevel(log.FATAL),
Action: runServ, Action: runServ,

View File

@ -17,7 +17,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/google/go-github/v53/github" "github.com/google/go-github/v57/github"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )

View File

@ -492,6 +492,11 @@ INTERNAL_TOKEN=
;; Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations. ;; Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations.
;; This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security. ;; This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
;SUCCESSFUL_TOKENS_CACHE_SIZE = 20 ;SUCCESSFUL_TOKENS_CACHE_SIZE = 20
;;
;; Reject API tokens sent in URL query string (Accept Header-based API tokens only). This avoids security vulnerabilities
;; stemming from cached/logged plain-text API tokens.
;; In future releases, this will become the default behavior
;DISABLE_QUERY_AUTH_TOKEN = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1207,6 +1212,9 @@ LEVEL = Info
;; Max size of files to be displayed (default is 8MiB) ;; Max size of files to be displayed (default is 8MiB)
;MAX_DISPLAY_FILE_SIZE = 8388608 ;MAX_DISPLAY_FILE_SIZE = 8388608
;; ;;
;; Detect ambiguous unicode characters in file contents and show warnings on the UI
;AMBIGUOUS_UNICODE_DETECTION = true
;;
;; Whether the email of the user should be shown in the Explore Users page ;; Whether the email of the user should be shown in the Explore Users page
;SHOW_USER_EMAIL = true ;SHOW_USER_EMAIL = true
;; ;;
@ -1697,9 +1705,6 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ;;
;; if the cache enabled
;ENABLED = true
;;
;; Either "memory", "redis", "memcache", or "twoqueue". default is "memory" ;; Either "memory", "redis", "memcache", or "twoqueue". default is "memory"
;ADAPTER = memory ;ADAPTER = memory
;; ;;
@ -1724,8 +1729,6 @@ LEVEL = Info
;[cache.last_commit] ;[cache.last_commit]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; if the cache enabled
;ENABLED = true
;; ;;
;; Time to keep items in cache if not used, default is 8760 hours. ;; Time to keep items in cache if not used, default is 8760 hours.
;; Setting it to -1 disables caching ;; Setting it to -1 disables caching

View File

@ -19,10 +19,10 @@ Some jurisdictions (such as EU), requires certain legal pages (e.g. Privacy Poli
## Getting Pages ## Getting Pages
Gitea source code ships with sample pages, available in `contrib/legal` directory. Copy them to `custom/public/`. For example, to add Privacy Policy: Gitea source code ships with sample pages, available in `contrib/legal` directory. Copy them to `custom/public/assets/`. For example, to add Privacy Policy:
``` ```
wget -O /path/to/custom/public/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample wget -O /path/to/custom/public/assets/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
``` ```
Now you need to edit the page to meet your requirements. In particular you must change the email addresses, web addresses and references to "Your Gitea Instance" to match your situation. Now you need to edit the page to meet your requirements. In particular you must change the email addresses, web addresses and references to "Your Gitea Instance" to match your situation.

View File

@ -19,10 +19,10 @@ menu:
## 获取页面 ## 获取页面
Gitea 源代码附带了示例页面,位于 `contrib/legal` 目录中。将它们复制到 `custom/public/` 目录下。例如,如果要添加隐私政策: Gitea 源代码附带了示例页面,位于 `contrib/legal` 目录中。将它们复制到 `custom/public/assets/` 目录下。例如,如果要添加隐私政策:
``` ```
wget -O /path/to/custom/public/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample wget -O /path/to/custom/public/assets/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
``` ```
现在,你需要编辑该页面以满足你的需求。特别是,你必须更改电子邮件地址、网址以及与 "Your Gitea Instance" 相关的引用,以匹配你的情况。 现在,你需要编辑该页面以满足你的需求。特别是,你必须更改电子邮件地址、网址以及与 "Your Gitea Instance" 相关的引用,以匹配你的情况。

View File

@ -220,6 +220,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `THEMES`: **gitea-auto,gitea-light,gitea-dark**: All available themes. Allow users select personalized themes. - `THEMES`: **gitea-auto,gitea-light,gitea-dark**: All available themes. Allow users select personalized themes.
regardless of the value of `DEFAULT_THEME`. regardless of the value of `DEFAULT_THEME`.
- `MAX_DISPLAY_FILE_SIZE`: **8388608**: Max size of files to be displayed (default is 8MiB) - `MAX_DISPLAY_FILE_SIZE`: **8388608**: Max size of files to be displayed (default is 8MiB)
- `AMBIGUOUS_UNICODE_DETECTION`: **true**: Detect ambiguous unicode characters in file contents and show warnings on the UI
- `REACTIONS`: All available reactions users can choose on issues/prs and comments - `REACTIONS`: All available reactions users can choose on issues/prs and comments
Values can be emoji alias (:smile:) or a unicode emoji. Values can be emoji alias (:smile:) or a unicode emoji.
For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png
@ -572,6 +573,7 @@ And the following unique queues:
- off - do not check password complexity - off - do not check password complexity
- `PASSWORD_CHECK_PWN`: **false**: Check [HaveIBeenPwned](https://haveibeenpwned.com/Passwords) to see if a password has been exposed. - `PASSWORD_CHECK_PWN`: **false**: Check [HaveIBeenPwned](https://haveibeenpwned.com/Passwords) to see if a password has been exposed.
- `SUCCESSFUL_TOKENS_CACHE_SIZE`: **20**: Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations. This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security. - `SUCCESSFUL_TOKENS_CACHE_SIZE`: **20**: Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations. This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
- `DISABLE_QUERY_AUTH_TOKEN`: **false**: Reject API tokens sent in URL query string (Accept Header-based API tokens only). This setting will default to `true` in Gitea 1.23 and be deprecated in Gitea 1.24.
## Camo (`camo`) ## Camo (`camo`)
@ -761,7 +763,6 @@ and
## Cache (`cache`) ## Cache (`cache`)
- `ENABLED`: **true**: Enable the cache.
- `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, `redis-cluster`, `twoqueue` or `memcache`. (`twoqueue` represents a size limited LRU cache.) - `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, `redis-cluster`, `twoqueue` or `memcache`. (`twoqueue` represents a size limited LRU cache.)
- `INTERVAL`: **60**: Garbage Collection interval (sec), for memory and twoqueue cache only. - `INTERVAL`: **60**: Garbage Collection interval (sec), for memory and twoqueue cache only.
- `HOST`: **_empty_**: Connection string for `redis`, `redis-cluster` and `memcache`. For `twoqueue` sets configuration for the queue. - `HOST`: **_empty_**: Connection string for `redis`, `redis-cluster` and `memcache`. For `twoqueue` sets configuration for the queue.
@ -773,7 +774,6 @@ and
## Cache - LastCommitCache settings (`cache.last_commit`) ## Cache - LastCommitCache settings (`cache.last_commit`)
- `ENABLED`: **true**: Enable the cache.
- `ITEM_TTL`: **8760h**: Time to keep items in cache if not used, Setting it to -1 disables caching. - `ITEM_TTL`: **8760h**: Time to keep items in cache if not used, Setting it to -1 disables caching.
- `COMMITS_COUNT`: **1000**: Only enable the cache when repository's commits count great than. - `COMMITS_COUNT`: **1000**: Only enable the cache when repository's commits count great than.

View File

@ -721,7 +721,6 @@ Gitea 创建以下非唯一队列:
## 缓存 (`cache`) ## 缓存 (`cache`)
- `ENABLED`: **true**: 是否启用缓存。
- `ADAPTER`: **memory**: 缓存引擎,可以为 `memory`, `redis`, `redis-cluster`, `twoqueue``memcache`. (`twoqueue` 代表缓冲区固定的LRU缓存) - `ADAPTER`: **memory**: 缓存引擎,可以为 `memory`, `redis`, `redis-cluster`, `twoqueue``memcache`. (`twoqueue` 代表缓冲区固定的LRU缓存)
- `INTERVAL`: **60**: 垃圾回收间隔(秒),只对`memory``towqueue`有效。 - `INTERVAL`: **60**: 垃圾回收间隔(秒),只对`memory``towqueue`有效。
- `HOST`: **_empty_**: 缓存配置。`redis`, `redis-cluster``memcache`配置连接字符串;`twoqueue` 设置队列参数 - `HOST`: **_empty_**: 缓存配置。`redis`, `redis-cluster``memcache`配置连接字符串;`twoqueue` 设置队列参数
@ -733,7 +732,6 @@ Gitea 创建以下非唯一队列:
### 缓存 - 最后提交缓存设置 (`cache.last_commit`) ### 缓存 - 最后提交缓存设置 (`cache.last_commit`)
- `ENABLED`: **true**:是否启用缓存。
- `ITEM_TTL`: **8760h**:如果未使用,保持缓存中的项目的时间,将其设置为 -1 会禁用缓存。 - `ITEM_TTL`: **8760h**:如果未使用,保持缓存中的项目的时间,将其设置为 -1 会禁用缓存。
- `COMMITS_COUNT`: **1000**:仅在存储库的提交计数大于时启用缓存。 - `COMMITS_COUNT`: **1000**:仅在存储库的提交计数大于时启用缓存。
@ -1039,10 +1037,11 @@ Gitea 创建以下非唯一队列:
## API (`api`) ## API (`api`)
- `ENABLE_SWAGGER`: **true**: 是否启用swagger路由 (`/api/swagger`, `/api/v1/swagger`, …)。 - `ENABLE_SWAGGER`: **true**: 启用API文档接口 (`/api/swagger`, `/api/v1/swagger`, …). True or false。
- `MAX_RESPONSE_ITEMS`: **50**: 单个页面的最大 Feed. - `MAX_RESPONSE_ITEMS`: **50**: API分页的最大单页项目数。
- `ENABLE_OPENID_SIGNIN`: **false**: 允许使用OpenID登录当设置为`true`时可以通过 `/user/login` 页面进行OpenID登录。 - `DEFAULT_PAGING_NUM`: **30**: API分页的默认分页数。
- `DISABLE_REGISTRATION`: **false**: 关闭用户注册。 - `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Git trees API的默认单页项目数。
- `DEFAULT_MAX_BLOB_SIZE`: **10485760** (10MiB): blobs API的默认最大文件大小。
## OAuth2 (`oauth2`) ## OAuth2 (`oauth2`)

View File

@ -42,11 +42,11 @@ Gitea 引用 `custom` 目录中的自定义配置文件来覆盖配置、模板
将自定义的公共文件(比如页面和图片)作为 webroot 放在 `custom/public/` 中来让 Gitea 提供这些自定义内容(符号链接将被追踪)。 将自定义的公共文件(比如页面和图片)作为 webroot 放在 `custom/public/` 中来让 Gitea 提供这些自定义内容(符号链接将被追踪)。
举例说明:`image.png` 存放在 `custom/public/`中,那么它可以通过链接 http://gitea.domain.tld/assets/image.png 访问。 举例说明:`image.png` 存放在 `custom/public/assets/`中,那么它可以通过链接 http://gitea.domain.tld/assets/image.png 访问。
## 修改默认头像 ## 修改默认头像
替换以下目录中的 png 图片: `custom/public/img/avatar\_default.png` 替换以下目录中的 png 图片: `custom/public/assets/img/avatar\_default.png`
## 自定义 Gitea 页面 ## 自定义 Gitea 页面

View File

@ -194,7 +194,7 @@ ALLOW_DATA_URI_IMAGES = true
} }
``` ```
将您的样式表添加到自定义目录中,例如 `custom/public/css/my-style-XXXXX.css`,并使用自定义的头文件 `custom/templates/custom/header.tmpl` 进行导入: 将您的样式表添加到自定义目录中,例如 `custom/public/assets/css/my-style-XXXXX.css`,并使用自定义的头文件 `custom/templates/custom/header.tmpl` 进行导入:
```html ```html
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/my-style-XXXXX.css" /> <link rel="stylesheet" href="{{AppSubUrl}}/assets/css/my-style-XXXXX.css" />

View File

@ -362,7 +362,7 @@ If you are receiving errors on upgrade of Gitea using MySQL that read:
> `ORM engine initialization failed: migrate: do migrate: Error: 1118: Row size too large...` > `ORM engine initialization failed: migrate: do migrate: Error: 1118: Row size too large...`
Please run `gitea convert` or run `ALTER TABLE table_name ROW_FORMAT=dynamic;` for each table in the database. Please run `gitea doctor convert` or run `ALTER TABLE table_name ROW_FORMAT=dynamic;` for each table in the database.
The underlying problem is that the space allocated for indices by the default row format The underlying problem is that the space allocated for indices by the default row format
is too small. Gitea requires that the `ROWFORMAT` for its tables is `DYNAMIC`. is too small. Gitea requires that the `ROWFORMAT` for its tables is `DYNAMIC`.
@ -385,7 +385,7 @@ Unfortunately MySQL's `utf8` charset does not completely allow all possible UTF-
They created a new charset and collation called `utf8mb4` that allows for emoji to be stored but tables which use They created a new charset and collation called `utf8mb4` that allows for emoji to be stored but tables which use
the `utf8` charset, and connections which use the `utf8` charset will not use this. the `utf8` charset, and connections which use the `utf8` charset will not use this.
Please run `gitea convert`, or run `ALTER DATABASE database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;` Please run `gitea doctor convert`, or run `ALTER DATABASE database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`
for the database_name and run `ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;` for the database_name and run `ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`
for each table in the database. for each table in the database.

View File

@ -189,7 +189,7 @@ Gitea 目前支持三个官方主题,分别是 `gitea-light`、`gitea-dark`
假设我们的主题是 `arc-blue`(这是一个真实的主题,可以在[此问题](https://github.com/go-gitea/gitea/issues/6011)中找到) 假设我们的主题是 `arc-blue`(这是一个真实的主题,可以在[此问题](https://github.com/go-gitea/gitea/issues/6011)中找到)
`.css`文件命名为`theme-arc-blue.css`并将其添加到`custom/public/css`文件夹中 `.css`文件命名为`theme-arc-blue.css`并将其添加到`custom/public/assets/css`文件夹中
通过将`arc-blue`添加到`app.ini`中的`THEMES`列表中,允许用户使用该主题 通过将`arc-blue`添加到`app.ini`中的`THEMES`列表中,允许用户使用该主题
@ -366,7 +366,7 @@ Gitea 提供了一个子命令`gitea migrate`来初始化数据库,然后您
> `ORM engine initialization failed: migrate: do migrate: Error: 1118: Row size too large...` > `ORM engine initialization failed: migrate: do migrate: Error: 1118: Row size too large...`
请运行`gitea convert`或对数据库中的每个表运行`ALTER TABLE table_name ROW_FORMAT=dynamic;` 请运行 `gitea doctor convert` 或对数据库中的每个表运行 `ALTER TABLE table_name ROW_FORMAT=dynamic;`
潜在问题是默认行格式分配给每个表的索引空间 潜在问题是默认行格式分配给每个表的索引空间
太小。Gitea 要求其表的`ROWFORMAT``DYNAMIC` 太小。Gitea 要求其表的`ROWFORMAT``DYNAMIC`
@ -389,9 +389,8 @@ SET GLOBAL innodb_large_prefix=1;
他们创建了一个名为 `utf8mb4`的字符集和校对规则,允许存储 Emoji但使用 他们创建了一个名为 `utf8mb4`的字符集和校对规则,允许存储 Emoji但使用
utf8 字符集的表和连接将不会使用它。 utf8 字符集的表和连接将不会使用它。
请运行 `gitea convert` 或对数据库运行`ALTER DATABASE database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;` 请运行 `gitea doctor convert` 或对数据库运行 `ALTER DATABASE database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`
并对每个表运行 并对每个表运行 `ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`
`ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`
您还需要将`app.ini`文件中的数据库字符集设置为`CHARSET=utf8mb4` 您还需要将`app.ini`文件中的数据库字符集设置为`CHARSET=utf8mb4`

View File

@ -145,25 +145,25 @@ Adds the following fields:
Uses the following fields: Uses the following fields:
- Group Search Base (optional) - Group Search Base DN (optional)
- The LDAP DN used for groups. - The LDAP DN used for groups.
- Example: `ou=group,dc=mydomain,dc=com` - Example: `ou=group,dc=mydomain,dc=com`
- Group Name Filter (optional) - Group Attribute Containing List Of Users (optional)
- The attribute of the group object that lists/contains the group members.
- Example: `memberUid` or `member`
- An LDAP filter declaring how to find valid groups in the above DN. - User Attribute Listed in Group (optional)
- Example: `(|(cn=gitea_users)(cn=admins))`
- User Attribute in Group (optional)
- The user attribute that is used to reference a user in the group object. - The user attribute that is used to reference a user in the group object.
- Example: `uid` if the group objects contains a `member: bender` and the user object contains a `uid: bender`. - Example: `uid` if the group objects contains a `member: bender` and the user object contains a `uid: bender`.
- Example: `dn` if the group object contains a `member: uid=bender,ou=users,dc=planetexpress,dc=com`. - Example: `dn` if the group object contains a `member: uid=bender,ou=users,dc=planetexpress,dc=com`.
- Group Attribute for User (optional) - Verify group membership in LDAP (optional)
- The attribute of the group object that lists/contains the group members.
- Example: `memberUid` or `member` - An LDAP filter declaring how to find valid groups in the above DN.
- Example: `(|(cn=gitea_users)(cn=admins))`
## PAM (Pluggable Authentication Module) ## PAM (Pluggable Authentication Module)

168
go.mod
View File

@ -5,7 +5,7 @@ go 1.21
require ( require (
code.gitea.io/actions-proto-go v0.3.1 code.gitea.io/actions-proto-go v0.3.1
code.gitea.io/gitea-vet v0.2.3 code.gitea.io/gitea-vet v0.2.3
code.gitea.io/sdk/gitea v0.16.0 code.gitea.io/sdk/gitea v0.17.0
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669
gitea.com/go-chi/cache v0.2.0 gitea.com/go-chi/cache v0.2.0
@ -17,12 +17,12 @@ require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/PuerkitoBio/goquery v1.8.1 github.com/PuerkitoBio/goquery v1.8.1
github.com/alecthomas/chroma/v2 v2.10.0 github.com/alecthomas/chroma/v2 v2.12.0
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.3.10 github.com/blevesearch/bleve/v2 v2.3.10
github.com/bufbuild/connect-go v1.10.0 github.com/bufbuild/connect-go v1.10.0
github.com/buildkite/terminal-to-html/v3 v3.9.1 github.com/buildkite/terminal-to-html/v3 v3.10.0
github.com/caddyserver/certmagic v0.19.2 github.com/caddyserver/certmagic v0.20.0
github.com/chi-middleware/proxy v1.1.1 github.com/chi-middleware/proxy v1.1.1
github.com/denisenkom/go-mssqldb v0.12.3 github.com/denisenkom/go-mssqldb v0.12.3
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
@ -35,13 +35,13 @@ require (
github.com/emirpasic/gods v1.18.1 github.com/emirpasic/gods v1.18.1
github.com/ethantkoenig/rupture v1.0.1 github.com/ethantkoenig/rupture v1.0.1
github.com/felixge/fgprof v0.9.3 github.com/felixge/fgprof v0.9.3
github.com/fsnotify/fsnotify v1.6.0 github.com/fsnotify/fsnotify v1.7.0
github.com/gliderlabs/ssh v0.3.6-0.20230927171611-ece6c7995e46 github.com/gliderlabs/ssh v0.3.6-0.20230927171611-ece6c7995e46
github.com/go-ap/activitypub v0.0.0-20231003111253-1fba3772399b github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.31.1 github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.8.6 github.com/go-enry/go-enry/v2 v2.8.6
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e
github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-billy/v5 v5.5.0
@ -50,34 +50,34 @@ require (
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/go-swagger/go-swagger v0.30.5 github.com/go-swagger/go-swagger v0.30.5
github.com/go-testfixtures/testfixtures/v3 v3.9.0 github.com/go-testfixtures/testfixtures/v3 v3.9.0
github.com/go-webauthn/webauthn v0.8.6 github.com/go-webauthn/webauthn v0.9.4
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.0.0 github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/go-github/v53 v53.2.0 github.com/google/go-github/v57 v57.0.0
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 github.com/google/pprof v0.0.0-20231212022811-ec68065c825e
github.com/google/uuid v1.3.1 github.com/google/uuid v1.5.0
github.com/gorilla/feeds v1.1.1 github.com/gorilla/feeds v1.1.2
github.com/gorilla/sessions v1.2.1 github.com/gorilla/sessions v1.2.2
github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huandu/xstrings v1.4.0 github.com/huandu/xstrings v1.4.0
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
github.com/jhillyerd/enmime v1.0.1 github.com/jhillyerd/enmime v1.1.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4
github.com/klauspost/compress v1.17.0 github.com/klauspost/compress v1.17.4
github.com/klauspost/cpuid/v2 v2.2.5 github.com/klauspost/cpuid/v2 v2.2.6
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/markbates/goth v1.78.0 github.com/markbates/goth v1.78.0
github.com/mattn/go-isatty v0.0.19 github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-sqlite3 v1.14.17 github.com/mattn/go-sqlite3 v1.14.19
github.com/meilisearch/meilisearch-go v0.25.1 github.com/meilisearch/meilisearch-go v0.26.0
github.com/mholt/archiver/v3 v3.5.1 github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.26 github.com/microcosm-cc/bluemonday v1.0.26
github.com/minio/minio-go/v7 v7.0.63 github.com/minio/minio-go/v7 v7.0.66
github.com/minio/sha256-simd v1.0.1 github.com/minio/sha256-simd v1.0.1
github.com/msteinert/pam v1.2.0 github.com/msteinert/pam v1.2.0
github.com/nektos/act v0.2.52 github.com/nektos/act v0.2.52
@ -89,7 +89,7 @@ require (
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.17.0 github.com/prometheus/client_golang v1.17.0
github.com/quasoft/websspi v1.1.2 github.com/quasoft/websspi v1.1.2
github.com/redis/go-redis/v9 v9.2.1 github.com/redis/go-redis/v9 v9.3.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/sassoftware/go-rpmutils v0.2.0 github.com/sassoftware/go-rpmutils v0.2.0
@ -99,21 +99,21 @@ require (
github.com/syndtr/goleveldb v1.0.0 github.com/syndtr/goleveldb v1.0.0
github.com/tstranex/u2f v1.0.0 github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.11 github.com/ulikunitz/xz v0.5.11
github.com/urfave/cli/v2 v2.25.7 github.com/urfave/cli/v2 v2.26.0
github.com/xanzy/go-gitlab v0.93.1 github.com/xanzy/go-gitlab v0.95.2
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
github.com/yohcop/openid-go v1.0.1 github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.5.6 github.com/yuin/goldmark v1.6.0
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
github.com/yuin/goldmark-meta v1.1.0 github.com/yuin/goldmark-meta v1.1.0
golang.org/x/crypto v0.14.0 golang.org/x/crypto v0.17.0
golang.org/x/image v0.13.0 golang.org/x/image v0.14.0
golang.org/x/net v0.17.0 golang.org/x/net v0.19.0
golang.org/x/oauth2 v0.13.0 golang.org/x/oauth2 v0.15.0
golang.org/x/sys v0.13.0 golang.org/x/sys v0.15.0
golang.org/x/text v0.13.0 golang.org/x/text v0.14.0
golang.org/x/tools v0.14.0 golang.org/x/tools v0.16.1
google.golang.org/grpc v1.58.3 google.golang.org/grpc v1.60.0
google.golang.org/protobuf v1.31.0 google.golang.org/protobuf v1.31.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.0
@ -125,33 +125,33 @@ require (
) )
require ( require (
cloud.google.com/go/compute v1.23.1 // indirect cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
github.com/ClickHouse/ch-go v0.58.2 // indirect github.com/ClickHouse/ch-go v0.61.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.14.3 // indirect github.com/ClickHouse/clickhouse-go/v2 v2.16.0 // indirect
github.com/DataDog/zstd v1.5.5 // indirect github.com/DataDog/zstd v1.5.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
github.com/RoaringBitmap/roaring v1.6.0 // indirect github.com/RoaringBitmap/roaring v1.7.0 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.9.0 // indirect github.com/bits-and-blooms/bitset v1.12.0 // indirect
github.com/blevesearch/bleve_index_api v1.0.6 // indirect github.com/blevesearch/bleve_index_api v1.1.4 // indirect
github.com/blevesearch/geo v0.1.18 // indirect github.com/blevesearch/geo v0.1.18 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.1.6 // indirect github.com/blevesearch/scorch_segment_api/v2 v2.2.5 // indirect
github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
@ -165,9 +165,9 @@ require (
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect github.com/cloudflare/circl v1.3.6 // indirect
github.com/couchbase/go-couchbase v0.1.1 // indirect github.com/couchbase/go-couchbase v0.1.1 // indirect
github.com/couchbase/gomemcached v0.2.1 // indirect github.com/couchbase/gomemcached v0.3.0 // indirect
github.com/couchbase/goutils v0.1.2 // indirect github.com/couchbase/goutils v0.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect
@ -175,28 +175,28 @@ require (
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
github.com/fatih/color v1.15.0 // indirect github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/fxamacker/cbor/v2 v2.5.0 // indirect
github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-enry/go-oniguruma v1.2.1 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect
github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.6.1 // indirect github.com/go-faster/errors v0.7.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/analysis v0.21.5 // indirect
github.com/go-openapi/errors v0.20.4 // indirect github.com/go-openapi/errors v0.21.0 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect github.com/go-openapi/inflect v0.19.0 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonpointer v0.20.1 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.3 // indirect
github.com/go-openapi/loads v0.21.2 // indirect github.com/go-openapi/loads v0.21.3 // indirect
github.com/go-openapi/runtime v0.26.0 // indirect github.com/go-openapi/runtime v0.26.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/spec v0.20.12 // indirect
github.com/go-openapi/strfmt v0.21.7 // indirect github.com/go-openapi/strfmt v0.21.10 // indirect
github.com/go-openapi/swag v0.22.4 // indirect github.com/go-openapi/swag v0.22.5 // indirect
github.com/go-openapi/validate v0.22.1 // indirect github.com/go-openapi/validate v0.22.4 // indirect
github.com/go-webauthn/x v0.1.4 // indirect github.com/go-webauthn/x v0.1.5 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
@ -207,12 +207,12 @@ require (
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.0 // indirect github.com/google/go-tpm v0.9.0 // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.4 // indirect github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect github.com/imdario/mergo v0.3.16 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
@ -228,9 +228,9 @@ require (
github.com/markbates/going v1.0.3 // indirect github.com/markbates/going v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mholt/acmez v1.2.0 // indirect github.com/mholt/acmez v1.2.0 // indirect
github.com/miekg/dns v1.1.56 // indirect github.com/miekg/dns v1.1.57 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -244,19 +244,19 @@ require (
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect
github.com/paulmach/orb v0.10.0 // indirect github.com/paulmach/orb v0.10.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pierrec/lz4/v4 v4.1.19 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect
github.com/rhysd/actionlint v1.6.26 // indirect github.com/rhysd/actionlint v1.6.26 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/rs/xid v1.5.0 // indirect github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect
@ -264,37 +264,37 @@ require (
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect github.com/skeema/knownhosts v1.2.1 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.5.1 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.17.0 // indirect github.com/spf13/viper v1.18.2 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/toqueteos/webbrowser v1.2.0 // indirect
github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/com v1.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.50.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect github.com/valyala/fastjson v1.6.4 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect
go.etcd.io/bbolt v1.3.7 // indirect go.etcd.io/bbolt v1.3.8 // indirect
go.mongodb.org/mongo-driver v1.12.1 // indirect go.mongodb.org/mongo-driver v1.13.1 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect go.uber.org/zap v1.26.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect
golang.org/x/mod v0.13.0 // indirect golang.org/x/mod v0.14.0 // indirect
golang.org/x/sync v0.4.0 // indirect golang.org/x/sync v0.5.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect

488
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,11 @@ type ActionRunner struct {
Deleted timeutil.TimeStamp `xorm:"deleted"` Deleted timeutil.TimeStamp `xorm:"deleted"`
} }
const (
RunnerOfflineTime = time.Minute
RunnerIdleTime = 10 * time.Second
)
// BelongsToOwnerName before calling, should guarantee that all attributes are loaded // BelongsToOwnerName before calling, should guarantee that all attributes are loaded
func (r *ActionRunner) BelongsToOwnerName() string { func (r *ActionRunner) BelongsToOwnerName() string {
if r.RepoID != 0 { if r.RepoID != 0 {
@ -76,11 +81,12 @@ func (r *ActionRunner) BelongsToOwnerType() types.OwnerType {
return types.OwnerTypeSystemGlobal return types.OwnerTypeSystemGlobal
} }
// if the logic here changed, you should also modify FindRunnerOptions.ToCond
func (r *ActionRunner) Status() runnerv1.RunnerStatus { func (r *ActionRunner) Status() runnerv1.RunnerStatus {
if time.Since(r.LastOnline.AsTime()) > time.Minute { if time.Since(r.LastOnline.AsTime()) > RunnerOfflineTime {
return runnerv1.RunnerStatus_RUNNER_STATUS_OFFLINE return runnerv1.RunnerStatus_RUNNER_STATUS_OFFLINE
} }
if time.Since(r.LastActive.AsTime()) > 10*time.Second { if time.Since(r.LastActive.AsTime()) > RunnerIdleTime {
return runnerv1.RunnerStatus_RUNNER_STATUS_IDLE return runnerv1.RunnerStatus_RUNNER_STATUS_IDLE
} }
return runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE return runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE
@ -153,6 +159,7 @@ type FindRunnerOptions struct {
OwnerID int64 OwnerID int64
Sort string Sort string
Filter string Filter string
IsOnline util.OptionalBool
WithAvailable bool // not only runners belong to, but also runners can be used WithAvailable bool // not only runners belong to, but also runners can be used
} }
@ -178,6 +185,12 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
if opts.Filter != "" { if opts.Filter != "" {
cond = cond.And(builder.Like{"name", opts.Filter}) cond = cond.And(builder.Like{"name", opts.Filter})
} }
if opts.IsOnline.IsTrue() {
cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
} else if opts.IsOnline.IsFalse() {
cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
}
return cond return cond
} }

View File

@ -234,7 +234,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
} }
var jobs []*ActionRunJob var jobs []*ActionRunJob
if err := e.Where("task_id=? AND status=?", 0, StatusWaiting).And(jobCond).Asc("id").Find(&jobs); err != nil { if err := e.Where("task_id=? AND status=?", 0, StatusWaiting).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil {
return nil, false, err return nil, false, err
} }

View File

@ -131,24 +131,22 @@ func AddDeployKey(ctx context.Context, repoID int64, name, content string, readO
} }
defer committer.Close() defer committer.Close()
pkey := &PublicKey{ pkey, exist, err := db.Get[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
Fingerprint: fingerprint,
}
has, err := db.GetByBean(ctx, pkey)
if err != nil { if err != nil {
return nil, err return nil, err
} } else if exist {
if has {
if pkey.Type != KeyTypeDeploy { if pkey.Type != KeyTypeDeploy {
return nil, ErrKeyAlreadyExist{0, fingerprint, ""} return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
} }
} else { } else {
// First time use this deploy key. // First time use this deploy key.
pkey.Mode = accessMode pkey = &PublicKey{
pkey.Type = KeyTypeDeploy Fingerprint: fingerprint,
pkey.Content = content Mode: accessMode,
pkey.Name = name Type: KeyTypeDeploy,
Content: content,
Name: name,
}
if err = addKey(ctx, pkey); err != nil { if err = addKey(ctx, pkey); err != nil {
return nil, fmt.Errorf("addKey: %w", err) return nil, fmt.Errorf("addKey: %w", err)
} }
@ -164,11 +162,10 @@ func AddDeployKey(ctx context.Context, repoID int64, name, content string, readO
// GetDeployKeyByID returns deploy key by given ID. // GetDeployKeyByID returns deploy key by given ID.
func GetDeployKeyByID(ctx context.Context, id int64) (*DeployKey, error) { func GetDeployKeyByID(ctx context.Context, id int64) (*DeployKey, error) {
key := new(DeployKey) key, exist, err := db.GetByID[DeployKey](ctx, id)
has, err := db.GetEngine(ctx).ID(id).Get(key)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !exist {
return nil, ErrDeployKeyNotExist{id, 0, 0} return nil, ErrDeployKeyNotExist{id, 0, 0}
} }
return key, nil return key, nil
@ -176,14 +173,10 @@ func GetDeployKeyByID(ctx context.Context, id int64) (*DeployKey, error) {
// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID. // GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
func GetDeployKeyByRepo(ctx context.Context, keyID, repoID int64) (*DeployKey, error) { func GetDeployKeyByRepo(ctx context.Context, keyID, repoID int64) (*DeployKey, error) {
key := &DeployKey{ key, exist, err := db.Get[DeployKey](ctx, builder.Eq{"key_id": keyID, "repo_id": repoID})
KeyID: keyID,
RepoID: repoID,
}
has, err := db.GetByBean(ctx, key)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !exist {
return nil, ErrDeployKeyNotExist{0, keyID, repoID} return nil, ErrDeployKeyNotExist{0, keyID, repoID}
} }
return key, nil return key, nil

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"xorm.io/builder"
) )
// ___________.__ .__ __ // ___________.__ .__ __
@ -31,9 +32,7 @@ import (
// checkKeyFingerprint only checks if key fingerprint has been used as public key, // checkKeyFingerprint only checks if key fingerprint has been used as public key,
// it is OK to use same key as deploy key for multiple repositories/users. // it is OK to use same key as deploy key for multiple repositories/users.
func checkKeyFingerprint(ctx context.Context, fingerprint string) error { func checkKeyFingerprint(ctx context.Context, fingerprint string) error {
has, err := db.GetByBean(ctx, &PublicKey{ has, err := db.Exist[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
Fingerprint: fingerprint,
})
if err != nil { if err != nil {
return err return err
} else if has { } else if has {

View File

@ -30,10 +30,15 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat
return "", ErrKeyNotExist{} return "", ErrKeyNotExist{}
} }
if err := sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea"); err != nil { err = sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea")
log.Error("Unable to validate token signature. Error: %v", err) if err != nil {
return "", ErrSSHInvalidTokenSignature{ // edge case for Windows based shells that will add CR LF if piped to ssh-keygen command
Fingerprint: key.Fingerprint, // see https://github.com/PowerShell/PowerShell/issues/5974
if sshsig.Verify(bytes.NewBuffer([]byte(token+"\r\n")), []byte(signature), []byte(key.Content), "gitea") != nil {
log.Error("Unable to validate token signature. Error: %v", err)
return "", ErrSSHInvalidTokenSignature{
Fingerprint: key.Fingerprint,
}
} }
} }

View File

@ -9,6 +9,8 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
) )
// Session represents a session compatible for go-chi session // Session represents a session compatible for go-chi session
@ -33,34 +35,28 @@ func UpdateSession(ctx context.Context, key string, data []byte) error {
// ReadSession reads the data for the provided session // ReadSession reads the data for the provided session
func ReadSession(ctx context.Context, key string) (*Session, error) { func ReadSession(ctx context.Context, key string) (*Session, error) {
session := Session{
Key: key,
}
ctx, committer, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer committer.Close() defer committer.Close()
if has, err := db.GetByBean(ctx, &session); err != nil { session, exist, err := db.Get[Session](ctx, builder.Eq{"key": key})
if err != nil {
return nil, err return nil, err
} else if !has { } else if !exist {
session.Expiry = timeutil.TimeStampNow() session.Expiry = timeutil.TimeStampNow()
if err := db.Insert(ctx, &session); err != nil { if err := db.Insert(ctx, &session); err != nil {
return nil, err return nil, err
} }
} }
return &session, committer.Commit() return session, committer.Commit()
} }
// ExistSession checks if a session exists // ExistSession checks if a session exists
func ExistSession(ctx context.Context, key string) (bool, error) { func ExistSession(ctx context.Context, key string) (bool, error) {
session := Session{ return db.Exist[Session](ctx, builder.Eq{"key": key})
Key: key,
}
return db.GetEngine(ctx).Get(&session)
} }
// DestroySession destroys a session // DestroySession destroys a session
@ -79,17 +75,13 @@ func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, er
} }
defer committer.Close() defer committer.Close()
if has, err := db.GetByBean(ctx, &Session{ if has, err := db.Exist[Session](ctx, builder.Eq{"key": newKey}); err != nil {
Key: newKey,
}); err != nil {
return nil, err return nil, err
} else if has { } else if has {
return nil, fmt.Errorf("session Key: %s already exists", newKey) return nil, fmt.Errorf("session Key: %s already exists", newKey)
} }
if has, err := db.GetByBean(ctx, &Session{ if has, err := db.Exist[Session](ctx, builder.Eq{"key": oldKey}); err != nil {
Key: oldKey,
}); err != nil {
return nil, err return nil, err
} else if !has { } else if !has {
if err := db.Insert(ctx, &Session{ if err := db.Insert(ctx, &Session{
@ -104,14 +96,13 @@ func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, er
return nil, err return nil, err
} }
s := Session{ s, _, err := db.Get[Session](ctx, builder.Eq{"key": newKey})
Key: newKey, if err != nil {
} // is not exist, it should be impossible
if _, err := db.GetByBean(ctx, &s); err != nil {
return nil, err return nil, err
} }
return &s, committer.Commit() return s, committer.Commit()
} }
// CountSessions returns the number of sessions // CountSessions returns the number of sessions

View File

@ -265,10 +265,10 @@ func IsSSPIEnabled(ctx context.Context) bool {
return false return false
} }
exist, err := db.Exists[Source](ctx, FindSourcesOptions{ exist, err := db.Exist[Source](ctx, FindSourcesOptions{
IsActive: util.OptionalBoolTrue, IsActive: util.OptionalBoolTrue,
LoginType: SSPI, LoginType: SSPI,
}) }.ToConds())
if err != nil { if err != nil {
log.Error("Active SSPI Sources: %v", err) log.Error("Active SSPI Sources: %v", err)
return false return false

View File

@ -173,9 +173,44 @@ func Exec(ctx context.Context, sqlAndArgs ...any) (sql.Result, error) {
return GetEngine(ctx).Exec(sqlAndArgs...) return GetEngine(ctx).Exec(sqlAndArgs...)
} }
// GetByBean filled empty fields of the bean according non-empty fields to query in database. func Get[T any](ctx context.Context, cond builder.Cond) (object *T, exist bool, err error) {
func GetByBean(ctx context.Context, bean any) (bool, error) { if !cond.IsValid() {
return GetEngine(ctx).Get(bean) return nil, false, ErrConditionRequired{}
}
var bean T
has, err := GetEngine(ctx).Where(cond).NoAutoCondition().Get(&bean)
if err != nil {
return nil, false, err
} else if !has {
return nil, false, nil
}
return &bean, true, nil
}
func GetByID[T any](ctx context.Context, id int64) (object *T, exist bool, err error) {
var bean T
has, err := GetEngine(ctx).ID(id).NoAutoCondition().Get(&bean)
if err != nil {
return nil, false, err
} else if !has {
return nil, false, nil
}
return &bean, true, nil
}
func Exist[T any](ctx context.Context, cond builder.Cond) (bool, error) {
if !cond.IsValid() {
return false, ErrConditionRequired{}
}
var bean T
return GetEngine(ctx).Where(cond).NoAutoCondition().Exist(&bean)
}
func ExistByID[T any](ctx context.Context, id int64) (bool, error) {
var bean T
return GetEngine(ctx).ID(id).NoAutoCondition().Exist(&bean)
} }
// DeleteByBean deletes all records according non-empty fields of the bean as conditions. // DeleteByBean deletes all records according non-empty fields of the bean as conditions.
@ -264,8 +299,3 @@ func inTransaction(ctx context.Context) (*xorm.Session, bool) {
return nil, false return nil, false
} }
} }
func Exists[T any](ctx context.Context, opts FindOptions) (bool, error) {
var bean T
return GetEngine(ctx).Where(opts.ToConds()).Exist(&bean)
}

View File

@ -72,3 +72,21 @@ func (err ErrNotExist) Error() string {
func (err ErrNotExist) Unwrap() error { func (err ErrNotExist) Unwrap() error {
return util.ErrNotExist return util.ErrNotExist
} }
// ErrConditionRequired represents an error which require condition.
type ErrConditionRequired struct{}
// IsErrConditionRequired checks if an error is an ErrConditionRequired
func IsErrConditionRequired(err error) bool {
_, ok := err.(ErrConditionRequired)
return ok
}
func (err ErrConditionRequired) Error() string {
return "condition is required"
}
// Unwrap unwraps this as a ErrNotExist err
func (err ErrConditionRequired) Unwrap() error {
return util.ErrInvalidArgument
}

View File

@ -31,11 +31,11 @@ func TestIterate(t *testing.T) {
assert.EqualValues(t, cnt, repoUnitCnt) assert.EqualValues(t, cnt, repoUnitCnt)
err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error { err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error {
reopUnit2 := repo_model.RepoUnit{ID: repoUnit.ID} has, err := db.ExistByID[repo_model.RepoUnit](ctx, repoUnit.ID)
has, err := db.GetByBean(ctx, &reopUnit2)
if err != nil { if err != nil {
return err return err
} else if !has { }
if !has {
return db.ErrNotExist{Resource: "repo_unit", ID: repoUnit.ID} return db.ErrNotExist{Resource: "repo_unit", ID: repoUnit.ID}
} }
assert.EqualValues(t, repoUnit.RepoID, repoUnit.RepoID) assert.EqualValues(t, repoUnit.RepoID, repoUnit.RepoID)

View File

@ -205,10 +205,9 @@ func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64
}) })
} }
// UpdateBranch updates the branch information in the database. If the branch exist, it will update latest commit of this branch information // UpdateBranch updates the branch information in the database.
// If it doest not exist, insert a new record into database func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) (int64, error) {
func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error { return db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName).
cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName).
Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix"). Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix").
Update(&Branch{ Update(&Branch{
CommitID: commit.ID.String(), CommitID: commit.ID.String(),
@ -217,21 +216,6 @@ func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
IsDeleted: false, IsDeleted: false,
}) })
if err != nil {
return err
}
if cnt > 0 {
return nil
}
return db.Insert(ctx, &Branch{
RepoID: repoID,
Name: branchName,
CommitID: commit.ID.String(),
CommitMessage: commit.Summary(),
PusherID: pusherID,
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
})
} }
// AddDeletedBranch adds a deleted branch to the database // AddDeletedBranch adds a deleted branch to the database
@ -308,6 +292,17 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
sess := db.GetEngine(ctx) sess := db.GetEngine(ctx)
var branch Branch
exist, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, from).Get(&branch)
if err != nil {
return err
} else if !exist || branch.IsDeleted {
return ErrBranchNotExist{
RepoID: repo.ID,
BranchName: from,
}
}
// 1. update branch in database // 1. update branch in database
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{ if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
Name: to, Name: to,

View File

@ -12,7 +12,6 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"xorm.io/builder" "xorm.io/builder"
"xorm.io/xorm"
) )
type BranchList []*Branch type BranchList []*Branch
@ -73,7 +72,7 @@ type FindBranchOptions struct {
Keyword string Keyword string
} }
func (opts *FindBranchOptions) Cond() builder.Cond { func (opts FindBranchOptions) ToConds() builder.Cond {
cond := builder.NewCond() cond := builder.NewCond()
if opts.RepoID > 0 { if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
@ -91,41 +90,30 @@ func (opts *FindBranchOptions) Cond() builder.Cond {
return cond return cond
} }
func CountBranches(ctx context.Context, opts FindBranchOptions) (int64, error) { func (opts FindBranchOptions) ToOrders() string {
return db.GetEngine(ctx).Where(opts.Cond()).Count(&Branch{}) orderBy := opts.OrderBy
}
func orderByBranches(sess *xorm.Session, opts FindBranchOptions) *xorm.Session {
if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end
sess = sess.OrderBy("is_deleted ASC") if orderBy != "" {
orderBy += ", "
}
orderBy += "is_deleted ASC"
} }
if orderBy == "" {
if opts.OrderBy == "" {
// the commit_time might be the same, so add the "name" to make sure the order is stable // the commit_time might be the same, so add the "name" to make sure the order is stable
opts.OrderBy = "commit_time DESC, name ASC" return "commit_time DESC, name ASC"
} }
return sess.OrderBy(opts.OrderBy)
}
func FindBranches(ctx context.Context, opts FindBranchOptions) (BranchList, error) { return orderBy
sess := db.GetEngine(ctx).Where(opts.Cond())
if opts.PageSize > 0 && !opts.IsListAll() {
sess = db.SetSessionPagination(sess, &opts.ListOptions)
}
sess = orderByBranches(sess, opts)
var branches []*Branch
return branches, sess.Find(&branches)
} }
func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, error) { func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, error) {
sess := db.GetEngine(ctx).Select("name").Where(opts.Cond()) sess := db.GetEngine(ctx).Select("name").Where(opts.ToConds())
if opts.PageSize > 0 && !opts.IsListAll() { if opts.PageSize > 0 && !opts.IsListAll() {
sess = db.SetSessionPagination(sess, &opts.ListOptions) sess = db.SetSessionPagination(sess, &opts.ListOptions)
} }
sess = orderByBranches(sess, opts)
var branches []string var branches []string
if err := sess.Table("branch").Find(&branches); err != nil { if err := sess.Table("branch").OrderBy(opts.ToOrders()).Find(&branches); err != nil {
return nil, err return nil, err
} }
return branches, nil return branches, nil

View File

@ -20,6 +20,7 @@ import (
func TestAddDeletedBranch(t *testing.T) { func TestAddDeletedBranch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.EqualValues(t, git.Sha1ObjectFormat.Name(), repo.ObjectFormatName)
firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1})
assert.True(t, firstBranch.IsDeleted) assert.True(t, firstBranch.IsDeleted)
@ -37,7 +38,7 @@ func TestAddDeletedBranch(t *testing.T) {
}, },
} }
err := git_model.UpdateBranch(db.DefaultContext, repo.ID, secondBranch.PusherID, secondBranch.Name, commit) _, err := git_model.UpdateBranch(db.DefaultContext, repo.ID, secondBranch.PusherID, secondBranch.Name, commit)
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -45,10 +46,8 @@ func TestGetDeletedBranches(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
branches, err := git_model.FindBranches(db.DefaultContext, git_model.FindBranchOptions{ branches, err := db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{
ListOptions: db.ListOptions{ ListOptions: db.ListOptionsAll,
ListAll: true,
},
RepoID: repo.ID, RepoID: repo.ID,
IsDeletedBranch: util.OptionalBoolTrue, IsDeletedBranch: util.OptionalBoolTrue,
}) })

View File

@ -114,7 +114,8 @@ WHEN NOT MATCHED
// GetNextCommitStatusIndex retried 3 times to generate a resource index // GetNextCommitStatusIndex retried 3 times to generate a resource index
func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
if !git.IsValidSHAPattern(sha) { _, err := git.NewIDFromString(sha)
if err != nil {
return 0, git.ErrInvalidSHA{SHA: sha} return 0, git.ErrInvalidSHA{SHA: sha}
} }
@ -323,7 +324,9 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA
Select("max( id ) as id, repo_id"). Select("max( id ) as id, repo_id").
GroupBy("context_hash, repo_id").OrderBy("max( id ) desc") GroupBy("context_hash, repo_id").OrderBy("max( id ) desc")
sess = db.SetSessionPagination(sess, &listOptions) if !listOptions.IsListAll() {
sess = db.SetSessionPagination(sess, &listOptions)
}
err := sess.Find(&results) err := sess.Find(&results)
if err != nil { if err != nil {
@ -423,7 +426,7 @@ func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, befor
type NewCommitStatusOptions struct { type NewCommitStatusOptions struct {
Repo *repo_model.Repository Repo *repo_model.Repository
Creator *user_model.User Creator *user_model.User
SHA string SHA git.ObjectID
CommitStatus *CommitStatus CommitStatus *CommitStatus
} }
@ -438,10 +441,6 @@ func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error {
return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA) return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA)
} }
if _, err := git.NewIDFromString(opts.SHA); err != nil {
return fmt.Errorf("NewCommitStatus[%s, %s]: invalid sha: %w", repoPath, opts.SHA, err)
}
ctx, committer, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", opts.Repo.ID, opts.Creator.ID, opts.SHA, err) return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", opts.Repo.ID, opts.Creator.ID, opts.SHA, err)
@ -449,7 +448,7 @@ func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error {
defer committer.Close() defer committer.Close()
// Get the next Status Index // Get the next Status Index
idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA) idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA.String())
if err != nil { if err != nil {
return fmt.Errorf("generate commit status index failed: %w", err) return fmt.Errorf("generate commit status index failed: %w", err)
} }
@ -457,7 +456,7 @@ func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error {
opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description) opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description)
opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context) opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL) opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL)
opts.CommitStatus.SHA = opts.SHA opts.CommitStatus.SHA = opts.SHA.String()
opts.CommitStatus.CreatorID = opts.Creator.ID opts.CommitStatus.CreatorID = opts.Creator.ID
opts.CommitStatus.RepoID = opts.Repo.ID opts.CommitStatus.RepoID = opts.Repo.ID
opts.CommitStatus.Index = idx opts.CommitStatus.Index = idx

View File

@ -135,7 +135,7 @@ var ErrLFSObjectNotExist = db.ErrNotExist{Resource: "LFS Meta object"}
// NewLFSMetaObject stores a given populated LFSMetaObject structure in the database // NewLFSMetaObject stores a given populated LFSMetaObject structure in the database
// if it is not already present. // if it is not already present.
func NewLFSMetaObject(ctx context.Context, m *LFSMetaObject) (*LFSMetaObject, error) { func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMetaObject, error) {
var err error var err error
ctx, committer, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
@ -144,16 +144,15 @@ func NewLFSMetaObject(ctx context.Context, m *LFSMetaObject) (*LFSMetaObject, er
} }
defer committer.Close() defer committer.Close()
has, err := db.GetByBean(ctx, m) m, exist, err := db.Get[LFSMetaObject](ctx, builder.Eq{"repository_id": repoID, "oid": p.Oid})
if err != nil { if err != nil {
return nil, err return nil, err
} } else if exist {
if has {
m.Existing = true m.Existing = true
return m, committer.Commit() return m, committer.Commit()
} }
m = &LFSMetaObject{Pointer: p, RepositoryID: repoID}
if err = db.Insert(ctx, m); err != nil { if err = db.Insert(ctx, m); err != nil {
return nil, err return nil, err
} }

View File

@ -24,6 +24,7 @@ import (
"github.com/gobwas/glob" "github.com/gobwas/glob"
"github.com/gobwas/glob/syntax" "github.com/gobwas/glob/syntax"
"xorm.io/builder"
) )
var ErrBranchIsProtected = errors.New("branch is protected") var ErrBranchIsProtected = errors.New("branch is protected")
@ -274,12 +275,11 @@ func (protectBranch *ProtectedBranch) IsUnprotectedFile(patterns []glob.Glob, pa
// GetProtectedBranchRuleByName getting protected branch rule by name // GetProtectedBranchRuleByName getting protected branch rule by name
func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) { func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) {
rel := &ProtectedBranch{RepoID: repoID, RuleName: ruleName} // branch_name is legacy name, it actually is rule name
has, err := db.GetByBean(ctx, rel) rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "branch_name": ruleName})
if err != nil { if err != nil {
return nil, err return nil, err
} } else if !exist {
if !has {
return nil, nil return nil, nil
} }
return rel, nil return rel, nil
@ -287,12 +287,10 @@ func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName st
// GetProtectedBranchRuleByID getting protected branch rule by rule ID // GetProtectedBranchRuleByID getting protected branch rule by rule ID
func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*ProtectedBranch, error) { func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*ProtectedBranch, error) {
rel := &ProtectedBranch{ID: ruleID, RepoID: repoID} rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "id": ruleID})
has, err := db.GetByBean(ctx, rel)
if err != nil { if err != nil {
return nil, err return nil, err
} } else if !exist {
if !has {
return nil, nil return nil, nil
} }
return rel, nil return rel, nil

View File

@ -10,6 +10,8 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"xorm.io/builder"
) )
// IssueAssignees saves all issue assignees // IssueAssignees saves all issue assignees
@ -59,7 +61,7 @@ func GetAssigneeIDsByIssue(ctx context.Context, issueID int64) ([]int64, error)
// IsUserAssignedToIssue returns true when the user is assigned to the issue // IsUserAssignedToIssue returns true when the user is assigned to the issue
func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.User) (isAssigned bool, err error) { func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.User) (isAssigned bool, err error) {
return db.GetByBean(ctx, &IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID}) return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
} }
// ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it. // ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.

View File

@ -23,6 +23,7 @@ import (
type IssuesOptions struct { //nolint type IssuesOptions struct { //nolint
db.Paginator db.Paginator
RepoIDs []int64 // overwrites RepoCond if the length is not 0 RepoIDs []int64 // overwrites RepoCond if the length is not 0
AllPublic bool // include also all public repositories
RepoCond builder.Cond RepoCond builder.Cond
AssigneeID int64 AssigneeID int64
PosterID int64 PosterID int64
@ -197,6 +198,12 @@ func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session
} else if len(opts.RepoIDs) > 1 { } else if len(opts.RepoIDs) > 1 {
opts.RepoCond = builder.In("issue.repo_id", opts.RepoIDs) opts.RepoCond = builder.In("issue.repo_id", opts.RepoIDs)
} }
if opts.AllPublic {
if opts.RepoCond == nil {
opts.RepoCond = builder.NewCond()
}
opts.RepoCond = opts.RepoCond.Or(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"is_private": false})))
}
if opts.RepoCond != nil { if opts.RepoCond != nil {
sess.And(opts.RepoCond) sess.And(opts.RepoCond)
} }

View File

@ -14,8 +14,8 @@ import (
// IssueUser represents an issue-user relation. // IssueUser represents an issue-user relation.
type IssueUser struct { type IssueUser struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"INDEX"` // User ID. UID int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
IssueID int64 `xorm:"INDEX"` IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
IsRead bool IsRead bool
IsMentioned bool IsMentioned bool
} }

View File

@ -304,15 +304,11 @@ func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (
return nil, ErrRepoLabelNotExist{0, repoID} return nil, ErrRepoLabelNotExist{0, repoID}
} }
l := &Label{ l, exist, err := db.Get[Label](ctx, builder.Eq{"name": labelName, "repo_id": repoID})
Name: labelName,
RepoID: repoID,
}
has, err := db.GetByBean(ctx, l)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !exist {
return nil, ErrRepoLabelNotExist{0, l.RepoID} return nil, ErrRepoLabelNotExist{0, repoID}
} }
return l, nil return l, nil
} }
@ -323,15 +319,11 @@ func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, err
return nil, ErrRepoLabelNotExist{labelID, repoID} return nil, ErrRepoLabelNotExist{labelID, repoID}
} }
l := &Label{ l, exist, err := db.Get[Label](ctx, builder.Eq{"id": labelID, "repo_id": repoID})
ID: labelID,
RepoID: repoID,
}
has, err := db.GetByBean(ctx, l)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !exist {
return nil, ErrRepoLabelNotExist{l.ID, l.RepoID} return nil, ErrRepoLabelNotExist{labelID, repoID}
} }
return l, nil return l, nil
} }
@ -408,15 +400,11 @@ func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*L
return nil, ErrOrgLabelNotExist{0, orgID} return nil, ErrOrgLabelNotExist{0, orgID}
} }
l := &Label{ l, exist, err := db.Get[Label](ctx, builder.Eq{"name": labelName, "org_id": orgID})
Name: labelName,
OrgID: orgID,
}
has, err := db.GetByBean(ctx, l)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !exist {
return nil, ErrOrgLabelNotExist{0, l.OrgID} return nil, ErrOrgLabelNotExist{0, orgID}
} }
return l, nil return l, nil
} }
@ -427,15 +415,11 @@ func GetLabelInOrgByID(ctx context.Context, orgID, labelID int64) (*Label, error
return nil, ErrOrgLabelNotExist{labelID, orgID} return nil, ErrOrgLabelNotExist{labelID, orgID}
} }
l := &Label{ l, exist, err := db.Get[Label](ctx, builder.Eq{"id": labelID, "org_id": orgID})
ID: labelID,
OrgID: orgID,
}
has, err := db.GetByBean(ctx, l)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !exist {
return nil, ErrOrgLabelNotExist{l.ID, l.OrgID} return nil, ErrOrgLabelNotExist{labelID, orgID}
} }
return l, nil return l, nil
} }

View File

@ -295,16 +295,15 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
return err return err
} }
numMilestones, err := CountMilestones(ctx, GetMilestonesOption{ numMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
RepoID: repo.ID, RepoID: repo.ID,
State: api.StateAll,
}) })
if err != nil { if err != nil {
return err return err
} }
numClosedMilestones, err := CountMilestones(ctx, GetMilestonesOption{ numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
RepoID: repo.ID, RepoID: repo.ID,
State: api.StateClosed, IsClosed: util.OptionalBoolTrue,
}) })
if err != nil { if err != nil {
return err return err

View File

@ -8,8 +8,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util"
api "code.gitea.io/gitea/modules/structs"
"xorm.io/builder" "xorm.io/builder"
) )
@ -25,31 +24,31 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
return ids return ids
} }
// GetMilestonesOption contain options to get milestones // FindMilestoneOptions contain options to get milestones
type GetMilestonesOption struct { type FindMilestoneOptions struct {
db.ListOptions db.ListOptions
RepoID int64 RepoID int64
State api.StateType IsClosed util.OptionalBool
Name string Name string
SortType string SortType string
RepoCond builder.Cond
RepoIDs []int64
} }
func (opts GetMilestonesOption) toCond() builder.Cond { func (opts FindMilestoneOptions) ToConds() builder.Cond {
cond := builder.NewCond() cond := builder.NewCond()
if opts.RepoID != 0 { if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
} }
if opts.IsClosed != util.OptionalBoolNone {
switch opts.State { cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()})
case api.StateClosed: }
cond = cond.And(builder.Eq{"is_closed": true}) if opts.RepoCond != nil && opts.RepoCond.IsValid() {
case api.StateAll: cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))
break }
// api.StateOpen: if len(opts.RepoIDs) > 0 {
default: cond = cond.And(builder.In("repo_id", opts.RepoIDs))
cond = cond.And(builder.Eq{"is_closed": false})
} }
if len(opts.Name) != 0 { if len(opts.Name) != 0 {
cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name)) cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
} }
@ -57,34 +56,23 @@ func (opts GetMilestonesOption) toCond() builder.Cond {
return cond return cond
} }
// GetMilestones returns milestones filtered by GetMilestonesOption's func (opts FindMilestoneOptions) ToOrders() string {
func GetMilestones(ctx context.Context, opts GetMilestonesOption) (MilestoneList, int64, error) {
sess := db.GetEngine(ctx).Where(opts.toCond())
if opts.Page != 0 {
sess = db.SetSessionPagination(sess, &opts)
}
switch opts.SortType { switch opts.SortType {
case "furthestduedate": case "furthestduedate":
sess.Desc("deadline_unix") return "deadline_unix DESC"
case "leastcomplete": case "leastcomplete":
sess.Asc("completeness") return "completeness ASC"
case "mostcomplete": case "mostcomplete":
sess.Desc("completeness") return "completeness DESC"
case "leastissues": case "leastissues":
sess.Asc("num_issues") return "num_issues ASC"
case "mostissues": case "mostissues":
sess.Desc("num_issues") return "num_issues DESC"
case "id": case "id":
sess.Asc("id") return "id ASC"
default: default:
sess.Asc("deadline_unix").Asc("id") return "deadline_unix ASC, id ASC"
} }
miles := make([]*Milestone, 0, opts.PageSize)
total, err := sess.FindAndCount(&miles)
return miles, total, err
} }
// GetMilestoneIDsByNames returns a list of milestone ids by given names. // GetMilestoneIDsByNames returns a list of milestone ids by given names.
@ -99,49 +87,6 @@ func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error
Find(&ids) Find(&ids)
} }
// SearchMilestones search milestones
func SearchMilestones(ctx context.Context, repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) {
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
if len(keyword) > 0 {
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
}
if repoCond.IsValid() {
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
}
if page > 0 {
sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
}
switch sortType {
case "furthestduedate":
sess.Desc("deadline_unix")
case "leastcomplete":
sess.Asc("completeness")
case "mostcomplete":
sess.Desc("completeness")
case "leastissues":
sess.Asc("num_issues")
case "mostissues":
sess.Desc("num_issues")
default:
sess.Asc("deadline_unix")
}
return miles, sess.Find(&miles)
}
// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
func GetMilestonesByRepoIDs(ctx context.Context, repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
return SearchMilestones(
ctx,
builder.In("repo_id", repoIDs),
page,
isClosed,
sortType,
"",
)
}
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error { func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error {
type totalTimesByMilestone struct { type totalTimesByMilestone struct {
@ -183,47 +128,9 @@ func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error
return nil return nil
} }
// CountMilestones returns number of milestones in given repository with other options
func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) {
return db.GetEngine(ctx).
Where(opts.toCond()).
Count(new(Milestone))
}
// CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
func CountMilestonesByRepoCond(ctx context.Context, repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
if repoCond.IsValid() {
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
}
countsSlice := make([]*struct {
RepoID int64
Count int64
}, 0, 10)
if err := sess.GroupBy("repo_id").
Select("repo_id AS repo_id, COUNT(*) AS count").
Table("milestone").
Find(&countsSlice); err != nil {
return nil, err
}
countMap := make(map[int64]int64, len(countsSlice))
for _, c := range countsSlice {
countMap[c.RepoID] = c.Count
}
return countMap, nil
}
// CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options` // CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
func CountMilestonesByRepoCondAndKw(ctx context.Context, repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) { func CountMilestonesMap(ctx context.Context, opts FindMilestoneOptions) (map[int64]int64, error) {
sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed) sess := db.GetEngine(ctx).Where(opts.ToConds())
if len(keyword) > 0 {
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
}
if repoCond.IsValid() {
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
}
countsSlice := make([]*struct { countsSlice := make([]*struct {
RepoID int64 RepoID int64

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"xorm.io/builder" "xorm.io/builder"
@ -39,10 +40,15 @@ func TestGetMilestoneByRepoID(t *testing.T) {
func TestGetMilestonesByRepoID(t *testing.T) { func TestGetMilestonesByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
test := func(repoID int64, state api.StateType) { test := func(repoID int64, state api.StateType) {
var isClosed util.OptionalBool
switch state {
case api.StateClosed, api.StateOpen:
isClosed = util.OptionalBoolOf(state == api.StateClosed)
}
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: repo.ID, RepoID: repo.ID,
State: state, IsClosed: isClosed,
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -77,9 +83,9 @@ func TestGetMilestonesByRepoID(t *testing.T) {
test(3, api.StateClosed) test(3, api.StateClosed)
test(3, api.StateAll) test(3, api.StateAll)
milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: unittest.NonexistentID, RepoID: unittest.NonexistentID,
State: api.StateOpen, IsClosed: util.OptionalBoolFalse,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, milestones, 0) assert.Len(t, milestones, 0)
@ -90,13 +96,13 @@ func TestGetMilestones(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
test := func(sortType string, sortCond func(*issues_model.Milestone) int) { test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
for _, page := range []int{0, 1} { for _, page := range []int{0, 1} {
milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{
Page: page, Page: page,
PageSize: setting.UI.IssuePagingNum, PageSize: setting.UI.IssuePagingNum,
}, },
RepoID: repo.ID, RepoID: repo.ID,
State: api.StateOpen, IsClosed: util.OptionalBoolFalse,
SortType: sortType, SortType: sortType,
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -107,13 +113,13 @@ func TestGetMilestones(t *testing.T) {
} }
assert.True(t, sort.IntsAreSorted(values)) assert.True(t, sort.IntsAreSorted(values))
milestones, _, err = issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ milestones, err = db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{
Page: page, Page: page,
PageSize: setting.UI.IssuePagingNum, PageSize: setting.UI.IssuePagingNum,
}, },
RepoID: repo.ID, RepoID: repo.ID,
State: api.StateClosed, IsClosed: util.OptionalBoolTrue,
Name: "", Name: "",
SortType: sortType, SortType: sortType,
}) })
@ -150,9 +156,8 @@ func TestCountRepoMilestones(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
test := func(repoID int64) { test := func(repoID int64) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: repoID, RepoID: repoID,
State: api.StateAll,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, repo.NumMilestones, count) assert.EqualValues(t, repo.NumMilestones, count)
@ -161,9 +166,8 @@ func TestCountRepoMilestones(t *testing.T) {
test(2) test(2)
test(3) test(3)
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: unittest.NonexistentID, RepoID: unittest.NonexistentID,
State: api.StateAll,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 0, count) assert.EqualValues(t, 0, count)
@ -173,9 +177,9 @@ func TestCountRepoClosedMilestones(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
test := func(repoID int64) { test := func(repoID int64) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: repoID, RepoID: repoID,
State: api.StateClosed, IsClosed: util.OptionalBoolTrue,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, repo.NumClosedMilestones, count) assert.EqualValues(t, repo.NumClosedMilestones, count)
@ -184,9 +188,9 @@ func TestCountRepoClosedMilestones(t *testing.T) {
test(2) test(2)
test(3) test(3)
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{ count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: unittest.NonexistentID, RepoID: unittest.NonexistentID,
State: api.StateClosed, IsClosed: util.OptionalBoolTrue,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 0, count) assert.EqualValues(t, 0, count)
@ -201,12 +205,19 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
repo1OpenCount, repo1ClosedCount := milestonesCount(1) repo1OpenCount, repo1ClosedCount := milestonesCount(1)
repo2OpenCount, repo2ClosedCount := milestonesCount(2) repo2OpenCount, repo2ClosedCount := milestonesCount(2)
openCounts, err := issues_model.CountMilestonesByRepoCond(db.DefaultContext, builder.In("repo_id", []int64{1, 2}), false) openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{
RepoIDs: []int64{1, 2},
IsClosed: util.OptionalBoolFalse,
})
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, repo1OpenCount, openCounts[1]) assert.EqualValues(t, repo1OpenCount, openCounts[1])
assert.EqualValues(t, repo2OpenCount, openCounts[2]) assert.EqualValues(t, repo2OpenCount, openCounts[2])
closedCounts, err := issues_model.CountMilestonesByRepoCond(db.DefaultContext, builder.In("repo_id", []int64{1, 2}), true) closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext,
issues_model.FindMilestoneOptions{
RepoIDs: []int64{1, 2},
IsClosed: util.OptionalBoolTrue,
})
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, repo1ClosedCount, closedCounts[1]) assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
assert.EqualValues(t, repo2ClosedCount, closedCounts[2]) assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
@ -218,7 +229,15 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
test := func(sortType string, sortCond func(*issues_model.Milestone) int) { test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
for _, page := range []int{0, 1} { for _, page := range []int{0, 1} {
openMilestones, err := issues_model.GetMilestonesByRepoIDs(db.DefaultContext, []int64{repo1.ID, repo2.ID}, page, false, sortType) openMilestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: setting.UI.IssuePagingNum,
},
RepoIDs: []int64{repo1.ID, repo2.ID},
IsClosed: util.OptionalBoolFalse,
SortType: sortType,
})
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones) assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
values := make([]int, len(openMilestones)) values := make([]int, len(openMilestones))
@ -227,7 +246,16 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
} }
assert.True(t, sort.IntsAreSorted(values)) assert.True(t, sort.IntsAreSorted(values))
closedMilestones, err := issues_model.GetMilestonesByRepoIDs(db.DefaultContext, []int64{repo1.ID, repo2.ID}, page, true, sortType) closedMilestones, err := db.Find[issues_model.Milestone](db.DefaultContext,
issues_model.FindMilestoneOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: setting.UI.IssuePagingNum,
},
RepoIDs: []int64{repo1.ID, repo2.ID},
IsClosed: util.OptionalBoolTrue,
SortType: sortType,
})
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones) assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
values = make([]int, len(closedMilestones)) values = make([]int, len(closedMilestones))

View File

@ -660,13 +660,10 @@ func GetPullRequestByIssueIDWithNoAttributes(ctx context.Context, issueID int64)
// GetPullRequestByIssueID returns pull request by given issue ID. // GetPullRequestByIssueID returns pull request by given issue ID.
func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, error) { func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, error) {
pr := &PullRequest{ pr, exist, err := db.Get[PullRequest](ctx, builder.Eq{"issue_id": issueID})
IssueID: issueID,
}
has, err := db.GetByBean(ctx, pr)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !exist {
return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""} return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
} }
return pr, pr.LoadAttributes(ctx) return pr, pr.LoadAttributes(ctx)

View File

@ -0,0 +1,20 @@
-
id: 1
uid: 1
issue_id: 1
is_read: true
is_mentioned: false
-
id: 2
uid: 2
issue_id: 1
is_read: true
is_mentioned: false
-
id: 3
uid: 2
issue_id: 1 # duplicated with id 2
is_read: false
is_mentioned: true

View File

@ -550,6 +550,8 @@ var migrations = []Migration{
NewMigration("Add auth_token table", v1_22.CreateAuthTokenTable), NewMigration("Add auth_token table", v1_22.CreateAuthTokenTable),
// v282 -> v283 // v282 -> v283
NewMigration("Add Index to pull_auto_merge.doer_id", v1_22.AddIndexToPullAutoMergeDoerID), NewMigration("Add Index to pull_auto_merge.doer_id", v1_22.AddIndexToPullAutoMergeDoerID),
// v283 -> v284
NewMigration("Add combined Index to issue_user.uid and issue_id", v1_22.AddCombinedIndexToIssueUser),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version

View File

@ -0,0 +1,14 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
)
func TestMain(m *testing.M) {
base.MainTest(m)
}

View File

@ -0,0 +1,34 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"xorm.io/xorm"
)
func AddCombinedIndexToIssueUser(x *xorm.Engine) error {
type OldIssueUser struct {
IssueID int64
UID int64
Cnt int64
}
var duplicatedIssueUsers []OldIssueUser
if err := x.SQL("select * from (select issue_id, uid, count(1) as cnt from issue_user group by issue_id, uid) a where a.cnt > 1").
Find(&duplicatedIssueUsers); err != nil {
return err
}
for _, issueUser := range duplicatedIssueUsers {
if _, err := x.Exec("delete from issue_user where id in (SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?)", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1); err != nil {
return err
}
}
type IssueUser struct {
UID int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
}
return x.Sync(&IssueUser{})
}

View File

@ -0,0 +1,28 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
)
func Test_AddCombinedIndexToIssueUser(t *testing.T) {
type IssueUser struct {
UID int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
}
// Prepare and load the testing database
x, deferable := base.PrepareTestEnv(t, 0, new(IssueUser))
defer deferable()
if x == nil || t.Failed() {
return
}
if err := AddCombinedIndexToIssueUser(x); err != nil {
t.Fatal(err)
}
}

View File

@ -162,7 +162,7 @@ func NewTeam(ctx context.Context, t *organization.Team) (err error) {
return err return err
} }
has, err := db.GetEngine(ctx).ID(t.OrgID).Get(new(user_model.User)) has, err := db.ExistByID[user_model.User](ctx, t.OrgID)
if err != nil { if err != nil {
return err return err
} }
@ -171,10 +171,10 @@ func NewTeam(ctx context.Context, t *organization.Team) (err error) {
} }
t.LowerName = strings.ToLower(t.Name) t.LowerName = strings.ToLower(t.Name)
has, err = db.GetEngine(ctx). has, err = db.Exist[organization.Team](ctx, builder.Eq{
Where("org_id=?", t.OrgID). "org_id": t.OrgID,
And("lower_name=?", t.LowerName). "lower_name": t.LowerName,
Get(new(organization.Team)) })
if err != nil { if err != nil {
return err return err
} }
@ -232,20 +232,20 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA
return err return err
} }
defer committer.Close() defer committer.Close()
sess := db.GetEngine(ctx)
t.LowerName = strings.ToLower(t.Name) t.LowerName = strings.ToLower(t.Name)
has, err := sess. has, err := db.Exist[organization.Team](ctx, builder.Eq{
Where("org_id=?", t.OrgID). "org_id": t.OrgID,
And("lower_name=?", t.LowerName). "lower_name": t.LowerName,
And("id!=?", t.ID). }.And(builder.Neq{"id": t.ID}),
Get(new(organization.Team)) )
if err != nil { if err != nil {
return err return err
} else if has { } else if has {
return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName} return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
} }
sess := db.GetEngine(ctx)
if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description", if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description",
"can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil { "can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil {
return fmt.Errorf("update: %w", err) return fmt.Errorf("update: %w", err)

View File

@ -16,6 +16,8 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"xorm.io/builder"
) )
// ___________ // ___________
@ -203,14 +205,10 @@ func IsUsableTeamName(name string) error {
// GetTeam returns team by given team name and organization. // GetTeam returns team by given team name and organization.
func GetTeam(ctx context.Context, orgID int64, name string) (*Team, error) { func GetTeam(ctx context.Context, orgID int64, name string) (*Team, error) {
t := &Team{ t, exist, err := db.Get[Team](ctx, builder.Eq{"org_id": orgID, "lower_name": strings.ToLower(name)})
OrgID: orgID,
LowerName: strings.ToLower(name),
}
has, err := db.GetByBean(ctx, t)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !exist {
return nil, ErrTeamNotExist{orgID, 0, name} return nil, ErrTeamNotExist{orgID, 0, name}
} }
return t, nil return t, nil

View File

@ -13,6 +13,8 @@ import (
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"xorm.io/builder"
) )
// Access represents the highest access level of a user to the repository. The only access type // Access represents the highest access level of a user to the repository. The only access type
@ -51,9 +53,11 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re
return perm.AccessModeOwner, nil return perm.AccessModeOwner, nil
} }
a := &Access{UserID: userID, RepoID: repo.ID} a, exist, err := db.Get[Access](ctx, builder.Eq{"user_id": userID, "repo_id": repo.ID})
if has, err := db.GetByBean(ctx, a); !has || err != nil { if err != nil {
return mode, err return mode, err
} else if !exist {
return mode, nil
} }
return a.Mode, nil return a.Mode, nil
} }

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -179,6 +180,7 @@ type Repository struct {
IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"` IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"`
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"` CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
Topics []string `xorm:"TEXT JSON"` Topics []string `xorm:"TEXT JSON"`
ObjectFormatName string `xorm:"-"`
TrustModel TrustModelType TrustModel TrustModelType
@ -274,6 +276,10 @@ func (repo *Repository) AfterLoad() {
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
// this is a temporary behaviour to support old repos, next step is to store the object format in the database
// and read from database so this line could be removed. To not depend on git module, we use a constant variable here
repo.ObjectFormatName = "sha1"
} }
// LoadAttributes loads attributes of the repository. // LoadAttributes loads attributes of the repository.
@ -313,7 +319,7 @@ func (repo *Repository) HTMLURL() string {
// CommitLink make link to by commit full ID // CommitLink make link to by commit full ID
// note: won't check whether it's an right id // note: won't check whether it's an right id
func (repo *Repository) CommitLink(commitID string) (result string) { func (repo *Repository) CommitLink(commitID string) (result string) {
if commitID == "" || commitID == "0000000000000000000000000000000000000000" { if git.IsEmptyCommitID(commitID) {
result = "" result = ""
} else { } else {
result = repo.Link() + "/commit/" + url.PathEscape(commitID) result = repo.Link() + "/commit/" + url.PathEscape(commitID)
@ -602,25 +608,23 @@ func ComposeHTTPSCloneURL(owner, repo string) string {
func ComposeSSHCloneURL(ownerName, repoName string) string { func ComposeSSHCloneURL(ownerName, repoName string) string {
sshUser := setting.SSH.User sshUser := setting.SSH.User
// if we have a ipv6 literal we need to put brackets around it
// for the git cloning to work.
sshDomain := setting.SSH.Domain sshDomain := setting.SSH.Domain
ip := net.ParseIP(setting.SSH.Domain)
if ip != nil && ip.To4() == nil { // non-standard port, it must use full URI
sshDomain = "[" + setting.SSH.Domain + "]" if setting.SSH.Port != 22 {
sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port))
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
} }
if setting.SSH.Port != 22 { // for standard port, it can use a shorter URI (without the port)
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost := sshDomain
net.JoinHostPort(setting.SSH.Domain, strconv.Itoa(setting.SSH.Port)), if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil {
url.PathEscape(ownerName), sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets
url.PathEscape(repoName))
} }
if setting.Repository.UseCompatSSHURI { if setting.Repository.UseCompatSSHURI {
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshDomain, url.PathEscape(ownerName), url.PathEscape(repoName)) return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
} }
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshDomain, url.PathEscape(ownerName), url.PathEscape(repoName)) return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
} }
func (repo *Repository) cloneLink(isWiki bool) *CloneLink { func (repo *Repository) cloneLink(isWiki bool) *CloneLink {

View File

@ -12,6 +12,8 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -186,3 +188,32 @@ func TestGetRepositoryByURL(t *testing.T) {
test(t, "try.gitea.io:user2/repo2.git") test(t, "try.gitea.io:user2/repo2.git")
}) })
} }
func TestComposeSSHCloneURL(t *testing.T) {
defer test.MockVariableValue(&setting.SSH, setting.SSH)()
defer test.MockVariableValue(&setting.Repository, setting.Repository)()
setting.SSH.User = "git"
// test SSH_DOMAIN
setting.SSH.Domain = "domain"
setting.SSH.Port = 22
setting.Repository.UseCompatSSHURI = false
assert.Equal(t, "git@domain:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
setting.Repository.UseCompatSSHURI = true
assert.Equal(t, "ssh://git@domain/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
// test SSH_DOMAIN while use non-standard SSH port
setting.SSH.Port = 123
setting.Repository.UseCompatSSHURI = false
assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
setting.Repository.UseCompatSSHURI = true
assert.Equal(t, "ssh://git@domain:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
// test IPv6 SSH_DOMAIN
setting.Repository.UseCompatSSHURI = false
setting.SSH.Domain = "::1"
setting.SSH.Port = 22
assert.Equal(t, "git@[::1]:user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
setting.SSH.Port = 123
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", repo_model.ComposeSSHCloneURL("user", "repo"))
}

View File

@ -377,3 +377,13 @@ func syncTopicsInRepository(sess db.Engine, repoID int64) error {
} }
return nil return nil
} }
// CountOrphanedAttachments returns the number of topics that don't belong to any repository.
func CountOrphanedTopics(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where("repo_count = 0").Count(new(Topic))
}
// DeleteOrphanedAttachments delete all topics that don't belong to any repository.
func DeleteOrphanedTopics(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where("repo_count = 0").Delete(new(Topic))
}

View File

@ -13,6 +13,8 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
) )
type Setting struct { type Setting struct {
@ -36,16 +38,17 @@ func init() {
const keyRevision = "revision" const keyRevision = "revision"
func GetRevision(ctx context.Context) int { func GetRevision(ctx context.Context) int {
revision := &Setting{SettingKey: keyRevision} revision, exist, err := db.Get[Setting](ctx, builder.Eq{"setting_key": keyRevision})
if has, err := db.GetByBean(ctx, revision); err != nil { if err != nil {
return 0 return 0
} else if !has { } else if !exist {
err = db.Insert(ctx, &Setting{SettingKey: keyRevision, Version: 1}) err = db.Insert(ctx, &Setting{SettingKey: keyRevision, Version: 1})
if err != nil { if err != nil {
return 0 return 0
} }
return 1 return 1
} else if revision.Version <= 0 || revision.Version >= math.MaxInt-1 { }
if revision.Version <= 0 || revision.Version >= math.MaxInt-1 {
_, err = db.Exec(ctx, "UPDATE system_setting SET version=1 WHERE setting_key=?", keyRevision) _, err = db.Exec(ctx, "UPDATE system_setting SET version=1 WHERE setting_key=?", keyRevision)
if err != nil { if err != nil {
return 0 return 0

View File

@ -41,14 +41,11 @@ func TestSettings(t *testing.T) {
assert.EqualValues(t, "false", settings[keyName]) assert.EqualValues(t, "false", settings[keyName])
// setting the same value should not trigger DuplicateKey error, and the "version" should be increased // setting the same value should not trigger DuplicateKey error, and the "version" should be increased
setting := &system.Setting{SettingKey: keyName}
_, err = db.GetByBean(db.DefaultContext, setting)
assert.NoError(t, err)
assert.EqualValues(t, 2, setting.Version)
err = system.SetSettings(db.DefaultContext, map[string]string{keyName: "false"}) err = system.SetSettings(db.DefaultContext, map[string]string{keyName: "false"})
assert.NoError(t, err) assert.NoError(t, err)
setting = &system.Setting{SettingKey: keyName}
_, err = db.GetByBean(db.DefaultContext, setting) rev, settings, err = system.GetAllSettings(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 3, setting.Version) assert.Len(t, settings, 2)
assert.EqualValues(t, 4, rev)
} }

View File

@ -527,12 +527,13 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
// Activate/deactivate a user's secondary email address // Activate/deactivate a user's secondary email address
// First check if there's another user active with the same address // First check if there's another user active with the same address
addr := EmailAddress{UID: userID, LowerEmail: strings.ToLower(email)} addr, exist, err := db.Get[EmailAddress](ctx, builder.Eq{"uid": userID, "lower_email": strings.ToLower(email)})
if has, err := db.GetByBean(ctx, &addr); err != nil { if err != nil {
return err return err
} else if !has { } else if !exist {
return fmt.Errorf("no such email: %d (%s)", userID, email) return fmt.Errorf("no such email: %d (%s)", userID, email)
} }
if addr.IsActivated == activate { if addr.IsActivated == activate {
// Already in the desired state; no action // Already in the desired state; no action
return nil return nil
@ -544,25 +545,26 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
return ErrEmailAlreadyUsed{Email: email} return ErrEmailAlreadyUsed{Email: email}
} }
} }
if err = updateActivation(ctx, &addr, activate); err != nil { if err = updateActivation(ctx, addr, activate); err != nil {
return fmt.Errorf("unable to updateActivation() for %d:%s: %w", addr.ID, addr.Email, err) return fmt.Errorf("unable to updateActivation() for %d:%s: %w", addr.ID, addr.Email, err)
} }
// Activate/deactivate a user's primary email address and account // Activate/deactivate a user's primary email address and account
if addr.IsPrimary { if addr.IsPrimary {
user := User{ID: userID, Email: email} user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email})
if has, err := db.GetByBean(ctx, &user); err != nil { if err != nil {
return err return err
} else if !has { } else if !exist {
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email) return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
} }
// The user's activation state should be synchronized with the primary email // The user's activation state should be synchronized with the primary email
if user.IsActive != activate { if user.IsActive != activate {
user.IsActive = activate user.IsActive = activate
if user.Rands, err = GetUserSalt(); err != nil { if user.Rands, err = GetUserSalt(); err != nil {
return fmt.Errorf("unable to generate salt: %w", err) return fmt.Errorf("unable to generate salt: %w", err)
} }
if err = UpdateUserCols(ctx, &user, "is_active", "rands"); err != nil { if err = UpdateUserCols(ctx, user, "is_active", "rands"); err != nil {
return fmt.Errorf("unable to updateUserCols() for user ID: %d: %w", userID, err) return fmt.Errorf("unable to updateUserCols() for user ID: %d: %w", userID, err)
} }
} }

View File

@ -98,9 +98,10 @@ func GetExternalLogin(ctx context.Context, externalLoginUser *ExternalLoginUser)
// LinkExternalToUser link the external user to the user // LinkExternalToUser link the external user to the user
func LinkExternalToUser(ctx context.Context, user *User, externalLoginUser *ExternalLoginUser) error { func LinkExternalToUser(ctx context.Context, user *User, externalLoginUser *ExternalLoginUser) error {
has, err := db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", externalLoginUser.ExternalID, externalLoginUser.LoginSourceID). has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
NoAutoCondition(). "external_id": externalLoginUser.ExternalID,
Exist(externalLoginUser) "login_source_id": externalLoginUser.LoginSourceID,
})
if err != nil { if err != nil {
return err return err
} else if has { } else if has {
@ -145,9 +146,10 @@ func GetUserIDByExternalUserID(ctx context.Context, provider, userID string) (in
// UpdateExternalUserByExternalID updates an external user's information // UpdateExternalUserByExternalID updates an external user's information
func UpdateExternalUserByExternalID(ctx context.Context, external *ExternalLoginUser) error { func UpdateExternalUserByExternalID(ctx context.Context, external *ExternalLoginUser) error {
has, err := db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID). has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
NoAutoCondition(). "external_id": external.ExternalID,
Exist(external) "login_source_id": external.LoginSourceID,
})
if err != nil { if err != nil {
return err return err
} else if !has { } else if !has {

View File

@ -16,6 +16,7 @@ import (
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
gouuid "github.com/google/uuid" gouuid "github.com/google/uuid"
"xorm.io/builder"
) )
// ___ ___ __ ___________ __ // ___ ___ __ ___________ __
@ -150,14 +151,10 @@ func UpdateHookTask(ctx context.Context, t *HookTask) error {
// ReplayHookTask copies a hook task to get re-delivered // ReplayHookTask copies a hook task to get re-delivered
func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) { func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) {
task := &HookTask{ task, exist, err := db.Get[HookTask](ctx, builder.Eq{"hook_id": hookID, "uuid": uuid})
HookID: hookID,
UUID: uuid,
}
has, err := db.GetByBean(ctx, task)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !exist {
return nil, ErrHookTaskNotExist{ return nil, ErrHookTaskNotExist{
HookID: hookID, HookID: hookID,
UUID: uuid, UUID: uuid,

View File

@ -24,11 +24,11 @@ func newCache(cacheConfig setting.Cache) (mc.Cache, error) {
}) })
} }
// NewContext start cache service // Init start cache service
func NewContext() error { func Init() error {
var err error var err error
if conn == nil && setting.CacheService.Enabled { if conn == nil {
if conn, err = newCache(setting.CacheService.Cache); err != nil { if conn, err = newCache(setting.CacheService.Cache); err != nil {
return err return err
} }

View File

@ -22,9 +22,9 @@ func createTestCache() {
} }
func TestNewContext(t *testing.T) { 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{ con, err := newCache(setting.Cache{
Adapter: "rand", Adapter: "rand",
Conn: "false conf", Conn: "false conf",

View File

@ -8,11 +8,12 @@
package charset package charset
import ( import (
"bufio" "html/template"
"io" "io"
"strings" "strings"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
) )
@ -20,20 +21,18 @@ import (
const RuneNBSP = 0xa0 const RuneNBSP = 0xa0
// EscapeControlHTML escapes the unicode control sequences in a provided html document // 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{} sb := &strings.Builder{}
outputStream := &HTMLStreamerWriter{Writer: sb} escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, allowed...) // err has been handled in EscapeControlReader
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer) return escaped, template.HTML(sb.String())
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()
} }
// 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) { 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} outputStream := &HTMLStreamerWriter{Writer: writer}
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer) streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
@ -43,41 +42,3 @@ func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.
} }
return streamer.escaped, err 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()
}

View File

@ -64,7 +64,7 @@ func (e *escapeStreamer) Text(data string) error {
until, next = nextIdxs[0]+pos, nextIdxs[1]+pos 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) runes := make([]rune, 0, next-until)
positions := make([]int, 0, next-until+1) positions := make([]int, 0, next-until+1)

View File

@ -4,11 +4,14 @@
package charset package charset
import ( import (
"reflect"
"strings" "strings"
"testing" "testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"github.com/stretchr/testify/assert"
) )
type escapeControlTest struct { type escapeControlTest struct {
@ -132,22 +135,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) { 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) tests := make([]escapeControlTest, 0, len(escapeControlTests)*3)
copy(tests, escapeControlTests) copy(tests, escapeControlTests)
@ -169,29 +158,20 @@ func TestEscapeControlReader(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
input := strings.NewReader(tt.text)
output := &strings.Builder{} output := &strings.Builder{}
status, err := EscapeControlReader(input, output, &translation.MockLocale{}) status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{})
result := output.String() assert.NoError(t, err)
if err != nil { assert.Equal(t, tt.status, *status)
t.Errorf("EscapeControlReader(): err = %v", err) assert.Equal(t, tt.result, output.String())
}
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)
}
}) })
} }
} }
func TestEscapeControlReader_panic(t *testing.T) { func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
bs := make([]byte, 0, 20479) defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
bs = append(bs, 'A') _, out := EscapeControlHTML("a test", &translation.MockLocale{})
for i := 0; i < 6826; i++ { assert.EqualValues(t, `a<span class="escaped-code-point" data-escaped="[U+00A0]"><span class="char"> </span></span>test`, out)
bs = append(bs, []byte("—")...) setting.UI.AmbiguousUnicodeDetection = false
} _, out = EscapeControlHTML("a test", &translation.MockLocale{})
_, _ = EscapeControlString(string(bs), &translation.MockLocale{}) assert.EqualValues(t, `a test`, out)
} }

View File

@ -308,6 +308,12 @@ func RepoRefForAPI(next http.Handler) http.Handler {
return return
} }
objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
return
}
if ref := ctx.FormTrim("ref"); len(ref) > 0 { if ref := ctx.FormTrim("ref"); len(ref) > 0 {
commit, err := ctx.Repo.GitRepo.GetCommit(ref) commit, err := ctx.Repo.GitRepo.GetCommit(ref)
if err != nil { if err != nil {
@ -325,7 +331,6 @@ func RepoRefForAPI(next http.Handler) http.Handler {
return return
} }
var err error
refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny) refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
if ctx.Repo.GitRepo.IsBranchExist(refName) { if ctx.Repo.GitRepo.IsBranchExist(refName) {
@ -342,7 +347,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
return return
} }
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
} else if len(refName) == git.SHAFullLength { } else if len(refName) == objectFormat.FullLength() {
ctx.Repo.CommitID = refName ctx.Repo.CommitID = refName
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName) ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
if err != nil { if err != nil {

View File

@ -668,11 +668,9 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
branchOpts := git_model.FindBranchOptions{ branchOpts := git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
IsDeletedBranch: util.OptionalBoolFalse, IsDeletedBranch: util.OptionalBoolFalse,
ListOptions: db.ListOptions{ ListOptions: db.ListOptionsAll,
ListAll: true,
},
} }
branchesTotal, err := git_model.CountBranches(ctx, branchOpts) branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts)
if err != nil { if err != nil {
ctx.ServerError("CountBranches", err) ctx.ServerError("CountBranches", err)
return cancel return cancel
@ -827,7 +825,9 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
} }
// For legacy and API support only full commit sha // For legacy and API support only full commit sha
parts := strings.Split(path, "/") parts := strings.Split(path, "/")
if len(parts) > 0 && len(parts[0]) == git.SHAFullLength { objectFormat, _ := repo.GitRepo.GetObjectFormat()
if len(parts) > 0 && len(parts[0]) == objectFormat.FullLength() {
repo.TreePath = strings.Join(parts[1:], "/") repo.TreePath = strings.Join(parts[1:], "/")
return parts[0] return parts[0]
} }
@ -871,7 +871,9 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist) return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist)
case RepoRefCommit: case RepoRefCommit:
parts := strings.Split(path, "/") parts := strings.Split(path, "/")
if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= git.SHAFullLength { objectFormat, _ := repo.GitRepo.GetObjectFormat()
if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= objectFormat.FullLength() {
repo.TreePath = strings.Join(parts[1:], "/") repo.TreePath = strings.Join(parts[1:], "/")
return parts[0] return parts[0]
} }
@ -931,6 +933,12 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
} }
} }
objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
if err != nil {
log.Error("Cannot determine objectFormat for repository: %w", err)
ctx.Repo.Repository.MarkAsBrokenEmpty()
}
// Get default branch. // Get default branch.
if len(ctx.Params("*")) == 0 { if len(ctx.Params("*")) == 0 {
refName = ctx.Repo.Repository.DefaultBranch refName = ctx.Repo.Repository.DefaultBranch
@ -997,7 +1005,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
return cancel return cancel
} }
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
} else if len(refName) >= 7 && len(refName) <= git.SHAFullLength { } else if len(refName) >= 7 && len(refName) <= objectFormat.FullLength() {
ctx.Repo.IsViewCommit = true ctx.Repo.IsViewCommit = true
ctx.Repo.CommitID = refName ctx.Repo.CommitID = refName
@ -1007,7 +1015,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
return cancel return cancel
} }
// If short commit ID add canonical link header // If short commit ID add canonical link header
if len(refName) < git.SHAFullLength { if len(refName) < objectFormat.FullLength() {
ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"", ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"",
util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)))) util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))))
} }

View File

@ -158,6 +158,12 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
Fixer: actions_model.FixRunnersWithoutBelongingOwner, Fixer: actions_model.FixRunnersWithoutBelongingOwner,
FixedMessage: "Removed", FixedMessage: "Removed",
}, },
{
Name: "Topics with empty repository count",
Counter: repo_model.CountOrphanedTopics,
Fixer: repo_model.DeleteOrphanedTopics,
FixedMessage: "Removed",
},
} }
// TODO: function to recalc all counters // TODO: function to recalc all counters

View File

@ -79,6 +79,7 @@ var Checks []*Check
// RunChecks runs the doctor checks for the provided list // RunChecks runs the doctor checks for the provided list
func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) error { func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) error {
SortChecks(checks)
// the checks output logs by a special logger, they do not use the default logger // the checks output logs by a special logger, they do not use the default logger
logger := log.BaseLoggerToGeneralLogger(&doctorCheckLogger{colorize: colorize}) logger := log.BaseLoggerToGeneralLogger(&doctorCheckLogger{colorize: colorize})
loggerStep := log.BaseLoggerToGeneralLogger(&doctorCheckStepLogger{colorize: colorize}) loggerStep := log.BaseLoggerToGeneralLogger(&doctorCheckStepLogger{colorize: colorize})
@ -104,20 +105,23 @@ func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) err
logger.Info("OK") logger.Info("OK")
} }
} }
logger.Info("\nAll done.") logger.Info("\nAll done (checks: %d).", len(checks))
return nil return nil
} }
// Register registers a command with the list // Register registers a command with the list
func Register(command *Check) { func Register(command *Check) {
Checks = append(Checks, command) 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 func SortChecks(checks []*Check) {
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 { if checks[i].Priority == 0 {
return false return false
} }
return Checks[i].Priority < Checks[j].Priority return checks[i].Priority < checks[j].Priority
}) })
} }

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/storage"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
"xorm.io/builder" "xorm.io/builder"
@ -31,6 +32,10 @@ func countOrphanedRepos(ctx context.Context) (int64, error) {
// deleteOrphanedRepos delete repository where user of owner_id do not exist // deleteOrphanedRepos delete repository where user of owner_id do not exist
func deleteOrphanedRepos(ctx context.Context) (int64, error) { func deleteOrphanedRepos(ctx context.Context) (int64, error) {
if err := storage.Init(); err != nil {
return 0, err
}
batchSize := db.MaxBatchInsertSize("repository") batchSize := db.MaxBatchInsertSize("repository")
e := db.GetEngine(ctx) e := db.GetEngine(ctx)
var deleted int64 var deleted int64

View File

@ -148,7 +148,7 @@ func CatFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi
// ReadBatchLine reads the header line from cat-file --batch // ReadBatchLine reads the header line from cat-file --batch
// We expect: // We expect:
// <sha> SP <type> SP <size> LF // <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) { func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
typ, err = rd.ReadString('\n') typ, err = rd.ReadString('\n')
if err != nil { if err != nil {
@ -251,20 +251,19 @@ headerLoop:
} }
// git tree files are a list: // 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 // 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" const hextable = "0123456789abcdef"
// To40ByteSHA converts a 20-byte SHA into a 40-byte sha. Input and output can be the // BinToHexHeash converts a binary Hash into a hex encoded one. Input and output can be the
// same 40 byte slice to support in place conversion without allocations. // same byte slice to support in place conversion without allocations.
// This is at least 100x quicker that hex.EncodeToString // This is at least 100x quicker that hex.EncodeToString
// NB This requires that out is a 40-byte slice func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
func To40ByteSHA(sha, out []byte) []byte { for i := objectFormat.FullLength()/2 - 1; i >= 0; i-- {
for i := 19; i >= 0; i-- {
v := sha[i] v := sha[i]
vhi, vlo := v>>4, v&0x0f vhi, vlo := v>>4, v&0x0f
shi, slo := hextable[vhi], hextable[vlo] shi, slo := hextable[vhi], hextable[vlo]
@ -278,10 +277,10 @@ func To40ByteSHA(sha, out []byte) []byte {
// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations // It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
// //
// Each line is composed of: // 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 // We don't attempt to convert the raw HASH to save a lot of time
func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
var readBytes []byte var readBytes []byte
// Read the Mode & fname // Read the Mode & fname
@ -324,11 +323,12 @@ func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fn
fnameBuf = fnameBuf[:len(fnameBuf)-1] fnameBuf = fnameBuf[:len(fnameBuf)-1]
fname = fnameBuf fname = fnameBuf
// Deal with the 20-byte SHA // Deal with the binary hash
idx = 0 idx = 0
for idx < 20 { len := objectFormat.FullLength() / 2
for idx < len {
var read int var read int
read, err = rd.Read(shaBuf[idx:20]) read, err = rd.Read(shaBuf[idx:len])
n += read n += read
if err != nil { if err != nil {
return mode, fname, sha, n, err return mode, fname, sha, n, err

View File

@ -10,8 +10,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"regexp"
"strings"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -33,14 +31,13 @@ type BlameReader struct {
done chan error done chan error
lastSha *string lastSha *string
ignoreRevsFile *string ignoreRevsFile *string
objectFormat ObjectFormat
} }
func (r *BlameReader) UsesIgnoreRevs() bool { func (r *BlameReader) UsesIgnoreRevs() bool {
return r.ignoreRevsFile != nil 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) // NextPart returns next part of blame (sequential code lines with the same commit)
func (r *BlameReader) NextPart() (*BlamePart, error) { func (r *BlameReader) NextPart() (*BlamePart, error) {
var blamePart *BlamePart var blamePart *BlamePart
@ -52,6 +49,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
} }
} }
const previousHeader = "previous "
var lineBytes []byte var lineBytes []byte
var isPrefix bool var isPrefix bool
var err error var err error
@ -67,21 +65,22 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
continue continue
} }
line := string(lineBytes) var objectID string
objectFormatLength := r.objectFormat.FullLength()
lines := shaLineRegex.FindStringSubmatch(line)
if lines != nil {
sha1 := lines[1]
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 { if blamePart == nil {
blamePart = &BlamePart{ blamePart = &BlamePart{
Sha: sha1, Sha: objectID,
Lines: make([]string, 0), Lines: make([]string, 0),
} }
} }
if blamePart.Sha != sha1 { if blamePart.Sha != objectID {
r.lastSha = &sha1 r.lastSha = &objectID
// need to munch to end of line... // need to munch to end of line...
for isPrefix { for isPrefix {
_, isPrefix, err = r.bufferedReader.ReadLine() _, isPrefix, err = r.bufferedReader.ReadLine()
@ -91,12 +90,13 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
} }
return blamePart, nil return blamePart, nil
} }
} else if line[0] == '\t' { } else if lineBytes[0] == '\t' {
blamePart.Lines = append(blamePart.Lines, line[1:]) blamePart.Lines = append(blamePart.Lines, string(lineBytes[1:]))
} else if strings.HasPrefix(line, "previous ") { } else if bytes.HasPrefix(lineBytes, []byte(previousHeader)) {
parts := strings.SplitN(line[len("previous "):], " ", 2) offset := len(previousHeader) // already includes a space
blamePart.PreviousSha = parts[0] blamePart.PreviousSha = string(lineBytes[offset : offset+objectFormatLength])
blamePart.PreviousPath = parts[1] offset += objectFormatLength + 1 // +1 for space
blamePart.PreviousPath = string(lineBytes[offset:])
} }
// need to munch to end of line... // need to munch to end of line...
@ -126,7 +126,7 @@ func (r *BlameReader) Close() error {
} }
// CreateBlameReader creates reader for given repository, commit and file // 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 var ignoreRevsFile *string
if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore { if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore {
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit) ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
@ -175,6 +175,7 @@ func CreateBlameReader(ctx context.Context, repoPath string, commit *Commit, fil
bufferedReader: bufferedReader, bufferedReader: bufferedReader,
done: done, done: done,
ignoreRevsFile: ignoreRevsFile, ignoreRevsFile: ignoreRevsFile,
objectFormat: objectFormat,
}, nil }, nil
} }

View File

@ -39,7 +39,7 @@ func TestReadingBlameOutput(t *testing.T) {
} }
for _, bypass := range []bool{false, true} { 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.NoError(t, err)
assert.NotNil(t, blameReader) assert.NotNil(t, blameReader)
defer blameReader.Close() defer blameReader.Close()
@ -122,7 +122,7 @@ func TestReadingBlameOutput(t *testing.T) {
commit, err := repo.GetCommit(c.CommitID) commit, err := repo.GetCommit(c.CommitID)
assert.NoError(t, err) assert.NoError(t, err)
blameReader, err := CreateBlameReader(ctx, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass) blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, blameReader) assert.NotNil(t, blameReader)
defer blameReader.Close() defer blameReader.Close()

View File

@ -14,7 +14,7 @@ import (
// Blob represents a Git object. // Blob represents a Git object.
type Blob struct { type Blob struct {
ID SHA1 ID ObjectID
gogitEncodedObj plumbing.EncodedObject gogitEncodedObj plumbing.EncodedObject
name string name string

View File

@ -16,7 +16,7 @@ import (
// Blob represents a Git object. // Blob represents a Git object.
type Blob struct { type Blob struct {
ID SHA1 ID ObjectID
gotSize bool gotSize bool
size int64 size int64

View File

@ -14,7 +14,6 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"time" "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/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" "code.gitea.io/gitea/modules/log"
@ -389,15 +388,11 @@ func (r *runStdError) IsExitCode(code int) bool {
return false 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). // 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) { func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) {
stdoutBytes, stderrBytes, err := c.RunStdBytes(opts) stdoutBytes, stderrBytes, err := c.RunStdBytes(opts)
stdout = bytesToString(stdoutBytes) stdout = util.UnsafeBytesToString(stdoutBytes)
stderr = bytesToString(stderrBytes) stderr = util.UnsafeBytesToString(stderrBytes)
if err != nil { if err != nil {
return stdout, stderr, &runStdError{err: err, stderr: stderr} return stdout, stderr, &runStdError{err: err, stderr: stderr}
} }
@ -432,7 +427,7 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS
err := c.Run(newOpts) err := c.Run(newOpts)
stderr = stderrBuf.Bytes() stderr = stderrBuf.Bytes()
if err != nil { 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 // even if there is no err, there could still be some stderr output
return stdoutBuf.Bytes(), stderr, nil return stdoutBuf.Bytes(), stderr, nil

View File

@ -21,13 +21,13 @@ import (
// Commit represents a git commit. // Commit represents a git commit.
type Commit struct { type Commit struct {
Tree Tree
ID SHA1 // The ID of this commit object ID ObjectID // The ID of this commit object
Author *Signature Author *Signature
Committer *Signature Committer *Signature
CommitMessage string CommitMessage string
Signature *CommitGPGSignature Signature *CommitGPGSignature
Parents []SHA1 // SHA1 strings Parents []ObjectID // ID strings
submoduleCache *ObjectCache submoduleCache *ObjectCache
} }
@ -50,9 +50,9 @@ func (c *Commit) Summary() string {
// ParentID returns oid of n-th parent (0-based index). // ParentID returns oid of n-th parent (0-based index).
// It returns nil if no such parent exists. // 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) { if n >= len(c.Parents) {
return SHA1{}, ErrNotExist{"", ""} return nil, ErrNotExist{"", ""}
} }
return c.Parents[n], nil return c.Parents[n], nil
} }
@ -209,9 +209,9 @@ func (c *Commit) CommitsBefore() ([]*Commit, error) {
} }
// HasPreviousCommit returns true if a given commitHash is contained in commit's parents // 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() this := c.ID.String()
that := commitHash.String() that := objectID.String()
if this == that { if this == that {
return false, nil return false, nil
@ -232,9 +232,14 @@ func (c *Commit) HasPreviousCommit(commitHash SHA1) (bool, error) {
// IsForcePush returns true if a push from oldCommitHash to this is a force push // IsForcePush returns true if a push from oldCommitHash to this is a force push
func (c *Commit) IsForcePush(oldCommitID string) (bool, error) { 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 return false, nil
} }
oldCommit, err := c.repo.GetCommit(oldCommitID) oldCommit, err := c.repo.GetCommit(oldCommitID)
if err != nil { if err != nil {
return false, err return false, err

View File

@ -59,11 +59,11 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
func convertCommit(c *object.Commit) *Commit { func convertCommit(c *object.Commit) *Commit {
return &Commit{ return &Commit{
ID: c.Hash, ID: ParseGogitHash(c.Hash),
CommitMessage: c.Message, CommitMessage: c.Message,
Committer: &c.Committer, Committer: &c.Committer,
Author: &c.Author, Author: &c.Author,
Signature: convertPGPSignature(c), Signature: convertPGPSignature(c),
Parents: c.ParentHashes, Parents: ParseGogitHashArray(c.ParentHashes),
} }
} }

View File

@ -29,7 +29,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
defer commitGraphFile.Close() defer commitGraphFile.Close()
} }
c, err := commitNodeIndex.Get(commit.ID) c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -14,9 +14,9 @@ import (
// We need this to interpret commits from cat-file or cat-file --batch // 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 // 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{ commit := &Commit{
ID: sha, ID: objectID,
Author: &Signature{}, Author: &Signature{},
Committer: &Signature{}, Committer: &Signature{},
} }

View File

@ -81,7 +81,7 @@ gpgsig -----BEGIN PGP SIGNATURE-----
empty commit` 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")) gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, gitRepo) assert.NotNil(t, gitRepo)

View File

@ -33,8 +33,8 @@ var (
// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx // DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx
DefaultContext context.Context DefaultContext context.Context
// SupportProcReceive version >= 2.29.0 SupportProcReceive bool // >= 2.29
SupportProcReceive bool SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an experimental curiosity
gitVersion *version.Version gitVersion *version.Version
) )
@ -189,7 +189,7 @@ func InitFull(ctx context.Context) (err error) {
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=") globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
} }
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil
if setting.LFS.StartServer { if setting.LFS.StartServer {
if CheckGitVersionAtLeast("2.1.2") != nil { if CheckGitVersionAtLeast("2.1.2") != nil {
return errors.New("LFS server support requires Git >= 2.1.2") return errors.New("LFS server support requires Git >= 2.1.2")

View File

@ -39,7 +39,7 @@ func NewLastCommitCache(count int64, repoPath string, gitRepo *Repository, cache
if cache == nil { if cache == nil {
return nil return nil
} }
if !setting.CacheService.LastCommit.Enabled || count < setting.CacheService.LastCommit.CommitsCount { if count < setting.CacheService.LastCommit.CommitsCount {
return nil return nil
} }
@ -92,17 +92,17 @@ func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) {
// GetCommitByPath gets the last commit for the entry in the provided commit // GetCommitByPath gets the last commit for the entry in the provided commit
func (c *LastCommitCache) GetCommitByPath(commitID, entryPath string) (*Commit, error) { func (c *LastCommitCache) GetCommitByPath(commitID, entryPath string) (*Commit, error) {
sha1, err := NewIDFromString(commitID) sha, err := NewIDFromString(commitID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
lastCommit, err := c.Get(sha1.String(), entryPath) lastCommit, err := c.Get(sha.String(), entryPath)
if err != nil || lastCommit != nil { if err != nil || lastCommit != nil {
return lastCommit, err return lastCommit, err
} }
lastCommit, err = c.repo.getCommitByPathWithID(sha1, entryPath) lastCommit, err = c.repo.getCommitByPathWithID(sha, entryPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -8,6 +8,7 @@ package git
import ( import (
"context" "context"
"github.com/go-git/go-git/v5/plumbing"
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph" 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() commitNodeIndex, _ := c.repo.CommitNodeIndex()
index, err := commitNodeIndex.Get(c.ID) index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
if err != nil { if err != nil {
return err return err
} }

View File

@ -143,17 +143,20 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int
} }
// Our "line" must look like: <commitid> SP (<parent> SP) * NUL // Our "line" must look like: <commitid> SP (<parent> SP) * NUL
ret.CommitID = string(g.next[0:40]) commitIds := string(g.next)
parents := string(g.next[41:])
if g.buffull { if g.buffull {
more, err := g.rd.ReadString('\x00') more, err := g.rd.ReadString('\x00')
if err != nil { if err != nil {
return nil, err 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" // now read the next "line"
g.buffull = false g.buffull = false

View File

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object" "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() defer commitGraphFile.Close()
} }
commitNode, err := commitNodeIndex.Get(notes.ID) commitNode, err := commitNodeIndex.Get(plumbing.Hash(notes.ID.RawValue()))
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,93 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"crypto/sha1"
"regexp"
"strconv"
)
// sha1Pattern can be used to determine if a string is an valid sha
var sha1Pattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
type ObjectFormat interface {
// Name returns the name of the object format
Name() string
// EmptyObjectID creates a new empty ObjectID from an object format hash name
EmptyObjectID() ObjectID
// EmptyTree is the hash of an empty tree
EmptyTree() ObjectID
// FullLength is the length of the hash's hex string
FullLength() int
// IsValid returns true if the input is a valid hash
IsValid(input string) bool
// MustID creates a new ObjectID from a byte slice
MustID(b []byte) ObjectID
// ComputeHash compute the hash for a given ObjectType and content
ComputeHash(t ObjectType, content []byte) ObjectID
}
type Sha1ObjectFormatImpl struct{}
var (
emptyObjectID = &Sha1Hash{}
emptyTree = &Sha1Hash{
0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60,
0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88, 0xfb, 0xee, 0x49, 0x04,
}
)
func (Sha1ObjectFormatImpl) Name() string { return "sha1" }
func (Sha1ObjectFormatImpl) EmptyObjectID() ObjectID {
return emptyObjectID
}
func (Sha1ObjectFormatImpl) EmptyTree() ObjectID {
return emptyTree
}
func (Sha1ObjectFormatImpl) FullLength() int { return 40 }
func (Sha1ObjectFormatImpl) IsValid(input string) bool {
return sha1Pattern.MatchString(input)
}
func (Sha1ObjectFormatImpl) MustID(b []byte) ObjectID {
var id Sha1Hash
copy(id[0:20], b)
return &id
}
// ComputeHash compute the hash for a given ObjectType and content
func (h Sha1ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID {
hasher := sha1.New()
_, _ = hasher.Write(t.Bytes())
_, _ = hasher.Write([]byte(" "))
_, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10)))
_, _ = hasher.Write([]byte{0})
// HashSum generates a SHA1 for the provided hash
var sha1 Sha1Hash
copy(sha1[:], hasher.Sum(nil))
return &sha1
}
var Sha1ObjectFormat ObjectFormat = Sha1ObjectFormatImpl{}
var SupportedObjectFormats = []ObjectFormat{
Sha1ObjectFormat,
// TODO: add sha256
}
func ObjectFormatFromName(name string) ObjectFormat {
for _, objectFormat := range SupportedObjectFormats {
if name == objectFormat.Name() {
return objectFormat
}
}
return nil
}
func IsValidObjectFormat(name string) bool {
return ObjectFormatFromName(name) != nil
}

90
modules/git/object_id.go Normal file
View File

@ -0,0 +1,90 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"bytes"
"encoding/hex"
"fmt"
)
type ObjectID interface {
String() string
IsZero() bool
RawValue() []byte
Type() ObjectFormat
}
type Sha1Hash [20]byte
func (h *Sha1Hash) String() string {
return hex.EncodeToString(h[:])
}
func (h *Sha1Hash) IsZero() bool {
empty := Sha1Hash{}
return bytes.Equal(empty[:], h[:])
}
func (h *Sha1Hash) RawValue() []byte { return h[:] }
func (*Sha1Hash) Type() ObjectFormat { return Sha1ObjectFormat }
var _ ObjectID = &Sha1Hash{}
func MustIDFromString(hexHash string) ObjectID {
id, err := NewIDFromString(hexHash)
if err != nil {
panic(err)
}
return id
}
func NewIDFromString(hexHash string) (ObjectID, error) {
var theObjectFormat ObjectFormat
for _, objectFormat := range SupportedObjectFormats {
if len(hexHash) == objectFormat.FullLength() {
theObjectFormat = objectFormat
break
}
}
if theObjectFormat == nil {
return nil, fmt.Errorf("length %d has no matched object format: %s", len(hexHash), hexHash)
}
b, err := hex.DecodeString(hexHash)
if err != nil {
return nil, err
}
if len(b) != theObjectFormat.FullLength()/2 {
return theObjectFormat.EmptyObjectID(), fmt.Errorf("length must be %d: %v", theObjectFormat.FullLength(), b)
}
return theObjectFormat.MustID(b), nil
}
func IsEmptyCommitID(commitID string) bool {
if commitID == "" {
return true
}
id, err := NewIDFromString(commitID)
if err != nil {
return false
}
return id.IsZero()
}
// ComputeBlobHash compute the hash for a given blob content
func ComputeBlobHash(hashType ObjectFormat, content []byte) ObjectID {
return hashType.ComputeHash(ObjectBlob, content)
}
type ErrInvalidSHA struct {
SHA string
}
func (err ErrInvalidSHA) Error() string {
return fmt.Sprintf("invalid sha: %s", err.SHA)
}

View File

@ -0,0 +1,28 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/hash"
)
func ParseGogitHash(h plumbing.Hash) ObjectID {
switch hash.Size {
case 20:
return Sha1ObjectFormat.MustID(h[:])
}
return nil
}
func ParseGogitHashArray(objectIDs []plumbing.Hash) []ObjectID {
ret := make([]ObjectID, len(objectIDs))
for i, h := range objectIDs {
ret[i] = ParseGogitHash(h)
}
return ret
}

View File

@ -0,0 +1,21 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsValidSHAPattern(t *testing.T) {
h := Sha1ObjectFormat
assert.True(t, h.IsValid("fee1"))
assert.True(t, h.IsValid("abc000"))
assert.True(t, h.IsValid("9023902390239023902390239023902390239023"))
assert.False(t, h.IsValid("90239023902390239023902390239023902390239023"))
assert.False(t, h.IsValid("abc"))
assert.False(t, h.IsValid("123g"))
assert.False(t, h.IsValid("some random text"))
}

View File

@ -11,12 +11,14 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/hash"
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
) )
// ParseTreeEntries parses the output of a `git ls-tree -l` command. // ParseTreeEntries parses the output of a `git ls-tree -l` command.
func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { func ParseTreeEntries(h ObjectFormat, data []byte) ([]*TreeEntry, error) {
return parseTreeEntries(data, nil) return parseTreeEntries(data, nil)
} }
@ -50,15 +52,16 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6])) return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6]))
} }
if pos+40 > len(data) { // in hex format, not byte format ....
if pos+hash.Size*2 > len(data) {
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
} }
id, err := NewIDFromString(string(data[pos : pos+40])) var err error
entry.ID, err = NewIDFromString(string(data[pos : pos+hash.Size*2]))
if err != nil { if err != nil {
return nil, fmt.Errorf("Invalid ls-tree output: %w", err) return nil, fmt.Errorf("invalid ls-tree output: %w", err)
} }
entry.ID = id entry.gogitTreeEntry.Hash = plumbing.Hash(entry.ID.RawValue())
entry.gogitTreeEntry.Hash = id
pos += 41 // skip over sha and trailing space pos += 41 // skip over sha and trailing space
end := pos + bytes.IndexByte(data[pos:], '\t') end := pos + bytes.IndexByte(data[pos:], '\t')
@ -77,6 +80,7 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
// In case entry name is surrounded by double quotes(it happens only in git-shell). // In case entry name is surrounded by double quotes(it happens only in git-shell).
if data[pos] == '"' { if data[pos] == '"' {
var err error
entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end])) entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end]))
if err != nil { if err != nil {
return nil, fmt.Errorf("Invalid ls-tree output: %w", err) return nil, fmt.Errorf("Invalid ls-tree output: %w", err)

View File

@ -6,8 +6,10 @@
package git package git
import ( import (
"fmt"
"testing" "testing"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -28,7 +30,7 @@ func TestParseTreeEntries(t *testing.T) {
{ {
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
gogitTreeEntry: &object.TreeEntry{ gogitTreeEntry: &object.TreeEntry{
Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
Name: "example/file2.txt", Name: "example/file2.txt",
Mode: filemode.Regular, Mode: filemode.Regular,
}, },
@ -44,7 +46,7 @@ func TestParseTreeEntries(t *testing.T) {
{ {
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
gogitTreeEntry: &object.TreeEntry{ gogitTreeEntry: &object.TreeEntry{
Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
Name: "example/\n.txt", Name: "example/\n.txt",
Mode: filemode.Symlink, Mode: filemode.Symlink,
}, },
@ -55,7 +57,7 @@ func TestParseTreeEntries(t *testing.T) {
ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"), ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
sized: true, sized: true,
gogitTreeEntry: &object.TreeEntry{ gogitTreeEntry: &object.TreeEntry{
Hash: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"), Hash: plumbing.Hash(MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8").RawValue()),
Name: "example", Name: "example",
Mode: filemode.Dir, Mode: filemode.Dir,
}, },
@ -65,8 +67,12 @@ func TestParseTreeEntries(t *testing.T) {
} }
for _, testCase := range testCases { for _, testCase := range testCases {
entries, err := ParseTreeEntries([]byte(testCase.Input)) entries, err := ParseTreeEntries(Sha1ObjectFormat, []byte(testCase.Input))
assert.NoError(t, err) assert.NoError(t, err)
if len(entries) > 1 {
fmt.Println(testCase.Expected[0].ID)
fmt.Println(entries[0].ID)
}
assert.EqualValues(t, testCase.Expected, entries) assert.EqualValues(t, testCase.Expected, entries)
} }
} }

View File

@ -17,13 +17,13 @@ import (
) )
// ParseTreeEntries parses the output of a `git ls-tree -l` command. // ParseTreeEntries parses the output of a `git ls-tree -l` command.
func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { func ParseTreeEntries(objectFormat ObjectFormat, data []byte) ([]*TreeEntry, error) {
return parseTreeEntries(data, nil) return parseTreeEntries(objectFormat, data, nil)
} }
var sepSpace = []byte{' '} var sepSpace = []byte{' '}
func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { func parseTreeEntries(objectFormat ObjectFormat, data []byte, ptree *Tree) ([]*TreeEntry, error) {
var err error var err error
entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1) entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
for pos := 0; pos < len(data); { for pos := 0; pos < len(data); {
@ -92,15 +92,15 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
return entries, nil return entries, nil
} }
func catBatchParseTreeEntries(ptree *Tree, rd *bufio.Reader, sz int64) ([]*TreeEntry, error) { func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio.Reader, sz int64) ([]*TreeEntry, error) {
fnameBuf := make([]byte, 4096) fnameBuf := make([]byte, 4096)
modeBuf := make([]byte, 40) modeBuf := make([]byte, 40)
shaBuf := make([]byte, 40) shaBuf := make([]byte, objectFormat.FullLength())
entries := make([]*TreeEntry, 0, 10) entries := make([]*TreeEntry, 0, 10)
loop: loop:
for sz > 0 { for sz > 0 {
mode, fname, sha, count, err := ParseTreeLine(rd, modeBuf, fnameBuf, shaBuf) mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
break loop break loop
@ -127,7 +127,7 @@ loop:
return nil, fmt.Errorf("unknown mode: %v", string(mode)) return nil, fmt.Errorf("unknown mode: %v", string(mode))
} }
entry.ID = MustID(sha) entry.ID = objectFormat.MustID(sha)
entry.name = string(fname) entry.name = string(fname)
entries = append(entries, entry) entries = append(entries, entry)
} }

View File

@ -12,6 +12,8 @@ import (
) )
func TestParseTreeEntriesLong(t *testing.T) { func TestParseTreeEntriesLong(t *testing.T) {
objectFormat := Sha1ObjectFormat
testCases := []struct { testCases := []struct {
Input string Input string
Expected []*TreeEntry Expected []*TreeEntry
@ -54,7 +56,7 @@ func TestParseTreeEntriesLong(t *testing.T) {
}, },
} }
for _, testCase := range testCases { for _, testCase := range testCases {
entries, err := ParseTreeEntries([]byte(testCase.Input)) entries, err := ParseTreeEntries(objectFormat, []byte(testCase.Input))
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, entries, len(testCase.Expected)) assert.Len(t, entries, len(testCase.Expected))
for i, entry := range entries { for i, entry := range entries {
@ -64,6 +66,8 @@ func TestParseTreeEntriesLong(t *testing.T) {
} }
func TestParseTreeEntriesShort(t *testing.T) { func TestParseTreeEntriesShort(t *testing.T) {
objectFormat := Sha1ObjectFormat
testCases := []struct { testCases := []struct {
Input string Input string
Expected []*TreeEntry Expected []*TreeEntry
@ -87,7 +91,7 @@ func TestParseTreeEntriesShort(t *testing.T) {
}, },
} }
for _, testCase := range testCases { for _, testCase := range testCases {
entries, err := ParseTreeEntries([]byte(testCase.Input)) entries, err := ParseTreeEntries(objectFormat, []byte(testCase.Input))
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, entries, len(testCase.Expected)) assert.Len(t, entries, len(testCase.Expected))
for i, entry := range entries { for i, entry := range entries {
@ -98,7 +102,7 @@ func TestParseTreeEntriesShort(t *testing.T) {
func TestParseTreeEntriesInvalid(t *testing.T) { func TestParseTreeEntriesInvalid(t *testing.T) {
// there was a panic: "runtime error: slice bounds out of range" when the input was invalid: #20315 // there was a panic: "runtime error: slice bounds out of range" when the input was invalid: #20315
entries, err := ParseTreeEntries([]byte("100644 blob ea0d83c9081af9500ac9f804101b3fd0a5c293af")) entries, err := ParseTreeEntries(Sha1ObjectFormat, []byte("100644 blob ea0d83c9081af9500ac9f804101b3fd0a5c293af"))
assert.Error(t, err) assert.Error(t, err)
assert.Len(t, entries, 0) assert.Len(t, entries, 0)
} }

Some files were not shown because too many files have changed in this diff Show More