mirror of https://github.com/go-gitea/gitea
Compare commits
28 Commits
e03737f618
...
9f272e5dc0
Author | SHA1 | Date |
---|---|---|
Lunny Xiao | 9f272e5dc0 | |
Lunny Xiao | 6ad77125ca | |
wxiaoguang | 9c08637eae | |
wxiaoguang | 7c613f100e | |
6543 | 8e8ca6c653 | |
wxiaoguang | eda10cc2bb | |
wxiaoguang | ce8b11ae13 | |
Kemal Zebari | 22c7b3a744 | |
wxiaoguang | 982b20d259 | |
wxiaoguang | 5c236bd4c0 | |
yp05327 | ecd1d96f49 | |
Neal Caffery | bb0e4ce581 | |
wxiaoguang | c7bb3aa034 | |
Lunny Xiao | e1ee0aaaeb | |
Lunny Xiao | bb45ecb707 | |
Lunny Xiao | 34e33b67fb | |
Lunny Xiao | ba953d1629 | |
silverwind | e923cd61b5 | |
Lunny Xiao | b1780fd50b | |
Lunny Xiao | 8e21ed833d | |
Lunny Xiao | 61bc8234bb | |
Lunny Xiao | ef37b8cf2a | |
Lunny Xiao | 63093769f2 | |
Lunny Xiao | 2963ac4eaa | |
Lunny Xiao | be695d3112 | |
Lunny Xiao | 9306c0981b | |
Lunny Xiao | 6dba76e744 | |
Lunny Xiao | ff9839525b |
|
@ -220,10 +220,7 @@ Gitea or set your environment appropriately.`, "")
|
|||
}
|
||||
}
|
||||
|
||||
supportProcReceive := false
|
||||
if git.CheckGitVersionAtLeast("2.29") == nil {
|
||||
supportProcReceive = true
|
||||
}
|
||||
supportProcReceive := git.DefaultFeatures().SupportProcReceive
|
||||
|
||||
for scanner.Scan() {
|
||||
// TODO: support news feeds for wiki
|
||||
|
@ -497,7 +494,7 @@ Gitea or set your environment appropriately.`, "")
|
|||
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.")
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ var CmdMigrateStorage = &cli.Command{
|
|||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Value: "",
|
||||
Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log'",
|
||||
Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "storage",
|
||||
|
@ -160,6 +160,13 @@ func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) er
|
|||
})
|
||||
}
|
||||
|
||||
func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||
return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error {
|
||||
_, err := storage.Copy(dstStorage, artifact.ArtifactPath, storage.ActionsArtifacts, artifact.ArtifactPath)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func runMigrateStorage(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
@ -223,13 +230,14 @@ func runMigrateStorage(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
migratedMethods := map[string]func(context.Context, storage.ObjectStorage) error{
|
||||
"attachments": migrateAttachments,
|
||||
"lfs": migrateLFS,
|
||||
"avatars": migrateAvatars,
|
||||
"repo-avatars": migrateRepoAvatars,
|
||||
"repo-archivers": migrateRepoArchivers,
|
||||
"packages": migratePackages,
|
||||
"actions-log": migrateActionsLog,
|
||||
"attachments": migrateAttachments,
|
||||
"lfs": migrateLFS,
|
||||
"avatars": migrateAvatars,
|
||||
"repo-avatars": migrateRepoAvatars,
|
||||
"repo-archivers": migrateRepoArchivers,
|
||||
"packages": migratePackages,
|
||||
"actions-log": migrateActionsLog,
|
||||
"actions-artifacts": migrateActionsArtifacts,
|
||||
}
|
||||
|
||||
tp := strings.ToLower(ctx.String("type"))
|
||||
|
|
|
@ -178,7 +178,7 @@ func runServ(c *cli.Context) error {
|
|||
}
|
||||
|
||||
if len(words) < 2 {
|
||||
if git.CheckGitVersionAtLeast("2.29") == nil {
|
||||
if git.DefaultFeatures().SupportProcReceive {
|
||||
// for AGit Flow
|
||||
if cmd == "ssh_info" {
|
||||
fmt.Print(`{"type":"gitea","version":1}`)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Gitea - Docker
|
||||
|
||||
Dockerfile is found in root of repository.
|
||||
Dockerfile is found in the root of the repository.
|
||||
|
||||
Docker image can be found on [docker hub](https://hub.docker.com/r/gitea/gitea)
|
||||
Docker image can be found on [docker hub](https://hub.docker.com/r/gitea/gitea).
|
||||
|
||||
Documentation on using docker image can be found on [Gitea Docs site](https://docs.gitea.com/installation/install-with-docker-rootless)
|
||||
Documentation on using docker image can be found on [Gitea Docs site](https://docs.gitea.com/installation/install-with-docker-rootless).
|
||||
|
|
|
@ -27,23 +27,27 @@ func init() {
|
|||
|
||||
// LoadAssignees load assignees of this issue.
|
||||
func (issue *Issue) LoadAssignees(ctx context.Context) (err error) {
|
||||
if issue.isAssigneeLoaded || len(issue.Assignees) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset maybe preexisting assignees
|
||||
issue.Assignees = []*user_model.User{}
|
||||
issue.Assignee = nil
|
||||
|
||||
err = db.GetEngine(ctx).Table("`user`").
|
||||
if err = db.GetEngine(ctx).Table("`user`").
|
||||
Join("INNER", "issue_assignees", "assignee_id = `user`.id").
|
||||
Where("issue_assignees.issue_id = ?", issue.ID).
|
||||
Find(&issue.Assignees)
|
||||
if err != nil {
|
||||
Find(&issue.Assignees); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue.isAssigneeLoaded = true
|
||||
// Check if we have at least one assignee and if yes put it in as `Assignee`
|
||||
if len(issue.Assignees) > 0 {
|
||||
issue.Assignee = issue.Assignees[0]
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
|
||||
|
|
|
@ -96,31 +96,34 @@ func (err ErrIssueWasClosed) Error() string {
|
|||
|
||||
// Issue represents an issue or pull request of repository.
|
||||
type Issue struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
||||
PosterID int64 `xorm:"INDEX"`
|
||||
Poster *user_model.User `xorm:"-"`
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64 `xorm:"index"`
|
||||
Title string `xorm:"name"`
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
Labels []*Label `xorm:"-"`
|
||||
MilestoneID int64 `xorm:"INDEX"`
|
||||
Milestone *Milestone `xorm:"-"`
|
||||
Project *project_model.Project `xorm:"-"`
|
||||
Priority int
|
||||
AssigneeID int64 `xorm:"-"`
|
||||
Assignee *user_model.User `xorm:"-"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
IsRead bool `xorm:"-"`
|
||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||
PullRequest *PullRequest `xorm:"-"`
|
||||
NumComments int
|
||||
Ref string
|
||||
PinOrder int `xorm:"DEFAULT 0"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
||||
PosterID int64 `xorm:"INDEX"`
|
||||
Poster *user_model.User `xorm:"-"`
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64 `xorm:"index"`
|
||||
Title string `xorm:"name"`
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
Labels []*Label `xorm:"-"`
|
||||
isLabelsLoaded bool `xorm:"-"`
|
||||
MilestoneID int64 `xorm:"INDEX"`
|
||||
Milestone *Milestone `xorm:"-"`
|
||||
isMilestoneLoaded bool `xorm:"-"`
|
||||
Project *project_model.Project `xorm:"-"`
|
||||
Priority int
|
||||
AssigneeID int64 `xorm:"-"`
|
||||
Assignee *user_model.User `xorm:"-"`
|
||||
isAssigneeLoaded bool `xorm:"-"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
IsRead bool `xorm:"-"`
|
||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||
PullRequest *PullRequest `xorm:"-"`
|
||||
NumComments int
|
||||
Ref string
|
||||
PinOrder int `xorm:"DEFAULT 0"`
|
||||
|
||||
DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
|
||||
|
@ -128,11 +131,12 @@ type Issue struct {
|
|||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
|
||||
Attachments []*repo_model.Attachment `xorm:"-"`
|
||||
Comments CommentList `xorm:"-"`
|
||||
Reactions ReactionList `xorm:"-"`
|
||||
TotalTrackedTime int64 `xorm:"-"`
|
||||
Assignees []*user_model.User `xorm:"-"`
|
||||
Attachments []*repo_model.Attachment `xorm:"-"`
|
||||
isAttachmentsLoaded bool `xorm:"-"`
|
||||
Comments CommentList `xorm:"-"`
|
||||
Reactions ReactionList `xorm:"-"`
|
||||
TotalTrackedTime int64 `xorm:"-"`
|
||||
Assignees []*user_model.User `xorm:"-"`
|
||||
|
||||
// IsLocked limits commenting abilities to users on an issue
|
||||
// with write access
|
||||
|
@ -184,6 +188,19 @@ func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (issue *Issue) LoadAttachments(ctx context.Context) (err error) {
|
||||
if issue.isAttachmentsLoaded || issue.Attachments != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
issue.isAttachmentsLoaded = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTimetrackerEnabled returns true if the repo enables timetracking
|
||||
func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
|
@ -284,11 +301,12 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) {
|
|||
|
||||
// LoadMilestone load milestone of this issue.
|
||||
func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
|
||||
if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
|
||||
if !issue.isMilestoneLoaded && (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
|
||||
issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
|
||||
if err != nil && !IsErrMilestoneNotExist(err) {
|
||||
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
|
||||
}
|
||||
issue.isMilestoneLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -324,11 +342,8 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
if issue.Attachments == nil {
|
||||
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
if err = issue.LoadAttachments(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = issue.loadComments(ctx); err != nil {
|
||||
|
@ -347,6 +362,13 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
|||
return issue.loadReactions(ctx)
|
||||
}
|
||||
|
||||
func (issue *Issue) ResetAttributesLoaded() {
|
||||
issue.isLabelsLoaded = false
|
||||
issue.isMilestoneLoaded = false
|
||||
issue.isAttachmentsLoaded = false
|
||||
issue.isAssigneeLoaded = false
|
||||
}
|
||||
|
||||
// GetIsRead load the `IsRead` field of the issue
|
||||
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
|
||||
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
|
||||
|
|
|
@ -111,6 +111,7 @@ func NewIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_m
|
|||
return err
|
||||
}
|
||||
|
||||
issue.isLabelsLoaded = false
|
||||
issue.Labels = nil
|
||||
if err = issue.LoadLabels(ctx); err != nil {
|
||||
return err
|
||||
|
@ -160,6 +161,8 @@ func NewIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *us
|
|||
return err
|
||||
}
|
||||
|
||||
// reload all labels
|
||||
issue.isLabelsLoaded = false
|
||||
issue.Labels = nil
|
||||
if err = issue.LoadLabels(ctx); err != nil {
|
||||
return err
|
||||
|
@ -325,11 +328,12 @@ func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
|
|||
|
||||
// LoadLabels loads labels
|
||||
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
||||
if issue.Labels == nil && issue.ID != 0 {
|
||||
if !issue.isLabelsLoaded && issue.Labels == nil && issue.ID != 0 {
|
||||
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
issue.isLabelsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -74,11 +74,11 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi
|
|||
|
||||
func (issues IssueList) getPosterIDs() []int64 {
|
||||
return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
|
||||
return issue.PosterID, true
|
||||
return issue.PosterID, issue.Poster == nil
|
||||
})
|
||||
}
|
||||
|
||||
func (issues IssueList) loadPosters(ctx context.Context) error {
|
||||
func (issues IssueList) LoadPosters(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ func (issues IssueList) getIssueIDs() []int64 {
|
|||
return ids
|
||||
}
|
||||
|
||||
func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||
func (issues IssueList) LoadLabels(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||
err = rows.Scan(&labelIssue)
|
||||
if err != nil {
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
|
||||
return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -177,7 +177,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||
// When there are no rows left and we try to close it.
|
||||
// Since that is not relevant for us, we can safely ignore it.
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
|
||||
return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
|
||||
}
|
||||
left -= limit
|
||||
issueIDs = issueIDs[limit:]
|
||||
|
@ -185,6 +185,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Labels = issueLabels[issue.ID]
|
||||
issue.isLabelsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -195,7 +196,7 @@ func (issues IssueList) getMilestoneIDs() []int64 {
|
|||
})
|
||||
}
|
||||
|
||||
func (issues IssueList) loadMilestones(ctx context.Context) error {
|
||||
func (issues IssueList) LoadMilestones(ctx context.Context) error {
|
||||
milestoneIDs := issues.getMilestoneIDs()
|
||||
if len(milestoneIDs) == 0 {
|
||||
return nil
|
||||
|
@ -220,6 +221,7 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||
issue.isMilestoneLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -263,7 +265,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadAssignees(ctx context.Context) error {
|
||||
func (issues IssueList) LoadAssignees(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -310,6 +312,7 @@ func (issues IssueList) loadAssignees(ctx context.Context) error {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Assignees = assignees[issue.ID]
|
||||
issue.isAssigneeLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -413,6 +416,7 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Attachments = attachments[issue.ID]
|
||||
issue.isAttachmentsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -538,23 +542,23 @@ func (issues IssueList) LoadAttributes(ctx context.Context) error {
|
|||
return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadPosters(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadPosters: %w", err)
|
||||
if err := issues.LoadPosters(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadPosters: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadLabels(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadLabels: %w", err)
|
||||
if err := issues.LoadLabels(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadLabels: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadMilestones(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
|
||||
if err := issues.LoadMilestones(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadMilestones: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.LoadProjects(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadAssignees(ctx); err != nil {
|
||||
if err := issues.LoadAssignees(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -159,10 +159,11 @@ type PullRequest struct {
|
|||
|
||||
ChangedProtectedFiles []string `xorm:"TEXT JSON"`
|
||||
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
Issue *Issue `xorm:"-"`
|
||||
Index int64
|
||||
RequestedReviewers []*user_model.User `xorm:"-"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
Issue *Issue `xorm:"-"`
|
||||
Index int64
|
||||
RequestedReviewers []*user_model.User `xorm:"-"`
|
||||
isRequestedReviewersLoaded bool `xorm:"-"`
|
||||
|
||||
HeadRepoID int64 `xorm:"INDEX"`
|
||||
HeadRepo *repo_model.Repository `xorm:"-"`
|
||||
|
@ -289,7 +290,7 @@ func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) {
|
|||
|
||||
// LoadRequestedReviewers loads the requested reviewers.
|
||||
func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||
if len(pr.RequestedReviewers) > 0 {
|
||||
if pr.isRequestedReviewersLoaded || len(pr.RequestedReviewers) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -297,10 +298,10 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = reviews.LoadReviewers(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
pr.isRequestedReviewersLoaded = true
|
||||
for _, review := range reviews {
|
||||
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
|
@ -123,7 +125,7 @@ func GetPullRequestIDsByCheckStatus(ctx context.Context, status PullRequestStatu
|
|||
}
|
||||
|
||||
// PullRequests returns all pull requests for a base Repo by the given conditions
|
||||
func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, int64, error) {
|
||||
func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) (PullRequestList, int64, error) {
|
||||
if opts.Page <= 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
|
@ -153,50 +155,93 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
|
|||
// PullRequestList defines a list of pull requests
|
||||
type PullRequestList []*PullRequest
|
||||
|
||||
func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
||||
if len(prs) == 0 {
|
||||
return nil
|
||||
func (prs PullRequestList) getRepositoryIDs() []int64 {
|
||||
repoIDs := make(container.Set[int64])
|
||||
for _, pr := range prs {
|
||||
if pr.BaseRepo == nil && pr.BaseRepoID > 0 {
|
||||
repoIDs.Add(pr.BaseRepoID)
|
||||
}
|
||||
if pr.HeadRepo == nil && pr.HeadRepoID > 0 {
|
||||
repoIDs.Add(pr.HeadRepoID)
|
||||
}
|
||||
}
|
||||
return repoIDs.Values()
|
||||
}
|
||||
|
||||
// Load issues.
|
||||
issueIDs := prs.GetIssueIDs()
|
||||
issues := make([]*Issue, 0, len(issueIDs))
|
||||
func (prs PullRequestList) LoadRepositories(ctx context.Context) error {
|
||||
repoIDs := prs.getRepositoryIDs()
|
||||
reposMap := make(map[int64]*repo_model.Repository, len(repoIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("id > 0").
|
||||
In("id", issueIDs).
|
||||
Find(&issues); err != nil {
|
||||
In("id", repoIDs).
|
||||
Find(&reposMap); err != nil {
|
||||
return fmt.Errorf("find issues: %w", err)
|
||||
}
|
||||
|
||||
set := make(map[int64]*Issue)
|
||||
for i := range issues {
|
||||
set[issues[i].ID] = issues[i]
|
||||
}
|
||||
for _, pr := range prs {
|
||||
pr.Issue = set[pr.IssueID]
|
||||
/*
|
||||
Old code:
|
||||
pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
|
||||
|
||||
It's worth panic because it's almost impossible to happen under normal use.
|
||||
But in integration testing, an asynchronous task could read a database that has been reset.
|
||||
So returning an error would make more sense, let the caller has a choice to ignore it.
|
||||
*/
|
||||
if pr.Issue == nil {
|
||||
return fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
|
||||
if pr.BaseRepo == nil {
|
||||
pr.BaseRepo = reposMap[pr.BaseRepoID]
|
||||
}
|
||||
if pr.HeadRepo == nil {
|
||||
pr.HeadRepo = reposMap[pr.HeadRepoID]
|
||||
pr.isHeadRepoLoaded = true
|
||||
}
|
||||
pr.Issue.PullRequest = pr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
||||
if _, err := prs.LoadIssues(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) {
|
||||
if len(prs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Load issues.
|
||||
issueIDs := prs.GetIssueIDs()
|
||||
issues := make(map[int64]*Issue, len(issueIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
In("id", issueIDs).
|
||||
Find(&issues); err != nil {
|
||||
return nil, fmt.Errorf("find issues: %w", err)
|
||||
}
|
||||
|
||||
issueList := make(IssueList, 0, len(prs))
|
||||
for _, pr := range prs {
|
||||
if pr.Issue == nil {
|
||||
pr.Issue = issues[pr.IssueID]
|
||||
/*
|
||||
Old code:
|
||||
pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
|
||||
|
||||
It's worth panic because it's almost impossible to happen under normal use.
|
||||
But in integration testing, an asynchronous task could read a database that has been reset.
|
||||
So returning an error would make more sense, let the caller has a choice to ignore it.
|
||||
*/
|
||||
if pr.Issue == nil {
|
||||
return nil, fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
|
||||
}
|
||||
}
|
||||
pr.Issue.PullRequest = pr
|
||||
if pr.Issue.Repo == nil {
|
||||
pr.Issue.Repo = pr.BaseRepo
|
||||
}
|
||||
issueList = append(issueList, pr.Issue)
|
||||
}
|
||||
return issueList, nil
|
||||
}
|
||||
|
||||
// GetIssueIDs returns all issue ids
|
||||
func (prs PullRequestList) GetIssueIDs() []int64 {
|
||||
issueIDs := make([]int64, 0, len(prs))
|
||||
for i := range prs {
|
||||
issueIDs = append(issueIDs, prs[i].IssueID)
|
||||
}
|
||||
return issueIDs
|
||||
return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
|
||||
if pr.Issue == nil {
|
||||
return pr.IssueID, pr.IssueID > 0
|
||||
}
|
||||
return 0, false
|
||||
})
|
||||
}
|
||||
|
||||
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
|
||||
|
|
|
@ -8,14 +8,14 @@ import "code.gitea.io/gitea/models/db"
|
|||
// SearchOrderByMap represents all possible search order
|
||||
var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{
|
||||
"asc": {
|
||||
"alpha": db.SearchOrderByAlphabetically,
|
||||
"alpha": "owner_name ASC, name ASC",
|
||||
"created": db.SearchOrderByOldest,
|
||||
"updated": db.SearchOrderByLeastUpdated,
|
||||
"size": db.SearchOrderBySize,
|
||||
"id": db.SearchOrderByID,
|
||||
},
|
||||
"desc": {
|
||||
"alpha": db.SearchOrderByAlphabeticallyReverse,
|
||||
"alpha": "owner_name DESC, name DESC",
|
||||
"created": db.SearchOrderByNewest,
|
||||
"updated": db.SearchOrderByRecentUpdated,
|
||||
"size": db.SearchOrderBySizeReverse,
|
||||
|
|
|
@ -859,6 +859,10 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
|
|||
|
||||
// GetUserByIDs returns the user objects by given IDs if exists.
|
||||
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
users := make([]*User, 0, len(ids))
|
||||
err := db.GetEngine(ctx).In("id", ids).
|
||||
Table("user").
|
||||
|
|
|
@ -132,7 +132,7 @@ func (r *BlameReader) Close() error {
|
|||
// 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) {
|
||||
var ignoreRevsFile *string
|
||||
if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore {
|
||||
if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
|
||||
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
|
||||
}
|
||||
|
||||
|
|
|
@ -423,7 +423,7 @@ func (c *Commit) GetSubModule(entryname string) (*SubModule, error) {
|
|||
// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
|
||||
func (c *Commit) GetBranchName() (string, error) {
|
||||
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("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String())
|
||||
|
|
|
@ -22,42 +22,63 @@ import (
|
|||
"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
|
||||
|
||||
type Features 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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
DefaultFeatures struct {
|
||||
GitVersion *version.Version
|
||||
|
||||
SupportProcReceive bool // >= 2.29
|
||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||
}
|
||||
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)
|
||||
if runErr != nil {
|
||||
return runErr
|
||||
return nil, runErr
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -85,56 +106,24 @@ func SetExecutablePath(path string) error {
|
|||
return fmt.Errorf("git not found: %w", err)
|
||||
}
|
||||
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/"
|
||||
if runtime.GOOS == "linux" {
|
||||
// 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
|
||||
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
|
||||
}
|
||||
|
@ -154,8 +143,12 @@ func HomeDir() string {
|
|||
// 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.
|
||||
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
|
||||
|
@ -165,7 +158,24 @@ func InitSimple(ctx context.Context) error {
|
|||
defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second
|
||||
}
|
||||
|
||||
return SetExecutablePath(setting.Git.Path)
|
||||
if err := SetExecutablePath(setting.Git.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
defaultFeatures, err = loadGitVersionFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = ensureGitVersion(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// when git works with gnupg (commit signing), there should be a stable home for gnupg commands
|
||||
if _, ok := os.LookupEnv("GNUPGHOME"); !ok {
|
||||
_ = os.Setenv("GNUPGHOME", filepath.Join(HomeDir(), ".gnupg"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitFull initializes git module with version check and change global variables, sync gitconfig.
|
||||
|
@ -175,30 +185,18 @@ func InitFull(ctx context.Context) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// when git works with gnupg (commit signing), there should be a stable home for gnupg commands
|
||||
if _, ok := os.LookupEnv("GNUPGHOME"); !ok {
|
||||
_ = os.Setenv("GNUPGHOME", filepath.Join(HomeDir(), ".gnupg"))
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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=")
|
||||
}
|
||||
DefaultFeatures.SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
|
||||
DefaultFeatures.SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit
|
||||
if DefaultFeatures.SupportHashSha256 {
|
||||
SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat)
|
||||
} else {
|
||||
log.Warn("sha256 hash support is disabled - requires Git >= 2.42. Gogit is currently unsupported")
|
||||
}
|
||||
|
||||
if setting.LFS.StartServer {
|
||||
if CheckGitVersionAtLeast("2.1.2") != nil {
|
||||
if !DefaultFeatures().CheckVersionAtLeast("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=")
|
||||
|
@ -238,13 +236,13 @@ func syncGitConfig() (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
if CheckGitVersionAtLeast("2.10") == nil {
|
||||
if DefaultFeatures().CheckVersionAtLeast("2.10") {
|
||||
if err := configSet("receive.advertisePushOptions", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if CheckGitVersionAtLeast("2.18") == nil {
|
||||
if DefaultFeatures().CheckVersionAtLeast("2.18") {
|
||||
if err := configSet("core.commitGraph", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -256,7 +254,7 @@ func syncGitConfig() (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if DefaultFeatures.SupportProcReceive {
|
||||
if DefaultFeatures().SupportProcReceive {
|
||||
// set support for AGit flow
|
||||
if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
|
@ -294,7 +292,7 @@ func syncGitConfig() (err error) {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
@ -309,21 +307,6 @@ func syncGitConfig() (err error) {
|
|||
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 {
|
||||
badVersions := []struct {
|
||||
Version *version.Version
|
||||
|
|
|
@ -120,12 +120,8 @@ var (
|
|||
Sha256ObjectFormat ObjectFormat = Sha256ObjectFormatImpl{}
|
||||
)
|
||||
|
||||
var SupportedObjectFormats = []ObjectFormat{
|
||||
Sha1ObjectFormat,
|
||||
}
|
||||
|
||||
func ObjectFormatFromName(name string) ObjectFormat {
|
||||
for _, objectFormat := range SupportedObjectFormats {
|
||||
for _, objectFormat := range DefaultFeatures().SupportedObjectFormats {
|
||||
if name == objectFormat.Name() {
|
||||
return objectFormat
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ func (*Sha256Hash) Type() ObjectFormat { return Sha256ObjectFormat }
|
|||
|
||||
func NewIDFromString(hexHash string) (ObjectID, error) {
|
||||
var theObjectFormat ObjectFormat
|
||||
for _, objectFormat := range SupportedObjectFormats {
|
||||
for _, objectFormat := range DefaultFeatures().SupportedObjectFormats {
|
||||
if len(hexHash) == objectFormat.FullLength() {
|
||||
theObjectFormat = objectFormat
|
||||
break
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
// GetRemoteAddress returns remote url of git repository in the repoPath with special remote name
|
||||
func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) {
|
||||
var cmd *Command
|
||||
if CheckGitVersionAtLeast("2.7") == nil {
|
||||
if DefaultFeatures().CheckVersionAtLeast("2.7") {
|
||||
cmd = NewCommand(ctx, "remote", "get-url").AddDynamicArguments(remoteName)
|
||||
} else {
|
||||
cmd = NewCommand(ctx, "config", "--get").AddDynamicArguments("remote." + remoteName + ".url")
|
||||
|
|
|
@ -7,7 +7,6 @@ package git
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
|
@ -63,32 +62,6 @@ func IsRepoURLAccessible(ctx context.Context, url string) bool {
|
|||
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.
|
||||
func InitRepository(ctx context.Context, repoPath string, bare bool, objectFormatName string) error {
|
||||
err := os.MkdirAll(repoPath, os.ModePerm)
|
||||
|
@ -101,7 +74,7 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma
|
|||
if !IsValidObjectFormat(objectFormatName) {
|
||||
return fmt.Errorf("invalid object format: %s", objectFormatName)
|
||||
}
|
||||
if DefaultFeatures.SupportHashSha256 {
|
||||
if DefaultFeatures().SupportHashSha256 {
|
||||
cmd.AddOptionValues("--object-format", objectFormatName)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
var isGogit bool
|
|
@ -22,9 +22,7 @@ import (
|
|||
"github.com/go-git/go-git/v5/storage/filesystem"
|
||||
)
|
||||
|
||||
func init() {
|
||||
isGogit = true
|
||||
}
|
||||
const isGogit = true
|
||||
|
||||
// Repository represents a Git repository.
|
||||
type Repository struct {
|
||||
|
|
|
@ -15,9 +15,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
isGogit = false
|
||||
}
|
||||
const isGogit = false
|
||||
|
||||
// Repository represents a Git repository.
|
||||
type Repository struct {
|
||||
|
|
|
@ -438,7 +438,7 @@ func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit,
|
|||
}
|
||||
|
||||
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)").
|
||||
AddOptionFormat("--count=%d", limit).
|
||||
AddOptionValues("--contains", commit.ID.String(), BranchPrefix).
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
// WriteCommitGraph write commit graph to speed up repo access
|
||||
// this requires git v2.18 to be installed
|
||||
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 {
|
||||
return fmt.Errorf("unable to write commit-graph for '%s' : %w", repoPath, err)
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan c
|
|||
go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg)
|
||||
|
||||
// 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()
|
||||
shasToCheckReader, shasToCheckWriter := io.Pipe()
|
||||
wg.Add(2)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
@ -54,7 +55,7 @@ var (
|
|||
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
|
||||
|
||||
// anyHashPattern splits url containing SHA into parts
|
||||
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`)
|
||||
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`)
|
||||
|
||||
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
|
||||
comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
|
||||
|
@ -591,7 +592,8 @@ func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) {
|
|||
|
||||
func mentionProcessor(ctx *RenderContext, node *html.Node) {
|
||||
start := 0
|
||||
for node != nil {
|
||||
nodeStop := node.NextSibling
|
||||
for node != nodeStop {
|
||||
found, loc := references.FindFirstMentionBytes(util.UnsafeStringToBytes(node.Data[start:]))
|
||||
if !found {
|
||||
node = node.NextSibling
|
||||
|
@ -962,57 +964,68 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
}
|
||||
}
|
||||
|
||||
type anyHashPatternResult struct {
|
||||
PosStart int
|
||||
PosEnd int
|
||||
FullURL string
|
||||
CommitID string
|
||||
SubPath string
|
||||
QueryHash string
|
||||
}
|
||||
|
||||
func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
|
||||
m := anyHashPattern.FindStringSubmatchIndex(s)
|
||||
if m == nil {
|
||||
return ret, false
|
||||
}
|
||||
|
||||
ret.PosStart, ret.PosEnd = m[0], m[1]
|
||||
ret.FullURL = s[ret.PosStart:ret.PosEnd]
|
||||
if strings.HasSuffix(ret.FullURL, ".") {
|
||||
// if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence.
|
||||
ret.PosEnd--
|
||||
ret.FullURL = ret.FullURL[:len(ret.FullURL)-1]
|
||||
for i := 0; i < len(m); i++ {
|
||||
m[i] = min(m[i], ret.PosEnd)
|
||||
}
|
||||
}
|
||||
|
||||
ret.CommitID = s[m[2]:m[3]]
|
||||
if m[5] > 0 {
|
||||
ret.SubPath = s[m[4]:m[5]]
|
||||
}
|
||||
|
||||
lastStart, lastEnd := m[len(m)-2], m[len(m)-1]
|
||||
if lastEnd > 0 {
|
||||
ret.QueryHash = s[lastStart:lastEnd][1:]
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// fullHashPatternProcessor renders SHA containing URLs
|
||||
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
return
|
||||
}
|
||||
|
||||
next := node.NextSibling
|
||||
for node != nil && node != next {
|
||||
m := anyHashPattern.FindStringSubmatchIndex(node.Data)
|
||||
if m == nil {
|
||||
return
|
||||
nodeStop := node.NextSibling
|
||||
for node != nodeStop {
|
||||
if node.Type != html.TextNode {
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
|
||||
urlFull := node.Data[m[0]:m[1]]
|
||||
text := base.ShortSha(node.Data[m[2]:m[3]])
|
||||
|
||||
// 3rd capture group matches a optional path
|
||||
subpath := ""
|
||||
if m[5] > 0 {
|
||||
subpath = node.Data[m[4]:m[5]]
|
||||
ret, ok := anyHashPatternExtract(node.Data)
|
||||
if !ok {
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
|
||||
// 4th capture group matches a optional url hash
|
||||
hash := ""
|
||||
if m[7] > 0 {
|
||||
hash = node.Data[m[6]:m[7]][1:]
|
||||
text := base.ShortSha(ret.CommitID)
|
||||
if ret.SubPath != "" {
|
||||
text += ret.SubPath
|
||||
}
|
||||
|
||||
start := m[0]
|
||||
end := m[1]
|
||||
|
||||
// If url ends in '.', it's very likely that it is not part of the
|
||||
// actual url but used to finish a sentence.
|
||||
if strings.HasSuffix(urlFull, ".") {
|
||||
end--
|
||||
urlFull = urlFull[:len(urlFull)-1]
|
||||
if hash != "" {
|
||||
hash = hash[:len(hash)-1]
|
||||
} else if subpath != "" {
|
||||
subpath = subpath[:len(subpath)-1]
|
||||
}
|
||||
if ret.QueryHash != "" {
|
||||
text += " (" + ret.QueryHash + ")"
|
||||
}
|
||||
|
||||
if subpath != "" {
|
||||
text += subpath
|
||||
}
|
||||
|
||||
if hash != "" {
|
||||
text += " (" + hash + ")"
|
||||
}
|
||||
replaceContent(node, start, end, createCodeLink(urlFull, text, "commit"))
|
||||
replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit"))
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
}
|
||||
|
@ -1021,19 +1034,16 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
if ctx.Metas == nil {
|
||||
return
|
||||
}
|
||||
|
||||
next := node.NextSibling
|
||||
for node != nil && node != next {
|
||||
m := comparePattern.FindStringSubmatchIndex(node.Data)
|
||||
if m == nil {
|
||||
return
|
||||
nodeStop := node.NextSibling
|
||||
for node != nodeStop {
|
||||
if node.Type != html.TextNode {
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure that every group (m[0]...m[7]) has a match
|
||||
for i := 0; i < 8; i++ {
|
||||
if m[i] == -1 {
|
||||
return
|
||||
}
|
||||
m := comparePattern.FindStringSubmatchIndex(node.Data)
|
||||
if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
|
||||
urlFull := node.Data[m[0]:m[1]]
|
||||
|
|
|
@ -60,7 +60,8 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
|
|||
}
|
||||
|
||||
func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
for node != nil {
|
||||
nodeStop := node.NextSibling
|
||||
for node != nodeStop {
|
||||
if node.Type != html.TextNode {
|
||||
node = node.NextSibling
|
||||
continue
|
||||
|
|
|
@ -399,36 +399,61 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRegExp_anySHA1Pattern(t *testing.T) {
|
||||
testCases := map[string][]string{
|
||||
testCases := map[string]anyHashPatternResult{
|
||||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": {
|
||||
"a644101ed04d0beacea864ce805e0c4f86ba1cd1",
|
||||
"/test/unit/event.js",
|
||||
"#L2703",
|
||||
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
|
||||
SubPath: "/test/unit/event.js",
|
||||
QueryHash: "L2703",
|
||||
},
|
||||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": {
|
||||
"a644101ed04d0beacea864ce805e0c4f86ba1cd1",
|
||||
"/test/unit/event.js",
|
||||
"",
|
||||
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
|
||||
SubPath: "/test/unit/event.js",
|
||||
},
|
||||
"https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": {
|
||||
"0705be475092aede1eddae01319ec931fb9c65fc",
|
||||
"",
|
||||
"",
|
||||
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
|
||||
},
|
||||
"https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": {
|
||||
"0705be475092aede1eddae01319ec931fb9c65fc",
|
||||
"/src",
|
||||
"",
|
||||
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
|
||||
SubPath: "/src",
|
||||
},
|
||||
"https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": {
|
||||
"d8a994ef243349f321568f9e36d5c3f444b99cae",
|
||||
"",
|
||||
"#diff-2",
|
||||
CommitID: "d8a994ef243349f321568f9e36d5c3f444b99cae",
|
||||
QueryHash: "diff-2",
|
||||
},
|
||||
"non-url": {},
|
||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b#L1-L2": {
|
||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
||||
QueryHash: "L1-L2",
|
||||
},
|
||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678.": {
|
||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
||||
},
|
||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678/sub.": {
|
||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
||||
SubPath: "/sub",
|
||||
},
|
||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b.": {
|
||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
||||
},
|
||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b&c=d": {
|
||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
||||
},
|
||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678#hash.": {
|
||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
||||
QueryHash: "hash",
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range testCases {
|
||||
assert.Equal(t, anyHashPattern.FindStringSubmatch(k)[1:], v)
|
||||
ret, ok := anyHashPatternExtract(k)
|
||||
if v.CommitID == "" {
|
||||
assert.False(t, ok)
|
||||
} else {
|
||||
assert.EqualValues(t, strings.TrimSuffix(k, "."), ret.FullURL)
|
||||
assert.EqualValues(t, v.CommitID, ret.CommitID)
|
||||
assert.EqualValues(t, v.SubPath, ret.SubPath)
|
||||
assert.EqualValues(t, v.QueryHash, ret.QueryHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -124,6 +124,11 @@ func TestRender_CrossReferences(t *testing.T) {
|
|||
test(
|
||||
util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"),
|
||||
`<p><a href="`+util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogitea/some-repo-name#12345</a></p>`)
|
||||
|
||||
inputURL := "https://host/a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3"
|
||||
test(
|
||||
inputURL,
|
||||
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789/foo.txt (L2-L3)</code></a></p>`)
|
||||
}
|
||||
|
||||
func TestMisc_IsSameDomain(t *testing.T) {
|
||||
|
@ -695,7 +700,7 @@ func TestIssue18471(t *testing.T) {
|
|||
}, strings.NewReader(data), &res)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String())
|
||||
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String())
|
||||
}
|
||||
|
||||
func TestIsFullURL(t *testing.T) {
|
||||
|
|
|
@ -5,6 +5,7 @@ package repository
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
|
@ -36,6 +37,15 @@ func SyncRepoBranches(ctx context.Context, repoID, 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]{}
|
||||
{
|
||||
branches, _, err := gitRepo.GetBranchNames(0, 0)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// 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)
|
||||
}
|
|
@ -116,23 +116,39 @@ func ListPullRequests(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
apiPrs := make([]*api.PullRequest, len(prs))
|
||||
// NOTE: load repository first, so that issue.Repo will be filled with pr.BaseRepo
|
||||
if err := prs.LoadRepositories(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
issueList, err := prs.LoadIssues(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := issueList.LoadLabels(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadLabels", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadPosters(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadPoster", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadAttachments(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadMilestones(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadMilestones", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadAssignees(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAssignees", err)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range prs {
|
||||
if err = prs[i].LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
}
|
||||
if err = prs[i].LoadAttributes(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
if err = prs[i].LoadBaseRepo(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
|
||||
return
|
||||
}
|
||||
if err = prs[i].LoadHeadRepo(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
|
||||
return
|
||||
}
|
||||
apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.Doer)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/system"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/routing"
|
||||
actions_router "code.gitea.io/gitea/routers/api/actions"
|
||||
|
@ -112,7 +113,10 @@ func InitWebInstallPage(ctx context.Context) {
|
|||
// InitWebInstalled is for global installed configuration.
|
||||
func InitWebInstalled(ctx context.Context) {
|
||||
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
|
||||
translation.InitLocales(ctx)
|
||||
|
|
|
@ -122,7 +122,7 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
|
|||
preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName)
|
||||
case refFullName.IsTag():
|
||||
preReceiveTag(ourCtx, refFullName)
|
||||
case git.DefaultFeatures.SupportProcReceive && refFullName.IsFor():
|
||||
case git.DefaultFeatures().SupportProcReceive && refFullName.IsFor():
|
||||
preReceiveFor(ourCtx, refFullName)
|
||||
default:
|
||||
ourCtx.AssertCanWriteCode()
|
||||
|
|
|
@ -18,7 +18,7 @@ import (
|
|||
// HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present
|
||||
func HookProcReceive(ctx *gitea_context.PrivateContext) {
|
||||
opts := web.GetForm(ctx).(*private.HookOptions)
|
||||
if !git.DefaultFeatures.SupportProcReceive {
|
||||
if !git.DefaultFeatures().SupportProcReceive {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -297,7 +297,7 @@ func ServCommand(ctx *context.PrivateContext) {
|
|||
}
|
||||
} else {
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ func Config(ctx *context.Context) {
|
|||
ctx.Data["OfflineMode"] = setting.OfflineMode
|
||||
ctx.Data["RunUser"] = setting.RunUser
|
||||
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["RepoRootPath"] = setting.RepoRootPath
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
)
|
||||
|
||||
func SSHInfo(rw http.ResponseWriter, req *http.Request) {
|
||||
if !git.DefaultFeatures.SupportProcReceive {
|
||||
if !git.DefaultFeatures().SupportProcReceive {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -183,7 +183,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
|
|||
|
||||
if repoExist {
|
||||
// Because of special ref "refs/for" .. , need delay write permission check
|
||||
if git.DefaultFeatures.SupportProcReceive {
|
||||
if git.DefaultFeatures().SupportProcReceive {
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
|
||||
|
|
|
@ -180,7 +180,7 @@ func Create(ctx *context.Context) {
|
|||
|
||||
ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
|
||||
ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
|
||||
ctx.Data["SupportedObjectFormats"] = git.SupportedObjectFormats
|
||||
ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats
|
||||
ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat
|
||||
|
||||
ctx.HTML(http.StatusOK, tplCreate)
|
||||
|
|
|
@ -40,6 +40,9 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
|
|||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
if err := issue.LoadAttachments(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
|
||||
apiIssue := &api.Issue{
|
||||
ID: issue.ID,
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -44,7 +45,16 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
|||
return nil
|
||||
}
|
||||
|
||||
p, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer)
|
||||
var doerID int64
|
||||
if doer != nil {
|
||||
doerID = doer.ID
|
||||
}
|
||||
|
||||
const repoDoerPermCacheKey = "repo_doer_perm_cache"
|
||||
p, err := cache.GetWithContextCache(ctx, repoDoerPermCacheKey, fmt.Sprintf("%d_%d", pr.BaseRepoID, doerID),
|
||||
func() (access_model.Permission, error) {
|
||||
return access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer)
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err)
|
||||
p.AccessMode = perm.AccessModeNone
|
||||
|
|
|
@ -1143,7 +1143,7 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
|
|||
// so if we are using at least this version of git we don't have to tell ParsePatch to do
|
||||
// the skipping for us
|
||||
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)
|
||||
parsePatchSkipToFile = ""
|
||||
}
|
||||
|
|
|
@ -39,7 +39,8 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Empty(t, issue.Assignees)
|
||||
|
||||
// Check they're gone
|
||||
// Reload to check they're gone
|
||||
issue.ResetAttributesLoaded()
|
||||
assert.NoError(t, issue.LoadAssignees(db.DefaultContext))
|
||||
assert.Empty(t, issue.Assignees)
|
||||
assert.Empty(t, issue.Assignee)
|
||||
|
|
|
@ -383,7 +383,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *
|
|||
cmdApply.AddArguments("--ignore-whitespace")
|
||||
}
|
||||
is3way := false
|
||||
if git.CheckGitVersionAtLeast("2.32.0") == nil {
|
||||
if git.DefaultFeatures().CheckVersionAtLeast("2.32.0") {
|
||||
cmdApply.AddArguments("--3way")
|
||||
is3way = true
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest)
|
|||
baseBranch := "base"
|
||||
|
||||
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
|
||||
fetchArgs = append(fetchArgs, "--no-write-commit-graph")
|
||||
}
|
||||
|
|
|
@ -195,6 +195,10 @@ func adoptRepository(ctx context.Context, repoPath string, repo *repo_model.Repo
|
|||
}
|
||||
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 {
|
||||
return fmt.Errorf("SyncReleasesWithTags: %w", err)
|
||||
}
|
||||
|
|
|
@ -148,7 +148,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
|
|||
stderr := &strings.Builder{}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
{{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" .}}
|
|
@ -180,94 +180,6 @@
|
|||
<input type="text" placeholder="place holder">
|
||||
</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>
|
||||
|
|
|
@ -157,168 +157,171 @@
|
|||
|
||||
<!-- Optional Settings -->
|
||||
<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 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>
|
||||
</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}}>
|
||||
<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>
|
||||
<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 {{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>
|
||||
<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">
|
||||
<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>
|
||||
<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 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 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 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 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 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 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 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 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 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">
|
||||
<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>
|
||||
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span>
|
||||
</div>
|
||||
</details>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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 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 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 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 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 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 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 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 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 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 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 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 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>
|
||||
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span>
|
||||
</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}}
|
||||
<!-- Environment Config -->
|
||||
|
@ -333,12 +336,11 @@
|
|||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="divider"></div>
|
||||
<div class="inline field">
|
||||
<div class="right-content">
|
||||
These configuration options will be written into: {{.CustomConfFile}}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
|
||||
<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 */}}
|
||||
<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">
|
||||
<span class="flex-text-block gt-ellipsis">
|
||||
{{if .release}}
|
||||
|
|
|
@ -128,107 +128,109 @@
|
|||
{{if .IsGenerated}}<div class="fork-flag">{{ctx.Locale.Tr "repo.generated_from"}} <a href="{{(.TemplateRepo ctx).Link}}">{{(.TemplateRepo ctx).FullName}}</a></div>{{end}}
|
||||
</div>
|
||||
{{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>
|
||||
{{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 .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 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 .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}}
|
||||
|
||||
{{$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 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}}
|
||||
|
||||
{{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 .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 .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 .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 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 .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}}
|
||||
|
||||
{{$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 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}}
|
||||
{{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">
|
||||
{{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">
|
||||
{{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>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form">
|
||||
{{$.CsrfTokenHtml}}
|
||||
</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"}}"
|
||||
{{if not .Issue}}data-for-new-issue="true"{{end}}
|
||||
>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{{.CsrfTokenHtml}}
|
||||
<div class="field">
|
||||
<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="menu"></div>
|
||||
</div>
|
||||
|
|
|
@ -4,29 +4,36 @@
|
|||
</div>
|
||||
{{end}}
|
||||
<div class="issue-title-header">
|
||||
<div class="issue-title" id="issue-title-wrapper">
|
||||
{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
|
||||
<div class="issue-title" id="issue-title-display">
|
||||
<h1 class="gt-word-break">
|
||||
<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
|
||||
</span>
|
||||
<div id="edit-title-input" class="ui input tw-flex-1 tw-hidden">
|
||||
<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">
|
||||
</div>
|
||||
{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}}
|
||||
<span class="index">#{{.Issue.Index}}</span>
|
||||
</h1>
|
||||
<div class="issue-title-buttons">
|
||||
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
|
||||
<button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} tw-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
|
||||
{{if $canEditIssueTitle}}
|
||||
<button id="issue-title-edit-show" class="ui small basic button">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
|
||||
{{end}}
|
||||
{{if not .Issue.IsPull}}
|
||||
<a role="button" class="ui small primary button new-issue-button tw-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
|
||||
<a role="button" class="ui small primary button" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
|
||||
<div class="edit-buttons">
|
||||
<button id="cancel-edit-title" class="ui small basic button in-edit tw-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
|
||||
<button id="save-edit-title" class="ui small primary button in-edit tw-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if $canEditIssueTitle}}
|
||||
<div class="ui form issue-title tw-hidden" id="issue-title-editor">
|
||||
<div class="ui input tw-flex-1">
|
||||
<input value="{{.Issue.Title}}" data-old-title="{{.Issue.Title}}" maxlength="255" autocomplete="off">
|
||||
</div>
|
||||
<div class="issue-title-buttons">
|
||||
<button class="ui small basic cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
|
||||
<button class="ui small primary button"
|
||||
data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title"
|
||||
{{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>
|
||||
{{ctx.Locale.Tr "repo.issues.save"}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="issue-title-meta">
|
||||
{{if .HasMerged}}
|
||||
<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
|
||||
|
@ -63,14 +70,14 @@
|
|||
{{end}}
|
||||
{{else}}
|
||||
{{if .Issue.OriginalAuthor}}
|
||||
<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
|
||||
<span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
|
||||
{{else}}
|
||||
<span id="pull-desc" class="pull-desc">
|
||||
<span id="pull-desc-display" class="pull-desc">
|
||||
<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
|
||||
{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}
|
||||
</span>
|
||||
{{end}}
|
||||
<span id="pull-desc-edit" class="tw-hidden flex-text-block">
|
||||
<span id="pull-desc-editor" class="tw-hidden flex-text-block">
|
||||
<div class="ui floating filter dropdown">
|
||||
<div class="ui basic small button tw-mr-0">
|
||||
<span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span>
|
||||
|
|
|
@ -345,7 +345,7 @@
|
|||
<div class="inline field">
|
||||
{{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}}
|
||||
<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 */}}
|
||||
<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>
|
||||
|
|
|
@ -195,14 +195,17 @@ func TestAPIEditUser(t *testing.T) {
|
|||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
|
||||
urlStr := fmt.Sprintf("/api/v1/admin/users/%s", "user2")
|
||||
|
||||
fullNameToChange := "Full Name User 2"
|
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||
// required
|
||||
"login_name": "user2",
|
||||
"source_id": "0",
|
||||
// to change
|
||||
"full_name": "Full Name User 2",
|
||||
"full_name": fullNameToChange,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
|
||||
assert.Equal(t, fullNameToChange, user2.FullName)
|
||||
|
||||
empty := ""
|
||||
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
|
||||
|
@ -216,7 +219,7 @@ func TestAPIEditUser(t *testing.T) {
|
|||
json.Unmarshal(resp.Body.Bytes(), &errMap)
|
||||
assert.EqualValues(t, "e-mail invalid [email: ]", errMap["message"].(string))
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
|
||||
user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
|
||||
assert.False(t, user2.IsRestricted)
|
||||
bTrue := true
|
||||
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
|
||||
|
|
|
@ -695,7 +695,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
|
|||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// skip this test if git version is low
|
||||
if git.CheckGitVersionAtLeast("2.29") != nil {
|
||||
if !git.DefaultFeatures().SupportProcReceive {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content
|
|||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
val := htmlDoc.doc.Find("#issue-title").Text()
|
||||
val := htmlDoc.doc.Find("#issue-title-display").Text()
|
||||
assert.Contains(t, val, title)
|
||||
val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
|
||||
assert.Equal(t, content, val)
|
||||
|
|
|
@ -125,7 +125,7 @@ func TestPullCreate_TitleEscape(t *testing.T) {
|
|||
req := NewRequest(t, "GET", url)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
editTestTitleURL, exists := htmlDoc.doc.Find("#save-edit-title").First().Attr("data-update-url")
|
||||
editTestTitleURL, exists := htmlDoc.doc.Find(".issue-title-buttons button[data-update-url]").First().Attr("data-update-url")
|
||||
assert.True(t, exists, "The template has changed")
|
||||
|
||||
req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{
|
||||
|
|
|
@ -342,8 +342,6 @@ a.label,
|
|||
|
||||
.ui.dropdown .menu > .item {
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ui.dropdown .menu > .item:hover {
|
||||
|
@ -374,7 +372,6 @@ a.label,
|
|||
|
||||
.ui.selection.dropdown .menu > .item {
|
||||
border-color: var(--color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui.selection.visible.dropdown > .text:not(.default) {
|
||||
|
@ -1342,7 +1339,11 @@ table th[data-sortt-desc] .svg {
|
|||
align-items: center;
|
||||
gap: .25rem;
|
||||
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 {
|
||||
|
@ -1369,3 +1370,16 @@ table th[data-sortt-desc] .svg {
|
|||
gap: .5rem;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -448,6 +448,10 @@ textarea:focus,
|
|||
}
|
||||
}
|
||||
|
||||
.ui.form .field > .selection.dropdown {
|
||||
min-width: 14em; /* matches the default min width */
|
||||
}
|
||||
|
||||
.new.webhook form .help {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
.page-content.install .ui.form .field > .help,
|
||||
.page-content.install .ui.form .field > .ui.checkbox:first-child,
|
||||
.page-content.install .ui.form .field > .right-content {
|
||||
margin-left: 30%;
|
||||
padding-left: 5px;
|
||||
margin-left: calc(30% + 5px);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
@ -24,10 +23,11 @@
|
|||
}
|
||||
|
||||
.page-content.install form.ui.form details.optional.field[open] {
|
||||
border-bottom: 1px dashed var(--color-secondary);
|
||||
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 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ input[type="radio"] {
|
|||
|
||||
.ui.checkbox label,
|
||||
.ui.radio.checkbox label {
|
||||
margin-left: 1.85714em;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.ui.checkbox + label {
|
||||
|
|
|
@ -2,26 +2,20 @@
|
|||
unused rules here after refactoring, please remove them. */
|
||||
|
||||
.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;
|
||||
max-width: calc(100% - calc(2 * var(--page-margin-x)));
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.ui.fluid.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui.container.fluid.padded {
|
||||
padding: 0 var(--page-margin-x);
|
||||
}
|
||||
|
||||
.ui[class*="center aligned"].container {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -575,34 +575,7 @@ td .commit-summary {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.issue-title-header {
|
||||
width: 100%;
|
||||
padding-bottom: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.issue-title-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.repository.view.issue .issue-title-buttons,
|
||||
.repository.view.issue .edit-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.repository.view.issue .issue-title {
|
||||
flex-direction: column;
|
||||
}
|
||||
.repository.view.issue .issue-title-buttons,
|
||||
.repository.view.issue .edit-buttons {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.repository.view.issue .edit-buttons {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
.comment.form .issue-content-left .avatar {
|
||||
display: none;
|
||||
}
|
||||
|
@ -617,15 +590,37 @@ td .commit-summary {
|
|||
}
|
||||
}
|
||||
|
||||
/* issue title & meta & edit */
|
||||
.issue-title-header {
|
||||
width: 100%;
|
||||
padding-bottom: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.issue-title-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.repository.view.issue .issue-title-buttons {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.repository.view.issue .issue-title-buttons > .ui.button {
|
||||
margin: 0;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.repository.view.issue .issue-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin-bottom: 8px;
|
||||
min-height: 40px; /* avoid layout shift on edit */
|
||||
}
|
||||
|
||||
.repository.view.issue .issue-title h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
font-weight: var(--font-weight-normal);
|
||||
|
@ -633,14 +628,24 @@ td .commit-summary {
|
|||
line-height: 40px;
|
||||
margin: 0;
|
||||
padding-right: 0.25rem;
|
||||
min-height: 41px; /* avoid layout shift on edit */
|
||||
}
|
||||
|
||||
.repository.view.issue .issue-title h1 .ui.input {
|
||||
font-size: 0.5em;
|
||||
@media (max-width: 767.98px) {
|
||||
.repository.view.issue .issue-title {
|
||||
flex-direction: column;
|
||||
}
|
||||
.repository.view.issue .issue-title-buttons {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.repository.view.issue .issue-title h1 .ui.input input {
|
||||
.repository.view.issue .issue-title .ui.input {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.repository.view.issue .issue-title .ui.input input {
|
||||
font-size: 1.5em;
|
||||
padding: 2px .5rem;
|
||||
}
|
||||
|
@ -653,10 +658,6 @@ td .commit-summary {
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.issue-title .edit-zone {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.issue-state-label {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
|
@ -2859,6 +2860,10 @@ tbody.commit-list {
|
|||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.ui.dropdown.branch-selector-dropdown .scrolling.menu {
|
||||
max-width: min(400px, 90vw);
|
||||
}
|
||||
|
||||
.branch-selector-dropdown .branch-dropdown-button {
|
||||
margin: 0;
|
||||
max-width: 340px;
|
||||
|
@ -2908,6 +2913,8 @@ tbody.commit-list {
|
|||
}
|
||||
|
||||
.branch-selector-dropdown .menu .item .rss-icon {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
visibility: hidden; /* only show RSS icon on hover */
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,20 @@
|
|||
// This file must be imported before any lazy-loading is being attempted.
|
||||
__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
|
||||
|
||||
export function showGlobalErrorMessage(msg) {
|
||||
const pageContent = document.querySelector('.page-content');
|
||||
if (!pageContent) return;
|
||||
function shouldIgnoreError(err) {
|
||||
const ignorePatterns = [
|
||||
'/assets/js/monaco.', // https://github.com/go-gitea/gitea/issues/30861 , https://github.com/microsoft/monaco-editor/issues/4496
|
||||
];
|
||||
for (const pattern of ignorePatterns) {
|
||||
if (err.stack?.includes(pattern)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// compact the message to a data attribute to avoid too many duplicated messages
|
||||
const msgCompact = msg.replace(/\W/g, '').trim();
|
||||
let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
|
||||
export function showGlobalErrorMessage(msg) {
|
||||
const msgContainer = document.querySelector('.page-content') ?? document.body;
|
||||
const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages
|
||||
let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
|
||||
if (!msgDiv) {
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = `<div class="ui container negative message center aligned js-global-error tw-mt-[15px] tw-whitespace-pre-line"></div>`;
|
||||
|
@ -23,7 +30,7 @@ export function showGlobalErrorMessage(msg) {
|
|||
msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact);
|
||||
msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString());
|
||||
msgDiv.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : '');
|
||||
pageContent.prepend(msgDiv);
|
||||
msgContainer.prepend(msgDiv);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,10 +59,12 @@ function processWindowErrorEvent({error, reason, message, type, filename, lineno
|
|||
if (runModeIsProd) return;
|
||||
}
|
||||
|
||||
// If the error stack trace does not include the base URL of our script assets, it likely came
|
||||
// from a browser extension or inline script. Do not show such errors in production.
|
||||
if (err instanceof Error && !err.stack?.includes(assetBaseUrl) && runModeIsProd) {
|
||||
return;
|
||||
if (err instanceof Error) {
|
||||
// If the error stack trace does not include the base URL of our script assets, it likely came
|
||||
// from a browser extension or inline script. Do not show such errors in production.
|
||||
if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return;
|
||||
// Ignore some known errors that are unable to fix
|
||||
if (shouldIgnoreError(err)) return;
|
||||
}
|
||||
|
||||
let msg = err?.message ?? message;
|
||||
|
|
|
@ -246,7 +246,7 @@ export function initRepoBranchTagSelector(selector) {
|
|||
export default sfc; // activate IDE's Vue plugin
|
||||
</script>
|
||||
<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">
|
||||
<span class="flex-text-block gt-ellipsis">
|
||||
<template v-if="release">{{ textReleaseCompare }}</template>
|
||||
|
@ -280,7 +280,7 @@ export default sfc; // activate IDE's Vue plugin
|
|||
<div class="ui label" v-if="item.name===repoDefaultBranch && mode === 'branches'">
|
||||
{{ textDefaultBranchLabel }}
|
||||
</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 -->
|
||||
<svg width="14" height="14" class="svg octicon-rss"><use href="#svg-symbol-octicon-rss"/></svg>
|
||||
</a>
|
||||
|
|
|
@ -67,7 +67,7 @@ export default {
|
|||
const weekValues = Object.values(this.data);
|
||||
const start = weekValues[0].week;
|
||||
const end = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(new Date(start), new Date(end));
|
||||
const startDays = startDaysBetween(start, end);
|
||||
this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
|
||||
this.errorText = '';
|
||||
} else {
|
||||
|
|
|
@ -114,7 +114,7 @@ export default {
|
|||
const weekValues = Object.values(total.weeks);
|
||||
this.xAxisStart = weekValues[0].week;
|
||||
this.xAxisEnd = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd));
|
||||
const startDays = startDaysBetween(this.xAxisStart, this.xAxisEnd);
|
||||
total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
|
||||
this.xAxisMin = this.xAxisStart;
|
||||
this.xAxisMax = this.xAxisEnd;
|
||||
|
|
|
@ -62,7 +62,7 @@ export default {
|
|||
const data = await response.json();
|
||||
const start = Object.values(data)[0].week;
|
||||
const end = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(new Date(start), new Date(end));
|
||||
const startDays = startDaysBetween(start, end);
|
||||
this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
|
||||
this.errorText = '';
|
||||
} else {
|
||||
|
|
|
@ -47,10 +47,18 @@ export function initFootLanguageMenu() {
|
|||
|
||||
export function initGlobalEnterQuickSubmit() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const isQuickSubmitEnter = ((e.ctrlKey && !e.altKey) || e.metaKey) && (e.key === 'Enter');
|
||||
if (isQuickSubmitEnter && e.target.matches('textarea')) {
|
||||
e.preventDefault();
|
||||
handleGlobalEnterQuickSubmit(e.target);
|
||||
if (e.key !== 'Enter') return;
|
||||
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
|
||||
if (hasCtrlOrMeta && e.target.matches('textarea')) {
|
||||
if (handleGlobalEnterQuickSubmit(e.target)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (e.target.matches('input') && !e.target.closest('form')) {
|
||||
// input in a normal form could handle Enter key by default, so we only handle the input outside a form
|
||||
// eslint-disable-next-line unicorn/no-lonely-if
|
||||
if (handleGlobalEnterQuickSubmit(e.target)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,16 +3,17 @@ export function handleGlobalEnterQuickSubmit(target) {
|
|||
if (form) {
|
||||
if (!form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
return;
|
||||
} else {
|
||||
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
|
||||
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
|
||||
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
|
||||
}
|
||||
|
||||
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
|
||||
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
|
||||
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
form = target.closest('.ui.form');
|
||||
if (form) {
|
||||
form.querySelector('.ui.primary.button')?.click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkd
|
|||
import {toAbsoluteUrl} from '../utils.js';
|
||||
import {initDropzone} from './common-global.js';
|
||||
import {POST, GET} from '../modules/fetch.js';
|
||||
import {showErrorToast} from '../modules/toast.js';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
|
@ -123,8 +124,8 @@ export function initRepoIssueSidebarList() {
|
|||
return;
|
||||
}
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
@ -298,23 +299,23 @@ export function initRepoPullRequestMergeInstruction() {
|
|||
export function initRepoPullRequestAllowMaintainerEdit() {
|
||||
const wrapper = document.getElementById('allow-edits-from-maintainers');
|
||||
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`;
|
||||
wrapper.classList.add('is-loading');
|
||||
e.target.disabled = true;
|
||||
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');
|
||||
}
|
||||
const data = await resp.json();
|
||||
checkbox.checked = data.allow_maintainer_edit;
|
||||
} catch (error) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
console.error(error);
|
||||
showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error'));
|
||||
} finally {
|
||||
wrapper.classList.remove('is-loading');
|
||||
e.target.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -602,85 +603,69 @@ export function initRepoIssueWipToggle() {
|
|||
});
|
||||
}
|
||||
|
||||
async function pullrequest_targetbranch_change(update_url) {
|
||||
const targetBranch = $('#pull-target-branch').data('branch');
|
||||
const $branchTarget = $('#branch_target');
|
||||
if (targetBranch === $branchTarget.text()) {
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
export function initRepoIssueTitleEdit() {
|
||||
// Edit issue title
|
||||
const $issueTitle = $('#issue-title');
|
||||
const $editInput = $('#edit-title-input input');
|
||||
const issueTitleDisplay = document.querySelector('#issue-title-display');
|
||||
const issueTitleEditor = document.querySelector('#issue-title-editor');
|
||||
if (!issueTitleEditor) return;
|
||||
|
||||
const editTitleToggle = function () {
|
||||
toggleElem($issueTitle);
|
||||
toggleElem('.not-in-edit');
|
||||
toggleElem('#edit-title-input');
|
||||
toggleElem('#pull-desc');
|
||||
toggleElem('#pull-desc-edit');
|
||||
toggleElem('.in-edit');
|
||||
toggleElem('.new-issue-button');
|
||||
document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active');
|
||||
$editInput[0].focus();
|
||||
$editInput[0].select();
|
||||
return false;
|
||||
};
|
||||
|
||||
$('#edit-title').on('click', editTitleToggle);
|
||||
$('#cancel-edit-title').on('click', editTitleToggle);
|
||||
$('#save-edit-title').on('click', editTitleToggle).on('click', async function () {
|
||||
const pullrequest_target_update_url = this.getAttribute('data-target-update-url');
|
||||
if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) {
|
||||
$editInput.val($issueTitle.text());
|
||||
await pullrequest_targetbranch_change(pullrequest_target_update_url);
|
||||
} else {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('title', $editInput.val());
|
||||
const response = await POST(this.getAttribute('data-update-url'), {data: params});
|
||||
const data = await response.json();
|
||||
$editInput.val(data.title);
|
||||
$issueTitle.text(data.title);
|
||||
if (pullrequest_target_update_url) {
|
||||
await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
const issueTitleInput = issueTitleEditor.querySelector('input');
|
||||
const oldTitle = issueTitleInput.getAttribute('data-old-title');
|
||||
issueTitleDisplay.querySelector('#issue-title-edit-show').addEventListener('click', () => {
|
||||
hideElem(issueTitleDisplay);
|
||||
hideElem('#pull-desc-display');
|
||||
showElem(issueTitleEditor);
|
||||
showElem('#pull-desc-editor');
|
||||
if (!issueTitleInput.value.trim()) {
|
||||
issueTitleInput.value = oldTitle;
|
||||
}
|
||||
issueTitleInput.focus();
|
||||
});
|
||||
issueTitleEditor.querySelector('.ui.cancel.button').addEventListener('click', () => {
|
||||
hideElem(issueTitleEditor);
|
||||
hideElem('#pull-desc-editor');
|
||||
showElem(issueTitleDisplay);
|
||||
showElem('#pull-desc-display');
|
||||
});
|
||||
const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
|
||||
editSaveButton.addEventListener('click', async () => {
|
||||
const prTargetUpdateUrl = editSaveButton.getAttribute('data-target-update-url');
|
||||
const newTitle = issueTitleInput.value.trim();
|
||||
try {
|
||||
if (newTitle && newTitle !== oldTitle) {
|
||||
const resp = await POST(editSaveButton.getAttribute('data-update-url'), {data: new URLSearchParams({title: newTitle})});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to update issue title: ${resp.statusText}`);
|
||||
}
|
||||
}
|
||||
if (prTargetUpdateUrl) {
|
||||
const newTargetBranch = document.querySelector('#pull-target-branch').getAttribute('data-branch');
|
||||
const oldTargetBranch = document.querySelector('#branch_target').textContent;
|
||||
if (newTargetBranch !== oldTargetBranch) {
|
||||
const resp = await POST(prTargetUpdateUrl, {data: new URLSearchParams({target_branch: newTargetBranch})});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to update PR target branch: ${resp.statusText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showErrorToast(error.message);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
export function initRepoIssueBranchSelect() {
|
||||
const changeBranchSelect = function () {
|
||||
const $selectionTextField = $('#pull-target-branch');
|
||||
|
||||
const baseName = $selectionTextField.data('basename');
|
||||
const branchNameNew = $(this).data('branch');
|
||||
const branchNameOld = $selectionTextField.data('branch');
|
||||
|
||||
// Replace branch name to keep translation from HTML template
|
||||
$selectionTextField.html($selectionTextField.html().replace(
|
||||
`${baseName}:${branchNameOld}`,
|
||||
`${baseName}:${branchNameNew}`,
|
||||
));
|
||||
$selectionTextField.data('branch', branchNameNew); // update branch name in setting
|
||||
};
|
||||
$('#branch-select > .item').on('click', changeBranchSelect);
|
||||
document.querySelector('#branch-select')?.addEventListener('click', (e) => {
|
||||
const el = e.target.closest('.item[data-branch]');
|
||||
if (!el) return;
|
||||
const pullTargetBranch = document.querySelector('#pull-target-branch');
|
||||
const baseName = pullTargetBranch.getAttribute('data-basename');
|
||||
const branchNameNew = el.getAttribute('data-branch');
|
||||
const branchNameOld = pullTargetBranch.getAttribute('data-branch');
|
||||
pullTargetBranch.textContent = pullTargetBranch.textContent.replace(`${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}`);
|
||||
pullTargetBranch.setAttribute('data-branch', branchNameNew);
|
||||
});
|
||||
}
|
||||
|
||||
export function initSingleCommentEditor($commentForm) {
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc.js';
|
||||
import {getCurrentLocale} from '../utils.js';
|
||||
|
||||
// Returns an array of millisecond-timestamps of start-of-week days (Sundays)
|
||||
export function startDaysBetween(startDate, endDate) {
|
||||
// Ensure the start date is a Sunday
|
||||
while (startDate.getDay() !== 0) {
|
||||
startDate.setDate(startDate.getDate() + 1);
|
||||
}
|
||||
dayjs.extend(utc);
|
||||
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate);
|
||||
const startDays = [];
|
||||
/**
|
||||
* Returns an array of millisecond-timestamps of start-of-week days (Sundays)
|
||||
*
|
||||
* @param startConfig The start date. Can take any type that `Date` accepts.
|
||||
* @param endConfig The end date. Can take any type that `Date` accepts.
|
||||
*/
|
||||
export function startDaysBetween(startDate, endDate) {
|
||||
const start = dayjs.utc(startDate);
|
||||
const end = dayjs.utc(endDate);
|
||||
|
||||
let current = start;
|
||||
|
||||
// Ensure the start date is a Sunday
|
||||
while (current.day() !== 0) {
|
||||
current = current.add(1, 'day');
|
||||
}
|
||||
|
||||
const startDays = [];
|
||||
while (current.isBefore(end)) {
|
||||
startDays.push(current.valueOf());
|
||||
// we are adding 7 * 24 hours instead of 1 week because we don't want
|
||||
// date library to use local time zone to calculate 1 week from now.
|
||||
// local time zone is problematic because of daylight saving time (dst)
|
||||
// used on some countries
|
||||
current = current.add(7 * 24, 'hour');
|
||||
current = current.add(1, 'week');
|
||||
}
|
||||
|
||||
return startDays;
|
||||
|
@ -29,10 +34,10 @@ export function firstStartDateAfterDate(inputDate) {
|
|||
if (!(inputDate instanceof Date)) {
|
||||
throw new Error('Invalid date');
|
||||
}
|
||||
const dayOfWeek = inputDate.getDay();
|
||||
const dayOfWeek = inputDate.getUTCDay();
|
||||
const daysUntilSunday = 7 - dayOfWeek;
|
||||
const resultDate = new Date(inputDate.getTime());
|
||||
resultDate.setDate(resultDate.getDate() + daysUntilSunday);
|
||||
resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday);
|
||||
return resultDate.valueOf();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue