mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 11:41:32 +01:00 
			
		
		
		
	Move some functions to service layer (#26969)
This commit is contained in:
		
							parent
							
								
									b8ad558c93
								
							
						
					
					
						commit
						e3ed67859a
					
				| @ -17,6 +17,7 @@ import ( | ||||
| 	project_model "code.gitea.io/gitea/models/project" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/container" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| @ -1247,3 +1248,44 @@ func FixCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) { | ||||
| func (c *Comment) HasOriginalAuthor() bool { | ||||
| 	return c.OriginalAuthor != "" && c.OriginalAuthorID != 0 | ||||
| } | ||||
| 
 | ||||
| // InsertIssueComments inserts many comments of issues. | ||||
| func InsertIssueComments(comments []*Comment) error { | ||||
| 	if len(comments) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	issueIDs := make(container.Set[int64]) | ||||
| 	for _, comment := range comments { | ||||
| 		issueIDs.Add(comment.IssueID) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 	for _, comment := range comments { | ||||
| 		if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		for _, reaction := range comment.Reactions { | ||||
| 			reaction.IssueID = comment.IssueID | ||||
| 			reaction.CommentID = comment.ID | ||||
| 		} | ||||
| 		if len(comment.Reactions) > 0 { | ||||
| 			if err := db.Insert(ctx, comment.Reactions); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for issueID := range issueIDs { | ||||
| 		if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", | ||||
| 			issueID, CommentTypeComment, issueID); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return committer.Commit() | ||||
| } | ||||
|  | ||||
| @ -70,3 +70,30 @@ func TestAsCommentType(t *testing.T) { | ||||
| 	assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment")) | ||||
| 	assert.Equal(t, issues_model.CommentTypePRUnScheduledToAutoMerge, issues_model.AsCommentType("pull_cancel_scheduled_merge")) | ||||
| } | ||||
| 
 | ||||
| func TestMigrate_InsertIssueComments(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) | ||||
| 	_ = issue.LoadRepo(db.DefaultContext) | ||||
| 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}) | ||||
| 	reaction := &issues_model.Reaction{ | ||||
| 		Type:   "heart", | ||||
| 		UserID: owner.ID, | ||||
| 	} | ||||
| 
 | ||||
| 	comment := &issues_model.Comment{ | ||||
| 		PosterID:  owner.ID, | ||||
| 		Poster:    owner, | ||||
| 		IssueID:   issue.ID, | ||||
| 		Issue:     issue, | ||||
| 		Reactions: []*issues_model.Reaction{reaction}, | ||||
| 	} | ||||
| 
 | ||||
| 	err := issues_model.InsertIssueComments([]*issues_model.Comment{comment}) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) | ||||
| 	assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments) | ||||
| 
 | ||||
| 	unittest.CheckConsistencyFor(t, &issues_model.Issue{}) | ||||
| } | ||||
|  | ||||
| @ -892,3 +892,50 @@ func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, erro | ||||
| func IsErrIssueMaxPinReached(err error) bool { | ||||
| 	return err == ErrIssueMaxPinReached | ||||
| } | ||||
| 
 | ||||
| // InsertIssues insert issues to database | ||||
| func InsertIssues(issues ...*Issue) error { | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 
 | ||||
| 	for _, issue := range issues { | ||||
| 		if err := insertIssue(ctx, issue); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return committer.Commit() | ||||
| } | ||||
| 
 | ||||
| func insertIssue(ctx context.Context, issue *Issue) error { | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 	if _, err := sess.NoAutoTime().Insert(issue); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	issueLabels := make([]IssueLabel, 0, len(issue.Labels)) | ||||
| 	for _, label := range issue.Labels { | ||||
| 		issueLabels = append(issueLabels, IssueLabel{ | ||||
| 			IssueID: issue.ID, | ||||
| 			LabelID: label.ID, | ||||
| 		}) | ||||
| 	} | ||||
| 	if len(issueLabels) > 0 { | ||||
| 		if _, err := sess.Insert(issueLabels); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, reaction := range issue.Reactions { | ||||
| 		reaction.IssueID = issue.ID | ||||
| 	} | ||||
| 
 | ||||
| 	if len(issue.Reactions) > 0 { | ||||
| 		if _, err := sess.Insert(issue.Reactions); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @ -573,3 +573,45 @@ func TestIssueLoadAttributes(t *testing.T) { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func assertCreateIssues(t *testing.T, isPull bool) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	reponame := "repo1" | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) | ||||
| 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) | ||||
| 	milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) | ||||
| 	assert.EqualValues(t, milestone.ID, 1) | ||||
| 	reaction := &issues_model.Reaction{ | ||||
| 		Type:   "heart", | ||||
| 		UserID: owner.ID, | ||||
| 	} | ||||
| 
 | ||||
| 	title := "issuetitle1" | ||||
| 	is := &issues_model.Issue{ | ||||
| 		RepoID:      repo.ID, | ||||
| 		MilestoneID: milestone.ID, | ||||
| 		Repo:        repo, | ||||
| 		Title:       title, | ||||
| 		Content:     "issuecontent1", | ||||
| 		IsPull:      isPull, | ||||
| 		PosterID:    owner.ID, | ||||
| 		Poster:      owner, | ||||
| 		IsClosed:    true, | ||||
| 		Labels:      []*issues_model.Label{label}, | ||||
| 		Reactions:   []*issues_model.Reaction{reaction}, | ||||
| 	} | ||||
| 	err := issues_model.InsertIssues(is) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title}) | ||||
| 	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID}) | ||||
| } | ||||
| 
 | ||||
| func TestMigrate_CreateIssuesIsPullFalse(t *testing.T) { | ||||
| 	assertCreateIssues(t, false) | ||||
| } | ||||
| 
 | ||||
| func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) { | ||||
| 	assertCreateIssues(t, true) | ||||
| } | ||||
|  | ||||
| @ -10,7 +10,6 @@ import ( | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| @ -323,261 +322,6 @@ func DeleteMilestoneByRepoID(repoID, id int64) error { | ||||
| 	return committer.Commit() | ||||
| } | ||||
| 
 | ||||
| // MilestoneList is a list of milestones offering additional functionality | ||||
| type MilestoneList []*Milestone | ||||
| 
 | ||||
| func (milestones MilestoneList) getMilestoneIDs() []int64 { | ||||
| 	ids := make([]int64, 0, len(milestones)) | ||||
| 	for _, ms := range milestones { | ||||
| 		ids = append(ids, ms.ID) | ||||
| 	} | ||||
| 	return ids | ||||
| } | ||||
| 
 | ||||
| // GetMilestonesOption contain options to get milestones | ||||
| type GetMilestonesOption struct { | ||||
| 	db.ListOptions | ||||
| 	RepoID   int64 | ||||
| 	State    api.StateType | ||||
| 	Name     string | ||||
| 	SortType string | ||||
| } | ||||
| 
 | ||||
| func (opts GetMilestonesOption) toCond() builder.Cond { | ||||
| 	cond := builder.NewCond() | ||||
| 	if opts.RepoID != 0 { | ||||
| 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) | ||||
| 	} | ||||
| 
 | ||||
| 	switch opts.State { | ||||
| 	case api.StateClosed: | ||||
| 		cond = cond.And(builder.Eq{"is_closed": true}) | ||||
| 	case api.StateAll: | ||||
| 		break | ||||
| 	// api.StateOpen: | ||||
| 	default: | ||||
| 		cond = cond.And(builder.Eq{"is_closed": false}) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(opts.Name) != 0 { | ||||
| 		cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name)) | ||||
| 	} | ||||
| 
 | ||||
| 	return cond | ||||
| } | ||||
| 
 | ||||
| // GetMilestones returns milestones filtered by GetMilestonesOption's | ||||
| func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) { | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where(opts.toCond()) | ||||
| 
 | ||||
| 	if opts.Page != 0 { | ||||
| 		sess = db.SetSessionPagination(sess, &opts) | ||||
| 	} | ||||
| 
 | ||||
| 	switch opts.SortType { | ||||
| 	case "furthestduedate": | ||||
| 		sess.Desc("deadline_unix") | ||||
| 	case "leastcomplete": | ||||
| 		sess.Asc("completeness") | ||||
| 	case "mostcomplete": | ||||
| 		sess.Desc("completeness") | ||||
| 	case "leastissues": | ||||
| 		sess.Asc("num_issues") | ||||
| 	case "mostissues": | ||||
| 		sess.Desc("num_issues") | ||||
| 	case "id": | ||||
| 		sess.Asc("id") | ||||
| 	default: | ||||
| 		sess.Asc("deadline_unix").Asc("id") | ||||
| 	} | ||||
| 
 | ||||
| 	miles := make([]*Milestone, 0, opts.PageSize) | ||||
| 	total, err := sess.FindAndCount(&miles) | ||||
| 	return miles, total, err | ||||
| } | ||||
| 
 | ||||
| // GetMilestoneIDsByNames returns a list of milestone ids by given names. | ||||
| // It doesn't filter them by repo, so it could return milestones belonging to different repos. | ||||
| // It's used for filtering issues via indexer, otherwise it would be useless. | ||||
| // Since it could return milestones with the same name, so the length of returned ids could be more than the length of names. | ||||
| func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) { | ||||
| 	var ids []int64 | ||||
| 	return ids, db.GetEngine(ctx).Table("milestone"). | ||||
| 		Where(db.BuildCaseInsensitiveIn("name", names)). | ||||
| 		Cols("id"). | ||||
| 		Find(&ids) | ||||
| } | ||||
| 
 | ||||
| // SearchMilestones search milestones | ||||
| func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) { | ||||
| 	miles := make([]*Milestone, 0, setting.UI.IssuePagingNum) | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) | ||||
| 	if len(keyword) > 0 { | ||||
| 		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) | ||||
| 	} | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) | ||||
| 	} | ||||
| 	if page > 0 { | ||||
| 		sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) | ||||
| 	} | ||||
| 
 | ||||
| 	switch sortType { | ||||
| 	case "furthestduedate": | ||||
| 		sess.Desc("deadline_unix") | ||||
| 	case "leastcomplete": | ||||
| 		sess.Asc("completeness") | ||||
| 	case "mostcomplete": | ||||
| 		sess.Desc("completeness") | ||||
| 	case "leastissues": | ||||
| 		sess.Asc("num_issues") | ||||
| 	case "mostissues": | ||||
| 		sess.Desc("num_issues") | ||||
| 	default: | ||||
| 		sess.Asc("deadline_unix") | ||||
| 	} | ||||
| 	return miles, sess.Find(&miles) | ||||
| } | ||||
| 
 | ||||
| // GetMilestonesByRepoIDs returns a list of milestones of given repositories and status. | ||||
| func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) { | ||||
| 	return SearchMilestones( | ||||
| 		builder.In("repo_id", repoIDs), | ||||
| 		page, | ||||
| 		isClosed, | ||||
| 		sortType, | ||||
| 		"", | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // MilestonesStats represents milestone statistic information. | ||||
| type MilestonesStats struct { | ||||
| 	OpenCount, ClosedCount int64 | ||||
| } | ||||
| 
 | ||||
| // Total returns the total counts of milestones | ||||
| func (m MilestonesStats) Total() int64 { | ||||
| 	return m.OpenCount + m.ClosedCount | ||||
| } | ||||
| 
 | ||||
| // GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions. | ||||
| func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) { | ||||
| 	var err error | ||||
| 	stats := &MilestonesStats{} | ||||
| 
 | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) | ||||
| 	} | ||||
| 	stats.OpenCount, err = sess.Count(new(Milestone)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) | ||||
| 	} | ||||
| 	stats.ClosedCount, err = sess.Count(new(Milestone)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return stats, nil | ||||
| } | ||||
| 
 | ||||
| // GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword. | ||||
| func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*MilestonesStats, error) { | ||||
| 	var err error | ||||
| 	stats := &MilestonesStats{} | ||||
| 
 | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) | ||||
| 	if len(keyword) > 0 { | ||||
| 		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) | ||||
| 	} | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) | ||||
| 	} | ||||
| 	stats.OpenCount, err = sess.Count(new(Milestone)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) | ||||
| 	if len(keyword) > 0 { | ||||
| 		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) | ||||
| 	} | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) | ||||
| 	} | ||||
| 	stats.ClosedCount, err = sess.Count(new(Milestone)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return stats, nil | ||||
| } | ||||
| 
 | ||||
| // CountMilestones returns number of milestones in given repository with other options | ||||
| func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) { | ||||
| 	return db.GetEngine(ctx). | ||||
| 		Where(opts.toCond()). | ||||
| 		Count(new(Milestone)) | ||||
| } | ||||
| 
 | ||||
| // CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options` | ||||
| func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) { | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) | ||||
| 	} | ||||
| 
 | ||||
| 	countsSlice := make([]*struct { | ||||
| 		RepoID int64 | ||||
| 		Count  int64 | ||||
| 	}, 0, 10) | ||||
| 	if err := sess.GroupBy("repo_id"). | ||||
| 		Select("repo_id AS repo_id, COUNT(*) AS count"). | ||||
| 		Table("milestone"). | ||||
| 		Find(&countsSlice); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	countMap := make(map[int64]int64, len(countsSlice)) | ||||
| 	for _, c := range countsSlice { | ||||
| 		countMap[c.RepoID] = c.Count | ||||
| 	} | ||||
| 	return countMap, nil | ||||
| } | ||||
| 
 | ||||
| // CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options` | ||||
| func CountMilestonesByRepoCondAndKw(repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) { | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) | ||||
| 	if len(keyword) > 0 { | ||||
| 		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) | ||||
| 	} | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) | ||||
| 	} | ||||
| 
 | ||||
| 	countsSlice := make([]*struct { | ||||
| 		RepoID int64 | ||||
| 		Count  int64 | ||||
| 	}, 0, 10) | ||||
| 	if err := sess.GroupBy("repo_id"). | ||||
| 		Select("repo_id AS repo_id, COUNT(*) AS count"). | ||||
| 		Table("milestone"). | ||||
| 		Find(&countsSlice); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	countMap := make(map[int64]int64, len(countsSlice)) | ||||
| 	for _, c := range countsSlice { | ||||
| 		countMap[c.RepoID] = c.Count | ||||
| 	} | ||||
| 	return countMap, nil | ||||
| } | ||||
| 
 | ||||
| func updateRepoMilestoneNum(ctx context.Context, repoID int64) error { | ||||
| 	_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?", | ||||
| 		repoID, | ||||
| @ -588,53 +332,6 @@ func updateRepoMilestoneNum(ctx context.Context, repoID int64) error { | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| //  _____               _            _ _____ _ | ||||
| // |_   _| __ __ _  ___| | _____  __| |_   _(_)_ __ ___   ___  ___ | ||||
| //   | || '__/ _` |/ __| |/ / _ \/ _` | | | | | '_ ` _ \ / _ \/ __| | ||||
| //   | || | | (_| | (__|   <  __/ (_| | | | | | | | | | |  __/\__ \ | ||||
| //   |_||_|  \__,_|\___|_|\_\___|\__,_| |_| |_|_| |_| |_|\___||___/ | ||||
| // | ||||
| 
 | ||||
| func (milestones MilestoneList) loadTotalTrackedTimes(ctx context.Context) error { | ||||
| 	type totalTimesByMilestone struct { | ||||
| 		MilestoneID int64 | ||||
| 		Time        int64 | ||||
| 	} | ||||
| 	if len(milestones) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	trackedTimes := make(map[int64]int64, len(milestones)) | ||||
| 
 | ||||
| 	// Get total tracked time by milestone_id | ||||
| 	rows, err := db.GetEngine(ctx).Table("issue"). | ||||
| 		Join("INNER", "milestone", "issue.milestone_id = milestone.id"). | ||||
| 		Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id"). | ||||
| 		Where("tracked_time.deleted = ?", false). | ||||
| 		Select("milestone_id, sum(time) as time"). | ||||
| 		In("milestone_id", milestones.getMilestoneIDs()). | ||||
| 		GroupBy("milestone_id"). | ||||
| 		Rows(new(totalTimesByMilestone)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	defer rows.Close() | ||||
| 
 | ||||
| 	for rows.Next() { | ||||
| 		var totalTime totalTimesByMilestone | ||||
| 		err = rows.Scan(&totalTime) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		trackedTimes[totalTime.MilestoneID] = totalTime.Time | ||||
| 	} | ||||
| 
 | ||||
| 	for _, milestone := range milestones { | ||||
| 		milestone.TotalTrackedTime = trackedTimes[milestone.ID] | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error { | ||||
| 	type totalTimesByMilestone struct { | ||||
| 		MilestoneID int64 | ||||
| @ -658,12 +355,33 @@ func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request | ||||
| func (milestones MilestoneList) LoadTotalTrackedTimes() error { | ||||
| 	return milestones.loadTotalTrackedTimes(db.DefaultContext) | ||||
| } | ||||
| 
 | ||||
| // LoadTotalTrackedTime loads the tracked time for the milestone | ||||
| func (m *Milestone) LoadTotalTrackedTime() error { | ||||
| 	return m.loadTotalTrackedTime(db.DefaultContext) | ||||
| } | ||||
| 
 | ||||
| // InsertMilestones creates milestones of repository. | ||||
| func InsertMilestones(ms ...*Milestone) (err error) { | ||||
| 	if len(ms) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 
 | ||||
| 	// to return the id, so we should not use batch insert | ||||
| 	for _, m := range ms { | ||||
| 		if _, err = sess.NoAutoTime().Insert(m); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return committer.Commit() | ||||
| } | ||||
|  | ||||
							
								
								
									
										315
									
								
								models/issues/milestone_list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								models/issues/milestone_list.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,315 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package issues | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 
 | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
| 
 | ||||
| // MilestoneList is a list of milestones offering additional functionality | ||||
| type MilestoneList []*Milestone | ||||
| 
 | ||||
| func (milestones MilestoneList) getMilestoneIDs() []int64 { | ||||
| 	ids := make([]int64, 0, len(milestones)) | ||||
| 	for _, ms := range milestones { | ||||
| 		ids = append(ids, ms.ID) | ||||
| 	} | ||||
| 	return ids | ||||
| } | ||||
| 
 | ||||
| // GetMilestonesOption contain options to get milestones | ||||
| type GetMilestonesOption struct { | ||||
| 	db.ListOptions | ||||
| 	RepoID   int64 | ||||
| 	State    api.StateType | ||||
| 	Name     string | ||||
| 	SortType string | ||||
| } | ||||
| 
 | ||||
| func (opts GetMilestonesOption) toCond() builder.Cond { | ||||
| 	cond := builder.NewCond() | ||||
| 	if opts.RepoID != 0 { | ||||
| 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) | ||||
| 	} | ||||
| 
 | ||||
| 	switch opts.State { | ||||
| 	case api.StateClosed: | ||||
| 		cond = cond.And(builder.Eq{"is_closed": true}) | ||||
| 	case api.StateAll: | ||||
| 		break | ||||
| 	// api.StateOpen: | ||||
| 	default: | ||||
| 		cond = cond.And(builder.Eq{"is_closed": false}) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(opts.Name) != 0 { | ||||
| 		cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name)) | ||||
| 	} | ||||
| 
 | ||||
| 	return cond | ||||
| } | ||||
| 
 | ||||
| // GetMilestones returns milestones filtered by GetMilestonesOption's | ||||
| func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) { | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where(opts.toCond()) | ||||
| 
 | ||||
| 	if opts.Page != 0 { | ||||
| 		sess = db.SetSessionPagination(sess, &opts) | ||||
| 	} | ||||
| 
 | ||||
| 	switch opts.SortType { | ||||
| 	case "furthestduedate": | ||||
| 		sess.Desc("deadline_unix") | ||||
| 	case "leastcomplete": | ||||
| 		sess.Asc("completeness") | ||||
| 	case "mostcomplete": | ||||
| 		sess.Desc("completeness") | ||||
| 	case "leastissues": | ||||
| 		sess.Asc("num_issues") | ||||
| 	case "mostissues": | ||||
| 		sess.Desc("num_issues") | ||||
| 	case "id": | ||||
| 		sess.Asc("id") | ||||
| 	default: | ||||
| 		sess.Asc("deadline_unix").Asc("id") | ||||
| 	} | ||||
| 
 | ||||
| 	miles := make([]*Milestone, 0, opts.PageSize) | ||||
| 	total, err := sess.FindAndCount(&miles) | ||||
| 	return miles, total, err | ||||
| } | ||||
| 
 | ||||
| // GetMilestoneIDsByNames returns a list of milestone ids by given names. | ||||
| // It doesn't filter them by repo, so it could return milestones belonging to different repos. | ||||
| // It's used for filtering issues via indexer, otherwise it would be useless. | ||||
| // Since it could return milestones with the same name, so the length of returned ids could be more than the length of names. | ||||
| func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) { | ||||
| 	var ids []int64 | ||||
| 	return ids, db.GetEngine(ctx).Table("milestone"). | ||||
| 		Where(db.BuildCaseInsensitiveIn("name", names)). | ||||
| 		Cols("id"). | ||||
| 		Find(&ids) | ||||
| } | ||||
| 
 | ||||
| // SearchMilestones search milestones | ||||
| func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) { | ||||
| 	miles := make([]*Milestone, 0, setting.UI.IssuePagingNum) | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) | ||||
| 	if len(keyword) > 0 { | ||||
| 		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) | ||||
| 	} | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) | ||||
| 	} | ||||
| 	if page > 0 { | ||||
| 		sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) | ||||
| 	} | ||||
| 
 | ||||
| 	switch sortType { | ||||
| 	case "furthestduedate": | ||||
| 		sess.Desc("deadline_unix") | ||||
| 	case "leastcomplete": | ||||
| 		sess.Asc("completeness") | ||||
| 	case "mostcomplete": | ||||
| 		sess.Desc("completeness") | ||||
| 	case "leastissues": | ||||
| 		sess.Asc("num_issues") | ||||
| 	case "mostissues": | ||||
| 		sess.Desc("num_issues") | ||||
| 	default: | ||||
| 		sess.Asc("deadline_unix") | ||||
| 	} | ||||
| 	return miles, sess.Find(&miles) | ||||
| } | ||||
| 
 | ||||
| // GetMilestonesByRepoIDs returns a list of milestones of given repositories and status. | ||||
| func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) { | ||||
| 	return SearchMilestones( | ||||
| 		builder.In("repo_id", repoIDs), | ||||
| 		page, | ||||
| 		isClosed, | ||||
| 		sortType, | ||||
| 		"", | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| func (milestones MilestoneList) loadTotalTrackedTimes(ctx context.Context) error { | ||||
| 	type totalTimesByMilestone struct { | ||||
| 		MilestoneID int64 | ||||
| 		Time        int64 | ||||
| 	} | ||||
| 	if len(milestones) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	trackedTimes := make(map[int64]int64, len(milestones)) | ||||
| 
 | ||||
| 	// Get total tracked time by milestone_id | ||||
| 	rows, err := db.GetEngine(ctx).Table("issue"). | ||||
| 		Join("INNER", "milestone", "issue.milestone_id = milestone.id"). | ||||
| 		Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id"). | ||||
| 		Where("tracked_time.deleted = ?", false). | ||||
| 		Select("milestone_id, sum(time) as time"). | ||||
| 		In("milestone_id", milestones.getMilestoneIDs()). | ||||
| 		GroupBy("milestone_id"). | ||||
| 		Rows(new(totalTimesByMilestone)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	defer rows.Close() | ||||
| 
 | ||||
| 	for rows.Next() { | ||||
| 		var totalTime totalTimesByMilestone | ||||
| 		err = rows.Scan(&totalTime) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		trackedTimes[totalTime.MilestoneID] = totalTime.Time | ||||
| 	} | ||||
| 
 | ||||
| 	for _, milestone := range milestones { | ||||
| 		milestone.TotalTrackedTime = trackedTimes[milestone.ID] | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request | ||||
| func (milestones MilestoneList) LoadTotalTrackedTimes() error { | ||||
| 	return milestones.loadTotalTrackedTimes(db.DefaultContext) | ||||
| } | ||||
| 
 | ||||
| // CountMilestones returns number of milestones in given repository with other options | ||||
| func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) { | ||||
| 	return db.GetEngine(ctx). | ||||
| 		Where(opts.toCond()). | ||||
| 		Count(new(Milestone)) | ||||
| } | ||||
| 
 | ||||
| // CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options` | ||||
| func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) { | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) | ||||
| 	} | ||||
| 
 | ||||
| 	countsSlice := make([]*struct { | ||||
| 		RepoID int64 | ||||
| 		Count  int64 | ||||
| 	}, 0, 10) | ||||
| 	if err := sess.GroupBy("repo_id"). | ||||
| 		Select("repo_id AS repo_id, COUNT(*) AS count"). | ||||
| 		Table("milestone"). | ||||
| 		Find(&countsSlice); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	countMap := make(map[int64]int64, len(countsSlice)) | ||||
| 	for _, c := range countsSlice { | ||||
| 		countMap[c.RepoID] = c.Count | ||||
| 	} | ||||
| 	return countMap, nil | ||||
| } | ||||
| 
 | ||||
| // CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options` | ||||
| func CountMilestonesByRepoCondAndKw(repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) { | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) | ||||
| 	if len(keyword) > 0 { | ||||
| 		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) | ||||
| 	} | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) | ||||
| 	} | ||||
| 
 | ||||
| 	countsSlice := make([]*struct { | ||||
| 		RepoID int64 | ||||
| 		Count  int64 | ||||
| 	}, 0, 10) | ||||
| 	if err := sess.GroupBy("repo_id"). | ||||
| 		Select("repo_id AS repo_id, COUNT(*) AS count"). | ||||
| 		Table("milestone"). | ||||
| 		Find(&countsSlice); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	countMap := make(map[int64]int64, len(countsSlice)) | ||||
| 	for _, c := range countsSlice { | ||||
| 		countMap[c.RepoID] = c.Count | ||||
| 	} | ||||
| 	return countMap, nil | ||||
| } | ||||
| 
 | ||||
| // MilestonesStats represents milestone statistic information. | ||||
| type MilestonesStats struct { | ||||
| 	OpenCount, ClosedCount int64 | ||||
| } | ||||
| 
 | ||||
| // Total returns the total counts of milestones | ||||
| func (m MilestonesStats) Total() int64 { | ||||
| 	return m.OpenCount + m.ClosedCount | ||||
| } | ||||
| 
 | ||||
| // GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions. | ||||
| func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) { | ||||
| 	var err error | ||||
| 	stats := &MilestonesStats{} | ||||
| 
 | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) | ||||
| 	} | ||||
| 	stats.OpenCount, err = sess.Count(new(Milestone)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) | ||||
| 	} | ||||
| 	stats.ClosedCount, err = sess.Count(new(Milestone)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return stats, nil | ||||
| } | ||||
| 
 | ||||
| // GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword. | ||||
| func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*MilestonesStats, error) { | ||||
| 	var err error | ||||
| 	stats := &MilestonesStats{} | ||||
| 
 | ||||
| 	sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) | ||||
| 	if len(keyword) > 0 { | ||||
| 		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) | ||||
| 	} | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) | ||||
| 	} | ||||
| 	stats.OpenCount, err = sess.Count(new(Milestone)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) | ||||
| 	if len(keyword) > 0 { | ||||
| 		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) | ||||
| 	} | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) | ||||
| 	} | ||||
| 	stats.ClosedCount, err = sess.Count(new(Milestone)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return stats, nil | ||||
| } | ||||
| @ -351,3 +351,21 @@ func TestUpdateMilestoneCounters(t *testing.T) { | ||||
| 	assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) | ||||
| 	unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) | ||||
| } | ||||
| 
 | ||||
| func TestMigrate_InsertMilestones(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	reponame := "repo1" | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) | ||||
| 	name := "milestonetest1" | ||||
| 	ms := &issues_model.Milestone{ | ||||
| 		RepoID: repo.ID, | ||||
| 		Name:   name, | ||||
| 	} | ||||
| 	err := issues_model.InsertMilestones(ms) | ||||
| 	assert.NoError(t, err) | ||||
| 	unittest.AssertExistsAndLoadBean(t, ms) | ||||
| 	repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) | ||||
| 	assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones) | ||||
| 
 | ||||
| 	unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) | ||||
| } | ||||
|  | ||||
| @ -1105,3 +1105,23 @@ func TokenizeCodeOwnersLine(line string) []string { | ||||
| 
 | ||||
| 	return tokens | ||||
| } | ||||
| 
 | ||||
| // InsertPullRequests inserted pull requests | ||||
| func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error { | ||||
| 	ctx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 	for _, pr := range prs { | ||||
| 		if err := insertIssue(ctx, pr.Issue); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		pr.IssueID = pr.Issue.ID | ||||
| 		if _, err := sess.NoAutoTime().Insert(pr); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return committer.Commit() | ||||
| } | ||||
|  | ||||
| @ -8,6 +8,7 @@ import ( | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	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/setting" | ||||
| @ -337,3 +338,31 @@ func TestGetApprovers(t *testing.T) { | ||||
| 	expected := "Reviewed-by: User Five <user5@example.com>\nReviewed-by: User Six <user6@example.com>\n" | ||||
| 	assert.EqualValues(t, expected, approvers) | ||||
| } | ||||
| 
 | ||||
| func TestMigrate_InsertPullRequests(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	reponame := "repo1" | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) | ||||
| 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 
 | ||||
| 	i := &issues_model.Issue{ | ||||
| 		RepoID:   repo.ID, | ||||
| 		Repo:     repo, | ||||
| 		Title:    "title1", | ||||
| 		Content:  "issuecontent1", | ||||
| 		IsPull:   true, | ||||
| 		PosterID: owner.ID, | ||||
| 		Poster:   owner, | ||||
| 	} | ||||
| 
 | ||||
| 	p := &issues_model.PullRequest{ | ||||
| 		Issue: i, | ||||
| 	} | ||||
| 
 | ||||
| 	err := issues_model.InsertPullRequests(db.DefaultContext, p) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	_ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID}) | ||||
| 
 | ||||
| 	unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{}) | ||||
| } | ||||
|  | ||||
| @ -1,196 +0,0 @@ | ||||
| // Copyright 2019 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/modules/container" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| ) | ||||
| 
 | ||||
| // InsertMilestones creates milestones of repository. | ||||
| func InsertMilestones(ms ...*issues_model.Milestone) (err error) { | ||||
| 	if len(ms) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 
 | ||||
| 	// to return the id, so we should not use batch insert | ||||
| 	for _, m := range ms { | ||||
| 		if _, err = sess.NoAutoTime().Insert(m); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return committer.Commit() | ||||
| } | ||||
| 
 | ||||
| // InsertIssues insert issues to database | ||||
| func InsertIssues(issues ...*issues_model.Issue) error { | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 
 | ||||
| 	for _, issue := range issues { | ||||
| 		if err := insertIssue(ctx, issue); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return committer.Commit() | ||||
| } | ||||
| 
 | ||||
| func insertIssue(ctx context.Context, issue *issues_model.Issue) error { | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 	if _, err := sess.NoAutoTime().Insert(issue); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	issueLabels := make([]issues_model.IssueLabel, 0, len(issue.Labels)) | ||||
| 	for _, label := range issue.Labels { | ||||
| 		issueLabels = append(issueLabels, issues_model.IssueLabel{ | ||||
| 			IssueID: issue.ID, | ||||
| 			LabelID: label.ID, | ||||
| 		}) | ||||
| 	} | ||||
| 	if len(issueLabels) > 0 { | ||||
| 		if _, err := sess.Insert(issueLabels); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, reaction := range issue.Reactions { | ||||
| 		reaction.IssueID = issue.ID | ||||
| 	} | ||||
| 
 | ||||
| 	if len(issue.Reactions) > 0 { | ||||
| 		if _, err := sess.Insert(issue.Reactions); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // InsertIssueComments inserts many comments of issues. | ||||
| func InsertIssueComments(comments []*issues_model.Comment) error { | ||||
| 	if len(comments) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	issueIDs := make(container.Set[int64]) | ||||
| 	for _, comment := range comments { | ||||
| 		issueIDs.Add(comment.IssueID) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 	for _, comment := range comments { | ||||
| 		if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		for _, reaction := range comment.Reactions { | ||||
| 			reaction.IssueID = comment.IssueID | ||||
| 			reaction.CommentID = comment.ID | ||||
| 		} | ||||
| 		if len(comment.Reactions) > 0 { | ||||
| 			if err := db.Insert(ctx, comment.Reactions); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for issueID := range issueIDs { | ||||
| 		if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", | ||||
| 			issueID, issues_model.CommentTypeComment, issueID); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return committer.Commit() | ||||
| } | ||||
| 
 | ||||
| // InsertPullRequests inserted pull requests | ||||
| func InsertPullRequests(ctx context.Context, prs ...*issues_model.PullRequest) error { | ||||
| 	ctx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 	for _, pr := range prs { | ||||
| 		if err := insertIssue(ctx, pr.Issue); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		pr.IssueID = pr.Issue.ID | ||||
| 		if _, err := sess.NoAutoTime().Insert(pr); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return committer.Commit() | ||||
| } | ||||
| 
 | ||||
| // InsertReleases migrates release | ||||
| func InsertReleases(rels ...*repo_model.Release) error { | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 
 | ||||
| 	for _, rel := range rels { | ||||
| 		if _, err := sess.NoAutoTime().Insert(rel); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		if len(rel.Attachments) > 0 { | ||||
| 			for i := range rel.Attachments { | ||||
| 				rel.Attachments[i].ReleaseID = rel.ID | ||||
| 			} | ||||
| 
 | ||||
| 			if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return committer.Commit() | ||||
| } | ||||
| 
 | ||||
| // UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID | ||||
| func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error { | ||||
| 	if err := issues_model.UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := issues_model.UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := repo_model.UpdateReleasesMigrationsByType(tp, externalUserID, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := issues_model.UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return issues_model.UpdateReviewsMigrationsByType(tp, externalUserID, userID) | ||||
| } | ||||
| @ -1,145 +0,0 @@ | ||||
| // Copyright 2022 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestMigrate_InsertMilestones(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	reponame := "repo1" | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) | ||||
| 	name := "milestonetest1" | ||||
| 	ms := &issues_model.Milestone{ | ||||
| 		RepoID: repo.ID, | ||||
| 		Name:   name, | ||||
| 	} | ||||
| 	err := InsertMilestones(ms) | ||||
| 	assert.NoError(t, err) | ||||
| 	unittest.AssertExistsAndLoadBean(t, ms) | ||||
| 	repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) | ||||
| 	assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones) | ||||
| 
 | ||||
| 	unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) | ||||
| } | ||||
| 
 | ||||
| func assertCreateIssues(t *testing.T, isPull bool) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	reponame := "repo1" | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) | ||||
| 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) | ||||
| 	milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) | ||||
| 	assert.EqualValues(t, milestone.ID, 1) | ||||
| 	reaction := &issues_model.Reaction{ | ||||
| 		Type:   "heart", | ||||
| 		UserID: owner.ID, | ||||
| 	} | ||||
| 
 | ||||
| 	title := "issuetitle1" | ||||
| 	is := &issues_model.Issue{ | ||||
| 		RepoID:      repo.ID, | ||||
| 		MilestoneID: milestone.ID, | ||||
| 		Repo:        repo, | ||||
| 		Title:       title, | ||||
| 		Content:     "issuecontent1", | ||||
| 		IsPull:      isPull, | ||||
| 		PosterID:    owner.ID, | ||||
| 		Poster:      owner, | ||||
| 		IsClosed:    true, | ||||
| 		Labels:      []*issues_model.Label{label}, | ||||
| 		Reactions:   []*issues_model.Reaction{reaction}, | ||||
| 	} | ||||
| 	err := InsertIssues(is) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title}) | ||||
| 	unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID}) | ||||
| } | ||||
| 
 | ||||
| func TestMigrate_CreateIssuesIsPullFalse(t *testing.T) { | ||||
| 	assertCreateIssues(t, false) | ||||
| } | ||||
| 
 | ||||
| func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) { | ||||
| 	assertCreateIssues(t, true) | ||||
| } | ||||
| 
 | ||||
| func TestMigrate_InsertIssueComments(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) | ||||
| 	_ = issue.LoadRepo(db.DefaultContext) | ||||
| 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}) | ||||
| 	reaction := &issues_model.Reaction{ | ||||
| 		Type:   "heart", | ||||
| 		UserID: owner.ID, | ||||
| 	} | ||||
| 
 | ||||
| 	comment := &issues_model.Comment{ | ||||
| 		PosterID:  owner.ID, | ||||
| 		Poster:    owner, | ||||
| 		IssueID:   issue.ID, | ||||
| 		Issue:     issue, | ||||
| 		Reactions: []*issues_model.Reaction{reaction}, | ||||
| 	} | ||||
| 
 | ||||
| 	err := InsertIssueComments([]*issues_model.Comment{comment}) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) | ||||
| 	assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments) | ||||
| 
 | ||||
| 	unittest.CheckConsistencyFor(t, &issues_model.Issue{}) | ||||
| } | ||||
| 
 | ||||
| func TestMigrate_InsertPullRequests(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	reponame := "repo1" | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) | ||||
| 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 
 | ||||
| 	i := &issues_model.Issue{ | ||||
| 		RepoID:   repo.ID, | ||||
| 		Repo:     repo, | ||||
| 		Title:    "title1", | ||||
| 		Content:  "issuecontent1", | ||||
| 		IsPull:   true, | ||||
| 		PosterID: owner.ID, | ||||
| 		Poster:   owner, | ||||
| 	} | ||||
| 
 | ||||
| 	p := &issues_model.PullRequest{ | ||||
| 		Issue: i, | ||||
| 	} | ||||
| 
 | ||||
| 	err := InsertPullRequests(db.DefaultContext, p) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	_ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID}) | ||||
| 
 | ||||
| 	unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{}) | ||||
| } | ||||
| 
 | ||||
| func TestMigrate_InsertReleases(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 
 | ||||
| 	a := &repo_model.Attachment{ | ||||
| 		UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12", | ||||
| 	} | ||||
| 	r := &repo_model.Release{ | ||||
| 		Attachments: []*repo_model.Attachment{a}, | ||||
| 	} | ||||
| 
 | ||||
| 	err := InsertReleases(r) | ||||
| 	assert.NoError(t, err) | ||||
| } | ||||
| @ -485,12 +485,12 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64 | ||||
| 		} | ||||
| 
 | ||||
| 		// Remove watches from now unaccessible | ||||
| 		if err := reconsiderWatches(ctx, repo, userID); err != nil { | ||||
| 		if err := ReconsiderWatches(ctx, repo, userID); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		// Remove issue assignments from now unaccessible | ||||
| 		if err := reconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { | ||||
| 		if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| @ -523,3 +523,33 @@ func RemoveTeamMember(team *organization.Team, userID int64) error { | ||||
| 	} | ||||
| 	return committer.Commit() | ||||
| } | ||||
| 
 | ||||
| func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error { | ||||
| 	user, err := user_model.GetUserByID(ctx, uid) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}). | ||||
| 		In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). | ||||
| 		Delete(&issues_model.IssueAssignees{}); err != nil { | ||||
| 		return fmt.Errorf("Could not delete assignee[%d] %w", uid, err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error { | ||||
| 	if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Remove all IssueWatches a user has subscribed to in the repository | ||||
| 	return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID) | ||||
| } | ||||
|  | ||||
| @ -552,3 +552,31 @@ func (r *Release) GetExternalName() string { return r.OriginalAuthor } | ||||
| 
 | ||||
| // ExternalID ExternalUserRemappable interface | ||||
| func (r *Release) GetExternalID() int64 { return r.OriginalAuthorID } | ||||
| 
 | ||||
| // InsertReleases migrates release | ||||
| func InsertReleases(rels ...*Release) error { | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 	sess := db.GetEngine(ctx) | ||||
| 
 | ||||
| 	for _, rel := range rels { | ||||
| 		if _, err := sess.NoAutoTime().Insert(rel); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		if len(rel.Attachments) > 0 { | ||||
| 			for i := range rel.Attachments { | ||||
| 				rel.Attachments[i].ReleaseID = rel.ID | ||||
| 			} | ||||
| 
 | ||||
| 			if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return committer.Commit() | ||||
| } | ||||
|  | ||||
							
								
								
									
										26
									
								
								models/repo/release_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								models/repo/release_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestMigrate_InsertReleases(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 
 | ||||
| 	a := &Attachment{ | ||||
| 		UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12", | ||||
| 	} | ||||
| 	r := &Release{ | ||||
| 		Attachments: []*Attachment{a}, | ||||
| 	} | ||||
| 
 | ||||
| 	err := InsertReleases(r) | ||||
| 	assert.NoError(t, err) | ||||
| } | ||||
| @ -1,83 +0,0 @@ | ||||
| // Copyright 2016 The Gogs Authors. All rights reserved. | ||||
| // Copyright 2020 The Gitea Authors. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	access_model "code.gitea.io/gitea/models/perm/access" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 
 | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
| 
 | ||||
| // DeleteCollaboration removes collaboration relation between the user and repository. | ||||
| func DeleteCollaboration(repo *repo_model.Repository, uid int64) (err error) { | ||||
| 	collaboration := &repo_model.Collaboration{ | ||||
| 		RepoID: repo.ID, | ||||
| 		UserID: uid, | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 
 | ||||
| 	if has, err := db.GetEngine(ctx).Delete(collaboration); err != nil || has == 0 { | ||||
| 		return err | ||||
| 	} else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err = reconsiderWatches(ctx, repo, uid); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Unassign a user from any issue (s)he has been assigned to in the repository | ||||
| 	if err := reconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return committer.Commit() | ||||
| } | ||||
| 
 | ||||
| func reconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error { | ||||
| 	user, err := user_model.GetUserByID(ctx, uid) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}). | ||||
| 		In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). | ||||
| 		Delete(&issues_model.IssueAssignees{}); err != nil { | ||||
| 		return fmt.Errorf("Could not delete assignee[%d] %w", uid, err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func reconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error { | ||||
| 	if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Remove all IssueWatches a user has subscribed to in the repository | ||||
| 	return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID) | ||||
| } | ||||
| @ -8,7 +8,6 @@ import ( | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/models/perm" | ||||
| 	access_model "code.gitea.io/gitea/models/perm/access" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| @ -19,6 +18,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||
| 	"code.gitea.io/gitea/services/convert" | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| ) | ||||
| 
 | ||||
| // ListCollaborators list a repository's collaborators | ||||
| @ -228,7 +228,7 @@ func DeleteCollaborator(ctx *context.APIContext) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := models.DeleteCollaboration(ctx.Repo.Repository, collaborator.ID); err != nil { | ||||
| 	if err := repo_service.DeleteCollaboration(ctx.Repo.Repository, collaborator.ID); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @ -7,7 +7,6 @@ import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/models/organization" | ||||
| 	"code.gitea.io/gitea/models/perm" | ||||
| @ -128,7 +127,7 @@ func ChangeCollaborationAccessMode(ctx *context.Context) { | ||||
| 
 | ||||
| // DeleteCollaboration delete a collaboration for a repository | ||||
| func DeleteCollaboration(ctx *context.Context) { | ||||
| 	if err := models.DeleteCollaboration(ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { | ||||
| 	if err := repo_service.DeleteCollaboration(ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { | ||||
| 		ctx.Flash.Error("DeleteCollaboration: " + err.Error()) | ||||
| 	} else { | ||||
| 		ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) | ||||
|  | ||||
| @ -6,8 +6,9 @@ package externalaccount | ||||
| import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/models/auth" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| 
 | ||||
| @ -62,7 +63,7 @@ func LinkAccountToUser(user *user_model.User, gothUser goth.User) error { | ||||
| 	} | ||||
| 
 | ||||
| 	if tp.Name() != "" { | ||||
| 		return models.UpdateMigrationsByType(tp, externalID, user.ID) | ||||
| 		return UpdateMigrationsByType(tp, externalID, user.ID) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| @ -77,3 +78,23 @@ func UpdateExternalUser(user *user_model.User, gothUser goth.User) error { | ||||
| 
 | ||||
| 	return user_model.UpdateExternalUserByExternalID(externalLoginUser) | ||||
| } | ||||
| 
 | ||||
| // UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID | ||||
| func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error { | ||||
| 	if err := issues_model.UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := issues_model.UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := repo_model.UpdateReleasesMigrationsByType(tp, externalUserID, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := issues_model.UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return issues_model.UpdateReviewsMigrationsByType(tp, externalUserID, userID) | ||||
| } | ||||
|  | ||||
| @ -205,7 +205,7 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err | ||||
| 		mss = append(mss, &ms) | ||||
| 	} | ||||
| 
 | ||||
| 	err := models.InsertMilestones(mss...) | ||||
| 	err := issues_model.InsertMilestones(mss...) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @ -350,7 +350,7 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { | ||||
| 		rels = append(rels, &rel) | ||||
| 	} | ||||
| 
 | ||||
| 	return models.InsertReleases(rels...) | ||||
| 	return repo_model.InsertReleases(rels...) | ||||
| } | ||||
| 
 | ||||
| // SyncTags syncs releases with tags in the database | ||||
| @ -430,7 +430,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { | ||||
| 	} | ||||
| 
 | ||||
| 	if len(iss) > 0 { | ||||
| 		if err := models.InsertIssues(iss...); err != nil { | ||||
| 		if err := issues_model.InsertIssues(iss...); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| @ -510,7 +510,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { | ||||
| 	if len(cms) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return models.InsertIssueComments(cms) | ||||
| 	return issues_model.InsertIssueComments(cms) | ||||
| } | ||||
| 
 | ||||
| // CreatePullRequests creates pull requests | ||||
| @ -529,7 +529,7 @@ func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error | ||||
| 
 | ||||
| 		gprs = append(gprs, gpr) | ||||
| 	} | ||||
| 	if err := models.InsertPullRequests(ctx, gprs...); err != nil { | ||||
| 	if err := issues_model.InsertPullRequests(ctx, gprs...); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, pr := range gprs { | ||||
|  | ||||
| @ -6,11 +6,11 @@ package migrations | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/services/externalaccount" | ||||
| ) | ||||
| 
 | ||||
| // UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID | ||||
| @ -62,7 +62,7 @@ func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServ | ||||
| 			default: | ||||
| 			} | ||||
| 			externalUserID := user.ExternalID | ||||
| 			if err := models.UpdateMigrationsByType(tp, externalUserID, user.UserID); err != nil { | ||||
| 			if err := externalaccount.UpdateMigrationsByType(tp, externalUserID, user.UserID); err != nil { | ||||
| 				log.Error("UpdateMigrationsByType type %s external user id %v to local user id %v failed: %v", tp.Name(), user.ExternalID, user.UserID, err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
							
								
								
									
										47
									
								
								services/repository/collaboration.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								services/repository/collaboration.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| // Copyright 2016 The Gogs Authors. All rights reserved. | ||||
| // Copyright 2020 The Gitea Authors. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package repository | ||||
| 
 | ||||
| import ( | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	access_model "code.gitea.io/gitea/models/perm/access" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| ) | ||||
| 
 | ||||
| // DeleteCollaboration removes collaboration relation between the user and repository. | ||||
| func DeleteCollaboration(repo *repo_model.Repository, uid int64) (err error) { | ||||
| 	collaboration := &repo_model.Collaboration{ | ||||
| 		RepoID: repo.ID, | ||||
| 		UserID: uid, | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 
 | ||||
| 	if has, err := db.GetEngine(ctx).Delete(collaboration); err != nil || has == 0 { | ||||
| 		return err | ||||
| 	} else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err = models.ReconsiderWatches(ctx, repo, uid); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Unassign a user from any issue (s)he has been assigned to in the repository | ||||
| 	if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return committer.Commit() | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package models | ||||
| package repository | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user