From 3650bd8528a8c0f43eb3d9083939735bf740fa3b Mon Sep 17 00:00:00 2001
From: Martin Hartkorn <github@hartkorn.net>
Date: Sun, 14 Feb 2016 21:12:00 +0100
Subject: [PATCH 1/4] Convert mirrors to regular repositories.

---
 conf/locale/locale_en-US.ini         |  5 ++
 models/repo.go                       | 14 ++++-
 modules/auth/repo_form.go            | 12 ++++
 routers/repo/setting.go              | 28 ++++++++++
 templates/repo/settings/options.tmpl | 83 +++++++++++++++++++++-------
 5 files changed, 120 insertions(+), 22 deletions(-)

diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini
index cb3e048d2..8273bf9d5 100644
--- a/conf/locale/locale_en-US.ini
+++ b/conf/locale/locale_en-US.ini
@@ -587,11 +587,16 @@ settings.danger_zone = Danger Zone
 settings.transfer = Transfer Ownership
 settings.transfer_desc = Transfer this repository to another user or to an organization in which you have admin rights.
 settings.new_owner_has_same_repo = The new owner already has a repository with same name. Please choose another name.
+settings.convert = Convert To Regular Repository
+settings.convert_desc = You can convert this mirror to a regular repository. This cannot be reversed.
 settings.delete = Delete This Repository
 settings.delete_desc = Once you delete a repository, there is no going back. Please be certain.
 settings.transfer_notices_1 = - You will lose access if new owner is a individual user.
 settings.transfer_notices_2 = - You will conserve access if new owner is an organization and if you're one of the owners.
 settings.transfer_form_title = Please enter following information to confirm your operation:
+settings.convert_notices_1 = - This operation will convert this repository mirror into a regular repository and cannot be undone.
+settings.convert_succeed = Repository successfully converted.
+settings.convert_failed = Unable to convert repository.
 settings.delete_notices_1 = - This operation <strong>CANNOT</strong> be undone.
 settings.delete_notices_2 = - This operation will permanently delete the everything of this repository, including Git data, issues, comments and accesses of collaborators.
 settings.delete_notices_fork_1 = - If this repository is public, all forks will become independent after deletion.
diff --git a/models/repo.go b/models/repo.go
index fec007c9c..71493810f 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -654,7 +654,17 @@ func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) {
 		return repo, UpdateRepository(repo, false)
 	}
 
-	if err = createUpdateHook(repoPath); err != nil {
+	repo, err = FinishMigrateRepository(repo, repoPath)
+	if err != nil {
+		return repo, err
+	}
+
+	return repo, UpdateRepository(repo, false)
+}
+
+// Finish migrating repository with things that don't need to be done for mirrors.
+func FinishMigrateRepository(repo *Repository, repoPath string) (*Repository, error) {
+	if err := createUpdateHook(repoPath); err != nil {
 		return repo, fmt.Errorf("createUpdateHook: %v", err)
 	}
 
@@ -695,7 +705,7 @@ func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) {
 		repo.DefaultBranch = headBranch.Name
 	}
 
-	return repo, UpdateRepository(repo, false)
+	return repo, nil
 }
 
 // initRepoCommit temporarily changes with work directory.
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go
index bd68feafc..5853c2c00 100644
--- a/modules/auth/repo_form.go
+++ b/modules/auth/repo_form.go
@@ -37,6 +37,18 @@ func (f *CreateRepoForm) Validate(ctx *macaron.Context, errs binding.Errors) bin
 	return validate(errs, ctx.Data, f, ctx.Locale)
 }
 
+type ConvertRepoForm struct {
+	Uid         int64  `binding:"Required"`
+	RepoId      int64  `binding:"Required"`
+	RepoName    string `binding:"Required;AlphaDashDot;MaxSize(100)"`
+	Private     bool
+	Description string `binding:"MaxSize(255)"`
+}
+
+func (f *ConvertRepoForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
+	return validate(errs, ctx.Data, f, ctx.Locale)
+}
+
 type MigrateRepoForm struct {
 	CloneAddr    string `json:"clone_addr" binding:"Required"`
 	AuthUsername string `json:"auth_username"`
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index 507984e56..9d7e4074c 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -141,6 +141,34 @@ func SettingsPost(ctx *middleware.Context, form auth.RepoSettingForm) {
 		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
 		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
 
+	case "convert":
+		if repo.Name != form.RepoName {
+			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil)
+			return
+		}
+
+		if ctx.Repo.Owner.IsOrganization() {
+			if !ctx.Repo.Owner.IsOwnedBy(ctx.User.Id) {
+				ctx.Error(404)
+				return
+			}
+		}
+
+		repo.IsMirror = false
+
+		if _, err := models.FinishMigrateRepository(repo, models.RepoPath(ctx.Repo.Owner.Name, repo.Name)); err != nil {
+			ctx.RenderWithErr(ctx.Tr("settings.convert.failed"), SETTINGS_OPTIONS, &form)
+			return
+		}
+
+		if 	err := models.UpdateRepository(repo, false); err != nil {
+			ctx.RenderWithErr(ctx.Tr("settings.convert.failed"), SETTINGS_OPTIONS, &form)
+			return
+		}
+		log.Trace("Repository converted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+		ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
+		ctx.Redirect(setting.AppSubUrl + "/" + ctx.Repo.Owner.Name + "/" + repo.Name)
+
 	case "transfer":
 		if repo.Name != form.RepoName {
 			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil)
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 4bb39bb07..90c03d3e3 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -138,6 +138,19 @@
 					{{.i18n.Tr "repo.settings.danger_zone"}}
 				</h4>
 				<div class="ui attached warning table danger segment">
+					{{if .Repository.IsMirror}}
+					<div class="item">
+						<div class="ui right">
+							<button class="ui basic red show-modal button" data-modal="#convert-repo-modal">{{.i18n.Tr "repo.settings.convert"}}</button>
+						</div>
+						<div>
+							<h5>{{.i18n.Tr "repo.settings.convert"}}</h5>
+							<p>{{.i18n.Tr "repo.settings.convert_desc"}}</p>
+						</div>
+					</div>
+
+					<div class="ui divider"></div>
+					{{end}}
 					<div class="item">
 						<div class="ui right">
 							<button class="ui basic red show-modal button" data-modal="#transfer-repo-modal">{{.i18n.Tr "repo.settings.transfer"}}</button>
@@ -165,6 +178,36 @@
 	</div>
 </div>
 
+<div class="ui small modal" id="convert-repo-modal">
+	<div class="header">
+		{{.i18n.Tr "repo.settings.convert"}}
+	</div>
+	<div class="content">
+		<div class="ui warning message text left">
+			{{.i18n.Tr "repo.settings.convert_notices_1" | Safe}}
+		</div>
+		<form class="ui form" action="{{.Link}}" method="post">
+			{{.CsrfTokenHtml}}
+			<input type="hidden" name="action" value="convert">
+			<div class="field">
+				<label>
+					{{.i18n.Tr "repo.settings.transfer_form_title"}}
+					<span class="text red">{{.Repository.Name}}</span>
+				</label>
+			</div>
+			<div class="required field">
+				<label for="repo_name">{{.i18n.Tr "repo.repo_name"}}</label>
+				<input id="repo_name" name="repo_name" required>
+			</div>
+
+			<div class="text right actions">
+				<div class="ui cancel button">{{.i18n.Tr "settings.cancel"}}</div>
+				<button class="ui red button">{{.i18n.Tr "repo.settings.confirm_convert"}}</button>
+			</div>
+		</form>
+	</div>
+</div>
+
 <div class="ui small modal" id="transfer-repo-modal">
 	<div class="header">
 		{{.i18n.Tr "repo.settings.transfer"}}
@@ -212,27 +255,27 @@
 			{{.i18n.Tr "repo.settings.delete_notices_fork_1" | Safe}} <br>
 			{{.i18n.Tr "repo.settings.delete_notices_fork_2" | Safe}} <br>
 			{{.i18n.Tr "repo.settings.delete_notices_fork_3" | Safe}}
-		{{end}}
-	</div>
-	<form class="ui form" action="{{.Link}}" method="post">
-		{{.CsrfTokenHtml}}
-		<input type="hidden" name="action" value="delete">
-		<div class="field">
-			<label>
-				{{.i18n.Tr "repo.settings.transfer_form_title"}}
-				<span class="text red">{{.Repository.Name}}</span>
-			</label>
-		</div>
-		<div class="required field">
-			<label for="repo_name">{{.i18n.Tr "repo.repo_name"}}</label>
-			<input id="repo_name" name="repo_name" required>
+			{{end}}
 		</div>
+		<form class="ui form" action="{{.Link}}" method="post">
+			{{.CsrfTokenHtml}}
+			<input type="hidden" name="action" value="delete">
+			<div class="field">
+				<label>
+					{{.i18n.Tr "repo.settings.transfer_form_title"}}
+					<span class="text red">{{.Repository.Name}}</span>
+				</label>
+			</div>
+			<div class="required field">
+				<label for="repo_name">{{.i18n.Tr "repo.repo_name"}}</label>
+				<input id="repo_name" name="repo_name" required>
+			</div>
 
-		<div class="text right actions">
-			<div class="ui cancel button">{{.i18n.Tr "settings.cancel"}}</div>
-			<button class="ui red button">{{.i18n.Tr "repo.settings.confirm_delete"}}</button>
-		</div>
-	</form>
-</div>
+			<div class="text right actions">
+				<div class="ui cancel button">{{.i18n.Tr "settings.cancel"}}</div>
+				<button class="ui red button">{{.i18n.Tr "repo.settings.confirm_delete"}}</button>
+			</div>
+		</form>
+	</div>
 </div>
 {{template "base/footer" .}}

From 15394f613f7c03d23903d75f12ba018b3b76dffa Mon Sep 17 00:00:00 2001
From: Martin Hartkorn <github@hartkorn.net>
Date: Sun, 14 Feb 2016 21:22:36 +0100
Subject: [PATCH 2/4] Add missing safety check

---
 routers/repo/setting.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index 9d7e4074c..690e7a465 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -154,6 +154,11 @@ func SettingsPost(ctx *middleware.Context, form auth.RepoSettingForm) {
 			}
 		}
 
+		if !repo.IsMirror {
+			ctx.Error(404)
+			return
+		}
+
 		repo.IsMirror = false
 
 		if _, err := models.FinishMigrateRepository(repo, models.RepoPath(ctx.Repo.Owner.Name, repo.Name)); err != nil {

From 15d37b7a95829e7551d843925fb034bbc613b8f2 Mon Sep 17 00:00:00 2001
From: Martin Hartkorn <github@hartkorn.net>
Date: Sun, 14 Feb 2016 22:40:39 +0100
Subject: [PATCH 3/4] Refactored according to suggestions

---
 conf/locale/locale_en-US.ini         | 14 +++++++-------
 models/repo.go                       |  4 ++--
 modules/auth/repo_form.go            | 12 ------------
 routers/repo/setting.go              |  7 +++----
 templates/repo/settings/options.tmpl |  2 ++
 5 files changed, 14 insertions(+), 25 deletions(-)

diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini
index 8273bf9d5..071557d6d 100644
--- a/conf/locale/locale_en-US.ini
+++ b/conf/locale/locale_en-US.ini
@@ -584,19 +584,19 @@ settings.tracker_url_format = External Issue Tracker URL Format
 settings.tracker_url_format_desc = You can use placeholder <code>{user} {repo} {index}</code> for user name, repository name and issue index.
 settings.pulls_desc = Enable pull requests to accept public contributions
 settings.danger_zone = Danger Zone
-settings.transfer = Transfer Ownership
-settings.transfer_desc = Transfer this repository to another user or to an organization in which you have admin rights.
 settings.new_owner_has_same_repo = The new owner already has a repository with same name. Please choose another name.
 settings.convert = Convert To Regular Repository
 settings.convert_desc = You can convert this mirror to a regular repository. This cannot be reversed.
-settings.delete = Delete This Repository
-settings.delete_desc = Once you delete a repository, there is no going back. Please be certain.
-settings.transfer_notices_1 = - You will lose access if new owner is a individual user.
-settings.transfer_notices_2 = - You will conserve access if new owner is an organization and if you're one of the owners.
-settings.transfer_form_title = Please enter following information to confirm your operation:
 settings.convert_notices_1 = - This operation will convert this repository mirror into a regular repository and cannot be undone.
 settings.convert_succeed = Repository successfully converted.
 settings.convert_failed = Unable to convert repository.
+settings.transfer = Transfer Ownership
+settings.transfer_desc = Transfer this repository to another user or to an organization in which you have admin rights.
+settings.transfer_notices_1 = - You will lose access if new owner is a individual user.
+settings.transfer_notices_2 = - You will conserve access if new owner is an organization and if you're one of the owners.
+settings.transfer_form_title = Please enter following information to confirm your operation:
+settings.delete = Delete This Repository
+settings.delete_desc = Once you delete a repository, there is no going back. Please be certain.
 settings.delete_notices_1 = - This operation <strong>CANNOT</strong> be undone.
 settings.delete_notices_2 = - This operation will permanently delete the everything of this repository, including Git data, issues, comments and accesses of collaborators.
 settings.delete_notices_fork_1 = - If this repository is public, all forks will become independent after deletion.
diff --git a/models/repo.go b/models/repo.go
index 71493810f..dfd07f198 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -654,7 +654,7 @@ func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) {
 		return repo, UpdateRepository(repo, false)
 	}
 
-	repo, err = FinishMigrateRepository(repo, repoPath)
+	repo, err = CleanUpMigrateInfo(repo, repoPath)
 	if err != nil {
 		return repo, err
 	}
@@ -663,7 +663,7 @@ func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) {
 }
 
 // Finish migrating repository with things that don't need to be done for mirrors.
-func FinishMigrateRepository(repo *Repository, repoPath string) (*Repository, error) {
+func CleanUpMigrateInfo(repo *Repository, repoPath string) (*Repository, error) {
 	if err := createUpdateHook(repoPath); err != nil {
 		return repo, fmt.Errorf("createUpdateHook: %v", err)
 	}
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go
index 5853c2c00..bd68feafc 100644
--- a/modules/auth/repo_form.go
+++ b/modules/auth/repo_form.go
@@ -37,18 +37,6 @@ func (f *CreateRepoForm) Validate(ctx *macaron.Context, errs binding.Errors) bin
 	return validate(errs, ctx.Data, f, ctx.Locale)
 }
 
-type ConvertRepoForm struct {
-	Uid         int64  `binding:"Required"`
-	RepoId      int64  `binding:"Required"`
-	RepoName    string `binding:"Required;AlphaDashDot;MaxSize(100)"`
-	Private     bool
-	Description string `binding:"MaxSize(255)"`
-}
-
-func (f *ConvertRepoForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
-	return validate(errs, ctx.Data, f, ctx.Locale)
-}
-
 type MigrateRepoForm struct {
 	CloneAddr    string `json:"clone_addr" binding:"Required"`
 	AuthUsername string `json:"auth_username"`
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index 690e7a465..cb541fc98 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -158,19 +158,18 @@ func SettingsPost(ctx *middleware.Context, form auth.RepoSettingForm) {
 			ctx.Error(404)
 			return
 		}
-
 		repo.IsMirror = false
 
-		if _, err := models.FinishMigrateRepository(repo, models.RepoPath(ctx.Repo.Owner.Name, repo.Name)); err != nil {
+		if _, err := models.CleanUpMigrateInfo(repo, models.RepoPath(ctx.Repo.Owner.Name, repo.Name)); err != nil {
 			ctx.RenderWithErr(ctx.Tr("settings.convert.failed"), SETTINGS_OPTIONS, &form)
 			return
 		}
 
-		if 	err := models.UpdateRepository(repo, false); err != nil {
+		if err := models.UpdateRepository(repo, false); err != nil {
 			ctx.RenderWithErr(ctx.Tr("settings.convert.failed"), SETTINGS_OPTIONS, &form)
 			return
 		}
-		log.Trace("Repository converted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+		log.Trace("Repository converted from mirror to regular: %s/%s", ctx.Repo.Owner.Name, repo.Name)
 		ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
 		ctx.Redirect(setting.AppSubUrl + "/" + ctx.Repo.Owner.Name + "/" + repo.Name)
 
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 90c03d3e3..4bc422c34 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -178,6 +178,7 @@
 	</div>
 </div>
 
+{{if .Repository.IsMirror}}
 <div class="ui small modal" id="convert-repo-modal">
 	<div class="header">
 		{{.i18n.Tr "repo.settings.convert"}}
@@ -207,6 +208,7 @@
 		</form>
 	</div>
 </div>
+{{end}}
 
 <div class="ui small modal" id="transfer-repo-modal">
 	<div class="header">

From bb595666ac3bdb5f16fa10663bd8890eb320fcb1 Mon Sep 17 00:00:00 2001
From: Martin Hartkorn <github@hartkorn.net>
Date: Mon, 15 Feb 2016 14:53:58 +0100
Subject: [PATCH 4/4] Moved UpdateRepository() to CleanUpMigrateInfo() and
 correctly delete mirror from database

---
 models/repo.go          | 14 +++++++-------
 routers/repo/setting.go |  2 +-
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/models/repo.go b/models/repo.go
index dfd07f198..be9e3fd75 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -654,12 +654,7 @@ func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) {
 		return repo, UpdateRepository(repo, false)
 	}
 
-	repo, err = CleanUpMigrateInfo(repo, repoPath)
-	if err != nil {
-		return repo, err
-	}
-
-	return repo, UpdateRepository(repo, false)
+	return CleanUpMigrateInfo(repo, repoPath)
 }
 
 // Finish migrating repository with things that don't need to be done for mirrors.
@@ -705,7 +700,7 @@ func CleanUpMigrateInfo(repo *Repository, repoPath string) (*Repository, error)
 		repo.DefaultBranch = headBranch.Name
 	}
 
-	return repo, nil
+	return repo, UpdateRepository(repo, false)
 }
 
 // initRepoCommit temporarily changes with work directory.
@@ -1622,6 +1617,11 @@ func MirrorUpdate() {
 	}
 }
 
+func DeleteMirrorByRepoID(repoId int64) error {
+	_, err := x.Delete(&Mirror{RepoID: repoId})
+	return err
+}
+
 // GitFsck calls 'git fsck' to check repository health.
 func GitFsck() {
 	if taskStatusPool.IsRunning(_GIT_FSCK) {
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index cb541fc98..3712a088b 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -165,7 +165,7 @@ func SettingsPost(ctx *middleware.Context, form auth.RepoSettingForm) {
 			return
 		}
 
-		if err := models.UpdateRepository(repo, false); err != nil {
+		if err := models.DeleteMirrorByRepoID(ctx.Repo.Repository.ID); err != nil {
 			ctx.RenderWithErr(ctx.Tr("settings.convert.failed"), SETTINGS_OPTIONS, &form)
 			return
 		}