} | } | ||||
} | } | ||||
supportProcReceive := false | |||||
if git.CheckGitVersionAtLeast("2.29") == nil { | |||||
supportProcReceive = true | |||||
} | |||||
supportProcReceive := git.DefaultFeatures().SupportProcReceive | |||||
for scanner.Scan() { | for scanner.Scan() { | ||||
// TODO: support news feeds for wiki | // TODO: support news feeds for wiki | ||||
return nil | return nil | ||||
} | } | ||||
if git.CheckGitVersionAtLeast("2.29") != nil { | |||||
if !git.DefaultFeatures().SupportProcReceive { | |||||
return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.") | return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.") | ||||
} | } | ||||
} | } | ||||
if len(words) < 2 { | if len(words) < 2 { | ||||
if git.CheckGitVersionAtLeast("2.29") == nil { | |||||
if git.DefaultFeatures().SupportProcReceive { | |||||
// for AGit Flow | // for AGit Flow | ||||
if cmd == "ssh_info" { | if cmd == "ssh_info" { | ||||
fmt.Print(`{"type":"gitea","version":1}`) | fmt.Print(`{"type":"gitea","version":1}`) |
// SearchOrderByMap represents all possible search order | // SearchOrderByMap represents all possible search order | ||||
var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ | var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ | ||||
"asc": { | "asc": { | ||||
"alpha": db.SearchOrderByAlphabetically, | |||||
"alpha": "owner_name ASC, name ASC", | |||||
"created": db.SearchOrderByOldest, | "created": db.SearchOrderByOldest, | ||||
"updated": db.SearchOrderByLeastUpdated, | "updated": db.SearchOrderByLeastUpdated, | ||||
"size": db.SearchOrderBySize, | "size": db.SearchOrderBySize, | ||||
"id": db.SearchOrderByID, | "id": db.SearchOrderByID, | ||||
}, | }, | ||||
"desc": { | "desc": { | ||||
"alpha": db.SearchOrderByAlphabeticallyReverse, | |||||
"alpha": "owner_name DESC, name DESC", | |||||
"created": db.SearchOrderByNewest, | "created": db.SearchOrderByNewest, | ||||
"updated": db.SearchOrderByRecentUpdated, | "updated": db.SearchOrderByRecentUpdated, | ||||
"size": db.SearchOrderBySizeReverse, | "size": db.SearchOrderBySizeReverse, |
// CreateBlameReader creates reader for given repository, commit and file | // CreateBlameReader creates reader for given repository, commit and file | ||||
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) { | func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) { | ||||
var ignoreRevsFile *string | var ignoreRevsFile *string | ||||
if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore { | |||||
if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore { | |||||
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit) | ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit) | ||||
} | } | ||||
// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') | // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') | ||||
func (c *Commit) GetBranchName() (string, error) { | func (c *Commit) GetBranchName() (string, error) { | ||||
cmd := NewCommand(c.repo.Ctx, "name-rev") | cmd := NewCommand(c.repo.Ctx, "name-rev") | ||||
if CheckGitVersionAtLeast("2.13.0") == nil { | |||||
if DefaultFeatures().CheckVersionAtLeast("2.13.0") { | |||||
cmd.AddArguments("--exclude", "refs/tags/*") | cmd.AddArguments("--exclude", "refs/tags/*") | ||||
} | } | ||||
cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) | cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) |
"github.com/hashicorp/go-version" | "github.com/hashicorp/go-version" | ||||
) | ) | ||||
// RequiredVersion is the minimum Git version required | |||||
const RequiredVersion = "2.0.0" | |||||
const RequiredVersion = "2.0.0" // the minimum Git version required | |||||
var ( | |||||
// GitExecutable is the command name of git | |||||
// Could be updated to an absolute path while initialization | |||||
GitExecutable = "git" | |||||
// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx | |||||
DefaultContext context.Context | |||||
type Features struct { | |||||
gitVersion *version.Version | |||||
DefaultFeatures struct { | |||||
GitVersion *version.Version | |||||
UsingGogit bool | |||||
SupportProcReceive bool // >= 2.29 | |||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ | |||||
SupportedObjectFormats []ObjectFormat // sha1, sha256 | |||||
} | |||||
SupportProcReceive bool // >= 2.29 | |||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ | |||||
} | |||||
var ( | |||||
GitExecutable = "git" // the command name of git, will be updated to an absolute path during initialization | |||||
DefaultContext context.Context // the default context to run git commands in, must be initialized by git.InitXxx | |||||
defaultFeatures *Features | |||||
) | ) | ||||
// loadGitVersion tries to get the current git version and stores it into a global variable | |||||
func loadGitVersion() error { | |||||
// doesn't need RWMutex because it's executed by Init() | |||||
if DefaultFeatures.GitVersion != nil { | |||||
return nil | |||||
func (f *Features) CheckVersionAtLeast(atLeast string) bool { | |||||
return f.gitVersion.Compare(version.Must(version.NewVersion(atLeast))) >= 0 | |||||
} | |||||
// VersionInfo returns git version information | |||||
func (f *Features) VersionInfo() string { | |||||
return f.gitVersion.Original() | |||||
} | |||||
func DefaultFeatures() *Features { | |||||
if defaultFeatures == nil { | |||||
if !setting.IsProd || setting.IsInTesting { | |||||
log.Warn("git.DefaultFeatures is called before git.InitXxx, initializing with default values") | |||||
} | |||||
if err := InitSimple(context.Background()); err != nil { | |||||
log.Fatal("git.InitSimple failed: %v", err) | |||||
} | |||||
} | } | ||||
return defaultFeatures | |||||
} | |||||
func loadGitVersionFeatures() (*Features, error) { | |||||
stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil) | stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil) | ||||
if runErr != nil { | if runErr != nil { | ||||
return runErr | |||||
return nil, runErr | |||||
} | } | ||||
ver, err := parseGitVersionLine(strings.TrimSpace(stdout)) | ver, err := parseGitVersionLine(strings.TrimSpace(stdout)) | ||||
if err == nil { | |||||
DefaultFeatures.GitVersion = ver | |||||
if err != nil { | |||||
return nil, err | |||||
} | } | ||||
return err | |||||
features := &Features{gitVersion: ver, UsingGogit: isGogit} | |||||
features.SupportProcReceive = features.CheckVersionAtLeast("2.29") | |||||
features.SupportHashSha256 = features.CheckVersionAtLeast("2.42") && !isGogit | |||||
features.SupportedObjectFormats = []ObjectFormat{Sha1ObjectFormat} | |||||
if features.SupportHashSha256 { | |||||
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat) | |||||
} | |||||
return features, nil | |||||
} | } | ||||
func parseGitVersionLine(s string) (*version.Version, error) { | func parseGitVersionLine(s string) (*version.Version, error) { | ||||
return fmt.Errorf("git not found: %w", err) | return fmt.Errorf("git not found: %w", err) | ||||
} | } | ||||
GitExecutable = absPath | GitExecutable = absPath | ||||
return nil | |||||
} | |||||
if err = loadGitVersion(); err != nil { | |||||
return fmt.Errorf("unable to load git version: %w", err) | |||||
} | |||||
versionRequired, err := version.NewVersion(RequiredVersion) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if DefaultFeatures.GitVersion.LessThan(versionRequired) { | |||||
func ensureGitVersion() error { | |||||
if !DefaultFeatures().CheckVersionAtLeast(RequiredVersion) { | |||||
moreHint := "get git: https://git-scm.com/download/" | moreHint := "get git: https://git-scm.com/download/" | ||||
if runtime.GOOS == "linux" { | if runtime.GOOS == "linux" { | ||||
// there are a lot of CentOS/RHEL users using old git, so we add a special hint for them | // there are a lot of CentOS/RHEL users using old git, so we add a special hint for them | ||||
if _, err = os.Stat("/etc/redhat-release"); err == nil { | |||||
if _, err := os.Stat("/etc/redhat-release"); err == nil { | |||||
// ius.io is the recommended official(git-scm.com) method to install git | // ius.io is the recommended official(git-scm.com) method to install git | ||||
moreHint = "get git: https://git-scm.com/download/linux and https://ius.io" | moreHint = "get git: https://git-scm.com/download/linux and https://ius.io" | ||||
} | } | ||||
} | } | ||||
return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", DefaultFeatures.GitVersion.Original(), RequiredVersion, moreHint) | |||||
return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", DefaultFeatures().gitVersion.Original(), RequiredVersion, moreHint) | |||||
} | } | ||||
if err = checkGitVersionCompatibility(DefaultFeatures.GitVersion); err != nil { | |||||
return fmt.Errorf("installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git", DefaultFeatures.GitVersion.String(), err) | |||||
} | |||||
return nil | |||||
} | |||||
// VersionInfo returns git version information | |||||
func VersionInfo() string { | |||||
if DefaultFeatures.GitVersion == nil { | |||||
return "(git not found)" | |||||
} | |||||
format := "%s" | |||||
args := []any{DefaultFeatures.GitVersion.Original()} | |||||
// Since git wire protocol has been released from git v2.18 | |||||
if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { | |||||
format += ", Wire Protocol %s Enabled" | |||||
args = append(args, "Version 2") // for focus color | |||||
} | |||||
return fmt.Sprintf(format, args...) | |||||
} | |||||
func checkInit() error { | |||||
if setting.Git.HomePath == "" { | |||||
return errors.New("unable to init Git's HomeDir, incorrect initialization of the setting and git modules") | |||||
} | |||||
if DefaultContext != nil { | |||||
log.Warn("git module has been initialized already, duplicate init may work but it's better to fix it") | |||||
if err := checkGitVersionCompatibility(DefaultFeatures().gitVersion); err != nil { | |||||
return fmt.Errorf("installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git", DefaultFeatures().gitVersion.String(), err) | |||||
} | } | ||||
return nil | return nil | ||||
} | } | ||||
// InitSimple initializes git module with a very simple step, no config changes, no global command arguments. | // InitSimple initializes git module with a very simple step, no config changes, no global command arguments. | ||||
// This method doesn't change anything to filesystem. At the moment, it is only used by some Gitea sub-commands. | // This method doesn't change anything to filesystem. At the moment, it is only used by some Gitea sub-commands. | ||||
func InitSimple(ctx context.Context) error { | func InitSimple(ctx context.Context) error { | ||||
if err := checkInit(); err != nil { | |||||
return err | |||||
if setting.Git.HomePath == "" { | |||||
return errors.New("unable to init Git's HomeDir, incorrect initialization of the setting and git modules") | |||||
} | |||||
if DefaultContext != nil && (!setting.IsProd || setting.IsInTesting) { | |||||
log.Warn("git module has been initialized already, duplicate init may work but it's better to fix it") | |||||
} | } | ||||
DefaultContext = ctx | DefaultContext = ctx | ||||
defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second | defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second | ||||
} | } | ||||
return SetExecutablePath(setting.Git.Path) | |||||
} | |||||
if err := SetExecutablePath(setting.Git.Path); err != nil { | |||||
return err | |||||
} | |||||
// InitFull initializes git module with version check and change global variables, sync gitconfig. | |||||
// It should only be called once at the beginning of the program initialization (TestMain/GlobalInitInstalled) as this code makes unsynchronized changes to variables. | |||||
func InitFull(ctx context.Context) (err error) { | |||||
if err = InitSimple(ctx); err != nil { | |||||
var err error | |||||
defaultFeatures, err = loadGitVersionFeatures() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if err = ensureGitVersion(); err != nil { | |||||
return err | return err | ||||
} | } | ||||
if _, ok := os.LookupEnv("GNUPGHOME"); !ok { | if _, ok := os.LookupEnv("GNUPGHOME"); !ok { | ||||
_ = os.Setenv("GNUPGHOME", filepath.Join(HomeDir(), ".gnupg")) | _ = os.Setenv("GNUPGHOME", filepath.Join(HomeDir(), ".gnupg")) | ||||
} | } | ||||
return nil | |||||
} | |||||
// InitFull initializes git module with version check and change global variables, sync gitconfig. | |||||
// It should only be called once at the beginning of the program initialization (TestMain/GlobalInitInstalled) as this code makes unsynchronized changes to variables. | |||||
func InitFull(ctx context.Context) (err error) { | |||||
if err = InitSimple(ctx); err != nil { | |||||
return err | |||||
} | |||||
// Since git wire protocol has been released from git v2.18 | // Since git wire protocol has been released from git v2.18 | ||||
if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { | |||||
if setting.Git.EnableAutoGitWireProtocol && DefaultFeatures().CheckVersionAtLeast("2.18") { | |||||
globalCommandArgs = append(globalCommandArgs, "-c", "protocol.version=2") | globalCommandArgs = append(globalCommandArgs, "-c", "protocol.version=2") | ||||
} | } | ||||
// Explicitly disable credential helper, otherwise Git credentials might leak | // Explicitly disable credential helper, otherwise Git credentials might leak | ||||
if CheckGitVersionAtLeast("2.9") == nil { | |||||
if DefaultFeatures().CheckVersionAtLeast("2.9") { | |||||
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=") | globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=") | ||||
} | } | ||||
DefaultFeatures.SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil | |||||
DefaultFeatures.SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit | |||||
if DefaultFeatures.SupportHashSha256 { | |||||
SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat) | |||||
} else { | |||||
log.Warn("sha256 hash support is disabled - requires Git >= 2.42. Gogit is currently unsupported") | |||||
} | |||||
if setting.LFS.StartServer { | if setting.LFS.StartServer { | ||||
if CheckGitVersionAtLeast("2.1.2") != nil { | |||||
if !DefaultFeatures().CheckVersionAtLeast("2.1.2") { | |||||
return errors.New("LFS server support requires Git >= 2.1.2") | return errors.New("LFS server support requires Git >= 2.1.2") | ||||
} | } | ||||
globalCommandArgs = append(globalCommandArgs, "-c", "filter.lfs.required=", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=") | globalCommandArgs = append(globalCommandArgs, "-c", "filter.lfs.required=", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=") | ||||
return err | return err | ||||
} | } | ||||
if CheckGitVersionAtLeast("2.10") == nil { | |||||
if DefaultFeatures().CheckVersionAtLeast("2.10") { | |||||
if err := configSet("receive.advertisePushOptions", "true"); err != nil { | if err := configSet("receive.advertisePushOptions", "true"); err != nil { | ||||
return err | return err | ||||
} | } | ||||
} | } | ||||
if CheckGitVersionAtLeast("2.18") == nil { | |||||
if DefaultFeatures().CheckVersionAtLeast("2.18") { | |||||
if err := configSet("core.commitGraph", "true"); err != nil { | if err := configSet("core.commitGraph", "true"); err != nil { | ||||
return err | return err | ||||
} | } | ||||
} | } | ||||
} | } | ||||
if DefaultFeatures.SupportProcReceive { | |||||
if DefaultFeatures().SupportProcReceive { | |||||
// set support for AGit flow | // set support for AGit flow | ||||
if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil { | if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil { | ||||
return err | return err | ||||
} | } | ||||
// By default partial clones are disabled, enable them from git v2.22 | // By default partial clones are disabled, enable them from git v2.22 | ||||
if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil { | |||||
if !setting.Git.DisablePartialClone && DefaultFeatures().CheckVersionAtLeast("2.22") { | |||||
if err = configSet("uploadpack.allowfilter", "true"); err != nil { | if err = configSet("uploadpack.allowfilter", "true"); err != nil { | ||||
return err | return err | ||||
} | } | ||||
return err | return err | ||||
} | } | ||||
// CheckGitVersionAtLeast check git version is at least the constraint version | |||||
func CheckGitVersionAtLeast(atLeast string) error { | |||||
if DefaultFeatures.GitVersion == nil { | |||||
panic("git module is not initialized") // it shouldn't happen | |||||
} | |||||
atLeastVersion, err := version.NewVersion(atLeast) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if DefaultFeatures.GitVersion.Compare(atLeastVersion) < 0 { | |||||
return fmt.Errorf("installed git binary version %s is not at least %s", DefaultFeatures.GitVersion.Original(), atLeast) | |||||
} | |||||
return nil | |||||
} | |||||
func checkGitVersionCompatibility(gitVer *version.Version) error { | func checkGitVersionCompatibility(gitVer *version.Version) error { | ||||
badVersions := []struct { | badVersions := []struct { | ||||
Version *version.Version | Version *version.Version |
Sha256ObjectFormat ObjectFormat = Sha256ObjectFormatImpl{} | Sha256ObjectFormat ObjectFormat = Sha256ObjectFormatImpl{} | ||||
) | ) | ||||
var SupportedObjectFormats = []ObjectFormat{ | |||||
Sha1ObjectFormat, | |||||
} | |||||
func ObjectFormatFromName(name string) ObjectFormat { | func ObjectFormatFromName(name string) ObjectFormat { | ||||
for _, objectFormat := range SupportedObjectFormats { | |||||
for _, objectFormat := range DefaultFeatures().SupportedObjectFormats { | |||||
if name == objectFormat.Name() { | if name == objectFormat.Name() { | ||||
return objectFormat | return objectFormat | ||||
} | } |
func NewIDFromString(hexHash string) (ObjectID, error) { | func NewIDFromString(hexHash string) (ObjectID, error) { | ||||
var theObjectFormat ObjectFormat | var theObjectFormat ObjectFormat | ||||
for _, objectFormat := range SupportedObjectFormats { | |||||
for _, objectFormat := range DefaultFeatures().SupportedObjectFormats { | |||||
if len(hexHash) == objectFormat.FullLength() { | if len(hexHash) == objectFormat.FullLength() { | ||||
theObjectFormat = objectFormat | theObjectFormat = objectFormat | ||||
break | break |
// GetRemoteAddress returns remote url of git repository in the repoPath with special remote name | // GetRemoteAddress returns remote url of git repository in the repoPath with special remote name | ||||
func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) { | func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) { | ||||
var cmd *Command | var cmd *Command | ||||
if CheckGitVersionAtLeast("2.7") == nil { | |||||
if DefaultFeatures().CheckVersionAtLeast("2.7") { | |||||
cmd = NewCommand(ctx, "remote", "get-url").AddDynamicArguments(remoteName) | cmd = NewCommand(ctx, "remote", "get-url").AddDynamicArguments(remoteName) | ||||
} else { | } else { | ||||
cmd = NewCommand(ctx, "config", "--get").AddDynamicArguments("remote." + remoteName + ".url") | cmd = NewCommand(ctx, "config", "--get").AddDynamicArguments("remote." + remoteName + ".url") |
import ( | import ( | ||||
"bytes" | "bytes" | ||||
"context" | "context" | ||||
"errors" | |||||
"fmt" | "fmt" | ||||
"io" | "io" | ||||
"net/url" | "net/url" | ||||
return err == nil | return err == nil | ||||
} | } | ||||
// GetObjectFormatOfRepo returns the hash type of repository at a given path | |||||
func GetObjectFormatOfRepo(ctx context.Context, repoPath string) (ObjectFormat, error) { | |||||
var stdout, stderr strings.Builder | |||||
err := NewCommand(ctx, "hash-object", "--stdin").Run(&RunOpts{ | |||||
Dir: repoPath, | |||||
Stdout: &stdout, | |||||
Stderr: &stderr, | |||||
Stdin: &strings.Reader{}, | |||||
}) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if stderr.Len() > 0 { | |||||
return nil, errors.New(stderr.String()) | |||||
} | |||||
h, err := NewIDFromString(strings.TrimRight(stdout.String(), "\n")) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return h.Type(), nil | |||||
} | |||||
// InitRepository initializes a new Git repository. | // InitRepository initializes a new Git repository. | ||||
func InitRepository(ctx context.Context, repoPath string, bare bool, objectFormatName string) error { | func InitRepository(ctx context.Context, repoPath string, bare bool, objectFormatName string) error { | ||||
err := os.MkdirAll(repoPath, os.ModePerm) | err := os.MkdirAll(repoPath, os.ModePerm) | ||||
if !IsValidObjectFormat(objectFormatName) { | if !IsValidObjectFormat(objectFormatName) { | ||||
return fmt.Errorf("invalid object format: %s", objectFormatName) | return fmt.Errorf("invalid object format: %s", objectFormatName) | ||||
} | } | ||||
if DefaultFeatures.SupportHashSha256 { | |||||
if DefaultFeatures().SupportHashSha256 { | |||||
cmd.AddOptionValues("--object-format", objectFormatName) | cmd.AddOptionValues("--object-format", objectFormatName) | ||||
} | } | ||||
// Copyright 2021 The Gitea Authors. All rights reserved. | |||||
// SPDX-License-Identifier: MIT | |||||
package git | |||||
var isGogit bool |
"github.com/go-git/go-git/v5/storage/filesystem" | "github.com/go-git/go-git/v5/storage/filesystem" | ||||
) | ) | ||||
func init() { | |||||
isGogit = true | |||||
} | |||||
const isGogit = true | |||||
// Repository represents a Git repository. | // Repository represents a Git repository. | ||||
type Repository struct { | type Repository struct { |
"code.gitea.io/gitea/modules/util" | "code.gitea.io/gitea/modules/util" | ||||
) | ) | ||||
func init() { | |||||
isGogit = false | |||||
} | |||||
const isGogit = false | |||||
// Repository represents a Git repository. | // Repository represents a Git repository. | ||||
type Repository struct { | type Repository struct { |
} | } | ||||
func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) { | func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) { | ||||
if CheckGitVersionAtLeast("2.7.0") == nil { | |||||
if DefaultFeatures().CheckVersionAtLeast("2.7.0") { | |||||
stdout, _, err := NewCommand(repo.Ctx, "for-each-ref", "--format=%(refname:strip=2)"). | stdout, _, err := NewCommand(repo.Ctx, "for-each-ref", "--format=%(refname:strip=2)"). | ||||
AddOptionFormat("--count=%d", limit). | AddOptionFormat("--count=%d", limit). | ||||
AddOptionValues("--contains", commit.ID.String(), BranchPrefix). | AddOptionValues("--contains", commit.ID.String(), BranchPrefix). |
// WriteCommitGraph write commit graph to speed up repo access | // WriteCommitGraph write commit graph to speed up repo access | ||||
// this requires git v2.18 to be installed | // this requires git v2.18 to be installed | ||||
func WriteCommitGraph(ctx context.Context, repoPath string) error { | func WriteCommitGraph(ctx context.Context, repoPath string) error { | ||||
if CheckGitVersionAtLeast("2.18") == nil { | |||||
if DefaultFeatures().CheckVersionAtLeast("2.18") { | |||||
if _, _, err := NewCommand(ctx, "commit-graph", "write").RunStdString(&RunOpts{Dir: repoPath}); err != nil { | if _, _, err := NewCommand(ctx, "commit-graph", "write").RunStdString(&RunOpts{Dir: repoPath}); err != nil { | ||||
return fmt.Errorf("unable to write commit-graph for '%s' : %w", repoPath, err) | return fmt.Errorf("unable to write commit-graph for '%s' : %w", repoPath, err) | ||||
} | } |
go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg) | go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg) | ||||
// 1. Run batch-check on all objects in the repository | // 1. Run batch-check on all objects in the repository | ||||
if git.CheckGitVersionAtLeast("2.6.0") != nil { | |||||
if !git.DefaultFeatures().CheckVersionAtLeast("2.6.0") { | |||||
revListReader, revListWriter := io.Pipe() | revListReader, revListWriter := io.Pipe() | ||||
shasToCheckReader, shasToCheckWriter := io.Pipe() | shasToCheckReader, shasToCheckWriter := io.Pipe() | ||||
wg.Add(2) | wg.Add(2) |
import ( | import ( | ||||
"context" | "context" | ||||
"fmt" | |||||
"code.gitea.io/gitea/models/db" | "code.gitea.io/gitea/models/db" | ||||
git_model "code.gitea.io/gitea/models/git" | git_model "code.gitea.io/gitea/models/git" | ||||
} | } | ||||
func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) { | func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) { | ||||
objFmt, err := gitRepo.GetObjectFormat() | |||||
if err != nil { | |||||
return 0, fmt.Errorf("GetObjectFormat: %w", err) | |||||
} | |||||
_, err = db.GetEngine(ctx).ID(repo.ID).Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()}) | |||||
if err != nil { | |||||
return 0, fmt.Errorf("UpdateRepository: %w", err) | |||||
} | |||||
allBranches := container.Set[string]{} | allBranches := container.Set[string]{} | ||||
{ | { | ||||
branches, _, err := gitRepo.GetBranchNames(0, 0) | branches, _, err := gitRepo.GetBranchNames(0, 0) |
// Copyright 2024 The Gitea Authors. All rights reserved. | |||||
// SPDX-License-Identifier: MIT | |||||
package repository | |||||
import ( | |||||
"testing" | |||||
"code.gitea.io/gitea/models/db" | |||||
git_model "code.gitea.io/gitea/models/git" | |||||
repo_model "code.gitea.io/gitea/models/repo" | |||||
"code.gitea.io/gitea/models/unittest" | |||||
"github.com/stretchr/testify/assert" | |||||
) | |||||
func TestSyncRepoBranches(t *testing.T) { | |||||
assert.NoError(t, unittest.PrepareTestDatabase()) | |||||
_, err := db.GetEngine(db.DefaultContext).ID(1).Update(&repo_model.Repository{ObjectFormatName: "bad-fmt"}) | |||||
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &git_model.Branch{})) | |||||
assert.NoError(t, err) | |||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) | |||||
assert.Equal(t, "bad-fmt", repo.ObjectFormatName) | |||||
_, err = SyncRepoBranches(db.DefaultContext, 1, 0) | |||||
assert.NoError(t, err) | |||||
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) | |||||
assert.Equal(t, "sha1", repo.ObjectFormatName) | |||||
branch, err := git_model.GetBranch(db.DefaultContext, 1, "master") | |||||
assert.NoError(t, err) | |||||
assert.EqualValues(t, "master", branch.Name) | |||||
} |
"code.gitea.io/gitea/modules/system" | "code.gitea.io/gitea/modules/system" | ||||
"code.gitea.io/gitea/modules/templates" | "code.gitea.io/gitea/modules/templates" | ||||
"code.gitea.io/gitea/modules/translation" | "code.gitea.io/gitea/modules/translation" | ||||
"code.gitea.io/gitea/modules/util" | |||||
"code.gitea.io/gitea/modules/web" | "code.gitea.io/gitea/modules/web" | ||||
"code.gitea.io/gitea/modules/web/routing" | "code.gitea.io/gitea/modules/web/routing" | ||||
actions_router "code.gitea.io/gitea/routers/api/actions" | actions_router "code.gitea.io/gitea/routers/api/actions" | ||||
// InitWebInstalled is for global installed configuration. | // InitWebInstalled is for global installed configuration. | ||||
func InitWebInstalled(ctx context.Context) { | func InitWebInstalled(ctx context.Context) { | ||||
mustInitCtx(ctx, git.InitFull) | mustInitCtx(ctx, git.InitFull) | ||||
log.Info("Git version: %s (home: %s)", git.VersionInfo(), git.HomeDir()) | |||||
log.Info("Git version: %s (home: %s)", git.DefaultFeatures().VersionInfo(), git.HomeDir()) | |||||
if !git.DefaultFeatures().SupportHashSha256 { | |||||
log.Warn("sha256 hash support is disabled - requires Git >= 2.42." + util.Iif(git.DefaultFeatures().UsingGogit, " Gogit is currently unsupported.", "")) | |||||
} | |||||
// Setup i18n | // Setup i18n | ||||
translation.InitLocales(ctx) | translation.InitLocales(ctx) |
preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName) | preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName) | ||||
case refFullName.IsTag(): | case refFullName.IsTag(): | ||||
preReceiveTag(ourCtx, refFullName) | preReceiveTag(ourCtx, refFullName) | ||||
case git.DefaultFeatures.SupportProcReceive && refFullName.IsFor(): | |||||
case git.DefaultFeatures().SupportProcReceive && refFullName.IsFor(): | |||||
preReceiveFor(ourCtx, refFullName) | preReceiveFor(ourCtx, refFullName) | ||||
default: | default: | ||||
ourCtx.AssertCanWriteCode() | ourCtx.AssertCanWriteCode() |
// HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present | // HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present | ||||
func HookProcReceive(ctx *gitea_context.PrivateContext) { | func HookProcReceive(ctx *gitea_context.PrivateContext) { | ||||
opts := web.GetForm(ctx).(*private.HookOptions) | opts := web.GetForm(ctx).(*private.HookOptions) | ||||
if !git.DefaultFeatures.SupportProcReceive { | |||||
if !git.DefaultFeatures().SupportProcReceive { | |||||
ctx.Status(http.StatusNotFound) | ctx.Status(http.StatusNotFound) | ||||
return | return | ||||
} | } |
} | } | ||||
} else { | } else { | ||||
// Because of the special ref "refs/for" we will need to delay write permission check | // Because of the special ref "refs/for" we will need to delay write permission check | ||||
if git.DefaultFeatures.SupportProcReceive && unitType == unit.TypeCode { | |||||
if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode { | |||||
mode = perm.AccessModeRead | mode = perm.AccessModeRead | ||||
} | } | ||||
ctx.Data["OfflineMode"] = setting.OfflineMode | ctx.Data["OfflineMode"] = setting.OfflineMode | ||||
ctx.Data["RunUser"] = setting.RunUser | ctx.Data["RunUser"] = setting.RunUser | ||||
ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode) | ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode) | ||||
ctx.Data["GitVersion"] = git.VersionInfo() | |||||
ctx.Data["GitVersion"] = git.DefaultFeatures().VersionInfo() | |||||
ctx.Data["AppDataPath"] = setting.AppDataPath | ctx.Data["AppDataPath"] = setting.AppDataPath | ||||
ctx.Data["RepoRootPath"] = setting.RepoRootPath | ctx.Data["RepoRootPath"] = setting.RepoRootPath |
) | ) | ||||
func SSHInfo(rw http.ResponseWriter, req *http.Request) { | func SSHInfo(rw http.ResponseWriter, req *http.Request) { | ||||
if !git.DefaultFeatures.SupportProcReceive { | |||||
if !git.DefaultFeatures().SupportProcReceive { | |||||
rw.WriteHeader(http.StatusNotFound) | rw.WriteHeader(http.StatusNotFound) | ||||
return | return | ||||
} | } |
if repoExist { | if repoExist { | ||||
// Because of special ref "refs/for" .. , need delay write permission check | // Because of special ref "refs/for" .. , need delay write permission check | ||||
if git.DefaultFeatures.SupportProcReceive { | |||||
if git.DefaultFeatures().SupportProcReceive { | |||||
accessMode = perm.AccessModeRead | accessMode = perm.AccessModeRead | ||||
} | } | ||||
ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() | ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() | ||||
ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() | ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() | ||||
ctx.Data["SupportedObjectFormats"] = git.SupportedObjectFormats | |||||
ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats | |||||
ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat | ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat | ||||
ctx.HTML(http.StatusOK, tplCreate) | ctx.HTML(http.StatusOK, tplCreate) |
// so if we are using at least this version of git we don't have to tell ParsePatch to do | // so if we are using at least this version of git we don't have to tell ParsePatch to do | ||||
// the skipping for us | // the skipping for us | ||||
parsePatchSkipToFile := opts.SkipTo | parsePatchSkipToFile := opts.SkipTo | ||||
if opts.SkipTo != "" && git.CheckGitVersionAtLeast("2.31") == nil { | |||||
if opts.SkipTo != "" && git.DefaultFeatures().CheckVersionAtLeast("2.31") { | |||||
cmdDiff.AddOptionFormat("--skip-to=%s", opts.SkipTo) | cmdDiff.AddOptionFormat("--skip-to=%s", opts.SkipTo) | ||||
parsePatchSkipToFile = "" | parsePatchSkipToFile = "" | ||||
} | } |
cmdApply.AddArguments("--ignore-whitespace") | cmdApply.AddArguments("--ignore-whitespace") | ||||
} | } | ||||
is3way := false | is3way := false | ||||
if git.CheckGitVersionAtLeast("2.32.0") == nil { | |||||
if git.DefaultFeatures().CheckVersionAtLeast("2.32.0") { | |||||
cmdApply.AddArguments("--3way") | cmdApply.AddArguments("--3way") | ||||
is3way = true | is3way = true | ||||
} | } |
baseBranch := "base" | baseBranch := "base" | ||||
fetchArgs := git.TrustedCmdArgs{"--no-tags"} | fetchArgs := git.TrustedCmdArgs{"--no-tags"} | ||||
if git.CheckGitVersionAtLeast("2.25.0") == nil { | |||||
if git.DefaultFeatures().CheckVersionAtLeast("2.25.0") { | |||||
// Writing the commit graph can be slow and is not needed here | // Writing the commit graph can be slow and is not needed here | ||||
fetchArgs = append(fetchArgs, "--no-write-commit-graph") | fetchArgs = append(fetchArgs, "--no-write-commit-graph") | ||||
} | } |
} | } | ||||
defer gitRepo.Close() | defer gitRepo.Close() | ||||
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, 0); err != nil { | |||||
return fmt.Errorf("SyncRepoBranches: %w", err) | |||||
} | |||||
if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { | if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { | ||||
return fmt.Errorf("SyncReleasesWithTags: %w", err) | return fmt.Errorf("SyncReleasesWithTags: %w", err) | ||||
} | } |
stderr := &strings.Builder{} | stderr := &strings.Builder{} | ||||
cmdApply := git.NewCommand(ctx, "apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary") | cmdApply := git.NewCommand(ctx, "apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary") | ||||
if git.CheckGitVersionAtLeast("2.32") == nil { | |||||
if git.DefaultFeatures().CheckVersionAtLeast("2.32") { | |||||
cmdApply.AddArguments("-3") | cmdApply.AddArguments("-3") | ||||
} | } | ||||
{{template "base/head" .}} | |||||
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}"> | |||||
<div class="page-content devtest ui container"> | |||||
<div> | |||||
<h2>Dropdown</h2> | |||||
<div> | |||||
<div class="ui dropdown tw-border tw-border-red tw-border-dashed" data-tooltip-content="border for demo purpose only"> | |||||
<span class="text">search-input & flex-item in menu</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
<div class="menu flex-items-menu"> | |||||
<div class="ui icon search input"><i class="icon">{{svg "octicon-search"}}</i><input type="text" value="search input in menu"></div> | |||||
<div class="item"><input type="radio">item</div> | |||||
<div class="item"><input type="radio">item</div> | |||||
</div> | |||||
</div> | |||||
<div class="ui search selection dropdown"> | |||||
<span class="text">search ...</span> | |||||
<input name="value" class="search"> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
{{svg "octicon-x" 14 "remove icon"}} | |||||
<div class="menu"> | |||||
<div class="item">item</div> | |||||
</div> | |||||
</div> | |||||
<div class="ui multiple selection dropdown"> | |||||
<input class="hidden" value="1"> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
{{svg "octicon-x" 14 "remove icon"}} | |||||
<div class="default text">empty multiple dropdown</div> | |||||
<div class="menu"> | |||||
<div class="item">item</div> | |||||
</div> | |||||
</div> | |||||
<div class="ui multiple clearable search selection dropdown"> | |||||
<input type="hidden" value="1"> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
{{svg "octicon-x" 14 "remove icon"}} | |||||
<div class="default text">clearable search dropdown</div> | |||||
<div class="menu"> | |||||
<div class="item" data-value="1">item</div> | |||||
</div> | |||||
</div> | |||||
<div class="ui buttons"> | |||||
<button class="ui button">Button with Dropdown</button> | |||||
<div class="ui dropdown button icon"> | |||||
{{svg "octicon-triangle-down"}} | |||||
<div class="menu"> | |||||
<div class="item">item</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<h2>Selection</h2> | |||||
<div> | |||||
{{/* the "selection" class is optional, it will be added by JS automatically */}} | |||||
<select class="ui dropdown selection ellipsis-items-nowrap"> | |||||
<option>a</option> | |||||
<option>abcdefuvwxyz</option> | |||||
<option>loooooooooooooooooooooooooooooooooooooooooooooooooooooooooong</option> | |||||
</select> | |||||
<select class="ui dropdown ellipsis-items-nowrap tw-max-w-[8em]"> | |||||
<option>loooooooooooooooooooooooooooooooooooooooooooooooooooooooooong</option> | |||||
<option>abcdefuvwxyz</option> | |||||
<option>a</option> | |||||
</select> | |||||
</div> | |||||
<h2>Dropdown Button (demo only without menu)</h2> | |||||
<div> | |||||
<div class="ui dropdown mini button"> | |||||
<span class="text">mini dropdown</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
<div class="ui dropdown tiny button"> | |||||
<span class="text">tiny dropdown</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
<div class="ui button dropdown"> | |||||
<span class="text">button dropdown</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
</div> | |||||
<div> | |||||
<div class="ui dropdown mini compact button"> | |||||
<span class="text">mini compact</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
<div class="ui dropdown tiny compact button"> | |||||
<span class="text">tiny compact</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
<div class="ui button compact dropdown"> | |||||
<span class="text">button compact</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
</div> | |||||
<div> | |||||
<hr> | |||||
<div class="ui tiny button">Other button align with ...</div> | |||||
<div class="ui dropdown tiny button"> | |||||
<span class="text">... Dropdown Button</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
{{template "base/footer" .}} |
<input type="text" placeholder="place holder"> | <input type="text" placeholder="place holder"> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<h2>Dropdown with SVG</h2> | |||||
<div> | |||||
<div class="ui dropdown tw-border tw-border-red tw-border-dashed" data-tooltip-content="border for demo purpose only"> | |||||
<span class="text">search-input & flex-item in menu</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
<div class="menu flex-items-menu"> | |||||
<div class="ui icon search input"><i class="icon">{{svg "octicon-search"}}</i><input type="text" value="search input in menu"></div> | |||||
<div class="item"><input type="radio">item</div> | |||||
<div class="item"><input type="radio">item</div> | |||||
</div> | |||||
</div> | |||||
<div class="ui search selection dropdown"> | |||||
<span class="text">search ...</span> | |||||
<input name="value" class="search"> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
{{svg "octicon-x" 14 "remove icon"}} | |||||
<div class="menu"> | |||||
<div class="item">item</div> | |||||
</div> | |||||
</div> | |||||
<div class="ui multiple selection dropdown"> | |||||
<input class="hidden" value="1"> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
{{svg "octicon-x" 14 "remove icon"}} | |||||
<div class="default text">empty multiple dropdown</div> | |||||
<div class="menu"> | |||||
<div class="item">item</div> | |||||
</div> | |||||
</div> | |||||
<div class="ui multiple clearable search selection dropdown"> | |||||
<input type="hidden" value="1"> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
{{svg "octicon-x" 14 "remove icon"}} | |||||
<div class="default text">clearable search dropdown</div> | |||||
<div class="menu"> | |||||
<div class="item" data-value="1">item</div> | |||||
</div> | |||||
</div> | |||||
<div class="ui buttons"> | |||||
<button class="ui button">Button with Dropdown</button> | |||||
<div class="ui dropdown button icon"> | |||||
{{svg "octicon-triangle-down"}} | |||||
<div class="menu"> | |||||
<div class="item">item</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div> | |||||
<div class="ui dropdown mini button"> | |||||
<span class="text">mini dropdown</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
<div class="ui dropdown tiny button"> | |||||
<span class="text">tiny dropdown</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
<div class="ui button dropdown"> | |||||
<span class="text">button dropdown</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
</div> | |||||
<div> | |||||
<div class="ui dropdown mini compact button"> | |||||
<span class="text">mini compact</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
<div class="ui dropdown tiny compact button"> | |||||
<span class="text">tiny compact</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
<div class="ui button compact dropdown"> | |||||
<span class="text">button compact</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
</div> | |||||
<div> | |||||
<hr> | |||||
<div class="ui tiny button">Button align with ...</div> | |||||
<div class="ui dropdown tiny button"> | |||||
<span class="text">... Dropdown Button</span> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
</div> | |||||
</div> | |||||
</div> | </div> | ||||
<div> | <div> |
<!-- Optional Settings --> | <!-- Optional Settings --> | ||||
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.optional_title"}}</h4> | <h4 class="ui dividing header">{{ctx.Locale.Tr "install.optional_title"}}</h4> | ||||
<!-- Email --> | |||||
<details class="optional field"> | |||||
<summary class="right-content tw-py-2{{if .Err_SMTP}} text red{{end}}"> | |||||
{{ctx.Locale.Tr "install.email_title"}} | |||||
</summary> | |||||
<div class="inline field"> | |||||
<label for="smtp_addr">{{ctx.Locale.Tr "install.smtp_addr"}}</label> | |||||
<input id="smtp_addr" name="smtp_addr" value="{{.smtp_addr}}"> | |||||
</div> | |||||
<div class="inline field"> | |||||
<label for="smtp_port">{{ctx.Locale.Tr "install.smtp_port"}}</label> | |||||
<input id="smtp_port" name="smtp_port" value="{{.smtp_port}}"> | |||||
</div> | |||||
<div class="inline field {{if .Err_SMTPFrom}}error{{end}}"> | |||||
<label for="smtp_from">{{ctx.Locale.Tr "install.smtp_from"}}</label> | |||||
<input id="smtp_from" name="smtp_from" value="{{.smtp_from}}"> | |||||
<span class="help">{{ctx.Locale.TrString "install.smtp_from_helper"}}{{/* it contains lt/gt chars*/}}</span> | |||||
</div> | |||||
<div class="inline field {{if .Err_SMTPUser}}error{{end}}"> | |||||
<label for="smtp_user">{{ctx.Locale.Tr "install.mailer_user"}}</label> | |||||
<input id="smtp_user" name="smtp_user" value="{{.smtp_user}}"> | |||||
</div> | |||||
<div class="inline field"> | |||||
<label for="smtp_passwd">{{ctx.Locale.Tr "install.mailer_password"}}</label> | |||||
<input id="smtp_passwd" name="smtp_passwd" type="password" value="{{.smtp_passwd}}"> | |||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label>{{ctx.Locale.Tr "install.register_confirm"}}</label> | |||||
<input name="register_confirm" type="checkbox" {{if .register_confirm}}checked{{end}}> | |||||
<div> | |||||
<!-- Email --> | |||||
<details class="optional field"> | |||||
<summary class="right-content tw-py-2{{if .Err_SMTP}} text red{{end}}"> | |||||
{{ctx.Locale.Tr "install.email_title"}} | |||||
</summary> | |||||
<div class="inline field"> | |||||
<label for="smtp_addr">{{ctx.Locale.Tr "install.smtp_addr"}}</label> | |||||
<input id="smtp_addr" name="smtp_addr" value="{{.smtp_addr}}"> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label>{{ctx.Locale.Tr "install.mail_notify"}}</label> | |||||
<input name="mail_notify" type="checkbox" {{if .mail_notify}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<label for="smtp_port">{{ctx.Locale.Tr "install.smtp_port"}}</label> | |||||
<input id="smtp_port" name="smtp_port" value="{{.smtp_port}}"> | |||||
</div> | </div> | ||||
</div> | |||||
</details> | |||||
<div class="inline field {{if .Err_SMTPFrom}}error{{end}}"> | |||||
<label for="smtp_from">{{ctx.Locale.Tr "install.smtp_from"}}</label> | |||||
<input id="smtp_from" name="smtp_from" value="{{.smtp_from}}"> | |||||
<span class="help">{{ctx.Locale.TrString "install.smtp_from_helper"}}{{/* it contains lt/gt chars*/}}</span> | |||||
</div> | |||||
<div class="inline field {{if .Err_SMTPUser}}error{{end}}"> | |||||
<label for="smtp_user">{{ctx.Locale.Tr "install.mailer_user"}}</label> | |||||
<input id="smtp_user" name="smtp_user" value="{{.smtp_user}}"> | |||||
</div> | |||||
<div class="inline field"> | |||||
<label for="smtp_passwd">{{ctx.Locale.Tr "install.mailer_password"}}</label> | |||||
<input id="smtp_passwd" name="smtp_passwd" type="password" value="{{.smtp_passwd}}"> | |||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label>{{ctx.Locale.Tr "install.register_confirm"}}</label> | |||||
<input name="register_confirm" type="checkbox" {{if .register_confirm}}checked{{end}}> | |||||
</div> | |||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label>{{ctx.Locale.Tr "install.mail_notify"}}</label> | |||||
<input name="mail_notify" type="checkbox" {{if .mail_notify}}checked{{end}}> | |||||
</div> | |||||
</div> | |||||
</details> | |||||
<!-- Server and other services --> | |||||
<details class="optional field"> | |||||
<summary class="right-content tw-py-2{{if .Err_Services}} text red{{end}}"> | |||||
{{ctx.Locale.Tr "install.server_service_title"}} | |||||
</summary> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="offline-mode"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.offline_mode_popup"}}">{{ctx.Locale.Tr "install.offline_mode"}}</label> | |||||
<input name="offline_mode" type="checkbox" {{if .offline_mode}}checked{{end}}> | |||||
<!-- Server and other services --> | |||||
<details class="optional field"> | |||||
<summary class="right-content tw-py-2{{if .Err_Services}} text red{{end}}"> | |||||
{{ctx.Locale.Tr "install.server_service_title"}} | |||||
</summary> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="offline-mode"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.offline_mode_popup"}}">{{ctx.Locale.Tr "install.offline_mode"}}</label> | |||||
<input name="offline_mode" type="checkbox" {{if .offline_mode}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="disable-gravatar"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_gravatar_popup"}}">{{ctx.Locale.Tr "install.disable_gravatar"}}</label> | |||||
<input name="disable_gravatar" type="checkbox" {{if .disable_gravatar}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="disable-gravatar"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_gravatar_popup"}}">{{ctx.Locale.Tr "install.disable_gravatar"}}</label> | |||||
<input name="disable_gravatar" type="checkbox" {{if .disable_gravatar}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="federated-avatar-lookup"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.federated_avatar_lookup_popup"}}">{{ctx.Locale.Tr "install.federated_avatar_lookup"}}</label> | |||||
<input name="enable_federated_avatar" type="checkbox" {{if .enable_federated_avatar}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="federated-avatar-lookup"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.federated_avatar_lookup_popup"}}">{{ctx.Locale.Tr "install.federated_avatar_lookup"}}</label> | |||||
<input name="enable_federated_avatar" type="checkbox" {{if .enable_federated_avatar}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="enable-openid-signin"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signin_popup"}}">{{ctx.Locale.Tr "install.openid_signin"}}</label> | |||||
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="enable-openid-signin"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signin_popup"}}">{{ctx.Locale.Tr "install.openid_signin"}}</label> | |||||
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="disable-registration"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_registration_popup"}}">{{ctx.Locale.Tr "install.disable_registration"}}</label> | |||||
<input name="disable_registration" type="checkbox" {{if .disable_registration}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="disable-registration"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_registration_popup"}}">{{ctx.Locale.Tr "install.disable_registration"}}</label> | |||||
<input name="disable_registration" type="checkbox" {{if .disable_registration}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="allow-only-external-registration"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}">{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}</label> | |||||
<input name="allow_only_external_registration" type="checkbox" {{if .allow_only_external_registration}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="allow-only-external-registration"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}">{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}</label> | |||||
<input name="allow_only_external_registration" type="checkbox" {{if .allow_only_external_registration}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="enable-openid-signup"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signup_popup"}}">{{ctx.Locale.Tr "install.openid_signup"}}</label> | |||||
<input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="enable-openid-signup"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signup_popup"}}">{{ctx.Locale.Tr "install.openid_signup"}}</label> | |||||
<input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="enable-captcha"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label> | |||||
<input name="enable_captcha" type="checkbox" {{if .enable_captcha}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox" id="enable-captcha"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label> | |||||
<input name="enable_captcha" type="checkbox" {{if .enable_captcha}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.require_sign_in_view_popup"}}">{{ctx.Locale.Tr "install.require_sign_in_view"}}</label> | |||||
<input name="require_sign_in_view" type="checkbox" {{if .require_sign_in_view}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.require_sign_in_view_popup"}}">{{ctx.Locale.Tr "install.require_sign_in_view"}}</label> | |||||
<input name="require_sign_in_view" type="checkbox" {{if .require_sign_in_view}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_keep_email_private_popup"}}">{{ctx.Locale.Tr "install.default_keep_email_private"}}</label> | |||||
<input name="default_keep_email_private" type="checkbox" {{if .default_keep_email_private}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_keep_email_private_popup"}}">{{ctx.Locale.Tr "install.default_keep_email_private"}}</label> | |||||
<input name="default_keep_email_private" type="checkbox" {{if .default_keep_email_private}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_organization_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_organization"}}</label> | |||||
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_organization_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_organization"}}</label> | |||||
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label> | |||||
<input name="default_enable_timetracking" type="checkbox" {{if .default_enable_timetracking}}checked{{end}}> | |||||
<div class="inline field"> | |||||
<div class="ui checkbox"> | |||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label> | |||||
<input name="default_enable_timetracking" type="checkbox" {{if .default_enable_timetracking}}checked{{end}}> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div class="inline field"> | |||||
<label for="no_reply_address">{{ctx.Locale.Tr "install.no_reply_address"}}</label> | |||||
<input id="_no_reply_address" name="no_reply_address" value="{{.no_reply_address}}"> | |||||
<span class="help">{{ctx.Locale.Tr "install.no_reply_address_helper"}}</span> | |||||
</div> | |||||
<div class="inline field"> | |||||
<label for="password_algorithm">{{ctx.Locale.Tr "install.password_algorithm"}}</label> | |||||
<div class="ui selection dropdown"> | |||||
<input id="password_algorithm" type="hidden" name="password_algorithm" value="{{.password_algorithm}}"> | |||||
<div class="text">{{.password_algorithm}}</div> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
<div class="menu"> | |||||
{{range .PasswordHashAlgorithms}} | |||||
<div class="item" data-value="{{.}}">{{.}}</div> | |||||
{{end}} | |||||
<div class="inline field"> | |||||
<label for="no_reply_address">{{ctx.Locale.Tr "install.no_reply_address"}}</label> | |||||
<input id="_no_reply_address" name="no_reply_address" value="{{.no_reply_address}}"> | |||||
<span class="help">{{ctx.Locale.Tr "install.no_reply_address_helper"}}</span> | |||||
</div> | |||||
<div class="inline field"> | |||||
<label for="password_algorithm">{{ctx.Locale.Tr "install.password_algorithm"}}</label> | |||||
<div class="ui selection dropdown"> | |||||
<input id="password_algorithm" type="hidden" name="password_algorithm" value="{{.password_algorithm}}"> | |||||
<div class="text">{{.password_algorithm}}</div> | |||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||||
<div class="menu"> | |||||
{{range .PasswordHashAlgorithms}} | |||||
<div class="item" data-value="{{.}}">{{.}}</div> | |||||
{{end}} | |||||
</div> | |||||
</div> | </div> | ||||
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span> | |||||
</div> | </div> | ||||
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span> | |||||
</div> | |||||
</details> | |||||
</details> | |||||
<!-- Admin --> | |||||
<details class="optional field"> | |||||
<summary class="right-content tw-py-2{{if .Err_Admin}} text red{{end}}"> | |||||
{{ctx.Locale.Tr "install.admin_title"}} | |||||
</summary> | |||||
<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p> | |||||
<div class="inline field {{if .Err_AdminName}}error{{end}}"> | |||||
<label for="admin_name">{{ctx.Locale.Tr "install.admin_name"}}</label> | |||||
<input id="admin_name" name="admin_name" value="{{.admin_name}}"> | |||||
</div> | |||||
<div class="inline field {{if .Err_AdminEmail}}error{{end}}"> | |||||
<label for="admin_email">{{ctx.Locale.Tr "install.admin_email"}}</label> | |||||
<input id="admin_email" name="admin_email" type="email" value="{{.admin_email}}"> | |||||
</div> | |||||
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}"> | |||||
<label for="admin_passwd">{{ctx.Locale.Tr "install.admin_password"}}</label> | |||||
<input id="admin_passwd" name="admin_passwd" type="password" autocomplete="new-password" value="{{.admin_passwd}}"> | |||||
</div> | |||||
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}"> | |||||
<label for="admin_confirm_passwd">{{ctx.Locale.Tr "install.confirm_password"}}</label> | |||||
<input id="admin_confirm_passwd" name="admin_confirm_passwd" autocomplete="new-password" type="password" value="{{.admin_confirm_passwd}}"> | |||||
</div> | |||||
</details> | |||||
<!-- Admin --> | |||||
<details class="optional field"> | |||||
<summary class="right-content tw-py-2{{if .Err_Admin}} text red{{end}}"> | |||||
{{ctx.Locale.Tr "install.admin_title"}} | |||||
</summary> | |||||
<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p> | |||||
<div class="inline field {{if .Err_AdminName}}error{{end}}"> | |||||
<label for="admin_name">{{ctx.Locale.Tr "install.admin_name"}}</label> | |||||
<input id="admin_name" name="admin_name" value="{{.admin_name}}"> | |||||
</div> | |||||
<div class="inline field {{if .Err_AdminEmail}}error{{end}}"> | |||||
<label for="admin_email">{{ctx.Locale.Tr "install.admin_email"}}</label> | |||||
<input id="admin_email" name="admin_email" type="email" value="{{.admin_email}}"> | |||||
</div> | |||||
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}"> | |||||
<label for="admin_passwd">{{ctx.Locale.Tr "install.admin_password"}}</label> | |||||
<input id="admin_passwd" name="admin_passwd" type="password" autocomplete="new-password" value="{{.admin_passwd}}"> | |||||
</div> | |||||
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}"> | |||||
<label for="admin_confirm_passwd">{{ctx.Locale.Tr "install.confirm_password"}}</label> | |||||
<input id="admin_confirm_passwd" name="admin_confirm_passwd" autocomplete="new-password" type="password" value="{{.admin_confirm_passwd}}"> | |||||
</div> | |||||
</details> | |||||
</div> | |||||
<div class="divider"></div> | |||||
{{if .EnvConfigKeys}} | {{if .EnvConfigKeys}} | ||||
<!-- Environment Config --> | <!-- Environment Config --> | ||||
</div> | </div> | ||||
{{end}} | {{end}} | ||||
<div class="divider"></div> | |||||
<div class="inline field"> | <div class="inline field"> | ||||
<div class="right-content"> | <div class="right-content"> | ||||
These configuration options will be written into: {{.CustomConfFile}} | These configuration options will be written into: {{.CustomConfFile}} | ||||
</div> | </div> | ||||
<div class="right-content tw-mt-2"> | |||||
<div class="tw-mt-4 tw-mb-2 tw-text-center"> | |||||
<button class="ui primary button">{{ctx.Locale.Tr "install.install_btn_confirm"}}</button> | <button class="ui primary button">{{ctx.Locale.Tr "install.install_btn_confirm"}}</button> | ||||
</div> | </div> | ||||
</div> | </div> |
<div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}"> | <div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}"> | ||||
{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}} | {{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}} | ||||
<div class="ui dropdown custom branch-selector-dropdown"> | |||||
<div class="ui dropdown custom branch-selector-dropdown ellipsis-items-nowrap"> | |||||
<div class="ui button branch-dropdown-button"> | <div class="ui button branch-dropdown-button"> | ||||
<span class="flex-text-block gt-ellipsis"> | <span class="flex-text-block gt-ellipsis"> | ||||
{{if .release}} | {{if .release}} |
{{if .IsGenerated}}<div class="fork-flag">{{ctx.Locale.Tr "repo.generated_from"}} <a href="{{(.TemplateRepo ctx).Link}}">{{(.TemplateRepo ctx).FullName}}</a></div>{{end}} | {{if .IsGenerated}}<div class="fork-flag">{{ctx.Locale.Tr "repo.generated_from"}} <a href="{{(.TemplateRepo ctx).Link}}">{{(.TemplateRepo ctx).FullName}}</a></div>{{end}} | ||||
</div> | </div> | ||||
{{end}} | {{end}} | ||||
<overflow-menu class="ui container secondary pointing tabular top attached borderless menu tw-pt-0 tw-my-0"> | |||||
{{if not (or .Repository.IsBeingCreated .Repository.IsBroken)}} | |||||
<div class="overflow-menu-items"> | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}} | |||||
<a class="{{if .PageIsViewCode}}active {{end}}item" href="{{.RepoLink}}{{if and (ne .BranchName .Repository.DefaultBranch) (not $.PageIsWiki)}}/src/{{.BranchNameSubURL}}{{end}}"> | |||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "repo.code"}} | |||||
</a> | |||||
{{end}} | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeIssues}} | |||||
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoLink}}/issues"> | |||||
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues"}} | |||||
{{if .Repository.NumOpenIssues}} | |||||
<span class="ui small label">{{CountFmt .Repository.NumOpenIssues}}</span> | |||||
{{end}} | |||||
<div class="ui container"> | |||||
<overflow-menu class="ui secondary pointing menu"> | |||||
{{if not (or .Repository.IsBeingCreated .Repository.IsBroken)}} | |||||
<div class="overflow-menu-items"> | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}} | |||||
<a class="{{if .PageIsViewCode}}active {{end}}item" href="{{.RepoLink}}{{if and (ne .BranchName .Repository.DefaultBranch) (not $.PageIsWiki)}}/src/{{.BranchNameSubURL}}{{end}}"> | |||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "repo.code"}} | |||||
</a> | </a> | ||||
{{end}} | |||||
{{end}} | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeExternalTracker}} | |||||
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoExternalIssuesLink}}" target="_blank" rel="noopener noreferrer"> | |||||
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.issues"}} | |||||
</a> | |||||
{{end}} | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeIssues}} | |||||
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoLink}}/issues"> | |||||
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues"}} | |||||
{{if .Repository.NumOpenIssues}} | |||||
<span class="ui small label">{{CountFmt .Repository.NumOpenIssues}}</span> | |||||
{{end}} | |||||
</a> | |||||
{{end}} | |||||
{{if and .Repository.CanEnablePulls (.Permission.CanRead ctx.Consts.RepoUnitTypePullRequests)}} | |||||
<a class="{{if .PageIsPullList}}active {{end}}item" href="{{.RepoLink}}/pulls"> | |||||
{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.pulls"}} | |||||
{{if .Repository.NumOpenPulls}} | |||||
<span class="ui small label">{{CountFmt .Repository.NumOpenPulls}}</span> | |||||
{{end}} | |||||
</a> | |||||
{{end}} | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeExternalTracker}} | |||||
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoExternalIssuesLink}}" target="_blank" rel="noopener noreferrer"> | |||||
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.issues"}} | |||||
</a> | |||||
{{end}} | |||||
{{if and .EnableActions (not .UnitActionsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}} | |||||
<a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions"> | |||||
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}} | |||||
{{if .Repository.NumOpenActionRuns}} | |||||
<span class="ui small label">{{CountFmt .Repository.NumOpenActionRuns}}</span> | |||||
{{end}} | |||||
</a> | |||||
{{end}} | |||||
{{if and .Repository.CanEnablePulls (.Permission.CanRead ctx.Consts.RepoUnitTypePullRequests)}} | |||||
<a class="{{if .PageIsPullList}}active {{end}}item" href="{{.RepoLink}}/pulls"> | |||||
{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.pulls"}} | |||||
{{if .Repository.NumOpenPulls}} | |||||
<span class="ui small label">{{CountFmt .Repository.NumOpenPulls}}</span> | |||||
{{end}} | |||||
</a> | |||||
{{end}} | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypePackages}} | |||||
<a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item"> | |||||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} | |||||
</a> | |||||
{{end}} | |||||
{{if and .EnableActions (not .UnitActionsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}} | |||||
<a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions"> | |||||
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}} | |||||
{{if .Repository.NumOpenActionRuns}} | |||||
<span class="ui small label">{{CountFmt .Repository.NumOpenActionRuns}}</span> | |||||
{{end}} | |||||
</a> | |||||
{{end}} | |||||
{{$projectsUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeProjects}} | |||||
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}} | |||||
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item"> | |||||
{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}} | |||||
{{if .Repository.NumOpenProjects}} | |||||
<span class="ui small label">{{CountFmt .Repository.NumOpenProjects}}</span> | |||||
{{end}} | |||||
</a> | |||||
{{end}} | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypePackages}} | |||||
<a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item"> | |||||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} | |||||
</a> | |||||
{{end}} | |||||
{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeReleases) (not .IsEmptyRepo)}} | |||||
<a class="{{if or .PageIsReleaseList .PageIsTagList}}active {{end}}item" href="{{.RepoLink}}/releases"> | |||||
{{svg "octicon-tag"}} {{ctx.Locale.Tr "repo.releases"}} | |||||
{{if .NumReleases}} | |||||
<span class="ui small label">{{CountFmt .NumReleases}}</span> | |||||
{{end}} | |||||
</a> | |||||
{{end}} | |||||
{{$projectsUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeProjects}} | |||||
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}} | |||||
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item"> | |||||
{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}} | |||||
{{if .Repository.NumOpenProjects}} | |||||
<span class="ui small label">{{CountFmt .Repository.NumOpenProjects}}</span> | |||||
{{end}} | |||||
</a> | |||||
{{end}} | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeWiki}} | |||||
<a class="{{if .PageIsWiki}}active {{end}}item" href="{{.RepoLink}}/wiki"> | |||||
{{svg "octicon-book"}} {{ctx.Locale.Tr "repo.wiki"}} | |||||
{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeReleases) (not .IsEmptyRepo)}} | |||||
<a class="{{if or .PageIsReleaseList .PageIsTagList}}active {{end}}item" href="{{.RepoLink}}/releases"> | |||||
{{svg "octicon-tag"}} {{ctx.Locale.Tr "repo.releases"}} | |||||
{{if .NumReleases}} | |||||
<span class="ui small label">{{CountFmt .NumReleases}}</span> | |||||
{{end}} | |||||
</a> | </a> | ||||
{{end}} | |||||
{{end}} | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeExternalWiki}} | |||||
<a class="item" href="{{(.Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}" target="_blank" rel="noopener noreferrer"> | |||||
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.wiki"}} | |||||
</a> | |||||
{{end}} | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeWiki}} | |||||
<a class="{{if .PageIsWiki}}active {{end}}item" href="{{.RepoLink}}/wiki"> | |||||
{{svg "octicon-book"}} {{ctx.Locale.Tr "repo.wiki"}} | |||||
</a> | |||||
{{end}} | |||||
{{if and (.Permission.CanReadAny ctx.Consts.RepoUnitTypePullRequests ctx.Consts.RepoUnitTypeIssues ctx.Consts.RepoUnitTypeReleases) (not .IsEmptyRepo)}} | |||||
<a class="{{if .PageIsActivity}}active {{end}}item" href="{{.RepoLink}}/activity"> | |||||
{{svg "octicon-pulse"}} {{ctx.Locale.Tr "repo.activity"}} | |||||
</a> | |||||
{{end}} | |||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeExternalWiki}} | |||||
<a class="item" href="{{(.Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}" target="_blank" rel="noopener noreferrer"> | |||||
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.wiki"}} | |||||
</a> | |||||
{{end}} | |||||
{{template "custom/extra_tabs" .}} | |||||
{{if and (.Permission.CanReadAny ctx.Consts.RepoUnitTypePullRequests ctx.Consts.RepoUnitTypeIssues ctx.Consts.RepoUnitTypeReleases) (not .IsEmptyRepo)}} | |||||
<a class="{{if .PageIsActivity}}active {{end}}item" href="{{.RepoLink}}/activity"> | |||||
{{svg "octicon-pulse"}} {{ctx.Locale.Tr "repo.activity"}} | |||||
</a> | |||||
{{end}} | |||||
{{if .Permission.IsAdmin}} | |||||
<span class="item-flex-space"></span> | |||||
{{template "custom/extra_tabs" .}} | |||||
{{if .Permission.IsAdmin}} | |||||
<span class="item-flex-space"></span> | |||||
<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings"> | |||||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} | |||||
</a> | |||||
{{end}} | |||||
</div> | |||||
{{else if .Permission.IsAdmin}} | |||||
<div class="overflow-menu-items"> | |||||
<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings"> | <a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings"> | ||||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} | {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} | ||||
</a> | </a> | ||||
{{end}} | |||||
</div> | |||||
{{else if .Permission.IsAdmin}} | |||||
<div class="overflow-menu-items"> | |||||
<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings"> | |||||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} | |||||
</a> | |||||
</div> | |||||
{{end}} | |||||
</overflow-menu> | |||||
</div> | |||||
{{end}} | |||||
</overflow-menu> | |||||
</div> | |||||
<div class="ui tabs divider"></div> | <div class="ui tabs divider"></div> | ||||
</div> | </div> |
<form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form"> | <form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form"> | ||||
{{$.CsrfTokenHtml}} | {{$.CsrfTokenHtml}} | ||||
</form> | </form> | ||||
<div class="ui dropdown select-branch branch-selector-dropdown {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}}" | |||||
<div class="ui dropdown select-branch branch-selector-dropdown ellipsis-items-nowrap {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}}" | |||||
data-no-results="{{ctx.Locale.Tr "no_results_found"}}" | data-no-results="{{ctx.Locale.Tr "no_results_found"}}" | ||||
{{if not .Issue}}data-for-new-issue="true"{{end}} | {{if not .Issue}}data-for-new-issue="true"{{end}} | ||||
> | > |
{{.CsrfTokenHtml}} | {{.CsrfTokenHtml}} | ||||
<div class="field"> | <div class="field"> | ||||
<label><strong>{{ctx.Locale.Tr "repository"}}</strong></label> | <label><strong>{{ctx.Locale.Tr "repository"}}</strong></label> | ||||
<div class="ui search selection dropdown issue_reference_repository_search"> | |||||
<div class="ui search selection dropdown issue_reference_repository_search ellipsis-items-nowrap"> | |||||
<div class="default text gt-ellipsis">{{.Repository.FullName}}</div> | <div class="default text gt-ellipsis">{{.Repository.FullName}}</div> | ||||
<div class="menu"></div> | <div class="menu"></div> | ||||
</div> | </div> |
<div class="inline field"> | <div class="inline field"> | ||||
{{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}} | {{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}} | ||||
<label>{{ctx.Locale.Tr "repo.settings.default_wiki_everyone_access"}}</label> | <label>{{ctx.Locale.Tr "repo.settings.default_wiki_everyone_access"}}</label> | ||||
<select name="default_wiki_everyone_access" class="ui dropdown"> | |||||
<select name="default_wiki_everyone_access" class="ui selection dropdown"> | |||||
{{/* everyone access mode is different from others, none means it is unset and won't be applied */}} | {{/* everyone access mode is different from others, none means it is unset and won't be applied */}} | ||||
<option value="none" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option> | <option value="none" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option> | ||||
<option value="read" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option> | <option value="read" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option> |
defer tests.PrintCurrentTest(t)() | defer tests.PrintCurrentTest(t)() | ||||
// skip this test if git version is low | // skip this test if git version is low | ||||
if git.CheckGitVersionAtLeast("2.29") != nil { | |||||
if !git.DefaultFeatures().SupportProcReceive { | |||||
return | return | ||||
} | } | ||||
.ui.dropdown .menu > .item { | .ui.dropdown .menu > .item { | ||||
color: var(--color-text); | color: var(--color-text); | ||||
overflow: hidden; | |||||
text-overflow: ellipsis; | |||||
} | } | ||||
.ui.dropdown .menu > .item:hover { | .ui.dropdown .menu > .item:hover { | ||||
.ui.selection.dropdown .menu > .item { | .ui.selection.dropdown .menu > .item { | ||||
border-color: var(--color-secondary); | border-color: var(--color-secondary); | ||||
white-space: nowrap; | |||||
} | } | ||||
.ui.selection.visible.dropdown > .text:not(.default) { | .ui.selection.visible.dropdown > .text:not(.default) { | ||||
align-items: center; | align-items: center; | ||||
gap: .25rem; | gap: .25rem; | ||||
vertical-align: middle; | vertical-align: middle; | ||||
min-width: 0; | |||||
min-width: 0; /* make ellipsis work */ | |||||
} | |||||
.ui.ui.dropdown.selection { | |||||
min-width: 14em; /* match the default min width */ | |||||
} | } | ||||
.ui.dropdown .ui.label .svg { | .ui.dropdown .ui.label .svg { | ||||
gap: .5rem; | gap: .5rem; | ||||
min-width: 0; | min-width: 0; | ||||
} | } | ||||
.ui.dropdown.ellipsis-items-nowrap > .text { | |||||
overflow: hidden; | |||||
white-space: nowrap; | |||||
text-overflow: ellipsis; | |||||
} | |||||
.ellipsis-items-nowrap > .item, | |||||
.ui.dropdown.ellipsis-items-nowrap .menu > .item { | |||||
white-space: nowrap !important; | |||||
overflow: hidden !important; | |||||
text-overflow: ellipsis !important; | |||||
} |
} | } | ||||
} | } | ||||
.ui.form .field > .selection.dropdown { | |||||
min-width: 14em; /* matches the default min width */ | |||||
} | |||||
.new.webhook form .help { | .new.webhook form .help { | ||||
margin-left: 25px; | margin-left: 25px; | ||||
} | } |
.page-content.install .ui.form .field > .help, | .page-content.install .ui.form .field > .help, | ||||
.page-content.install .ui.form .field > .ui.checkbox:first-child, | .page-content.install .ui.form .field > .ui.checkbox:first-child, | ||||
.page-content.install .ui.form .field > .right-content { | .page-content.install .ui.form .field > .right-content { | ||||
margin-left: 30%; | |||||
padding-left: 5px; | |||||
margin-left: calc(30% + 5px); | |||||
width: auto; | width: auto; | ||||
} | } | ||||
} | } | ||||
.page-content.install form.ui.form details.optional.field[open] { | .page-content.install form.ui.form details.optional.field[open] { | ||||
border-bottom: 1px dashed var(--color-secondary); | |||||
padding-bottom: 10px; | padding-bottom: 10px; | ||||
} | } | ||||
.page-content.install form.ui.form details.optional.field[open]:not(:last-child) { | |||||
border-bottom: 1px dashed var(--color-secondary); | |||||
} | |||||
.page-content.install form.ui.form details.optional.field[open] summary { | .page-content.install form.ui.form details.optional.field[open] summary { | ||||
margin-bottom: 10px; | margin-bottom: 10px; | ||||
} | } |
.ui.checkbox label, | .ui.checkbox label, | ||||
.ui.radio.checkbox label { | .ui.radio.checkbox label { | ||||
margin-left: 1.85714em; | |||||
margin-left: 20px; | |||||
} | } | ||||
.ui.checkbox + label { | .ui.checkbox + label { |
unused rules here after refactoring, please remove them. */ | unused rules here after refactoring, please remove them. */ | ||||
.ui.container { | .ui.container { | ||||
display: block; | |||||
max-width: 100%; | |||||
} | |||||
.ui.fluid.container { | |||||
width: 100%; | |||||
} | |||||
.ui[class*="center aligned"].container { | |||||
text-align: center; | |||||
} | |||||
/* overwrite width of containers inside the main page content div (div with class "page-content") */ | |||||
.page-content .ui.ui.ui.container:not(.fluid) { | |||||
width: 1280px; | width: 1280px; | ||||
max-width: calc(100% - calc(2 * var(--page-margin-x))); | max-width: calc(100% - calc(2 * var(--page-margin-x))); | ||||
margin-left: auto; | margin-left: auto; | ||||
margin-right: auto; | margin-right: auto; | ||||
} | } | ||||
.ui.fluid.container { | |||||
width: 100%; | |||||
} | |||||
.ui.container.fluid.padded { | .ui.container.fluid.padded { | ||||
padding: 0 var(--page-margin-x); | padding: 0 var(--page-margin-x); | ||||
} | } | ||||
.ui[class*="center aligned"].container { | |||||
text-align: center; | |||||
} |
margin-top: 4px; | margin-top: 4px; | ||||
} | } | ||||
.ui.dropdown.branch-selector-dropdown .scrolling.menu { | |||||
max-width: min(400px, 90vw); | |||||
} | |||||
.branch-selector-dropdown .branch-dropdown-button { | .branch-selector-dropdown .branch-dropdown-button { | ||||
margin: 0; | margin: 0; | ||||
max-width: 340px; | max-width: 340px; | ||||
} | } | ||||
.branch-selector-dropdown .menu .item .rss-icon { | .branch-selector-dropdown .menu .item .rss-icon { | ||||
position: absolute; | |||||
right: 4px; | |||||
visibility: hidden; /* only show RSS icon on hover */ | visibility: hidden; /* only show RSS icon on hover */ | ||||
} | } | ||||
export default sfc; // activate IDE's Vue plugin | export default sfc; // activate IDE's Vue plugin | ||||
</script> | </script> | ||||
<template> | <template> | ||||
<div class="ui dropdown custom branch-selector-dropdown"> | |||||
<div class="ui dropdown custom branch-selector-dropdown ellipsis-items-nowrap"> | |||||
<div class="ui button branch-dropdown-button" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> | <div class="ui button branch-dropdown-button" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> | ||||
<span class="flex-text-block gt-ellipsis"> | <span class="flex-text-block gt-ellipsis"> | ||||
<template v-if="release">{{ textReleaseCompare }}</template> | <template v-if="release">{{ textReleaseCompare }}</template> | ||||
<div class="ui label" v-if="item.name===repoDefaultBranch && mode === 'branches'"> | <div class="ui label" v-if="item.name===repoDefaultBranch && mode === 'branches'"> | ||||
{{ textDefaultBranchLabel }} | {{ textDefaultBranchLabel }} | ||||
</div> | </div> | ||||
<a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon tw-float-right" :href="rssURLPrefix + item.url" target="_blank" @click.stop> | |||||
<a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon" :href="rssURLPrefix + item.url" target="_blank" @click.stop> | |||||
<!-- creating a lot of Vue component is pretty slow, so we use a static SVG here --> | <!-- creating a lot of Vue component is pretty slow, so we use a static SVG here --> | ||||
<svg width="14" height="14" class="svg octicon-rss"><use href="#svg-symbol-octicon-rss"/></svg> | <svg width="14" height="14" class="svg octicon-rss"><use href="#svg-symbol-octicon-rss"/></svg> | ||||
</a> | </a> |
return; | return; | ||||
} | } | ||||
filteredResponse.results.push({ | filteredResponse.results.push({ | ||||
name: `#${issue.number} ${htmlEscape(issue.title) | |||||
}<div class="text small gt-word-break">${htmlEscape(issue.repository.full_name)}</div>`, | |||||
name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div> | |||||
<div class="text small gt-word-break">${htmlEscape(issue.repository.full_name)}</div>`, | |||||
value: issue.id, | value: issue.id, | ||||
}); | }); | ||||
}); | }); | ||||
export function initRepoPullRequestAllowMaintainerEdit() { | export function initRepoPullRequestAllowMaintainerEdit() { | ||||
const wrapper = document.getElementById('allow-edits-from-maintainers'); | const wrapper = document.getElementById('allow-edits-from-maintainers'); | ||||
if (!wrapper) return; | if (!wrapper) return; | ||||
wrapper.querySelector('input[type="checkbox"]')?.addEventListener('change', async (e) => { | |||||
const checked = e.target.checked; | |||||
const checkbox = wrapper.querySelector('input[type="checkbox"]'); | |||||
checkbox.addEventListener('input', async () => { | |||||
const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`; | const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`; | ||||
wrapper.classList.add('is-loading'); | wrapper.classList.add('is-loading'); | ||||
e.target.disabled = true; | |||||
try { | try { | ||||
const response = await POST(url, {data: {allow_maintainer_edit: checked}}); | |||||
if (!response.ok) { | |||||
const resp = await POST(url, {data: new URLSearchParams({allow_maintainer_edit: checkbox.checked})}); | |||||
if (!resp.ok) { | |||||
throw new Error('Failed to update maintainer edit permission'); | throw new Error('Failed to update maintainer edit permission'); | ||||
} | } | ||||
const data = await resp.json(); | |||||
checkbox.checked = data.allow_maintainer_edit; | |||||
} catch (error) { | } catch (error) { | ||||
checkbox.checked = !checkbox.checked; | |||||
console.error(error); | console.error(error); | ||||
showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error')); | showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error')); | ||||
} finally { | } finally { | ||||
wrapper.classList.remove('is-loading'); | wrapper.classList.remove('is-loading'); | ||||
e.target.disabled = false; | |||||
} | } | ||||
}); | }); | ||||
} | } |