##// END OF EJS Templates
phabricator: fix a crash when submitting binaries (issue6260)...
Matt Harbison -
r44630:5d85e9dd stable
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (1909 lines changed) Show them Hide them
@@ -0,0 +1,1909 b''
1 {
2 "version": 1,
3 "interactions": [
4 {
5 "response": {
6 "status": {
7 "message": "OK",
8 "code": 200
9 },
10 "headers": {
11 "strict-transport-security": [
12 "max-age=0; includeSubdomains; preload"
13 ],
14 "server": [
15 "Apache/2.4.10 (Debian)"
16 ],
17 "referrer-policy": [
18 "no-referrer"
19 ],
20 "x-frame-options": [
21 "Deny"
22 ],
23 "expires": [
24 "Sat, 01 Jan 2000 00:00:00 GMT"
25 ],
26 "x-content-type-options": [
27 "nosniff"
28 ],
29 "cache-control": [
30 "no-store"
31 ],
32 "content-type": [
33 "application/json"
34 ],
35 "x-xss-protection": [
36 "1; mode=block"
37 ],
38 "transfer-encoding": [
39 "chunked"
40 ],
41 "date": [
42 "Sat, 25 Jan 2020 06:01:03 GMT"
43 ]
44 },
45 "body": {
46 "string": "{\"result\":{\"data\":[{\"id\":2,\"type\":\"REPO\",\"phid\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"fields\":{\"name\":\"Mercurial\",\"vcs\":\"hg\",\"callsign\":\"HG\",\"shortName\":null,\"status\":\"active\",\"isImporting\":false,\"almanacServicePHID\":null,\"refRules\":{\"fetchRules\":[],\"trackRules\":[],\"permanentRefRules\":[]},\"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}"
47 }
48 },
49 "request": {
50 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22constraints%22%3A+%7B%22callsigns%22%3A+%5B%22HG%22%5D%7D%7D",
51 "method": "POST",
52 "headers": {
53 "content-type": [
54 "application/x-www-form-urlencoded"
55 ],
56 "user-agent": [
57 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
58 ],
59 "content-length": [
60 "183"
61 ],
62 "host": [
63 "phab.mercurial-scm.org"
64 ],
65 "accept": [
66 "application/mercurial-0.1"
67 ]
68 },
69 "uri": "https://phab.mercurial-scm.org//api/diffusion.repository.search"
70 }
71 },
72 {
73 "response": {
74 "status": {
75 "message": "OK",
76 "code": 200
77 },
78 "headers": {
79 "strict-transport-security": [
80 "max-age=0; includeSubdomains; preload"
81 ],
82 "server": [
83 "Apache/2.4.10 (Debian)"
84 ],
85 "referrer-policy": [
86 "no-referrer"
87 ],
88 "x-frame-options": [
89 "Deny"
90 ],
91 "expires": [
92 "Sat, 01 Jan 2000 00:00:00 GMT"
93 ],
94 "x-content-type-options": [
95 "nosniff"
96 ],
97 "cache-control": [
98 "no-store"
99 ],
100 "content-type": [
101 "application/json"
102 ],
103 "x-xss-protection": [
104 "1; mode=block"
105 ],
106 "transfer-encoding": [
107 "chunked"
108 ],
109 "date": [
110 "Sat, 25 Jan 2020 06:01:04 GMT"
111 ]
112 },
113 "body": {
114 "string": "{\"result\":{\"upload\":true,\"filePHID\":null},\"error_code\":null,\"error_info\":null}"
115 }
116 },
117 "request": {
118 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22contentHash%22%3A+%22022a6979e6dab7aa5ae4c3e5e45f7e977112a7e63593820dbec1ec738a24f93c%22%2C+%22contentLength%22%3A+2%2C+%22name%22%3A+%22bin%22%7D",
119 "method": "POST",
120 "headers": {
121 "content-type": [
122 "application/x-www-form-urlencoded"
123 ],
124 "user-agent": [
125 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
126 ],
127 "content-length": [
128 "269"
129 ],
130 "host": [
131 "phab.mercurial-scm.org"
132 ],
133 "accept": [
134 "application/mercurial-0.1"
135 ]
136 },
137 "uri": "https://phab.mercurial-scm.org//api/file.allocate"
138 }
139 },
140 {
141 "response": {
142 "status": {
143 "message": "OK",
144 "code": 200
145 },
146 "headers": {
147 "strict-transport-security": [
148 "max-age=0; includeSubdomains; preload"
149 ],
150 "server": [
151 "Apache/2.4.10 (Debian)"
152 ],
153 "referrer-policy": [
154 "no-referrer"
155 ],
156 "x-frame-options": [
157 "Deny"
158 ],
159 "expires": [
160 "Sat, 01 Jan 2000 00:00:00 GMT"
161 ],
162 "x-content-type-options": [
163 "nosniff"
164 ],
165 "cache-control": [
166 "no-store"
167 ],
168 "content-type": [
169 "application/json"
170 ],
171 "x-xss-protection": [
172 "1; mode=block"
173 ],
174 "transfer-encoding": [
175 "chunked"
176 ],
177 "date": [
178 "Sat, 25 Jan 2020 06:01:04 GMT"
179 ]
180 },
181 "body": {
182 "string": "{\"result\":\"PHID-FILE-6icohlowpfhuvuut4kt4\",\"error_code\":null,\"error_info\":null}"
183 }
184 },
185 "request": {
186 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data_base64%22%3A+%22AGE%3D%22%2C+%22name%22%3A+%22bin%22%7D",
187 "method": "POST",
188 "headers": {
189 "content-type": [
190 "application/x-www-form-urlencoded"
191 ],
192 "user-agent": [
193 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
194 ],
195 "content-length": [
196 "183"
197 ],
198 "host": [
199 "phab.mercurial-scm.org"
200 ],
201 "accept": [
202 "application/mercurial-0.1"
203 ]
204 },
205 "uri": "https://phab.mercurial-scm.org//api/file.upload"
206 }
207 },
208 {
209 "response": {
210 "status": {
211 "message": "OK",
212 "code": 200
213 },
214 "headers": {
215 "strict-transport-security": [
216 "max-age=0; includeSubdomains; preload"
217 ],
218 "server": [
219 "Apache/2.4.10 (Debian)"
220 ],
221 "referrer-policy": [
222 "no-referrer"
223 ],
224 "x-frame-options": [
225 "Deny"
226 ],
227 "expires": [
228 "Sat, 01 Jan 2000 00:00:00 GMT"
229 ],
230 "x-content-type-options": [
231 "nosniff"
232 ],
233 "cache-control": [
234 "no-store"
235 ],
236 "content-type": [
237 "application/json"
238 ],
239 "x-xss-protection": [
240 "1; mode=block"
241 ],
242 "transfer-encoding": [
243 "chunked"
244 ],
245 "date": [
246 "Sat, 25 Jan 2020 06:01:05 GMT"
247 ]
248 },
249 "body": {
250 "string": "{\"result\":{\"diffid\":19599,\"phid\":\"PHID-DIFF-uvk7qaq6iglk4wgk2lxb\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/differential\\/diff\\/19599\\/\"},\"error_code\":null,\"error_info\":null}"
251 }
252 },
253 "request": {
254 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22bookmark%22%3A+null%2C+%22branch%22%3A+%22default%22%2C+%22changes%22%3A+%7B%22bin%22%3A+%7B%22addLines%22%3A+0%2C+%22awayPaths%22%3A+%5B%5D%2C+%22commitHash%22%3A+null%2C+%22currentPath%22%3A+%22bin%22%2C+%22delLines%22%3A+0%2C+%22fileType%22%3A+3%2C+%22hunks%22%3A+%5B%5D%2C+%22metadata%22%3A+%7B%22new%3Abinary-phid%22%3A+%22PHID-FILE-6icohlowpfhuvuut4kt4%22%2C+%22new%3Afile%3Asize%22%3A+2%7D%2C+%22newProperties%22%3A+%7B%22unix%3Afilemode%22%3A+%22100644%22%7D%2C+%22oldPath%22%3A+null%2C+%22oldProperties%22%3A+%7B%7D%2C+%22type%22%3A+1%7D%7D%2C+%22creationMethod%22%3A+%22phabsend%22%2C+%22lintStatus%22%3A+%22none%22%2C+%22repositoryPHID%22%3A+%22PHID-REPO-bvunnehri4u2isyr7bc3%22%2C+%22sourceControlBaseRevision%22%3A+%221849d7828727a28e14c589323e4f8c9a1c8d2816%22%2C+%22sourceControlPath%22%3A+%22%2F%22%2C+%22sourceControlSystem%22%3A+%22hg%22%2C+%22sourceMachine%22%3A+%22%22%2C+%22sourcePath%22%3A+%22%2F%22%2C+%22unitStatus%22%3A+%22none%22%7D",
255 "method": "POST",
256 "headers": {
257 "content-type": [
258 "application/x-www-form-urlencoded"
259 ],
260 "user-agent": [
261 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
262 ],
263 "content-length": [
264 "1081"
265 ],
266 "host": [
267 "phab.mercurial-scm.org"
268 ],
269 "accept": [
270 "application/mercurial-0.1"
271 ]
272 },
273 "uri": "https://phab.mercurial-scm.org//api/differential.creatediff"
274 }
275 },
276 {
277 "response": {
278 "status": {
279 "message": "OK",
280 "code": 200
281 },
282 "headers": {
283 "strict-transport-security": [
284 "max-age=0; includeSubdomains; preload"
285 ],
286 "server": [
287 "Apache/2.4.10 (Debian)"
288 ],
289 "referrer-policy": [
290 "no-referrer"
291 ],
292 "x-frame-options": [
293 "Deny"
294 ],
295 "expires": [
296 "Sat, 01 Jan 2000 00:00:00 GMT"
297 ],
298 "x-content-type-options": [
299 "nosniff"
300 ],
301 "cache-control": [
302 "no-store"
303 ],
304 "content-type": [
305 "application/json"
306 ],
307 "x-xss-protection": [
308 "1; mode=block"
309 ],
310 "transfer-encoding": [
311 "chunked"
312 ],
313 "date": [
314 "Sat, 25 Jan 2020 06:01:06 GMT"
315 ]
316 },
317 "body": {
318 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
319 }
320 },
321 "request": {
322 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22date%5C%22%3A+%5C%220+0%5C%22%2C+%5C%22node%5C%22%3A+%5C%22aa24a81f55de50addfce4a824eeb919d59b19683%5C%22%2C+%5C%22parent%5C%22%3A+%5C%221849d7828727a28e14c589323e4f8c9a1c8d2816%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+19599%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
323 "method": "POST",
324 "headers": {
325 "content-type": [
326 "application/x-www-form-urlencoded"
327 ],
328 "user-agent": [
329 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
330 ],
331 "content-length": [
332 "482"
333 ],
334 "host": [
335 "phab.mercurial-scm.org"
336 ],
337 "accept": [
338 "application/mercurial-0.1"
339 ]
340 },
341 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
342 }
343 },
344 {
345 "response": {
346 "status": {
347 "message": "OK",
348 "code": 200
349 },
350 "headers": {
351 "strict-transport-security": [
352 "max-age=0; includeSubdomains; preload"
353 ],
354 "server": [
355 "Apache/2.4.10 (Debian)"
356 ],
357 "referrer-policy": [
358 "no-referrer"
359 ],
360 "x-frame-options": [
361 "Deny"
362 ],
363 "expires": [
364 "Sat, 01 Jan 2000 00:00:00 GMT"
365 ],
366 "x-content-type-options": [
367 "nosniff"
368 ],
369 "cache-control": [
370 "no-store"
371 ],
372 "content-type": [
373 "application/json"
374 ],
375 "x-xss-protection": [
376 "1; mode=block"
377 ],
378 "transfer-encoding": [
379 "chunked"
380 ],
381 "date": [
382 "Sat, 25 Jan 2020 06:01:06 GMT"
383 ]
384 },
385 "body": {
386 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
387 }
388 },
389 "request": {
390 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22aa24a81f55de50addfce4a824eeb919d59b19683%5C%22%3A+%7B%5C%22author%5C%22%3A+%5C%22test%5C%22%2C+%5C%22authorEmail%5C%22%3A+%5C%22test%5C%22%2C+%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22commit%5C%22%3A+%5C%22aa24a81f55de50addfce4a824eeb919d59b19683%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%221849d7828727a28e14c589323e4f8c9a1c8d2816%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+19599%2C+%22name%22%3A+%22local%3Acommits%22%7D",
391 "method": "POST",
392 "headers": {
393 "content-type": [
394 "application/x-www-form-urlencoded"
395 ],
396 "user-agent": [
397 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
398 ],
399 "content-length": [
400 "594"
401 ],
402 "host": [
403 "phab.mercurial-scm.org"
404 ],
405 "accept": [
406 "application/mercurial-0.1"
407 ]
408 },
409 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
410 }
411 },
412 {
413 "response": {
414 "status": {
415 "message": "OK",
416 "code": 200
417 },
418 "headers": {
419 "strict-transport-security": [
420 "max-age=0; includeSubdomains; preload"
421 ],
422 "server": [
423 "Apache/2.4.10 (Debian)"
424 ],
425 "referrer-policy": [
426 "no-referrer"
427 ],
428 "x-frame-options": [
429 "Deny"
430 ],
431 "expires": [
432 "Sat, 01 Jan 2000 00:00:00 GMT"
433 ],
434 "x-content-type-options": [
435 "nosniff"
436 ],
437 "cache-control": [
438 "no-store"
439 ],
440 "content-type": [
441 "application/json"
442 ],
443 "x-xss-protection": [
444 "1; mode=block"
445 ],
446 "transfer-encoding": [
447 "chunked"
448 ],
449 "date": [
450 "Sat, 25 Jan 2020 06:01:07 GMT"
451 ]
452 },
453 "body": {
454 "string": "{\"result\":{\"errors\":[],\"fields\":{\"title\":\"add binary\"},\"revisionIDFieldInfo\":{\"value\":null,\"validDomain\":\"https:\\/\\/phab.mercurial-scm.org\"},\"transactions\":[{\"type\":\"title\",\"value\":\"add binary\"}]},\"error_code\":null,\"error_info\":null}"
455 }
456 },
457 "request": {
458 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22corpus%22%3A+%22add+binary%22%7D",
459 "method": "POST",
460 "headers": {
461 "content-type": [
462 "application/x-www-form-urlencoded"
463 ],
464 "user-agent": [
465 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
466 ],
467 "content-length": [
468 "155"
469 ],
470 "host": [
471 "phab.mercurial-scm.org"
472 ],
473 "accept": [
474 "application/mercurial-0.1"
475 ]
476 },
477 "uri": "https://phab.mercurial-scm.org//api/differential.parsecommitmessage"
478 }
479 },
480 {
481 "response": {
482 "status": {
483 "message": "OK",
484 "code": 200
485 },
486 "headers": {
487 "strict-transport-security": [
488 "max-age=0; includeSubdomains; preload"
489 ],
490 "server": [
491 "Apache/2.4.10 (Debian)"
492 ],
493 "referrer-policy": [
494 "no-referrer"
495 ],
496 "x-frame-options": [
497 "Deny"
498 ],
499 "expires": [
500 "Sat, 01 Jan 2000 00:00:00 GMT"
501 ],
502 "x-content-type-options": [
503 "nosniff"
504 ],
505 "cache-control": [
506 "no-store"
507 ],
508 "content-type": [
509 "application/json"
510 ],
511 "x-xss-protection": [
512 "1; mode=block"
513 ],
514 "transfer-encoding": [
515 "chunked"
516 ],
517 "date": [
518 "Sat, 25 Jan 2020 06:01:08 GMT"
519 ]
520 },
521 "body": {
522 "string": "{\"result\":{\"object\":{\"id\":8007,\"phid\":\"PHID-DREV-477vjqq7vbddmjluy2cd\"},\"transactions\":[{\"phid\":\"PHID-XACT-DREV-3mzn77j7wvia63r\"},{\"phid\":\"PHID-XACT-DREV-qs7abx4c65qud72\"},{\"phid\":\"PHID-XACT-DREV-yzrxnvdwpapxruz\"},{\"phid\":\"PHID-XACT-DREV-byrci62wwrwl262\"},{\"phid\":\"PHID-XACT-DREV-3ialrkyr7elyr72\"}]},\"error_code\":null,\"error_info\":null}"
523 }
524 },
525 "request": {
526 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22transactions%22%3A+%5B%7B%22type%22%3A+%22update%22%2C+%22value%22%3A+%22PHID-DIFF-uvk7qaq6iglk4wgk2lxb%22%7D%2C+%7B%22type%22%3A+%22title%22%2C+%22value%22%3A+%22add+binary%22%7D%5D%7D",
527 "method": "POST",
528 "headers": {
529 "content-type": [
530 "application/x-www-form-urlencoded"
531 ],
532 "user-agent": [
533 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
534 ],
535 "content-length": [
536 "308"
537 ],
538 "host": [
539 "phab.mercurial-scm.org"
540 ],
541 "accept": [
542 "application/mercurial-0.1"
543 ]
544 },
545 "uri": "https://phab.mercurial-scm.org//api/differential.revision.edit"
546 }
547 },
548 {
549 "response": {
550 "status": {
551 "message": "OK",
552 "code": 200
553 },
554 "headers": {
555 "strict-transport-security": [
556 "max-age=0; includeSubdomains; preload"
557 ],
558 "server": [
559 "Apache/2.4.10 (Debian)"
560 ],
561 "referrer-policy": [
562 "no-referrer"
563 ],
564 "x-frame-options": [
565 "Deny"
566 ],
567 "expires": [
568 "Sat, 01 Jan 2000 00:00:00 GMT"
569 ],
570 "x-content-type-options": [
571 "nosniff"
572 ],
573 "cache-control": [
574 "no-store"
575 ],
576 "content-type": [
577 "application/json"
578 ],
579 "x-xss-protection": [
580 "1; mode=block"
581 ],
582 "transfer-encoding": [
583 "chunked"
584 ],
585 "date": [
586 "Sat, 25 Jan 2020 06:01:08 GMT"
587 ]
588 },
589 "body": {
590 "string": "{\"result\":{\"upload\":true,\"filePHID\":null},\"error_code\":null,\"error_info\":null}"
591 }
592 },
593 "request": {
594 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22contentHash%22%3A+%2257eb35615d47f34ec714cacdf5fd74608a5e8e102724e80b24b287c0c27b6a31%22%2C+%22contentLength%22%3A+2%2C+%22name%22%3A+%22bin%22%7D",
595 "method": "POST",
596 "headers": {
597 "content-type": [
598 "application/x-www-form-urlencoded"
599 ],
600 "user-agent": [
601 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
602 ],
603 "content-length": [
604 "269"
605 ],
606 "host": [
607 "phab.mercurial-scm.org"
608 ],
609 "accept": [
610 "application/mercurial-0.1"
611 ]
612 },
613 "uri": "https://phab.mercurial-scm.org//api/file.allocate"
614 }
615 },
616 {
617 "response": {
618 "status": {
619 "message": "OK",
620 "code": 200
621 },
622 "headers": {
623 "strict-transport-security": [
624 "max-age=0; includeSubdomains; preload"
625 ],
626 "server": [
627 "Apache/2.4.10 (Debian)"
628 ],
629 "referrer-policy": [
630 "no-referrer"
631 ],
632 "x-frame-options": [
633 "Deny"
634 ],
635 "expires": [
636 "Sat, 01 Jan 2000 00:00:00 GMT"
637 ],
638 "x-content-type-options": [
639 "nosniff"
640 ],
641 "cache-control": [
642 "no-store"
643 ],
644 "content-type": [
645 "application/json"
646 ],
647 "x-xss-protection": [
648 "1; mode=block"
649 ],
650 "transfer-encoding": [
651 "chunked"
652 ],
653 "date": [
654 "Sat, 25 Jan 2020 06:01:09 GMT"
655 ]
656 },
657 "body": {
658 "string": "{\"result\":\"PHID-FILE-smitcgkxyyu7u3qodnmj\",\"error_code\":null,\"error_info\":null}"
659 }
660 },
661 "request": {
662 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data_base64%22%3A+%22AGI%3D%22%2C+%22name%22%3A+%22bin%22%7D",
663 "method": "POST",
664 "headers": {
665 "content-type": [
666 "application/x-www-form-urlencoded"
667 ],
668 "user-agent": [
669 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
670 ],
671 "content-length": [
672 "183"
673 ],
674 "host": [
675 "phab.mercurial-scm.org"
676 ],
677 "accept": [
678 "application/mercurial-0.1"
679 ]
680 },
681 "uri": "https://phab.mercurial-scm.org//api/file.upload"
682 }
683 },
684 {
685 "response": {
686 "status": {
687 "message": "OK",
688 "code": 200
689 },
690 "headers": {
691 "strict-transport-security": [
692 "max-age=0; includeSubdomains; preload"
693 ],
694 "server": [
695 "Apache/2.4.10 (Debian)"
696 ],
697 "referrer-policy": [
698 "no-referrer"
699 ],
700 "x-frame-options": [
701 "Deny"
702 ],
703 "expires": [
704 "Sat, 01 Jan 2000 00:00:00 GMT"
705 ],
706 "x-content-type-options": [
707 "nosniff"
708 ],
709 "cache-control": [
710 "no-store"
711 ],
712 "content-type": [
713 "application/json"
714 ],
715 "x-xss-protection": [
716 "1; mode=block"
717 ],
718 "transfer-encoding": [
719 "chunked"
720 ],
721 "date": [
722 "Sat, 25 Jan 2020 06:01:10 GMT"
723 ]
724 },
725 "body": {
726 "string": "{\"result\":{\"upload\":false,\"filePHID\":\"PHID-FILE-ulotc37zwk3yic5hh72n\"},\"error_code\":null,\"error_info\":null}"
727 }
728 },
729 "request": {
730 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22contentHash%22%3A+%22022a6979e6dab7aa5ae4c3e5e45f7e977112a7e63593820dbec1ec738a24f93c%22%2C+%22contentLength%22%3A+2%2C+%22name%22%3A+%22bin%22%7D",
731 "method": "POST",
732 "headers": {
733 "content-type": [
734 "application/x-www-form-urlencoded"
735 ],
736 "user-agent": [
737 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
738 ],
739 "content-length": [
740 "269"
741 ],
742 "host": [
743 "phab.mercurial-scm.org"
744 ],
745 "accept": [
746 "application/mercurial-0.1"
747 ]
748 },
749 "uri": "https://phab.mercurial-scm.org//api/file.allocate"
750 }
751 },
752 {
753 "response": {
754 "status": {
755 "message": "OK",
756 "code": 200
757 },
758 "headers": {
759 "strict-transport-security": [
760 "max-age=0; includeSubdomains; preload"
761 ],
762 "server": [
763 "Apache/2.4.10 (Debian)"
764 ],
765 "referrer-policy": [
766 "no-referrer"
767 ],
768 "x-frame-options": [
769 "Deny"
770 ],
771 "expires": [
772 "Sat, 01 Jan 2000 00:00:00 GMT"
773 ],
774 "x-content-type-options": [
775 "nosniff"
776 ],
777 "cache-control": [
778 "no-store"
779 ],
780 "content-type": [
781 "application/json"
782 ],
783 "x-xss-protection": [
784 "1; mode=block"
785 ],
786 "transfer-encoding": [
787 "chunked"
788 ],
789 "date": [
790 "Sat, 25 Jan 2020 06:01:10 GMT"
791 ]
792 },
793 "body": {
794 "string": "{\"result\":{\"diffid\":19600,\"phid\":\"PHID-DIFF-wdmlzkf4huoiqpl4vbsr\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/differential\\/diff\\/19600\\/\"},\"error_code\":null,\"error_info\":null}"
795 }
796 },
797 "request": {
798 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22bookmark%22%3A+null%2C+%22branch%22%3A+%22default%22%2C+%22changes%22%3A+%7B%22bin%22%3A+%7B%22addLines%22%3A+0%2C+%22awayPaths%22%3A+%5B%5D%2C+%22commitHash%22%3A+null%2C+%22currentPath%22%3A+%22bin%22%2C+%22delLines%22%3A+0%2C+%22fileType%22%3A+3%2C+%22hunks%22%3A+%5B%5D%2C+%22metadata%22%3A+%7B%22new%3Abinary-phid%22%3A+%22PHID-FILE-smitcgkxyyu7u3qodnmj%22%2C+%22new%3Afile%3Asize%22%3A+2%2C+%22old%3Abinary-phid%22%3A+%22PHID-FILE-ulotc37zwk3yic5hh72n%22%2C+%22old%3Afile%3Asize%22%3A+2%7D%2C+%22newProperties%22%3A+%7B%7D%2C+%22oldPath%22%3A+%22bin%22%2C+%22oldProperties%22%3A+%7B%7D%2C+%22type%22%3A+2%7D%7D%2C+%22creationMethod%22%3A+%22phabsend%22%2C+%22lintStatus%22%3A+%22none%22%2C+%22repositoryPHID%22%3A+%22PHID-REPO-bvunnehri4u2isyr7bc3%22%2C+%22sourceControlBaseRevision%22%3A+%22aa24a81f55de50addfce4a824eeb919d59b19683%22%2C+%22sourceControlPath%22%3A+%22%2F%22%2C+%22sourceControlSystem%22%3A+%22hg%22%2C+%22sourceMachine%22%3A+%22%22%2C+%22sourcePath%22%3A+%22%2F%22%2C+%22unitStatus%22%3A+%22none%22%7D",
799 "method": "POST",
800 "headers": {
801 "content-type": [
802 "application/x-www-form-urlencoded"
803 ],
804 "user-agent": [
805 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
806 ],
807 "content-length": [
808 "1148"
809 ],
810 "host": [
811 "phab.mercurial-scm.org"
812 ],
813 "accept": [
814 "application/mercurial-0.1"
815 ]
816 },
817 "uri": "https://phab.mercurial-scm.org//api/differential.creatediff"
818 }
819 },
820 {
821 "response": {
822 "status": {
823 "message": "OK",
824 "code": 200
825 },
826 "headers": {
827 "strict-transport-security": [
828 "max-age=0; includeSubdomains; preload"
829 ],
830 "server": [
831 "Apache/2.4.10 (Debian)"
832 ],
833 "referrer-policy": [
834 "no-referrer"
835 ],
836 "x-frame-options": [
837 "Deny"
838 ],
839 "expires": [
840 "Sat, 01 Jan 2000 00:00:00 GMT"
841 ],
842 "x-content-type-options": [
843 "nosniff"
844 ],
845 "cache-control": [
846 "no-store"
847 ],
848 "content-type": [
849 "application/json"
850 ],
851 "x-xss-protection": [
852 "1; mode=block"
853 ],
854 "transfer-encoding": [
855 "chunked"
856 ],
857 "date": [
858 "Sat, 25 Jan 2020 06:01:11 GMT"
859 ]
860 },
861 "body": {
862 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
863 }
864 },
865 "request": {
866 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22date%5C%22%3A+%5C%220+0%5C%22%2C+%5C%22node%5C%22%3A+%5C%22d8d62a881b546814f012cef7c1bd0c438cc153e2%5C%22%2C+%5C%22parent%5C%22%3A+%5C%22aa24a81f55de50addfce4a824eeb919d59b19683%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+19600%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
867 "method": "POST",
868 "headers": {
869 "content-type": [
870 "application/x-www-form-urlencoded"
871 ],
872 "user-agent": [
873 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
874 ],
875 "content-length": [
876 "482"
877 ],
878 "host": [
879 "phab.mercurial-scm.org"
880 ],
881 "accept": [
882 "application/mercurial-0.1"
883 ]
884 },
885 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
886 }
887 },
888 {
889 "response": {
890 "status": {
891 "message": "OK",
892 "code": 200
893 },
894 "headers": {
895 "strict-transport-security": [
896 "max-age=0; includeSubdomains; preload"
897 ],
898 "server": [
899 "Apache/2.4.10 (Debian)"
900 ],
901 "referrer-policy": [
902 "no-referrer"
903 ],
904 "x-frame-options": [
905 "Deny"
906 ],
907 "expires": [
908 "Sat, 01 Jan 2000 00:00:00 GMT"
909 ],
910 "x-content-type-options": [
911 "nosniff"
912 ],
913 "cache-control": [
914 "no-store"
915 ],
916 "content-type": [
917 "application/json"
918 ],
919 "x-xss-protection": [
920 "1; mode=block"
921 ],
922 "transfer-encoding": [
923 "chunked"
924 ],
925 "date": [
926 "Sat, 25 Jan 2020 06:01:12 GMT"
927 ]
928 },
929 "body": {
930 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
931 }
932 },
933 "request": {
934 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22d8d62a881b546814f012cef7c1bd0c438cc153e2%5C%22%3A+%7B%5C%22author%5C%22%3A+%5C%22test%5C%22%2C+%5C%22authorEmail%5C%22%3A+%5C%22test%5C%22%2C+%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22commit%5C%22%3A+%5C%22d8d62a881b546814f012cef7c1bd0c438cc153e2%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%22aa24a81f55de50addfce4a824eeb919d59b19683%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+19600%2C+%22name%22%3A+%22local%3Acommits%22%7D",
935 "method": "POST",
936 "headers": {
937 "content-type": [
938 "application/x-www-form-urlencoded"
939 ],
940 "user-agent": [
941 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
942 ],
943 "content-length": [
944 "594"
945 ],
946 "host": [
947 "phab.mercurial-scm.org"
948 ],
949 "accept": [
950 "application/mercurial-0.1"
951 ]
952 },
953 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
954 }
955 },
956 {
957 "response": {
958 "status": {
959 "message": "OK",
960 "code": 200
961 },
962 "headers": {
963 "strict-transport-security": [
964 "max-age=0; includeSubdomains; preload"
965 ],
966 "server": [
967 "Apache/2.4.10 (Debian)"
968 ],
969 "referrer-policy": [
970 "no-referrer"
971 ],
972 "x-frame-options": [
973 "Deny"
974 ],
975 "expires": [
976 "Sat, 01 Jan 2000 00:00:00 GMT"
977 ],
978 "x-content-type-options": [
979 "nosniff"
980 ],
981 "cache-control": [
982 "no-store"
983 ],
984 "content-type": [
985 "application/json"
986 ],
987 "x-xss-protection": [
988 "1; mode=block"
989 ],
990 "transfer-encoding": [
991 "chunked"
992 ],
993 "date": [
994 "Sat, 25 Jan 2020 06:01:12 GMT"
995 ]
996 },
997 "body": {
998 "string": "{\"result\":{\"errors\":[],\"fields\":{\"title\":\"modify binary\"},\"revisionIDFieldInfo\":{\"value\":null,\"validDomain\":\"https:\\/\\/phab.mercurial-scm.org\"},\"transactions\":[{\"type\":\"title\",\"value\":\"modify binary\"}]},\"error_code\":null,\"error_info\":null}"
999 }
1000 },
1001 "request": {
1002 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22corpus%22%3A+%22modify+binary%22%7D",
1003 "method": "POST",
1004 "headers": {
1005 "content-type": [
1006 "application/x-www-form-urlencoded"
1007 ],
1008 "user-agent": [
1009 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1010 ],
1011 "content-length": [
1012 "158"
1013 ],
1014 "host": [
1015 "phab.mercurial-scm.org"
1016 ],
1017 "accept": [
1018 "application/mercurial-0.1"
1019 ]
1020 },
1021 "uri": "https://phab.mercurial-scm.org//api/differential.parsecommitmessage"
1022 }
1023 },
1024 {
1025 "response": {
1026 "status": {
1027 "message": "OK",
1028 "code": 200
1029 },
1030 "headers": {
1031 "strict-transport-security": [
1032 "max-age=0; includeSubdomains; preload"
1033 ],
1034 "server": [
1035 "Apache/2.4.10 (Debian)"
1036 ],
1037 "referrer-policy": [
1038 "no-referrer"
1039 ],
1040 "x-frame-options": [
1041 "Deny"
1042 ],
1043 "expires": [
1044 "Sat, 01 Jan 2000 00:00:00 GMT"
1045 ],
1046 "x-content-type-options": [
1047 "nosniff"
1048 ],
1049 "cache-control": [
1050 "no-store"
1051 ],
1052 "content-type": [
1053 "application/json"
1054 ],
1055 "x-xss-protection": [
1056 "1; mode=block"
1057 ],
1058 "transfer-encoding": [
1059 "chunked"
1060 ],
1061 "date": [
1062 "Sat, 25 Jan 2020 06:01:13 GMT"
1063 ]
1064 },
1065 "body": {
1066 "string": "{\"result\":{\"object\":{\"id\":8008,\"phid\":\"PHID-DREV-q5cr6ap6pwfczhlhjrmv\"},\"transactions\":[{\"phid\":\"PHID-XACT-DREV-54nmhipbtei7cmf\"},{\"phid\":\"PHID-XACT-DREV-zh5tb4ay2no4lr7\"},{\"phid\":\"PHID-XACT-DREV-mi6n5eteba4pb2h\"},{\"phid\":\"PHID-XACT-DREV-zmbupfrlsvzty6c\"},{\"phid\":\"PHID-XACT-DREV-h2mxx7gkkriw5b5\"},{\"phid\":\"PHID-XACT-DREV-6zpi3cnarzr5f6i\"}]},\"error_code\":null,\"error_info\":null}"
1067 }
1068 },
1069 "request": {
1070 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22transactions%22%3A+%5B%7B%22type%22%3A+%22update%22%2C+%22value%22%3A+%22PHID-DIFF-wdmlzkf4huoiqpl4vbsr%22%7D%2C+%7B%22type%22%3A+%22parents.set%22%2C+%22value%22%3A+%5B%22PHID-DREV-477vjqq7vbddmjluy2cd%22%5D%7D%2C+%7B%22type%22%3A+%22title%22%2C+%22value%22%3A+%22modify+binary%22%7D%5D%7D",
1071 "method": "POST",
1072 "headers": {
1073 "content-type": [
1074 "application/x-www-form-urlencoded"
1075 ],
1076 "user-agent": [
1077 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1078 ],
1079 "content-length": [
1080 "413"
1081 ],
1082 "host": [
1083 "phab.mercurial-scm.org"
1084 ],
1085 "accept": [
1086 "application/mercurial-0.1"
1087 ]
1088 },
1089 "uri": "https://phab.mercurial-scm.org//api/differential.revision.edit"
1090 }
1091 },
1092 {
1093 "response": {
1094 "status": {
1095 "message": "OK",
1096 "code": 200
1097 },
1098 "headers": {
1099 "strict-transport-security": [
1100 "max-age=0; includeSubdomains; preload"
1101 ],
1102 "server": [
1103 "Apache/2.4.10 (Debian)"
1104 ],
1105 "referrer-policy": [
1106 "no-referrer"
1107 ],
1108 "x-frame-options": [
1109 "Deny"
1110 ],
1111 "expires": [
1112 "Sat, 01 Jan 2000 00:00:00 GMT"
1113 ],
1114 "x-content-type-options": [
1115 "nosniff"
1116 ],
1117 "cache-control": [
1118 "no-store"
1119 ],
1120 "content-type": [
1121 "application/json"
1122 ],
1123 "x-xss-protection": [
1124 "1; mode=block"
1125 ],
1126 "transfer-encoding": [
1127 "chunked"
1128 ],
1129 "date": [
1130 "Sat, 25 Jan 2020 06:01:14 GMT"
1131 ]
1132 },
1133 "body": {
1134 "string": "{\"result\":{\"diffid\":19601,\"phid\":\"PHID-DIFF-burakyhnzac52zcbjn2l\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/differential\\/diff\\/19601\\/\"},\"error_code\":null,\"error_info\":null}"
1135 }
1136 },
1137 "request": {
1138 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22bookmark%22%3A+null%2C+%22branch%22%3A+%22default%22%2C+%22changes%22%3A+%7B%22bin%22%3A+%7B%22addLines%22%3A+0%2C+%22awayPaths%22%3A+%5B%5D%2C+%22commitHash%22%3A+null%2C+%22currentPath%22%3A+%22bin%22%2C+%22delLines%22%3A+0%2C+%22fileType%22%3A+1%2C+%22hunks%22%3A+%5B%5D%2C+%22metadata%22%3A+%7B%7D%2C+%22newProperties%22%3A+%7B%7D%2C+%22oldPath%22%3A+%22bin%22%2C+%22oldProperties%22%3A+%7B%22unix%3Afilemode%22%3A+%22100644%22%7D%2C+%22type%22%3A+3%7D%7D%2C+%22creationMethod%22%3A+%22phabsend%22%2C+%22lintStatus%22%3A+%22none%22%2C+%22repositoryPHID%22%3A+%22PHID-REPO-bvunnehri4u2isyr7bc3%22%2C+%22sourceControlBaseRevision%22%3A+%22d8d62a881b546814f012cef7c1bd0c438cc153e2%22%2C+%22sourceControlPath%22%3A+%22%2F%22%2C+%22sourceControlSystem%22%3A+%22hg%22%2C+%22sourceMachine%22%3A+%22%22%2C+%22sourcePath%22%3A+%22%2F%22%2C+%22unitStatus%22%3A+%22none%22%7D",
1139 "method": "POST",
1140 "headers": {
1141 "content-type": [
1142 "application/x-www-form-urlencoded"
1143 ],
1144 "user-agent": [
1145 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1146 ],
1147 "content-length": [
1148 "991"
1149 ],
1150 "host": [
1151 "phab.mercurial-scm.org"
1152 ],
1153 "accept": [
1154 "application/mercurial-0.1"
1155 ]
1156 },
1157 "uri": "https://phab.mercurial-scm.org//api/differential.creatediff"
1158 }
1159 },
1160 {
1161 "response": {
1162 "status": {
1163 "message": "OK",
1164 "code": 200
1165 },
1166 "headers": {
1167 "strict-transport-security": [
1168 "max-age=0; includeSubdomains; preload"
1169 ],
1170 "server": [
1171 "Apache/2.4.10 (Debian)"
1172 ],
1173 "referrer-policy": [
1174 "no-referrer"
1175 ],
1176 "x-frame-options": [
1177 "Deny"
1178 ],
1179 "expires": [
1180 "Sat, 01 Jan 2000 00:00:00 GMT"
1181 ],
1182 "x-content-type-options": [
1183 "nosniff"
1184 ],
1185 "cache-control": [
1186 "no-store"
1187 ],
1188 "content-type": [
1189 "application/json"
1190 ],
1191 "x-xss-protection": [
1192 "1; mode=block"
1193 ],
1194 "transfer-encoding": [
1195 "chunked"
1196 ],
1197 "date": [
1198 "Sat, 25 Jan 2020 06:01:14 GMT"
1199 ]
1200 },
1201 "body": {
1202 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
1203 }
1204 },
1205 "request": {
1206 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22date%5C%22%3A+%5C%220+0%5C%22%2C+%5C%22node%5C%22%3A+%5C%22af55645b2e292f3168e993e74356af15dee0abdb%5C%22%2C+%5C%22parent%5C%22%3A+%5C%22d8d62a881b546814f012cef7c1bd0c438cc153e2%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+19601%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
1207 "method": "POST",
1208 "headers": {
1209 "content-type": [
1210 "application/x-www-form-urlencoded"
1211 ],
1212 "user-agent": [
1213 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1214 ],
1215 "content-length": [
1216 "482"
1217 ],
1218 "host": [
1219 "phab.mercurial-scm.org"
1220 ],
1221 "accept": [
1222 "application/mercurial-0.1"
1223 ]
1224 },
1225 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
1226 }
1227 },
1228 {
1229 "response": {
1230 "status": {
1231 "message": "OK",
1232 "code": 200
1233 },
1234 "headers": {
1235 "strict-transport-security": [
1236 "max-age=0; includeSubdomains; preload"
1237 ],
1238 "server": [
1239 "Apache/2.4.10 (Debian)"
1240 ],
1241 "referrer-policy": [
1242 "no-referrer"
1243 ],
1244 "x-frame-options": [
1245 "Deny"
1246 ],
1247 "expires": [
1248 "Sat, 01 Jan 2000 00:00:00 GMT"
1249 ],
1250 "x-content-type-options": [
1251 "nosniff"
1252 ],
1253 "cache-control": [
1254 "no-store"
1255 ],
1256 "content-type": [
1257 "application/json"
1258 ],
1259 "x-xss-protection": [
1260 "1; mode=block"
1261 ],
1262 "transfer-encoding": [
1263 "chunked"
1264 ],
1265 "date": [
1266 "Sat, 25 Jan 2020 06:01:15 GMT"
1267 ]
1268 },
1269 "body": {
1270 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
1271 }
1272 },
1273 "request": {
1274 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22af55645b2e292f3168e993e74356af15dee0abdb%5C%22%3A+%7B%5C%22author%5C%22%3A+%5C%22test%5C%22%2C+%5C%22authorEmail%5C%22%3A+%5C%22test%5C%22%2C+%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22commit%5C%22%3A+%5C%22af55645b2e292f3168e993e74356af15dee0abdb%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%22d8d62a881b546814f012cef7c1bd0c438cc153e2%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+19601%2C+%22name%22%3A+%22local%3Acommits%22%7D",
1275 "method": "POST",
1276 "headers": {
1277 "content-type": [
1278 "application/x-www-form-urlencoded"
1279 ],
1280 "user-agent": [
1281 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1282 ],
1283 "content-length": [
1284 "594"
1285 ],
1286 "host": [
1287 "phab.mercurial-scm.org"
1288 ],
1289 "accept": [
1290 "application/mercurial-0.1"
1291 ]
1292 },
1293 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
1294 }
1295 },
1296 {
1297 "response": {
1298 "status": {
1299 "message": "OK",
1300 "code": 200
1301 },
1302 "headers": {
1303 "strict-transport-security": [
1304 "max-age=0; includeSubdomains; preload"
1305 ],
1306 "server": [
1307 "Apache/2.4.10 (Debian)"
1308 ],
1309 "referrer-policy": [
1310 "no-referrer"
1311 ],
1312 "x-frame-options": [
1313 "Deny"
1314 ],
1315 "expires": [
1316 "Sat, 01 Jan 2000 00:00:00 GMT"
1317 ],
1318 "x-content-type-options": [
1319 "nosniff"
1320 ],
1321 "cache-control": [
1322 "no-store"
1323 ],
1324 "content-type": [
1325 "application/json"
1326 ],
1327 "x-xss-protection": [
1328 "1; mode=block"
1329 ],
1330 "transfer-encoding": [
1331 "chunked"
1332 ],
1333 "date": [
1334 "Sat, 25 Jan 2020 06:01:15 GMT"
1335 ]
1336 },
1337 "body": {
1338 "string": "{\"result\":{\"errors\":[],\"fields\":{\"title\":\"remove binary\"},\"revisionIDFieldInfo\":{\"value\":null,\"validDomain\":\"https:\\/\\/phab.mercurial-scm.org\"},\"transactions\":[{\"type\":\"title\",\"value\":\"remove binary\"}]},\"error_code\":null,\"error_info\":null}"
1339 }
1340 },
1341 "request": {
1342 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22corpus%22%3A+%22remove+binary%22%7D",
1343 "method": "POST",
1344 "headers": {
1345 "content-type": [
1346 "application/x-www-form-urlencoded"
1347 ],
1348 "user-agent": [
1349 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1350 ],
1351 "content-length": [
1352 "158"
1353 ],
1354 "host": [
1355 "phab.mercurial-scm.org"
1356 ],
1357 "accept": [
1358 "application/mercurial-0.1"
1359 ]
1360 },
1361 "uri": "https://phab.mercurial-scm.org//api/differential.parsecommitmessage"
1362 }
1363 },
1364 {
1365 "response": {
1366 "status": {
1367 "message": "OK",
1368 "code": 200
1369 },
1370 "headers": {
1371 "strict-transport-security": [
1372 "max-age=0; includeSubdomains; preload"
1373 ],
1374 "server": [
1375 "Apache/2.4.10 (Debian)"
1376 ],
1377 "referrer-policy": [
1378 "no-referrer"
1379 ],
1380 "x-frame-options": [
1381 "Deny"
1382 ],
1383 "expires": [
1384 "Sat, 01 Jan 2000 00:00:00 GMT"
1385 ],
1386 "x-content-type-options": [
1387 "nosniff"
1388 ],
1389 "cache-control": [
1390 "no-store"
1391 ],
1392 "content-type": [
1393 "application/json"
1394 ],
1395 "x-xss-protection": [
1396 "1; mode=block"
1397 ],
1398 "transfer-encoding": [
1399 "chunked"
1400 ],
1401 "date": [
1402 "Sat, 25 Jan 2020 06:01:16 GMT"
1403 ]
1404 },
1405 "body": {
1406 "string": "{\"result\":{\"object\":{\"id\":8009,\"phid\":\"PHID-DREV-dhu2sy4tbldt4npi6sm7\"},\"transactions\":[{\"phid\":\"PHID-XACT-DREV-bj3vveste6uhvwa\"},{\"phid\":\"PHID-XACT-DREV-nh6m26vnt2lglxa\"},{\"phid\":\"PHID-XACT-DREV-qpaic5oz5gjiyac\"},{\"phid\":\"PHID-XACT-DREV-2e4pylpz72lnq4u\"},{\"phid\":\"PHID-XACT-DREV-4ajvkrw3bwabrxb\"},{\"phid\":\"PHID-XACT-DREV-xs75m6gczvk4fbb\"}]},\"error_code\":null,\"error_info\":null}"
1407 }
1408 },
1409 "request": {
1410 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22transactions%22%3A+%5B%7B%22type%22%3A+%22update%22%2C+%22value%22%3A+%22PHID-DIFF-burakyhnzac52zcbjn2l%22%7D%2C+%7B%22type%22%3A+%22parents.set%22%2C+%22value%22%3A+%5B%22PHID-DREV-q5cr6ap6pwfczhlhjrmv%22%5D%7D%2C+%7B%22type%22%3A+%22title%22%2C+%22value%22%3A+%22remove+binary%22%7D%5D%7D",
1411 "method": "POST",
1412 "headers": {
1413 "content-type": [
1414 "application/x-www-form-urlencoded"
1415 ],
1416 "user-agent": [
1417 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1418 ],
1419 "content-length": [
1420 "413"
1421 ],
1422 "host": [
1423 "phab.mercurial-scm.org"
1424 ],
1425 "accept": [
1426 "application/mercurial-0.1"
1427 ]
1428 },
1429 "uri": "https://phab.mercurial-scm.org//api/differential.revision.edit"
1430 }
1431 },
1432 {
1433 "response": {
1434 "status": {
1435 "message": "OK",
1436 "code": 200
1437 },
1438 "headers": {
1439 "strict-transport-security": [
1440 "max-age=0; includeSubdomains; preload"
1441 ],
1442 "server": [
1443 "Apache/2.4.10 (Debian)"
1444 ],
1445 "referrer-policy": [
1446 "no-referrer"
1447 ],
1448 "x-frame-options": [
1449 "Deny"
1450 ],
1451 "expires": [
1452 "Sat, 01 Jan 2000 00:00:00 GMT"
1453 ],
1454 "x-content-type-options": [
1455 "nosniff"
1456 ],
1457 "cache-control": [
1458 "no-store"
1459 ],
1460 "content-type": [
1461 "application/json"
1462 ],
1463 "x-xss-protection": [
1464 "1; mode=block"
1465 ],
1466 "transfer-encoding": [
1467 "chunked"
1468 ],
1469 "date": [
1470 "Sat, 25 Jan 2020 06:01:17 GMT"
1471 ]
1472 },
1473 "body": {
1474 "string": "{\"result\":[{\"id\":\"8009\",\"phid\":\"PHID-DREV-dhu2sy4tbldt4npi6sm7\",\"title\":\"remove binary\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D8009\",\"dateCreated\":\"1579932076\",\"dateModified\":\"1579932076\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"0\",\"statusName\":\"Needs Review\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":0,\"lines.removed\":0},\"branch\":\"default\",\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"0\",\"activeDiffPHID\":\"PHID-DIFF-burakyhnzac52zcbjn2l\",\"diffs\":[\"19601\"],\"commits\":[],\"reviewers\":{\"PHID-PROJ-3dvcxzznrjru2xmmses3\":\"PHID-PROJ-3dvcxzznrjru2xmmses3\"},\"ccs\":[\"PHID-USER-q42dn7cc3donqriafhjx\"],\"hashes\":[[\"hgcm\",\"\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\"]],\"auxiliary\":{\"phabricator:projects\":[],\"phabricator:depends-on\":[\"PHID-DREV-q5cr6ap6pwfczhlhjrmv\"]},\"repositoryPHID\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"sourcePath\":\"\\/\"},{\"id\":\"8008\",\"phid\":\"PHID-DREV-q5cr6ap6pwfczhlhjrmv\",\"title\":\"modify binary\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D8008\",\"dateCreated\":\"1579932073\",\"dateModified\":\"1579932076\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"0\",\"statusName\":\"Needs Review\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":0,\"lines.removed\":0},\"branch\":\"default\",\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"0\",\"activeDiffPHID\":\"PHID-DIFF-wdmlzkf4huoiqpl4vbsr\",\"diffs\":[\"19600\"],\"commits\":[],\"reviewers\":{\"PHID-PROJ-3dvcxzznrjru2xmmses3\":\"PHID-PROJ-3dvcxzznrjru2xmmses3\"},\"ccs\":[\"PHID-USER-q42dn7cc3donqriafhjx\"],\"hashes\":[[\"hgcm\",\"\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\"]],\"auxiliary\":{\"phabricator:projects\":[],\"phabricator:depends-on\":[\"PHID-DREV-477vjqq7vbddmjluy2cd\"]},\"repositoryPHID\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"sourcePath\":\"\\/\"},{\"id\":\"8007\",\"phid\":\"PHID-DREV-477vjqq7vbddmjluy2cd\",\"title\":\"add binary\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D8007\",\"dateCreated\":\"1579932068\",\"dateModified\":\"1579932073\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"0\",\"statusName\":\"Needs Review\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":0,\"lines.removed\":0},\"branch\":\"default\",\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"0\",\"activeDiffPHID\":\"PHID-DIFF-uvk7qaq6iglk4wgk2lxb\",\"diffs\":[\"19599\"],\"commits\":[],\"reviewers\":{\"PHID-PROJ-3dvcxzznrjru2xmmses3\":\"PHID-PROJ-3dvcxzznrjru2xmmses3\"},\"ccs\":[\"PHID-USER-q42dn7cc3donqriafhjx\"],\"hashes\":[[\"hgcm\",\"\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\\u0000\"]],\"auxiliary\":{\"phabricator:projects\":[],\"phabricator:depends-on\":[]},\"repositoryPHID\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"sourcePath\":\"\\/\"}],\"error_code\":null,\"error_info\":null}"
1475 }
1476 },
1477 "request": {
1478 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22ids%22%3A+%5B8007%2C+8008%2C+8009%5D%7D",
1479 "method": "POST",
1480 "headers": {
1481 "content-type": [
1482 "application/x-www-form-urlencoded"
1483 ],
1484 "user-agent": [
1485 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1486 ],
1487 "content-length": [
1488 "162"
1489 ],
1490 "host": [
1491 "phab.mercurial-scm.org"
1492 ],
1493 "accept": [
1494 "application/mercurial-0.1"
1495 ]
1496 },
1497 "uri": "https://phab.mercurial-scm.org//api/differential.query"
1498 }
1499 },
1500 {
1501 "response": {
1502 "status": {
1503 "message": "OK",
1504 "code": 200
1505 },
1506 "headers": {
1507 "strict-transport-security": [
1508 "max-age=0; includeSubdomains; preload"
1509 ],
1510 "server": [
1511 "Apache/2.4.10 (Debian)"
1512 ],
1513 "referrer-policy": [
1514 "no-referrer"
1515 ],
1516 "x-frame-options": [
1517 "Deny"
1518 ],
1519 "expires": [
1520 "Sat, 01 Jan 2000 00:00:00 GMT"
1521 ],
1522 "x-content-type-options": [
1523 "nosniff"
1524 ],
1525 "cache-control": [
1526 "no-store"
1527 ],
1528 "content-type": [
1529 "application/json"
1530 ],
1531 "x-xss-protection": [
1532 "1; mode=block"
1533 ],
1534 "transfer-encoding": [
1535 "chunked"
1536 ],
1537 "date": [
1538 "Sat, 25 Jan 2020 06:01:17 GMT"
1539 ]
1540 },
1541 "body": {
1542 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
1543 }
1544 },
1545 "request": {
1546 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22date%5C%22%3A+%5C%220+0%5C%22%2C+%5C%22node%5C%22%3A+%5C%22b8139fbb4a579a69dccd906a1a8bf34ba2b885a3%5C%22%2C+%5C%22parent%5C%22%3A+%5C%221849d7828727a28e14c589323e4f8c9a1c8d2816%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+19599%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
1547 "method": "POST",
1548 "headers": {
1549 "content-type": [
1550 "application/x-www-form-urlencoded"
1551 ],
1552 "user-agent": [
1553 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1554 ],
1555 "content-length": [
1556 "482"
1557 ],
1558 "host": [
1559 "phab.mercurial-scm.org"
1560 ],
1561 "accept": [
1562 "application/mercurial-0.1"
1563 ]
1564 },
1565 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
1566 }
1567 },
1568 {
1569 "response": {
1570 "status": {
1571 "message": "OK",
1572 "code": 200
1573 },
1574 "headers": {
1575 "strict-transport-security": [
1576 "max-age=0; includeSubdomains; preload"
1577 ],
1578 "server": [
1579 "Apache/2.4.10 (Debian)"
1580 ],
1581 "referrer-policy": [
1582 "no-referrer"
1583 ],
1584 "x-frame-options": [
1585 "Deny"
1586 ],
1587 "expires": [
1588 "Sat, 01 Jan 2000 00:00:00 GMT"
1589 ],
1590 "x-content-type-options": [
1591 "nosniff"
1592 ],
1593 "cache-control": [
1594 "no-store"
1595 ],
1596 "content-type": [
1597 "application/json"
1598 ],
1599 "x-xss-protection": [
1600 "1; mode=block"
1601 ],
1602 "transfer-encoding": [
1603 "chunked"
1604 ],
1605 "date": [
1606 "Sat, 25 Jan 2020 06:01:18 GMT"
1607 ]
1608 },
1609 "body": {
1610 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
1611 }
1612 },
1613 "request": {
1614 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22b8139fbb4a579a69dccd906a1a8bf34ba2b885a3%5C%22%3A+%7B%5C%22author%5C%22%3A+%5C%22test%5C%22%2C+%5C%22authorEmail%5C%22%3A+%5C%22test%5C%22%2C+%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22commit%5C%22%3A+%5C%22b8139fbb4a579a69dccd906a1a8bf34ba2b885a3%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%221849d7828727a28e14c589323e4f8c9a1c8d2816%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+19599%2C+%22name%22%3A+%22local%3Acommits%22%7D",
1615 "method": "POST",
1616 "headers": {
1617 "content-type": [
1618 "application/x-www-form-urlencoded"
1619 ],
1620 "user-agent": [
1621 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1622 ],
1623 "content-length": [
1624 "594"
1625 ],
1626 "host": [
1627 "phab.mercurial-scm.org"
1628 ],
1629 "accept": [
1630 "application/mercurial-0.1"
1631 ]
1632 },
1633 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
1634 }
1635 },
1636 {
1637 "response": {
1638 "status": {
1639 "message": "OK",
1640 "code": 200
1641 },
1642 "headers": {
1643 "strict-transport-security": [
1644 "max-age=0; includeSubdomains; preload"
1645 ],
1646 "server": [
1647 "Apache/2.4.10 (Debian)"
1648 ],
1649 "referrer-policy": [
1650 "no-referrer"
1651 ],
1652 "x-frame-options": [
1653 "Deny"
1654 ],
1655 "expires": [
1656 "Sat, 01 Jan 2000 00:00:00 GMT"
1657 ],
1658 "x-content-type-options": [
1659 "nosniff"
1660 ],
1661 "cache-control": [
1662 "no-store"
1663 ],
1664 "content-type": [
1665 "application/json"
1666 ],
1667 "x-xss-protection": [
1668 "1; mode=block"
1669 ],
1670 "transfer-encoding": [
1671 "chunked"
1672 ],
1673 "date": [
1674 "Sat, 25 Jan 2020 06:01:19 GMT"
1675 ]
1676 },
1677 "body": {
1678 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
1679 }
1680 },
1681 "request": {
1682 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22date%5C%22%3A+%5C%220+0%5C%22%2C+%5C%22node%5C%22%3A+%5C%22c88ce4c2d2ad6337b772bc093398f85858196eb5%5C%22%2C+%5C%22parent%5C%22%3A+%5C%22b8139fbb4a579a69dccd906a1a8bf34ba2b885a3%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+19600%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
1683 "method": "POST",
1684 "headers": {
1685 "content-type": [
1686 "application/x-www-form-urlencoded"
1687 ],
1688 "user-agent": [
1689 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1690 ],
1691 "content-length": [
1692 "482"
1693 ],
1694 "host": [
1695 "phab.mercurial-scm.org"
1696 ],
1697 "accept": [
1698 "application/mercurial-0.1"
1699 ]
1700 },
1701 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
1702 }
1703 },
1704 {
1705 "response": {
1706 "status": {
1707 "message": "OK",
1708 "code": 200
1709 },
1710 "headers": {
1711 "strict-transport-security": [
1712 "max-age=0; includeSubdomains; preload"
1713 ],
1714 "server": [
1715 "Apache/2.4.10 (Debian)"
1716 ],
1717 "referrer-policy": [
1718 "no-referrer"
1719 ],
1720 "x-frame-options": [
1721 "Deny"
1722 ],
1723 "expires": [
1724 "Sat, 01 Jan 2000 00:00:00 GMT"
1725 ],
1726 "x-content-type-options": [
1727 "nosniff"
1728 ],
1729 "cache-control": [
1730 "no-store"
1731 ],
1732 "content-type": [
1733 "application/json"
1734 ],
1735 "x-xss-protection": [
1736 "1; mode=block"
1737 ],
1738 "transfer-encoding": [
1739 "chunked"
1740 ],
1741 "date": [
1742 "Sat, 25 Jan 2020 06:01:19 GMT"
1743 ]
1744 },
1745 "body": {
1746 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
1747 }
1748 },
1749 "request": {
1750 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22c88ce4c2d2ad6337b772bc093398f85858196eb5%5C%22%3A+%7B%5C%22author%5C%22%3A+%5C%22test%5C%22%2C+%5C%22authorEmail%5C%22%3A+%5C%22test%5C%22%2C+%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22commit%5C%22%3A+%5C%22c88ce4c2d2ad6337b772bc093398f85858196eb5%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%22b8139fbb4a579a69dccd906a1a8bf34ba2b885a3%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+19600%2C+%22name%22%3A+%22local%3Acommits%22%7D",
1751 "method": "POST",
1752 "headers": {
1753 "content-type": [
1754 "application/x-www-form-urlencoded"
1755 ],
1756 "user-agent": [
1757 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1758 ],
1759 "content-length": [
1760 "594"
1761 ],
1762 "host": [
1763 "phab.mercurial-scm.org"
1764 ],
1765 "accept": [
1766 "application/mercurial-0.1"
1767 ]
1768 },
1769 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
1770 }
1771 },
1772 {
1773 "response": {
1774 "status": {
1775 "message": "OK",
1776 "code": 200
1777 },
1778 "headers": {
1779 "strict-transport-security": [
1780 "max-age=0; includeSubdomains; preload"
1781 ],
1782 "server": [
1783 "Apache/2.4.10 (Debian)"
1784 ],
1785 "referrer-policy": [
1786 "no-referrer"
1787 ],
1788 "x-frame-options": [
1789 "Deny"
1790 ],
1791 "expires": [
1792 "Sat, 01 Jan 2000 00:00:00 GMT"
1793 ],
1794 "x-content-type-options": [
1795 "nosniff"
1796 ],
1797 "cache-control": [
1798 "no-store"
1799 ],
1800 "content-type": [
1801 "application/json"
1802 ],
1803 "x-xss-protection": [
1804 "1; mode=block"
1805 ],
1806 "transfer-encoding": [
1807 "chunked"
1808 ],
1809 "date": [
1810 "Sat, 25 Jan 2020 06:01:20 GMT"
1811 ]
1812 },
1813 "body": {
1814 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
1815 }
1816 },
1817 "request": {
1818 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22date%5C%22%3A+%5C%220+0%5C%22%2C+%5C%22node%5C%22%3A+%5C%2275dbbc901145d7beb190197aa232f74540e5a9f3%5C%22%2C+%5C%22parent%5C%22%3A+%5C%22c88ce4c2d2ad6337b772bc093398f85858196eb5%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+19601%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
1819 "method": "POST",
1820 "headers": {
1821 "content-type": [
1822 "application/x-www-form-urlencoded"
1823 ],
1824 "user-agent": [
1825 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1826 ],
1827 "content-length": [
1828 "482"
1829 ],
1830 "host": [
1831 "phab.mercurial-scm.org"
1832 ],
1833 "accept": [
1834 "application/mercurial-0.1"
1835 ]
1836 },
1837 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
1838 }
1839 },
1840 {
1841 "response": {
1842 "status": {
1843 "message": "OK",
1844 "code": 200
1845 },
1846 "headers": {
1847 "strict-transport-security": [
1848 "max-age=0; includeSubdomains; preload"
1849 ],
1850 "server": [
1851 "Apache/2.4.10 (Debian)"
1852 ],
1853 "referrer-policy": [
1854 "no-referrer"
1855 ],
1856 "x-frame-options": [
1857 "Deny"
1858 ],
1859 "expires": [
1860 "Sat, 01 Jan 2000 00:00:00 GMT"
1861 ],
1862 "x-content-type-options": [
1863 "nosniff"
1864 ],
1865 "cache-control": [
1866 "no-store"
1867 ],
1868 "content-type": [
1869 "application/json"
1870 ],
1871 "x-xss-protection": [
1872 "1; mode=block"
1873 ],
1874 "transfer-encoding": [
1875 "chunked"
1876 ],
1877 "date": [
1878 "Sat, 25 Jan 2020 06:01:21 GMT"
1879 ]
1880 },
1881 "body": {
1882 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
1883 }
1884 },
1885 "request": {
1886 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22data%22%3A+%22%7B%5C%2275dbbc901145d7beb190197aa232f74540e5a9f3%5C%22%3A+%7B%5C%22author%5C%22%3A+%5C%22test%5C%22%2C+%5C%22authorEmail%5C%22%3A+%5C%22test%5C%22%2C+%5C%22branch%5C%22%3A+%5C%22default%5C%22%2C+%5C%22commit%5C%22%3A+%5C%2275dbbc901145d7beb190197aa232f74540e5a9f3%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%22c88ce4c2d2ad6337b772bc093398f85858196eb5%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+19601%2C+%22name%22%3A+%22local%3Acommits%22%7D",
1887 "method": "POST",
1888 "headers": {
1889 "content-type": [
1890 "application/x-www-form-urlencoded"
1891 ],
1892 "user-agent": [
1893 "mercurial/proto-1.0 (Mercurial 5.3rc1+3-624fe53ce1e7+20200124)"
1894 ],
1895 "content-length": [
1896 "594"
1897 ],
1898 "host": [
1899 "phab.mercurial-scm.org"
1900 ],
1901 "accept": [
1902 "application/mercurial-0.1"
1903 ]
1904 },
1905 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty"
1906 }
1907 }
1908 ]
1909 } No newline at end of file
@@ -1,1797 +1,1797 b''
1 # phabricator.py - simple Phabricator integration
1 # phabricator.py - simple Phabricator integration
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """simple Phabricator integration (EXPERIMENTAL)
7 """simple Phabricator integration (EXPERIMENTAL)
8
8
9 This extension provides a ``phabsend`` command which sends a stack of
9 This extension provides a ``phabsend`` command which sends a stack of
10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
12 to update statuses in batch.
12 to update statuses in batch.
13
13
14 A "phabstatus" view for :hg:`show` is also provided; it displays status
14 A "phabstatus" view for :hg:`show` is also provided; it displays status
15 information of Phabricator differentials associated with unfinished
15 information of Phabricator differentials associated with unfinished
16 changesets.
16 changesets.
17
17
18 By default, Phabricator requires ``Test Plan`` which might prevent some
18 By default, Phabricator requires ``Test Plan`` which might prevent some
19 changeset from being sent. The requirement could be disabled by changing
19 changeset from being sent. The requirement could be disabled by changing
20 ``differential.require-test-plan-field`` config server side.
20 ``differential.require-test-plan-field`` config server side.
21
21
22 Config::
22 Config::
23
23
24 [phabricator]
24 [phabricator]
25 # Phabricator URL
25 # Phabricator URL
26 url = https://phab.example.com/
26 url = https://phab.example.com/
27
27
28 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
28 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
29 # callsign is "FOO".
29 # callsign is "FOO".
30 callsign = FOO
30 callsign = FOO
31
31
32 # curl command to use. If not set (default), use builtin HTTP library to
32 # curl command to use. If not set (default), use builtin HTTP library to
33 # communicate. If set, use the specified curl command. This could be useful
33 # communicate. If set, use the specified curl command. This could be useful
34 # if you need to specify advanced options that is not easily supported by
34 # if you need to specify advanced options that is not easily supported by
35 # the internal library.
35 # the internal library.
36 curlcmd = curl --connect-timeout 2 --retry 3 --silent
36 curlcmd = curl --connect-timeout 2 --retry 3 --silent
37
37
38 [auth]
38 [auth]
39 example.schemes = https
39 example.schemes = https
40 example.prefix = phab.example.com
40 example.prefix = phab.example.com
41
41
42 # API token. Get it from https://$HOST/conduit/login/
42 # API token. Get it from https://$HOST/conduit/login/
43 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
43 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
44 """
44 """
45
45
46 from __future__ import absolute_import
46 from __future__ import absolute_import
47
47
48 import base64
48 import base64
49 import contextlib
49 import contextlib
50 import hashlib
50 import hashlib
51 import itertools
51 import itertools
52 import json
52 import json
53 import mimetypes
53 import mimetypes
54 import operator
54 import operator
55 import re
55 import re
56
56
57 from mercurial.node import bin, nullid
57 from mercurial.node import bin, nullid
58 from mercurial.i18n import _
58 from mercurial.i18n import _
59 from mercurial.pycompat import getattr
59 from mercurial.pycompat import getattr
60 from mercurial.thirdparty import attr
60 from mercurial.thirdparty import attr
61 from mercurial import (
61 from mercurial import (
62 cmdutil,
62 cmdutil,
63 context,
63 context,
64 encoding,
64 encoding,
65 error,
65 error,
66 exthelper,
66 exthelper,
67 graphmod,
67 graphmod,
68 httpconnection as httpconnectionmod,
68 httpconnection as httpconnectionmod,
69 localrepo,
69 localrepo,
70 logcmdutil,
70 logcmdutil,
71 match,
71 match,
72 mdiff,
72 mdiff,
73 obsutil,
73 obsutil,
74 parser,
74 parser,
75 patch,
75 patch,
76 phases,
76 phases,
77 pycompat,
77 pycompat,
78 scmutil,
78 scmutil,
79 smartset,
79 smartset,
80 tags,
80 tags,
81 templatefilters,
81 templatefilters,
82 templateutil,
82 templateutil,
83 url as urlmod,
83 url as urlmod,
84 util,
84 util,
85 )
85 )
86 from mercurial.utils import (
86 from mercurial.utils import (
87 procutil,
87 procutil,
88 stringutil,
88 stringutil,
89 )
89 )
90 from . import show
90 from . import show
91
91
92
92
93 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
93 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
94 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
94 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
95 # be specifying the version(s) of Mercurial they are tested with, or
95 # be specifying the version(s) of Mercurial they are tested with, or
96 # leave the attribute unspecified.
96 # leave the attribute unspecified.
97 testedwith = b'ships-with-hg-core'
97 testedwith = b'ships-with-hg-core'
98
98
99 eh = exthelper.exthelper()
99 eh = exthelper.exthelper()
100
100
101 cmdtable = eh.cmdtable
101 cmdtable = eh.cmdtable
102 command = eh.command
102 command = eh.command
103 configtable = eh.configtable
103 configtable = eh.configtable
104 templatekeyword = eh.templatekeyword
104 templatekeyword = eh.templatekeyword
105 uisetup = eh.finaluisetup
105 uisetup = eh.finaluisetup
106
106
107 # developer config: phabricator.batchsize
107 # developer config: phabricator.batchsize
108 eh.configitem(
108 eh.configitem(
109 b'phabricator', b'batchsize', default=12,
109 b'phabricator', b'batchsize', default=12,
110 )
110 )
111 eh.configitem(
111 eh.configitem(
112 b'phabricator', b'callsign', default=None,
112 b'phabricator', b'callsign', default=None,
113 )
113 )
114 eh.configitem(
114 eh.configitem(
115 b'phabricator', b'curlcmd', default=None,
115 b'phabricator', b'curlcmd', default=None,
116 )
116 )
117 # developer config: phabricator.repophid
117 # developer config: phabricator.repophid
118 eh.configitem(
118 eh.configitem(
119 b'phabricator', b'repophid', default=None,
119 b'phabricator', b'repophid', default=None,
120 )
120 )
121 eh.configitem(
121 eh.configitem(
122 b'phabricator', b'url', default=None,
122 b'phabricator', b'url', default=None,
123 )
123 )
124 eh.configitem(
124 eh.configitem(
125 b'phabsend', b'confirm', default=False,
125 b'phabsend', b'confirm', default=False,
126 )
126 )
127
127
128 colortable = {
128 colortable = {
129 b'phabricator.action.created': b'green',
129 b'phabricator.action.created': b'green',
130 b'phabricator.action.skipped': b'magenta',
130 b'phabricator.action.skipped': b'magenta',
131 b'phabricator.action.updated': b'magenta',
131 b'phabricator.action.updated': b'magenta',
132 b'phabricator.desc': b'',
132 b'phabricator.desc': b'',
133 b'phabricator.drev': b'bold',
133 b'phabricator.drev': b'bold',
134 b'phabricator.node': b'',
134 b'phabricator.node': b'',
135 b'phabricator.status.abandoned': b'magenta dim',
135 b'phabricator.status.abandoned': b'magenta dim',
136 b'phabricator.status.accepted': b'green bold',
136 b'phabricator.status.accepted': b'green bold',
137 b'phabricator.status.closed': b'green',
137 b'phabricator.status.closed': b'green',
138 b'phabricator.status.needsreview': b'yellow',
138 b'phabricator.status.needsreview': b'yellow',
139 b'phabricator.status.needsrevision': b'red',
139 b'phabricator.status.needsrevision': b'red',
140 b'phabricator.status.changesplanned': b'red',
140 b'phabricator.status.changesplanned': b'red',
141 }
141 }
142
142
143 _VCR_FLAGS = [
143 _VCR_FLAGS = [
144 (
144 (
145 b'',
145 b'',
146 b'test-vcr',
146 b'test-vcr',
147 b'',
147 b'',
148 _(
148 _(
149 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
149 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
150 b', otherwise will mock all http requests using the specified vcr file.'
150 b', otherwise will mock all http requests using the specified vcr file.'
151 b' (ADVANCED)'
151 b' (ADVANCED)'
152 ),
152 ),
153 ),
153 ),
154 ]
154 ]
155
155
156
156
157 @eh.wrapfunction(localrepo, "loadhgrc")
157 @eh.wrapfunction(localrepo, "loadhgrc")
158 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):
158 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):
159 """Load ``.arcconfig`` content into a ui instance on repository open.
159 """Load ``.arcconfig`` content into a ui instance on repository open.
160 """
160 """
161 result = False
161 result = False
162 arcconfig = {}
162 arcconfig = {}
163
163
164 try:
164 try:
165 # json.loads only accepts bytes from 3.6+
165 # json.loads only accepts bytes from 3.6+
166 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
166 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
167 # json.loads only returns unicode strings
167 # json.loads only returns unicode strings
168 arcconfig = pycompat.rapply(
168 arcconfig = pycompat.rapply(
169 lambda x: encoding.unitolocal(x)
169 lambda x: encoding.unitolocal(x)
170 if isinstance(x, pycompat.unicode)
170 if isinstance(x, pycompat.unicode)
171 else x,
171 else x,
172 pycompat.json_loads(rawparams),
172 pycompat.json_loads(rawparams),
173 )
173 )
174
174
175 result = True
175 result = True
176 except ValueError:
176 except ValueError:
177 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
177 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
178 except IOError:
178 except IOError:
179 pass
179 pass
180
180
181 cfg = util.sortdict()
181 cfg = util.sortdict()
182
182
183 if b"repository.callsign" in arcconfig:
183 if b"repository.callsign" in arcconfig:
184 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
184 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
185
185
186 if b"phabricator.uri" in arcconfig:
186 if b"phabricator.uri" in arcconfig:
187 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
187 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
188
188
189 if cfg:
189 if cfg:
190 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
190 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
191
191
192 return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg/hgrc
192 return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg/hgrc
193
193
194
194
195 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
195 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
196 fullflags = flags + _VCR_FLAGS
196 fullflags = flags + _VCR_FLAGS
197
197
198 def hgmatcher(r1, r2):
198 def hgmatcher(r1, r2):
199 if r1.uri != r2.uri or r1.method != r2.method:
199 if r1.uri != r2.uri or r1.method != r2.method:
200 return False
200 return False
201 r1params = util.urlreq.parseqs(r1.body)
201 r1params = util.urlreq.parseqs(r1.body)
202 r2params = util.urlreq.parseqs(r2.body)
202 r2params = util.urlreq.parseqs(r2.body)
203 for key in r1params:
203 for key in r1params:
204 if key not in r2params:
204 if key not in r2params:
205 return False
205 return False
206 value = r1params[key][0]
206 value = r1params[key][0]
207 # we want to compare json payloads without worrying about ordering
207 # we want to compare json payloads without worrying about ordering
208 if value.startswith(b'{') and value.endswith(b'}'):
208 if value.startswith(b'{') and value.endswith(b'}'):
209 r1json = pycompat.json_loads(value)
209 r1json = pycompat.json_loads(value)
210 r2json = pycompat.json_loads(r2params[key][0])
210 r2json = pycompat.json_loads(r2params[key][0])
211 if r1json != r2json:
211 if r1json != r2json:
212 return False
212 return False
213 elif r2params[key][0] != value:
213 elif r2params[key][0] != value:
214 return False
214 return False
215 return True
215 return True
216
216
217 def sanitiserequest(request):
217 def sanitiserequest(request):
218 request.body = re.sub(
218 request.body = re.sub(
219 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
219 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
220 )
220 )
221 return request
221 return request
222
222
223 def sanitiseresponse(response):
223 def sanitiseresponse(response):
224 if 'set-cookie' in response['headers']:
224 if 'set-cookie' in response['headers']:
225 del response['headers']['set-cookie']
225 del response['headers']['set-cookie']
226 return response
226 return response
227
227
228 def decorate(fn):
228 def decorate(fn):
229 def inner(*args, **kwargs):
229 def inner(*args, **kwargs):
230 cassette = pycompat.fsdecode(kwargs.pop('test_vcr', None))
230 cassette = pycompat.fsdecode(kwargs.pop('test_vcr', None))
231 if cassette:
231 if cassette:
232 import hgdemandimport
232 import hgdemandimport
233
233
234 with hgdemandimport.deactivated():
234 with hgdemandimport.deactivated():
235 import vcr as vcrmod
235 import vcr as vcrmod
236 import vcr.stubs as stubs
236 import vcr.stubs as stubs
237
237
238 vcr = vcrmod.VCR(
238 vcr = vcrmod.VCR(
239 serializer='json',
239 serializer='json',
240 before_record_request=sanitiserequest,
240 before_record_request=sanitiserequest,
241 before_record_response=sanitiseresponse,
241 before_record_response=sanitiseresponse,
242 custom_patches=[
242 custom_patches=[
243 (
243 (
244 urlmod,
244 urlmod,
245 'httpconnection',
245 'httpconnection',
246 stubs.VCRHTTPConnection,
246 stubs.VCRHTTPConnection,
247 ),
247 ),
248 (
248 (
249 urlmod,
249 urlmod,
250 'httpsconnection',
250 'httpsconnection',
251 stubs.VCRHTTPSConnection,
251 stubs.VCRHTTPSConnection,
252 ),
252 ),
253 ],
253 ],
254 )
254 )
255 vcr.register_matcher('hgmatcher', hgmatcher)
255 vcr.register_matcher('hgmatcher', hgmatcher)
256 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
256 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
257 return fn(*args, **kwargs)
257 return fn(*args, **kwargs)
258 return fn(*args, **kwargs)
258 return fn(*args, **kwargs)
259
259
260 inner.__name__ = fn.__name__
260 inner.__name__ = fn.__name__
261 inner.__doc__ = fn.__doc__
261 inner.__doc__ = fn.__doc__
262 return command(
262 return command(
263 name,
263 name,
264 fullflags,
264 fullflags,
265 spec,
265 spec,
266 helpcategory=helpcategory,
266 helpcategory=helpcategory,
267 optionalrepo=optionalrepo,
267 optionalrepo=optionalrepo,
268 )(inner)
268 )(inner)
269
269
270 return decorate
270 return decorate
271
271
272
272
273 def urlencodenested(params):
273 def urlencodenested(params):
274 """like urlencode, but works with nested parameters.
274 """like urlencode, but works with nested parameters.
275
275
276 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
276 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
277 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
277 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
278 urlencode. Note: the encoding is consistent with PHP's http_build_query.
278 urlencode. Note: the encoding is consistent with PHP's http_build_query.
279 """
279 """
280 flatparams = util.sortdict()
280 flatparams = util.sortdict()
281
281
282 def process(prefix, obj):
282 def process(prefix, obj):
283 if isinstance(obj, bool):
283 if isinstance(obj, bool):
284 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
284 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
285 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
285 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
286 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
286 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
287 if items is None:
287 if items is None:
288 flatparams[prefix] = obj
288 flatparams[prefix] = obj
289 else:
289 else:
290 for k, v in items(obj):
290 for k, v in items(obj):
291 if prefix:
291 if prefix:
292 process(b'%s[%s]' % (prefix, k), v)
292 process(b'%s[%s]' % (prefix, k), v)
293 else:
293 else:
294 process(k, v)
294 process(k, v)
295
295
296 process(b'', params)
296 process(b'', params)
297 return util.urlreq.urlencode(flatparams)
297 return util.urlreq.urlencode(flatparams)
298
298
299
299
300 def readurltoken(ui):
300 def readurltoken(ui):
301 """return conduit url, token and make sure they exist
301 """return conduit url, token and make sure they exist
302
302
303 Currently read from [auth] config section. In the future, it might
303 Currently read from [auth] config section. In the future, it might
304 make sense to read from .arcconfig and .arcrc as well.
304 make sense to read from .arcconfig and .arcrc as well.
305 """
305 """
306 url = ui.config(b'phabricator', b'url')
306 url = ui.config(b'phabricator', b'url')
307 if not url:
307 if not url:
308 raise error.Abort(
308 raise error.Abort(
309 _(b'config %s.%s is required') % (b'phabricator', b'url')
309 _(b'config %s.%s is required') % (b'phabricator', b'url')
310 )
310 )
311
311
312 res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)
312 res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)
313 token = None
313 token = None
314
314
315 if res:
315 if res:
316 group, auth = res
316 group, auth = res
317
317
318 ui.debug(b"using auth.%s.* for authentication\n" % group)
318 ui.debug(b"using auth.%s.* for authentication\n" % group)
319
319
320 token = auth.get(b'phabtoken')
320 token = auth.get(b'phabtoken')
321
321
322 if not token:
322 if not token:
323 raise error.Abort(
323 raise error.Abort(
324 _(b'Can\'t find conduit token associated to %s') % (url,)
324 _(b'Can\'t find conduit token associated to %s') % (url,)
325 )
325 )
326
326
327 return url, token
327 return url, token
328
328
329
329
330 def callconduit(ui, name, params):
330 def callconduit(ui, name, params):
331 """call Conduit API, params is a dict. return json.loads result, or None"""
331 """call Conduit API, params is a dict. return json.loads result, or None"""
332 host, token = readurltoken(ui)
332 host, token = readurltoken(ui)
333 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
333 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
334 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
334 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
335 params = params.copy()
335 params = params.copy()
336 params[b'__conduit__'] = {
336 params[b'__conduit__'] = {
337 b'token': token,
337 b'token': token,
338 }
338 }
339 rawdata = {
339 rawdata = {
340 b'params': templatefilters.json(params),
340 b'params': templatefilters.json(params),
341 b'output': b'json',
341 b'output': b'json',
342 b'__conduit__': 1,
342 b'__conduit__': 1,
343 }
343 }
344 data = urlencodenested(rawdata)
344 data = urlencodenested(rawdata)
345 curlcmd = ui.config(b'phabricator', b'curlcmd')
345 curlcmd = ui.config(b'phabricator', b'curlcmd')
346 if curlcmd:
346 if curlcmd:
347 sin, sout = procutil.popen2(
347 sin, sout = procutil.popen2(
348 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
348 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
349 )
349 )
350 sin.write(data)
350 sin.write(data)
351 sin.close()
351 sin.close()
352 body = sout.read()
352 body = sout.read()
353 else:
353 else:
354 urlopener = urlmod.opener(ui, authinfo)
354 urlopener = urlmod.opener(ui, authinfo)
355 request = util.urlreq.request(pycompat.strurl(url), data=data)
355 request = util.urlreq.request(pycompat.strurl(url), data=data)
356 with contextlib.closing(urlopener.open(request)) as rsp:
356 with contextlib.closing(urlopener.open(request)) as rsp:
357 body = rsp.read()
357 body = rsp.read()
358 ui.debug(b'Conduit Response: %s\n' % body)
358 ui.debug(b'Conduit Response: %s\n' % body)
359 parsed = pycompat.rapply(
359 parsed = pycompat.rapply(
360 lambda x: encoding.unitolocal(x)
360 lambda x: encoding.unitolocal(x)
361 if isinstance(x, pycompat.unicode)
361 if isinstance(x, pycompat.unicode)
362 else x,
362 else x,
363 # json.loads only accepts bytes from py3.6+
363 # json.loads only accepts bytes from py3.6+
364 pycompat.json_loads(encoding.unifromlocal(body)),
364 pycompat.json_loads(encoding.unifromlocal(body)),
365 )
365 )
366 if parsed.get(b'error_code'):
366 if parsed.get(b'error_code'):
367 msg = _(b'Conduit Error (%s): %s') % (
367 msg = _(b'Conduit Error (%s): %s') % (
368 parsed[b'error_code'],
368 parsed[b'error_code'],
369 parsed[b'error_info'],
369 parsed[b'error_info'],
370 )
370 )
371 raise error.Abort(msg)
371 raise error.Abort(msg)
372 return parsed[b'result']
372 return parsed[b'result']
373
373
374
374
375 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
375 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
376 def debugcallconduit(ui, repo, name):
376 def debugcallconduit(ui, repo, name):
377 """call Conduit API
377 """call Conduit API
378
378
379 Call parameters are read from stdin as a JSON blob. Result will be written
379 Call parameters are read from stdin as a JSON blob. Result will be written
380 to stdout as a JSON blob.
380 to stdout as a JSON blob.
381 """
381 """
382 # json.loads only accepts bytes from 3.6+
382 # json.loads only accepts bytes from 3.6+
383 rawparams = encoding.unifromlocal(ui.fin.read())
383 rawparams = encoding.unifromlocal(ui.fin.read())
384 # json.loads only returns unicode strings
384 # json.loads only returns unicode strings
385 params = pycompat.rapply(
385 params = pycompat.rapply(
386 lambda x: encoding.unitolocal(x)
386 lambda x: encoding.unitolocal(x)
387 if isinstance(x, pycompat.unicode)
387 if isinstance(x, pycompat.unicode)
388 else x,
388 else x,
389 pycompat.json_loads(rawparams),
389 pycompat.json_loads(rawparams),
390 )
390 )
391 # json.dumps only accepts unicode strings
391 # json.dumps only accepts unicode strings
392 result = pycompat.rapply(
392 result = pycompat.rapply(
393 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
393 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
394 callconduit(ui, name, params),
394 callconduit(ui, name, params),
395 )
395 )
396 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
396 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
397 ui.write(b'%s\n' % encoding.unitolocal(s))
397 ui.write(b'%s\n' % encoding.unitolocal(s))
398
398
399
399
400 def getrepophid(repo):
400 def getrepophid(repo):
401 """given callsign, return repository PHID or None"""
401 """given callsign, return repository PHID or None"""
402 # developer config: phabricator.repophid
402 # developer config: phabricator.repophid
403 repophid = repo.ui.config(b'phabricator', b'repophid')
403 repophid = repo.ui.config(b'phabricator', b'repophid')
404 if repophid:
404 if repophid:
405 return repophid
405 return repophid
406 callsign = repo.ui.config(b'phabricator', b'callsign')
406 callsign = repo.ui.config(b'phabricator', b'callsign')
407 if not callsign:
407 if not callsign:
408 return None
408 return None
409 query = callconduit(
409 query = callconduit(
410 repo.ui,
410 repo.ui,
411 b'diffusion.repository.search',
411 b'diffusion.repository.search',
412 {b'constraints': {b'callsigns': [callsign]}},
412 {b'constraints': {b'callsigns': [callsign]}},
413 )
413 )
414 if len(query[b'data']) == 0:
414 if len(query[b'data']) == 0:
415 return None
415 return None
416 repophid = query[b'data'][0][b'phid']
416 repophid = query[b'data'][0][b'phid']
417 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
417 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
418 return repophid
418 return repophid
419
419
420
420
421 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
421 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
422 _differentialrevisiondescre = re.compile(
422 _differentialrevisiondescre = re.compile(
423 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
423 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
424 )
424 )
425
425
426
426
427 def getoldnodedrevmap(repo, nodelist):
427 def getoldnodedrevmap(repo, nodelist):
428 """find previous nodes that has been sent to Phabricator
428 """find previous nodes that has been sent to Phabricator
429
429
430 return {node: (oldnode, Differential diff, Differential Revision ID)}
430 return {node: (oldnode, Differential diff, Differential Revision ID)}
431 for node in nodelist with known previous sent versions, or associated
431 for node in nodelist with known previous sent versions, or associated
432 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
432 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
433 be ``None``.
433 be ``None``.
434
434
435 Examines commit messages like "Differential Revision:" to get the
435 Examines commit messages like "Differential Revision:" to get the
436 association information.
436 association information.
437
437
438 If such commit message line is not found, examines all precursors and their
438 If such commit message line is not found, examines all precursors and their
439 tags. Tags with format like "D1234" are considered a match and the node
439 tags. Tags with format like "D1234" are considered a match and the node
440 with that tag, and the number after "D" (ex. 1234) will be returned.
440 with that tag, and the number after "D" (ex. 1234) will be returned.
441
441
442 The ``old node``, if not None, is guaranteed to be the last diff of
442 The ``old node``, if not None, is guaranteed to be the last diff of
443 corresponding Differential Revision, and exist in the repo.
443 corresponding Differential Revision, and exist in the repo.
444 """
444 """
445 unfi = repo.unfiltered()
445 unfi = repo.unfiltered()
446 has_node = unfi.changelog.index.has_node
446 has_node = unfi.changelog.index.has_node
447
447
448 result = {} # {node: (oldnode?, lastdiff?, drev)}
448 result = {} # {node: (oldnode?, lastdiff?, drev)}
449 toconfirm = {} # {node: (force, {precnode}, drev)}
449 toconfirm = {} # {node: (force, {precnode}, drev)}
450 for node in nodelist:
450 for node in nodelist:
451 ctx = unfi[node]
451 ctx = unfi[node]
452 # For tags like "D123", put them into "toconfirm" to verify later
452 # For tags like "D123", put them into "toconfirm" to verify later
453 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
453 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
454 for n in precnodes:
454 for n in precnodes:
455 if has_node(n):
455 if has_node(n):
456 for tag in unfi.nodetags(n):
456 for tag in unfi.nodetags(n):
457 m = _differentialrevisiontagre.match(tag)
457 m = _differentialrevisiontagre.match(tag)
458 if m:
458 if m:
459 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
459 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
460 break
460 break
461 else:
461 else:
462 continue # move to next predecessor
462 continue # move to next predecessor
463 break # found a tag, stop
463 break # found a tag, stop
464 else:
464 else:
465 # Check commit message
465 # Check commit message
466 m = _differentialrevisiondescre.search(ctx.description())
466 m = _differentialrevisiondescre.search(ctx.description())
467 if m:
467 if m:
468 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
468 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
469
469
470 # Double check if tags are genuine by collecting all old nodes from
470 # Double check if tags are genuine by collecting all old nodes from
471 # Phabricator, and expect precursors overlap with it.
471 # Phabricator, and expect precursors overlap with it.
472 if toconfirm:
472 if toconfirm:
473 drevs = [drev for force, precs, drev in toconfirm.values()]
473 drevs = [drev for force, precs, drev in toconfirm.values()]
474 alldiffs = callconduit(
474 alldiffs = callconduit(
475 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
475 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
476 )
476 )
477 getnode = lambda d: bin(getdiffmeta(d).get(b'node', b'')) or None
477 getnode = lambda d: bin(getdiffmeta(d).get(b'node', b'')) or None
478 for newnode, (force, precset, drev) in toconfirm.items():
478 for newnode, (force, precset, drev) in toconfirm.items():
479 diffs = [
479 diffs = [
480 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
480 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
481 ]
481 ]
482
482
483 # "precursors" as known by Phabricator
483 # "precursors" as known by Phabricator
484 phprecset = set(getnode(d) for d in diffs)
484 phprecset = set(getnode(d) for d in diffs)
485
485
486 # Ignore if precursors (Phabricator and local repo) do not overlap,
486 # Ignore if precursors (Phabricator and local repo) do not overlap,
487 # and force is not set (when commit message says nothing)
487 # and force is not set (when commit message says nothing)
488 if not force and not bool(phprecset & precset):
488 if not force and not bool(phprecset & precset):
489 tagname = b'D%d' % drev
489 tagname = b'D%d' % drev
490 tags.tag(
490 tags.tag(
491 repo,
491 repo,
492 tagname,
492 tagname,
493 nullid,
493 nullid,
494 message=None,
494 message=None,
495 user=None,
495 user=None,
496 date=None,
496 date=None,
497 local=True,
497 local=True,
498 )
498 )
499 unfi.ui.warn(
499 unfi.ui.warn(
500 _(
500 _(
501 b'D%d: local tag removed - does not match '
501 b'D%d: local tag removed - does not match '
502 b'Differential history\n'
502 b'Differential history\n'
503 )
503 )
504 % drev
504 % drev
505 )
505 )
506 continue
506 continue
507
507
508 # Find the last node using Phabricator metadata, and make sure it
508 # Find the last node using Phabricator metadata, and make sure it
509 # exists in the repo
509 # exists in the repo
510 oldnode = lastdiff = None
510 oldnode = lastdiff = None
511 if diffs:
511 if diffs:
512 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
512 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
513 oldnode = getnode(lastdiff)
513 oldnode = getnode(lastdiff)
514 if oldnode and not has_node(oldnode):
514 if oldnode and not has_node(oldnode):
515 oldnode = None
515 oldnode = None
516
516
517 result[newnode] = (oldnode, lastdiff, drev)
517 result[newnode] = (oldnode, lastdiff, drev)
518
518
519 return result
519 return result
520
520
521
521
522 def getdrevmap(repo, revs):
522 def getdrevmap(repo, revs):
523 """Return a dict mapping each rev in `revs` to their Differential Revision
523 """Return a dict mapping each rev in `revs` to their Differential Revision
524 ID or None.
524 ID or None.
525 """
525 """
526 result = {}
526 result = {}
527 for rev in revs:
527 for rev in revs:
528 result[rev] = None
528 result[rev] = None
529 ctx = repo[rev]
529 ctx = repo[rev]
530 # Check commit message
530 # Check commit message
531 m = _differentialrevisiondescre.search(ctx.description())
531 m = _differentialrevisiondescre.search(ctx.description())
532 if m:
532 if m:
533 result[rev] = int(m.group('id'))
533 result[rev] = int(m.group('id'))
534 continue
534 continue
535 # Check tags
535 # Check tags
536 for tag in repo.nodetags(ctx.node()):
536 for tag in repo.nodetags(ctx.node()):
537 m = _differentialrevisiontagre.match(tag)
537 m = _differentialrevisiontagre.match(tag)
538 if m:
538 if m:
539 result[rev] = int(m.group(1))
539 result[rev] = int(m.group(1))
540 break
540 break
541
541
542 return result
542 return result
543
543
544
544
545 def getdiff(ctx, diffopts):
545 def getdiff(ctx, diffopts):
546 """plain-text diff without header (user, commit message, etc)"""
546 """plain-text diff without header (user, commit message, etc)"""
547 output = util.stringio()
547 output = util.stringio()
548 for chunk, _label in patch.diffui(
548 for chunk, _label in patch.diffui(
549 ctx.repo(), ctx.p1().node(), ctx.node(), None, opts=diffopts
549 ctx.repo(), ctx.p1().node(), ctx.node(), None, opts=diffopts
550 ):
550 ):
551 output.write(chunk)
551 output.write(chunk)
552 return output.getvalue()
552 return output.getvalue()
553
553
554
554
555 class DiffChangeType(object):
555 class DiffChangeType(object):
556 ADD = 1
556 ADD = 1
557 CHANGE = 2
557 CHANGE = 2
558 DELETE = 3
558 DELETE = 3
559 MOVE_AWAY = 4
559 MOVE_AWAY = 4
560 COPY_AWAY = 5
560 COPY_AWAY = 5
561 MOVE_HERE = 6
561 MOVE_HERE = 6
562 COPY_HERE = 7
562 COPY_HERE = 7
563 MULTICOPY = 8
563 MULTICOPY = 8
564
564
565
565
566 class DiffFileType(object):
566 class DiffFileType(object):
567 TEXT = 1
567 TEXT = 1
568 IMAGE = 2
568 IMAGE = 2
569 BINARY = 3
569 BINARY = 3
570
570
571
571
572 @attr.s
572 @attr.s
573 class phabhunk(dict):
573 class phabhunk(dict):
574 """Represents a Differential hunk, which is owned by a Differential change
574 """Represents a Differential hunk, which is owned by a Differential change
575 """
575 """
576
576
577 oldOffset = attr.ib(default=0) # camelcase-required
577 oldOffset = attr.ib(default=0) # camelcase-required
578 oldLength = attr.ib(default=0) # camelcase-required
578 oldLength = attr.ib(default=0) # camelcase-required
579 newOffset = attr.ib(default=0) # camelcase-required
579 newOffset = attr.ib(default=0) # camelcase-required
580 newLength = attr.ib(default=0) # camelcase-required
580 newLength = attr.ib(default=0) # camelcase-required
581 corpus = attr.ib(default='')
581 corpus = attr.ib(default='')
582 # These get added to the phabchange's equivalents
582 # These get added to the phabchange's equivalents
583 addLines = attr.ib(default=0) # camelcase-required
583 addLines = attr.ib(default=0) # camelcase-required
584 delLines = attr.ib(default=0) # camelcase-required
584 delLines = attr.ib(default=0) # camelcase-required
585
585
586
586
587 @attr.s
587 @attr.s
588 class phabchange(object):
588 class phabchange(object):
589 """Represents a Differential change, owns Differential hunks and owned by a
589 """Represents a Differential change, owns Differential hunks and owned by a
590 Differential diff. Each one represents one file in a diff.
590 Differential diff. Each one represents one file in a diff.
591 """
591 """
592
592
593 currentPath = attr.ib(default=None) # camelcase-required
593 currentPath = attr.ib(default=None) # camelcase-required
594 oldPath = attr.ib(default=None) # camelcase-required
594 oldPath = attr.ib(default=None) # camelcase-required
595 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
595 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
596 metadata = attr.ib(default=attr.Factory(dict))
596 metadata = attr.ib(default=attr.Factory(dict))
597 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
597 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
598 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
598 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
599 type = attr.ib(default=DiffChangeType.CHANGE)
599 type = attr.ib(default=DiffChangeType.CHANGE)
600 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
600 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
601 commitHash = attr.ib(default=None) # camelcase-required
601 commitHash = attr.ib(default=None) # camelcase-required
602 addLines = attr.ib(default=0) # camelcase-required
602 addLines = attr.ib(default=0) # camelcase-required
603 delLines = attr.ib(default=0) # camelcase-required
603 delLines = attr.ib(default=0) # camelcase-required
604 hunks = attr.ib(default=attr.Factory(list))
604 hunks = attr.ib(default=attr.Factory(list))
605
605
606 def copynewmetadatatoold(self):
606 def copynewmetadatatoold(self):
607 for key in list(self.metadata.keys()):
607 for key in list(self.metadata.keys()):
608 newkey = key.replace(b'new:', b'old:')
608 newkey = key.replace(b'new:', b'old:')
609 self.metadata[newkey] = self.metadata[key]
609 self.metadata[newkey] = self.metadata[key]
610
610
611 def addoldmode(self, value):
611 def addoldmode(self, value):
612 self.oldProperties[b'unix:filemode'] = value
612 self.oldProperties[b'unix:filemode'] = value
613
613
614 def addnewmode(self, value):
614 def addnewmode(self, value):
615 self.newProperties[b'unix:filemode'] = value
615 self.newProperties[b'unix:filemode'] = value
616
616
617 def addhunk(self, hunk):
617 def addhunk(self, hunk):
618 if not isinstance(hunk, phabhunk):
618 if not isinstance(hunk, phabhunk):
619 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
619 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
620 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
620 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
621 # It's useful to include these stats since the Phab web UI shows them,
621 # It's useful to include these stats since the Phab web UI shows them,
622 # and uses them to estimate how large a change a Revision is. Also used
622 # and uses them to estimate how large a change a Revision is. Also used
623 # in email subjects for the [+++--] bit.
623 # in email subjects for the [+++--] bit.
624 self.addLines += hunk.addLines
624 self.addLines += hunk.addLines
625 self.delLines += hunk.delLines
625 self.delLines += hunk.delLines
626
626
627
627
628 @attr.s
628 @attr.s
629 class phabdiff(object):
629 class phabdiff(object):
630 """Represents a Differential diff, owns Differential changes. Corresponds
630 """Represents a Differential diff, owns Differential changes. Corresponds
631 to a commit.
631 to a commit.
632 """
632 """
633
633
634 # Doesn't seem to be any reason to send this (output of uname -n)
634 # Doesn't seem to be any reason to send this (output of uname -n)
635 sourceMachine = attr.ib(default=b'') # camelcase-required
635 sourceMachine = attr.ib(default=b'') # camelcase-required
636 sourcePath = attr.ib(default=b'/') # camelcase-required
636 sourcePath = attr.ib(default=b'/') # camelcase-required
637 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
637 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
638 sourceControlPath = attr.ib(default=b'/') # camelcase-required
638 sourceControlPath = attr.ib(default=b'/') # camelcase-required
639 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
639 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
640 branch = attr.ib(default=b'default')
640 branch = attr.ib(default=b'default')
641 bookmark = attr.ib(default=None)
641 bookmark = attr.ib(default=None)
642 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
642 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
643 lintStatus = attr.ib(default=b'none') # camelcase-required
643 lintStatus = attr.ib(default=b'none') # camelcase-required
644 unitStatus = attr.ib(default=b'none') # camelcase-required
644 unitStatus = attr.ib(default=b'none') # camelcase-required
645 changes = attr.ib(default=attr.Factory(dict))
645 changes = attr.ib(default=attr.Factory(dict))
646 repositoryPHID = attr.ib(default=None) # camelcase-required
646 repositoryPHID = attr.ib(default=None) # camelcase-required
647
647
648 def addchange(self, change):
648 def addchange(self, change):
649 if not isinstance(change, phabchange):
649 if not isinstance(change, phabchange):
650 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
650 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
651 self.changes[change.currentPath] = pycompat.byteskwargs(
651 self.changes[change.currentPath] = pycompat.byteskwargs(
652 attr.asdict(change)
652 attr.asdict(change)
653 )
653 )
654
654
655
655
656 def maketext(pchange, ctx, fname):
656 def maketext(pchange, ctx, fname):
657 """populate the phabchange for a text file"""
657 """populate the phabchange for a text file"""
658 repo = ctx.repo()
658 repo = ctx.repo()
659 fmatcher = match.exact([fname])
659 fmatcher = match.exact([fname])
660 diffopts = mdiff.diffopts(git=True, context=32767)
660 diffopts = mdiff.diffopts(git=True, context=32767)
661 _pfctx, _fctx, header, fhunks = next(
661 _pfctx, _fctx, header, fhunks = next(
662 patch.diffhunks(repo, ctx.p1(), ctx, fmatcher, opts=diffopts)
662 patch.diffhunks(repo, ctx.p1(), ctx, fmatcher, opts=diffopts)
663 )
663 )
664
664
665 for fhunk in fhunks:
665 for fhunk in fhunks:
666 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
666 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
667 corpus = b''.join(lines[1:])
667 corpus = b''.join(lines[1:])
668 shunk = list(header)
668 shunk = list(header)
669 shunk.extend(lines)
669 shunk.extend(lines)
670 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
670 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
671 patch.diffstatdata(util.iterlines(shunk))
671 patch.diffstatdata(util.iterlines(shunk))
672 )
672 )
673 pchange.addhunk(
673 pchange.addhunk(
674 phabhunk(
674 phabhunk(
675 oldOffset,
675 oldOffset,
676 oldLength,
676 oldLength,
677 newOffset,
677 newOffset,
678 newLength,
678 newLength,
679 corpus,
679 corpus,
680 addLines,
680 addLines,
681 delLines,
681 delLines,
682 )
682 )
683 )
683 )
684
684
685
685
686 def uploadchunks(fctx, fphid):
686 def uploadchunks(fctx, fphid):
687 """upload large binary files as separate chunks.
687 """upload large binary files as separate chunks.
688 Phab requests chunking over 8MiB, and splits into 4MiB chunks
688 Phab requests chunking over 8MiB, and splits into 4MiB chunks
689 """
689 """
690 ui = fctx.repo().ui
690 ui = fctx.repo().ui
691 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
691 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
692 with ui.makeprogress(
692 with ui.makeprogress(
693 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
693 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
694 ) as progress:
694 ) as progress:
695 for chunk in chunks:
695 for chunk in chunks:
696 progress.increment()
696 progress.increment()
697 if chunk[b'complete']:
697 if chunk[b'complete']:
698 continue
698 continue
699 bstart = int(chunk[b'byteStart'])
699 bstart = int(chunk[b'byteStart'])
700 bend = int(chunk[b'byteEnd'])
700 bend = int(chunk[b'byteEnd'])
701 callconduit(
701 callconduit(
702 ui,
702 ui,
703 b'file.uploadchunk',
703 b'file.uploadchunk',
704 {
704 {
705 b'filePHID': fphid,
705 b'filePHID': fphid,
706 b'byteStart': bstart,
706 b'byteStart': bstart,
707 b'data': base64.b64encode(fctx.data()[bstart:bend]),
707 b'data': base64.b64encode(fctx.data()[bstart:bend]),
708 b'dataEncoding': b'base64',
708 b'dataEncoding': b'base64',
709 },
709 },
710 )
710 )
711
711
712
712
713 def uploadfile(fctx):
713 def uploadfile(fctx):
714 """upload binary files to Phabricator"""
714 """upload binary files to Phabricator"""
715 repo = fctx.repo()
715 repo = fctx.repo()
716 ui = repo.ui
716 ui = repo.ui
717 fname = fctx.path()
717 fname = fctx.path()
718 size = fctx.size()
718 size = fctx.size()
719 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
719 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
720
720
721 # an allocate call is required first to see if an upload is even required
721 # an allocate call is required first to see if an upload is even required
722 # (Phab might already have it) and to determine if chunking is needed
722 # (Phab might already have it) and to determine if chunking is needed
723 allocateparams = {
723 allocateparams = {
724 b'name': fname,
724 b'name': fname,
725 b'contentLength': size,
725 b'contentLength': size,
726 b'contentHash': fhash,
726 b'contentHash': fhash,
727 }
727 }
728 filealloc = callconduit(ui, b'file.allocate', allocateparams)
728 filealloc = callconduit(ui, b'file.allocate', allocateparams)
729 fphid = filealloc[b'filePHID']
729 fphid = filealloc[b'filePHID']
730
730
731 if filealloc[b'upload']:
731 if filealloc[b'upload']:
732 ui.write(_(b'uploading %s\n') % bytes(fctx))
732 ui.write(_(b'uploading %s\n') % bytes(fctx))
733 if not fphid:
733 if not fphid:
734 uploadparams = {
734 uploadparams = {
735 b'name': fname,
735 b'name': fname,
736 b'data_base64': base64.b64encode(fctx.data()),
736 b'data_base64': base64.b64encode(fctx.data()),
737 }
737 }
738 fphid = callconduit(ui, b'file.upload', uploadparams)
738 fphid = callconduit(ui, b'file.upload', uploadparams)
739 else:
739 else:
740 uploadchunks(fctx, fphid)
740 uploadchunks(fctx, fphid)
741 else:
741 else:
742 ui.debug(b'server already has %s\n' % bytes(fctx))
742 ui.debug(b'server already has %s\n' % bytes(fctx))
743
743
744 if not fphid:
744 if not fphid:
745 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
745 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
746
746
747 return fphid
747 return fphid
748
748
749
749
750 def addoldbinary(pchange, fctx, originalfname):
750 def addoldbinary(pchange, fctx):
751 """add the metadata for the previous version of a binary file to the
751 """add the metadata for the previous version of a binary file to the
752 phabchange for the new version
752 phabchange for the new version
753 """
753 """
754 oldfctx = fctx.p1()[originalfname]
754 oldfctx = fctx.p1()
755 if fctx.cmp(oldfctx):
755 if fctx.cmp(oldfctx):
756 # Files differ, add the old one
756 # Files differ, add the old one
757 pchange.metadata[b'old:file:size'] = oldfctx.size()
757 pchange.metadata[b'old:file:size'] = oldfctx.size()
758 mimeguess, _enc = mimetypes.guess_type(
758 mimeguess, _enc = mimetypes.guess_type(
759 encoding.unifromlocal(oldfctx.path())
759 encoding.unifromlocal(oldfctx.path())
760 )
760 )
761 if mimeguess:
761 if mimeguess:
762 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
762 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
763 mimeguess
763 mimeguess
764 )
764 )
765 fphid = uploadfile(oldfctx)
765 fphid = uploadfile(oldfctx)
766 pchange.metadata[b'old:binary-phid'] = fphid
766 pchange.metadata[b'old:binary-phid'] = fphid
767 else:
767 else:
768 # If it's left as IMAGE/BINARY web UI might try to display it
768 # If it's left as IMAGE/BINARY web UI might try to display it
769 pchange.fileType = DiffFileType.TEXT
769 pchange.fileType = DiffFileType.TEXT
770 pchange.copynewmetadatatoold()
770 pchange.copynewmetadatatoold()
771
771
772
772
773 def makebinary(pchange, fctx):
773 def makebinary(pchange, fctx):
774 """populate the phabchange for a binary file"""
774 """populate the phabchange for a binary file"""
775 pchange.fileType = DiffFileType.BINARY
775 pchange.fileType = DiffFileType.BINARY
776 fphid = uploadfile(fctx)
776 fphid = uploadfile(fctx)
777 pchange.metadata[b'new:binary-phid'] = fphid
777 pchange.metadata[b'new:binary-phid'] = fphid
778 pchange.metadata[b'new:file:size'] = fctx.size()
778 pchange.metadata[b'new:file:size'] = fctx.size()
779 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
779 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
780 if mimeguess:
780 if mimeguess:
781 mimeguess = pycompat.bytestr(mimeguess)
781 mimeguess = pycompat.bytestr(mimeguess)
782 pchange.metadata[b'new:file:mime-type'] = mimeguess
782 pchange.metadata[b'new:file:mime-type'] = mimeguess
783 if mimeguess.startswith(b'image/'):
783 if mimeguess.startswith(b'image/'):
784 pchange.fileType = DiffFileType.IMAGE
784 pchange.fileType = DiffFileType.IMAGE
785
785
786
786
787 # Copied from mercurial/patch.py
787 # Copied from mercurial/patch.py
788 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
788 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
789
789
790
790
791 def notutf8(fctx):
791 def notutf8(fctx):
792 """detect non-UTF-8 text files since Phabricator requires them to be marked
792 """detect non-UTF-8 text files since Phabricator requires them to be marked
793 as binary
793 as binary
794 """
794 """
795 try:
795 try:
796 fctx.data().decode('utf-8')
796 fctx.data().decode('utf-8')
797 if fctx.parents():
797 if fctx.parents():
798 fctx.p1().data().decode('utf-8')
798 fctx.p1().data().decode('utf-8')
799 return False
799 return False
800 except UnicodeDecodeError:
800 except UnicodeDecodeError:
801 fctx.repo().ui.write(
801 fctx.repo().ui.write(
802 _(b'file %s detected as non-UTF-8, marked as binary\n')
802 _(b'file %s detected as non-UTF-8, marked as binary\n')
803 % fctx.path()
803 % fctx.path()
804 )
804 )
805 return True
805 return True
806
806
807
807
808 def addremoved(pdiff, ctx, removed):
808 def addremoved(pdiff, ctx, removed):
809 """add removed files to the phabdiff. Shouldn't include moves"""
809 """add removed files to the phabdiff. Shouldn't include moves"""
810 for fname in removed:
810 for fname in removed:
811 pchange = phabchange(
811 pchange = phabchange(
812 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
812 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
813 )
813 )
814 pchange.addoldmode(gitmode[ctx.p1()[fname].flags()])
814 pchange.addoldmode(gitmode[ctx.p1()[fname].flags()])
815 fctx = ctx.p1()[fname]
815 fctx = ctx.p1()[fname]
816 if not (fctx.isbinary() or notutf8(fctx)):
816 if not (fctx.isbinary() or notutf8(fctx)):
817 maketext(pchange, ctx, fname)
817 maketext(pchange, ctx, fname)
818
818
819 pdiff.addchange(pchange)
819 pdiff.addchange(pchange)
820
820
821
821
822 def addmodified(pdiff, ctx, modified):
822 def addmodified(pdiff, ctx, modified):
823 """add modified files to the phabdiff"""
823 """add modified files to the phabdiff"""
824 for fname in modified:
824 for fname in modified:
825 fctx = ctx[fname]
825 fctx = ctx[fname]
826 pchange = phabchange(currentPath=fname, oldPath=fname)
826 pchange = phabchange(currentPath=fname, oldPath=fname)
827 filemode = gitmode[ctx[fname].flags()]
827 filemode = gitmode[ctx[fname].flags()]
828 originalmode = gitmode[ctx.p1()[fname].flags()]
828 originalmode = gitmode[ctx.p1()[fname].flags()]
829 if filemode != originalmode:
829 if filemode != originalmode:
830 pchange.addoldmode(originalmode)
830 pchange.addoldmode(originalmode)
831 pchange.addnewmode(filemode)
831 pchange.addnewmode(filemode)
832
832
833 if fctx.isbinary() or notutf8(fctx):
833 if fctx.isbinary() or notutf8(fctx):
834 makebinary(pchange, fctx)
834 makebinary(pchange, fctx)
835 addoldbinary(pchange, fctx, fname)
835 addoldbinary(pchange, fctx)
836 else:
836 else:
837 maketext(pchange, ctx, fname)
837 maketext(pchange, ctx, fname)
838
838
839 pdiff.addchange(pchange)
839 pdiff.addchange(pchange)
840
840
841
841
842 def addadded(pdiff, ctx, added, removed):
842 def addadded(pdiff, ctx, added, removed):
843 """add file adds to the phabdiff, both new files and copies/moves"""
843 """add file adds to the phabdiff, both new files and copies/moves"""
844 # Keep track of files that've been recorded as moved/copied, so if there are
844 # Keep track of files that've been recorded as moved/copied, so if there are
845 # additional copies we can mark them (moves get removed from removed)
845 # additional copies we can mark them (moves get removed from removed)
846 copiedchanges = {}
846 copiedchanges = {}
847 movedchanges = {}
847 movedchanges = {}
848 for fname in added:
848 for fname in added:
849 fctx = ctx[fname]
849 fctx = ctx[fname]
850 pchange = phabchange(currentPath=fname)
850 pchange = phabchange(currentPath=fname)
851
851
852 filemode = gitmode[ctx[fname].flags()]
852 filemode = gitmode[ctx[fname].flags()]
853 renamed = fctx.renamed()
853 renamed = fctx.renamed()
854
854
855 if renamed:
855 if renamed:
856 originalfname = renamed[0]
856 originalfname = renamed[0]
857 originalmode = gitmode[ctx.p1()[originalfname].flags()]
857 originalmode = gitmode[ctx.p1()[originalfname].flags()]
858 pchange.oldPath = originalfname
858 pchange.oldPath = originalfname
859
859
860 if originalfname in removed:
860 if originalfname in removed:
861 origpchange = phabchange(
861 origpchange = phabchange(
862 currentPath=originalfname,
862 currentPath=originalfname,
863 oldPath=originalfname,
863 oldPath=originalfname,
864 type=DiffChangeType.MOVE_AWAY,
864 type=DiffChangeType.MOVE_AWAY,
865 awayPaths=[fname],
865 awayPaths=[fname],
866 )
866 )
867 movedchanges[originalfname] = origpchange
867 movedchanges[originalfname] = origpchange
868 removed.remove(originalfname)
868 removed.remove(originalfname)
869 pchange.type = DiffChangeType.MOVE_HERE
869 pchange.type = DiffChangeType.MOVE_HERE
870 elif originalfname in movedchanges:
870 elif originalfname in movedchanges:
871 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
871 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
872 movedchanges[originalfname].awayPaths.append(fname)
872 movedchanges[originalfname].awayPaths.append(fname)
873 pchange.type = DiffChangeType.COPY_HERE
873 pchange.type = DiffChangeType.COPY_HERE
874 else: # pure copy
874 else: # pure copy
875 if originalfname not in copiedchanges:
875 if originalfname not in copiedchanges:
876 origpchange = phabchange(
876 origpchange = phabchange(
877 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
877 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
878 )
878 )
879 copiedchanges[originalfname] = origpchange
879 copiedchanges[originalfname] = origpchange
880 else:
880 else:
881 origpchange = copiedchanges[originalfname]
881 origpchange = copiedchanges[originalfname]
882 origpchange.awayPaths.append(fname)
882 origpchange.awayPaths.append(fname)
883 pchange.type = DiffChangeType.COPY_HERE
883 pchange.type = DiffChangeType.COPY_HERE
884
884
885 if filemode != originalmode:
885 if filemode != originalmode:
886 pchange.addoldmode(originalmode)
886 pchange.addoldmode(originalmode)
887 pchange.addnewmode(filemode)
887 pchange.addnewmode(filemode)
888 else: # Brand-new file
888 else: # Brand-new file
889 pchange.addnewmode(gitmode[fctx.flags()])
889 pchange.addnewmode(gitmode[fctx.flags()])
890 pchange.type = DiffChangeType.ADD
890 pchange.type = DiffChangeType.ADD
891
891
892 if fctx.isbinary() or notutf8(fctx):
892 if fctx.isbinary() or notutf8(fctx):
893 makebinary(pchange, fctx)
893 makebinary(pchange, fctx)
894 if renamed:
894 if renamed:
895 addoldbinary(pchange, fctx, originalfname)
895 addoldbinary(pchange, fctx, originalfname)
896 else:
896 else:
897 maketext(pchange, ctx, fname)
897 maketext(pchange, ctx, fname)
898
898
899 pdiff.addchange(pchange)
899 pdiff.addchange(pchange)
900
900
901 for _path, copiedchange in copiedchanges.items():
901 for _path, copiedchange in copiedchanges.items():
902 pdiff.addchange(copiedchange)
902 pdiff.addchange(copiedchange)
903 for _path, movedchange in movedchanges.items():
903 for _path, movedchange in movedchanges.items():
904 pdiff.addchange(movedchange)
904 pdiff.addchange(movedchange)
905
905
906
906
907 def creatediff(ctx):
907 def creatediff(ctx):
908 """create a Differential Diff"""
908 """create a Differential Diff"""
909 repo = ctx.repo()
909 repo = ctx.repo()
910 repophid = getrepophid(repo)
910 repophid = getrepophid(repo)
911 # Create a "Differential Diff" via "differential.creatediff" API
911 # Create a "Differential Diff" via "differential.creatediff" API
912 pdiff = phabdiff(
912 pdiff = phabdiff(
913 sourceControlBaseRevision=b'%s' % ctx.p1().hex(),
913 sourceControlBaseRevision=b'%s' % ctx.p1().hex(),
914 branch=b'%s' % ctx.branch(),
914 branch=b'%s' % ctx.branch(),
915 )
915 )
916 modified, added, removed, _d, _u, _i, _c = ctx.p1().status(ctx)
916 modified, added, removed, _d, _u, _i, _c = ctx.p1().status(ctx)
917 # addadded will remove moved files from removed, so addremoved won't get
917 # addadded will remove moved files from removed, so addremoved won't get
918 # them
918 # them
919 addadded(pdiff, ctx, added, removed)
919 addadded(pdiff, ctx, added, removed)
920 addmodified(pdiff, ctx, modified)
920 addmodified(pdiff, ctx, modified)
921 addremoved(pdiff, ctx, removed)
921 addremoved(pdiff, ctx, removed)
922 if repophid:
922 if repophid:
923 pdiff.repositoryPHID = repophid
923 pdiff.repositoryPHID = repophid
924 diff = callconduit(
924 diff = callconduit(
925 repo.ui,
925 repo.ui,
926 b'differential.creatediff',
926 b'differential.creatediff',
927 pycompat.byteskwargs(attr.asdict(pdiff)),
927 pycompat.byteskwargs(attr.asdict(pdiff)),
928 )
928 )
929 if not diff:
929 if not diff:
930 raise error.Abort(_(b'cannot create diff for %s') % ctx)
930 raise error.Abort(_(b'cannot create diff for %s') % ctx)
931 return diff
931 return diff
932
932
933
933
934 def writediffproperties(ctx, diff):
934 def writediffproperties(ctx, diff):
935 """write metadata to diff so patches could be applied losslessly"""
935 """write metadata to diff so patches could be applied losslessly"""
936 # creatediff returns with a diffid but query returns with an id
936 # creatediff returns with a diffid but query returns with an id
937 diffid = diff.get(b'diffid', diff.get(b'id'))
937 diffid = diff.get(b'diffid', diff.get(b'id'))
938 params = {
938 params = {
939 b'diff_id': diffid,
939 b'diff_id': diffid,
940 b'name': b'hg:meta',
940 b'name': b'hg:meta',
941 b'data': templatefilters.json(
941 b'data': templatefilters.json(
942 {
942 {
943 b'user': ctx.user(),
943 b'user': ctx.user(),
944 b'date': b'%d %d' % ctx.date(),
944 b'date': b'%d %d' % ctx.date(),
945 b'branch': ctx.branch(),
945 b'branch': ctx.branch(),
946 b'node': ctx.hex(),
946 b'node': ctx.hex(),
947 b'parent': ctx.p1().hex(),
947 b'parent': ctx.p1().hex(),
948 }
948 }
949 ),
949 ),
950 }
950 }
951 callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)
951 callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)
952
952
953 params = {
953 params = {
954 b'diff_id': diffid,
954 b'diff_id': diffid,
955 b'name': b'local:commits',
955 b'name': b'local:commits',
956 b'data': templatefilters.json(
956 b'data': templatefilters.json(
957 {
957 {
958 ctx.hex(): {
958 ctx.hex(): {
959 b'author': stringutil.person(ctx.user()),
959 b'author': stringutil.person(ctx.user()),
960 b'authorEmail': stringutil.email(ctx.user()),
960 b'authorEmail': stringutil.email(ctx.user()),
961 b'time': int(ctx.date()[0]),
961 b'time': int(ctx.date()[0]),
962 b'commit': ctx.hex(),
962 b'commit': ctx.hex(),
963 b'parents': [ctx.p1().hex()],
963 b'parents': [ctx.p1().hex()],
964 b'branch': ctx.branch(),
964 b'branch': ctx.branch(),
965 },
965 },
966 }
966 }
967 ),
967 ),
968 }
968 }
969 callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)
969 callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)
970
970
971
971
972 def createdifferentialrevision(
972 def createdifferentialrevision(
973 ctx,
973 ctx,
974 revid=None,
974 revid=None,
975 parentrevphid=None,
975 parentrevphid=None,
976 oldnode=None,
976 oldnode=None,
977 olddiff=None,
977 olddiff=None,
978 actions=None,
978 actions=None,
979 comment=None,
979 comment=None,
980 ):
980 ):
981 """create or update a Differential Revision
981 """create or update a Differential Revision
982
982
983 If revid is None, create a new Differential Revision, otherwise update
983 If revid is None, create a new Differential Revision, otherwise update
984 revid. If parentrevphid is not None, set it as a dependency.
984 revid. If parentrevphid is not None, set it as a dependency.
985
985
986 If oldnode is not None, check if the patch content (without commit message
986 If oldnode is not None, check if the patch content (without commit message
987 and metadata) has changed before creating another diff.
987 and metadata) has changed before creating another diff.
988
988
989 If actions is not None, they will be appended to the transaction.
989 If actions is not None, they will be appended to the transaction.
990 """
990 """
991 repo = ctx.repo()
991 repo = ctx.repo()
992 if oldnode:
992 if oldnode:
993 diffopts = mdiff.diffopts(git=True, context=32767)
993 diffopts = mdiff.diffopts(git=True, context=32767)
994 oldctx = repo.unfiltered()[oldnode]
994 oldctx = repo.unfiltered()[oldnode]
995 neednewdiff = getdiff(ctx, diffopts) != getdiff(oldctx, diffopts)
995 neednewdiff = getdiff(ctx, diffopts) != getdiff(oldctx, diffopts)
996 else:
996 else:
997 neednewdiff = True
997 neednewdiff = True
998
998
999 transactions = []
999 transactions = []
1000 if neednewdiff:
1000 if neednewdiff:
1001 diff = creatediff(ctx)
1001 diff = creatediff(ctx)
1002 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1002 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1003 if comment:
1003 if comment:
1004 transactions.append({b'type': b'comment', b'value': comment})
1004 transactions.append({b'type': b'comment', b'value': comment})
1005 else:
1005 else:
1006 # Even if we don't need to upload a new diff because the patch content
1006 # Even if we don't need to upload a new diff because the patch content
1007 # does not change. We might still need to update its metadata so
1007 # does not change. We might still need to update its metadata so
1008 # pushers could know the correct node metadata.
1008 # pushers could know the correct node metadata.
1009 assert olddiff
1009 assert olddiff
1010 diff = olddiff
1010 diff = olddiff
1011 writediffproperties(ctx, diff)
1011 writediffproperties(ctx, diff)
1012
1012
1013 # Set the parent Revision every time, so commit re-ordering is picked-up
1013 # Set the parent Revision every time, so commit re-ordering is picked-up
1014 if parentrevphid:
1014 if parentrevphid:
1015 transactions.append(
1015 transactions.append(
1016 {b'type': b'parents.set', b'value': [parentrevphid]}
1016 {b'type': b'parents.set', b'value': [parentrevphid]}
1017 )
1017 )
1018
1018
1019 if actions:
1019 if actions:
1020 transactions += actions
1020 transactions += actions
1021
1021
1022 # Parse commit message and update related fields.
1022 # Parse commit message and update related fields.
1023 desc = ctx.description()
1023 desc = ctx.description()
1024 info = callconduit(
1024 info = callconduit(
1025 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1025 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1026 )
1026 )
1027 for k, v in info[b'fields'].items():
1027 for k, v in info[b'fields'].items():
1028 if k in [b'title', b'summary', b'testPlan']:
1028 if k in [b'title', b'summary', b'testPlan']:
1029 transactions.append({b'type': k, b'value': v})
1029 transactions.append({b'type': k, b'value': v})
1030
1030
1031 params = {b'transactions': transactions}
1031 params = {b'transactions': transactions}
1032 if revid is not None:
1032 if revid is not None:
1033 # Update an existing Differential Revision
1033 # Update an existing Differential Revision
1034 params[b'objectIdentifier'] = revid
1034 params[b'objectIdentifier'] = revid
1035
1035
1036 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1036 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1037 if not revision:
1037 if not revision:
1038 raise error.Abort(_(b'cannot create revision for %s') % ctx)
1038 raise error.Abort(_(b'cannot create revision for %s') % ctx)
1039
1039
1040 return revision, diff
1040 return revision, diff
1041
1041
1042
1042
1043 def userphids(repo, names):
1043 def userphids(repo, names):
1044 """convert user names to PHIDs"""
1044 """convert user names to PHIDs"""
1045 names = [name.lower() for name in names]
1045 names = [name.lower() for name in names]
1046 query = {b'constraints': {b'usernames': names}}
1046 query = {b'constraints': {b'usernames': names}}
1047 result = callconduit(repo.ui, b'user.search', query)
1047 result = callconduit(repo.ui, b'user.search', query)
1048 # username not found is not an error of the API. So check if we have missed
1048 # username not found is not an error of the API. So check if we have missed
1049 # some names here.
1049 # some names here.
1050 data = result[b'data']
1050 data = result[b'data']
1051 resolved = set(entry[b'fields'][b'username'].lower() for entry in data)
1051 resolved = set(entry[b'fields'][b'username'].lower() for entry in data)
1052 unresolved = set(names) - resolved
1052 unresolved = set(names) - resolved
1053 if unresolved:
1053 if unresolved:
1054 raise error.Abort(
1054 raise error.Abort(
1055 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1055 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1056 )
1056 )
1057 return [entry[b'phid'] for entry in data]
1057 return [entry[b'phid'] for entry in data]
1058
1058
1059
1059
1060 @vcrcommand(
1060 @vcrcommand(
1061 b'phabsend',
1061 b'phabsend',
1062 [
1062 [
1063 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1063 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1064 (b'', b'amend', True, _(b'update commit messages')),
1064 (b'', b'amend', True, _(b'update commit messages')),
1065 (b'', b'reviewer', [], _(b'specify reviewers')),
1065 (b'', b'reviewer', [], _(b'specify reviewers')),
1066 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1066 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1067 (
1067 (
1068 b'm',
1068 b'm',
1069 b'comment',
1069 b'comment',
1070 b'',
1070 b'',
1071 _(b'add a comment to Revisions with new/updated Diffs'),
1071 _(b'add a comment to Revisions with new/updated Diffs'),
1072 ),
1072 ),
1073 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1073 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1074 ],
1074 ],
1075 _(b'REV [OPTIONS]'),
1075 _(b'REV [OPTIONS]'),
1076 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1076 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1077 )
1077 )
1078 def phabsend(ui, repo, *revs, **opts):
1078 def phabsend(ui, repo, *revs, **opts):
1079 """upload changesets to Phabricator
1079 """upload changesets to Phabricator
1080
1080
1081 If there are multiple revisions specified, they will be send as a stack
1081 If there are multiple revisions specified, they will be send as a stack
1082 with a linear dependencies relationship using the order specified by the
1082 with a linear dependencies relationship using the order specified by the
1083 revset.
1083 revset.
1084
1084
1085 For the first time uploading changesets, local tags will be created to
1085 For the first time uploading changesets, local tags will be created to
1086 maintain the association. After the first time, phabsend will check
1086 maintain the association. After the first time, phabsend will check
1087 obsstore and tags information so it can figure out whether to update an
1087 obsstore and tags information so it can figure out whether to update an
1088 existing Differential Revision, or create a new one.
1088 existing Differential Revision, or create a new one.
1089
1089
1090 If --amend is set, update commit messages so they have the
1090 If --amend is set, update commit messages so they have the
1091 ``Differential Revision`` URL, remove related tags. This is similar to what
1091 ``Differential Revision`` URL, remove related tags. This is similar to what
1092 arcanist will do, and is more desired in author-push workflows. Otherwise,
1092 arcanist will do, and is more desired in author-push workflows. Otherwise,
1093 use local tags to record the ``Differential Revision`` association.
1093 use local tags to record the ``Differential Revision`` association.
1094
1094
1095 The --confirm option lets you confirm changesets before sending them. You
1095 The --confirm option lets you confirm changesets before sending them. You
1096 can also add following to your configuration file to make it default
1096 can also add following to your configuration file to make it default
1097 behaviour::
1097 behaviour::
1098
1098
1099 [phabsend]
1099 [phabsend]
1100 confirm = true
1100 confirm = true
1101
1101
1102 phabsend will check obsstore and the above association to decide whether to
1102 phabsend will check obsstore and the above association to decide whether to
1103 update an existing Differential Revision, or create a new one.
1103 update an existing Differential Revision, or create a new one.
1104 """
1104 """
1105 opts = pycompat.byteskwargs(opts)
1105 opts = pycompat.byteskwargs(opts)
1106 revs = list(revs) + opts.get(b'rev', [])
1106 revs = list(revs) + opts.get(b'rev', [])
1107 revs = scmutil.revrange(repo, revs)
1107 revs = scmutil.revrange(repo, revs)
1108 revs.sort() # ascending order to preserve topological parent/child in phab
1108 revs.sort() # ascending order to preserve topological parent/child in phab
1109
1109
1110 if not revs:
1110 if not revs:
1111 raise error.Abort(_(b'phabsend requires at least one changeset'))
1111 raise error.Abort(_(b'phabsend requires at least one changeset'))
1112 if opts.get(b'amend'):
1112 if opts.get(b'amend'):
1113 cmdutil.checkunfinished(repo)
1113 cmdutil.checkunfinished(repo)
1114
1114
1115 # {newnode: (oldnode, olddiff, olddrev}
1115 # {newnode: (oldnode, olddiff, olddrev}
1116 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1116 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1117
1117
1118 confirm = ui.configbool(b'phabsend', b'confirm')
1118 confirm = ui.configbool(b'phabsend', b'confirm')
1119 confirm |= bool(opts.get(b'confirm'))
1119 confirm |= bool(opts.get(b'confirm'))
1120 if confirm:
1120 if confirm:
1121 confirmed = _confirmbeforesend(repo, revs, oldmap)
1121 confirmed = _confirmbeforesend(repo, revs, oldmap)
1122 if not confirmed:
1122 if not confirmed:
1123 raise error.Abort(_(b'phabsend cancelled'))
1123 raise error.Abort(_(b'phabsend cancelled'))
1124
1124
1125 actions = []
1125 actions = []
1126 reviewers = opts.get(b'reviewer', [])
1126 reviewers = opts.get(b'reviewer', [])
1127 blockers = opts.get(b'blocker', [])
1127 blockers = opts.get(b'blocker', [])
1128 phids = []
1128 phids = []
1129 if reviewers:
1129 if reviewers:
1130 phids.extend(userphids(repo, reviewers))
1130 phids.extend(userphids(repo, reviewers))
1131 if blockers:
1131 if blockers:
1132 phids.extend(
1132 phids.extend(
1133 map(lambda phid: b'blocking(%s)' % phid, userphids(repo, blockers))
1133 map(lambda phid: b'blocking(%s)' % phid, userphids(repo, blockers))
1134 )
1134 )
1135 if phids:
1135 if phids:
1136 actions.append({b'type': b'reviewers.add', b'value': phids})
1136 actions.append({b'type': b'reviewers.add', b'value': phids})
1137
1137
1138 drevids = [] # [int]
1138 drevids = [] # [int]
1139 diffmap = {} # {newnode: diff}
1139 diffmap = {} # {newnode: diff}
1140
1140
1141 # Send patches one by one so we know their Differential Revision PHIDs and
1141 # Send patches one by one so we know their Differential Revision PHIDs and
1142 # can provide dependency relationship
1142 # can provide dependency relationship
1143 lastrevphid = None
1143 lastrevphid = None
1144 for rev in revs:
1144 for rev in revs:
1145 ui.debug(b'sending rev %d\n' % rev)
1145 ui.debug(b'sending rev %d\n' % rev)
1146 ctx = repo[rev]
1146 ctx = repo[rev]
1147
1147
1148 # Get Differential Revision ID
1148 # Get Differential Revision ID
1149 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1149 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1150 if oldnode != ctx.node() or opts.get(b'amend'):
1150 if oldnode != ctx.node() or opts.get(b'amend'):
1151 # Create or update Differential Revision
1151 # Create or update Differential Revision
1152 revision, diff = createdifferentialrevision(
1152 revision, diff = createdifferentialrevision(
1153 ctx,
1153 ctx,
1154 revid,
1154 revid,
1155 lastrevphid,
1155 lastrevphid,
1156 oldnode,
1156 oldnode,
1157 olddiff,
1157 olddiff,
1158 actions,
1158 actions,
1159 opts.get(b'comment'),
1159 opts.get(b'comment'),
1160 )
1160 )
1161 diffmap[ctx.node()] = diff
1161 diffmap[ctx.node()] = diff
1162 newrevid = int(revision[b'object'][b'id'])
1162 newrevid = int(revision[b'object'][b'id'])
1163 newrevphid = revision[b'object'][b'phid']
1163 newrevphid = revision[b'object'][b'phid']
1164 if revid:
1164 if revid:
1165 action = b'updated'
1165 action = b'updated'
1166 else:
1166 else:
1167 action = b'created'
1167 action = b'created'
1168
1168
1169 # Create a local tag to note the association, if commit message
1169 # Create a local tag to note the association, if commit message
1170 # does not have it already
1170 # does not have it already
1171 m = _differentialrevisiondescre.search(ctx.description())
1171 m = _differentialrevisiondescre.search(ctx.description())
1172 if not m or int(m.group('id')) != newrevid:
1172 if not m or int(m.group('id')) != newrevid:
1173 tagname = b'D%d' % newrevid
1173 tagname = b'D%d' % newrevid
1174 tags.tag(
1174 tags.tag(
1175 repo,
1175 repo,
1176 tagname,
1176 tagname,
1177 ctx.node(),
1177 ctx.node(),
1178 message=None,
1178 message=None,
1179 user=None,
1179 user=None,
1180 date=None,
1180 date=None,
1181 local=True,
1181 local=True,
1182 )
1182 )
1183 else:
1183 else:
1184 # Nothing changed. But still set "newrevphid" so the next revision
1184 # Nothing changed. But still set "newrevphid" so the next revision
1185 # could depend on this one and "newrevid" for the summary line.
1185 # could depend on this one and "newrevid" for the summary line.
1186 newrevphid = querydrev(repo, b'%d' % revid)[0][b'phid']
1186 newrevphid = querydrev(repo, b'%d' % revid)[0][b'phid']
1187 newrevid = revid
1187 newrevid = revid
1188 action = b'skipped'
1188 action = b'skipped'
1189
1189
1190 actiondesc = ui.label(
1190 actiondesc = ui.label(
1191 {
1191 {
1192 b'created': _(b'created'),
1192 b'created': _(b'created'),
1193 b'skipped': _(b'skipped'),
1193 b'skipped': _(b'skipped'),
1194 b'updated': _(b'updated'),
1194 b'updated': _(b'updated'),
1195 }[action],
1195 }[action],
1196 b'phabricator.action.%s' % action,
1196 b'phabricator.action.%s' % action,
1197 )
1197 )
1198 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1198 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1199 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
1199 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
1200 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
1200 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
1201 ui.write(
1201 ui.write(
1202 _(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc)
1202 _(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc)
1203 )
1203 )
1204 drevids.append(newrevid)
1204 drevids.append(newrevid)
1205 lastrevphid = newrevphid
1205 lastrevphid = newrevphid
1206
1206
1207 # Update commit messages and remove tags
1207 # Update commit messages and remove tags
1208 if opts.get(b'amend'):
1208 if opts.get(b'amend'):
1209 unfi = repo.unfiltered()
1209 unfi = repo.unfiltered()
1210 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1210 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1211 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1211 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1212 wnode = unfi[b'.'].node()
1212 wnode = unfi[b'.'].node()
1213 mapping = {} # {oldnode: [newnode]}
1213 mapping = {} # {oldnode: [newnode]}
1214 for i, rev in enumerate(revs):
1214 for i, rev in enumerate(revs):
1215 old = unfi[rev]
1215 old = unfi[rev]
1216 drevid = drevids[i]
1216 drevid = drevids[i]
1217 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1217 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1218 newdesc = getdescfromdrev(drev)
1218 newdesc = getdescfromdrev(drev)
1219 # Make sure commit message contain "Differential Revision"
1219 # Make sure commit message contain "Differential Revision"
1220 if old.description() != newdesc:
1220 if old.description() != newdesc:
1221 if old.phase() == phases.public:
1221 if old.phase() == phases.public:
1222 ui.warn(
1222 ui.warn(
1223 _(b"warning: not updating public commit %s\n")
1223 _(b"warning: not updating public commit %s\n")
1224 % scmutil.formatchangeid(old)
1224 % scmutil.formatchangeid(old)
1225 )
1225 )
1226 continue
1226 continue
1227 parents = [
1227 parents = [
1228 mapping.get(old.p1().node(), (old.p1(),))[0],
1228 mapping.get(old.p1().node(), (old.p1(),))[0],
1229 mapping.get(old.p2().node(), (old.p2(),))[0],
1229 mapping.get(old.p2().node(), (old.p2(),))[0],
1230 ]
1230 ]
1231 new = context.metadataonlyctx(
1231 new = context.metadataonlyctx(
1232 repo,
1232 repo,
1233 old,
1233 old,
1234 parents=parents,
1234 parents=parents,
1235 text=newdesc,
1235 text=newdesc,
1236 user=old.user(),
1236 user=old.user(),
1237 date=old.date(),
1237 date=old.date(),
1238 extra=old.extra(),
1238 extra=old.extra(),
1239 )
1239 )
1240
1240
1241 newnode = new.commit()
1241 newnode = new.commit()
1242
1242
1243 mapping[old.node()] = [newnode]
1243 mapping[old.node()] = [newnode]
1244 # Update diff property
1244 # Update diff property
1245 # If it fails just warn and keep going, otherwise the DREV
1245 # If it fails just warn and keep going, otherwise the DREV
1246 # associations will be lost
1246 # associations will be lost
1247 try:
1247 try:
1248 writediffproperties(unfi[newnode], diffmap[old.node()])
1248 writediffproperties(unfi[newnode], diffmap[old.node()])
1249 except util.urlerr.urlerror:
1249 except util.urlerr.urlerror:
1250 ui.warnnoi18n(
1250 ui.warnnoi18n(
1251 b'Failed to update metadata for D%d\n' % drevid
1251 b'Failed to update metadata for D%d\n' % drevid
1252 )
1252 )
1253 # Remove local tags since it's no longer necessary
1253 # Remove local tags since it's no longer necessary
1254 tagname = b'D%d' % drevid
1254 tagname = b'D%d' % drevid
1255 if tagname in repo.tags():
1255 if tagname in repo.tags():
1256 tags.tag(
1256 tags.tag(
1257 repo,
1257 repo,
1258 tagname,
1258 tagname,
1259 nullid,
1259 nullid,
1260 message=None,
1260 message=None,
1261 user=None,
1261 user=None,
1262 date=None,
1262 date=None,
1263 local=True,
1263 local=True,
1264 )
1264 )
1265 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1265 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1266 if wnode in mapping:
1266 if wnode in mapping:
1267 unfi.setparents(mapping[wnode][0])
1267 unfi.setparents(mapping[wnode][0])
1268
1268
1269
1269
1270 # Map from "hg:meta" keys to header understood by "hg import". The order is
1270 # Map from "hg:meta" keys to header understood by "hg import". The order is
1271 # consistent with "hg export" output.
1271 # consistent with "hg export" output.
1272 _metanamemap = util.sortdict(
1272 _metanamemap = util.sortdict(
1273 [
1273 [
1274 (b'user', b'User'),
1274 (b'user', b'User'),
1275 (b'date', b'Date'),
1275 (b'date', b'Date'),
1276 (b'branch', b'Branch'),
1276 (b'branch', b'Branch'),
1277 (b'node', b'Node ID'),
1277 (b'node', b'Node ID'),
1278 (b'parent', b'Parent '),
1278 (b'parent', b'Parent '),
1279 ]
1279 ]
1280 )
1280 )
1281
1281
1282
1282
1283 def _confirmbeforesend(repo, revs, oldmap):
1283 def _confirmbeforesend(repo, revs, oldmap):
1284 url, token = readurltoken(repo.ui)
1284 url, token = readurltoken(repo.ui)
1285 ui = repo.ui
1285 ui = repo.ui
1286 for rev in revs:
1286 for rev in revs:
1287 ctx = repo[rev]
1287 ctx = repo[rev]
1288 desc = ctx.description().splitlines()[0]
1288 desc = ctx.description().splitlines()[0]
1289 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1289 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1290 if drevid:
1290 if drevid:
1291 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1291 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1292 else:
1292 else:
1293 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1293 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1294
1294
1295 ui.write(
1295 ui.write(
1296 _(b'%s - %s: %s\n')
1296 _(b'%s - %s: %s\n')
1297 % (
1297 % (
1298 drevdesc,
1298 drevdesc,
1299 ui.label(bytes(ctx), b'phabricator.node'),
1299 ui.label(bytes(ctx), b'phabricator.node'),
1300 ui.label(desc, b'phabricator.desc'),
1300 ui.label(desc, b'phabricator.desc'),
1301 )
1301 )
1302 )
1302 )
1303
1303
1304 if ui.promptchoice(
1304 if ui.promptchoice(
1305 _(b'Send the above changes to %s (yn)?$$ &Yes $$ &No') % url
1305 _(b'Send the above changes to %s (yn)?$$ &Yes $$ &No') % url
1306 ):
1306 ):
1307 return False
1307 return False
1308
1308
1309 return True
1309 return True
1310
1310
1311
1311
1312 _knownstatusnames = {
1312 _knownstatusnames = {
1313 b'accepted',
1313 b'accepted',
1314 b'needsreview',
1314 b'needsreview',
1315 b'needsrevision',
1315 b'needsrevision',
1316 b'closed',
1316 b'closed',
1317 b'abandoned',
1317 b'abandoned',
1318 b'changesplanned',
1318 b'changesplanned',
1319 }
1319 }
1320
1320
1321
1321
1322 def _getstatusname(drev):
1322 def _getstatusname(drev):
1323 """get normalized status name from a Differential Revision"""
1323 """get normalized status name from a Differential Revision"""
1324 return drev[b'statusName'].replace(b' ', b'').lower()
1324 return drev[b'statusName'].replace(b' ', b'').lower()
1325
1325
1326
1326
1327 # Small language to specify differential revisions. Support symbols: (), :X,
1327 # Small language to specify differential revisions. Support symbols: (), :X,
1328 # +, and -.
1328 # +, and -.
1329
1329
1330 _elements = {
1330 _elements = {
1331 # token-type: binding-strength, primary, prefix, infix, suffix
1331 # token-type: binding-strength, primary, prefix, infix, suffix
1332 b'(': (12, None, (b'group', 1, b')'), None, None),
1332 b'(': (12, None, (b'group', 1, b')'), None, None),
1333 b':': (8, None, (b'ancestors', 8), None, None),
1333 b':': (8, None, (b'ancestors', 8), None, None),
1334 b'&': (5, None, None, (b'and_', 5), None),
1334 b'&': (5, None, None, (b'and_', 5), None),
1335 b'+': (4, None, None, (b'add', 4), None),
1335 b'+': (4, None, None, (b'add', 4), None),
1336 b'-': (4, None, None, (b'sub', 4), None),
1336 b'-': (4, None, None, (b'sub', 4), None),
1337 b')': (0, None, None, None, None),
1337 b')': (0, None, None, None, None),
1338 b'symbol': (0, b'symbol', None, None, None),
1338 b'symbol': (0, b'symbol', None, None, None),
1339 b'end': (0, None, None, None, None),
1339 b'end': (0, None, None, None, None),
1340 }
1340 }
1341
1341
1342
1342
1343 def _tokenize(text):
1343 def _tokenize(text):
1344 view = memoryview(text) # zero-copy slice
1344 view = memoryview(text) # zero-copy slice
1345 special = b'():+-& '
1345 special = b'():+-& '
1346 pos = 0
1346 pos = 0
1347 length = len(text)
1347 length = len(text)
1348 while pos < length:
1348 while pos < length:
1349 symbol = b''.join(
1349 symbol = b''.join(
1350 itertools.takewhile(
1350 itertools.takewhile(
1351 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1351 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1352 )
1352 )
1353 )
1353 )
1354 if symbol:
1354 if symbol:
1355 yield (b'symbol', symbol, pos)
1355 yield (b'symbol', symbol, pos)
1356 pos += len(symbol)
1356 pos += len(symbol)
1357 else: # special char, ignore space
1357 else: # special char, ignore space
1358 if text[pos : pos + 1] != b' ':
1358 if text[pos : pos + 1] != b' ':
1359 yield (text[pos : pos + 1], None, pos)
1359 yield (text[pos : pos + 1], None, pos)
1360 pos += 1
1360 pos += 1
1361 yield (b'end', None, pos)
1361 yield (b'end', None, pos)
1362
1362
1363
1363
1364 def _parse(text):
1364 def _parse(text):
1365 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1365 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1366 if pos != len(text):
1366 if pos != len(text):
1367 raise error.ParseError(b'invalid token', pos)
1367 raise error.ParseError(b'invalid token', pos)
1368 return tree
1368 return tree
1369
1369
1370
1370
1371 def _parsedrev(symbol):
1371 def _parsedrev(symbol):
1372 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1372 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1373 if symbol.startswith(b'D') and symbol[1:].isdigit():
1373 if symbol.startswith(b'D') and symbol[1:].isdigit():
1374 return int(symbol[1:])
1374 return int(symbol[1:])
1375 if symbol.isdigit():
1375 if symbol.isdigit():
1376 return int(symbol)
1376 return int(symbol)
1377
1377
1378
1378
1379 def _prefetchdrevs(tree):
1379 def _prefetchdrevs(tree):
1380 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1380 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1381 drevs = set()
1381 drevs = set()
1382 ancestordrevs = set()
1382 ancestordrevs = set()
1383 op = tree[0]
1383 op = tree[0]
1384 if op == b'symbol':
1384 if op == b'symbol':
1385 r = _parsedrev(tree[1])
1385 r = _parsedrev(tree[1])
1386 if r:
1386 if r:
1387 drevs.add(r)
1387 drevs.add(r)
1388 elif op == b'ancestors':
1388 elif op == b'ancestors':
1389 r, a = _prefetchdrevs(tree[1])
1389 r, a = _prefetchdrevs(tree[1])
1390 drevs.update(r)
1390 drevs.update(r)
1391 ancestordrevs.update(r)
1391 ancestordrevs.update(r)
1392 ancestordrevs.update(a)
1392 ancestordrevs.update(a)
1393 else:
1393 else:
1394 for t in tree[1:]:
1394 for t in tree[1:]:
1395 r, a = _prefetchdrevs(t)
1395 r, a = _prefetchdrevs(t)
1396 drevs.update(r)
1396 drevs.update(r)
1397 ancestordrevs.update(a)
1397 ancestordrevs.update(a)
1398 return drevs, ancestordrevs
1398 return drevs, ancestordrevs
1399
1399
1400
1400
1401 def querydrev(repo, spec):
1401 def querydrev(repo, spec):
1402 """return a list of "Differential Revision" dicts
1402 """return a list of "Differential Revision" dicts
1403
1403
1404 spec is a string using a simple query language, see docstring in phabread
1404 spec is a string using a simple query language, see docstring in phabread
1405 for details.
1405 for details.
1406
1406
1407 A "Differential Revision dict" looks like:
1407 A "Differential Revision dict" looks like:
1408
1408
1409 {
1409 {
1410 "id": "2",
1410 "id": "2",
1411 "phid": "PHID-DREV-672qvysjcczopag46qty",
1411 "phid": "PHID-DREV-672qvysjcczopag46qty",
1412 "title": "example",
1412 "title": "example",
1413 "uri": "https://phab.example.com/D2",
1413 "uri": "https://phab.example.com/D2",
1414 "dateCreated": "1499181406",
1414 "dateCreated": "1499181406",
1415 "dateModified": "1499182103",
1415 "dateModified": "1499182103",
1416 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1416 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1417 "status": "0",
1417 "status": "0",
1418 "statusName": "Needs Review",
1418 "statusName": "Needs Review",
1419 "properties": [],
1419 "properties": [],
1420 "branch": null,
1420 "branch": null,
1421 "summary": "",
1421 "summary": "",
1422 "testPlan": "",
1422 "testPlan": "",
1423 "lineCount": "2",
1423 "lineCount": "2",
1424 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1424 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1425 "diffs": [
1425 "diffs": [
1426 "3",
1426 "3",
1427 "4",
1427 "4",
1428 ],
1428 ],
1429 "commits": [],
1429 "commits": [],
1430 "reviewers": [],
1430 "reviewers": [],
1431 "ccs": [],
1431 "ccs": [],
1432 "hashes": [],
1432 "hashes": [],
1433 "auxiliary": {
1433 "auxiliary": {
1434 "phabricator:projects": [],
1434 "phabricator:projects": [],
1435 "phabricator:depends-on": [
1435 "phabricator:depends-on": [
1436 "PHID-DREV-gbapp366kutjebt7agcd"
1436 "PHID-DREV-gbapp366kutjebt7agcd"
1437 ]
1437 ]
1438 },
1438 },
1439 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1439 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1440 "sourcePath": null
1440 "sourcePath": null
1441 }
1441 }
1442 """
1442 """
1443
1443
1444 def fetch(params):
1444 def fetch(params):
1445 """params -> single drev or None"""
1445 """params -> single drev or None"""
1446 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1446 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1447 if key in prefetched:
1447 if key in prefetched:
1448 return prefetched[key]
1448 return prefetched[key]
1449 drevs = callconduit(repo.ui, b'differential.query', params)
1449 drevs = callconduit(repo.ui, b'differential.query', params)
1450 # Fill prefetched with the result
1450 # Fill prefetched with the result
1451 for drev in drevs:
1451 for drev in drevs:
1452 prefetched[drev[b'phid']] = drev
1452 prefetched[drev[b'phid']] = drev
1453 prefetched[int(drev[b'id'])] = drev
1453 prefetched[int(drev[b'id'])] = drev
1454 if key not in prefetched:
1454 if key not in prefetched:
1455 raise error.Abort(
1455 raise error.Abort(
1456 _(b'cannot get Differential Revision %r') % params
1456 _(b'cannot get Differential Revision %r') % params
1457 )
1457 )
1458 return prefetched[key]
1458 return prefetched[key]
1459
1459
1460 def getstack(topdrevids):
1460 def getstack(topdrevids):
1461 """given a top, get a stack from the bottom, [id] -> [id]"""
1461 """given a top, get a stack from the bottom, [id] -> [id]"""
1462 visited = set()
1462 visited = set()
1463 result = []
1463 result = []
1464 queue = [{b'ids': [i]} for i in topdrevids]
1464 queue = [{b'ids': [i]} for i in topdrevids]
1465 while queue:
1465 while queue:
1466 params = queue.pop()
1466 params = queue.pop()
1467 drev = fetch(params)
1467 drev = fetch(params)
1468 if drev[b'id'] in visited:
1468 if drev[b'id'] in visited:
1469 continue
1469 continue
1470 visited.add(drev[b'id'])
1470 visited.add(drev[b'id'])
1471 result.append(int(drev[b'id']))
1471 result.append(int(drev[b'id']))
1472 auxiliary = drev.get(b'auxiliary', {})
1472 auxiliary = drev.get(b'auxiliary', {})
1473 depends = auxiliary.get(b'phabricator:depends-on', [])
1473 depends = auxiliary.get(b'phabricator:depends-on', [])
1474 for phid in depends:
1474 for phid in depends:
1475 queue.append({b'phids': [phid]})
1475 queue.append({b'phids': [phid]})
1476 result.reverse()
1476 result.reverse()
1477 return smartset.baseset(result)
1477 return smartset.baseset(result)
1478
1478
1479 # Initialize prefetch cache
1479 # Initialize prefetch cache
1480 prefetched = {} # {id or phid: drev}
1480 prefetched = {} # {id or phid: drev}
1481
1481
1482 tree = _parse(spec)
1482 tree = _parse(spec)
1483 drevs, ancestordrevs = _prefetchdrevs(tree)
1483 drevs, ancestordrevs = _prefetchdrevs(tree)
1484
1484
1485 # developer config: phabricator.batchsize
1485 # developer config: phabricator.batchsize
1486 batchsize = repo.ui.configint(b'phabricator', b'batchsize')
1486 batchsize = repo.ui.configint(b'phabricator', b'batchsize')
1487
1487
1488 # Prefetch Differential Revisions in batch
1488 # Prefetch Differential Revisions in batch
1489 tofetch = set(drevs)
1489 tofetch = set(drevs)
1490 for r in ancestordrevs:
1490 for r in ancestordrevs:
1491 tofetch.update(range(max(1, r - batchsize), r + 1))
1491 tofetch.update(range(max(1, r - batchsize), r + 1))
1492 if drevs:
1492 if drevs:
1493 fetch({b'ids': list(tofetch)})
1493 fetch({b'ids': list(tofetch)})
1494 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1494 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1495
1495
1496 # Walk through the tree, return smartsets
1496 # Walk through the tree, return smartsets
1497 def walk(tree):
1497 def walk(tree):
1498 op = tree[0]
1498 op = tree[0]
1499 if op == b'symbol':
1499 if op == b'symbol':
1500 drev = _parsedrev(tree[1])
1500 drev = _parsedrev(tree[1])
1501 if drev:
1501 if drev:
1502 return smartset.baseset([drev])
1502 return smartset.baseset([drev])
1503 elif tree[1] in _knownstatusnames:
1503 elif tree[1] in _knownstatusnames:
1504 drevs = [
1504 drevs = [
1505 r
1505 r
1506 for r in validids
1506 for r in validids
1507 if _getstatusname(prefetched[r]) == tree[1]
1507 if _getstatusname(prefetched[r]) == tree[1]
1508 ]
1508 ]
1509 return smartset.baseset(drevs)
1509 return smartset.baseset(drevs)
1510 else:
1510 else:
1511 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1511 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1512 elif op in {b'and_', b'add', b'sub'}:
1512 elif op in {b'and_', b'add', b'sub'}:
1513 assert len(tree) == 3
1513 assert len(tree) == 3
1514 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1514 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1515 elif op == b'group':
1515 elif op == b'group':
1516 return walk(tree[1])
1516 return walk(tree[1])
1517 elif op == b'ancestors':
1517 elif op == b'ancestors':
1518 return getstack(walk(tree[1]))
1518 return getstack(walk(tree[1]))
1519 else:
1519 else:
1520 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1520 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1521
1521
1522 return [prefetched[r] for r in walk(tree)]
1522 return [prefetched[r] for r in walk(tree)]
1523
1523
1524
1524
1525 def getdescfromdrev(drev):
1525 def getdescfromdrev(drev):
1526 """get description (commit message) from "Differential Revision"
1526 """get description (commit message) from "Differential Revision"
1527
1527
1528 This is similar to differential.getcommitmessage API. But we only care
1528 This is similar to differential.getcommitmessage API. But we only care
1529 about limited fields: title, summary, test plan, and URL.
1529 about limited fields: title, summary, test plan, and URL.
1530 """
1530 """
1531 title = drev[b'title']
1531 title = drev[b'title']
1532 summary = drev[b'summary'].rstrip()
1532 summary = drev[b'summary'].rstrip()
1533 testplan = drev[b'testPlan'].rstrip()
1533 testplan = drev[b'testPlan'].rstrip()
1534 if testplan:
1534 if testplan:
1535 testplan = b'Test Plan:\n%s' % testplan
1535 testplan = b'Test Plan:\n%s' % testplan
1536 uri = b'Differential Revision: %s' % drev[b'uri']
1536 uri = b'Differential Revision: %s' % drev[b'uri']
1537 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1537 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1538
1538
1539
1539
1540 def getdiffmeta(diff):
1540 def getdiffmeta(diff):
1541 """get commit metadata (date, node, user, p1) from a diff object
1541 """get commit metadata (date, node, user, p1) from a diff object
1542
1542
1543 The metadata could be "hg:meta", sent by phabsend, like:
1543 The metadata could be "hg:meta", sent by phabsend, like:
1544
1544
1545 "properties": {
1545 "properties": {
1546 "hg:meta": {
1546 "hg:meta": {
1547 "date": "1499571514 25200",
1547 "date": "1499571514 25200",
1548 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
1548 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
1549 "user": "Foo Bar <foo@example.com>",
1549 "user": "Foo Bar <foo@example.com>",
1550 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
1550 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
1551 }
1551 }
1552 }
1552 }
1553
1553
1554 Or converted from "local:commits", sent by "arc", like:
1554 Or converted from "local:commits", sent by "arc", like:
1555
1555
1556 "properties": {
1556 "properties": {
1557 "local:commits": {
1557 "local:commits": {
1558 "98c08acae292b2faf60a279b4189beb6cff1414d": {
1558 "98c08acae292b2faf60a279b4189beb6cff1414d": {
1559 "author": "Foo Bar",
1559 "author": "Foo Bar",
1560 "time": 1499546314,
1560 "time": 1499546314,
1561 "branch": "default",
1561 "branch": "default",
1562 "tag": "",
1562 "tag": "",
1563 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
1563 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
1564 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
1564 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
1565 "local": "1000",
1565 "local": "1000",
1566 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
1566 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
1567 "summary": "...",
1567 "summary": "...",
1568 "message": "...",
1568 "message": "...",
1569 "authorEmail": "foo@example.com"
1569 "authorEmail": "foo@example.com"
1570 }
1570 }
1571 }
1571 }
1572 }
1572 }
1573
1573
1574 Note: metadata extracted from "local:commits" will lose time zone
1574 Note: metadata extracted from "local:commits" will lose time zone
1575 information.
1575 information.
1576 """
1576 """
1577 props = diff.get(b'properties') or {}
1577 props = diff.get(b'properties') or {}
1578 meta = props.get(b'hg:meta')
1578 meta = props.get(b'hg:meta')
1579 if not meta:
1579 if not meta:
1580 if props.get(b'local:commits'):
1580 if props.get(b'local:commits'):
1581 commit = sorted(props[b'local:commits'].values())[0]
1581 commit = sorted(props[b'local:commits'].values())[0]
1582 meta = {}
1582 meta = {}
1583 if b'author' in commit and b'authorEmail' in commit:
1583 if b'author' in commit and b'authorEmail' in commit:
1584 meta[b'user'] = b'%s <%s>' % (
1584 meta[b'user'] = b'%s <%s>' % (
1585 commit[b'author'],
1585 commit[b'author'],
1586 commit[b'authorEmail'],
1586 commit[b'authorEmail'],
1587 )
1587 )
1588 if b'time' in commit:
1588 if b'time' in commit:
1589 meta[b'date'] = b'%d 0' % int(commit[b'time'])
1589 meta[b'date'] = b'%d 0' % int(commit[b'time'])
1590 if b'branch' in commit:
1590 if b'branch' in commit:
1591 meta[b'branch'] = commit[b'branch']
1591 meta[b'branch'] = commit[b'branch']
1592 node = commit.get(b'commit', commit.get(b'rev'))
1592 node = commit.get(b'commit', commit.get(b'rev'))
1593 if node:
1593 if node:
1594 meta[b'node'] = node
1594 meta[b'node'] = node
1595 if len(commit.get(b'parents', ())) >= 1:
1595 if len(commit.get(b'parents', ())) >= 1:
1596 meta[b'parent'] = commit[b'parents'][0]
1596 meta[b'parent'] = commit[b'parents'][0]
1597 else:
1597 else:
1598 meta = {}
1598 meta = {}
1599 if b'date' not in meta and b'dateCreated' in diff:
1599 if b'date' not in meta and b'dateCreated' in diff:
1600 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
1600 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
1601 if b'branch' not in meta and diff.get(b'branch'):
1601 if b'branch' not in meta and diff.get(b'branch'):
1602 meta[b'branch'] = diff[b'branch']
1602 meta[b'branch'] = diff[b'branch']
1603 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
1603 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
1604 meta[b'parent'] = diff[b'sourceControlBaseRevision']
1604 meta[b'parent'] = diff[b'sourceControlBaseRevision']
1605 return meta
1605 return meta
1606
1606
1607
1607
1608 def readpatch(repo, drevs, write):
1608 def readpatch(repo, drevs, write):
1609 """generate plain-text patch readable by 'hg import'
1609 """generate plain-text patch readable by 'hg import'
1610
1610
1611 write is usually ui.write. drevs is what "querydrev" returns, results of
1611 write is usually ui.write. drevs is what "querydrev" returns, results of
1612 "differential.query".
1612 "differential.query".
1613 """
1613 """
1614 # Prefetch hg:meta property for all diffs
1614 # Prefetch hg:meta property for all diffs
1615 diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))
1615 diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))
1616 diffs = callconduit(repo.ui, b'differential.querydiffs', {b'ids': diffids})
1616 diffs = callconduit(repo.ui, b'differential.querydiffs', {b'ids': diffids})
1617
1617
1618 # Generate patch for each drev
1618 # Generate patch for each drev
1619 for drev in drevs:
1619 for drev in drevs:
1620 repo.ui.note(_(b'reading D%s\n') % drev[b'id'])
1620 repo.ui.note(_(b'reading D%s\n') % drev[b'id'])
1621
1621
1622 diffid = max(int(v) for v in drev[b'diffs'])
1622 diffid = max(int(v) for v in drev[b'diffs'])
1623 body = callconduit(
1623 body = callconduit(
1624 repo.ui, b'differential.getrawdiff', {b'diffID': diffid}
1624 repo.ui, b'differential.getrawdiff', {b'diffID': diffid}
1625 )
1625 )
1626 desc = getdescfromdrev(drev)
1626 desc = getdescfromdrev(drev)
1627 header = b'# HG changeset patch\n'
1627 header = b'# HG changeset patch\n'
1628
1628
1629 # Try to preserve metadata from hg:meta property. Write hg patch
1629 # Try to preserve metadata from hg:meta property. Write hg patch
1630 # headers that can be read by the "import" command. See patchheadermap
1630 # headers that can be read by the "import" command. See patchheadermap
1631 # and extract in mercurial/patch.py for supported headers.
1631 # and extract in mercurial/patch.py for supported headers.
1632 meta = getdiffmeta(diffs[b'%d' % diffid])
1632 meta = getdiffmeta(diffs[b'%d' % diffid])
1633 for k in _metanamemap.keys():
1633 for k in _metanamemap.keys():
1634 if k in meta:
1634 if k in meta:
1635 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
1635 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
1636
1636
1637 content = b'%s%s\n%s' % (header, desc, body)
1637 content = b'%s%s\n%s' % (header, desc, body)
1638 write(content)
1638 write(content)
1639
1639
1640
1640
1641 @vcrcommand(
1641 @vcrcommand(
1642 b'phabread',
1642 b'phabread',
1643 [(b'', b'stack', False, _(b'read dependencies'))],
1643 [(b'', b'stack', False, _(b'read dependencies'))],
1644 _(b'DREVSPEC [OPTIONS]'),
1644 _(b'DREVSPEC [OPTIONS]'),
1645 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1645 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1646 )
1646 )
1647 def phabread(ui, repo, spec, **opts):
1647 def phabread(ui, repo, spec, **opts):
1648 """print patches from Phabricator suitable for importing
1648 """print patches from Phabricator suitable for importing
1649
1649
1650 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
1650 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
1651 the number ``123``. It could also have common operators like ``+``, ``-``,
1651 the number ``123``. It could also have common operators like ``+``, ``-``,
1652 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
1652 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
1653 select a stack.
1653 select a stack.
1654
1654
1655 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
1655 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
1656 could be used to filter patches by status. For performance reason, they
1656 could be used to filter patches by status. For performance reason, they
1657 only represent a subset of non-status selections and cannot be used alone.
1657 only represent a subset of non-status selections and cannot be used alone.
1658
1658
1659 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
1659 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
1660 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
1660 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
1661 stack up to D9.
1661 stack up to D9.
1662
1662
1663 If --stack is given, follow dependencies information and read all patches.
1663 If --stack is given, follow dependencies information and read all patches.
1664 It is equivalent to the ``:`` operator.
1664 It is equivalent to the ``:`` operator.
1665 """
1665 """
1666 opts = pycompat.byteskwargs(opts)
1666 opts = pycompat.byteskwargs(opts)
1667 if opts.get(b'stack'):
1667 if opts.get(b'stack'):
1668 spec = b':(%s)' % spec
1668 spec = b':(%s)' % spec
1669 drevs = querydrev(repo, spec)
1669 drevs = querydrev(repo, spec)
1670 readpatch(repo, drevs, ui.write)
1670 readpatch(repo, drevs, ui.write)
1671
1671
1672
1672
1673 @vcrcommand(
1673 @vcrcommand(
1674 b'phabupdate',
1674 b'phabupdate',
1675 [
1675 [
1676 (b'', b'accept', False, _(b'accept revisions')),
1676 (b'', b'accept', False, _(b'accept revisions')),
1677 (b'', b'reject', False, _(b'reject revisions')),
1677 (b'', b'reject', False, _(b'reject revisions')),
1678 (b'', b'abandon', False, _(b'abandon revisions')),
1678 (b'', b'abandon', False, _(b'abandon revisions')),
1679 (b'', b'reclaim', False, _(b'reclaim revisions')),
1679 (b'', b'reclaim', False, _(b'reclaim revisions')),
1680 (b'm', b'comment', b'', _(b'comment on the last revision')),
1680 (b'm', b'comment', b'', _(b'comment on the last revision')),
1681 ],
1681 ],
1682 _(b'DREVSPEC [OPTIONS]'),
1682 _(b'DREVSPEC [OPTIONS]'),
1683 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1683 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1684 )
1684 )
1685 def phabupdate(ui, repo, spec, **opts):
1685 def phabupdate(ui, repo, spec, **opts):
1686 """update Differential Revision in batch
1686 """update Differential Revision in batch
1687
1687
1688 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
1688 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
1689 """
1689 """
1690 opts = pycompat.byteskwargs(opts)
1690 opts = pycompat.byteskwargs(opts)
1691 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
1691 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
1692 if len(flags) > 1:
1692 if len(flags) > 1:
1693 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
1693 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
1694
1694
1695 actions = []
1695 actions = []
1696 for f in flags:
1696 for f in flags:
1697 actions.append({b'type': f, b'value': True})
1697 actions.append({b'type': f, b'value': True})
1698
1698
1699 drevs = querydrev(repo, spec)
1699 drevs = querydrev(repo, spec)
1700 for i, drev in enumerate(drevs):
1700 for i, drev in enumerate(drevs):
1701 if i + 1 == len(drevs) and opts.get(b'comment'):
1701 if i + 1 == len(drevs) and opts.get(b'comment'):
1702 actions.append({b'type': b'comment', b'value': opts[b'comment']})
1702 actions.append({b'type': b'comment', b'value': opts[b'comment']})
1703 if actions:
1703 if actions:
1704 params = {
1704 params = {
1705 b'objectIdentifier': drev[b'phid'],
1705 b'objectIdentifier': drev[b'phid'],
1706 b'transactions': actions,
1706 b'transactions': actions,
1707 }
1707 }
1708 callconduit(ui, b'differential.revision.edit', params)
1708 callconduit(ui, b'differential.revision.edit', params)
1709
1709
1710
1710
1711 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
1711 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
1712 def template_review(context, mapping):
1712 def template_review(context, mapping):
1713 """:phabreview: Object describing the review for this changeset.
1713 """:phabreview: Object describing the review for this changeset.
1714 Has attributes `url` and `id`.
1714 Has attributes `url` and `id`.
1715 """
1715 """
1716 ctx = context.resource(mapping, b'ctx')
1716 ctx = context.resource(mapping, b'ctx')
1717 m = _differentialrevisiondescre.search(ctx.description())
1717 m = _differentialrevisiondescre.search(ctx.description())
1718 if m:
1718 if m:
1719 return templateutil.hybriddict(
1719 return templateutil.hybriddict(
1720 {b'url': m.group('url'), b'id': b"D%s" % m.group('id'),}
1720 {b'url': m.group('url'), b'id': b"D%s" % m.group('id'),}
1721 )
1721 )
1722 else:
1722 else:
1723 tags = ctx.repo().nodetags(ctx.node())
1723 tags = ctx.repo().nodetags(ctx.node())
1724 for t in tags:
1724 for t in tags:
1725 if _differentialrevisiontagre.match(t):
1725 if _differentialrevisiontagre.match(t):
1726 url = ctx.repo().ui.config(b'phabricator', b'url')
1726 url = ctx.repo().ui.config(b'phabricator', b'url')
1727 if not url.endswith(b'/'):
1727 if not url.endswith(b'/'):
1728 url += b'/'
1728 url += b'/'
1729 url += t
1729 url += t
1730
1730
1731 return templateutil.hybriddict({b'url': url, b'id': t,})
1731 return templateutil.hybriddict({b'url': url, b'id': t,})
1732 return None
1732 return None
1733
1733
1734
1734
1735 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
1735 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
1736 def template_status(context, mapping):
1736 def template_status(context, mapping):
1737 """:phabstatus: String. Status of Phabricator differential.
1737 """:phabstatus: String. Status of Phabricator differential.
1738 """
1738 """
1739 ctx = context.resource(mapping, b'ctx')
1739 ctx = context.resource(mapping, b'ctx')
1740 repo = context.resource(mapping, b'repo')
1740 repo = context.resource(mapping, b'repo')
1741 ui = context.resource(mapping, b'ui')
1741 ui = context.resource(mapping, b'ui')
1742
1742
1743 rev = ctx.rev()
1743 rev = ctx.rev()
1744 try:
1744 try:
1745 drevid = getdrevmap(repo, [rev])[rev]
1745 drevid = getdrevmap(repo, [rev])[rev]
1746 except KeyError:
1746 except KeyError:
1747 return None
1747 return None
1748 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
1748 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
1749 for drev in drevs:
1749 for drev in drevs:
1750 if int(drev[b'id']) == drevid:
1750 if int(drev[b'id']) == drevid:
1751 return templateutil.hybriddict(
1751 return templateutil.hybriddict(
1752 {b'url': drev[b'uri'], b'status': drev[b'statusName'],}
1752 {b'url': drev[b'uri'], b'status': drev[b'statusName'],}
1753 )
1753 )
1754 return None
1754 return None
1755
1755
1756
1756
1757 @show.showview(b'phabstatus', csettopic=b'work')
1757 @show.showview(b'phabstatus', csettopic=b'work')
1758 def phabstatusshowview(ui, repo, displayer):
1758 def phabstatusshowview(ui, repo, displayer):
1759 """Phabricator differiential status"""
1759 """Phabricator differiential status"""
1760 revs = repo.revs('sort(_underway(), topo)')
1760 revs = repo.revs('sort(_underway(), topo)')
1761 drevmap = getdrevmap(repo, revs)
1761 drevmap = getdrevmap(repo, revs)
1762 unknownrevs, drevids, revsbydrevid = [], set([]), {}
1762 unknownrevs, drevids, revsbydrevid = [], set([]), {}
1763 for rev, drevid in pycompat.iteritems(drevmap):
1763 for rev, drevid in pycompat.iteritems(drevmap):
1764 if drevid is not None:
1764 if drevid is not None:
1765 drevids.add(drevid)
1765 drevids.add(drevid)
1766 revsbydrevid.setdefault(drevid, set([])).add(rev)
1766 revsbydrevid.setdefault(drevid, set([])).add(rev)
1767 else:
1767 else:
1768 unknownrevs.append(rev)
1768 unknownrevs.append(rev)
1769
1769
1770 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
1770 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
1771 drevsbyrev = {}
1771 drevsbyrev = {}
1772 for drev in drevs:
1772 for drev in drevs:
1773 for rev in revsbydrevid[int(drev[b'id'])]:
1773 for rev in revsbydrevid[int(drev[b'id'])]:
1774 drevsbyrev[rev] = drev
1774 drevsbyrev[rev] = drev
1775
1775
1776 def phabstatus(ctx):
1776 def phabstatus(ctx):
1777 drev = drevsbyrev[ctx.rev()]
1777 drev = drevsbyrev[ctx.rev()]
1778 status = ui.label(
1778 status = ui.label(
1779 b'%(statusName)s' % drev,
1779 b'%(statusName)s' % drev,
1780 b'phabricator.status.%s' % _getstatusname(drev),
1780 b'phabricator.status.%s' % _getstatusname(drev),
1781 )
1781 )
1782 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
1782 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
1783
1783
1784 revs -= smartset.baseset(unknownrevs)
1784 revs -= smartset.baseset(unknownrevs)
1785 revdag = graphmod.dagwalker(repo, revs)
1785 revdag = graphmod.dagwalker(repo, revs)
1786
1786
1787 ui.setconfig(b'experimental', b'graphshorten', True)
1787 ui.setconfig(b'experimental', b'graphshorten', True)
1788 displayer._exthook = phabstatus
1788 displayer._exthook = phabstatus
1789 nodelen = show.longestshortest(repo, revs)
1789 nodelen = show.longestshortest(repo, revs)
1790 logcmdutil.displaygraph(
1790 logcmdutil.displaygraph(
1791 ui,
1791 ui,
1792 repo,
1792 repo,
1793 revdag,
1793 revdag,
1794 displayer,
1794 displayer,
1795 graphmod.asciiedges,
1795 graphmod.asciiedges,
1796 props={b'nodelen': nodelen},
1796 props={b'nodelen': nodelen},
1797 )
1797 )
@@ -1,246 +1,263 b''
1 #require vcr
1 #require vcr
2 $ cat >> $HGRCPATH <<EOF
2 $ cat >> $HGRCPATH <<EOF
3 > [extensions]
3 > [extensions]
4 > phabricator =
4 > phabricator =
5 > EOF
5 > EOF
6 $ hg init repo
6 $ hg init repo
7 $ cd repo
7 $ cd repo
8 $ cat >> .hg/hgrc <<EOF
8 $ cat >> .hg/hgrc <<EOF
9 > [phabricator]
9 > [phabricator]
10 > url = https://phab.mercurial-scm.org/
10 > url = https://phab.mercurial-scm.org/
11 > callsign = HG
11 > callsign = HG
12 >
12 >
13 > [auth]
13 > [auth]
14 > hgphab.schemes = https
14 > hgphab.schemes = https
15 > hgphab.prefix = phab.mercurial-scm.org
15 > hgphab.prefix = phab.mercurial-scm.org
16 > # When working on the extension and making phabricator interaction
16 > # When working on the extension and making phabricator interaction
17 > # changes, edit this to be a real phabricator token. When done, edit
17 > # changes, edit this to be a real phabricator token. When done, edit
18 > # it back. The VCR transcripts will be auto-sanitised to replace your real
18 > # it back. The VCR transcripts will be auto-sanitised to replace your real
19 > # token with this value.
19 > # token with this value.
20 > hgphab.phabtoken = cli-hahayouwish
20 > hgphab.phabtoken = cli-hahayouwish
21 > EOF
21 > EOF
22 $ VCR="$TESTDIR/phabricator"
22 $ VCR="$TESTDIR/phabricator"
23
23
24 Error is handled reasonably. We override the phabtoken here so that
24 Error is handled reasonably. We override the phabtoken here so that
25 when you're developing changes to phabricator.py you can edit the
25 when you're developing changes to phabricator.py you can edit the
26 above config and have a real token in the test but not have to edit
26 above config and have a real token in the test but not have to edit
27 this test.
27 this test.
28 $ hg phabread --config auth.hgphab.phabtoken=cli-notavalidtoken \
28 $ hg phabread --config auth.hgphab.phabtoken=cli-notavalidtoken \
29 > --test-vcr "$VCR/phabread-conduit-error.json" D4480 | head
29 > --test-vcr "$VCR/phabread-conduit-error.json" D4480 | head
30 abort: Conduit Error (ERR-INVALID-AUTH): API token "cli-notavalidtoken" has the wrong length. API tokens should be 32 characters long.
30 abort: Conduit Error (ERR-INVALID-AUTH): API token "cli-notavalidtoken" has the wrong length. API tokens should be 32 characters long.
31
31
32 Basic phabread:
32 Basic phabread:
33 $ hg phabread --test-vcr "$VCR/phabread-4480.json" D4480 | head
33 $ hg phabread --test-vcr "$VCR/phabread-4480.json" D4480 | head
34 # HG changeset patch
34 # HG changeset patch
35 # Date 1536771503 0
35 # Date 1536771503 0
36 # Parent a5de21c9e3703f8e8eb064bd7d893ff2f703c66a
36 # Parent a5de21c9e3703f8e8eb064bd7d893ff2f703c66a
37 exchangev2: start to implement pull with wire protocol v2
37 exchangev2: start to implement pull with wire protocol v2
38
38
39 Wire protocol version 2 will take a substantially different
39 Wire protocol version 2 will take a substantially different
40 approach to exchange than version 1 (at least as far as pulling
40 approach to exchange than version 1 (at least as far as pulling
41 is concerned).
41 is concerned).
42
42
43 This commit establishes a new exchangev2 module for holding
43 This commit establishes a new exchangev2 module for holding
44
44
45 phabupdate with an accept:
45 phabupdate with an accept:
46 $ hg phabupdate --accept D4564 \
46 $ hg phabupdate --accept D4564 \
47 > -m 'I think I like where this is headed. Will read rest of series later.'\
47 > -m 'I think I like where this is headed. Will read rest of series later.'\
48 > --test-vcr "$VCR/accept-4564.json"
48 > --test-vcr "$VCR/accept-4564.json"
49 abort: Conduit Error (ERR-CONDUIT-CORE): Validation errors:
49 abort: Conduit Error (ERR-CONDUIT-CORE): Validation errors:
50 - You can not accept this revision because it has already been closed. Only open revisions can be accepted.
50 - You can not accept this revision because it has already been closed. Only open revisions can be accepted.
51 [255]
51 [255]
52 $ hg phabupdate --accept D7913 -m 'LGTM' --test-vcr "$VCR/accept-7913.json"
52 $ hg phabupdate --accept D7913 -m 'LGTM' --test-vcr "$VCR/accept-7913.json"
53
53
54 Create a differential diff:
54 Create a differential diff:
55 $ HGENCODING=utf-8; export HGENCODING
55 $ HGENCODING=utf-8; export HGENCODING
56 $ echo alpha > alpha
56 $ echo alpha > alpha
57 $ hg ci --addremove -m 'create alpha for phabricator test €'
57 $ hg ci --addremove -m 'create alpha for phabricator test €'
58 adding alpha
58 adding alpha
59 $ hg phabsend -r . --test-vcr "$VCR/phabsend-create-alpha.json"
59 $ hg phabsend -r . --test-vcr "$VCR/phabsend-create-alpha.json"
60 D7915 - created - d386117f30e6: create alpha for phabricator test \xe2\x82\xac (esc)
60 D7915 - created - d386117f30e6: create alpha for phabricator test \xe2\x82\xac (esc)
61 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d386117f30e6-24ffe649-phabsend.hg
61 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d386117f30e6-24ffe649-phabsend.hg
62 $ echo more >> alpha
62 $ echo more >> alpha
63 $ HGEDITOR=true hg ci --amend
63 $ HGEDITOR=true hg ci --amend
64 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/347bf67801e5-3bf313e4-amend.hg
64 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/347bf67801e5-3bf313e4-amend.hg
65 $ echo beta > beta
65 $ echo beta > beta
66 $ hg ci --addremove -m 'create beta for phabricator test'
66 $ hg ci --addremove -m 'create beta for phabricator test'
67 adding beta
67 adding beta
68 $ hg phabsend -r ".^::" --test-vcr "$VCR/phabsend-update-alpha-create-beta.json"
68 $ hg phabsend -r ".^::" --test-vcr "$VCR/phabsend-update-alpha-create-beta.json"
69 D7915 - updated - c44b38f24a45: create alpha for phabricator test \xe2\x82\xac (esc)
69 D7915 - updated - c44b38f24a45: create alpha for phabricator test \xe2\x82\xac (esc)
70 D7916 - created - 9e6901f21d5b: create beta for phabricator test
70 D7916 - created - 9e6901f21d5b: create beta for phabricator test
71 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9e6901f21d5b-1fcd4f0e-phabsend.hg
71 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9e6901f21d5b-1fcd4f0e-phabsend.hg
72 $ unset HGENCODING
72 $ unset HGENCODING
73
73
74 The amend won't explode after posting a public commit. The local tag is left
74 The amend won't explode after posting a public commit. The local tag is left
75 behind to identify it.
75 behind to identify it.
76
76
77 $ echo 'public change' > beta
77 $ echo 'public change' > beta
78 $ hg ci -m 'create public change for phabricator testing'
78 $ hg ci -m 'create public change for phabricator testing'
79 $ hg phase --public .
79 $ hg phase --public .
80 $ echo 'draft change' > alpha
80 $ echo 'draft change' > alpha
81 $ hg ci -m 'create draft change for phabricator testing'
81 $ hg ci -m 'create draft change for phabricator testing'
82 $ hg phabsend --amend -r '.^::' --test-vcr "$VCR/phabsend-create-public.json"
82 $ hg phabsend --amend -r '.^::' --test-vcr "$VCR/phabsend-create-public.json"
83 D7917 - created - 7b4185ab5d16: create public change for phabricator testing
83 D7917 - created - 7b4185ab5d16: create public change for phabricator testing
84 D7918 - created - 251c1c333fc6: create draft change for phabricator testing
84 D7918 - created - 251c1c333fc6: create draft change for phabricator testing
85 warning: not updating public commit 2:7b4185ab5d16
85 warning: not updating public commit 2:7b4185ab5d16
86 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/251c1c333fc6-41cb7c3b-phabsend.hg
86 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/251c1c333fc6-41cb7c3b-phabsend.hg
87 $ hg tags -v
87 $ hg tags -v
88 tip 3:3244dc4a3334
88 tip 3:3244dc4a3334
89 D7917 2:7b4185ab5d16 local
89 D7917 2:7b4185ab5d16 local
90
90
91 $ hg debugcallconduit user.search --test-vcr "$VCR/phab-conduit.json" <<EOF
91 $ hg debugcallconduit user.search --test-vcr "$VCR/phab-conduit.json" <<EOF
92 > {
92 > {
93 > "constraints": {
93 > "constraints": {
94 > "isBot": true
94 > "isBot": true
95 > }
95 > }
96 > }
96 > }
97 > EOF
97 > EOF
98 {
98 {
99 "cursor": {
99 "cursor": {
100 "after": null,
100 "after": null,
101 "before": null,
101 "before": null,
102 "limit": 100,
102 "limit": 100,
103 "order": null
103 "order": null
104 },
104 },
105 "data": [],
105 "data": [],
106 "maps": {},
106 "maps": {},
107 "query": {
107 "query": {
108 "queryKey": null
108 "queryKey": null
109 }
109 }
110 }
110 }
111
111
112 Template keywords
112 Template keywords
113 $ hg log -T'{rev} {phabreview|json}\n'
113 $ hg log -T'{rev} {phabreview|json}\n'
114 3 {"id": "D7918", "url": "https://phab.mercurial-scm.org/D7918"}
114 3 {"id": "D7918", "url": "https://phab.mercurial-scm.org/D7918"}
115 2 {"id": "D7917", "url": "https://phab.mercurial-scm.org/D7917"}
115 2 {"id": "D7917", "url": "https://phab.mercurial-scm.org/D7917"}
116 1 {"id": "D7916", "url": "https://phab.mercurial-scm.org/D7916"}
116 1 {"id": "D7916", "url": "https://phab.mercurial-scm.org/D7916"}
117 0 {"id": "D7915", "url": "https://phab.mercurial-scm.org/D7915"}
117 0 {"id": "D7915", "url": "https://phab.mercurial-scm.org/D7915"}
118
118
119 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n'
119 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n'
120 3 https://phab.mercurial-scm.org/D7918 D7918
120 3 https://phab.mercurial-scm.org/D7918 D7918
121 2 https://phab.mercurial-scm.org/D7917 D7917
121 2 https://phab.mercurial-scm.org/D7917 D7917
122 1 https://phab.mercurial-scm.org/D7916 D7916
122 1 https://phab.mercurial-scm.org/D7916 D7916
123 0 https://phab.mercurial-scm.org/D7915 D7915
123 0 https://phab.mercurial-scm.org/D7915 D7915
124
124
125 Commenting when phabsending:
125 Commenting when phabsending:
126 $ echo comment > comment
126 $ echo comment > comment
127 $ hg ci --addremove -m "create comment for phabricator test"
127 $ hg ci --addremove -m "create comment for phabricator test"
128 adding comment
128 adding comment
129 $ hg phabsend -r . -m "For default branch" --test-vcr "$VCR/phabsend-comment-created.json"
129 $ hg phabsend -r . -m "For default branch" --test-vcr "$VCR/phabsend-comment-created.json"
130 D7919 - created - d5dddca9023d: create comment for phabricator test
130 D7919 - created - d5dddca9023d: create comment for phabricator test
131 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d5dddca9023d-adf673ba-phabsend.hg
131 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d5dddca9023d-adf673ba-phabsend.hg
132 $ echo comment2 >> comment
132 $ echo comment2 >> comment
133 $ hg ci --amend
133 $ hg ci --amend
134 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f7db812bbe1d-8fcded77-amend.hg
134 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f7db812bbe1d-8fcded77-amend.hg
135 $ hg phabsend -r . -m "Address review comments" --test-vcr "$VCR/phabsend-comment-updated.json"
135 $ hg phabsend -r . -m "Address review comments" --test-vcr "$VCR/phabsend-comment-updated.json"
136 D7919 - updated - 1849d7828727: create comment for phabricator test
136 D7919 - updated - 1849d7828727: create comment for phabricator test
137
137
138 Phabsending a skipped commit:
138 Phabsending a skipped commit:
139 $ hg phabsend --no-amend -r . --test-vcr "$VCR/phabsend-skipped.json"
139 $ hg phabsend --no-amend -r . --test-vcr "$VCR/phabsend-skipped.json"
140 D7919 - skipped - 1849d7828727: create comment for phabricator test
140 D7919 - skipped - 1849d7828727: create comment for phabricator test
141
141
142 Phabesending a new binary, a modified binary, and a removed binary
143
144 >>> open('bin', 'wb').write(b'\0a') and None
145 $ hg ci -Am 'add binary'
146 adding bin
147 >>> open('bin', 'wb').write(b'\0b') and None
148 $ hg ci -m 'modify binary'
149 $ hg rm bin
150 $ hg ci -m 'remove binary'
151 $ hg phabsend -r .~2:: --test-vcr "$VCR/phabsend-binary.json"
152 uploading bin@aa24a81f55de
153 D8007 - created - aa24a81f55de: add binary
154 uploading bin@d8d62a881b54
155 D8008 - created - d8d62a881b54: modify binary
156 D8009 - created - af55645b2e29: remove binary
157 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/aa24a81f55de-a3a0cf24-phabsend.hg
158
142 Phabreading a DREV with a local:commits time as a string:
159 Phabreading a DREV with a local:commits time as a string:
143 $ hg phabread --test-vcr "$VCR/phabread-str-time.json" D1285
160 $ hg phabread --test-vcr "$VCR/phabread-str-time.json" D1285
144 # HG changeset patch
161 # HG changeset patch
145 # User Pulkit Goyal <7895pulkit@gmail.com>
162 # User Pulkit Goyal <7895pulkit@gmail.com>
146 # Date 1509404054 -19800
163 # Date 1509404054 -19800
147 # Node ID 44fc1c1f1774a76423b9c732af6938435099bcc5
164 # Node ID 44fc1c1f1774a76423b9c732af6938435099bcc5
148 # Parent 8feef8ef8389a3b544e0a74624f1efc3a8d85d35
165 # Parent 8feef8ef8389a3b544e0a74624f1efc3a8d85d35
149 repoview: add a new attribute _visibilityexceptions and related API
166 repoview: add a new attribute _visibilityexceptions and related API
150
167
151 Currently we don't have a defined way in core to make some hidden revisions
168 Currently we don't have a defined way in core to make some hidden revisions
152 visible in filtered repo. Extensions to achieve the purpose of unhiding some
169 visible in filtered repo. Extensions to achieve the purpose of unhiding some
153 hidden commits, wrap repoview.pinnedrevs() function.
170 hidden commits, wrap repoview.pinnedrevs() function.
154
171
155 To make the above task simple and have well defined API, this patch adds a new
172 To make the above task simple and have well defined API, this patch adds a new
156 attribute '_visibilityexceptions' to repoview class which will contains
173 attribute '_visibilityexceptions' to repoview class which will contains
157 the hidden revs which should be exception.
174 the hidden revs which should be exception.
158 This will allow to set different exceptions for different repoview objects
175 This will allow to set different exceptions for different repoview objects
159 backed by the same unfiltered repo.
176 backed by the same unfiltered repo.
160
177
161 This patch also adds API to add revs to the attribute set and get them.
178 This patch also adds API to add revs to the attribute set and get them.
162
179
163 Thanks to Jun for suggesting the use of repoview class instead of localrepo.
180 Thanks to Jun for suggesting the use of repoview class instead of localrepo.
164
181
165 Differential Revision: https://phab.mercurial-scm.org/D1285
182 Differential Revision: https://phab.mercurial-scm.org/D1285
166 diff --git a/mercurial/repoview.py b/mercurial/repoview.py
183 diff --git a/mercurial/repoview.py b/mercurial/repoview.py
167 --- a/mercurial/repoview.py
184 --- a/mercurial/repoview.py
168 +++ b/mercurial/repoview.py
185 +++ b/mercurial/repoview.py
169 @@ * @@ (glob)
186 @@ * @@ (glob)
170 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
187 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
171 """
188 """
172
189
173 + # hidden revs which should be visible
190 + # hidden revs which should be visible
174 + _visibilityexceptions = set()
191 + _visibilityexceptions = set()
175 +
192 +
176 def __init__(self, repo, filtername):
193 def __init__(self, repo, filtername):
177 object.__setattr__(self, r'_unfilteredrepo', repo)
194 object.__setattr__(self, r'_unfilteredrepo', repo)
178 object.__setattr__(self, r'filtername', filtername)
195 object.__setattr__(self, r'filtername', filtername)
179 @@ -231,6 +234,14 @@
196 @@ -231,6 +234,14 @@
180 return self
197 return self
181 return self.unfiltered().filtered(name)
198 return self.unfiltered().filtered(name)
182
199
183 + def addvisibilityexceptions(self, revs):
200 + def addvisibilityexceptions(self, revs):
184 + """adds hidden revs which should be visible to set of exceptions"""
201 + """adds hidden revs which should be visible to set of exceptions"""
185 + self._visibilityexceptions.update(revs)
202 + self._visibilityexceptions.update(revs)
186 +
203 +
187 + def getvisibilityexceptions(self):
204 + def getvisibilityexceptions(self):
188 + """returns the set of hidden revs which should be visible"""
205 + """returns the set of hidden revs which should be visible"""
189 + return self._visibilityexceptions
206 + return self._visibilityexceptions
190 +
207 +
191 # everything access are forwarded to the proxied repo
208 # everything access are forwarded to the proxied repo
192 def __getattr__(self, attr):
209 def __getattr__(self, attr):
193 return getattr(self._unfilteredrepo, attr)
210 return getattr(self._unfilteredrepo, attr)
194 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
211 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
195 --- a/mercurial/localrepo.py
212 --- a/mercurial/localrepo.py
196 +++ b/mercurial/localrepo.py
213 +++ b/mercurial/localrepo.py
197 @@ -570,6 +570,14 @@
214 @@ -570,6 +570,14 @@
198 def close(self):
215 def close(self):
199 self._writecaches()
216 self._writecaches()
200
217
201 + def addvisibilityexceptions(self, exceptions):
218 + def addvisibilityexceptions(self, exceptions):
202 + # should be called on a filtered repository
219 + # should be called on a filtered repository
203 + pass
220 + pass
204 +
221 +
205 + def getvisibilityexceptions(self):
222 + def getvisibilityexceptions(self):
206 + # should be called on a filtered repository
223 + # should be called on a filtered repository
207 + return set()
224 + return set()
208 +
225 +
209 def _loadextensions(self):
226 def _loadextensions(self):
210 extensions.loadall(self.ui)
227 extensions.loadall(self.ui)
211
228
212
229
213 A bad .arcconfig doesn't error out
230 A bad .arcconfig doesn't error out
214 $ echo 'garbage' > .arcconfig
231 $ echo 'garbage' > .arcconfig
215 $ hg config phabricator --debug
232 $ hg config phabricator --debug
216 invalid JSON in $TESTTMP/repo/.arcconfig
233 invalid JSON in $TESTTMP/repo/.arcconfig
217 read config from: */.hgrc (glob)
234 read config from: */.hgrc (glob)
218 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=https://phab.mercurial-scm.org/ (glob)
235 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=https://phab.mercurial-scm.org/ (glob)
219 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=HG (glob)
236 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=HG (glob)
220
237
221 The .arcconfig content overrides global config
238 The .arcconfig content overrides global config
222 $ cat >> $HGRCPATH << EOF
239 $ cat >> $HGRCPATH << EOF
223 > [phabricator]
240 > [phabricator]
224 > url = global
241 > url = global
225 > callsign = global
242 > callsign = global
226 > EOF
243 > EOF
227 $ cp $TESTDIR/../.arcconfig .
244 $ cp $TESTDIR/../.arcconfig .
228 $ mv .hg/hgrc .hg/hgrc.bak
245 $ mv .hg/hgrc .hg/hgrc.bak
229 $ hg config phabricator --debug
246 $ hg config phabricator --debug
230 read config from: */.hgrc (glob)
247 read config from: */.hgrc (glob)
231 $TESTTMP/repo/.arcconfig: phabricator.callsign=HG
248 $TESTTMP/repo/.arcconfig: phabricator.callsign=HG
232 $TESTTMP/repo/.arcconfig: phabricator.url=https://phab.mercurial-scm.org/
249 $TESTTMP/repo/.arcconfig: phabricator.url=https://phab.mercurial-scm.org/
233
250
234 But it doesn't override local config
251 But it doesn't override local config
235 $ cat >> .hg/hgrc << EOF
252 $ cat >> .hg/hgrc << EOF
236 > [phabricator]
253 > [phabricator]
237 > url = local
254 > url = local
238 > callsign = local
255 > callsign = local
239 > EOF
256 > EOF
240 $ hg config phabricator --debug
257 $ hg config phabricator --debug
241 read config from: */.hgrc (glob)
258 read config from: */.hgrc (glob)
242 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=local (glob)
259 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=local (glob)
243 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=local (glob)
260 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=local (glob)
244 $ mv .hg/hgrc.bak .hg/hgrc
261 $ mv .hg/hgrc.bak .hg/hgrc
245
262
246 $ cd ..
263 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now