mirror of https://github.com/go-gitea/gitea.git
Merge branch 'main' into lunny/rename_board_column
This commit is contained in:
commit
42bec44df6
4
.ignore
4
.ignore
|
@ -4,6 +4,8 @@
|
|||
/modules/options/bindata.go
|
||||
/modules/public/bindata.go
|
||||
/modules/templates/bindata.go
|
||||
/vendor
|
||||
/options/gitignore
|
||||
/options/license
|
||||
/public/assets
|
||||
/vendor
|
||||
node_modules
|
||||
|
|
296
cmd/dump.go
296
cmd/dump.go
|
@ -6,14 +6,13 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/dump"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -25,89 +24,17 @@ import (
|
|||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error {
|
||||
if verbose {
|
||||
log.Info("Adding file %s", customName)
|
||||
}
|
||||
|
||||
return w.Write(archiver.File{
|
||||
FileInfo: archiver.FileInfo{
|
||||
FileInfo: info,
|
||||
CustomName: customName,
|
||||
},
|
||||
ReadCloser: r,
|
||||
})
|
||||
}
|
||||
|
||||
func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error {
|
||||
file, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, file, fileInfo, filePath, verbose)
|
||||
}
|
||||
|
||||
func isSubdir(upper, lower string) (bool, error) {
|
||||
if relPath, err := filepath.Rel(upper, lower); err != nil {
|
||||
return false, err
|
||||
} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type outputType struct {
|
||||
Enum []string
|
||||
Default string
|
||||
selected string
|
||||
}
|
||||
|
||||
func (o outputType) Join() string {
|
||||
return strings.Join(o.Enum, ", ")
|
||||
}
|
||||
|
||||
func (o *outputType) Set(value string) error {
|
||||
for _, enum := range o.Enum {
|
||||
if enum == value {
|
||||
o.selected = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("allowed values are %s", o.Join())
|
||||
}
|
||||
|
||||
func (o outputType) String() string {
|
||||
if o.selected == "" {
|
||||
return o.Default
|
||||
}
|
||||
return o.selected
|
||||
}
|
||||
|
||||
var outputTypeEnum = &outputType{
|
||||
Enum: []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"},
|
||||
Default: "zip",
|
||||
}
|
||||
|
||||
// CmdDump represents the available dump sub-command.
|
||||
var CmdDump = &cli.Command{
|
||||
Name: "dump",
|
||||
Usage: "Dump Gitea files and database",
|
||||
Description: `Dump compresses all related files and database into zip file.
|
||||
It can be used for backup and capture Gitea server image to send to maintainer`,
|
||||
Action: runDump,
|
||||
Name: "dump",
|
||||
Usage: "Dump Gitea files and database",
|
||||
Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`,
|
||||
Action: runDump,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Value: fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix()),
|
||||
Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.",
|
||||
Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
|
@ -160,64 +87,52 @@ It can be used for backup and capture Gitea server image to send to maintainer`,
|
|||
Name: "skip-index",
|
||||
Usage: "Skip bleve index data",
|
||||
},
|
||||
&cli.GenericFlag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Value: outputTypeEnum,
|
||||
Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
|
||||
Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||
log.Fatal(format, args...)
|
||||
}
|
||||
|
||||
func runDump(ctx *cli.Context) error {
|
||||
var file *os.File
|
||||
fileName := ctx.String("file")
|
||||
outType := ctx.String("type")
|
||||
if fileName == "-" {
|
||||
file = os.Stdout
|
||||
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
|
||||
} else {
|
||||
for _, suffix := range outputTypeEnum.Enum {
|
||||
if strings.HasSuffix(fileName, "."+suffix) {
|
||||
fileName = strings.TrimSuffix(fileName, "."+suffix)
|
||||
break
|
||||
}
|
||||
}
|
||||
fileName += "." + outType
|
||||
}
|
||||
setting.MustInstalled()
|
||||
|
||||
// make sure we are logging to the console no matter what the configuration tells us do to
|
||||
// FIXME: don't use CfgProvider directly
|
||||
if _, err := setting.CfgProvider.Section("log").NewKey("MODE", "console"); err != nil {
|
||||
fatal("Setting logging mode to console failed: %v", err)
|
||||
}
|
||||
if _, err := setting.CfgProvider.Section("log.console").NewKey("STDERR", "true"); err != nil {
|
||||
fatal("Setting console logger to stderr failed: %v", err)
|
||||
}
|
||||
|
||||
// Set loglevel to Warn if quiet-mode is requested
|
||||
if ctx.Bool("quiet") {
|
||||
if _, err := setting.CfgProvider.Section("log.console").NewKey("LEVEL", "Warn"); err != nil {
|
||||
fatal("Setting console log-level failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !setting.InstallLock {
|
||||
log.Error("Is '%s' really the right config path?\n", setting.CustomConf)
|
||||
return fmt.Errorf("gitea is not initialized")
|
||||
}
|
||||
setting.LoadSettings() // cannot access session settings otherwise
|
||||
|
||||
quite := ctx.Bool("quiet")
|
||||
verbose := ctx.Bool("verbose")
|
||||
if verbose && ctx.Bool("quiet") {
|
||||
return fmt.Errorf("--quiet and --verbose cannot both be set")
|
||||
if verbose && quite {
|
||||
fatal("Option --quiet and --verbose cannot both be set")
|
||||
}
|
||||
|
||||
// outFileName is either "-" or a file name (will be made absolute)
|
||||
outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
|
||||
if outType == "" {
|
||||
fatal("Invalid output type")
|
||||
}
|
||||
|
||||
outFile := os.Stdout
|
||||
if outFileName != "-" {
|
||||
var err error
|
||||
if outFileName, err = filepath.Abs(outFileName); err != nil {
|
||||
fatal("Unable to get absolute path of dump file: %v", err)
|
||||
}
|
||||
if exist, _ := util.IsExist(outFileName); exist {
|
||||
fatal("Dump file %q exists", outFileName)
|
||||
}
|
||||
if outFile, err = os.Create(outFileName); err != nil {
|
||||
fatal("Unable to create dump file %q: %v", outFileName, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
}
|
||||
|
||||
setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
|
||||
|
||||
setting.DisableLoggerInit()
|
||||
setting.LoadSettings() // cannot access session settings otherwise
|
||||
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
|
@ -226,44 +141,32 @@ func runDump(ctx *cli.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := storage.Init(); err != nil {
|
||||
if err = storage.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if file == nil {
|
||||
file, err = os.Create(fileName)
|
||||
if err != nil {
|
||||
fatal("Unable to open %s: %v", fileName, err)
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
absFileName, err := filepath.Abs(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var iface any
|
||||
if fileName == "-" {
|
||||
iface, err = archiver.ByExtension(fmt.Sprintf(".%s", outType))
|
||||
} else {
|
||||
iface, err = archiver.ByExtension(fileName)
|
||||
}
|
||||
archiverGeneric, err := archiver.ByExtension("." + outType)
|
||||
if err != nil {
|
||||
fatal("Unable to get archiver for extension: %v", err)
|
||||
}
|
||||
|
||||
w, _ := iface.(archiver.Writer)
|
||||
if err := w.Create(file); err != nil {
|
||||
archiverWriter := archiverGeneric.(archiver.Writer)
|
||||
if err := archiverWriter.Create(outFile); err != nil {
|
||||
fatal("Creating archiver.Writer failed: %v", err)
|
||||
}
|
||||
defer w.Close()
|
||||
defer archiverWriter.Close()
|
||||
|
||||
dumper := &dump.Dumper{
|
||||
Writer: archiverWriter,
|
||||
Verbose: verbose,
|
||||
}
|
||||
dumper.GlobalExcludeAbsPath(outFileName)
|
||||
|
||||
if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
|
||||
log.Info("Skip dumping local repositories")
|
||||
} else {
|
||||
log.Info("Dumping local repositories... %s", setting.RepoRootPath)
|
||||
if err := addRecursiveExclude(w, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil {
|
||||
if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
|
||||
fatal("Failed to include repositories: %v", err)
|
||||
}
|
||||
|
||||
|
@ -276,8 +179,7 @@ func runDump(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose)
|
||||
return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump LFS objects: %v", err)
|
||||
}
|
||||
|
@ -310,15 +212,13 @@ func runDump(ctx *cli.Context) error {
|
|||
fatal("Failed to dump database: %v", err)
|
||||
}
|
||||
|
||||
if err := addFile(w, "gitea-db.sql", dbDump.Name(), verbose); err != nil {
|
||||
if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil {
|
||||
fatal("Failed to include gitea-db.sql: %v", err)
|
||||
}
|
||||
|
||||
if len(setting.CustomConf) > 0 {
|
||||
log.Info("Adding custom configuration file from %s", setting.CustomConf)
|
||||
if err := addFile(w, "app.ini", setting.CustomConf, verbose); err != nil {
|
||||
fatal("Failed to include specified app.ini: %v", err)
|
||||
}
|
||||
log.Info("Adding custom configuration file from %s", setting.CustomConf)
|
||||
if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
|
||||
fatal("Failed to include specified app.ini: %v", err)
|
||||
}
|
||||
|
||||
if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
|
||||
|
@ -326,8 +226,8 @@ func runDump(ctx *cli.Context) error {
|
|||
} else {
|
||||
customDir, err := os.Stat(setting.CustomPath)
|
||||
if err == nil && customDir.IsDir() {
|
||||
if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is {
|
||||
if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil {
|
||||
if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
|
||||
if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
|
||||
fatal("Failed to include custom: %v", err)
|
||||
}
|
||||
} else {
|
||||
|
@ -364,8 +264,7 @@ func runDump(ctx *cli.Context) error {
|
|||
excludes = append(excludes, setting.Attachment.Storage.Path)
|
||||
excludes = append(excludes, setting.Packages.Storage.Path)
|
||||
excludes = append(excludes, setting.Log.RootPath)
|
||||
excludes = append(excludes, absFileName)
|
||||
if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
|
||||
if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
|
||||
fatal("Failed to include data directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -377,8 +276,7 @@ func runDump(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose)
|
||||
return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump attachments: %v", err)
|
||||
}
|
||||
|
@ -392,8 +290,7 @@ func runDump(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, object, info, path.Join("data", "packages", objPath), verbose)
|
||||
return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump packages: %v", err)
|
||||
}
|
||||
|
@ -409,80 +306,23 @@ func runDump(ctx *cli.Context) error {
|
|||
log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
|
||||
}
|
||||
if isExist {
|
||||
if err := addRecursiveExclude(w, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil {
|
||||
if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil {
|
||||
fatal("Failed to include log: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fileName != "-" {
|
||||
if err = w.Close(); err != nil {
|
||||
_ = util.Remove(fileName)
|
||||
fatal("Failed to save %s: %v", fileName, err)
|
||||
if outFileName == "-" {
|
||||
log.Info("Finish dumping to stdout")
|
||||
} else {
|
||||
if err = archiverWriter.Close(); err != nil {
|
||||
_ = os.Remove(outFileName)
|
||||
fatal("Failed to save %q: %v", outFileName, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(fileName, 0o600); err != nil {
|
||||
if err = os.Chmod(outFileName, 0o600); err != nil {
|
||||
log.Info("Can't change file access permissions mask to 0600: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if fileName != "-" {
|
||||
log.Info("Finish dumping in file %s", fileName)
|
||||
} else {
|
||||
log.Info("Finish dumping to stdout")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath
|
||||
func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error {
|
||||
absPath, err := filepath.Abs(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
files, err := dir.Readdir(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
currentAbsPath := filepath.Join(absPath, file.Name())
|
||||
currentInsidePath := path.Join(insidePath, file.Name())
|
||||
if file.IsDir() {
|
||||
if !util.SliceContainsString(excludeAbsPath, currentAbsPath) {
|
||||
if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = addRecursiveExclude(w, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
|
||||
shouldAdd := file.Mode().IsRegular()
|
||||
if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
target, err := filepath.EvalSymlinks(currentAbsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetStat, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shouldAdd = targetStat.Mode().IsRegular()
|
||||
}
|
||||
if shouldAdd {
|
||||
if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Info("Finish dumping in file %s", outFileName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -545,7 +545,7 @@ In this option, the idea is that the host SSH uses an `AuthorizedKeysCommand` in
|
|||
```bash
|
||||
cat <<"EOF" | sudo tee /home/git/docker-shell
|
||||
#!/bin/sh
|
||||
/usr/bin/docker exec -i --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
|
||||
/usr/bin/docker exec -i -u git --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
|
||||
EOF
|
||||
sudo chmod +x /home/git/docker-shell
|
||||
sudo usermod -s /home/git/docker-shell git
|
||||
|
@ -560,7 +560,7 @@ Add the following block to `/etc/ssh/sshd_config`, on the host:
|
|||
```bash
|
||||
Match User git
|
||||
AuthorizedKeysCommandUser git
|
||||
AuthorizedKeysCommand /usr/bin/docker exec -i gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
|
||||
AuthorizedKeysCommand /usr/bin/docker exec -i -u git gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
|
||||
```
|
||||
|
||||
(From 1.16.0 you will not need to set the `-c /data/gitea/conf/app.ini` option.)
|
||||
|
|
|
@ -139,13 +139,7 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerific
|
|||
}
|
||||
}
|
||||
|
||||
keyID := ""
|
||||
if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
|
||||
keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
|
||||
}
|
||||
if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
|
||||
keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
|
||||
}
|
||||
keyID := tryGetKeyIDFromSignature(sig)
|
||||
defaultReason := NoKeyFound
|
||||
|
||||
// First check if the sig has a keyID and if so just look at that
|
||||
|
|
|
@ -134,3 +134,13 @@ func extractSignature(s string) (*packet.Signature, error) {
|
|||
}
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
func tryGetKeyIDFromSignature(sig *packet.Signature) string {
|
||||
if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
|
||||
return fmt.Sprintf("%016X", *sig.IssuerKeyId)
|
||||
}
|
||||
if sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
|
||||
return fmt.Sprintf("%016X", sig.IssuerFingerprint[12:20])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ import (
|
|||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -391,3 +393,13 @@ epiDVQ==
|
|||
assert.Equal(t, time.Unix(1586105389, 0), expire)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryGetKeyIDFromSignature(t *testing.T) {
|
||||
assert.Empty(t, tryGetKeyIDFromSignature(&packet.Signature{}))
|
||||
assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{
|
||||
IssuerKeyId: util.ToPointer(uint64(0x38D1A3EADDBEA9C)),
|
||||
}))
|
||||
assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{
|
||||
IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c},
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
-
|
||||
id: 1
|
||||
project_id: 1
|
||||
issue_id: 1
|
||||
|
||||
-
|
||||
id: 2
|
||||
project_id: 1
|
||||
issue_id: 1
|
|
@ -21,6 +21,7 @@ import (
|
|||
"code.gitea.io/gitea/models/migrations/v1_20"
|
||||
"code.gitea.io/gitea/models/migrations/v1_21"
|
||||
"code.gitea.io/gitea/models/migrations/v1_22"
|
||||
"code.gitea.io/gitea/models/migrations/v1_23"
|
||||
"code.gitea.io/gitea/models/migrations/v1_6"
|
||||
"code.gitea.io/gitea/models/migrations/v1_7"
|
||||
"code.gitea.io/gitea/models/migrations/v1_8"
|
||||
|
@ -572,6 +573,10 @@ var migrations = []Migration{
|
|||
NewMigration("Ensure every project has exactly one default column - No Op", noopMigration),
|
||||
// v293 -> v294
|
||||
NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
|
||||
|
||||
// Gitea 1.22.0 ends at 294
|
||||
|
||||
NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
base.MainTest(m)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// AddUniqueIndexForProjectIssue adds unique indexes for project issue table
|
||||
func AddUniqueIndexForProjectIssue(x *xorm.Engine) error {
|
||||
// remove possible duplicated records in table project_issue
|
||||
type result struct {
|
||||
IssueID int64
|
||||
ProjectID int64
|
||||
Cnt int
|
||||
}
|
||||
var results []result
|
||||
if err := x.Select("issue_id, project_id, count(*) as cnt").
|
||||
Table("project_issue").
|
||||
GroupBy("issue_id, project_id").
|
||||
Having("count(*) > 1").
|
||||
Find(&results); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range results {
|
||||
if x.Dialect().URI().DBType == schemas.MSSQL {
|
||||
if _, err := x.Exec(fmt.Sprintf("delete from project_issue where id in (SELECT top %d id FROM project_issue WHERE issue_id = ? and project_id = ?)", r.Cnt-1), r.IssueID, r.ProjectID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var ids []int64
|
||||
if err := x.SQL("SELECT id FROM project_issue WHERE issue_id = ? and project_id = ? limit ?", r.IssueID, r.ProjectID, r.Cnt-1).Find(&ids); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := x.Table("project_issue").In("id", ids).Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add unique index for project_issue table
|
||||
type ProjectIssue struct { //revive:disable-line:exported
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX unique(s)"`
|
||||
ProjectID int64 `xorm:"INDEX unique(s)"`
|
||||
}
|
||||
|
||||
return x.Sync(new(ProjectIssue))
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
func Test_AddUniqueIndexForProjectIssue(t *testing.T) {
|
||||
type ProjectIssue struct { //revive:disable-line:exported
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
ProjectID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
// Prepare and load the testing database
|
||||
x, deferable := base.PrepareTestEnv(t, 0, new(ProjectIssue))
|
||||
defer deferable()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
cnt, err := x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, cnt)
|
||||
|
||||
assert.NoError(t, AddUniqueIndexForProjectIssue(x))
|
||||
|
||||
cnt, err = x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, cnt)
|
||||
|
||||
tables, err := x.DBMetas()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, len(tables))
|
||||
found := false
|
||||
for _, index := range tables[0].Indexes {
|
||||
if index.Type == schemas.UniqueType {
|
||||
found = true
|
||||
slices.Equal(index.Cols, []string{"project_id", "issue_id"})
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
}
|
|
@ -256,14 +256,6 @@ func IsEmailUsed(ctx context.Context, email string) (bool, error) {
|
|||
return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{})
|
||||
}
|
||||
|
||||
// DeleteInactiveEmailAddresses deletes inactive email addresses
|
||||
func DeleteInactiveEmailAddresses(ctx context.Context) error {
|
||||
_, err := db.GetEngine(ctx).
|
||||
Where("is_activated = ?", false).
|
||||
Delete(new(EmailAddress))
|
||||
return err
|
||||
}
|
||||
|
||||
// ActivateEmail activates the email address to given user.
|
||||
func ActivateEmail(ctx context.Context, email *EmailAddress) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package charset
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
@ -156,13 +157,16 @@ func TestEscapeControlReader(t *testing.T) {
|
|||
tests = append(tests, test)
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`repo.ambiguous_character:\d+,\d+`) // simplify the output for the tests, remove the translation variants
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output := &strings.Builder{}
|
||||
status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.status, *status)
|
||||
assert.Equal(t, tt.result, output.String())
|
||||
outStr := output.String()
|
||||
outStr = re.ReplaceAllString(outStr, "repo.ambiguous_character")
|
||||
assert.Equal(t, tt.result, outStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -561,14 +561,14 @@ func TestFormatError(t *testing.T) {
|
|||
err: &csv.ParseError{
|
||||
Err: csv.ErrFieldCount,
|
||||
},
|
||||
expectedMessage: "repo.error.csv.invalid_field_count",
|
||||
expectedMessage: "repo.error.csv.invalid_field_count:0",
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
err: &csv.ParseError{
|
||||
Err: csv.ErrBareQuote,
|
||||
},
|
||||
expectedMessage: "repo.error.csv.unexpected",
|
||||
expectedMessage: "repo.error.csv.unexpected:0,0",
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package dump
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
)
|
||||
|
||||
var SupportedOutputTypes = []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}
|
||||
|
||||
// PrepareFileNameAndType prepares the output file name and type, if the type is not supported, it returns an empty "outType"
|
||||
func PrepareFileNameAndType(argFile, argType string) (outFileName, outType string) {
|
||||
if argFile == "" && argType == "" {
|
||||
outType = SupportedOutputTypes[0]
|
||||
outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
|
||||
} else if argFile == "" {
|
||||
outType = argType
|
||||
outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
|
||||
} else if argType == "" {
|
||||
if filepath.Ext(outFileName) == "" {
|
||||
outType = SupportedOutputTypes[0]
|
||||
outFileName = argFile
|
||||
} else {
|
||||
for _, t := range SupportedOutputTypes {
|
||||
if strings.HasSuffix(argFile, "."+t) {
|
||||
outFileName = argFile
|
||||
outType = t
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
outFileName, outType = argFile, argType
|
||||
}
|
||||
if !slices.Contains(SupportedOutputTypes, outType) {
|
||||
return "", ""
|
||||
}
|
||||
return outFileName, outType
|
||||
}
|
||||
|
||||
func IsSubdir(upper, lower string) (bool, error) {
|
||||
if relPath, err := filepath.Rel(upper, lower); err != nil {
|
||||
return false, err
|
||||
} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type Dumper struct {
|
||||
Writer archiver.Writer
|
||||
Verbose bool
|
||||
|
||||
globalExcludeAbsPaths []string
|
||||
}
|
||||
|
||||
func (dumper *Dumper) AddReader(r io.ReadCloser, info os.FileInfo, customName string) error {
|
||||
if dumper.Verbose {
|
||||
log.Info("Adding file %s", customName)
|
||||
}
|
||||
|
||||
return dumper.Writer.Write(archiver.File{
|
||||
FileInfo: archiver.FileInfo{
|
||||
FileInfo: info,
|
||||
CustomName: customName,
|
||||
},
|
||||
ReadCloser: r,
|
||||
})
|
||||
}
|
||||
|
||||
func (dumper *Dumper) AddFile(filePath, absPath string) error {
|
||||
file, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dumper.AddReader(file, fileInfo, filePath)
|
||||
}
|
||||
|
||||
func (dumper *Dumper) normalizeFilePath(absPath string) string {
|
||||
absPath = filepath.Clean(absPath)
|
||||
if setting.IsWindows {
|
||||
absPath = strings.ToLower(absPath)
|
||||
}
|
||||
return absPath
|
||||
}
|
||||
|
||||
func (dumper *Dumper) GlobalExcludeAbsPath(absPaths ...string) {
|
||||
for _, absPath := range absPaths {
|
||||
dumper.globalExcludeAbsPaths = append(dumper.globalExcludeAbsPaths, dumper.normalizeFilePath(absPath))
|
||||
}
|
||||
}
|
||||
|
||||
func (dumper *Dumper) shouldExclude(absPath string, excludes []string) bool {
|
||||
norm := dumper.normalizeFilePath(absPath)
|
||||
return slices.Contains(dumper.globalExcludeAbsPaths, norm) || slices.Contains(excludes, norm)
|
||||
}
|
||||
|
||||
func (dumper *Dumper) AddRecursiveExclude(insidePath, absPath string, excludes []string) error {
|
||||
excludes = slices.Clone(excludes)
|
||||
for i := range excludes {
|
||||
excludes[i] = dumper.normalizeFilePath(excludes[i])
|
||||
}
|
||||
return dumper.addFileOrDir(insidePath, absPath, excludes)
|
||||
}
|
||||
|
||||
func (dumper *Dumper) addFileOrDir(insidePath, absPath string, excludes []string) error {
|
||||
absPath, err := filepath.Abs(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
files, err := dir.Readdir(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
currentAbsPath := filepath.Join(absPath, file.Name())
|
||||
if dumper.shouldExclude(currentAbsPath, excludes) {
|
||||
continue
|
||||
}
|
||||
|
||||
currentInsidePath := path.Join(insidePath, file.Name())
|
||||
if file.IsDir() {
|
||||
if err := dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = dumper.addFileOrDir(currentInsidePath, currentAbsPath, excludes); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
|
||||
shouldAdd := file.Mode().IsRegular()
|
||||
if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
target, err := filepath.EvalSymlinks(currentAbsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetStat, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shouldAdd = targetStat.Mode().IsRegular()
|
||||
}
|
||||
if shouldAdd {
|
||||
if err = dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package dump
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrepareFileNameAndType(t *testing.T) {
|
||||
defer timeutil.MockSet(time.Unix(1234, 0))()
|
||||
test := func(argFile, argType, expFile, expType string) {
|
||||
outFile, outType := PrepareFileNameAndType(argFile, argType)
|
||||
assert.Equal(t,
|
||||
fmt.Sprintf("outFile=%s, outType=%s", expFile, expType),
|
||||
fmt.Sprintf("outFile=%s, outType=%s", outFile, outType),
|
||||
fmt.Sprintf("argFile=%s, argType=%s", argFile, argType),
|
||||
)
|
||||
}
|
||||
|
||||
test("", "", "gitea-dump-1234.zip", "zip")
|
||||
test("", "tar.gz", "gitea-dump-1234.tar.gz", "tar.gz")
|
||||
test("", "no-such", "", "")
|
||||
|
||||
test("-", "", "-", "zip")
|
||||
test("-", "tar.gz", "-", "tar.gz")
|
||||
test("-", "no-such", "", "")
|
||||
|
||||
test("a", "", "a", "zip")
|
||||
test("a", "tar.gz", "a", "tar.gz")
|
||||
test("a", "no-such", "", "")
|
||||
|
||||
test("a.zip", "", "a.zip", "zip")
|
||||
test("a.zip", "tar.gz", "a.zip", "tar.gz")
|
||||
test("a.zip", "no-such", "", "")
|
||||
|
||||
test("a.tar.gz", "", "a.tar.gz", "zip")
|
||||
test("a.tar.gz", "tar.gz", "a.tar.gz", "tar.gz")
|
||||
test("a.tar.gz", "no-such", "", "")
|
||||
}
|
||||
|
||||
func TestIsSubDir(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
|
||||
|
||||
isSub, err := IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include"))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isSub)
|
||||
|
||||
isSub, err = IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include/sub"))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isSub)
|
||||
|
||||
isSub, err = IsSubdir(filepath.Join(tmpDir, "include/sub"), filepath.Join(tmpDir, "include"))
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isSub)
|
||||
}
|
||||
|
||||
type testWriter struct {
|
||||
added []string
|
||||
}
|
||||
|
||||
func (t *testWriter) Create(out io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testWriter) Write(f archiver.File) error {
|
||||
t.added = append(t.added, f.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDumper(t *testing.T) {
|
||||
sortStrings := func(s []string) []string {
|
||||
sort.Strings(s)
|
||||
return s
|
||||
}
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude1"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude2"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/a"), nil, 0o644)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/sub/b"), nil, 0o644)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude1/a-1"), nil, 0o644)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude2/a-2"), nil, 0o644)
|
||||
|
||||
tw := &testWriter{}
|
||||
d := &Dumper{Writer: tw}
|
||||
d.GlobalExcludeAbsPath(filepath.Join(tmpDir, "include/exclude1"))
|
||||
err := d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), []string{filepath.Join(tmpDir, "include/exclude2")})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, sortStrings([]string{"include/a", "include/sub", "include/sub/b"}), sortStrings(tw.added))
|
||||
|
||||
tw = &testWriter{}
|
||||
d = &Dumper{Writer: tw}
|
||||
err = d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), nil)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, sortStrings([]string{"include/exclude2", "include/exclude2/a-2", "include/a", "include/sub", "include/sub/b", "include/exclude1", "include/exclude1/a-1"}), sortStrings(tw.added))
|
||||
}
|
|
@ -26,14 +26,14 @@ type Commit struct {
|
|||
Author *Signature
|
||||
Committer *Signature
|
||||
CommitMessage string
|
||||
Signature *CommitGPGSignature
|
||||
Signature *CommitSignature
|
||||
|
||||
Parents []ObjectID // ID strings
|
||||
submoduleCache *ObjectCache
|
||||
}
|
||||
|
||||
// CommitGPGSignature represents a git commit signature part.
|
||||
type CommitGPGSignature struct {
|
||||
// CommitSignature represents a git commit signature part.
|
||||
type CommitSignature struct {
|
||||
Signature string
|
||||
Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
||||
func convertPGPSignature(c *object.Commit) *CommitSignature {
|
||||
if c.PGPSignature == "" {
|
||||
return nil
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
|||
return nil
|
||||
}
|
||||
|
||||
return &CommitGPGSignature{
|
||||
return &CommitSignature{
|
||||
Signature: c.PGPSignature,
|
||||
Payload: w.String(),
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ readLoop:
|
|||
}
|
||||
}
|
||||
commit.CommitMessage = messageSB.String()
|
||||
commit.Signature = &CommitGPGSignature{
|
||||
commit.Signature = &CommitSignature{
|
||||
Signature: signatureSB.String(),
|
||||
Payload: payloadSB.String(),
|
||||
}
|
||||
|
|
|
@ -185,17 +185,15 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) {
|
|||
|
||||
tag.Tagger = parseSignatureFromCommitLine(ref["creator"])
|
||||
tag.Message = ref["contents"]
|
||||
// strip PGP signature if present in contents field
|
||||
pgpStart := strings.Index(tag.Message, beginpgp)
|
||||
if pgpStart >= 0 {
|
||||
tag.Message = tag.Message[0:pgpStart]
|
||||
}
|
||||
|
||||
// strip any signature if present in contents field
|
||||
_, tag.Message, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.Message), 0)
|
||||
|
||||
// annotated tag with GPG signature
|
||||
if tag.Type == "tag" && ref["contents:signature"] != "" {
|
||||
payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n",
|
||||
tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message))
|
||||
tag.Signature = &CommitGPGSignature{
|
||||
tag.Signature = &CommitSignature{
|
||||
Signature: ref["contents:signature"],
|
||||
Payload: payload,
|
||||
}
|
||||
|
|
|
@ -315,7 +315,7 @@ qbHDASXl
|
|||
Type: "tag",
|
||||
Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
|
||||
Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md",
|
||||
Signature: &CommitGPGSignature{
|
||||
Signature: &CommitSignature{
|
||||
Signature: `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3
|
||||
|
|
|
@ -6,16 +6,10 @@ package git
|
|||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n"
|
||||
endpgp = "\n-----END PGP SIGNATURE-----"
|
||||
)
|
||||
|
||||
// Tag represents a Git tag.
|
||||
type Tag struct {
|
||||
Name string
|
||||
|
@ -24,7 +18,7 @@ type Tag struct {
|
|||
Type string
|
||||
Tagger *Signature
|
||||
Message string
|
||||
Signature *CommitGPGSignature
|
||||
Signature *CommitSignature
|
||||
}
|
||||
|
||||
// Commit return the commit of the tag reference
|
||||
|
@ -32,6 +26,36 @@ func (tag *Tag) Commit(gitRepo *Repository) (*Commit, error) {
|
|||
return gitRepo.getCommit(tag.Object)
|
||||
}
|
||||
|
||||
func parsePayloadSignature(data []byte, messageStart int) (payload, msg, sign string) {
|
||||
pos := messageStart
|
||||
signStart, signEnd := -1, -1
|
||||
for {
|
||||
eol := bytes.IndexByte(data[pos:], '\n')
|
||||
if eol < 0 {
|
||||
break
|
||||
}
|
||||
line := data[pos : pos+eol]
|
||||
signType, hasPrefix := bytes.CutPrefix(line, []byte("-----BEGIN "))
|
||||
signType, hasSuffix := bytes.CutSuffix(signType, []byte(" SIGNATURE-----"))
|
||||
if hasPrefix && hasSuffix {
|
||||
signEndBytes := append([]byte("\n-----END "), signType...)
|
||||
signEndBytes = append(signEndBytes, []byte(" SIGNATURE-----")...)
|
||||
signEnd = bytes.Index(data[pos:], signEndBytes)
|
||||
if signEnd != -1 {
|
||||
signStart = pos
|
||||
signEnd = pos + signEnd + len(signEndBytes)
|
||||
}
|
||||
}
|
||||
pos += eol + 1
|
||||
}
|
||||
|
||||
if signStart != -1 && signEnd != -1 {
|
||||
msgEnd := max(messageStart, signStart-1)
|
||||
return string(data[:msgEnd]), string(data[messageStart:msgEnd]), string(data[signStart:signEnd])
|
||||
}
|
||||
return string(data), string(data[messageStart:]), ""
|
||||
}
|
||||
|
||||
// Parse commit information from the (uncompressed) raw
|
||||
// data from the commit object.
|
||||
// \n\n separate headers from message
|
||||
|
@ -40,47 +64,37 @@ func parseTagData(objectFormat ObjectFormat, data []byte) (*Tag, error) {
|
|||
tag.ID = objectFormat.EmptyObjectID()
|
||||
tag.Object = objectFormat.EmptyObjectID()
|
||||
tag.Tagger = &Signature{}
|
||||
// we now have the contents of the commit object. Let's investigate...
|
||||
nextline := 0
|
||||
l:
|
||||
|
||||
pos := 0
|
||||
for {
|
||||
eol := bytes.IndexByte(data[nextline:], '\n')
|
||||
switch {
|
||||
case eol > 0:
|
||||
line := data[nextline : nextline+eol]
|
||||
spacepos := bytes.IndexByte(line, ' ')
|
||||
reftype := line[:spacepos]
|
||||
switch string(reftype) {
|
||||
case "object":
|
||||
id, err := NewIDFromString(string(line[spacepos+1:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag.Object = id
|
||||
case "type":
|
||||
// A commit can have one or more parents
|
||||
tag.Type = string(line[spacepos+1:])
|
||||
case "tagger":
|
||||
tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(line[spacepos+1:]))
|
||||
}
|
||||
nextline += eol + 1
|
||||
case eol == 0:
|
||||
tag.Message = string(data[nextline+1:])
|
||||
break l
|
||||
default:
|
||||
break l
|
||||
eol := bytes.IndexByte(data[pos:], '\n')
|
||||
if eol == -1 {
|
||||
break // shouldn't happen, but could just tolerate it
|
||||
}
|
||||
if eol == 0 {
|
||||
pos++
|
||||
break // end of headers
|
||||
}
|
||||
line := data[pos : pos+eol]
|
||||
key, val, _ := bytes.Cut(line, []byte(" "))
|
||||
switch string(key) {
|
||||
case "object":
|
||||
id, err := NewIDFromString(string(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag.Object = id
|
||||
case "type":
|
||||
tag.Type = string(val) // A commit can have one or more parents
|
||||
case "tagger":
|
||||
tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(val))
|
||||
}
|
||||
pos += eol + 1
|
||||
}
|
||||
idx := strings.LastIndex(tag.Message, beginpgp)
|
||||
if idx > 0 {
|
||||
endSigIdx := strings.Index(tag.Message[idx:], endpgp)
|
||||
if endSigIdx > 0 {
|
||||
tag.Signature = &CommitGPGSignature{
|
||||
Signature: tag.Message[idx+1 : idx+endSigIdx+len(endpgp)],
|
||||
Payload: string(data[:bytes.LastIndex(data, []byte(beginpgp))+1]),
|
||||
}
|
||||
tag.Message = tag.Message[:idx+1]
|
||||
}
|
||||
payload, msg, sign := parsePayloadSignature(data, pos)
|
||||
tag.Message = msg
|
||||
if len(sign) > 0 {
|
||||
tag.Signature = &CommitSignature{Signature: sign, Payload: payload}
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
|
|
@ -12,24 +12,28 @@ import (
|
|||
|
||||
func Test_parseTagData(t *testing.T) {
|
||||
testData := []struct {
|
||||
data []byte
|
||||
tag Tag
|
||||
data string
|
||||
expected Tag
|
||||
}{
|
||||
{data: []byte(`object 3b114ab800c6432ad42387ccf6bc8d4388a2885a
|
||||
{
|
||||
data: `object 3b114ab800c6432ad42387ccf6bc8d4388a2885a
|
||||
type commit
|
||||
tag 1.22.0
|
||||
tagger Lucas Michot <lucas@semalead.com> 1484491741 +0100
|
||||
|
||||
`), tag: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: &Sha1Hash{0x3b, 0x11, 0x4a, 0xb8, 0x0, 0xc6, 0x43, 0x2a, 0xd4, 0x23, 0x87, 0xcc, 0xf6, 0xbc, 0x8d, 0x43, 0x88, 0xa2, 0x88, 0x5a},
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0)},
|
||||
Message: "",
|
||||
Signature: nil,
|
||||
}},
|
||||
{data: []byte(`object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc
|
||||
`,
|
||||
expected: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: MustIDFromString("3b114ab800c6432ad42387ccf6bc8d4388a2885a"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
|
||||
Message: "",
|
||||
Signature: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc
|
||||
type commit
|
||||
tag 1.22.1
|
||||
tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100
|
||||
|
@ -37,37 +41,57 @@ tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100
|
|||
test message
|
||||
o
|
||||
|
||||
ono`), tag: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: &Sha1Hash{0x7c, 0xdf, 0x42, 0xc0, 0xb1, 0xcc, 0x76, 0x3a, 0xb7, 0xe4, 0xc3, 0x3c, 0x47, 0xa2, 0x4e, 0x27, 0xc6, 0x6b, 0xfc, 0xcc},
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0)},
|
||||
Message: "test message\no\n\nono",
|
||||
Signature: nil,
|
||||
}},
|
||||
ono`,
|
||||
expected: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0).In(time.FixedZone("", 3600))},
|
||||
Message: "test message\no\n\nono",
|
||||
Signature: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa
|
||||
type commit
|
||||
tag v0
|
||||
tagger dummy user <dummy-email@example.com> 1484491741 +0100
|
||||
|
||||
dummy message
|
||||
-----BEGIN SSH SIGNATURE-----
|
||||
dummy signature
|
||||
-----END SSH SIGNATURE-----
|
||||
`,
|
||||
expected: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "dummy user", Email: "dummy-email@example.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
|
||||
Message: "dummy message",
|
||||
Signature: &CommitSignature{
|
||||
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||
dummy signature
|
||||
-----END SSH SIGNATURE-----`,
|
||||
Payload: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa
|
||||
type commit
|
||||
tag v0
|
||||
tagger dummy user <dummy-email@example.com> 1484491741 +0100
|
||||
|
||||
dummy message`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testData {
|
||||
tag, err := parseTagData(Sha1ObjectFormat, test.data)
|
||||
tag, err := parseTagData(Sha1ObjectFormat, []byte(test.data))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, test.tag.ID, tag.ID)
|
||||
assert.EqualValues(t, test.tag.Object, tag.Object)
|
||||
assert.EqualValues(t, test.tag.Name, tag.Name)
|
||||
assert.EqualValues(t, test.tag.Message, tag.Message)
|
||||
assert.EqualValues(t, test.tag.Type, tag.Type)
|
||||
if test.tag.Signature != nil && assert.NotNil(t, tag.Signature) {
|
||||
assert.EqualValues(t, test.tag.Signature.Signature, tag.Signature.Signature)
|
||||
assert.EqualValues(t, test.tag.Signature.Payload, tag.Signature.Payload)
|
||||
} else {
|
||||
assert.Nil(t, tag.Signature)
|
||||
}
|
||||
if test.tag.Tagger != nil && assert.NotNil(t, tag.Tagger) {
|
||||
assert.EqualValues(t, test.tag.Tagger.Name, tag.Tagger.Name)
|
||||
assert.EqualValues(t, test.tag.Tagger.Email, tag.Tagger.Email)
|
||||
assert.EqualValues(t, test.tag.Tagger.When.Unix(), tag.Tagger.When.Unix())
|
||||
} else {
|
||||
assert.Nil(t, tag.Tagger)
|
||||
}
|
||||
assert.Equal(t, test.expected, *tag)
|
||||
}
|
||||
|
||||
tag, err := parseTagData(Sha1ObjectFormat, []byte("type commit\n\nfoo\n-----BEGIN SSH SIGNATURE-----\ncorrupted..."))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...", tag.Message)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ type Result struct {
|
|||
UpdatedUnix timeutil.TimeStamp
|
||||
Language string
|
||||
Color string
|
||||
Lines []ResultLine
|
||||
Lines []*ResultLine
|
||||
}
|
||||
|
||||
type ResultLine struct {
|
||||
|
@ -70,16 +70,18 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func HighlightSearchResultCode(filename string, lineNums []int, code string) []ResultLine {
|
||||
func HighlightSearchResultCode(filename, language string, lineNums []int, code string) []*ResultLine {
|
||||
// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
|
||||
hl, _ := highlight.Code(filename, "", code)
|
||||
hl, _ := highlight.Code(filename, language, code)
|
||||
highlightedLines := strings.Split(string(hl), "\n")
|
||||
|
||||
// The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n`
|
||||
lines := make([]ResultLine, min(len(highlightedLines), len(lineNums)))
|
||||
lines := make([]*ResultLine, min(len(highlightedLines), len(lineNums)))
|
||||
for i := 0; i < len(lines); i++ {
|
||||
lines[i].Num = lineNums[i]
|
||||
lines[i].FormattedContent = template.HTML(highlightedLines[i])
|
||||
lines[i] = &ResultLine{
|
||||
Num: lineNums[i],
|
||||
FormattedContent: template.HTML(highlightedLines[i]),
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
@ -122,7 +124,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
|
|||
UpdatedUnix: result.UpdatedUnix,
|
||||
Language: result.Language,
|
||||
Color: result.Color,
|
||||
Lines: HighlightSearchResultCode(result.Filename, lineNums, formattedLinesBuffer.String()),
|
||||
Lines: HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -171,6 +171,7 @@ type processor func(ctx *RenderContext, node *html.Node)
|
|||
var defaultProcessors = []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
codePreviewPatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
shortLinkProcessor,
|
||||
linkProcessor,
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
|
||||
var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
|
||||
|
||||
type RenderCodePreviewOptions struct {
|
||||
FullURL string
|
||||
OwnerName string
|
||||
RepoName string
|
||||
CommitID string
|
||||
FilePath string
|
||||
|
||||
LineStart, LineStop int
|
||||
}
|
||||
|
||||
func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) {
|
||||
m := codePreviewPattern.FindStringSubmatchIndex(node.Data)
|
||||
if m == nil {
|
||||
return 0, 0, "", nil
|
||||
}
|
||||
|
||||
opts := RenderCodePreviewOptions{
|
||||
FullURL: node.Data[m[0]:m[1]],
|
||||
OwnerName: node.Data[m[2]:m[3]],
|
||||
RepoName: node.Data[m[4]:m[5]],
|
||||
CommitID: node.Data[m[6]:m[7]],
|
||||
FilePath: node.Data[m[8]:m[9]],
|
||||
}
|
||||
if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) {
|
||||
return 0, 0, "", nil
|
||||
}
|
||||
u, err := url.Parse(opts.FilePath)
|
||||
if err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
opts.FilePath = strings.TrimPrefix(u.Path, "/")
|
||||
|
||||
lineStartStr, lineStopStr, _ := strings.Cut(node.Data[m[10]:m[11]], "-")
|
||||
lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
|
||||
lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
|
||||
opts.LineStart, opts.LineStop = lineStart, lineStop
|
||||
h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
|
||||
return m[0], m[1], h, err
|
||||
}
|
||||
|
||||
func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
for node != nil {
|
||||
if node.Type != html.TextNode {
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node)
|
||||
if err != nil || h == "" {
|
||||
if err != nil {
|
||||
log.Error("Unable to render code preview: %v", err)
|
||||
}
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
next := node.NextSibling
|
||||
textBefore := node.Data[:urlPosStart]
|
||||
textAfter := node.Data[urlPosEnd:]
|
||||
// "textBefore" could be empty if there is only a URL in the text node, then an empty node (p, or li) will be left here.
|
||||
// However, the empty node can't be simply removed, because:
|
||||
// 1. the following processors will still try to access it (need to double-check undefined behaviors)
|
||||
// 2. the new node is inserted as "<p>{TextBefore}<div NewNode/>{TextAfter}</p>" (the parent could also be "li")
|
||||
// then it is resolved as: "<p>{TextBefore}</p><div NewNode/><p>{TextAfter}</p>",
|
||||
// so unless it could correctly replace the parent "p/li" node, it is very difficult to eliminate the "TextBefore" empty node.
|
||||
node.Data = textBefore
|
||||
node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next)
|
||||
if textAfter != "" {
|
||||
node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: textAfter}, next)
|
||||
}
|
||||
node = next
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRenderCodePreview(t *testing.T) {
|
||||
markup.Init(&markup.ProcessorHelper{
|
||||
RenderRepoFileCodePreview: func(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
|
||||
return "<div>code preview</div>", nil
|
||||
},
|
||||
})
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Type: "markdown",
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>")
|
||||
test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
|
||||
}
|
|
@ -511,9 +511,17 @@ func TestMathBlock(t *testing.T) {
|
|||
`\(a\) \(b\)`,
|
||||
`<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl,
|
||||
},
|
||||
{
|
||||
`$a$.`,
|
||||
`<p><code class="language-math is-loading">a</code>.</p>` + nl,
|
||||
},
|
||||
{
|
||||
`.$a$`,
|
||||
`<p>.$a$</p>` + nl,
|
||||
},
|
||||
{
|
||||
`$a a$b b$`,
|
||||
`<p><code class="language-math is-loading">a a$b b</code></p>` + nl,
|
||||
`<p>$a a$b b$</p>` + nl,
|
||||
},
|
||||
{
|
||||
`a a$b b`,
|
||||
|
@ -521,7 +529,15 @@ func TestMathBlock(t *testing.T) {
|
|||
},
|
||||
{
|
||||
`a$b $a a$b b$`,
|
||||
`<p>a$b <code class="language-math is-loading">a a$b b</code></p>` + nl,
|
||||
`<p>a$b $a a$b b$</p>` + nl,
|
||||
},
|
||||
{
|
||||
"a$x$",
|
||||
`<p>a$x$</p>` + nl,
|
||||
},
|
||||
{
|
||||
"$x$a",
|
||||
`<p>$x$a</p>` + nl,
|
||||
},
|
||||
{
|
||||
"$$a$$",
|
||||
|
|
|
@ -41,9 +41,12 @@ func (parser *inlineParser) Trigger() []byte {
|
|||
return parser.start[0:1]
|
||||
}
|
||||
|
||||
func isPunctuation(b byte) bool {
|
||||
return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
|
||||
}
|
||||
|
||||
func isAlphanumeric(b byte) bool {
|
||||
// Github only cares about 0-9A-Za-z
|
||||
return (b >= '0' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
||||
}
|
||||
|
||||
// Parse parses the current line and returns a result of parsing.
|
||||
|
@ -56,7 +59,7 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
|
|||
}
|
||||
|
||||
precedingCharacter := block.PrecendingCharacter()
|
||||
if precedingCharacter < 256 && isAlphanumeric(byte(precedingCharacter)) {
|
||||
if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
|
||||
// need to exclude things like `a$` from being considered a start
|
||||
return nil
|
||||
}
|
||||
|
@ -75,14 +78,19 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
|
|||
ender += pos
|
||||
|
||||
// Now we want to check the character at the end of our parser section
|
||||
// that is ender + len(parser.end)
|
||||
// that is ender + len(parser.end) and check if char before ender is '\'
|
||||
pos = ender + len(parser.end)
|
||||
if len(line) <= pos {
|
||||
break
|
||||
}
|
||||
if !isAlphanumeric(line[pos]) {
|
||||
suceedingCharacter := line[pos]
|
||||
if !isPunctuation(suceedingCharacter) && !(suceedingCharacter == ' ') {
|
||||
return nil
|
||||
}
|
||||
if line[ender-1] != '\\' {
|
||||
break
|
||||
}
|
||||
|
||||
// move the pointer onwards
|
||||
ender += len(parser.end)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
|
@ -33,6 +34,8 @@ type ProcessorHelper struct {
|
|||
IsUsernameMentionable func(ctx context.Context, username string) bool
|
||||
|
||||
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
|
||||
|
||||
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
|
||||
}
|
||||
|
||||
var DefaultProcessorHelper ProcessorHelper
|
||||
|
|
|
@ -60,6 +60,21 @@ func createDefaultPolicy() *bluemonday.Policy {
|
|||
// For JS code copy and Mermaid loading state
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||
|
||||
// For code preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("code")
|
||||
|
||||
// For code preview (unicode escape)
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
||||
|
||||
// For color preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
||||
|
||||
|
|
|
@ -185,8 +185,13 @@ func InitLoggersForTest() {
|
|||
initAllLoggers()
|
||||
}
|
||||
|
||||
var initLoggerDisabled bool
|
||||
|
||||
// initAllLoggers creates all the log services
|
||||
func initAllLoggers() {
|
||||
if initLoggerDisabled {
|
||||
return
|
||||
}
|
||||
initManagedLoggers(log.GetManager(), CfgProvider)
|
||||
|
||||
golog.SetFlags(0)
|
||||
|
@ -194,6 +199,10 @@ func initAllLoggers() {
|
|||
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
|
||||
}
|
||||
|
||||
func DisableLoggerInit() {
|
||||
initLoggerDisabled = true
|
||||
}
|
||||
|
||||
func initManagedLoggers(manager *log.LoggerManager, cfg ConfigProvider) {
|
||||
loadLogGlobalFrom(cfg)
|
||||
prepareLoggerConfig(cfg)
|
||||
|
|
|
@ -21,8 +21,9 @@ var (
|
|||
)
|
||||
|
||||
// MockSet sets the time to a mocked time.Time
|
||||
func MockSet(now time.Time) {
|
||||
func MockSet(now time.Time) func() {
|
||||
mockNow = now
|
||||
return MockUnset
|
||||
}
|
||||
|
||||
// MockUnset will unset the mocked time.Time
|
||||
|
|
|
@ -6,6 +6,7 @@ package translation
|
|||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MockLocale provides a mocked locale without any translations
|
||||
|
@ -19,18 +20,25 @@ func (l MockLocale) Language() string {
|
|||
return "en"
|
||||
}
|
||||
|
||||
func (l MockLocale) TrString(s string, _ ...any) string {
|
||||
return s
|
||||
func (l MockLocale) TrString(s string, args ...any) string {
|
||||
return sprintAny(s, args...)
|
||||
}
|
||||
|
||||
func (l MockLocale) Tr(s string, a ...any) template.HTML {
|
||||
return template.HTML(s)
|
||||
func (l MockLocale) Tr(s string, args ...any) template.HTML {
|
||||
return template.HTML(sprintAny(s, args...))
|
||||
}
|
||||
|
||||
func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
|
||||
return template.HTML(key1)
|
||||
return template.HTML(sprintAny(key1, args...))
|
||||
}
|
||||
|
||||
func (l MockLocale) PrettyNumber(v any) string {
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
||||
func sprintAny(s string, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return s
|
||||
}
|
||||
return s + ":" + fmt.Sprintf(strings.Repeat(",%v", len(args))[1:], args...)
|
||||
}
|
||||
|
|
|
@ -213,6 +213,14 @@ func ToPointer[T any](val T) *T {
|
|||
return &val
|
||||
}
|
||||
|
||||
// Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal"
|
||||
func Iif[T comparable](condition bool, trueVal, falseVal T) T {
|
||||
if condition {
|
||||
return trueVal
|
||||
}
|
||||
return falseVal
|
||||
}
|
||||
|
||||
// IfZero returns "def" if "v" is a zero value, otherwise "v"
|
||||
func IfZero[T comparable](v, def T) T {
|
||||
var zero T
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
Copyright 1989, 1990 Advanced Micro Devices, Inc.
|
||||
|
||||
This software is the property of Advanced Micro Devices, Inc (AMD) which
|
||||
specifically grants the user the right to modify, use and distribute this
|
||||
software provided this notice is not removed or altered. All other rights
|
||||
are reserved by AMD.
|
||||
|
||||
AMD MAKES NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, WITH REGARD TO THIS
|
||||
SOFTWARE. IN NO EVENT SHALL AMD BE LIABLE FOR INCIDENTAL OR CONSEQUENTIAL
|
||||
DAMAGES IN CONNECTION WITH OR ARISING FROM THE FURNISHING, PERFORMANCE, OR
|
||||
USE OF THIS SOFTWARE.
|
|
@ -0,0 +1,12 @@
|
|||
COPYRIGHT (c) 1989-2013, 2015.
|
||||
On-Line Applications Research Corporation (OAR).
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose without fee is hereby granted, provided that this entire notice
|
||||
is included in all copies of any software which is or includes a copy
|
||||
or modification of this software.
|
||||
|
||||
THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
|
||||
WARRANTY. IN PARTICULAR, THE AUTHOR MAKES NO REPRESENTATION
|
||||
OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS
|
||||
SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.
|
|
@ -0,0 +1,12 @@
|
|||
Copyright Itai Nahshon 1995, 1996.
|
||||
This program is distributed with no warranty.
|
||||
|
||||
Source files for this program may be distributed freely.
|
||||
Modifications to this file are okay as long as:
|
||||
a. This copyright notice and comment are preserved and
|
||||
left at the top of the file.
|
||||
b. The man page is fixed to reflect the change.
|
||||
c. The author of this change adds his name and change
|
||||
description to the list of changes below.
|
||||
Executable files may be distributed with sources, or with
|
||||
exact location where the source code can be obtained.
|
|
@ -2790,7 +2790,6 @@ settings=Nastavení správce
|
|||
|
||||
dashboard.new_version_hint=Gitea %s je nyní k dispozici, právě u vás běži %s. Podívej se na <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blogu</a> pro více informací.
|
||||
dashboard.statistic=Souhrn
|
||||
dashboard.operations=Operace údržby
|
||||
dashboard.system_status=Status systému
|
||||
dashboard.operation_name=Název operace
|
||||
dashboard.operation_switch=Přepnout
|
||||
|
|
|
@ -2798,7 +2798,6 @@ settings=Administratoreinstellungen
|
|||
|
||||
dashboard.new_version_hint=Gitea %s ist jetzt verfügbar, deine derzeitige Version ist %s. Weitere Details findest du im <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">Blog</a>.
|
||||
dashboard.statistic=Übersicht
|
||||
dashboard.operations=Wartungsoperationen
|
||||
dashboard.system_status=System-Status
|
||||
dashboard.operation_name=Name der Operation
|
||||
dashboard.operation_switch=Wechseln
|
||||
|
|
|
@ -2687,7 +2687,6 @@ settings=Ρυθμίσεις Διαχειριστή
|
|||
|
||||
dashboard.new_version_hint=Το Gitea %s είναι διαθέσιμο, τώρα εκτελείτε το %s. Ανατρέξτε <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">στο blog</a> για περισσότερες λεπτομέρειες.
|
||||
dashboard.statistic=Περίληψη
|
||||
dashboard.operations=Λειτουργίες Συντήρησης
|
||||
dashboard.system_status=Κατάσταση Συστήματος
|
||||
dashboard.operation_name=Όνομα Λειτουργίας
|
||||
dashboard.operation_switch=Αλλαγή
|
||||
|
|
|
@ -1233,6 +1233,8 @@ file_view_rendered = View Rendered
|
|||
file_view_raw = View Raw
|
||||
file_permalink = Permalink
|
||||
file_too_large = The file is too large to be shown.
|
||||
code_preview_line_from_to = Lines %[1]d to %[2]d in %[3]s
|
||||
code_preview_line_in = Line %[1]d in %[2]s
|
||||
invisible_runes_header = `This file contains invisible Unicode characters`
|
||||
invisible_runes_description = `This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.`
|
||||
ambiguous_runes_header = `This file contains ambiguous Unicode characters`
|
||||
|
|
|
@ -2672,7 +2672,6 @@ settings=Configuración de Admin
|
|||
|
||||
dashboard.new_version_hint=Gitea %s ya está disponible, estás ejecutando %s. Revisa <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">el blog</a> para más detalles.
|
||||
dashboard.statistic=Resumen
|
||||
dashboard.operations=Operaciones de mantenimiento
|
||||
dashboard.system_status=Estado del sistema
|
||||
dashboard.operation_name=Nombre de la operación
|
||||
dashboard.operation_switch=Interruptor
|
||||
|
|
|
@ -2064,7 +2064,6 @@ last_page=واپسین
|
|||
total=مجموع: %d
|
||||
|
||||
dashboard.statistic=چکیده
|
||||
dashboard.operations=عملیاتهای نگهداری
|
||||
dashboard.system_status=وضعیت سامانه
|
||||
dashboard.operation_name=نام عملیات
|
||||
dashboard.operation_switch=تعویض
|
||||
|
|
|
@ -1407,7 +1407,6 @@ last_page=Viimeisin
|
|||
total=Yhteensä: %d
|
||||
|
||||
dashboard.statistic=Yhteenveto
|
||||
dashboard.operations=Huoltotoimet
|
||||
dashboard.system_status=Järjestelmän tila
|
||||
dashboard.operation_name=Toiminnon nimi
|
||||
dashboard.operation_switch=Vaihda
|
||||
|
|
|
@ -2712,7 +2712,6 @@ settings=Paramètres administrateur
|
|||
|
||||
dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">le blog</a> pour plus de détails.
|
||||
dashboard.statistic=Résumé
|
||||
dashboard.operations=Opérations de maintenance
|
||||
dashboard.system_status=État du système
|
||||
dashboard.operation_name=Nom de l'Opération
|
||||
dashboard.operation_switch=Basculer
|
||||
|
|
|
@ -1266,7 +1266,6 @@ last_page=Utolsó
|
|||
total=Összesen: %d
|
||||
|
||||
dashboard.statistic=Összefoglaló
|
||||
dashboard.operations=Karbantartási műveletek
|
||||
dashboard.system_status=Rendszer Állapota
|
||||
dashboard.operation_name=Művelet Neve
|
||||
dashboard.operation_switch=Váltás
|
||||
|
|
|
@ -2233,7 +2233,6 @@ last_page=Ultima
|
|||
total=Totale: %d
|
||||
|
||||
dashboard.statistic=Riepilogo
|
||||
dashboard.operations=Operazioni di manutenzione
|
||||
dashboard.system_status=Stato del sistema
|
||||
dashboard.operation_name=Nome Operazione
|
||||
dashboard.operation_switch=Cambia
|
||||
|
|
|
@ -2719,7 +2719,6 @@ settings=管理設定
|
|||
|
||||
dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">ブログ</a> を確認してください。
|
||||
dashboard.statistic=サマリー
|
||||
dashboard.operations=メンテナンス操作
|
||||
dashboard.system_status=システム状況
|
||||
dashboard.operation_name=操作の名称
|
||||
dashboard.operation_switch=切り替え
|
||||
|
|
|
@ -2693,7 +2693,6 @@ settings=Administratora iestatījumi
|
|||
|
||||
dashboard.new_version_hint=Ir pieejama Gitea versija %s, pašreizējā versija %s. Papildus informācija par jauno versiju ir pieejama <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">mājas lapā</a>.
|
||||
dashboard.statistic=Kopsavilkums
|
||||
dashboard.operations=Uzturēšanas darbības
|
||||
dashboard.system_status=Sistēmas statuss
|
||||
dashboard.operation_name=Darbības nosaukums
|
||||
dashboard.operation_switch=Pārslēgt
|
||||
|
|
|
@ -2135,7 +2135,6 @@ last_page=Laatste
|
|||
total=Totaal: %d
|
||||
|
||||
dashboard.statistic=Overzicht
|
||||
dashboard.operations=Onderhoudswerkzaamheden
|
||||
dashboard.system_status=Systeemtatus
|
||||
dashboard.operation_name=Bewerking naam
|
||||
dashboard.operation_switch=Omschakelen
|
||||
|
|
|
@ -2010,7 +2010,6 @@ last_page=Ostatnia
|
|||
total=Ogółem: %d
|
||||
|
||||
dashboard.statistic=Podsumowanie
|
||||
dashboard.operations=Operacje konserwacji
|
||||
dashboard.system_status=Status strony
|
||||
dashboard.operation_name=Nazwa operacji
|
||||
dashboard.operation_switch=Przełącz
|
||||
|
|
|
@ -2648,7 +2648,6 @@ settings=Configurações de Administrador
|
|||
|
||||
dashboard.new_version_hint=Uma nova versão está disponível: %s. Versão atual: %s. Visite <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">o blog</a> para mais informações.
|
||||
dashboard.statistic=Resumo
|
||||
dashboard.operations=Operações de manutenção
|
||||
dashboard.system_status=Status do sistema
|
||||
dashboard.operation_name=Nome da operação
|
||||
dashboard.operation_switch=Trocar
|
||||
|
|
|
@ -2775,6 +2775,7 @@ teams.invite.by=Convidado(a) por %s
|
|||
teams.invite.description=Clique no botão abaixo para se juntar à equipa.
|
||||
|
||||
[admin]
|
||||
maintenance=Manutenção
|
||||
dashboard=Painel de controlo
|
||||
self_check=Auto-verificação
|
||||
identity_access=Identidade e acesso
|
||||
|
@ -2798,7 +2799,7 @@ settings=Configurações de administração
|
|||
|
||||
dashboard.new_version_hint=O Gitea %s está disponível, você está a correr a versão %s. Verifique o <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blog</a> para mais detalhes.
|
||||
dashboard.statistic=Resumo
|
||||
dashboard.operations=Operações de manutenção
|
||||
dashboard.maintenance_operations=Operações de manutenção
|
||||
dashboard.system_status=Estado do sistema
|
||||
dashboard.operation_name=Nome da operação
|
||||
dashboard.operation_switch=Comutar
|
||||
|
@ -3305,6 +3306,7 @@ notices.op=Op.
|
|||
notices.delete_success=As notificações do sistema foram eliminadas.
|
||||
|
||||
self_check.no_problem_found=Nenhum problema encontrado até agora.
|
||||
self_check.startup_warnings=Alertas do arranque:
|
||||
self_check.database_collation_mismatch=Supor que a base de dados usa a colação: %s
|
||||
self_check.database_collation_case_insensitive=A base de dados está a usar a colação %s, que é insensível à diferença entre maiúsculas e minúsculas. Embora o Gitea possa trabalhar com ela, pode haver alguns casos raros que não funcionem como esperado.
|
||||
self_check.database_inconsistent_collation_columns=A base de dados está a usar a colação %s, mas estas colunas estão a usar colações diferentes. Isso poderá causar alguns problemas inesperados.
|
||||
|
|
|
@ -2634,7 +2634,6 @@ total=Всего: %d
|
|||
|
||||
dashboard.new_version_hint=Доступна новая версия Gitea %s, вы используете %s. Более подробную информацию читайте в <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">блоге</a>.
|
||||
dashboard.statistic=Статистика
|
||||
dashboard.operations=Операции
|
||||
dashboard.system_status=Состояние системы
|
||||
dashboard.operation_name=Имя операции
|
||||
dashboard.operation_switch=Переключить
|
||||
|
|
|
@ -2024,7 +2024,6 @@ last_page=පසුගිය
|
|||
total=මුළු: %d
|
||||
|
||||
dashboard.statistic=සාරාංශය
|
||||
dashboard.operations=නඩත්තු මෙහෙයුම්
|
||||
dashboard.system_status=පද්ධතියේ තත්වය
|
||||
dashboard.operation_name=මෙහෙයුමේ නම
|
||||
dashboard.operation_switch=මාරුවන්න
|
||||
|
|
|
@ -1647,7 +1647,6 @@ last_page=Sista
|
|||
total=Totalt: %d
|
||||
|
||||
dashboard.statistic=Översikt
|
||||
dashboard.operations=Operationer för underhåll
|
||||
dashboard.system_status=Status
|
||||
dashboard.operation_name=Operationsnamn
|
||||
dashboard.operation_switch=Byt till
|
||||
|
|
|
@ -2687,7 +2687,6 @@ settings=Yönetici Ayarları
|
|||
|
||||
dashboard.new_version_hint=Gitea %s şimdi hazır, %s çalıştırıyorsunuz. Ayrıntılar için <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blog</a>'a bakabilirsiniz.
|
||||
dashboard.statistic=Özet
|
||||
dashboard.operations=Bakım İşlemleri
|
||||
dashboard.system_status=Sistem Durumu
|
||||
dashboard.operation_name=İşlem Adı
|
||||
dashboard.operation_switch=Geç
|
||||
|
|
|
@ -2074,7 +2074,6 @@ last_page=Остання
|
|||
total=Разом: %d
|
||||
|
||||
dashboard.statistic=Підсумок
|
||||
dashboard.operations=Технічне обслуговування
|
||||
dashboard.system_status=Статус системи
|
||||
dashboard.operation_name=Назва операції
|
||||
dashboard.operation_switch=Перемкнути
|
||||
|
|
|
@ -2711,7 +2711,6 @@ settings=管理设置
|
|||
|
||||
dashboard.new_version_hint=Gitea %s 现已可用,您正在运行 %s。查看 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">博客</a> 了解详情。
|
||||
dashboard.statistic=摘要
|
||||
dashboard.operations=维护操作
|
||||
dashboard.system_status=系统状态
|
||||
dashboard.operation_name=操作名称
|
||||
dashboard.operation_switch=开关
|
||||
|
|
|
@ -2439,7 +2439,6 @@ total=總計:%d
|
|||
|
||||
dashboard.new_version_hint=現已推出 Gitea %s,您正在執行 %s。詳情請參閱<a target="_blank" rel="noreferrer" href="https://blog.gitea.io">部落格</a>的說明。
|
||||
dashboard.statistic=摘要
|
||||
dashboard.operations=維護作業
|
||||
dashboard.system_status=系統狀態
|
||||
dashboard.operation_name=作業名稱
|
||||
dashboard.operation_switch=開關
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
|
@ -31,6 +32,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/validation"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
"code.gitea.io/gitea/services/issue"
|
||||
|
@ -1035,6 +1037,9 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
|
|||
ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
|
||||
return err
|
||||
}
|
||||
if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
|
||||
log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
|
||||
}
|
||||
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
} else {
|
||||
if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil {
|
||||
|
@ -1042,6 +1047,11 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
|
|||
ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
|
||||
return err
|
||||
}
|
||||
if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
|
||||
if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
|
||||
log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
|
||||
}
|
||||
}
|
||||
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/highlight"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
@ -87,9 +88,16 @@ func RefBlame(ctx *context.Context) {
|
|||
|
||||
ctx.Data["IsBlame"] = true
|
||||
|
||||
ctx.Data["FileSize"] = blob.Size()
|
||||
fileSize := blob.Size()
|
||||
ctx.Data["FileSize"] = fileSize
|
||||
ctx.Data["FileName"] = blob.Name()
|
||||
|
||||
if fileSize >= setting.UI.MaxDisplayFileSize {
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
ctx.HTML(http.StatusOK, tplRepoHome)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["NumLines"], err = blob.GetBlobLineCount()
|
||||
ctx.Data["NumLinesSet"] = true
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ func Search(ctx *context.Context) {
|
|||
// UpdatedUnix: not supported yet
|
||||
// Language: not supported yet
|
||||
// Color: not supported yet
|
||||
Lines: code_indexer.HighlightSearchResultCode(r.Filename, r.LineNumbers, strings.Join(r.LineCodes, "\n")),
|
||||
Lines: code_indexer.HighlightSearchResultCode(r.Filename, "", r.LineNumbers, strings.Join(r.LineCodes, "\n")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -287,22 +287,19 @@ func LFSFileGet(ctx *context.Context) {
|
|||
|
||||
st := typesniffer.DetectContentType(buf)
|
||||
ctx.Data["IsTextFile"] = st.IsText()
|
||||
isRepresentableAsText := st.IsRepresentableAsText()
|
||||
|
||||
fileSize := meta.Size
|
||||
ctx.Data["FileSize"] = meta.Size
|
||||
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct")
|
||||
switch {
|
||||
case isRepresentableAsText:
|
||||
if st.IsSvgImage() {
|
||||
ctx.Data["IsImageFile"] = true
|
||||
}
|
||||
|
||||
if fileSize >= setting.UI.MaxDisplayFileSize {
|
||||
case st.IsRepresentableAsText():
|
||||
if meta.Size >= setting.UI.MaxDisplayFileSize {
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
break
|
||||
}
|
||||
|
||||
if st.IsSvgImage() {
|
||||
ctx.Data["IsImageFile"] = true
|
||||
}
|
||||
|
||||
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||
|
||||
// Building code view blocks with line number on server side.
|
||||
|
@ -338,6 +335,8 @@ func LFSFileGet(ctx *context.Context) {
|
|||
ctx.Data["IsAudioFile"] = true
|
||||
case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
|
||||
ctx.Data["IsImageFile"] = true
|
||||
default:
|
||||
// TODO: the logic is not the same as "renderFile" in "view.go"
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplSettingsLFSFile)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
|
@ -29,6 +30,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
@ -897,6 +899,10 @@ func SettingsPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
|
||||
log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
|
||||
|
||||
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
|
@ -915,6 +921,12 @@ func SettingsPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
|
||||
if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
|
||||
log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
|
||||
|
||||
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
|
|
|
@ -482,17 +482,17 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
|||
|
||||
switch {
|
||||
case isRepresentableAsText:
|
||||
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
break
|
||||
}
|
||||
|
||||
if fInfo.st.IsSvgImage() {
|
||||
ctx.Data["IsImageFile"] = true
|
||||
ctx.Data["CanCopyContent"] = true
|
||||
ctx.Data["HasSourceRenderedToggle"] = true
|
||||
}
|
||||
|
||||
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
break
|
||||
}
|
||||
|
||||
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||
|
||||
shouldRenderSource := ctx.FormString("display") == "source"
|
||||
|
@ -606,6 +606,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
|||
break
|
||||
}
|
||||
|
||||
// TODO: this logic seems strange, it duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
|
||||
// maybe for this case, the file is a binary file, and shouldn't be rendered?
|
||||
if markupType := markup.Type(blob.Name()); markupType != "" {
|
||||
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
|
||||
ctx.Data["IsMarkup"] = true
|
||||
|
|
|
@ -145,7 +145,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) {
|
|||
})
|
||||
NewWikiPost(ctx)
|
||||
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
|
||||
assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page"), ctx.Flash.ErrorMsg)
|
||||
assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
|
||||
assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
|
||||
}
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@ func notify(ctx context.Context, input *notifyInput) error {
|
|||
log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
|
||||
return nil
|
||||
}
|
||||
if input.Repo.IsEmpty {
|
||||
if input.Repo.IsEmpty || input.Repo.IsArchived {
|
||||
return nil
|
||||
}
|
||||
if unit_model.TypeActions.UnitGlobalDisabled() {
|
||||
|
@ -501,7 +501,7 @@ func handleSchedules(
|
|||
|
||||
// DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks
|
||||
func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error {
|
||||
if repo.IsEmpty {
|
||||
if repo.IsEmpty || repo.IsArchived {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -66,6 +66,11 @@ func startTasks(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
if row.Repo.IsArchived {
|
||||
// Skip if the repo is archived
|
||||
continue
|
||||
}
|
||||
|
||||
cfg, err := row.Repo.GetUnit(ctx, unit.TypeActions)
|
||||
if err != nil {
|
||||
if repo_model.IsErrUnitTypeNotExist(err) {
|
||||
|
|
|
@ -63,6 +63,7 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
|
|||
base.Locale = &translation.MockLocale{}
|
||||
|
||||
ctx := context.NewWebContext(base, opt.Render, nil)
|
||||
ctx.AppendContextValue(context.WebContextKey, ctx)
|
||||
ctx.PageData = map[string]any{}
|
||||
ctx.Data["PageStartTime"] = time.Now()
|
||||
chiCtx := chi.NewRouteContext()
|
||||
|
|
|
@ -11,6 +11,6 @@ import (
|
|||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, &unittest.TestOptions{
|
||||
FixtureFiles: []string{"user.yml"},
|
||||
FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
func ProcessorHelper() *markup.ProcessorHelper {
|
||||
return &markup.ProcessorHelper{
|
||||
ElementDir: "auto", // set dir="auto" for necessary (eg: <p>, <h?>, etc) tags
|
||||
|
||||
RenderRepoFileCodePreview: renderRepoFileCodePreview,
|
||||
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
||||
mentionedUser, err := user.GetUserByName(ctx, username)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/indexer/code"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
|
||||
opts.LineStop = max(opts.LineStop, opts.LineStart)
|
||||
lineCount := opts.LineStop - opts.LineStart + 1
|
||||
if lineCount <= 0 || lineCount > 140 /* GitHub at most show 140 lines */ {
|
||||
lineCount = 10
|
||||
opts.LineStop = opts.LineStart + lineCount
|
||||
}
|
||||
|
||||
dbRepo, err := repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("context is not a web context")
|
||||
}
|
||||
doer := webCtx.Doer
|
||||
|
||||
perms, err := access.GetUserRepoPermission(ctx, dbRepo, doer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !perms.CanRead(unit.TypeCode) {
|
||||
return "", fmt.Errorf("no permission")
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetCommit(opts.CommitID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
language, _ := files.TryGetContentLanguage(gitRepo, opts.CommitID, opts.FilePath)
|
||||
blob, err := commit.GetBlobByPath(opts.FilePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if blob.Size() > setting.UI.MaxDisplayFileSize {
|
||||
return "", fmt.Errorf("file is too large")
|
||||
}
|
||||
|
||||
dataRc, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer dataRc.Close()
|
||||
|
||||
reader := bufio.NewReader(dataRc)
|
||||
for i := 1; i < opts.LineStart; i++ {
|
||||
if _, err = reader.ReadBytes('\n'); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
lineNums := make([]int, 0, lineCount)
|
||||
lineCodes := make([]string, 0, lineCount)
|
||||
for i := opts.LineStart; i <= opts.LineStop; i++ {
|
||||
if line, err := reader.ReadString('\n'); err != nil && line == "" {
|
||||
break
|
||||
} else {
|
||||
lineNums = append(lineNums, i)
|
||||
lineCodes = append(lineCodes, line)
|
||||
}
|
||||
}
|
||||
realLineStop := max(opts.LineStart, opts.LineStart+len(lineNums)-1)
|
||||
highlightLines := code.HighlightSearchResultCode(opts.FilePath, language, lineNums, strings.Join(lineCodes, ""))
|
||||
|
||||
escapeStatus := &charset.EscapeStatus{}
|
||||
lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines))
|
||||
for i, hl := range highlightLines {
|
||||
lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, webCtx.Base.Locale, charset.RuneNBSP)
|
||||
escapeStatus = escapeStatus.Or(lineEscapeStatus[i])
|
||||
}
|
||||
|
||||
return webCtx.RenderToHTML("base/markup_codepreview", map[string]any{
|
||||
"FullURL": opts.FullURL,
|
||||
"FilePath": opts.FilePath,
|
||||
"LineStart": opts.LineStart,
|
||||
"LineStop": realLineStop,
|
||||
"RepoLink": dbRepo.Link(),
|
||||
"CommitID": opts.CommitID,
|
||||
"HighlightLines": highlightLines,
|
||||
"EscapeStatus": escapeStatus,
|
||||
"LineEscapeStatus": lineEscapeStatus,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProcessorHelperCodePreview(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
|
||||
htm, err := renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
|
||||
FullURL: "http://full",
|
||||
OwnerName: "user2",
|
||||
RepoName: "repo1",
|
||||
CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
||||
FilePath: "/README.md",
|
||||
LineStart: 1,
|
||||
LineStop: 2,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<div class="code-preview-container file-content">
|
||||
<div class="code-preview-header">
|
||||
<a href="http://full" class="muted" rel="nofollow">/README.md</a>
|
||||
repo.code_preview_line_from_to:1,2,<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a>
|
||||
</div>
|
||||
<table class="file-view">
|
||||
<tbody><tr>
|
||||
<td class="lines-num"><span data-line-number="1"></span></td>
|
||||
<td class="lines-code chroma"><code class="code-inner"><span class="gh"># repo1</code></td>
|
||||
</tr><tr>
|
||||
<td class="lines-num"><span data-line-number="2"></span></td>
|
||||
<td class="lines-code chroma"><code class="code-inner"></span><span class="gh"></span></code></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
`, string(htm))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
|
||||
htm, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
|
||||
FullURL: "http://full",
|
||||
OwnerName: "user2",
|
||||
RepoName: "repo1",
|
||||
CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
||||
FilePath: "/README.md",
|
||||
LineStart: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<div class="code-preview-container file-content">
|
||||
<div class="code-preview-header">
|
||||
<a href="http://full" class="muted" rel="nofollow">/README.md</a>
|
||||
repo.code_preview_line_in:1,<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a>
|
||||
</div>
|
||||
<table class="file-view">
|
||||
<tbody><tr>
|
||||
<td class="lines-num"><span data-line-number="1"></span></td>
|
||||
<td class="lines-code chroma"><code class="code-inner"><span class="gh"># repo1</code></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
`, string(htm))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
|
||||
_, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
|
||||
FullURL: "http://full",
|
||||
OwnerName: "user15",
|
||||
RepoName: "big_test_private_1",
|
||||
CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
||||
FilePath: "/README.md",
|
||||
LineStart: 1,
|
||||
LineStop: 10,
|
||||
})
|
||||
assert.ErrorContains(t, err, "no permission")
|
||||
}
|
|
@ -126,7 +126,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
|
|||
return fmt.Errorf("%s is an organization not a user", u.Name)
|
||||
}
|
||||
|
||||
if user_model.IsLastAdminUser(ctx, u) {
|
||||
if u.IsActive && user_model.IsLastAdminUser(ctx, u) {
|
||||
return models.ErrDeleteLastAdminUser{UID: u.ID}
|
||||
}
|
||||
|
||||
|
@ -250,7 +250,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
|
|||
if err := committer.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
committer.Close()
|
||||
_ = committer.Close()
|
||||
|
||||
if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
|
||||
return err
|
||||
|
@ -259,50 +259,45 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Note: There are something just cannot be roll back,
|
||||
// so just keep error logs of those operations.
|
||||
// Note: There are something just cannot be roll back, so just keep error logs of those operations.
|
||||
path := user_model.UserPath(u.Name)
|
||||
if err := util.RemoveAll(path); err != nil {
|
||||
err = fmt.Errorf("Failed to RemoveAll %s: %w", path, err)
|
||||
if err = util.RemoveAll(path); err != nil {
|
||||
err = fmt.Errorf("failed to RemoveAll %s: %w", path, err)
|
||||
_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
if u.Avatar != "" {
|
||||
avatarPath := u.CustomAvatarRelativePath()
|
||||
if err := storage.Avatars.Delete(avatarPath); err != nil {
|
||||
err = fmt.Errorf("Failed to remove %s: %w", avatarPath, err)
|
||||
if err = storage.Avatars.Delete(avatarPath); err != nil {
|
||||
err = fmt.Errorf("failed to remove %s: %w", avatarPath, err)
|
||||
_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteInactiveUsers deletes all inactive users and email addresses.
|
||||
// DeleteInactiveUsers deletes all inactive users and their email addresses.
|
||||
func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
|
||||
users, err := user_model.GetInactiveUsers(ctx, olderThan)
|
||||
inactiveUsers, err := user_model.GetInactiveUsers(ctx, olderThan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME: should only update authorized_keys file once after all deletions.
|
||||
for _, u := range users {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return db.ErrCancelledf("Before delete inactive user %s", u.Name)
|
||||
default:
|
||||
}
|
||||
if err := DeleteUser(ctx, u, false); err != nil {
|
||||
// Ignore users that were set inactive by admin.
|
||||
if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) ||
|
||||
models.IsErrUserOwnPackages(err) || models.IsErrDeleteLastAdminUser(err) {
|
||||
for _, u := range inactiveUsers {
|
||||
if err = DeleteUser(ctx, u, false); err != nil {
|
||||
// Ignore inactive users that were ever active but then were set inactive by admin
|
||||
if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return db.ErrCancelledf("when deleting inactive user %q", u.Name)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return user_model.DeleteInactiveEmailAddresses(ctx)
|
||||
return nil // TODO: there could be still inactive users left, and the number would increase gradually
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -185,3 +187,26 @@ func TestCreateUser_Issue5882(t *testing.T) {
|
|||
assert.NoError(t, DeleteUser(db.DefaultContext, v.user, false))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteInactiveUsers(t *testing.T) {
|
||||
addUser := func(name, email string, createdUnix timeutil.TimeStamp, active bool) {
|
||||
inactiveUser := &user_model.User{Name: name, LowerName: strings.ToLower(name), Email: email, CreatedUnix: createdUnix, IsActive: active}
|
||||
_, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(inactiveUser)
|
||||
assert.NoError(t, err)
|
||||
inactiveUserEmail := &user_model.EmailAddress{UID: inactiveUser.ID, IsPrimary: true, Email: email, LowerEmail: strings.ToLower(email), IsActivated: active}
|
||||
err = db.Insert(db.DefaultContext, inactiveUserEmail)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
addUser("user-inactive-10", "user-inactive-10@test.com", timeutil.TimeStampNow().Add(-600), false)
|
||||
addUser("user-inactive-5", "user-inactive-5@test.com", timeutil.TimeStampNow().Add(-300), false)
|
||||
addUser("user-active-10", "user-active-10@test.com", timeutil.TimeStampNow().Add(-600), true)
|
||||
addUser("user-active-5", "user-active-5@test.com", timeutil.TimeStampNow().Add(-300), true)
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-10"})
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user-inactive-10@test.com"})
|
||||
assert.NoError(t, DeleteInactiveUsers(db.DefaultContext, 8*time.Minute))
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: "user-inactive-10"})
|
||||
unittest.AssertNotExistsBean(t, &user_model.EmailAddress{Email: "user-inactive-10@test.com"})
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-5"})
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-10"})
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-5"})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<div class="code-preview-container file-content">
|
||||
<div class="code-preview-header">
|
||||
<a href="{{.FullURL}}" class="muted" rel="nofollow">{{.FilePath}}</a>
|
||||
{{$link := HTMLFormat `<a href="%s/src/commit/%s" rel="nofollow">%s</a>` .RepoLink .CommitID (.CommitID | ShortSha) -}}
|
||||
{{- if eq .LineStart .LineStop -}}
|
||||
{{ctx.Locale.Tr "repo.code_preview_line_in" .LineStart $link}}
|
||||
{{- else -}}
|
||||
{{ctx.Locale.Tr "repo.code_preview_line_from_to" .LineStart .LineStop $link}}
|
||||
{{- end}}
|
||||
</div>
|
||||
<table class="file-view">
|
||||
<tbody>
|
||||
{{- range $idx, $line := .HighlightLines -}}
|
||||
<tr>
|
||||
<td class="lines-num"><span data-line-number="{{$line.Num}}"></span></td>
|
||||
{{- if $.EscapeStatus.Escaped -}}
|
||||
{{- $lineEscapeStatus := index $.LineEscapeStatus $idx -}}
|
||||
<td class="lines-escape">{{if $lineEscapeStatus.Escaped}}<a href="#" class="toggle-escape-button btn interact-bg" title="{{if $lineEscapeStatus.HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{if $lineEscapeStatus.HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}"></a>{{end}}</td>
|
||||
{{- end}}
|
||||
<td class="lines-code chroma"><code class="code-inner">{{$line.FormattedContent}}</code></td>
|
||||
</tr>
|
||||
{{- end -}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -102,7 +102,7 @@
|
|||
|
||||
<div>
|
||||
<h1>Loading</h1>
|
||||
<div class="is-loading small-loading-icon tw-border tw-border-secondary tw-py-1"><span>loading ...</span></div>
|
||||
<div class="is-loading loading-icon-2px tw-border tw-border-secondary tw-py-1"><span>loading ...</span></div>
|
||||
<div class="is-loading tw-border tw-border-secondary tw-py-4">
|
||||
<p>loading ...</p>
|
||||
<p>loading ...</p>
|
||||
|
|
|
@ -30,6 +30,9 @@
|
|||
</h4>
|
||||
<div class="ui attached table unstackable segment">
|
||||
<div class="file-view code-view unicode-escaped">
|
||||
{{if .IsFileTooLarge}}
|
||||
{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
|
||||
{{else}}
|
||||
<table>
|
||||
<tbody>
|
||||
{{range $row := .BlameRows}}
|
||||
|
@ -75,6 +78,7 @@
|
|||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}{{/* end if .IsFileTooLarge */}}
|
||||
<div class="code-line-menu tippy-target">
|
||||
{{if $.Permission.CanRead $.UnitTypeIssues}}
|
||||
<a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
|
||||
|
|
|
@ -18,22 +18,21 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-1" id="repo-topics">
|
||||
{{range .Topics}}<a class="ui repo-topic large label topic tw-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
|
||||
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics">
|
||||
{{/* it should match the code in issue-home.js */}}
|
||||
{{range .Topics}}<a class="repo-topic ui large label" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
|
||||
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
|
||||
<div class="ui form tw-hidden tw-flex tw-flex-col tw-mt-4" id="topic_edit">
|
||||
<div class="field tw-flex-1 tw-mb-1">
|
||||
<div class="ui fluid multiple search selection dropdown tw-flex-wrap" data-text-count-prompt="{{ctx.Locale.Tr "repo.topic.count_prompt"}}" data-text-format-prompt="{{ctx.Locale.Tr "repo.topic.format_prompt"}}">
|
||||
<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
|
||||
{{range .Topics}}
|
||||
{{/* keey the same layout as Fomantic UI generated labels */}}
|
||||
<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
|
||||
{{end}}
|
||||
<div class="text"></div>
|
||||
</div>
|
||||
<div class="ui form tw-hidden tw-flex tw-gap-2 tw-my-2" id="topic_edit">
|
||||
<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1">
|
||||
<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
|
||||
{{range .Topics}}
|
||||
{{/* keep the same layout as Fomantic UI generated labels */}}
|
||||
<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
|
||||
{{end}}
|
||||
<div class="text"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<div class="navbar">
|
||||
<div class="issue-navbar">
|
||||
{{template "repo/issue/navbar" .}}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div role="main" aria-label="{{.Title}}" class="page-content repository labels">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="navbar tw-mb-4">
|
||||
<div class="issue-navbar tw-mb-4">
|
||||
{{template "repo/issue/navbar" .}}
|
||||
{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}}
|
||||
<button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="left menu">
|
||||
<div class="menu">
|
||||
<a class="{{if or (eq .SortType "alphabetically") (not .SortType)}}active {{end}}item" href="?sort=alphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
|
||||
<a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
|
||||
<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?sort=leastissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div role="main" aria-label="{{.Title}}" class="page-content repository new milestone">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="navbar">
|
||||
<div class="issue-navbar">
|
||||
{{template "repo/issue/navbar" .}}
|
||||
{{if and (or .CanWriteIssues .CanWritePulls) .PageIsEditMilestone}}
|
||||
<div class="ui right floated secondary menu">
|
||||
|
|
|
@ -677,7 +677,7 @@
|
|||
{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
|
||||
<div class="divider"></div>
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox small-loading-icon" id="allow-edits-from-maintainers"
|
||||
<div class="ui checkbox loading-icon-2px" id="allow-edits-from-maintainers"
|
||||
data-url="{{.Issue.Link}}"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}"
|
||||
data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}"
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
<div class="ui attached table unstackable segment">
|
||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
|
||||
<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextFile}} code-view{{end}}">
|
||||
{{if .IsMarkup}}
|
||||
{{if .IsFileTooLarge}}
|
||||
{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
|
||||
{{else if .IsMarkup}}
|
||||
{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}
|
||||
{{else if .IsPlainText}}
|
||||
<pre>{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}</pre>
|
||||
|
@ -33,19 +35,15 @@
|
|||
{{else if .IsPDFFile}}
|
||||
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div>
|
||||
{{else}}
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else if .FileSize}}
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
{{if .IsFileTooLarge}}
|
||||
<td><strong>{{ctx.Locale.Tr "repo.file_too_large"}}</strong></td>
|
||||
{{else}}
|
||||
<td class="lines-num">{{.LineNums}}</td>
|
||||
<td class="lines-code"><pre><code class="{{.HighlightClass}}"><ol>{{.FileContent}}</ol></code></pre></td>
|
||||
{{end}}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -89,7 +89,9 @@
|
|||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
|
||||
{{end}}
|
||||
<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextSource}} code-view{{end}}">
|
||||
{{if .IsMarkup}}
|
||||
{{if .IsFileTooLarge}}
|
||||
{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
|
||||
{{else if .IsMarkup}}
|
||||
{{if .FileContent}}{{.FileContent}}{{end}}
|
||||
{{else if .IsPlainText}}
|
||||
<pre>{{if .FileContent}}{{.FileContent}}{{end}}</pre>
|
||||
|
@ -108,19 +110,10 @@
|
|||
{{else if .IsPDFFile}}
|
||||
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
||||
{{else}}
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else if .FileSize}}
|
||||
{{if .IsFileTooLarge}}
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{{ctx.Locale.Tr "repo.file_too_large"}}</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<table>
|
||||
<tbody>
|
||||
{{range $idx, $code := .FileContent}}
|
||||
|
@ -142,7 +135,6 @@
|
|||
<a class="item view_git_blame" role="menuitem" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a>
|
||||
<a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<div class="tw-p-4">
|
||||
{{ctx.Locale.Tr "repo.file_too_large"}}
|
||||
{{if .RawFileLink}}<a href="{{.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>{{end}}
|
||||
</div>
|
|
@ -305,12 +305,6 @@ a.label,
|
|||
background-color: var(--color-label-bg);
|
||||
}
|
||||
|
||||
/* fix Fomantic's line-height cutting off "g" on Windows Chrome with Segoe UI */
|
||||
.ui.input > input {
|
||||
line-height: var(--line-height-default);
|
||||
text-align: start; /* Override fomantic's `text-align: left` to make RTL work via HTML `dir="auto"` */
|
||||
}
|
||||
|
||||
/* fix Fomantic's line-height causing vertical scrollbars to appear */
|
||||
ul.ui.list li,
|
||||
ol.ui.list li,
|
||||
|
@ -319,47 +313,6 @@ ol.ui.list li,
|
|||
line-height: var(--line-height-default);
|
||||
}
|
||||
|
||||
.ui.input.focus > input,
|
||||
.ui.input > input:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ui.action.input .ui.ui.button {
|
||||
border-color: var(--color-input-border);
|
||||
padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* currently used for search bar dropdowns in repo search and explore code */
|
||||
.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection {
|
||||
min-width: 10em;
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus) {
|
||||
border-right: none;
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(.active):hover {
|
||||
border-color: var(--color-input-border);
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) .ui.dropdown.selection.upward.visible {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) > input,
|
||||
.ui.action.input:not([class*="left action"]) > input:hover {
|
||||
border-right: none;
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection,
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .button,
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .button:hover,
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button,
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover {
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) > input:focus {
|
||||
border-right-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ui.menu {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -1233,10 +1186,13 @@ overflow-menu .ui.label {
|
|||
content: attr(data-line-number);
|
||||
line-height: 20px !important;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.code-view .lines-num span::after {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lines-type-marker {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
@ -1599,16 +1555,6 @@ table th[data-sortt-desc] .svg {
|
|||
align-items: stretch;
|
||||
}
|
||||
|
||||
.ui.ui.icon.input .icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui.icon.input > i.icon {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.flex-items-block > .item,
|
||||
.flex-text-block {
|
||||
display: flex;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@import "./modules/container.css";
|
||||
@import "./modules/divider.css";
|
||||
@import "./modules/header.css";
|
||||
@import "./modules/input.css";
|
||||
@import "./modules/label.css";
|
||||
@import "./modules/segment.css";
|
||||
@import "./modules/grid.css";
|
||||
|
@ -40,6 +41,7 @@
|
|||
|
||||
@import "./markup/content.css";
|
||||
@import "./markup/codecopy.css";
|
||||
@import "./markup/codepreview.css";
|
||||
@import "./markup/asciicast.css";
|
||||
|
||||
@import "./chroma/base.css";
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
.markup .code-preview-container {
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.markup .code-preview-container .code-preview-header {
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
padding: 0.5em;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markup .code-preview-container table {
|
||||
width: 100%;
|
||||
max-height: 240px; /* 12 lines at 20px per line */
|
||||
overflow-y: auto;
|
||||
margin: 0; /* override ".markup table {margin}" */
|
||||
}
|
||||
|
||||
/* workaround to hide empty p before container - more details are in "html_codepreview.go" */
|
||||
.markup p:empty:has(+ .code-preview-container) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* override the polluted styles from the content.css: ".markup table ..." */
|
||||
.markup .code-preview-container table tr {
|
||||
border: 0 !important;
|
||||
}
|
||||
.markup .code-preview-container table th,
|
||||
.markup .code-preview-container table td {
|
||||
border: 0 !important;
|
||||
padding: 0 0 0 5px !important;
|
||||
}
|
||||
.markup .code-preview-container table tr:nth-child(2n) {
|
||||
background: none !important;
|
||||
}
|
|
@ -382,7 +382,7 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.markup span.align-center span img
|
||||
.markup span.align-center span img,
|
||||
.markup span.align-center span video {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
|
@ -432,7 +432,7 @@
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.markup code,
|
||||
.markup code:not(.code-inner),
|
||||
.markup tt {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
|
|
|
@ -34,10 +34,14 @@
|
|||
border-radius: var(--border-radius-circle);
|
||||
}
|
||||
|
||||
.is-loading.small-loading-icon::after {
|
||||
.is-loading.loading-icon-2px::after {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.is-loading.loading-icon-3px::after {
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
/* for single form button, the loading state should be on the button, but not go semi-transparent, just replace the text on the button with the loader. */
|
||||
form.single-button-form.is-loading > * {
|
||||
opacity: 1;
|
||||
|
@ -62,7 +66,7 @@ form.single-button-form.is-loading .button {
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
/* TODO: not needed, use "is-loading small-loading-icon" instead */
|
||||
/* TODO: not needed, use "is-loading loading-icon-2px" instead */
|
||||
code.language-math.is-loading::after {
|
||||
padding: 0;
|
||||
border-width: 2px;
|
||||
|
|
|
@ -135,6 +135,12 @@ h4.ui.header .sub.header {
|
|||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
/* open dropdown menus to the left in right-attached headers */
|
||||
.ui.attached.header > .ui.right .ui.dropdown .menu {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
/* if a .top.attached.header is followed by a .segment, add some margin */
|
||||
.ui.segments + .ui.top.attached.header,
|
||||
.ui.attached.segment + .ui.top.attached.header {
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
/* based on Fomantic UI input module, with just the parts extracted that we use. If you find any
|
||||
unused rules here after refactoring, please remove them. */
|
||||
|
||||
.ui.input {
|
||||
position: relative;
|
||||
font-weight: var(--font-weight-normal);
|
||||
display: inline-flex;
|
||||
color: var(--color-input-text);
|
||||
}
|
||||
.ui.input > input {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
flex: 1 0 auto;
|
||||
outline: none;
|
||||
font-family: var(--fonts-regular);
|
||||
padding: 0.67857143em 1em;
|
||||
border: 1px solid var(--color-input-border);
|
||||
color: var(--color-input-text);
|
||||
border-radius: 0.28571429rem;
|
||||
line-height: var(--line-height-default);
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.ui.disabled.input,
|
||||
.ui.input:not(.disabled) input[disabled] {
|
||||
opacity: var(--opacity-disabled);
|
||||
}
|
||||
.ui.disabled.input > input,
|
||||
.ui.input:not(.disabled) input[disabled] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ui.input.focus > input,
|
||||
.ui.input > input:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ui.input.error > input {
|
||||
background: var(--color-error-bg);
|
||||
border-color: var(--color-error-border);
|
||||
color: var(--color-error-text);
|
||||
}
|
||||
|
||||
.ui.icon.input > i.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0.5;
|
||||
border-radius: 0 0.28571429rem 0.28571429rem 0;
|
||||
pointer-events: none;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.ui.icon.input > i.icon.is-loading {
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
.ui.icon.input > i.icon.is-loading > * {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ui.ui.ui.ui.icon.input > textarea,
|
||||
.ui.ui.ui.ui.icon.input > input {
|
||||
padding-right: 2.67142857em;
|
||||
}
|
||||
.ui.icon.input > i.link.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui.icon.input > i.circular.icon {
|
||||
top: 0.35em;
|
||||
right: 0.5em;
|
||||
}
|
||||
|
||||
.ui[class*="left icon"].input > i.icon {
|
||||
right: auto;
|
||||
left: 8px;
|
||||
border-radius: 0.28571429rem 0 0 0.28571429rem;
|
||||
}
|
||||
.ui[class*="left icon"].input > i.circular.icon {
|
||||
right: auto;
|
||||
left: 0.5em;
|
||||
}
|
||||
.ui.ui.ui.ui[class*="left icon"].input > textarea,
|
||||
.ui.ui.ui.ui[class*="left icon"].input > input {
|
||||
padding-left: 2.67142857em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.ui.icon.input > textarea:focus ~ .icon,
|
||||
.ui.icon.input > input:focus ~ .icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ui.icon.input > textarea ~ i.icon {
|
||||
height: 3em;
|
||||
}
|
||||
|
||||
.ui.form .field.error > .ui.action.input > .ui.button,
|
||||
.ui.action.input.error > .ui.button {
|
||||
border-top: 1px solid var(--color-error-border);
|
||||
border-bottom: 1px solid var(--color-error-border);
|
||||
}
|
||||
|
||||
.ui.action.input > .button,
|
||||
.ui.action.input > .buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.ui.action.input > .button,
|
||||
.ui.action.input > .buttons > .button {
|
||||
padding-top: 0.78571429em;
|
||||
padding-bottom: 0.78571429em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ui.action.input:not([class*="left action"]) > input {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
.ui.action.input > .dropdown:first-child,
|
||||
.ui.action.input > .button:first-child,
|
||||
.ui.action.input > .buttons:first-child > .button {
|
||||
border-radius: 0.28571429rem 0 0 0.28571429rem;
|
||||
}
|
||||
.ui.action.input > .dropdown:not(:first-child),
|
||||
.ui.action.input > .button:not(:first-child),
|
||||
.ui.action.input > .buttons:not(:first-child) > .button {
|
||||
border-radius: 0;
|
||||
}
|
||||
.ui.action.input > .dropdown:last-child,
|
||||
.ui.action.input > .button:last-child,
|
||||
.ui.action.input > .buttons:last-child > .button {
|
||||
border-radius: 0 0.28571429rem 0.28571429rem 0;
|
||||
}
|
||||
|
||||
.ui.fluid.input {
|
||||
display: flex;
|
||||
}
|
||||
.ui.fluid.input > input {
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
.ui.tiny.input {
|
||||
font-size: 0.85714286em;
|
||||
}
|
||||
.ui.small.input {
|
||||
font-size: 0.92857143em;
|
||||
}
|
||||
|
||||
.ui.action.input .ui.ui.button {
|
||||
border-color: var(--color-input-border);
|
||||
padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* currently used for search bar dropdowns in repo search and explore code */
|
||||
.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection {
|
||||
min-width: 10em;
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus) {
|
||||
border-right: none;
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(.active):hover {
|
||||
border-color: var(--color-input-border);
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) .ui.dropdown.selection.upward.visible {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) > input,
|
||||
.ui.action.input:not([class*="left action"]) > input:hover {
|
||||
border-right: none;
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection,
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .button,
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .button:hover,
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button,
|
||||
.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover {
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
.ui.action.input:not([class*="left action"]) > input:focus {
|
||||
border-right-color: var(--color-primary);
|
||||
}
|
|
@ -140,3 +140,8 @@
|
|||
.secondary-nav {
|
||||
background: var(--color-secondary-nav-bg) !important; /* important because of .ui.secondary.menu */
|
||||
}
|
||||
|
||||
.issue-navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
|
|
@ -2437,6 +2437,7 @@ tbody.commit-list {
|
|||
#repo-topics .repo-topic {
|
||||
font-weight: var(--font-weight-normal);
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#new-dependency-drop-list.ui.selection.dropdown {
|
||||
|
|
|
@ -6474,750 +6474,6 @@ select.ui.dropdown {
|
|||
Theme Overrides
|
||||
*******************************/
|
||||
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
/*!
|
||||
* # Fomantic-UI - Input
|
||||
* http://github.com/fomantic/Fomantic-UI/
|
||||
*
|
||||
*
|
||||
* Released under the MIT license
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
*/
|
||||
|
||||
/*******************************
|
||||
Standard
|
||||
*******************************/
|
||||
|
||||
/*--------------------
|
||||
Inputs
|
||||
---------------------*/
|
||||
|
||||
.ui.input {
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
display: inline-flex;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
.ui.input > input {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
flex: 1 0 auto;
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
|
||||
text-align: left;
|
||||
line-height: 1.21428571em;
|
||||
font-family: var(--fonts-regular);
|
||||
padding: 0.67857143em 1em;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid rgba(34, 36, 38, 0.15);
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
border-radius: 0.28571429rem;
|
||||
transition: box-shadow 0.1s ease, border-color 0.1s ease;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*--------------------
|
||||
Placeholder
|
||||
---------------------*/
|
||||
|
||||
/* browsers require these rules separate */
|
||||
|
||||
.ui.input > input::-webkit-input-placeholder {
|
||||
color: rgba(191, 191, 191, 0.87);
|
||||
}
|
||||
|
||||
.ui.input > input::-moz-placeholder {
|
||||
color: rgba(191, 191, 191, 0.87);
|
||||
}
|
||||
|
||||
.ui.input > input:-ms-input-placeholder {
|
||||
color: rgba(191, 191, 191, 0.87);
|
||||
}
|
||||
|
||||
/*******************************
|
||||
States
|
||||
*******************************/
|
||||
|
||||
/*--------------------
|
||||
Disabled
|
||||
---------------------*/
|
||||
|
||||
.ui.disabled.input,
|
||||
.ui.input:not(.disabled) input[disabled] {
|
||||
opacity: var(--opacity-disabled);
|
||||
}
|
||||
|
||||
.ui.disabled.input > input,
|
||||
.ui.input:not(.disabled) input[disabled] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*--------------------
|
||||
Active
|
||||
---------------------*/
|
||||
|
||||
.ui.input > input:active,
|
||||
.ui.input.down input {
|
||||
border-color: rgba(0, 0, 0, 0.3);
|
||||
background: #FAFAFA;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*--------------------
|
||||
Loading
|
||||
---------------------*/
|
||||
|
||||
.ui.loading.loading.input > i.icon:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -0.64285714em 0 0 -0.64285714em;
|
||||
width: 1.28571429em;
|
||||
height: 1.28571429em;
|
||||
border-radius: 500rem;
|
||||
border: 0.2em solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ui.loading.loading.input > i.icon:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -0.64285714em 0 0 -0.64285714em;
|
||||
width: 1.28571429em;
|
||||
height: 1.28571429em;
|
||||
animation: loader 0.6s infinite linear;
|
||||
border: 0.2em solid #767676;
|
||||
border-radius: 500rem;
|
||||
box-shadow: 0 0 0 1px transparent;
|
||||
}
|
||||
|
||||
/*--------------------
|
||||
Focus
|
||||
---------------------*/
|
||||
|
||||
.ui.input.focus > input,
|
||||
.ui.input > input:focus {
|
||||
border-color: #85B7D9;
|
||||
background: #FFFFFF;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ui.input.focus > input::-webkit-input-placeholder,
|
||||
.ui.input > input:focus::-webkit-input-placeholder {
|
||||
color: rgba(115, 115, 115, 0.87);
|
||||
}
|
||||
|
||||
.ui.input.focus > input::-moz-placeholder,
|
||||
.ui.input > input:focus::-moz-placeholder {
|
||||
color: rgba(115, 115, 115, 0.87);
|
||||
}
|
||||
|
||||
.ui.input.focus > input:-ms-input-placeholder,
|
||||
.ui.input > input:focus:-ms-input-placeholder {
|
||||
color: rgba(115, 115, 115, 0.87);
|
||||
}
|
||||
|
||||
/*--------------------
|
||||
States
|
||||
---------------------*/
|
||||
|
||||
.ui.input.error > input {
|
||||
background-color: #FFF6F6;
|
||||
border-color: #E0B4B4;
|
||||
color: #9F3A38;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
|
||||
.ui.input.error > input::-webkit-input-placeholder {
|
||||
color: #e7bdbc;
|
||||
}
|
||||
|
||||
.ui.input.error > input::-moz-placeholder {
|
||||
color: #e7bdbc;
|
||||
}
|
||||
|
||||
.ui.input.error > input:-ms-input-placeholder {
|
||||
color: #e7bdbc !important;
|
||||
}
|
||||
|
||||
/* Focused Placeholder */
|
||||
|
||||
.ui.input.error > input:focus::-webkit-input-placeholder {
|
||||
color: #da9796;
|
||||
}
|
||||
|
||||
.ui.input.error > input:focus::-moz-placeholder {
|
||||
color: #da9796;
|
||||
}
|
||||
|
||||
.ui.input.error > input:focus:-ms-input-placeholder {
|
||||
color: #da9796 !important;
|
||||
}
|
||||
|
||||
.ui.input.info > input {
|
||||
background-color: #F8FFFF;
|
||||
border-color: #A9D5DE;
|
||||
color: #276F86;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
|
||||
.ui.input.info > input::-webkit-input-placeholder {
|
||||
color: #98cfe1;
|
||||
}
|
||||
|
||||
.ui.input.info > input::-moz-placeholder {
|
||||
color: #98cfe1;
|
||||
}
|
||||
|
||||
.ui.input.info > input:-ms-input-placeholder {
|
||||
color: #98cfe1 !important;
|
||||
}
|
||||
|
||||
/* Focused Placeholder */
|
||||
|
||||
.ui.input.info > input:focus::-webkit-input-placeholder {
|
||||
color: #70bdd6;
|
||||
}
|
||||
|
||||
.ui.input.info > input:focus::-moz-placeholder {
|
||||
color: #70bdd6;
|
||||
}
|
||||
|
||||
.ui.input.info > input:focus:-ms-input-placeholder {
|
||||
color: #70bdd6 !important;
|
||||
}
|
||||
|
||||
.ui.input.success > input {
|
||||
background-color: #FCFFF5;
|
||||
border-color: #A3C293;
|
||||
color: #2C662D;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
|
||||
.ui.input.success > input::-webkit-input-placeholder {
|
||||
color: #8fcf90;
|
||||
}
|
||||
|
||||
.ui.input.success > input::-moz-placeholder {
|
||||
color: #8fcf90;
|
||||
}
|
||||
|
||||
.ui.input.success > input:-ms-input-placeholder {
|
||||
color: #8fcf90 !important;
|
||||
}
|
||||
|
||||
/* Focused Placeholder */
|
||||
|
||||
.ui.input.success > input:focus::-webkit-input-placeholder {
|
||||
color: #6cbf6d;
|
||||
}
|
||||
|
||||
.ui.input.success > input:focus::-moz-placeholder {
|
||||
color: #6cbf6d;
|
||||
}
|
||||
|
||||
.ui.input.success > input:focus:-ms-input-placeholder {
|
||||
color: #6cbf6d !important;
|
||||
}
|
||||
|
||||
.ui.input.warning > input {
|
||||
background-color: #FFFAF3;
|
||||
border-color: #C9BA9B;
|
||||
color: #573A08;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
|
||||
.ui.input.warning > input::-webkit-input-placeholder {
|
||||
color: #edad3e;
|
||||
}
|
||||
|
||||
.ui.input.warning > input::-moz-placeholder {
|
||||
color: #edad3e;
|
||||
}
|
||||
|
||||
.ui.input.warning > input:-ms-input-placeholder {
|
||||
color: #edad3e !important;
|
||||
}
|
||||
|
||||
/* Focused Placeholder */
|
||||
|
||||
.ui.input.warning > input:focus::-webkit-input-placeholder {
|
||||
color: #e39715;
|
||||
}
|
||||
|
||||
.ui.input.warning > input:focus::-moz-placeholder {
|
||||
color: #e39715;
|
||||
}
|
||||
|
||||
.ui.input.warning > input:focus:-ms-input-placeholder {
|
||||
color: #e39715 !important;
|
||||
}
|
||||
|
||||
/*******************************
|
||||
Variations
|
||||
*******************************/
|
||||
|
||||
/*--------------------
|
||||
Transparent
|
||||
---------------------*/
|
||||
|
||||
.ui.transparent.input > textarea,
|
||||
.ui.transparent.input > input {
|
||||
border-color: transparent !important;
|
||||
background-color: transparent !important;
|
||||
padding: 0;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.field .ui.transparent.input > textarea {
|
||||
padding: 0.67857143em 1em;
|
||||
}
|
||||
|
||||
/* Transparent Icon */
|
||||
|
||||
:not(.field) > .ui.transparent.icon.input > i.icon {
|
||||
width: 1.1em;
|
||||
}
|
||||
|
||||
:not(.field) > .ui.ui.ui.transparent.icon.input > input {
|
||||
padding-left: 0;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
:not(.field) > .ui.ui.ui.transparent[class*="left icon"].input > input {
|
||||
padding-left: 2em;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/*--------------------
|
||||
Icon
|
||||
---------------------*/
|
||||
|
||||
.ui.icon.input > i.icon {
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 2.67142857em;
|
||||
opacity: 0.5;
|
||||
border-radius: 0 0.28571429rem 0.28571429rem 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.ui.icon.input > i.icon:not(.link) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ui.ui.ui.ui.icon.input > textarea,
|
||||
.ui.ui.ui.ui.icon.input > input {
|
||||
padding-right: 2.67142857em;
|
||||
}
|
||||
|
||||
.ui.icon.input > i.icon:before,
|
||||
.ui.icon.input > i.icon:after {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
margin-top: -0.5em;
|
||||
}
|
||||
|
||||
.ui.icon.input > i.link.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui.icon.input > i.circular.icon {
|
||||
top: 0.35em;
|
||||
right: 0.5em;
|
||||
}
|
||||
|
||||
/* Left Icon Input */
|
||||
|
||||
.ui[class*="left icon"].input > i.icon {
|
||||
right: auto;
|
||||
left: 1px;
|
||||
border-radius: 0.28571429rem 0 0 0.28571429rem;
|
||||
}
|
||||
|
||||
.ui[class*="left icon"].input > i.circular.icon {
|
||||
right: auto;
|
||||
left: 0.5em;
|
||||
}
|
||||
|
||||
.ui.ui.ui.ui[class*="left icon"].input > textarea,
|
||||
.ui.ui.ui.ui[class*="left icon"].input > input {
|
||||
padding-left: 2.67142857em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
/* Focus */
|
||||
|
||||
.ui.icon.input > textarea:focus ~ i.icon,
|
||||
.ui.icon.input > input:focus ~ i.icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*--------------------
|
||||
Labeled
|
||||
---------------------*/
|
||||
|
||||
/* Adjacent Label */
|
||||
|
||||
.ui.labeled.input > .label {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.ui.labeled.input > .label:not(.corner) {
|
||||
padding-top: 0.78571429em;
|
||||
padding-bottom: 0.78571429em;
|
||||
}
|
||||
|
||||
/* Regular Label on Left */
|
||||
|
||||
.ui.labeled.input:not([class*="corner labeled"]) .label:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.ui.labeled.input:not([class*="corner labeled"]) .label:first-child + input {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
.ui.labeled.input:not([class*="corner labeled"]) .label:first-child + input:focus {
|
||||
border-left-color: #85B7D9;
|
||||
}
|
||||
|
||||
/* Regular Label on Right */
|
||||
|
||||
.ui[class*="right labeled"].input > input {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-right-color: transparent !important;
|
||||
}
|
||||
|
||||
.ui[class*="right labeled"].input > input + .label {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.ui[class*="right labeled"].input > input:focus {
|
||||
border-right-color: #85B7D9 !important;
|
||||
}
|
||||
|
||||
/* Corner Label */
|
||||
|
||||
.ui.labeled.input .corner.label {
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
font-size: 0.64285714em;
|
||||
border-radius: 0 0.28571429rem 0 0;
|
||||
}
|
||||
|
||||
/* Spacing with corner label */
|
||||
|
||||
.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input > textarea,
|
||||
.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input > input {
|
||||
padding-right: 2.5em !important;
|
||||
}
|
||||
|
||||
.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > textarea,
|
||||
.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > input {
|
||||
padding-right: 3.25em !important;
|
||||
}
|
||||
|
||||
.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > i.icon {
|
||||
margin-right: 1.25em;
|
||||
}
|
||||
|
||||
/* Left Labeled */
|
||||
|
||||
.ui[class*="left corner labeled"].labeled.input > textarea,
|
||||
.ui[class*="left corner labeled"].labeled.input > input {
|
||||
padding-left: 2.5em !important;
|
||||
}
|
||||
|
||||
.ui[class*="left corner labeled"].icon.input > textarea,
|
||||
.ui[class*="left corner labeled"].icon.input > input {
|
||||
padding-left: 3.25em !important;
|
||||
}
|
||||
|
||||
.ui[class*="left corner labeled"].icon.input > i.icon {
|
||||
margin-left: 1.25em;
|
||||
}
|
||||
|
||||
.ui.icon.input > textarea ~ i.icon {
|
||||
height: 3em;
|
||||
}
|
||||
|
||||
:not(.field) > .ui.transparent.icon.input > textarea ~ i.icon {
|
||||
height: 1.3em;
|
||||
}
|
||||
|
||||
/* Corner Label Position */
|
||||
|
||||
.ui.input > .ui.corner.label {
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
.ui.input > .ui.left.corner.label {
|
||||
right: auto;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
/* Labeled and action input states */
|
||||
|
||||
.ui.form .field.error > .ui.action.input > .ui.button,
|
||||
.ui.form .field.error > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
|
||||
.ui.action.input.error > .ui.button,
|
||||
.ui.labeled.input.error:not([class*="corner labeled"]) > .ui.label {
|
||||
border-top: 1px solid #E0B4B4;
|
||||
border-bottom: 1px solid #E0B4B4;
|
||||
}
|
||||
|
||||
.ui.form .field.error > .ui[class*="left action"].input > .ui.button,
|
||||
.ui.form .field.error > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
|
||||
.ui[class*="left action"].input.error > .ui.button,
|
||||
.ui.labeled.input.error:not(.right):not([class*="corner labeled"]) > .ui.label {
|
||||
border-left: 1px solid #E0B4B4;
|
||||
}
|
||||
|
||||
.ui.form .field.error > .ui.action.input:not([class*="left action"]) > input + .ui.button,
|
||||
.ui.form .field.error > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
|
||||
.ui.action.input.error:not([class*="left action"]) > input + .ui.button,
|
||||
.ui.right.labeled.input.error:not([class*="corner labeled"]) > input + .ui.label {
|
||||
border-right: 1px solid #E0B4B4;
|
||||
}
|
||||
|
||||
.ui.form .field.error > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
|
||||
.ui.right.labeled.input.error:not([class*="corner labeled"]) > .ui.label:first-child {
|
||||
border-left: 1px solid #E0B4B4;
|
||||
}
|
||||
|
||||
.ui.form .field.info > .ui.action.input > .ui.button,
|
||||
.ui.form .field.info > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
|
||||
.ui.action.input.info > .ui.button,
|
||||
.ui.labeled.input.info:not([class*="corner labeled"]) > .ui.label {
|
||||
border-top: 1px solid #A9D5DE;
|
||||
border-bottom: 1px solid #A9D5DE;
|
||||
}
|
||||
|
||||
.ui.form .field.info > .ui[class*="left action"].input > .ui.button,
|
||||
.ui.form .field.info > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
|
||||
.ui[class*="left action"].input.info > .ui.button,
|
||||
.ui.labeled.input.info:not(.right):not([class*="corner labeled"]) > .ui.label {
|
||||
border-left: 1px solid #A9D5DE;
|
||||
}
|
||||
|
||||
.ui.form .field.info > .ui.action.input:not([class*="left action"]) > input + .ui.button,
|
||||
.ui.form .field.info > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
|
||||
.ui.action.input.info:not([class*="left action"]) > input + .ui.button,
|
||||
.ui.right.labeled.input.info:not([class*="corner labeled"]) > input + .ui.label {
|
||||
border-right: 1px solid #A9D5DE;
|
||||
}
|
||||
|
||||
.ui.form .field.info > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
|
||||
.ui.right.labeled.input.info:not([class*="corner labeled"]) > .ui.label:first-child {
|
||||
border-left: 1px solid #A9D5DE;
|
||||
}
|
||||
|
||||
.ui.form .field.success > .ui.action.input > .ui.button,
|
||||
.ui.form .field.success > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
|
||||
.ui.action.input.success > .ui.button,
|
||||
.ui.labeled.input.success:not([class*="corner labeled"]) > .ui.label {
|
||||
border-top: 1px solid #A3C293;
|
||||
border-bottom: 1px solid #A3C293;
|
||||
}
|
||||
|
||||
.ui.form .field.success > .ui[class*="left action"].input > .ui.button,
|
||||
.ui.form .field.success > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
|
||||
.ui[class*="left action"].input.success > .ui.button,
|
||||
.ui.labeled.input.success:not(.right):not([class*="corner labeled"]) > .ui.label {
|
||||
border-left: 1px solid #A3C293;
|
||||
}
|
||||
|
||||
.ui.form .field.success > .ui.action.input:not([class*="left action"]) > input + .ui.button,
|
||||
.ui.form .field.success > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
|
||||
.ui.action.input.success:not([class*="left action"]) > input + .ui.button,
|
||||
.ui.right.labeled.input.success:not([class*="corner labeled"]) > input + .ui.label {
|
||||
border-right: 1px solid #A3C293;
|
||||
}
|
||||
|
||||
.ui.form .field.success > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
|
||||
.ui.right.labeled.input.success:not([class*="corner labeled"]) > .ui.label:first-child {
|
||||
border-left: 1px solid #A3C293;
|
||||
}
|
||||
|
||||
.ui.form .field.warning > .ui.action.input > .ui.button,
|
||||
.ui.form .field.warning > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label,
|
||||
.ui.action.input.warning > .ui.button,
|
||||
.ui.labeled.input.warning:not([class*="corner labeled"]) > .ui.label {
|
||||
border-top: 1px solid #C9BA9B;
|
||||
border-bottom: 1px solid #C9BA9B;
|
||||
}
|
||||
|
||||
.ui.form .field.warning > .ui[class*="left action"].input > .ui.button,
|
||||
.ui.form .field.warning > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label,
|
||||
.ui[class*="left action"].input.warning > .ui.button,
|
||||
.ui.labeled.input.warning:not(.right):not([class*="corner labeled"]) > .ui.label {
|
||||
border-left: 1px solid #C9BA9B;
|
||||
}
|
||||
|
||||
.ui.form .field.warning > .ui.action.input:not([class*="left action"]) > input + .ui.button,
|
||||
.ui.form .field.warning > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label,
|
||||
.ui.action.input.warning:not([class*="left action"]) > input + .ui.button,
|
||||
.ui.right.labeled.input.warning:not([class*="corner labeled"]) > input + .ui.label {
|
||||
border-right: 1px solid #C9BA9B;
|
||||
}
|
||||
|
||||
.ui.form .field.warning > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child,
|
||||
.ui.right.labeled.input.warning:not([class*="corner labeled"]) > .ui.label:first-child {
|
||||
border-left: 1px solid #C9BA9B;
|
||||
}
|
||||
|
||||
/*--------------------
|
||||
Action
|
||||
---------------------*/
|
||||
|
||||
.ui.action.input > .button,
|
||||
.ui.action.input > .buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.ui.action.input > .button,
|
||||
.ui.action.input > .buttons > .button {
|
||||
padding-top: 0.78571429em;
|
||||
padding-bottom: 0.78571429em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Input when ui Left*/
|
||||
|
||||
.ui[class*="left action"].input > input {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
/* Input when ui Right*/
|
||||
|
||||
.ui.action.input:not([class*="left action"]) > input {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
/* Button and Dropdown */
|
||||
|
||||
.ui.action.input > .dropdown:first-child,
|
||||
.ui.action.input > .button:first-child,
|
||||
.ui.action.input > .buttons:first-child > .button {
|
||||
border-radius: 0.28571429rem 0 0 0.28571429rem;
|
||||
}
|
||||
|
||||
.ui.action.input > .dropdown:not(:first-child),
|
||||
.ui.action.input > .button:not(:first-child),
|
||||
.ui.action.input > .buttons:not(:first-child) > .button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ui.action.input > .dropdown:last-child,
|
||||
.ui.action.input > .button:last-child,
|
||||
.ui.action.input > .buttons:last-child > .button {
|
||||
border-radius: 0 0.28571429rem 0.28571429rem 0;
|
||||
}
|
||||
|
||||
/* Input Focus */
|
||||
|
||||
.ui.action.input:not([class*="left action"]) > input:focus {
|
||||
border-right-color: #85B7D9;
|
||||
}
|
||||
|
||||
.ui.ui[class*="left action"].input > input:focus {
|
||||
border-left-color: #85B7D9;
|
||||
}
|
||||
|
||||
/*--------------------
|
||||
Fluid
|
||||
---------------------*/
|
||||
|
||||
.ui.fluid.input {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ui.fluid.input > input {
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
/*--------------------
|
||||
Size
|
||||
---------------------*/
|
||||
|
||||
.ui.input {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.ui.mini.input {
|
||||
font-size: 0.78571429em;
|
||||
}
|
||||
|
||||
.ui.tiny.input {
|
||||
font-size: 0.85714286em;
|
||||
}
|
||||
|
||||
.ui.small.input {
|
||||
font-size: 0.92857143em;
|
||||
}
|
||||
|
||||
.ui.large.input {
|
||||
font-size: 1.14285714em;
|
||||
}
|
||||
|
||||
.ui.big.input {
|
||||
font-size: 1.28571429em;
|
||||
}
|
||||
|
||||
.ui.huge.input {
|
||||
font-size: 1.42857143em;
|
||||
}
|
||||
|
||||
.ui.massive.input {
|
||||
font-size: 1.71428571em;
|
||||
}
|
||||
|
||||
/*******************************
|
||||
Theme Overrides
|
||||
*******************************/
|
||||
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
"dimmer",
|
||||
"dropdown",
|
||||
"form",
|
||||
"input",
|
||||
"list",
|
||||
"menu",
|
||||
"modal",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue