##// END OF EJS Templates
phabricator: add commenting to phabsend for new/updated Diffs...
Ian Moody -
r42624:29528c42 default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (653 lines changed) Show them Hide them
@@ -0,0 +1,653 b''
1 {
2 "version": 1,
3 "interactions": [
4 {
5 "response": {
6 "status": {
7 "message": "OK",
8 "code": 200
9 },
10 "body": {
11 "string": "{\"result\":{\"data\":[{\"id\":12,\"type\":\"REPO\",\"phid\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"fields\":{\"name\":\"Mercurial\",\"vcs\":\"hg\",\"callsign\":\"HG\",\"shortName\":\"Mercurial\",\"status\":\"active\",\"isImporting\":false,\"almanacServicePHID\":null,\"refRules\":{\"fetchRules\":[],\"trackRules\":[],\"permanentRefRules\":[]},\"spacePHID\":null,\"dateCreated\":1523292927,\"dateModified\":1523297359,\"policy\":{\"view\":\"public\",\"edit\":\"admin\",\"diffusion.push\":\"users\"}},\"attachments\":{}}],\"maps\":{},\"query\":{\"queryKey\":null},\"cursor\":{\"limit\":100,\"after\":null,\"before\":null,\"order\":null}},\"error_code\":null,\"error_info\":null}"
12 },
13 "headers": {
14 "date": [
15 "Fri, 07 Jun 2019 20:23:04 GMT"
16 ],
17 "expires": [
18 "Sat, 01 Jan 2000 00:00:00 GMT"
19 ],
20 "x-content-type-options": [
21 "nosniff"
22 ],
23 "vary": [
24 "Accept-Encoding"
25 ],
26 "cache-control": [
27 "no-store"
28 ],
29 "content-length": [
30 "587"
31 ],
32 "connection": [
33 "keep-alive"
34 ],
35 "content-type": [
36 "application/json"
37 ],
38 "referrer-policy": [
39 "no-referrer",
40 "strict-origin-when-cross-origin"
41 ],
42 "x-frame-options": [
43 "Deny"
44 ],
45 "x-xss-protection": [
46 "1; mode=block"
47 ],
48 "strict-transport-security": [
49 "max-age=31536000; includeSubdomains; preload"
50 ]
51 }
52 },
53 "request": {
54 "method": "POST",
55 "uri": "https://phab.mercurial-scm.org//api/diffusion.repository.search",
56 "body": "constraints%5Bcallsigns%5D%5B0%5D=HG&api.token=cli-hahayouwish",
57 "headers": {
58 "accept": [
59 "application/mercurial-0.1"
60 ],
61 "content-type": [
62 "application/x-www-form-urlencoded"
63 ],
64 "host": [
65 "phab.mercurial-scm.org"
66 ],
67 "content-length": [
68 "81"
69 ],
70 "user-agent": [
71 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
72 ]
73 }
74 }
75 },
76 {
77 "response": {
78 "status": {
79 "message": "OK",
80 "code": 200
81 },
82 "body": {
83 "string": "{\"result\":{\"id\":1989,\"phid\":\"PHID-DIFF-3mtjdk4tjjkaw4arccah\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/differential\\/diff\\/1989\\/\"},\"error_code\":null,\"error_info\":null}"
84 },
85 "headers": {
86 "date": [
87 "Fri, 07 Jun 2019 20:23:05 GMT"
88 ],
89 "expires": [
90 "Sat, 01 Jan 2000 00:00:00 GMT"
91 ],
92 "x-content-type-options": [
93 "nosniff"
94 ],
95 "vary": [
96 "Accept-Encoding"
97 ],
98 "cache-control": [
99 "no-store"
100 ],
101 "content-length": [
102 "172"
103 ],
104 "connection": [
105 "keep-alive"
106 ],
107 "content-type": [
108 "application/json"
109 ],
110 "referrer-policy": [
111 "no-referrer",
112 "strict-origin-when-cross-origin"
113 ],
114 "x-frame-options": [
115 "Deny"
116 ],
117 "x-xss-protection": [
118 "1; mode=block"
119 ],
120 "strict-transport-security": [
121 "max-age=31536000; includeSubdomains; preload"
122 ]
123 }
124 },
125 "request": {
126 "method": "POST",
127 "uri": "https://phab.mercurial-scm.org//api/differential.createrawdiff",
128 "body": "repositoryPHID=PHID-REPO-bvunnehri4u2isyr7bc3&diff=diff+--git+a%2Fcomment+b%2Fcomment%0Anew+file+mode+100644%0A---+%2Fdev%2Fnull%0A%2B%2B%2B+b%2Fcomment%0A%40%40+-0%2C0+%2B1%2C1+%40%40%0A%2Bcomment%0A&api.token=cli-hahayouwish",
129 "headers": {
130 "accept": [
131 "application/mercurial-0.1"
132 ],
133 "content-type": [
134 "application/x-www-form-urlencoded"
135 ],
136 "host": [
137 "phab.mercurial-scm.org"
138 ],
139 "content-length": [
140 "243"
141 ],
142 "user-agent": [
143 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
144 ]
145 }
146 }
147 },
148 {
149 "response": {
150 "status": {
151 "message": "OK",
152 "code": 200
153 },
154 "body": {
155 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
156 },
157 "headers": {
158 "date": [
159 "Fri, 07 Jun 2019 20:23:06 GMT"
160 ],
161 "expires": [
162 "Sat, 01 Jan 2000 00:00:00 GMT"
163 ],
164 "x-content-type-options": [
165 "nosniff"
166 ],
167 "vary": [
168 "Accept-Encoding"
169 ],
170 "cache-control": [
171 "no-store"
172 ],
173 "content-length": [
174 "51"
175 ],
176 "connection": [
177 "keep-alive"
178 ],
179 "content-type": [
180 "application/json"
181 ],
182 "referrer-policy": [
183 "no-referrer",
184 "strict-origin-when-cross-origin"
185 ],
186 "x-frame-options": [
187 "Deny"
188 ],
189 "x-xss-protection": [
190 "1; mode=block"
191 ],
192 "strict-transport-security": [
193 "max-age=31536000; includeSubdomains; preload"
194 ]
195 }
196 },
197 "request": {
198 "method": "POST",
199 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
200 "body": "api.token=cli-hahayouwish&data=%7B%22branch%22%3A+%22default%22%2C+%22date%22%3A+%220+0%22%2C+%22node%22%3A+%22a7ee4bac036ae424bfc9e1a4228c4fa06d637f53%22%2C+%22parent%22%3A+%22a19f1434f9a578325eb9799c9961b5465d4e6e40%22%2C+%22user%22%3A+%22test%22%7D&name=hg%3Ameta&diff_id=1989",
201 "headers": {
202 "accept": [
203 "application/mercurial-0.1"
204 ],
205 "content-type": [
206 "application/x-www-form-urlencoded"
207 ],
208 "host": [
209 "phab.mercurial-scm.org"
210 ],
211 "content-length": [
212 "296"
213 ],
214 "user-agent": [
215 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
216 ]
217 }
218 }
219 },
220 {
221 "response": {
222 "status": {
223 "message": "OK",
224 "code": 200
225 },
226 "body": {
227 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
228 },
229 "headers": {
230 "date": [
231 "Fri, 07 Jun 2019 20:23:07 GMT"
232 ],
233 "expires": [
234 "Sat, 01 Jan 2000 00:00:00 GMT"
235 ],
236 "x-content-type-options": [
237 "nosniff"
238 ],
239 "vary": [
240 "Accept-Encoding"
241 ],
242 "cache-control": [
243 "no-store"
244 ],
245 "content-length": [
246 "51"
247 ],
248 "connection": [
249 "keep-alive"
250 ],
251 "content-type": [
252 "application/json"
253 ],
254 "referrer-policy": [
255 "no-referrer",
256 "strict-origin-when-cross-origin"
257 ],
258 "x-frame-options": [
259 "Deny"
260 ],
261 "x-xss-protection": [
262 "1; mode=block"
263 ],
264 "strict-transport-security": [
265 "max-age=31536000; includeSubdomains; preload"
266 ]
267 }
268 },
269 "request": {
270 "method": "POST",
271 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
272 "body": "api.token=cli-hahayouwish&data=%7B%22a7ee4bac036ae424bfc9e1a4228c4fa06d637f53%22%3A+%7B%22author%22%3A+%22test%22%2C+%22authorEmail%22%3A+%22test%22%2C+%22branch%22%3A+%22default%22%2C+%22commit%22%3A+%22a7ee4bac036ae424bfc9e1a4228c4fa06d637f53%22%2C+%22parents%22%3A+%5B%22a19f1434f9a578325eb9799c9961b5465d4e6e40%22%5D%2C+%22time%22%3A+0%7D%7D&name=local%3Acommits&diff_id=1989",
273 "headers": {
274 "accept": [
275 "application/mercurial-0.1"
276 ],
277 "content-type": [
278 "application/x-www-form-urlencoded"
279 ],
280 "host": [
281 "phab.mercurial-scm.org"
282 ],
283 "content-length": [
284 "396"
285 ],
286 "user-agent": [
287 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
288 ]
289 }
290 }
291 },
292 {
293 "response": {
294 "status": {
295 "message": "OK",
296 "code": 200
297 },
298 "body": {
299 "string": "{\"result\":{\"errors\":[],\"fields\":{\"title\":\"create comment for phabricator test\"},\"revisionIDFieldInfo\":{\"value\":null,\"validDomain\":\"https:\\/\\/phab.mercurial-scm.org\"},\"transactions\":[{\"type\":\"title\",\"value\":\"create comment for phabricator test\"}]},\"error_code\":null,\"error_info\":null}"
300 },
301 "headers": {
302 "date": [
303 "Fri, 07 Jun 2019 20:23:07 GMT"
304 ],
305 "expires": [
306 "Sat, 01 Jan 2000 00:00:00 GMT"
307 ],
308 "x-content-type-options": [
309 "nosniff"
310 ],
311 "vary": [
312 "Accept-Encoding"
313 ],
314 "cache-control": [
315 "no-store"
316 ],
317 "content-length": [
318 "288"
319 ],
320 "connection": [
321 "keep-alive"
322 ],
323 "content-type": [
324 "application/json"
325 ],
326 "referrer-policy": [
327 "no-referrer",
328 "strict-origin-when-cross-origin"
329 ],
330 "x-frame-options": [
331 "Deny"
332 ],
333 "x-xss-protection": [
334 "1; mode=block"
335 ],
336 "strict-transport-security": [
337 "max-age=31536000; includeSubdomains; preload"
338 ]
339 }
340 },
341 "request": {
342 "method": "POST",
343 "uri": "https://phab.mercurial-scm.org//api/differential.parsecommitmessage",
344 "body": "corpus=create+comment+for+phabricator+test&api.token=cli-hahayouwish",
345 "headers": {
346 "accept": [
347 "application/mercurial-0.1"
348 ],
349 "content-type": [
350 "application/x-www-form-urlencoded"
351 ],
352 "host": [
353 "phab.mercurial-scm.org"
354 ],
355 "content-length": [
356 "85"
357 ],
358 "user-agent": [
359 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
360 ]
361 }
362 }
363 },
364 {
365 "response": {
366 "status": {
367 "message": "OK",
368 "code": 200
369 },
370 "body": {
371 "string": "{\"result\":{\"object\":{\"id\":1253,\"phid\":\"PHID-DREV-4rhqd6v3yxbtodc7wbv7\"},\"transactions\":[{\"phid\":\"PHID-XACT-DREV-g73sutb5nezcyh6\"},{\"phid\":\"PHID-XACT-DREV-yg6ysul7pcxtqce\"},{\"phid\":\"PHID-XACT-DREV-vxhpgk64u3kax45\"},{\"phid\":\"PHID-XACT-DREV-mkt5rq3racrpzhe\"},{\"phid\":\"PHID-XACT-DREV-s7la723tgqhwovt\"}]},\"error_code\":null,\"error_info\":null}"
372 },
373 "headers": {
374 "date": [
375 "Fri, 07 Jun 2019 20:23:08 GMT"
376 ],
377 "expires": [
378 "Sat, 01 Jan 2000 00:00:00 GMT"
379 ],
380 "x-content-type-options": [
381 "nosniff"
382 ],
383 "vary": [
384 "Accept-Encoding"
385 ],
386 "cache-control": [
387 "no-store"
388 ],
389 "content-length": [
390 "336"
391 ],
392 "connection": [
393 "keep-alive"
394 ],
395 "content-type": [
396 "application/json"
397 ],
398 "referrer-policy": [
399 "no-referrer",
400 "strict-origin-when-cross-origin"
401 ],
402 "x-frame-options": [
403 "Deny"
404 ],
405 "x-xss-protection": [
406 "1; mode=block"
407 ],
408 "strict-transport-security": [
409 "max-age=31536000; includeSubdomains; preload"
410 ]
411 }
412 },
413 "request": {
414 "method": "POST",
415 "uri": "https://phab.mercurial-scm.org//api/differential.revision.edit",
416 "body": "transactions%5B0%5D%5Bvalue%5D=PHID-DIFF-3mtjdk4tjjkaw4arccah&transactions%5B0%5D%5Btype%5D=update&transactions%5B1%5D%5Bvalue%5D=For+default+branch&transactions%5B1%5D%5Btype%5D=comment&transactions%5B2%5D%5Bvalue%5D=create+comment+for+phabricator+test&transactions%5B2%5D%5Btype%5D=title&api.token=cli-hahayouwish",
417 "headers": {
418 "accept": [
419 "application/mercurial-0.1"
420 ],
421 "content-type": [
422 "application/x-www-form-urlencoded"
423 ],
424 "host": [
425 "phab.mercurial-scm.org"
426 ],
427 "content-length": [
428 "332"
429 ],
430 "user-agent": [
431 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
432 ]
433 }
434 }
435 },
436 {
437 "response": {
438 "status": {
439 "message": "OK",
440 "code": 200
441 },
442 "body": {
443 "string": "{\"result\":[{\"id\":\"1253\",\"phid\":\"PHID-DREV-4rhqd6v3yxbtodc7wbv7\",\"title\":\"create comment for phabricator test\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D1253\",\"dateCreated\":\"1559938988\",\"dateModified\":\"1559938988\",\"authorPHID\":\"PHID-USER-qmzis76vb2yh3ogldu6r\",\"status\":\"0\",\"statusName\":\"Draft\",\"properties\":{\"draft.broadcast\":false,\"lines.added\":1,\"lines.removed\":0},\"branch\":null,\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"1\",\"activeDiffPHID\":\"PHID-DIFF-3mtjdk4tjjkaw4arccah\",\"diffs\":[\"1989\"],\"commits\":[],\"reviewers\":[],\"ccs\":[],\"hashes\":[],\"auxiliary\":{\"bugzilla.bug-id\":null,\"phabricator:projects\":[\"PHID-PROJ-f2a3wl5wxtqdtfgdjqzk\"],\"phabricator:depends-on\":[]},\"repositoryPHID\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"sourcePath\":null}],\"error_code\":null,\"error_info\":null}"
444 },
445 "headers": {
446 "date": [
447 "Fri, 07 Jun 2019 20:23:09 GMT"
448 ],
449 "expires": [
450 "Sat, 01 Jan 2000 00:00:00 GMT"
451 ],
452 "x-content-type-options": [
453 "nosniff"
454 ],
455 "vary": [
456 "Accept-Encoding"
457 ],
458 "cache-control": [
459 "no-store"
460 ],
461 "content-length": [
462 "773"
463 ],
464 "connection": [
465 "keep-alive"
466 ],
467 "content-type": [
468 "application/json"
469 ],
470 "referrer-policy": [
471 "no-referrer",
472 "strict-origin-when-cross-origin"
473 ],
474 "x-frame-options": [
475 "Deny"
476 ],
477 "x-xss-protection": [
478 "1; mode=block"
479 ],
480 "strict-transport-security": [
481 "max-age=31536000; includeSubdomains; preload"
482 ]
483 }
484 },
485 "request": {
486 "method": "POST",
487 "uri": "https://phab.mercurial-scm.org//api/differential.query",
488 "body": "api.token=cli-hahayouwish&ids%5B0%5D=1253",
489 "headers": {
490 "accept": [
491 "application/mercurial-0.1"
492 ],
493 "content-type": [
494 "application/x-www-form-urlencoded"
495 ],
496 "host": [
497 "phab.mercurial-scm.org"
498 ],
499 "content-length": [
500 "58"
501 ],
502 "user-agent": [
503 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
504 ]
505 }
506 }
507 },
508 {
509 "response": {
510 "status": {
511 "message": "OK",
512 "code": 200
513 },
514 "body": {
515 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
516 },
517 "headers": {
518 "date": [
519 "Fri, 07 Jun 2019 20:23:10 GMT"
520 ],
521 "expires": [
522 "Sat, 01 Jan 2000 00:00:00 GMT"
523 ],
524 "x-content-type-options": [
525 "nosniff"
526 ],
527 "vary": [
528 "Accept-Encoding"
529 ],
530 "cache-control": [
531 "no-store"
532 ],
533 "content-length": [
534 "51"
535 ],
536 "connection": [
537 "keep-alive"
538 ],
539 "content-type": [
540 "application/json"
541 ],
542 "referrer-policy": [
543 "no-referrer",
544 "strict-origin-when-cross-origin"
545 ],
546 "x-frame-options": [
547 "Deny"
548 ],
549 "x-xss-protection": [
550 "1; mode=block"
551 ],
552 "strict-transport-security": [
553 "max-age=31536000; includeSubdomains; preload"
554 ]
555 }
556 },
557 "request": {
558 "method": "POST",
559 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
560 "body": "api.token=cli-hahayouwish&data=%7B%22branch%22%3A+%22default%22%2C+%22date%22%3A+%220+0%22%2C+%22node%22%3A+%2281fce7de1b7d8ea6b8309a58058d3b5793506c34%22%2C+%22parent%22%3A+%22a19f1434f9a578325eb9799c9961b5465d4e6e40%22%2C+%22user%22%3A+%22test%22%7D&name=hg%3Ameta&diff_id=1989",
561 "headers": {
562 "accept": [
563 "application/mercurial-0.1"
564 ],
565 "content-type": [
566 "application/x-www-form-urlencoded"
567 ],
568 "host": [
569 "phab.mercurial-scm.org"
570 ],
571 "content-length": [
572 "296"
573 ],
574 "user-agent": [
575 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
576 ]
577 }
578 }
579 },
580 {
581 "response": {
582 "status": {
583 "message": "OK",
584 "code": 200
585 },
586 "body": {
587 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
588 },
589 "headers": {
590 "date": [
591 "Fri, 07 Jun 2019 20:23:10 GMT"
592 ],
593 "expires": [
594 "Sat, 01 Jan 2000 00:00:00 GMT"
595 ],
596 "x-content-type-options": [
597 "nosniff"
598 ],
599 "vary": [
600 "Accept-Encoding"
601 ],
602 "cache-control": [
603 "no-store"
604 ],
605 "content-length": [
606 "51"
607 ],
608 "connection": [
609 "keep-alive"
610 ],
611 "content-type": [
612 "application/json"
613 ],
614 "referrer-policy": [
615 "no-referrer",
616 "strict-origin-when-cross-origin"
617 ],
618 "x-frame-options": [
619 "Deny"
620 ],
621 "x-xss-protection": [
622 "1; mode=block"
623 ],
624 "strict-transport-security": [
625 "max-age=31536000; includeSubdomains; preload"
626 ]
627 }
628 },
629 "request": {
630 "method": "POST",
631 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
632 "body": "api.token=cli-hahayouwish&data=%7B%2281fce7de1b7d8ea6b8309a58058d3b5793506c34%22%3A+%7B%22author%22%3A+%22test%22%2C+%22authorEmail%22%3A+%22test%22%2C+%22branch%22%3A+%22default%22%2C+%22commit%22%3A+%2281fce7de1b7d8ea6b8309a58058d3b5793506c34%22%2C+%22parents%22%3A+%5B%22a19f1434f9a578325eb9799c9961b5465d4e6e40%22%5D%2C+%22time%22%3A+0%7D%7D&name=local%3Acommits&diff_id=1989",
633 "headers": {
634 "accept": [
635 "application/mercurial-0.1"
636 ],
637 "content-type": [
638 "application/x-www-form-urlencoded"
639 ],
640 "host": [
641 "phab.mercurial-scm.org"
642 ],
643 "content-length": [
644 "396"
645 ],
646 "user-agent": [
647 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
648 ]
649 }
650 }
651 }
652 ]
653 } No newline at end of file
This diff has been collapsed as it changes many lines, (581 lines changed) Show them Hide them
@@ -0,0 +1,581 b''
1 {
2 "interactions": [
3 {
4 "request": {
5 "method": "POST",
6 "body": "api.token=cli-hahayouwish&revisionIDs%5B0%5D=1253",
7 "uri": "https://phab.mercurial-scm.org//api/differential.querydiffs",
8 "headers": {
9 "content-type": [
10 "application/x-www-form-urlencoded"
11 ],
12 "accept": [
13 "application/mercurial-0.1"
14 ],
15 "user-agent": [
16 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
17 ],
18 "host": [
19 "phab.mercurial-scm.org"
20 ],
21 "content-length": [
22 "66"
23 ]
24 }
25 },
26 "response": {
27 "status": {
28 "code": 200,
29 "message": "OK"
30 },
31 "body": {
32 "string": "{\"result\":{\"1989\":{\"id\":\"1989\",\"revisionID\":\"1253\",\"dateCreated\":\"1559938985\",\"dateModified\":\"1559938988\",\"sourceControlBaseRevision\":null,\"sourceControlPath\":null,\"sourceControlSystem\":null,\"branch\":null,\"bookmark\":null,\"creationMethod\":\"web\",\"description\":null,\"unitStatus\":\"4\",\"lintStatus\":\"4\",\"changes\":[{\"id\":\"5273\",\"metadata\":{\"line:first\":1,\"hash.effect\":\"mzg_LBhhVYqb\"},\"oldPath\":null,\"currentPath\":\"comment\",\"awayPaths\":[],\"oldProperties\":[],\"newProperties\":{\"unix:filemode\":\"100644\"},\"type\":\"1\",\"fileType\":\"1\",\"commitHash\":null,\"addLines\":\"1\",\"delLines\":\"0\",\"hunks\":[{\"oldOffset\":\"0\",\"newOffset\":\"1\",\"oldLength\":\"0\",\"newLength\":\"1\",\"addLines\":null,\"delLines\":null,\"isMissingOldNewline\":null,\"isMissingNewNewline\":null,\"corpus\":\"+comment\\n\"}]}],\"properties\":{\"hg:meta\":{\"branch\":\"default\",\"date\":\"0 0\",\"node\":\"0025df7d064f9c916862d19e207429a0f799fa7d\",\"parent\":\"a19f1434f9a578325eb9799c9961b5465d4e6e40\",\"user\":\"test\"},\"local:commits\":{\"0025df7d064f9c916862d19e207429a0f799fa7d\":{\"author\":\"test\",\"authorEmail\":\"test\",\"branch\":\"default\",\"commit\":\"0025df7d064f9c916862d19e207429a0f799fa7d\",\"parents\":[\"a19f1434f9a578325eb9799c9961b5465d4e6e40\"],\"time\":0}}},\"authorName\":\"test\",\"authorEmail\":\"test\"}},\"error_code\":null,\"error_info\":null}"
33 },
34 "headers": {
35 "expires": [
36 "Sat, 01 Jan 2000 00:00:00 GMT"
37 ],
38 "content-type": [
39 "application/json"
40 ],
41 "connection": [
42 "keep-alive"
43 ],
44 "vary": [
45 "Accept-Encoding"
46 ],
47 "x-frame-options": [
48 "Deny"
49 ],
50 "strict-transport-security": [
51 "max-age=31536000; includeSubdomains; preload"
52 ],
53 "date": [
54 "Fri, 07 Jun 2019 20:26:57 GMT"
55 ],
56 "cache-control": [
57 "no-store"
58 ],
59 "referrer-policy": [
60 "no-referrer",
61 "strict-origin-when-cross-origin"
62 ],
63 "x-content-type-options": [
64 "nosniff"
65 ],
66 "content-length": [
67 "1243"
68 ],
69 "x-xss-protection": [
70 "1; mode=block"
71 ]
72 }
73 }
74 },
75 {
76 "request": {
77 "method": "POST",
78 "body": "constraints%5Bcallsigns%5D%5B0%5D=HG&api.token=cli-hahayouwish",
79 "uri": "https://phab.mercurial-scm.org//api/diffusion.repository.search",
80 "headers": {
81 "content-type": [
82 "application/x-www-form-urlencoded"
83 ],
84 "accept": [
85 "application/mercurial-0.1"
86 ],
87 "user-agent": [
88 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
89 ],
90 "host": [
91 "phab.mercurial-scm.org"
92 ],
93 "content-length": [
94 "81"
95 ]
96 }
97 },
98 "response": {
99 "status": {
100 "code": 200,
101 "message": "OK"
102 },
103 "body": {
104 "string": "{\"result\":{\"data\":[{\"id\":12,\"type\":\"REPO\",\"phid\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"fields\":{\"name\":\"Mercurial\",\"vcs\":\"hg\",\"callsign\":\"HG\",\"shortName\":\"Mercurial\",\"status\":\"active\",\"isImporting\":false,\"almanacServicePHID\":null,\"refRules\":{\"fetchRules\":[],\"trackRules\":[],\"permanentRefRules\":[]},\"spacePHID\":null,\"dateCreated\":1523292927,\"dateModified\":1523297359,\"policy\":{\"view\":\"public\",\"edit\":\"admin\",\"diffusion.push\":\"users\"}},\"attachments\":{}}],\"maps\":{},\"query\":{\"queryKey\":null},\"cursor\":{\"limit\":100,\"after\":null,\"before\":null,\"order\":null}},\"error_code\":null,\"error_info\":null}"
105 },
106 "headers": {
107 "expires": [
108 "Sat, 01 Jan 2000 00:00:00 GMT"
109 ],
110 "content-type": [
111 "application/json"
112 ],
113 "connection": [
114 "keep-alive"
115 ],
116 "vary": [
117 "Accept-Encoding"
118 ],
119 "x-frame-options": [
120 "Deny"
121 ],
122 "strict-transport-security": [
123 "max-age=31536000; includeSubdomains; preload"
124 ],
125 "date": [
126 "Fri, 07 Jun 2019 20:26:58 GMT"
127 ],
128 "cache-control": [
129 "no-store"
130 ],
131 "referrer-policy": [
132 "no-referrer",
133 "strict-origin-when-cross-origin"
134 ],
135 "x-content-type-options": [
136 "nosniff"
137 ],
138 "content-length": [
139 "587"
140 ],
141 "x-xss-protection": [
142 "1; mode=block"
143 ]
144 }
145 }
146 },
147 {
148 "request": {
149 "method": "POST",
150 "body": "repositoryPHID=PHID-REPO-bvunnehri4u2isyr7bc3&api.token=cli-hahayouwish&diff=diff+--git+a%2Fcomment+b%2Fcomment%0Anew+file+mode+100644%0A---+%2Fdev%2Fnull%0A%2B%2B%2B+b%2Fcomment%0A%40%40+-0%2C0+%2B1%2C2+%40%40%0A%2Bcomment%0A%2Bcomment2%0A",
151 "uri": "https://phab.mercurial-scm.org//api/differential.createrawdiff",
152 "headers": {
153 "content-type": [
154 "application/x-www-form-urlencoded"
155 ],
156 "accept": [
157 "application/mercurial-0.1"
158 ],
159 "user-agent": [
160 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
161 ],
162 "host": [
163 "phab.mercurial-scm.org"
164 ],
165 "content-length": [
166 "257"
167 ]
168 }
169 },
170 "response": {
171 "status": {
172 "code": 200,
173 "message": "OK"
174 },
175 "body": {
176 "string": "{\"result\":{\"id\":1990,\"phid\":\"PHID-DIFF-xfa4yzc5h2cvjfhpx4dv\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/differential\\/diff\\/1990\\/\"},\"error_code\":null,\"error_info\":null}"
177 },
178 "headers": {
179 "expires": [
180 "Sat, 01 Jan 2000 00:00:00 GMT"
181 ],
182 "content-type": [
183 "application/json"
184 ],
185 "connection": [
186 "keep-alive"
187 ],
188 "vary": [
189 "Accept-Encoding"
190 ],
191 "x-frame-options": [
192 "Deny"
193 ],
194 "strict-transport-security": [
195 "max-age=31536000; includeSubdomains; preload"
196 ],
197 "date": [
198 "Fri, 07 Jun 2019 20:26:59 GMT"
199 ],
200 "cache-control": [
201 "no-store"
202 ],
203 "referrer-policy": [
204 "no-referrer",
205 "strict-origin-when-cross-origin"
206 ],
207 "x-content-type-options": [
208 "nosniff"
209 ],
210 "content-length": [
211 "172"
212 ],
213 "x-xss-protection": [
214 "1; mode=block"
215 ]
216 }
217 }
218 },
219 {
220 "request": {
221 "method": "POST",
222 "body": "diff_id=1990&data=%7B%22branch%22%3A+%22default%22%2C+%22date%22%3A+%220+0%22%2C+%22node%22%3A+%221acd4b60af38c934182468719a8a431248f49bef%22%2C+%22parent%22%3A+%22a19f1434f9a578325eb9799c9961b5465d4e6e40%22%2C+%22user%22%3A+%22test%22%7D&api.token=cli-hahayouwish&name=hg%3Ameta",
223 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
224 "headers": {
225 "content-type": [
226 "application/x-www-form-urlencoded"
227 ],
228 "accept": [
229 "application/mercurial-0.1"
230 ],
231 "user-agent": [
232 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
233 ],
234 "host": [
235 "phab.mercurial-scm.org"
236 ],
237 "content-length": [
238 "296"
239 ]
240 }
241 },
242 "response": {
243 "status": {
244 "code": 200,
245 "message": "OK"
246 },
247 "body": {
248 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
249 },
250 "headers": {
251 "expires": [
252 "Sat, 01 Jan 2000 00:00:00 GMT"
253 ],
254 "content-type": [
255 "application/json"
256 ],
257 "connection": [
258 "keep-alive"
259 ],
260 "vary": [
261 "Accept-Encoding"
262 ],
263 "x-frame-options": [
264 "Deny"
265 ],
266 "strict-transport-security": [
267 "max-age=31536000; includeSubdomains; preload"
268 ],
269 "date": [
270 "Fri, 07 Jun 2019 20:26:59 GMT"
271 ],
272 "cache-control": [
273 "no-store"
274 ],
275 "referrer-policy": [
276 "no-referrer",
277 "strict-origin-when-cross-origin"
278 ],
279 "x-content-type-options": [
280 "nosniff"
281 ],
282 "content-length": [
283 "51"
284 ],
285 "x-xss-protection": [
286 "1; mode=block"
287 ]
288 }
289 }
290 },
291 {
292 "request": {
293 "method": "POST",
294 "body": "diff_id=1990&data=%7B%221acd4b60af38c934182468719a8a431248f49bef%22%3A+%7B%22author%22%3A+%22test%22%2C+%22authorEmail%22%3A+%22test%22%2C+%22branch%22%3A+%22default%22%2C+%22commit%22%3A+%221acd4b60af38c934182468719a8a431248f49bef%22%2C+%22parents%22%3A+%5B%22a19f1434f9a578325eb9799c9961b5465d4e6e40%22%5D%2C+%22time%22%3A+0%7D%7D&api.token=cli-hahayouwish&name=local%3Acommits",
295 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
296 "headers": {
297 "content-type": [
298 "application/x-www-form-urlencoded"
299 ],
300 "accept": [
301 "application/mercurial-0.1"
302 ],
303 "user-agent": [
304 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
305 ],
306 "host": [
307 "phab.mercurial-scm.org"
308 ],
309 "content-length": [
310 "396"
311 ]
312 }
313 },
314 "response": {
315 "status": {
316 "code": 200,
317 "message": "OK"
318 },
319 "body": {
320 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
321 },
322 "headers": {
323 "expires": [
324 "Sat, 01 Jan 2000 00:00:00 GMT"
325 ],
326 "content-type": [
327 "application/json"
328 ],
329 "connection": [
330 "keep-alive"
331 ],
332 "vary": [
333 "Accept-Encoding"
334 ],
335 "x-frame-options": [
336 "Deny"
337 ],
338 "strict-transport-security": [
339 "max-age=31536000; includeSubdomains; preload"
340 ],
341 "date": [
342 "Fri, 07 Jun 2019 20:27:00 GMT"
343 ],
344 "cache-control": [
345 "no-store"
346 ],
347 "referrer-policy": [
348 "no-referrer",
349 "strict-origin-when-cross-origin"
350 ],
351 "x-content-type-options": [
352 "nosniff"
353 ],
354 "content-length": [
355 "51"
356 ],
357 "x-xss-protection": [
358 "1; mode=block"
359 ]
360 }
361 }
362 },
363 {
364 "request": {
365 "method": "POST",
366 "body": "api.token=cli-hahayouwish&corpus=create+comment+for+phabricator+test%0A%0ADifferential+Revision%3A+https%3A%2F%2Fphab.mercurial-scm.org%2FD1253",
367 "uri": "https://phab.mercurial-scm.org//api/differential.parsecommitmessage",
368 "headers": {
369 "content-type": [
370 "application/x-www-form-urlencoded"
371 ],
372 "accept": [
373 "application/mercurial-0.1"
374 ],
375 "user-agent": [
376 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
377 ],
378 "host": [
379 "phab.mercurial-scm.org"
380 ],
381 "content-length": [
382 "165"
383 ]
384 }
385 },
386 "response": {
387 "status": {
388 "code": 200,
389 "message": "OK"
390 },
391 "body": {
392 "string": "{\"result\":{\"errors\":[],\"fields\":{\"title\":\"create comment for phabricator test\",\"revisionID\":1253},\"revisionIDFieldInfo\":{\"value\":1253,\"validDomain\":\"https:\\/\\/phab.mercurial-scm.org\"},\"transactions\":[{\"type\":\"title\",\"value\":\"create comment for phabricator test\"}]},\"error_code\":null,\"error_info\":null}"
393 },
394 "headers": {
395 "expires": [
396 "Sat, 01 Jan 2000 00:00:00 GMT"
397 ],
398 "content-type": [
399 "application/json"
400 ],
401 "connection": [
402 "keep-alive"
403 ],
404 "vary": [
405 "Accept-Encoding"
406 ],
407 "x-frame-options": [
408 "Deny"
409 ],
410 "strict-transport-security": [
411 "max-age=31536000; includeSubdomains; preload"
412 ],
413 "date": [
414 "Fri, 07 Jun 2019 20:27:01 GMT"
415 ],
416 "cache-control": [
417 "no-store"
418 ],
419 "referrer-policy": [
420 "no-referrer",
421 "strict-origin-when-cross-origin"
422 ],
423 "x-content-type-options": [
424 "nosniff"
425 ],
426 "content-length": [
427 "306"
428 ],
429 "x-xss-protection": [
430 "1; mode=block"
431 ]
432 }
433 }
434 },
435 {
436 "request": {
437 "method": "POST",
438 "body": "api.token=cli-hahayouwish&transactions%5B0%5D%5Btype%5D=update&transactions%5B0%5D%5Bvalue%5D=PHID-DIFF-xfa4yzc5h2cvjfhpx4dv&transactions%5B1%5D%5Btype%5D=comment&transactions%5B1%5D%5Bvalue%5D=Address+review+comments&transactions%5B2%5D%5Btype%5D=title&transactions%5B2%5D%5Bvalue%5D=create+comment+for+phabricator+test&objectIdentifier=1253",
439 "uri": "https://phab.mercurial-scm.org//api/differential.revision.edit",
440 "headers": {
441 "content-type": [
442 "application/x-www-form-urlencoded"
443 ],
444 "accept": [
445 "application/mercurial-0.1"
446 ],
447 "user-agent": [
448 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
449 ],
450 "host": [
451 "phab.mercurial-scm.org"
452 ],
453 "content-length": [
454 "359"
455 ]
456 }
457 },
458 "response": {
459 "status": {
460 "code": 200,
461 "message": "OK"
462 },
463 "body": {
464 "string": "{\"result\":{\"object\":{\"id\":1253,\"phid\":\"PHID-DREV-4rhqd6v3yxbtodc7wbv7\"},\"transactions\":[{\"phid\":\"PHID-XACT-DREV-punz3dredrxghth\"},{\"phid\":\"PHID-XACT-DREV-ykwxppmzdgrtgye\"}]},\"error_code\":null,\"error_info\":null}"
465 },
466 "headers": {
467 "expires": [
468 "Sat, 01 Jan 2000 00:00:00 GMT"
469 ],
470 "content-type": [
471 "application/json"
472 ],
473 "connection": [
474 "keep-alive"
475 ],
476 "vary": [
477 "Accept-Encoding"
478 ],
479 "x-frame-options": [
480 "Deny"
481 ],
482 "strict-transport-security": [
483 "max-age=31536000; includeSubdomains; preload"
484 ],
485 "date": [
486 "Fri, 07 Jun 2019 20:27:02 GMT"
487 ],
488 "cache-control": [
489 "no-store"
490 ],
491 "referrer-policy": [
492 "no-referrer",
493 "strict-origin-when-cross-origin"
494 ],
495 "x-content-type-options": [
496 "nosniff"
497 ],
498 "content-length": [
499 "210"
500 ],
501 "x-xss-protection": [
502 "1; mode=block"
503 ]
504 }
505 }
506 },
507 {
508 "request": {
509 "method": "POST",
510 "body": "api.token=cli-hahayouwish&ids%5B0%5D=1253",
511 "uri": "https://phab.mercurial-scm.org//api/differential.query",
512 "headers": {
513 "content-type": [
514 "application/x-www-form-urlencoded"
515 ],
516 "accept": [
517 "application/mercurial-0.1"
518 ],
519 "user-agent": [
520 "mercurial/proto-1.0 (Mercurial 5.0.1+253-f2ebe61e9a8e+20190607)"
521 ],
522 "host": [
523 "phab.mercurial-scm.org"
524 ],
525 "content-length": [
526 "58"
527 ]
528 }
529 },
530 "response": {
531 "status": {
532 "code": 200,
533 "message": "OK"
534 },
535 "body": {
536 "string": "{\"result\":[{\"id\":\"1253\",\"phid\":\"PHID-DREV-4rhqd6v3yxbtodc7wbv7\",\"title\":\"create comment for phabricator test\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D1253\",\"dateCreated\":\"1559938988\",\"dateModified\":\"1559939221\",\"authorPHID\":\"PHID-USER-qmzis76vb2yh3ogldu6r\",\"status\":\"0\",\"statusName\":\"Needs Review\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":2,\"lines.removed\":0,\"buildables\":{\"PHID-HMBB-hsvjwe4uccbkgjpvffhz\":{\"status\":\"passed\"}}},\"branch\":null,\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"2\",\"activeDiffPHID\":\"PHID-DIFF-xfa4yzc5h2cvjfhpx4dv\",\"diffs\":[\"1990\",\"1989\"],\"commits\":[],\"reviewers\":[],\"ccs\":[],\"hashes\":[],\"auxiliary\":{\"bugzilla.bug-id\":null,\"phabricator:projects\":[],\"phabricator:depends-on\":[]},\"repositoryPHID\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"sourcePath\":null}],\"error_code\":null,\"error_info\":null}"
537 },
538 "headers": {
539 "expires": [
540 "Sat, 01 Jan 2000 00:00:00 GMT"
541 ],
542 "content-type": [
543 "application/json"
544 ],
545 "connection": [
546 "keep-alive"
547 ],
548 "vary": [
549 "Accept-Encoding"
550 ],
551 "x-frame-options": [
552 "Deny"
553 ],
554 "strict-transport-security": [
555 "max-age=31536000; includeSubdomains; preload"
556 ],
557 "date": [
558 "Fri, 07 Jun 2019 20:27:02 GMT"
559 ],
560 "cache-control": [
561 "no-store"
562 ],
563 "referrer-policy": [
564 "no-referrer",
565 "strict-origin-when-cross-origin"
566 ],
567 "x-content-type-options": [
568 "nosniff"
569 ],
570 "content-length": [
571 "822"
572 ],
573 "x-xss-protection": [
574 "1; mode=block"
575 ]
576 }
577 }
578 }
579 ],
580 "version": 1
581 } No newline at end of file
@@ -1,1061 +1,1066 b''
1 1 # phabricator.py - simple Phabricator integration
2 2 #
3 3 # Copyright 2017 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """simple Phabricator integration (EXPERIMENTAL)
8 8
9 9 This extension provides a ``phabsend`` command which sends a stack of
10 10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
11 11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
12 12 to update statuses in batch.
13 13
14 14 By default, Phabricator requires ``Test Plan`` which might prevent some
15 15 changeset from being sent. The requirement could be disabled by changing
16 16 ``differential.require-test-plan-field`` config server side.
17 17
18 18 Config::
19 19
20 20 [phabricator]
21 21 # Phabricator URL
22 22 url = https://phab.example.com/
23 23
24 24 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
25 25 # callsign is "FOO".
26 26 callsign = FOO
27 27
28 28 # curl command to use. If not set (default), use builtin HTTP library to
29 29 # communicate. If set, use the specified curl command. This could be useful
30 30 # if you need to specify advanced options that is not easily supported by
31 31 # the internal library.
32 32 curlcmd = curl --connect-timeout 2 --retry 3 --silent
33 33
34 34 [auth]
35 35 example.schemes = https
36 36 example.prefix = phab.example.com
37 37
38 38 # API token. Get it from https://$HOST/conduit/login/
39 39 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
40 40 """
41 41
42 42 from __future__ import absolute_import
43 43
44 44 import contextlib
45 45 import itertools
46 46 import json
47 47 import operator
48 48 import re
49 49
50 50 from mercurial.node import bin, nullid
51 51 from mercurial.i18n import _
52 52 from mercurial import (
53 53 cmdutil,
54 54 context,
55 55 encoding,
56 56 error,
57 57 httpconnection as httpconnectionmod,
58 58 mdiff,
59 59 obsutil,
60 60 parser,
61 61 patch,
62 62 phases,
63 63 pycompat,
64 64 registrar,
65 65 scmutil,
66 66 smartset,
67 67 tags,
68 68 templatefilters,
69 69 templateutil,
70 70 url as urlmod,
71 71 util,
72 72 )
73 73 from mercurial.utils import (
74 74 procutil,
75 75 stringutil,
76 76 )
77 77
78 78 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
79 79 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
80 80 # be specifying the version(s) of Mercurial they are tested with, or
81 81 # leave the attribute unspecified.
82 82 testedwith = 'ships-with-hg-core'
83 83
84 84 cmdtable = {}
85 85 command = registrar.command(cmdtable)
86 86
87 87 configtable = {}
88 88 configitem = registrar.configitem(configtable)
89 89
90 90 # developer config: phabricator.batchsize
91 91 configitem(b'phabricator', b'batchsize',
92 92 default=12,
93 93 )
94 94 configitem(b'phabricator', b'callsign',
95 95 default=None,
96 96 )
97 97 configitem(b'phabricator', b'curlcmd',
98 98 default=None,
99 99 )
100 100 # developer config: phabricator.repophid
101 101 configitem(b'phabricator', b'repophid',
102 102 default=None,
103 103 )
104 104 configitem(b'phabricator', b'url',
105 105 default=None,
106 106 )
107 107 configitem(b'phabsend', b'confirm',
108 108 default=False,
109 109 )
110 110
111 111 colortable = {
112 112 b'phabricator.action.created': b'green',
113 113 b'phabricator.action.skipped': b'magenta',
114 114 b'phabricator.action.updated': b'magenta',
115 115 b'phabricator.desc': b'',
116 116 b'phabricator.drev': b'bold',
117 117 b'phabricator.node': b'',
118 118 }
119 119
120 120 _VCR_FLAGS = [
121 121 (b'', b'test-vcr', b'',
122 122 _(b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
123 123 b', otherwise will mock all http requests using the specified vcr file.'
124 124 b' (ADVANCED)'
125 125 )),
126 126 ]
127 127
128 128 def vcrcommand(name, flags, spec, helpcategory=None):
129 129 fullflags = flags + _VCR_FLAGS
130 130 def hgmatcher(r1, r2):
131 131 if r1.uri != r2.uri or r1.method != r2.method:
132 132 return False
133 133 r1params = r1.body.split(b'&')
134 134 r2params = r2.body.split(b'&')
135 135 return set(r1params) == set(r2params)
136 136
137 137 def decorate(fn):
138 138 def inner(*args, **kwargs):
139 139 cassette = pycompat.fsdecode(kwargs.pop(r'test_vcr', None))
140 140 if cassette:
141 141 import hgdemandimport
142 142 with hgdemandimport.deactivated():
143 143 import vcr as vcrmod
144 144 import vcr.stubs as stubs
145 145 vcr = vcrmod.VCR(
146 146 serializer=r'json',
147 147 custom_patches=[
148 148 (urlmod, r'httpconnection',
149 149 stubs.VCRHTTPConnection),
150 150 (urlmod, r'httpsconnection',
151 151 stubs.VCRHTTPSConnection),
152 152 ])
153 153 vcr.register_matcher(r'hgmatcher', hgmatcher)
154 154 with vcr.use_cassette(cassette, match_on=[r'hgmatcher']):
155 155 return fn(*args, **kwargs)
156 156 return fn(*args, **kwargs)
157 157 inner.__name__ = fn.__name__
158 158 inner.__doc__ = fn.__doc__
159 159 return command(name, fullflags, spec, helpcategory=helpcategory)(inner)
160 160 return decorate
161 161
162 162 def urlencodenested(params):
163 163 """like urlencode, but works with nested parameters.
164 164
165 165 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
166 166 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
167 167 urlencode. Note: the encoding is consistent with PHP's http_build_query.
168 168 """
169 169 flatparams = util.sortdict()
170 170 def process(prefix, obj):
171 171 if isinstance(obj, bool):
172 172 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
173 173 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
174 174 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
175 175 if items is None:
176 176 flatparams[prefix] = obj
177 177 else:
178 178 for k, v in items(obj):
179 179 if prefix:
180 180 process(b'%s[%s]' % (prefix, k), v)
181 181 else:
182 182 process(k, v)
183 183 process(b'', params)
184 184 return util.urlreq.urlencode(flatparams)
185 185
186 186 def readurltoken(repo):
187 187 """return conduit url, token and make sure they exist
188 188
189 189 Currently read from [auth] config section. In the future, it might
190 190 make sense to read from .arcconfig and .arcrc as well.
191 191 """
192 192 url = repo.ui.config(b'phabricator', b'url')
193 193 if not url:
194 194 raise error.Abort(_(b'config %s.%s is required')
195 195 % (b'phabricator', b'url'))
196 196
197 197 res = httpconnectionmod.readauthforuri(repo.ui, url, util.url(url).user)
198 198 token = None
199 199
200 200 if res:
201 201 group, auth = res
202 202
203 203 repo.ui.debug(b"using auth.%s.* for authentication\n" % group)
204 204
205 205 token = auth.get(b'phabtoken')
206 206
207 207 if not token:
208 208 raise error.Abort(_(b'Can\'t find conduit token associated to %s')
209 209 % (url,))
210 210
211 211 return url, token
212 212
213 213 def callconduit(repo, name, params):
214 214 """call Conduit API, params is a dict. return json.loads result, or None"""
215 215 host, token = readurltoken(repo)
216 216 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
217 217 repo.ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
218 218 params = params.copy()
219 219 params[b'api.token'] = token
220 220 data = urlencodenested(params)
221 221 curlcmd = repo.ui.config(b'phabricator', b'curlcmd')
222 222 if curlcmd:
223 223 sin, sout = procutil.popen2(b'%s -d @- %s'
224 224 % (curlcmd, procutil.shellquote(url)))
225 225 sin.write(data)
226 226 sin.close()
227 227 body = sout.read()
228 228 else:
229 229 urlopener = urlmod.opener(repo.ui, authinfo)
230 230 request = util.urlreq.request(pycompat.strurl(url), data=data)
231 231 with contextlib.closing(urlopener.open(request)) as rsp:
232 232 body = rsp.read()
233 233 repo.ui.debug(b'Conduit Response: %s\n' % body)
234 234 parsed = pycompat.rapply(
235 235 lambda x: encoding.unitolocal(x) if isinstance(x, pycompat.unicode)
236 236 else x,
237 237 json.loads(body)
238 238 )
239 239 if parsed.get(b'error_code'):
240 240 msg = (_(b'Conduit Error (%s): %s')
241 241 % (parsed[b'error_code'], parsed[b'error_info']))
242 242 raise error.Abort(msg)
243 243 return parsed[b'result']
244 244
245 245 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'))
246 246 def debugcallconduit(ui, repo, name):
247 247 """call Conduit API
248 248
249 249 Call parameters are read from stdin as a JSON blob. Result will be written
250 250 to stdout as a JSON blob.
251 251 """
252 252 # json.loads only accepts bytes from 3.6+
253 253 rawparams = encoding.unifromlocal(ui.fin.read())
254 254 # json.loads only returns unicode strings
255 255 params = pycompat.rapply(lambda x:
256 256 encoding.unitolocal(x) if isinstance(x, pycompat.unicode) else x,
257 257 json.loads(rawparams)
258 258 )
259 259 # json.dumps only accepts unicode strings
260 260 result = pycompat.rapply(lambda x:
261 261 encoding.unifromlocal(x) if isinstance(x, bytes) else x,
262 262 callconduit(repo, name, params)
263 263 )
264 264 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
265 265 ui.write(b'%s\n' % encoding.unitolocal(s))
266 266
267 267 def getrepophid(repo):
268 268 """given callsign, return repository PHID or None"""
269 269 # developer config: phabricator.repophid
270 270 repophid = repo.ui.config(b'phabricator', b'repophid')
271 271 if repophid:
272 272 return repophid
273 273 callsign = repo.ui.config(b'phabricator', b'callsign')
274 274 if not callsign:
275 275 return None
276 276 query = callconduit(repo, b'diffusion.repository.search',
277 277 {b'constraints': {b'callsigns': [callsign]}})
278 278 if len(query[b'data']) == 0:
279 279 return None
280 280 repophid = query[b'data'][0][b'phid']
281 281 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
282 282 return repophid
283 283
284 284 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
285 285 _differentialrevisiondescre = re.compile(
286 286 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M)
287 287
288 288 def getoldnodedrevmap(repo, nodelist):
289 289 """find previous nodes that has been sent to Phabricator
290 290
291 291 return {node: (oldnode, Differential diff, Differential Revision ID)}
292 292 for node in nodelist with known previous sent versions, or associated
293 293 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
294 294 be ``None``.
295 295
296 296 Examines commit messages like "Differential Revision:" to get the
297 297 association information.
298 298
299 299 If such commit message line is not found, examines all precursors and their
300 300 tags. Tags with format like "D1234" are considered a match and the node
301 301 with that tag, and the number after "D" (ex. 1234) will be returned.
302 302
303 303 The ``old node``, if not None, is guaranteed to be the last diff of
304 304 corresponding Differential Revision, and exist in the repo.
305 305 """
306 306 unfi = repo.unfiltered()
307 307 nodemap = unfi.changelog.nodemap
308 308
309 309 result = {} # {node: (oldnode?, lastdiff?, drev)}
310 310 toconfirm = {} # {node: (force, {precnode}, drev)}
311 311 for node in nodelist:
312 312 ctx = unfi[node]
313 313 # For tags like "D123", put them into "toconfirm" to verify later
314 314 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
315 315 for n in precnodes:
316 316 if n in nodemap:
317 317 for tag in unfi.nodetags(n):
318 318 m = _differentialrevisiontagre.match(tag)
319 319 if m:
320 320 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
321 321 continue
322 322
323 323 # Check commit message
324 324 m = _differentialrevisiondescre.search(ctx.description())
325 325 if m:
326 326 toconfirm[node] = (1, set(precnodes), int(m.group(r'id')))
327 327
328 328 # Double check if tags are genuine by collecting all old nodes from
329 329 # Phabricator, and expect precursors overlap with it.
330 330 if toconfirm:
331 331 drevs = [drev for force, precs, drev in toconfirm.values()]
332 332 alldiffs = callconduit(unfi, b'differential.querydiffs',
333 333 {b'revisionIDs': drevs})
334 334 getnode = lambda d: bin(
335 335 getdiffmeta(d).get(b'node', b'')) or None
336 336 for newnode, (force, precset, drev) in toconfirm.items():
337 337 diffs = [d for d in alldiffs.values()
338 338 if int(d[b'revisionID']) == drev]
339 339
340 340 # "precursors" as known by Phabricator
341 341 phprecset = set(getnode(d) for d in diffs)
342 342
343 343 # Ignore if precursors (Phabricator and local repo) do not overlap,
344 344 # and force is not set (when commit message says nothing)
345 345 if not force and not bool(phprecset & precset):
346 346 tagname = b'D%d' % drev
347 347 tags.tag(repo, tagname, nullid, message=None, user=None,
348 348 date=None, local=True)
349 349 unfi.ui.warn(_(b'D%s: local tag removed - does not match '
350 350 b'Differential history\n') % drev)
351 351 continue
352 352
353 353 # Find the last node using Phabricator metadata, and make sure it
354 354 # exists in the repo
355 355 oldnode = lastdiff = None
356 356 if diffs:
357 357 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
358 358 oldnode = getnode(lastdiff)
359 359 if oldnode and oldnode not in nodemap:
360 360 oldnode = None
361 361
362 362 result[newnode] = (oldnode, lastdiff, drev)
363 363
364 364 return result
365 365
366 366 def getdiff(ctx, diffopts):
367 367 """plain-text diff without header (user, commit message, etc)"""
368 368 output = util.stringio()
369 369 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
370 370 None, opts=diffopts):
371 371 output.write(chunk)
372 372 return output.getvalue()
373 373
374 374 def creatediff(ctx):
375 375 """create a Differential Diff"""
376 376 repo = ctx.repo()
377 377 repophid = getrepophid(repo)
378 378 # Create a "Differential Diff" via "differential.createrawdiff" API
379 379 params = {b'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
380 380 if repophid:
381 381 params[b'repositoryPHID'] = repophid
382 382 diff = callconduit(repo, b'differential.createrawdiff', params)
383 383 if not diff:
384 384 raise error.Abort(_(b'cannot create diff for %s') % ctx)
385 385 return diff
386 386
387 387 def writediffproperties(ctx, diff):
388 388 """write metadata to diff so patches could be applied losslessly"""
389 389 params = {
390 390 b'diff_id': diff[b'id'],
391 391 b'name': b'hg:meta',
392 392 b'data': templatefilters.json({
393 393 b'user': ctx.user(),
394 394 b'date': b'%d %d' % ctx.date(),
395 395 b'branch': ctx.branch(),
396 396 b'node': ctx.hex(),
397 397 b'parent': ctx.p1().hex(),
398 398 }),
399 399 }
400 400 callconduit(ctx.repo(), b'differential.setdiffproperty', params)
401 401
402 402 params = {
403 403 b'diff_id': diff[b'id'],
404 404 b'name': b'local:commits',
405 405 b'data': templatefilters.json({
406 406 ctx.hex(): {
407 407 b'author': stringutil.person(ctx.user()),
408 408 b'authorEmail': stringutil.email(ctx.user()),
409 409 b'time': int(ctx.date()[0]),
410 410 b'commit': ctx.hex(),
411 411 b'parents': [ctx.p1().hex()],
412 412 b'branch': ctx.branch(),
413 413 },
414 414 }),
415 415 }
416 416 callconduit(ctx.repo(), b'differential.setdiffproperty', params)
417 417
418 418 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
419 olddiff=None, actions=None):
419 olddiff=None, actions=None, comment=None):
420 420 """create or update a Differential Revision
421 421
422 422 If revid is None, create a new Differential Revision, otherwise update
423 423 revid. If parentrevid is not None, set it as a dependency.
424 424
425 425 If oldnode is not None, check if the patch content (without commit message
426 426 and metadata) has changed before creating another diff.
427 427
428 428 If actions is not None, they will be appended to the transaction.
429 429 """
430 430 repo = ctx.repo()
431 431 if oldnode:
432 432 diffopts = mdiff.diffopts(git=True, context=32767)
433 433 oldctx = repo.unfiltered()[oldnode]
434 434 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
435 435 else:
436 436 neednewdiff = True
437 437
438 438 transactions = []
439 439 if neednewdiff:
440 440 diff = creatediff(ctx)
441 441 transactions.append({b'type': b'update', b'value': diff[b'phid']})
442 if comment:
443 transactions.append({b'type': b'comment', b'value': comment})
442 444 else:
443 445 # Even if we don't need to upload a new diff because the patch content
444 446 # does not change. We might still need to update its metadata so
445 447 # pushers could know the correct node metadata.
446 448 assert olddiff
447 449 diff = olddiff
448 450 writediffproperties(ctx, diff)
449 451
450 452 # Use a temporary summary to set dependency. There might be better ways but
451 453 # I cannot find them for now. But do not do that if we are updating an
452 454 # existing revision (revid is not None) since that introduces visible
453 455 # churns (someone edited "Summary" twice) on the web page.
454 456 if parentrevid and revid is None:
455 457 summary = b'Depends on D%d' % parentrevid
456 458 transactions += [{b'type': b'summary', b'value': summary},
457 459 {b'type': b'summary', b'value': b' '}]
458 460
459 461 if actions:
460 462 transactions += actions
461 463
462 464 # Parse commit message and update related fields.
463 465 desc = ctx.description()
464 466 info = callconduit(repo, b'differential.parsecommitmessage',
465 467 {b'corpus': desc})
466 468 for k, v in info[b'fields'].items():
467 469 if k in [b'title', b'summary', b'testPlan']:
468 470 transactions.append({b'type': k, b'value': v})
469 471
470 472 params = {b'transactions': transactions}
471 473 if revid is not None:
472 474 # Update an existing Differential Revision
473 475 params[b'objectIdentifier'] = revid
474 476
475 477 revision = callconduit(repo, b'differential.revision.edit', params)
476 478 if not revision:
477 479 raise error.Abort(_(b'cannot create revision for %s') % ctx)
478 480
479 481 return revision, diff
480 482
481 483 def userphids(repo, names):
482 484 """convert user names to PHIDs"""
483 485 names = [name.lower() for name in names]
484 486 query = {b'constraints': {b'usernames': names}}
485 487 result = callconduit(repo, b'user.search', query)
486 488 # username not found is not an error of the API. So check if we have missed
487 489 # some names here.
488 490 data = result[b'data']
489 491 resolved = set(entry[b'fields'][b'username'].lower() for entry in data)
490 492 unresolved = set(names) - resolved
491 493 if unresolved:
492 494 raise error.Abort(_(b'unknown username: %s')
493 495 % b' '.join(sorted(unresolved)))
494 496 return [entry[b'phid'] for entry in data]
495 497
496 498 @vcrcommand(b'phabsend',
497 499 [(b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
498 500 (b'', b'amend', True, _(b'update commit messages')),
499 501 (b'', b'reviewer', [], _(b'specify reviewers')),
502 (b'm', b'comment', b'',
503 _(b'add a comment to Revisions with new/updated Diffs')),
500 504 (b'', b'confirm', None, _(b'ask for confirmation before sending'))],
501 505 _(b'REV [OPTIONS]'),
502 506 helpcategory=command.CATEGORY_IMPORT_EXPORT)
503 507 def phabsend(ui, repo, *revs, **opts):
504 508 """upload changesets to Phabricator
505 509
506 510 If there are multiple revisions specified, they will be send as a stack
507 511 with a linear dependencies relationship using the order specified by the
508 512 revset.
509 513
510 514 For the first time uploading changesets, local tags will be created to
511 515 maintain the association. After the first time, phabsend will check
512 516 obsstore and tags information so it can figure out whether to update an
513 517 existing Differential Revision, or create a new one.
514 518
515 519 If --amend is set, update commit messages so they have the
516 520 ``Differential Revision`` URL, remove related tags. This is similar to what
517 521 arcanist will do, and is more desired in author-push workflows. Otherwise,
518 522 use local tags to record the ``Differential Revision`` association.
519 523
520 524 The --confirm option lets you confirm changesets before sending them. You
521 525 can also add following to your configuration file to make it default
522 526 behaviour::
523 527
524 528 [phabsend]
525 529 confirm = true
526 530
527 531 phabsend will check obsstore and the above association to decide whether to
528 532 update an existing Differential Revision, or create a new one.
529 533 """
530 534 opts = pycompat.byteskwargs(opts)
531 535 revs = list(revs) + opts.get(b'rev', [])
532 536 revs = scmutil.revrange(repo, revs)
533 537
534 538 if not revs:
535 539 raise error.Abort(_(b'phabsend requires at least one changeset'))
536 540 if opts.get(b'amend'):
537 541 cmdutil.checkunfinished(repo)
538 542
539 543 # {newnode: (oldnode, olddiff, olddrev}
540 544 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
541 545
542 546 confirm = ui.configbool(b'phabsend', b'confirm')
543 547 confirm |= bool(opts.get(b'confirm'))
544 548 if confirm:
545 549 confirmed = _confirmbeforesend(repo, revs, oldmap)
546 550 if not confirmed:
547 551 raise error.Abort(_(b'phabsend cancelled'))
548 552
549 553 actions = []
550 554 reviewers = opts.get(b'reviewer', [])
551 555 if reviewers:
552 556 phids = userphids(repo, reviewers)
553 557 actions.append({b'type': b'reviewers.add', b'value': phids})
554 558
555 559 drevids = [] # [int]
556 560 diffmap = {} # {newnode: diff}
557 561
558 562 # Send patches one by one so we know their Differential Revision IDs and
559 563 # can provide dependency relationship
560 564 lastrevid = None
561 565 for rev in revs:
562 566 ui.debug(b'sending rev %d\n' % rev)
563 567 ctx = repo[rev]
564 568
565 569 # Get Differential Revision ID
566 570 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
567 571 if oldnode != ctx.node() or opts.get(b'amend'):
568 572 # Create or update Differential Revision
569 573 revision, diff = createdifferentialrevision(
570 ctx, revid, lastrevid, oldnode, olddiff, actions)
574 ctx, revid, lastrevid, oldnode, olddiff, actions,
575 opts.get(b'comment'))
571 576 diffmap[ctx.node()] = diff
572 577 newrevid = int(revision[b'object'][b'id'])
573 578 if revid:
574 579 action = b'updated'
575 580 else:
576 581 action = b'created'
577 582
578 583 # Create a local tag to note the association, if commit message
579 584 # does not have it already
580 585 m = _differentialrevisiondescre.search(ctx.description())
581 586 if not m or int(m.group(r'id')) != newrevid:
582 587 tagname = b'D%d' % newrevid
583 588 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
584 589 date=None, local=True)
585 590 else:
586 591 # Nothing changed. But still set "newrevid" so the next revision
587 592 # could depend on this one.
588 593 newrevid = revid
589 594 action = b'skipped'
590 595
591 596 actiondesc = ui.label(
592 597 {b'created': _(b'created'),
593 598 b'skipped': _(b'skipped'),
594 599 b'updated': _(b'updated')}[action],
595 600 b'phabricator.action.%s' % action)
596 601 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
597 602 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
598 603 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
599 604 ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc,
600 605 desc))
601 606 drevids.append(newrevid)
602 607 lastrevid = newrevid
603 608
604 609 # Update commit messages and remove tags
605 610 if opts.get(b'amend'):
606 611 unfi = repo.unfiltered()
607 612 drevs = callconduit(repo, b'differential.query', {b'ids': drevids})
608 613 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
609 614 wnode = unfi[b'.'].node()
610 615 mapping = {} # {oldnode: [newnode]}
611 616 for i, rev in enumerate(revs):
612 617 old = unfi[rev]
613 618 drevid = drevids[i]
614 619 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
615 620 newdesc = getdescfromdrev(drev)
616 621 # Make sure commit message contain "Differential Revision"
617 622 if old.description() != newdesc:
618 623 if old.phase() == phases.public:
619 624 ui.warn(_("warning: not updating public commit %s\n")
620 625 % scmutil.formatchangeid(old))
621 626 continue
622 627 parents = [
623 628 mapping.get(old.p1().node(), (old.p1(),))[0],
624 629 mapping.get(old.p2().node(), (old.p2(),))[0],
625 630 ]
626 631 new = context.metadataonlyctx(
627 632 repo, old, parents=parents, text=newdesc,
628 633 user=old.user(), date=old.date(), extra=old.extra())
629 634
630 635 newnode = new.commit()
631 636
632 637 mapping[old.node()] = [newnode]
633 638 # Update diff property
634 639 writediffproperties(unfi[newnode], diffmap[old.node()])
635 640 # Remove local tags since it's no longer necessary
636 641 tagname = b'D%d' % drevid
637 642 if tagname in repo.tags():
638 643 tags.tag(repo, tagname, nullid, message=None, user=None,
639 644 date=None, local=True)
640 645 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
641 646 if wnode in mapping:
642 647 unfi.setparents(mapping[wnode][0])
643 648
644 649 # Map from "hg:meta" keys to header understood by "hg import". The order is
645 650 # consistent with "hg export" output.
646 651 _metanamemap = util.sortdict([(b'user', b'User'), (b'date', b'Date'),
647 652 (b'branch', b'Branch'), (b'node', b'Node ID'),
648 653 (b'parent', b'Parent ')])
649 654
650 655 def _confirmbeforesend(repo, revs, oldmap):
651 656 url, token = readurltoken(repo)
652 657 ui = repo.ui
653 658 for rev in revs:
654 659 ctx = repo[rev]
655 660 desc = ctx.description().splitlines()[0]
656 661 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
657 662 if drevid:
658 663 drevdesc = ui.label(b'D%s' % drevid, b'phabricator.drev')
659 664 else:
660 665 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
661 666
662 667 ui.write(_(b'%s - %s: %s\n')
663 668 % (drevdesc,
664 669 ui.label(bytes(ctx), b'phabricator.node'),
665 670 ui.label(desc, b'phabricator.desc')))
666 671
667 672 if ui.promptchoice(_(b'Send the above changes to %s (yn)?'
668 673 b'$$ &Yes $$ &No') % url):
669 674 return False
670 675
671 676 return True
672 677
673 678 _knownstatusnames = {b'accepted', b'needsreview', b'needsrevision', b'closed',
674 679 b'abandoned'}
675 680
676 681 def _getstatusname(drev):
677 682 """get normalized status name from a Differential Revision"""
678 683 return drev[b'statusName'].replace(b' ', b'').lower()
679 684
680 685 # Small language to specify differential revisions. Support symbols: (), :X,
681 686 # +, and -.
682 687
683 688 _elements = {
684 689 # token-type: binding-strength, primary, prefix, infix, suffix
685 690 b'(': (12, None, (b'group', 1, b')'), None, None),
686 691 b':': (8, None, (b'ancestors', 8), None, None),
687 692 b'&': (5, None, None, (b'and_', 5), None),
688 693 b'+': (4, None, None, (b'add', 4), None),
689 694 b'-': (4, None, None, (b'sub', 4), None),
690 695 b')': (0, None, None, None, None),
691 696 b'symbol': (0, b'symbol', None, None, None),
692 697 b'end': (0, None, None, None, None),
693 698 }
694 699
695 700 def _tokenize(text):
696 701 view = memoryview(text) # zero-copy slice
697 702 special = b'():+-& '
698 703 pos = 0
699 704 length = len(text)
700 705 while pos < length:
701 706 symbol = b''.join(itertools.takewhile(lambda ch: ch not in special,
702 707 pycompat.iterbytestr(view[pos:])))
703 708 if symbol:
704 709 yield (b'symbol', symbol, pos)
705 710 pos += len(symbol)
706 711 else: # special char, ignore space
707 712 if text[pos] != b' ':
708 713 yield (text[pos], None, pos)
709 714 pos += 1
710 715 yield (b'end', None, pos)
711 716
712 717 def _parse(text):
713 718 tree, pos = parser.parser(_elements).parse(_tokenize(text))
714 719 if pos != len(text):
715 720 raise error.ParseError(b'invalid token', pos)
716 721 return tree
717 722
718 723 def _parsedrev(symbol):
719 724 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
720 725 if symbol.startswith(b'D') and symbol[1:].isdigit():
721 726 return int(symbol[1:])
722 727 if symbol.isdigit():
723 728 return int(symbol)
724 729
725 730 def _prefetchdrevs(tree):
726 731 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
727 732 drevs = set()
728 733 ancestordrevs = set()
729 734 op = tree[0]
730 735 if op == b'symbol':
731 736 r = _parsedrev(tree[1])
732 737 if r:
733 738 drevs.add(r)
734 739 elif op == b'ancestors':
735 740 r, a = _prefetchdrevs(tree[1])
736 741 drevs.update(r)
737 742 ancestordrevs.update(r)
738 743 ancestordrevs.update(a)
739 744 else:
740 745 for t in tree[1:]:
741 746 r, a = _prefetchdrevs(t)
742 747 drevs.update(r)
743 748 ancestordrevs.update(a)
744 749 return drevs, ancestordrevs
745 750
746 751 def querydrev(repo, spec):
747 752 """return a list of "Differential Revision" dicts
748 753
749 754 spec is a string using a simple query language, see docstring in phabread
750 755 for details.
751 756
752 757 A "Differential Revision dict" looks like:
753 758
754 759 {
755 760 "id": "2",
756 761 "phid": "PHID-DREV-672qvysjcczopag46qty",
757 762 "title": "example",
758 763 "uri": "https://phab.example.com/D2",
759 764 "dateCreated": "1499181406",
760 765 "dateModified": "1499182103",
761 766 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
762 767 "status": "0",
763 768 "statusName": "Needs Review",
764 769 "properties": [],
765 770 "branch": null,
766 771 "summary": "",
767 772 "testPlan": "",
768 773 "lineCount": "2",
769 774 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
770 775 "diffs": [
771 776 "3",
772 777 "4",
773 778 ],
774 779 "commits": [],
775 780 "reviewers": [],
776 781 "ccs": [],
777 782 "hashes": [],
778 783 "auxiliary": {
779 784 "phabricator:projects": [],
780 785 "phabricator:depends-on": [
781 786 "PHID-DREV-gbapp366kutjebt7agcd"
782 787 ]
783 788 },
784 789 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
785 790 "sourcePath": null
786 791 }
787 792 """
788 793 def fetch(params):
789 794 """params -> single drev or None"""
790 795 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
791 796 if key in prefetched:
792 797 return prefetched[key]
793 798 drevs = callconduit(repo, b'differential.query', params)
794 799 # Fill prefetched with the result
795 800 for drev in drevs:
796 801 prefetched[drev[b'phid']] = drev
797 802 prefetched[int(drev[b'id'])] = drev
798 803 if key not in prefetched:
799 804 raise error.Abort(_(b'cannot get Differential Revision %r')
800 805 % params)
801 806 return prefetched[key]
802 807
803 808 def getstack(topdrevids):
804 809 """given a top, get a stack from the bottom, [id] -> [id]"""
805 810 visited = set()
806 811 result = []
807 812 queue = [{b'ids': [i]} for i in topdrevids]
808 813 while queue:
809 814 params = queue.pop()
810 815 drev = fetch(params)
811 816 if drev[b'id'] in visited:
812 817 continue
813 818 visited.add(drev[b'id'])
814 819 result.append(int(drev[b'id']))
815 820 auxiliary = drev.get(b'auxiliary', {})
816 821 depends = auxiliary.get(b'phabricator:depends-on', [])
817 822 for phid in depends:
818 823 queue.append({b'phids': [phid]})
819 824 result.reverse()
820 825 return smartset.baseset(result)
821 826
822 827 # Initialize prefetch cache
823 828 prefetched = {} # {id or phid: drev}
824 829
825 830 tree = _parse(spec)
826 831 drevs, ancestordrevs = _prefetchdrevs(tree)
827 832
828 833 # developer config: phabricator.batchsize
829 834 batchsize = repo.ui.configint(b'phabricator', b'batchsize')
830 835
831 836 # Prefetch Differential Revisions in batch
832 837 tofetch = set(drevs)
833 838 for r in ancestordrevs:
834 839 tofetch.update(range(max(1, r - batchsize), r + 1))
835 840 if drevs:
836 841 fetch({b'ids': list(tofetch)})
837 842 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
838 843
839 844 # Walk through the tree, return smartsets
840 845 def walk(tree):
841 846 op = tree[0]
842 847 if op == b'symbol':
843 848 drev = _parsedrev(tree[1])
844 849 if drev:
845 850 return smartset.baseset([drev])
846 851 elif tree[1] in _knownstatusnames:
847 852 drevs = [r for r in validids
848 853 if _getstatusname(prefetched[r]) == tree[1]]
849 854 return smartset.baseset(drevs)
850 855 else:
851 856 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
852 857 elif op in {b'and_', b'add', b'sub'}:
853 858 assert len(tree) == 3
854 859 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
855 860 elif op == b'group':
856 861 return walk(tree[1])
857 862 elif op == b'ancestors':
858 863 return getstack(walk(tree[1]))
859 864 else:
860 865 raise error.ProgrammingError(b'illegal tree: %r' % tree)
861 866
862 867 return [prefetched[r] for r in walk(tree)]
863 868
864 869 def getdescfromdrev(drev):
865 870 """get description (commit message) from "Differential Revision"
866 871
867 872 This is similar to differential.getcommitmessage API. But we only care
868 873 about limited fields: title, summary, test plan, and URL.
869 874 """
870 875 title = drev[b'title']
871 876 summary = drev[b'summary'].rstrip()
872 877 testplan = drev[b'testPlan'].rstrip()
873 878 if testplan:
874 879 testplan = b'Test Plan:\n%s' % testplan
875 880 uri = b'Differential Revision: %s' % drev[b'uri']
876 881 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
877 882
878 883 def getdiffmeta(diff):
879 884 """get commit metadata (date, node, user, p1) from a diff object
880 885
881 886 The metadata could be "hg:meta", sent by phabsend, like:
882 887
883 888 "properties": {
884 889 "hg:meta": {
885 890 "date": "1499571514 25200",
886 891 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
887 892 "user": "Foo Bar <foo@example.com>",
888 893 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
889 894 }
890 895 }
891 896
892 897 Or converted from "local:commits", sent by "arc", like:
893 898
894 899 "properties": {
895 900 "local:commits": {
896 901 "98c08acae292b2faf60a279b4189beb6cff1414d": {
897 902 "author": "Foo Bar",
898 903 "time": 1499546314,
899 904 "branch": "default",
900 905 "tag": "",
901 906 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
902 907 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
903 908 "local": "1000",
904 909 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
905 910 "summary": "...",
906 911 "message": "...",
907 912 "authorEmail": "foo@example.com"
908 913 }
909 914 }
910 915 }
911 916
912 917 Note: metadata extracted from "local:commits" will lose time zone
913 918 information.
914 919 """
915 920 props = diff.get(b'properties') or {}
916 921 meta = props.get(b'hg:meta')
917 922 if not meta:
918 923 if props.get(b'local:commits'):
919 924 commit = sorted(props[b'local:commits'].values())[0]
920 925 meta = {}
921 926 if b'author' in commit and b'authorEmail' in commit:
922 927 meta[b'user'] = b'%s <%s>' % (commit[b'author'],
923 928 commit[b'authorEmail'])
924 929 if b'time' in commit:
925 930 meta[b'date'] = b'%d 0' % commit[b'time']
926 931 if b'branch' in commit:
927 932 meta[b'branch'] = commit[b'branch']
928 933 node = commit.get(b'commit', commit.get(b'rev'))
929 934 if node:
930 935 meta[b'node'] = node
931 936 if len(commit.get(b'parents', ())) >= 1:
932 937 meta[b'parent'] = commit[b'parents'][0]
933 938 else:
934 939 meta = {}
935 940 if b'date' not in meta and b'dateCreated' in diff:
936 941 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
937 942 if b'branch' not in meta and diff.get(b'branch'):
938 943 meta[b'branch'] = diff[b'branch']
939 944 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
940 945 meta[b'parent'] = diff[b'sourceControlBaseRevision']
941 946 return meta
942 947
943 948 def readpatch(repo, drevs, write):
944 949 """generate plain-text patch readable by 'hg import'
945 950
946 951 write is usually ui.write. drevs is what "querydrev" returns, results of
947 952 "differential.query".
948 953 """
949 954 # Prefetch hg:meta property for all diffs
950 955 diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))
951 956 diffs = callconduit(repo, b'differential.querydiffs', {b'ids': diffids})
952 957
953 958 # Generate patch for each drev
954 959 for drev in drevs:
955 960 repo.ui.note(_(b'reading D%s\n') % drev[b'id'])
956 961
957 962 diffid = max(int(v) for v in drev[b'diffs'])
958 963 body = callconduit(repo, b'differential.getrawdiff',
959 964 {b'diffID': diffid})
960 965 desc = getdescfromdrev(drev)
961 966 header = b'# HG changeset patch\n'
962 967
963 968 # Try to preserve metadata from hg:meta property. Write hg patch
964 969 # headers that can be read by the "import" command. See patchheadermap
965 970 # and extract in mercurial/patch.py for supported headers.
966 971 meta = getdiffmeta(diffs[b'%d' % diffid])
967 972 for k in _metanamemap.keys():
968 973 if k in meta:
969 974 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
970 975
971 976 content = b'%s%s\n%s' % (header, desc, body)
972 977 write(content)
973 978
974 979 @vcrcommand(b'phabread',
975 980 [(b'', b'stack', False, _(b'read dependencies'))],
976 981 _(b'DREVSPEC [OPTIONS]'),
977 982 helpcategory=command.CATEGORY_IMPORT_EXPORT)
978 983 def phabread(ui, repo, spec, **opts):
979 984 """print patches from Phabricator suitable for importing
980 985
981 986 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
982 987 the number ``123``. It could also have common operators like ``+``, ``-``,
983 988 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
984 989 select a stack.
985 990
986 991 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
987 992 could be used to filter patches by status. For performance reason, they
988 993 only represent a subset of non-status selections and cannot be used alone.
989 994
990 995 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
991 996 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
992 997 stack up to D9.
993 998
994 999 If --stack is given, follow dependencies information and read all patches.
995 1000 It is equivalent to the ``:`` operator.
996 1001 """
997 1002 opts = pycompat.byteskwargs(opts)
998 1003 if opts.get(b'stack'):
999 1004 spec = b':(%s)' % spec
1000 1005 drevs = querydrev(repo, spec)
1001 1006 readpatch(repo, drevs, ui.write)
1002 1007
1003 1008 @vcrcommand(b'phabupdate',
1004 1009 [(b'', b'accept', False, _(b'accept revisions')),
1005 1010 (b'', b'reject', False, _(b'reject revisions')),
1006 1011 (b'', b'abandon', False, _(b'abandon revisions')),
1007 1012 (b'', b'reclaim', False, _(b'reclaim revisions')),
1008 1013 (b'm', b'comment', b'', _(b'comment on the last revision')),
1009 1014 ], _(b'DREVSPEC [OPTIONS]'),
1010 1015 helpcategory=command.CATEGORY_IMPORT_EXPORT)
1011 1016 def phabupdate(ui, repo, spec, **opts):
1012 1017 """update Differential Revision in batch
1013 1018
1014 1019 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
1015 1020 """
1016 1021 opts = pycompat.byteskwargs(opts)
1017 1022 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
1018 1023 if len(flags) > 1:
1019 1024 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
1020 1025
1021 1026 actions = []
1022 1027 for f in flags:
1023 1028 actions.append({b'type': f, b'value': b'true'})
1024 1029
1025 1030 drevs = querydrev(repo, spec)
1026 1031 for i, drev in enumerate(drevs):
1027 1032 if i + 1 == len(drevs) and opts.get(b'comment'):
1028 1033 actions.append({b'type': b'comment', b'value': opts[b'comment']})
1029 1034 if actions:
1030 1035 params = {b'objectIdentifier': drev[b'phid'],
1031 1036 b'transactions': actions}
1032 1037 callconduit(repo, b'differential.revision.edit', params)
1033 1038
1034 1039 templatekeyword = registrar.templatekeyword()
1035 1040
1036 1041 @templatekeyword(b'phabreview', requires={b'ctx'})
1037 1042 def template_review(context, mapping):
1038 1043 """:phabreview: Object describing the review for this changeset.
1039 1044 Has attributes `url` and `id`.
1040 1045 """
1041 1046 ctx = context.resource(mapping, b'ctx')
1042 1047 m = _differentialrevisiondescre.search(ctx.description())
1043 1048 if m:
1044 1049 return templateutil.hybriddict({
1045 1050 b'url': m.group(r'url'),
1046 1051 b'id': b"D%s" % m.group(r'id'),
1047 1052 })
1048 1053 else:
1049 1054 tags = ctx.repo().nodetags(ctx.node())
1050 1055 for t in tags:
1051 1056 if _differentialrevisiontagre.match(t):
1052 1057 url = ctx.repo().ui.config(b'phabricator', b'url')
1053 1058 if not url.endswith(b'/'):
1054 1059 url += b'/'
1055 1060 url += t
1056 1061
1057 1062 return templateutil.hybriddict({
1058 1063 b'url': url,
1059 1064 b'id': t,
1060 1065 })
1061 1066 return None
@@ -1,121 +1,134 b''
1 1 #require vcr
2 2 $ cat >> $HGRCPATH <<EOF
3 3 > [extensions]
4 4 > phabricator =
5 5 > EOF
6 6 $ hg init repo
7 7 $ cd repo
8 8 $ cat >> .hg/hgrc <<EOF
9 9 > [phabricator]
10 10 > url = https://phab.mercurial-scm.org/
11 11 > callsign = HG
12 12 >
13 13 > [auth]
14 14 > hgphab.schemes = https
15 15 > hgphab.prefix = phab.mercurial-scm.org
16 16 > # When working on the extension and making phabricator interaction
17 17 > # changes, edit this to be a real phabricator token. When done, edit
18 18 > # it back, and make sure to also edit your VCR transcripts to match
19 19 > # whatever value you put here.
20 20 > hgphab.phabtoken = cli-hahayouwish
21 21 > EOF
22 22 $ VCR="$TESTDIR/phabricator"
23 23
24 24 Error is handled reasonably. We override the phabtoken here so that
25 25 when you're developing changes to phabricator.py you can edit the
26 26 above config and have a real token in the test but not have to edit
27 27 this test.
28 28 $ hg phabread --config auth.hgphab.phabtoken=cli-notavalidtoken \
29 29 > --test-vcr "$VCR/phabread-conduit-error.json" D4480 | head
30 30 abort: Conduit Error (ERR-INVALID-AUTH): API token "cli-notavalidtoken" has the wrong length. API tokens should be 32 characters long.
31 31
32 32 Basic phabread:
33 33 $ hg phabread --test-vcr "$VCR/phabread-4480.json" D4480 | head
34 34 # HG changeset patch
35 35 # Date 1536771503 0
36 36 # Parent a5de21c9e3703f8e8eb064bd7d893ff2f703c66a
37 37 exchangev2: start to implement pull with wire protocol v2
38 38
39 39 Wire protocol version 2 will take a substantially different
40 40 approach to exchange than version 1 (at least as far as pulling
41 41 is concerned).
42 42
43 43 This commit establishes a new exchangev2 module for holding
44 44
45 45 phabupdate with an accept:
46 46 $ hg phabupdate --accept D4564 \
47 47 > -m 'I think I like where this is headed. Will read rest of series later.'\
48 48 > --test-vcr "$VCR/accept-4564.json"
49 49
50 50 Create a differential diff:
51 51 $ HGENCODING=utf-8; export HGENCODING
52 52 $ echo alpha > alpha
53 53 $ hg ci --addremove -m 'create alpha for phabricator test €'
54 54 adding alpha
55 55 $ hg phabsend -r . --test-vcr "$VCR/phabsend-create-alpha.json"
56 56 D1190 - created - d386117f30e6: create alpha for phabricator test \xe2\x82\xac (esc)
57 57 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d386117f30e6-24ffe649-phabsend.hg
58 58 $ echo more >> alpha
59 59 $ HGEDITOR=true hg ci --amend
60 60 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/a86ed7d85e86-b7a54f3b-amend.hg
61 61 $ echo beta > beta
62 62 $ hg ci --addremove -m 'create beta for phabricator test'
63 63 adding beta
64 64 $ hg phabsend -r ".^::" --test-vcr "$VCR/phabsend-update-alpha-create-beta.json"
65 65 D1190 - updated - d940d39fb603: create alpha for phabricator test \xe2\x82\xac (esc)
66 66 D1191 - created - 4b2486dfc8c7: create beta for phabricator test
67 67 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/4b2486dfc8c7-d90584fa-phabsend.hg
68 68 $ unset HGENCODING
69 69
70 70 The amend won't explode after posting a public commit. The local tag is left
71 71 behind to identify it.
72 72
73 73 $ echo 'public change' > beta
74 74 $ hg ci -m 'create public change for phabricator testing'
75 75 $ hg phase --public .
76 76 $ echo 'draft change' > alpha
77 77 $ hg ci -m 'create draft change for phabricator testing'
78 78 $ hg phabsend --amend -r '.^::' --test-vcr "$VCR/phabsend-create-public.json"
79 79 D1192 - created - 24ffd6bca53a: create public change for phabricator testing
80 80 D1193 - created - ac331633be79: create draft change for phabricator testing
81 81 warning: not updating public commit 2:24ffd6bca53a
82 82 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/ac331633be79-719b961c-phabsend.hg
83 83 $ hg tags -v
84 84 tip 3:a19f1434f9a5
85 85 D1192 2:24ffd6bca53a local
86 86
87 87 $ hg debugcallconduit user.search --test-vcr "$VCR/phab-conduit.json" <<EOF
88 88 > {
89 89 > "constraints": {
90 90 > "isBot": true
91 91 > }
92 92 > }
93 93 > EOF
94 94 {
95 95 "cursor": {
96 96 "after": null,
97 97 "before": null,
98 98 "limit": 100,
99 99 "order": null
100 100 },
101 101 "data": [],
102 102 "maps": {},
103 103 "query": {
104 104 "queryKey": null
105 105 }
106 106 }
107 107
108 108 Template keywords
109 109 $ hg log -T'{rev} {phabreview|json}\n'
110 110 3 {"id": "D1193", "url": "https://phab.mercurial-scm.org/D1193"}
111 111 2 {"id": "D1192", "url": "https://phab.mercurial-scm.org/D1192"}
112 112 1 {"id": "D1191", "url": "https://phab.mercurial-scm.org/D1191"}
113 113 0 {"id": "D1190", "url": "https://phab.mercurial-scm.org/D1190"}
114 114
115 115 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n'
116 116 3 https://phab.mercurial-scm.org/D1193 D1193
117 117 2 https://phab.mercurial-scm.org/D1192 D1192
118 118 1 https://phab.mercurial-scm.org/D1191 D1191
119 119 0 https://phab.mercurial-scm.org/D1190 D1190
120 120
121 Commenting when phabsending:
122 $ echo comment > comment
123 $ hg ci --addremove -m "create comment for phabricator test"
124 adding comment
125 $ hg phabsend -r . -m "For default branch" --test-vcr "$VCR/phabsend-comment-created.json"
126 D1253 - created - a7ee4bac036a: create comment for phabricator test
127 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/a7ee4bac036a-8009b5a0-phabsend.hg
128 $ echo comment2 >> comment
129 $ hg ci --amend
130 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/81fce7de1b7d-05339e5b-amend.hg
131 $ hg phabsend -r . -m "Address review comments" --test-vcr "$VCR/phabsend-comment-updated.json"
132 D1253 - updated - 1acd4b60af38: create comment for phabricator test
133
121 134 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now