Browse Source

#2018 able to sync now for mirrors

- Refactor code to use sync.UniqueQueue
- Closes #3509
Unknwon 7 years ago
parent
commit
8516dfcb6c

+ 5 - 1
.editorconfig

@@ -8,10 +8,14 @@ end_of_line = lf
 insert_final_newline = true
 trim_trailing_whitespace = true
 
-[*.{go,tmpl}]
+[*.go]
 indent_style = tab
 indent_size = 4
 
+[*.tmpl]
+indent_style = tab
+indent_size = 2
+
 [*.{less,yml}]
 indent_style = space
 indent_size = 2

+ 1 - 1
README.md

@@ -3,7 +3,7 @@ Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?bra
 
 ![](https://github.com/gogits/gogs/blob/master/public/img/gogs-large-resize.png?raw=true)
 
-##### Current tip version: 0.9.95 (see [Releases](https://github.com/gogits/gogs/releases) for binary versions)
+##### Current tip version: 0.9.96 (see [Releases](https://github.com/gogits/gogs/releases) for binary versions)
 
 | Web | UI  | Preview  |
 |:-------------:|:-------:|:-------:|

+ 6 - 4
conf/app.ini

@@ -17,8 +17,10 @@ ANSI_CHARSET =
 FORCE_PRIVATE = false
 ; Global maximum creation limit of repository per user, -1 means no limit
 MAX_CREATION_LIMIT = -1
-; Patch test queue length, make it as large as possible
-PULL_REQUEST_QUEUE_LENGTH = 10000
+; Mirror sync queue length, increase if mirror syncing starts hanging
+MIRROR_QUEUE_LENGTH = 1000
+; Patch test queue length, increase if pull request patch testing starts hanging
+PULL_REQUEST_QUEUE_LENGTH = 1000
 ; Preferred Licenses to place at the top of the List
 ; Name must match file name in conf/license or custom/conf/license
 PREFERRED_LICENSES = Apache License 2.0,MIT License
@@ -184,7 +186,7 @@ ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false
 ENABLE_CAPTCHA = true
 
 [webhook]
-; Hook task queue length
+; Hook task queue length, increase if webhook shooting starts hanging
 QUEUE_LENGTH = 1000
 ; Deliver timeout in seconds
 DELIVER_TIMEOUT = 5
@@ -389,7 +391,7 @@ GC = 60
 
 [mirror]
 ; Default interval in hours between each check
-DEFAULT_INTERVAL = 24
+DEFAULT_INTERVAL = 8
 
 [api]
 ; Max number of items will response in a page

+ 4 - 0
conf/locale/locale_en-US.ini

@@ -370,6 +370,7 @@ mirror_prune_desc = Remove any remote-tracking references that no longer exist o
 mirror_interval = Mirror Interval (hour)
 mirror_address = Mirror Address
 mirror_address_desc = Please include necessary user credentials in the address.
+mirror_last_synced = Last Synced
 watchers = Watchers
 stargazers = Stargazers
 forks = Forks
@@ -631,6 +632,9 @@ settings.collaboration.undefined = Undefined
 settings.hooks = Webhooks
 settings.githooks = Git Hooks
 settings.basic_settings = Basic Settings
+settings.mirror_settings = Mirror Settings
+settings.sync_mirror = Sync Now
+settings.mirror_sync_in_progress = Mirror syncing is in progress, please refresh page in about a minute.
 settings.site = Official Site
 settings.update_settings = Update Settings
 settings.change_reponame_prompt = This change will affect how links relate to the repository.

+ 1 - 1
gogs.go

@@ -17,7 +17,7 @@ import (
 	"github.com/gogits/gogs/modules/setting"
 )
 
-const APP_VER = "0.9.95.0830"
+const APP_VER = "0.9.96.0830"
 
 func init() {
 	runtime.GOMAXPROCS(runtime.NumCPU())

+ 6 - 195
models/repo.go

@@ -381,7 +381,7 @@ func (repo *Repository) IssueStats(uid int64, filterMode int, isPull bool) (int6
 }
 
 func (repo *Repository) GetMirror() (err error) {
-	repo.Mirror, err = GetMirror(repo.ID)
+	repo.Mirror, err = GetMirrorByRepoID(repo.ID)
 	return err
 }
 
@@ -574,136 +574,6 @@ func (repo *Repository) CloneLink() (cl *CloneLink) {
 	return repo.cloneLink(false)
 }
 
-// Mirror represents a mirror information of repository.
-type Mirror struct {
-	ID          int64 `xorm:"pk autoincr"`
-	RepoID      int64
-	Repo        *Repository `xorm:"-"`
-	Interval    int         // Hour.
-	EnablePrune bool        `xorm:"NOT NULL DEFAULT true"`
-
-	Updated        time.Time `xorm:"-"`
-	UpdatedUnix    int64
-	NextUpdate     time.Time `xorm:"-"`
-	NextUpdateUnix int64
-
-	address string `xorm:"-"`
-}
-
-func (m *Mirror) BeforeInsert() {
-	m.NextUpdateUnix = m.NextUpdate.Unix()
-}
-
-func (m *Mirror) BeforeUpdate() {
-	m.UpdatedUnix = time.Now().Unix()
-	m.NextUpdateUnix = m.NextUpdate.Unix()
-}
-
-func (m *Mirror) AfterSet(colName string, _ xorm.Cell) {
-	var err error
-	switch colName {
-	case "repo_id":
-		m.Repo, err = GetRepositoryByID(m.RepoID)
-		if err != nil {
-			log.Error(3, "GetRepositoryByID[%d]: %v", m.ID, err)
-		}
-	case "updated_unix":
-		m.Updated = time.Unix(m.UpdatedUnix, 0).Local()
-	case "next_updated_unix":
-		m.NextUpdate = time.Unix(m.NextUpdateUnix, 0).Local()
-	}
-}
-
-func (m *Mirror) readAddress() {
-	if len(m.address) > 0 {
-		return
-	}
-
-	cfg, err := ini.Load(m.Repo.GitConfigPath())
-	if err != nil {
-		log.Error(4, "Load: %v", err)
-		return
-	}
-	m.address = cfg.Section("remote \"origin\"").Key("url").Value()
-}
-
-// HandleCloneUserCredentials replaces user credentials from HTTP/HTTPS URL
-// with placeholder <credentials>.
-// It will fail for any other forms of clone addresses.
-func HandleCloneUserCredentials(url string, mosaics bool) string {
-	i := strings.Index(url, "@")
-	if i == -1 {
-		return url
-	}
-	start := strings.Index(url, "://")
-	if start == -1 {
-		return url
-	}
-	if mosaics {
-		return url[:start+3] + "<credentials>" + url[i:]
-	}
-	return url[:start+3] + url[i+1:]
-}
-
-// Address returns mirror address from Git repository config without credentials.
-func (m *Mirror) Address() string {
-	m.readAddress()
-	return HandleCloneUserCredentials(m.address, false)
-}
-
-// FullAddress returns mirror address from Git repository config.
-func (m *Mirror) FullAddress() string {
-	m.readAddress()
-	return m.address
-}
-
-// SaveAddress writes new address to Git repository config.
-func (m *Mirror) SaveAddress(addr string) error {
-	configPath := m.Repo.GitConfigPath()
-	cfg, err := ini.Load(configPath)
-	if err != nil {
-		return fmt.Errorf("Load: %v", err)
-	}
-
-	cfg.Section("remote \"origin\"").Key("url").SetValue(addr)
-	return cfg.SaveToIndent(configPath, "\t")
-}
-
-func getMirror(e Engine, repoId int64) (*Mirror, error) {
-	m := &Mirror{RepoID: repoId}
-	has, err := e.Get(m)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, ErrMirrorNotExist
-	}
-	return m, nil
-}
-
-// GetMirror returns mirror object by given repository ID.
-func GetMirror(repoId int64) (*Mirror, error) {
-	return getMirror(x, repoId)
-}
-
-func updateMirror(e Engine, m *Mirror) error {
-	_, err := e.Id(m.ID).AllCols().Update(m)
-	return err
-}
-
-func UpdateMirror(m *Mirror) error {
-	return updateMirror(x, m)
-}
-
-func DeleteMirrorByRepoID(repoID int64) error {
-	_, err := x.Delete(&Mirror{RepoID: repoID})
-	return err
-}
-
-func createUpdateHook(repoPath string) error {
-	return git.SetUpdateHook(repoPath,
-		fmt.Sprintf(_TPL_UPDATE_HOOK, setting.ScriptType, "\""+setting.AppPath+"\"", setting.CustomConf))
-}
-
 type MigrateRepoOptions struct {
 	Name        string
 	Description string
@@ -839,6 +709,11 @@ func cleanUpMigrateGitConfig(configPath string) error {
 	return nil
 }
 
+func createUpdateHook(repoPath string) error {
+	return git.SetUpdateHook(repoPath,
+		fmt.Sprintf(_TPL_UPDATE_HOOK, setting.ScriptType, "\""+setting.AppPath+"\"", setting.CustomConf))
+}
+
 // Finish migrating repository and/or wiki with things that don't need to be done for mirrors.
 func CleanUpMigrateInfo(repo *Repository) (*Repository, error) {
 	repoPath := repo.RepoPath()
@@ -1748,70 +1623,6 @@ const (
 	_CHECK_REPOs   = "check_repos"
 )
 
-// MirrorUpdate checks and updates mirror repositories.
-func MirrorUpdate() {
-	if taskStatusTable.IsRunning(_MIRROR_UPDATE) {
-		return
-	}
-	taskStatusTable.Start(_MIRROR_UPDATE)
-	defer taskStatusTable.Stop(_MIRROR_UPDATE)
-
-	log.Trace("Doing: MirrorUpdate")
-
-	mirrors := make([]*Mirror, 0, 10)
-	if err := x.Where("next_update_unix<=?", time.Now().Unix()).Iterate(new(Mirror), func(idx int, bean interface{}) error {
-		m := bean.(*Mirror)
-		if m.Repo == nil {
-			log.Error(4, "Disconnected mirror repository found: %d", m.ID)
-			return nil
-		}
-
-		repoPath := m.Repo.RepoPath()
-		wikiPath := m.Repo.WikiPath()
-		timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
-
-		gitArgs := []string{"remote", "update"}
-		if m.EnablePrune {
-			gitArgs = append(gitArgs, "--prune")
-		}
-
-		if _, stderr, err := process.ExecDir(
-			timeout, repoPath, fmt.Sprintf("MirrorUpdate: %s", repoPath),
-			"git", gitArgs...); err != nil {
-			desc := fmt.Sprintf("Fail to update mirror repository(%s): %s", repoPath, stderr)
-			log.Error(4, desc)
-			if err = CreateRepositoryNotice(desc); err != nil {
-				log.Error(4, "CreateRepositoryNotice: %v", err)
-			}
-			return nil
-		}
-		if m.Repo.HasWiki() {
-			if _, stderr, err := process.ExecDir(
-				timeout, wikiPath, fmt.Sprintf("MirrorUpdate: %s", wikiPath),
-				"git", "remote", "update", "--prune"); err != nil {
-				desc := fmt.Sprintf("Fail to update mirror wiki repository(%s): %s", wikiPath, stderr)
-				log.Error(4, desc)
-				if err = CreateRepositoryNotice(desc); err != nil {
-					log.Error(4, "CreateRepositoryNotice: %v", err)
-				}
-				return nil
-			}
-		}
-
-		m.NextUpdate = time.Now().Add(time.Duration(m.Interval) * time.Hour)
-		mirrors = append(mirrors, m)
-		return nil
-	}); err != nil {
-		log.Error(4, "MirrorUpdate: %v", err)
-	}
-
-	for i := range mirrors {
-		if err := UpdateMirror(mirrors[i]); err != nil {
-			log.Error(4, "UpdateMirror[%d]: %v", mirrors[i].ID, err)
-		}
-	}
-}
-
 // GitFsck calls 'git fsck' to check repository health.
 func GitFsck() {
 	if taskStatusTable.IsRunning(_GIT_FSCK) {

+ 243 - 0
models/repo_mirror.go

@@ -0,0 +1,243 @@
+// Copyright 2016 The Gogs 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 models
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/Unknwon/com"
+	"github.com/go-xorm/xorm"
+	"gopkg.in/ini.v1"
+
+	"github.com/gogits/gogs/modules/log"
+	"github.com/gogits/gogs/modules/process"
+	"github.com/gogits/gogs/modules/setting"
+	"github.com/gogits/gogs/modules/sync"
+)
+
+var MirrorQueue = sync.NewUniqueQueue(setting.Repository.MirrorQueueLength)
+
+// Mirror represents mirror information of a repository.
+type Mirror struct {
+	ID          int64 `xorm:"pk autoincr"`
+	RepoID      int64
+	Repo        *Repository `xorm:"-"`
+	Interval    int         // Hour.
+	EnablePrune bool        `xorm:"NOT NULL DEFAULT true"`
+
+	Updated        time.Time `xorm:"-"`
+	UpdatedUnix    int64
+	NextUpdate     time.Time `xorm:"-"`
+	NextUpdateUnix int64
+
+	address string `xorm:"-"`
+}
+
+func (m *Mirror) BeforeInsert() {
+	m.NextUpdateUnix = m.NextUpdate.Unix()
+}
+
+func (m *Mirror) BeforeUpdate() {
+	m.UpdatedUnix = time.Now().Unix()
+	m.NextUpdateUnix = m.NextUpdate.Unix()
+}
+
+func (m *Mirror) AfterSet(colName string, _ xorm.Cell) {
+	var err error
+	switch colName {
+	case "repo_id":
+		m.Repo, err = GetRepositoryByID(m.RepoID)
+		if err != nil {
+			log.Error(3, "GetRepositoryByID[%d]: %v", m.ID, err)
+		}
+	case "updated_unix":
+		m.Updated = time.Unix(m.UpdatedUnix, 0).Local()
+	case "next_updated_unix":
+		m.NextUpdate = time.Unix(m.NextUpdateUnix, 0).Local()
+	}
+}
+
+// ScheduleNextUpdate calculates and sets next update time.
+func (m *Mirror) ScheduleNextUpdate() {
+	m.NextUpdate = time.Now().Add(time.Duration(m.Interval) * time.Hour)
+}
+
+func (m *Mirror) readAddress() {
+	if len(m.address) > 0 {
+		return
+	}
+
+	cfg, err := ini.Load(m.Repo.GitConfigPath())
+	if err != nil {
+		log.Error(4, "Load: %v", err)
+		return
+	}
+	m.address = cfg.Section("remote \"origin\"").Key("url").Value()
+}
+
+// HandleCloneUserCredentials replaces user credentials from HTTP/HTTPS URL
+// with placeholder <credentials>.
+// It will fail for any other forms of clone addresses.
+func HandleCloneUserCredentials(url string, mosaics bool) string {
+	i := strings.Index(url, "@")
+	if i == -1 {
+		return url
+	}
+	start := strings.Index(url, "://")
+	if start == -1 {
+		return url
+	}
+	if mosaics {
+		return url[:start+3] + "<credentials>" + url[i:]
+	}
+	return url[:start+3] + url[i+1:]
+}
+
+// Address returns mirror address from Git repository config without credentials.
+func (m *Mirror) Address() string {
+	m.readAddress()
+	return HandleCloneUserCredentials(m.address, false)
+}
+
+// FullAddress returns mirror address from Git repository config.
+func (m *Mirror) FullAddress() string {
+	m.readAddress()
+	return m.address
+}
+
+// SaveAddress writes new address to Git repository config.
+func (m *Mirror) SaveAddress(addr string) error {
+	configPath := m.Repo.GitConfigPath()
+	cfg, err := ini.Load(configPath)
+	if err != nil {
+		return fmt.Errorf("Load: %v", err)
+	}
+
+	cfg.Section("remote \"origin\"").Key("url").SetValue(addr)
+	return cfg.SaveToIndent(configPath, "\t")
+}
+
+// runSync returns true if sync finished without error.
+func (m *Mirror) runSync() bool {
+	repoPath := m.Repo.RepoPath()
+	wikiPath := m.Repo.WikiPath()
+	timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
+
+	gitArgs := []string{"remote", "update"}
+	if m.EnablePrune {
+		gitArgs = append(gitArgs, "--prune")
+	}
+
+	if _, stderr, err := process.ExecDir(
+		timeout, repoPath, fmt.Sprintf("runSync: %s", repoPath),
+		"git", gitArgs...); err != nil {
+		desc := fmt.Sprintf("Fail to update mirror repository '%s': %s", repoPath, stderr)
+		log.Error(4, desc)
+		if err = CreateRepositoryNotice(desc); err != nil {
+			log.Error(4, "CreateRepositoryNotice: %v", err)
+		}
+		return false
+	}
+	if m.Repo.HasWiki() {
+		if _, stderr, err := process.ExecDir(
+			timeout, wikiPath, fmt.Sprintf("runSync: %s", wikiPath),
+			"git", "remote", "update", "--prune"); err != nil {
+			desc := fmt.Sprintf("Fail to update mirror wiki repository '%s': %s", wikiPath, stderr)
+			log.Error(4, desc)
+			if err = CreateRepositoryNotice(desc); err != nil {
+				log.Error(4, "CreateRepositoryNotice: %v", err)
+			}
+			return false
+		}
+	}
+
+	return true
+}
+
+func getMirrorByRepoID(e Engine, repoID int64) (*Mirror, error) {
+	m := &Mirror{RepoID: repoID}
+	has, err := e.Get(m)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrMirrorNotExist
+	}
+	return m, nil
+}
+
+// GetMirrorByRepoID returns mirror information of a repository.
+func GetMirrorByRepoID(repoID int64) (*Mirror, error) {
+	return getMirrorByRepoID(x, repoID)
+}
+
+func updateMirror(e Engine, m *Mirror) error {
+	_, err := e.Id(m.ID).AllCols().Update(m)
+	return err
+}
+
+func UpdateMirror(m *Mirror) error {
+	return updateMirror(x, m)
+}
+
+func DeleteMirrorByRepoID(repoID int64) error {
+	_, err := x.Delete(&Mirror{RepoID: repoID})
+	return err
+}
+
+// MirrorUpdate checks and updates mirror repositories.
+func MirrorUpdate() {
+	if taskStatusTable.IsRunning(_MIRROR_UPDATE) {
+		return
+	}
+	taskStatusTable.Start(_MIRROR_UPDATE)
+	defer taskStatusTable.Stop(_MIRROR_UPDATE)
+
+	log.Trace("Doing: MirrorUpdate")
+
+	if err := x.Where("next_update_unix<=?", time.Now().Unix()).Iterate(new(Mirror), func(idx int, bean interface{}) error {
+		m := bean.(*Mirror)
+		if m.Repo == nil {
+			log.Error(4, "Disconnected mirror repository found: %d", m.ID)
+			return nil
+		}
+
+		MirrorQueue.Add(m.RepoID)
+		return nil
+	}); err != nil {
+		log.Error(4, "MirrorUpdate: %v", err)
+	}
+}
+
+// SyncMirrors checks and syncs mirrors.
+// TODO: sync more mirrors at same time.
+func SyncMirrors() {
+	// Start listening on new sync requests.
+	for repoID := range MirrorQueue.Queue() {
+		log.Trace("SyncMirrors [repo_id: %v]", repoID)
+		MirrorQueue.Remove(repoID)
+
+		m, err := GetMirrorByRepoID(com.StrTo(repoID).MustInt64())
+		if err != nil {
+			log.Error(4, "GetMirrorByRepoID [%d]: %v", repoID, err)
+			continue
+		}
+
+		if !m.runSync() {
+			continue
+		}
+
+		m.ScheduleNextUpdate()
+		if err = UpdateMirror(m); err != nil {
+			log.Error(4, "UpdateMirror [%d]: %v", repoID, err)
+			continue
+		}
+	}
+}
+
+func InitSyncMirrors() {
+	go SyncMirrors()
+}

+ 1 - 1
models/webhook.go

@@ -597,7 +597,7 @@ func DeliverHooks() {
 
 	// Start listening on new hook requests.
 	for repoID := range HookQueue.Queue() {
-		log.Trace("DeliverHooks [%v]: processing delivery hooks", repoID)
+		log.Trace("DeliverHooks [repo_id: %v]", repoID)
 		HookQueue.Remove(repoID)
 
 		tasks = make([]*HookTask, 0, 5)

File diff suppressed because it is too large
+ 0 - 0
modules/bindata/bindata.go


+ 1 - 1
modules/context/repo.go

@@ -216,7 +216,7 @@ func RepoAssignment(args ...bool) macaron.Handler {
 		ctx.Data["HasAccess"] = true
 
 		if repo.IsMirror {
-			ctx.Repo.Mirror, err = models.GetMirror(repo.ID)
+			ctx.Repo.Mirror, err = models.GetMirrorByRepoID(repo.ID)
 			if err != nil {
 				ctx.Handle(500, "GetMirror", err)
 				return

+ 1 - 0
modules/setting/setting.go

@@ -113,6 +113,7 @@ var (
 		AnsiCharset            string
 		ForcePrivate           bool
 		MaxCreationLimit       int
+		MirrorQueueLength      int
 		PullRequestQueueLength int
 		PreferredLicenses      []string
 

+ 1 - 0
routers/install.go

@@ -75,6 +75,7 @@ func GlobalInit() {
 
 		// Booting long running goroutines.
 		cron.NewContext()
+		models.InitSyncMirrors()
 		models.InitDeliverHooks()
 		models.InitTestPullRequests()
 		log.NewGitLogger(path.Join(setting.LogRootPath, "http.log"))

+ 0 - 1
routers/repo/issue.go

@@ -490,7 +490,6 @@ func UploadIssueAttachment(ctx *context.Context) {
 
 func ViewIssue(ctx *context.Context) {
 	ctx.Data["RequireHighlightJS"] = true
-	ctx.Data["RequireSimpleMDE"] = true
 	ctx.Data["RequireDropzone"] = true
 	renderAttachmentSettings(ctx)
 

+ 31 - 20
routers/repo/setting.go

@@ -104,34 +104,42 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
 			}
 		}
 
-		if repo.IsMirror {
-			if isNameChanged {
-				var err error
-				ctx.Repo.Mirror, err = models.GetMirror(repo.ID)
-				if err != nil {
-					ctx.Handle(500, "RefreshRepositoryMirror", err)
-					return
-				}
-			}
+		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+		ctx.Redirect(repo.Link() + "/settings")
 
-			if form.Interval > 0 {
-				ctx.Repo.Mirror.EnablePrune = form.EnablePrune
-				ctx.Repo.Mirror.Interval = form.Interval
-				ctx.Repo.Mirror.NextUpdate = time.Now().Add(time.Duration(form.Interval) * time.Hour)
-				if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
-					ctx.Handle(500, "UpdateMirror", err)
-					return
-				}
-			}
-			if err := ctx.Repo.Mirror.SaveAddress(form.MirrorAddress); err != nil {
-				ctx.Handle(500, "SaveAddress", err)
+	case "mirror":
+		if !repo.IsMirror {
+			ctx.Handle(404, "", nil)
+			return
+		}
+
+		if form.Interval > 0 {
+			ctx.Repo.Mirror.EnablePrune = form.EnablePrune
+			ctx.Repo.Mirror.Interval = form.Interval
+			ctx.Repo.Mirror.NextUpdate = time.Now().Add(time.Duration(form.Interval) * time.Hour)
+			if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
+				ctx.Handle(500, "UpdateMirror", err)
 				return
 			}
 		}
+		if err := ctx.Repo.Mirror.SaveAddress(form.MirrorAddress); err != nil {
+			ctx.Handle(500, "SaveAddress", err)
+			return
+		}
 
 		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
 		ctx.Redirect(repo.Link() + "/settings")
 
+	case "mirror-sync":
+		if !repo.IsMirror {
+			ctx.Handle(404, "", nil)
+			return
+		}
+
+		go models.MirrorQueue.Add(repo.ID)
+		ctx.Flash.Info(ctx.Tr("repo.settings.mirror_sync_in_progress"))
+		ctx.Redirect(repo.Link() + "/settings")
+
 	case "advanced":
 		repo.EnableWiki = form.EnableWiki
 		repo.EnableExternalWiki = form.EnableExternalWiki
@@ -278,6 +286,9 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
 
 		ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
 		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+	default:
+		ctx.Handle(404, "", nil)
 	}
 }
 

+ 1 - 1
templates/.VERSION

@@ -1 +1 @@
-0.9.95.0830
+0.9.96.0830

+ 39 - 12
templates/repo/settings/options.tmpl

@@ -50,12 +50,26 @@
 								</div>
 							</div>
 						{{end}}
-						{{if .Repository.IsMirror}}
+
+						<div class="field">
+							<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
+						</div>
+					</form>
+				</div>
+
+				{{if .Repository.IsMirror}}
+					<h4 class="ui top attached header">
+						{{.i18n.Tr "repo.settings.mirror_settings"}}
+					</h4>
+					<div class="ui attached segment">
+						<form class="ui form" method="post">
+							{{.CsrfTokenHtml}}
+							<input type="hidden" name="action" value="mirror">
 							<div class="inline field {{if .Err_EnablePrune}}error{{end}}">
-							        <label>{{.i18n.Tr "repo.mirror_prune"}}</label>
+							  <label>{{.i18n.Tr "repo.mirror_prune"}}</label>
 								<div class="ui checkbox">
-								        <input id="enable_prune" name="enable_prune" type="checkbox" {{if .MirrorEnablePrune}}checked{{end}}>
-								        <label>{{.i18n.Tr "repo.mirror_prune_desc"}}</label>
+					        <input id="enable_prune" name="enable_prune" type="checkbox" {{if .MirrorEnablePrune}}checked{{end}}>
+					        <label>{{.i18n.Tr "repo.mirror_prune_desc"}}</label>
 								</div>
 							</div>
 							<div class="inline field {{if .Err_Interval}}error{{end}}">
@@ -64,23 +78,36 @@
 							</div>
 							<div class="field">
 								<label for="mirror_address">{{.i18n.Tr "repo.mirror_address"}}</label>
-								<input id="mirror_address" name="mirror_address" value="{{.Mirror.FullAddress}}">
+								<input id="mirror_address" name="mirror_address" value="{{.Mirror.FullAddress}}" required>
 								<p class="help">{{.i18n.Tr "repo.mirror_address_desc"}}</p>
 							</div>
-						{{end}}
+
+							<div class="field">
+								<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
+							</div>
+						</form>
 
 						<div class="ui divider"></div>
-						<div class="field">
-							<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
-						</div>
-					</form>
-				</div>
+
+						<form class="ui form" method="post">
+							{{.CsrfTokenHtml}}
+							<input type="hidden" name="action" value="mirror-sync">
+							<div class="inline field">
+								<label>{{.i18n.Tr "repo.mirror_last_synced"}}</label>
+								<span>{{.Mirror.Updated}}</span>
+							</div>
+							<div class="field">
+								<button class="ui blue button">{{$.i18n.Tr "repo.settings.sync_mirror"}}</button>
+							</div>
+						</form>
+					</div>
+				{{end}}
 
 				<h4 class="ui top attached header">
 					{{.i18n.Tr "repo.settings.advanced_settings"}}
 				</h4>
 				<div class="ui attached segment">
-					<form class="ui form" action="{{.Link}}" method="post">
+					<form class="ui form" method="post">
 						{{.CsrfTokenHtml}}
 						<input type="hidden" name="action" value="advanced">
 

Some files were not shown because too many files changed in this diff