You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue.go 103KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package repo
  5. import (
  6. "bytes"
  7. stdCtx "context"
  8. "errors"
  9. "fmt"
  10. "math/big"
  11. "net/http"
  12. "net/url"
  13. "sort"
  14. "strconv"
  15. "strings"
  16. "time"
  17. activities_model "code.gitea.io/gitea/models/activities"
  18. "code.gitea.io/gitea/models/db"
  19. git_model "code.gitea.io/gitea/models/git"
  20. issues_model "code.gitea.io/gitea/models/issues"
  21. "code.gitea.io/gitea/models/organization"
  22. access_model "code.gitea.io/gitea/models/perm/access"
  23. project_model "code.gitea.io/gitea/models/project"
  24. pull_model "code.gitea.io/gitea/models/pull"
  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/modules/base"
  29. "code.gitea.io/gitea/modules/container"
  30. "code.gitea.io/gitea/modules/context"
  31. "code.gitea.io/gitea/modules/git"
  32. issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
  33. issue_template "code.gitea.io/gitea/modules/issue/template"
  34. "code.gitea.io/gitea/modules/log"
  35. "code.gitea.io/gitea/modules/markup"
  36. "code.gitea.io/gitea/modules/markup/markdown"
  37. repo_module "code.gitea.io/gitea/modules/repository"
  38. "code.gitea.io/gitea/modules/setting"
  39. api "code.gitea.io/gitea/modules/structs"
  40. "code.gitea.io/gitea/modules/templates/vars"
  41. "code.gitea.io/gitea/modules/timeutil"
  42. "code.gitea.io/gitea/modules/upload"
  43. "code.gitea.io/gitea/modules/util"
  44. "code.gitea.io/gitea/modules/web"
  45. "code.gitea.io/gitea/routers/utils"
  46. asymkey_service "code.gitea.io/gitea/services/asymkey"
  47. "code.gitea.io/gitea/services/convert"
  48. "code.gitea.io/gitea/services/forms"
  49. issue_service "code.gitea.io/gitea/services/issue"
  50. pull_service "code.gitea.io/gitea/services/pull"
  51. repo_service "code.gitea.io/gitea/services/repository"
  52. )
  53. const (
  54. tplAttachment base.TplName = "repo/issue/view_content/attachments"
  55. tplIssues base.TplName = "repo/issue/list"
  56. tplIssueNew base.TplName = "repo/issue/new"
  57. tplIssueChoose base.TplName = "repo/issue/choose"
  58. tplIssueView base.TplName = "repo/issue/view"
  59. tplReactions base.TplName = "repo/issue/view_content/reactions"
  60. issueTemplateKey = "IssueTemplate"
  61. issueTemplateTitleKey = "IssueTemplateTitle"
  62. )
  63. // IssueTemplateCandidates issue templates
  64. var IssueTemplateCandidates = []string{
  65. "ISSUE_TEMPLATE.md",
  66. "ISSUE_TEMPLATE.yaml",
  67. "ISSUE_TEMPLATE.yml",
  68. "issue_template.md",
  69. "issue_template.yaml",
  70. "issue_template.yml",
  71. ".gitea/ISSUE_TEMPLATE.md",
  72. ".gitea/ISSUE_TEMPLATE.yaml",
  73. ".gitea/ISSUE_TEMPLATE.yml",
  74. ".gitea/issue_template.md",
  75. ".gitea/issue_template.yaml",
  76. ".gitea/issue_template.yml",
  77. ".github/ISSUE_TEMPLATE.md",
  78. ".github/ISSUE_TEMPLATE.yaml",
  79. ".github/ISSUE_TEMPLATE.yml",
  80. ".github/issue_template.md",
  81. ".github/issue_template.yaml",
  82. ".github/issue_template.yml",
  83. }
  84. // MustAllowUserComment checks to make sure if an issue is locked.
  85. // If locked and user has permissions to write to the repository,
  86. // then the comment is allowed, else it is blocked
  87. func MustAllowUserComment(ctx *context.Context) {
  88. issue := GetActionIssue(ctx)
  89. if ctx.Written() {
  90. return
  91. }
  92. if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
  93. ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
  94. ctx.Redirect(issue.Link())
  95. return
  96. }
  97. }
  98. // MustEnableIssues check if repository enable internal issues
  99. func MustEnableIssues(ctx *context.Context) {
  100. if !ctx.Repo.CanRead(unit.TypeIssues) &&
  101. !ctx.Repo.CanRead(unit.TypeExternalTracker) {
  102. ctx.NotFound("MustEnableIssues", nil)
  103. return
  104. }
  105. unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
  106. if err == nil {
  107. ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
  108. return
  109. }
  110. }
  111. // MustAllowPulls check if repository enable pull requests and user have right to do that
  112. func MustAllowPulls(ctx *context.Context) {
  113. if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
  114. ctx.NotFound("MustAllowPulls", nil)
  115. return
  116. }
  117. // User can send pull request if owns a forked repository.
  118. if ctx.IsSigned && repo_model.HasForkedRepo(ctx.Doer.ID, ctx.Repo.Repository.ID) {
  119. ctx.Repo.PullRequest.Allowed = true
  120. ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Doer.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName)
  121. }
  122. }
  123. func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
  124. var err error
  125. viewType := ctx.FormString("type")
  126. sortType := ctx.FormString("sort")
  127. types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested", "reviewed_by"}
  128. if !util.SliceContainsString(types, viewType, true) {
  129. viewType = "all"
  130. }
  131. var (
  132. assigneeID = ctx.FormInt64("assignee")
  133. posterID = ctx.FormInt64("poster")
  134. mentionedID int64
  135. reviewRequestedID int64
  136. reviewedID int64
  137. forceEmpty bool
  138. )
  139. if ctx.IsSigned {
  140. switch viewType {
  141. case "created_by":
  142. posterID = ctx.Doer.ID
  143. case "mentioned":
  144. mentionedID = ctx.Doer.ID
  145. case "assigned":
  146. assigneeID = ctx.Doer.ID
  147. case "review_requested":
  148. reviewRequestedID = ctx.Doer.ID
  149. case "reviewed_by":
  150. reviewedID = ctx.Doer.ID
  151. }
  152. }
  153. repo := ctx.Repo.Repository
  154. var labelIDs []int64
  155. // 1,-2 means including label 1 and excluding label 2
  156. // 0 means issues with no label
  157. // blank means labels will not be filtered for issues
  158. selectLabels := ctx.FormString("labels")
  159. if selectLabels == "" {
  160. ctx.Data["AllLabels"] = true
  161. } else if selectLabels == "0" {
  162. ctx.Data["NoLabel"] = true
  163. } else if len(selectLabels) > 0 {
  164. labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
  165. if err != nil {
  166. ctx.ServerError("StringsToInt64s", err)
  167. return
  168. }
  169. }
  170. keyword := strings.Trim(ctx.FormString("q"), " ")
  171. if bytes.Contains([]byte(keyword), []byte{0x00}) {
  172. keyword = ""
  173. }
  174. var issueIDs []int64
  175. if len(keyword) > 0 {
  176. issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, []int64{repo.ID}, keyword, ctx.FormString("state"))
  177. if err != nil {
  178. if issue_indexer.IsAvailable(ctx) {
  179. ctx.ServerError("issueIndexer.Search", err)
  180. return
  181. }
  182. ctx.Data["IssueIndexerUnavailable"] = true
  183. }
  184. if len(issueIDs) == 0 {
  185. forceEmpty = true
  186. }
  187. }
  188. var mileIDs []int64
  189. if milestoneID > 0 || milestoneID == db.NoConditionID { // -1 to get those issues which have no any milestone assigned
  190. mileIDs = []int64{milestoneID}
  191. }
  192. var issueStats *issues_model.IssueStats
  193. if forceEmpty {
  194. issueStats = &issues_model.IssueStats{}
  195. } else {
  196. issueStats, err = issues_model.GetIssueStats(&issues_model.IssuesOptions{
  197. RepoIDs: []int64{repo.ID},
  198. LabelIDs: labelIDs,
  199. MilestoneIDs: mileIDs,
  200. ProjectID: projectID,
  201. AssigneeID: assigneeID,
  202. MentionedID: mentionedID,
  203. PosterID: posterID,
  204. ReviewRequestedID: reviewRequestedID,
  205. ReviewedID: reviewedID,
  206. IsPull: isPullOption,
  207. IssueIDs: issueIDs,
  208. })
  209. if err != nil {
  210. ctx.ServerError("GetIssueStats", err)
  211. return
  212. }
  213. }
  214. isShowClosed := ctx.FormString("state") == "closed"
  215. // if open issues are zero and close don't, use closed as default
  216. if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
  217. isShowClosed = true
  218. }
  219. page := ctx.FormInt("page")
  220. if page <= 1 {
  221. page = 1
  222. }
  223. var total int
  224. if !isShowClosed {
  225. total = int(issueStats.OpenCount)
  226. } else {
  227. total = int(issueStats.ClosedCount)
  228. }
  229. pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
  230. var issues []*issues_model.Issue
  231. if forceEmpty {
  232. issues = []*issues_model.Issue{}
  233. } else {
  234. issues, err = issues_model.Issues(ctx, &issues_model.IssuesOptions{
  235. ListOptions: db.ListOptions{
  236. Page: pager.Paginater.Current(),
  237. PageSize: setting.UI.IssuePagingNum,
  238. },
  239. RepoIDs: []int64{repo.ID},
  240. AssigneeID: assigneeID,
  241. PosterID: posterID,
  242. MentionedID: mentionedID,
  243. ReviewRequestedID: reviewRequestedID,
  244. ReviewedID: reviewedID,
  245. MilestoneIDs: mileIDs,
  246. ProjectID: projectID,
  247. IsClosed: util.OptionalBoolOf(isShowClosed),
  248. IsPull: isPullOption,
  249. LabelIDs: labelIDs,
  250. SortType: sortType,
  251. IssueIDs: issueIDs,
  252. })
  253. if err != nil {
  254. ctx.ServerError("Issues", err)
  255. return
  256. }
  257. }
  258. issueList := issues_model.IssueList(issues)
  259. approvalCounts, err := issueList.GetApprovalCounts(ctx)
  260. if err != nil {
  261. ctx.ServerError("ApprovalCounts", err)
  262. return
  263. }
  264. // Get posters.
  265. for i := range issues {
  266. // Check read status
  267. if !ctx.IsSigned {
  268. issues[i].IsRead = true
  269. } else if err = issues[i].GetIsRead(ctx.Doer.ID); err != nil {
  270. ctx.ServerError("GetIsRead", err)
  271. return
  272. }
  273. }
  274. commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
  275. if err != nil {
  276. ctx.ServerError("GetIssuesAllCommitStatus", err)
  277. return
  278. }
  279. ctx.Data["Issues"] = issues
  280. ctx.Data["CommitLastStatus"] = lastStatus
  281. ctx.Data["CommitStatuses"] = commitStatuses
  282. // Get assignees.
  283. assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo)
  284. if err != nil {
  285. ctx.ServerError("GetRepoAssignees", err)
  286. return
  287. }
  288. ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers)
  289. handleTeamMentions(ctx)
  290. if ctx.Written() {
  291. return
  292. }
  293. labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
  294. if err != nil {
  295. ctx.ServerError("GetLabelsByRepoID", err)
  296. return
  297. }
  298. if repo.Owner.IsOrganization() {
  299. orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
  300. if err != nil {
  301. ctx.ServerError("GetLabelsByOrgID", err)
  302. return
  303. }
  304. ctx.Data["OrgLabels"] = orgLabels
  305. labels = append(labels, orgLabels...)
  306. }
  307. // Get the exclusive scope for every label ID
  308. labelExclusiveScopes := make([]string, 0, len(labelIDs))
  309. for _, labelID := range labelIDs {
  310. foundExclusiveScope := false
  311. for _, label := range labels {
  312. if label.ID == labelID || label.ID == -labelID {
  313. labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
  314. foundExclusiveScope = true
  315. break
  316. }
  317. }
  318. if !foundExclusiveScope {
  319. labelExclusiveScopes = append(labelExclusiveScopes, "")
  320. }
  321. }
  322. for _, l := range labels {
  323. l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
  324. }
  325. ctx.Data["Labels"] = labels
  326. ctx.Data["NumLabels"] = len(labels)
  327. if ctx.FormInt64("assignee") == 0 {
  328. assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
  329. }
  330. ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
  331. ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
  332. counts, ok := approvalCounts[issueID]
  333. if !ok || len(counts) == 0 {
  334. return 0
  335. }
  336. reviewTyp := issues_model.ReviewTypeApprove
  337. if typ == "reject" {
  338. reviewTyp = issues_model.ReviewTypeReject
  339. } else if typ == "waiting" {
  340. reviewTyp = issues_model.ReviewTypeRequest
  341. }
  342. for _, count := range counts {
  343. if count.Type == reviewTyp {
  344. return count.Count
  345. }
  346. }
  347. return 0
  348. }
  349. retrieveProjects(ctx, repo)
  350. if ctx.Written() {
  351. return
  352. }
  353. pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.IsTrue())
  354. if err != nil {
  355. ctx.ServerError("GetPinnedIssues", err)
  356. return
  357. }
  358. ctx.Data["PinnedIssues"] = pinned
  359. ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
  360. ctx.Data["IssueStats"] = issueStats
  361. ctx.Data["SelLabelIDs"] = labelIDs
  362. ctx.Data["SelectLabels"] = selectLabels
  363. ctx.Data["ViewType"] = viewType
  364. ctx.Data["SortType"] = sortType
  365. ctx.Data["MilestoneID"] = milestoneID
  366. ctx.Data["ProjectID"] = projectID
  367. ctx.Data["AssigneeID"] = assigneeID
  368. ctx.Data["PosterID"] = posterID
  369. ctx.Data["IsShowClosed"] = isShowClosed
  370. ctx.Data["Keyword"] = keyword
  371. if isShowClosed {
  372. ctx.Data["State"] = "closed"
  373. } else {
  374. ctx.Data["State"] = "open"
  375. }
  376. pager.AddParam(ctx, "q", "Keyword")
  377. pager.AddParam(ctx, "type", "ViewType")
  378. pager.AddParam(ctx, "sort", "SortType")
  379. pager.AddParam(ctx, "state", "State")
  380. pager.AddParam(ctx, "labels", "SelectLabels")
  381. pager.AddParam(ctx, "milestone", "MilestoneID")
  382. pager.AddParam(ctx, "project", "ProjectID")
  383. pager.AddParam(ctx, "assignee", "AssigneeID")
  384. pager.AddParam(ctx, "poster", "PosterID")
  385. ctx.Data["Page"] = pager
  386. }
  387. // Issues render issues page
  388. func Issues(ctx *context.Context) {
  389. isPullList := ctx.Params(":type") == "pulls"
  390. if isPullList {
  391. MustAllowPulls(ctx)
  392. if ctx.Written() {
  393. return
  394. }
  395. ctx.Data["Title"] = ctx.Tr("repo.pulls")
  396. ctx.Data["PageIsPullList"] = true
  397. } else {
  398. MustEnableIssues(ctx)
  399. if ctx.Written() {
  400. return
  401. }
  402. ctx.Data["Title"] = ctx.Tr("repo.issues")
  403. ctx.Data["PageIsIssueList"] = true
  404. ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
  405. }
  406. issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
  407. if ctx.Written() {
  408. return
  409. }
  410. renderMilestones(ctx)
  411. if ctx.Written() {
  412. return
  413. }
  414. ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
  415. ctx.HTML(http.StatusOK, tplIssues)
  416. }
  417. func renderMilestones(ctx *context.Context) {
  418. // Get milestones
  419. milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
  420. RepoID: ctx.Repo.Repository.ID,
  421. State: api.StateAll,
  422. })
  423. if err != nil {
  424. ctx.ServerError("GetAllRepoMilestones", err)
  425. return
  426. }
  427. openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{}
  428. for _, milestone := range milestones {
  429. if milestone.IsClosed {
  430. closedMilestones = append(closedMilestones, milestone)
  431. } else {
  432. openMilestones = append(openMilestones, milestone)
  433. }
  434. }
  435. ctx.Data["OpenMilestones"] = openMilestones
  436. ctx.Data["ClosedMilestones"] = closedMilestones
  437. }
  438. // RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
  439. func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
  440. var err error
  441. ctx.Data["OpenMilestones"], _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{
  442. RepoID: repo.ID,
  443. State: api.StateOpen,
  444. })
  445. if err != nil {
  446. ctx.ServerError("GetMilestones", err)
  447. return
  448. }
  449. ctx.Data["ClosedMilestones"], _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{
  450. RepoID: repo.ID,
  451. State: api.StateClosed,
  452. })
  453. if err != nil {
  454. ctx.ServerError("GetMilestones", err)
  455. return
  456. }
  457. assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo)
  458. if err != nil {
  459. ctx.ServerError("GetRepoAssignees", err)
  460. return
  461. }
  462. ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers)
  463. handleTeamMentions(ctx)
  464. }
  465. func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
  466. // Distinguish whether the owner of the repository
  467. // is an individual or an organization
  468. repoOwnerType := project_model.TypeIndividual
  469. if repo.Owner.IsOrganization() {
  470. repoOwnerType = project_model.TypeOrganization
  471. }
  472. var err error
  473. projects, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
  474. RepoID: repo.ID,
  475. Page: -1,
  476. IsClosed: util.OptionalBoolFalse,
  477. Type: project_model.TypeRepository,
  478. })
  479. if err != nil {
  480. ctx.ServerError("GetProjects", err)
  481. return
  482. }
  483. projects2, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
  484. OwnerID: repo.OwnerID,
  485. Page: -1,
  486. IsClosed: util.OptionalBoolFalse,
  487. Type: repoOwnerType,
  488. })
  489. if err != nil {
  490. ctx.ServerError("GetProjects", err)
  491. return
  492. }
  493. ctx.Data["OpenProjects"] = append(projects, projects2...)
  494. projects, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
  495. RepoID: repo.ID,
  496. Page: -1,
  497. IsClosed: util.OptionalBoolTrue,
  498. Type: project_model.TypeRepository,
  499. })
  500. if err != nil {
  501. ctx.ServerError("GetProjects", err)
  502. return
  503. }
  504. projects2, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
  505. OwnerID: repo.OwnerID,
  506. Page: -1,
  507. IsClosed: util.OptionalBoolTrue,
  508. Type: repoOwnerType,
  509. })
  510. if err != nil {
  511. ctx.ServerError("GetProjects", err)
  512. return
  513. }
  514. ctx.Data["ClosedProjects"] = append(projects, projects2...)
  515. }
  516. // repoReviewerSelection items to bee shown
  517. type repoReviewerSelection struct {
  518. IsTeam bool
  519. Team *organization.Team
  520. User *user_model.User
  521. Review *issues_model.Review
  522. CanChange bool
  523. Checked bool
  524. ItemID int64
  525. }
  526. // RetrieveRepoReviewers find all reviewers of a repository
  527. func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) {
  528. ctx.Data["CanChooseReviewer"] = canChooseReviewer
  529. originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(issue.ID)
  530. if err != nil {
  531. ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
  532. return
  533. }
  534. ctx.Data["OriginalReviews"] = originalAuthorReviews
  535. reviews, err := issues_model.GetReviewsByIssueID(issue.ID)
  536. if err != nil {
  537. ctx.ServerError("GetReviewersByIssueID", err)
  538. return
  539. }
  540. if len(reviews) == 0 && !canChooseReviewer {
  541. return
  542. }
  543. var (
  544. pullReviews []*repoReviewerSelection
  545. reviewersResult []*repoReviewerSelection
  546. teamReviewersResult []*repoReviewerSelection
  547. teamReviewers []*organization.Team
  548. reviewers []*user_model.User
  549. )
  550. if canChooseReviewer {
  551. posterID := issue.PosterID
  552. if issue.OriginalAuthorID > 0 {
  553. posterID = 0
  554. }
  555. reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
  556. if err != nil {
  557. ctx.ServerError("GetReviewers", err)
  558. return
  559. }
  560. teamReviewers, err = repo_service.GetReviewerTeams(ctx, repo)
  561. if err != nil {
  562. ctx.ServerError("GetReviewerTeams", err)
  563. return
  564. }
  565. if len(reviewers) > 0 {
  566. reviewersResult = make([]*repoReviewerSelection, 0, len(reviewers))
  567. }
  568. if len(teamReviewers) > 0 {
  569. teamReviewersResult = make([]*repoReviewerSelection, 0, len(teamReviewers))
  570. }
  571. }
  572. pullReviews = make([]*repoReviewerSelection, 0, len(reviews))
  573. for _, review := range reviews {
  574. tmp := &repoReviewerSelection{
  575. Checked: review.Type == issues_model.ReviewTypeRequest,
  576. Review: review,
  577. ItemID: review.ReviewerID,
  578. }
  579. if review.ReviewerTeamID > 0 {
  580. tmp.IsTeam = true
  581. tmp.ItemID = -review.ReviewerTeamID
  582. }
  583. if ctx.Repo.IsAdmin() {
  584. // Admin can dismiss or re-request any review requests
  585. tmp.CanChange = true
  586. } else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
  587. // A user can refuse review requests
  588. tmp.CanChange = true
  589. } else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest &&
  590. ctx.Doer.ID != review.ReviewerID {
  591. // The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
  592. tmp.CanChange = true
  593. }
  594. pullReviews = append(pullReviews, tmp)
  595. if canChooseReviewer {
  596. if tmp.IsTeam {
  597. teamReviewersResult = append(teamReviewersResult, tmp)
  598. } else {
  599. reviewersResult = append(reviewersResult, tmp)
  600. }
  601. }
  602. }
  603. if len(pullReviews) > 0 {
  604. // Drop all non-existing users and teams from the reviews
  605. currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
  606. for _, item := range pullReviews {
  607. if item.Review.ReviewerID > 0 {
  608. if err = item.Review.LoadReviewer(ctx); err != nil {
  609. if user_model.IsErrUserNotExist(err) {
  610. continue
  611. }
  612. ctx.ServerError("LoadReviewer", err)
  613. return
  614. }
  615. item.User = item.Review.Reviewer
  616. } else if item.Review.ReviewerTeamID > 0 {
  617. if err = item.Review.LoadReviewerTeam(ctx); err != nil {
  618. if organization.IsErrTeamNotExist(err) {
  619. continue
  620. }
  621. ctx.ServerError("LoadReviewerTeam", err)
  622. return
  623. }
  624. item.Team = item.Review.ReviewerTeam
  625. } else {
  626. continue
  627. }
  628. currentPullReviewers = append(currentPullReviewers, item)
  629. }
  630. ctx.Data["PullReviewers"] = currentPullReviewers
  631. }
  632. if canChooseReviewer && reviewersResult != nil {
  633. preadded := len(reviewersResult)
  634. for _, reviewer := range reviewers {
  635. found := false
  636. reviewAddLoop:
  637. for _, tmp := range reviewersResult[:preadded] {
  638. if tmp.ItemID == reviewer.ID {
  639. tmp.User = reviewer
  640. found = true
  641. break reviewAddLoop
  642. }
  643. }
  644. if found {
  645. continue
  646. }
  647. reviewersResult = append(reviewersResult, &repoReviewerSelection{
  648. IsTeam: false,
  649. CanChange: true,
  650. User: reviewer,
  651. ItemID: reviewer.ID,
  652. })
  653. }
  654. ctx.Data["Reviewers"] = reviewersResult
  655. }
  656. if canChooseReviewer && teamReviewersResult != nil {
  657. preadded := len(teamReviewersResult)
  658. for _, team := range teamReviewers {
  659. found := false
  660. teamReviewAddLoop:
  661. for _, tmp := range teamReviewersResult[:preadded] {
  662. if tmp.ItemID == -team.ID {
  663. tmp.Team = team
  664. found = true
  665. break teamReviewAddLoop
  666. }
  667. }
  668. if found {
  669. continue
  670. }
  671. teamReviewersResult = append(teamReviewersResult, &repoReviewerSelection{
  672. IsTeam: true,
  673. CanChange: true,
  674. Team: team,
  675. ItemID: -team.ID,
  676. })
  677. }
  678. ctx.Data["TeamReviewers"] = teamReviewersResult
  679. }
  680. }
  681. // RetrieveRepoMetas find all the meta information of a repository
  682. func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label {
  683. if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
  684. return nil
  685. }
  686. labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
  687. if err != nil {
  688. ctx.ServerError("GetLabelsByRepoID", err)
  689. return nil
  690. }
  691. ctx.Data["Labels"] = labels
  692. if repo.Owner.IsOrganization() {
  693. orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
  694. if err != nil {
  695. return nil
  696. }
  697. ctx.Data["OrgLabels"] = orgLabels
  698. labels = append(labels, orgLabels...)
  699. }
  700. RetrieveRepoMilestonesAndAssignees(ctx, repo)
  701. if ctx.Written() {
  702. return nil
  703. }
  704. retrieveProjects(ctx, repo)
  705. if ctx.Written() {
  706. return nil
  707. }
  708. PrepareBranchList(ctx)
  709. if ctx.Written() {
  710. return nil
  711. }
  712. // Contains true if the user can create issue dependencies
  713. ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.Doer, isPull)
  714. return labels
  715. }
  716. func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) map[string]error {
  717. commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
  718. if err != nil {
  719. return nil
  720. }
  721. templateCandidates := make([]string, 0, 1+len(possibleFiles))
  722. if t := ctx.FormString("template"); t != "" {
  723. templateCandidates = append(templateCandidates, t)
  724. }
  725. templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback
  726. templateErrs := map[string]error{}
  727. for _, filename := range templateCandidates {
  728. if ok, _ := commit.HasFile(filename); !ok {
  729. continue
  730. }
  731. template, err := issue_template.UnmarshalFromCommit(commit, filename)
  732. if err != nil {
  733. templateErrs[filename] = err
  734. continue
  735. }
  736. ctx.Data[issueTemplateTitleKey] = template.Title
  737. ctx.Data[ctxDataKey] = template.Content
  738. if template.Type() == api.IssueTemplateTypeYaml {
  739. // Replace field default values by values from query
  740. for _, field := range template.Fields {
  741. fieldValue := ctx.FormString("field:" + field.ID)
  742. if fieldValue != "" {
  743. field.Attributes["value"] = fieldValue
  744. }
  745. }
  746. ctx.Data["Fields"] = template.Fields
  747. ctx.Data["TemplateFile"] = template.FileName
  748. }
  749. labelIDs := make([]string, 0, len(template.Labels))
  750. if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
  751. ctx.Data["Labels"] = repoLabels
  752. if ctx.Repo.Owner.IsOrganization() {
  753. if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
  754. ctx.Data["OrgLabels"] = orgLabels
  755. repoLabels = append(repoLabels, orgLabels...)
  756. }
  757. }
  758. for _, metaLabel := range template.Labels {
  759. for _, repoLabel := range repoLabels {
  760. if strings.EqualFold(repoLabel.Name, metaLabel) {
  761. repoLabel.IsChecked = true
  762. labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
  763. break
  764. }
  765. }
  766. }
  767. }
  768. if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
  769. template.Ref = git.BranchPrefix + template.Ref
  770. }
  771. ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
  772. ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
  773. ctx.Data["Reference"] = template.Ref
  774. ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName()
  775. return templateErrs
  776. }
  777. return templateErrs
  778. }
  779. // NewIssue render creating issue page
  780. func NewIssue(ctx *context.Context) {
  781. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  782. ctx.Data["PageIsIssueList"] = true
  783. ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
  784. ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
  785. title := ctx.FormString("title")
  786. ctx.Data["TitleQuery"] = title
  787. body := ctx.FormString("body")
  788. ctx.Data["BodyQuery"] = body
  789. isProjectsEnabled := ctx.Repo.CanRead(unit.TypeProjects)
  790. ctx.Data["IsProjectsEnabled"] = isProjectsEnabled
  791. ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
  792. upload.AddUploadContext(ctx, "comment")
  793. milestoneID := ctx.FormInt64("milestone")
  794. if milestoneID > 0 {
  795. milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
  796. if err != nil {
  797. log.Error("GetMilestoneByID: %d: %v", milestoneID, err)
  798. } else {
  799. ctx.Data["milestone_id"] = milestoneID
  800. ctx.Data["Milestone"] = milestone
  801. }
  802. }
  803. projectID := ctx.FormInt64("project")
  804. if projectID > 0 && isProjectsEnabled {
  805. project, err := project_model.GetProjectByID(ctx, projectID)
  806. if err != nil {
  807. log.Error("GetProjectByID: %d: %v", projectID, err)
  808. } else if project.RepoID != ctx.Repo.Repository.ID {
  809. log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID))
  810. } else {
  811. ctx.Data["project_id"] = projectID
  812. ctx.Data["Project"] = project
  813. }
  814. if len(ctx.Req.URL.Query().Get("project")) > 0 {
  815. ctx.Data["redirect_after_creation"] = "project"
  816. }
  817. }
  818. RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
  819. tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
  820. if err != nil {
  821. ctx.ServerError("GetTagNamesByRepoID", err)
  822. return
  823. }
  824. ctx.Data["Tags"] = tags
  825. _, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  826. if errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates); len(errs) > 0 {
  827. for k, v := range errs {
  828. templateErrs[k] = v
  829. }
  830. }
  831. if ctx.Written() {
  832. return
  833. }
  834. if len(templateErrs) > 0 {
  835. ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
  836. }
  837. ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
  838. ctx.HTML(http.StatusOK, tplIssueNew)
  839. }
  840. func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string {
  841. var files []string
  842. for k := range errs {
  843. files = append(files, k)
  844. }
  845. sort.Strings(files) // keep the output stable
  846. var lines []string
  847. for _, file := range files {
  848. lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file]))
  849. }
  850. flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  851. "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
  852. "Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)),
  853. "Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")),
  854. })
  855. if err != nil {
  856. log.Debug("render flash error: %v", err)
  857. flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates")
  858. }
  859. return flashError
  860. }
  861. // NewIssueChooseTemplate render creating issue from template page
  862. func NewIssueChooseTemplate(ctx *context.Context) {
  863. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  864. ctx.Data["PageIsIssueList"] = true
  865. issueTemplates, errs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  866. ctx.Data["IssueTemplates"] = issueTemplates
  867. if len(errs) > 0 {
  868. ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
  869. }
  870. if !issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) {
  871. // The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters.
  872. ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
  873. return
  874. }
  875. issueConfig, err := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  876. ctx.Data["IssueConfig"] = issueConfig
  877. ctx.Data["IssueConfigError"] = err // ctx.Flash.Err makes problems here
  878. ctx.Data["milestone"] = ctx.FormInt64("milestone")
  879. ctx.Data["project"] = ctx.FormInt64("project")
  880. ctx.HTML(http.StatusOK, tplIssueChoose)
  881. }
  882. // DeleteIssue deletes an issue
  883. func DeleteIssue(ctx *context.Context) {
  884. issue := GetActionIssue(ctx)
  885. if ctx.Written() {
  886. return
  887. }
  888. if err := issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil {
  889. ctx.ServerError("DeleteIssueByID", err)
  890. return
  891. }
  892. if issue.IsPull {
  893. ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.Link()), http.StatusSeeOther)
  894. return
  895. }
  896. ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.Link()), http.StatusSeeOther)
  897. }
  898. // ValidateRepoMetas check and returns repository's meta information
  899. func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
  900. var (
  901. repo = ctx.Repo.Repository
  902. err error
  903. )
  904. labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
  905. if ctx.Written() {
  906. return nil, nil, 0, 0
  907. }
  908. var labelIDs []int64
  909. hasSelected := false
  910. // Check labels.
  911. if len(form.LabelIDs) > 0 {
  912. labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
  913. if err != nil {
  914. return nil, nil, 0, 0
  915. }
  916. labelIDMark := make(container.Set[int64])
  917. labelIDMark.AddMultiple(labelIDs...)
  918. for i := range labels {
  919. if labelIDMark.Contains(labels[i].ID) {
  920. labels[i].IsChecked = true
  921. hasSelected = true
  922. }
  923. }
  924. }
  925. ctx.Data["Labels"] = labels
  926. ctx.Data["HasSelectedLabel"] = hasSelected
  927. ctx.Data["label_ids"] = form.LabelIDs
  928. // Check milestone.
  929. milestoneID := form.MilestoneID
  930. if milestoneID > 0 {
  931. milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
  932. if err != nil {
  933. ctx.ServerError("GetMilestoneByID", err)
  934. return nil, nil, 0, 0
  935. }
  936. if milestone.RepoID != repo.ID {
  937. ctx.ServerError("GetMilestoneByID", err)
  938. return nil, nil, 0, 0
  939. }
  940. ctx.Data["Milestone"] = milestone
  941. ctx.Data["milestone_id"] = milestoneID
  942. }
  943. if form.ProjectID > 0 {
  944. p, err := project_model.GetProjectByID(ctx, form.ProjectID)
  945. if err != nil {
  946. ctx.ServerError("GetProjectByID", err)
  947. return nil, nil, 0, 0
  948. }
  949. if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID {
  950. ctx.NotFound("", nil)
  951. return nil, nil, 0, 0
  952. }
  953. ctx.Data["Project"] = p
  954. ctx.Data["project_id"] = form.ProjectID
  955. }
  956. // Check assignees
  957. var assigneeIDs []int64
  958. if len(form.AssigneeIDs) > 0 {
  959. assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
  960. if err != nil {
  961. return nil, nil, 0, 0
  962. }
  963. // Check if the passed assignees actually exists and is assignable
  964. for _, aID := range assigneeIDs {
  965. assignee, err := user_model.GetUserByID(ctx, aID)
  966. if err != nil {
  967. ctx.ServerError("GetUserByID", err)
  968. return nil, nil, 0, 0
  969. }
  970. valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull)
  971. if err != nil {
  972. ctx.ServerError("CanBeAssigned", err)
  973. return nil, nil, 0, 0
  974. }
  975. if !valid {
  976. ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
  977. return nil, nil, 0, 0
  978. }
  979. }
  980. }
  981. // Keep the old assignee id thingy for compatibility reasons
  982. if form.AssigneeID > 0 {
  983. assigneeIDs = append(assigneeIDs, form.AssigneeID)
  984. }
  985. return labelIDs, assigneeIDs, milestoneID, form.ProjectID
  986. }
  987. // NewIssuePost response for creating new issue
  988. func NewIssuePost(ctx *context.Context) {
  989. form := web.GetForm(ctx).(*forms.CreateIssueForm)
  990. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  991. ctx.Data["PageIsIssueList"] = true
  992. ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
  993. ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
  994. ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
  995. upload.AddUploadContext(ctx, "comment")
  996. var (
  997. repo = ctx.Repo.Repository
  998. attachments []string
  999. )
  1000. labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
  1001. if ctx.Written() {
  1002. return
  1003. }
  1004. if setting.Attachment.Enabled {
  1005. attachments = form.Files
  1006. }
  1007. if ctx.HasError() {
  1008. ctx.JSONError(ctx.GetErrMsg())
  1009. return
  1010. }
  1011. if util.IsEmptyString(form.Title) {
  1012. ctx.JSONError(ctx.Tr("repo.issues.new.title_empty"))
  1013. return
  1014. }
  1015. content := form.Content
  1016. if filename := ctx.Req.Form.Get("template-file"); filename != "" {
  1017. if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {
  1018. content = issue_template.RenderToMarkdown(template, ctx.Req.Form)
  1019. }
  1020. }
  1021. issue := &issues_model.Issue{
  1022. RepoID: repo.ID,
  1023. Repo: repo,
  1024. Title: form.Title,
  1025. PosterID: ctx.Doer.ID,
  1026. Poster: ctx.Doer,
  1027. MilestoneID: milestoneID,
  1028. Content: content,
  1029. Ref: form.Ref,
  1030. }
  1031. if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
  1032. if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
  1033. ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
  1034. return
  1035. }
  1036. ctx.ServerError("NewIssue", err)
  1037. return
  1038. }
  1039. if projectID > 0 {
  1040. if !ctx.Repo.CanRead(unit.TypeProjects) {
  1041. // User must also be able to see the project.
  1042. ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
  1043. return
  1044. }
  1045. if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
  1046. ctx.ServerError("ChangeProjectAssign", err)
  1047. return
  1048. }
  1049. }
  1050. log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
  1051. if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 {
  1052. ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10))
  1053. } else {
  1054. ctx.JSONRedirect(issue.Link())
  1055. }
  1056. }
  1057. // roleDescriptor returns the Role Descriptor for a comment in/with the given repo, poster and issue
  1058. func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) {
  1059. if hasOriginalAuthor {
  1060. return issues_model.RoleDescriptorNone, nil
  1061. }
  1062. perm, err := access_model.GetUserRepoPermission(ctx, repo, poster)
  1063. if err != nil {
  1064. return issues_model.RoleDescriptorNone, err
  1065. }
  1066. // By default the poster has no roles on the comment.
  1067. roleDescriptor := issues_model.RoleDescriptorNone
  1068. // Check if the poster is owner of the repo.
  1069. if perm.IsOwner() {
  1070. // If the poster isn't a admin, enable the owner role.
  1071. if !poster.IsAdmin {
  1072. roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner)
  1073. } else {
  1074. // Otherwise check if poster is the real repo admin.
  1075. ok, err := access_model.IsUserRealRepoAdmin(repo, poster)
  1076. if err != nil {
  1077. return issues_model.RoleDescriptorNone, err
  1078. }
  1079. if ok {
  1080. roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner)
  1081. }
  1082. }
  1083. }
  1084. // Is the poster can write issues or pulls to the repo, enable the Writer role.
  1085. // Only enable this if the poster doesn't have the owner role already.
  1086. if !roleDescriptor.HasRole("Owner") && perm.CanWriteIssuesOrPulls(issue.IsPull) {
  1087. roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorWriter)
  1088. }
  1089. // If the poster is the actual poster of the issue, enable Poster role.
  1090. if issue.IsPoster(poster.ID) {
  1091. roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorPoster)
  1092. }
  1093. return roleDescriptor, nil
  1094. }
  1095. func getBranchData(ctx *context.Context, issue *issues_model.Issue) {
  1096. ctx.Data["BaseBranch"] = nil
  1097. ctx.Data["HeadBranch"] = nil
  1098. ctx.Data["HeadUserName"] = nil
  1099. ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName
  1100. if issue.IsPull {
  1101. pull := issue.PullRequest
  1102. ctx.Data["BaseBranch"] = pull.BaseBranch
  1103. ctx.Data["HeadBranch"] = pull.HeadBranch
  1104. ctx.Data["HeadUserName"] = pull.MustHeadUserName(ctx)
  1105. }
  1106. }
  1107. // ViewIssue render issue view page
  1108. func ViewIssue(ctx *context.Context) {
  1109. if ctx.Params(":type") == "issues" {
  1110. // If issue was requested we check if repo has external tracker and redirect
  1111. extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
  1112. if err == nil && extIssueUnit != nil {
  1113. if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
  1114. metas := ctx.Repo.Repository.ComposeMetas()
  1115. metas["index"] = ctx.Params(":index")
  1116. res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)
  1117. if err != nil {
  1118. log.Error("unable to expand template vars for issue url. issue: %s, err: %v", metas["index"], err)
  1119. ctx.ServerError("Expand", err)
  1120. return
  1121. }
  1122. ctx.Redirect(res)
  1123. return
  1124. }
  1125. } else if err != nil && !repo_model.IsErrUnitTypeNotExist(err) {
  1126. ctx.ServerError("GetUnit", err)
  1127. return
  1128. }
  1129. }
  1130. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  1131. if err != nil {
  1132. if issues_model.IsErrIssueNotExist(err) {
  1133. ctx.NotFound("GetIssueByIndex", err)
  1134. } else {
  1135. ctx.ServerError("GetIssueByIndex", err)
  1136. }
  1137. return
  1138. }
  1139. if issue.Repo == nil {
  1140. issue.Repo = ctx.Repo.Repository
  1141. }
  1142. // Make sure type and URL matches.
  1143. if ctx.Params(":type") == "issues" && issue.IsPull {
  1144. ctx.Redirect(issue.Link())
  1145. return
  1146. } else if ctx.Params(":type") == "pulls" && !issue.IsPull {
  1147. ctx.Redirect(issue.Link())
  1148. return
  1149. }
  1150. if issue.IsPull {
  1151. MustAllowPulls(ctx)
  1152. if ctx.Written() {
  1153. return
  1154. }
  1155. ctx.Data["PageIsPullList"] = true
  1156. ctx.Data["PageIsPullConversation"] = true
  1157. } else {
  1158. MustEnableIssues(ctx)
  1159. if ctx.Written() {
  1160. return
  1161. }
  1162. ctx.Data["PageIsIssueList"] = true
  1163. ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1164. }
  1165. if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
  1166. ctx.Data["IssueType"] = "pulls"
  1167. } else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) {
  1168. ctx.Data["IssueType"] = "issues"
  1169. } else {
  1170. ctx.Data["IssueType"] = "all"
  1171. }
  1172. ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
  1173. ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
  1174. upload.AddUploadContext(ctx, "comment")
  1175. if err = issue.LoadAttributes(ctx); err != nil {
  1176. ctx.ServerError("LoadAttributes", err)
  1177. return
  1178. }
  1179. if err = filterXRefComments(ctx, issue); err != nil {
  1180. ctx.ServerError("filterXRefComments", err)
  1181. return
  1182. }
  1183. ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
  1184. iw := new(issues_model.IssueWatch)
  1185. if ctx.Doer != nil {
  1186. iw.UserID = ctx.Doer.ID
  1187. iw.IssueID = issue.ID
  1188. iw.IsWatching, err = issues_model.CheckIssueWatch(ctx.Doer, issue)
  1189. if err != nil {
  1190. ctx.ServerError("CheckIssueWatch", err)
  1191. return
  1192. }
  1193. }
  1194. ctx.Data["IssueWatch"] = iw
  1195. issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  1196. URLPrefix: ctx.Repo.RepoLink,
  1197. Metas: ctx.Repo.Repository.ComposeMetas(),
  1198. GitRepo: ctx.Repo.GitRepo,
  1199. Ctx: ctx,
  1200. }, issue.Content)
  1201. if err != nil {
  1202. ctx.ServerError("RenderString", err)
  1203. return
  1204. }
  1205. repo := ctx.Repo.Repository
  1206. // Get more information if it's a pull request.
  1207. if issue.IsPull {
  1208. if issue.PullRequest.HasMerged {
  1209. ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged
  1210. PrepareMergedViewPullInfo(ctx, issue)
  1211. } else {
  1212. PrepareViewPullInfo(ctx, issue)
  1213. ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed
  1214. }
  1215. if ctx.Written() {
  1216. return
  1217. }
  1218. }
  1219. // Metas.
  1220. // Check labels.
  1221. labelIDMark := make(container.Set[int64])
  1222. for _, label := range issue.Labels {
  1223. labelIDMark.Add(label.ID)
  1224. }
  1225. labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
  1226. if err != nil {
  1227. ctx.ServerError("GetLabelsByRepoID", err)
  1228. return
  1229. }
  1230. ctx.Data["Labels"] = labels
  1231. if repo.Owner.IsOrganization() {
  1232. orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
  1233. if err != nil {
  1234. ctx.ServerError("GetLabelsByOrgID", err)
  1235. return
  1236. }
  1237. ctx.Data["OrgLabels"] = orgLabels
  1238. labels = append(labels, orgLabels...)
  1239. }
  1240. hasSelected := false
  1241. for i := range labels {
  1242. if labelIDMark.Contains(labels[i].ID) {
  1243. labels[i].IsChecked = true
  1244. hasSelected = true
  1245. }
  1246. }
  1247. ctx.Data["HasSelectedLabel"] = hasSelected
  1248. // Check milestone and assignee.
  1249. if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  1250. RetrieveRepoMilestonesAndAssignees(ctx, repo)
  1251. retrieveProjects(ctx, repo)
  1252. if ctx.Written() {
  1253. return
  1254. }
  1255. }
  1256. if issue.IsPull {
  1257. canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests)
  1258. if ctx.Doer != nil && ctx.IsSigned {
  1259. if !canChooseReviewer {
  1260. canChooseReviewer = ctx.Doer.ID == issue.PosterID
  1261. }
  1262. if !canChooseReviewer {
  1263. canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer)
  1264. if err != nil {
  1265. ctx.ServerError("IsOfficialReviewer", err)
  1266. return
  1267. }
  1268. }
  1269. }
  1270. RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
  1271. if ctx.Written() {
  1272. return
  1273. }
  1274. }
  1275. if ctx.IsSigned {
  1276. // Update issue-user.
  1277. if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
  1278. ctx.ServerError("ReadBy", err)
  1279. return
  1280. }
  1281. }
  1282. var (
  1283. role issues_model.RoleDescriptor
  1284. ok bool
  1285. marked = make(map[int64]issues_model.RoleDescriptor)
  1286. comment *issues_model.Comment
  1287. participants = make([]*user_model.User, 1, 10)
  1288. latestCloseCommentID int64
  1289. )
  1290. if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
  1291. if ctx.IsSigned {
  1292. // Deal with the stopwatch
  1293. ctx.Data["IsStopwatchRunning"] = issues_model.StopwatchExists(ctx.Doer.ID, issue.ID)
  1294. if !ctx.Data["IsStopwatchRunning"].(bool) {
  1295. var exists bool
  1296. var swIssue *issues_model.Issue
  1297. if exists, _, swIssue, err = issues_model.HasUserStopwatch(ctx, ctx.Doer.ID); err != nil {
  1298. ctx.ServerError("HasUserStopwatch", err)
  1299. return
  1300. }
  1301. ctx.Data["HasUserStopwatch"] = exists
  1302. if exists {
  1303. // Add warning if the user has already a stopwatch
  1304. // Add link to the issue of the already running stopwatch
  1305. ctx.Data["OtherStopwatchURL"] = swIssue.Link()
  1306. }
  1307. }
  1308. ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.Doer)
  1309. } else {
  1310. ctx.Data["CanUseTimetracker"] = false
  1311. }
  1312. if ctx.Data["WorkingUsers"], err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
  1313. ctx.ServerError("TotalTimes", err)
  1314. return
  1315. }
  1316. }
  1317. // Check if the user can use the dependencies
  1318. ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.Doer, issue.IsPull)
  1319. // check if dependencies can be created across repositories
  1320. ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
  1321. if issue.ShowRole, err = roleDescriptor(ctx, repo, issue.Poster, issue, issue.HasOriginalAuthor()); err != nil {
  1322. ctx.ServerError("roleDescriptor", err)
  1323. return
  1324. }
  1325. marked[issue.PosterID] = issue.ShowRole
  1326. // Render comments and and fetch participants.
  1327. participants[0] = issue.Poster
  1328. for _, comment = range issue.Comments {
  1329. comment.Issue = issue
  1330. if err := comment.LoadPoster(ctx); err != nil {
  1331. ctx.ServerError("LoadPoster", err)
  1332. return
  1333. }
  1334. if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
  1335. if err := comment.LoadAttachments(ctx); err != nil {
  1336. ctx.ServerError("LoadAttachments", err)
  1337. return
  1338. }
  1339. comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  1340. URLPrefix: ctx.Repo.RepoLink,
  1341. Metas: ctx.Repo.Repository.ComposeMetas(),
  1342. GitRepo: ctx.Repo.GitRepo,
  1343. Ctx: ctx,
  1344. }, comment.Content)
  1345. if err != nil {
  1346. ctx.ServerError("RenderString", err)
  1347. return
  1348. }
  1349. // Check tag.
  1350. role, ok = marked[comment.PosterID]
  1351. if ok {
  1352. comment.ShowRole = role
  1353. continue
  1354. }
  1355. comment.ShowRole, err = roleDescriptor(ctx, repo, comment.Poster, issue, comment.HasOriginalAuthor())
  1356. if err != nil {
  1357. ctx.ServerError("roleDescriptor", err)
  1358. return
  1359. }
  1360. marked[comment.PosterID] = comment.ShowRole
  1361. participants = addParticipant(comment.Poster, participants)
  1362. } else if comment.Type == issues_model.CommentTypeLabel {
  1363. if err = comment.LoadLabel(); err != nil {
  1364. ctx.ServerError("LoadLabel", err)
  1365. return
  1366. }
  1367. } else if comment.Type == issues_model.CommentTypeMilestone {
  1368. if err = comment.LoadMilestone(ctx); err != nil {
  1369. ctx.ServerError("LoadMilestone", err)
  1370. return
  1371. }
  1372. ghostMilestone := &issues_model.Milestone{
  1373. ID: -1,
  1374. Name: ctx.Tr("repo.issues.deleted_milestone"),
  1375. }
  1376. if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
  1377. comment.OldMilestone = ghostMilestone
  1378. }
  1379. if comment.MilestoneID > 0 && comment.Milestone == nil {
  1380. comment.Milestone = ghostMilestone
  1381. }
  1382. } else if comment.Type == issues_model.CommentTypeProject {
  1383. if err = comment.LoadProject(); err != nil {
  1384. ctx.ServerError("LoadProject", err)
  1385. return
  1386. }
  1387. ghostProject := &project_model.Project{
  1388. ID: -1,
  1389. Title: ctx.Tr("repo.issues.deleted_project"),
  1390. }
  1391. if comment.OldProjectID > 0 && comment.OldProject == nil {
  1392. comment.OldProject = ghostProject
  1393. }
  1394. if comment.ProjectID > 0 && comment.Project == nil {
  1395. comment.Project = ghostProject
  1396. }
  1397. } else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest {
  1398. if err = comment.LoadAssigneeUserAndTeam(); err != nil {
  1399. ctx.ServerError("LoadAssigneeUserAndTeam", err)
  1400. return
  1401. }
  1402. } else if comment.Type == issues_model.CommentTypeRemoveDependency || comment.Type == issues_model.CommentTypeAddDependency {
  1403. if err = comment.LoadDepIssueDetails(); err != nil {
  1404. if !issues_model.IsErrIssueNotExist(err) {
  1405. ctx.ServerError("LoadDepIssueDetails", err)
  1406. return
  1407. }
  1408. }
  1409. } else if comment.Type.HasContentSupport() {
  1410. comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  1411. URLPrefix: ctx.Repo.RepoLink,
  1412. Metas: ctx.Repo.Repository.ComposeMetas(),
  1413. GitRepo: ctx.Repo.GitRepo,
  1414. Ctx: ctx,
  1415. }, comment.Content)
  1416. if err != nil {
  1417. ctx.ServerError("RenderString", err)
  1418. return
  1419. }
  1420. if err = comment.LoadReview(); err != nil && !issues_model.IsErrReviewNotExist(err) {
  1421. ctx.ServerError("LoadReview", err)
  1422. return
  1423. }
  1424. participants = addParticipant(comment.Poster, participants)
  1425. if comment.Review == nil {
  1426. continue
  1427. }
  1428. if err = comment.Review.LoadAttributes(ctx); err != nil {
  1429. if !user_model.IsErrUserNotExist(err) {
  1430. ctx.ServerError("Review.LoadAttributes", err)
  1431. return
  1432. }
  1433. comment.Review.Reviewer = user_model.NewGhostUser()
  1434. }
  1435. if err = comment.Review.LoadCodeComments(ctx); err != nil {
  1436. ctx.ServerError("Review.LoadCodeComments", err)
  1437. return
  1438. }
  1439. for _, codeComments := range comment.Review.CodeComments {
  1440. for _, lineComments := range codeComments {
  1441. for _, c := range lineComments {
  1442. // Check tag.
  1443. role, ok = marked[c.PosterID]
  1444. if ok {
  1445. c.ShowRole = role
  1446. continue
  1447. }
  1448. c.ShowRole, err = roleDescriptor(ctx, repo, c.Poster, issue, c.HasOriginalAuthor())
  1449. if err != nil {
  1450. ctx.ServerError("roleDescriptor", err)
  1451. return
  1452. }
  1453. marked[c.PosterID] = c.ShowRole
  1454. participants = addParticipant(c.Poster, participants)
  1455. }
  1456. }
  1457. }
  1458. if err = comment.LoadResolveDoer(); err != nil {
  1459. ctx.ServerError("LoadResolveDoer", err)
  1460. return
  1461. }
  1462. } else if comment.Type == issues_model.CommentTypePullRequestPush {
  1463. participants = addParticipant(comment.Poster, participants)
  1464. if err = comment.LoadPushCommits(ctx); err != nil {
  1465. ctx.ServerError("LoadPushCommits", err)
  1466. return
  1467. }
  1468. } else if comment.Type == issues_model.CommentTypeAddTimeManual ||
  1469. comment.Type == issues_model.CommentTypeStopTracking ||
  1470. comment.Type == issues_model.CommentTypeDeleteTimeManual {
  1471. // drop error since times could be pruned from DB..
  1472. _ = comment.LoadTime()
  1473. if comment.Content != "" {
  1474. // Content before v1.21 did store the formated string instead of seconds,
  1475. // so "|" is used as delimeter to mark the new format
  1476. if comment.Content[0] != '|' {
  1477. // handle old time comments that have formatted text stored
  1478. comment.RenderedContent = comment.Content
  1479. comment.Content = ""
  1480. } else {
  1481. // else it's just a duration in seconds to pass on to the frontend
  1482. comment.Content = comment.Content[1:]
  1483. }
  1484. }
  1485. }
  1486. if comment.Type == issues_model.CommentTypeClose || comment.Type == issues_model.CommentTypeMergePull {
  1487. // record ID of the latest closed/merged comment.
  1488. // if PR is closed, the comments whose type is CommentTypePullRequestPush(29) after latestCloseCommentID won't be rendered.
  1489. latestCloseCommentID = comment.ID
  1490. }
  1491. }
  1492. ctx.Data["LatestCloseCommentID"] = latestCloseCommentID
  1493. // Combine multiple label assignments into a single comment
  1494. combineLabelComments(issue)
  1495. getBranchData(ctx, issue)
  1496. if issue.IsPull {
  1497. pull := issue.PullRequest
  1498. pull.Issue = issue
  1499. canDelete := false
  1500. ctx.Data["AllowMerge"] = false
  1501. if ctx.IsSigned {
  1502. if err := pull.LoadHeadRepo(ctx); err != nil {
  1503. log.Error("LoadHeadRepo: %v", err)
  1504. } else if pull.HeadRepo != nil {
  1505. perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
  1506. if err != nil {
  1507. ctx.ServerError("GetUserRepoPermission", err)
  1508. return
  1509. }
  1510. if perm.CanWrite(unit.TypeCode) {
  1511. // Check if branch is not protected
  1512. if pull.HeadBranch != pull.HeadRepo.DefaultBranch {
  1513. if protected, err := git_model.IsBranchProtected(ctx, pull.HeadRepo.ID, pull.HeadBranch); err != nil {
  1514. log.Error("IsProtectedBranch: %v", err)
  1515. } else if !protected {
  1516. canDelete = true
  1517. ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup"
  1518. }
  1519. }
  1520. ctx.Data["CanWriteToHeadRepo"] = true
  1521. }
  1522. }
  1523. if err := pull.LoadBaseRepo(ctx); err != nil {
  1524. log.Error("LoadBaseRepo: %v", err)
  1525. }
  1526. perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer)
  1527. if err != nil {
  1528. ctx.ServerError("GetUserRepoPermission", err)
  1529. return
  1530. }
  1531. ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer)
  1532. if err != nil {
  1533. ctx.ServerError("IsUserAllowedToMerge", err)
  1534. return
  1535. }
  1536. if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(issue, ctx.Doer); err != nil {
  1537. ctx.ServerError("CanMarkConversation", err)
  1538. return
  1539. }
  1540. }
  1541. prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
  1542. if err != nil {
  1543. ctx.ServerError("GetUnit", err)
  1544. return
  1545. }
  1546. prConfig := prUnit.PullRequestsConfig()
  1547. var mergeStyle repo_model.MergeStyle
  1548. // Check correct values and select default
  1549. if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
  1550. !prConfig.IsMergeStyleAllowed(ms) {
  1551. defaultMergeStyle := prConfig.GetDefaultMergeStyle()
  1552. if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
  1553. mergeStyle = defaultMergeStyle
  1554. } else if prConfig.AllowMerge {
  1555. mergeStyle = repo_model.MergeStyleMerge
  1556. } else if prConfig.AllowRebase {
  1557. mergeStyle = repo_model.MergeStyleRebase
  1558. } else if prConfig.AllowRebaseMerge {
  1559. mergeStyle = repo_model.MergeStyleRebaseMerge
  1560. } else if prConfig.AllowSquash {
  1561. mergeStyle = repo_model.MergeStyleSquash
  1562. } else if prConfig.AllowManualMerge {
  1563. mergeStyle = repo_model.MergeStyleManuallyMerged
  1564. }
  1565. }
  1566. ctx.Data["MergeStyle"] = mergeStyle
  1567. defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
  1568. if err != nil {
  1569. ctx.ServerError("GetDefaultMergeMessage", err)
  1570. return
  1571. }
  1572. ctx.Data["DefaultMergeMessage"] = defaultMergeMessage
  1573. ctx.Data["DefaultMergeBody"] = defaultMergeBody
  1574. defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
  1575. if err != nil {
  1576. ctx.ServerError("GetDefaultSquashMergeMessage", err)
  1577. return
  1578. }
  1579. ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage
  1580. ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody
  1581. pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
  1582. if err != nil {
  1583. ctx.ServerError("LoadProtectedBranch", err)
  1584. return
  1585. }
  1586. ctx.Data["ShowMergeInstructions"] = true
  1587. if pb != nil {
  1588. pb.Repo = pull.BaseRepo
  1589. var showMergeInstructions bool
  1590. if ctx.Doer != nil {
  1591. showMergeInstructions = pb.CanUserPush(ctx, ctx.Doer)
  1592. }
  1593. ctx.Data["ProtectedBranch"] = pb
  1594. ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull)
  1595. ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull)
  1596. ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull)
  1597. ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull)
  1598. ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull)
  1599. ctx.Data["RequireSigned"] = pb.RequireSignedCommits
  1600. ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
  1601. ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
  1602. ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
  1603. ctx.Data["ShowMergeInstructions"] = showMergeInstructions
  1604. }
  1605. ctx.Data["WillSign"] = false
  1606. if ctx.Doer != nil {
  1607. sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName())
  1608. ctx.Data["WillSign"] = sign
  1609. ctx.Data["SigningKey"] = key
  1610. if err != nil {
  1611. if asymkey_service.IsErrWontSign(err) {
  1612. ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason
  1613. } else {
  1614. ctx.Data["WontSignReason"] = "error"
  1615. log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
  1616. }
  1617. }
  1618. } else {
  1619. ctx.Data["WontSignReason"] = "not_signed_in"
  1620. }
  1621. isPullBranchDeletable := canDelete &&
  1622. pull.HeadRepo != nil &&
  1623. git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.HeadBranch) &&
  1624. (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
  1625. if isPullBranchDeletable && pull.HasMerged {
  1626. exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch)
  1627. if err != nil {
  1628. ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
  1629. return
  1630. }
  1631. isPullBranchDeletable = !exist
  1632. }
  1633. ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable
  1634. stillCanManualMerge := func() bool {
  1635. if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
  1636. return false
  1637. }
  1638. if pull.CanAutoMerge() || pull.IsWorkInProgress() || pull.IsChecking() {
  1639. return false
  1640. }
  1641. if (ctx.Doer.IsAdmin || ctx.Repo.IsAdmin()) && prConfig.AllowManualMerge {
  1642. return true
  1643. }
  1644. return false
  1645. }
  1646. ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
  1647. // Check if there is a pending pr merge
  1648. ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
  1649. if err != nil {
  1650. ctx.ServerError("GetScheduledMergeByPullID", err)
  1651. return
  1652. }
  1653. }
  1654. // Get Dependencies
  1655. blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
  1656. if err != nil {
  1657. ctx.ServerError("BlockedByDependencies", err)
  1658. return
  1659. }
  1660. ctx.Data["BlockedByDependencies"], ctx.Data["BlockedByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blockedBy)
  1661. if ctx.Written() {
  1662. return
  1663. }
  1664. blocking, err := issue.BlockingDependencies(ctx)
  1665. if err != nil {
  1666. ctx.ServerError("BlockingDependencies", err)
  1667. return
  1668. }
  1669. ctx.Data["BlockingDependencies"], ctx.Data["BlockingByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking)
  1670. if ctx.Written() {
  1671. return
  1672. }
  1673. var pinAllowed bool
  1674. if !issue.IsPinned() {
  1675. pinAllowed, err = issues_model.IsNewPinAllowed(ctx, issue.RepoID, issue.IsPull)
  1676. if err != nil {
  1677. ctx.ServerError("IsNewPinAllowed", err)
  1678. return
  1679. }
  1680. } else {
  1681. pinAllowed = true
  1682. }
  1683. ctx.Data["Participants"] = participants
  1684. ctx.Data["NumParticipants"] = len(participants)
  1685. ctx.Data["Issue"] = issue
  1686. ctx.Data["Reference"] = issue.Ref
  1687. ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string))
  1688. ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
  1689. ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
  1690. ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects)
  1691. ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
  1692. ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
  1693. ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName()
  1694. ctx.Data["NewPinAllowed"] = pinAllowed
  1695. ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0
  1696. var hiddenCommentTypes *big.Int
  1697. if ctx.IsSigned {
  1698. val, err := user_model.GetUserSetting(ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
  1699. if err != nil {
  1700. ctx.ServerError("GetUserSetting", err)
  1701. return
  1702. }
  1703. hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here
  1704. }
  1705. ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool {
  1706. return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0
  1707. }
  1708. // For sidebar
  1709. PrepareBranchList(ctx)
  1710. if ctx.Written() {
  1711. return
  1712. }
  1713. tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
  1714. if err != nil {
  1715. ctx.ServerError("GetTagNamesByRepoID", err)
  1716. return
  1717. }
  1718. ctx.Data["Tags"] = tags
  1719. ctx.HTML(http.StatusOK, tplIssueView)
  1720. }
  1721. // checkBlockedByIssues return canRead and notPermitted
  1722. func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.DependencyInfo) (canRead, notPermitted []*issues_model.DependencyInfo) {
  1723. var (
  1724. lastRepoID int64
  1725. lastPerm access_model.Permission
  1726. )
  1727. for i, blocker := range blockers {
  1728. // Get the permissions for this repository
  1729. perm := lastPerm
  1730. if lastRepoID != blocker.Repository.ID {
  1731. if blocker.Repository.ID == ctx.Repo.Repository.ID {
  1732. perm = ctx.Repo.Permission
  1733. } else {
  1734. var err error
  1735. perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
  1736. if err != nil {
  1737. ctx.ServerError("GetUserRepoPermission", err)
  1738. return nil, nil
  1739. }
  1740. }
  1741. lastRepoID = blocker.Repository.ID
  1742. }
  1743. // check permission
  1744. if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
  1745. blockers[len(notPermitted)], blockers[i] = blocker, blockers[len(notPermitted)]
  1746. notPermitted = blockers[:len(notPermitted)+1]
  1747. }
  1748. }
  1749. blockers = blockers[len(notPermitted):]
  1750. sortDependencyInfo(blockers)
  1751. sortDependencyInfo(notPermitted)
  1752. return blockers, notPermitted
  1753. }
  1754. func sortDependencyInfo(blockers []*issues_model.DependencyInfo) {
  1755. sort.Slice(blockers, func(i, j int) bool {
  1756. if blockers[i].RepoID == blockers[j].RepoID {
  1757. return blockers[i].Issue.CreatedUnix < blockers[j].Issue.CreatedUnix
  1758. }
  1759. return blockers[i].RepoID < blockers[j].RepoID
  1760. })
  1761. }
  1762. // GetActionIssue will return the issue which is used in the context.
  1763. func GetActionIssue(ctx *context.Context) *issues_model.Issue {
  1764. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  1765. if err != nil {
  1766. ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
  1767. return nil
  1768. }
  1769. issue.Repo = ctx.Repo.Repository
  1770. checkIssueRights(ctx, issue)
  1771. if ctx.Written() {
  1772. return nil
  1773. }
  1774. if err = issue.LoadAttributes(ctx); err != nil {
  1775. ctx.ServerError("LoadAttributes", err)
  1776. return nil
  1777. }
  1778. return issue
  1779. }
  1780. func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) {
  1781. if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) ||
  1782. !issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
  1783. ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
  1784. }
  1785. }
  1786. func getActionIssues(ctx *context.Context) issues_model.IssueList {
  1787. commaSeparatedIssueIDs := ctx.FormString("issue_ids")
  1788. if len(commaSeparatedIssueIDs) == 0 {
  1789. return nil
  1790. }
  1791. issueIDs := make([]int64, 0, 10)
  1792. for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
  1793. issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
  1794. if err != nil {
  1795. ctx.ServerError("ParseInt", err)
  1796. return nil
  1797. }
  1798. issueIDs = append(issueIDs, issueID)
  1799. }
  1800. issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
  1801. if err != nil {
  1802. ctx.ServerError("GetIssuesByIDs", err)
  1803. return nil
  1804. }
  1805. // Check access rights for all issues
  1806. issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues)
  1807. prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
  1808. for _, issue := range issues {
  1809. if issue.RepoID != ctx.Repo.Repository.ID {
  1810. ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect"))
  1811. return nil
  1812. }
  1813. if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
  1814. ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
  1815. return nil
  1816. }
  1817. if err = issue.LoadAttributes(ctx); err != nil {
  1818. ctx.ServerError("LoadAttributes", err)
  1819. return nil
  1820. }
  1821. }
  1822. return issues
  1823. }
  1824. // GetIssueInfo get an issue of a repository
  1825. func GetIssueInfo(ctx *context.Context) {
  1826. issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  1827. if err != nil {
  1828. if issues_model.IsErrIssueNotExist(err) {
  1829. ctx.Error(http.StatusNotFound)
  1830. } else {
  1831. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
  1832. }
  1833. return
  1834. }
  1835. if issue.IsPull {
  1836. // Need to check if Pulls are enabled and we can read Pulls
  1837. if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
  1838. ctx.Error(http.StatusNotFound)
  1839. return
  1840. }
  1841. } else {
  1842. // Need to check if Issues are enabled and we can read Issues
  1843. if !ctx.Repo.CanRead(unit.TypeIssues) {
  1844. ctx.Error(http.StatusNotFound)
  1845. return
  1846. }
  1847. }
  1848. ctx.JSON(http.StatusOK, convert.ToIssue(ctx, issue))
  1849. }
  1850. // UpdateIssueTitle change issue's title
  1851. func UpdateIssueTitle(ctx *context.Context) {
  1852. issue := GetActionIssue(ctx)
  1853. if ctx.Written() {
  1854. return
  1855. }
  1856. if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
  1857. ctx.Error(http.StatusForbidden)
  1858. return
  1859. }
  1860. title := ctx.FormTrim("title")
  1861. if len(title) == 0 {
  1862. ctx.Error(http.StatusNoContent)
  1863. return
  1864. }
  1865. if err := issue_service.ChangeTitle(ctx, issue, ctx.Doer, title); err != nil {
  1866. ctx.ServerError("ChangeTitle", err)
  1867. return
  1868. }
  1869. ctx.JSON(http.StatusOK, map[string]any{
  1870. "title": issue.Title,
  1871. })
  1872. }
  1873. // UpdateIssueRef change issue's ref (branch)
  1874. func UpdateIssueRef(ctx *context.Context) {
  1875. issue := GetActionIssue(ctx)
  1876. if ctx.Written() {
  1877. return
  1878. }
  1879. if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
  1880. ctx.Error(http.StatusForbidden)
  1881. return
  1882. }
  1883. ref := ctx.FormTrim("ref")
  1884. if err := issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, ref); err != nil {
  1885. ctx.ServerError("ChangeRef", err)
  1886. return
  1887. }
  1888. ctx.JSON(http.StatusOK, map[string]any{
  1889. "ref": ref,
  1890. })
  1891. }
  1892. // UpdateIssueContent change issue's content
  1893. func UpdateIssueContent(ctx *context.Context) {
  1894. issue := GetActionIssue(ctx)
  1895. if ctx.Written() {
  1896. return
  1897. }
  1898. if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
  1899. ctx.Error(http.StatusForbidden)
  1900. return
  1901. }
  1902. if err := issue_service.ChangeContent(issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
  1903. ctx.ServerError("ChangeContent", err)
  1904. return
  1905. }
  1906. // when update the request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
  1907. if !ctx.FormBool("ignore_attachments") {
  1908. if err := updateAttachments(ctx, issue, ctx.FormStrings("files[]")); err != nil {
  1909. ctx.ServerError("UpdateAttachments", err)
  1910. return
  1911. }
  1912. }
  1913. content, err := markdown.RenderString(&markup.RenderContext{
  1914. URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
  1915. Metas: ctx.Repo.Repository.ComposeMetas(),
  1916. GitRepo: ctx.Repo.GitRepo,
  1917. Ctx: ctx,
  1918. }, issue.Content)
  1919. if err != nil {
  1920. ctx.ServerError("RenderString", err)
  1921. return
  1922. }
  1923. ctx.JSON(http.StatusOK, map[string]any{
  1924. "content": content,
  1925. "attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
  1926. })
  1927. }
  1928. // UpdateIssueDeadline updates an issue deadline
  1929. func UpdateIssueDeadline(ctx *context.Context) {
  1930. form := web.GetForm(ctx).(*api.EditDeadlineOption)
  1931. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  1932. if err != nil {
  1933. if issues_model.IsErrIssueNotExist(err) {
  1934. ctx.NotFound("GetIssueByIndex", err)
  1935. } else {
  1936. ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
  1937. }
  1938. return
  1939. }
  1940. if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  1941. ctx.Error(http.StatusForbidden, "", "Not repo writer")
  1942. return
  1943. }
  1944. var deadlineUnix timeutil.TimeStamp
  1945. var deadline time.Time
  1946. if form.Deadline != nil && !form.Deadline.IsZero() {
  1947. deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  1948. 23, 59, 59, 0, time.Local)
  1949. deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  1950. }
  1951. if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
  1952. ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
  1953. return
  1954. }
  1955. ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
  1956. }
  1957. // UpdateIssueMilestone change issue's milestone
  1958. func UpdateIssueMilestone(ctx *context.Context) {
  1959. issues := getActionIssues(ctx)
  1960. if ctx.Written() {
  1961. return
  1962. }
  1963. milestoneID := ctx.FormInt64("id")
  1964. for _, issue := range issues {
  1965. oldMilestoneID := issue.MilestoneID
  1966. if oldMilestoneID == milestoneID {
  1967. continue
  1968. }
  1969. issue.MilestoneID = milestoneID
  1970. if err := issue_service.ChangeMilestoneAssign(issue, ctx.Doer, oldMilestoneID); err != nil {
  1971. ctx.ServerError("ChangeMilestoneAssign", err)
  1972. return
  1973. }
  1974. }
  1975. ctx.JSONOK()
  1976. }
  1977. // UpdateIssueAssignee change issue's or pull's assignee
  1978. func UpdateIssueAssignee(ctx *context.Context) {
  1979. issues := getActionIssues(ctx)
  1980. if ctx.Written() {
  1981. return
  1982. }
  1983. assigneeID := ctx.FormInt64("id")
  1984. action := ctx.FormString("action")
  1985. for _, issue := range issues {
  1986. switch action {
  1987. case "clear":
  1988. if err := issue_service.DeleteNotPassedAssignee(ctx, issue, ctx.Doer, []*user_model.User{}); err != nil {
  1989. ctx.ServerError("ClearAssignees", err)
  1990. return
  1991. }
  1992. default:
  1993. assignee, err := user_model.GetUserByID(ctx, assigneeID)
  1994. if err != nil {
  1995. ctx.ServerError("GetUserByID", err)
  1996. return
  1997. }
  1998. valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull)
  1999. if err != nil {
  2000. ctx.ServerError("canBeAssigned", err)
  2001. return
  2002. }
  2003. if !valid {
  2004. ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
  2005. return
  2006. }
  2007. _, _, err = issue_service.ToggleAssignee(ctx, issue, ctx.Doer, assigneeID)
  2008. if err != nil {
  2009. ctx.ServerError("ToggleAssignee", err)
  2010. return
  2011. }
  2012. }
  2013. }
  2014. ctx.JSONOK()
  2015. }
  2016. // UpdatePullReviewRequest add or remove review request
  2017. func UpdatePullReviewRequest(ctx *context.Context) {
  2018. issues := getActionIssues(ctx)
  2019. if ctx.Written() {
  2020. return
  2021. }
  2022. reviewID := ctx.FormInt64("id")
  2023. action := ctx.FormString("action")
  2024. // TODO: Not support 'clear' now
  2025. if action != "attach" && action != "detach" {
  2026. ctx.Status(http.StatusForbidden)
  2027. return
  2028. }
  2029. for _, issue := range issues {
  2030. if err := issue.LoadRepo(ctx); err != nil {
  2031. ctx.ServerError("issue.LoadRepo", err)
  2032. return
  2033. }
  2034. if !issue.IsPull {
  2035. log.Warn(
  2036. "UpdatePullReviewRequest: refusing to add review request for non-PR issue %-v#%d",
  2037. issue.Repo, issue.Index,
  2038. )
  2039. ctx.Status(http.StatusForbidden)
  2040. return
  2041. }
  2042. if reviewID < 0 {
  2043. // negative reviewIDs represent team requests
  2044. if err := issue.Repo.LoadOwner(ctx); err != nil {
  2045. ctx.ServerError("issue.Repo.LoadOwner", err)
  2046. return
  2047. }
  2048. if !issue.Repo.Owner.IsOrganization() {
  2049. log.Warn(
  2050. "UpdatePullReviewRequest: refusing to add team review request for %s#%d owned by non organization UID[%d]",
  2051. issue.Repo.FullName(), issue.Index, issue.Repo.ID,
  2052. )
  2053. ctx.Status(http.StatusForbidden)
  2054. return
  2055. }
  2056. team, err := organization.GetTeamByID(ctx, -reviewID)
  2057. if err != nil {
  2058. ctx.ServerError("GetTeamByID", err)
  2059. return
  2060. }
  2061. if team.OrgID != issue.Repo.OwnerID {
  2062. log.Warn(
  2063. "UpdatePullReviewRequest: refusing to add team review request for UID[%d] team %s to %s#%d owned by UID[%d]",
  2064. team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID)
  2065. ctx.Status(http.StatusForbidden)
  2066. return
  2067. }
  2068. err = issue_service.IsValidTeamReviewRequest(ctx, team, ctx.Doer, action == "attach", issue)
  2069. if err != nil {
  2070. if issues_model.IsErrNotValidReviewRequest(err) {
  2071. log.Warn(
  2072. "UpdatePullReviewRequest: refusing to add invalid team review request for UID[%d] team %s to %s#%d owned by UID[%d]: Error: %v",
  2073. team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID,
  2074. err,
  2075. )
  2076. ctx.Status(http.StatusForbidden)
  2077. return
  2078. }
  2079. ctx.ServerError("IsValidTeamReviewRequest", err)
  2080. return
  2081. }
  2082. _, err = issue_service.TeamReviewRequest(ctx, issue, ctx.Doer, team, action == "attach")
  2083. if err != nil {
  2084. ctx.ServerError("TeamReviewRequest", err)
  2085. return
  2086. }
  2087. continue
  2088. }
  2089. reviewer, err := user_model.GetUserByID(ctx, reviewID)
  2090. if err != nil {
  2091. if user_model.IsErrUserNotExist(err) {
  2092. log.Warn(
  2093. "UpdatePullReviewRequest: requested reviewer [%d] for %-v to %-v#%d is not exist: Error: %v",
  2094. reviewID, issue.Repo, issue.Index,
  2095. err,
  2096. )
  2097. ctx.Status(http.StatusForbidden)
  2098. return
  2099. }
  2100. ctx.ServerError("GetUserByID", err)
  2101. return
  2102. }
  2103. err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, action == "attach", issue, nil)
  2104. if err != nil {
  2105. if issues_model.IsErrNotValidReviewRequest(err) {
  2106. log.Warn(
  2107. "UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v",
  2108. reviewer, issue.Repo, issue.Index,
  2109. err,
  2110. )
  2111. ctx.Status(http.StatusForbidden)
  2112. return
  2113. }
  2114. ctx.ServerError("isValidReviewRequest", err)
  2115. return
  2116. }
  2117. _, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach")
  2118. if err != nil {
  2119. ctx.ServerError("ReviewRequest", err)
  2120. return
  2121. }
  2122. }
  2123. ctx.JSONOK()
  2124. }
  2125. // SearchIssues searches for issues across the repositories that the user has access to
  2126. func SearchIssues(ctx *context.Context) {
  2127. before, since, err := context.GetQueryBeforeSince(ctx.Base)
  2128. if err != nil {
  2129. ctx.Error(http.StatusUnprocessableEntity, err.Error())
  2130. return
  2131. }
  2132. var isClosed util.OptionalBool
  2133. switch ctx.FormString("state") {
  2134. case "closed":
  2135. isClosed = util.OptionalBoolTrue
  2136. case "all":
  2137. isClosed = util.OptionalBoolNone
  2138. default:
  2139. isClosed = util.OptionalBoolFalse
  2140. }
  2141. // find repos user can access (for issue search)
  2142. opts := &repo_model.SearchRepoOptions{
  2143. Private: false,
  2144. AllPublic: true,
  2145. TopicOnly: false,
  2146. Collaborate: util.OptionalBoolNone,
  2147. // This needs to be a column that is not nil in fixtures or
  2148. // MySQL will return different results when sorting by null in some cases
  2149. OrderBy: db.SearchOrderByAlphabetically,
  2150. Actor: ctx.Doer,
  2151. }
  2152. if ctx.IsSigned {
  2153. opts.Private = true
  2154. opts.AllLimited = true
  2155. }
  2156. if ctx.FormString("owner") != "" {
  2157. owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
  2158. if err != nil {
  2159. if user_model.IsErrUserNotExist(err) {
  2160. ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
  2161. } else {
  2162. ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
  2163. }
  2164. return
  2165. }
  2166. opts.OwnerID = owner.ID
  2167. opts.AllLimited = false
  2168. opts.AllPublic = false
  2169. opts.Collaborate = util.OptionalBoolFalse
  2170. }
  2171. if ctx.FormString("team") != "" {
  2172. if ctx.FormString("owner") == "" {
  2173. ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
  2174. return
  2175. }
  2176. team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
  2177. if err != nil {
  2178. if organization.IsErrTeamNotExist(err) {
  2179. ctx.Error(http.StatusBadRequest, "Team not found", err.Error())
  2180. } else {
  2181. ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
  2182. }
  2183. return
  2184. }
  2185. opts.TeamID = team.ID
  2186. }
  2187. repoCond := repo_model.SearchRepositoryCondition(opts)
  2188. repoIDs, _, err := repo_model.SearchRepositoryIDs(opts)
  2189. if err != nil {
  2190. ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
  2191. return
  2192. }
  2193. var issues []*issues_model.Issue
  2194. var filteredCount int64
  2195. keyword := ctx.FormTrim("q")
  2196. if strings.IndexByte(keyword, 0) >= 0 {
  2197. keyword = ""
  2198. }
  2199. var issueIDs []int64
  2200. if len(keyword) > 0 && len(repoIDs) > 0 {
  2201. if issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, repoIDs, keyword, ctx.FormString("state")); err != nil {
  2202. ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err.Error())
  2203. return
  2204. }
  2205. }
  2206. var isPull util.OptionalBool
  2207. switch ctx.FormString("type") {
  2208. case "pulls":
  2209. isPull = util.OptionalBoolTrue
  2210. case "issues":
  2211. isPull = util.OptionalBoolFalse
  2212. default:
  2213. isPull = util.OptionalBoolNone
  2214. }
  2215. labels := ctx.FormTrim("labels")
  2216. var includedLabelNames []string
  2217. if len(labels) > 0 {
  2218. includedLabelNames = strings.Split(labels, ",")
  2219. }
  2220. milestones := ctx.FormTrim("milestones")
  2221. var includedMilestones []string
  2222. if len(milestones) > 0 {
  2223. includedMilestones = strings.Split(milestones, ",")
  2224. }
  2225. projectID := ctx.FormInt64("project")
  2226. // this api is also used in UI,
  2227. // so the default limit is set to fit UI needs
  2228. limit := ctx.FormInt("limit")
  2229. if limit == 0 {
  2230. limit = setting.UI.IssuePagingNum
  2231. } else if limit > setting.API.MaxResponseItems {
  2232. limit = setting.API.MaxResponseItems
  2233. }
  2234. // Only fetch the issues if we either don't have a keyword or the search returned issues
  2235. // This would otherwise return all issues if no issues were found by the search.
  2236. if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 {
  2237. issuesOpt := &issues_model.IssuesOptions{
  2238. ListOptions: db.ListOptions{
  2239. Page: ctx.FormInt("page"),
  2240. PageSize: limit,
  2241. },
  2242. RepoCond: repoCond,
  2243. IsClosed: isClosed,
  2244. IssueIDs: issueIDs,
  2245. IncludedLabelNames: includedLabelNames,
  2246. IncludeMilestones: includedMilestones,
  2247. ProjectID: projectID,
  2248. SortType: "priorityrepo",
  2249. PriorityRepoID: ctx.FormInt64("priority_repo_id"),
  2250. IsPull: isPull,
  2251. UpdatedBeforeUnix: before,
  2252. UpdatedAfterUnix: since,
  2253. }
  2254. ctxUserID := int64(0)
  2255. if ctx.IsSigned {
  2256. ctxUserID = ctx.Doer.ID
  2257. }
  2258. // Filter for: Created by User, Assigned to User, Mentioning User, Review of User Requested
  2259. if ctx.FormBool("created") {
  2260. issuesOpt.PosterID = ctxUserID
  2261. }
  2262. if ctx.FormBool("assigned") {
  2263. issuesOpt.AssigneeID = ctxUserID
  2264. }
  2265. if ctx.FormBool("mentioned") {
  2266. issuesOpt.MentionedID = ctxUserID
  2267. }
  2268. if ctx.FormBool("review_requested") {
  2269. issuesOpt.ReviewRequestedID = ctxUserID
  2270. }
  2271. if ctx.FormBool("reviewed") {
  2272. issuesOpt.ReviewedID = ctxUserID
  2273. }
  2274. if issues, err = issues_model.Issues(ctx, issuesOpt); err != nil {
  2275. ctx.Error(http.StatusInternalServerError, "Issues", err.Error())
  2276. return
  2277. }
  2278. issuesOpt.ListOptions = db.ListOptions{
  2279. Page: -1,
  2280. }
  2281. if filteredCount, err = issues_model.CountIssues(ctx, issuesOpt); err != nil {
  2282. ctx.Error(http.StatusInternalServerError, "CountIssues", err.Error())
  2283. return
  2284. }
  2285. }
  2286. ctx.SetTotalCountHeader(filteredCount)
  2287. ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
  2288. }
  2289. func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
  2290. userName := ctx.FormString(queryName)
  2291. if len(userName) == 0 {
  2292. return 0
  2293. }
  2294. user, err := user_model.GetUserByName(ctx, userName)
  2295. if user_model.IsErrUserNotExist(err) {
  2296. ctx.NotFound("", err)
  2297. return 0
  2298. }
  2299. if err != nil {
  2300. ctx.Error(http.StatusInternalServerError, err.Error())
  2301. return 0
  2302. }
  2303. return user.ID
  2304. }
  2305. // ListIssues list the issues of a repository
  2306. func ListIssues(ctx *context.Context) {
  2307. before, since, err := context.GetQueryBeforeSince(ctx.Base)
  2308. if err != nil {
  2309. ctx.Error(http.StatusUnprocessableEntity, err.Error())
  2310. return
  2311. }
  2312. var isClosed util.OptionalBool
  2313. switch ctx.FormString("state") {
  2314. case "closed":
  2315. isClosed = util.OptionalBoolTrue
  2316. case "all":
  2317. isClosed = util.OptionalBoolNone
  2318. default:
  2319. isClosed = util.OptionalBoolFalse
  2320. }
  2321. var issues []*issues_model.Issue
  2322. var filteredCount int64
  2323. keyword := ctx.FormTrim("q")
  2324. if strings.IndexByte(keyword, 0) >= 0 {
  2325. keyword = ""
  2326. }
  2327. var issueIDs []int64
  2328. var labelIDs []int64
  2329. if len(keyword) > 0 {
  2330. issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, []int64{ctx.Repo.Repository.ID}, keyword, ctx.FormString("state"))
  2331. if err != nil {
  2332. ctx.Error(http.StatusInternalServerError, err.Error())
  2333. return
  2334. }
  2335. }
  2336. if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
  2337. labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
  2338. if err != nil {
  2339. ctx.Error(http.StatusInternalServerError, err.Error())
  2340. return
  2341. }
  2342. }
  2343. var mileIDs []int64
  2344. if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
  2345. for i := range part {
  2346. // uses names and fall back to ids
  2347. // non existent milestones are discarded
  2348. mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i])
  2349. if err == nil {
  2350. mileIDs = append(mileIDs, mile.ID)
  2351. continue
  2352. }
  2353. if !issues_model.IsErrMilestoneNotExist(err) {
  2354. ctx.Error(http.StatusInternalServerError, err.Error())
  2355. return
  2356. }
  2357. id, err := strconv.ParseInt(part[i], 10, 64)
  2358. if err != nil {
  2359. continue
  2360. }
  2361. mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id)
  2362. if err == nil {
  2363. mileIDs = append(mileIDs, mile.ID)
  2364. continue
  2365. }
  2366. if issues_model.IsErrMilestoneNotExist(err) {
  2367. continue
  2368. }
  2369. ctx.Error(http.StatusInternalServerError, err.Error())
  2370. }
  2371. }
  2372. projectID := ctx.FormInt64("project")
  2373. listOptions := db.ListOptions{
  2374. Page: ctx.FormInt("page"),
  2375. PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
  2376. }
  2377. var isPull util.OptionalBool
  2378. switch ctx.FormString("type") {
  2379. case "pulls":
  2380. isPull = util.OptionalBoolTrue
  2381. case "issues":
  2382. isPull = util.OptionalBoolFalse
  2383. default:
  2384. isPull = util.OptionalBoolNone
  2385. }
  2386. // FIXME: we should be more efficient here
  2387. createdByID := getUserIDForFilter(ctx, "created_by")
  2388. if ctx.Written() {
  2389. return
  2390. }
  2391. assignedByID := getUserIDForFilter(ctx, "assigned_by")
  2392. if ctx.Written() {
  2393. return
  2394. }
  2395. mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
  2396. if ctx.Written() {
  2397. return
  2398. }
  2399. // Only fetch the issues if we either don't have a keyword or the search returned issues
  2400. // This would otherwise return all issues if no issues were found by the search.
  2401. if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
  2402. issuesOpt := &issues_model.IssuesOptions{
  2403. ListOptions: listOptions,
  2404. RepoIDs: []int64{ctx.Repo.Repository.ID},
  2405. IsClosed: isClosed,
  2406. IssueIDs: issueIDs,
  2407. LabelIDs: labelIDs,
  2408. MilestoneIDs: mileIDs,
  2409. ProjectID: projectID,
  2410. IsPull: isPull,
  2411. UpdatedBeforeUnix: before,
  2412. UpdatedAfterUnix: since,
  2413. PosterID: createdByID,
  2414. AssigneeID: assignedByID,
  2415. MentionedID: mentionedByID,
  2416. }
  2417. if issues, err = issues_model.Issues(ctx, issuesOpt); err != nil {
  2418. ctx.Error(http.StatusInternalServerError, err.Error())
  2419. return
  2420. }
  2421. issuesOpt.ListOptions = db.ListOptions{
  2422. Page: -1,
  2423. }
  2424. if filteredCount, err = issues_model.CountIssues(ctx, issuesOpt); err != nil {
  2425. ctx.Error(http.StatusInternalServerError, err.Error())
  2426. return
  2427. }
  2428. }
  2429. ctx.SetTotalCountHeader(filteredCount)
  2430. ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
  2431. }
  2432. func BatchDeleteIssues(ctx *context.Context) {
  2433. issues := getActionIssues(ctx)
  2434. if ctx.Written() {
  2435. return
  2436. }
  2437. for _, issue := range issues {
  2438. if err := issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil {
  2439. ctx.ServerError("DeleteIssue", err)
  2440. return
  2441. }
  2442. }
  2443. ctx.JSONOK()
  2444. }
  2445. // UpdateIssueStatus change issue's status
  2446. func UpdateIssueStatus(ctx *context.Context) {
  2447. issues := getActionIssues(ctx)
  2448. if ctx.Written() {
  2449. return
  2450. }
  2451. var isClosed bool
  2452. switch action := ctx.FormString("action"); action {
  2453. case "open":
  2454. isClosed = false
  2455. case "close":
  2456. isClosed = true
  2457. default:
  2458. log.Warn("Unrecognized action: %s", action)
  2459. }
  2460. if _, err := issues.LoadRepositories(ctx); err != nil {
  2461. ctx.ServerError("LoadRepositories", err)
  2462. return
  2463. }
  2464. if err := issues.LoadPullRequests(ctx); err != nil {
  2465. ctx.ServerError("LoadPullRequests", err)
  2466. return
  2467. }
  2468. for _, issue := range issues {
  2469. if issue.IsPull && issue.PullRequest.HasMerged {
  2470. continue
  2471. }
  2472. if issue.IsClosed != isClosed {
  2473. if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
  2474. if issues_model.IsErrDependenciesLeft(err) {
  2475. ctx.JSON(http.StatusPreconditionFailed, map[string]any{
  2476. "error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index),
  2477. })
  2478. return
  2479. }
  2480. ctx.ServerError("ChangeStatus", err)
  2481. return
  2482. }
  2483. }
  2484. }
  2485. ctx.JSONOK()
  2486. }
  2487. // NewComment create a comment for issue
  2488. func NewComment(ctx *context.Context) {
  2489. form := web.GetForm(ctx).(*forms.CreateCommentForm)
  2490. issue := GetActionIssue(ctx)
  2491. if ctx.Written() {
  2492. return
  2493. }
  2494. if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
  2495. if log.IsTrace() {
  2496. if ctx.IsSigned {
  2497. issueType := "issues"
  2498. if issue.IsPull {
  2499. issueType = "pulls"
  2500. }
  2501. log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
  2502. "User in Repo has Permissions: %-+v",
  2503. ctx.Doer,
  2504. issue.PosterID,
  2505. issueType,
  2506. ctx.Repo.Repository,
  2507. ctx.Repo.Permission)
  2508. } else {
  2509. log.Trace("Permission Denied: Not logged in")
  2510. }
  2511. }
  2512. ctx.Error(http.StatusForbidden)
  2513. return
  2514. }
  2515. if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
  2516. ctx.JSONError(ctx.Tr("repo.issues.comment_on_locked"))
  2517. return
  2518. }
  2519. var attachments []string
  2520. if setting.Attachment.Enabled {
  2521. attachments = form.Files
  2522. }
  2523. if ctx.HasError() {
  2524. ctx.JSONError(ctx.GetErrMsg())
  2525. return
  2526. }
  2527. var comment *issues_model.Comment
  2528. defer func() {
  2529. // Check if issue admin/poster changes the status of issue.
  2530. if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) &&
  2531. (form.Status == "reopen" || form.Status == "close") &&
  2532. !(issue.IsPull && issue.PullRequest.HasMerged) {
  2533. // Duplication and conflict check should apply to reopen pull request.
  2534. var pr *issues_model.PullRequest
  2535. if form.Status == "reopen" && issue.IsPull {
  2536. pull := issue.PullRequest
  2537. var err error
  2538. pr, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
  2539. if err != nil {
  2540. if !issues_model.IsErrPullRequestNotExist(err) {
  2541. ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
  2542. return
  2543. }
  2544. }
  2545. // Regenerate patch and test conflict.
  2546. if pr == nil {
  2547. issue.PullRequest.HeadCommitID = ""
  2548. pull_service.AddToTaskQueue(ctx, issue.PullRequest)
  2549. }
  2550. // check whether the ref of PR <refs/pulls/pr_index/head> in base repo is consistent with the head commit of head branch in the head repo
  2551. // get head commit of PR
  2552. prHeadRef := pull.GetGitRefName()
  2553. if err := pull.LoadBaseRepo(ctx); err != nil {
  2554. ctx.ServerError("Unable to load base repo", err)
  2555. return
  2556. }
  2557. prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef)
  2558. if err != nil {
  2559. ctx.ServerError("Get head commit Id of pr fail", err)
  2560. return
  2561. }
  2562. // get head commit of branch in the head repo
  2563. if err := pull.LoadHeadRepo(ctx); err != nil {
  2564. ctx.ServerError("Unable to load head repo", err)
  2565. return
  2566. }
  2567. if ok := git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.BaseBranch); !ok {
  2568. // todo localize
  2569. ctx.JSONError("The origin branch is delete, cannot reopen.")
  2570. return
  2571. }
  2572. headBranchRef := pull.GetGitHeadBranchRefName()
  2573. headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef)
  2574. if err != nil {
  2575. ctx.ServerError("Get head commit Id of head branch fail", err)
  2576. return
  2577. }
  2578. err = pull.LoadIssue(ctx)
  2579. if err != nil {
  2580. ctx.ServerError("load the issue of pull request error", err)
  2581. return
  2582. }
  2583. if prHeadCommitID != headBranchCommitID {
  2584. // force push to base repo
  2585. err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
  2586. Remote: pull.BaseRepo.RepoPath(),
  2587. Branch: pull.HeadBranch + ":" + prHeadRef,
  2588. Force: true,
  2589. Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
  2590. })
  2591. if err != nil {
  2592. ctx.ServerError("force push error", err)
  2593. return
  2594. }
  2595. }
  2596. }
  2597. if pr != nil {
  2598. ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
  2599. } else {
  2600. isClosed := form.Status == "close"
  2601. if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
  2602. log.Error("ChangeStatus: %v", err)
  2603. if issues_model.IsErrDependenciesLeft(err) {
  2604. if issue.IsPull {
  2605. ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
  2606. } else {
  2607. ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
  2608. }
  2609. return
  2610. }
  2611. } else {
  2612. if err := stopTimerIfAvailable(ctx.Doer, issue); err != nil {
  2613. ctx.ServerError("CreateOrStopIssueStopwatch", err)
  2614. return
  2615. }
  2616. log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
  2617. }
  2618. }
  2619. }
  2620. // Redirect to comment hashtag if there is any actual content.
  2621. typeName := "issues"
  2622. if issue.IsPull {
  2623. typeName = "pulls"
  2624. }
  2625. if comment != nil {
  2626. ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
  2627. } else {
  2628. ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
  2629. }
  2630. }()
  2631. // Fix #321: Allow empty comments, as long as we have attachments.
  2632. if len(form.Content) == 0 && len(attachments) == 0 {
  2633. return
  2634. }
  2635. comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
  2636. if err != nil {
  2637. ctx.ServerError("CreateIssueComment", err)
  2638. return
  2639. }
  2640. log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
  2641. }
  2642. // UpdateCommentContent change comment of issue's content
  2643. func UpdateCommentContent(ctx *context.Context) {
  2644. comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
  2645. if err != nil {
  2646. ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
  2647. return
  2648. }
  2649. if err := comment.LoadIssue(ctx); err != nil {
  2650. ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
  2651. return
  2652. }
  2653. if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
  2654. ctx.Error(http.StatusForbidden)
  2655. return
  2656. }
  2657. if !comment.Type.HasContentSupport() {
  2658. ctx.Error(http.StatusNoContent)
  2659. return
  2660. }
  2661. oldContent := comment.Content
  2662. comment.Content = ctx.FormString("content")
  2663. if len(comment.Content) == 0 {
  2664. ctx.JSON(http.StatusOK, map[string]any{
  2665. "content": "",
  2666. })
  2667. return
  2668. }
  2669. if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
  2670. ctx.ServerError("UpdateComment", err)
  2671. return
  2672. }
  2673. if err := comment.LoadAttachments(ctx); err != nil {
  2674. ctx.ServerError("LoadAttachments", err)
  2675. return
  2676. }
  2677. // when the update request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
  2678. if !ctx.FormBool("ignore_attachments") {
  2679. if err := updateAttachments(ctx, comment, ctx.FormStrings("files[]")); err != nil {
  2680. ctx.ServerError("UpdateAttachments", err)
  2681. return
  2682. }
  2683. }
  2684. content, err := markdown.RenderString(&markup.RenderContext{
  2685. URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
  2686. Metas: ctx.Repo.Repository.ComposeMetas(),
  2687. GitRepo: ctx.Repo.GitRepo,
  2688. Ctx: ctx,
  2689. }, comment.Content)
  2690. if err != nil {
  2691. ctx.ServerError("RenderString", err)
  2692. return
  2693. }
  2694. ctx.JSON(http.StatusOK, map[string]any{
  2695. "content": content,
  2696. "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
  2697. })
  2698. }
  2699. // DeleteComment delete comment of issue
  2700. func DeleteComment(ctx *context.Context) {
  2701. comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
  2702. if err != nil {
  2703. ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
  2704. return
  2705. }
  2706. if err := comment.LoadIssue(ctx); err != nil {
  2707. ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
  2708. return
  2709. }
  2710. if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
  2711. ctx.Error(http.StatusForbidden)
  2712. return
  2713. } else if !comment.Type.HasContentSupport() {
  2714. ctx.Error(http.StatusNoContent)
  2715. return
  2716. }
  2717. if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
  2718. ctx.ServerError("DeleteComment", err)
  2719. return
  2720. }
  2721. ctx.Status(http.StatusOK)
  2722. }
  2723. // ChangeIssueReaction create a reaction for issue
  2724. func ChangeIssueReaction(ctx *context.Context) {
  2725. form := web.GetForm(ctx).(*forms.ReactionForm)
  2726. issue := GetActionIssue(ctx)
  2727. if ctx.Written() {
  2728. return
  2729. }
  2730. if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
  2731. if log.IsTrace() {
  2732. if ctx.IsSigned {
  2733. issueType := "issues"
  2734. if issue.IsPull {
  2735. issueType = "pulls"
  2736. }
  2737. log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
  2738. "User in Repo has Permissions: %-+v",
  2739. ctx.Doer,
  2740. issue.PosterID,
  2741. issueType,
  2742. ctx.Repo.Repository,
  2743. ctx.Repo.Permission)
  2744. } else {
  2745. log.Trace("Permission Denied: Not logged in")
  2746. }
  2747. }
  2748. ctx.Error(http.StatusForbidden)
  2749. return
  2750. }
  2751. if ctx.HasError() {
  2752. ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
  2753. return
  2754. }
  2755. switch ctx.Params(":action") {
  2756. case "react":
  2757. reaction, err := issues_model.CreateIssueReaction(ctx.Doer.ID, issue.ID, form.Content)
  2758. if err != nil {
  2759. if issues_model.IsErrForbiddenIssueReaction(err) {
  2760. ctx.ServerError("ChangeIssueReaction", err)
  2761. return
  2762. }
  2763. log.Info("CreateIssueReaction: %s", err)
  2764. break
  2765. }
  2766. // Reload new reactions
  2767. issue.Reactions = nil
  2768. if err = issue.LoadAttributes(ctx); err != nil {
  2769. log.Info("issue.LoadAttributes: %s", err)
  2770. break
  2771. }
  2772. log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
  2773. case "unreact":
  2774. if err := issues_model.DeleteIssueReaction(ctx.Doer.ID, issue.ID, form.Content); err != nil {
  2775. ctx.ServerError("DeleteIssueReaction", err)
  2776. return
  2777. }
  2778. // Reload new reactions
  2779. issue.Reactions = nil
  2780. if err := issue.LoadAttributes(ctx); err != nil {
  2781. log.Info("issue.LoadAttributes: %s", err)
  2782. break
  2783. }
  2784. log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
  2785. default:
  2786. ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
  2787. return
  2788. }
  2789. if len(issue.Reactions) == 0 {
  2790. ctx.JSON(http.StatusOK, map[string]any{
  2791. "empty": true,
  2792. "html": "",
  2793. })
  2794. return
  2795. }
  2796. html, err := ctx.RenderToString(tplReactions, map[string]any{
  2797. "ctxData": ctx.Data,
  2798. "ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
  2799. "Reactions": issue.Reactions.GroupByType(),
  2800. })
  2801. if err != nil {
  2802. ctx.ServerError("ChangeIssueReaction.HTMLString", err)
  2803. return
  2804. }
  2805. ctx.JSON(http.StatusOK, map[string]any{
  2806. "html": html,
  2807. })
  2808. }
  2809. // ChangeCommentReaction create a reaction for comment
  2810. func ChangeCommentReaction(ctx *context.Context) {
  2811. form := web.GetForm(ctx).(*forms.ReactionForm)
  2812. comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
  2813. if err != nil {
  2814. ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
  2815. return
  2816. }
  2817. if err := comment.LoadIssue(ctx); err != nil {
  2818. ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
  2819. return
  2820. }
  2821. if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
  2822. if log.IsTrace() {
  2823. if ctx.IsSigned {
  2824. issueType := "issues"
  2825. if comment.Issue.IsPull {
  2826. issueType = "pulls"
  2827. }
  2828. log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
  2829. "User in Repo has Permissions: %-+v",
  2830. ctx.Doer,
  2831. comment.Issue.PosterID,
  2832. issueType,
  2833. ctx.Repo.Repository,
  2834. ctx.Repo.Permission)
  2835. } else {
  2836. log.Trace("Permission Denied: Not logged in")
  2837. }
  2838. }
  2839. ctx.Error(http.StatusForbidden)
  2840. return
  2841. }
  2842. if !comment.Type.HasContentSupport() {
  2843. ctx.Error(http.StatusNoContent)
  2844. return
  2845. }
  2846. switch ctx.Params(":action") {
  2847. case "react":
  2848. reaction, err := issues_model.CreateCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
  2849. if err != nil {
  2850. if issues_model.IsErrForbiddenIssueReaction(err) {
  2851. ctx.ServerError("ChangeIssueReaction", err)
  2852. return
  2853. }
  2854. log.Info("CreateCommentReaction: %s", err)
  2855. break
  2856. }
  2857. // Reload new reactions
  2858. comment.Reactions = nil
  2859. if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
  2860. log.Info("comment.LoadReactions: %s", err)
  2861. break
  2862. }
  2863. log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID)
  2864. case "unreact":
  2865. if err := issues_model.DeleteCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content); err != nil {
  2866. ctx.ServerError("DeleteCommentReaction", err)
  2867. return
  2868. }
  2869. // Reload new reactions
  2870. comment.Reactions = nil
  2871. if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
  2872. log.Info("comment.LoadReactions: %s", err)
  2873. break
  2874. }
  2875. log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID)
  2876. default:
  2877. ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
  2878. return
  2879. }
  2880. if len(comment.Reactions) == 0 {
  2881. ctx.JSON(http.StatusOK, map[string]any{
  2882. "empty": true,
  2883. "html": "",
  2884. })
  2885. return
  2886. }
  2887. html, err := ctx.RenderToString(tplReactions, map[string]any{
  2888. "ctxData": ctx.Data,
  2889. "ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
  2890. "Reactions": comment.Reactions.GroupByType(),
  2891. })
  2892. if err != nil {
  2893. ctx.ServerError("ChangeCommentReaction.HTMLString", err)
  2894. return
  2895. }
  2896. ctx.JSON(http.StatusOK, map[string]any{
  2897. "html": html,
  2898. })
  2899. }
  2900. func addParticipant(poster *user_model.User, participants []*user_model.User) []*user_model.User {
  2901. for _, part := range participants {
  2902. if poster.ID == part.ID {
  2903. return participants
  2904. }
  2905. }
  2906. return append(participants, poster)
  2907. }
  2908. func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error {
  2909. // Remove comments that the user has no permissions to see
  2910. for i := 0; i < len(issue.Comments); {
  2911. c := issue.Comments[i]
  2912. if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
  2913. var err error
  2914. // Set RefRepo for description in template
  2915. c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID)
  2916. if err != nil {
  2917. return err
  2918. }
  2919. perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, ctx.Doer)
  2920. if err != nil {
  2921. return err
  2922. }
  2923. if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
  2924. issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
  2925. continue
  2926. }
  2927. }
  2928. i++
  2929. }
  2930. return nil
  2931. }
  2932. // GetIssueAttachments returns attachments for the issue
  2933. func GetIssueAttachments(ctx *context.Context) {
  2934. issue := GetActionIssue(ctx)
  2935. if ctx.Written() {
  2936. return
  2937. }
  2938. attachments := make([]*api.Attachment, len(issue.Attachments))
  2939. for i := 0; i < len(issue.Attachments); i++ {
  2940. attachments[i] = convert.ToAttachment(ctx.Repo.Repository, issue.Attachments[i])
  2941. }
  2942. ctx.JSON(http.StatusOK, attachments)
  2943. }
  2944. // GetCommentAttachments returns attachments for the comment
  2945. func GetCommentAttachments(ctx *context.Context) {
  2946. comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
  2947. if err != nil {
  2948. ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
  2949. return
  2950. }
  2951. if !comment.Type.HasAttachmentSupport() {
  2952. ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type))
  2953. return
  2954. }
  2955. attachments := make([]*api.Attachment, 0)
  2956. if err := comment.LoadAttachments(ctx); err != nil {
  2957. ctx.ServerError("LoadAttachments", err)
  2958. return
  2959. }
  2960. for i := 0; i < len(comment.Attachments); i++ {
  2961. attachments = append(attachments, convert.ToAttachment(ctx.Repo.Repository, comment.Attachments[i]))
  2962. }
  2963. ctx.JSON(http.StatusOK, attachments)
  2964. }
  2965. func updateAttachments(ctx *context.Context, item any, files []string) error {
  2966. var attachments []*repo_model.Attachment
  2967. switch content := item.(type) {
  2968. case *issues_model.Issue:
  2969. attachments = content.Attachments
  2970. case *issues_model.Comment:
  2971. attachments = content.Attachments
  2972. default:
  2973. return fmt.Errorf("unknown Type: %T", content)
  2974. }
  2975. for i := 0; i < len(attachments); i++ {
  2976. if util.SliceContainsString(files, attachments[i].UUID) {
  2977. continue
  2978. }
  2979. if err := repo_model.DeleteAttachment(attachments[i], true); err != nil {
  2980. return err
  2981. }
  2982. }
  2983. var err error
  2984. if len(files) > 0 {
  2985. switch content := item.(type) {
  2986. case *issues_model.Issue:
  2987. err = issues_model.UpdateIssueAttachments(content.ID, files)
  2988. case *issues_model.Comment:
  2989. err = content.UpdateAttachments(files)
  2990. default:
  2991. return fmt.Errorf("unknown Type: %T", content)
  2992. }
  2993. if err != nil {
  2994. return err
  2995. }
  2996. }
  2997. switch content := item.(type) {
  2998. case *issues_model.Issue:
  2999. content.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, content.ID)
  3000. case *issues_model.Comment:
  3001. content.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, content.ID)
  3002. default:
  3003. return fmt.Errorf("unknown Type: %T", content)
  3004. }
  3005. return err
  3006. }
  3007. func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string {
  3008. attachHTML, err := ctx.RenderToString(tplAttachment, map[string]any{
  3009. "ctxData": ctx.Data,
  3010. "Attachments": attachments,
  3011. "Content": content,
  3012. })
  3013. if err != nil {
  3014. ctx.ServerError("attachmentsHTML.HTMLString", err)
  3015. return ""
  3016. }
  3017. return attachHTML
  3018. }
  3019. // combineLabelComments combine the nearby label comments as one.
  3020. func combineLabelComments(issue *issues_model.Issue) {
  3021. var prev, cur *issues_model.Comment
  3022. for i := 0; i < len(issue.Comments); i++ {
  3023. cur = issue.Comments[i]
  3024. if i > 0 {
  3025. prev = issue.Comments[i-1]
  3026. }
  3027. if i == 0 || cur.Type != issues_model.CommentTypeLabel ||
  3028. (prev != nil && prev.PosterID != cur.PosterID) ||
  3029. (prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
  3030. if cur.Type == issues_model.CommentTypeLabel && cur.Label != nil {
  3031. if cur.Content != "1" {
  3032. cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
  3033. } else {
  3034. cur.AddedLabels = append(cur.AddedLabels, cur.Label)
  3035. }
  3036. }
  3037. continue
  3038. }
  3039. if cur.Label != nil { // now cur MUST be label comment
  3040. if prev.Type == issues_model.CommentTypeLabel { // we can combine them only prev is a label comment
  3041. if cur.Content != "1" {
  3042. // remove labels from the AddedLabels list if the label that was removed is already
  3043. // in this list, and if it's not in this list, add the label to RemovedLabels
  3044. addedAndRemoved := false
  3045. for i, label := range prev.AddedLabels {
  3046. if cur.Label.ID == label.ID {
  3047. prev.AddedLabels = append(prev.AddedLabels[:i], prev.AddedLabels[i+1:]...)
  3048. addedAndRemoved = true
  3049. break
  3050. }
  3051. }
  3052. if !addedAndRemoved {
  3053. prev.RemovedLabels = append(prev.RemovedLabels, cur.Label)
  3054. }
  3055. } else {
  3056. // remove labels from the RemovedLabels list if the label that was added is already
  3057. // in this list, and if it's not in this list, add the label to AddedLabels
  3058. removedAndAdded := false
  3059. for i, label := range prev.RemovedLabels {
  3060. if cur.Label.ID == label.ID {
  3061. prev.RemovedLabels = append(prev.RemovedLabels[:i], prev.RemovedLabels[i+1:]...)
  3062. removedAndAdded = true
  3063. break
  3064. }
  3065. }
  3066. if !removedAndAdded {
  3067. prev.AddedLabels = append(prev.AddedLabels, cur.Label)
  3068. }
  3069. }
  3070. prev.CreatedUnix = cur.CreatedUnix
  3071. // remove the current comment since it has been combined to prev comment
  3072. issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
  3073. i--
  3074. } else { // if prev is not a label comment, start a new group
  3075. if cur.Content != "1" {
  3076. cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
  3077. } else {
  3078. cur.AddedLabels = append(cur.AddedLabels, cur.Label)
  3079. }
  3080. }
  3081. }
  3082. }
  3083. }
  3084. // get all teams that current user can mention
  3085. func handleTeamMentions(ctx *context.Context) {
  3086. if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() {
  3087. return
  3088. }
  3089. var isAdmin bool
  3090. var err error
  3091. var teams []*organization.Team
  3092. org := organization.OrgFromUser(ctx.Repo.Owner)
  3093. // Admin has super access.
  3094. if ctx.Doer.IsAdmin {
  3095. isAdmin = true
  3096. } else {
  3097. isAdmin, err = org.IsOwnedBy(ctx.Doer.ID)
  3098. if err != nil {
  3099. ctx.ServerError("IsOwnedBy", err)
  3100. return
  3101. }
  3102. }
  3103. if isAdmin {
  3104. teams, err = org.LoadTeams()
  3105. if err != nil {
  3106. ctx.ServerError("LoadTeams", err)
  3107. return
  3108. }
  3109. } else {
  3110. teams, err = org.GetUserTeams(ctx.Doer.ID)
  3111. if err != nil {
  3112. ctx.ServerError("GetUserTeams", err)
  3113. return
  3114. }
  3115. }
  3116. ctx.Data["MentionableTeams"] = teams
  3117. ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
  3118. ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.AvatarLink(ctx)
  3119. }
  3120. type userSearchInfo struct {
  3121. UserID int64 `json:"user_id"`
  3122. UserName string `json:"username"`
  3123. AvatarLink string `json:"avatar_link"`
  3124. FullName string `json:"full_name"`
  3125. }
  3126. type userSearchResponse struct {
  3127. Results []*userSearchInfo `json:"results"`
  3128. }
  3129. // IssuePosters get posters for current repo's issues/pull requests
  3130. func IssuePosters(ctx *context.Context) {
  3131. issuePosters(ctx, false)
  3132. }
  3133. func PullPosters(ctx *context.Context) {
  3134. issuePosters(ctx, true)
  3135. }
  3136. func issuePosters(ctx *context.Context, isPullList bool) {
  3137. repo := ctx.Repo.Repository
  3138. search := strings.TrimSpace(ctx.FormString("q"))
  3139. posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search, setting.UI.DefaultShowFullName)
  3140. if err != nil {
  3141. ctx.JSON(http.StatusInternalServerError, err)
  3142. return
  3143. }
  3144. if search == "" && ctx.Doer != nil {
  3145. // the returned posters slice only contains limited number of users,
  3146. // to make the current user (doer) can quickly filter their own issues, always add doer to the posters slice
  3147. if !util.SliceContainsFunc(posters, func(user *user_model.User) bool { return user.ID == ctx.Doer.ID }) {
  3148. posters = append(posters, ctx.Doer)
  3149. }
  3150. }
  3151. posters = MakeSelfOnTop(ctx, posters)
  3152. resp := &userSearchResponse{}
  3153. resp.Results = make([]*userSearchInfo, len(posters))
  3154. for i, user := range posters {
  3155. resp.Results[i] = &userSearchInfo{UserID: user.ID, UserName: user.Name, AvatarLink: user.AvatarLink(ctx)}
  3156. if setting.UI.DefaultShowFullName {
  3157. resp.Results[i].FullName = user.FullName
  3158. }
  3159. }
  3160. ctx.JSON(http.StatusOK, resp)
  3161. }