Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Private README.md for organization #32872

Merged
merged 24 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
784bedd
Use view as to get public and private profile repo
Dec 16, 2024
3682adb
make query string right
Dec 17, 2024
ef826a1
make sure the drop down appears only when criteria are met
Dec 17, 2024
6496b3c
Add hint for public and private profile repo name
changchaishi Dec 20, 2024
4b3a8ac
fix that drop down manu shows in repository tab
changchaishi Dec 20, 2024
d71cba7
add check icons and translation
changchaishi Dec 20, 2024
86e698d
add rollback to get private profile when view as is not present
changchaishi Dec 23, 2024
e95d716
Ignore view_as param when the profiles not both exist
changchaishi Dec 23, 2024
48b4be2
adding profile test case
changchaishi Dec 28, 2024
84ef527
use tw-hidden instead of display:none
changchaishi Dec 29, 2024
0c70525
Merge branch 'main' into private-readme
wxiaoguang Dec 30, 2024
a47cf32
temp
wxiaoguang Dec 30, 2024
586d304
temp
wxiaoguang Dec 30, 2024
0202a09
refactor git repo usage
wxiaoguang Dec 30, 2024
93e7a01
move view-as dropdown to sidebar
wxiaoguang Dec 30, 2024
ae3627f
clarify HasOrgProfileReadme vs HasUserProfileReadme, fix tests
wxiaoguang Dec 30, 2024
dc10881
fix comments
wxiaoguang Dec 30, 2024
db2c451
fix js
wxiaoguang Dec 30, 2024
b9b2701
fix mistake
wxiaoguang Dec 30, 2024
873dc8f
improve tests
wxiaoguang Dec 30, 2024
c1ae1ad
auto toggle "private" checkbox by repo name
wxiaoguang Dec 31, 2024
95312c7
make "view as" translatable
wxiaoguang Dec 31, 2024
bc5718d
fine tune "view as"
wxiaoguang Dec 31, 2024
2c0e19d
Merge branch 'main' into private-readme
wxiaoguang Dec 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions modules/gitrepo/gitrepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
26 changes: 21 additions & 5 deletions modules/reqctx/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -97,27 +112,28 @@ 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)
}

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()
}
}

// 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)}}
}
62 changes: 46 additions & 16 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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&params", new-param-pairs...} => "old&params&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)
Expand All @@ -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 {
Expand All @@ -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 = "&"
}
Expand All @@ -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)
}

Expand Down
55 changes: 55 additions & 0 deletions modules/templates/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,58 @@ func TestTemplateEscape(t *testing.T) {
assert.Equal(t, `<a k="&#34;">&lt;&gt;</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", "")))
})
}
1 change: 1 addition & 0 deletions modules/util/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions routers/api/v1/repo/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/repo/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/repo/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/repo/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion routers/private/internal_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Loading
Loading