From 0cc1bdac537db6c7b43dec3b9c4f00dec8aec9fd Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 2 Aug 2020 21:05:57 +0100 Subject: [PATCH 01/21] Don't automatically delete repository files if they are present Prior to this PR Gitea would delete any repository files if they are present during creation or migration. This can in certain circumstances lead to data-loss and is slightly unpleasant. This PR provides a mechanism for Gitea to adopt repositories on creation and otherwise requires an explicit flag for deletion. PushCreate is slightly different - the create will cause adoption if that is allowed otherwise it will delete the data if that is allowed. Signed-off-by: Andrew Thornton --- custom/conf/app.example.ini | 4 ++ models/error.go | 16 +++++++ models/repo.go | 34 ++++++++------ models/repo_generate.go | 19 ++++---- modules/auth/repo_form.go | 24 ++++++---- modules/repository/create.go | 78 +++++++++++++++++++++++++------ modules/repository/generate.go | 18 ++++++- modules/repository/init.go | 46 ++++++++++++++++-- modules/setting/repository.go | 2 + modules/structs/repo.go | 4 ++ options/locale/locale_en-US.ini | 7 +++ routers/api/v1/repo/repo.go | 20 ++++---- routers/repo/repo.go | 55 ++++++++++++++-------- routers/repo/setting.go | 4 ++ services/repository/repository.go | 13 +++--- templates/repo/create.tmpl | 19 ++++++++ templates/repo/migrate.tmpl | 8 +++- web_src/less/_form.less | 3 +- 18 files changed, 286 insertions(+), 88 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index a15b9be54fc0..731c5586edd3 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -59,6 +59,10 @@ PREFIX_ARCHIVE_FILES = true DISABLE_MIRRORS = false ; The default branch name of new repositories DEFAULT_BRANCH=master +; Allow adoption of unadopted repositories +ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES=false +; Allow overwrite of unadopted repositories +ALLOW_OVERWRITE_OF_UNADOPTED_REPOSITORIES=false [repository.editor] ; List of file extensions for which lines should be wrapped in the Monaco editor diff --git a/models/error.go b/models/error.go index e9343cbe7c68..2529436248d6 100644 --- a/models/error.go +++ b/models/error.go @@ -743,6 +743,22 @@ func (err ErrRepoAlreadyExist) Error() string { return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name) } +// ErrRepoFilesAlreadyExist represents a "RepoFilesAlreadyExist" kind of error. +type ErrRepoFilesAlreadyExist struct { + Uname string + Name string +} + +// IsErrRepoFilesAlreadyExist checks if an error is a ErrRepoAlreadyExist. +func IsErrRepoFilesAlreadyExist(err error) bool { + _, ok := err.(ErrRepoFilesAlreadyExist) + return ok +} + +func (err ErrRepoFilesAlreadyExist) Error() string { + return fmt.Sprintf("repository files already exist [uname: %s, name: %s]", err.Uname, err.Name) +} + // ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error. type ErrForkAlreadyExist struct { Uname string diff --git a/models/repo.go b/models/repo.go index b2b6e1a26f84..526803f0c6b1 100644 --- a/models/repo.go +++ b/models/repo.go @@ -996,7 +996,7 @@ func (repo *Repository) CloneLink() (cl *CloneLink) { } // CheckCreateRepository check if could created a repository -func CheckCreateRepository(doer, u *User, name string) error { +func CheckCreateRepository(doer, u *User, name string, overwriteOrAdopt bool) error { if !doer.CanCreateRepo() { return ErrReachLimitOfRepo{u.MaxRepoCreation} } @@ -1011,24 +1011,30 @@ func CheckCreateRepository(doer, u *User, name string) error { } else if has { return ErrRepoAlreadyExist{u.Name, name} } + + if !overwriteOrAdopt && com.IsExist(RepoPath(u.Name, name)) { + return ErrRepoFilesAlreadyExist{u.Name, name} + } return nil } // CreateRepoOptions contains the create repository options type CreateRepoOptions struct { - Name string - Description string - OriginalURL string - GitServiceType api.GitServiceType - Gitignores string - IssueLabels string - License string - Readme string - DefaultBranch string - IsPrivate bool - IsMirror bool - AutoInit bool - Status RepositoryStatus + Name string + Description string + OriginalURL string + GitServiceType api.GitServiceType + Gitignores string + IssueLabels string + License string + Readme string + DefaultBranch string + IsPrivate bool + IsMirror bool + AutoInit bool + Status RepositoryStatus + AdoptPreExisting bool + OverwritePreExisting bool } // GetRepoInitFile returns repository init files diff --git a/models/repo_generate.go b/models/repo_generate.go index 480683cd4a5b..7d1a4062b01e 100644 --- a/models/repo_generate.go +++ b/models/repo_generate.go @@ -18,15 +18,16 @@ import ( // GenerateRepoOptions contains the template units to generate type GenerateRepoOptions struct { - Name string - Description string - Private bool - GitContent bool - Topics bool - GitHooks bool - Webhooks bool - Avatar bool - IssueLabels bool + Name string + Description string + Private bool + GitContent bool + Topics bool + GitHooks bool + Webhooks bool + Avatar bool + IssueLabels bool + OverwritePreExisting bool } // IsValid checks whether at least one option is chosen for generation diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 6c3421e4f7d8..8a1e0d68dd60 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -45,6 +45,9 @@ type CreateRepoForm struct { Webhooks bool Avatar bool Labels bool + + AdoptPreExisting bool + OverwritePreExisting bool } // Validate validates the fields @@ -61,16 +64,17 @@ type MigrateRepoForm struct { // required: true UID int64 `json:"uid" binding:"Required"` // required: true - RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` - Mirror bool `json:"mirror"` - Private bool `json:"private"` - Description string `json:"description" binding:"MaxSize(255)"` - Wiki bool `json:"wiki"` - Milestones bool `json:"milestones"` - Labels bool `json:"labels"` - Issues bool `json:"issues"` - PullRequests bool `json:"pull_requests"` - Releases bool `json:"releases"` + RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` + Mirror bool `json:"mirror"` + Private bool `json:"private"` + Description string `json:"description" binding:"MaxSize(255)"` + Wiki bool `json:"wiki"` + Milestones bool `json:"milestones"` + Labels bool `json:"labels"` + Issues bool `json:"issues"` + PullRequests bool `json:"pull_requests"` + Releases bool `json:"releases"` + OverwritePreExisting bool `json:"overwrite_pre_existing"` } // Validate validates the fields diff --git a/modules/repository/create.go b/modules/repository/create.go index 2f7d10f0d111..9e69223b0786 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -13,10 +13,11 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "github.com/unknwon/com" ) // CreateRepository creates a repository for the user/organization. -func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *models.Repository, err error) { +func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (*models.Repository, error) { if !doer.IsAdmin && !u.CanCreateRepo() { return nil, models.ErrReachLimitOfRepo{ Limit: u.MaxRepoCreation, @@ -39,26 +40,70 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m IsEmpty: !opts.AutoInit, } - err = models.WithTx(func(ctx models.DBContext) error { - if err = models.CreateRepository(ctx, doer, u, repo); err != nil { + overwriteOrAdopt := (!opts.IsMirror && opts.AdoptPreExisting && setting.Repository.AllowAdoptionOfUnadoptedRepositories) || + (opts.OverwritePreExisting && setting.Repository.AllowOverwriteOfUnadoptedRepositories) + + repoPath := models.RepoPath(u.Name, repo.Name) + if !overwriteOrAdopt && com.IsExist(repoPath) { + log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) + return nil, models.ErrRepoFilesAlreadyExist{ + Uname: u.Name, + Name: opts.Name, + } + } + + if err := models.WithTx(func(ctx models.DBContext) error { + if err := models.CreateRepository(ctx, doer, u, repo); err != nil { return err } // No need for init mirror. if !opts.IsMirror { - repoPath := models.RepoPath(u.Name, repo.Name) - if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil { - if err2 := os.RemoveAll(repoPath); err2 != nil { - log.Error("initRepository: %v", err) - return fmt.Errorf( - "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) + // repo already exists - We have two or three options. + // 1. We fail stating that the directory exists + // 2. We create the db repository to go with this data and adopt the git repo + // 3. We delete it and start afresh + // + // Previously Gitea would just delete and start afresh - this was naughty. + shouldInit := true + if com.IsExist(repoPath) { + if opts.AdoptPreExisting { + shouldInit = false + if err := adoptRepository(ctx, repoPath, doer, repo, opts); err != nil { + return fmt.Errorf("createDelegateHooks: %v", err) + } + } else if opts.OverwritePreExisting { + log.Warn("An already existing repository was deleted at %s", repoPath) + if err := os.RemoveAll(repoPath); err != nil { + log.Error("Unable to remove already existing repository at %s: Error: %v", repoPath, err) + return fmt.Errorf( + "unable to delete repo directory %s/%s: %v", u.Name, repo.Name, err) + } + } else { + log.Error("Files already exist in %s and not going to adopt or delete.", repoPath) + return fmt.Errorf("data already exists on the filesystem for %s/%s. You will need to adopt these or delete these explicitly", u.Name, repo.Name) + } + } + + if shouldInit { + if err := initRepository(ctx, repoPath, doer, repo, opts); err != nil { + if err2 := os.RemoveAll(repoPath); err2 != nil { + log.Error("initRepository: %v", err) + return fmt.Errorf( + "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) + } + return fmt.Errorf("initRepository: %v", err) } - return fmt.Errorf("initRepository: %v", err) } // Initialize Issue Labels if selected if len(opts.IssueLabels) > 0 { - if err = models.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { + if err := models.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { + if shouldInit { + if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) + } + } return fmt.Errorf("InitializeLabels: %v", err) } } @@ -67,11 +112,18 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). RunInDir(repoPath); err != nil { log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) + if shouldInit { + if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) + } + } return fmt.Errorf("CreateRepository(git update-server-info): %v", err) } } return nil - }) + }); err != nil { + return nil, err + } - return repo, err + return repo, nil } diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 6d80488de786..c9498ab2ca9d 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/log" "github.com/huandu/xstrings" + "github.com/unknwon/com" ) type transformer struct { @@ -248,8 +249,21 @@ func GenerateRepository(ctx models.DBContext, doer, owner *models.User, template return nil, err } - repoPath := models.RepoPath(owner.Name, generateRepo.Name) - if err = checkInitRepository(repoPath); err != nil { + repoPath := generateRepo.RepoPath() + if com.IsExist(repoPath) { + if opts.OverwritePreExisting { + if err = os.RemoveAll(repoPath); err != nil { + return nil, err + } + } else { + return nil, models.ErrRepoFilesAlreadyExist{ + Uname: generateRepo.OwnerName, + Name: generateRepo.Name, + } + } + } + + if err = checkInitRepository(owner.Name, generateRepo.Name); err != nil { return generateRepo, err } diff --git a/modules/repository/init.go b/modules/repository/init.go index 5bdfa7490d81..5ec6213cd460 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -161,10 +161,14 @@ func initRepoCommit(tmpPath string, repo *models.Repository, u *models.User, def return nil } -func checkInitRepository(repoPath string) (err error) { +func checkInitRepository(owner, name string) (err error) { // Somehow the directory could exist. + repoPath := models.RepoPath(owner, name) if com.IsExist(repoPath) { - return fmt.Errorf("checkInitRepository: path already exists: %s", repoPath) + return models.ErrRepoFilesAlreadyExist{ + Uname: owner, + Name: name, + } } // Init git bare new repository. @@ -176,9 +180,45 @@ func checkInitRepository(repoPath string) (err error) { return nil } +func adoptRepository(ctx models.DBContext, repoPath string, u *models.User, repo *models.Repository, opts models.CreateRepoOptions) (err error) { + if !com.IsExist(repoPath) { + return fmt.Errorf("adoptRepository: path does not already exist: %s", repoPath) + } + + if err := createDelegateHooks(repoPath); err != nil { + return fmt.Errorf("createDelegateHooks: %v", err) + } + + // Re-fetch the repository from database before updating it (else it would + // override changes that were done earlier with sql) + if repo, err = models.GetRepositoryByIDCtx(ctx, repo.ID); err != nil { + return fmt.Errorf("getRepositoryByID: %v", err) + } + + repo.IsEmpty = false + + repo.DefaultBranch = "master" + if len(opts.DefaultBranch) > 0 { + repo.DefaultBranch = opts.DefaultBranch + gitRepo, err := git.OpenRepository(repo.RepoPath()) + if err != nil { + return fmt.Errorf("openRepository: %v", err) + } + if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + return fmt.Errorf("setDefaultBranch: %v", err) + } + } + + if err = models.UpdateRepositoryCtx(ctx, repo, false); err != nil { + return fmt.Errorf("updateRepository: %v", err) + } + + return nil +} + // InitRepository initializes README and .gitignore if needed. func initRepository(ctx models.DBContext, repoPath string, u *models.User, repo *models.Repository, opts models.CreateRepoOptions) (err error) { - if err = checkInitRepository(repoPath); err != nil { + if err = checkInitRepository(repo.OwnerName, repo.Name); err != nil { return err } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index eb1501d7b86e..efcecb1ac82f 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -44,6 +44,8 @@ var ( PrefixArchiveFiles bool DisableMirrors bool DefaultBranch string + AllowAdoptionOfUnadoptedRepositories bool + AllowOverwriteOfUnadoptedRepositories bool // Repository editor settings Editor struct { diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 2ff1a1ec2672..7c9e4053d26a 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -115,6 +115,10 @@ type CreateRepoOption struct { Readme string `json:"readme"` // DefaultBranch of the repository (used when initializes and in template) DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"` + // AdoptPreExisting adopt pre-existing file-system repository if it exists and this installation permits this + AdoptPreExisting bool `json:"adopt_if_exists"` + // DeleteIfExists delete pre-exisiting file-system repository if it exists and this installation permist this + OverwritePreExisting bool `json:"delete_if_exists"` } // EditRepoOption options when editing a repository's properties diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 751cce65835f..8e7be7614044 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -350,6 +350,7 @@ lang_select_error = Select a language from the list. username_been_taken = The username is already taken. repo_name_been_taken = The repository name is already used. +repository_files_already_exist = Files already exist for this repository. Adopt them or explicitly overwrite them. visit_rate_limit = Remote visit addressed rate limitation. 2fa_auth_required = Remote visit required two factors authentication. org_name_been_taken = The organization name is already taken. @@ -672,6 +673,12 @@ pick_reaction = Pick your reaction reactions_more = and %d more unit_disabled = The site administrator has disabled this repository section. language_other = Other +adopt_preexisting_label = Adopt Files +adopt_preexisting = Adopt pre-existing files +adopt_preexisting_disabled = The site administrator has disabled adoption +overwrite_preexisting_label = Overwrite +overwrite_preexisting = Overwrite pre-existing files +overwrite_preexisting_disabled = The site administrator has disabled overwriting desc.private = Private desc.public = Public diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index f812ac6788e7..8fe0ebf6e6eb 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -234,15 +234,17 @@ func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateR opt.Readme = "Default" } repo, err := repo_service.CreateRepository(ctx.User, owner, models.CreateRepoOptions{ - Name: opt.Name, - Description: opt.Description, - IssueLabels: opt.IssueLabels, - Gitignores: opt.Gitignores, - License: opt.License, - Readme: opt.Readme, - IsPrivate: opt.Private, - AutoInit: opt.AutoInit, - DefaultBranch: opt.DefaultBranch, + Name: opt.Name, + Description: opt.Description, + IssueLabels: opt.IssueLabels, + Gitignores: opt.Gitignores, + License: opt.License, + Readme: opt.Readme, + IsPrivate: opt.Private, + AutoInit: opt.AutoInit, + DefaultBranch: opt.DefaultBranch, + AdoptPreExisting: opt.AdoptPreExisting && setting.Repository.AllowAdoptionOfUnadoptedRepositories, + OverwritePreExisting: opt.OverwritePreExisting && setting.Repository.AllowOverwriteOfUnadoptedRepositories, }) if err != nil { if models.IsErrRepoAlreadyExist(err) { diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 27c8ff1e03eb..8e005d6eac45 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -135,6 +135,8 @@ func Create(ctx *context.Context) { ctx.Data["private"] = getRepoPrivate(ctx) ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate ctx.Data["default_branch"] = setting.Repository.DefaultBranch + ctx.Data["allowAdoption"] = setting.Repository.AllowAdoptionOfUnadoptedRepositories + ctx.Data["allowOverwrite"] = setting.Repository.AllowOverwriteOfUnadoptedRepositories ctxUser := checkContextUser(ctx, ctx.QueryInt64("org")) if ctx.Written() { @@ -166,6 +168,10 @@ func handleCreateError(ctx *context.Context, owner *models.User, err error, name case models.IsErrRepoAlreadyExist(err): ctx.Data["Err_RepoName"] = true ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + case models.IsErrRepoFilesAlreadyExist(err): + ctx.Data["Err_RepoName"] = true + ctx.Data["Err_AdoptOrDelete"] = true + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) case models.IsErrNameReserved(err): ctx.Data["Err_RepoName"] = true ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form) @@ -185,6 +191,8 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { ctx.Data["LabelTemplates"] = models.LabelTemplates ctx.Data["Licenses"] = models.Licenses ctx.Data["Readmes"] = models.Readmes + ctx.Data["allowAdoption"] = setting.Repository.AllowAdoptionOfUnadoptedRepositories + ctx.Data["allowOverwrite"] = setting.Repository.AllowOverwriteOfUnadoptedRepositories ctxUser := checkContextUser(ctx, form.UID) if ctx.Written() { @@ -201,15 +209,16 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { var err error if form.RepoTemplate > 0 { opts := models.GenerateRepoOptions{ - Name: form.RepoName, - Description: form.Description, - Private: form.Private, - GitContent: form.GitContent, - Topics: form.Topics, - GitHooks: form.GitHooks, - Webhooks: form.Webhooks, - Avatar: form.Avatar, - IssueLabels: form.Labels, + Name: form.RepoName, + Description: form.Description, + Private: form.Private, + GitContent: form.GitContent, + Topics: form.Topics, + GitHooks: form.GitHooks, + Webhooks: form.Webhooks, + Avatar: form.Avatar, + IssueLabels: form.Labels, + OverwritePreExisting: form.OverwritePreExisting && setting.Repository.AllowOverwriteOfUnadoptedRepositories, } if !opts.IsValid() { @@ -235,15 +244,17 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { } } else { repo, err = repo_service.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{ - Name: form.RepoName, - Description: form.Description, - Gitignores: form.Gitignores, - IssueLabels: form.IssueLabels, - License: form.License, - Readme: form.Readme, - IsPrivate: form.Private || setting.Repository.ForcePrivate, - DefaultBranch: form.DefaultBranch, - AutoInit: form.AutoInit, + Name: form.RepoName, + Description: form.Description, + Gitignores: form.Gitignores, + IssueLabels: form.IssueLabels, + License: form.License, + Readme: form.Readme, + IsPrivate: form.Private || setting.Repository.ForcePrivate, + DefaultBranch: form.DefaultBranch, + AutoInit: form.AutoInit, + AdoptPreExisting: form.AdoptPreExisting && setting.Repository.AllowAdoptionOfUnadoptedRepositories, + OverwritePreExisting: form.OverwritePreExisting && setting.Repository.AllowOverwriteOfUnadoptedRepositories, }) if err == nil { log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) @@ -269,6 +280,7 @@ func Migrate(ctx *context.Context) { ctx.Data["pull_requests"] = ctx.Query("pull_requests") == "1" ctx.Data["releases"] = ctx.Query("releases") == "1" ctx.Data["LFSActive"] = setting.LFS.StartServer + ctx.Data["allowOverwrite"] = setting.Repository.AllowOverwriteOfUnadoptedRepositories ctxUser := checkContextUser(ctx, ctx.QueryInt64("org")) if ctx.Written() { @@ -290,6 +302,10 @@ func handleMigrateError(ctx *context.Context, owner *models.User, err error, nam case models.IsErrRepoAlreadyExist(err): ctx.Data["Err_RepoName"] = true ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + case models.IsErrRepoFilesAlreadyExist(err): + ctx.Data["Err_RepoName"] = true + ctx.Data["Err_AdoptOrDelete"] = true + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) case models.IsErrNameReserved(err): ctx.Data["Err_RepoName"] = true ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form) @@ -316,6 +332,7 @@ func handleMigrateError(ctx *context.Context, owner *models.User, err error, nam // MigratePost response for migrating from external git repository func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { ctx.Data["Title"] = ctx.Tr("new_migrate") + ctx.Data["allowOverwrite"] = setting.Repository.AllowOverwriteOfUnadoptedRepositories ctxUser := checkContextUser(ctx, form.UID) if ctx.Written() { @@ -382,7 +399,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { opts.Releases = false } - err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName) + err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName, form.OverwritePreExisting && setting.Repository.AllowOverwriteOfUnadoptedRepositories) if err != nil { handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) return diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 02331c232b08..fbd1c8094db9 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -83,6 +83,10 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form) case models.IsErrNameReserved(err): ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplSettingsOptions, &form) + case models.IsErrRepoFilesAlreadyExist(err): + ctx.Data["Err_RepoName"] = true + ctx.Data["Err_AdoptOrDelete"] = true + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form) case models.IsErrNamePatternNotAllowed(err): ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) default: diff --git a/services/repository/repository.go b/services/repository/repository.go index f50b98b64099..908acebcd2e7 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" pull_service "code.gitea.io/gitea/services/pull" ) @@ -18,11 +19,7 @@ import ( func CreateRepository(doer, owner *models.User, opts models.CreateRepoOptions) (*models.Repository, error) { repo, err := repo_module.CreateRepository(doer, owner, opts) if err != nil { - if repo != nil { - if errDelete := models.DeleteRepository(doer, owner.ID, repo.ID); errDelete != nil { - log.Error("Rollback deleteRepository: %v", errDelete) - } - } + // No need to rollback here we should do this in CreateRepository... return nil, err } @@ -78,8 +75,10 @@ func PushCreateRepo(authUser, owner *models.User, repoName string) (*models.Repo } repo, err := CreateRepository(authUser, owner, models.CreateRepoOptions{ - Name: repoName, - IsPrivate: true, + Name: repoName, + IsPrivate: true, + AdoptPreExisting: setting.Repository.AllowAdoptionOfUnadoptedRepositories, + OverwritePreExisting: setting.Repository.AllowOverwriteOfUnadoptedRepositories, }) if err != nil { return nil, err diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index c4b25c73d831..9d73b75fff1d 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -100,6 +100,12 @@ +
+
+ + +
+
@@ -167,6 +173,19 @@
+
+ +
+ + +
+
+
+
+ + +
+

diff --git a/templates/repo/migrate.tmpl b/templates/repo/migrate.tmpl index 60b432beaa26..74357e0b5e28 100644 --- a/templates/repo/migrate.tmpl +++ b/templates/repo/migrate.tmpl @@ -129,7 +129,13 @@ - +
+ +
+ + +
+
-
-
- - + {{if $.allowOverwrite}} +
+
+ + +
-
+ {{end}}
@@ -173,19 +175,23 @@
-
- -
- - + {{if $.allowAdoption}} +
+ +
+ + +
-
-
-
- - + {{end}} + {{if $.allowOverwrite}} +
+
+ + +
-
+ {{end}}

diff --git a/templates/repo/migrate.tmpl b/templates/repo/migrate.tmpl index 74357e0b5e28..6881ca8ce94e 100644 --- a/templates/repo/migrate.tmpl +++ b/templates/repo/migrate.tmpl @@ -129,13 +129,15 @@
-
- -
- - + {{if $.allowOverwrite}} +
+ +
+ + +
-
+ {{end}}
{{if $.allowOverwrite}} -
-
-
+
+ +
+ + +
+
{{end}}
diff --git a/templates/repo/migrate/gitlab.tmpl b/templates/repo/migrate/gitlab.tmpl index bd9267ddce69..c64339e7114c 100644 --- a/templates/repo/migrate/gitlab.tmpl +++ b/templates/repo/migrate/gitlab.tmpl @@ -122,12 +122,13 @@
{{if $.allowOverwrite}} -
-
- +
+ +
+ + +
+
{{end}}
From ee66a298590e901852c4fd51b4aaec14bb62cbd9 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Fri, 18 Sep 2020 20:47:52 +0100 Subject: [PATCH 09/21] ensure repo closed Signed-off-by: Andrew Thornton --- modules/repository/init.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/repository/init.go b/modules/repository/init.go index 08ddb3bcd50a..1b255738c3d0 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -204,6 +204,7 @@ func adoptRepository(ctx models.DBContext, repoPath string, u *models.User, repo if err != nil { return fmt.Errorf("openRepository: %v", err) } + defer gitRepo.Close() if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %v", err) } From c18ba9b5fd06663bf41c36a4e45beaa69ecfe4f9 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 19 Sep 2020 19:24:19 +0100 Subject: [PATCH 10/21] Rewrite of adoption as per @6543 and @lunny Signed-off-by: Andrew Thornton --- custom/conf/app.example.ini | 4 +- models/repo.go | 36 +++-- models/repo_generate.go | 19 ++- models/repo_list.go | 2 + models/user.go | 4 +- modules/auth/repo_form.go | 24 ++- modules/migrations/base/options.go | 29 ++-- modules/migrations/gitea.go | 15 +- modules/repository/adopt.go | 108 ++++++++++++++ modules/repository/create.go | 69 ++++----- modules/repository/generate.go | 16 +- modules/setting/repository.go | 2 +- modules/structs/repo.go | 23 ++- modules/task/task.go | 16 +- options/locale/locale_en-US.ini | 10 +- routers/admin/repos.go | 220 +++++++++++++++++++++++++++- routers/api/v1/repo/migrate.go | 50 +++---- routers/api/v1/repo/repo.go | 20 ++- routers/repo/migrate.go | 48 +++--- routers/repo/repo.go | 52 +++---- routers/repo/setting.go | 9 +- routers/routes/routes.go | 2 + routers/user/setting/adopt.go | 55 +++++++ routers/user/setting/profile.go | 102 ++++++++++--- services/repository/repository.go | 25 +++- templates/admin/repo/list.tmpl | 3 + templates/admin/repo/unadopted.tmpl | 43 ++++++ templates/repo/create.tmpl | 25 ---- templates/repo/migrate/git.tmpl | 10 +- templates/repo/migrate/github.tmpl | 10 +- templates/repo/migrate/gitlab.tmpl | 10 +- templates/user/settings/repos.tmpl | 108 ++++++++++---- web_src/less/_admin.less | 9 ++ web_src/less/_form.less | 3 +- web_src/less/_user.less | 9 ++ 35 files changed, 842 insertions(+), 348 deletions(-) create mode 100644 modules/repository/adopt.go create mode 100644 routers/user/setting/adopt.go create mode 100644 templates/admin/repo/unadopted.tmpl diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 160c648ed6f7..17c044f20984 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -66,8 +66,8 @@ DISABLE_MIRRORS = false DEFAULT_BRANCH=master ; Allow adoption of unadopted repositories ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES=false -; Allow overwrite of unadopted repositories -ALLOW_OVERWRITE_OF_UNADOPTED_REPOSITORIES=false +; Allow deletion of unadopted repositories +ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES=false [repository.editor] ; List of file extensions for which lines should be wrapped in the Monaco editor diff --git a/models/repo.go b/models/repo.go index ba4bd7a3d848..8d7e647ae1ab 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1029,21 +1029,19 @@ func CheckCreateRepository(doer, u *User, name string, overwriteOrAdopt bool) er // CreateRepoOptions contains the create repository options type CreateRepoOptions struct { - Name string - Description string - OriginalURL string - GitServiceType api.GitServiceType - Gitignores string - IssueLabels string - License string - Readme string - DefaultBranch string - IsPrivate bool - IsMirror bool - AutoInit bool - Status RepositoryStatus - AdoptPreExisting bool - OverwritePreExisting bool + Name string + Description string + OriginalURL string + GitServiceType api.GitServiceType + Gitignores string + IssueLabels string + License string + Readme string + DefaultBranch string + IsPrivate bool + IsMirror bool + AutoInit bool + Status RepositoryStatus } // GetRepoInitFile returns repository init files @@ -1078,6 +1076,10 @@ var ( // IsUsableRepoName returns true when repository is usable func IsUsableRepoName(name string) error { + if alphaDashDotPattern.MatchString(name) { + // Note: usually this error is normally caught up earlier in the UI + return ErrNameCharsNotAllowed{Name: name} + } return isUsableName(reservedRepoNames, reservedRepoPatterns, name) } @@ -1827,6 +1829,10 @@ func GetUserRepositories(opts *SearchRepoOptions) ([]*Repository, int64, error) cond = cond.And(builder.Eq{"is_private": false}) } + if opts.LowerNames != nil && len(opts.LowerNames) > 0 { + cond = cond.And(builder.In("lower_name", opts.LowerNames)) + } + sess := x.NewSession() defer sess.Close() diff --git a/models/repo_generate.go b/models/repo_generate.go index 7d1a4062b01e..480683cd4a5b 100644 --- a/models/repo_generate.go +++ b/models/repo_generate.go @@ -18,16 +18,15 @@ import ( // GenerateRepoOptions contains the template units to generate type GenerateRepoOptions struct { - Name string - Description string - Private bool - GitContent bool - Topics bool - GitHooks bool - Webhooks bool - Avatar bool - IssueLabels bool - OverwritePreExisting bool + Name string + Description string + Private bool + GitContent bool + Topics bool + GitHooks bool + Webhooks bool + Avatar bool + IssueLabels bool } // IsValid checks whether at least one option is chosen for generation diff --git a/models/repo_list.go b/models/repo_list.go index dea88d8816c0..355b801a7ef0 100644 --- a/models/repo_list.go +++ b/models/repo_list.go @@ -175,6 +175,8 @@ type SearchRepoOptions struct { // True -> include just has milestones // False -> include just has no milestone HasMilestones util.OptionalBool + // LowerNames represents valid lower names to restrict to + LowerNames []string } //SearchOrderBy is used to sort the result diff --git a/models/user.go b/models/user.go index c7b3f0981e0e..650d5a803aa9 100644 --- a/models/user.go +++ b/models/user.go @@ -646,8 +646,8 @@ func (u *User) GetOrganizationCount() (int64, error) { } // GetRepositories returns repositories that user owns, including private repositories. -func (u *User) GetRepositories(listOpts ListOptions) (err error) { - u.Repos, _, err = GetUserRepositories(&SearchRepoOptions{Actor: u, Private: true, ListOptions: listOpts}) +func (u *User) GetRepositories(listOpts ListOptions, names ...string) (err error) { + u.Repos, _, err = GetUserRepositories(&SearchRepoOptions{Actor: u, Private: true, ListOptions: listOpts, LowerNames: names}) return err } diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 71a787fbbad0..3ad57085b053 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -45,9 +45,6 @@ type CreateRepoForm struct { Webhooks bool Avatar bool Labels bool - - AdoptPreExisting bool - OverwritePreExisting bool } // Validate validates the fields @@ -67,17 +64,16 @@ type MigrateRepoForm struct { // required: true UID int64 `json:"uid" binding:"Required"` // required: true - RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` - Mirror bool `json:"mirror"` - Private bool `json:"private"` - Description string `json:"description" binding:"MaxSize(255)"` - Wiki bool `json:"wiki"` - Milestones bool `json:"milestones"` - Labels bool `json:"labels"` - Issues bool `json:"issues"` - PullRequests bool `json:"pull_requests"` - Releases bool `json:"releases"` - OverwritePreExisting bool `json:"overwrite_pre_existing"` + RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` + Mirror bool `json:"mirror"` + Private bool `json:"private"` + Description string `json:"description" binding:"MaxSize(255)"` + Wiki bool `json:"wiki"` + Milestones bool `json:"milestones"` + Labels bool `json:"labels"` + Issues bool `json:"issues"` + PullRequests bool `json:"pull_requests"` + Releases bool `json:"releases"` } // Validate validates the fields diff --git a/modules/migrations/base/options.go b/modules/migrations/base/options.go index b6d3cb6e7b85..dbc40b138aad 100644 --- a/modules/migrations/base/options.go +++ b/modules/migrations/base/options.go @@ -18,19 +18,18 @@ type MigrateOptions struct { // required: true UID int `json:"uid" binding:"Required"` // required: true - RepoName string `json:"repo_name" binding:"Required"` - Mirror bool `json:"mirror"` - Private bool `json:"private"` - Description string `json:"description"` - OriginalURL string - GitServiceType structs.GitServiceType - Wiki bool - Issues bool - Milestones bool - Labels bool - Releases bool - Comments bool - PullRequests bool - MigrateToRepoID int64 - OverwritePreExisting bool `json:"overwrite_pre_existing"` + RepoName string `json:"repo_name" binding:"Required"` + Mirror bool `json:"mirror"` + Private bool `json:"private"` + Description string `json:"description"` + OriginalURL string + GitServiceType structs.GitServiceType + Wiki bool + Issues bool + Milestones bool + Labels bool + Releases bool + Comments bool + PullRequests bool + MigrateToRepoID int64 } diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go index 9184e6b93876..d4ba66fd389e 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -108,14 +108,13 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate var r *models.Repository if opts.MigrateToRepoID <= 0 { r, err = repo_module.CreateRepository(g.doer, owner, models.CreateRepoOptions{ - Name: g.repoName, - Description: repo.Description, - OriginalURL: repo.OriginalURL, - GitServiceType: opts.GitServiceType, - IsPrivate: opts.Private, - IsMirror: opts.Mirror, - Status: models.RepositoryBeingMigrated, - OverwritePreExisting: opts.OverwritePreExisting && (g.doer.IsAdmin || setting.Repository.AllowOverwriteOfUnadoptedRepositories), + Name: g.repoName, + Description: repo.Description, + OriginalURL: repo.OriginalURL, + GitServiceType: opts.GitServiceType, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: models.RepositoryBeingMigrated, }) } else { r, err = models.GetRepositoryByID(opts.MigrateToRepoID) diff --git a/modules/repository/adopt.go b/modules/repository/adopt.go new file mode 100644 index 000000000000..d5d6028377d4 --- /dev/null +++ b/modules/repository/adopt.go @@ -0,0 +1,108 @@ +// Copyright 2020 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 repository + +import ( + "fmt" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "github.com/unknwon/com" +) + +// AdoptRepository adopts a repository for the user/organization. +func AdoptRepository(doer, u *models.User, opts models.CreateRepoOptions) (*models.Repository, error) { + if !doer.IsAdmin && !u.CanCreateRepo() { + return nil, models.ErrReachLimitOfRepo{ + Limit: u.MaxRepoCreation, + } + } + + if len(opts.DefaultBranch) == 0 { + opts.DefaultBranch = setting.Repository.DefaultBranch + } + + repo := &models.Repository{ + OwnerID: u.ID, + Owner: u, + OwnerName: u.Name, + Name: opts.Name, + LowerName: strings.ToLower(opts.Name), + Description: opts.Description, + OriginalURL: opts.OriginalURL, + OriginalServiceType: opts.GitServiceType, + IsPrivate: opts.IsPrivate, + IsFsckEnabled: !opts.IsMirror, + CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, + Status: opts.Status, + IsEmpty: !opts.AutoInit, + } + + if err := models.WithTx(func(ctx models.DBContext) error { + repoPath := models.RepoPath(u.Name, repo.Name) + if !com.IsExist(repoPath) { + return models.ErrRepoNotExist{ + OwnerName: u.Name, + Name: repo.Name, + } + } + + if err := models.CreateRepository(ctx, doer, u, repo, true); err != nil { + return err + } + if err := adoptRepository(ctx, repoPath, doer, repo, opts); err != nil { + return fmt.Errorf("createDelegateHooks: %v", err) + } + + // Initialize Issue Labels if selected + if len(opts.IssueLabels) > 0 { + if err := models.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { + return fmt.Errorf("InitializeLabels: %v", err) + } + } + + if stdout, err := git.NewCommand("update-server-info"). + SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). + RunInDir(repoPath); err != nil { + log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) + return fmt.Errorf("CreateRepository(git update-server-info): %v", err) + } + return nil + }); err != nil { + return nil, err + } + + return repo, nil +} + +// DeleteUnadoptedRepository deletes unadopted repository files from the filesystem +func DeleteUnadoptedRepository(doer, u *models.User, repoName string) error { + if err := models.IsUsableRepoName(repoName); err != nil { + return err + } + + repoPath := models.RepoPath(u.Name, repoName) + if !com.IsExist(repoPath) { + return models.ErrRepoNotExist{ + OwnerName: u.Name, + Name: repoName, + } + } + + if exist, err := models.IsRepositoryExist(u, repoName); err != nil { + return err + } else if exist { + return models.ErrRepoAlreadyExist{ + Uname: u.Name, + Name: repoName, + } + } + + return util.RemoveAll(repoPath) +} diff --git a/modules/repository/create.go b/modules/repository/create.go index 850dad7dc8de..f77daeb5bf05 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -45,11 +45,8 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (*mod IsEmpty: !opts.AutoInit, } - overwriteOrAdopt := (!opts.IsMirror && opts.AdoptPreExisting && (doer.IsAdmin || setting.Repository.AllowAdoptionOfUnadoptedRepositories)) || - (opts.OverwritePreExisting && (doer.IsAdmin || setting.Repository.AllowOverwriteOfUnadoptedRepositories)) - if err := models.WithTx(func(ctx models.DBContext) error { - if err := models.CreateRepository(ctx, doer, u, repo, overwriteOrAdopt); err != nil { + if err := models.CreateRepository(ctx, doer, u, repo, false); err != nil { return err } @@ -58,8 +55,6 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (*mod return nil } - shouldInit := true - repoPath := models.RepoPath(u.Name, repo.Name) if com.IsExist(repoPath) { // repo already exists - We have two or three options. @@ -68,45 +63,41 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (*mod // 3. We delete it and start afresh // // Previously Gitea would just delete and start afresh - this was naughty. - if opts.AdoptPreExisting { - shouldInit = false - if err := adoptRepository(ctx, repoPath, doer, repo, opts); err != nil { - return fmt.Errorf("createDelegateHooks: %v", err) - } - } else if opts.OverwritePreExisting { - log.Warn("An already existing repository was deleted at %s", repoPath) - if err := util.RemoveAll(repoPath); err != nil { - log.Error("Unable to remove already existing repository at %s: Error: %v", repoPath, err) - return fmt.Errorf( - "unable to delete repo directory %s/%s: %v", u.Name, repo.Name, err) - } - } else { - log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) - return models.ErrRepoFilesAlreadyExist{ - Uname: u.Name, - Name: repo.Name, - } + // if opts.AdoptPreExisting { + // shouldInit = false + // if err := adoptRepository(ctx, repoPath, doer, repo, opts); err != nil { + // return fmt.Errorf("createDelegateHooks: %v", err) + // } + // } else if opts.OverwritePreExisting { + // log.Warn("An already existing repository was deleted at %s", repoPath) + // if err := util.RemoveAll(repoPath); err != nil { + // log.Error("Unable to remove already existing repository at %s: Error: %v", repoPath, err) + // return fmt.Errorf( + // "unable to delete repo directory %s/%s: %v", u.Name, repo.Name, err) + // } + // } else { + log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) + return models.ErrRepoFilesAlreadyExist{ + Uname: u.Name, + Name: repo.Name, } + // } } - if shouldInit { - if err := initRepository(ctx, repoPath, doer, repo, opts); err != nil { - if err2 := util.RemoveAll(repoPath); err2 != nil { - log.Error("initRepository: %v", err) - return fmt.Errorf( - "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) - } - return fmt.Errorf("initRepository: %v", err) + if err := initRepository(ctx, repoPath, doer, repo, opts); err != nil { + if err2 := util.RemoveAll(repoPath); err2 != nil { + log.Error("initRepository: %v", err) + return fmt.Errorf( + "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) } + return fmt.Errorf("initRepository: %v", err) } // Initialize Issue Labels if selected if len(opts.IssueLabels) > 0 { if err := models.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { - if shouldInit { - if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { - log.Error("Rollback deleteRepository: %v", errDelete) - } + if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) } return fmt.Errorf("InitializeLabels: %v", err) } @@ -116,10 +107,8 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (*mod SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). RunInDir(repoPath); err != nil { log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) - if shouldInit { - if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { - log.Error("Rollback deleteRepository: %v", errDelete) - } + if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) } return fmt.Errorf("CreateRepository(git update-server-info): %v", err) } diff --git a/modules/repository/generate.go b/modules/repository/generate.go index f3b19e9102d9..f78848c8950b 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/huandu/xstrings" @@ -246,23 +245,16 @@ func GenerateRepository(ctx models.DBContext, doer, owner *models.User, template IsFsckEnabled: templateRepo.IsFsckEnabled, TemplateID: templateRepo.ID, } - overwriteOrAdopt := opts.OverwritePreExisting && (doer.IsAdmin || setting.Repository.AllowOverwriteOfUnadoptedRepositories) - if err = models.CreateRepository(ctx, doer, owner, generateRepo, overwriteOrAdopt); err != nil { + if err = models.CreateRepository(ctx, doer, owner, generateRepo, false); err != nil { return nil, err } repoPath := generateRepo.RepoPath() if com.IsExist(repoPath) { - if opts.OverwritePreExisting { - if err = util.RemoveAll(repoPath); err != nil { - return nil, err - } - } else { - return nil, models.ErrRepoFilesAlreadyExist{ - Uname: generateRepo.OwnerName, - Name: generateRepo.Name, - } + return nil, models.ErrRepoFilesAlreadyExist{ + Uname: generateRepo.OwnerName, + Name: generateRepo.Name, } } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 37700cd68aa8..526bd6ec0863 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -45,7 +45,7 @@ var ( DisableMirrors bool DefaultBranch string AllowAdoptionOfUnadoptedRepositories bool - AllowOverwriteOfUnadoptedRepositories bool + AllowDeleteOfUnadoptedRepositories bool // Repository editor settings Editor struct { diff --git a/modules/structs/repo.go b/modules/structs/repo.go index c1027d011dbc..c57702b2821b 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -117,10 +117,6 @@ type CreateRepoOption struct { Readme string `json:"readme"` // DefaultBranch of the repository (used when initializes and in template) DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"` - // AdoptPreExisting adopt pre-existing file-system repository if it exists and this installation permits this - AdoptPreExisting bool `json:"adopt_if_exists"` - // DeleteIfExists delete pre-exisiting file-system repository if it exists and this installation permist this - OverwritePreExisting bool `json:"delete_if_exists"` } // EditRepoOption options when editing a repository's properties @@ -248,16 +244,15 @@ type MigrateRepoOptions struct { AuthPassword string `json:"auth_password"` AuthToken string `json:"auth_token"` - Mirror bool `json:"mirror"` - Private bool `json:"private"` - Description string `json:"description" binding:"MaxSize(255)"` - Wiki bool `json:"wiki"` - Milestones bool `json:"milestones"` - Labels bool `json:"labels"` - Issues bool `json:"issues"` - PullRequests bool `json:"pull_requests"` - Releases bool `json:"releases"` - OverwritePreExisting bool `json:"overwrite_pre_existing"` + Mirror bool `json:"mirror"` + Private bool `json:"private"` + Description string `json:"description" binding:"MaxSize(255)"` + Wiki bool `json:"wiki"` + Milestones bool `json:"milestones"` + Labels bool `json:"labels"` + Issues bool `json:"issues"` + PullRequests bool `json:"pull_requests"` + Releases bool `json:"releases"` } // TokenAuth represents whether a service type supports token-based auth diff --git a/modules/task/task.go b/modules/task/task.go index 831eef25953a..72f111ecc7c5 100644 --- a/modules/task/task.go +++ b/modules/task/task.go @@ -14,7 +14,6 @@ import ( "code.gitea.io/gitea/modules/migrations/base" "code.gitea.io/gitea/modules/queue" repo_module "code.gitea.io/gitea/modules/repository" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" ) @@ -84,14 +83,13 @@ func CreateMigrateTask(doer, u *models.User, opts base.MigrateOptions) (*models. } repo, err := repo_module.CreateRepository(doer, u, models.CreateRepoOptions{ - Name: opts.RepoName, - Description: opts.Description, - OriginalURL: opts.OriginalURL, - GitServiceType: opts.GitServiceType, - IsPrivate: opts.Private, - IsMirror: opts.Mirror, - Status: models.RepositoryBeingMigrated, - OverwritePreExisting: opts.OverwritePreExisting && (doer.IsAdmin || setting.Repository.AllowOverwriteOfUnadoptedRepositories), + Name: opts.RepoName, + Description: opts.Description, + OriginalURL: opts.OriginalURL, + GitServiceType: opts.GitServiceType, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: models.RepositoryBeingMigrated, }) if err != nil { task.EndTime = timeutil.TimeStampNow() diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4ec1605b5b03..4fcfb5212cb1 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -358,8 +358,8 @@ username_been_taken = The username is already taken. repo_name_been_taken = The repository name is already used. repository_files_already_exist = Files already exist for this repository. Contact the system administrator. repository_files_already_exist.adopt = Files already exist for this repository and can only be Adopted. -repository_files_already_exist.overwrite = Files already exist for this repository. You must explicitly overwrite them. -repository_files_already_exist.adopt_or_overwrite = Files already exist for this repository. Either adopt them or explicitly overwrite them. +repository_files_already_exist.delete = Files already exist for this repository. You must delete them. +repository_files_already_exist.adopt_or_delete = Files already exist for this repository. Either adopt them or delete them. visit_rate_limit = Remote visit addressed rate limitation. 2fa_auth_required = Remote visit required two factors authentication. org_name_been_taken = The organization name is already taken. @@ -688,8 +688,8 @@ unit_disabled = The site administrator has disabled this repository section. language_other = Other adopt_preexisting_label = Adopt Files adopt_preexisting = Adopt pre-existing files -overwrite_preexisting_label = Overwrite -overwrite_preexisting = Overwrite pre-existing files +delete_preexisting_label = Delete +delete_preexisting = Delete pre-existing files desc.private = Private desc.public = Public @@ -2064,6 +2064,8 @@ orgs.members = Members orgs.new_orga = New Organization repos.repo_manage_panel = Repository Management +repos.unadopted = Unadopted Repositories +repos.unadopted.no_more = No more unadopted repositories found repos.owner = Owner repos.name = Name repos.private = Private diff --git a/routers/admin/repos.go b/routers/admin/repos.go index 39a1d7596c3f..2cb7021333be 100644 --- a/routers/admin/repos.go +++ b/routers/admin/repos.go @@ -5,17 +5,25 @@ package admin import ( + "fmt" + "os" + "path/filepath" + "strings" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers" repo_service "code.gitea.io/gitea/services/repository" + "github.com/unknwon/com" ) const ( - tplRepos base.TplName = "admin/repo/list" + tplRepos base.TplName = "admin/repo/list" + tplUnadoptedRepos base.TplName = "admin/repo/unadopted" ) // Repos show all the repositories @@ -50,3 +58,213 @@ func DeleteRepo(ctx *context.Context) { "redirect": setting.AppSubURL + "/admin/repos?page=" + ctx.Query("page") + "&sort=" + ctx.Query("sort"), }) } + +// UnadoptedRepos lists the unadopted repositories +func UnadoptedRepos(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("admin.repositories") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminRepositories"] = true + + opts := models.ListOptions{ + PageSize: setting.UI.Admin.UserPagingNum, + Page: ctx.QueryInt("page"), + } + + if opts.Page <= 0 { + opts.Page = 1 + } + start := (opts.Page - 1) * opts.PageSize + end := start + opts.PageSize + + repoNamesToCheck := make([]string, 0, opts.PageSize) + + repoNames := make([]string, 0, opts.PageSize) + var ctxUser *models.User + + count := 0 + + // We're going to iterate by pagesize. + root := filepath.Join(setting.RepoRootPath) + if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() || path == root { + return nil + } + + if strings.IndexRune(path[len(root)+1:], filepath.Separator) < 0 { + // Got a new user + + // Clean up old repoNamesToCheck + if len(repoNamesToCheck) > 0 { + repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: models.ListOptions{ + Page: 1, + PageSize: opts.PageSize, + }, LowerNames: repoNamesToCheck}) + if err != nil { + return err + } + for _, name := range repoNamesToCheck { + found := false + repoLoopCatchup: + for i, repo := range repos { + if repo.LowerName == name { + found = true + repos = append(repos[:i], repos[i+1:]...) + break repoLoopCatchup + } + } + if !found { + if count >= start && count < end { + repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name)) + } + count++ + } + } + repoNamesToCheck = repoNamesToCheck[:0] + } + + ctxUser, err = models.GetUserByName(info.Name()) + if err != nil { + if models.IsErrUserNotExist(err) { + log.Debug("Missing user: %s", info.Name()) + return filepath.SkipDir + } + return err + } + return nil + } + + name := info.Name() + + if !strings.HasSuffix(name, ".git") { + return filepath.SkipDir + } + name = name[:len(name)-4] + if models.IsUsableRepoName(name) != nil || strings.ToLower(name) != name { + return filepath.SkipDir + } + if count < end { + repoNamesToCheck = append(repoNamesToCheck, name) + if len(repoNamesToCheck) >= opts.PageSize { + repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: models.ListOptions{ + Page: 1, + PageSize: opts.PageSize, + }, LowerNames: repoNamesToCheck}) + if err != nil { + return err + } + for _, name := range repoNamesToCheck { + found := false + repoLoop: + for i, repo := range repos { + if repo.Name == name { + found = true + repos = append(repos[:i], repos[i+1:]...) + break repoLoop + } + } + if !found { + if count >= start && count < end { + repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name)) + } + count++ + } + } + repoNamesToCheck = repoNamesToCheck[:0] + } + return filepath.SkipDir + } + count++ + return filepath.SkipDir + }); err != nil { + ctx.ServerError("filepath.Walk", err) + return + } + + if len(repoNamesToCheck) > 0 { + repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: models.ListOptions{ + Page: 1, + PageSize: opts.PageSize, + }, LowerNames: repoNamesToCheck}) + if err != nil { + ctx.ServerError("filepath.Walk", err) + return + } + for _, name := range repoNamesToCheck { + found := false + repoLoop: + for i, repo := range repos { + if repo.LowerName == name { + found = true + repos = append(repos[:i], repos[i+1:]...) + break repoLoop + } + } + if !found { + if count >= start && count < end { + repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name)) + } + count++ + } + if count >= end { + break + } + } + } + ctx.Data["Dirs"] = repoNames + pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager + ctx.HTML(200, tplUnadoptedRepos) +} + +// AdoptOrDeleteRepository adopts or deletes a repository +func AdoptOrDeleteRepository(ctx *context.Context) { + dir := ctx.Query("name") + action := ctx.Query("action") + dirSplit := strings.SplitN(dir, "/", 2) + log.Debug("dir: %s action %s %s", dir, action, dirSplit) + if len(dirSplit) != 2 { + ctx.Redirect(setting.AppSubURL + "/admin/repos") + return + } + + ctxUser, err := models.GetUserByName(dirSplit[0]) + if err != nil { + if models.IsErrUserNotExist(err) { + log.Debug("User does not exist: %s", dirSplit[0]) + ctx.Redirect(setting.AppSubURL + "/admin/repos") + return + } + ctx.ServerError("GetUserByName", err) + return + } + + repoName := dirSplit[1] + + // check not a repo + if has, err := models.IsRepositoryExist(ctxUser, repoName); err != nil { + ctx.ServerError("IsRepositoryExist", err) + return + } else if has || !com.IsDir(models.RepoPath(ctxUser.Name, repoName)) { + log.Debug("has: %t, notDir: %s", has, models.RepoPath(ctxUser.Name, repoName)) + // Fallthrough to failure mode + } else if action == "adopt" { + if _, err := repository.AdoptRepository(ctx.User, ctxUser, models.CreateRepoOptions{ + Name: dirSplit[1], + IsPrivate: true, + }); err != nil { + ctx.ServerError("repository.AdoptRepository", err) + return + } + } else if action == "delete" { + if err := repository.DeleteUnadoptedRepository(ctx.User, ctxUser, dirSplit[1]); err != nil { + ctx.ServerError("repository.AdoptRepository", err) + return + } + } + ctx.Redirect(setting.AppSubURL + "/admin/repos/unadopted") + return +} diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index 9489ca34e292..f9cddbb7cdce 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -120,23 +120,22 @@ func Migrate(ctx *context.APIContext, form api.MigrateRepoOptions) { } var opts = migrations.MigrateOptions{ - CloneAddr: remoteAddr, - RepoName: form.RepoName, - Description: form.Description, - Private: form.Private || setting.Repository.ForcePrivate, - Mirror: form.Mirror, - AuthUsername: form.AuthUsername, - AuthPassword: form.AuthPassword, - AuthToken: form.AuthToken, - Wiki: form.Wiki, - Issues: form.Issues, - Milestones: form.Milestones, - Labels: form.Labels, - Comments: true, - PullRequests: form.PullRequests, - Releases: form.Releases, - GitServiceType: gitServiceType, - OverwritePreExisting: form.OverwritePreExisting, + CloneAddr: remoteAddr, + RepoName: form.RepoName, + Description: form.Description, + Private: form.Private || setting.Repository.ForcePrivate, + Mirror: form.Mirror, + AuthUsername: form.AuthUsername, + AuthPassword: form.AuthPassword, + AuthToken: form.AuthToken, + Wiki: form.Wiki, + Issues: form.Issues, + Milestones: form.Milestones, + Labels: form.Labels, + Comments: true, + PullRequests: form.PullRequests, + Releases: form.Releases, + GitServiceType: gitServiceType, } if opts.Mirror { opts.Issues = false @@ -148,14 +147,13 @@ func Migrate(ctx *context.APIContext, form api.MigrateRepoOptions) { } repo, err := repo_module.CreateRepository(ctx.User, repoOwner, models.CreateRepoOptions{ - Name: opts.RepoName, - Description: opts.Description, - OriginalURL: form.CloneAddr, - GitServiceType: gitServiceType, - IsPrivate: opts.Private, - IsMirror: opts.Mirror, - Status: models.RepositoryBeingMigrated, - OverwritePreExisting: opts.OverwritePreExisting && (ctx.User.IsAdmin || setting.Repository.AllowOverwriteOfUnadoptedRepositories), + Name: opts.RepoName, + Description: opts.Description, + OriginalURL: form.CloneAddr, + GitServiceType: gitServiceType, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: models.RepositoryBeingMigrated, }) if err != nil { handleMigrateError(ctx, repoOwner, remoteAddr, err) @@ -201,7 +199,7 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *models.User, remoteA case models.IsErrRepoAlreadyExist(err): ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") case models.IsErrRepoFilesAlreadyExist(err): - ctx.Error(http.StatusConflict, "", "Files already exist for this repository. Adopt them or explicitly overwrite them.") + ctx.Error(http.StatusConflict, "", "Files already exist for this repository. Adopt them or delete them.") case migrations.IsRateLimitError(err): ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit addressed rate limitation.") case migrations.IsTwoFactorAuthError(err): diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 41d80b11d0f0..603187c16dbb 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -235,17 +235,15 @@ func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateR opt.Readme = "Default" } repo, err := repo_service.CreateRepository(ctx.User, owner, models.CreateRepoOptions{ - Name: opt.Name, - Description: opt.Description, - IssueLabels: opt.IssueLabels, - Gitignores: opt.Gitignores, - License: opt.License, - Readme: opt.Readme, - IsPrivate: opt.Private, - AutoInit: opt.AutoInit, - DefaultBranch: opt.DefaultBranch, - AdoptPreExisting: opt.AdoptPreExisting && setting.Repository.AllowAdoptionOfUnadoptedRepositories, - OverwritePreExisting: opt.OverwritePreExisting && (ctx.User.IsAdmin || setting.Repository.AllowOverwriteOfUnadoptedRepositories), + Name: opt.Name, + Description: opt.Description, + IssueLabels: opt.IssueLabels, + Gitignores: opt.Gitignores, + License: opt.License, + Readme: opt.Readme, + IsPrivate: opt.Private, + AutoInit: opt.AutoInit, + DefaultBranch: opt.DefaultBranch, }) if err != nil { if models.IsErrRepoAlreadyExist(err) { diff --git a/routers/repo/migrate.go b/routers/repo/migrate.go index f90e150ba7c3..92e1f46e4b0d 100644 --- a/routers/repo/migrate.go +++ b/routers/repo/migrate.go @@ -46,7 +46,6 @@ func Migrate(ctx *context.Context) { ctx.Data["LFSActive"] = setting.LFS.StartServer // Plain git should be first ctx.Data["service"] = structs.GitServiceType(serviceType) - ctx.Data["allowOverwrite"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowOverwriteOfUnadoptedRepositories ctxUser := checkContextUser(ctx, ctx.QueryInt64("org")) if ctx.Written() { @@ -70,14 +69,13 @@ func handleMigrateError(ctx *context.Context, owner *models.User, err error, nam ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) case models.IsErrRepoFilesAlreadyExist(err): ctx.Data["Err_RepoName"] = true - ctx.Data["Err_AdoptOrDelete"] = true switch { - case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowOverwriteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_overwrite"), tpl, form) + case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) - case setting.Repository.AllowOverwriteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.overwrite"), tpl, form) + case setting.Repository.AllowDeleteOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) default: ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) } @@ -110,7 +108,6 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { // Plain git should be first ctx.Data["service"] = form.Service ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...) - ctx.Data["allowOverwrite"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowOverwriteOfUnadoptedRepositories ctxUser := checkContextUser(ctx, form.UID) if ctx.Written() { @@ -145,24 +142,23 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { } var opts = migrations.MigrateOptions{ - OriginalURL: form.CloneAddr, - GitServiceType: structs.GitServiceType(form.Service), - CloneAddr: remoteAddr, - RepoName: form.RepoName, - Description: form.Description, - Private: form.Private || setting.Repository.ForcePrivate, - Mirror: form.Mirror && !setting.Repository.DisableMirrors, - AuthUsername: form.AuthUsername, - AuthPassword: form.AuthPassword, - AuthToken: form.AuthToken, - Wiki: form.Wiki, - Issues: form.Issues, - Milestones: form.Milestones, - Labels: form.Labels, - Comments: form.Issues || form.PullRequests, - PullRequests: form.PullRequests, - Releases: form.Releases, - OverwritePreExisting: form.OverwritePreExisting, + OriginalURL: form.CloneAddr, + GitServiceType: structs.GitServiceType(form.Service), + CloneAddr: remoteAddr, + RepoName: form.RepoName, + Description: form.Description, + Private: form.Private || setting.Repository.ForcePrivate, + Mirror: form.Mirror && !setting.Repository.DisableMirrors, + AuthUsername: form.AuthUsername, + AuthPassword: form.AuthPassword, + AuthToken: form.AuthToken, + Wiki: form.Wiki, + Issues: form.Issues, + Milestones: form.Milestones, + Labels: form.Labels, + Comments: form.Issues || form.PullRequests, + PullRequests: form.PullRequests, + Releases: form.Releases, } if opts.Mirror { opts.Issues = false @@ -173,7 +169,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { opts.Releases = false } - err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName, form.OverwritePreExisting && (ctx.IsUserSiteAdmin() || setting.Repository.AllowOverwriteOfUnadoptedRepositories)) + err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName, false) if err != nil { handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) return diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 9a87b8dfc4bb..65358fc34457 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -129,8 +129,6 @@ func Create(ctx *context.Context) { ctx.Data["private"] = getRepoPrivate(ctx) ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate ctx.Data["default_branch"] = setting.Repository.DefaultBranch - ctx.Data["allowAdoption"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories - ctx.Data["allowOverwrite"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowOverwriteOfUnadoptedRepositories ctxUser := checkContextUser(ctx, ctx.QueryInt64("org")) if ctx.Written() { @@ -164,14 +162,13 @@ func handleCreateError(ctx *context.Context, owner *models.User, err error, name ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) case models.IsErrRepoFilesAlreadyExist(err): ctx.Data["Err_RepoName"] = true - ctx.Data["Err_AdoptOrDelete"] = true switch { - case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowOverwriteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_overwrite"), tpl, form) + case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) - case setting.Repository.AllowOverwriteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.overwrite"), tpl, form) + case setting.Repository.AllowDeleteOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) default: ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) } @@ -194,8 +191,6 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { ctx.Data["LabelTemplates"] = models.LabelTemplates ctx.Data["Licenses"] = models.Licenses ctx.Data["Readmes"] = models.Readmes - ctx.Data["allowAdoption"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories - ctx.Data["allowOverwrite"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowOverwriteOfUnadoptedRepositories ctxUser := checkContextUser(ctx, form.UID) if ctx.Written() { @@ -212,16 +207,15 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { var err error if form.RepoTemplate > 0 { opts := models.GenerateRepoOptions{ - Name: form.RepoName, - Description: form.Description, - Private: form.Private, - GitContent: form.GitContent, - Topics: form.Topics, - GitHooks: form.GitHooks, - Webhooks: form.Webhooks, - Avatar: form.Avatar, - IssueLabels: form.Labels, - OverwritePreExisting: form.OverwritePreExisting && (ctx.IsUserSiteAdmin() || setting.Repository.AllowOverwriteOfUnadoptedRepositories), + Name: form.RepoName, + Description: form.Description, + Private: form.Private, + GitContent: form.GitContent, + Topics: form.Topics, + GitHooks: form.GitHooks, + Webhooks: form.Webhooks, + Avatar: form.Avatar, + IssueLabels: form.Labels, } if !opts.IsValid() { @@ -247,17 +241,15 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { } } else { repo, err = repo_service.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{ - Name: form.RepoName, - Description: form.Description, - Gitignores: form.Gitignores, - IssueLabels: form.IssueLabels, - License: form.License, - Readme: form.Readme, - IsPrivate: form.Private || setting.Repository.ForcePrivate, - DefaultBranch: form.DefaultBranch, - AutoInit: form.AutoInit, - AdoptPreExisting: form.AdoptPreExisting && (ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories), - OverwritePreExisting: form.OverwritePreExisting && (ctx.IsUserSiteAdmin() || setting.Repository.AllowOverwriteOfUnadoptedRepositories), + Name: form.RepoName, + Description: form.Description, + Gitignores: form.Gitignores, + IssueLabels: form.IssueLabels, + License: form.License, + Readme: form.Readme, + IsPrivate: form.Private || setting.Repository.ForcePrivate, + DefaultBranch: form.DefaultBranch, + AutoInit: form.AutoInit, }) if err == nil { log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 8fb2824862d7..da83bffdbeaa 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -85,14 +85,13 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplSettingsOptions, &form) case models.IsErrRepoFilesAlreadyExist(err): ctx.Data["Err_RepoName"] = true - ctx.Data["Err_AdoptOrDelete"] = true switch { - case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowOverwriteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_overwrite"), tplSettingsOptions, form) + case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form) - case setting.Repository.AllowOverwriteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.overwrite"), tplSettingsOptions, form) + case setting.Repository.AllowDeleteOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form) default: ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form) } diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 5345a1017198..f60af5dad040 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -402,6 +402,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/keys/delete", userSetting.DeleteKey) m.Get("/organization", userSetting.Organization) m.Get("/repos", userSetting.Repos) + m.Post("/repos/unadopted", userSetting.AdoptOrDeleteRepository) }, reqSignIn, func(ctx *context.Context) { ctx.Data["PageIsUserSettings"] = true ctx.Data["AllThemes"] = setting.UI.Themes @@ -461,6 +462,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/repos", func() { m.Get("", admin.Repos) + m.Combo("/unadopted").Get(admin.UnadoptedRepos).Post(admin.AdoptOrDeleteRepository) m.Post("/delete", admin.DeleteRepo) }) diff --git a/routers/user/setting/adopt.go b/routers/user/setting/adopt.go new file mode 100644 index 000000000000..ffa42d817e42 --- /dev/null +++ b/routers/user/setting/adopt.go @@ -0,0 +1,55 @@ +// Copyright 2020 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 setting + +import ( + "path/filepath" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + "github.com/unknwon/com" +) + +// AdoptOrDeleteRepository adopts or deletes a repository +func AdoptOrDeleteRepository(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsRepos"] = true + allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories + ctx.Data["allowAdopt"] = allowAdopt + allowDelete := ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories + ctx.Data["allowDelete"] = allowDelete + + dir := ctx.Query("name") + action := ctx.Query("action") + + ctxUser := ctx.User + root := filepath.Join(models.UserPath(ctxUser.LowerName)) + + // check not a repo + if has, err := models.IsRepositoryExist(ctxUser, dir); err != nil { + ctx.ServerError("IsRepositoryExist", err) + return + } else if has || !com.IsDir(filepath.Join(root, dir+".git")) { + // Fallthrough to failure mode + } else if action == "adopt" && allowAdopt { + if _, err := repository.AdoptRepository(ctxUser, ctxUser, models.CreateRepoOptions{ + Name: dir, + IsPrivate: true, + }); err != nil { + ctx.ServerError("repository.AdoptRepository", err) + return + } + } else if action == "delete" && allowDelete { + if err := repository.DeleteUnadoptedRepository(ctxUser, ctxUser, dir); err != nil { + ctx.ServerError("repository.AdoptRepository", err) + return + } + } + + ctx.Redirect(setting.AppSubURL + "/user/settings/repos") + return +} diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go index ba9ba2b257fa..fe0506946ad6 100644 --- a/routers/user/setting/profile.go +++ b/routers/user/setting/profile.go @@ -9,6 +9,8 @@ import ( "errors" "fmt" "io/ioutil" + "os" + "path/filepath" "strings" "code.gitea.io/gitea/models" @@ -197,32 +199,96 @@ func Organization(ctx *context.Context) { func Repos(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsRepos"] = true - ctxUser := ctx.User + ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories + ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories - var err error - if err = ctxUser.GetRepositories(models.ListOptions{Page: 1, PageSize: setting.UI.User.RepoPagingNum}); err != nil { - ctx.ServerError("GetRepositories", err) - return + opts := models.ListOptions{ + PageSize: setting.UI.Admin.UserPagingNum, + Page: ctx.QueryInt("page"), + } + + if opts.Page <= 0 { + opts.Page = 1 } - repos := ctxUser.Repos + start := (opts.Page - 1) * opts.PageSize + end := start + opts.PageSize - for i := range repos { - if repos[i].IsFork { - err := repos[i].GetBaseRepo() + adoptOrDelete := ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories) + + ctxUser := ctx.User + count := 0 + + if adoptOrDelete { + repoNames := make([]string, 0, setting.UI.Admin.UserPagingNum) + repos := map[string]*models.Repository{} + // We're going to iterate by pagesize. + root := filepath.Join(models.UserPath(ctxUser.Name)) + if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { - ctx.ServerError("GetBaseRepo", err) - return + return err } - err = repos[i].BaseRepo.GetOwner() - if err != nil { - ctx.ServerError("GetOwner", err) - return + if !info.IsDir() || path == root { + return nil } + name := info.Name() + if !strings.HasSuffix(name, ".git") { + return filepath.SkipDir + } + name = name[:len(name)-4] + if models.IsUsableRepoName(name) != nil || strings.ToLower(name) != name { + return filepath.SkipDir + } + if count >= start && count < end { + repoNames = append(repoNames, name) + } + count++ + return filepath.SkipDir + }); err != nil { + ctx.ServerError("filepath.Walk", err) + return } - } - ctx.Data["Owner"] = ctxUser - ctx.Data["Repos"] = repos + if err := ctxUser.GetRepositories(models.ListOptions{Page: 1, PageSize: setting.UI.Admin.UserPagingNum}, repoNames...); err != nil { + ctx.ServerError("GetRepositories", err) + return + } + for _, repo := range ctxUser.Repos { + if repo.IsFork { + if err := repo.GetBaseRepo(); err != nil { + ctx.ServerError("GetBaseRepo", err) + return + } + } + repos[repo.LowerName] = repo + } + ctx.Data["Dirs"] = repoNames + ctx.Data["ReposMap"] = repos + } else { + var err error + var count64 int64 + ctxUser.Repos, count64, err = models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) + if err != nil { + ctx.ServerError("GetRepositories", err) + return + } + count = int(count64) + repos := ctxUser.Repos + + for i := range repos { + if repos[i].IsFork { + if err := repos[i].GetBaseRepo(); err != nil { + ctx.ServerError("GetBaseRepo", err) + return + } + } + } + + ctx.Data["Repos"] = repos + } + ctx.Data["Owner"] = ctxUser + pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager ctx.HTML(200, tplSettingsRepositories) } diff --git a/services/repository/repository.go b/services/repository/repository.go index c5ee255483ff..c6768f3f00f1 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" repo_module "code.gitea.io/gitea/modules/repository" - "code.gitea.io/gitea/modules/setting" pull_service "code.gitea.io/gitea/services/pull" ) @@ -28,6 +27,24 @@ func CreateRepository(doer, owner *models.User, opts models.CreateRepoOptions) ( return repo, nil } +// AdoptRepository adopts pre-existing repository files for the user/organization. +func AdoptRepository(doer, owner *models.User, opts models.CreateRepoOptions) (*models.Repository, error) { + repo, err := repo_module.AdoptRepository(doer, owner, opts) + if err != nil { + // No need to rollback here we should do this in AdoptRepository... + return nil, err + } + + notification.NotifyCreateRepository(doer, owner, repo) + + return repo, nil +} + +// DeleteUnadoptedRepository adopts pre-existing repository files for the user/organization. +func DeleteUnadoptedRepository(doer, owner *models.User, name string) error { + return repo_module.DeleteUnadoptedRepository(doer, owner, name) +} + // ForkRepository forks a repository func ForkRepository(doer, u *models.User, oldRepo *models.Repository, name, desc string) (*models.Repository, error) { repo, err := repo_module.ForkRepository(doer, u, oldRepo, name, desc) @@ -70,10 +87,8 @@ func PushCreateRepo(authUser, owner *models.User, repoName string) (*models.Repo } repo, err := CreateRepository(authUser, owner, models.CreateRepoOptions{ - Name: repoName, - IsPrivate: true, - AdoptPreExisting: authUser.IsAdmin || setting.Repository.AllowAdoptionOfUnadoptedRepositories, - OverwritePreExisting: authUser.IsAdmin || setting.Repository.AllowOverwriteOfUnadoptedRepositories, + Name: repoName, + IsPrivate: true, }) if err != nil { return nil, err diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 4c3b77dcfb4d..51e329e03835 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -5,6 +5,9 @@ {{template "base/alert" .}}

{{.i18n.Tr "admin.repos.repo_manage_panel"}} ({{.i18n.Tr "admin.total" .Total}}) +

{{template "admin/repo/search" .}} diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl new file mode 100644 index 000000000000..73d2793a2dda --- /dev/null +++ b/templates/admin/repo/unadopted.tmpl @@ -0,0 +1,43 @@ +{{template "base/head" .}} +
+ {{template "admin/navbar" .}} +
+ {{template "base/alert" .}} +

+ {{.i18n.Tr "admin.repos.unadopted"}} + +

+
+ {{if .Dirs}} +
+ {{range $dirI, $dir := .Dirs}} +
+
+ {{svg "octicon-file-directory"}} + {{$dir}} +
+
+ {{$.CsrfTokenHtml}} + + + +
+
+
+
+ {{end}} +
+ {{template "base/paginate" .}} + {{else}} +
+ {{.i18n.Tr "admin.repos.unadopted.no_more"}} +
+ {{template "base/paginate" .}} + {{end}} +
+
+
+ +{{template "base/footer" .}} diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index b0c4077e5630..c4b25c73d831 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -100,14 +100,6 @@
- {{if $.allowOverwrite}} -
-
- - -
-
- {{end}}
@@ -175,23 +167,6 @@
- {{if $.allowAdoption}} -
- -
- - -
-
- {{end}} - {{if $.allowOverwrite}} -
-
- - -
-
- {{end}}
diff --git a/templates/repo/migrate/git.tmpl b/templates/repo/migrate/git.tmpl index a48b55ec0272..34a1c7bd0d1e 100644 --- a/templates/repo/migrate/git.tmpl +++ b/templates/repo/migrate/git.tmpl @@ -87,15 +87,7 @@ - {{if $.allowOverwrite}} -
- -
- - -
-
- {{end}} +
- {{if $.allowOverwrite}} -
- -
- - -
-
- {{end}} +
- {{if $.allowOverwrite}} -
- -
- - -
-
- {{end}} +
+ {{end}} + {{if $.allowDelete}} + + {{end}} + +
+ {{end}} + - - {{end}} - + {{end}} + + {{template "base/paginate" .}} + {{else}} +
+ {{.i18n.Tr "settings.repos_none"}} +
+ {{end}} {{else}} -
- {{.i18n.Tr "settings.repos_none"}} -
+ {{if .Repos}} +
+ {{range .Repos}} +
+
+ {{if .IsPrivate}} + {{svg "octicon-lock"}} + {{else if .IsFork}} + {{svg "octicon-repo-forked"}} + {{else if .IsMirror}} + {{svg "octicon-mirror"}} + {{else if .IsTemplate}} + {{svg "octicon-repo-template"}} + {{else}} + {{svg "octicon-repo"}} + {{end}} + {{$.OwnerName}}/{{.Name}} + {{SizeFmt .Size}} + {{if .IsFork}} + {{$.i18n.Tr "repo.forked_from"}} + {{.BaseRepo.OwnerName}}/{{.BaseRepo.Name}} + {{end}} +
+
+ {{end}} +
+ {{template "base/paginate" .}} + {{else}} +
+ {{.i18n.Tr "settings.repos_none"}} +
+ {{end}} {{end}} diff --git a/web_src/less/_admin.less b/web_src/less/_admin.less index 29afe96b067c..bd759f34d5fb 100644 --- a/web_src/less/_admin.less +++ b/web_src/less/_admin.less @@ -30,6 +30,15 @@ form tbody button[type='submit'] { padding: 5px 8px; } + + .settings .button.adopt, + .settings .button.delete { + margin-top: -15px; + margin-bottom: -15px; + .label { + vertical-align: middle; + } + } } .ui.header, diff --git a/web_src/less/_form.less b/web_src/less/_form.less index 40120c89a17b..f8123b8f9bff 100644 --- a/web_src/less/_form.less +++ b/web_src/less/_form.less @@ -204,8 +204,7 @@ &.new.repo { .ui.form { @media only screen and (min-width: 768px) { - #auto-init, - #overwrite-pre-existing { + #auto-init { margin-left: @create-page-form-input-padding+15px; } } diff --git a/web_src/less/_user.less b/web_src/less/_user.less index 0b983a382c19..fcc5c0290fc3 100644 --- a/web_src/less/_user.less +++ b/web_src/less/_user.less @@ -135,6 +135,15 @@ } } + .button.adopt, + .button.delete { + margin-top: -15px; + margin-bottom: -15px; + .label { + vertical-align: middle; + } + } + &.link-account:not(.icon) { padding-top: 15px; padding-bottom: 5px; From 0dc082437fc9801486398a98dcdd5d73ef210bca Mon Sep 17 00:00:00 2001 From: zeripath Date: Mon, 21 Sep 2020 23:01:43 +0100 Subject: [PATCH 11/21] Apply suggestions from code review --- routers/admin/repos.go | 6 +----- routers/user/setting/adopt.go | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/routers/admin/repos.go b/routers/admin/repos.go index 2cb7021333be..4b4634b85683 100644 --- a/routers/admin/repos.go +++ b/routers/admin/repos.go @@ -93,7 +93,7 @@ func UnadoptedRepos(ctx *context.Context) { return nil } - if strings.IndexRune(path[len(root)+1:], filepath.Separator) < 0 { + if strings.ContainsRune(path[len(root)+1:], filepath.Separator) { // Got a new user // Clean up old repoNamesToCheck @@ -208,9 +208,6 @@ func UnadoptedRepos(ctx *context.Context) { } count++ } - if count >= end { - break - } } } ctx.Data["Dirs"] = repoNames @@ -266,5 +263,4 @@ func AdoptOrDeleteRepository(ctx *context.Context) { } } ctx.Redirect(setting.AppSubURL + "/admin/repos/unadopted") - return } diff --git a/routers/user/setting/adopt.go b/routers/user/setting/adopt.go index ffa42d817e42..e4836df2614f 100644 --- a/routers/user/setting/adopt.go +++ b/routers/user/setting/adopt.go @@ -51,5 +51,4 @@ func AdoptOrDeleteRepository(ctx *context.Context) { } ctx.Redirect(setting.AppSubURL + "/user/settings/repos") - return } From d4a7c0cd03b0c192ed9572592f690f2931edddcf Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 22 Sep 2020 08:53:43 +0100 Subject: [PATCH 12/21] update swagger Signed-off-by: Andrew Thornton --- templates/swagger/v1_json.tmpl | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 730e9fef9593..6792f7444b06 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -11892,11 +11892,6 @@ "name" ], "properties": { - "adopt_if_exists": { - "description": "AdoptPreExisting adopt pre-existing file-system repository if it exists and this installation permits this", - "type": "boolean", - "x-go-name": "AdoptPreExisting" - }, "auto_init": { "description": "Whether the repository should be auto-intialized?", "type": "boolean", @@ -11907,11 +11902,6 @@ "type": "string", "x-go-name": "DefaultBranch" }, - "delete_if_exists": { - "description": "DeleteIfExists delete pre-exisiting file-system repository if it exists and this installation permist this", - "type": "boolean", - "x-go-name": "OverwritePreExisting" - }, "description": { "description": "Description of the repository to create", "type": "string", @@ -13651,10 +13641,6 @@ "type": "boolean", "x-go-name": "Mirror" }, - "overwrite_pre_existing": { - "type": "boolean", - "x-go-name": "OverwritePreExisting" - }, "private": { "type": "boolean", "x-go-name": "Private" @@ -13732,10 +13718,6 @@ "type": "boolean", "x-go-name": "Mirror" }, - "overwrite_pre_existing": { - "type": "boolean", - "x-go-name": "OverwritePreExisting" - }, "private": { "type": "boolean", "x-go-name": "Private" From 08a333f66782fee9e3e5af80d33de4180baf0603 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 23 Sep 2020 09:24:28 +0100 Subject: [PATCH 13/21] missing not Signed-off-by: Andrew Thornton --- routers/admin/repos.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/routers/admin/repos.go b/routers/admin/repos.go index 4b4634b85683..f36fcddab99f 100644 --- a/routers/admin/repos.go +++ b/routers/admin/repos.go @@ -93,7 +93,7 @@ func UnadoptedRepos(ctx *context.Context) { return nil } - if strings.ContainsRune(path[len(root)+1:], filepath.Separator) { + if !strings.ContainsRune(path[len(root)+1:], filepath.Separator) { // Got a new user // Clean up old repoNamesToCheck @@ -222,7 +222,6 @@ func AdoptOrDeleteRepository(ctx *context.Context) { dir := ctx.Query("name") action := ctx.Query("action") dirSplit := strings.SplitN(dir, "/", 2) - log.Debug("dir: %s action %s %s", dir, action, dirSplit) if len(dirSplit) != 2 { ctx.Redirect(setting.AppSubURL + "/admin/repos") return From d0d7ff86ecb5a26d11d0ac13f03abf2cca01134c Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 23 Sep 2020 18:07:23 +0100 Subject: [PATCH 14/21] add modals and flash reporting Signed-off-by: Andrew Thornton --- options/locale/locale_en-US.ini | 4 ++ routers/admin/repos.go | 4 +- routers/user/setting/adopt.go | 4 +- templates/admin/repo/unadopted.tmpl | 56 ++++++++++++++++++++++--- templates/user/settings/repos.tmpl | 64 ++++++++++++++++++++++++----- web_src/less/_admin.less | 15 +++---- 6 files changed, 122 insertions(+), 25 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ab69da30983f..288df2be43c8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -688,8 +688,12 @@ unit_disabled = The site administrator has disabled this repository section. language_other = Other adopt_preexisting_label = Adopt Files adopt_preexisting = Adopt pre-existing files +adopt_preexisting_content = Create repository from %s +adopt_preexisting_success = Adopted files and created repository from %s delete_preexisting_label = Delete delete_preexisting = Delete pre-existing files +delete_preexisting_content = Delete files in %s +delete_preexisting_success = Deleted unadopted files in %s desc.private = Private desc.public = Public diff --git a/routers/admin/repos.go b/routers/admin/repos.go index f36fcddab99f..84639c945a9e 100644 --- a/routers/admin/repos.go +++ b/routers/admin/repos.go @@ -219,7 +219,7 @@ func UnadoptedRepos(ctx *context.Context) { // AdoptOrDeleteRepository adopts or deletes a repository func AdoptOrDeleteRepository(ctx *context.Context) { - dir := ctx.Query("name") + dir := ctx.Query("id") action := ctx.Query("action") dirSplit := strings.SplitN(dir, "/", 2) if len(dirSplit) != 2 { @@ -255,11 +255,13 @@ func AdoptOrDeleteRepository(ctx *context.Context) { ctx.ServerError("repository.AdoptRepository", err) return } + ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir)) } else if action == "delete" { if err := repository.DeleteUnadoptedRepository(ctx.User, ctxUser, dirSplit[1]); err != nil { ctx.ServerError("repository.AdoptRepository", err) return } + ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir)) } ctx.Redirect(setting.AppSubURL + "/admin/repos/unadopted") } diff --git a/routers/user/setting/adopt.go b/routers/user/setting/adopt.go index e4836df2614f..6ff07d6daa91 100644 --- a/routers/user/setting/adopt.go +++ b/routers/user/setting/adopt.go @@ -23,7 +23,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { allowDelete := ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories ctx.Data["allowDelete"] = allowDelete - dir := ctx.Query("name") + dir := ctx.Query("id") action := ctx.Query("action") ctxUser := ctx.User @@ -43,11 +43,13 @@ func AdoptOrDeleteRepository(ctx *context.Context) { ctx.ServerError("repository.AdoptRepository", err) return } + ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir)) } else if action == "delete" && allowDelete { if err := repository.DeleteUnadoptedRepository(ctxUser, ctxUser, dir); err != nil { ctx.ServerError("repository.AdoptRepository", err) return } + ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir)) } ctx.Redirect(setting.AppSubURL + "/user/settings/repos") diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl index 73d2793a2dda..d3e81ced6a6d 100644 --- a/templates/admin/repo/unadopted.tmpl +++ b/templates/admin/repo/unadopted.tmpl @@ -18,12 +18,56 @@ {{svg "octicon-file-directory"}} {{$dir}}
-
- {{$.CsrfTokenHtml}} - - - -
+ + + +
diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl index e6df86514053..456647d9bec5 100644 --- a/templates/user/settings/repos.tmpl +++ b/templates/user/settings/repos.tmpl @@ -36,16 +36,60 @@ {{svg "octicon-file-directory"}} {{$.Owner.Name}}/{{$dir}}
-
- {{$.CsrfTokenHtml}} - - {{if $.allowAdopt}} - - {{end}} - {{if $.allowDelete}} - - {{end}} -
+ {{if $.allowAdopt}} + + + {{end}} + {{if $.allowDelete}} + + + {{end}}
{{end}} diff --git a/web_src/less/_admin.less b/web_src/less/_admin.less index bd759f34d5fb..052c29dd6255 100644 --- a/web_src/less/_admin.less +++ b/web_src/less/_admin.less @@ -31,13 +31,14 @@ padding: 5px 8px; } - .settings .button.adopt, - .settings .button.delete { - margin-top: -15px; - margin-bottom: -15px; - .label { - vertical-align: middle; - } + } + + .settings .button.adopt, + .settings .button.delete { + margin-top: -15px; + margin-bottom: -15px; + .label { + vertical-align: middle; } } From 19119650e00bc02d9d242583b7a3ff82b24dc53b Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 23 Sep 2020 20:43:27 +0100 Subject: [PATCH 15/21] Make the unadopted page searchable Signed-off-by: Andrew Thornton --- options/locale/locale_en-US.ini | 1 + routers/admin/repos.go | 41 +++++++- templates/admin/repo/unadopted.tmpl | 143 +++++++++++++++------------- 3 files changed, 118 insertions(+), 67 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 288df2be43c8..37d8d7272a77 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -686,6 +686,7 @@ pick_reaction = Pick your reaction reactions_more = and %d more unit_disabled = The site administrator has disabled this repository section. language_other = Other +adopt_search = Enter username to search for unadopted repositories... (leave blank to find all) adopt_preexisting_label = Adopt Files adopt_preexisting = Adopt pre-existing files adopt_preexisting_content = Create repository from %s diff --git a/routers/admin/repos.go b/routers/admin/repos.go index 84639c945a9e..4da26036df10 100644 --- a/routers/admin/repos.go +++ b/routers/admin/repos.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers" repo_service "code.gitea.io/gitea/services/repository" + "github.com/gobwas/glob" "github.com/unknwon/com" ) @@ -73,6 +74,40 @@ func UnadoptedRepos(ctx *context.Context) { if opts.Page <= 0 { opts.Page = 1 } + + doSearch := ctx.QueryBool("search") + + ctx.Data["search"] = doSearch + q := ctx.Query("q") + + if !doSearch { + pager := context.NewPagination(0, opts.PageSize, opts.Page, 5) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager + ctx.HTML(200, tplUnadoptedRepos) + return + } + + ctx.Data["Keyword"] = q + qsplit := strings.SplitN(q, "/", 2) + + globUser, _ := glob.Compile("*") + globRepo, _ := glob.Compile("*") + + if len(qsplit) > 0 && len(q) > 0 { + var err error + globUser, err = glob.Compile(qsplit[0]) + if err != nil { + log.Info("Invalid glob expresion '%s' (skipped): %v", qsplit[0], err) + } + if len(qsplit) > 1 { + globRepo, err = glob.Compile(qsplit[1]) + if err != nil { + log.Info("Invalid glob expresion '%s' (skipped): %v", qsplit[1], err) + } + } + } + start := (opts.Page - 1) * opts.PageSize end := start + opts.PageSize @@ -125,6 +160,10 @@ func UnadoptedRepos(ctx *context.Context) { repoNamesToCheck = repoNamesToCheck[:0] } + if !globUser.Match(info.Name()) { + return filepath.SkipDir + } + ctxUser, err = models.GetUserByName(info.Name()) if err != nil { if models.IsErrUserNotExist(err) { @@ -142,7 +181,7 @@ func UnadoptedRepos(ctx *context.Context) { return filepath.SkipDir } name = name[:len(name)-4] - if models.IsUsableRepoName(name) != nil || strings.ToLower(name) != name { + if models.IsUsableRepoName(name) != nil || strings.ToLower(name) != name || !globRepo.Match(name) { return filepath.SkipDir } if count < end { diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl index d3e81ced6a6d..7a046c6026d8 100644 --- a/templates/admin/repo/unadopted.tmpl +++ b/templates/admin/repo/unadopted.tmpl @@ -9,78 +9,89 @@ {{.i18n.Tr "admin.repos.repo_manage_panel"}} -
- {{if .Dirs}} -
- {{range $dirI, $dir := .Dirs}} -
-
- {{svg "octicon-file-directory"}} - {{$dir}} -
- -
From b96ba62804e09108f010cc5262c558263c1d5236 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 23 Sep 2020 21:25:48 +0100 Subject: [PATCH 16/21] Add API Signed-off-by: Andrew Thornton --- modules/repository/adopt.go | 164 +++++++++++++++++++++++++++++++++ routers/admin/repos.go | 166 +--------------------------------- routers/api/v1/admin/adopt.go | 164 +++++++++++++++++++++++++++++++++ routers/api/v1/api.go | 5 + 4 files changed, 336 insertions(+), 163 deletions(-) create mode 100644 routers/api/v1/admin/adopt.go diff --git a/modules/repository/adopt.go b/modules/repository/adopt.go index d5d6028377d4..22cd6dd91f7f 100644 --- a/modules/repository/adopt.go +++ b/modules/repository/adopt.go @@ -6,6 +6,8 @@ package repository import ( "fmt" + "os" + "path/filepath" "strings" "code.gitea.io/gitea/models" @@ -13,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "github.com/gobwas/glob" "github.com/unknwon/com" ) @@ -106,3 +109,164 @@ func DeleteUnadoptedRepository(doer, u *models.User, repoName string) error { return util.RemoveAll(repoPath) } + +// ListUnadoptedRepositories lists all the unadopted repositories that match the provided query +func ListUnadoptedRepositories(query string, opts *models.ListOptions) ([]string, int, error) { + globUser, _ := glob.Compile("*") + globRepo, _ := glob.Compile("*") + + qsplit := strings.SplitN(query, "/", 2) + if len(qsplit) > 0 && len(query) > 0 { + var err error + globUser, err = glob.Compile(qsplit[0]) + if err != nil { + log.Info("Invalid glob expresion '%s' (skipped): %v", qsplit[0], err) + } + if len(qsplit) > 1 { + globRepo, err = glob.Compile(qsplit[1]) + if err != nil { + log.Info("Invalid glob expresion '%s' (skipped): %v", qsplit[1], err) + } + } + } + start := (opts.Page - 1) * opts.PageSize + end := start + opts.PageSize + + repoNamesToCheck := make([]string, 0, opts.PageSize) + + repoNames := make([]string, 0, opts.PageSize) + var ctxUser *models.User + + count := 0 + + // We're going to iterate by pagesize. + root := filepath.Join(setting.RepoRootPath) + if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() || path == root { + return nil + } + + if !strings.ContainsRune(path[len(root)+1:], filepath.Separator) { + // Got a new user + + // Clean up old repoNamesToCheck + if len(repoNamesToCheck) > 0 { + repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: models.ListOptions{ + Page: 1, + PageSize: opts.PageSize, + }, LowerNames: repoNamesToCheck}) + if err != nil { + return err + } + for _, name := range repoNamesToCheck { + found := false + repoLoopCatchup: + for i, repo := range repos { + if repo.LowerName == name { + found = true + repos = append(repos[:i], repos[i+1:]...) + break repoLoopCatchup + } + } + if !found { + if count >= start && count < end { + repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name)) + } + count++ + } + } + repoNamesToCheck = repoNamesToCheck[:0] + } + + if !globUser.Match(info.Name()) { + return filepath.SkipDir + } + + ctxUser, err = models.GetUserByName(info.Name()) + if err != nil { + if models.IsErrUserNotExist(err) { + log.Debug("Missing user: %s", info.Name()) + return filepath.SkipDir + } + return err + } + return nil + } + + name := info.Name() + + if !strings.HasSuffix(name, ".git") { + return filepath.SkipDir + } + name = name[:len(name)-4] + if models.IsUsableRepoName(name) != nil || strings.ToLower(name) != name || !globRepo.Match(name) { + return filepath.SkipDir + } + if count < end { + repoNamesToCheck = append(repoNamesToCheck, name) + if len(repoNamesToCheck) >= opts.PageSize { + repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: models.ListOptions{ + Page: 1, + PageSize: opts.PageSize, + }, LowerNames: repoNamesToCheck}) + if err != nil { + return err + } + for _, name := range repoNamesToCheck { + found := false + repoLoop: + for i, repo := range repos { + if repo.Name == name { + found = true + repos = append(repos[:i], repos[i+1:]...) + break repoLoop + } + } + if !found { + if count >= start && count < end { + repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name)) + } + count++ + } + } + repoNamesToCheck = repoNamesToCheck[:0] + } + return filepath.SkipDir + } + count++ + return filepath.SkipDir + }); err != nil { + return nil, 0, err + } + + if len(repoNamesToCheck) > 0 { + repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: models.ListOptions{ + Page: 1, + PageSize: opts.PageSize, + }, LowerNames: repoNamesToCheck}) + if err != nil { + return nil, 0, err + } + for _, name := range repoNamesToCheck { + found := false + repoLoop: + for i, repo := range repos { + if repo.LowerName == name { + found = true + repos = append(repos[:i], repos[i+1:]...) + break repoLoop + } + } + if !found { + if count >= start && count < end { + repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name)) + } + count++ + } + } + } + return repoNames, count, nil +} diff --git a/routers/admin/repos.go b/routers/admin/repos.go index 4da26036df10..445bd5a03407 100644 --- a/routers/admin/repos.go +++ b/routers/admin/repos.go @@ -5,9 +5,6 @@ package admin import ( - "fmt" - "os" - "path/filepath" "strings" "code.gitea.io/gitea/models" @@ -18,7 +15,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers" repo_service "code.gitea.io/gitea/services/repository" - "github.com/gobwas/glob" "github.com/unknwon/com" ) @@ -89,165 +85,9 @@ func UnadoptedRepos(ctx *context.Context) { } ctx.Data["Keyword"] = q - qsplit := strings.SplitN(q, "/", 2) - - globUser, _ := glob.Compile("*") - globRepo, _ := glob.Compile("*") - - if len(qsplit) > 0 && len(q) > 0 { - var err error - globUser, err = glob.Compile(qsplit[0]) - if err != nil { - log.Info("Invalid glob expresion '%s' (skipped): %v", qsplit[0], err) - } - if len(qsplit) > 1 { - globRepo, err = glob.Compile(qsplit[1]) - if err != nil { - log.Info("Invalid glob expresion '%s' (skipped): %v", qsplit[1], err) - } - } - } - - start := (opts.Page - 1) * opts.PageSize - end := start + opts.PageSize - - repoNamesToCheck := make([]string, 0, opts.PageSize) - - repoNames := make([]string, 0, opts.PageSize) - var ctxUser *models.User - - count := 0 - - // We're going to iterate by pagesize. - root := filepath.Join(setting.RepoRootPath) - if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() || path == root { - return nil - } - - if !strings.ContainsRune(path[len(root)+1:], filepath.Separator) { - // Got a new user - - // Clean up old repoNamesToCheck - if len(repoNamesToCheck) > 0 { - repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: models.ListOptions{ - Page: 1, - PageSize: opts.PageSize, - }, LowerNames: repoNamesToCheck}) - if err != nil { - return err - } - for _, name := range repoNamesToCheck { - found := false - repoLoopCatchup: - for i, repo := range repos { - if repo.LowerName == name { - found = true - repos = append(repos[:i], repos[i+1:]...) - break repoLoopCatchup - } - } - if !found { - if count >= start && count < end { - repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name)) - } - count++ - } - } - repoNamesToCheck = repoNamesToCheck[:0] - } - - if !globUser.Match(info.Name()) { - return filepath.SkipDir - } - - ctxUser, err = models.GetUserByName(info.Name()) - if err != nil { - if models.IsErrUserNotExist(err) { - log.Debug("Missing user: %s", info.Name()) - return filepath.SkipDir - } - return err - } - return nil - } - - name := info.Name() - - if !strings.HasSuffix(name, ".git") { - return filepath.SkipDir - } - name = name[:len(name)-4] - if models.IsUsableRepoName(name) != nil || strings.ToLower(name) != name || !globRepo.Match(name) { - return filepath.SkipDir - } - if count < end { - repoNamesToCheck = append(repoNamesToCheck, name) - if len(repoNamesToCheck) >= opts.PageSize { - repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: models.ListOptions{ - Page: 1, - PageSize: opts.PageSize, - }, LowerNames: repoNamesToCheck}) - if err != nil { - return err - } - for _, name := range repoNamesToCheck { - found := false - repoLoop: - for i, repo := range repos { - if repo.Name == name { - found = true - repos = append(repos[:i], repos[i+1:]...) - break repoLoop - } - } - if !found { - if count >= start && count < end { - repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name)) - } - count++ - } - } - repoNamesToCheck = repoNamesToCheck[:0] - } - return filepath.SkipDir - } - count++ - return filepath.SkipDir - }); err != nil { - ctx.ServerError("filepath.Walk", err) - return - } - - if len(repoNamesToCheck) > 0 { - repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: models.ListOptions{ - Page: 1, - PageSize: opts.PageSize, - }, LowerNames: repoNamesToCheck}) - if err != nil { - ctx.ServerError("filepath.Walk", err) - return - } - for _, name := range repoNamesToCheck { - found := false - repoLoop: - for i, repo := range repos { - if repo.LowerName == name { - found = true - repos = append(repos[:i], repos[i+1:]...) - break repoLoop - } - } - if !found { - if count >= start && count < end { - repoNames = append(repoNames, fmt.Sprintf("%s/%s", ctxUser.Name, name)) - } - count++ - } - } + repoNames, count, err := repository.ListUnadoptedRepositories(q, &opts) + if err != nil { + ctx.ServerError("ListUnadoptedRepositories", err) } ctx.Data["Dirs"] = repoNames pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go new file mode 100644 index 000000000000..38637a09c29c --- /dev/null +++ b/routers/api/v1/admin/adopt.go @@ -0,0 +1,164 @@ +// Copyright 2020 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 admin + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/routers/api/v1/utils" + "github.com/unknwon/com" +) + +// ListUnadoptedRepositories lists the unadopted repositories that match the provided names +func ListUnadoptedRepositories(ctx *context.APIContext) { + // swagger:operation GET /admin/unadopted admin adminUnadoptedList + // --- + // summary: List unadopted repositories + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // - name: pattern + // in: query + // description: pattern of repositories to search for + // type: string + // responses: + // "200": + // "$ref": "#/responses/StringSlice" + // "403": + // "$ref": "#/responses/forbidden" + + listOptions := utils.GetListOptions(ctx) + repoNames, count, err := repository.ListUnadoptedRepositories(ctx.Query("query"), &listOptions) + if err != nil { + ctx.InternalServerError(err) + } + + ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", count)) + ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count") + + ctx.JSON(http.StatusOK, repoNames) +} + +// AdoptRepository will adopt an unadopted repository +func AdoptRepository(ctx *context.APIContext) { + // swagger:operation POST /admin/unadopted/* admin adminAdoptRepository + // --- + // summary: Adopt unadopted files as a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/Empty" + // "404": + // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" + ownerName := ctx.Params(":username") + repoName := ctx.Params(":reponame") + + ctxUser, err := models.GetUserByName(ownerName) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.NotFound() + return + } + ctx.InternalServerError(err) + return + } + + // check not a repo + if has, err := models.IsRepositoryExist(ctxUser, repoName); err != nil { + ctx.InternalServerError(err) + return + } else if has || !com.IsDir(models.RepoPath(ctxUser.Name, repoName)) { + ctx.NotFound() + return + } + if _, err := repository.AdoptRepository(ctx.User, ctxUser, models.CreateRepoOptions{ + Name: repoName, + IsPrivate: true, + }); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// DeleteUnadoptedRepository will delete an unadopted repository +func DeleteUnadoptedRepository(ctx *context.APIContext) { + // swagger:operation DELETE /admin/unadopted/* admin adminDeleteUnadoptedRepository + // --- + // summary: Delete unadopted files + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/Empty" + // "403": + // "$ref": "#/responses/forbidden" + ownerName := ctx.Params(":username") + repoName := ctx.Params(":reponame") + + ctxUser, err := models.GetUserByName(ownerName) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.NotFound() + return + } + ctx.InternalServerError(err) + return + } + + // check not a repo + if has, err := models.IsRepositoryExist(ctxUser, repoName); err != nil { + ctx.InternalServerError(err) + return + } else if has || !com.IsDir(models.RepoPath(ctxUser.Name, repoName)) { + ctx.NotFound() + return + } + + if err := repository.DeleteUnadoptedRepository(ctx.User, ctxUser, repoName); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8b3a7545c63a..3b6f8dbba3d0 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -957,6 +957,11 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo) }) }) + m.Group("/unadopted", func() { + m.Get("", admin.ListUnadoptedRepositories) + m.Post("/:username/:reponame", admin.AdoptRepository) + m.Delete("/:username/:reponame", admin.DeleteUnadoptedRepository) + }) }, reqToken(), reqSiteAdmin()) m.Group("/topics", func() { From 971585be659848f3300c8fb85dd37fddef57cc0f Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 23 Sep 2020 21:31:00 +0100 Subject: [PATCH 17/21] Fix swagger Signed-off-by: Andrew Thornton --- routers/admin/repos.go | 1 - templates/swagger/v1_json.tmpl | 113 +++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/routers/admin/repos.go b/routers/admin/repos.go index 445bd5a03407..10abaf9547ad 100644 --- a/routers/admin/repos.go +++ b/routers/admin/repos.go @@ -124,7 +124,6 @@ func AdoptOrDeleteRepository(ctx *context.Context) { ctx.ServerError("IsRepositoryExist", err) return } else if has || !com.IsDir(models.RepoPath(ctxUser.Name, repoName)) { - log.Debug("has: %t, notDir: %s", has, models.RepoPath(ctxUser.Name, repoName)) // Fallthrough to failure mode } else if action == "adopt" { if _, err := repository.AdoptRepository(ctx.User, ctxUser, models.CreateRepoOptions{ diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 6792f7444b06..18f8ed505dc5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -120,6 +120,119 @@ } } }, + "/admin/unadopted": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List unadopted repositories", + "operationId": "adminUnadoptedList", + "parameters": [ + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "pattern of repositories to search for", + "name": "pattern", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/StringSlice" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, + "/admin/unadopted/*": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Adopt unadopted files as a repository", + "operationId": "adminAdoptRepository", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/Empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete unadopted files", + "operationId": "adminDeleteUnadoptedRepository", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/Empty" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, "/admin/users": { "get": { "produces": [ From 89e59ced4ef69b094d73048bed53f1d65ebd868f Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Thu, 24 Sep 2020 17:35:18 +0100 Subject: [PATCH 18/21] fix swagger Signed-off-by: Andrew Thornton --- routers/api/v1/admin/adopt.go | 8 ++++---- templates/swagger/v1_json.tmpl | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index 38637a09c29c..1a7a62a55cf0 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -55,7 +55,7 @@ func ListUnadoptedRepositories(ctx *context.APIContext) { // AdoptRepository will adopt an unadopted repository func AdoptRepository(ctx *context.APIContext) { - // swagger:operation POST /admin/unadopted/* admin adminAdoptRepository + // swagger:operation POST /admin/unadopted/{owner}/{repo} admin adminAdoptRepository // --- // summary: Adopt unadopted files as a repository // produces: @@ -73,7 +73,7 @@ func AdoptRepository(ctx *context.APIContext) { // required: true // responses: // "204": - // "$ref": "#/responses/Empty" + // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" // "403": @@ -112,7 +112,7 @@ func AdoptRepository(ctx *context.APIContext) { // DeleteUnadoptedRepository will delete an unadopted repository func DeleteUnadoptedRepository(ctx *context.APIContext) { - // swagger:operation DELETE /admin/unadopted/* admin adminDeleteUnadoptedRepository + // swagger:operation DELETE /admin/unadopted/{owner}/{repo} admin adminDeleteUnadoptedRepository // --- // summary: Delete unadopted files // produces: @@ -130,7 +130,7 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) { // required: true // responses: // "204": - // "$ref": "#/responses/Empty" + // "$ref": "#/responses/empty" // "403": // "$ref": "#/responses/forbidden" ownerName := ctx.Params(":username") diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 18f8ed505dc5..71137fc0bcf1 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -160,7 +160,7 @@ } } }, - "/admin/unadopted/*": { + "/admin/unadopted/{owner}/{repo}": { "post": { "produces": [ "application/json" @@ -188,7 +188,7 @@ ], "responses": { "204": { - "$ref": "#/responses/Empty" + "$ref": "#/responses/empty" }, "403": { "$ref": "#/responses/forbidden" @@ -225,7 +225,7 @@ ], "responses": { "204": { - "$ref": "#/responses/Empty" + "$ref": "#/responses/empty" }, "403": { "$ref": "#/responses/forbidden" From 8c7f8967ea48deb71644410222dc358c1d631e0a Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Thu, 24 Sep 2020 18:29:48 +0100 Subject: [PATCH 19/21] Handle empty and non-master branched repositories Signed-off-by: Andrew Thornton --- modules/git/repo_branch.go | 5 ++++ modules/repository/init.go | 49 ++++++++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 8f9c802e016d..cd30c191ea31 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -74,6 +74,11 @@ func (repo *Repository) SetDefaultBranch(name string) error { return err } +// GetDefaultBranch gets default branch of repository. +func (repo *Repository) GetDefaultBranch() (string, error) { + return NewCommand("symbolic-ref", "HEAD").RunInDir(repo.Path) +} + // GetBranches returns all branches of the repository. func (repo *Repository) GetBranches() ([]string, error) { var branchNames []string diff --git a/modules/repository/init.go b/modules/repository/init.go index 3d8364d69332..02b63104400f 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -207,15 +207,54 @@ func adoptRepository(ctx models.DBContext, repoPath string, u *models.User, repo } repo.IsEmpty = false - - repo.DefaultBranch = setting.Repository.DefaultBranch + gitRepo, err := git.OpenRepository(repo.RepoPath()) + if err != nil { + return fmt.Errorf("openRepository: %v", err) + } + defer gitRepo.Close() if len(opts.DefaultBranch) > 0 { repo.DefaultBranch = opts.DefaultBranch - gitRepo, err := git.OpenRepository(repo.RepoPath()) + + if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + return fmt.Errorf("setDefaultBranch: %v", err) + } + } else { + repo.DefaultBranch, err = gitRepo.GetDefaultBranch() if err != nil { - return fmt.Errorf("openRepository: %v", err) + repo.DefaultBranch = setting.Repository.DefaultBranch + if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + return fmt.Errorf("setDefaultBranch: %v", err) + } + } else if strings.HasPrefix(repo.DefaultBranch, git.BranchPrefix) { + repo.DefaultBranch = repo.DefaultBranch[len(git.BranchPrefix):] + } + } + branches, _ := gitRepo.GetBranches() + found := false + hasDefault := false + hasMaster := false + for _, branch := range branches { + if branch == repo.DefaultBranch { + found = true + break + } else if branch == setting.Repository.DefaultBranch { + hasDefault = true + } else if branch == "master" { + hasMaster = true + } + } + if !found { + if hasDefault { + repo.DefaultBranch = setting.Repository.DefaultBranch + } else if hasMaster { + repo.DefaultBranch = "master" + } else if len(branches) > 0 { + repo.DefaultBranch = branches[0] + } else { + repo.IsEmpty = true + repo.DefaultBranch = setting.Repository.DefaultBranch } - defer gitRepo.Close() + if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %v", err) } From 4a65d7752a7577c0f70a43c9e2a44bbe4744afc3 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Thu, 24 Sep 2020 18:37:07 +0100 Subject: [PATCH 20/21] placate lint Signed-off-by: Andrew Thornton --- modules/repository/init.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/repository/init.go b/modules/repository/init.go index 02b63104400f..707f8f5250bd 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -225,9 +225,9 @@ func adoptRepository(ctx models.DBContext, repoPath string, u *models.User, repo if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %v", err) } - } else if strings.HasPrefix(repo.DefaultBranch, git.BranchPrefix) { - repo.DefaultBranch = repo.DefaultBranch[len(git.BranchPrefix):] } + + repo.DefaultBranch = strings.TrimPrefix(repo.DefaultBranch, git.BranchPrefix) } branches, _ := gitRepo.GetBranches() found := false From 05030b12a6e16274ac6922ff55f621343132d770 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Thu, 24 Sep 2020 22:22:14 +0100 Subject: [PATCH 21/21] remove commented out code Signed-off-by: Andrew Thornton --- modules/repository/create.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/modules/repository/create.go b/modules/repository/create.go index b12c3790932c..e6a3e7081d69 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -64,25 +64,12 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (*mod // 3. We delete it and start afresh // // Previously Gitea would just delete and start afresh - this was naughty. - // if opts.AdoptPreExisting { - // shouldInit = false - // if err := adoptRepository(ctx, repoPath, doer, repo, opts); err != nil { - // return fmt.Errorf("createDelegateHooks: %v", err) - // } - // } else if opts.OverwritePreExisting { - // log.Warn("An already existing repository was deleted at %s", repoPath) - // if err := util.RemoveAll(repoPath); err != nil { - // log.Error("Unable to remove already existing repository at %s: Error: %v", repoPath, err) - // return fmt.Errorf( - // "unable to delete repo directory %s/%s: %v", u.Name, repo.Name, err) - // } - // } else { + // So we will now fail and delegate to other functionality to adopt or delete log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) return models.ErrRepoFilesAlreadyExist{ Uname: u.Name, Name: repo.Name, } - // } } if err := initRepository(ctx, repoPath, doer, repo, opts); err != nil {