aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--cmd/web.go10
-rw-r--r--conf/app.ini2
-rw-r--r--models/repo.go62
-rw-r--r--pkg/setting/setting.go16
-rw-r--r--public/less/_dashboard.less18
-rw-r--r--routes/repo/setting.go58
-rw-r--r--templates/explore/repo_list.tmpl11
-rw-r--r--templates/org/team/repositories.tmpl1
-rw-r--r--templates/repo/header.tmpl3
-rw-r--r--templates/repo/settings/options.tmpl15
-rw-r--r--templates/user/dashboard/dashboard.tmpl8
12 files changed, 191 insertions, 15 deletions
diff --git a/Makefile b/Makefile
index e2c08489..9834ddf4 100644
--- a/Makefile
+++ b/Makefile
@@ -62,7 +62,7 @@ pkg/bindata/bindata.go: $(DATA_FILES)
less: public/css/gogs.css
public/css/gogs.css: $(LESS_FILES)
- lessc $< $@
+ lessc $< >$@
clean:
go clean -i ./...
diff --git a/cmd/web.go b/cmd/web.go
index dc8937f9..cd4e49cb 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -100,6 +100,13 @@ func newMacaron() *macaron.Macaron {
SkipLogging: setting.DisableRouterLog,
},
))
+ m.Use(macaron.Static(
+ setting.RepositoryAvatarUploadPath,
+ macaron.StaticOptions{
+ Prefix: "repo-avatars",
+ SkipLogging: setting.DisableRouterLog,
+ },
+ ))
funcMap := template.NewFuncMap()
m.Use(macaron.Renderer(macaron.RenderOptions{
@@ -419,6 +426,9 @@ func runWeb(c *cli.Context) error {
m.Group("/settings", func() {
m.Combo("").Get(repo.Settings).
Post(bindIgnErr(form.RepoSetting{}), repo.SettingsPost)
+ m.Combo("/avatar").Get(repo.SettingsAvatar).
+ Post(binding.MultipartForm(form.Avatar{}), repo.SettingsAvatarPost)
+ m.Post("/avatar/delete", repo.SettingsDeleteAvatar)
m.Group("/collaboration", func() {
m.Combo("").Get(repo.SettingsCollaboration).Post(repo.SettingsCollaborationPost)
m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
diff --git a/conf/app.ini b/conf/app.ini
index 798712cb..b910d82b 100644
--- a/conf/app.ini
+++ b/conf/app.ini
@@ -286,6 +286,8 @@ CSRF_COOKIE_NAME = _csrf
[picture]
; Path to store user uploaded avatars
AVATAR_UPLOAD_PATH = data/avatars
+; Path to store repository uploaded avatars
+REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars
; Chinese users can choose "duoshuo"
; or a custom avatar source, like: http://cn.gravatar.com/avatar/
GRAVATAR_SOURCE = gravatar
diff --git a/models/repo.go b/models/repo.go
index b15b0175..3a9f388b 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -15,10 +15,14 @@ import (
"sort"
"strings"
"time"
+ "image"
+ _ "image/jpeg"
+ "image/png"
"github.com/Unknwon/cae/zip"
"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
+ "github.com/nfnt/resize"
"github.com/mcuadros/go-version"
log "gopkg.in/clog.v1"
"gopkg.in/ini.v1"
@@ -27,6 +31,7 @@ import (
api "github.com/gogs/go-gogs-client"
"github.com/gogs/gogs/models/errors"
+ "github.com/gogs/gogs/pkg/avatar"
"github.com/gogs/gogs/pkg/bindata"
"github.com/gogs/gogs/pkg/markup"
"github.com/gogs/gogs/pkg/process"
@@ -284,6 +289,61 @@ func (repo *Repository) HTMLURL() string {
return setting.AppURL + repo.FullName()
}
+// CustomAvatarPath returns repository custom avatar file path.
+func (repo *Repository) CustomAvatarPath() string {
+ return filepath.Join(setting.RepositoryAvatarUploadPath, com.ToStr(repo.ID))
+}
+
+// RelAvatarLink returns relative avatar link to the site domain,
+// which includes app sub-url as prefix.
+// Since Gravatar support not needed here - just check for image path.
+func (repo *Repository) RelAvatarLink() string {
+ defaultImgUrl := ""
+ if !com.IsExist(repo.CustomAvatarPath()) {
+ return defaultImgUrl
+ }
+ return setting.AppSubURL + "/repo-avatars/" + com.ToStr(repo.ID)
+}
+
+// AvatarLink returns user avatar absolute link.
+func (repo *Repository) AvatarLink() string {
+ link := repo.RelAvatarLink()
+ if link[0] == '/' && link[1] != '/' {
+ return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
+ }
+ return link
+}
+
+// UploadAvatar saves custom avatar for repository.
+// FIXME: split uploads to different subdirs
+// in case we have massive number of repositories.
+func (repo *Repository) UploadAvatar(data []byte) error {
+ img, _, err := image.Decode(bytes.NewReader(data))
+ if err != nil {
+ return fmt.Errorf("Decode: %v", err)
+ }
+
+ m := resize.Resize(avatar.AVATAR_SIZE, avatar.AVATAR_SIZE, img, resize.NearestNeighbor)
+ os.MkdirAll(setting.RepositoryAvatarUploadPath, os.ModePerm)
+ fw, err := os.Create(repo.CustomAvatarPath())
+ if err != nil {
+ return fmt.Errorf("Create: %v", err)
+ }
+ defer fw.Close()
+
+ if err = png.Encode(fw, m); err != nil {
+ return fmt.Errorf("Encode: %v", err)
+ }
+
+ return nil
+}
+
+// DeleteAvatar deletes the repository custom avatar.
+func (repo *Repository) DeleteAvatar() error {
+ log.Trace("DeleteAvatar [%d]: %s", repo.ID, repo.CustomAvatarPath())
+ return os.Remove(repo.CustomAvatarPath())
+}
+
// This method assumes following fields have been assigned with valid values:
// Required - BaseRepo (if fork)
// Arguments that are allowed to be nil: permission
@@ -312,6 +372,8 @@ func (repo *Repository) APIFormat(permission *api.Permission, user ...*User) *ap
Created: repo.Created,
Updated: repo.Updated,
Permissions: permission,
+// Reserved for go-gogs-client change
+// AvatarUrl: repo.AvatarLink(),
}
if repo.IsFork {
p := &api.Permission{Pull: true}
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index b06c1bf3..5e239249 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -188,11 +188,12 @@ var (
}
// Picture settings
- AvatarUploadPath string
- GravatarSource string
- DisableGravatar bool
- EnableFederatedAvatar bool
- LibravatarService *libravatar.Libravatar
+ AvatarUploadPath string
+ RepositoryAvatarUploadPath string
+ GravatarSource string
+ DisableGravatar bool
+ EnableFederatedAvatar bool
+ LibravatarService *libravatar.Libravatar
// Log settings
LogRootPath string
@@ -611,6 +612,11 @@ func NewContext() {
if !filepath.IsAbs(AvatarUploadPath) {
AvatarUploadPath = path.Join(workDir, AvatarUploadPath)
}
+ RepositoryAvatarUploadPath = sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "repo-avatars"))
+ forcePathSeparator(RepositoryAvatarUploadPath)
+ if !filepath.IsAbs(RepositoryAvatarUploadPath) {
+ RepositoryAvatarUploadPath = path.Join(workDir, RepositoryAvatarUploadPath)
+ }
switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source {
case "duoshuo":
GravatarSource = "http://gravatar.duoshuo.com/avatar/"
diff --git a/public/less/_dashboard.less b/public/less/_dashboard.less
index 3c84b77d..688a023b 100644
--- a/public/less/_dashboard.less
+++ b/public/less/_dashboard.less
@@ -141,18 +141,28 @@
.repo-owner-name-list {
.item-name {
max-width: 70%;
- margin-bottom: -4px;
+ margin-bottom: -4px;
+ }
+ .ui.micro.image {
+ width: 16px;
+ height: auto;
+ display: inline-block;
}
}
#collaborative-repo-list {
.owner-and-repo {
- max-width: 80%;
- margin-bottom: -5px;
+ max-width: 75%;
+ margin-bottom: -5px;
}
.owner-name {
max-width: 120px;
- margin-bottom: -5px;
+ margin-bottom: -5px;
+ }
+ .ui.micro.image {
+ width: 16px;
+ height: auto;
+ display: inline-block;
}
}
}
diff --git a/routes/repo/setting.go b/routes/repo/setting.go
index 0b0f294d..da9d8fe4 100644
--- a/routes/repo/setting.go
+++ b/routes/repo/setting.go
@@ -8,9 +8,10 @@ import (
"fmt"
"strings"
"time"
+ "io/ioutil"
log "gopkg.in/clog.v1"
-
+ "github.com/Unknwon/com"
"github.com/gogs/git-module"
"github.com/gogs/gogs/models"
@@ -19,10 +20,12 @@ import (
"github.com/gogs/gogs/pkg/form"
"github.com/gogs/gogs/pkg/mailer"
"github.com/gogs/gogs/pkg/setting"
+ "github.com/gogs/gogs/pkg/tool"
)
const (
SETTINGS_OPTIONS = "repo/settings/options"
+ SETTINGS_REPO_AVATAR = "repo/settings/avatar"
SETTINGS_COLLABORATION = "repo/settings/collaboration"
SETTINGS_BRANCHES = "repo/settings/branches"
SETTINGS_PROTECTED_BRANCH = "repo/settings/protected_branch"
@@ -632,3 +635,56 @@ func DeleteDeployKey(c *context.Context) {
"redirect": c.Repo.RepoLink + "/settings/keys",
})
}
+
+func SettingsAvatar(c *context.Context) {
+ c.Title("settings.avatar")
+ c.PageIs("SettingsAvatar")
+ c.Success(SETTINGS_REPO_AVATAR)
+}
+
+func SettingsAvatarPost(c *context.Context, f form.Avatar) {
+ f.Source = form.AVATAR_LOCAL
+ if err := UpdateAvatarSetting(c, f); err != nil {
+ c.Flash.Error(err.Error())
+ } else {
+ c.Flash.Success(c.Tr("settings.update_avatar_success"))
+ }
+ c.SubURLRedirect(c.Repo.RepoLink + "/settings")
+}
+
+func SettingsDeleteAvatar(c *context.Context) {
+ if err := c.Repo.Repository.DeleteAvatar(); err != nil {
+ c.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err))
+ }
+ c.SubURLRedirect(c.Repo.RepoLink + "/settings")
+}
+
+// FIXME: limit size.
+func UpdateAvatarSetting(c *context.Context, f form.Avatar) error {
+ ctxRepo := c.Repo.Repository;
+ if f.Avatar != nil {
+ r, err := f.Avatar.Open()
+ if err != nil {
+ return fmt.Errorf("Avatar.Open: %v", err)
+ }
+ defer r.Close()
+
+ data, err := ioutil.ReadAll(r)
+ if err != nil {
+ return fmt.Errorf("ioutil.ReadAll: %v", err)
+ }
+ if !tool.IsImageFile(data) {
+ return errors.New(c.Tr("settings.uploaded_avatar_not_a_image"))
+ }
+ if err = ctxRepo.UploadAvatar(data); err != nil {
+ return fmt.Errorf("UploadAvatar: %v", err)
+ }
+ } else {
+ // No avatar is uploaded but setting has been changed to enable
+ // No random avatar here.
+ if !com.IsFile(ctxRepo.CustomAvatarPath()) {
+ log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID)
+ }
+ }
+ return nil
+}
diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl
index 43abad41..413cdc53 100644
--- a/templates/explore/repo_list.tmpl
+++ b/templates/explore/repo_list.tmpl
@@ -1,7 +1,12 @@
<div class="ui repository list">
{{range .Repos}}
<div class="item">
- <div class="ui header">
+ <div class="ui grid">
+ <div class="ui two wide column middle aligned">
+ {{if .RelAvatarLink}}<img class="ui tiny image" src="{{.RelAvatarLink}}">{{else}}<i class="mega-octicon octicon-repo"></i>{{end}}
+ </div>
+ <div class="ui fourteen wide column">
+ <div class="ui header">
<a class="name" href="{{AppSubURL}}/{{if .Owner}}{{.Owner.Name}}{{else if $.Org}}{{$.Org.Name}}{{else}}{{$.Owner.Name}}{{end}}/{{.Name}}">{{if $.PageIsExplore}}{{.Owner.Name}} / {{end}}{{.Name}}</a>
{{if .IsPrivate}}
<span class="text gold"><i class="octicon octicon-lock"></i></span>
@@ -9,6 +14,8 @@
<span><i class="octicon octicon-repo-forked"></i></span>
{{else if .IsMirror}}
<span><i class="octicon octicon-repo-clone"></i></span>
+ {{else}}
+ <span class="text"><i class="octicon octicon-globe"></i></span>
{{end}}
<div class="ui right metas">
@@ -18,6 +25,8 @@
</div>
{{if .Description}}<p class="has-emoji">{{.Description | Str2html}}</p>{{end}}
<p class="time">{{$.i18n.Tr "org.repo_updated"}} {{TimeSince .Updated $.i18n.Lang}}</p>
+ </div>
+ </div>
</div>
{{end}}
</div>
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl
index 4f1ad313..e2cfbe95 100644
--- a/templates/org/team/repositories.tmpl
+++ b/templates/org/team/repositories.tmpl
@@ -17,6 +17,7 @@
<a class="ui red small button right" href="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/repo/remove?repoid={{.ID}}">{{$.i18n.Tr "org.teams.remove_repo"}}</a>
{{end}}
<a class="member" href="{{AppSubURL}}/{{$.Org.Name}}/{{.Name}}">
+ <img height="16px" class="octicon" src="{{.RelAvatarLink}}" />
<i class="octicon octicon-{{if .IsPrivate}}lock{{else if .IsFork}}repo-forked{{else if .IsMirror}}repo-clone{{else}}repo{{end}}"></i>
<strong>{{$.Org.Name}}/{{.Name}}</strong>
</a>
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 77685c5e..1bef1903 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -5,7 +5,8 @@
<div class="column"><!-- start column -->
<div class="ui header">
<div class="ui huge breadcrumb">
- <i class="mega-octicon octicon-{{if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else if .IsFork}}repo-forked{{else}}repo{{end}}"></i>
+ {{if .RelAvatarLink}}<img class="ui mini spaced image" src="{{.RelAvatarLink}}">{{else}}<i class="mega-octicon octicon-repo"></i>{{end}}
+ <i class="mega-octicon octicon-{{if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else if .IsFork}}repo-forked{{else}}globe{{end}}"></i>
<a href="{{AppSubURL}}/{{.Owner.Name}}">{{.Owner.Name}}</a>
<div class="divider"> / </div>
<a href="{{$.RepoLink}}">{{.Name}}</a>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 83966254..8aa4a1f7 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -41,6 +41,21 @@
<div class="field">
<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
</div>
+
+ </form>
+
+ <div class="ui divider"></div>
+
+ <form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data">
+ {{.CSRFTokenHTML}}
+ <div class="inline field">
+ <label for="avatar">{{.i18n.Tr "settings.choose_new_avatar"}}</label>
+ <input name="avatar" type="file" >
+ </div>
+ <div class="field">
+ <button class="ui green button">{{$.i18n.Tr "settings.update_avatar"}}</button>
+ <a class="ui red button delete-post" data-request-url="{{.Link}}/avatar/delete" data-done-url="{{.Link}}">{{$.i18n.Tr "settings.delete_current_avatar"}}</a>
+ </div>
</form>
</div>
diff --git a/templates/user/dashboard/dashboard.tmpl b/templates/user/dashboard/dashboard.tmpl
index 4a0c90a2..f9f97447 100644
--- a/templates/user/dashboard/dashboard.tmpl
+++ b/templates/user/dashboard/dashboard.tmpl
@@ -32,7 +32,8 @@
{{range .Repos}}
<li {{if .IsPrivate}}class="private"{{end}}>
<a href="{{AppSubURL}}/{{$.ContextUser.Name}}/{{.Name}}">
- <i class="octicon octicon-{{if .IsFork}}repo-forked{{else if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else}}repo{{end}}"></i>
+ {{if .RelAvatarLink}}<img class="ui micro image" src="{{.RelAvatarLink}}" />{{else}}<i class="octicon octicon-repo"></i>{{end}}
+ <i class="octicon octicon-{{if .IsFork}}repo-forked{{else if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else}}globe{{end}}"></i>
<strong class="text truncate item-name">{{.Name}}</strong>
<span class="ui right text light grey">
{{.NumStars}} <i class="octicon octicon-star rear"></i>
@@ -57,7 +58,8 @@
{{range .CollaborativeRepos}}
<li {{if .IsPrivate}}class="private"{{end}}>
<a href="{{AppSubURL}}/{{.Owner.Name}}/{{.Name}}">
- <i class="octicon octicon-{{if .IsPrivate}}lock{{else if .IsFork}}repo-forked{{else if .IsMirror}}repo-clone{{else}}repo{{end}}"></i>
+ {{if .RelAvatarLink}}<img class="ui micro image" src="{{.RelAvatarLink}}" />{{else}}<i class="octicon octicon-repo"></i>{{end}}
+ <i class="octicon octicon-{{if .IsPrivate}}lock{{else if .IsFork}}repo-forked{{else if .IsMirror}}repo-clone{{else}}globe{{end}}"></i>
<span class="text truncate owner-and-repo">
<span class="text truncate owner-name">{{.Owner.Name}}</span> / <strong>{{.Name}}</strong>
</span>
@@ -88,6 +90,7 @@
{{range .ContextUser.Orgs}}
<li>
<a href="{{AppSubURL}}/{{.Name}}">
+ {{if .RelAvatarLink}}<img class="ui micro image" src="{{.RelAvatarLink}}" />{{else}}<i class="octicon octicon-repo"></i>{{end}}
<i class="octicon octicon-organization"></i>
<strong class="text truncate item-name">{{.Name}}</strong>
<span class="ui right text light grey">
@@ -116,6 +119,7 @@
{{range .Mirrors}}
<li {{if .IsPrivate}}class="private"{{end}}>
<a href="{{AppSubURL}}/{{$.ContextUser.Name}}/{{.Name}}">
+ {{if .RelAvatarLink}}<img class="ui micro image" src="{{.RelAvatarLink}}" />{{else}}<i class="octicon octicon-repo"></i>{{end}}
<i class="octicon octicon-repo-clone"></i>
<strong class="text truncate item-name">{{.Name}}</strong>
<span class="ui right text light grey">