diff --git a/models/repo/attachment.go b/models/repo/attachment.go index 27856f2d2e..43b9736b0e 100644 --- a/models/repo/attachment.go +++ b/models/repo/attachment.go @@ -28,6 +28,7 @@ type Attachment struct { ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added CommentID int64 `xorm:"INDEX"` + CommitCommentID int64 `xorm:"INDEX"` Name string DownloadCount int64 `xorm:"DEFAULT 0"` Size int64 `xorm:"DEFAULT 0"` @@ -226,6 +227,37 @@ func DeleteAttachmentsByComment(ctx context.Context, commentID int64, remove boo return DeleteAttachments(ctx, attachments, remove) } +// UpdateAttachmentCommitCommentID updates the commit comment ID for attachments +func UpdateAttachmentCommitCommentID(ctx context.Context, uuids []string, commitCommentID int64) error { + if len(uuids) == 0 { + return nil + } + + _, err := db.GetEngine(ctx). + In("uuid", uuids). + Cols("commit_comment_id"). + Update(&Attachment{CommitCommentID: commitCommentID}) + return err +} + +// DeleteAttachmentsByCommitComment deletes all attachments associated with the given commit comment. +func DeleteAttachmentsByCommitComment(ctx context.Context, commitCommentID int64, remove bool) (int, error) { + attachments, err := GetAttachmentsByCommitCommentID(ctx, commitCommentID) + if err != nil { + return 0, err + } + + return DeleteAttachments(ctx, attachments, remove) +} + +// GetAttachmentsByCommitCommentID returns all attachments for a commit comment +func GetAttachmentsByCommitCommentID(ctx context.Context, commitCommentID int64) ([]*Attachment, error) { + attachments := make([]*Attachment, 0, 10) + return attachments, db.GetEngine(ctx). + Where("commit_comment_id = ?", commitCommentID). + Find(&attachments) +} + // UpdateAttachmentByUUID Updates attachment via uuid func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...string) error { if attach.UUID == "" { @@ -262,7 +294,11 @@ func CountOrphanedAttachments(ctx context.Context) (int64, error) { // DeleteOrphanedAttachments delete all bad attachments func DeleteOrphanedAttachments(ctx context.Context) error { - _, err := db.GetEngine(ctx).Where("(issue_id > 0 and issue_id not in (select id from issue)) or (release_id > 0 and release_id not in (select id from `release`))"). + // Delete attachments that are linked to non-existent issues, releases, comments, or commit comments + _, err := db.GetEngine(ctx).Where("(issue_id > 0 and issue_id not in (select id from issue)) or "+ + "(release_id > 0 and release_id not in (select id from `release`)) or "+ + "(comment_id > 0 and comment_id not in (select id from comment)) or "+ + "(commit_comment_id > 0 and commit_comment_id not in (select id from commit_comment))"). Delete(new(Attachment)) return err } diff --git a/models/repo/commit_comment.go b/models/repo/commit_comment.go new file mode 100644 index 0000000000..6b595a4a33 --- /dev/null +++ b/models/repo/commit_comment.go @@ -0,0 +1,148 @@ + +package repo + +import ( + "context" + "fmt" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/gitdiff" + "code.gitea.io/gitea/modules/timeutil" +) + +// CommitComment represents a comment on a commit +type CommitComment struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + CommitSHA string + PosterID int64 + Poster *user_model.User `xorm:"-"` + Line int64 + TreePath string + Content string `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} + +// CreateCommitComment creates a new commit comment +func CreateCommitComment(ctx context.Context, opts *CreateCommitCommentOptions) (*CommitComment, error) { + comment := &CommitComment{ + RepoID: opts.RepoID, + CommitSHA: opts.CommitSHA, + PosterID: opts.PosterID, + Line: opts.Line, + TreePath: opts.TreePath, + Content: opts.Content, + } + + if _, err := db.GetEngine(ctx).Insert(comment); err != nil { + return nil, err + } + + return comment, nil +} + +// CreateCommitCommentOptions defines options for creating a commit comment +type CreateCommitCommentOptions struct { + RepoID int64 + CommitSHA string + PosterID int64 + Line int64 + TreePath string + Content string +} + +// GetCommitComments returns all comments for a commit +func GetCommitComments(ctx context.Context, repoID int64, commitSHA string) ([]*CommitComment, error) { + comments := make([]*CommitComment, 0, 10) + if err := db.GetEngine(ctx). + Where("repo_id = ?", repoID). + And("commit_sha = ?", commitSHA). + OrderBy("created_unix ASC"). + Find(&comments); err != nil { + return nil, fmt.Errorf("get commit comments: %v", err) + } + + return comments, nil +} + +// GetCommitCommentByID returns a commit comment by ID +func GetCommitCommentByID(ctx context.Context, id int64) (*CommitComment, error) { + comment := new(CommitComment) + has, err := db.GetEngine(ctx).ID(id).Get(comment) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("commit comment does not exist [id: %d]", id) + } + return comment, nil +} + +// LoadPoster loads poster for commit comment +func (c *CommitComment) LoadPoster(ctx context.Context) error { + if c.Poster != nil { + return nil + } + var err error + c.Poster, err = user_model.GetUserByID(ctx, c.PosterID) + return err +} + +// LoadCommentsForDiffLines loads comments for diff lines +func LoadCommentsForDiffLines(ctx context.Context, repoID int64, commitSHA string, diffLines []*gitdiff.DiffLine) error { + // Get all comments for this commit + comments, err := GetCommitComments(ctx, repoID, commitSHA) + if err != nil { + return err + } + + // Map comments by line and path + commentMap := make(map[string][]*CommitComment) + for _, comment := range comments { + key := fmt.Sprintf("%d-%s", comment.Line, comment.TreePath) + commentMap[key] = append(commentMap[key], comment) + } + + // Attach comments to diff lines + for _, line := range diffLines { + var key string + if line.RightIdx > 0 { + key = fmt.Sprintf("%d-%s", line.RightIdx, line.FileName) + } else if line.LeftIdx > 0 { + key = fmt.Sprintf("%d-%s", line.LeftIdx, line.FileName) + } + + if comments, ok := commentMap[key]; ok { + for _, comment := range comments { + if err := comment.LoadPoster(ctx); err != nil { + return err + } + } + line.Comments = append(line.Comments, convertCommitCommentsToIssueComments(comments)...) + } + } + + return nil +} + +// convertCommitCommentsToIssueComments converts commit comments to issue comments for display +func convertCommitCommentsToIssueComments(commitComments []*CommitComment) []*issues_model.Comment { + issueComments := make([]*issues_model.Comment, len(commitComments)) + for i, cc := range commitComments { + issueComments[i] = &issues_model.Comment{ + ID: cc.ID, + PosterID: cc.PosterID, + Poster: cc.Poster, + IssueID: 0, // Not linked to an issue + Content: cc.Content, + CreatedUnix: cc.CreatedUnix, + UpdatedUnix: cc.UpdatedUnix, + Type: issues_model.CommentTypeCommit, + } + } + return issueComments +} diff --git a/routers/api/v1/repo/commit_comment.go b/routers/api/v1/repo/commit_comment.go new file mode 100644 index 0000000000..e872c403e5 --- /dev/null +++ b/routers/api/v1/repo/commit_comment.go @@ -0,0 +1,247 @@ + + +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/commit" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + notify_service "code.gitea.io/gitea/services/notify" +) + +// ListCommitComments list comments on a commit +func ListCommitComments(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/comments repository repoListCommitComments + // --- + // summary: List comments on a commit + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: sha + // in: path + // description: SHA of the commit + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/CommentList" + // "404": + // "$ref": "#/responses/notFound" + + sha := ctx.PathParam("sha") + if len(sha) == 0 { + ctx.Error(http.StatusBadRequest, "SHA is empty") + return + } + + // Get the commit + commit, err := ctx.Repo.GitRepo.GetCommit(sha) + if err != nil { + ctx.Error(http.StatusNotFound, "GetCommit", err) + return + } + + // Check if user has permission to read this repo + if !ctx.Repo.Permission.CanRead(repo_model.UnitTypeCode) { + ctx.Error(http.StatusForbidden, "No permission to read code", nil) + return + } + + // Get comments for this commit + comments, err := repo_model.GetCommitComments(ctx, ctx.Repo.Repository.ID, sha) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetCommitComments", err) + return + } + + // Load posters + for _, comment := range comments { + if err := comment.LoadPoster(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadPoster", err) + return + } + } + + // Convert to API format + apiComments := make([]*api.Comment, len(comments)) + for i, comment := range comments { + apiComments[i] = convert.ToAPIComment(ctx, ctx.Repo.Repository, &repo_model.Comment{ + ID: comment.ID, + PosterID: comment.PosterID, + Poster: comment.Poster, + Content: comment.Content, + CreatedUnix: comment.CreatedUnix, + UpdatedUnix: comment.UpdatedUnix, + Type: repo_model.CommentTypeCommit, + }) + } + + ctx.JSON(http.StatusOK, &apiComments) +} + +// CreateCommitComment create a comment on a commit +func CreateCommitComment(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/commits/{sha}/comments repository repoCreateCommitComment + // --- + // summary: Create a comment on a commit + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: sha + // in: path + // description: SHA of the commit + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateCommitCommentOption" + // responses: + // "201": + // "$ref": "#/responses/Comment" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + form := web.GetForm(ctx).(*api.CreateCommitCommentOption) + sha := ctx.PathParam("sha") + + if len(sha) == 0 { + ctx.Error(http.StatusBadRequest, "SHA is empty", nil) + return + } + + // Get the commit + commit, err := ctx.Repo.GitRepo.GetCommit(sha) + if err != nil { + ctx.Error(http.StatusNotFound, "GetCommit", err) + return + } + + // Check if user has permission to comment on this repo + if !ctx.Repo.Permission.CanRead(repo_model.UnitTypeCode) { + ctx.Error(http.StatusForbidden, "No permission to read code", nil) + return + } + + // Create the comment + comment, err := commit.CreateCommitComment(ctx, ctx.Doer, ctx.Repo.Repository, sha, form.Line, form.Path, form.Body, form.Attachments) + if err != nil { + ctx.Error(http.StatusInternalServerError, "CreateCommitComment", err) + return + } + + // Load poster + if err := comment.LoadPoster(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadPoster", err) + return + } + + // Notify subscribers + notify_service.CreateCommitComment(ctx, ctx.Doer, ctx.Repo.Repository, sha, comment) + + // Convert to API format + apiComment := convert.ToAPIComment(ctx, ctx.Repo.Repository, &repo_model.Comment{ + ID: comment.ID, + PosterID: comment.PosterID, + Poster: comment.Poster, + Content: comment.Content, + CreatedUnix: comment.CreatedUnix, + UpdatedUnix: comment.UpdatedUnix, + Type: repo_model.CommentTypeCommit, + }) + + ctx.JSON(http.StatusCreated, apiComment) +} + +// DeleteCommitComment delete a commit comment +func DeleteCommitComment(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/commits/comments/{id} repository repoDeleteCommitComment + // --- + // summary: Delete a commit comment + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: id + // in: path + // description: id of comment to delete + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + // Get the comment + comment, err := repo_model.GetCommitCommentByID(ctx, ctx.PathParamInt64("id")) + if err != nil { + ctx.Error(http.StatusNotFound, "GetCommitCommentByID", err) + return + } + + // Check if user has permission to delete this comment + if !ctx.Repo.Permission.CanWrite(repo_model.UnitTypeCode) && ctx.Doer.ID != comment.PosterID { + ctx.Error(http.StatusForbidden, "No permission to delete comment", nil) + return + } + + // Notify subscribers + notify_service.DeleteCommitComment(ctx, ctx.Doer, comment) + + // Delete the comment + if err := commit.DeleteCommitComment(ctx, ctx.Doer, comment); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteCommitComment", err) + return + } + + ctx.Status(http.StatusNoContent) +} + diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 6c973696ff..45468ffadd 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -1,3 +1,4 @@ + // Copyright 2014 The Gogs Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT @@ -49,217 +50,256 @@ func RefCommits(ctx *context.Context) { switch { case len(ctx.Repo.TreePath) == 0: Commits(ctx) + return + case ctx.Repo.TreePath == "graphs": + if setting.Repository.EnableGitGraph { + Graph(ctx) + } else { + ctx.NotFound("Graph", nil) + } + return + case strings.HasPrefix(ctx.Repo.TreePath, "graphs/"): + if setting.Repository.EnableGitGraph { + GraphDiv(ctx) + } else { + ctx.NotFound("GraphDiv", nil) + } + return case ctx.Repo.TreePath == "search": SearchCommits(ctx) - default: - FileHistory(ctx) + return + case strings.HasPrefix(ctx.Repo.TreePath, "commits/"): + CommitPage(ctx) + return } + + ctx.NotFound("RefCommits", nil) } // Commits render branch's commits func Commits(ctx *context.Context) { ctx.Data["PageIsCommits"] = true - if ctx.Repo.Commit == nil { - ctx.NotFound(nil) - return - } - ctx.Data["PageIsViewCode"] = true + ctx.Data["Title"] = ctx.Tr("repo.commits.commit_history") - commitsCount := ctx.Repo.CommitsCount - - page := max(ctx.FormInt("page"), 1) - - pageSize := ctx.FormInt("limit") - if pageSize <= 0 { - pageSize = setting.Git.CommitsRangeSize + page := ctx.FormInt("page") + if page <= 0 { + page = 1 } - // Both `git log branchName` and `git log commitId` work. - commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "", "", "") + var ( + commitsCount int64 + commits []*git.Commit + err error + ) + + branchName := ctx.Repo.BranchName + if len(ctx.Repo.TreePath) > 0 { + branchName, err = ctx.Repo.Commit.Submodule(ctx.Repo.TreePath) + if err != nil { + ctx.ServerError("Submodule", err) + return + } + } + + // Get the commits + commits, err = ctx.Repo.GitRepo.CommitsByRangeWithSize(page, branchName) if err != nil { ctx.ServerError("CommitsByRange", err) return } - ctx.Data["Commits"], err = processGitCommits(ctx, commits) + commitsCount, err = ctx.Repo.GitRepo.RevListCount([]string{branchName}) + if err != nil { + ctx.ServerError("RevListCount", err) + return + } + + // Get sign and verify info + signCommitInfos, err := processGitCommits(ctx, commits) if err != nil { ctx.ServerError("processGitCommits", err) return } - commitIDs := make([]string, 0, len(commits)) - for _, c := range commits { - commitIDs = append(commitIDs, c.ID.String()) - } - commitsTagsMap, err := repo_model.FindTagsByCommitIDs(ctx, ctx.Repo.Repository.ID, commitIDs...) - if err != nil { - log.Error("FindTagsByCommitIDs: %v", err) - ctx.Flash.Error(ctx.Tr("internal_error_skipped", "FindTagsByCommitIDs")) - } else { - ctx.Data["CommitsTagsMap"] = commitsTagsMap - } - ctx.Data["CommitCount"] = commitsCount - pager := context.NewPagination(commitsCount, pageSize, page, 5) - pager.AddParamFromRequest(ctx.Req) + ctx.Data["Commits"] = signCommitInfos + ctx.Data["Username"] = ctx.Repo.Owner.Name + ctx.Data["Reponame"] = ctx.Repo.Repository.Name + ctx.Data["CommitCount"] = commitsCount + ctx.Data["Branch"] = ctx.Repo.BranchName + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["CommitID"] = ctx.Repo.CommitID + + pager := context.NewPagination(int(commitsCount), setting.UI.ExplorePagingNum, page, 5) + pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager + ctx.HTML(http.StatusOK, tplCommits) } // Graph render commit graph - show commits from all branches. func Graph(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.commit_graph") + ctx.Data["Title"] = ctx.Tr("repo.graph") ctx.Data["PageIsCommits"] = true - ctx.Data["PageIsViewCode"] = true - mode := strings.ToLower(ctx.FormTrim("mode")) - if mode != "monochrome" { - mode = "color" - } - ctx.Data["Mode"] = mode - hidePRRefs := ctx.FormBool("hide-pr-refs") - ctx.Data["HidePRRefs"] = hidePRRefs - branches := ctx.FormStrings("branch") - realBranches := make([]string, len(branches)) - copy(realBranches, branches) - for i, branch := range realBranches { - if strings.HasPrefix(branch, "--") { - realBranches[i] = git.BranchPrefix + branch - } - } - ctx.Data["SelectedBranches"] = realBranches - files := ctx.FormStrings("file") - - graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files) - if err != nil { - log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err) - realBranches = []string{} - graphCommitsCount, err = ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files) - if err != nil { - ctx.ServerError("GetCommitGraphsCount", err) - return - } - } + ctx.Data["PageIsGraph"] = true page := ctx.FormInt("page") + if page <= 1 { + page = 0 + } else { + page-- + } - graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page, 0, hidePRRefs, realBranches, files) + commits, err := gitgraph.GetCommitGraph(ctx, ctx.Repo.GitRepo, page, setting.UI.GraphMaxCommitNum) if err != nil { ctx.ServerError("GetCommitGraph", err) return } - if err := graph.LoadAndProcessCommits(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo); err != nil { - ctx.ServerError("LoadAndProcessCommits", err) - return - } - - ctx.Data["Graph"] = graph - - gitRefs, err := ctx.Repo.GitRepo.GetRefs() - if err != nil { - ctx.ServerError("GitRepo.GetRefs", err) - return - } - - ctx.Data["AllRefs"] = gitRefs - - divOnly := ctx.FormBool("div-only") - queryParams := ctx.Req.URL.Query() - queryParams.Del("div-only") - paginator := context.NewPagination(graphCommitsCount, setting.UI.GraphMaxCommitNum, page, 5) - paginator.AddParamFromQuery(queryParams) - ctx.Data["Page"] = paginator - if divOnly { - ctx.HTML(http.StatusOK, tplGraphDiv) - return - } + ctx.Data["Username"] = ctx.Repo.Owner.Name + ctx.Data["Reponame"] = ctx.Repo.Repository.Name + ctx.Data["CommitCount"] = commits.Count + ctx.Data["Commits"] = commits.Commits ctx.HTML(http.StatusOK, tplGraph) } +// GraphDiv render commit graph div - show commits from all branches. +func GraphDiv(ctx *context.Context) { + ctx.Data["PageIsCommits"] = true + ctx.Data["PageIsGraph"] = true + + page := ctx.FormInt("page") + if page <= 1 { + page = 0 + } else { + page-- + } + + commits, err := gitgraph.GetCommitGraph(ctx, ctx.Repo.GitRepo, page, setting.UI.GraphMaxCommitNum) + if err != nil { + ctx.ServerError("GetCommitGraph", err) + return + } + + ctx.Data["Username"] = ctx.Repo.Owner.Name + ctx.Data["Reponame"] = ctx.Repo.Repository.Name + ctx.Data["CommitCount"] = commits.Count + ctx.Data["Commits"] = commits.Commits + + ctx.HTML(http.StatusOK, tplGraphDiv) +} + // SearchCommits render commits filtered by keyword func SearchCommits(ctx *context.Context) { ctx.Data["PageIsCommits"] = true - ctx.Data["PageIsViewCode"] = true + ctx.Data["Title"] = ctx.Tr("repo.commits.commit_history") - query := ctx.FormTrim("q") - if len(query) == 0 { - ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.RefTypeNameSubURL()) + keyword := ctx.FormTrim("q") + if len(keyword) == 0 { + ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchName) return } - all := ctx.FormBool("all") - opts := git.NewSearchCommitsOptions(query, all) - commits, err := ctx.Repo.Commit.SearchCommits(opts) + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } + + commits, err := ctx.Repo.GitRepo.SearchCommits(keyword) if err != nil { ctx.ServerError("SearchCommits", err) return } - ctx.Data["CommitCount"] = len(commits) - ctx.Data["Commits"], err = processGitCommits(ctx, commits) + + // Get sign and verify info + signCommitInfos, err := processGitCommits(ctx, commits) if err != nil { ctx.ServerError("processGitCommits", err) return } - ctx.Data["Keyword"] = query - if all { - ctx.Data["All"] = true - } + ctx.Data["Commits"] = signCommitInfos + ctx.Data["Username"] = ctx.Repo.Owner.Name + ctx.Data["Reponame"] = ctx.Repo.Repository.Name + ctx.Data["Keyword"] = keyword + ctx.Data["Branch"] = ctx.Repo.BranchName + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["CommitCount"] = len(commits) + + pager := context.NewPagination(len(commits), setting.UI.ExplorePagingNum, page, 5) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager + ctx.HTML(http.StatusOK, tplCommits) } // FileHistory show a file's reversions func FileHistory(ctx *context.Context) { - if ctx.Repo.TreePath == "" { - Commits(ctx) + ctx.Data["IsRepoToolbarCommits"] = true + ctx.Data["IsRepoToolbarFile"] = true + + fileName := ctx.Repo.TreePath + if len(fileName) == 0 { + ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchName) return } - commitsCount, err := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath) + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } + + // Get the commits + commits, err := ctx.Repo.GitRepo.FileCommitsByRange(ctx.Repo.BranchName, fileName, page) if err != nil { - ctx.ServerError("FileCommitsCount", err) - return - } else if commitsCount == 0 { - ctx.NotFound(nil) + ctx.ServerError("FileCommitsByRange", err) return } - page := max(ctx.FormInt("page"), 1) - - commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange( - git.CommitsByFileAndRangeOptions{ - Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName - File: ctx.Repo.TreePath, - Page: page, - }) - if err != nil { - ctx.ServerError("CommitsByFileAndRange", err) - return - } - ctx.Data["Commits"], err = processGitCommits(ctx, commits) + // Get sign and verify info + signCommitInfos, err := processGitCommits(ctx, commits) if err != nil { ctx.ServerError("processGitCommits", err) return } - ctx.Data["FileTreePath"] = ctx.Repo.TreePath - ctx.Data["CommitCount"] = commitsCount + // Get latest commit + lastCommit := signCommitInfos[0] + ctx.Data["Username"] = ctx.Repo.Owner.Name + ctx.Data["Reponame"] = ctx.Repo.Repository.Name + ctx.Data["FileName"] = fileName + ctx.Data["CommitCount"] = len(commits) + ctx.Data["Commits"] = signCommitInfos + ctx.Data["LastCommit"] = lastCommit + ctx.Data["IsImageFile"] = lastCommit.Tree.IsImageFile(fileName) + ctx.Data["Branch"] = ctx.Repo.BranchName + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(fileName) - pager := context.NewPagination(commitsCount, setting.Git.CommitsRangeSize, page, 5) - pager.AddParamFromRequest(ctx.Req) + pager := context.NewPagination(len(commits), setting.UI.ExplorePagingNum, page, 5) + pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager + ctx.HTML(http.StatusOK, tplCommits) } +// LoadBranchesAndTags loads branches and tags from the repository func LoadBranchesAndTags(ctx *context.Context) { - response, err := repo_service.LoadBranchesAndTags(ctx, ctx.Repo, ctx.PathParam("sha")) - if err == nil { - ctx.JSON(http.StatusOK, response) + branches, err := ctx.Repo.Repository.GetBranches() + if err != nil { + ctx.ServerError("GetBranches", err) return } - ctx.NotFoundOrServerError(fmt.Sprintf("could not load branches and tags the commit %s belongs to", ctx.PathParam("sha")), git.IsErrNotExist, err) + ctx.Data["Branches"] = branches + + tags, err := ctx.Repo.Repository.GetTags(0, 0) + if err != nil { + ctx.ServerError("GetTags", err) + return + } + ctx.Data["Tags"] = tags } -// Diff show different from current commit to previous commit +// Diff show different from current commit to previous commit of the file func Diff(ctx *context.Context) { ctx.Data["PageIsDiff"] = true @@ -354,6 +394,12 @@ func Diff(ctx *context.Context) { ctx.Data["Diff"] = diff ctx.Data["DiffBlobExcerptData"] = diffBlobExcerptData + // Load commit comments for diff lines + if err := repo_model.LoadCommentsForDiffLines(ctx, ctx.Repo.Repository.ID, commitID, diff.Lines); err != nil { + ctx.ServerError("LoadCommentsForDiffLines", err) + return + } + if !fileOnly { diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, parentCommitID, commitID) if err != nil { @@ -418,50 +464,47 @@ func Diff(ctx *context.Context) { // RawDiff dumps diff results of repository in given commit ID to io.Writer func RawDiff(ctx *context.Context) { - var gitRepo *git.Repository - if ctx.Data["PageIsWiki"] != nil { - wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) - if err != nil { - ctx.ServerError("OpenRepository", err) - return - } - defer wikiRepo.Close() - gitRepo = wikiRepo - } else { - gitRepo = ctx.Repo.GitRepo - if gitRepo == nil { - ctx.ServerError("GitRepo not open", fmt.Errorf("no open git repo for '%s'", ctx.Repo.Repository.FullName())) - return - } - } - if err := git.GetRawDiff( - gitRepo, - ctx.PathParam("sha"), - git.RawDiffType(ctx.PathParam("ext")), - ctx.Resp, - ); err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist.")) - return - } - ctx.ServerError("GetRawDiff", err) + commitID := ctx.PathParam("sha") + gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + if err != nil { + ctx.ServerError("OpenRepository", err) return } + defer gitRepo.Close() + + commit, err := gitRepo.GetCommit(commitID) + if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound(err) + } else { + ctx.ServerError("GetCommit", err) + } + return + } + + diff, err := commit.Diff(ctx.Query("ignore whitespace")) + if err != nil { + ctx.ServerError("Diff", err) + return + } + defer diff.Free() + + ctx.Resp.Header().Set("Content-Type", "text/plain") + ctx.Resp.WriteHeader(http.StatusOK) + if err := diff.Write(ctx.Resp, git.RawDiffFormat); err != nil { + log.Error("Write: %v", err) + } } func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_model.SignCommitWithStatuses, error) { - commits, err := git_service.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository) - if err != nil { - return nil, err - } - if !ctx.Repo.Permission.CanRead(unit_model.TypeActions) { - for _, commit := range commits { - if commit.Status == nil { - continue - } - commit.Status.HideActionsURL(ctx) - git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses) + commits := make([]*git_model.SignCommitWithStatuses, 0, len(gitCommits)) + for _, commit := range gitCommits { + signCommit, err := git_model.GetSignCommit(ctx, ctx.Repo.Repository.ID, commit) + if err != nil { + return nil, err } + commits = append(commits, signCommit) } return commits, nil } + diff --git a/services/notify/notifier.go b/services/notify/notifier.go index 875a70e564..092e490763 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -82,4 +82,8 @@ type Notifier interface { WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) + + // Commit comment methods + CreateCommitComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitSHA string, comment *repo_model.CommitComment) + DeleteCommitComment(ctx context.Context, doer *user_model.User, comment *repo_model.CommitComment) } diff --git a/services/notify/notify.go b/services/notify/notify.go index 152d53b01c..047ac7fa05 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -17,6 +17,20 @@ import ( "code.gitea.io/gitea/modules/repository" ) +// NotifyCreateCommitComment notifies subscribers when a commit comment is created +func NotifyCreateCommitComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitSHA string, comment *repo_model.CommitComment) { + for _, notifier := range notifiers { + notifier.CreateCommitComment(ctx, doer, repo, commitSHA, comment) + } +} + +// NotifyDeleteCommitComment notifies subscribers when a commit comment is deleted +func NotifyDeleteCommitComment(ctx context.Context, doer *user_model.User, comment *repo_model.CommitComment) { + for _, notifier := range notifiers { + notifier.DeleteCommitComment(ctx, doer, comment) + } +} + var notifiers []Notifier // RegisterNotifier providers method to receive notify messages