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 a15b9be54fc04..731c5586edd33 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 e9343cbe7c680..2529436248d6c 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 b2b6e1a26f841..526803f0c6b14 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 480683cd4a5be..7d1a4062b01e1 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 6c3421e4f7d85..8a1e0d68dd60b 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 2f7d10f0d1118..9e69223b07862 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 6d80488de786c..c9498ab2ca9d4 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 5bdfa7490d81e..5ec6213cd460a 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 eb1501d7b86eb..efcecb1ac82fe 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 2ff1a1ec2672c..7c9e4053d26a6 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 751cce65835fb..8e7be76140440 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 f812ac6788e76..8fe0ebf6e6eb3 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 27c8ff1e03ebb..8e005d6eac453 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 02331c232b081..fbd1c8094db9c 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 f50b98b64099a..908acebcd2e7d 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 c4b25c73d8315..9d73b75fff1d4 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 60b432beaa268..74357e0b5e281 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 74357e0b5e281..6881ca8ce94ea 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 bd9267ddce69b..c64339e7114cf 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 08ddb3bcd50ae..1b255738c3d00 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 160c648ed6f7d..17c044f20984f 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 ba4bd7a3d8481..8d7e647ae1abb 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 7d1a4062b01e1..480683cd4a5be 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 dea88d8816c0e..355b801a7ef05 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 c7b3f0981e0e3..650d5a803aa9a 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 71a787fbbad02..3ad57085b0533 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 b6d3cb6e7b856..dbc40b138aadc 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 9184e6b938763..d4ba66fd389ed 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 0000000000000..d5d6028377d42 --- /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 850dad7dc8de4..f77daeb5bf051 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 f3b19e9102d97..f78848c8950be 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 37700cd68aa8d..526bd6ec08638 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 c1027d011dbc8..c57702b2821b6 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 831eef25953a7..72f111ecc7c52 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 4ec1605b5b03c..4fcfb5212cb16 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 39a1d7596c3f1..2cb7021333be8 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 9489ca34e2920..f9cddbb7cdce8 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 41d80b11d0f02..603187c16dbb4 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 f90e150ba7c31..92e1f46e4b0d5 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 9a87b8dfc4bbd..65358fc344570 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 8fb2824862d71..da83bffdbeaa9 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 5345a1017198e..f60af5dad0400 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 0000000000000..ffa42d817e429 --- /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 ba9ba2b257fa9..fe0506946ad68 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 c5ee255483ffe..c6768f3f00f12 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 4c3b77dcfb4d7..51e329e03835e 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 0000000000000..73d2793a2ddad --- /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 b0c4077e56301..c4b25c73d8315 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 a48b55ec0272e..34a1c7bd0d1e3 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 29afe96b067c6..bd759f34d5fbe 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 40120c89a17b5..f8123b8f9bff1 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 0b983a382c194..fcc5c0290fc32 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 2cb7021333be8..4b4634b856831 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 ffa42d817e429..e4836df2614f1 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 730e9fef95938..6792f7444b062 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 4b4634b856831..f36fcddab99fc 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 ab69da30983f3..288df2be43c84 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 f36fcddab99fc..84639c945a9eb 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 e4836df2614f1..6ff07d6daa916 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 73d2793a2ddad..d3e81ced6a6d7 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 e6df865140532..456647d9bec53 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 bd759f34d5fbe..052c29dd62558 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 288df2be43c84..37d8d7272a77d 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 84639c945a9eb..4da26036df10d 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 d3e81ced6a6d7..7a046c6026d86 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 d5d6028377d42..22cd6dd91f7f1 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 4da26036df10d..445bd5a034074 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 0000000000000..38637a09c29c9 --- /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 8b3a7545c63ae..3b6f8dbba3d0c 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 445bd5a034074..10abaf9547ad1 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 6792f7444b062..18f8ed505dc59 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 38637a09c29c9..1a7a62a55cf06 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 18f8ed505dc59..71137fc0bcf13 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 8f9c802e016dc..cd30c191ea31e 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 3d8364d693324..02b63104400f6 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 02b63104400f6..707f8f5250bdc 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 b12c3790932cf..e6a3e7081d692 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 {