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