Skip to content

add button to export issues to Excel #35313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -245,19 +245,25 @@ require (
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rhysd/actionlint v1.7.7 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/tiendc/go-deepcopy v1.6.0 // indirect
github.com/unknwon/com v1.0.1 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/excelize/v2 v2.9.1 // indirect
github.com/xuri/nfp v0.0.1 // indirect
github.com/zeebo/assert v1.3.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
Expand Down
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,11 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6O
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rhysd/actionlint v1.7.7 h1:0KgkoNTrYY7vmOCs9BW2AHxLvvpoY9nEUzgBHiPUr0k=
github.com/rhysd/actionlint v1.7.7/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
Expand Down Expand Up @@ -678,6 +683,8 @@ github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08Yu
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
Expand Down Expand Up @@ -711,6 +718,12 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3539,6 +3539,7 @@ review_dismissed_reason = Reason:
create_branch = created branch <a href="%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a>
starred_repo = starred <a href="%[1]s">%[2]s</a>
watched_repo = started watching <a href="%[1]s">%[2]s</a>
export_to_excel = Export to Excel

[tool]
now = now
Expand Down
39 changes: 30 additions & 9 deletions routers/web/repo/issue_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/export"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
)
Expand Down Expand Up @@ -258,14 +259,13 @@
return user.ID
}

// SearchRepoIssuesJSON lists the issues of a repository
// This function was copied from API (decouple the web and API routes),
// it is only used by frontend to search some dependency or related issues
func SearchRepoIssuesJSON(ctx *context.Context) {
func SearchRepoIssues(ctx *context.Context) (issues_model.IssueList, int64) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.HTTPError(http.StatusUnprocessableEntity, err.Error())
return
return nil, 0
}

var isClosed optional.Option[bool]
Expand Down Expand Up @@ -295,7 +295,7 @@
}
if !issues_model.IsErrMilestoneNotExist(err) {
ctx.HTTPError(http.StatusInternalServerError, err.Error())
return
return nil, 0
}
id, err := strconv.ParseInt(part[i], 10, 64)
if err != nil {
Expand Down Expand Up @@ -329,15 +329,15 @@
// FIXME: we should be more efficient here
createdByID := getUserIDForFilter(ctx, "created_by")
if ctx.Written() {
return
return nil, 0
}
assignedByID := getUserIDForFilter(ctx, "assigned_by")
if ctx.Written() {
return
return nil, 0
}
mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
if ctx.Written() {
return
return nil, 0
}

searchOpt := &issue_indexer.SearchOptions{
Expand Down Expand Up @@ -380,18 +380,39 @@
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
if err != nil {
ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error())
return
return nil, 0
}
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
if err != nil {
ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
return
return nil, 0
}

return issues, total
}

// SearchRepoIssuesJSON lists the issues of a repository
func SearchRepoIssuesJSON(ctx *context.Context) {
issues, total := SearchRepoIssues(ctx)

ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
}

func ExportIssues(ctx *context.Context) {
issues, total := SearchRepoIssues(ctx)

if total == 0 {
return
}

f := export.IssuesToExcel(ctx, issues)

ctx.Resp.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")

Check failure on line 411 in routers/web/repo/issue_list.go

View workflow job for this annotation

GitHub Actions / lint-backend

File is not properly formatted (gofmt)

Check failure on line 411 in routers/web/repo/issue_list.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

File is not properly formatted (gofmt)

Check failure on line 411 in routers/web/repo/issue_list.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

File is not properly formatted (gofmt)
ctx.Resp.Header().Set("Content-Disposition", `attachment; filename="issues.xlsx"`)
_ = f.Write(ctx.Resp)
}

func BatchDeleteIssues(ctx *context.Context) {
issues := getActionIssues(ctx)
if ctx.Written() {
Expand Down
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,7 @@ func registerWebRoutes(m *web.Router) {
m.Get("/choose", repo.NewIssueChooseTemplate)
})
m.Get("/search", repo.SearchRepoIssuesJSON)
m.Get("/export", reqRepoAdmin, repo.ExportIssues)
}, reqUnitIssuesReader)

addIssuesPullsUpdateRoutes := func() {
Expand Down
62 changes: 62 additions & 0 deletions services/export/excel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package export

import (
"fmt"

Check failure on line 7 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-backend

File is not properly formatted (gofumpt)

Check failure on line 7 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

File is not properly formatted (gofumpt)

Check failure on line 7 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

File is not properly formatted (gofumpt)
"github.com/xuri/excelize/v2"

issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/services/context"
)

func IssuesToExcel(ctx *context.Context, issues issues_model.IssueList) *excelize.File {
f := excelize.NewFile()
sheet := f.GetSheetName(f.GetActiveSheetIndex())

headers := []string{"ID", "Title", "Status", "Assignee(s)", "Label(s)", "Created At"}
for col, h := range headers {
cell, _ := excelize.CoordinatesToCellName(col+1, 1)
f.SetCellValue(sheet, cell, h)

Check failure on line 21 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-backend

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 21 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 21 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

Error return value of `f.SetCellValue` is not checked (errcheck)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ref: https://xuri.me/excelize/en/stream.html

Streaming write should be used here, as ordinary write will occupy a lot of memory when the data volume is slightly larger.

}

for i, issue := range issues {

Check failure on line 24 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-backend

empty-lines: extra empty line at the start of a block (revive)

Check failure on line 24 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

empty-lines: extra empty line at the start of a block (revive)

Check failure on line 24 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

empty-lines: extra empty line at the start of a block (revive)

assignees := ""
if err := issue.LoadAssignees(ctx); err == nil {
if len(issue.Assignees) > 0 {
for _, assignee := range issue.Assignees {
if assignees != "" {
assignees += ", "
}
if assignee.FullName != "" {
assignees += assignee.FullName
} else {
assignees += assignee.Name
}
}
}
}

labels := ""
if err := issue.LoadLabels(ctx); err == nil {
if len(issue.Labels) > 0 {
for _, label := range issue.Labels {
if labels != "" {
labels += ", "
}
labels += label.Name
}
}
}

f.SetCellValue(sheet, fmt.Sprintf("A%d", i+2), issue.Index)

Check failure on line 54 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-backend

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 54 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 54 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

Error return value of `f.SetCellValue` is not checked (errcheck)
f.SetCellValue(sheet, fmt.Sprintf("B%d", i+2), issue.Title)

Check failure on line 55 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-backend

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 55 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 55 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

Error return value of `f.SetCellValue` is not checked (errcheck)
f.SetCellValue(sheet, fmt.Sprintf("C%d", i+2), issue.State())

Check failure on line 56 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-backend

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 56 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 56 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

Error return value of `f.SetCellValue` is not checked (errcheck)
f.SetCellValue(sheet, fmt.Sprintf("D%d", i+2), assignees)

Check failure on line 57 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-backend

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 57 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 57 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

Error return value of `f.SetCellValue` is not checked (errcheck)
f.SetCellValue(sheet, fmt.Sprintf("E%d", i+2), labels)

Check failure on line 58 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-backend

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 58 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 58 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

Error return value of `f.SetCellValue` is not checked (errcheck)
f.SetCellValue(sheet, fmt.Sprintf("F%d", i+2), issue.CreatedUnix.AsTime()) // .Format("2006-01-02"))

Check failure on line 59 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-backend

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 59 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

Error return value of `f.SetCellValue` is not checked (errcheck)

Check failure on line 59 in services/export/excel.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

Error return value of `f.SetCellValue` is not checked (errcheck)
}
return f
}
1 change: 1 addition & 0 deletions templates/repo/issue/list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<a class="ui small primary small button issue-list-new{{if not .PullRequestCtx.Allowed}} disabled{{end}}" href="{{if .PullRequestCtx.Allowed}}{{.PullRequestCtx.BaseRepo.Link}}/compare/{{.PullRequestCtx.BaseRepo.DefaultBranch | PathEscapeSegments}}...{{if ne .Repository.Owner.Name .PullRequestCtx.BaseRepo.Owner.Name}}{{PathEscape .Repository.Owner.Name}}:{{end}}{{.Repository.DefaultBranch | PathEscapeSegments}}{{end}}">{{ctx.Locale.Tr "action.compare_commits_general"}}</a>
{{end}}
{{end}}
<a class="ui small primary button issue-list-export" href="{{.RepoLink}}/issues/export?{{.Page.GetParams}}">{{ctx.Locale.Tr "action.export_to_excel"}}</a>
</div>

{{template "repo/issue/filters" .}}
Expand Down
Loading