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