mirror of
https://github.com/go-gitea/gitea
synced 2025-01-02 22:14:26 +00:00
[Feature] Private README.md for organization (#32872)
Implemented #29503 --------- Co-authored-by: Ben Chang <ben_chang@htc.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
c09656e0e0
commit
0387195abb
@ -43,19 +43,20 @@ type contextKey struct {
|
||||
}
|
||||
|
||||
// RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it
|
||||
// The caller must call "defer gitRepo.Close()"
|
||||
func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) {
|
||||
ds := reqctx.GetRequestDataStore(ctx)
|
||||
if ds != nil {
|
||||
gitRepo, err := RepositoryFromRequestContextOrOpen(ctx, ds, repo)
|
||||
reqCtx := reqctx.FromContext(ctx)
|
||||
if reqCtx != nil {
|
||||
gitRepo, err := RepositoryFromRequestContextOrOpen(reqCtx, repo)
|
||||
return gitRepo, util.NopCloser{}, err
|
||||
}
|
||||
gitRepo, err := OpenRepository(ctx, repo)
|
||||
return gitRepo, gitRepo, err
|
||||
}
|
||||
|
||||
// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context
|
||||
// The repo will be automatically closed when the request context is done
|
||||
func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDataStore, repo Repository) (*git.Repository, error) {
|
||||
// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context.
|
||||
// Caller shouldn't close the git repo manually, the git repo will be automatically closed when the request context is done.
|
||||
func RepositoryFromRequestContextOrOpen(ctx reqctx.RequestContext, repo Repository) (*git.Repository, error) {
|
||||
ck := contextKey{repoPath: repoPath(repo)}
|
||||
if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok {
|
||||
return gitRepo, nil
|
||||
@ -64,7 +65,7 @@ func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDa
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ds.AddCloser(gitRepo)
|
||||
ds.SetContextValue(ck, gitRepo)
|
||||
ctx.AddCloser(gitRepo)
|
||||
ctx.SetContextValue(ck, gitRepo)
|
||||
return gitRepo, nil
|
||||
}
|
||||
|
@ -88,6 +88,21 @@ func (r *requestDataStore) cleanUp() {
|
||||
}
|
||||
}
|
||||
|
||||
type RequestContext interface {
|
||||
context.Context
|
||||
RequestDataStore
|
||||
}
|
||||
|
||||
func FromContext(ctx context.Context) RequestContext {
|
||||
// here we must use the current ctx and the underlying store
|
||||
// the current ctx guarantees that the ctx deadline/cancellation/values are respected
|
||||
// the underlying store guarantees that the request-specific data is available
|
||||
if store := GetRequestDataStore(ctx); store != nil {
|
||||
return &requestContext{Context: ctx, RequestDataStore: store}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetRequestDataStore(ctx context.Context) RequestDataStore {
|
||||
if req, ok := ctx.Value(RequestDataStoreKey).(*requestDataStore); ok {
|
||||
return req
|
||||
@ -97,11 +112,11 @@ func GetRequestDataStore(ctx context.Context) RequestDataStore {
|
||||
|
||||
type requestContext struct {
|
||||
context.Context
|
||||
dataStore *requestDataStore
|
||||
RequestDataStore
|
||||
}
|
||||
|
||||
func (c *requestContext) Value(key any) any {
|
||||
if v := c.dataStore.GetContextValue(key); v != nil {
|
||||
if v := c.GetContextValue(key); v != nil {
|
||||
return v
|
||||
}
|
||||
return c.Context.Value(key)
|
||||
@ -109,9 +124,10 @@ func (c *requestContext) Value(key any) any {
|
||||
|
||||
func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Context, finished func()) {
|
||||
ctx, _, processFinished := process.GetManager().AddTypedContext(parentCtx, profDesc, process.RequestProcessType, true)
|
||||
reqCtx := &requestContext{Context: ctx, dataStore: &requestDataStore{values: make(map[any]any)}}
|
||||
store := &requestDataStore{values: make(map[any]any)}
|
||||
reqCtx := &requestContext{Context: ctx, RequestDataStore: store}
|
||||
return reqCtx, func() {
|
||||
reqCtx.dataStore.cleanUp()
|
||||
store.cleanUp()
|
||||
processFinished()
|
||||
}
|
||||
}
|
||||
@ -119,5 +135,5 @@ func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Co
|
||||
// NewRequestContextForTest creates a new RequestContext for testing purposes
|
||||
// It doesn't add the context to the process manager, nor do cleanup
|
||||
func NewRequestContextForTest(parentCtx context.Context) context.Context {
|
||||
return &requestContext{Context: parentCtx, dataStore: &requestDataStore{values: make(map[any]any)}}
|
||||
return &requestContext{Context: parentCtx, RequestDataStore: &requestDataStore{values: make(map[any]any)}}
|
||||
}
|
||||
|
@ -264,22 +264,42 @@ func userThemeName(user *user_model.User) string {
|
||||
return setting.UI.DefaultTheme
|
||||
}
|
||||
|
||||
func isQueryParamEmpty(v any) bool {
|
||||
return v == nil || v == false || v == 0 || v == int64(0) || v == ""
|
||||
}
|
||||
|
||||
// QueryBuild builds a query string from a list of key-value pairs.
|
||||
// It omits the nil and empty strings, but it doesn't omit other zero values,
|
||||
// because the zero value of number types may have a meaning.
|
||||
// It omits the nil, false, zero int/int64 and empty string values,
|
||||
// because they are default empty values for "ctx.FormXxx" calls.
|
||||
// If 0 or false need to be included, use string values: "0" and "false".
|
||||
// Build rules:
|
||||
// * Even parameters: always build as query string: a=b&c=d
|
||||
// * Odd parameters:
|
||||
// * * {"/anything", param-pairs...} => "/?param-paris"
|
||||
// * * {"anything?old-params", new-param-pairs...} => "anything?old-params&new-param-paris"
|
||||
// * * Otherwise: {"old¶ms", new-param-pairs...} => "old¶ms&new-param-paris"
|
||||
// * * Other behaviors are undefined yet.
|
||||
func QueryBuild(a ...any) template.URL {
|
||||
var s string
|
||||
var reqPath, s string
|
||||
hasTrailingSep := false
|
||||
if len(a)%2 == 1 {
|
||||
if v, ok := a[0].(string); ok {
|
||||
if v == "" || (v[0] != '?' && v[0] != '&') {
|
||||
panic("QueryBuild: invalid argument")
|
||||
}
|
||||
s = v
|
||||
} else if v, ok := a[0].(template.URL); ok {
|
||||
s = string(v)
|
||||
} else {
|
||||
panic("QueryBuild: invalid argument")
|
||||
}
|
||||
hasTrailingSep = s != "&" && strings.HasSuffix(s, "&")
|
||||
if strings.HasPrefix(s, "/") || strings.Contains(s, "?") {
|
||||
if s1, s2, ok := strings.Cut(s, "?"); ok {
|
||||
reqPath = s1 + "?"
|
||||
s = s2
|
||||
} else {
|
||||
reqPath += s + "?"
|
||||
s = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := len(a) % 2; i < len(a); i += 2 {
|
||||
k, ok := a[i].(string)
|
||||
@ -290,19 +310,16 @@ func QueryBuild(a ...any) template.URL {
|
||||
if va, ok := a[i+1].(string); ok {
|
||||
v = va
|
||||
} else if a[i+1] != nil {
|
||||
v = fmt.Sprint(a[i+1])
|
||||
if !isQueryParamEmpty(a[i+1]) {
|
||||
v = fmt.Sprint(a[i+1])
|
||||
}
|
||||
}
|
||||
// pos1 to pos2 is the "k=v&" part, "&" is optional
|
||||
pos1 := strings.Index(s, "&"+k+"=")
|
||||
if pos1 != -1 {
|
||||
pos1++
|
||||
} else {
|
||||
pos1 = strings.Index(s, "?"+k+"=")
|
||||
if pos1 != -1 {
|
||||
pos1++
|
||||
} else if strings.HasPrefix(s, k+"=") {
|
||||
pos1 = 0
|
||||
}
|
||||
} else if strings.HasPrefix(s, k+"=") {
|
||||
pos1 = 0
|
||||
}
|
||||
pos2 := len(s)
|
||||
if pos1 == -1 {
|
||||
@ -315,7 +332,7 @@ func QueryBuild(a ...any) template.URL {
|
||||
}
|
||||
if v != "" {
|
||||
sep := ""
|
||||
hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && (s[pos1-1] == '?' || s[pos1-1] == '&'))
|
||||
hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && s[pos1-1] == '&')
|
||||
if !hasPrefixSep {
|
||||
sep = "&"
|
||||
}
|
||||
@ -324,9 +341,22 @@ func QueryBuild(a ...any) template.URL {
|
||||
s = s[:pos1] + s[pos2:]
|
||||
}
|
||||
}
|
||||
if s != "" && s != "&" && s[len(s)-1] == '&' {
|
||||
if s != "" && s[len(s)-1] == '&' && !hasTrailingSep {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
if reqPath != "" {
|
||||
if s == "" {
|
||||
s = reqPath
|
||||
if s != "?" {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
} else {
|
||||
if s[0] == '&' {
|
||||
s = s[1:]
|
||||
}
|
||||
s = reqPath + s
|
||||
}
|
||||
}
|
||||
return template.URL(s)
|
||||
}
|
||||
|
||||
|
@ -118,3 +118,58 @@ func TestTemplateEscape(t *testing.T) {
|
||||
assert.Equal(t, `<a k="""><></a>`, actual)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryBuild(t *testing.T) {
|
||||
t.Run("construct", func(t *testing.T) {
|
||||
assert.Equal(t, "", string(QueryBuild()))
|
||||
assert.Equal(t, "", string(QueryBuild("a", nil, "b", false, "c", 0, "d", "")))
|
||||
assert.Equal(t, "a=1&b=true", string(QueryBuild("a", 1, "b", "true")))
|
||||
|
||||
// path with query parameters
|
||||
assert.Equal(t, "/?k=1", string(QueryBuild("/", "k", 1)))
|
||||
assert.Equal(t, "/", string(QueryBuild("/?k=a", "k", 0)))
|
||||
|
||||
// no path but question mark with query parameters
|
||||
assert.Equal(t, "?k=1", string(QueryBuild("?", "k", 1)))
|
||||
assert.Equal(t, "?", string(QueryBuild("?", "k", 0)))
|
||||
assert.Equal(t, "path?k=1", string(QueryBuild("path?", "k", 1)))
|
||||
assert.Equal(t, "path", string(QueryBuild("path?", "k", 0)))
|
||||
|
||||
// only query parameters
|
||||
assert.Equal(t, "&k=1", string(QueryBuild("&", "k", 1)))
|
||||
assert.Equal(t, "", string(QueryBuild("&", "k", 0)))
|
||||
assert.Equal(t, "", string(QueryBuild("&k=a", "k", 0)))
|
||||
assert.Equal(t, "", string(QueryBuild("k=a&", "k", 0)))
|
||||
assert.Equal(t, "a=1&b=2", string(QueryBuild("a=1", "b", 2)))
|
||||
assert.Equal(t, "&a=1&b=2", string(QueryBuild("&a=1", "b", 2)))
|
||||
assert.Equal(t, "a=1&b=2&", string(QueryBuild("a=1&", "b", 2)))
|
||||
})
|
||||
|
||||
t.Run("replace", func(t *testing.T) {
|
||||
assert.Equal(t, "a=1&c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "a", 1)))
|
||||
assert.Equal(t, "a=b&c=1&e=f", string(QueryBuild("a=b&c=d&e=f", "c", 1)))
|
||||
assert.Equal(t, "a=b&c=d&e=1", string(QueryBuild("a=b&c=d&e=f", "e", 1)))
|
||||
assert.Equal(t, "a=b&c=d&e=f&k=1", string(QueryBuild("a=b&c=d&e=f", "k", 1)))
|
||||
})
|
||||
|
||||
t.Run("replace-&", func(t *testing.T) {
|
||||
assert.Equal(t, "&a=1&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "a", 1)))
|
||||
assert.Equal(t, "&a=b&c=1&e=f", string(QueryBuild("&a=b&c=d&e=f", "c", 1)))
|
||||
assert.Equal(t, "&a=b&c=d&e=1", string(QueryBuild("&a=b&c=d&e=f", "e", 1)))
|
||||
assert.Equal(t, "&a=b&c=d&e=f&k=1", string(QueryBuild("&a=b&c=d&e=f", "k", 1)))
|
||||
})
|
||||
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
assert.Equal(t, "c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "a", "")))
|
||||
assert.Equal(t, "a=b&e=f", string(QueryBuild("a=b&c=d&e=f", "c", "")))
|
||||
assert.Equal(t, "a=b&c=d", string(QueryBuild("a=b&c=d&e=f", "e", "")))
|
||||
assert.Equal(t, "a=b&c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "k", "")))
|
||||
})
|
||||
|
||||
t.Run("delete-&", func(t *testing.T) {
|
||||
assert.Equal(t, "&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "a", "")))
|
||||
assert.Equal(t, "&a=b&e=f", string(QueryBuild("&a=b&c=d&e=f", "c", "")))
|
||||
assert.Equal(t, "&a=b&c=d", string(QueryBuild("&a=b&c=d&e=f", "e", "")))
|
||||
assert.Equal(t, "&a=b&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "k", "")))
|
||||
})
|
||||
}
|
||||
|
@ -203,6 +203,7 @@ func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool
|
||||
//
|
||||
// Slice does not include given path itself.
|
||||
// If subdirectories is enabled, they will have suffix '/'.
|
||||
// FIXME: it doesn't like dot-files, for example: "owner/.profile.git"
|
||||
func StatDir(rootPath string, includeDir ...bool) ([]string, error) {
|
||||
if isDir, err := IsDir(rootPath); err != nil {
|
||||
return nil, err
|
||||
|
@ -1015,7 +1015,9 @@ new_repo_helper = A repository contains all project files, including revision hi
|
||||
owner = Owner
|
||||
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.
|
||||
repo_name = Repository Name
|
||||
repo_name_helper = Good repository names use short, memorable and unique keywords.
|
||||
repo_name_profile_public_hint= .profile is a special repository that you can use to add README.md to your public organization profile, visible to anyone. Make sure it’s public and initialize it with a README in the profile directory to get started.
|
||||
repo_name_profile_private_hint = .profile-private is a special repository that you can use to add a README.md to your organization member profile, visible only to organization members. Make sure it’s private and initialize it with a README in the profile directory to get started.
|
||||
repo_name_helper = Good repository names use short, memorable and unique keywords. A repository named '.profile' or '.profile-private' could be used to add a README.md for the user/organization profile.
|
||||
repo_size = Repository Size
|
||||
template = Template
|
||||
template_select = Select a template.
|
||||
@ -2862,6 +2864,10 @@ teams.invite.title = You have been invited to join team <strong>%s</strong> in o
|
||||
teams.invite.by = Invited by %s
|
||||
teams.invite.description = Please click the button below to join the team.
|
||||
|
||||
view_as_role = View as: %s
|
||||
view_as_public_hint = You are viewing the README a public user.
|
||||
view_as_member_hint = You are viewing the README a member of this organization.
|
||||
|
||||
[admin]
|
||||
maintenance = Maintenance
|
||||
dashboard = Dashboard
|
||||
|
@ -729,7 +729,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
if !isPlainRule {
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository)
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
return
|
||||
@ -1057,7 +1057,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
if !isPlainRule {
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository)
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
return
|
||||
|
@ -45,7 +45,7 @@ func CompareDiff(ctx *context.APIContext) {
|
||||
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
var err error
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository)
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
return
|
||||
|
@ -29,7 +29,7 @@ func DownloadArchive(ctx *context.APIContext) {
|
||||
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
var err error
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository)
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
return
|
||||
|
@ -282,7 +282,7 @@ func GetArchive(ctx *context.APIContext) {
|
||||
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
var err error
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository)
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
return
|
||||
|
@ -726,7 +726,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
||||
|
||||
if ctx.Repo.GitRepo == nil && !repo.IsEmpty {
|
||||
var err error
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo)
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err)
|
||||
return err
|
||||
|
@ -27,7 +27,7 @@ func RepoAssignment(ctx *gitea_context.PrivateContext) {
|
||||
return
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo)
|
||||
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -21,9 +22,7 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplOrgHome templates.TplName = "org/home"
|
||||
)
|
||||
const tplOrgHome templates.TplName = "org/home"
|
||||
|
||||
// Home show organization home page
|
||||
func Home(ctx *context.Context) {
|
||||
@ -110,15 +109,19 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
|
||||
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
|
||||
|
||||
if !prepareOrgProfileReadme(ctx, viewRepositories) {
|
||||
ctx.Data["PageIsViewRepositories"] = true
|
||||
prepareResult, err := shared_user.PrepareOrgHeader(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("PrepareOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
repos []*repo_model.Repository
|
||||
count int64
|
||||
)
|
||||
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
||||
// if no profile readme, it still means "view repositories"
|
||||
isViewOverview := !viewRepositories && prepareOrgProfileReadme(ctx, prepareResult)
|
||||
ctx.Data["PageIsViewRepositories"] = !isViewOverview
|
||||
ctx.Data["PageIsViewOverview"] = isViewOverview
|
||||
ctx.Data["ShowOrgProfileReadmeSelector"] = isViewOverview && prepareResult.ProfilePublicReadmeBlob != nil && prepareResult.ProfilePrivateReadmeBlob != nil
|
||||
|
||||
repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.User.RepoPagingNum,
|
||||
Page: page,
|
||||
@ -151,28 +154,45 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
ctx.HTML(http.StatusOK, tplOrgHome)
|
||||
}
|
||||
|
||||
func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool {
|
||||
profileDbRepo, profileGitRepo, profileReadme, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer)
|
||||
defer profileClose()
|
||||
ctx.Data["HasProfileReadme"] = profileReadme != nil
|
||||
func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.PrepareOrgHeaderResult) bool {
|
||||
viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public"))
|
||||
viewAsMember := viewAs == "member"
|
||||
|
||||
if profileGitRepo == nil || profileReadme == nil || viewRepositories {
|
||||
var profileRepo *repo_model.Repository
|
||||
var readmeBlob *git.Blob
|
||||
if viewAsMember {
|
||||
if prepareResult.ProfilePrivateReadmeBlob != nil {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob
|
||||
} else {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob
|
||||
viewAsMember = false
|
||||
}
|
||||
} else {
|
||||
if prepareResult.ProfilePublicReadmeBlob != nil {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob
|
||||
} else {
|
||||
profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob
|
||||
viewAsMember = true
|
||||
}
|
||||
}
|
||||
if readmeBlob == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
|
||||
log.Error("failed to GetBlobContent: %v", err)
|
||||
} else {
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
||||
})
|
||||
if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
|
||||
log.Error("failed to RenderString: %v", err)
|
||||
} else {
|
||||
ctx.Data["ProfileReadme"] = profileContent
|
||||
}
|
||||
readmeBytes, err := readmeBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err != nil {
|
||||
log.Error("failed to GetBlobContent for profile %q (view as %q) readme: %v", profileRepo.FullName(), viewAs, err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Data["PageIsViewOverview"] = true
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileRepo, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileRepo.DefaultBranch)),
|
||||
})
|
||||
ctx.Data["ProfileReadmeContent"], err = markdown.RenderString(rctx, readmeBytes)
|
||||
if err != nil {
|
||||
log.Error("failed to GetBlobContent for profile %q (view as %q) readme: %v", profileRepo.FullName(), viewAs, err)
|
||||
return false
|
||||
}
|
||||
ctx.Data["IsViewingOrgAsMember"] = viewAsMember
|
||||
return true
|
||||
}
|
||||
|
@ -54,9 +54,9 @@ func Members(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = shared_user.RenderOrgHeader(ctx)
|
||||
_, err = shared_user.PrepareOrgHeader(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderOrgHeader", err)
|
||||
ctx.ServerError("PrepareOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -58,9 +58,9 @@ func Teams(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["Teams"] = ctx.Org.Teams
|
||||
|
||||
err := shared_user.RenderOrgHeader(ctx)
|
||||
_, err := shared_user.PrepareOrgHeader(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderOrgHeader", err)
|
||||
ctx.ServerError("PrepareOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
@ -102,37 +103,46 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
|
||||
profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ".profile")
|
||||
if err == nil {
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, profileDbRepo, doer)
|
||||
if err == nil && !profileDbRepo.IsEmpty && perm.CanRead(unit.TypeCode) {
|
||||
if profileGitRepo, err = gitrepo.OpenRepository(ctx, profileDbRepo); err != nil {
|
||||
log.Error("FindUserProfileReadme failed to OpenRepository: %v", err)
|
||||
} else {
|
||||
if commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch); err != nil {
|
||||
log.Error("FindUserProfileReadme failed to GetBranchCommit: %v", err)
|
||||
} else {
|
||||
profileReadmeBlob, _ = commit.GetBlobByPath("README.md")
|
||||
}
|
||||
}
|
||||
func FindOwnerProfileReadme(ctx *context.Context, doer *user_model.User, optProfileRepoName ...string) (profileDbRepo *repo_model.Repository, profileReadmeBlob *git.Blob) {
|
||||
profileRepoName := util.OptionalArg(optProfileRepoName, RepoNameProfile)
|
||||
profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, profileRepoName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err)
|
||||
}
|
||||
} else if !repo_model.IsErrRepoNotExist(err) {
|
||||
log.Error("FindUserProfileReadme failed to GetRepositoryByName: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
return profileDbRepo, profileGitRepo, profileReadmeBlob, func() {
|
||||
if profileGitRepo != nil {
|
||||
_ = profileGitRepo.Close()
|
||||
}
|
||||
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, profileDbRepo, doer)
|
||||
if err != nil {
|
||||
log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
if profileDbRepo.IsEmpty || !perm.CanRead(unit.TypeCode) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
profileGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, profileDbRepo)
|
||||
if err != nil {
|
||||
log.Error("FindOwnerProfileReadme failed to OpenRepository: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch)
|
||||
if err != nil {
|
||||
log.Error("FindOwnerProfileReadme failed to GetBranchCommit: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
profileReadmeBlob, _ = commit.GetBlobByPath("README.md") // no need to handle this error
|
||||
return profileDbRepo, profileReadmeBlob
|
||||
}
|
||||
|
||||
func RenderUserHeader(ctx *context.Context) {
|
||||
prepareContextForCommonProfile(ctx)
|
||||
|
||||
_, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer)
|
||||
defer profileClose()
|
||||
ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil
|
||||
_, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
|
||||
ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
|
||||
}
|
||||
|
||||
func LoadHeaderCount(ctx *context.Context) error {
|
||||
@ -169,14 +179,28 @@ func LoadHeaderCount(ctx *context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func RenderOrgHeader(ctx *context.Context) error {
|
||||
if err := LoadHeaderCount(ctx); err != nil {
|
||||
return err
|
||||
const (
|
||||
RepoNameProfilePrivate = ".profile-private"
|
||||
RepoNameProfile = ".profile"
|
||||
)
|
||||
|
||||
type PrepareOrgHeaderResult struct {
|
||||
ProfilePublicRepo *repo_model.Repository
|
||||
ProfilePublicReadmeBlob *git.Blob
|
||||
ProfilePrivateRepo *repo_model.Repository
|
||||
ProfilePrivateReadmeBlob *git.Blob
|
||||
HasOrgProfileReadme bool
|
||||
}
|
||||
|
||||
func PrepareOrgHeader(ctx *context.Context) (result *PrepareOrgHeaderResult, err error) {
|
||||
if err = LoadHeaderCount(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer)
|
||||
defer profileClose()
|
||||
ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil
|
||||
|
||||
return nil
|
||||
result = &PrepareOrgHeaderResult{}
|
||||
result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer)
|
||||
result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
|
||||
result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
|
||||
ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
|
||||
return result, nil
|
||||
}
|
||||
|
@ -74,8 +74,7 @@ func userProfile(ctx *context.Context) {
|
||||
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
|
||||
}
|
||||
|
||||
profileDbRepo, _ /*profileGitRepo*/, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer)
|
||||
defer profileClose()
|
||||
profileDbRepo, profileReadmeBlob := shared_user.FindOwnerProfileReadme(ctx, ctx.Doer)
|
||||
|
||||
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||
prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileReadmeBlob)
|
||||
@ -96,7 +95,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
|
||||
}
|
||||
}
|
||||
ctx.Data["TabName"] = tab
|
||||
ctx.Data["HasProfileReadme"] = profileReadme != nil
|
||||
ctx.Data["HasUserProfileReadme"] = profileReadme != nil
|
||||
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 0 {
|
||||
@ -254,7 +253,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
|
||||
if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
|
||||
log.Error("failed to RenderString: %v", err)
|
||||
} else {
|
||||
ctx.Data["ProfileReadme"] = profileContent
|
||||
ctx.Data["ProfileReadmeContent"] = profileContent
|
||||
}
|
||||
}
|
||||
case "organizations":
|
||||
|
@ -274,7 +274,7 @@ func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) {
|
||||
// For API calls.
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
var err error
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository)
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err)
|
||||
return
|
||||
|
@ -8,11 +8,16 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// FormString returns the first value matching the provided key in the form as a string
|
||||
func (b *Base) FormString(key string) string {
|
||||
return b.Req.FormValue(key)
|
||||
func (b *Base) FormString(key string, def ...string) string {
|
||||
s := b.Req.FormValue(key)
|
||||
if s == "" {
|
||||
s = util.OptionalArg(def)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// FormStrings returns a string slice for the provided key from the form
|
||||
|
@ -622,7 +622,7 @@ func RepoAssignment(ctx *Context) {
|
||||
ctx.Repo.GitRepo = nil
|
||||
}
|
||||
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo)
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") {
|
||||
log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err)
|
||||
@ -881,7 +881,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
|
||||
)
|
||||
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository)
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError(fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err)
|
||||
return
|
||||
|
@ -5,8 +5,8 @@
|
||||
<div class="ui container">
|
||||
<div class="ui mobile reversed stackable grid">
|
||||
<div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column">
|
||||
{{if .ProfileReadme}}
|
||||
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
|
||||
{{if .ProfileReadmeContent}}
|
||||
<div id="readme_profile" class="markup" data-profile-view-as-member="{{.IsViewingOrgAsMember}}">{{.ProfileReadmeContent}}</div>
|
||||
{{end}}
|
||||
{{template "shared/repo_search" .}}
|
||||
{{template "explore/repo_list" .}}
|
||||
@ -24,6 +24,29 @@
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
{{end}}
|
||||
|
||||
{{if and .ShowMemberAndTeamTab .ShowOrgProfileReadmeSelector}}
|
||||
<div class="tw-my-4">
|
||||
<div id="org-home-view-as-dropdown" class="ui dropdown jump">
|
||||
{{- $viewAsRole := Iif (.IsViewingOrgAsMember) (ctx.Locale.Tr "org.members.member") (ctx.Locale.Tr "settings.visibility.public") -}}
|
||||
<span class="text">{{svg "octicon-eye"}} {{ctx.Locale.Tr "org.view_as_role" $viewAsRole}}</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
{{/* TODO: does it really need to use CurrentURL with query parameters? Why not construct a new link with clear parameters */}}
|
||||
<a href="?view_as=public" class="item {{if not .IsViewingOrgAsMember}}selected{{end}}">
|
||||
{{svg "octicon-check" 14 (Iif (not .IsViewingOrgAsMember) "" "tw-invisible")}} {{ctx.Locale.Tr "settings.visibility.public"}}
|
||||
</a>
|
||||
<a href="?view_as=member" class="item {{if .IsViewingOrgAsMember}}selected{{end}}">
|
||||
{{svg "octicon-check" 14 (Iif .IsViewingOrgAsMember "" "tw-invisible")}} {{ctx.Locale.Tr "org.members.member"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-my-2">
|
||||
{{if .IsViewingOrgAsMember}}{{ctx.Locale.Tr "org.view_as_member_hint"}}{{else}}{{ctx.Locale.Tr "org.view_as_public_hint"}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .NumMembers}}
|
||||
<h4 class="ui top attached header tw-flex">
|
||||
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.members"}}</strong>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<div class="ui container">
|
||||
<overflow-menu class="ui secondary pointing tabular borderless menu tw-mb-4">
|
||||
<div class="overflow-menu-items">
|
||||
{{if .HasProfileReadme}}
|
||||
{{if .HasOrgProfileReadme}}
|
||||
<a class="{{if .PageIsViewOverview}}active {{end}}item" href="{{$.Org.HomeLink}}">
|
||||
{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}{{if .HasProfileReadme}}/-/repositories{{end}}">
|
||||
<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}{{if .HasOrgProfileReadme}}/-/repositories{{end}}">
|
||||
{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
|
||||
{{if .RepoCount}}
|
||||
<div class="ui small label">{{.RepoCount}}</div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository new repo">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository new-repo">
|
||||
<div class="ui middle very relaxed page one column grid">
|
||||
<div class="column">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
<form class="ui form new-repo-form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<h3 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "new_repo"}}
|
||||
@ -44,8 +44,11 @@
|
||||
<div class="inline required field {{if .Err_RepoName}}error{{end}}">
|
||||
<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
|
||||
<input id="repo_name" name="repo_name" value="{{.repo_name}}" autofocus required maxlength="100">
|
||||
<span class="help">{{ctx.Locale.Tr "repo.repo_name_helper"}}</span>
|
||||
<span class="help" data-help-for-repo-name>{{ctx.Locale.Tr "repo.repo_name_helper"}}</span>
|
||||
<span class="help tw-hidden" data-help-for-repo-name=".profile">{{ctx.Locale.Tr "repo.repo_name_profile_public_hint"}}</span>
|
||||
<span class="help tw-hidden" data-help-for-repo-name=".profile-private">{{ctx.Locale.Tr "repo.repo_name_profile_private_hint"}}</span>
|
||||
</div>
|
||||
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
|
||||
<div class="ui checkbox">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<overflow-menu class="ui secondary pointing tabular borderless menu">
|
||||
<div class="overflow-menu-items">
|
||||
{{if and .HasProfileReadme .ContextUser.IsIndividual}}
|
||||
{{if and .HasUserProfileReadme .ContextUser.IsIndividual}}
|
||||
<a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview">
|
||||
{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
|
||||
</a>
|
||||
|
132
tests/integration/org_profile_test.go
Normal file
132
tests/integration/org_profile_test.go
Normal file
@ -0,0 +1,132 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/web/shared/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getCreateProfileReadmeFileOptions(content string) api.CreateFileOptions {
|
||||
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
|
||||
return api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: "main",
|
||||
NewBranchName: "main",
|
||||
Message: "create the profile README.md",
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Unix(946684810, 0),
|
||||
Committer: time.Unix(978307190, 0),
|
||||
},
|
||||
},
|
||||
ContentBase64: contentEncoded,
|
||||
}
|
||||
}
|
||||
|
||||
func createTestProfile(t *testing.T, orgName, profileRepoName, readmeContent string) {
|
||||
isPrivate := profileRepoName == user.RepoNameProfilePrivate
|
||||
|
||||
ctx := NewAPITestContext(t, "user1", profileRepoName, auth_model.AccessTokenScopeAll)
|
||||
session := loginUser(t, "user1")
|
||||
tokenAdmin := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||
|
||||
// create repo
|
||||
doAPICreateOrganizationRepository(ctx, orgName, &api.CreateRepoOption{Name: profileRepoName, Private: isPrivate})(t)
|
||||
|
||||
// create readme
|
||||
createFileOptions := getCreateProfileReadmeFileOptions(readmeContent)
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", orgName, profileRepoName, "README.md"), &createFileOptions).
|
||||
AddTokenAuth(tokenAdmin)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
func TestOrgProfile(t *testing.T) {
|
||||
onGiteaRun(t, testOrgProfile)
|
||||
}
|
||||
|
||||
func testOrgProfile(t *testing.T, u *url.URL) {
|
||||
const contentPublicReadme = "Public Readme Content"
|
||||
const contentPrivateReadme = "Private Readme Content"
|
||||
// HTML: "#org-home-view-as-dropdown" (indicate whether the view as dropdown menu is present)
|
||||
|
||||
// PART 1: Test Both Private and Public
|
||||
createTestProfile(t, "org3", user.RepoNameProfile, contentPublicReadme)
|
||||
createTestProfile(t, "org3", user.RepoNameProfilePrivate, contentPrivateReadme)
|
||||
|
||||
// Anonymous User
|
||||
req := NewRequest(t, "GET", "org3")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
bodyString := util.UnsafeBytesToString(resp.Body.Bytes())
|
||||
assert.Contains(t, bodyString, contentPublicReadme)
|
||||
assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`)
|
||||
|
||||
// Logged in but not member
|
||||
session := loginUser(t, "user24")
|
||||
req = NewRequest(t, "GET", "org3")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
bodyString = util.UnsafeBytesToString(resp.Body.Bytes())
|
||||
assert.Contains(t, bodyString, contentPublicReadme)
|
||||
assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`)
|
||||
|
||||
// Site Admin
|
||||
session = loginUser(t, "user1")
|
||||
req = NewRequest(t, "GET", "/org3")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
bodyString = util.UnsafeBytesToString(resp.Body.Bytes())
|
||||
assert.Contains(t, bodyString, contentPrivateReadme) // as an org member, default to show the private profile
|
||||
assert.Contains(t, bodyString, `id="org-home-view-as-dropdown"`)
|
||||
|
||||
req = NewRequest(t, "GET", "/org3?view_as=member")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
bodyString = util.UnsafeBytesToString(resp.Body.Bytes())
|
||||
assert.Contains(t, bodyString, contentPrivateReadme)
|
||||
assert.Contains(t, bodyString, `id="org-home-view-as-dropdown"`)
|
||||
|
||||
req = NewRequest(t, "GET", "/org3?view_as=public")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
bodyString = util.UnsafeBytesToString(resp.Body.Bytes())
|
||||
assert.Contains(t, bodyString, contentPublicReadme)
|
||||
assert.Contains(t, bodyString, `id="org-home-view-as-dropdown"`)
|
||||
|
||||
// PART 2: Each org has either one of private pr public profile
|
||||
createTestProfile(t, "org41", user.RepoNameProfile, contentPublicReadme)
|
||||
createTestProfile(t, "org42", user.RepoNameProfilePrivate, contentPrivateReadme)
|
||||
|
||||
// Anonymous User
|
||||
req = NewRequest(t, "GET", "/org41")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
bodyString = util.UnsafeBytesToString(resp.Body.Bytes())
|
||||
assert.Contains(t, bodyString, contentPublicReadme)
|
||||
assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`)
|
||||
|
||||
req = NewRequest(t, "GET", "/org42")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
bodyString = util.UnsafeBytesToString(resp.Body.Bytes())
|
||||
assert.NotContains(t, bodyString, contentPrivateReadme)
|
||||
assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`)
|
||||
|
||||
// Site Admin
|
||||
req = NewRequest(t, "GET", "/org41")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
bodyString = util.UnsafeBytesToString(resp.Body.Bytes())
|
||||
assert.Contains(t, bodyString, contentPublicReadme)
|
||||
assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`)
|
||||
|
||||
req = NewRequest(t, "GET", "/org42")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
bodyString = util.UnsafeBytesToString(resp.Body.Bytes())
|
||||
assert.Contains(t, bodyString, contentPrivateReadme)
|
||||
assert.NotContains(t, bodyString, `id="org-home-view-as-dropdown"`)
|
||||
}
|
@ -325,50 +325,50 @@ textarea:focus,
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.repository.new.repo form,
|
||||
.repository.new-repo form,
|
||||
.repository.new.migrate form,
|
||||
.repository.new.fork form {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.repository.new.repo form .ui.message,
|
||||
.repository.new-repo form .ui.message,
|
||||
.repository.new.migrate form .ui.message,
|
||||
.repository.new.fork form .ui.message {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.repository.new.repo form,
|
||||
.repository.new-repo form,
|
||||
.repository.new.migrate form,
|
||||
.repository.new.fork form {
|
||||
width: 800px !important;
|
||||
}
|
||||
.repository.new.repo form .header,
|
||||
.repository.new-repo form .header,
|
||||
.repository.new.migrate form .header,
|
||||
.repository.new.fork form .header {
|
||||
padding-left: 280px !important;
|
||||
}
|
||||
.repository.new.repo form .inline.field > label,
|
||||
.repository.new-repo form .inline.field > label,
|
||||
.repository.new.migrate form .inline.field > label,
|
||||
.repository.new.fork form .inline.field > label {
|
||||
text-align: right;
|
||||
width: 250px !important;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.repository.new.repo form .help,
|
||||
.repository.new-repo form .help,
|
||||
.repository.new.migrate form .help,
|
||||
.repository.new.fork form .help {
|
||||
margin-left: 265px !important;
|
||||
}
|
||||
.repository.new.repo form .optional .title,
|
||||
.repository.new-repo form .optional .title,
|
||||
.repository.new.migrate form .optional .title,
|
||||
.repository.new.fork form .optional .title {
|
||||
margin-left: 250px !important;
|
||||
}
|
||||
.repository.new.repo form .inline.field > input,
|
||||
.repository.new-repo form .inline.field > input,
|
||||
.repository.new.migrate form .inline.field > input,
|
||||
.repository.new.fork form .inline.field > input,
|
||||
.repository.new.repo form .inline.field > textarea,
|
||||
.repository.new-repo form .inline.field > textarea,
|
||||
.repository.new.migrate form .inline.field > textarea,
|
||||
.repository.new.fork form .inline.field > textarea {
|
||||
width: 50%;
|
||||
@ -376,32 +376,32 @@ textarea:focus,
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.repository.new.repo form .optional .title,
|
||||
.repository.new-repo form .optional .title,
|
||||
.repository.new.migrate form .optional .title,
|
||||
.repository.new.fork form .optional .title {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.repository.new.repo form .inline.field > label,
|
||||
.repository.new-repo form .inline.field > label,
|
||||
.repository.new.migrate form .inline.field > label,
|
||||
.repository.new.fork form .inline.field > label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.repository.new.repo form .dropdown .text,
|
||||
.repository.new-repo form .dropdown .text,
|
||||
.repository.new.migrate form .dropdown .text,
|
||||
.repository.new.fork form .dropdown .text {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.repository.new.repo form .header,
|
||||
.repository.new-repo form .header,
|
||||
.repository.new.migrate form .header,
|
||||
.repository.new.fork form .header {
|
||||
padding-left: 0 !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.repository.new.repo form .selection.dropdown,
|
||||
.repository.new-repo form .selection.dropdown,
|
||||
.repository.new.migrate form .selection.dropdown,
|
||||
.repository.new.fork form .selection.dropdown,
|
||||
.repository.new.fork form .field a {
|
||||
@ -410,22 +410,22 @@ textarea:focus,
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.repository.new.repo form label,
|
||||
.repository.new-repo form label,
|
||||
.repository.new.migrate form label,
|
||||
.repository.new.fork form label,
|
||||
.repository.new.repo form .inline.field > input,
|
||||
.repository.new-repo form .inline.field > input,
|
||||
.repository.new.migrate form .inline.field > input,
|
||||
.repository.new.fork form .inline.field > input,
|
||||
.repository.new.fork form .field a,
|
||||
.repository.new.repo form .selection.dropdown,
|
||||
.repository.new-repo form .selection.dropdown,
|
||||
.repository.new.migrate form .selection.dropdown,
|
||||
.repository.new.fork form .selection.dropdown {
|
||||
width: 100% !important;
|
||||
}
|
||||
.repository.new.repo form .field button,
|
||||
.repository.new-repo form .field button,
|
||||
.repository.new.migrate form .field button,
|
||||
.repository.new.fork form .field button,
|
||||
.repository.new.repo form .field a,
|
||||
.repository.new-repo form .field a,
|
||||
.repository.new.migrate form .field a {
|
||||
margin-bottom: 1em;
|
||||
width: 100%;
|
||||
@ -433,17 +433,17 @@ textarea:focus,
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.repository.new.repo .ui.form #auto-init {
|
||||
.repository.new-repo .ui.form #auto-init {
|
||||
margin-left: 265px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.repository.new.repo .ui.form .selection.dropdown:not(.owner) {
|
||||
.repository.new-repo .ui.form .selection.dropdown:not(.owner) {
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.repository.new.repo .ui.form .selection.dropdown:not(.owner) {
|
||||
.repository.new-repo .ui.form .selection.dropdown:not(.owner) {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,34 @@
|
||||
import $ from 'jquery';
|
||||
import {hideElem, showElem} from '../utils/dom.ts';
|
||||
|
||||
export function initRepoNew() {
|
||||
// Repo Creation
|
||||
if ($('.repository.new.repo').length > 0) {
|
||||
$('input[name="gitignores"], input[name="license"]').on('change', () => {
|
||||
const gitignores = $('input[name="gitignores"]').val();
|
||||
const license = $('input[name="license"]').val();
|
||||
if (gitignores || license) {
|
||||
document.querySelector<HTMLInputElement>('input[name="auto_init"]').checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
const pageContent = document.querySelector('.page-content.repository.new-repo');
|
||||
if (!pageContent) return;
|
||||
|
||||
const form = document.querySelector('.new-repo-form');
|
||||
const inputGitIgnores = form.querySelector<HTMLInputElement>('input[name="gitignores"]');
|
||||
const inputLicense = form.querySelector<HTMLInputElement>('input[name="license"]');
|
||||
const inputAutoInit = form.querySelector<HTMLInputElement>('input[name="auto_init"]');
|
||||
const updateUiAutoInit = () => {
|
||||
inputAutoInit.checked = Boolean(inputGitIgnores.value || inputLicense.value);
|
||||
};
|
||||
form.addEventListener('change', updateUiAutoInit);
|
||||
updateUiAutoInit();
|
||||
|
||||
const inputRepoName = form.querySelector<HTMLInputElement>('input[name="repo_name"]');
|
||||
const inputPrivate = form.querySelector<HTMLInputElement>('input[name="private"]');
|
||||
const updateUiRepoName = () => {
|
||||
const helps = form.querySelectorAll(`.help[data-help-for-repo-name]`);
|
||||
hideElem(helps);
|
||||
let help = form.querySelector(`.help[data-help-for-repo-name="${CSS.escape(inputRepoName.value)}"]`);
|
||||
if (!help) help = form.querySelector(`.help[data-help-for-repo-name=""]`);
|
||||
showElem(help);
|
||||
const repoNamePreferPrivate = {'.profile': false, '.profile-private': true};
|
||||
const preferPrivate = repoNamePreferPrivate[inputRepoName.value];
|
||||
// inputPrivate might be disabled because site admin "force private"
|
||||
if (preferPrivate !== undefined && !inputPrivate.closest('.disabled, [disabled]')) {
|
||||
inputPrivate.checked = preferPrivate;
|
||||
}
|
||||
};
|
||||
inputRepoName.addEventListener('input', updateUiRepoName);
|
||||
updateUiRepoName();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user