diff --git a/docs/content/usage/actions/act-runner.en-us.md b/docs/content/usage/actions/act-runner.en-us.md index 1be81c5c78..e2915be365 100644 --- a/docs/content/usage/actions/act-runner.en-us.md +++ b/docs/content/usage/actions/act-runner.en-us.md @@ -81,7 +81,7 @@ docker run --entrypoint="" --rm -it gitea/act_runner:latest act_runner generate- When you are using the docker image, you can specify the configuration file by using the `CONFIG_FILE` environment variable. Make sure that the file is mounted into the container as a volume: ```bash -docker run -v $(pwd)/config.yaml:/config.yaml -e CONFIG_FILE=/config.yaml ... +docker run -v $PWD/config.yaml:/config.yaml -e CONFIG_FILE=/config.yaml ... ``` You may notice the commands above are both incomplete, because it is not the time to run the act runner yet. @@ -157,8 +157,8 @@ If you are using the docker image, behaviour will be slightly different. Registr ```bash docker run \ - -v $(pwd)/config.yaml:/config.yaml \ - -v $(pwd)/data:/data \ + -v $PWD/config.yaml:/config.yaml \ + -v $PWD/data:/data \ -v /var/run/docker.sock:/var/run/docker.sock \ -e CONFIG_FILE=/config.yaml \ -e GITEA_INSTANCE_URL= \ diff --git a/docs/content/usage/multi-factor-authentication.en-us.md b/docs/content/usage/multi-factor-authentication.en-us.md new file mode 100644 index 0000000000..16b57b7bdc --- /dev/null +++ b/docs/content/usage/multi-factor-authentication.en-us.md @@ -0,0 +1,35 @@ +--- +date: "2023-08-22T14:21:00+08:00" +title: "Usage: Multi-factor Authentication (MFA)" +slug: "multi-factor-authentication" +weight: 15 +toc: false +draft: false +menu: + sidebar: + parent: "usage" + name: "Multi-factor Authentication (MFA)" + weight: 15 + identifier: "multi-factor-authentication" +--- + +# Multi-factor Authentication (MFA) + +Multi-factor Authentication (also referred to as MFA or 2FA) enhances security by requiring a time-sensitive set of credentials in addition to a password. +If a password were later to be compromised, logging into Gitea will not be possible without the additional credentials and the account would remain secure. +Gitea supports both TOTP (Time-based One-Time Password) tokens and FIDO-based hardware keys using the Webauthn API. + +MFA can be configured within the "Security" tab of the user settings page. + +## MFA Considerations + +Enabling MFA on a user does affect how the Git HTTP protocol can be used with the Git CLI. +This interface does not support MFA, and trying to use a password normally will no longer be possible whilst MFA is enabled. +If SSH is not an option for Git operations, an access token can be generated within the "Applications" tab of the user settings page. +This access token can be used as if it were a password in order to allow the Git CLI to function over HTTP. + +> **Warning** - By its very nature, an access token sidesteps the security benefits of MFA. +> It must be kept secure and should only be used as a last resort. + +The Gitea API supports providing the relevant TOTP password in the `X-Gitea-OTP` header, as described in [API Usage](development/api-usage.md). +This should be used instead of an access token where possible. diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index 6c249c2244..1654e6ce75 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -116,70 +116,71 @@ func GetIssueStats(opts *IssuesOptions) (*IssueStats, error) { func getIssueStatsChunk(opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) { stats := &IssueStats{} - countSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session { - sess := db.GetEngine(db.DefaultContext). - Join("INNER", "repository", "`issue`.repo_id = `repository`.id") - if len(opts.RepoIDs) > 1 { - sess.In("issue.repo_id", opts.RepoIDs) - } else if len(opts.RepoIDs) == 1 { - sess.And("issue.repo_id = ?", opts.RepoIDs[0]) - } - - if len(issueIDs) > 0 { - sess.In("issue.id", issueIDs) - } - - applyLabelsCondition(sess, opts) - - applyMilestoneCondition(sess, opts) - - applyProjectCondition(sess, opts) - - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } else if opts.AssigneeID == db.NoConditionID { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") - } - - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } - - if opts.MentionedID > 0 { - applyMentionedCondition(sess, opts.MentionedID) - } - - if opts.ReviewRequestedID > 0 { - applyReviewRequestedCondition(sess, opts.ReviewRequestedID) - } - - if opts.ReviewedID > 0 { - applyReviewedCondition(sess, opts.ReviewedID) - } - - switch opts.IsPull { - case util.OptionalBoolTrue: - sess.And("issue.is_pull=?", true) - case util.OptionalBoolFalse: - sess.And("issue.is_pull=?", false) - } - - return sess - } + sess := db.GetEngine(db.DefaultContext). + Join("INNER", "repository", "`issue`.repo_id = `repository`.id") var err error - stats.OpenCount, err = countSession(opts, issueIDs). + stats.OpenCount, err = applyIssuesOptions(sess, opts, issueIDs). And("issue.is_closed = ?", false). Count(new(Issue)) if err != nil { return stats, err } - stats.ClosedCount, err = countSession(opts, issueIDs). + stats.ClosedCount, err = applyIssuesOptions(sess, opts, issueIDs). And("issue.is_closed = ?", true). Count(new(Issue)) return stats, err } +func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int64) *xorm.Session { + if len(opts.RepoIDs) > 1 { + sess.In("issue.repo_id", opts.RepoIDs) + } else if len(opts.RepoIDs) == 1 { + sess.And("issue.repo_id = ?", opts.RepoIDs[0]) + } + + if len(issueIDs) > 0 { + sess.In("issue.id", issueIDs) + } + + applyLabelsCondition(sess, opts) + + applyMilestoneCondition(sess, opts) + + applyProjectCondition(sess, opts) + + if opts.AssigneeID > 0 { + applyAssigneeCondition(sess, opts.AssigneeID) + } else if opts.AssigneeID == db.NoConditionID { + sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") + } + + if opts.PosterID > 0 { + applyPosterCondition(sess, opts.PosterID) + } + + if opts.MentionedID > 0 { + applyMentionedCondition(sess, opts.MentionedID) + } + + if opts.ReviewRequestedID > 0 { + applyReviewRequestedCondition(sess, opts.ReviewRequestedID) + } + + if opts.ReviewedID > 0 { + applyReviewedCondition(sess, opts.ReviewedID) + } + + switch opts.IsPull { + case util.OptionalBoolTrue: + sess.And("issue.is_pull=?", true) + case util.OptionalBoolFalse: + sess.And("issue.is_pull=?", false) + } + + return sess +} + // GetUserIssueStats returns issue statistic information for dashboard by given conditions. func GetUserIssueStats(filterMode int, opts IssuesOptions) (*IssueStats, error) { if opts.User == nil { diff --git a/models/issues/label.go b/models/issues/label.go index 70906efb47..0087c933a6 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -113,10 +113,11 @@ func (l *Label) CalOpenIssues() { // SetArchived set the label as archived func (l *Label) SetArchived(isArchived bool) { - if isArchived && l.ArchivedUnix.IsZero() { - l.ArchivedUnix = timeutil.TimeStampNow() - } else { + if !isArchived { l.ArchivedUnix = timeutil.TimeStamp(0) + } else if isArchived && l.ArchivedUnix.IsZero() { + // Only change the date when it is newly archived. + l.ArchivedUnix = timeutil.TimeStampNow() } } diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index b8427bec4e..a0485ed8d4 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -130,6 +130,10 @@ type SearchRepoOptions struct { // True -> include just collaborative // False -> include just non-collaborative Collaborate util.OptionalBool + // What type of unit the user can be collaborative in, + // it is ignored if Collaborate is False. + // TypeInvalid means any unit type. + UnitType unit.Type // None -> include forks AND non-forks // True -> include just forks // False -> include just non-forks @@ -382,19 +386,25 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { if opts.Collaborate != util.OptionalBoolFalse { // A Collaboration is: - collaborateCond := builder.And( - // 1. Repository we don't own - builder.Neq{"owner_id": opts.OwnerID}, - // 2. But we can see because of: - builder.Or( - // A. We have unit independent access - UserAccessRepoCond("`repository`.id", opts.OwnerID), - // B. We are in a team for - UserOrgTeamRepoCond("`repository`.id", opts.OwnerID), - // C. Public repositories in organizations that we are member of - userOrgPublicRepoCondPrivate(opts.OwnerID), - ), - ) + + collaborateCond := builder.NewCond() + // 1. Repository we don't own + collaborateCond = collaborateCond.And(builder.Neq{"owner_id": opts.OwnerID}) + // 2. But we can see because of: + { + userAccessCond := builder.NewCond() + // A. We have unit independent access + userAccessCond = userAccessCond.Or(UserAccessRepoCond("`repository`.id", opts.OwnerID)) + // B. We are in a team for + if opts.UnitType == unit.TypeInvalid { + userAccessCond = userAccessCond.Or(UserOrgTeamRepoCond("`repository`.id", opts.OwnerID)) + } else { + userAccessCond = userAccessCond.Or(userOrgTeamUnitRepoCond("`repository`.id", opts.OwnerID, opts.UnitType)) + } + // C. Public repositories in organizations that we are member of + userAccessCond = userAccessCond.Or(userOrgPublicRepoCondPrivate(opts.OwnerID)) + collaborateCond = collaborateCond.And(userAccessCond) + } if !opts.Private { collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false)) } diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 6619949104..020659c82b 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -13,6 +13,7 @@ import ( db_model "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/indexer/issues/bleve" "code.gitea.io/gitea/modules/indexer/issues/db" @@ -277,7 +278,7 @@ func IsAvailable(ctx context.Context) bool { } // SearchOptions indicates the options for searching issues -type SearchOptions internal.SearchOptions +type SearchOptions = internal.SearchOptions const ( SortByCreatedDesc = internal.SortByCreatedDesc @@ -291,7 +292,6 @@ const ( ) // SearchIssues search issues by options. -// It returns issue ids and a bool value indicates if the result is imprecise. func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) { indexer := *globalIndexer.Load() @@ -305,7 +305,7 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err indexer = db.NewIndexer() } - result, err := indexer.Search(ctx, (*internal.SearchOptions)(opts)) + result, err := indexer.Search(ctx, opts) if err != nil { return nil, 0, err } @@ -317,3 +317,38 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err return ret, result.Total, nil } + +// CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count. +func CountIssues(ctx context.Context, opts *SearchOptions) (int64, error) { + opts = opts.Copy(func(options *SearchOptions) { opts.Paginator = &db_model.ListOptions{PageSize: 0} }) + + _, total, err := SearchIssues(ctx, opts) + return total, err +} + +// CountIssuesByRepo counts issues by options and group by repo id. +// It's not a complete implementation, since it requires the caller should provide the repo ids. +// That means opts.RepoIDs must be specified, and opts.AllPublic must be false. +// It's good enough for the current usage, and it can be improved if needed. +// TODO: use "group by" of the indexer engines to implement it. +func CountIssuesByRepo(ctx context.Context, opts *SearchOptions) (map[int64]int64, error) { + if len(opts.RepoIDs) == 0 { + return nil, fmt.Errorf("opts.RepoIDs must be specified") + } + if opts.AllPublic { + return nil, fmt.Errorf("opts.AllPublic must be false") + } + + repoIDs := container.SetOf(opts.RepoIDs...).Values() + ret := make(map[int64]int64, len(repoIDs)) + // TODO: it could be faster if do it in parallel for some indexer engines. Improve it if users report it's slow. + for _, repoID := range repoIDs { + count, err := CountIssues(ctx, opts.Copy(func(o *internal.SearchOptions) { o.RepoIDs = []int64{repoID} })) + if err != nil { + return nil, err + } + ret[repoID] = count + } + + return ret, nil +} diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 2de1e0e2bf..031745dd2f 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -109,6 +109,19 @@ type SearchOptions struct { SortBy SortBy // sort by field } +// Copy returns a copy of the options. +// Be careful, it's not a deep copy, so `SearchOptions.RepoIDs = {...}` is OK while `SearchOptions.RepoIDs[0] = ...` is not. +func (o *SearchOptions) Copy(edit ...func(options *SearchOptions)) *SearchOptions { + if o == nil { + return nil + } + v := *o + for _, e := range edit { + e(&v) + } + return &v +} + type SortBy string const ( diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 03c89bdab1..e560a88b4c 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -480,5 +480,5 @@ func DeleteAvatar(ctx *context.Context) { ctx.Flash.Error(err.Error()) } - ctx.Redirect(setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10)) + ctx.JSONRedirect(setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10)) } diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 5ae61c79be..957daab646 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -156,7 +156,7 @@ func SettingsDeleteAvatar(ctx *context.Context) { ctx.Flash.Error(err.Error()) } - ctx.Redirect(ctx.Org.OrgLink + "/settings") + ctx.JSONRedirect(ctx.Org.OrgLink + "/settings") } // SettingsDelete response for deleting an organization diff --git a/routers/web/repo/setting/avatar.go b/routers/web/repo/setting/avatar.go index ec673ca288..ae80f1db01 100644 --- a/routers/web/repo/setting/avatar.go +++ b/routers/web/repo/setting/avatar.go @@ -72,5 +72,5 @@ func SettingsDeleteAvatar(ctx *context.Context) { if err := repo_service.DeleteAvatar(ctx, ctx.Repo.Repository); err != nil { ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err)) } - ctx.Redirect(ctx.Repo.RepoLink + "/settings") + ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings") } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 8c1447f707..d1a4877e6d 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -448,21 +448,26 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // - Team org's owns the repository. // - Team has read permission to repository. repoOpts := &repo_model.SearchRepoOptions{ - Actor: ctx.Doer, - OwnerID: ctx.Doer.ID, - Private: true, - AllPublic: false, - AllLimited: false, + Actor: ctx.Doer, + OwnerID: ctx.Doer.ID, + Private: true, + AllPublic: false, + AllLimited: false, + Collaborate: util.OptionalBoolNone, + UnitType: unitType, + Archived: util.OptionalBoolFalse, } if team != nil { repoOpts.TeamID = team.ID } + accessibleRepos := container.Set[int64]{} { ids, _, err := repo_model.SearchRepositoryIDs(repoOpts) if err != nil { ctx.ServerError("SearchRepositoryIDs", err) return } + accessibleRepos.AddMultiple(ids...) opts.RepoIDs = ids if len(opts.RepoIDs) == 0 { // no repos found, don't let the indexer return all repos @@ -489,40 +494,16 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { keyword := strings.Trim(ctx.FormString("q"), " ") ctx.Data["Keyword"] = keyword - accessibleRepos := container.Set[int64]{} - { - ids, err := issues_model.GetRepoIDsForIssuesOptions(opts, ctxUser) - if err != nil { - ctx.ServerError("GetRepoIDsForIssuesOptions", err) - return - } - for _, id := range ids { - accessibleRepos.Add(id) - } - } - // Educated guess: Do or don't show closed issues. isShowClosed := ctx.FormString("state") == "closed" opts.IsClosed = util.OptionalBoolOf(isShowClosed) // Filter repos and count issues in them. Count will be used later. // USING NON-FINAL STATE OF opts FOR A QUERY. - var issueCountByRepo map[int64]int64 - { - issueIDs, err := issueIDsFromSearch(ctx, keyword, opts) - if err != nil { - ctx.ServerError("issueIDsFromSearch", err) - return - } - if len(issueIDs) > 0 { // else, no issues found, just leave issueCountByRepo empty - opts.IssueIDs = issueIDs - issueCountByRepo, err = issues_model.CountIssuesByRepo(ctx, opts) - if err != nil { - ctx.ServerError("CountIssuesByRepo", err) - return - } - opts.IssueIDs = nil // reset, the opts will be used later - } + issueCountByRepo, err := issue_indexer.CountIssuesByRepo(ctx, issue_indexer.ToSearchOptions(keyword, opts)) + if err != nil { + ctx.ServerError("CountIssuesByRepo", err) + return } // Make sure page number is at least 1. Will be posted to ctx.Data. @@ -551,13 +532,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // Parse ctx.FormString("repos") and remember matched repo IDs for later. // Gets set when clicking filters on the issues overview page. - repoIDs := getRepoIDs(ctx.FormString("repos")) - if len(repoIDs) > 0 { - // Remove repo IDs that are not accessible to the user. - repoIDs = util.SliceRemoveAllFunc(repoIDs, func(v int64) bool { - return !accessibleRepos.Contains(v) - }) - opts.RepoIDs = repoIDs + selectedRepoIDs := getRepoIDs(ctx.FormString("repos")) + // Remove repo IDs that are not accessible to the user. + selectedRepoIDs = util.SliceRemoveAllFunc(selectedRepoIDs, func(v int64) bool { + return !accessibleRepos.Contains(v) + }) + if len(selectedRepoIDs) > 0 { + opts.RepoIDs = selectedRepoIDs } // ------------------------------ @@ -568,7 +549,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // USING FINAL STATE OF opts FOR A QUERY. var issues issues_model.IssueList { - issueIDs, err := issueIDsFromSearch(ctx, keyword, opts) + issueIDs, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts)) if err != nil { ctx.ServerError("issueIDsFromSearch", err) return @@ -584,6 +565,18 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // Add repository pointers to Issues. // ---------------------------------- + // Remove repositories that should not be shown, + // which are repositories that have no issues and are not selected by the user. + selectedReposMap := make(map[int64]struct{}, len(selectedRepoIDs)) + for _, repoID := range selectedRepoIDs { + selectedReposMap[repoID] = struct{}{} + } + for k, v := range issueCountByRepo { + if _, ok := selectedReposMap[k]; !ok && v == 0 { + delete(issueCountByRepo, k) + } + } + // showReposMap maps repository IDs to their Repository pointers. showReposMap, err := loadRepoByIDs(ctxUser, issueCountByRepo, unitType) if err != nil { @@ -615,44 +608,10 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // ------------------------------- // Fill stats to post to ctx.Data. // ------------------------------- - var issueStats *issues_model.IssueStats - { - statsOpts := issues_model.IssuesOptions{ - RepoIDs: repoIDs, - User: ctx.Doer, - IsPull: util.OptionalBoolOf(isPullList), - IsClosed: util.OptionalBoolOf(isShowClosed), - IssueIDs: nil, - IsArchived: util.OptionalBoolFalse, - LabelIDs: opts.LabelIDs, - Org: org, - Team: team, - RepoCond: opts.RepoCond, - } - - if keyword != "" { - statsOpts.RepoIDs = opts.RepoIDs - allIssueIDs, err := issueIDsFromSearch(ctx, keyword, &statsOpts) - if err != nil { - ctx.ServerError("issueIDsFromSearch", err) - return - } - statsOpts.IssueIDs = allIssueIDs - } - - if keyword != "" && len(statsOpts.IssueIDs) == 0 { - // So it did search with the keyword, but no issue found. - // Just set issueStats to empty. - issueStats = &issues_model.IssueStats{} - } else { - // So it did search with the keyword, and found some issues. It needs to get issueStats of these issues. - // Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts. - issueStats, err = issues_model.GetUserIssueStats(filterMode, statsOpts) - if err != nil { - ctx.ServerError("GetUserIssueStats", err) - return - } - } + issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts), ctx.Doer.ID) + if err != nil { + ctx.ServerError("getUserIssueStats", err) + return } // Will be posted to ctx.Data. @@ -722,7 +681,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { ctx.Data["IssueStats"] = issueStats ctx.Data["ViewType"] = viewType ctx.Data["SortType"] = sortType - ctx.Data["RepoIDs"] = opts.RepoIDs + ctx.Data["RepoIDs"] = selectedRepoIDs ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["SelectLabels"] = selectedLabels @@ -777,14 +736,6 @@ func getRepoIDs(reposQuery string) []int64 { return repoIDs } -func issueIDsFromSearch(ctx *context.Context, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) { - ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts)) - if err != nil { - return nil, fmt.Errorf("SearchIssues: %w", err) - } - return ids, nil -} - func loadRepoByIDs(ctxUser *user_model.User, issueCountByRepo map[int64]int64, unitType unit.Type) (map[int64]*repo_model.Repository, error) { totalRes := make(map[int64]*repo_model.Repository, len(issueCountByRepo)) repoIDs := make([]int64, 0, 500) @@ -913,3 +864,71 @@ func UsernameSubRoute(ctx *context.Context) { } } } + +func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions, doerID int64) (*issues_model.IssueStats, error) { + opts = opts.Copy(func(o *issue_indexer.SearchOptions) { + o.AssigneeID = nil + o.PosterID = nil + o.MentionID = nil + o.ReviewRequestedID = nil + o.ReviewedID = nil + }) + + var ( + err error + ret = &issues_model.IssueStats{} + ) + + { + openClosedOpts := opts.Copy() + switch filterMode { + case issues_model.FilterModeAll, issues_model.FilterModeYourRepositories: + case issues_model.FilterModeAssign: + openClosedOpts.AssigneeID = &doerID + case issues_model.FilterModeCreate: + openClosedOpts.PosterID = &doerID + case issues_model.FilterModeMention: + openClosedOpts.MentionID = &doerID + case issues_model.FilterModeReviewRequested: + openClosedOpts.ReviewRequestedID = &doerID + case issues_model.FilterModeReviewed: + openClosedOpts.ReviewedID = &doerID + } + openClosedOpts.IsClosed = util.OptionalBoolFalse + ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts) + if err != nil { + return nil, err + } + openClosedOpts.IsClosed = util.OptionalBoolTrue + ret.ClosedCount, err = issue_indexer.CountIssues(ctx, openClosedOpts) + if err != nil { + return nil, err + } + } + + ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts) + if err != nil { + return nil, err + } + ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = &doerID })) + if err != nil { + return nil, err + } + ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = &doerID })) + if err != nil { + return nil, err + } + ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = &doerID })) + if err != nil { + return nil, err + } + ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = &doerID })) + if err != nil { + return nil, err + } + ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = &doerID })) + if err != nil { + return nil, err + } + return ret, nil +} diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index ab7d2e58b3..61089d0947 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -194,7 +194,7 @@ func DeleteAvatar(ctx *context.Context) { ctx.Flash.Error(err.Error()) } - ctx.Redirect(setting.AppSubURL + "/user/settings") + ctx.JSONRedirect(setting.AppSubURL + "/user/settings") } // Organization render all the organization of the user diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 4fea418e6f..e99a4532d3 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -186,7 +186,7 @@
- +
diff --git a/templates/devtest/flex-list.tmpl b/templates/devtest/flex-list.tmpl index 37f3f04004..f9087a5714 100644 --- a/templates/devtest/flex-list.tmpl +++ b/templates/devtest/flex-list.tmpl @@ -48,6 +48,7 @@
Very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong content + Truncate very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong content
diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 683b886467..03827e4c3f 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -94,7 +94,7 @@
- +
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl index 4ec2fd557c..a011111d5b 100644 --- a/templates/repo/branch_dropdown.tmpl +++ b/templates/repo/branch_dropdown.tmpl @@ -67,7 +67,7 @@
{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}} - diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index aad44f57df..6bfd8b12be 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -80,25 +80,21 @@ {{end}}
{{if or (eq .GetOpType 5) (eq .GetOpType 18)}} -
- {{$push := ActionContent2Commits .}} - {{$repoLink := .GetRepoLink}} - {{range $push.Commits}} - {{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}} -
- - {{ShortSha .Sha1}} - - {{RenderCommitMessage $.Context .Message $repoLink $.ComposeMetas}} - -
- {{end}} - {{if and (gt $push.Len 1) $push.CompareURL}} - - {{end}} -
+ {{$push := ActionContent2Commits .}} + {{$repoLink := .GetRepoLink}} + {{range $push.Commits}} + {{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}} +
+ + {{ShortSha .Sha1}} + + {{RenderCommitMessage $.Context .Message $repoLink $.ComposeMetas}} + +
+ {{end}} + {{if and (gt $push.Len 1) $push.CompareURL}} + {{$.locale.Tr "action.compare_commits" $push.Len}} ยป + {{end}} {{else if eq .GetOpType 6}} {{index .GetIssueInfos 1 | RenderEmoji $.Context | RenderCodeBlock}} {{else if eq .GetOpType 7}} diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 8d6cc67afe..a89098c6ab 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -5,29 +5,29 @@
diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css index 1eb480845b..402eb7b34b 100644 --- a/web_src/css/dashboard.css +++ b/web_src/css/dashboard.css @@ -96,10 +96,6 @@ } } -.feeds .commit-id { - font-family: var(--fonts-monospace); -} - .feeds code { padding: 2px 4px; border-radius: 3px; diff --git a/web_src/css/shared/flex-list.css b/web_src/css/shared/flex-list.css index ec22fbba9e..c73f78ebfe 100644 --- a/web_src/css/shared/flex-list.css +++ b/web_src/css/shared/flex-list.css @@ -29,7 +29,8 @@ display: flex; flex-direction: column; flex-grow: 1; - flex-basis: 60%; + flex-basis: 60%; /* avoid wrapping the "flex-item-trailing" too aggressively */ + min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */ } .flex-item-header { @@ -66,6 +67,7 @@ font-size: 16px; font-weight: var(--font-weight-semibold); word-break: break-word; + min-width: 0; } .flex-item .flex-item-title a { diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index 488f80a76f..07fbd634fd 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -1,5 +1,5 @@