From e422342eebc18034ef586ec58f1e2fff0340091d Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 19 Apr 2023 21:40:42 +0800 Subject: [PATCH] Allow adding new files to an empty repo (#24164) ![image](https://user-images.githubusercontent.com/2114189/232561612-2bfcfd0a-fc04-47ba-965f-5d0bcea46c54.png) --- models/db/iterate_test.go | 11 ++-- models/db/list_test.go | 11 +++- models/dbfs/dbfs_test.go | 2 - models/fixtures/repo_unit.yml | 6 ++ models/fixtures/repository.yml | 1 + models/fixtures/user.yml | 2 +- models/repo/repo.go | 8 ++- models/unittest/fixtures.go | 20 +++---- models/user/user_test.go | 8 ++- modules/context/context.go | 2 +- modules/context/repo.go | 37 +++++++----- modules/git/command.go | 16 +++-- modules/git/repo.go | 2 +- modules/git/repo_base_nogogit.go | 2 +- modules/indexer/code/indexer.go | 4 +- modules/indexer/stats/db.go | 7 ++- modules/indexer/stats/queue.go | 5 +- modules/setting/setting.go | 15 ++++- routers/init.go | 2 +- routers/web/repo/editor.go | 44 ++++++++------ routers/web/repo/view.go | 42 ++++++------- routers/web/web.go | 2 +- services/auth/source/db/authenticate.go | 2 +- services/repository/files/upload.go | 19 ++++-- templates/repo/editor/commit_form.tmpl | 2 + templates/repo/empty.tmpl | 17 +++++- templates/repo/home.tmpl | 28 ++++----- tests/integration/empty_repo_test.go | 80 ++++++++++++++++++++++++- tests/integration/integration_test.go | 13 +++- tests/test_utils.go | 37 ++++++++---- web_src/css/repository.css | 5 +- 31 files changed, 314 insertions(+), 138 deletions(-) diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go index f9f1213721..5362f34075 100644 --- a/models/db/iterate_test.go +++ b/models/db/iterate_test.go @@ -19,13 +19,16 @@ func TestIterate(t *testing.T) { xe := unittest.GetXORMEngine() assert.NoError(t, xe.Sync(&repo_model.RepoUnit{})) - var repoCnt int - err := db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repo *repo_model.RepoUnit) error { - repoCnt++ + cnt, err := db.GetEngine(db.DefaultContext).Count(&repo_model.RepoUnit{}) + assert.NoError(t, err) + + var repoUnitCnt int + err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repo *repo_model.RepoUnit) error { + repoUnitCnt++ return nil }) assert.NoError(t, err) - assert.EqualValues(t, 89, repoCnt) + assert.EqualValues(t, cnt, repoUnitCnt) err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error { reopUnit2 := repo_model.RepoUnit{ID: repoUnit.ID} diff --git a/models/db/list_test.go b/models/db/list_test.go index 195450b1e7..6b9bebd64b 100644 --- a/models/db/list_test.go +++ b/models/db/list_test.go @@ -31,15 +31,20 @@ func TestFind(t *testing.T) { xe := unittest.GetXORMEngine() assert.NoError(t, xe.Sync(&repo_model.RepoUnit{})) + var repoUnitCount int + _, err := db.GetEngine(db.DefaultContext).SQL("SELECT COUNT(*) FROM repo_unit").Get(&repoUnitCount) + assert.NoError(t, err) + assert.NotEmpty(t, repoUnitCount) + opts := mockListOptions{} var repoUnits []repo_model.RepoUnit - err := db.Find(db.DefaultContext, &opts, &repoUnits) + err = db.Find(db.DefaultContext, &opts, &repoUnits) assert.NoError(t, err) - assert.EqualValues(t, 89, len(repoUnits)) + assert.EqualValues(t, repoUnitCount, len(repoUnits)) cnt, err := db.Count(db.DefaultContext, &opts, new(repo_model.RepoUnit)) assert.NoError(t, err) - assert.EqualValues(t, 89, cnt) + assert.EqualValues(t, repoUnitCount, cnt) repoUnits = make([]repo_model.RepoUnit, 0, 10) newCnt, err := db.FindAndCount(db.DefaultContext, &opts, &repoUnits) diff --git a/models/dbfs/dbfs_test.go b/models/dbfs/dbfs_test.go index 30aa6463c5..300758c623 100644 --- a/models/dbfs/dbfs_test.go +++ b/models/dbfs/dbfs_test.go @@ -12,8 +12,6 @@ import ( "code.gitea.io/gitea/models/db" "github.com/stretchr/testify/assert" - - _ "github.com/mattn/go-sqlite3" ) func changeDefaultFileBlockSize(n int64) (restore func()) { diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index 184be2d861..5bb974a7d7 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -601,3 +601,9 @@ repo_id: 57 type: 5 created_unix: 946684810 + +- + id: 90 + repo_id: 52 + type: 1 + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 496d1a3e7a..ef7730780f 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1560,6 +1560,7 @@ owner_name: user30 lower_name: empty name: empty + default_branch: master num_watches: 0 num_stars: 0 num_forks: 0 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index fce4a4bda0..eba33a7c36 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -1091,7 +1091,7 @@ max_repo_creation: -1 is_active: true is_admin: false - is_restricted: true + is_restricted: false allow_git_hook: false allow_import_local: false allow_create_organization: true diff --git a/models/repo/repo.go b/models/repo/repo.go index 3653dae015..266cbc288c 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -225,6 +225,12 @@ func (repo *Repository) IsBroken() bool { return repo.Status == RepositoryBroken } +// MarkAsBrokenEmpty marks the repo as broken and empty +func (repo *Repository) MarkAsBrokenEmpty() { + repo.Status = RepositoryBroken + repo.IsEmpty = true +} + // AfterLoad is invoked from XORM after setting the values of all fields of this object. func (repo *Repository) AfterLoad() { repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues @@ -729,7 +735,7 @@ func IsRepositoryExist(ctx context.Context, u *user_model.User, repoName string) return false, err } isDir, err := util.IsDir(RepoPath(u.Name, repoName)) - return has && isDir, err + return has || isDir, err } // GetTemplateRepo populates repo.TemplateRepo for a generated repository and diff --git a/models/unittest/fixtures.go b/models/unittest/fixtures.go index 545452a159..f7ee766731 100644 --- a/models/unittest/fixtures.go +++ b/models/unittest/fixtures.go @@ -17,7 +17,7 @@ import ( "xorm.io/xorm/schemas" ) -var fixtures *testfixtures.Loader +var fixturesLoader *testfixtures.Loader // GetXORMEngine gets the XORM engine func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) { @@ -30,11 +30,11 @@ func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) { // InitFixtures initialize test fixtures for a test database func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { e := GetXORMEngine(engine...) - var testfiles func(*testfixtures.Loader) error + var fixtureOptionFiles func(*testfixtures.Loader) error if opts.Dir != "" { - testfiles = testfixtures.Directory(opts.Dir) + fixtureOptionFiles = testfixtures.Directory(opts.Dir) } else { - testfiles = testfixtures.Files(opts.Files...) + fixtureOptionFiles = testfixtures.Files(opts.Files...) } dialect := "unknown" switch e.Dialect().URI().DBType { @@ -54,14 +54,14 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { testfixtures.Database(e.DB().DB), testfixtures.Dialect(dialect), testfixtures.DangerousSkipTestDatabaseCheck(), - testfiles, + fixtureOptionFiles, } if e.Dialect().URI().DBType == schemas.POSTGRES { loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences()) } - fixtures, err = testfixtures.New(loaderOptions...) + fixturesLoader, err = testfixtures.New(loaderOptions...) if err != nil { return err } @@ -78,11 +78,9 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { func LoadFixtures(engine ...*xorm.Engine) error { e := GetXORMEngine(engine...) var err error - // Database transaction conflicts could occur and result in ROLLBACK - // As a simple workaround, we just retry 20 times. - for i := 0; i < 20; i++ { - err = fixtures.Load() - if err == nil { + // (doubt) database transaction conflicts could occur and result in ROLLBACK? just try for a few times. + for i := 0; i < 5; i++ { + if err = fixturesLoader.Load(); err == nil { break } time.Sleep(200 * time.Millisecond) diff --git a/models/user/user_test.go b/models/user/user_test.go index 8e78fee6b3..c2314d5c03 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -5,6 +5,7 @@ package user_test import ( "context" + "fmt" "math/rand" "strings" "testing" @@ -64,9 +65,10 @@ func TestSearchUsers(t *testing.T) { testSuccess := func(opts *user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) { users, _, err := user_model.SearchUsers(opts) assert.NoError(t, err) - if assert.Len(t, users, len(expectedUserOrOrgIDs), opts) { + cassText := fmt.Sprintf("ids: %v, opts: %v", expectedUserOrOrgIDs, opts) + if assert.Len(t, users, len(expectedUserOrOrgIDs), "case: %s", cassText) { for i, expectedID := range expectedUserOrOrgIDs { - assert.EqualValues(t, expectedID, users[i].ID) + assert.EqualValues(t, expectedID, users[i].ID, "case: %s", cassText) } } } @@ -118,7 +120,7 @@ func TestSearchUsers(t *testing.T) { []int64{1}) testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: util.OptionalBoolTrue}, - []int64{29, 30}) + []int64{29}) testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: util.OptionalBoolTrue}, []int64{30}) diff --git a/modules/context/context.go b/modules/context/context.go index 2507cc10c0..21bae91129 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -301,7 +301,7 @@ func (ctx *Context) serverErrorInternal(logMsg string, logErr error) { // it's safe to show internal error to admin users, and it helps if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) { - ctx.Data["ErrorMsg"] = logErr + ctx.Data["ErrorMsg"] = fmt.Sprintf("%s, %s", logMsg, logErr) } } diff --git a/modules/context/repo.go b/modules/context/repo.go index 8b4d0c1bf4..1736de2e4b 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -184,6 +184,9 @@ func (r *Repository) CanCreateIssueDependencies(user *user_model.User, isPull bo // GetCommitsCount returns cached commit count for current view func (r *Repository) GetCommitsCount() (int64, error) { + if r.Commit == nil { + return 0, nil + } var contextName string if r.IsViewBranch { contextName = r.BranchName @@ -642,8 +645,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { if err != nil { if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") { log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err) - ctx.Repo.Repository.Status = repo_model.RepositoryBroken - ctx.Repo.Repository.IsEmpty = true + ctx.Repo.Repository.MarkAsBrokenEmpty() ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch // Only allow access to base of repo or settings if !isHomeOrSettings { @@ -689,7 +691,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { ctx.Data["BranchesCount"] = len(brs) // If not branch selected, try default one. - // If default branch doesn't exists, fall back to some other branch. + // If default branch doesn't exist, fall back to some other branch. if len(ctx.Repo.BranchName) == 0 { if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch @@ -878,6 +880,10 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return func(ctx *Context) (cancel context.CancelFunc) { // Empty repository does not have reference information. if ctx.Repo.Repository.IsEmpty { + // assume the user is viewing the (non-existent) default branch + ctx.Repo.IsViewBranch = true + ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch + ctx.Data["TreePath"] = "" return } @@ -907,27 +913,30 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context refName = ctx.Repo.Repository.DefaultBranch if !ctx.Repo.GitRepo.IsBranchExist(refName) { brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) - if err != nil { - ctx.ServerError("GetBranches", err) - return + if err == nil && len(brs) != 0 { + refName = brs[0] } else if len(brs) == 0 { - err = fmt.Errorf("No branches in non-empty repository %s", - ctx.Repo.GitRepo.Path) - ctx.ServerError("GetBranches", err) - return + log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path) + ctx.Repo.Repository.MarkAsBrokenEmpty() + } else { + log.Error("GetBranches error: %v", err) + ctx.Repo.Repository.MarkAsBrokenEmpty() } - refName = brs[0] } ctx.Repo.RefName = refName ctx.Repo.BranchName = refName ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) - if err != nil { + if err == nil { + ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() + } else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") { + // if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users + log.Error("GetBranchCommit: %v", err) + ctx.Repo.Repository.MarkAsBrokenEmpty() + } else { ctx.ServerError("GetBranchCommit", err) return } - ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() ctx.Repo.IsViewBranch = true - } else { refName = getRefName(ctx, refType) ctx.Repo.RefName = refName diff --git a/modules/git/command.go b/modules/git/command.go index a42d859f55..ac013d4ea1 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -211,10 +211,18 @@ type RunOpts struct { Env []string Timeout time.Duration UseContextTimeout bool - Dir string - Stdout, Stderr io.Writer - Stdin io.Reader - PipelineFunc func(context.Context, context.CancelFunc) error + + // Dir is the working dir for the git command, however: + // FIXME: this could be incorrect in many cases, for example: + // * /some/path/.git + // * /some/path/.git/gitea-data/data/repositories/user/repo.git + // If "user/repo.git" is invalid/broken, then running git command in it will use "/some/path/.git", and produce unexpected results + // The correct approach is to use `--git-dir" global argument + Dir string + + Stdout, Stderr io.Writer + Stdin io.Reader + PipelineFunc func(context.Context, context.CancelFunc) error } func commonBaseEnvs() []string { diff --git a/modules/git/repo.go b/modules/git/repo.go index d29ec40ae2..3637aa47c4 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -80,7 +80,7 @@ func InitRepository(ctx context.Context, repoPath string, bare bool) error { // IsEmpty Check if repository is empty. func (repo *Repository) IsEmpty() (bool, error) { var errbuf, output strings.Builder - if err := NewCommand(repo.Ctx, "show-ref", "--head", "^HEAD$"). + if err := NewCommand(repo.Ctx).AddOptionFormat("--git-dir=%s", repo.Path).AddArguments("show-ref", "--head", "^HEAD$"). Run(&RunOpts{ Dir: repo.Path, Stdout: &output, diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index a0216d14a6..e0f2d563b3 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -61,7 +61,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { } repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repoPath) - repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repo.Path) + repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repoPath) return repo, nil } diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index 027d13555c..2c493ccf94 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -154,7 +154,9 @@ func Init() { log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) if err := index(ctx, indexer, indexerData.RepoID); err != nil { - log.Error("index: %v", err) + if !setting.IsInTesting { + log.Error("indexer index error for repo %v: %v", indexerData.RepoID, err) + } if indexer.Ping() { continue } diff --git a/modules/indexer/stats/db.go b/modules/indexer/stats/db.go index 9bbdcad60d..2a0475dea6 100644 --- a/modules/indexer/stats/db.go +++ b/modules/indexer/stats/db.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/setting" ) // DBIndexer implements Indexer interface to use database's like search @@ -46,7 +47,7 @@ func (db *DBIndexer) Index(id int64) error { // Get latest commit for default branch commitID, err := gitRepo.GetBranchCommitID(repo.DefaultBranch) if err != nil { - if git.IsErrBranchNotExist(err) || git.IsErrNotExist(err) { + if git.IsErrBranchNotExist(err) || git.IsErrNotExist(err) || setting.IsInTesting { log.Debug("Unable to get commit ID for default branch %s in %s ... skipping this repository", repo.DefaultBranch, repo.RepoPath()) return nil } @@ -62,7 +63,9 @@ func (db *DBIndexer) Index(id int64) error { // Calculate and save language statistics to database stats, err := gitRepo.GetLanguageStats(commitID) if err != nil { - log.Error("Unable to get language stats for ID %s for default branch %s in %s. Error: %v", commitID, repo.DefaultBranch, repo.RepoPath(), err) + if !setting.IsInTesting { + log.Error("Unable to get language stats for ID %s for default branch %s in %s. Error: %v", commitID, repo.DefaultBranch, repo.RepoPath(), err) + } return err } err = repo_model.UpdateLanguageStats(repo, commitID, stats) diff --git a/modules/indexer/stats/queue.go b/modules/indexer/stats/queue.go index 32379f2859..a57338e07d 100644 --- a/modules/indexer/stats/queue.go +++ b/modules/indexer/stats/queue.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/setting" ) // statsQueue represents a queue to handle repository stats updates @@ -20,7 +21,9 @@ func handle(data ...queue.Data) []queue.Data { for _, datum := range data { opts := datum.(int64) if err := indexer.Index(opts); err != nil { - log.Error("stats queue indexer.Index(%d) failed: %v", opts, err) + if !setting.IsInTesting { + log.Error("stats queue indexer.Index(%d) failed: %v", opts, err) + } } } return nil diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 4d7a7caab8..e1a57615a8 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -27,7 +27,7 @@ import ( var ( // AppVer is the version of the current build of Gitea. It is set in main.go from main.Version. AppVer string - // AppBuiltWith represents a human readable version go runtime build version and build tags. (See main.go formatBuiltWith().) + // AppBuiltWith represents a human-readable version go runtime build version and build tags. (See main.go formatBuiltWith().) AppBuiltWith string // AppStartTime store time gitea has started AppStartTime time.Time @@ -40,7 +40,8 @@ var ( // AppWorkPath is used as the base path for several other paths. AppWorkPath string - // Global setting objects + // Other global setting objects + CfgProvider ConfigProvider CustomPath string // Custom directory path CustomConf string @@ -48,6 +49,10 @@ var ( RunUser string IsProd bool IsWindows bool + + // IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing + // TODO: this is only a temporary solution, we should make the test code more reliable + IsInTesting = false ) func getAppPath() (string, error) { @@ -108,8 +113,12 @@ func getWorkPath(appPath string) string { func init() { IsWindows = runtime.GOOS == "windows" + if AppVer == "" { + AppVer = "dev" + } + // We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically - // By default set this logger at Info - we'll change it later but we need to start with something. + // By default set this logger at Info - we'll change it later, but we need to start with something. log.NewLogger(0, "console", "console", fmt.Sprintf(`{"level": "info", "colorize": %t, "stacktraceLevel": "none"}`, log.CanColorStdout)) var err error diff --git a/routers/init.go b/routers/init.go index c539975aca..af768abbf4 100644 --- a/routers/init.go +++ b/routers/init.go @@ -108,6 +108,7 @@ func GlobalInitInstalled(ctx context.Context) { } mustInitCtx(ctx, git.InitFull) + log.Info("Gitea Version: %s%s", setting.AppVer, setting.AppBuiltWith) log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir()) log.Info("AppPath: %s", setting.AppPath) log.Info("AppWorkPath: %s", setting.AppWorkPath) @@ -115,7 +116,6 @@ func GlobalInitInstalled(ctx context.Context) { log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) log.Info("Run Mode: %s", util.ToTitleCase(setting.RunMode)) - log.Info("Gitea v%s%s", setting.AppVer, setting.AppBuiltWith) // Setup i18n translation.InitLocales(ctx) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 476c1d5ddd..63387df281 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -82,7 +82,7 @@ func editFile(ctx *context.Context, isNewFile bool) { } // Check if the filename (and additional path) is specified in the querystring - // (filename is a misnomer, but kept for compatibility with Github) + // (filename is a misnomer, but kept for compatibility with GitHub) filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename")) filePath = strings.Trim(filePath, "/") treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath)) @@ -327,6 +327,10 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b } } + if ctx.Repo.Repository.IsEmpty { + _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty") + } + if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) } else { @@ -617,25 +621,25 @@ func UploadFilePost(ctx *context.Context) { return } - var newTreePath string - for _, part := range treeNames { - newTreePath = path.Join(newTreePath, part) - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath) - if err != nil { - if git.IsErrNotExist(err) { - // Means there is no item with that name, so we're good - break + if !ctx.Repo.Repository.IsEmpty { + var newTreePath string + for _, part := range treeNames { + newTreePath = path.Join(newTreePath, part) + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath) + if err != nil { + if git.IsErrNotExist(err) { + break // Means there is no item with that name, so we're good + } + ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err) + return } - ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err) - return - } - - // User can only upload files to a directory. - if !entry.IsDir() { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form) - return + // User can only upload files to a directory, the directory name shouldn't be an existing file. + if !entry.IsDir() { + ctx.Data["Err_TreePath"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form) + return + } } } @@ -714,6 +718,10 @@ func UploadFilePost(ctx *context.Context) { return } + if ctx.Repo.Repository.IsEmpty { + _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty") + } + if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) } else { diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 150050f76b..5a11073ba9 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -154,16 +154,6 @@ func renderDirectory(ctx *context.Context, treeLink string) { ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) } - // Check permission to add or upload new file. - if ctx.Repo.CanWrite(unit_model.TypeCode) && ctx.Repo.IsViewBranch { - ctx.Data["CanAddFile"] = !ctx.Repo.Repository.IsArchived - ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled && !ctx.Repo.Repository.IsArchived - } - - if ctx.Written() { - return - } - subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true) if err != nil { ctx.ServerError("findReadmeFileInEntries", err) @@ -868,21 +858,25 @@ func renderRepoTopics(ctx *context.Context) { func renderCode(ctx *context.Context) { ctx.Data["PageIsViewCode"] = true + ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled - if ctx.Repo.Repository.IsEmpty { - reallyEmpty := true + if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { + showEmpty := true var err error if ctx.Repo.GitRepo != nil { - reallyEmpty, err = ctx.Repo.GitRepo.IsEmpty() + showEmpty, err = ctx.Repo.GitRepo.IsEmpty() if err != nil { - ctx.ServerError("GitRepo.IsEmpty", err) - return + log.Error("GitRepo.IsEmpty: %v", err) + ctx.Repo.Repository.Status = repo_model.RepositoryBroken + showEmpty = true + ctx.Flash.Error(ctx.Tr("error.occurred"), true) } } - if reallyEmpty { + if showEmpty { ctx.HTML(http.StatusOK, tplRepoEMPTY) return } + // the repo is not really empty, so we should update the modal in database // such problem may be caused by: // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually @@ -898,6 +892,14 @@ func renderCode(ctx *context.Context) { ctx.ServerError("UpdateRepoSize", err) return } + + // the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values + link := ctx.Link + if ctx.Req.URL.RawQuery != "" { + link += "?" + ctx.Req.URL.RawQuery + } + ctx.Redirect(link) + return } title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name @@ -927,11 +929,9 @@ func renderCode(ctx *context.Context) { return } - if !ctx.Repo.Repository.IsEmpty { - checkCitationFile(ctx, entry) - if ctx.Written() { - return - } + checkCitationFile(ctx, entry) + if ctx.Written() { + return } renderLanguageStats(ctx) diff --git a/routers/web/web.go b/routers/web/web.go index a4a1b7113c..30a8314691 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1184,7 +1184,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/upload-file", repo.UploadFileToServer) m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) }, repo.MustBeEditable, repo.MustBeAbleToUpload) - }, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived(), repo.MustBeNotEmpty) + }, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived()) m.Group("/branches", func() { m.Group("/_new", func() { diff --git a/services/auth/source/db/authenticate.go b/services/auth/source/db/authenticate.go index 76445e0d6d..773ec601ba 100644 --- a/services/auth/source/db/authenticate.go +++ b/services/auth/source/db/authenticate.go @@ -32,7 +32,7 @@ func Authenticate(user *user_model.User, login, password string) (*user_model.Us } // WARN: DON'T check user.IsActive, that will be checked on reqSign so that - // user could be hint to resend confirm email. + // user could be hinted to resend confirm email. if user.ProhibitLogin { return nil, user_model.ErrUserProhibitLogin{ UID: user.ID, diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index cf2f7019b6..338811f0f1 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -86,11 +86,22 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return err } defer t.Close() - if err := t.Clone(opts.OldBranch); err != nil { - return err + + hasOldBranch := true + if err = t.Clone(opts.OldBranch); err != nil { + if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { + return err + } + if err = t.Init(); err != nil { + return err + } + hasOldBranch = false + opts.LastCommitID = "" } - if err := t.SetDefaultIndex(); err != nil { - return err + if hasOldBranch { + if err = t.SetDefaultIndex(); err != nil { + return err + } } var filename2attribute2info map[string]map[string]string diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl index 7ac0ed3df1..db798d92e8 100644 --- a/templates/repo/editor/commit_form.tmpl +++ b/templates/repo/editor/commit_form.tmpl @@ -39,6 +39,7 @@ + {{if not .Repository.IsEmpty}}
{{$pullRequestEnabled := .Repository.UnitEnabled $.Context $.UnitTypePullRequests}} {{$prUnit := .Repository.MustGetUnit $.Context $.UnitTypePullRequests}} @@ -65,6 +66,7 @@
+ {{end}} {{end}} + {{if and (eq $n 0) (.Repository.IsTemplate)}} {{.locale.Tr "repo.use_template"}} diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go index 1a83de1292..80697c7329 100644 --- a/tests/integration/empty_repo_test.go +++ b/tests/integration/empty_repo_test.go @@ -4,12 +4,19 @@ package integration import ( + "bytes" + "io" + "mime/multipart" "net/http" "testing" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -17,7 +24,7 @@ import ( func TestEmptyRepo(t *testing.T) { defer tests.PrepareTestEnv(t)() - subpaths := []string{ + subPaths := []string{ "commits/master", "raw/foo", "commit/1ae57b34ccf7e18373", @@ -26,8 +33,75 @@ func TestEmptyRepo(t *testing.T) { emptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5}) assert.True(t, emptyRepo.IsEmpty) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: emptyRepo.OwnerID}) - for _, subpath := range subpaths { - req := NewRequestf(t, "GET", "/%s/%s/%s", owner.Name, emptyRepo.Name, subpath) + for _, subPath := range subPaths { + req := NewRequestf(t, "GET", "/%s/%s/%s", owner.Name, emptyRepo.Name, subPath) MakeRequest(t, req, http.StatusNotFound) } } + +func TestEmptyRepoAddFile(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + err := user_model.UpdateUserCols(db.DefaultContext, &user_model.User{ID: 30, ProhibitLogin: false}, "prohibit_login") + assert.NoError(t, err) + + session := loginUser(t, "user30") + req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`) + assert.Equal(t, "", doc.AttrOr("checked", "_no_")) + req = NewRequestWithValues(t, "POST", "/user30/empty/_new/"+setting.Repository.DefaultBranch, map[string]string{ + "_csrf": GetCSRF(t, session, "/user/settings"), + "commit_choice": "direct", + "tree_path": "test-file.md", + "content": "newly-added-test-file", + }) + + resp = session.MakeRequest(t, req, http.StatusSeeOther) + redirect := test.RedirectURL(resp) + assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/test-file.md", redirect) + + req = NewRequest(t, "GET", redirect) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "newly-added-test-file") +} + +func TestEmptyRepoUploadFile(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + err := user_model.UpdateUserCols(db.DefaultContext, &user_model.User{ID: 30, ProhibitLogin: false}, "prohibit_login") + assert.NoError(t, err) + + session := loginUser(t, "user30") + req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`) + assert.Equal(t, "", doc.AttrOr("checked", "_no_")) + + body := &bytes.Buffer{} + mpForm := multipart.NewWriter(body) + _ = mpForm.WriteField("_csrf", GetCSRF(t, session, "/user/settings")) + file, _ := mpForm.CreateFormFile("file", "uploaded-file.txt") + _, _ = io.Copy(file, bytes.NewBufferString("newly-uploaded-test-file")) + _ = mpForm.Close() + + req = NewRequestWithBody(t, "POST", "/user30/empty/upload-file", body) + req.Header.Add("Content-Type", mpForm.FormDataContentType()) + resp = session.MakeRequest(t, req, http.StatusOK) + respMap := map[string]string{} + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &respMap)) + + req = NewRequestWithValues(t, "POST", "/user30/empty/_upload/"+setting.Repository.DefaultBranch, map[string]string{ + "_csrf": GetCSRF(t, session, "/user/settings"), + "commit_choice": "direct", + "files": respMap["uuid"], + "tree_path": "", + }) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + redirect := test.RedirectURL(resp) + assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/", redirect) + + req = NewRequest(t, "GET", redirect) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "uploaded-file.txt") +} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 965bae576c..33a815b154 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -124,6 +124,9 @@ func TestMain(m *testing.M) { fmt.Printf("Error initializing test database: %v\n", err) os.Exit(1) } + + // FIXME: the console logger is deleted by mistake, so if there is any `log.Fatal`, developers won't see any error message. + // Instead, "No tests were found", last nonsense log is "According to the configuration, subsequent logs will not be printed to the console" exitCode := m.Run() tests.WriterCloser.Reset() @@ -366,10 +369,12 @@ const NoExpectedStatus = -1 func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *httptest.ResponseRecorder { t.Helper() recorder := httptest.NewRecorder() + if req.RemoteAddr == "" { + req.RemoteAddr = "test-mock:12345" + } c.ServeHTTP(recorder, req) if expectedStatus != NoExpectedStatus { - if !assert.EqualValues(t, expectedStatus, recorder.Code, - "Request: %s %s", req.Method, req.URL.String()) { + if !assert.EqualValues(t, expectedStatus, recorder.Code, "Request: %s %s", req.Method, req.URL.String()) { logUnexpectedResponse(t, recorder) } } @@ -410,8 +415,10 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) { return } else if len(respBytes) < 500 { // if body is short, just log the whole thing - t.Log("Response:", string(respBytes)) + t.Log("Response: ", string(respBytes)) return + } else { + t.Log("Response length: ", len(respBytes)) } // log the "flash" error message, if one exists diff --git a/tests/test_utils.go b/tests/test_utils.go index 102dd3d298..b3c98427c3 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -10,7 +10,6 @@ import ( "os" "path" "path/filepath" - "runtime" "testing" "code.gitea.io/gitea/models/db" @@ -30,29 +29,44 @@ import ( "github.com/stretchr/testify/assert" ) +func exitf(format string, args ...interface{}) { + fmt.Printf(format+"\n", args...) + os.Exit(1) +} + func InitTest(requireGitea bool) { giteaRoot := base.SetupGiteaRoot() if giteaRoot == "" { - fmt.Println("Environment variable $GITEA_ROOT not set") - os.Exit(1) + exitf("Environment variable $GITEA_ROOT not set") } + setting.AppWorkPath = giteaRoot if requireGitea { giteaBinary := "gitea" - if runtime.GOOS == "windows" { + if setting.IsWindows { giteaBinary += ".exe" } setting.AppPath = path.Join(giteaRoot, giteaBinary) if _, err := os.Stat(setting.AppPath); err != nil { - fmt.Printf("Could not find gitea binary at %s\n", setting.AppPath) - os.Exit(1) + exitf("Could not find gitea binary at %s", setting.AppPath) } } giteaConf := os.Getenv("GITEA_CONF") if giteaConf == "" { - fmt.Println("Environment variable $GITEA_CONF not set") - os.Exit(1) - } else if !path.IsAbs(giteaConf) { + // By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger. + // It's easier for developers to debug bugs step by step with a debugger. + // Notice: when doing "ssh push", Gitea executes sub processes, debugger won't work for the sub processes. + giteaConf = "tests/sqlite.ini" + _ = os.Setenv("GITEA_CONF", giteaConf) + fmt.Printf("Environment variable $GITEA_CONF not set, use default: %s\n", giteaConf) + if !setting.EnableSQLite3 { + exitf(`Need to enable SQLite3 for sqlite.ini testing, please set: -tags "sqlite,sqlite_unlock_notify"`) + } + } + + setting.IsInTesting = true + + if !path.IsAbs(giteaConf) { setting.CustomConf = path.Join(giteaRoot, giteaConf) } else { setting.CustomConf = giteaConf @@ -69,8 +83,7 @@ func InitTest(requireGitea bool) { setting.LoadDBSetting() if err := storage.Init(); err != nil { - fmt.Printf("Init storage failed: %v", err) - os.Exit(1) + exitf("Init storage failed: %v", err) } switch { @@ -221,7 +234,7 @@ func PrepareTestEnv(t testing.TB, skip ...int) func() { return deferFn } -// resetFixtures flushes queues, reloads fixtures and resets test repositories within a single test. +// ResetFixtures flushes queues, reloads fixtures and resets test repositories within a single test. // Most tests should call defer tests.PrepareTestEnv(t)() (or have onGiteaRun do that for them) but sometimes // within a single test this is required func ResetFixtures(t *testing.T) { diff --git a/web_src/css/repository.css b/web_src/css/repository.css index 05e50548d9..8e764f54cd 100644 --- a/web_src/css/repository.css +++ b/web_src/css/repository.css @@ -1911,15 +1911,12 @@ border-radius: var(--border-radius) 0 0 var(--border-radius); } -.repository.quickstart .guide .ui.action.small.input { - width: 100%; -} - .repository.quickstart .guide #repo-clone-url { border-radius: 0; padding: 5px 10px; font-size: 1.2em; line-height: 1.4; + flex: 1 } .repository.release #release-list {