Files @ aa0560cfca9b
Branch filter:

Location: kallithea/kallithea/controllers/pullrequests.py - annotation

Mads Kiilerich
pullrequests: when updating a PR, only add and remove the reviewers that actually were added/removed
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
24c0d584ba86
d1addaf7a91e
1948ede028ef
1948ede028ef
d1addaf7a91e
d1addaf7a91e
1948ede028ef
ad38f9f93b3b
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
f295fad8adff
d1addaf7a91e
af3539a458f6
d1addaf7a91e
d9b78d8f1db3
d1addaf7a91e
af3539a458f6
e33b17db388d
e33b17db388d
edb24bc0f71a
d1addaf7a91e
e33b17db388d
e33b17db388d
e33b17db388d
9a23b444a7fe
e33b17db388d
d1addaf7a91e
e33b17db388d
d1addaf7a91e
e33b17db388d
edb24bc0f71a
b52ada72fc99
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
96bd919192b0
d1addaf7a91e
b3ddd87f214f
b3ddd87f214f
65a964fc9053
c733124b6262
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
c3a68137453a
c3a68137453a
c3a68137453a
c3a68137453a
8f5ecadb7ec1
d1addaf7a91e
8f5ecadb7ec1
d1addaf7a91e
8f5ecadb7ec1
8f5ecadb7ec1
8f5ecadb7ec1
8f5ecadb7ec1
8f5ecadb7ec1
d1addaf7a91e
d1addaf7a91e
980691743559
980691743559
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
980691743559
980691743559
980691743559
3dda68220092
980691743559
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
c3a68137453a
c3a68137453a
c3a68137453a
c3a68137453a
c3a68137453a
c3a68137453a
c3a68137453a
c3a68137453a
d1addaf7a91e
d1addaf7a91e
c3a68137453a
556e98fd06d2
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
980691743559
980691743559
d1addaf7a91e
d1addaf7a91e
7c917e15c045
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
1337ada582a1
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
980691743559
980691743559
5fdad98e5b2e
0ece1a4cf6e3
5fdad98e5b2e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
1ffa190f686a
1ffa190f686a
1ffa190f686a
cd6176c0634a
20a094053606
20a094053606
20a094053606
20a094053606
20a094053606
d208416c84c6
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
fb64046d02c2
d1addaf7a91e
8ad40ef0ea80
8ad40ef0ea80
8ad40ef0ea80
8ad40ef0ea80
8ad40ef0ea80
8ad40ef0ea80
8ad40ef0ea80
40e362a4ffc7
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
6564d82e1469
e79993216c66
e79993216c66
6564d82e1469
8ad40ef0ea80
8ad40ef0ea80
8ad40ef0ea80
cd6176c0634a
6564d82e1469
0cb43732260b
0cb43732260b
0cb43732260b
8ad40ef0ea80
8ad40ef0ea80
8ad40ef0ea80
8ad40ef0ea80
8ad40ef0ea80
0cb43732260b
0cb43732260b
0cb43732260b
0cb43732260b
0cb43732260b
6564d82e1469
e79993216c66
6564d82e1469
6564d82e1469
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
9581233e9275
296b37f6fcdc
d1addaf7a91e
296b37f6fcdc
d69aa464f373
d1addaf7a91e
d1addaf7a91e
d9b78d8f1db3
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
666e4d4567f8
666e4d4567f8
666e4d4567f8
666e4d4567f8
666e4d4567f8
666e4d4567f8
666e4d4567f8
666e4d4567f8
d1addaf7a91e
d1addaf7a91e
6cb077e99873
6cb077e99873
6cb077e99873
d1addaf7a91e
556e98fd06d2
556e98fd06d2
556e98fd06d2
556e98fd06d2
d1addaf7a91e
6cb077e99873
296b37f6fcdc
296b37f6fcdc
556e98fd06d2
556e98fd06d2
6cb077e99873
6cb077e99873
556e98fd06d2
666e4d4567f8
556e98fd06d2
296b37f6fcdc
6cb077e99873
666e4d4567f8
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
6cb077e99873
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
296b37f6fcdc
296b37f6fcdc
7c0b55fb3a85
296b37f6fcdc
296b37f6fcdc
296b37f6fcdc
296b37f6fcdc
296b37f6fcdc
296b37f6fcdc
296b37f6fcdc
296b37f6fcdc
296b37f6fcdc
296b37f6fcdc
296b37f6fcdc
d1addaf7a91e
7c0b55fb3a85
d1addaf7a91e
d1addaf7a91e
d69aa464f373
d1addaf7a91e
c35429c32d7f
65a964fc9053
d1addaf7a91e
c35429c32d7f
d1addaf7a91e
65a964fc9053
65a964fc9053
65a964fc9053
65a964fc9053
494eab6ec837
65a964fc9053
65a964fc9053
ddfb998e3898
ddfb998e3898
d053efba3c0c
d053efba3c0c
65a964fc9053
65a964fc9053
10cff43c7bd9
65a964fc9053
65a964fc9053
65a964fc9053
65a964fc9053
323ccdd4579b
323ccdd4579b
323ccdd4579b
323ccdd4579b
65a964fc9053
65a964fc9053
65a964fc9053
65a964fc9053
65a964fc9053
65a964fc9053
68bdd7646187
68bdd7646187
956c557749cf
65a964fc9053
f295fad8adff
10cff43c7bd9
65a964fc9053
10cff43c7bd9
6744baed1e96
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
14bd5dc3010b
14bd5dc3010b
14bd5dc3010b
14bd5dc3010b
14bd5dc3010b
14bd5dc3010b
96bd919192b0
d1addaf7a91e
d1addaf7a91e
65a964fc9053
6744baed1e96
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d69aa464f373
9a23b444a7fe
9a23b444a7fe
d1addaf7a91e
bf011c9f7f58
d1addaf7a91e
d1addaf7a91e
d9b78d8f1db3
d1addaf7a91e
d9b78d8f1db3
d1addaf7a91e
6744baed1e96
f295fad8adff
f295fad8adff
8049bb8502bd
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
86d9bee4738e
f9bc28c44f30
d42d7b2a3b2f
f295fad8adff
f295fad8adff
f295fad8adff
86d9bee4738e
f295fad8adff
f295fad8adff
531ff6651672
ed7e0730a973
f295fad8adff
f295fad8adff
86d9bee4738e
f295fad8adff
f295fad8adff
531ff6651672
531ff6651672
f295fad8adff
f295fad8adff
86d9bee4738e
ccda18435e42
f295fad8adff
f295fad8adff
f295fad8adff
f9bc28c44f30
f295fad8adff
86d9bee4738e
f295fad8adff
86d9bee4738e
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
d42d7b2a3b2f
f295fad8adff
f295fad8adff
f295fad8adff
d42d7b2a3b2f
531ff6651672
86d9bee4738e
d42d7b2a3b2f
531ff6651672
531ff6651672
531ff6651672
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
6744baed1e96
f295fad8adff
d69aa464f373
9a23b444a7fe
9a23b444a7fe
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
d9b78d8f1db3
f295fad8adff
395be5fa6eef
86d9bee4738e
e99a33d7d7f5
d1ed15ef8714
d42d7b2a3b2f
f295fad8adff
d42d7b2a3b2f
f295fad8adff
f295fad8adff
86d9bee4738e
f295fad8adff
f295fad8adff
d9b78d8f1db3
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
f295fad8adff
96bd919192b0
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
6262ca7ed934
d1addaf7a91e
cd6176c0634a
d1addaf7a91e
81057be7a5c1
2145dfdc3af3
2145dfdc3af3
2145dfdc3af3
6744baed1e96
d42d7b2a3b2f
69ee6a249f55
69ee6a249f55
69ee6a249f55
69ee6a249f55
69ee6a249f55
69ee6a249f55
69ee6a249f55
69ee6a249f55
69ee6a249f55
69ee6a249f55
69ee6a249f55
69ee6a249f55
69ee6a249f55
d42d7b2a3b2f
8049bb8502bd
d42d7b2a3b2f
d42d7b2a3b2f
d42d7b2a3b2f
6744baed1e96
d1addaf7a91e
2145dfdc3af3
2145dfdc3af3
2145dfdc3af3
b9c9216d6fa7
f24851239566
aa0560cfca9b
aa0560cfca9b
9a23b444a7fe
f24851239566
aa0560cfca9b
aa0560cfca9b
d69aa464f373
9a23b444a7fe
9a23b444a7fe
2145dfdc3af3
2145dfdc3af3
2145dfdc3af3
2145dfdc3af3
d9b78d8f1db3
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
e99a33d7d7f5
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d9b78d8f1db3
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
e30401bac6e1
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
6cb077e99873
6cb077e99873
6cb077e99873
6cb077e99873
23def9978ec3
6cb077e99873
6cb077e99873
6cb077e99873
6cb077e99873
23def9978ec3
6cb077e99873
6cb077e99873
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
19e619f3cde1
19e619f3cde1
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
43a4f3b285a6
f6376261296d
f6376261296d
f6376261296d
f6376261296d
f6376261296d
f6376261296d
f6376261296d
f6376261296d
f6376261296d
f6376261296d
43a4f3b285a6
5d5d8ec14aa7
5d5d8ec14aa7
6cb077e99873
4949076c8bb2
6cb077e99873
d2f71ee1c738
d2f71ee1c738
3f646f7bac39
19e619f3cde1
19e619f3cde1
19e619f3cde1
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
833488c0a20a
422667bdb47b
3f646f7bac39
3f646f7bac39
833488c0a20a
23def9978ec3
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
ded49765b47a
3f646f7bac39
5d5d8ec14aa7
3f646f7bac39
3f646f7bac39
86d9bee4738e
3f646f7bac39
19e619f3cde1
23def9978ec3
5d5d8ec14aa7
5d5d8ec14aa7
5d5d8ec14aa7
b486cf5da28d
23def9978ec3
6cb077e99873
6cb077e99873
23def9978ec3
23def9978ec3
cf7d952c292f
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
0210d0b769d4
0210d0b769d4
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
3f646f7bac39
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
4034992774fa
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
23def9978ec3
72acb38da217
72acb38da217
4034992774fa
4034992774fa
12ce88eece5f
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
9581233e9275
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
9581233e9275
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
cfd9115db2a5
cfd9115db2a5
cfd9115db2a5
cfd9115db2a5
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
7570d6665f0f
7570d6665f0f
779d43be59c6
7570d6665f0f
7570d6665f0f
7570d6665f0f
779d43be59c6
7570d6665f0f
7570d6665f0f
7570d6665f0f
d1addaf7a91e
7570d6665f0f
7570d6665f0f
7570d6665f0f
7570d6665f0f
7570d6665f0f
779d43be59c6
e99a33d7d7f5
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
779d43be59c6
140f2811fc6f
9cfc66a665ae
b3ddd87f214f
b3ddd87f214f
b3ddd87f214f
b3ddd87f214f
7570d6665f0f
7570d6665f0f
b3ddd87f214f
d1addaf7a91e
d1addaf7a91e
d208416c84c6
d1addaf7a91e
9581233e9275
d1addaf7a91e
7570d6665f0f
7570d6665f0f
7570d6665f0f
7570d6665f0f
7570d6665f0f
7570d6665f0f
7570d6665f0f
7570d6665f0f
d1addaf7a91e
7570d6665f0f
7570d6665f0f
7570d6665f0f
7570d6665f0f
7570d6665f0f
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d9b78d8f1db3
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
63bed817308c
579110ca5178
579110ca5178
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
e99a33d7d7f5
d1addaf7a91e
81057be7a5c1
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
d1addaf7a91e
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
kallithea.controllers.pullrequests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

pull requests controller for Kallithea for initializing pull requests

This file was forked by the Kallithea project in July 2014.
Original author and date, and relevant copyright and licensing information is below:
:created_on: May 7, 2012
:author: marcink
:copyright: (c) 2013 RhodeCode GmbH, and others.
:license: GPLv3, see LICENSE.md for more details.
"""

import logging
import traceback
import formencode
import re

from pylons import request, tmpl_context as c
from pylons.i18n.translation import _
from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest

from kallithea.config.routing import url
from kallithea.lib import helpers as h
from kallithea.lib import diffs
from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
    NotAnonymous
from kallithea.lib.base import BaseRepoController, render
from kallithea.lib.compat import json, OrderedDict
from kallithea.lib.diffs import LimitedDiffContainer
from kallithea.lib.exceptions import UserInvalidException
from kallithea.lib.page import Page
from kallithea.lib.utils import action_logger, jsonify
from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
from kallithea.lib.vcs.utils import safe_str
from kallithea.lib.vcs.utils.hgcompat import unionrepo
from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
    PullRequestReviewers, User
from kallithea.model.pull_request import PullRequestModel
from kallithea.model.meta import Session
from kallithea.model.repo import RepoModel
from kallithea.model.comment import ChangesetCommentsModel
from kallithea.model.changeset_status import ChangesetStatusModel
from kallithea.model.forms import PullRequestForm, PullRequestPostForm
from kallithea.lib.utils2 import safe_int
from kallithea.controllers.changeset import _ignorews_url, _context_url, \
    create_comment
from kallithea.controllers.compare import CompareController
from kallithea.lib.graphmod import graph_data

log = logging.getLogger(__name__)


class PullrequestsController(BaseRepoController):

    def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
        """return a structure with repo's interesting changesets, suitable for
        the selectors in pullrequest.html

        rev: a revision that must be in the list somehow and selected by default
        branch: a branch that must be in the list and selected by default - even if closed
        branch_rev: a revision of which peers should be preferred and available."""
        # list named branches that has been merged to this named branch - it should probably merge back
        peers = []

        if rev:
            rev = safe_str(rev)

        if branch:
            branch = safe_str(branch)

        if branch_rev:
            branch_rev = safe_str(branch_rev)
            # a revset not restricting to merge() would be better
            # (especially because it would get the branch point)
            # ... but is currently too expensive
            # including branches of children could be nice too
            peerbranches = set()
            for i in repo._repo.revs(
                "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
                branch_rev, branch_rev):
                abranch = repo.get_changeset(i).branch
                if abranch not in peerbranches:
                    n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
                    peers.append((n, abranch))
                    peerbranches.add(abranch)

        selected = None
        tiprev = repo.tags.get('tip')
        tipbranch = None

        branches = []
        for abranch, branchrev in repo.branches.iteritems():
            n = 'branch:%s:%s' % (abranch, branchrev)
            desc = abranch
            if branchrev == tiprev:
                tipbranch = abranch
                desc = '%s (current tip)' % desc
            branches.append((n, desc))
            if rev == branchrev:
                selected = n
            if branch == abranch:
                if not rev:
                    selected = n
                branch = None
        if branch:  # branch not in list - it is probably closed
            branchrev = repo.closed_branches.get(branch)
            if branchrev:
                n = 'branch:%s:%s' % (branch, branchrev)
                branches.append((n, _('%s (closed)') % branch))
                selected = n
                branch = None
            if branch:
                log.debug('branch %r not found in %s', branch, repo)

        bookmarks = []
        for bookmark, bookmarkrev in repo.bookmarks.iteritems():
            n = 'book:%s:%s' % (bookmark, bookmarkrev)
            bookmarks.append((n, bookmark))
            if rev == bookmarkrev:
                selected = n

        tags = []
        for tag, tagrev in repo.tags.iteritems():
            if tag == 'tip':
                continue
            n = 'tag:%s:%s' % (tag, tagrev)
            tags.append((n, tag))
            # note: even if rev == tagrev, don't select the static tag - it must be chosen explicitly

        # prio 1: rev was selected as existing entry above

        # prio 2: create special entry for rev; rev _must_ be used
        specials = []
        if rev and selected is None:
            selected = 'rev:%s:%s' % (rev, rev)
            specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]

        # prio 3: most recent peer branch
        if peers and not selected:
            selected = peers[0][0]

        # prio 4: tip revision
        if not selected:
            if h.is_hg(repo):
                if tipbranch:
                    selected = 'branch:%s:%s' % (tipbranch, tiprev)
                else:
                    selected = 'tag:null:' + repo.EMPTY_CHANGESET
                    tags.append((selected, 'null'))
            else:
                if 'master' in repo.branches:
                    selected = 'branch:master:%s' % repo.branches['master']
                else:
                    k, v = repo.branches.items()[0]
                    selected = 'branch:%s:%s' % (k, v)

        groups = [(specials, _("Special")),
                  (peers, _("Peer branches")),
                  (bookmarks, _("Bookmarks")),
                  (branches, _("Branches")),
                  (tags, _("Tags")),
                  ]
        return [g for g in groups if g[0]], selected

    def _get_is_allowed_change_status(self, pull_request):
        if pull_request.is_closed():
            return False

        owner = self.authuser.user_id == pull_request.owner_id
        reviewer = PullRequestReviewers.query() \
            .filter(PullRequestReviewers.pull_request == pull_request) \
            .filter(PullRequestReviewers.user_id == self.authuser.user_id) \
            .count() != 0

        return self.authuser.admin or owner or reviewer

    @LoginRequired()
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                   'repository.admin')
    def show_all(self, repo_name):
        c.from_ = request.GET.get('from_') or ''
        c.closed = request.GET.get('closed') or ''
        p = safe_int(request.GET.get('page'), 1)

        q = PullRequest.query(include_closed=c.closed, sorted=True)
        if c.from_:
            q = q.filter_by(org_repo=c.db_repo)
        else:
            q = q.filter_by(other_repo=c.db_repo)
        c.pull_requests = q.all()

        c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)

        return render('/pullrequests/pullrequest_show_all.html')

    @LoginRequired()
    @NotAnonymous()
    def show_my(self):
        c.closed = request.GET.get('closed') or ''

        c.my_pull_requests = PullRequest.query(
            include_closed=c.closed,
            sorted=True,
        ).filter_by(owner_id=self.authuser.user_id).all()

        c.participate_in_pull_requests = []
        c.participate_in_pull_requests_todo = []
        done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
        for pr in PullRequest.query(
            include_closed=c.closed,
            reviewer_id=self.authuser.user_id,
            sorted=True,
        ):
            status = pr.user_review_status(c.authuser.user_id) # very inefficient!!!
            if status in done_status:
                c.participate_in_pull_requests.append(pr)
            else:
                c.participate_in_pull_requests_todo.append(pr)

        return render('/pullrequests/pullrequest_show_my.html')

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                   'repository.admin')
    def index(self):
        org_repo = c.db_repo
        org_scm_instance = org_repo.scm_instance
        try:
            org_scm_instance.get_changeset()
        except EmptyRepositoryError as e:
            h.flash(h.literal(_('There are no changesets yet')),
                    category='warning')
            raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))

        org_rev = request.GET.get('rev_end')
        # rev_start is not directly useful - its parent could however be used
        # as default for other and thus give a simple compare view
        rev_start = request.GET.get('rev_start')
        other_rev = None
        if rev_start:
            starters = org_repo.get_changeset(rev_start).parents
            if starters:
                other_rev = starters[0].raw_id
            else:
                other_rev = org_repo.scm_instance.EMPTY_CHANGESET
        branch = request.GET.get('branch')

        c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
        c.default_cs_repo = org_repo.repo_name
        c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)

        default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
        if default_cs_ref_type != 'branch':
            default_cs_branch = org_repo.get_changeset(default_cs_rev).branch

        # add org repo to other so we can open pull request against peer branches on itself
        c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]

        if org_repo.parent:
            # add parent of this fork also and select it.
            # use the same branch on destination as on source, if available.
            c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
            c.a_repo = org_repo.parent
            c.a_refs, c.default_a_ref = self._get_repo_refs(
                    org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)

        else:
            c.a_repo = org_repo
            c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)

        # gather forks and add to this list ... even though it is rare to
        # request forks to pull from their parent
        for fork in org_repo.forks:
            c.a_repos.append((fork.repo_name, fork.repo_name))

        return render('/pullrequests/pullrequest.html')

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                   'repository.admin')
    @jsonify
    def repo_info(self, repo_name):
        repo = c.db_repo
        refs, selected_ref = self._get_repo_refs(repo.scm_instance)
        return {
            'description': repo.description.split('\n', 1)[0],
            'selected_ref': selected_ref,
            'refs': refs,
            }

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                   'repository.admin')
    def create(self, repo_name):
        repo = c.db_repo
        try:
            _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
        except formencode.Invalid as errors:
            log.error(traceback.format_exc())
            log.error(str(errors))
            msg = _('Error creating pull request: %s') % errors.msg
            h.flash(msg, 'error')
            raise HTTPBadRequest

        # heads up: org and other might seem backward here ...
        org_repo_name = _form['org_repo']
        org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
        org_repo = RepoModel()._get_repo(org_repo_name)
        (org_ref_type,
         org_ref_name,
         org_rev) = org_ref.split(':')
        if org_ref_type == 'rev':
            org_ref_type = 'branch'
            cs = org_repo.scm_instance.get_changeset(org_rev)
            org_ref = '%s:%s:%s' % (org_ref_type, cs.branch, cs.raw_id)

        other_repo_name = _form['other_repo']
        other_ref = _form['other_ref'] # will have symbolic name and head revision
        other_repo = RepoModel()._get_repo(other_repo_name)
        (other_ref_type,
         other_ref_name,
         other_rev) = other_ref.split(':')
        if other_ref_type == 'rev':
            cs = other_repo.scm_instance.get_changeset(other_rev)
            other_ref_name = cs.raw_id[:12]
            other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, cs.raw_id)

        cs_ranges, _cs_ranges_not, ancestor_rev = \
            CompareController._get_changesets(org_repo.scm_instance.alias,
                                              other_repo.scm_instance, other_rev, # org and other "swapped"
                                              org_repo.scm_instance, org_rev,
                                              )
        if ancestor_rev is None:
            ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
        revisions = [cs_.raw_id for cs_ in cs_ranges]

        # hack: ancestor_rev is not an other_rev but we want to show the
        # requested destination and have the exact ancestor
        other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)

        reviewer_ids = []

        title = _form['pullrequest_title']
        if not title:
            if org_repo_name == other_repo_name:
                title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
                                      h.short_ref(other_ref_type, other_ref_name))
            else:
                title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
                                            other_repo_name, h.short_ref(other_ref_type, other_ref_name))
        description = _form['pullrequest_desc'].strip() or _('No description')
        try:
            pull_request = PullRequestModel().create(
                self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
                other_ref, revisions, reviewer_ids, title, description
            )
            Session().commit()
            h.flash(_('Successfully opened new pull request'),
                    category='success')
        except UserInvalidException as u:
            h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
            raise HTTPBadRequest()
        except Exception:
            h.flash(_('Error occurred while creating pull request'),
                    category='error')
            log.error(traceback.format_exc())
            raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))

        raise HTTPFound(location=pull_request.url())

    def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewer_ids):
        org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
        org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
        new_org_rev = self._get_ref_rev(org_repo, 'rev', new_rev)

        other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
        other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
        #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
        new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)

        cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
            other_repo.scm_instance, new_other_rev, # org and other "swapped"
            org_repo.scm_instance, new_org_rev)

        old_revisions = set(old_pull_request.revisions)
        revisions = [cs.raw_id for cs in cs_ranges]
        new_revisions = [r for r in revisions if r not in old_revisions]
        lost = old_revisions.difference(revisions)

        infos = ['This is a new iteration of %s "%s".' %
                 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
                      pull_request_id=old_pull_request.pull_request_id),
                  old_pull_request.title)]

        if lost:
            infos.append(_('Missing changesets since the previous iteration:'))
            for r in old_pull_request.revisions:
                if r in lost:
                    rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
                    infos.append('  %s %s' % (h.short_id(r), rev_desc))

        if new_revisions:
            infos.append(_('New changesets on %s %s since the previous iteration:') % (org_ref_type, org_ref_name))
            for r in reversed(revisions):
                if r in new_revisions:
                    rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
                    infos.append('  %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))

            if ancestor_rev == other_rev:
                infos.append(_("Ancestor didn't change - diff since previous iteration:"))
                infos.append(h.canonical_url('compare_url',
                                 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
                                 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
                                 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
                                 )) # note: linear diff, merge or not doesn't matter
            else:
                infos.append(_('This iteration is based on another %s revision and there is no simple diff.') % other_ref_name)
        else:
           infos.append(_('No changes found on %s %s since previous iteration.') % (org_ref_type, org_ref_name))
           # TODO: fail?

        # hack: ancestor_rev is not an other_ref but we want to show the
        # requested destination and have the exact ancestor
        new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
        new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)

        try:
            title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
            v = int(old_v) + 1
        except (AttributeError, ValueError):
            v = 2
        title = '%s (v%s)' % (title.strip(), v)

        # using a mail-like separator, insert new iteration info in description with latest first
        descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
        description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
        if len(descriptions) > 1:
            description += '\n\n' + descriptions[1].strip()

        try:
            pull_request = PullRequestModel().create(
                self.authuser.user_id,
                old_pull_request.org_repo.repo_name, new_org_ref,
                old_pull_request.other_repo.repo_name, new_other_ref,
                revisions, reviewer_ids, title, description
            )
        except UserInvalidException as u:
            h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
            raise HTTPBadRequest()
        except Exception:
            h.flash(_('Error occurred while creating pull request'),
                    category='error')
            log.error(traceback.format_exc())
            raise HTTPFound(location=old_pull_request.url())

        ChangesetCommentsModel().create(
            text=_('Closed, next iteration: %s .') % pull_request.url(canonical=True),
            repo=old_pull_request.other_repo_id,
            author=c.authuser.user_id,
            pull_request=old_pull_request.pull_request_id,
            closing_pr=True)
        PullRequestModel().close_pull_request(old_pull_request.pull_request_id)

        Session().commit()
        h.flash(_('New pull request iteration created'),
                category='success')

        raise HTTPFound(location=pull_request.url())

    # pullrequest_post for PR editing
    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                   'repository.admin')
    def post(self, repo_name, pull_request_id):
        pull_request = PullRequest.get_or_404(pull_request_id)
        if pull_request.is_closed():
            raise HTTPForbidden()
        assert pull_request.other_repo.repo_name == repo_name
        #only owner or admin can update it
        owner = pull_request.owner_id == c.authuser.user_id
        repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
        if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
            raise HTTPForbidden()

        _form = PullRequestPostForm()().to_python(request.POST)
        reviewer_ids = set(int(s) for s in _form['review_members'])

        org_reviewer_ids = set(int(s) for s in _form['org_review_members'])
        current_reviewer_ids = set(prr.user_id for prr in pull_request.reviewers)
        other_added = [User.get(u) for u in current_reviewer_ids - org_reviewer_ids]
        other_removed = [User.get(u) for u in org_reviewer_ids - current_reviewer_ids]
        if other_added:
            h.flash(_('Meanwhile, the following reviewers have been added: %s') %
                    (', '.join(u.username for u in other_added)),
                    category='warning')
        if other_removed:
            h.flash(_('Meanwhile, the following reviewers have been removed: %s') %
                    (', '.join(u.username for u in other_removed)),
                    category='warning')

        if _form['updaterev']:
            return self.create_new_iteration(pull_request,
                                      _form['updaterev'],
                                      _form['pullrequest_title'],
                                      _form['pullrequest_desc'],
                                      reviewer_ids)

        old_description = pull_request.description
        pull_request.title = _form['pullrequest_title']
        pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
        pull_request.owner = User.get_by_username(_form['owner'])
        user = User.get(c.authuser.user_id)
        add_reviewer_ids = reviewer_ids - org_reviewer_ids - current_reviewer_ids
        remove_reviewer_ids = (org_reviewer_ids - reviewer_ids) & current_reviewer_ids
        try:
            PullRequestModel().mention_from_description(user, pull_request, old_description)
            PullRequestModel().add_reviewers(user, pull_request, add_reviewer_ids)
            PullRequestModel().remove_reviewers(user, pull_request, remove_reviewer_ids)
        except UserInvalidException as u:
            h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
            raise HTTPBadRequest()

        Session().commit()
        h.flash(_('Pull request updated'), category='success')

        raise HTTPFound(location=pull_request.url())

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                   'repository.admin')
    @jsonify
    def delete(self, repo_name, pull_request_id):
        pull_request = PullRequest.get_or_404(pull_request_id)
        #only owner can delete it !
        if pull_request.owner_id == c.authuser.user_id:
            PullRequestModel().delete(pull_request)
            Session().commit()
            h.flash(_('Successfully deleted pull request'),
                    category='success')
            raise HTTPFound(location=url('my_pullrequests'))
        raise HTTPForbidden()

    @LoginRequired()
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                   'repository.admin')
    def show(self, repo_name, pull_request_id, extra=None):
        repo_model = RepoModel()
        c.users_array = repo_model.get_users_js()
        c.user_groups_array = repo_model.get_user_groups_js()
        c.pull_request = PullRequest.get_or_404(pull_request_id)
        c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
        cc_model = ChangesetCommentsModel()
        cs_model = ChangesetStatusModel()

        # pull_requests repo_name we opened it against
        # ie. other_repo must match
        if repo_name != c.pull_request.other_repo.repo_name:
            raise HTTPNotFound

        # load compare data into template context
        c.cs_repo = c.pull_request.org_repo
        (c.cs_ref_type,
         c.cs_ref_name,
         c.cs_rev) = c.pull_request.org_ref.split(':')

        c.a_repo = c.pull_request.other_repo
        (c.a_ref_type,
         c.a_ref_name,
         c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor

        org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
        c.cs_repo = c.cs_repo
        try:
            c.cs_ranges = [org_scm_instance.get_changeset(x)
                           for x in c.pull_request.revisions]
        except ChangesetDoesNotExistError:
            c.cs_ranges = []
            h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
                'error')
        c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
        revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
        c.jsdata = json.dumps(graph_data(org_scm_instance, revs))

        c.is_range = False
        try:
            if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
                cs_a = org_scm_instance.get_changeset(c.a_rev)
                root_parents = c.cs_ranges[0].parents
                c.is_range = cs_a in root_parents
                #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
        except ChangesetDoesNotExistError: # probably because c.a_rev not found
            pass
        except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing
            pass

        avail_revs = set()
        avail_show = []
        c.cs_branch_name = c.cs_ref_name
        c.a_branch_name = None
        other_scm_instance = c.a_repo.scm_instance
        c.update_msg = ""
        c.update_msg_other = ""
        try:
            if not c.cs_ranges:
                c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev
            elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
                if c.cs_ref_type != 'branch':
                    c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
                c.a_branch_name = c.a_ref_name
                if c.a_ref_type != 'branch':
                    try:
                        c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
                    except EmptyRepositoryError:
                        c.a_branch_name = 'null' # not a branch name ... but close enough
                # candidates: descendants of old head that are on the right branch
                #             and not are the old head itself ...
                #             and nothing at all if old head is a descendant of target ref name
                if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
                    c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
                elif c.pull_request.is_closed():
                    c.update_msg = _('This pull request has been closed and can not be updated.')
                else: # look for descendants of PR head on source branch in org repo
                    avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
                                                             revs[0], c.cs_branch_name)
                    if len(avail_revs) > 1: # more than just revs[0]
                        # also show changesets that not are descendants but would be merged in
                        targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
                        if org_scm_instance.path != other_scm_instance.path:
                            # Note: org_scm_instance.path must come first so all
                            # valid revision numbers are 100% org_scm compatible
                            # - both for avail_revs and for revset results
                            hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
                                                               org_scm_instance.path,
                                                               other_scm_instance.path)
                        else:
                            hgrepo = org_scm_instance._repo
                        show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
                                               avail_revs, revs[0], targethead))
                        c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name
                    else:
                        show = set()
                        avail_revs = set() # drop revs[0]
                        c.update_msg = _('No additional changesets found for iterating on this pull request.')

                    # TODO: handle branch heads that not are tip-most
                    brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
                    if brevs:
                        # also show changesets that are on branch but neither ancestors nor descendants
                        show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
                        show.add(revs[0]) # make sure graph shows this so we can see how they relate
                        c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
                            h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))

                    avail_show = sorted(show, reverse=True)

            elif org_scm_instance.alias == 'git':
                c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
                c.update_msg = _("Git pull requests don't support iterating yet.")
        except ChangesetDoesNotExistError:
            c.update_msg = _('Error: some changesets not found when displaying pull request from %s.') % c.cs_rev

        c.avail_revs = avail_revs
        c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
        c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))

        raw_ids = [x.raw_id for x in c.cs_ranges]
        c.cs_comments = c.cs_repo.get_comments(raw_ids)
        c.statuses = c.cs_repo.statuses(raw_ids)

        ignore_whitespace = request.GET.get('ignorews') == '1'
        line_context = safe_int(request.GET.get('context'), 3)
        c.ignorews_url = _ignorews_url
        c.context_url = _context_url
        c.fulldiff = request.GET.get('fulldiff')
        diff_limit = self.cut_off_limit if not c.fulldiff else None

        # we swap org/other ref since we run a simple diff on one repo
        log.debug('running diff between %s and %s in %s',
                  c.a_rev, c.cs_rev, org_scm_instance.path)
        try:
            txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
                                                ignore_whitespace=ignore_whitespace,
                                                context=line_context)
        except ChangesetDoesNotExistError:
            txtdiff =  _("The diff can't be shown - the PR revisions could not be found.")
        diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
                                             diff_limit=diff_limit)
        _parsed = diff_processor.prepare()

        c.limited_diff = False
        if isinstance(_parsed, LimitedDiffContainer):
            c.limited_diff = True

        c.file_diff_data = OrderedDict()
        c.lines_added = 0
        c.lines_deleted = 0

        for f in _parsed:
            st = f['stats']
            c.lines_added += st['added']
            c.lines_deleted += st['deleted']
            filename = f['filename']
            fid = h.FID('', filename)
            diff = diff_processor.as_html(enable_comments=True,
                                          parsed_lines=[f])
            c.file_diff_data[fid] = (None, f['operation'], f['old_filename'], filename, diff, st)

        # inline comments
        c.inline_cnt = 0
        c.inline_comments = cc_model.get_inline_comments(
                                c.db_repo.repo_id,
                                pull_request=pull_request_id)
        # count inline comments
        for __, lines in c.inline_comments:
            for comments in lines.values():
                c.inline_cnt += len(comments)
        # comments
        c.comments = cc_model.get_comments(c.db_repo.repo_id,
                                           pull_request=pull_request_id)

        # (badly named) pull-request status calculation based on reviewer votes
        (c.pull_request_reviewers,
         c.pull_request_pending_reviewers,
         c.current_voting_result,
         ) = cs_model.calculate_pull_request_result(c.pull_request)
        c.changeset_statuses = ChangesetStatus.STATUSES

        c.as_form = False
        c.ancestor = None # there is one - but right here we don't know which
        return render('/pullrequests/pullrequest_show.html')

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                   'repository.admin')
    @jsonify
    def comment(self, repo_name, pull_request_id):
        pull_request = PullRequest.get_or_404(pull_request_id)

        status = request.POST.get('changeset_status')
        close_pr = request.POST.get('save_close')
        delete = request.POST.get('save_delete')
        f_path = request.POST.get('f_path')
        line_no = request.POST.get('line')

        if (status or close_pr or delete) and (f_path or line_no):
            # status votes and closing is only possible in general comments
            raise HTTPBadRequest()

        allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
        if not allowed_to_change_status:
            if status or close_pr:
                h.flash(_('No permission to change pull request status'), 'error')
                raise HTTPForbidden()

        if delete == "delete":
            if (pull_request.owner_id == c.authuser.user_id or
                h.HasPermissionAny('hg.admin')() or
                h.HasRepoPermissionAny('repository.admin')(pull_request.org_repo.repo_name) or
                h.HasRepoPermissionAny('repository.admin')(pull_request.other_repo.repo_name)
                ) and not pull_request.is_closed():
                PullRequestModel().delete(pull_request)
                Session().commit()
                h.flash(_('Successfully deleted pull request %s') % pull_request_id,
                        category='success')
                return {
                   'location': url('my_pullrequests'), # or repo pr list?
                }
                raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
            raise HTTPForbidden()

        text = request.POST.get('text', '').strip()

        comment = create_comment(
            text,
            status,
            pull_request_id=pull_request_id,
            f_path=f_path,
            line_no=line_no,
            closing_pr=close_pr,
        )

        action_logger(self.authuser,
                      'user_commented_pull_request:%s' % pull_request_id,
                      c.db_repo, self.ip_addr, self.sa)

        if status:
            ChangesetStatusModel().set_status(
                c.db_repo.repo_id,
                status,
                c.authuser.user_id,
                comment,
                pull_request=pull_request_id
            )

        if close_pr:
            PullRequestModel().close_pull_request(pull_request_id)
            action_logger(self.authuser,
                          'user_closed_pull_request:%s' % pull_request_id,
                          c.db_repo, self.ip_addr, self.sa)

        Session().commit()

        if not request.environ.get('HTTP_X_PARTIAL_XHR'):
            raise HTTPFound(location=pull_request.url())

        data = {
           'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
        }
        if comment is not None:
            c.comment = comment
            data.update(comment.get_dict())
            data.update({'rendered_text':
                         render('changeset/changeset_comment_block.html')})

        return data

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                   'repository.admin')
    @jsonify
    def delete_comment(self, repo_name, comment_id):
        co = ChangesetComment.get(comment_id)
        if co.pull_request.is_closed():
            #don't allow deleting comments on closed pull request
            raise HTTPForbidden()

        owner = co.author_id == c.authuser.user_id
        repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
        if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
            ChangesetCommentsModel().delete(comment=co)
            Session().commit()
            return True
        else:
            raise HTTPForbidden()