1// Copyright 2014 The Gogs Authors. All rights reserved.
2// Copyright 2017 The Gitea Authors. All rights reserved.
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file.
5
6package models
7
8import (
9	"context"
10	"fmt"
11	"os"
12	"path"
13	"path/filepath"
14	"sort"
15	"strconv"
16	"strings"
17	"unicode/utf8"
18
19	_ "image/jpeg" // Needed for jpeg support
20
21	admin_model "code.gitea.io/gitea/models/admin"
22	asymkey_model "code.gitea.io/gitea/models/asymkey"
23	"code.gitea.io/gitea/models/db"
24	"code.gitea.io/gitea/models/perm"
25	repo_model "code.gitea.io/gitea/models/repo"
26	"code.gitea.io/gitea/models/unit"
27	user_model "code.gitea.io/gitea/models/user"
28	"code.gitea.io/gitea/models/webhook"
29	"code.gitea.io/gitea/modules/lfs"
30	"code.gitea.io/gitea/modules/log"
31	"code.gitea.io/gitea/modules/options"
32	"code.gitea.io/gitea/modules/setting"
33	"code.gitea.io/gitea/modules/storage"
34	api "code.gitea.io/gitea/modules/structs"
35	"code.gitea.io/gitea/modules/util"
36
37	"xorm.io/builder"
38)
39
40var (
41	// Gitignores contains the gitiginore files
42	Gitignores []string
43
44	// Licenses contains the license files
45	Licenses []string
46
47	// Readmes contains the readme files
48	Readmes []string
49
50	// LabelTemplates contains the label template files and the list of labels for each file
51	LabelTemplates map[string]string
52
53	// ItemsPerPage maximum items per page in forks, watchers and stars of a repo
54	ItemsPerPage = 40
55)
56
57// loadRepoConfig loads the repository config
58func loadRepoConfig() {
59	// Load .gitignore and license files and readme templates.
60	types := []string{"gitignore", "license", "readme", "label"}
61	typeFiles := make([][]string, 4)
62	for i, t := range types {
63		files, err := options.Dir(t)
64		if err != nil {
65			log.Fatal("Failed to get %s files: %v", t, err)
66		}
67		customPath := path.Join(setting.CustomPath, "options", t)
68		isDir, err := util.IsDir(customPath)
69		if err != nil {
70			log.Fatal("Failed to get custom %s files: %v", t, err)
71		}
72		if isDir {
73			customFiles, err := util.StatDir(customPath)
74			if err != nil {
75				log.Fatal("Failed to get custom %s files: %v", t, err)
76			}
77
78			for _, f := range customFiles {
79				if !util.IsStringInSlice(f, files, true) {
80					files = append(files, f)
81				}
82			}
83		}
84		typeFiles[i] = files
85	}
86
87	Gitignores = typeFiles[0]
88	Licenses = typeFiles[1]
89	Readmes = typeFiles[2]
90	LabelTemplatesFiles := typeFiles[3]
91	sort.Strings(Gitignores)
92	sort.Strings(Licenses)
93	sort.Strings(Readmes)
94	sort.Strings(LabelTemplatesFiles)
95
96	// Load label templates
97	LabelTemplates = make(map[string]string)
98	for _, templateFile := range LabelTemplatesFiles {
99		labels, err := LoadLabelsFormatted(templateFile)
100		if err != nil {
101			log.Error("Failed to load labels: %v", err)
102		}
103		LabelTemplates[templateFile] = labels
104	}
105
106	// Filter out invalid names and promote preferred licenses.
107	sortedLicenses := make([]string, 0, len(Licenses))
108	for _, name := range setting.Repository.PreferredLicenses {
109		if util.IsStringInSlice(name, Licenses, true) {
110			sortedLicenses = append(sortedLicenses, name)
111		}
112	}
113	for _, name := range Licenses {
114		if !util.IsStringInSlice(name, setting.Repository.PreferredLicenses, true) {
115			sortedLicenses = append(sortedLicenses, name)
116		}
117	}
118	Licenses = sortedLicenses
119}
120
121// NewRepoContext creates a new repository context
122func NewRepoContext() {
123	loadRepoConfig()
124	unit.LoadUnitConfig()
125
126	admin_model.RemoveAllWithNotice(db.DefaultContext, "Clean up repository temporary data", filepath.Join(setting.AppDataPath, "tmp"))
127}
128
129// CheckRepoUnitUser check whether user could visit the unit of this repository
130func CheckRepoUnitUser(repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool {
131	return checkRepoUnitUser(db.DefaultContext, repo, user, unitType)
132}
133
134func checkRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool {
135	if user.IsAdmin {
136		return true
137	}
138	perm, err := getUserRepoPermission(ctx, repo, user)
139	if err != nil {
140		log.Error("getUserRepoPermission(): %v", err)
141		return false
142	}
143
144	return perm.CanRead(unitType)
145}
146
147func getRepoAssignees(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
148	if err = repo.GetOwner(ctx); err != nil {
149		return nil, err
150	}
151
152	e := db.GetEngine(ctx)
153	userIDs := make([]int64, 0, 10)
154	if err = e.Table("access").
155		Where("repo_id = ? AND mode >= ?", repo.ID, perm.AccessModeWrite).
156		Select("user_id").
157		Find(&userIDs); err != nil {
158		return nil, err
159	}
160
161	additionalUserIDs := make([]int64, 0, 10)
162	if err = e.Table("team_user").
163		Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
164		Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
165		Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode >= ?", repo.ID, perm.AccessModeWrite).
166		Distinct("`team_user`.uid").
167		Select("`team_user`.uid").
168		Find(&additionalUserIDs); err != nil {
169		return nil, err
170	}
171
172	uidMap := map[int64]bool{}
173	i := 0
174	for _, uid := range userIDs {
175		if uidMap[uid] {
176			continue
177		}
178		uidMap[uid] = true
179		userIDs[i] = uid
180		i++
181	}
182	userIDs = userIDs[:i]
183	userIDs = append(userIDs, additionalUserIDs...)
184
185	for _, uid := range additionalUserIDs {
186		if uidMap[uid] {
187			continue
188		}
189		userIDs[i] = uid
190		i++
191	}
192	userIDs = userIDs[:i]
193
194	// Leave a seat for owner itself to append later, but if owner is an organization
195	// and just waste 1 unit is cheaper than re-allocate memory once.
196	users := make([]*user_model.User, 0, len(userIDs)+1)
197	if len(userIDs) > 0 {
198		if err = e.In("id", userIDs).Find(&users); err != nil {
199			return nil, err
200		}
201	}
202	if !repo.Owner.IsOrganization() && !uidMap[repo.OwnerID] {
203		users = append(users, repo.Owner)
204	}
205
206	return users, nil
207}
208
209// GetRepoAssignees returns all users that have write access and can be assigned to issues
210// of the repository,
211func GetRepoAssignees(repo *repo_model.Repository) (_ []*user_model.User, err error) {
212	return getRepoAssignees(db.DefaultContext, repo)
213}
214
215func getReviewers(ctx context.Context, repo *repo_model.Repository, doerID, posterID int64) ([]*user_model.User, error) {
216	// Get the owner of the repository - this often already pre-cached and if so saves complexity for the following queries
217	if err := repo.GetOwner(ctx); err != nil {
218		return nil, err
219	}
220
221	var users []*user_model.User
222	e := db.GetEngine(ctx)
223
224	if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
225		// This a private repository:
226		// Anyone who can read the repository is a requestable reviewer
227		if err := e.
228			SQL("SELECT * FROM `user` WHERE id in (SELECT user_id FROM `access` WHERE repo_id = ? AND mode >= ? AND user_id NOT IN ( ?, ?)) ORDER BY name",
229				repo.ID, perm.AccessModeRead,
230				doerID, posterID).
231			Find(&users); err != nil {
232			return nil, err
233		}
234
235		return users, nil
236	}
237
238	// This is a "public" repository:
239	// Any user that has read access, is a watcher or organization member can be requested to review
240	if err := e.
241		SQL("SELECT * FROM `user` WHERE id IN ( "+
242			"SELECT user_id FROM `access` WHERE repo_id = ? AND mode >= ? "+
243			"UNION "+
244			"SELECT user_id FROM `watch` WHERE repo_id = ? AND mode IN (?, ?) "+
245			"UNION "+
246			"SELECT uid AS user_id FROM `org_user` WHERE org_id = ? "+
247			") AND id NOT IN (?, ?) ORDER BY name",
248			repo.ID, perm.AccessModeRead,
249			repo.ID, repo_model.WatchModeNormal, repo_model.WatchModeAuto,
250			repo.OwnerID,
251			doerID, posterID).
252		Find(&users); err != nil {
253		return nil, err
254	}
255
256	return users, nil
257}
258
259// GetReviewers get all users can be requested to review:
260// * for private repositories this returns all users that have read access or higher to the repository.
261// * for public repositories this returns all users that have read access or higher to the repository,
262// all repo watchers and all organization members.
263// TODO: may be we should have a busy choice for users to block review request to them.
264func GetReviewers(repo *repo_model.Repository, doerID, posterID int64) ([]*user_model.User, error) {
265	return getReviewers(db.DefaultContext, repo, doerID, posterID)
266}
267
268// GetReviewerTeams get all teams can be requested to review
269func GetReviewerTeams(repo *repo_model.Repository) ([]*Team, error) {
270	if err := repo.GetOwner(db.DefaultContext); err != nil {
271		return nil, err
272	}
273	if !repo.Owner.IsOrganization() {
274		return nil, nil
275	}
276
277	teams, err := GetTeamsWithAccessToRepo(repo.OwnerID, repo.ID, perm.AccessModeRead)
278	if err != nil {
279		return nil, err
280	}
281
282	return teams, err
283}
284
285func updateRepoSize(e db.Engine, repo *repo_model.Repository) error {
286	size, err := util.GetDirectorySize(repo.RepoPath())
287	if err != nil {
288		return fmt.Errorf("updateSize: %v", err)
289	}
290
291	lfsSize, err := e.Where("repository_id = ?", repo.ID).SumInt(new(LFSMetaObject), "size")
292	if err != nil {
293		return fmt.Errorf("updateSize: GetLFSMetaObjects: %v", err)
294	}
295
296	repo.Size = size + lfsSize
297	_, err = e.ID(repo.ID).Cols("size").NoAutoTime().Update(repo)
298	return err
299}
300
301// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize
302func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error {
303	return updateRepoSize(db.GetEngine(ctx), repo)
304}
305
306// CanUserForkRepo returns true if specified user can fork repository.
307func CanUserForkRepo(user *user_model.User, repo *repo_model.Repository) (bool, error) {
308	if user == nil {
309		return false, nil
310	}
311	if repo.OwnerID != user.ID && !repo_model.HasForkedRepo(user.ID, repo.ID) {
312		return true, nil
313	}
314	ownedOrgs, err := GetOrgsCanCreateRepoByUserID(user.ID)
315	if err != nil {
316		return false, err
317	}
318	for _, org := range ownedOrgs {
319		if repo.OwnerID != org.ID && !repo_model.HasForkedRepo(org.ID, repo.ID) {
320			return true, nil
321		}
322	}
323	return false, nil
324}
325
326// FindUserOrgForks returns the forked repositories for one user from a repository
327func FindUserOrgForks(repoID, userID int64) ([]*repo_model.Repository, error) {
328	var cond builder.Cond = builder.And(
329		builder.Eq{"fork_id": repoID},
330		builder.In("owner_id",
331			builder.Select("org_id").
332				From("org_user").
333				Where(builder.Eq{"uid": userID}),
334		),
335	)
336
337	var repos []*repo_model.Repository
338	return repos, db.GetEngine(db.DefaultContext).Table("repository").Where(cond).Find(&repos)
339}
340
341// GetForksByUserAndOrgs return forked repos of the user and owned orgs
342func GetForksByUserAndOrgs(user *user_model.User, repo *repo_model.Repository) ([]*repo_model.Repository, error) {
343	var repoList []*repo_model.Repository
344	if user == nil {
345		return repoList, nil
346	}
347	forkedRepo, err := repo_model.GetUserFork(repo.ID, user.ID)
348	if err != nil {
349		return repoList, err
350	}
351	if forkedRepo != nil {
352		repoList = append(repoList, forkedRepo)
353	}
354	orgForks, err := FindUserOrgForks(repo.ID, user.ID)
355	if err != nil {
356		return nil, err
357	}
358	repoList = append(repoList, orgForks...)
359	return repoList, nil
360}
361
362// CanUserDelete returns true if user could delete the repository
363func CanUserDelete(repo *repo_model.Repository, user *user_model.User) (bool, error) {
364	if user.IsAdmin || user.ID == repo.OwnerID {
365		return true, nil
366	}
367
368	if err := repo.GetOwner(db.DefaultContext); err != nil {
369		return false, err
370	}
371
372	if repo.Owner.IsOrganization() {
373		isOwner, err := OrgFromUser(repo.Owner).IsOwnedBy(user.ID)
374		if err != nil {
375			return false, err
376		} else if isOwner {
377			return true, nil
378		}
379	}
380
381	return false, nil
382}
383
384// getUsersWithAccessMode returns users that have at least given access mode to the repository.
385func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm.AccessMode) (_ []*user_model.User, err error) {
386	if err = repo.GetOwner(ctx); err != nil {
387		return nil, err
388	}
389
390	e := db.GetEngine(ctx)
391	accesses := make([]*Access, 0, 10)
392	if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
393		return nil, err
394	}
395
396	// Leave a seat for owner itself to append later, but if owner is an organization
397	// and just waste 1 unit is cheaper than re-allocate memory once.
398	users := make([]*user_model.User, 0, len(accesses)+1)
399	if len(accesses) > 0 {
400		userIDs := make([]int64, len(accesses))
401		for i := 0; i < len(accesses); i++ {
402			userIDs[i] = accesses[i].UserID
403		}
404
405		if err = e.In("id", userIDs).Find(&users); err != nil {
406			return nil, err
407		}
408	}
409	if !repo.Owner.IsOrganization() {
410		users = append(users, repo.Owner)
411	}
412
413	return users, nil
414}
415
416// SetRepoReadBy sets repo to be visited by given user.
417func SetRepoReadBy(repoID, userID int64) error {
418	return setRepoNotificationStatusReadIfUnread(db.GetEngine(db.DefaultContext), userID, repoID)
419}
420
421// CreateRepoOptions contains the create repository options
422type CreateRepoOptions struct {
423	Name           string
424	Description    string
425	OriginalURL    string
426	GitServiceType api.GitServiceType
427	Gitignores     string
428	IssueLabels    string
429	License        string
430	Readme         string
431	DefaultBranch  string
432	IsPrivate      bool
433	IsMirror       bool
434	IsTemplate     bool
435	AutoInit       bool
436	Status         repo_model.RepositoryStatus
437	TrustModel     repo_model.TrustModelType
438	MirrorInterval string
439}
440
441// GetRepoInitFile returns repository init files
442func GetRepoInitFile(tp, name string) ([]byte, error) {
443	cleanedName := strings.TrimLeft(path.Clean("/"+name), "/")
444	relPath := path.Join("options", tp, cleanedName)
445
446	// Use custom file when available.
447	customPath := path.Join(setting.CustomPath, relPath)
448	isFile, err := util.IsFile(customPath)
449	if err != nil {
450		log.Error("Unable to check if %s is a file. Error: %v", customPath, err)
451	}
452	if isFile {
453		return os.ReadFile(customPath)
454	}
455
456	switch tp {
457	case "readme":
458		return options.Readme(cleanedName)
459	case "gitignore":
460		return options.Gitignore(cleanedName)
461	case "license":
462		return options.License(cleanedName)
463	case "label":
464		return options.Labels(cleanedName)
465	default:
466		return []byte{}, fmt.Errorf("Invalid init file type")
467	}
468}
469
470// CreateRepository creates a repository for the user/organization.
471func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt bool) (err error) {
472	if err = repo_model.IsUsableRepoName(repo.Name); err != nil {
473		return err
474	}
475
476	has, err := repo_model.IsRepositoryExistCtx(ctx, u, repo.Name)
477	if err != nil {
478		return fmt.Errorf("IsRepositoryExist: %v", err)
479	} else if has {
480		return repo_model.ErrRepoAlreadyExist{
481			Uname: u.Name,
482			Name:  repo.Name,
483		}
484	}
485
486	repoPath := repo_model.RepoPath(u.Name, repo.Name)
487	isExist, err := util.IsExist(repoPath)
488	if err != nil {
489		log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
490		return err
491	}
492	if !overwriteOrAdopt && isExist {
493		log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath)
494		return repo_model.ErrRepoFilesAlreadyExist{
495			Uname: u.Name,
496			Name:  repo.Name,
497		}
498	}
499
500	if err = db.Insert(ctx, repo); err != nil {
501		return err
502	}
503	if err = repo_model.DeleteRedirect(ctx, u.ID, repo.Name); err != nil {
504		return err
505	}
506
507	// insert units for repo
508	units := make([]repo_model.RepoUnit, 0, len(unit.DefaultRepoUnits))
509	for _, tp := range unit.DefaultRepoUnits {
510		if tp == unit.TypeIssues {
511			units = append(units, repo_model.RepoUnit{
512				RepoID: repo.ID,
513				Type:   tp,
514				Config: &repo_model.IssuesConfig{
515					EnableTimetracker:                setting.Service.DefaultEnableTimetracking,
516					AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
517					EnableDependencies:               setting.Service.DefaultEnableDependencies,
518				},
519			})
520		} else if tp == unit.TypePullRequests {
521			units = append(units, repo_model.RepoUnit{
522				RepoID: repo.ID,
523				Type:   tp,
524				Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyleMerge},
525			})
526		} else {
527			units = append(units, repo_model.RepoUnit{
528				RepoID: repo.ID,
529				Type:   tp,
530			})
531		}
532	}
533
534	if err = db.Insert(ctx, units); err != nil {
535		return err
536	}
537
538	// Remember visibility preference.
539	u.LastRepoVisibility = repo.IsPrivate
540	if err = user_model.UpdateUserColsEngine(db.GetEngine(ctx), u, "last_repo_visibility"); err != nil {
541		return fmt.Errorf("updateUser: %v", err)
542	}
543
544	if _, err = db.GetEngine(ctx).Incr("num_repos").ID(u.ID).Update(new(user_model.User)); err != nil {
545		return fmt.Errorf("increment user total_repos: %v", err)
546	}
547	u.NumRepos++
548
549	// Give access to all members in teams with access to all repositories.
550	if u.IsOrganization() {
551		teams, err := OrgFromUser(u).loadTeams(db.GetEngine(ctx))
552		if err != nil {
553			return fmt.Errorf("loadTeams: %v", err)
554		}
555		for _, t := range teams {
556			if t.IncludesAllRepositories {
557				if err := t.addRepository(ctx, repo); err != nil {
558					return fmt.Errorf("addRepository: %v", err)
559				}
560			}
561		}
562
563		if isAdmin, err := isUserRepoAdmin(db.GetEngine(ctx), repo, doer); err != nil {
564			return fmt.Errorf("isUserRepoAdmin: %v", err)
565		} else if !isAdmin {
566			// Make creator repo admin if it wasn't assigned automatically
567			if err = addCollaborator(ctx, repo, doer); err != nil {
568				return fmt.Errorf("AddCollaborator: %v", err)
569			}
570			if err = changeCollaborationAccessMode(db.GetEngine(ctx), repo, doer.ID, perm.AccessModeAdmin); err != nil {
571				return fmt.Errorf("ChangeCollaborationAccessMode: %v", err)
572			}
573		}
574	} else if err = recalculateAccesses(ctx, repo); err != nil {
575		// Organization automatically called this in addRepository method.
576		return fmt.Errorf("recalculateAccesses: %v", err)
577	}
578
579	if setting.Service.AutoWatchNewRepos {
580		if err = repo_model.WatchRepoCtx(ctx, doer.ID, repo.ID, true); err != nil {
581			return fmt.Errorf("watchRepo: %v", err)
582		}
583	}
584
585	if err = webhook.CopyDefaultWebhooksToRepo(ctx, repo.ID); err != nil {
586		return fmt.Errorf("copyDefaultWebhooksToRepo: %v", err)
587	}
588
589	return nil
590}
591
592// CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon...
593func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error {
594	if err := repo.GetOwner(ctx); err != nil {
595		return err
596	}
597
598	// Create/Remove git-daemon-export-ok for git-daemon...
599	daemonExportFile := path.Join(repo.RepoPath(), `git-daemon-export-ok`)
600
601	isExist, err := util.IsExist(daemonExportFile)
602	if err != nil {
603		log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
604		return err
605	}
606
607	isPublic := !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePublic
608	if !isPublic && isExist {
609		if err = util.Remove(daemonExportFile); err != nil {
610			log.Error("Failed to remove %s: %v", daemonExportFile, err)
611		}
612	} else if isPublic && !isExist {
613		if f, err := os.Create(daemonExportFile); err != nil {
614			log.Error("Failed to create %s: %v", daemonExportFile, err)
615		} else {
616			f.Close()
617		}
618	}
619
620	return nil
621}
622
623// IncrementRepoForkNum increment repository fork number
624func IncrementRepoForkNum(ctx context.Context, repoID int64) error {
625	_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID)
626	return err
627}
628
629// DecrementRepoForkNum decrement repository fork number
630func DecrementRepoForkNum(ctx context.Context, repoID int64) error {
631	_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repoID)
632	return err
633}
634
635func updateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) {
636	repo.LowerName = strings.ToLower(repo.Name)
637
638	if utf8.RuneCountInString(repo.Description) > 255 {
639		repo.Description = string([]rune(repo.Description)[:255])
640	}
641	if utf8.RuneCountInString(repo.Website) > 255 {
642		repo.Website = string([]rune(repo.Website)[:255])
643	}
644
645	e := db.GetEngine(ctx)
646
647	if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil {
648		return fmt.Errorf("update: %v", err)
649	}
650
651	if err = updateRepoSize(e, repo); err != nil {
652		log.Error("Failed to update size for repository: %v", err)
653	}
654
655	if visibilityChanged {
656		if err = repo.GetOwner(ctx); err != nil {
657			return fmt.Errorf("getOwner: %v", err)
658		}
659		if repo.Owner.IsOrganization() {
660			// Organization repository need to recalculate access table when visibility is changed.
661			if err = recalculateTeamAccesses(ctx, repo, 0); err != nil {
662				return fmt.Errorf("recalculateTeamAccesses: %v", err)
663			}
664		}
665
666		// If repo has become private, we need to set its actions to private.
667		if repo.IsPrivate {
668			_, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&Action{
669				IsPrivate: true,
670			})
671			if err != nil {
672				return err
673			}
674		}
675
676		// Create/Remove git-daemon-export-ok for git-daemon...
677		if err := CheckDaemonExportOK(db.WithEngine(ctx, e), repo); err != nil {
678			return err
679		}
680
681		forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID)
682		if err != nil {
683			return fmt.Errorf("getRepositoriesByForkID: %v", err)
684		}
685		for i := range forkRepos {
686			forkRepos[i].IsPrivate = repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate
687			if err = updateRepository(ctx, forkRepos[i], true); err != nil {
688				return fmt.Errorf("updateRepository[%d]: %v", forkRepos[i].ID, err)
689			}
690		}
691	}
692
693	return nil
694}
695
696// UpdateRepositoryCtx updates a repository with db context
697func UpdateRepositoryCtx(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) error {
698	return updateRepository(ctx, repo, visibilityChanged)
699}
700
701// UpdateRepository updates a repository
702func UpdateRepository(repo *repo_model.Repository, visibilityChanged bool) (err error) {
703	ctx, committer, err := db.TxContext()
704	if err != nil {
705		return err
706	}
707	defer committer.Close()
708
709	if err = updateRepository(ctx, repo, visibilityChanged); err != nil {
710		return fmt.Errorf("updateRepository: %v", err)
711	}
712
713	return committer.Commit()
714}
715
716// DeleteRepository deletes a repository for a user or organization.
717// make sure if you call this func to close open sessions (sqlite will otherwise get a deadlock)
718func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
719	ctx, committer, err := db.TxContext()
720	if err != nil {
721		return err
722	}
723	defer committer.Close()
724	sess := db.GetEngine(ctx)
725
726	// In case is a organization.
727	org, err := user_model.GetUserByIDEngine(sess, uid)
728	if err != nil {
729		return err
730	}
731
732	repo := &repo_model.Repository{OwnerID: uid}
733	has, err := sess.ID(repoID).Get(repo)
734	if err != nil {
735		return err
736	} else if !has {
737		return repo_model.ErrRepoNotExist{
738			ID:        repoID,
739			UID:       uid,
740			OwnerName: "",
741			Name:      "",
742		}
743	}
744
745	// Delete Deploy Keys
746	deployKeys, err := asymkey_model.ListDeployKeys(ctx, &asymkey_model.ListDeployKeysOptions{RepoID: repoID})
747	if err != nil {
748		return fmt.Errorf("listDeployKeys: %v", err)
749	}
750	var needRewriteKeysFile = len(deployKeys) > 0
751	for _, dKey := range deployKeys {
752		if err := DeleteDeployKey(ctx, doer, dKey.ID); err != nil {
753			return fmt.Errorf("deleteDeployKeys: %v", err)
754		}
755	}
756
757	if cnt, err := sess.ID(repoID).Delete(&repo_model.Repository{}); err != nil {
758		return err
759	} else if cnt != 1 {
760		return repo_model.ErrRepoNotExist{
761			ID:        repoID,
762			UID:       uid,
763			OwnerName: "",
764			Name:      "",
765		}
766	}
767
768	if org.IsOrganization() {
769		teams, err := OrgFromUser(org).loadTeams(sess)
770		if err != nil {
771			return err
772		}
773		for _, t := range teams {
774			if !t.hasRepository(sess, repoID) {
775				continue
776			} else if err = t.removeRepository(ctx, repo, false); err != nil {
777				return err
778			}
779		}
780	}
781
782	attachments := make([]*repo_model.Attachment, 0, 20)
783	if err = sess.Join("INNER", "`release`", "`release`.id = `attachment`.release_id").
784		Where("`release`.repo_id = ?", repoID).
785		Find(&attachments); err != nil {
786		return err
787	}
788	releaseAttachments := make([]string, 0, len(attachments))
789	for i := 0; i < len(attachments); i++ {
790		releaseAttachments = append(releaseAttachments, attachments[i].RelativePath())
791	}
792
793	if _, err := sess.Exec("UPDATE `user` SET num_stars=num_stars-1 WHERE id IN (SELECT `uid` FROM `star` WHERE repo_id = ?)", repo.ID); err != nil {
794		return err
795	}
796
797	if err := deleteBeans(sess,
798		&Access{RepoID: repo.ID},
799		&Action{RepoID: repo.ID},
800		&Collaboration{RepoID: repoID},
801		&Comment{RefRepoID: repoID},
802		&CommitStatus{RepoID: repoID},
803		&DeletedBranch{RepoID: repoID},
804		&webhook.HookTask{RepoID: repoID},
805		&LFSLock{RepoID: repoID},
806		&repo_model.LanguageStat{RepoID: repoID},
807		&Milestone{RepoID: repoID},
808		&repo_model.Mirror{RepoID: repoID},
809		&Notification{RepoID: repoID},
810		&ProtectedBranch{RepoID: repoID},
811		&ProtectedTag{RepoID: repoID},
812		&PullRequest{BaseRepoID: repoID},
813		&repo_model.PushMirror{RepoID: repoID},
814		&Release{RepoID: repoID},
815		&repo_model.RepoIndexerStatus{RepoID: repoID},
816		&repo_model.Redirect{RedirectRepoID: repoID},
817		&repo_model.RepoUnit{RepoID: repoID},
818		&repo_model.Star{RepoID: repoID},
819		&Task{RepoID: repoID},
820		&repo_model.Watch{RepoID: repoID},
821		&webhook.Webhook{RepoID: repoID},
822	); err != nil {
823		return fmt.Errorf("deleteBeans: %v", err)
824	}
825
826	// Delete Labels and related objects
827	if err := deleteLabelsByRepoID(sess, repoID); err != nil {
828		return err
829	}
830
831	// Delete Issues and related objects
832	var attachmentPaths []string
833	if attachmentPaths, err = deleteIssuesByRepoID(sess, repoID); err != nil {
834		return err
835	}
836
837	// Delete issue index
838	if err := db.DeleteResouceIndex(sess, "issue_index", repoID); err != nil {
839		return err
840	}
841
842	if repo.IsFork {
843		if _, err := sess.Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil {
844			return fmt.Errorf("decrease fork count: %v", err)
845		}
846	}
847
848	if _, err := sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", uid); err != nil {
849		return err
850	}
851
852	if len(repo.Topics) > 0 {
853		if err := repo_model.RemoveTopicsFromRepo(ctx, repo.ID); err != nil {
854			return err
855		}
856	}
857
858	projects, _, err := getProjects(sess, ProjectSearchOptions{
859		RepoID: repoID,
860	})
861	if err != nil {
862		return fmt.Errorf("get projects: %v", err)
863	}
864	for i := range projects {
865		if err := deleteProjectByID(sess, projects[i].ID); err != nil {
866			return fmt.Errorf("delete project [%d]: %v", projects[i].ID, err)
867		}
868	}
869
870	// Remove LFS objects
871	var lfsObjects []*LFSMetaObject
872	if err = sess.Where("repository_id=?", repoID).Find(&lfsObjects); err != nil {
873		return err
874	}
875
876	var lfsPaths = make([]string, 0, len(lfsObjects))
877	for _, v := range lfsObjects {
878		count, err := sess.Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: v.Oid}})
879		if err != nil {
880			return err
881		}
882		if count > 1 {
883			continue
884		}
885
886		lfsPaths = append(lfsPaths, v.RelativePath())
887	}
888
889	if _, err := sess.Delete(&LFSMetaObject{RepositoryID: repoID}); err != nil {
890		return err
891	}
892
893	// Remove archives
894	var archives []*repo_model.RepoArchiver
895	if err = sess.Where("repo_id=?", repoID).Find(&archives); err != nil {
896		return err
897	}
898
899	var archivePaths = make([]string, 0, len(archives))
900	for _, v := range archives {
901		p, _ := v.RelativePath()
902		archivePaths = append(archivePaths, p)
903	}
904
905	if _, err := sess.Delete(&repo_model.RepoArchiver{RepoID: repoID}); err != nil {
906		return err
907	}
908
909	if repo.NumForks > 0 {
910		if _, err = sess.Exec("UPDATE `repository` SET fork_id=0,is_fork=? WHERE fork_id=?", false, repo.ID); err != nil {
911			log.Error("reset 'fork_id' and 'is_fork': %v", err)
912		}
913	}
914
915	// Get all attachments with both issue_id and release_id are zero
916	var newAttachments []*repo_model.Attachment
917	if err := sess.Where(builder.Eq{
918		"repo_id":    repo.ID,
919		"issue_id":   0,
920		"release_id": 0,
921	}).Find(&newAttachments); err != nil {
922		return err
923	}
924
925	var newAttachmentPaths = make([]string, 0, len(newAttachments))
926	for _, attach := range newAttachments {
927		newAttachmentPaths = append(newAttachmentPaths, attach.RelativePath())
928	}
929
930	if _, err := sess.Where("repo_id=?", repo.ID).Delete(new(repo_model.Attachment)); err != nil {
931		return err
932	}
933
934	if err = committer.Commit(); err != nil {
935		return err
936	}
937
938	committer.Close()
939
940	if needRewriteKeysFile {
941		if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
942			log.Error("RewriteAllPublicKeys failed: %v", err)
943		}
944	}
945
946	// We should always delete the files after the database transaction succeed. If
947	// we delete the file but the database rollback, the repository will be broken.
948
949	// Remove repository files.
950	repoPath := repo.RepoPath()
951	admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository files", repoPath)
952
953	// Remove wiki files
954	if repo.HasWiki() {
955		admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath())
956	}
957
958	// Remove archives
959	for i := range archivePaths {
960		admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archivePaths[i])
961	}
962
963	// Remove lfs objects
964	for i := range lfsPaths {
965		admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsPaths[i])
966	}
967
968	// Remove issue attachment files.
969	for i := range attachmentPaths {
970		admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachmentPaths[i])
971	}
972
973	// Remove release attachment files.
974	for i := range releaseAttachments {
975		admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachments[i])
976	}
977
978	// Remove attachment with no issue_id and release_id.
979	for i := range newAttachmentPaths {
980		admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", newAttachmentPaths[i])
981	}
982
983	if len(repo.Avatar) > 0 {
984		if err := storage.RepoAvatars.Delete(repo.CustomAvatarRelativePath()); err != nil {
985			return fmt.Errorf("Failed to remove %s: %v", repo.Avatar, err)
986		}
987	}
988
989	return nil
990}
991
992type repoChecker struct {
993	querySQL   func(ctx context.Context) ([]map[string][]byte, error)
994	correctSQL func(ctx context.Context, id int64) error
995	desc       string
996}
997
998func repoStatsCheck(ctx context.Context, checker *repoChecker) {
999	results, err := checker.querySQL(ctx)
1000	if err != nil {
1001		log.Error("Select %s: %v", checker.desc, err)
1002		return
1003	}
1004	for _, result := range results {
1005		id, _ := strconv.ParseInt(string(result["id"]), 10, 64)
1006		select {
1007		case <-ctx.Done():
1008			log.Warn("CheckRepoStats: Cancelled before checking %s for with id=%d", checker.desc, id)
1009			return
1010		default:
1011		}
1012		log.Trace("Updating %s: %d", checker.desc, id)
1013		err = checker.correctSQL(ctx, id)
1014		if err != nil {
1015			log.Error("Update %s[%d]: %v", checker.desc, id, err)
1016		}
1017	}
1018}
1019
1020func StatsCorrectSQL(ctx context.Context, sql string, id int64) error {
1021	_, err := db.GetEngine(ctx).Exec(sql, id, id)
1022	return err
1023}
1024
1025func repoStatsCorrectNumWatches(ctx context.Context, id int64) error {
1026	return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id)
1027}
1028
1029func repoStatsCorrectNumStars(ctx context.Context, id int64) error {
1030	return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id)
1031}
1032
1033func labelStatsCorrectNumIssues(ctx context.Context, id int64) error {
1034	return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id)
1035}
1036
1037func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
1038	_, err := db.GetEngine(ctx).Exec("UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=id) WHERE repo_id=?", id)
1039	return err
1040}
1041
1042func labelStatsCorrectNumClosedIssues(ctx context.Context, id int64) error {
1043	_, err := db.GetEngine(ctx).Exec("UPDATE `label` SET num_closed_issues=(SELECT COUNT(*) FROM `issue_label`,`issue` WHERE `issue_label`.label_id=`label`.id AND `issue_label`.issue_id=`issue`.id AND `issue`.is_closed=?) WHERE `label`.id=?", true, id)
1044	return err
1045}
1046
1047func labelStatsCorrectNumClosedIssuesRepo(ctx context.Context, id int64) error {
1048	_, err := db.GetEngine(ctx).Exec("UPDATE `label` SET num_closed_issues=(SELECT COUNT(*) FROM `issue_label`,`issue` WHERE `issue_label`.label_id=`label`.id AND `issue_label`.issue_id=`issue`.id AND `issue`.is_closed=?) WHERE `label`.repo_id=?", true, id)
1049	return err
1050}
1051
1052var milestoneStatsQueryNumIssues = "SELECT `milestone`.id FROM `milestone` WHERE `milestone`.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id AND `issue`.is_closed=?) OR `milestone`.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id)"
1053
1054func milestoneStatsCorrectNumIssues(ctx context.Context, id int64) error {
1055	return updateMilestoneCounters(ctx, id)
1056}
1057
1058func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
1059	e := db.GetEngine(ctx)
1060	results, err := e.Query(milestoneStatsQueryNumIssues+" AND `milestone`.repo_id = ?", true, id)
1061	if err != nil {
1062		return err
1063	}
1064	for _, result := range results {
1065		id, _ := strconv.ParseInt(string(result["id"]), 10, 64)
1066		err = milestoneStatsCorrectNumIssues(ctx, id)
1067		if err != nil {
1068			return err
1069		}
1070	}
1071	return nil
1072}
1073
1074func userStatsCorrectNumRepos(ctx context.Context, id int64) error {
1075	return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id)
1076}
1077
1078func repoStatsCorrectIssueNumComments(ctx context.Context, id int64) error {
1079	return StatsCorrectSQL(ctx, "UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?", id)
1080}
1081
1082func repoStatsCorrectNumIssues(ctx context.Context, id int64) error {
1083	return repoStatsCorrectNum(ctx, id, false, "num_issues")
1084}
1085
1086func repoStatsCorrectNumPulls(ctx context.Context, id int64) error {
1087	return repoStatsCorrectNum(ctx, id, true, "num_pulls")
1088}
1089
1090func repoStatsCorrectNum(ctx context.Context, id int64, isPull bool, field string) error {
1091	_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET "+field+"=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_pull=?) WHERE id=?", id, isPull, id)
1092	return err
1093}
1094
1095func repoStatsCorrectNumClosedIssues(ctx context.Context, id int64) error {
1096	return repoStatsCorrectNumClosed(ctx, id, false, "num_closed_issues")
1097}
1098
1099func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error {
1100	return repoStatsCorrectNumClosed(ctx, id, true, "num_closed_pulls")
1101}
1102
1103func repoStatsCorrectNumClosed(ctx context.Context, id int64, isPull bool, field string) error {
1104	_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET "+field+"=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_closed=? AND is_pull=?) WHERE id=?", id, true, isPull, id)
1105	return err
1106}
1107
1108func statsQuery(args ...interface{}) func(context.Context) ([]map[string][]byte, error) {
1109	return func(ctx context.Context) ([]map[string][]byte, error) {
1110		return db.GetEngine(ctx).Query(args...)
1111	}
1112}
1113
1114// CheckRepoStats checks the repository stats
1115func CheckRepoStats(ctx context.Context) error {
1116	log.Trace("Doing: CheckRepoStats")
1117
1118	checkers := []*repoChecker{
1119		// Repository.NumWatches
1120		{
1121			statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_watches!=(SELECT COUNT(*) FROM `watch` WHERE repo_id=repo.id AND mode<>2)"),
1122			repoStatsCorrectNumWatches,
1123			"repository count 'num_watches'",
1124		},
1125		// Repository.NumStars
1126		{
1127			statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_stars!=(SELECT COUNT(*) FROM `star` WHERE repo_id=repo.id)"),
1128			repoStatsCorrectNumStars,
1129			"repository count 'num_stars'",
1130		},
1131		// Repository.NumClosedIssues
1132		{
1133			statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", true, false),
1134			repoStatsCorrectNumClosedIssues,
1135			"repository count 'num_closed_issues'",
1136		},
1137		// Repository.NumClosedPulls
1138		{
1139			statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", true, true),
1140			repoStatsCorrectNumClosedPulls,
1141			"repository count 'num_closed_pulls'",
1142		},
1143		// Label.NumIssues
1144		{
1145			statsQuery("SELECT label.id FROM `label` WHERE label.num_issues!=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=label.id)"),
1146			labelStatsCorrectNumIssues,
1147			"label count 'num_issues'",
1148		},
1149		// Label.NumClosedIssues
1150		{
1151			statsQuery("SELECT `label`.id FROM `label` WHERE `label`.num_closed_issues!=(SELECT COUNT(*) FROM `issue_label`,`issue` WHERE `issue_label`.label_id=`label`.id AND `issue_label`.issue_id=`issue`.id AND `issue`.is_closed=?)", true),
1152			labelStatsCorrectNumClosedIssues,
1153			"label count 'num_closed_issues'",
1154		},
1155		// Milestone.Num{,Closed}Issues
1156		{
1157			statsQuery(milestoneStatsQueryNumIssues, true),
1158			milestoneStatsCorrectNumIssues,
1159			"milestone count 'num_closed_issues' and 'num_issues'",
1160		},
1161		// User.NumRepos
1162		{
1163			statsQuery("SELECT `user`.id FROM `user` WHERE `user`.num_repos!=(SELECT COUNT(*) FROM `repository` WHERE owner_id=`user`.id)"),
1164			userStatsCorrectNumRepos,
1165			"user count 'num_repos'",
1166		},
1167		// Issue.NumComments
1168		{
1169			statsQuery("SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)"),
1170			repoStatsCorrectIssueNumComments,
1171			"issue count 'num_comments'",
1172		},
1173	}
1174	for _, checker := range checkers {
1175		select {
1176		case <-ctx.Done():
1177			log.Warn("CheckRepoStats: Cancelled before %s", checker.desc)
1178			return db.ErrCancelledf("before checking %s", checker.desc)
1179		default:
1180			repoStatsCheck(ctx, checker)
1181		}
1182	}
1183
1184	// FIXME: use checker when stop supporting old fork repo format.
1185	// ***** START: Repository.NumForks *****
1186	e := db.GetEngine(ctx)
1187	results, err := e.Query("SELECT repo.id FROM `repository` repo WHERE repo.num_forks!=(SELECT COUNT(*) FROM `repository` WHERE fork_id=repo.id)")
1188	if err != nil {
1189		log.Error("Select repository count 'num_forks': %v", err)
1190	} else {
1191		for _, result := range results {
1192			id, _ := strconv.ParseInt(string(result["id"]), 10, 64)
1193			select {
1194			case <-ctx.Done():
1195				log.Warn("CheckRepoStats: Cancelled")
1196				return db.ErrCancelledf("during repository count 'num_fork' for repo ID %d", id)
1197			default:
1198			}
1199			log.Trace("Updating repository count 'num_forks': %d", id)
1200
1201			repo, err := repo_model.GetRepositoryByID(id)
1202			if err != nil {
1203				log.Error("repo_model.GetRepositoryByID[%d]: %v", id, err)
1204				continue
1205			}
1206
1207			rawResult, err := db.GetEngine(db.DefaultContext).Query("SELECT COUNT(*) FROM `repository` WHERE fork_id=?", repo.ID)
1208			if err != nil {
1209				log.Error("Select count of forks[%d]: %v", repo.ID, err)
1210				continue
1211			}
1212			repo.NumForks = int(parseCountResult(rawResult))
1213
1214			if err = UpdateRepository(repo, false); err != nil {
1215				log.Error("UpdateRepository[%d]: %v", id, err)
1216				continue
1217			}
1218		}
1219	}
1220	// ***** END: Repository.NumForks *****
1221	return nil
1222}
1223
1224func UpdateRepoStats(ctx context.Context, id int64) error {
1225	var err error
1226
1227	for _, f := range []func(ctx context.Context, id int64) error{
1228		repoStatsCorrectNumWatches,
1229		repoStatsCorrectNumStars,
1230		repoStatsCorrectNumIssues,
1231		repoStatsCorrectNumPulls,
1232		repoStatsCorrectNumClosedIssues,
1233		repoStatsCorrectNumClosedPulls,
1234		labelStatsCorrectNumIssuesRepo,
1235		labelStatsCorrectNumClosedIssuesRepo,
1236		milestoneStatsCorrectNumIssuesRepo,
1237	} {
1238		err = f(ctx, id)
1239		if err != nil {
1240			return err
1241		}
1242	}
1243	return nil
1244}
1245
1246func updateUserStarNumbers(users []user_model.User) error {
1247	ctx, committer, err := db.TxContext()
1248	if err != nil {
1249		return err
1250	}
1251	defer committer.Close()
1252
1253	for _, user := range users {
1254		if _, err = db.Exec(ctx, "UPDATE `user` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE uid=?) WHERE id=?", user.ID, user.ID); err != nil {
1255			return err
1256		}
1257	}
1258
1259	return committer.Commit()
1260}
1261
1262// DoctorUserStarNum recalculate Stars number for all user
1263func DoctorUserStarNum() (err error) {
1264	const batchSize = 100
1265
1266	for start := 0; ; start += batchSize {
1267		users := make([]user_model.User, 0, batchSize)
1268		if err = db.GetEngine(db.DefaultContext).Limit(batchSize, start).Where("type = ?", 0).Cols("id").Find(&users); err != nil {
1269			return
1270		}
1271		if len(users) == 0 {
1272			break
1273		}
1274
1275		if err = updateUserStarNumbers(users); err != nil {
1276			return
1277		}
1278	}
1279
1280	log.Debug("recalculate Stars number for all user finished")
1281
1282	return
1283}
1284
1285// LinkedRepository returns the linked repo if any
1286func LinkedRepository(a *repo_model.Attachment) (*repo_model.Repository, unit.Type, error) {
1287	if a.IssueID != 0 {
1288		iss, err := GetIssueByID(a.IssueID)
1289		if err != nil {
1290			return nil, unit.TypeIssues, err
1291		}
1292		repo, err := repo_model.GetRepositoryByID(iss.RepoID)
1293		unitType := unit.TypeIssues
1294		if iss.IsPull {
1295			unitType = unit.TypePullRequests
1296		}
1297		return repo, unitType, err
1298	} else if a.ReleaseID != 0 {
1299		rel, err := GetReleaseByID(a.ReleaseID)
1300		if err != nil {
1301			return nil, unit.TypeReleases, err
1302		}
1303		repo, err := repo_model.GetRepositoryByID(rel.RepoID)
1304		return repo, unit.TypeReleases, err
1305	}
1306	return nil, -1, nil
1307}
1308
1309// DeleteDeployKey delete deploy keys
1310func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error {
1311	key, err := asymkey_model.GetDeployKeyByID(ctx, id)
1312	if err != nil {
1313		if asymkey_model.IsErrDeployKeyNotExist(err) {
1314			return nil
1315		}
1316		return fmt.Errorf("GetDeployKeyByID: %v", err)
1317	}
1318
1319	sess := db.GetEngine(ctx)
1320
1321	// Check if user has access to delete this key.
1322	if !doer.IsAdmin {
1323		repo, err := repo_model.GetRepositoryByIDCtx(ctx, key.RepoID)
1324		if err != nil {
1325			return fmt.Errorf("GetRepositoryByID: %v", err)
1326		}
1327		has, err := isUserRepoAdmin(sess, repo, doer)
1328		if err != nil {
1329			return fmt.Errorf("GetUserRepoPermission: %v", err)
1330		} else if !has {
1331			return asymkey_model.ErrKeyAccessDenied{
1332				UserID: doer.ID,
1333				KeyID:  key.ID,
1334				Note:   "deploy",
1335			}
1336		}
1337	}
1338
1339	if _, err = sess.ID(key.ID).Delete(new(asymkey_model.DeployKey)); err != nil {
1340		return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err)
1341	}
1342
1343	// Check if this is the last reference to same key content.
1344	has, err := sess.
1345		Where("key_id = ?", key.KeyID).
1346		Get(new(asymkey_model.DeployKey))
1347	if err != nil {
1348		return err
1349	} else if !has {
1350		if err = asymkey_model.DeletePublicKeys(ctx, key.KeyID); err != nil {
1351			return err
1352		}
1353	}
1354
1355	return nil
1356}
1357