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