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 109KB

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