aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSergey Dryabzhinsky <sergey.dryabzhinsky+github@gmail.com>2019-05-30 05:22:26 +0300
committertechknowlogick <techknowlogick@gitea.io>2019-05-29 22:22:26 -0400
commit3fd18838aa5c549842e88b770b8718f693614c75 (patch)
tree614d7aeeb9419f187fa39e9ff7a664afe67ca593
parentd7494046ac8aaf3b01f0eae0039e38c6c4f9c246 (diff)
downloadgitea-3fd18838aa5c549842e88b770b8718f693614c75.tar.gz
gitea-3fd18838aa5c549842e88b770b8718f693614c75.zip
Repository avatars (#6986)
* Repository avatars - first variant of code from old work for gogs - add migration 87 - add new option in app.ini - add en-US locale string - add new class in repository.less * Add changed index.css, remove unused template name * Update en-us doc about configuration options * Add comments to new functions, add new option to docker app.ini * Add comment for lint * Remove variable, not needed * Fix formatting * Update swagger api template * Check if avatar exists * Fix avatar link/path checks * Typo * TEXT column can't have a default value * Fixes: - remove old avatar file on upload - use ID in name of avatar file - users may upload same files - add simple tests * Fix fmt check * Generate PNG instead of "static" GIF * More informative comment * Fix error message * Update avatar upload checks: - add file size check - add new option - update config docs - add new string to en-us locale * Fixes: - use FileHEader field for check file size - add new test - upload big image * Fix formatting * Update comments * Update log message * Removed wrong style - not needed * Use Sync2 to migrate * Update repos list view - bigger avatar - fix html blocks alignment * A little adjust avatar size * Use small icons for explore/repo list * Use new cool avatar preparation func by @lafriks * Missing changes for new function * Remove unused import, move imports * Missed new option definition in app.ini Add file size check in user/profile avatar upload * Use smaller field length for Avatar * Use session to update repo DB data, update DeleteAvatar - use session too * Fix err variable definition * As suggested @lafriks - return as soon as possible, code readability
-rw-r--r--custom/conf/app.ini.sample8
-rw-r--r--docker/root/etc/templates/app.ini1
-rw-r--r--docs/content/doc/advanced/config-cheat-sheet.en-us.md6
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v87.go18
-rw-r--r--models/repo.go134
-rw-r--r--models/repo_test.go53
-rw-r--r--modules/setting/setting.go24
-rw-r--r--modules/structs/repo.go1
-rw-r--r--options/locale/locale_en-US.ini2
-rw-r--r--public/css/index.css1
-rw-r--r--public/less/_explore.less5
-rw-r--r--routers/repo/setting.go59
-rw-r--r--routers/routes/routes.go11
-rw-r--r--routers/user/setting/profile.go4
-rw-r--r--templates/explore/repo_list.tmpl21
-rw-r--r--templates/repo/header.tmpl4
-rw-r--r--templates/repo/settings/options.tmpl16
-rw-r--r--templates/swagger/v1_json.tmpl4
19 files changed, 354 insertions, 20 deletions
diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample
index e13f5aeeda..e8e3ffada6 100644
--- a/custom/conf/app.ini.sample
+++ b/custom/conf/app.ini.sample
@@ -504,10 +504,14 @@ SESSION_LIFE_TIME = 86400
[picture]
AVATAR_UPLOAD_PATH = data/avatars
-; Max Width and Height of uploaded avatars. This is to limit the amount of RAM
-; used when resizing the image.
+REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars
+; Max Width and Height of uploaded avatars.
+; This is to limit the amount of RAM used when resizing the image.
AVATAR_MAX_WIDTH = 4096
AVATAR_MAX_HEIGHT = 3072
+; Maximum alloved file size for uploaded avatars.
+; This is to limit the amount of RAM used when resizing the image.
+AVATAR_MAX_FILE_SIZE = 1048576
; Chinese users can choose "duoshuo"
; or a custom avatar source, like: http://cn.gravatar.com/avatar/
GRAVATAR_SOURCE = gravatar
diff --git a/docker/root/etc/templates/app.ini b/docker/root/etc/templates/app.ini
index 589271b4a0..20cbb9053c 100644
--- a/docker/root/etc/templates/app.ini
+++ b/docker/root/etc/templates/app.ini
@@ -35,6 +35,7 @@ PROVIDER_CONFIG = /data/gitea/sessions
[picture]
AVATAR_UPLOAD_PATH = /data/gitea/avatars
+REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars
[attachment]
PATH = /data/gitea/attachments
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 140eb6ffb7..052ced6e2a 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -290,7 +290,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only.
- `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see
[http://www.libravatar.org](http://www.libravatar.org)).
-- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store local and cached files.
+- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files.
+- `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: Path to store repository avatar image files.
+- `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels.
+- `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels.
+- `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes.
## Attachment (`attachment`)
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index f3a090e41c..b95a74c362 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -227,6 +227,8 @@ var migrations = []Migration{
NewMigration("hash application token", hashAppToken),
// v86 -> v87
NewMigration("add http method to webhook", addHTTPMethodToWebhook),
+ // v87 -> v88
+ NewMigration("add avatar field to repository", addAvatarFieldToRepository),
}
// Migrate database to current version
diff --git a/models/migrations/v87.go b/models/migrations/v87.go
new file mode 100644
index 0000000000..94711ac669
--- /dev/null
+++ b/models/migrations/v87.go
@@ -0,0 +1,18 @@
+// Copyright 2019 Gitea. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+ "github.com/go-xorm/xorm"
+)
+
+func addAvatarFieldToRepository(x *xorm.Engine) error {
+ type Repository struct {
+ // ID(10-20)-md5(32) - must fit into 64 symbols
+ Avatar string `xorm:"VARCHAR(64)"`
+ }
+
+ return x.Sync2(new(Repository))
+}
diff --git a/models/repo.go b/models/repo.go
index 3283223d5b..b8a3714abf 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -7,9 +7,14 @@ package models
import (
"bytes"
+ "crypto/md5"
"errors"
"fmt"
"html/template"
+
+ // Needed for jpeg support
+ _ "image/jpeg"
+ "image/png"
"io/ioutil"
"net/url"
"os"
@@ -21,6 +26,7 @@ import (
"strings"
"time"
+ "code.gitea.io/gitea/modules/avatar"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
@@ -166,6 +172,9 @@ type Repository struct {
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
Topics []string `xorm:"TEXT JSON"`
+ // Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
+ Avatar string `xorm:"VARCHAR(64)"`
+
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
}
@@ -290,6 +299,7 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool)
Created: repo.CreatedUnix.AsTime(),
Updated: repo.UpdatedUnix.AsTime(),
Permissions: permission,
+ AvatarURL: repo.AvatarLink(),
}
}
@@ -1869,6 +1879,15 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
go HookQueue.Add(repo.ID)
}
+ if len(repo.Avatar) > 0 {
+ avatarPath := repo.CustomAvatarPath()
+ if com.IsExist(avatarPath) {
+ if err := os.Remove(avatarPath); err != nil {
+ return fmt.Errorf("Failed to remove %s: %v", avatarPath, err)
+ }
+ }
+ }
+
DeleteRepoFromIndexer(repo)
return nil
}
@@ -2452,3 +2471,118 @@ func (repo *Repository) GetUserFork(userID int64) (*Repository, error) {
}
return &forkedRepo, nil
}
+
+// CustomAvatarPath returns repository custom avatar file path.
+func (repo *Repository) CustomAvatarPath() string {
+ // Avatar empty by default
+ if len(repo.Avatar) <= 0 {
+ return ""
+ }
+ return filepath.Join(setting.RepositoryAvatarUploadPath, repo.Avatar)
+}
+
+// RelAvatarLink returns a relative link to the user's avatar.
+// The link a sub-URL to this site
+// Since Gravatar support not needed here - just check for image path.
+func (repo *Repository) RelAvatarLink() string {
+ // If no avatar - path is empty
+ avatarPath := repo.CustomAvatarPath()
+ if len(avatarPath) <= 0 {
+ return ""
+ }
+ if !com.IsFile(avatarPath) {
+ return ""
+ }
+ return setting.AppSubURL + "/repo-avatars/" + repo.Avatar
+}
+
+// AvatarLink returns user avatar absolute link.
+func (repo *Repository) AvatarLink() string {
+ link := repo.RelAvatarLink()
+ // link may be empty!
+ if len(link) > 0 {
+ 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 repos.
+func (repo *Repository) UploadAvatar(data []byte) error {
+ m, err := avatar.Prepare(data)
+ if err != nil {
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ oldAvatarPath := repo.CustomAvatarPath()
+
+ // Users can upload the same image to other repo - prefix it with ID
+ // Then repo will be removed - only it avatar file will be removed
+ repo.Avatar = fmt.Sprintf("%d-%x", repo.ID, md5.Sum(data))
+ if _, err := sess.ID(repo.ID).Cols("avatar").Update(repo); err != nil {
+ return fmt.Errorf("UploadAvatar: Update repository avatar: %v", err)
+ }
+
+ if err := os.MkdirAll(setting.RepositoryAvatarUploadPath, os.ModePerm); err != nil {
+ return fmt.Errorf("UploadAvatar: Failed to create dir %s: %v", setting.RepositoryAvatarUploadPath, err)
+ }
+
+ fw, err := os.Create(repo.CustomAvatarPath())
+ if err != nil {
+ return fmt.Errorf("UploadAvatar: Create file: %v", err)
+ }
+ defer fw.Close()
+
+ if err = png.Encode(fw, *m); err != nil {
+ return fmt.Errorf("UploadAvatar: Encode png: %v", err)
+ }
+
+ if len(oldAvatarPath) > 0 && oldAvatarPath != repo.CustomAvatarPath() {
+ if err := os.Remove(oldAvatarPath); err != nil {
+ return fmt.Errorf("UploadAvatar: Failed to remove old repo avatar %s: %v", oldAvatarPath, err)
+ }
+ }
+
+ return sess.Commit()
+}
+
+// DeleteAvatar deletes the repos's custom avatar.
+func (repo *Repository) DeleteAvatar() error {
+
+ // Avatar not exists
+ if len(repo.Avatar) == 0 {
+ return nil
+ }
+
+ avatarPath := repo.CustomAvatarPath()
+ log.Trace("DeleteAvatar[%d]: %s", repo.ID, avatarPath)
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ repo.Avatar = ""
+ if _, err := sess.ID(repo.ID).Cols("avatar").Update(repo); err != nil {
+ return fmt.Errorf("DeleteAvatar: Update repository avatar: %v", err)
+ }
+
+ if _, err := os.Stat(avatarPath); err == nil {
+ if err := os.Remove(avatarPath); err != nil {
+ return fmt.Errorf("DeleteAvatar: Failed to remove %s: %v", avatarPath, err)
+ }
+ } else {
+ // // Schrodinger: file may or may not exist. See err for details.
+ log.Trace("DeleteAvatar[%d]: %v", err)
+ }
+ return sess.Commit()
+}
diff --git a/models/repo_test.go b/models/repo_test.go
index eee3997868..8411536d70 100644
--- a/models/repo_test.go
+++ b/models/repo_test.go
@@ -5,6 +5,11 @@
package models
import (
+ "bytes"
+ "crypto/md5"
+ "fmt"
+ "image"
+ "image/png"
"testing"
"code.gitea.io/gitea/modules/markup"
@@ -158,3 +163,51 @@ func TestTransferOwnership(t *testing.T) {
CheckConsistencyFor(t, &Repository{}, &User{}, &Team{})
}
+
+func TestUploadAvatar(t *testing.T) {
+
+ // Generate image
+ myImage := image.NewRGBA(image.Rect(0, 0, 1, 1))
+ var buff bytes.Buffer
+ png.Encode(&buff, myImage)
+
+ assert.NoError(t, PrepareTestDatabase())
+ repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository)
+
+ err := repo.UploadAvatar(buff.Bytes())
+ assert.NoError(t, err)
+ assert.Equal(t, fmt.Sprintf("%d-%x", 10, md5.Sum(buff.Bytes())), repo.Avatar)
+}
+
+func TestUploadBigAvatar(t *testing.T) {
+
+ // Generate BIG image
+ myImage := image.NewRGBA(image.Rect(0, 0, 5000, 1))
+ var buff bytes.Buffer
+ png.Encode(&buff, myImage)
+
+ assert.NoError(t, PrepareTestDatabase())
+ repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository)
+
+ err := repo.UploadAvatar(buff.Bytes())
+ assert.Error(t, err)
+}
+
+func TestDeleteAvatar(t *testing.T) {
+
+ // Generate image
+ myImage := image.NewRGBA(image.Rect(0, 0, 1, 1))
+ var buff bytes.Buffer
+ png.Encode(&buff, myImage)
+
+ assert.NoError(t, PrepareTestDatabase())
+ repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository)
+
+ err := repo.UploadAvatar(buff.Bytes())
+ assert.NoError(t, err)
+
+ err = repo.DeleteAvatar()
+ assert.NoError(t, err)
+
+ assert.Equal(t, "", repo.Avatar)
+}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index de89c67d04..9e96105788 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -250,14 +250,16 @@ var (
}
// Picture settings
- AvatarUploadPath string
- AvatarMaxWidth int
- AvatarMaxHeight int
- GravatarSource string
- GravatarSourceURL *url.URL
- DisableGravatar bool
- EnableFederatedAvatar bool
- LibravatarService *libravatar.Libravatar
+ AvatarUploadPath string
+ AvatarMaxWidth int
+ AvatarMaxHeight int
+ GravatarSource string
+ GravatarSourceURL *url.URL
+ DisableGravatar bool
+ EnableFederatedAvatar bool
+ LibravatarService *libravatar.Libravatar
+ AvatarMaxFileSize int64
+ RepositoryAvatarUploadPath string
// Log settings
LogLevel string
@@ -835,8 +837,14 @@ func NewContext() {
if !filepath.IsAbs(AvatarUploadPath) {
AvatarUploadPath = path.Join(AppWorkPath, AvatarUploadPath)
}
+ RepositoryAvatarUploadPath = sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "repo-avatars"))
+ forcePathSeparator(RepositoryAvatarUploadPath)
+ if !filepath.IsAbs(RepositoryAvatarUploadPath) {
+ RepositoryAvatarUploadPath = path.Join(AppWorkPath, RepositoryAvatarUploadPath)
+ }
AvatarMaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096)
AvatarMaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072)
+ AvatarMaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576)
switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source {
case "duoshuo":
GravatarSource = "http://gravatar.duoshuo.com/avatar/"
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index b5283beeaa..19f5ff8afe 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -43,6 +43,7 @@ type Repository struct {
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
Permissions *Permission `json:"permissions,omitempty"`
+ AvatarURL string `json:"avatar_url"`
}
// CreateRepoOption options when creating repository
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index a691232cff..645c9770a4 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -389,6 +389,7 @@ choose_new_avatar = Choose new avatar
update_avatar = Update Avatar
delete_current_avatar = Delete Current Avatar
uploaded_avatar_not_a_image = The uploaded file is not an image.
+uploaded_avatar_is_too_big = The uploaded file has exceeded the maximum size.
update_avatar_success = Your avatar has been updated.
change_password = Update Password
@@ -1314,6 +1315,7 @@ settings.unarchive.header = Un-Archive This Repo
settings.unarchive.text = Un-Archiving the repo will restore its ability to recieve commits and pushes, as well as new issues and pull-requests.
settings.unarchive.success = The repo was successfully un-archived.
settings.unarchive.error = An error occured while trying to un-archive the repo. See the log for more details.
+settings.update_avatar_success = The repository avatar has been updated.
diff.browse_source = Browse Source
diff.parent = parent
diff --git a/public/css/index.css b/public/css/index.css
index 8cea4e2c1d..8950cc7038 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -956,6 +956,7 @@ tbody.commit-list{vertical-align:baseline}
.ui.repository.list .item .ui.header .metas span:not(:last-child){margin-right:5px}
.ui.repository.list .item .time{font-size:12px;color:grey}
.ui.repository.list .item .ui.tags{margin-bottom:1em}
+.ui.repository.list .item .ui.avatar.image{width:24px;height:24px}
.ui.repository.branches .time{font-size:12px;color:grey}
.ui.user.list .item{padding-bottom:25px}
.ui.user.list .item:not(:first-child){border-top:1px solid #eee;padding-top:25px}
diff --git a/public/less/_explore.less b/public/less/_explore.less
index 809a138a6c..c5065a35bc 100644
--- a/public/less/_explore.less
+++ b/public/less/_explore.less
@@ -53,6 +53,11 @@
.ui.tags {
margin-bottom: 1em;
}
+
+ .ui.avatar.image {
+ width: 24px;
+ height: 24px;
+ }
}
}
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index f58601633a..07649982d2 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -7,11 +7,14 @@ package repo
import (
"errors"
+ "fmt"
+ "io/ioutil"
"net/url"
"regexp"
"strings"
"time"
+ "github.com/Unknwon/com"
"mvdan.cc/xurls/v2"
"code.gitea.io/gitea/models"
@@ -727,3 +730,59 @@ func init() {
panic(err)
}
}
+
+// UpdateAvatarSetting update repo's avatar
+func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm) error {
+ ctxRepo := ctx.Repo.Repository
+
+ if form.Avatar == nil {
+ // No avatar is uploaded and we not removing it here.
+ // No random avatar generated here.
+ // Just exit, no action.
+ if !com.IsFile(ctxRepo.CustomAvatarPath()) {
+ log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID)
+ }
+ return nil
+ }
+
+ r, err := form.Avatar.Open()
+ if err != nil {
+ return fmt.Errorf("Avatar.Open: %v", err)
+ }
+ defer r.Close()
+
+ if form.Avatar.Size > setting.AvatarMaxFileSize {
+ return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
+ }
+
+ data, err := ioutil.ReadAll(r)
+ if err != nil {
+ return fmt.Errorf("ioutil.ReadAll: %v", err)
+ }
+ if !base.IsImageFile(data) {
+ return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
+ }
+ if err = ctxRepo.UploadAvatar(data); err != nil {
+ return fmt.Errorf("UploadAvatar: %v", err)
+ }
+ return nil
+}
+
+// SettingsAvatar save new POSTed repository avatar
+func SettingsAvatar(ctx *context.Context, form auth.AvatarForm) {
+ form.Source = auth.AvatarLocal
+ if err := UpdateAvatarSetting(ctx, form); err != nil {
+ ctx.Flash.Error(err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success"))
+ }
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
+
+// SettingsDeleteAvatar delete repository avatar
+func SettingsDeleteAvatar(ctx *context.Context) {
+ if err := ctx.Repo.Repository.DeleteAvatar(); err != nil {
+ ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err))
+ }
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index d19823714b..eb5f73768e 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -159,6 +159,14 @@ func NewMacaron() *macaron.Macaron {
ExpiresAfter: time.Hour * 6,
},
))
+ m.Use(public.StaticHandler(
+ setting.RepositoryAvatarUploadPath,
+ &public.Options{
+ Prefix: "repo-avatars",
+ SkipLogging: setting.DisableRouterLog,
+ ExpiresAfter: time.Hour * 6,
+ },
+ ))
m.Use(templates.HTMLRenderer())
models.InitMailRender(templates.Mailer())
@@ -613,6 +621,9 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/settings", func() {
m.Combo("").Get(repo.Settings).
Post(bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost)
+ m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), repo.SettingsAvatar)
+ m.Post("/avatar/delete", repo.SettingsDeleteAvatar)
+
m.Group("/collaboration", func() {
m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go
index 85c9c83fd1..ac5c4c97fb 100644
--- a/routers/user/setting/profile.go
+++ b/routers/user/setting/profile.go
@@ -127,6 +127,10 @@ func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *mo
}
defer fr.Close()
+ if form.Avatar.Size > setting.AvatarMaxFileSize {
+ return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
+ }
+
data, err := ioutil.ReadAll(fr)
if err != nil {
return fmt.Errorf("ioutil.ReadAll: %v", err)
diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl
index b176817001..34aab6477a 100644
--- a/templates/explore/repo_list.tmpl
+++ b/templates/explore/repo_list.tmpl
@@ -2,6 +2,7 @@
{{range .Repos}}
<div class="item">
<div class="ui header">
+ <img class="ui avatar image" src="{{.RelAvatarLink}}">
<a class="name" href="{{.Link}}">
{{if or $.PageIsExplore $.PageIsProfileStarList }}{{if .Owner}}{{.Owner.Name}} / {{end}}{{end}}{{.Name}}
{{if .IsArchived}}<i class="archive icon archived-icon"></i>{{end}}
@@ -14,7 +15,7 @@
<span><i class="octicon octicon-repo-clone"></i></span>
{{else if .Owner}}
{{if .Owner.Visibility.IsPrivate}}
- <span class="text gold"><i class="octicon octicon-lock"></i></span>
+ <span class="text gold"><i class="octicon octicon-lock"></i></span>
{{end}}
{{end}}
<div class="ui right metas">
@@ -22,15 +23,17 @@
<span class="text grey"><i class="octicon octicon-git-branch"></i> {{.NumForks}}</span>
</div>
</div>
- {{if .DescriptionHTML}}<p class="has-emoji">{{.DescriptionHTML}}</p>{{end}}
- {{if .Topics }}
- <div class="ui tags">
- {{range .Topics}}
- {{if ne . "" }}<a href="{{AppSubUrl}}/explore/repos?q={{.}}&topic=1"><div class="ui small label topic">{{.}}</div></a>{{end}}
+ <div class="description">
+ {{if .DescriptionHTML}}<p class="has-emoji">{{.DescriptionHTML}}</p>{{end}}
+ {{if .Topics }}
+ <div class="ui tags">
+ {{range .Topics}}
+ {{if ne . "" }}<a href="{{AppSubUrl}}/explore/repos?q={{.}}&topic=1"><div class="ui small label topic">{{.}}</div></a>{{end}}
+ {{end}}
+ </div>
{{end}}
- </div>
- {{end}}
- <p class="time">{{$.i18n.Tr "org.repo_updated"}} {{TimeSinceUnix .UpdatedUnix $.i18n.Lang}}</p>
+ <p class="time">{{$.i18n.Tr "org.repo_updated"}} {{TimeSinceUnix .UpdatedUnix $.i18n.Lang}}</p>
+ </div>
</div>
{{else}}
<div>
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index d340575353..f4eefd3fde 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -3,7 +3,11 @@
<div class="ui container">
<div class="repo-header">
<div class="ui huge breadcrumb repo-title">
+ {{if .RelAvatarLink}}
+ <img class="ui avatar image" src="{{.RelAvatarLink}}">
+ {{else}}
<i class="mega-octicon octicon-{{if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else if .IsFork}}repo-forked{{else}}repo{{end}}"></i>
+ {{end}}
<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 25120fcb9f..c6d715acbe 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -41,6 +41,22 @@
<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>
{{if .Repository.IsMirror}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index c0790ac23e..7307d1284b 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -9066,6 +9066,10 @@
"type": "boolean",
"x-go-name": "Archived"
},
+ "avatar_url": {
+ "type": "string",
+ "x-go-name": "AvatarURL"
+ },
"clone_url": {
"type": "string",
"x-go-name": "CloneURL"