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

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