mirror of
https://github.com/go-gitea/gitea.git
synced 2025-11-03 08:02:36 +09:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdf1856011 | ||
|
|
6037f183cf | ||
|
|
4b187f5167 | ||
|
|
aee45d072e | ||
|
|
e893aced89 | ||
|
|
d63ca66623 | ||
|
|
59afb62ab2 | ||
|
|
8a19c6b9a2 |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## [1.3.2](https://github.com/go-gitea/gitea/releases/tag/v1.3.2) - 2017-12-14
|
||||
* BUGFIXES
|
||||
* fix run web with -p push failed (#3154) (#3179)
|
||||
* Fix source download link when no code unit allowed (#3166) (#3169)
|
||||
* Allow adding collaborators with (fullname) (#3103) (#3168)
|
||||
* Fix repo links (#3093) (#3163)
|
||||
* Fix Uninitialized variable in ParsePatch (#3156) (#3162)
|
||||
* Fix migration order v1.3 (#3157)
|
||||
* Fix avatar URLs (#3069) (#3143)
|
||||
|
||||
## [1.3.1](https://github.com/go-gitea/gitea/releases/tag/v1.3.1) - 2017-12-08
|
||||
* BUGFIXES
|
||||
* Sanitize logs for mirror sync (#3057, #3082) (#3078)
|
||||
|
||||
30
cmd/web.go
30
cmd/web.go
@@ -19,8 +19,10 @@ import (
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/routers/routes"
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
context2 "github.com/gorilla/context"
|
||||
"github.com/urfave/cli"
|
||||
ini "gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// CmdWeb represents the available web sub-command.
|
||||
@@ -69,6 +71,34 @@ func runWeb(ctx *cli.Context) error {
|
||||
if ctx.IsSet("port") {
|
||||
setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, ctx.String("port"), 1)
|
||||
setting.HTTPPort = ctx.String("port")
|
||||
|
||||
switch setting.Protocol {
|
||||
case setting.UnixSocket:
|
||||
case setting.FCGI:
|
||||
default:
|
||||
// Save LOCAL_ROOT_URL if port changed
|
||||
cfg := ini.Empty()
|
||||
if com.IsFile(setting.CustomConf) {
|
||||
// Keeps custom settings if there is already something.
|
||||
if err := cfg.Append(setting.CustomConf); err != nil {
|
||||
return fmt.Errorf("Failed to load custom conf '%s': %v", setting.CustomConf, err)
|
||||
}
|
||||
}
|
||||
|
||||
defaultLocalURL := string(setting.Protocol) + "://"
|
||||
if setting.HTTPAddr == "0.0.0.0" {
|
||||
defaultLocalURL += "localhost"
|
||||
} else {
|
||||
defaultLocalURL += setting.HTTPAddr
|
||||
}
|
||||
defaultLocalURL += ":" + setting.HTTPPort + "/"
|
||||
|
||||
cfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL)
|
||||
|
||||
if err := cfg.SaveTo(setting.CustomConf); err != nil {
|
||||
return fmt.Errorf("Error saving generated JWT Secret to custom config: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var listenAddr string
|
||||
|
||||
@@ -238,7 +238,7 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D
|
||||
var (
|
||||
diff = &Diff{Files: make([]*DiffFile, 0)}
|
||||
|
||||
curFile *DiffFile
|
||||
curFile = &DiffFile{}
|
||||
curSection = &DiffSection{
|
||||
Lines: make([]*DiffLine, 0, 10),
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@ type Version struct {
|
||||
Version int64
|
||||
}
|
||||
|
||||
func emptyMigration(x *xorm.Engine) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is a sequence of migrations. Add new migrations to the bottom of the list.
|
||||
// If you want to "retire" a migration, remove it from the top of the list and
|
||||
// update minDBVersion accordingly
|
||||
@@ -127,17 +131,17 @@ var migrations = []Migration{
|
||||
// v38 -> v39
|
||||
NewMigration("remove commits and settings unit types", removeCommitsUnitType),
|
||||
// v39 -> v40
|
||||
NewMigration("adds time tracking and stopwatches", addTimetracking),
|
||||
// v40 -> v41
|
||||
NewMigration("migrate protected branch struct", migrateProtectedBranchStruct),
|
||||
// v41 -> v42
|
||||
NewMigration("add default value to user prohibit_login", addDefaultValueToUserProhibitLogin),
|
||||
// v42 -> v43
|
||||
NewMigration("add tags to releases and sync existing repositories", releaseAddColumnIsTagAndSyncTags),
|
||||
// v43 -> v44
|
||||
NewMigration("fix protected branch can push value to false", fixProtectedBranchCanPushValue),
|
||||
// v44 -> v45
|
||||
// v40 -> v41
|
||||
NewMigration("add tags to releases and sync existing repositories", releaseAddColumnIsTagAndSyncTags),
|
||||
// v41 -> v42
|
||||
NewMigration("remove duplicate unit types", removeDuplicateUnitTypes),
|
||||
// v42 -> v43
|
||||
NewMigration("empty step", emptyMigration),
|
||||
// v43 -> v44
|
||||
NewMigration("empty step", emptyMigration),
|
||||
// v44 -> v45
|
||||
NewMigration("empty step", emptyMigration),
|
||||
// v45 -> v46
|
||||
NewMigration("remove index column from repo_unit table", removeIndexColumnFromRepoUnitTable),
|
||||
// v46 -> v47
|
||||
@@ -146,6 +150,12 @@ var migrations = []Migration{
|
||||
NewMigration("add deleted branches", addDeletedBranch),
|
||||
// v48 -> v49
|
||||
NewMigration("add repo indexer status", addRepoIndexerStatus),
|
||||
// v49 -> v50
|
||||
NewMigration("adds time tracking and stopwatches", addTimetracking),
|
||||
// v50 -> v51
|
||||
NewMigration("migrate protected branch struct", migrateProtectedBranchStruct),
|
||||
// v51 -> v52
|
||||
NewMigration("add default value to user prohibit_login", addDefaultValueToUserProhibitLogin),
|
||||
}
|
||||
|
||||
// Migrate database to current version
|
||||
|
||||
@@ -6,69 +6,21 @@ package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
func addTimetracking(x *xorm.Engine) error {
|
||||
// RepoUnit describes all units of a repository
|
||||
type RepoUnit struct {
|
||||
ID int64
|
||||
RepoID int64 `xorm:"INDEX(s)"`
|
||||
Type int `xorm:"INDEX(s)"`
|
||||
Index int
|
||||
Config map[string]interface{} `xorm:"JSON"`
|
||||
CreatedUnix int64 `xorm:"INDEX CREATED"`
|
||||
Created time.Time `xorm:"-"`
|
||||
func fixProtectedBranchCanPushValue(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
CanPush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
// Stopwatch see models/issue_stopwatch.go
|
||||
type Stopwatch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
}
|
||||
|
||||
// TrackedTime see models/issue_tracked_time.go
|
||||
type TrackedTime struct {
|
||||
ID int64 `xorm:"pk autoincr" json:"id"`
|
||||
IssueID int64 `xorm:"INDEX" json:"issue_id"`
|
||||
UserID int64 `xorm:"INDEX" json:"user_id"`
|
||||
Created time.Time `xorm:"-" json:"created"`
|
||||
CreatedUnix int64 `json:"-"`
|
||||
Time int64 `json:"time"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(Stopwatch)); err != nil {
|
||||
if err := x.Sync2(new(ProtectedBranch)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
if err := x.Sync2(new(TrackedTime)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
//Updating existing issue units
|
||||
units := make([]*RepoUnit, 0, 100)
|
||||
err := x.Where("`type` = ?", V16UnitTypeIssues).Find(&units)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Query repo units: %v", err)
|
||||
}
|
||||
for _, unit := range units {
|
||||
if unit.Config == nil {
|
||||
unit.Config = make(map[string]interface{})
|
||||
}
|
||||
if _, ok := unit.Config["EnableTimetracker"]; !ok {
|
||||
unit.Config["EnableTimetracker"] = setting.Service.DefaultEnableTimetracking
|
||||
}
|
||||
if _, ok := unit.Config["AllowOnlyContributorsToTrackTime"]; !ok {
|
||||
unit.Config["AllowOnlyContributorsToTrackTime"] = setting.Service.DefaultAllowOnlyContributorsToTrackTime
|
||||
}
|
||||
if _, err := x.ID(unit.ID).Cols("config").Update(unit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
_, err := x.Cols("can_push").Update(&ProtectedBranch{
|
||||
CanPush: false,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,50 +6,52 @@ package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/git"
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
func migrateProtectedBranchStruct(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE(s)"`
|
||||
BranchName string `xorm:"UNIQUE(s)"`
|
||||
CanPush bool
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
Updated time.Time `xorm:"-"`
|
||||
UpdatedUnix int64
|
||||
// ReleaseV39 describes the added field for Release
|
||||
type ReleaseV39 struct {
|
||||
IsTag bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (*ReleaseV39) TableName() string {
|
||||
return "release"
|
||||
}
|
||||
|
||||
func releaseAddColumnIsTagAndSyncTags(x *xorm.Engine) error {
|
||||
if err := x.Sync2(new(ReleaseV39)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
|
||||
var pbs []ProtectedBranch
|
||||
err := x.Find(&pbs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// For the sake of SQLite3, we can't use x.Iterate here.
|
||||
offset := 0
|
||||
pageSize := 20
|
||||
for {
|
||||
repos := make([]*models.Repository, 0, pageSize)
|
||||
if err := x.Table("repository").Asc("id").Limit(pageSize, offset).Find(&repos); err != nil {
|
||||
return fmt.Errorf("select repos [offset: %d]: %v", offset, err)
|
||||
}
|
||||
for _, repo := range repos {
|
||||
gitRepo, err := git.OpenRepository(repo.RepoPath())
|
||||
if err != nil {
|
||||
log.Warn("OpenRepository: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, pb := range pbs {
|
||||
if pb.CanPush {
|
||||
if _, err = x.ID(pb.ID).Delete(new(ProtectedBranch)); err != nil {
|
||||
return err
|
||||
if err = models.SyncReleasesWithTags(repo, gitRepo); err != nil {
|
||||
log.Warn("SyncReleasesWithTags: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case setting.UseSQLite3:
|
||||
log.Warn("Unable to drop columns in SQLite")
|
||||
case setting.UseMySQL, setting.UsePostgreSQL, setting.UseMSSQL, setting.UseTiDB:
|
||||
if _, err := x.Exec("ALTER TABLE protected_branch DROP COLUMN can_push"); err != nil {
|
||||
return fmt.Errorf("DROP COLUMN can_push: %v", err)
|
||||
if len(repos) < pageSize {
|
||||
break
|
||||
}
|
||||
default:
|
||||
log.Fatal(4, "Unrecognized DB")
|
||||
offset += pageSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,36 +7,63 @@ package migrations
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
func addDefaultValueToUserProhibitLogin(x *xorm.Engine) (err error) {
|
||||
user := &models.User{
|
||||
ProhibitLogin: false,
|
||||
func removeDuplicateUnitTypes(x *xorm.Engine) error {
|
||||
// RepoUnit describes all units of a repository
|
||||
type RepoUnit struct {
|
||||
RepoID int64
|
||||
Type int
|
||||
}
|
||||
|
||||
if _, err := x.Where("`prohibit_login` IS NULL").Cols("prohibit_login").Update(user); err != nil {
|
||||
// Enumerate all the unit types
|
||||
const (
|
||||
UnitTypeCode = iota + 1 // 1 code
|
||||
UnitTypeIssues // 2 issues
|
||||
UnitTypePullRequests // 3 PRs
|
||||
UnitTypeReleases // 4 Releases
|
||||
UnitTypeWiki // 5 Wiki
|
||||
UnitTypeExternalWiki // 6 ExternalWiki
|
||||
UnitTypeExternalTracker // 7 ExternalTracker
|
||||
)
|
||||
|
||||
var externalIssueRepoUnits []RepoUnit
|
||||
err := x.Where("type = ?", UnitTypeExternalTracker).Find(&externalIssueRepoUnits)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Query repositories: %v", err)
|
||||
}
|
||||
|
||||
var externalWikiRepoUnits []RepoUnit
|
||||
err = x.Where("type = ?", UnitTypeExternalWiki).Find(&externalWikiRepoUnits)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Query repositories: %v", err)
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dialect := x.Dialect().DriverName()
|
||||
|
||||
switch dialect {
|
||||
case "mysql":
|
||||
_, err = x.Exec("ALTER TABLE user MODIFY `prohibit_login` tinyint(1) NOT NULL DEFAULT 0")
|
||||
case "postgres":
|
||||
_, err = x.Exec("ALTER TABLE \"user\" ALTER COLUMN `prohibit_login` SET NOT NULL, ALTER COLUMN `prohibit_login` SET DEFAULT false")
|
||||
case "mssql":
|
||||
// xorm already set DEFAULT 0 for data type BIT in mssql
|
||||
_, err = x.Exec(`ALTER TABLE [user] ALTER COLUMN "prohibit_login" BIT NOT NULL`)
|
||||
case "sqlite3":
|
||||
for _, repoUnit := range externalIssueRepoUnits {
|
||||
if _, err = sess.Delete(&RepoUnit{
|
||||
RepoID: repoUnit.RepoID,
|
||||
Type: UnitTypeIssues,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Delete repo unit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error changing user prohibit_login column definition: %v", err)
|
||||
for _, repoUnit := range externalWikiRepoUnits {
|
||||
if _, err = sess.Delete(&RepoUnit{
|
||||
RepoID: repoUnit.RepoID,
|
||||
Type: UnitTypeWiki,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Delete repo unit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. 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 (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/git"
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
// ReleaseV39 describes the added field for Release
|
||||
type ReleaseV39 struct {
|
||||
IsTag bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
// TableName will be invoked by XORM to customrize the table name
|
||||
func (*ReleaseV39) TableName() string {
|
||||
return "release"
|
||||
}
|
||||
|
||||
func releaseAddColumnIsTagAndSyncTags(x *xorm.Engine) error {
|
||||
if err := x.Sync2(new(ReleaseV39)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
|
||||
// For the sake of SQLite3, we can't use x.Iterate here.
|
||||
offset := 0
|
||||
pageSize := 20
|
||||
for {
|
||||
repos := make([]*models.Repository, 0, pageSize)
|
||||
if err := x.Table("repository").Asc("id").Limit(pageSize, offset).Find(&repos); err != nil {
|
||||
return fmt.Errorf("select repos [offset: %d]: %v", offset, err)
|
||||
}
|
||||
for _, repo := range repos {
|
||||
gitRepo, err := git.OpenRepository(repo.RepoPath())
|
||||
if err != nil {
|
||||
log.Warn("OpenRepository: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = models.SyncReleasesWithTags(repo, gitRepo); err != nil {
|
||||
log.Warn("SyncReleasesWithTags: %v", err)
|
||||
}
|
||||
}
|
||||
if len(repos) < pageSize {
|
||||
break
|
||||
}
|
||||
offset += pageSize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
func fixProtectedBranchCanPushValue(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
CanPush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(ProtectedBranch)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
|
||||
_, err := x.Cols("can_push").Update(&ProtectedBranch{
|
||||
CanPush: false,
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
func removeDuplicateUnitTypes(x *xorm.Engine) error {
|
||||
// RepoUnit describes all units of a repository
|
||||
type RepoUnit struct {
|
||||
RepoID int64
|
||||
Type int
|
||||
}
|
||||
|
||||
// Enumerate all the unit types
|
||||
const (
|
||||
UnitTypeCode = iota + 1 // 1 code
|
||||
UnitTypeIssues // 2 issues
|
||||
UnitTypePullRequests // 3 PRs
|
||||
UnitTypeReleases // 4 Releases
|
||||
UnitTypeWiki // 5 Wiki
|
||||
UnitTypeExternalWiki // 6 ExternalWiki
|
||||
UnitTypeExternalTracker // 7 ExternalTracker
|
||||
)
|
||||
|
||||
var externalIssueRepoUnits []RepoUnit
|
||||
err := x.Where("type = ?", UnitTypeExternalTracker).Find(&externalIssueRepoUnits)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Query repositories: %v", err)
|
||||
}
|
||||
|
||||
var externalWikiRepoUnits []RepoUnit
|
||||
err = x.Where("type = ?", UnitTypeExternalWiki).Find(&externalWikiRepoUnits)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Query repositories: %v", err)
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, repoUnit := range externalIssueRepoUnits {
|
||||
if _, err = sess.Delete(&RepoUnit{
|
||||
RepoID: repoUnit.RepoID,
|
||||
Type: UnitTypeIssues,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Delete repo unit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, repoUnit := range externalWikiRepoUnits {
|
||||
if _, err = sess.Delete(&RepoUnit{
|
||||
RepoID: repoUnit.RepoID,
|
||||
Type: UnitTypeWiki,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Delete repo unit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
73
models/migrations/v49.go
Normal file
73
models/migrations/v49.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2017 The Gitea Authors. 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 (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
func addTimetracking(x *xorm.Engine) error {
|
||||
// RepoUnit describes all units of a repository
|
||||
type RepoUnit struct {
|
||||
ID int64
|
||||
RepoID int64 `xorm:"INDEX(s)"`
|
||||
Type int `xorm:"INDEX(s)"`
|
||||
Config map[string]interface{} `xorm:"JSON"`
|
||||
CreatedUnix int64 `xorm:"INDEX CREATED"`
|
||||
Created time.Time `xorm:"-"`
|
||||
}
|
||||
|
||||
// Stopwatch see models/issue_stopwatch.go
|
||||
type Stopwatch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
}
|
||||
|
||||
// TrackedTime see models/issue_tracked_time.go
|
||||
type TrackedTime struct {
|
||||
ID int64 `xorm:"pk autoincr" json:"id"`
|
||||
IssueID int64 `xorm:"INDEX" json:"issue_id"`
|
||||
UserID int64 `xorm:"INDEX" json:"user_id"`
|
||||
Created time.Time `xorm:"-" json:"created"`
|
||||
CreatedUnix int64 `json:"-"`
|
||||
Time int64 `json:"time"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(Stopwatch)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
if err := x.Sync2(new(TrackedTime)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
//Updating existing issue units
|
||||
units := make([]*RepoUnit, 0, 100)
|
||||
err := x.Where("`type` = ?", V16UnitTypeIssues).Find(&units)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Query repo units: %v", err)
|
||||
}
|
||||
for _, unit := range units {
|
||||
if unit.Config == nil {
|
||||
unit.Config = make(map[string]interface{})
|
||||
}
|
||||
if _, ok := unit.Config["EnableTimetracker"]; !ok {
|
||||
unit.Config["EnableTimetracker"] = setting.Service.DefaultEnableTimetracking
|
||||
}
|
||||
if _, ok := unit.Config["AllowOnlyContributorsToTrackTime"]; !ok {
|
||||
unit.Config["AllowOnlyContributorsToTrackTime"] = setting.Service.DefaultAllowOnlyContributorsToTrackTime
|
||||
}
|
||||
if _, err := x.ID(unit.ID).Cols("config").Update(unit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
55
models/migrations/v50.go
Normal file
55
models/migrations/v50.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2017 The Gitea Authors. 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 (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
func migrateProtectedBranchStruct(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE(s)"`
|
||||
BranchName string `xorm:"UNIQUE(s)"`
|
||||
CanPush bool
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64
|
||||
Updated time.Time `xorm:"-"`
|
||||
UpdatedUnix int64
|
||||
}
|
||||
|
||||
var pbs []ProtectedBranch
|
||||
err := x.Find(&pbs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pb := range pbs {
|
||||
if pb.CanPush {
|
||||
if _, err = x.ID(pb.ID).Delete(new(ProtectedBranch)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case setting.UseSQLite3:
|
||||
log.Warn("Unable to drop columns in SQLite")
|
||||
case setting.UseMySQL, setting.UsePostgreSQL, setting.UseMSSQL, setting.UseTiDB:
|
||||
if _, err := x.Exec("ALTER TABLE protected_branch DROP COLUMN can_push"); err != nil {
|
||||
// Ignoring this error in case we run this migration second time (after migration reordering)
|
||||
log.Warn("DROP COLUMN can_push: %v", err)
|
||||
}
|
||||
default:
|
||||
log.Fatal(4, "Unrecognized DB")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
42
models/migrations/v51.go
Normal file
42
models/migrations/v51.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2017 The Gitea Authors. 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 (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
func addDefaultValueToUserProhibitLogin(x *xorm.Engine) (err error) {
|
||||
user := &models.User{
|
||||
ProhibitLogin: false,
|
||||
}
|
||||
|
||||
if _, err := x.Where("`prohibit_login` IS NULL").Cols("prohibit_login").Update(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dialect := x.Dialect().DriverName()
|
||||
|
||||
switch dialect {
|
||||
case "mysql":
|
||||
_, err = x.Exec("ALTER TABLE user MODIFY `prohibit_login` tinyint(1) NOT NULL DEFAULT 0")
|
||||
case "postgres":
|
||||
_, err = x.Exec("ALTER TABLE \"user\" ALTER COLUMN `prohibit_login` SET NOT NULL, ALTER COLUMN `prohibit_login` SET DEFAULT false")
|
||||
case "mssql":
|
||||
// xorm already set DEFAULT 0 for data type BIT in mssql
|
||||
_, err = x.Exec(`ALTER TABLE [user] ALTER COLUMN "prohibit_login" BIT NOT NULL`)
|
||||
case "sqlite3":
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Ignoring this error in case we run this migration second time (after migration reordering)
|
||||
log.Warn("Error changing user prohibit_login column definition: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -315,10 +315,9 @@ func (u *User) generateRandomAvatar(e Engine) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RelAvatarLink returns relative avatar link to the site domain,
|
||||
// which includes app sub-url as prefix. However, it is possible
|
||||
// to return full URL if user enables Gravatar-like service.
|
||||
func (u *User) RelAvatarLink() string {
|
||||
// SizedRelAvatarLink returns a relative link to the user's avatar. When
|
||||
// applicable, the link is for an avatar of the indicated size (in pixels).
|
||||
func (u *User) SizedRelAvatarLink(size int) string {
|
||||
if u.ID == -1 {
|
||||
return base.DefaultAvatarLink()
|
||||
}
|
||||
@@ -338,7 +337,14 @@ func (u *User) RelAvatarLink() string {
|
||||
|
||||
return setting.AppSubURL + "/avatars/" + u.Avatar
|
||||
}
|
||||
return base.AvatarLink(u.AvatarEmail)
|
||||
return base.SizedAvatarLink(u.AvatarEmail, size)
|
||||
}
|
||||
|
||||
// RelAvatarLink returns a relative link to the user's avatar. The link
|
||||
// may either be a sub-URL to this site, or a full URL to an external avatar
|
||||
// service.
|
||||
func (u *User) RelAvatarLink() string {
|
||||
return u.SizedRelAvatarLink(base.DefaultAvatarSize)
|
||||
}
|
||||
|
||||
// AvatarLink returns user avatar absolute link.
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"math"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -197,24 +199,59 @@ func DefaultAvatarLink() string {
|
||||
return setting.AppSubURL + "/img/avatar_default.png"
|
||||
}
|
||||
|
||||
// DefaultAvatarSize is a sentinel value for the default avatar size, as
|
||||
// determined by the avatar-hosting service.
|
||||
const DefaultAvatarSize = -1
|
||||
|
||||
// libravatarURL returns the URL for the given email. This function should only
|
||||
// be called if a federated avatar service is enabled.
|
||||
func libravatarURL(email string) (*url.URL, error) {
|
||||
urlStr, err := setting.LibravatarService.FromEmail(email)
|
||||
if err != nil {
|
||||
log.Error(4, "LibravatarService.FromEmail(email=%s): error %v", email, err)
|
||||
return nil, err
|
||||
}
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
log.Error(4, "Failed to parse libravatar url(%s): error %v", urlStr, err)
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// SizedAvatarLink returns a sized link to the avatar for the given email
|
||||
// address.
|
||||
func SizedAvatarLink(email string, size int) string {
|
||||
var avatarURL *url.URL
|
||||
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
|
||||
var err error
|
||||
avatarURL, err = libravatarURL(email)
|
||||
if err != nil {
|
||||
return DefaultAvatarLink()
|
||||
}
|
||||
} else if !setting.DisableGravatar {
|
||||
// copy GravatarSourceURL, because we will modify its Path.
|
||||
copyOfGravatarSourceURL := *setting.GravatarSourceURL
|
||||
avatarURL = ©OfGravatarSourceURL
|
||||
avatarURL.Path = path.Join(avatarURL.Path, HashEmail(email))
|
||||
} else {
|
||||
return DefaultAvatarLink()
|
||||
}
|
||||
|
||||
vals := avatarURL.Query()
|
||||
vals.Set("d", "identicon")
|
||||
if size != DefaultAvatarSize {
|
||||
vals.Set("s", strconv.Itoa(size))
|
||||
}
|
||||
avatarURL.RawQuery = vals.Encode()
|
||||
return avatarURL.String()
|
||||
}
|
||||
|
||||
// AvatarLink returns relative avatar link to the site domain by given email,
|
||||
// which includes app sub-url as prefix. However, it is possible
|
||||
// to return full URL if user enables Gravatar-like service.
|
||||
func AvatarLink(email string) string {
|
||||
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
|
||||
url, err := setting.LibravatarService.FromEmail(email)
|
||||
if err != nil {
|
||||
log.Error(4, "LibravatarService.FromEmail(email=%s): error %v", email, err)
|
||||
return DefaultAvatarLink()
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
if !setting.DisableGravatar {
|
||||
return setting.GravatarSource + HashEmail(email) + "?d=identicon"
|
||||
}
|
||||
|
||||
return DefaultAvatarLink()
|
||||
return SizedAvatarLink(email, DefaultAvatarSize)
|
||||
}
|
||||
|
||||
// Seconds-based time units
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/Unknwon/i18n"
|
||||
macaroni18n "github.com/go-macaron/i18n"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -126,16 +128,40 @@ func TestHashEmail(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
func TestAvatarLink(t *testing.T) {
|
||||
const gravatarSource = "https://secure.gravatar.com/avatar/"
|
||||
|
||||
func disableGravatar() {
|
||||
setting.EnableFederatedAvatar = false
|
||||
setting.LibravatarService = nil
|
||||
setting.DisableGravatar = true
|
||||
}
|
||||
|
||||
assert.Equal(t, "/img/avatar_default.png", AvatarLink(""))
|
||||
|
||||
func enableGravatar(t *testing.T) {
|
||||
setting.DisableGravatar = false
|
||||
var err error
|
||||
setting.GravatarSourceURL, err = url.Parse(gravatarSource)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSizedAvatarLink(t *testing.T) {
|
||||
disableGravatar()
|
||||
assert.Equal(t, "/img/avatar_default.png",
|
||||
SizedAvatarLink("gitea@example.com", 100))
|
||||
|
||||
enableGravatar(t)
|
||||
assert.Equal(t,
|
||||
"353cbad9b58e69c96154ad99f92bedc7?d=identicon",
|
||||
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100",
|
||||
SizedAvatarLink("gitea@example.com", 100),
|
||||
)
|
||||
}
|
||||
|
||||
func TestAvatarLink(t *testing.T) {
|
||||
disableGravatar()
|
||||
assert.Equal(t, "/img/avatar_default.png", AvatarLink("gitea@example.com"))
|
||||
|
||||
enableGravatar(t)
|
||||
assert.Equal(t,
|
||||
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon",
|
||||
AvatarLink("gitea@example.com"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -325,6 +325,7 @@ var (
|
||||
// Picture settings
|
||||
AvatarUploadPath string
|
||||
GravatarSource string
|
||||
GravatarSourceURL *url.URL
|
||||
DisableGravatar bool
|
||||
EnableFederatedAvatar bool
|
||||
LibravatarService *libravatar.Libravatar
|
||||
@@ -1026,18 +1027,22 @@ func NewContext() {
|
||||
if DisableGravatar {
|
||||
EnableFederatedAvatar = false
|
||||
}
|
||||
if EnableFederatedAvatar || !DisableGravatar {
|
||||
GravatarSourceURL, err = url.Parse(GravatarSource)
|
||||
if err != nil {
|
||||
log.Fatal(4, "Failed to parse Gravatar URL(%s): %v",
|
||||
GravatarSource, err)
|
||||
}
|
||||
}
|
||||
|
||||
if EnableFederatedAvatar {
|
||||
LibravatarService = libravatar.New()
|
||||
parts := strings.Split(GravatarSource, "/")
|
||||
if len(parts) >= 3 {
|
||||
if parts[0] == "https:" {
|
||||
LibravatarService.SetUseHTTPS(true)
|
||||
LibravatarService.SetSecureFallbackHost(parts[2])
|
||||
} else {
|
||||
LibravatarService.SetUseHTTPS(false)
|
||||
LibravatarService.SetFallbackHost(parts[2])
|
||||
}
|
||||
if GravatarSourceURL.Scheme == "https" {
|
||||
LibravatarService.SetUseHTTPS(true)
|
||||
LibravatarService.SetSecureFallbackHost(GravatarSourceURL.Host)
|
||||
} else {
|
||||
LibravatarService.SetUseHTTPS(false)
|
||||
LibravatarService.SetFallbackHost(GravatarSourceURL.Host)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/routers/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -76,11 +77,7 @@ func TeamsAction(ctx *context.Context) {
|
||||
ctx.Error(404)
|
||||
return
|
||||
}
|
||||
uname := ctx.Query("uname")
|
||||
// uname may be formatted as "username (fullname)"
|
||||
if strings.Contains(uname, "(") && strings.HasSuffix(uname, ")") {
|
||||
uname = strings.TrimSpace(strings.Split(uname, "(")[0])
|
||||
}
|
||||
uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("uname")))
|
||||
var u *models.User
|
||||
u, err = models.GetUserByName(uname)
|
||||
if err != nil {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/routers/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -366,7 +367,7 @@ func Collaboration(ctx *context.Context) {
|
||||
|
||||
// CollaborationPost response for actions for a collaboration of a repository
|
||||
func CollaborationPost(ctx *context.Context) {
|
||||
name := strings.ToLower(ctx.Query("collaborator"))
|
||||
name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("collaborator")))
|
||||
if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
|
||||
return
|
||||
|
||||
17
routers/utils/utils.go
Normal file
17
routers/utils/utils.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RemoveUsernameParameterSuffix returns the username parameter without the (fullname) suffix - leaving just the username
|
||||
func RemoveUsernameParameterSuffix(name string) string {
|
||||
if index := strings.Index(name, " ("); index >= 0 {
|
||||
name = name[:index]
|
||||
}
|
||||
return name
|
||||
}
|
||||
17
routers/utils/utils_test.go
Normal file
17
routers/utils/utils_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRemoveUsernameParameterSuffix(t *testing.T) {
|
||||
assert.Equal(t, "foobar", RemoveUsernameParameterSuffix("foobar (Foo Bar)"))
|
||||
assert.Equal(t, "foobar", RemoveUsernameParameterSuffix("foobar"))
|
||||
assert.Equal(t, "", RemoveUsernameParameterSuffix(""))
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
{{range .Repos}}
|
||||
<div class="item">
|
||||
<div class="ui header">
|
||||
<a class="name" href="{{AppSubUrl}}/{{if .Owner}}{{.Owner.Name}}{{else if $.Org}}{{$.Org.Name}}{{else}}{{$.Owner.Name}}{{end}}/{{.Name}}">{{if or $.PageIsExplore $.PageIsProfileStarList }}{{if .Owner}}{{.Owner.Name}} / {{end}}{{end}}{{.Name}}</a>
|
||||
<a class="name" href="{{.HTMLURL}}">{{if or $.PageIsExplore $.PageIsProfileStarList }}{{if .Owner}}{{.Owner.Name}} / {{end}}{{end}}{{.Name}}</a>
|
||||
{{if .IsPrivate}}
|
||||
<span class="text gold"><i class="octicon octicon-lock"></i></span>
|
||||
{{else if .IsFork}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="ui vertically grid head">
|
||||
<div class="column">
|
||||
<div class="ui header">
|
||||
<img class="ui image" src="{{.RelAvatarLink}}?s=100">
|
||||
<img class="ui image" src="{{.SizedRelAvatarLink 100}}">
|
||||
<span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span>
|
||||
|
||||
<div class="ui right">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
<div class="ui sixteen wide column">
|
||||
<img class="ui left" id="org-avatar" src="{{.Org.RelAvatarLink}}?s=140"/>
|
||||
<img class="ui left" id="org-avatar" src="{{.Org.SizedRelAvatarLink 140}}"/>
|
||||
<div id="org-info">
|
||||
<div class="ui header">
|
||||
{{.Org.DisplayName}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{{range .Members}}
|
||||
<div class="item ui grid">
|
||||
<div class="ui one wide column">
|
||||
<img class="ui avatar" src="{{.RelAvatarLink}}?s=48">
|
||||
<img class="ui avatar" src="{{.SizedRelAvatarLink 48}}">
|
||||
</div>
|
||||
<div class="ui three wide column">
|
||||
<div class="meta"><a href="{{.HomeLink}}">{{.Name}}</a></div>
|
||||
|
||||
@@ -41,9 +41,11 @@
|
||||
<a href="{{$.RepoLink}}/src/tag/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
|
||||
</h4>
|
||||
<div class="download">
|
||||
{{if $.Repository.UnitEnabled $.UnitTypeCode}}
|
||||
<a href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
|
||||
<a href="{{$.RepoLink}}/archive/{{.TagName}}.zip" rel="nofollow"><i class="octicon octicon-file-zip"></i> ZIP</a>
|
||||
<a href="{{$.RepoLink}}/archive/{{.TagName}}.tar.gz"><i class="octicon octicon-file-zip"></i> TAR.GZ</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<h3>
|
||||
@@ -64,12 +66,14 @@
|
||||
<div class="download">
|
||||
<h2>{{$.i18n.Tr "repo.release.downloads"}}</h2>
|
||||
<ul class="list">
|
||||
{{if $.Repository.UnitEnabled $.UnitTypeCode}}
|
||||
<li>
|
||||
<a href="{{$.RepoLink}}/archive/{{.TagName}}.zip" rel="nofollow"><i class="octicon octicon-file-zip"></i> {{$.i18n.Tr "repo.release.source_code"}} (ZIP)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{$.RepoLink}}/archive/{{.TagName}}.tar.gz"><i class="octicon octicon-file-zip"></i> {{$.i18n.Tr "repo.release.source_code"}} (TAR.GZ)</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{if .Attachments}}
|
||||
{{range .Attachments}}
|
||||
<li>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<div class="ui card">
|
||||
{{if eq .SignedUserName .Owner.Name}}
|
||||
<a class="image poping up" href="{{AppSubUrl}}/user/settings/avatar" id="profile-avatar" data-content="{{.i18n.Tr "user.change_avatar"}}" data-variation="inverted tiny" data-position="bottom center">
|
||||
<img src="{{.Owner.RelAvatarLink}}?s=290" title="{{.Owner.Name}}"/>
|
||||
<img src="{{.Owner.SizedRelAvatarLink 290}}" title="{{.Owner.Name}}"/>
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="image">
|
||||
<img src="{{.Owner.RelAvatarLink}}?s=290" title="{{.Owner.Name}}"/>
|
||||
<img src="{{.Owner.SizedRelAvatarLink 290}}" title="{{.Owner.Name}}"/>
|
||||
</span>
|
||||
{{end}}
|
||||
<div class="content">
|
||||
|
||||
Reference in New Issue
Block a user