diff --git a/integrations/pull_update_test.go b/integrations/pull_update_test.go index 484390001..251051582 100644 --- a/integrations/pull_update_test.go +++ b/integrations/pull_update_test.go @@ -58,7 +58,7 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *models.User) *models.PullReq assert.NoError(t, err) assert.NotEmpty(t, baseRepo) - headRepo, err := repo_module.ForkRepository(actor, forkOrg, baseRepo, "repo-pr-update", "desc") + headRepo, err := repo_module.ForkRepository(actor, forkOrg, baseRepo, "repo-pr-update", "desc", "") assert.NoError(t, err) assert.NotEmpty(t, headRepo) diff --git a/models/action.go b/models/action.go index 003dc1b20..4821910db 100755 --- a/models/action.go +++ b/models/action.go @@ -164,12 +164,24 @@ func (a *Action) GetRepoName() string { return a.Repo.Name } +// GetRepoName returns the name of the action repository. +func (a *Action) GetRepoDisplayName() string { + a.loadRepo() + return a.Repo.DisplayName() +} + // ShortRepoName returns the name of the action repository // trimmed to max 33 chars. func (a *Action) ShortRepoName() string { return base.EllipsisString(a.GetRepoName(), 33) } +// ShortRepoName returns the name of the action repository +// trimmed to max 33 chars. +func (a *Action) ShortRepoDisplayName() string { + return base.EllipsisString(a.GetRepoDisplayName(), 33) +} + // GetRepoPath returns the virtual path to the action repository. func (a *Action) GetRepoPath() string { return path.Join(a.GetRepoUserName(), a.GetRepoName()) @@ -181,6 +193,12 @@ func (a *Action) ShortRepoPath() string { return path.Join(a.ShortRepoUserName(), a.ShortRepoName()) } +// ShortRepoPath returns the virtual path to the action repository +// trimmed to max 20 + 1 + 33 chars. +func (a *Action) ShortRepoFullDisplayName() string { + return path.Join(a.ShortRepoUserName(), a.ShortRepoDisplayName()) +} + // GetRepoLink returns relative link to action repository. func (a *Action) GetRepoLink() string { if len(setting.AppSubURL) > 0 { diff --git a/models/repo.go b/models/repo.go index f393b51b2..93e2cb140 100755 --- a/models/repo.go +++ b/models/repo.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "html/template" + "math/rand" "xorm.io/xorm" "code.gitea.io/gitea/modules/blockchain" @@ -139,6 +140,7 @@ func NewRepoContext() { // RepositoryStatus defines the status of repository type RepositoryStatus int type RepoBlockChainStatus int +type RepoType int // all kinds of RepositoryStatus const ( @@ -152,6 +154,11 @@ const ( RepoBlockChainFailed ) +const ( + RepoNormal RepoType = iota + RepoCourse +) + // Repository represents a git repository. type Repository struct { ID int64 `xorm:"pk autoincr"` @@ -165,7 +172,8 @@ type Repository struct { OriginalServiceType api.GitServiceType `xorm:"index"` OriginalURL string `xorm:"VARCHAR(2048)"` DefaultBranch string - + CreatorID int64 `xorm:"INDEX NOT NULL DEFAULT 0"` + Creator *User `xorm:"-"` NumWatches int NumStars int NumForks int @@ -174,11 +182,12 @@ type Repository struct { NumOpenIssues int `xorm:"-"` NumPulls int NumClosedPulls int - NumOpenPulls int `xorm:"-"` - NumMilestones int `xorm:"NOT NULL DEFAULT 0"` - NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"` - NumOpenMilestones int `xorm:"-"` - NumCommit int64 `xorm:"NOT NULL DEFAULT 0"` + NumOpenPulls int `xorm:"-"` + NumMilestones int `xorm:"NOT NULL DEFAULT 0"` + NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"` + NumOpenMilestones int `xorm:"-"` + NumCommit int64 `xorm:"NOT NULL DEFAULT 0"` + RepoType RepoType `xorm:"NOT NULL DEFAULT 0"` IsPrivate bool `xorm:"INDEX"` IsEmpty bool `xorm:"INDEX"` @@ -223,6 +232,7 @@ type Repository struct { Hot int64 `xorm:"-"` Active int64 `xorm:"-"` + Alias string } // SanitizedOriginalURL returns a sanitized OriginalURL @@ -233,6 +243,14 @@ func (repo *Repository) SanitizedOriginalURL() string { return util.SanitizeURLCredentials(repo.OriginalURL, false) } +// GetAlias returns a sanitized OriginalURL +func (repo *Repository) DisplayName() string { + if repo.Alias == "" { + return repo.Name + } + return repo.Alias +} + // ColorFormat returns a colored string to represent this repo func (repo *Repository) ColorFormat(s fmt.State) { var ownerName interface{} @@ -286,6 +304,11 @@ func (repo *Repository) FullName() string { return repo.OwnerName + "/" + repo.Name } +// FullDisplayName returns the repository full display name +func (repo *Repository) FullDisplayName() string { + return repo.OwnerName + "/" + repo.DisplayName() +} + // HTMLURL returns the repository HTML URL func (repo *Repository) HTMLURL() string { return setting.AppURL + repo.FullName() @@ -386,6 +409,7 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool) Owner: repo.Owner.APIFormat(), Name: repo.Name, FullName: repo.FullName(), + FullDisplayName: repo.FullDisplayName(), Description: repo.Description, Private: repo.IsPrivate, Template: repo.IsTemplate, @@ -921,17 +945,23 @@ func (repo *Repository) DescriptionHTML() template.HTML { return template.HTML(markup.Sanitize(string(desc))) } -func isRepositoryExist(e Engine, u *User, repoName string) (bool, error) { - has, err := e.Get(&Repository{ - OwnerID: u.ID, - LowerName: strings.ToLower(repoName), - }) - return has && com.IsDir(RepoPath(u.Name, repoName)), err +func isRepositoryExist(e Engine, u *User, repoName string, alias string) (bool, error) { + var cond = builder.NewCond() + cond = cond.And(builder.Eq{"owner_id": u.ID}) + if alias != "" { + subCon := builder.NewCond() + subCon = subCon.Or(builder.Eq{"alias": alias}, builder.Eq{"lower_name": repoName}) + cond = cond.And(subCon) + } else { + cond = cond.And(builder.Eq{"lower_name": repoName}) + } + count, err := e.Where(cond).Count(&Repository{}) + return count > 0 || com.IsDir(RepoPath(u.Name, repoName)), err } // IsRepositoryExist returns true if the repository with given name under user has already existed. -func IsRepositoryExist(u *User, repoName string) (bool, error) { - return isRepositoryExist(x, u, repoName) +func IsRepositoryExist(u *User, repoName string, alias string) (bool, error) { + return isRepositoryExist(x, u, repoName, alias) } // CloneLink represents different types of clone URLs of repository. @@ -975,20 +1005,24 @@ 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, repoName, alias string) error { if !doer.CanCreateRepo() { return ErrReachLimitOfRepo{u.MaxRepoCreation} } - if err := IsUsableRepoName(name); err != nil { + if err := IsUsableRepoName(repoName); err != nil { return err } - has, err := isRepositoryExist(x, u, name) + if err := IsUsableRepoAlias(alias); err != nil { + return err + } + + has, err := isRepositoryExist(x, u, repoName, alias) if err != nil { return fmt.Errorf("IsRepositoryExist: %v", err) } else if has { - return ErrRepoAlreadyExist{u.Name, name} + return ErrRepoAlreadyExist{u.Name, repoName} } return nil } @@ -996,6 +1030,7 @@ func CheckCreateRepository(doer, u *User, name string) error { // CreateRepoOptions contains the create repository options type CreateRepoOptions struct { Name string + Alias string Description string OriginalURL string GitServiceType api.GitServiceType @@ -1008,6 +1043,8 @@ type CreateRepoOptions struct { IsMirror bool AutoInit bool Status RepositoryStatus + IsCourse bool + Topics []string } // GetRepoInitFile returns repository init files @@ -1036,8 +1073,10 @@ func GetRepoInitFile(tp, name string) ([]byte, error) { } var ( - reservedRepoNames = []string{".", ".."} - reservedRepoPatterns = []string{"*.git", "*.wiki"} + reservedRepoNames = []string{".", ".."} + reservedRepoPatterns = []string{"*.git", "*.wiki"} + reservedRepoAliasNames = []string{} + reservedRepoAliasPatterns = []string{} ) // IsUsableRepoName returns true when repository is usable @@ -1045,19 +1084,30 @@ func IsUsableRepoName(name string) error { return isUsableName(reservedRepoNames, reservedRepoPatterns, name) } +// IsUsableRepoAlias returns true when repository alias is usable +func IsUsableRepoAlias(name string) error { + return isUsableName(reservedRepoAliasNames, reservedRepoAliasPatterns, name) +} + // CreateRepository creates a repository for the user/organization. -func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error) { +func CreateRepository(ctx DBContext, doer, u *User, repo *Repository, opts ...CreateRepoOptions) (err error) { if err = IsUsableRepoName(repo.Name); err != nil { return err } - has, err := isRepositoryExist(ctx.e, u, repo.Name) + if err := IsUsableRepoAlias(repo.Alias); err != nil { + return err + } + has, err := isRepositoryExist(ctx.e, u, repo.Name, repo.Alias) if err != nil { return fmt.Errorf("IsRepositoryExist: %v", err) } else if has { return ErrRepoAlreadyExist{u.Name, repo.Name} } - + isCourse := isCourse(opts) + if isCourse { + repo.CreatorID = doer.ID + } if _, err = ctx.e.Insert(repo); err != nil { return err } @@ -1091,17 +1141,23 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error Config: &PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true}, }) } else if tp == UnitTypeDatasets { - units = append(units, RepoUnit{ - RepoID: repo.ID, - Type: tp, - Config: &DatasetConfig{EnableDataset: true}, - }) + if !isCourse { + units = append(units, RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: &DatasetConfig{EnableDataset: true}, + }) + } + } else if tp == UnitTypeCloudBrain { - units = append(units, RepoUnit{ - RepoID: repo.ID, - Type: tp, - Config: &CloudBrainConfig{EnableCloudBrain: true}, - }) + if !isCourse { + units = append(units, RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: &CloudBrainConfig{EnableCloudBrain: true}, + }) + } + } else if tp == UnitTypeBlockChain { units = append(units, RepoUnit{ RepoID: repo.ID, @@ -1109,11 +1165,13 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error Config: &BlockChainConfig{EnableBlockChain: true}, }) } else if tp == UnitTypeModelManage { - units = append(units, RepoUnit{ - RepoID: repo.ID, - Type: tp, - Config: &ModelManageConfig{EnableModelManage: true}, - }) + if !isCourse { + units = append(units, RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: &ModelManageConfig{EnableModelManage: true}, + }) + } } else { units = append(units, RepoUnit{ RepoID: repo.ID, @@ -1183,6 +1241,14 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error return nil } +func isCourse(opts []CreateRepoOptions) bool { + var isCourse = false + if len(opts) > 0 { + isCourse = opts[0].IsCourse + } + return isCourse +} + func countRepositories(userID int64, private bool) int64 { sess := x.Where("id > 0") @@ -1233,7 +1299,7 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error } // Check if new owner has repository with same name. - has, err := IsRepositoryExist(newOwner, repo.Name) + has, err := IsRepositoryExist(newOwner, repo.Name, repo.Alias) if err != nil { return fmt.Errorf("IsRepositoryExist: %v", err) } else if has { @@ -1366,7 +1432,7 @@ func ChangeRepositoryName(doer *User, repo *Repository, newRepoName string) (err return err } - has, err := IsRepositoryExist(repo.Owner, newRepoName) + has, err := IsRepositoryExist(repo.Owner, newRepoName, "") if err != nil { return fmt.Errorf("IsRepositoryExist: %v", err) } else if has { @@ -2521,6 +2587,14 @@ func UpdateRepositoryCommitNum(repo *Repository) error { return nil } +func GenerateDefaultRepoName(ownerName string) string { + if len(ownerName) > 5 { + ownerName = ownerName[:5] + } + now := time.Now().Format("20060102150405") + return ownerName + now + fmt.Sprint(rand.Intn(10)) +} + type RepoFile struct { CommitId string Content []byte diff --git a/models/repo_generate.go b/models/repo_generate.go index 480683cd4..08bb1463d 100644 --- a/models/repo_generate.go +++ b/models/repo_generate.go @@ -19,6 +19,7 @@ import ( // GenerateRepoOptions contains the template units to generate type GenerateRepoOptions struct { Name string + Alias string Description string Private bool GitContent bool diff --git a/models/repo_list.go b/models/repo_list.go index c4d8ee823..6fb9380de 100755 --- a/models/repo_list.go +++ b/models/repo_list.go @@ -48,9 +48,12 @@ func (repos RepositoryList) loadAttributes(e Engine) error { set := make(map[int64]struct{}) repoIDs := make([]int64, len(repos)) + setCreator := make(map[int64]struct{}) for i := range repos { set[repos[i].OwnerID] = struct{}{} repoIDs[i] = repos[i].ID + setCreator[repos[i].CreatorID] = struct{}{} + } // Load owners. @@ -61,8 +64,18 @@ func (repos RepositoryList) loadAttributes(e Engine) error { Find(&users); err != nil { return fmt.Errorf("find users: %v", err) } + //Load creator + creators := make(map[int64]*User, len(set)) + if err := e. + Where("id > 0"). + In("id", keysInt64(setCreator)). + Find(&creators); err != nil { + return fmt.Errorf("find create repo users: %v", err) + } + for i := range repos { repos[i].Owner = users[repos[i].OwnerID] + repos[i].Creator = creators[repos[i].CreatorID] } // Load primary language. @@ -174,6 +187,10 @@ type SearchRepoOptions struct { // True -> include just has milestones // False -> include just has no milestone HasMilestones util.OptionalBool + // None -> include all repos + // True -> include just courses + // False -> include just no courses + Course util.OptionalBool } //SearchOrderBy is used to sort the result @@ -200,8 +217,8 @@ const ( SearchOrderByForks SearchOrderBy = "num_forks ASC" SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" SearchOrderByDownloadTimes SearchOrderBy = "download_times DESC" - SearchOrderByHot SearchOrderBy = "(num_watches + num_stars + num_forks + clone_cnt) DESC" - SearchOrderByActive SearchOrderBy = "(num_issues + num_pulls + num_commit) DESC" + SearchOrderByHot SearchOrderBy = "(num_watches + num_stars + num_forks + clone_cnt) DESC" + SearchOrderByActive SearchOrderBy = "(num_issues + num_pulls + num_commit) DESC" ) // SearchRepositoryCondition creates a query condition according search repository options @@ -321,6 +338,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { var likes = builder.NewCond() for _, v := range strings.Split(opts.Keyword, ",") { likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)}) + likes = likes.Or(builder.Like{"alias", v}) if opts.IncludeDescription { likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)}) } @@ -350,6 +368,10 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue}) } + if opts.Course == util.OptionalBoolTrue { + cond = cond.And(builder.Eq{"repo_type": RepoCourse}) + } + if opts.Actor != nil && opts.Actor.IsRestricted { cond = cond.And(accessibleRepositoryCondition(opts.Actor)) } diff --git a/modules/auth/auth.go b/modules/auth/auth.go index 352e50ca0..6a156491d 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -186,6 +186,8 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field)) case validation.ErrGlobPattern: data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message) + case validation.ErrAlphaDashDotChinese: + data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_chinese_error") default: data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification } diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 8061c6469..c113aa890 100755 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -29,6 +29,7 @@ import ( type CreateRepoForm struct { UID int64 `binding:"Required"` RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` + Alias string `binding:"Required;MaxSize(100);AlphaDashDotChinese"` Private bool Description string `binding:"MaxSize(1024)"` DefaultBranch string `binding:"GitRefName;MaxSize(100)"` @@ -62,6 +63,7 @@ type MigrateRepoForm struct { UID int64 `json:"uid" binding:"Required"` // required: true RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` + Alias string `json:"alias" binding:"Required;AlphaDashDotChinese;MaxSize(100)"` Mirror bool `json:"mirror"` Private bool `json:"private"` Description string `json:"description" binding:"MaxSize(255)"` @@ -109,6 +111,7 @@ func (f MigrateRepoForm) ParseRemoteAddr(user *models.User) (string, error) { // RepoSettingForm form for changing repository settings type RepoSettingForm struct { RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` + Alias string `binding:"Required;AlphaDashDotChinese;MaxSize(100)"` Description string `binding:"MaxSize(255)"` Website string `binding:"ValidUrl;MaxSize(255)"` Interval string @@ -725,3 +728,15 @@ type DeadlineForm struct { func (f *DeadlineForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { return validate(errs, ctx.Data, f, ctx.Locale) } + +type CreateCourseForm struct { + RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` + Alias string `binding:"Required;MaxSize(100);AlphaDashDotChinese"` + Topics string + Description string `binding:"MaxSize(1024)"` +} + +// Validate validates the fields +func (f *CreateCourseForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} diff --git a/modules/repository/create.go b/modules/repository/create.go index d740c58b1..2a24cb708 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -22,12 +22,17 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m Limit: u.MaxRepoCreation, } } + var RepoType = models.RepoNormal + if opts.IsCourse { + RepoType = models.RepoCourse + } repo := &models.Repository{ OwnerID: u.ID, Owner: u, OwnerName: u.Name, Name: opts.Name, + Alias: opts.Alias, LowerName: strings.ToLower(opts.Name), Description: opts.Description, OriginalURL: opts.OriginalURL, @@ -37,10 +42,14 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, Status: opts.Status, IsEmpty: !opts.AutoInit, + RepoType: RepoType, } err = models.WithTx(func(ctx models.DBContext) error { - if err = models.CreateRepository(ctx, doer, u, repo); err != nil { + if err = models.CreateRepository(ctx, doer, u, repo, opts); err != nil { + return err + } + if err = models.SaveTopics(repo.ID, opts.Topics...); err != nil { return err } diff --git a/modules/repository/fork.go b/modules/repository/fork.go index 2ed2a0eb7..da9039d00 100644 --- a/modules/repository/fork.go +++ b/modules/repository/fork.go @@ -15,7 +15,7 @@ import ( ) // ForkRepository forks a repository -func ForkRepository(doer, owner *models.User, oldRepo *models.Repository, name, desc string) (_ *models.Repository, err error) { +func ForkRepository(doer, owner *models.User, oldRepo *models.Repository, name, desc, alias string) (_ *models.Repository, err error) { forkedRepo, err := oldRepo.GetUserFork(owner.ID) if err != nil { return nil, err @@ -33,6 +33,7 @@ func ForkRepository(doer, owner *models.User, oldRepo *models.Repository, name, Owner: owner, OwnerName: owner.Name, Name: name, + Alias: alias, LowerName: strings.ToLower(name), Description: desc, DefaultBranch: oldRepo.DefaultBranch, diff --git a/modules/repository/fork_test.go b/modules/repository/fork_test.go index cb3526bcc..f599ead68 100644 --- a/modules/repository/fork_test.go +++ b/modules/repository/fork_test.go @@ -18,7 +18,7 @@ func TestForkRepository(t *testing.T) { user := models.AssertExistsAndLoadBean(t, &models.User{ID: 13}).(*models.User) repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 10}).(*models.Repository) - fork, err := ForkRepository(user, user, repo, "test", "test") + fork, err := ForkRepository(user, user, repo, "test", "test", "test") assert.Nil(t, fork) assert.Error(t, err) assert.True(t, models.IsErrForkAlreadyExist(err)) diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 6d80488de..86c9a5c28 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -236,6 +236,7 @@ func GenerateRepository(ctx models.DBContext, doer, owner *models.User, template Owner: owner, OwnerName: owner.Name, Name: opts.Name, + Alias: opts.Alias, LowerName: strings.ToLower(opts.Name), Description: opts.Description, IsPrivate: opts.Private, diff --git a/modules/setting/setting.go b/modules/setting/setting.go index e7ab0b7d2..3d2bd91c5 100755 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -568,6 +568,11 @@ var ( }{} Warn_Notify_Mails []string + + Course = struct { + OrgName string + TeamName string + }{} ) // DateLang transforms standard language locale name to corresponding value in datetime plugin. @@ -1331,6 +1336,10 @@ func NewContext() { sec = Cfg.Section("warn_mail") Warn_Notify_Mails = strings.Split(sec.Key("mails").MustString(""), ",") + + sec = Cfg.Section("course") + Course.OrgName = sec.Key("org_name").MustString("") + Course.TeamName = sec.Key("team_name").MustString("") } func SetRadarMapConfig() { diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 70de9b746..b824813bb 100755 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -46,31 +46,32 @@ type ExternalWiki struct { // Repository represents a repository type Repository struct { - ID int64 `json:"id"` - Owner *User `json:"owner"` - Name string `json:"name"` - FullName string `json:"full_name"` - Description string `json:"description"` - Empty bool `json:"empty"` - Private bool `json:"private"` - Fork bool `json:"fork"` - Template bool `json:"template"` - Parent *Repository `json:"parent"` - Mirror bool `json:"mirror"` - Size int `json:"size"` - HTMLURL string `json:"html_url"` - SSHURL string `json:"ssh_url"` - CloneURL string `json:"clone_url"` - OriginalURL string `json:"original_url"` - Website string `json:"website"` - Stars int `json:"stars_count"` - Forks int `json:"forks_count"` - Watchers int `json:"watchers_count"` - OpenIssues int `json:"open_issues_count"` - OpenPulls int `json:"open_pr_counter"` - Releases int `json:"release_counter"` - DefaultBranch string `json:"default_branch"` - Archived bool `json:"archived"` + ID int64 `json:"id"` + Owner *User `json:"owner"` + Name string `json:"name"` + FullName string `json:"full_name"` + FullDisplayName string `json:"full_display_name"` + Description string `json:"description"` + Empty bool `json:"empty"` + Private bool `json:"private"` + Fork bool `json:"fork"` + Template bool `json:"template"` + Parent *Repository `json:"parent"` + Mirror bool `json:"mirror"` + Size int `json:"size"` + HTMLURL string `json:"html_url"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` + OriginalURL string `json:"original_url"` + Website string `json:"website"` + Stars int `json:"stars_count"` + Forks int `json:"forks_count"` + Watchers int `json:"watchers_count"` + OpenIssues int `json:"open_issues_count"` + OpenPulls int `json:"open_pr_counter"` + Releases int `json:"release_counter"` + DefaultBranch string `json:"default_branch"` + Archived bool `json:"archived"` // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time @@ -217,6 +218,7 @@ type MigrateRepoOption struct { UID int `json:"uid" binding:"Required"` // required: true RepoName string `json:"repo_name" binding:"Required"` + Alias string `json:"alias" binding:"Required"` Mirror bool `json:"mirror"` Private bool `json:"private"` Description string `json:"description"` diff --git a/modules/task/task.go b/modules/task/task.go index 72f111ecc..722e39bec 100644 --- a/modules/task/task.go +++ b/modules/task/task.go @@ -84,6 +84,7 @@ func CreateMigrateTask(doer, u *models.User, opts base.MigrateOptions) (*models. repo, err := repo_module.CreateRepository(doer, u, models.CreateRepoOptions{ Name: opts.RepoName, + Alias: opts.Alias, Description: opts.Description, OriginalURL: opts.OriginalURL, GitServiceType: opts.GitServiceType, diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 1c67878ea..b608cdea2 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -19,6 +19,8 @@ const ( // ErrGlobPattern is returned when glob pattern is invalid ErrGlobPattern = "GlobPattern" + + ErrAlphaDashDotChinese = "AlphaDashDotChineseError" ) var ( @@ -26,6 +28,8 @@ var ( // They cannot have ASCII control characters (i.e. bytes whose values are lower than \040, or \177 DEL), space, tilde ~, caret ^, or colon : anywhere. // They cannot have question-mark ?, asterisk *, or open bracket [ anywhere GitRefNamePatternInvalid = regexp.MustCompile(`[\000-\037\177 \\~^:?*[]+`) + + AlphaDashDotChinese = regexp.MustCompile("^[\u4e00-\u9fa5\\.\\-_A-Za-z0-9]+$") ) // CheckGitRefAdditionalRulesValid check name is valid on additional rules @@ -53,6 +57,7 @@ func AddBindingRules() { addGitRefNameBindingRule() addValidURLBindingRule() addGlobPatternRule() + addAlphaDashDotChineseRule() } func addGitRefNameBindingRule() { @@ -117,6 +122,21 @@ func addGlobPatternRule() { }) } +func addAlphaDashDotChineseRule() { + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return strings.HasPrefix(rule, "AlphaDashDotChinese") + }, + IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + if !ValidAlphaDashDotChinese(fmt.Sprintf("%v", val)) { + errs.Add([]string{name}, ErrAlphaDashDotChinese, "ErrAlphaDashDotChinese") + return false, errs + } + return true, errs + }, + }) +} + func portOnly(hostport string) string { colon := strings.IndexByte(hostport, ':') if colon == -1 { @@ -139,3 +159,7 @@ func validPort(p string) bool { } return true } + +func ValidAlphaDashDotChinese(value string) bool { + return AlphaDashDotChinese.MatchString(value) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 21890babe..3b57b22f5 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -50,6 +50,7 @@ repository = Repository organization = Organization mirror = Mirror new_repo = New Repository +new_course=Publish Course new_migrate = New Migration new_dataset = New Dataset edit_dataset = Edit Dataset @@ -346,7 +347,9 @@ modify = Update [form] UserName = Username -RepoName = Repository name +Alias = Repository name +RepoPath = Repository path +RepoAdress = Repository Adress Email = Email address Password = Password Retype = Re-Type Password @@ -370,7 +373,10 @@ SSPIDefaultLanguage = Default Language require_error = ` cannot be empty.` alpha_dash_error = ` should contain only alphanumeric, dash ('-') and underscore ('_') characters.` alpha_dash_dot_error = ` should contain only alphanumeric, dash ('-'), underscore ('_') and dot ('.') characters.` +reponame_dash_dot_error=` Please enter Chinese, alphanumeric, dash ('-') ,underscore ('_') and dot ('.')characters, up to 100 characters. ` +repoadd_dash_dot_error=` Path only allows input alphanumeric, dash ('-') ,underscore ('_') and dot ('.')characters, up to 100 characters. ` git_ref_name_error = ` must be a well-formed Git reference name.` +alpha_dash_dot_chinese_error= ` should contain only alphanumeric, chinese, dash ('-') and underscore ('_') characters.` size_error = ` must be size %s.` min_size_error = ` must contain at least %s characters.` max_size_error = ` must contain at most %s characters.` @@ -384,7 +390,8 @@ password_not_match = The passwords do not match. 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. +repo_name_been_taken = The repository name or path is already used. +course_name_been_taken=The course path is already used. 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. @@ -797,6 +804,8 @@ readme = README readme_helper = Select a README file template. auto_init = Initialize Repository (Adds .gitignore, License and README) create_repo = Create Repository +create_course = Publish Course +failed_to_create_course=Fail to publish course, please try again later. default_branch = Default Branch mirror_prune = Prune mirror_prune_desc = Remove obsolete remote-tracking references @@ -894,7 +903,7 @@ modelarts.train_job.description=Description modelarts.train_job.parameter_setting=Parameter setting modelarts.train_job.parameter_setting_info=Parameter Info modelarts.train_job.fast_parameter_setting=fast_parameter_setting -modelarts.train_job.fast_parameter_setting_config=fast_parameter_setting_config +modelarts.train_job.fast_parameter_setting_config=fast_parameter_setting_config modelarts.train_job.fast_parameter_setting_config_link=fast_parameter_setting_config_link modelarts.train_job.frames=frames modelarts.train_job.algorithm_origin=Algorithm Origin @@ -954,14 +963,21 @@ template.avatar = Avatar template.issue_labels = Issue Labels template.one_item = Must select at least one template item template.invalid = Must select a template repository +template.repo_adress=Adress +template.repo_path=path +template.repo_name=Name archive.title = This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests. archive.issue.nocomment = This repo is archived. You cannot comment on issues. archive.pull.nocomment = This repo is archived. You cannot comment on pull requests. form.reach_limit_of_creation = You have already reached your limit of %d repositories. +form.reach_limit_of_course_creation=You have already reached your limit of %d courses or repositories. form.name_reserved = The repository name '%s' is reserved. +form.course_name_reserved=The course name '%s' is reserved. form.name_pattern_not_allowed = The pattern '%s' is not allowed in a repository name. +form.course_name_pattern_not_allowed=The pattern '%s' is not allowed in a course name. +add_course_org_fail=Fail to add organization, please try again later. need_auth = Clone Authorization migrate_type = Migration Type @@ -1174,7 +1190,7 @@ issues.filter_label_exclude = `Use alt + click/enter t issues.filter_label_no_select = All labels issues.filter_milestone = Milestone issues.filter_milestone_no_select = All milestones -issues.filter_milestone_no_add = Not add milestones +issues.filter_milestone_no_add = Not add milestones issues.filter_assignee = Assignee issues.filter_assginee_no_select = All assignees issues.filter_type = Type diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 6dd44d848..d034b1722 100755 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -50,6 +50,7 @@ repository=项目 organization=组织 mirror=镜像 new_repo=创建项目 +new_course=发布课程 new_dataset=创建数据集 new_migrate=迁移外部项目 edit_dataset = Edit Dataset @@ -350,7 +351,10 @@ modify=更新 [form] UserName=用户名 -RepoName=项目名称 +RepoName=项目路径 +Alias=项目名称 +RepoPath=项目路径 +RepoAdress=项目地址 Email=邮箱地址 Password=密码 Retype=重新输入密码 @@ -374,7 +378,10 @@ SSPIDefaultLanguage=默认语言 require_error=不能为空。 alpha_dash_error=应该只包含字母数字、破折号 ('-') 和下划线 ('_') 字符。 alpha_dash_dot_error=应该只包含字母数字, 破折号 ('-'), 下划线 ('_') 和点 ('. ') 。 +reponame_dash_dot_error=请输入中文、字母、数字和-_.、最多100个字符。 +repoadd_dash_dot_error=路径只允许字母、数字和-_.,最多100个字符。 git_ref_name_error=` 必须是格式良好的 git 引用名称。` +alpha_dash_dot_chinese_error=应该只包含字母数字中文, 破折号 ('-'), 下划线 ('_') 和点 ('. ') 。 size_error=长度必须为 %s。 min_size_error=长度最小为 %s 个字符。 max_size_error=长度最大为 %s 个字符。 @@ -388,7 +395,8 @@ password_not_match=密码不匹配。 lang_select_error=从列表中选出语言 username_been_taken=用户名已被使用。 -repo_name_been_taken=项目名称已被使用。 +repo_name_been_taken=项目名称或项目路径已被使用。 +course_name_been_taken=课程名称或路径已被使用。 visit_rate_limit=远程访问达到速度限制。 2fa_auth_required=远程访问需要双重验证。 org_name_been_taken=组织名称已被使用。 @@ -802,6 +810,8 @@ readme=自述 readme_helper=选择自述文件模板。 auto_init=初始化存储库 (添加. gitignore、许可证和自述文件) create_repo=创建项目 +create_course=发布课程 +failed_to_create_course=发布课程失败,请稍后再试。 default_branch=默认分支 mirror_prune=修剪 mirror_prune_desc=删除过时的远程跟踪引用 @@ -965,14 +975,21 @@ template.avatar=头像 template.issue_labels=任务标签 template.one_item=必须至少选择一个模板项 template.invalid=必须选择一个模板项目 +template.repo_adress=项目地址 +template.repo_path=项目地址 +template.repo_name=项目名称 archive.title=此项目已存档。您可以查看文件和克隆,但不能推送或创建任务/合并请求。 archive.issue.nocomment=此项目已存档,您不能在此任务添加评论。 archive.pull.nocomment=此项目已存档,您不能在此合并请求添加评论。 form.reach_limit_of_creation=你已经达到了您的 %d 项目的限制。 +form.reach_limit_of_course_creation=你已经达到了您的 %d 课程的限制。 form.name_reserved=项目名称 '%s' 是被保留的。 +form.course_name_reserved=课程名称 '%s' 是被保留的。 form.name_pattern_not_allowed=项目名称中不允许使用模式 "%s"。 +form.course_name_pattern_not_allowed=课程名称中不允许使用模式 "%s"。 +add_course_org_fail=加入组织失败,请稍后重试。 need_auth=需要授权验证 migrate_type=迁移类型 diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 3536b7f43..a753f192d 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -118,7 +118,7 @@ func CreateFork(ctx *context.APIContext, form api.CreateForkOption) { forker = org } - fork, err := repo_service.ForkRepository(ctx.User, forker, repo, repo.Name, repo.Description) + fork, err := repo_service.ForkRepository(ctx.User, forker, repo, repo.Name, repo.Description, repo.Alias) if err != nil { ctx.Error(http.StatusInternalServerError, "ForkRepository", err) return diff --git a/routers/home.go b/routers/home.go index 24de1a10c..397e1990d 100755 --- a/routers/home.go +++ b/routers/home.go @@ -7,11 +7,11 @@ package routers import ( "bytes" - "fmt" - "io/ioutil" "net/http" "strings" + "code.gitea.io/gitea/services/repository" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -133,6 +133,7 @@ type RepoSearchOptions struct { Restricted bool PageSize int TplName base.TplName + Course util.OptionalBool } var ( @@ -211,6 +212,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { AllLimited: true, TopicName: topic, IncludeDescription: setting.UI.SearchRepoDescription, + Course: opts.Course, }) if err != nil { ctx.ServerError("SearchRepository", err) @@ -559,7 +561,7 @@ func NotFound(ctx *context.Context) { func RecommendOrgFromPromote(ctx *context.Context) { url := setting.RecommentRepoAddr + "organizations" - result, err := recommendFromPromote(url) + result, err := repository.RecommendFromPromote(url) if err != nil { ctx.ServerError("500", err) return @@ -586,62 +588,11 @@ func RecommendOrgFromPromote(ctx *context.Context) { ctx.JSON(200, resultOrg) } -func recommendFromPromote(url string) ([]string, error) { - resp, err := http.Get(url) - if err != nil || resp.StatusCode != 200 { - log.Info("Get organizations url error=" + err.Error()) - return nil, err - } - bytes, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - log.Info("Get organizations url error=" + err.Error()) - return nil, err - } - - allLineStr := string(bytes) - lines := strings.Split(allLineStr, "\n") - result := make([]string, len(lines)) - for i, line := range lines { - log.Info("i=" + fmt.Sprint(i) + " line=" + line) - result[i] = strings.Trim(line, " ") - } - return result, nil -} - func RecommendRepoFromPromote(ctx *context.Context) { - url := setting.RecommentRepoAddr + "projects" - result, err := recommendFromPromote(url) + result, err := repository.GetRecommendRepoFromPromote("projects") if err != nil { ctx.ServerError("500", err) - return - } - resultRepo := make([]map[string]interface{}, 0) - //resultRepo := make([]*models.Repository, 0) - for _, repoName := range result { - tmpIndex := strings.Index(repoName, "/") - if tmpIndex == -1 { - log.Info("error repo name format.") - } else { - ownerName := strings.Trim(repoName[0:tmpIndex], " ") - repoName := strings.Trim(repoName[tmpIndex+1:], " ") - repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) - if err == nil { - repoMap := make(map[string]interface{}) - repoMap["ID"] = fmt.Sprint(repo.ID) - repoMap["Name"] = repo.Name - repoMap["OwnerName"] = repo.OwnerName - repoMap["NumStars"] = repo.NumStars - repoMap["NumForks"] = repo.NumForks - repoMap["Description"] = repo.Description - repoMap["NumWatchs"] = repo.NumWatches - repoMap["Topics"] = repo.Topics - repoMap["Avatar"] = repo.RelAvatarLink() - resultRepo = append(resultRepo, repoMap) - } else { - log.Info("query repo error," + err.Error()) - } - } + } else { + ctx.JSON(200, result) } - ctx.JSON(200, resultRepo) } diff --git a/routers/org/home.go b/routers/org/home.go index df600d96d..4c350d352 100755 --- a/routers/org/home.go +++ b/routers/org/home.go @@ -7,6 +7,10 @@ package org import ( "strings" + "code.gitea.io/gitea/services/repository" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -14,7 +18,8 @@ import ( ) const ( - tplOrgHome base.TplName = "org/home" + tplOrgHome base.TplName = "org/home" + tplOrgCourseHome base.TplName = "org/home_courses" ) // Home show organization home page @@ -59,10 +64,16 @@ func Home(ctx *context.Context) { case "fewestforks": orderBy = models.SearchOrderByForks default: - ctx.Data["SortType"] = "recentupdate" - orderBy = models.SearchOrderByRecentUpdated - } + if setting.Course.OrgName == org.Name { + ctx.Data["SortType"] = "newest" + orderBy = models.SearchOrderByNewest + } else { + ctx.Data["SortType"] = "recentupdate" + orderBy = models.SearchOrderByRecentUpdated + } + } + orderBy = orderBy + ",id" keyword := strings.Trim(ctx.Query("q"), " ") ctx.Data["Keyword"] = keyword @@ -76,9 +87,21 @@ func Home(ctx *context.Context) { count int64 err error ) + pageSize := setting.UI.User.RepoPagingNum + var CourseOptional util.OptionalBool = util.OptionalBoolNone + if setting.Course.OrgName == org.Name { + pageSize = 15 + CourseOptional = util.OptionalBoolTrue + recommendCourses, _ := repository.GetRecommendRepoFromPromote("courses") + ctx.Data["RecommendCourses"] = recommendCourses + recommendCourseKeyWords, _ := repository.GetRecommendCourseKeyWords() + ctx.Data["CoursesKeywords"] = recommendCourseKeyWords + + } + repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ ListOptions: models.ListOptions{ - PageSize: setting.UI.User.RepoPagingNum, + PageSize: pageSize, Page: page, }, Keyword: keyword, @@ -87,6 +110,7 @@ func Home(ctx *context.Context) { Private: ctx.IsSigned, Actor: ctx.User, IncludeDescription: setting.UI.SearchRepoDescription, + Course: CourseOptional, }) if err != nil { ctx.ServerError("SearchRepository", err) @@ -138,5 +162,10 @@ func Home(ctx *context.Context) { } ctx.Data["tags"] = tags - ctx.HTML(200, tplOrgHome) + if setting.Course.OrgName == org.Name { + ctx.HTML(200, tplOrgCourseHome) + } else { + ctx.HTML(200, tplOrgHome) + } + } diff --git a/routers/org/members.go b/routers/org/members.go index 9f13d1be3..39df692d2 100755 --- a/routers/org/members.go +++ b/routers/org/members.go @@ -17,7 +17,8 @@ import ( const ( // tplMembers template for organization members page - tplMembers base.TplName = "org/member/members" + tplMembers base.TplName = "org/member/members" + tplCourseMembers base.TplName = "org/member/course_members" ) // Members render organization users page @@ -64,8 +65,11 @@ func Members(ctx *context.Context) { ctx.Data["MembersIsPublicMember"] = membersIsPublic ctx.Data["MembersIsUserOrgOwner"] = members.IsUserOrgOwner(org.ID) ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus() - - ctx.HTML(200, tplMembers) + if setting.Course.OrgName == org.Name { + ctx.HTML(200, tplCourseMembers) + } else { + ctx.HTML(200, tplMembers) + } } // MembersAction response for operation to a member of organization diff --git a/routers/org/teams.go b/routers/org/teams.go index 03fbf068d..8aa3e3947 100644 --- a/routers/org/teams.go +++ b/routers/org/teams.go @@ -10,6 +10,8 @@ import ( "path" "strings" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" @@ -22,7 +24,8 @@ import ( const ( // tplTeams template path for teams list page - tplTeams base.TplName = "org/team/teams" + tplTeams base.TplName = "org/team/teams" + tplCourseTeams base.TplName = "org/team/courseTeams" // tplTeamNew template path for create new team page tplTeamNew base.TplName = "org/team/new" // tplTeamMembers template path for showing team members page @@ -44,8 +47,12 @@ func Teams(ctx *context.Context) { } } ctx.Data["Teams"] = org.Teams + if setting.Course.OrgName == org.Name { + ctx.HTML(200, tplCourseTeams) + } else { + ctx.HTML(200, tplTeams) + } - ctx.HTML(200, tplTeams) } // TeamsAction response for join, leave, remove, add operations to team diff --git a/routers/repo/course.go b/routers/repo/course.go new file mode 100644 index 000000000..b8f39817b --- /dev/null +++ b/routers/repo/course.go @@ -0,0 +1,189 @@ +package repo + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + repo_service "code.gitea.io/gitea/services/repository" +) + +const ( + tplCreateCourse base.TplName = "repo/createCourse" +) + +func CreateCourse(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("new_course") + + ctx.Data["Owner"] = setting.Course.OrgName + + ctx.HTML(200, tplCreateCourse) + +} + +func CreateCoursePost(ctx *context.Context, form auth.CreateCourseForm) { + ctx.Data["Title"] = ctx.Tr("new_course") + + if ctx.Written() { + return + } + + var topics = make([]string, 0) + var topicsStr = strings.TrimSpace(form.Topics) + if len(topicsStr) > 0 { + topics = strings.Split(topicsStr, ",") + } + + validTopics, invalidTopics := models.SanitizeAndValidateTopics(topics) + + if len(validTopics) > 25 { + ctx.RenderWithErr(ctx.Tr("repo.topic.count_prompt"), tplCreateCourse, form) + return + } + + if len(invalidTopics) > 0 { + ctx.RenderWithErr(ctx.Tr("repo.topic.format_prompt"), tplCreateCourse, form) + return + } + + var repo *models.Repository + var err error + + if setting.Course.OrgName == "" || setting.Course.TeamName == "" { + log.Error("then organization name or team name of course is empty.") + ctx.RenderWithErr(ctx.Tr("repo.failed_to_create_course"), tplCreateCourse, form) + return + } + + org, team, err := getOrgAndTeam() + + if err != nil { + log.Error("Failed to get team from db.", err) + ctx.RenderWithErr(ctx.Tr("repo.failed_to_create_course"), tplCreateCourse, form) + return + } + isInTeam, err := models.IsUserInTeams(ctx.User.ID, []int64{team.ID}) + if err != nil { + log.Error("Failed to get user in team from db.") + ctx.RenderWithErr(ctx.Tr("repo.failed_to_create_course"), tplCreateCourse, form) + return + } + + if !isInTeam { + err = models.AddTeamMember(team, ctx.User.ID) + if err != nil { + log.Error("Failed to add user to team.") + ctx.RenderWithErr(ctx.Tr("repo.failed_to_create_course"), tplCreateCourse, form) + return + } + } + + if ctx.HasError() { + ctx.HTML(200, tplCreateCourse) + return + } + + repo, err = repo_service.CreateRepository(ctx.User, org, models.CreateRepoOptions{ + Name: form.RepoName, + Alias: form.Alias, + Description: form.Description, + Gitignores: "", + IssueLabels: "", + License: "", + Readme: "Default", + IsPrivate: false, + DefaultBranch: "master", + AutoInit: true, + IsCourse: true, + Topics: validTopics, + }) + if err == nil { + log.Trace("Repository created [%d]: %s/%s", repo.ID, org.Name, repo.Name) + ctx.Redirect(setting.AppSubURL + "/" + org.Name + "/" + repo.Name) + return + } + + handleCreateCourseError(ctx, org, err, "CreateCoursePost", tplCreateCourse, &form) +} + +func AddCourseOrg(ctx *context.Context) { + + _, team, err := getOrgAndTeam() + + if err != nil { + log.Error("Failed to get team from db.", err) + ctx.JSON(http.StatusOK, map[string]interface{}{ + "code": 1, + "message": ctx.Tr("repo.addCourseOrgFail"), + }) + return + } + isInTeam, err := models.IsUserInTeams(ctx.User.ID, []int64{team.ID}) + if err != nil { + log.Error("Failed to get user in team from db.", err) + ctx.JSON(http.StatusOK, map[string]interface{}{ + "code": 1, + "message": ctx.Tr("repo.add_course_org_fail"), + }) + return + } + + if !isInTeam { + err = models.AddTeamMember(team, ctx.User.ID) + if err != nil { + log.Error("Failed to add user to team.", err) + ctx.JSON(http.StatusOK, map[string]interface{}{ + "code": 1, + "message": ctx.Tr("repo.add_course_org_fail"), + }) + return + } + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "code": 0, + "message": "", + }) + +} + +func getOrgAndTeam() (*models.User, *models.Team, error) { + org, err := models.GetUserByName(setting.Course.OrgName) + + if err != nil { + log.Error("Failed to get organization from db.", err) + return nil, nil, err + } + + team, err := models.GetTeam(org.ID, setting.Course.TeamName) + + if err != nil { + log.Error("Failed to get team from db.", err) + + return nil, nil, err + } + return org, team, nil +} + +func handleCreateCourseError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form interface{}) { + switch { + case models.IsErrReachLimitOfRepo(err): + ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_course_creation", owner.MaxCreationLimit()), tpl, form) + case models.IsErrRepoAlreadyExist(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("form.course_name_been_taken"), tpl, form) + case models.IsErrNameReserved(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("repo.form.course_name_reserved", err.(models.ErrNameReserved).Name), tpl, form) + case models.IsErrNamePatternNotAllowed(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("repo.form.course_name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form) + default: + ctx.ServerError(name, err) + } +} diff --git a/routers/repo/pull.go b/routers/repo/pull.go index 8841e4755..22ea3a54c 100755 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -105,6 +105,7 @@ func getForkRepository(ctx *context.Context) *models.Repository { return nil } ctx.Data["ForkFrom"] = forkRepo.Owner.Name + "/" + forkRepo.Name + ctx.Data["ForkDisplayName"] = forkRepo.FullDisplayName() ctx.Data["ForkFromOwnerID"] = forkRepo.Owner.ID if err := ctx.User.GetOwnedOrganizations(); err != nil { @@ -221,7 +222,7 @@ func ForkPost(ctx *context.Context, form auth.CreateRepoForm) { } } - repo, err := repo_service.ForkRepository(ctx.User, ctxUser, forkRepo, form.RepoName, form.Description) + repo, err := repo_service.ForkRepository(ctx.User, ctxUser, forkRepo, form.RepoName, form.Description, form.Alias) if err != nil { ctx.Data["Err_RepoName"] = true switch { diff --git a/routers/repo/repo.go b/routers/repo/repo.go index a182e9087..a509cb52e 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -6,10 +6,12 @@ package repo import ( + "code.gitea.io/gitea/modules/validation" "fmt" "net/url" "os" "path" + "regexp" "strings" "code.gitea.io/gitea/models" @@ -201,6 +203,7 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { if form.RepoTemplate > 0 { opts := models.GenerateRepoOptions{ Name: form.RepoName, + Alias: form.Alias, Description: form.Description, Private: form.Private, GitContent: form.GitContent, @@ -235,6 +238,7 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { } else { repo, err = repo_service.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{ Name: form.RepoName, + Alias: form.Alias, Description: form.Description, Gitignores: form.Gitignores, IssueLabels: form.IssueLabels, @@ -358,6 +362,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { GitServiceType: gitServiceType, CloneAddr: remoteAddr, RepoName: form.RepoName, + Alias: form.Alias, Description: form.Description, Private: form.Private || setting.Repository.ForcePrivate, Mirror: form.Mirror, @@ -380,7 +385,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, opts.Alias) if err != nil { handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) return @@ -552,3 +557,27 @@ func Status(ctx *context.Context) { "err": task.Errors, }) } + +var repoNamePattern = regexp.MustCompile("^[0-9a-zA-Z\\.\\-_]{1,100}$") +var repoAliasPattern = regexp.MustCompile("^[\u4e00-\u9fa5\\.\\-_A-Za-z0-9]{1,100}$") + +// CheckName returns repository's default name(by given alias) +func CheckName(ctx *context.Context) { + var r = make(map[string]string, 1) + q := ctx.Query("q") + owner := ctx.Query("owner") + if q == "" || owner == "" || len(q) > 100 || !validation.ValidAlphaDashDotChinese(q) { + r["name"] = "" + ctx.JSON(200, r) + return + } + if repoNamePattern.MatchString(q) { + r["name"] = q + ctx.JSON(200, r) + return + } + n := models.GenerateDefaultRepoName(owner) + r["name"] = n + ctx.JSON(200, r) + return +} diff --git a/routers/repo/setting.go b/routers/repo/setting.go index f7da8f4a8..055627fc1 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -98,6 +98,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { repo.Description = form.Description repo.Website = form.Website repo.IsTemplate = form.Template + repo.Alias = form.Alias // Visibility of forked repository is forced sync with base repository. if repo.IsFork { diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 439c17a92..5f2237dd8 100755 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -708,6 +708,13 @@ func RegisterRoutes(m *macaron.Macaron) { }, reqSignIn) // ***** END: Organization ***** + m.Group("/course", func() { + m.Get("/create", repo.CreateCourse) + m.Post("/create", bindIgnErr(auth.CreateCourseForm{}), repo.CreateCoursePost) + m.Get("/addOrg", repo.AddCourseOrg) + + }, reqSignIn) + // ***** START: Repository ***** m.Group("/repo", func() { m.Get("/create", repo.Create) @@ -718,6 +725,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Combo("/:repoid").Get(repo.Fork). Post(bindIgnErr(auth.CreateRepoForm{}), repo.ForkPost) }, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader) + m.Get("/check_name", repo.CheckName) }, reqSignIn) // ***** Release Attachment Download without Signin diff --git a/services/repository/repository.go b/services/repository/repository.go index eafad988e..86ee9370e 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -5,12 +5,17 @@ package repository import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + "code.gitea.io/gitea/models" "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" - "fmt" ) // CreateRepository creates a repository for the user/organization. @@ -31,8 +36,8 @@ func CreateRepository(doer, owner *models.User, opts models.CreateRepoOptions) ( } // 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) +func ForkRepository(doer, u *models.User, oldRepo *models.Repository, name, desc, alias string) (*models.Repository, error) { + repo, err := repo_module.ForkRepository(doer, u, oldRepo, name, desc, alias) if err != nil { if repo != nil { if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { @@ -86,3 +91,79 @@ func PushCreateRepo(authUser, owner *models.User, repoName string) (*models.Repo return repo, nil } + +func GetRecommendCourseKeyWords() ([]string, error) { + + url := setting.RecommentRepoAddr + "course_keywords" + result, err := RecommendFromPromote(url) + + if err != nil { + return []string{}, err + } + return result, err + +} + +func GetRecommendRepoFromPromote(filename string) ([]map[string]interface{}, error) { + resultRepo := make([]map[string]interface{}, 0) + url := setting.RecommentRepoAddr + filename + result, err := RecommendFromPromote(url) + + if err != nil { + + return resultRepo, err + } + + //resultRepo := make([]*models.Repository, 0) + for _, repoName := range result { + tmpIndex := strings.Index(repoName, "/") + if tmpIndex == -1 { + log.Info("error repo name format.") + } else { + ownerName := strings.Trim(repoName[0:tmpIndex], " ") + repoName := strings.Trim(repoName[tmpIndex+1:], " ") + repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) + if err == nil { + repoMap := make(map[string]interface{}) + repoMap["ID"] = fmt.Sprint(repo.ID) + repoMap["Name"] = repo.Name + repoMap["Alias"] = repo.Alias + repoMap["Creator"] = repo.Creator + repoMap["OwnerName"] = repo.OwnerName + repoMap["NumStars"] = repo.NumStars + repoMap["NumForks"] = repo.NumForks + repoMap["Description"] = repo.Description + repoMap["NumWatchs"] = repo.NumWatches + repoMap["Topics"] = repo.Topics + repoMap["Avatar"] = repo.RelAvatarLink() + resultRepo = append(resultRepo, repoMap) + } else { + log.Info("query repo error," + err.Error()) + } + } + } + return resultRepo, nil +} + +func RecommendFromPromote(url string) ([]string, error) { + resp, err := http.Get(url) + if err != nil || resp.StatusCode != 200 { + log.Info("Get organizations url error=" + err.Error()) + return nil, err + } + bytes, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + log.Info("Get organizations url error=" + err.Error()) + return nil, err + } + + allLineStr := string(bytes) + lines := strings.Split(allLineStr, "\n") + result := make([]string, len(lines)) + for i, line := range lines { + log.Info("i=" + fmt.Sprint(i) + " line=" + line) + result[i] = strings.Trim(line, " ") + } + return result, nil +} diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index d7560837f..0e01186b0 100755 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -5,7 +5,7 @@ border-radius: 0.8rem; margin-bottom: 1.0rem; padding: 1.0rem !important; - } + } .ui.repository.list>.item .header { font-size: 1.4rem !important; font-weight: 200; @@ -24,7 +24,7 @@ content: ""; height: 1px; background-color: #E1E3E6; - bottom: 2.8rem; + bottom: 2.8rem; } .repository .ui.mini.menu{ font-size: .6rem; @@ -43,13 +43,13 @@ + 热门{{.i18n.Tr "explore.repos"}} + 活跃{{.i18n.Tr "explore.repos"}} {{end}} @@ -93,7 +93,7 @@
- {{if or $.PageIsExplore $.PageIsProfileStarList }}{{if .Owner}}{{.Owner.Name}} / {{end}}{{end}}{{.Name}} + {{if or $.PageIsExplore $.PageIsProfileStarList }}{{if .Owner}}{{.Owner.Name}} / {{end}}{{end}}{{.DisplayName}} {{if .IsArchived}}{{end}} {{if .IsPrivate}} @@ -114,7 +114,7 @@ - + {{.Hot}} {{else if eq $.SortType "active"}} @@ -130,7 +130,7 @@ {{svg "octicon-git-branch" 16}} {{.NumForks}} - {{end}} + {{end}} {{svg "octicon-star" 16}} {{.NumStars}} diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 641dd3de6..61893a87d 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -2,43 +2,15 @@
-
+ {{.CsrfTokenHtml}}

{{.i18n.Tr "new_repo"}}

{{template "base/alert" .}} -
- - - {{.i18n.Tr "repo.repo_owner_helper"}} -
- -
- - - -
+ {{template "repo/repo_name" .}} +
@@ -172,7 +144,7 @@
- {{.i18n.Tr "cancel"}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 2091610df..aab458cea 100755 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -24,7 +24,7 @@ {{end}} {{.Owner.Name}}
/
- {{.Name}} + {{.DisplayName}} {{if .RelAvatarLink}} {{if .IsTemplate}} {{if .IsPrivate}} @@ -46,7 +46,7 @@ {{end}} {{if .IsArchived}}{{end}} {{if .IsMirror}}{{end}} - {{if .IsFork}}
{{$.i18n.Tr "repo.forked_from"}} {{SubStr .BaseRepo.RelLink 1 -1}}
{{end}} + {{if .IsFork}}
{{$.i18n.Tr "repo.forked_from"}} {{.BaseRepo.FullDisplayName}}
{{end}} {{if .IsGenerated}}
{{$.i18n.Tr "repo.generated_from"}} {{SubStr .TemplateRepo.RelLink 1 -1}}
{{end}}
{{if not .IsBeingCreated}} @@ -114,9 +114,9 @@ {{end}}
- + {{end}} - + {{if .Permission.CanRead $.UnitTypeIssues}} @@ -152,7 +152,7 @@ {{if .Permission.CanRead $.UnitTypeCloudBrain}} - + {{.i18n.Tr "repo.cloudbrain"}} diff --git a/templates/repo/migrate.tmpl b/templates/repo/migrate.tmpl index daf8c549e..da4b31896 100644 --- a/templates/repo/migrate.tmpl +++ b/templates/repo/migrate.tmpl @@ -2,7 +2,7 @@
- + {{.CsrfTokenHtml}}

{{.i18n.Tr "new_migrate"}} @@ -37,35 +37,7 @@

- -
- - -
- -
- - -
+ {{template "repo/repo_name" .}}
@@ -127,7 +99,7 @@
- {{.i18n.Tr "cancel"}} diff --git a/templates/repo/pulls/fork.tmpl b/templates/repo/pulls/fork.tmpl index 30f9939fa..b647fa60c 100644 --- a/templates/repo/pulls/fork.tmpl +++ b/templates/repo/pulls/fork.tmpl @@ -2,47 +2,18 @@
- + {{.CsrfTokenHtml}}

{{.i18n.Tr "new_fork"}}

{{template "base/alert" .}} -
- - -
-
- {{.ForkFrom}} -
-
- - + {{.ForkDisplayName}}
+ {{template "repo/repo_name" .}}
diff --git a/templates/repo/repo_name.tmpl b/templates/repo/repo_name.tmpl new file mode 100644 index 000000000..7c43cf814 --- /dev/null +++ b/templates/repo/repo_name.tmpl @@ -0,0 +1,50 @@ +
+ + + {{.i18n.Tr "form.reponame_dash_dot_error"}} +
+ +
+ +
+ + + +
+ +
/
+
+ + + +
+ + +
+ {{.i18n.Tr "form.repoadd_dash_dot_error"}} + \ No newline at end of file diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index b9195a266..726f963fe 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -8,12 +8,46 @@ {{.i18n.Tr "repo.settings.basic_settings"}}
- + {{.CsrfTokenHtml}} -
+ +
+ + + {{.i18n.Tr "form.reponame_dash_dot_error"}} +
+ +
+ +
+
+ +
+
/
+
+ + + {{.i18n.Tr "form.repoadd_dash_dot_error"}} +
+
+ +
+ +
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index a1b4218dc..3eefca78c 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -13,12 +13,12 @@ {{.ShortActUserName}} {{end}} {{if eq .GetOpType 1}} - {{$.i18n.Tr "action.create_repo" .GetRepoLink .ShortRepoPath | Str2html}} + {{$.i18n.Tr "action.create_repo" .GetRepoLink .ShortRepoFullDisplayName | Str2html}} {{else if eq .GetOpType 2}} - {{$.i18n.Tr "action.rename_repo" .GetContent .GetRepoLink .ShortRepoPath | Str2html}} + {{$.i18n.Tr "action.rename_repo" .GetContent .GetRepoLink .ShortRepoFullDisplayName | Str2html}} {{else if eq .GetOpType 5}} {{ $branchLink := .GetBranch | EscapePound | Escape}} - {{$.i18n.Tr "action.commit_repo" .GetRepoLink $branchLink (Escape .GetBranch) .ShortRepoPath | Str2html}} + {{$.i18n.Tr "action.commit_repo" .GetRepoLink $branchLink (Escape .GetBranch) .ShortRepoFullDisplayName | Str2html}} {{else if eq .GetOpType 6}} {{ $index := index .GetIssueInfos 0}} {{$.i18n.Tr "action.create_issue" .GetRepoLink $index .ShortRepoPath | Str2html}} diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index dc1507403..5df52d77a 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -104,7 +104,7 @@
  • - ${repo.full_name} + ${repo.full_display_name} ${repo.stars_count} {{svg "octicon-star" 16}} diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl index d28f067fd..d859954be 100644 --- a/templates/user/settings/repos.tmpl +++ b/templates/user/settings/repos.tmpl @@ -21,7 +21,7 @@ {{else}} {{svg "octicon-repo" 16}} {{end}} - {{$.Owner.Name}}/{{.Name}} + {{$.Owner.Name}}/{{.DisplayName}} {{SizeFmt .Size}} {{if .IsFork}} {{$.i18n.Tr "repo.forked_from"}} diff --git a/web_src/js/index.js b/web_src/js/index.js index 7d59cc0eb..b34e40451 100755 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -4136,4 +4136,84 @@ $('.question.circle.icon').hover(function(){ //云脑详情页面跳转回上一个页面 $(".section.backTodeBug").attr("href",localStorage.getItem('all')) //新建调试取消跳转 -$(".ui.button.cancel").attr("href",localStorage.getItem('all')) \ No newline at end of file +$(".ui.button.cancel").attr("href",localStorage.getItem('all')) + +function initcreateRepo(){ + let timeout; + let keydown_flag = false + const urlAdd = location.href.split('/')[0] + '//' + location.href.split('/')[2] + let owner = $('#ownerDropdown div.text').attr("title") + $(document).ready(function(){ + $('#ownerDropdown').dropdown({ + onChange:function(value,text,$choice){ + owner = $choice[0].getAttribute("title") + $('#repoAdress').css("display","block") + $('#repoAdress span').text(urlAdd+'/'+owner+'/'+$('#repo_name').val()+'.git') + } + }); + }) + $('#repo_name').keyup(function(){ + keydown_flag = $('#repo_name').val() ? true : false + if(keydown_flag){ + $('#repoAdress').css("display","block") + $('#repoAdress span').text(urlAdd+'/'+owner+'/'+$('#repo_name').val()+'.git') + } + else{ + $('#repoAdress').css("display","none") + $('#repo_name').attr("placeholder","") + } + }) + $("#create_repo_form") + .form({ + on: 'blur', + // inline:true, + fields: { + alias: { + identifier : 'alias', + rules: [ + { + type: 'regExp[/^[\u4E00-\u9FA5A-Za-z0-9_.-]{1,100}$/]', + } + ] + }, + repo_name:{ + identifier : 'repo_name', + rules: [ + { + type: 'regExp[/^[A-Za-z0-9_.-]{1,100}$/]', + + } + ] + }, + }, + onFailure: function(e){ + return false; + } + }) + $('#alias').bind('input propertychange', function (event) { + clearTimeout(timeout) + timeout = setTimeout(() => { + //在此处写调用的方法,可以实现仅最后一次操作生效 + const aliasValue = $('#alias').val() + if(keydown_flag){ + $('#repo_name').attr("placeholder","") + } + else if(aliasValue){ + $('#repo_name').attr("placeholder","正在获取路径...") + $.get(`${window.config.AppSubUrl}/repo/check_name?q=${aliasValue}&owner=${owner}`,(data)=>{ + const repo_name = data.name + $('#repo_name').val(repo_name) + $('#repoAdress').css("display","block") + $('#repoAdress span').text(urlAdd+'/'+owner+'/'+$('#repo_name').val()+'.git') + $('#repo_name').attr("placeholder","") + }) + }else{ + $('#repo_name').val('') + $('#repo_name').attr("placeholder","") + $('#repoAdress').css("display","none") + } + }, 500) + }); +} + +initcreateRepo() diff --git a/web_src/less/_form.less b/web_src/less/_form.less index de48f0ba5..e41c428c8 100644 --- a/web_src/less/_form.less +++ b/web_src/less/_form.less @@ -2,7 +2,7 @@ .help { color: #999999; padding-top: .6em; - padding-bottom: .6em; + display: inline-block; } }