##// END OF EJS Templates
phabricator: avoid creating unstable children within the review stack...
Matt Harbison -
r45212:0680b8a1 default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (617 lines changed) Show them Hide them
@@ -0,0 +1,617 b''
1 {
2 "version": 1,
3 "interactions": [
4 {
5 "response": {
6 "headers": {
7 "referrer-policy": [
8 "no-referrer"
9 ],
10 "x-frame-options": [
11 "Deny"
12 ],
13 "date": [
14 "Wed, 15 Apr 2020 23:43:39 GMT"
15 ],
16 "server": [
17 "Apache/2.4.10 (Debian)"
18 ],
19 "content-type": [
20 "application/json"
21 ],
22 "expires": [
23 "Sat, 01 Jan 2000 00:00:00 GMT"
24 ],
25 "transfer-encoding": [
26 "chunked"
27 ],
28 "x-xss-protection": [
29 "1; mode=block"
30 ],
31 "strict-transport-security": [
32 "max-age=0; includeSubdomains; preload"
33 ],
34 "cache-control": [
35 "no-store"
36 ],
37 "x-content-type-options": [
38 "nosniff"
39 ]
40 },
41 "status": {
42 "code": 200,
43 "message": "OK"
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 "headers": {
51 "content-length": [
52 "183"
53 ],
54 "content-type": [
55 "application/x-www-form-urlencoded"
56 ],
57 "user-agent": [
58 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
59 ],
60 "accept": [
61 "application/mercurial-0.1"
62 ],
63 "host": [
64 "phab.mercurial-scm.org"
65 ]
66 },
67 "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",
68 "uri": "https://phab.mercurial-scm.org//api/diffusion.repository.search",
69 "method": "POST"
70 }
71 },
72 {
73 "response": {
74 "headers": {
75 "referrer-policy": [
76 "no-referrer"
77 ],
78 "x-frame-options": [
79 "Deny"
80 ],
81 "date": [
82 "Wed, 15 Apr 2020 23:43:39 GMT"
83 ],
84 "server": [
85 "Apache/2.4.10 (Debian)"
86 ],
87 "content-type": [
88 "application/json"
89 ],
90 "expires": [
91 "Sat, 01 Jan 2000 00:00:00 GMT"
92 ],
93 "transfer-encoding": [
94 "chunked"
95 ],
96 "x-xss-protection": [
97 "1; mode=block"
98 ],
99 "strict-transport-security": [
100 "max-age=0; includeSubdomains; preload"
101 ],
102 "cache-control": [
103 "no-store"
104 ],
105 "x-content-type-options": [
106 "nosniff"
107 ]
108 },
109 "status": {
110 "code": 200,
111 "message": "OK"
112 },
113 "body": {
114 "string": "{\"result\":{\"diffid\":21110,\"phid\":\"PHID-DIFF-g25jdc5b5khduwpp3p3b\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/differential\\/diff\\/21110\\/\"},\"error_code\":null,\"error_info\":null}"
115 }
116 },
117 "request": {
118 "headers": {
119 "content-length": [
120 "1162"
121 ],
122 "content-type": [
123 "application/x-www-form-urlencoded"
124 ],
125 "user-agent": [
126 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
127 ],
128 "accept": [
129 "application/mercurial-0.1"
130 ],
131 "host": [
132 "phab.mercurial-scm.org"
133 ]
134 },
135 "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%22file1.txt%22%3A+%7B%22addLines%22%3A+1%2C+%22awayPaths%22%3A+%5B%5D%2C+%22commitHash%22%3A+null%2C+%22currentPath%22%3A+%22file1.txt%22%2C+%22delLines%22%3A+1%2C+%22fileType%22%3A+1%2C+%22hunks%22%3A+%5B%7B%22addLines%22%3A+1%2C+%22corpus%22%3A+%22-mod1%5Cn%2Bmod2%5Cn%22%2C+%22delLines%22%3A+1%2C+%22newLength%22%3A+1%2C+%22newOffset%22%3A+1%2C+%22oldLength%22%3A+1%2C+%22oldOffset%22%3A+1%7D%5D%2C+%22metadata%22%3A+%7B%7D%2C+%22newProperties%22%3A+%7B%7D%2C+%22oldPath%22%3A+%22file1.txt%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+%22d549263bcb2db54042adf048047b368f1ed246df%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",
136 "uri": "https://phab.mercurial-scm.org//api/differential.creatediff",
137 "method": "POST"
138 }
139 },
140 {
141 "response": {
142 "headers": {
143 "referrer-policy": [
144 "no-referrer"
145 ],
146 "x-frame-options": [
147 "Deny"
148 ],
149 "date": [
150 "Wed, 15 Apr 2020 23:43:39 GMT"
151 ],
152 "server": [
153 "Apache/2.4.10 (Debian)"
154 ],
155 "content-type": [
156 "application/json"
157 ],
158 "expires": [
159 "Sat, 01 Jan 2000 00:00:00 GMT"
160 ],
161 "transfer-encoding": [
162 "chunked"
163 ],
164 "x-xss-protection": [
165 "1; mode=block"
166 ],
167 "strict-transport-security": [
168 "max-age=0; includeSubdomains; preload"
169 ],
170 "cache-control": [
171 "no-store"
172 ],
173 "x-content-type-options": [
174 "nosniff"
175 ]
176 },
177 "status": {
178 "code": 200,
179 "message": "OK"
180 },
181 "body": {
182 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
183 }
184 },
185 "request": {
186 "headers": {
187 "content-length": [
188 "482"
189 ],
190 "content-type": [
191 "application/x-www-form-urlencoded"
192 ],
193 "user-agent": [
194 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
195 ],
196 "accept": [
197 "application/mercurial-0.1"
198 ],
199 "host": [
200 "phab.mercurial-scm.org"
201 ]
202 },
203 "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%225d3959e20d1d71d9e906e562f891582b4072570f%5C%22%2C+%5C%22parent%5C%22%3A+%5C%22d549263bcb2db54042adf048047b368f1ed246df%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+21110%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
204 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
205 "method": "POST"
206 }
207 },
208 {
209 "response": {
210 "headers": {
211 "referrer-policy": [
212 "no-referrer"
213 ],
214 "x-frame-options": [
215 "Deny"
216 ],
217 "date": [
218 "Wed, 15 Apr 2020 23:43:40 GMT"
219 ],
220 "server": [
221 "Apache/2.4.10 (Debian)"
222 ],
223 "content-type": [
224 "application/json"
225 ],
226 "expires": [
227 "Sat, 01 Jan 2000 00:00:00 GMT"
228 ],
229 "transfer-encoding": [
230 "chunked"
231 ],
232 "x-xss-protection": [
233 "1; mode=block"
234 ],
235 "strict-transport-security": [
236 "max-age=0; includeSubdomains; preload"
237 ],
238 "cache-control": [
239 "no-store"
240 ],
241 "x-content-type-options": [
242 "nosniff"
243 ]
244 },
245 "status": {
246 "code": 200,
247 "message": "OK"
248 },
249 "body": {
250 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
251 }
252 },
253 "request": {
254 "headers": {
255 "content-length": [
256 "594"
257 ],
258 "content-type": [
259 "application/x-www-form-urlencoded"
260 ],
261 "user-agent": [
262 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
263 ],
264 "accept": [
265 "application/mercurial-0.1"
266 ],
267 "host": [
268 "phab.mercurial-scm.org"
269 ]
270 },
271 "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%225d3959e20d1d71d9e906e562f891582b4072570f%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%225d3959e20d1d71d9e906e562f891582b4072570f%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%22d549263bcb2db54042adf048047b368f1ed246df%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+21110%2C+%22name%22%3A+%22local%3Acommits%22%7D",
272 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
273 "method": "POST"
274 }
275 },
276 {
277 "response": {
278 "headers": {
279 "referrer-policy": [
280 "no-referrer"
281 ],
282 "x-frame-options": [
283 "Deny"
284 ],
285 "date": [
286 "Wed, 15 Apr 2020 23:43:40 GMT"
287 ],
288 "server": [
289 "Apache/2.4.10 (Debian)"
290 ],
291 "content-type": [
292 "application/json"
293 ],
294 "expires": [
295 "Sat, 01 Jan 2000 00:00:00 GMT"
296 ],
297 "transfer-encoding": [
298 "chunked"
299 ],
300 "x-xss-protection": [
301 "1; mode=block"
302 ],
303 "strict-transport-security": [
304 "max-age=0; includeSubdomains; preload"
305 ],
306 "cache-control": [
307 "no-store"
308 ],
309 "x-content-type-options": [
310 "nosniff"
311 ]
312 },
313 "status": {
314 "code": 200,
315 "message": "OK"
316 },
317 "body": {
318 "string": "{\"result\":{\"errors\":[],\"fields\":{\"title\":\"modified 2\"},\"revisionIDFieldInfo\":{\"value\":null,\"validDomain\":\"https:\\/\\/phab.mercurial-scm.org\"},\"transactions\":[{\"type\":\"title\",\"value\":\"modified 2\"}]},\"error_code\":null,\"error_info\":null}"
319 }
320 },
321 "request": {
322 "headers": {
323 "content-length": [
324 "155"
325 ],
326 "content-type": [
327 "application/x-www-form-urlencoded"
328 ],
329 "user-agent": [
330 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
331 ],
332 "accept": [
333 "application/mercurial-0.1"
334 ],
335 "host": [
336 "phab.mercurial-scm.org"
337 ]
338 },
339 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22corpus%22%3A+%22modified+2%22%7D",
340 "uri": "https://phab.mercurial-scm.org//api/differential.parsecommitmessage",
341 "method": "POST"
342 }
343 },
344 {
345 "response": {
346 "headers": {
347 "referrer-policy": [
348 "no-referrer"
349 ],
350 "x-frame-options": [
351 "Deny"
352 ],
353 "date": [
354 "Wed, 15 Apr 2020 23:43:41 GMT"
355 ],
356 "server": [
357 "Apache/2.4.10 (Debian)"
358 ],
359 "content-type": [
360 "application/json"
361 ],
362 "expires": [
363 "Sat, 01 Jan 2000 00:00:00 GMT"
364 ],
365 "transfer-encoding": [
366 "chunked"
367 ],
368 "x-xss-protection": [
369 "1; mode=block"
370 ],
371 "strict-transport-security": [
372 "max-age=0; includeSubdomains; preload"
373 ],
374 "cache-control": [
375 "no-store"
376 ],
377 "x-content-type-options": [
378 "nosniff"
379 ]
380 },
381 "status": {
382 "code": 200,
383 "message": "OK"
384 },
385 "body": {
386 "string": "{\"result\":{\"object\":{\"id\":8433,\"phid\":\"PHID-DREV-kpkwhtylyxrzikfspl5r\"},\"transactions\":[{\"phid\":\"PHID-XACT-DREV-txmvtexe4kw2pjn\"},{\"phid\":\"PHID-XACT-DREV-l2em7ff7nlcb2fu\"},{\"phid\":\"PHID-XACT-DREV-ttcilh74gwwixze\"},{\"phid\":\"PHID-XACT-DREV-ytvu3llz6pqwg46\"},{\"phid\":\"PHID-XACT-DREV-jeqenvlffsbdv5y\"}]},\"error_code\":null,\"error_info\":null}"
387 }
388 },
389 "request": {
390 "headers": {
391 "content-length": [
392 "308"
393 ],
394 "content-type": [
395 "application/x-www-form-urlencoded"
396 ],
397 "user-agent": [
398 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
399 ],
400 "accept": [
401 "application/mercurial-0.1"
402 ],
403 "host": [
404 "phab.mercurial-scm.org"
405 ]
406 },
407 "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-g25jdc5b5khduwpp3p3b%22%7D%2C+%7B%22type%22%3A+%22title%22%2C+%22value%22%3A+%22modified+2%22%7D%5D%7D",
408 "uri": "https://phab.mercurial-scm.org//api/differential.revision.edit",
409 "method": "POST"
410 }
411 },
412 {
413 "response": {
414 "headers": {
415 "referrer-policy": [
416 "no-referrer"
417 ],
418 "x-frame-options": [
419 "Deny"
420 ],
421 "date": [
422 "Wed, 15 Apr 2020 23:43:41 GMT"
423 ],
424 "server": [
425 "Apache/2.4.10 (Debian)"
426 ],
427 "content-type": [
428 "application/json"
429 ],
430 "expires": [
431 "Sat, 01 Jan 2000 00:00:00 GMT"
432 ],
433 "transfer-encoding": [
434 "chunked"
435 ],
436 "x-xss-protection": [
437 "1; mode=block"
438 ],
439 "strict-transport-security": [
440 "max-age=0; includeSubdomains; preload"
441 ],
442 "cache-control": [
443 "no-store"
444 ],
445 "x-content-type-options": [
446 "nosniff"
447 ]
448 },
449 "status": {
450 "code": 200,
451 "message": "OK"
452 },
453 "body": {
454 "string": "{\"result\":[{\"id\":\"8433\",\"phid\":\"PHID-DREV-kpkwhtylyxrzikfspl5r\",\"title\":\"modified 2\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D8433\",\"dateCreated\":\"1586994221\",\"dateModified\":\"1586994221\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"0\",\"statusName\":\"Needs Review\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":1,\"lines.removed\":1},\"branch\":\"default\",\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"2\",\"activeDiffPHID\":\"PHID-DIFF-g25jdc5b5khduwpp3p3b\",\"diffs\":[\"21110\"],\"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}"
455 }
456 },
457 "request": {
458 "headers": {
459 "content-length": [
460 "146"
461 ],
462 "content-type": [
463 "application/x-www-form-urlencoded"
464 ],
465 "user-agent": [
466 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
467 ],
468 "accept": [
469 "application/mercurial-0.1"
470 ],
471 "host": [
472 "phab.mercurial-scm.org"
473 ]
474 },
475 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22ids%22%3A+%5B8433%5D%7D",
476 "uri": "https://phab.mercurial-scm.org//api/differential.query",
477 "method": "POST"
478 }
479 },
480 {
481 "response": {
482 "headers": {
483 "referrer-policy": [
484 "no-referrer"
485 ],
486 "x-frame-options": [
487 "Deny"
488 ],
489 "date": [
490 "Wed, 15 Apr 2020 23:43:42 GMT"
491 ],
492 "server": [
493 "Apache/2.4.10 (Debian)"
494 ],
495 "content-type": [
496 "application/json"
497 ],
498 "expires": [
499 "Sat, 01 Jan 2000 00:00:00 GMT"
500 ],
501 "transfer-encoding": [
502 "chunked"
503 ],
504 "x-xss-protection": [
505 "1; mode=block"
506 ],
507 "strict-transport-security": [
508 "max-age=0; includeSubdomains; preload"
509 ],
510 "cache-control": [
511 "no-store"
512 ],
513 "x-content-type-options": [
514 "nosniff"
515 ]
516 },
517 "status": {
518 "code": 200,
519 "message": "OK"
520 },
521 "body": {
522 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
523 }
524 },
525 "request": {
526 "headers": {
527 "content-length": [
528 "482"
529 ],
530 "content-type": [
531 "application/x-www-form-urlencoded"
532 ],
533 "user-agent": [
534 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
535 ],
536 "accept": [
537 "application/mercurial-0.1"
538 ],
539 "host": [
540 "phab.mercurial-scm.org"
541 ]
542 },
543 "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%222b4aa8a88d617ca3ba0afd40e0e840b30588f126%5C%22%2C+%5C%22parent%5C%22%3A+%5C%22d549263bcb2db54042adf048047b368f1ed246df%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+21110%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
544 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
545 "method": "POST"
546 }
547 },
548 {
549 "response": {
550 "headers": {
551 "referrer-policy": [
552 "no-referrer"
553 ],
554 "x-frame-options": [
555 "Deny"
556 ],
557 "date": [
558 "Wed, 15 Apr 2020 23:43:42 GMT"
559 ],
560 "server": [
561 "Apache/2.4.10 (Debian)"
562 ],
563 "content-type": [
564 "application/json"
565 ],
566 "expires": [
567 "Sat, 01 Jan 2000 00:00:00 GMT"
568 ],
569 "transfer-encoding": [
570 "chunked"
571 ],
572 "x-xss-protection": [
573 "1; mode=block"
574 ],
575 "strict-transport-security": [
576 "max-age=0; includeSubdomains; preload"
577 ],
578 "cache-control": [
579 "no-store"
580 ],
581 "x-content-type-options": [
582 "nosniff"
583 ]
584 },
585 "status": {
586 "code": 200,
587 "message": "OK"
588 },
589 "body": {
590 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
591 }
592 },
593 "request": {
594 "headers": {
595 "content-length": [
596 "594"
597 ],
598 "content-type": [
599 "application/x-www-form-urlencoded"
600 ],
601 "user-agent": [
602 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
603 ],
604 "accept": [
605 "application/mercurial-0.1"
606 ],
607 "host": [
608 "phab.mercurial-scm.org"
609 ]
610 },
611 "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%222b4aa8a88d617ca3ba0afd40e0e840b30588f126%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%222b4aa8a88d617ca3ba0afd40e0e840b30588f126%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%22d549263bcb2db54042adf048047b368f1ed246df%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+21110%2C+%22name%22%3A+%22local%3Acommits%22%7D",
612 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
613 "method": "POST"
614 }
615 }
616 ]
617 } No newline at end of file
This diff has been collapsed as it changes many lines, (1093 lines changed) Show them Hide them
@@ -0,0 +1,1093 b''
1 {
2 "version": 1,
3 "interactions": [
4 {
5 "response": {
6 "headers": {
7 "referrer-policy": [
8 "no-referrer"
9 ],
10 "x-frame-options": [
11 "Deny"
12 ],
13 "date": [
14 "Wed, 15 Apr 2020 23:43:44 GMT"
15 ],
16 "server": [
17 "Apache/2.4.10 (Debian)"
18 ],
19 "content-type": [
20 "application/json"
21 ],
22 "expires": [
23 "Sat, 01 Jan 2000 00:00:00 GMT"
24 ],
25 "transfer-encoding": [
26 "chunked"
27 ],
28 "x-xss-protection": [
29 "1; mode=block"
30 ],
31 "strict-transport-security": [
32 "max-age=0; includeSubdomains; preload"
33 ],
34 "cache-control": [
35 "no-store"
36 ],
37 "x-content-type-options": [
38 "nosniff"
39 ]
40 },
41 "status": {
42 "code": 200,
43 "message": "OK"
44 },
45 "body": {
46 "string": "{\"result\":{\"21110\":{\"id\":\"21110\",\"revisionID\":\"8433\",\"dateCreated\":\"1586994219\",\"dateModified\":\"1586994221\",\"sourceControlBaseRevision\":\"d549263bcb2db54042adf048047b368f1ed246df\",\"sourceControlPath\":\"\\/\",\"sourceControlSystem\":\"hg\",\"branch\":\"default\",\"bookmark\":null,\"creationMethod\":\"phabsend\",\"description\":null,\"unitStatus\":\"0\",\"lintStatus\":\"0\",\"changes\":[{\"id\":\"57078\",\"metadata\":{\"line:first\":1,\"hash.effect\":\"ftEQkHimiyJo\"},\"oldPath\":\"file1.txt\",\"currentPath\":\"file1.txt\",\"awayPaths\":[],\"oldProperties\":[],\"newProperties\":[],\"type\":\"2\",\"fileType\":\"1\",\"commitHash\":null,\"addLines\":\"1\",\"delLines\":\"1\",\"hunks\":[{\"oldOffset\":\"1\",\"newOffset\":\"1\",\"oldLength\":\"1\",\"newLength\":\"1\",\"addLines\":null,\"delLines\":null,\"isMissingOldNewline\":null,\"isMissingNewNewline\":null,\"corpus\":\"-mod1\\n+mod2\\n\"}]}],\"properties\":{\"hg:meta\":{\"branch\":\"default\",\"date\":\"0 0\",\"node\":\"2b4aa8a88d617ca3ba0afd40e0e840b30588f126\",\"parent\":\"d549263bcb2db54042adf048047b368f1ed246df\",\"user\":\"test\"},\"local:commits\":{\"2b4aa8a88d617ca3ba0afd40e0e840b30588f126\":{\"author\":\"test\",\"authorEmail\":\"test\",\"branch\":\"default\",\"commit\":\"2b4aa8a88d617ca3ba0afd40e0e840b30588f126\",\"parents\":[\"d549263bcb2db54042adf048047b368f1ed246df\"],\"time\":0}}},\"authorName\":\"test\",\"authorEmail\":\"test\"}},\"error_code\":null,\"error_info\":null}"
47 }
48 },
49 "request": {
50 "headers": {
51 "content-length": [
52 "154"
53 ],
54 "content-type": [
55 "application/x-www-form-urlencoded"
56 ],
57 "user-agent": [
58 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
59 ],
60 "accept": [
61 "application/mercurial-0.1"
62 ],
63 "host": [
64 "phab.mercurial-scm.org"
65 ]
66 },
67 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22revisionIDs%22%3A+%5B8433%5D%7D",
68 "uri": "https://phab.mercurial-scm.org//api/differential.querydiffs",
69 "method": "POST"
70 }
71 },
72 {
73 "response": {
74 "headers": {
75 "referrer-policy": [
76 "no-referrer"
77 ],
78 "x-frame-options": [
79 "Deny"
80 ],
81 "date": [
82 "Wed, 15 Apr 2020 23:43:45 GMT"
83 ],
84 "server": [
85 "Apache/2.4.10 (Debian)"
86 ],
87 "content-type": [
88 "application/json"
89 ],
90 "expires": [
91 "Sat, 01 Jan 2000 00:00:00 GMT"
92 ],
93 "transfer-encoding": [
94 "chunked"
95 ],
96 "x-xss-protection": [
97 "1; mode=block"
98 ],
99 "strict-transport-security": [
100 "max-age=0; includeSubdomains; preload"
101 ],
102 "cache-control": [
103 "no-store"
104 ],
105 "x-content-type-options": [
106 "nosniff"
107 ]
108 },
109 "status": {
110 "code": 200,
111 "message": "OK"
112 },
113 "body": {
114 "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}"
115 }
116 },
117 "request": {
118 "headers": {
119 "content-length": [
120 "183"
121 ],
122 "content-type": [
123 "application/x-www-form-urlencoded"
124 ],
125 "user-agent": [
126 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
127 ],
128 "accept": [
129 "application/mercurial-0.1"
130 ],
131 "host": [
132 "phab.mercurial-scm.org"
133 ]
134 },
135 "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",
136 "uri": "https://phab.mercurial-scm.org//api/diffusion.repository.search",
137 "method": "POST"
138 }
139 },
140 {
141 "response": {
142 "headers": {
143 "referrer-policy": [
144 "no-referrer"
145 ],
146 "x-frame-options": [
147 "Deny"
148 ],
149 "date": [
150 "Wed, 15 Apr 2020 23:43:45 GMT"
151 ],
152 "server": [
153 "Apache/2.4.10 (Debian)"
154 ],
155 "content-type": [
156 "application/json"
157 ],
158 "expires": [
159 "Sat, 01 Jan 2000 00:00:00 GMT"
160 ],
161 "transfer-encoding": [
162 "chunked"
163 ],
164 "x-xss-protection": [
165 "1; mode=block"
166 ],
167 "strict-transport-security": [
168 "max-age=0; includeSubdomains; preload"
169 ],
170 "cache-control": [
171 "no-store"
172 ],
173 "x-content-type-options": [
174 "nosniff"
175 ]
176 },
177 "status": {
178 "code": 200,
179 "message": "OK"
180 },
181 "body": {
182 "string": "{\"result\":{\"diffid\":21111,\"phid\":\"PHID-DIFF-qat4sqpqqvytzhf7rpti\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/differential\\/diff\\/21111\\/\"},\"error_code\":null,\"error_info\":null}"
183 }
184 },
185 "request": {
186 "headers": {
187 "content-length": [
188 "1161"
189 ],
190 "content-type": [
191 "application/x-www-form-urlencoded"
192 ],
193 "user-agent": [
194 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
195 ],
196 "accept": [
197 "application/mercurial-0.1"
198 ],
199 "host": [
200 "phab.mercurial-scm.org"
201 ]
202 },
203 "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%22file1.txt%22%3A+%7B%22addLines%22%3A+1%2C+%22awayPaths%22%3A+%5B%5D%2C+%22commitHash%22%3A+null%2C+%22currentPath%22%3A+%22file1.txt%22%2C+%22delLines%22%3A+1%2C+%22fileType%22%3A+1%2C+%22hunks%22%3A+%5B%7B%22addLines%22%3A+1%2C+%22corpus%22%3A+%22-add%5Cn%2Bmod1%5Cn%22%2C+%22delLines%22%3A+1%2C+%22newLength%22%3A+1%2C+%22newOffset%22%3A+1%2C+%22oldLength%22%3A+1%2C+%22oldOffset%22%3A+1%7D%5D%2C+%22metadata%22%3A+%7B%7D%2C+%22newProperties%22%3A+%7B%7D%2C+%22oldPath%22%3A+%22file1.txt%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+%225cbade24e0fae40d67c568e86a978a2a946b9aed%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",
204 "uri": "https://phab.mercurial-scm.org//api/differential.creatediff",
205 "method": "POST"
206 }
207 },
208 {
209 "response": {
210 "headers": {
211 "referrer-policy": [
212 "no-referrer"
213 ],
214 "x-frame-options": [
215 "Deny"
216 ],
217 "date": [
218 "Wed, 15 Apr 2020 23:43:46 GMT"
219 ],
220 "server": [
221 "Apache/2.4.10 (Debian)"
222 ],
223 "content-type": [
224 "application/json"
225 ],
226 "expires": [
227 "Sat, 01 Jan 2000 00:00:00 GMT"
228 ],
229 "transfer-encoding": [
230 "chunked"
231 ],
232 "x-xss-protection": [
233 "1; mode=block"
234 ],
235 "strict-transport-security": [
236 "max-age=0; includeSubdomains; preload"
237 ],
238 "cache-control": [
239 "no-store"
240 ],
241 "x-content-type-options": [
242 "nosniff"
243 ]
244 },
245 "status": {
246 "code": 200,
247 "message": "OK"
248 },
249 "body": {
250 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
251 }
252 },
253 "request": {
254 "headers": {
255 "content-length": [
256 "482"
257 ],
258 "content-type": [
259 "application/x-www-form-urlencoded"
260 ],
261 "user-agent": [
262 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
263 ],
264 "accept": [
265 "application/mercurial-0.1"
266 ],
267 "host": [
268 "phab.mercurial-scm.org"
269 ]
270 },
271 "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%22d549263bcb2db54042adf048047b368f1ed246df%5C%22%2C+%5C%22parent%5C%22%3A+%5C%225cbade24e0fae40d67c568e86a978a2a946b9aed%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+21111%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
272 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
273 "method": "POST"
274 }
275 },
276 {
277 "response": {
278 "headers": {
279 "referrer-policy": [
280 "no-referrer"
281 ],
282 "x-frame-options": [
283 "Deny"
284 ],
285 "date": [
286 "Wed, 15 Apr 2020 23:43:46 GMT"
287 ],
288 "server": [
289 "Apache/2.4.10 (Debian)"
290 ],
291 "content-type": [
292 "application/json"
293 ],
294 "expires": [
295 "Sat, 01 Jan 2000 00:00:00 GMT"
296 ],
297 "transfer-encoding": [
298 "chunked"
299 ],
300 "x-xss-protection": [
301 "1; mode=block"
302 ],
303 "strict-transport-security": [
304 "max-age=0; includeSubdomains; preload"
305 ],
306 "cache-control": [
307 "no-store"
308 ],
309 "x-content-type-options": [
310 "nosniff"
311 ]
312 },
313 "status": {
314 "code": 200,
315 "message": "OK"
316 },
317 "body": {
318 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
319 }
320 },
321 "request": {
322 "headers": {
323 "content-length": [
324 "594"
325 ],
326 "content-type": [
327 "application/x-www-form-urlencoded"
328 ],
329 "user-agent": [
330 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
331 ],
332 "accept": [
333 "application/mercurial-0.1"
334 ],
335 "host": [
336 "phab.mercurial-scm.org"
337 ]
338 },
339 "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%22d549263bcb2db54042adf048047b368f1ed246df%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%22d549263bcb2db54042adf048047b368f1ed246df%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%225cbade24e0fae40d67c568e86a978a2a946b9aed%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+21111%2C+%22name%22%3A+%22local%3Acommits%22%7D",
340 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
341 "method": "POST"
342 }
343 },
344 {
345 "response": {
346 "headers": {
347 "referrer-policy": [
348 "no-referrer"
349 ],
350 "x-frame-options": [
351 "Deny"
352 ],
353 "date": [
354 "Wed, 15 Apr 2020 23:43:47 GMT"
355 ],
356 "server": [
357 "Apache/2.4.10 (Debian)"
358 ],
359 "content-type": [
360 "application/json"
361 ],
362 "expires": [
363 "Sat, 01 Jan 2000 00:00:00 GMT"
364 ],
365 "transfer-encoding": [
366 "chunked"
367 ],
368 "x-xss-protection": [
369 "1; mode=block"
370 ],
371 "strict-transport-security": [
372 "max-age=0; includeSubdomains; preload"
373 ],
374 "cache-control": [
375 "no-store"
376 ],
377 "x-content-type-options": [
378 "nosniff"
379 ]
380 },
381 "status": {
382 "code": 200,
383 "message": "OK"
384 },
385 "body": {
386 "string": "{\"result\":{\"errors\":[],\"fields\":{\"title\":\"modified 1\"},\"revisionIDFieldInfo\":{\"value\":null,\"validDomain\":\"https:\\/\\/phab.mercurial-scm.org\"},\"transactions\":[{\"type\":\"title\",\"value\":\"modified 1\"}]},\"error_code\":null,\"error_info\":null}"
387 }
388 },
389 "request": {
390 "headers": {
391 "content-length": [
392 "155"
393 ],
394 "content-type": [
395 "application/x-www-form-urlencoded"
396 ],
397 "user-agent": [
398 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
399 ],
400 "accept": [
401 "application/mercurial-0.1"
402 ],
403 "host": [
404 "phab.mercurial-scm.org"
405 ]
406 },
407 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22corpus%22%3A+%22modified+1%22%7D",
408 "uri": "https://phab.mercurial-scm.org//api/differential.parsecommitmessage",
409 "method": "POST"
410 }
411 },
412 {
413 "response": {
414 "headers": {
415 "referrer-policy": [
416 "no-referrer"
417 ],
418 "x-frame-options": [
419 "Deny"
420 ],
421 "date": [
422 "Wed, 15 Apr 2020 23:43:47 GMT"
423 ],
424 "server": [
425 "Apache/2.4.10 (Debian)"
426 ],
427 "content-type": [
428 "application/json"
429 ],
430 "expires": [
431 "Sat, 01 Jan 2000 00:00:00 GMT"
432 ],
433 "transfer-encoding": [
434 "chunked"
435 ],
436 "x-xss-protection": [
437 "1; mode=block"
438 ],
439 "strict-transport-security": [
440 "max-age=0; includeSubdomains; preload"
441 ],
442 "cache-control": [
443 "no-store"
444 ],
445 "x-content-type-options": [
446 "nosniff"
447 ]
448 },
449 "status": {
450 "code": 200,
451 "message": "OK"
452 },
453 "body": {
454 "string": "{\"result\":{\"object\":{\"id\":8434,\"phid\":\"PHID-DREV-l5ocnglddqa4hwbdzcky\"},\"transactions\":[{\"phid\":\"PHID-XACT-DREV-ov2izjdfoe74m42\"},{\"phid\":\"PHID-XACT-DREV-yqeh2if4ea4pio4\"},{\"phid\":\"PHID-XACT-DREV-a74civfucvpjkpl\"},{\"phid\":\"PHID-XACT-DREV-s3ikmvejpazw2cd\"},{\"phid\":\"PHID-XACT-DREV-2wojx4fyghzgkgw\"}]},\"error_code\":null,\"error_info\":null}"
455 }
456 },
457 "request": {
458 "headers": {
459 "content-length": [
460 "308"
461 ],
462 "content-type": [
463 "application/x-www-form-urlencoded"
464 ],
465 "user-agent": [
466 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
467 ],
468 "accept": [
469 "application/mercurial-0.1"
470 ],
471 "host": [
472 "phab.mercurial-scm.org"
473 ]
474 },
475 "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-qat4sqpqqvytzhf7rpti%22%7D%2C+%7B%22type%22%3A+%22title%22%2C+%22value%22%3A+%22modified+1%22%7D%5D%7D",
476 "uri": "https://phab.mercurial-scm.org//api/differential.revision.edit",
477 "method": "POST"
478 }
479 },
480 {
481 "response": {
482 "headers": {
483 "referrer-policy": [
484 "no-referrer"
485 ],
486 "x-frame-options": [
487 "Deny"
488 ],
489 "date": [
490 "Wed, 15 Apr 2020 23:43:48 GMT"
491 ],
492 "server": [
493 "Apache/2.4.10 (Debian)"
494 ],
495 "content-type": [
496 "application/json"
497 ],
498 "expires": [
499 "Sat, 01 Jan 2000 00:00:00 GMT"
500 ],
501 "transfer-encoding": [
502 "chunked"
503 ],
504 "x-xss-protection": [
505 "1; mode=block"
506 ],
507 "strict-transport-security": [
508 "max-age=0; includeSubdomains; preload"
509 ],
510 "cache-control": [
511 "no-store"
512 ],
513 "x-content-type-options": [
514 "nosniff"
515 ]
516 },
517 "status": {
518 "code": 200,
519 "message": "OK"
520 },
521 "body": {
522 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
523 }
524 },
525 "request": {
526 "headers": {
527 "content-length": [
528 "488"
529 ],
530 "content-type": [
531 "application/x-www-form-urlencoded"
532 ],
533 "user-agent": [
534 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
535 ],
536 "accept": [
537 "application/mercurial-0.1"
538 ],
539 "host": [
540 "phab.mercurial-scm.org"
541 ]
542 },
543 "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%222b4aa8a88d617ca3ba0afd40e0e840b30588f126%5C%22%2C+%5C%22parent%5C%22%3A+%5C%22d549263bcb2db54042adf048047b368f1ed246df%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+%2221110%22%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
544 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
545 "method": "POST"
546 }
547 },
548 {
549 "response": {
550 "headers": {
551 "referrer-policy": [
552 "no-referrer"
553 ],
554 "x-frame-options": [
555 "Deny"
556 ],
557 "date": [
558 "Wed, 15 Apr 2020 23:43:48 GMT"
559 ],
560 "server": [
561 "Apache/2.4.10 (Debian)"
562 ],
563 "content-type": [
564 "application/json"
565 ],
566 "expires": [
567 "Sat, 01 Jan 2000 00:00:00 GMT"
568 ],
569 "transfer-encoding": [
570 "chunked"
571 ],
572 "x-xss-protection": [
573 "1; mode=block"
574 ],
575 "strict-transport-security": [
576 "max-age=0; includeSubdomains; preload"
577 ],
578 "cache-control": [
579 "no-store"
580 ],
581 "x-content-type-options": [
582 "nosniff"
583 ]
584 },
585 "status": {
586 "code": 200,
587 "message": "OK"
588 },
589 "body": {
590 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
591 }
592 },
593 "request": {
594 "headers": {
595 "content-length": [
596 "600"
597 ],
598 "content-type": [
599 "application/x-www-form-urlencoded"
600 ],
601 "user-agent": [
602 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
603 ],
604 "accept": [
605 "application/mercurial-0.1"
606 ],
607 "host": [
608 "phab.mercurial-scm.org"
609 ]
610 },
611 "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%222b4aa8a88d617ca3ba0afd40e0e840b30588f126%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%222b4aa8a88d617ca3ba0afd40e0e840b30588f126%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%22d549263bcb2db54042adf048047b368f1ed246df%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+%2221110%22%2C+%22name%22%3A+%22local%3Acommits%22%7D",
612 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
613 "method": "POST"
614 }
615 },
616 {
617 "response": {
618 "headers": {
619 "referrer-policy": [
620 "no-referrer"
621 ],
622 "x-frame-options": [
623 "Deny"
624 ],
625 "date": [
626 "Wed, 15 Apr 2020 23:43:49 GMT"
627 ],
628 "server": [
629 "Apache/2.4.10 (Debian)"
630 ],
631 "content-type": [
632 "application/json"
633 ],
634 "expires": [
635 "Sat, 01 Jan 2000 00:00:00 GMT"
636 ],
637 "transfer-encoding": [
638 "chunked"
639 ],
640 "x-xss-protection": [
641 "1; mode=block"
642 ],
643 "strict-transport-security": [
644 "max-age=0; includeSubdomains; preload"
645 ],
646 "cache-control": [
647 "no-store"
648 ],
649 "x-content-type-options": [
650 "nosniff"
651 ]
652 },
653 "status": {
654 "code": 200,
655 "message": "OK"
656 },
657 "body": {
658 "string": "{\"result\":{\"errors\":[],\"fields\":{\"title\":\"modified 2\",\"revisionID\":8433},\"revisionIDFieldInfo\":{\"value\":8433,\"validDomain\":\"https:\\/\\/phab.mercurial-scm.org\"},\"transactions\":[{\"type\":\"title\",\"value\":\"modified 2\"}]},\"error_code\":null,\"error_info\":null}"
659 }
660 },
661 "request": {
662 "headers": {
663 "content-length": [
664 "232"
665 ],
666 "content-type": [
667 "application/x-www-form-urlencoded"
668 ],
669 "user-agent": [
670 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
671 ],
672 "accept": [
673 "application/mercurial-0.1"
674 ],
675 "host": [
676 "phab.mercurial-scm.org"
677 ]
678 },
679 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22corpus%22%3A+%22modified+2%5Cn%5CnDifferential+Revision%3A+https%3A%2F%2Fphab.mercurial-scm.org%2FD8433%22%7D",
680 "uri": "https://phab.mercurial-scm.org//api/differential.parsecommitmessage",
681 "method": "POST"
682 }
683 },
684 {
685 "response": {
686 "headers": {
687 "referrer-policy": [
688 "no-referrer"
689 ],
690 "x-frame-options": [
691 "Deny"
692 ],
693 "date": [
694 "Wed, 15 Apr 2020 23:43:49 GMT"
695 ],
696 "server": [
697 "Apache/2.4.10 (Debian)"
698 ],
699 "content-type": [
700 "application/json"
701 ],
702 "expires": [
703 "Sat, 01 Jan 2000 00:00:00 GMT"
704 ],
705 "transfer-encoding": [
706 "chunked"
707 ],
708 "x-xss-protection": [
709 "1; mode=block"
710 ],
711 "strict-transport-security": [
712 "max-age=0; includeSubdomains; preload"
713 ],
714 "cache-control": [
715 "no-store"
716 ],
717 "x-content-type-options": [
718 "nosniff"
719 ]
720 },
721 "status": {
722 "code": 200,
723 "message": "OK"
724 },
725 "body": {
726 "string": "{\"result\":{\"object\":{\"id\":8433,\"phid\":\"PHID-DREV-kpkwhtylyxrzikfspl5r\"},\"transactions\":[{\"phid\":\"PHID-XACT-DREV-vhgtprwan3k3k6l\"}]},\"error_code\":null,\"error_info\":null}"
727 }
728 },
729 "request": {
730 "headers": {
731 "content-length": [
732 "353"
733 ],
734 "content-type": [
735 "application/x-www-form-urlencoded"
736 ],
737 "user-agent": [
738 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
739 ],
740 "accept": [
741 "application/mercurial-0.1"
742 ],
743 "host": [
744 "phab.mercurial-scm.org"
745 ]
746 },
747 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22objectIdentifier%22%3A+8433%2C+%22transactions%22%3A+%5B%7B%22type%22%3A+%22parents.set%22%2C+%22value%22%3A+%5B%22PHID-DREV-l5ocnglddqa4hwbdzcky%22%5D%7D%2C+%7B%22type%22%3A+%22title%22%2C+%22value%22%3A+%22modified+2%22%7D%5D%7D",
748 "uri": "https://phab.mercurial-scm.org//api/differential.revision.edit",
749 "method": "POST"
750 }
751 },
752 {
753 "response": {
754 "headers": {
755 "referrer-policy": [
756 "no-referrer"
757 ],
758 "x-frame-options": [
759 "Deny"
760 ],
761 "date": [
762 "Wed, 15 Apr 2020 23:43:50 GMT"
763 ],
764 "server": [
765 "Apache/2.4.10 (Debian)"
766 ],
767 "content-type": [
768 "application/json"
769 ],
770 "expires": [
771 "Sat, 01 Jan 2000 00:00:00 GMT"
772 ],
773 "transfer-encoding": [
774 "chunked"
775 ],
776 "x-xss-protection": [
777 "1; mode=block"
778 ],
779 "strict-transport-security": [
780 "max-age=0; includeSubdomains; preload"
781 ],
782 "cache-control": [
783 "no-store"
784 ],
785 "x-content-type-options": [
786 "nosniff"
787 ]
788 },
789 "status": {
790 "code": 200,
791 "message": "OK"
792 },
793 "body": {
794 "string": "{\"result\":[{\"id\":\"8434\",\"phid\":\"PHID-DREV-l5ocnglddqa4hwbdzcky\",\"title\":\"modified 1\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D8434\",\"dateCreated\":\"1586994227\",\"dateModified\":\"1586994229\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"0\",\"statusName\":\"Needs Review\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":1,\"lines.removed\":1},\"branch\":\"default\",\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"2\",\"activeDiffPHID\":\"PHID-DIFF-qat4sqpqqvytzhf7rpti\",\"diffs\":[\"21111\"],\"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\":\"\\/\"},{\"id\":\"8433\",\"phid\":\"PHID-DREV-kpkwhtylyxrzikfspl5r\",\"title\":\"modified 2\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D8433\",\"dateCreated\":\"1586994221\",\"dateModified\":\"1586994229\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"0\",\"statusName\":\"Needs Review\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":1,\"lines.removed\":1},\"branch\":\"default\",\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"2\",\"activeDiffPHID\":\"PHID-DIFF-g25jdc5b5khduwpp3p3b\",\"diffs\":[\"21110\"],\"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-l5ocnglddqa4hwbdzcky\"]},\"repositoryPHID\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"sourcePath\":\"\\/\"}],\"error_code\":null,\"error_info\":null}"
795 }
796 },
797 "request": {
798 "headers": {
799 "content-length": [
800 "154"
801 ],
802 "content-type": [
803 "application/x-www-form-urlencoded"
804 ],
805 "user-agent": [
806 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
807 ],
808 "accept": [
809 "application/mercurial-0.1"
810 ],
811 "host": [
812 "phab.mercurial-scm.org"
813 ]
814 },
815 "body": "output=json&__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22ids%22%3A+%5B8434%2C+8433%5D%7D",
816 "uri": "https://phab.mercurial-scm.org//api/differential.query",
817 "method": "POST"
818 }
819 },
820 {
821 "response": {
822 "headers": {
823 "referrer-policy": [
824 "no-referrer"
825 ],
826 "x-frame-options": [
827 "Deny"
828 ],
829 "date": [
830 "Wed, 15 Apr 2020 23:43:50 GMT"
831 ],
832 "server": [
833 "Apache/2.4.10 (Debian)"
834 ],
835 "content-type": [
836 "application/json"
837 ],
838 "expires": [
839 "Sat, 01 Jan 2000 00:00:00 GMT"
840 ],
841 "transfer-encoding": [
842 "chunked"
843 ],
844 "x-xss-protection": [
845 "1; mode=block"
846 ],
847 "strict-transport-security": [
848 "max-age=0; includeSubdomains; preload"
849 ],
850 "cache-control": [
851 "no-store"
852 ],
853 "x-content-type-options": [
854 "nosniff"
855 ]
856 },
857 "status": {
858 "code": 200,
859 "message": "OK"
860 },
861 "body": {
862 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
863 }
864 },
865 "request": {
866 "headers": {
867 "content-length": [
868 "482"
869 ],
870 "content-type": [
871 "application/x-www-form-urlencoded"
872 ],
873 "user-agent": [
874 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
875 ],
876 "accept": [
877 "application/mercurial-0.1"
878 ],
879 "host": [
880 "phab.mercurial-scm.org"
881 ]
882 },
883 "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%22876a60d024de493e35a1c6f963f2604056cdc0b9%5C%22%2C+%5C%22parent%5C%22%3A+%5C%225cbade24e0fae40d67c568e86a978a2a946b9aed%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+21111%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
884 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
885 "method": "POST"
886 }
887 },
888 {
889 "response": {
890 "headers": {
891 "referrer-policy": [
892 "no-referrer"
893 ],
894 "x-frame-options": [
895 "Deny"
896 ],
897 "date": [
898 "Wed, 15 Apr 2020 23:43:51 GMT"
899 ],
900 "server": [
901 "Apache/2.4.10 (Debian)"
902 ],
903 "content-type": [
904 "application/json"
905 ],
906 "expires": [
907 "Sat, 01 Jan 2000 00:00:00 GMT"
908 ],
909 "transfer-encoding": [
910 "chunked"
911 ],
912 "x-xss-protection": [
913 "1; mode=block"
914 ],
915 "strict-transport-security": [
916 "max-age=0; includeSubdomains; preload"
917 ],
918 "cache-control": [
919 "no-store"
920 ],
921 "x-content-type-options": [
922 "nosniff"
923 ]
924 },
925 "status": {
926 "code": 200,
927 "message": "OK"
928 },
929 "body": {
930 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
931 }
932 },
933 "request": {
934 "headers": {
935 "content-length": [
936 "594"
937 ],
938 "content-type": [
939 "application/x-www-form-urlencoded"
940 ],
941 "user-agent": [
942 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
943 ],
944 "accept": [
945 "application/mercurial-0.1"
946 ],
947 "host": [
948 "phab.mercurial-scm.org"
949 ]
950 },
951 "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%22876a60d024de493e35a1c6f963f2604056cdc0b9%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%22876a60d024de493e35a1c6f963f2604056cdc0b9%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%225cbade24e0fae40d67c568e86a978a2a946b9aed%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+21111%2C+%22name%22%3A+%22local%3Acommits%22%7D",
952 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
953 "method": "POST"
954 }
955 },
956 {
957 "response": {
958 "headers": {
959 "referrer-policy": [
960 "no-referrer"
961 ],
962 "x-frame-options": [
963 "Deny"
964 ],
965 "date": [
966 "Wed, 15 Apr 2020 23:43:51 GMT"
967 ],
968 "server": [
969 "Apache/2.4.10 (Debian)"
970 ],
971 "content-type": [
972 "application/json"
973 ],
974 "expires": [
975 "Sat, 01 Jan 2000 00:00:00 GMT"
976 ],
977 "transfer-encoding": [
978 "chunked"
979 ],
980 "x-xss-protection": [
981 "1; mode=block"
982 ],
983 "strict-transport-security": [
984 "max-age=0; includeSubdomains; preload"
985 ],
986 "cache-control": [
987 "no-store"
988 ],
989 "x-content-type-options": [
990 "nosniff"
991 ]
992 },
993 "status": {
994 "code": 200,
995 "message": "OK"
996 },
997 "body": {
998 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
999 }
1000 },
1001 "request": {
1002 "headers": {
1003 "content-length": [
1004 "488"
1005 ],
1006 "content-type": [
1007 "application/x-www-form-urlencoded"
1008 ],
1009 "user-agent": [
1010 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
1011 ],
1012 "accept": [
1013 "application/mercurial-0.1"
1014 ],
1015 "host": [
1016 "phab.mercurial-scm.org"
1017 ]
1018 },
1019 "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%220c6523cb1d0f560a958bcc0f4f938c91cb1141dc%5C%22%2C+%5C%22parent%5C%22%3A+%5C%22876a60d024de493e35a1c6f963f2604056cdc0b9%5C%22%2C+%5C%22user%5C%22%3A+%5C%22test%5C%22%7D%22%2C+%22diff_id%22%3A+%2221110%22%2C+%22name%22%3A+%22hg%3Ameta%22%7D",
1020 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
1021 "method": "POST"
1022 }
1023 },
1024 {
1025 "response": {
1026 "headers": {
1027 "referrer-policy": [
1028 "no-referrer"
1029 ],
1030 "x-frame-options": [
1031 "Deny"
1032 ],
1033 "date": [
1034 "Wed, 15 Apr 2020 23:43:52 GMT"
1035 ],
1036 "server": [
1037 "Apache/2.4.10 (Debian)"
1038 ],
1039 "content-type": [
1040 "application/json"
1041 ],
1042 "expires": [
1043 "Sat, 01 Jan 2000 00:00:00 GMT"
1044 ],
1045 "transfer-encoding": [
1046 "chunked"
1047 ],
1048 "x-xss-protection": [
1049 "1; mode=block"
1050 ],
1051 "strict-transport-security": [
1052 "max-age=0; includeSubdomains; preload"
1053 ],
1054 "cache-control": [
1055 "no-store"
1056 ],
1057 "x-content-type-options": [
1058 "nosniff"
1059 ]
1060 },
1061 "status": {
1062 "code": 200,
1063 "message": "OK"
1064 },
1065 "body": {
1066 "string": "{\"result\":null,\"error_code\":null,\"error_info\":null}"
1067 }
1068 },
1069 "request": {
1070 "headers": {
1071 "content-length": [
1072 "600"
1073 ],
1074 "content-type": [
1075 "application/x-www-form-urlencoded"
1076 ],
1077 "user-agent": [
1078 "mercurial/proto-1.0 (Mercurial 5.3.2+497-112d1ad30f88)"
1079 ],
1080 "accept": [
1081 "application/mercurial-0.1"
1082 ],
1083 "host": [
1084 "phab.mercurial-scm.org"
1085 ]
1086 },
1087 "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%220c6523cb1d0f560a958bcc0f4f938c91cb1141dc%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%220c6523cb1d0f560a958bcc0f4f938c91cb1141dc%5C%22%2C+%5C%22parents%5C%22%3A+%5B%5C%22876a60d024de493e35a1c6f963f2604056cdc0b9%5C%22%5D%2C+%5C%22time%5C%22%3A+0%7D%7D%22%2C+%22diff_id%22%3A+%2221110%22%2C+%22name%22%3A+%22local%3Acommits%22%7D",
1088 "uri": "https://phab.mercurial-scm.org//api/differential.setdiffproperty",
1089 "method": "POST"
1090 }
1091 }
1092 ]
1093 } No newline at end of file
@@ -1,2219 +1,2223 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, short
57 from mercurial.node import bin, nullid, short
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 copies,
64 copies,
65 encoding,
65 encoding,
66 error,
66 error,
67 exthelper,
67 exthelper,
68 graphmod,
68 graphmod,
69 httpconnection as httpconnectionmod,
69 httpconnection as httpconnectionmod,
70 localrepo,
70 localrepo,
71 logcmdutil,
71 logcmdutil,
72 match,
72 match,
73 mdiff,
73 mdiff,
74 obsutil,
74 obsutil,
75 parser,
75 parser,
76 patch,
76 patch,
77 phases,
77 phases,
78 pycompat,
78 pycompat,
79 scmutil,
79 scmutil,
80 smartset,
80 smartset,
81 tags,
81 tags,
82 templatefilters,
82 templatefilters,
83 templateutil,
83 templateutil,
84 url as urlmod,
84 url as urlmod,
85 util,
85 util,
86 )
86 )
87 from mercurial.utils import (
87 from mercurial.utils import (
88 procutil,
88 procutil,
89 stringutil,
89 stringutil,
90 )
90 )
91 from . import show
91 from . import show
92
92
93
93
94 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
94 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
95 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
95 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
96 # be specifying the version(s) of Mercurial they are tested with, or
96 # be specifying the version(s) of Mercurial they are tested with, or
97 # leave the attribute unspecified.
97 # leave the attribute unspecified.
98 testedwith = b'ships-with-hg-core'
98 testedwith = b'ships-with-hg-core'
99
99
100 eh = exthelper.exthelper()
100 eh = exthelper.exthelper()
101
101
102 cmdtable = eh.cmdtable
102 cmdtable = eh.cmdtable
103 command = eh.command
103 command = eh.command
104 configtable = eh.configtable
104 configtable = eh.configtable
105 templatekeyword = eh.templatekeyword
105 templatekeyword = eh.templatekeyword
106 uisetup = eh.finaluisetup
106 uisetup = eh.finaluisetup
107
107
108 # developer config: phabricator.batchsize
108 # developer config: phabricator.batchsize
109 eh.configitem(
109 eh.configitem(
110 b'phabricator', b'batchsize', default=12,
110 b'phabricator', b'batchsize', default=12,
111 )
111 )
112 eh.configitem(
112 eh.configitem(
113 b'phabricator', b'callsign', default=None,
113 b'phabricator', b'callsign', default=None,
114 )
114 )
115 eh.configitem(
115 eh.configitem(
116 b'phabricator', b'curlcmd', default=None,
116 b'phabricator', b'curlcmd', default=None,
117 )
117 )
118 # developer config: phabricator.debug
118 # developer config: phabricator.debug
119 eh.configitem(
119 eh.configitem(
120 b'phabricator', b'debug', default=False,
120 b'phabricator', b'debug', default=False,
121 )
121 )
122 # developer config: phabricator.repophid
122 # developer config: phabricator.repophid
123 eh.configitem(
123 eh.configitem(
124 b'phabricator', b'repophid', default=None,
124 b'phabricator', b'repophid', default=None,
125 )
125 )
126 eh.configitem(
126 eh.configitem(
127 b'phabricator', b'url', default=None,
127 b'phabricator', b'url', default=None,
128 )
128 )
129 eh.configitem(
129 eh.configitem(
130 b'phabsend', b'confirm', default=False,
130 b'phabsend', b'confirm', default=False,
131 )
131 )
132 eh.configitem(
132 eh.configitem(
133 b'phabimport', b'secret', default=False,
133 b'phabimport', b'secret', default=False,
134 )
134 )
135 eh.configitem(
135 eh.configitem(
136 b'phabimport', b'obsolete', default=False,
136 b'phabimport', b'obsolete', default=False,
137 )
137 )
138
138
139 colortable = {
139 colortable = {
140 b'phabricator.action.created': b'green',
140 b'phabricator.action.created': b'green',
141 b'phabricator.action.skipped': b'magenta',
141 b'phabricator.action.skipped': b'magenta',
142 b'phabricator.action.updated': b'magenta',
142 b'phabricator.action.updated': b'magenta',
143 b'phabricator.desc': b'',
143 b'phabricator.desc': b'',
144 b'phabricator.drev': b'bold',
144 b'phabricator.drev': b'bold',
145 b'phabricator.node': b'',
145 b'phabricator.node': b'',
146 b'phabricator.status.abandoned': b'magenta dim',
146 b'phabricator.status.abandoned': b'magenta dim',
147 b'phabricator.status.accepted': b'green bold',
147 b'phabricator.status.accepted': b'green bold',
148 b'phabricator.status.closed': b'green',
148 b'phabricator.status.closed': b'green',
149 b'phabricator.status.needsreview': b'yellow',
149 b'phabricator.status.needsreview': b'yellow',
150 b'phabricator.status.needsrevision': b'red',
150 b'phabricator.status.needsrevision': b'red',
151 b'phabricator.status.changesplanned': b'red',
151 b'phabricator.status.changesplanned': b'red',
152 }
152 }
153
153
154 _VCR_FLAGS = [
154 _VCR_FLAGS = [
155 (
155 (
156 b'',
156 b'',
157 b'test-vcr',
157 b'test-vcr',
158 b'',
158 b'',
159 _(
159 _(
160 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
160 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
161 b', otherwise will mock all http requests using the specified vcr file.'
161 b', otherwise will mock all http requests using the specified vcr file.'
162 b' (ADVANCED)'
162 b' (ADVANCED)'
163 ),
163 ),
164 ),
164 ),
165 ]
165 ]
166
166
167
167
168 @eh.wrapfunction(localrepo, "loadhgrc")
168 @eh.wrapfunction(localrepo, "loadhgrc")
169 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):
169 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):
170 """Load ``.arcconfig`` content into a ui instance on repository open.
170 """Load ``.arcconfig`` content into a ui instance on repository open.
171 """
171 """
172 result = False
172 result = False
173 arcconfig = {}
173 arcconfig = {}
174
174
175 try:
175 try:
176 # json.loads only accepts bytes from 3.6+
176 # json.loads only accepts bytes from 3.6+
177 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
177 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
178 # json.loads only returns unicode strings
178 # json.loads only returns unicode strings
179 arcconfig = pycompat.rapply(
179 arcconfig = pycompat.rapply(
180 lambda x: encoding.unitolocal(x)
180 lambda x: encoding.unitolocal(x)
181 if isinstance(x, pycompat.unicode)
181 if isinstance(x, pycompat.unicode)
182 else x,
182 else x,
183 pycompat.json_loads(rawparams),
183 pycompat.json_loads(rawparams),
184 )
184 )
185
185
186 result = True
186 result = True
187 except ValueError:
187 except ValueError:
188 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
188 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
189 except IOError:
189 except IOError:
190 pass
190 pass
191
191
192 cfg = util.sortdict()
192 cfg = util.sortdict()
193
193
194 if b"repository.callsign" in arcconfig:
194 if b"repository.callsign" in arcconfig:
195 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
195 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
196
196
197 if b"phabricator.uri" in arcconfig:
197 if b"phabricator.uri" in arcconfig:
198 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
198 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
199
199
200 if cfg:
200 if cfg:
201 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
201 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
202
202
203 return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg/hgrc
203 return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg/hgrc
204
204
205
205
206 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
206 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
207 fullflags = flags + _VCR_FLAGS
207 fullflags = flags + _VCR_FLAGS
208
208
209 def hgmatcher(r1, r2):
209 def hgmatcher(r1, r2):
210 if r1.uri != r2.uri or r1.method != r2.method:
210 if r1.uri != r2.uri or r1.method != r2.method:
211 return False
211 return False
212 r1params = util.urlreq.parseqs(r1.body)
212 r1params = util.urlreq.parseqs(r1.body)
213 r2params = util.urlreq.parseqs(r2.body)
213 r2params = util.urlreq.parseqs(r2.body)
214 for key in r1params:
214 for key in r1params:
215 if key not in r2params:
215 if key not in r2params:
216 return False
216 return False
217 value = r1params[key][0]
217 value = r1params[key][0]
218 # we want to compare json payloads without worrying about ordering
218 # we want to compare json payloads without worrying about ordering
219 if value.startswith(b'{') and value.endswith(b'}'):
219 if value.startswith(b'{') and value.endswith(b'}'):
220 r1json = pycompat.json_loads(value)
220 r1json = pycompat.json_loads(value)
221 r2json = pycompat.json_loads(r2params[key][0])
221 r2json = pycompat.json_loads(r2params[key][0])
222 if r1json != r2json:
222 if r1json != r2json:
223 return False
223 return False
224 elif r2params[key][0] != value:
224 elif r2params[key][0] != value:
225 return False
225 return False
226 return True
226 return True
227
227
228 def sanitiserequest(request):
228 def sanitiserequest(request):
229 request.body = re.sub(
229 request.body = re.sub(
230 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
230 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
231 )
231 )
232 return request
232 return request
233
233
234 def sanitiseresponse(response):
234 def sanitiseresponse(response):
235 if 'set-cookie' in response['headers']:
235 if 'set-cookie' in response['headers']:
236 del response['headers']['set-cookie']
236 del response['headers']['set-cookie']
237 return response
237 return response
238
238
239 def decorate(fn):
239 def decorate(fn):
240 def inner(*args, **kwargs):
240 def inner(*args, **kwargs):
241 cassette = pycompat.fsdecode(kwargs.pop('test_vcr', None))
241 cassette = pycompat.fsdecode(kwargs.pop('test_vcr', None))
242 if cassette:
242 if cassette:
243 import hgdemandimport
243 import hgdemandimport
244
244
245 with hgdemandimport.deactivated():
245 with hgdemandimport.deactivated():
246 import vcr as vcrmod
246 import vcr as vcrmod
247 import vcr.stubs as stubs
247 import vcr.stubs as stubs
248
248
249 vcr = vcrmod.VCR(
249 vcr = vcrmod.VCR(
250 serializer='json',
250 serializer='json',
251 before_record_request=sanitiserequest,
251 before_record_request=sanitiserequest,
252 before_record_response=sanitiseresponse,
252 before_record_response=sanitiseresponse,
253 custom_patches=[
253 custom_patches=[
254 (
254 (
255 urlmod,
255 urlmod,
256 'httpconnection',
256 'httpconnection',
257 stubs.VCRHTTPConnection,
257 stubs.VCRHTTPConnection,
258 ),
258 ),
259 (
259 (
260 urlmod,
260 urlmod,
261 'httpsconnection',
261 'httpsconnection',
262 stubs.VCRHTTPSConnection,
262 stubs.VCRHTTPSConnection,
263 ),
263 ),
264 ],
264 ],
265 )
265 )
266 vcr.register_matcher('hgmatcher', hgmatcher)
266 vcr.register_matcher('hgmatcher', hgmatcher)
267 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
267 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
268 return fn(*args, **kwargs)
268 return fn(*args, **kwargs)
269 return fn(*args, **kwargs)
269 return fn(*args, **kwargs)
270
270
271 cmd = util.checksignature(inner, depth=2)
271 cmd = util.checksignature(inner, depth=2)
272 cmd.__name__ = fn.__name__
272 cmd.__name__ = fn.__name__
273 cmd.__doc__ = fn.__doc__
273 cmd.__doc__ = fn.__doc__
274
274
275 return command(
275 return command(
276 name,
276 name,
277 fullflags,
277 fullflags,
278 spec,
278 spec,
279 helpcategory=helpcategory,
279 helpcategory=helpcategory,
280 optionalrepo=optionalrepo,
280 optionalrepo=optionalrepo,
281 )(cmd)
281 )(cmd)
282
282
283 return decorate
283 return decorate
284
284
285
285
286 def _debug(ui, *msg, **opts):
286 def _debug(ui, *msg, **opts):
287 """write debug output for Phabricator if ``phabricator.debug`` is set
287 """write debug output for Phabricator if ``phabricator.debug`` is set
288
288
289 Specifically, this avoids dumping Conduit and HTTP auth chatter that is
289 Specifically, this avoids dumping Conduit and HTTP auth chatter that is
290 printed with the --debug argument.
290 printed with the --debug argument.
291 """
291 """
292 if ui.configbool(b"phabricator", b"debug"):
292 if ui.configbool(b"phabricator", b"debug"):
293 flag = ui.debugflag
293 flag = ui.debugflag
294 try:
294 try:
295 ui.debugflag = True
295 ui.debugflag = True
296 ui.write(*msg, **opts)
296 ui.write(*msg, **opts)
297 finally:
297 finally:
298 ui.debugflag = flag
298 ui.debugflag = flag
299
299
300
300
301 def urlencodenested(params):
301 def urlencodenested(params):
302 """like urlencode, but works with nested parameters.
302 """like urlencode, but works with nested parameters.
303
303
304 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
304 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
305 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
305 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
306 urlencode. Note: the encoding is consistent with PHP's http_build_query.
306 urlencode. Note: the encoding is consistent with PHP's http_build_query.
307 """
307 """
308 flatparams = util.sortdict()
308 flatparams = util.sortdict()
309
309
310 def process(prefix, obj):
310 def process(prefix, obj):
311 if isinstance(obj, bool):
311 if isinstance(obj, bool):
312 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
312 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
313 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
313 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
314 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
314 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
315 if items is None:
315 if items is None:
316 flatparams[prefix] = obj
316 flatparams[prefix] = obj
317 else:
317 else:
318 for k, v in items(obj):
318 for k, v in items(obj):
319 if prefix:
319 if prefix:
320 process(b'%s[%s]' % (prefix, k), v)
320 process(b'%s[%s]' % (prefix, k), v)
321 else:
321 else:
322 process(k, v)
322 process(k, v)
323
323
324 process(b'', params)
324 process(b'', params)
325 return util.urlreq.urlencode(flatparams)
325 return util.urlreq.urlencode(flatparams)
326
326
327
327
328 def readurltoken(ui):
328 def readurltoken(ui):
329 """return conduit url, token and make sure they exist
329 """return conduit url, token and make sure they exist
330
330
331 Currently read from [auth] config section. In the future, it might
331 Currently read from [auth] config section. In the future, it might
332 make sense to read from .arcconfig and .arcrc as well.
332 make sense to read from .arcconfig and .arcrc as well.
333 """
333 """
334 url = ui.config(b'phabricator', b'url')
334 url = ui.config(b'phabricator', b'url')
335 if not url:
335 if not url:
336 raise error.Abort(
336 raise error.Abort(
337 _(b'config %s.%s is required') % (b'phabricator', b'url')
337 _(b'config %s.%s is required') % (b'phabricator', b'url')
338 )
338 )
339
339
340 res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)
340 res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)
341 token = None
341 token = None
342
342
343 if res:
343 if res:
344 group, auth = res
344 group, auth = res
345
345
346 ui.debug(b"using auth.%s.* for authentication\n" % group)
346 ui.debug(b"using auth.%s.* for authentication\n" % group)
347
347
348 token = auth.get(b'phabtoken')
348 token = auth.get(b'phabtoken')
349
349
350 if not token:
350 if not token:
351 raise error.Abort(
351 raise error.Abort(
352 _(b'Can\'t find conduit token associated to %s') % (url,)
352 _(b'Can\'t find conduit token associated to %s') % (url,)
353 )
353 )
354
354
355 return url, token
355 return url, token
356
356
357
357
358 def callconduit(ui, name, params):
358 def callconduit(ui, name, params):
359 """call Conduit API, params is a dict. return json.loads result, or None"""
359 """call Conduit API, params is a dict. return json.loads result, or None"""
360 host, token = readurltoken(ui)
360 host, token = readurltoken(ui)
361 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
361 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
362 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
362 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
363 params = params.copy()
363 params = params.copy()
364 params[b'__conduit__'] = {
364 params[b'__conduit__'] = {
365 b'token': token,
365 b'token': token,
366 }
366 }
367 rawdata = {
367 rawdata = {
368 b'params': templatefilters.json(params),
368 b'params': templatefilters.json(params),
369 b'output': b'json',
369 b'output': b'json',
370 b'__conduit__': 1,
370 b'__conduit__': 1,
371 }
371 }
372 data = urlencodenested(rawdata)
372 data = urlencodenested(rawdata)
373 curlcmd = ui.config(b'phabricator', b'curlcmd')
373 curlcmd = ui.config(b'phabricator', b'curlcmd')
374 if curlcmd:
374 if curlcmd:
375 sin, sout = procutil.popen2(
375 sin, sout = procutil.popen2(
376 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
376 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
377 )
377 )
378 sin.write(data)
378 sin.write(data)
379 sin.close()
379 sin.close()
380 body = sout.read()
380 body = sout.read()
381 else:
381 else:
382 urlopener = urlmod.opener(ui, authinfo)
382 urlopener = urlmod.opener(ui, authinfo)
383 request = util.urlreq.request(pycompat.strurl(url), data=data)
383 request = util.urlreq.request(pycompat.strurl(url), data=data)
384 with contextlib.closing(urlopener.open(request)) as rsp:
384 with contextlib.closing(urlopener.open(request)) as rsp:
385 body = rsp.read()
385 body = rsp.read()
386 ui.debug(b'Conduit Response: %s\n' % body)
386 ui.debug(b'Conduit Response: %s\n' % body)
387 parsed = pycompat.rapply(
387 parsed = pycompat.rapply(
388 lambda x: encoding.unitolocal(x)
388 lambda x: encoding.unitolocal(x)
389 if isinstance(x, pycompat.unicode)
389 if isinstance(x, pycompat.unicode)
390 else x,
390 else x,
391 # json.loads only accepts bytes from py3.6+
391 # json.loads only accepts bytes from py3.6+
392 pycompat.json_loads(encoding.unifromlocal(body)),
392 pycompat.json_loads(encoding.unifromlocal(body)),
393 )
393 )
394 if parsed.get(b'error_code'):
394 if parsed.get(b'error_code'):
395 msg = _(b'Conduit Error (%s): %s') % (
395 msg = _(b'Conduit Error (%s): %s') % (
396 parsed[b'error_code'],
396 parsed[b'error_code'],
397 parsed[b'error_info'],
397 parsed[b'error_info'],
398 )
398 )
399 raise error.Abort(msg)
399 raise error.Abort(msg)
400 return parsed[b'result']
400 return parsed[b'result']
401
401
402
402
403 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
403 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
404 def debugcallconduit(ui, repo, name):
404 def debugcallconduit(ui, repo, name):
405 """call Conduit API
405 """call Conduit API
406
406
407 Call parameters are read from stdin as a JSON blob. Result will be written
407 Call parameters are read from stdin as a JSON blob. Result will be written
408 to stdout as a JSON blob.
408 to stdout as a JSON blob.
409 """
409 """
410 # json.loads only accepts bytes from 3.6+
410 # json.loads only accepts bytes from 3.6+
411 rawparams = encoding.unifromlocal(ui.fin.read())
411 rawparams = encoding.unifromlocal(ui.fin.read())
412 # json.loads only returns unicode strings
412 # json.loads only returns unicode strings
413 params = pycompat.rapply(
413 params = pycompat.rapply(
414 lambda x: encoding.unitolocal(x)
414 lambda x: encoding.unitolocal(x)
415 if isinstance(x, pycompat.unicode)
415 if isinstance(x, pycompat.unicode)
416 else x,
416 else x,
417 pycompat.json_loads(rawparams),
417 pycompat.json_loads(rawparams),
418 )
418 )
419 # json.dumps only accepts unicode strings
419 # json.dumps only accepts unicode strings
420 result = pycompat.rapply(
420 result = pycompat.rapply(
421 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
421 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
422 callconduit(ui, name, params),
422 callconduit(ui, name, params),
423 )
423 )
424 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
424 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
425 ui.write(b'%s\n' % encoding.unitolocal(s))
425 ui.write(b'%s\n' % encoding.unitolocal(s))
426
426
427
427
428 def getrepophid(repo):
428 def getrepophid(repo):
429 """given callsign, return repository PHID or None"""
429 """given callsign, return repository PHID or None"""
430 # developer config: phabricator.repophid
430 # developer config: phabricator.repophid
431 repophid = repo.ui.config(b'phabricator', b'repophid')
431 repophid = repo.ui.config(b'phabricator', b'repophid')
432 if repophid:
432 if repophid:
433 return repophid
433 return repophid
434 callsign = repo.ui.config(b'phabricator', b'callsign')
434 callsign = repo.ui.config(b'phabricator', b'callsign')
435 if not callsign:
435 if not callsign:
436 return None
436 return None
437 query = callconduit(
437 query = callconduit(
438 repo.ui,
438 repo.ui,
439 b'diffusion.repository.search',
439 b'diffusion.repository.search',
440 {b'constraints': {b'callsigns': [callsign]}},
440 {b'constraints': {b'callsigns': [callsign]}},
441 )
441 )
442 if len(query[b'data']) == 0:
442 if len(query[b'data']) == 0:
443 return None
443 return None
444 repophid = query[b'data'][0][b'phid']
444 repophid = query[b'data'][0][b'phid']
445 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
445 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
446 return repophid
446 return repophid
447
447
448
448
449 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
449 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
450 _differentialrevisiondescre = re.compile(
450 _differentialrevisiondescre = re.compile(
451 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
451 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
452 )
452 )
453
453
454
454
455 def getoldnodedrevmap(repo, nodelist):
455 def getoldnodedrevmap(repo, nodelist):
456 """find previous nodes that has been sent to Phabricator
456 """find previous nodes that has been sent to Phabricator
457
457
458 return {node: (oldnode, Differential diff, Differential Revision ID)}
458 return {node: (oldnode, Differential diff, Differential Revision ID)}
459 for node in nodelist with known previous sent versions, or associated
459 for node in nodelist with known previous sent versions, or associated
460 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
460 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
461 be ``None``.
461 be ``None``.
462
462
463 Examines commit messages like "Differential Revision:" to get the
463 Examines commit messages like "Differential Revision:" to get the
464 association information.
464 association information.
465
465
466 If such commit message line is not found, examines all precursors and their
466 If such commit message line is not found, examines all precursors and their
467 tags. Tags with format like "D1234" are considered a match and the node
467 tags. Tags with format like "D1234" are considered a match and the node
468 with that tag, and the number after "D" (ex. 1234) will be returned.
468 with that tag, and the number after "D" (ex. 1234) will be returned.
469
469
470 The ``old node``, if not None, is guaranteed to be the last diff of
470 The ``old node``, if not None, is guaranteed to be the last diff of
471 corresponding Differential Revision, and exist in the repo.
471 corresponding Differential Revision, and exist in the repo.
472 """
472 """
473 unfi = repo.unfiltered()
473 unfi = repo.unfiltered()
474 has_node = unfi.changelog.index.has_node
474 has_node = unfi.changelog.index.has_node
475
475
476 result = {} # {node: (oldnode?, lastdiff?, drev)}
476 result = {} # {node: (oldnode?, lastdiff?, drev)}
477 # ordered for test stability when printing new -> old mapping below
477 # ordered for test stability when printing new -> old mapping below
478 toconfirm = util.sortdict() # {node: (force, {precnode}, drev)}
478 toconfirm = util.sortdict() # {node: (force, {precnode}, drev)}
479 for node in nodelist:
479 for node in nodelist:
480 ctx = unfi[node]
480 ctx = unfi[node]
481 # For tags like "D123", put them into "toconfirm" to verify later
481 # For tags like "D123", put them into "toconfirm" to verify later
482 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
482 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
483 for n in precnodes:
483 for n in precnodes:
484 if has_node(n):
484 if has_node(n):
485 for tag in unfi.nodetags(n):
485 for tag in unfi.nodetags(n):
486 m = _differentialrevisiontagre.match(tag)
486 m = _differentialrevisiontagre.match(tag)
487 if m:
487 if m:
488 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
488 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
489 break
489 break
490 else:
490 else:
491 continue # move to next predecessor
491 continue # move to next predecessor
492 break # found a tag, stop
492 break # found a tag, stop
493 else:
493 else:
494 # Check commit message
494 # Check commit message
495 m = _differentialrevisiondescre.search(ctx.description())
495 m = _differentialrevisiondescre.search(ctx.description())
496 if m:
496 if m:
497 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
497 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
498
498
499 # Double check if tags are genuine by collecting all old nodes from
499 # Double check if tags are genuine by collecting all old nodes from
500 # Phabricator, and expect precursors overlap with it.
500 # Phabricator, and expect precursors overlap with it.
501 if toconfirm:
501 if toconfirm:
502 drevs = [drev for force, precs, drev in toconfirm.values()]
502 drevs = [drev for force, precs, drev in toconfirm.values()]
503 alldiffs = callconduit(
503 alldiffs = callconduit(
504 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
504 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
505 )
505 )
506
506
507 def getnodes(d, precset):
507 def getnodes(d, precset):
508 # Ignore other nodes that were combined into the Differential
508 # Ignore other nodes that were combined into the Differential
509 # that aren't predecessors of the current local node.
509 # that aren't predecessors of the current local node.
510 return [n for n in getlocalcommits(d) if n in precset]
510 return [n for n in getlocalcommits(d) if n in precset]
511
511
512 for newnode, (force, precset, drev) in toconfirm.items():
512 for newnode, (force, precset, drev) in toconfirm.items():
513 diffs = [
513 diffs = [
514 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
514 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
515 ]
515 ]
516
516
517 # local predecessors known by Phabricator
517 # local predecessors known by Phabricator
518 phprecset = {n for d in diffs for n in getnodes(d, precset)}
518 phprecset = {n for d in diffs for n in getnodes(d, precset)}
519
519
520 # Ignore if precursors (Phabricator and local repo) do not overlap,
520 # Ignore if precursors (Phabricator and local repo) do not overlap,
521 # and force is not set (when commit message says nothing)
521 # and force is not set (when commit message says nothing)
522 if not force and not phprecset:
522 if not force and not phprecset:
523 tagname = b'D%d' % drev
523 tagname = b'D%d' % drev
524 tags.tag(
524 tags.tag(
525 repo,
525 repo,
526 tagname,
526 tagname,
527 nullid,
527 nullid,
528 message=None,
528 message=None,
529 user=None,
529 user=None,
530 date=None,
530 date=None,
531 local=True,
531 local=True,
532 )
532 )
533 unfi.ui.warn(
533 unfi.ui.warn(
534 _(
534 _(
535 b'D%d: local tag removed - does not match '
535 b'D%d: local tag removed - does not match '
536 b'Differential history\n'
536 b'Differential history\n'
537 )
537 )
538 % drev
538 % drev
539 )
539 )
540 continue
540 continue
541
541
542 # Find the last node using Phabricator metadata, and make sure it
542 # Find the last node using Phabricator metadata, and make sure it
543 # exists in the repo
543 # exists in the repo
544 oldnode = lastdiff = None
544 oldnode = lastdiff = None
545 if diffs:
545 if diffs:
546 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
546 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
547 oldnodes = getnodes(lastdiff, precset)
547 oldnodes = getnodes(lastdiff, precset)
548
548
549 _debug(
549 _debug(
550 unfi.ui,
550 unfi.ui,
551 b"%s mapped to old nodes %s\n"
551 b"%s mapped to old nodes %s\n"
552 % (
552 % (
553 short(newnode),
553 short(newnode),
554 stringutil.pprint([short(n) for n in sorted(oldnodes)]),
554 stringutil.pprint([short(n) for n in sorted(oldnodes)]),
555 ),
555 ),
556 )
556 )
557
557
558 # If this commit was the result of `hg fold` after submission,
558 # If this commit was the result of `hg fold` after submission,
559 # and now resubmitted with --fold, the easiest thing to do is
559 # and now resubmitted with --fold, the easiest thing to do is
560 # to leave the node clear. This only results in creating a new
560 # to leave the node clear. This only results in creating a new
561 # diff for the _same_ Differential Revision if this commit is
561 # diff for the _same_ Differential Revision if this commit is
562 # the first or last in the selected range. If we picked a node
562 # the first or last in the selected range. If we picked a node
563 # from the list instead, it would have to be the lowest if at
563 # from the list instead, it would have to be the lowest if at
564 # the beginning of the --fold range, or the highest at the end.
564 # the beginning of the --fold range, or the highest at the end.
565 # Otherwise, one or more of the nodes wouldn't be considered in
565 # Otherwise, one or more of the nodes wouldn't be considered in
566 # the diff, and the Differential wouldn't be properly updated.
566 # the diff, and the Differential wouldn't be properly updated.
567 # If this commit is the result of `hg split` in the same
567 # If this commit is the result of `hg split` in the same
568 # scenario, there is a single oldnode here (and multiple
568 # scenario, there is a single oldnode here (and multiple
569 # newnodes mapped to it). That makes it the same as the normal
569 # newnodes mapped to it). That makes it the same as the normal
570 # case, as the edges of the newnode range cleanly maps to one
570 # case, as the edges of the newnode range cleanly maps to one
571 # oldnode each.
571 # oldnode each.
572 if len(oldnodes) == 1:
572 if len(oldnodes) == 1:
573 oldnode = oldnodes[0]
573 oldnode = oldnodes[0]
574 if oldnode and not has_node(oldnode):
574 if oldnode and not has_node(oldnode):
575 oldnode = None
575 oldnode = None
576
576
577 result[newnode] = (oldnode, lastdiff, drev)
577 result[newnode] = (oldnode, lastdiff, drev)
578
578
579 return result
579 return result
580
580
581
581
582 def getdrevmap(repo, revs):
582 def getdrevmap(repo, revs):
583 """Return a dict mapping each rev in `revs` to their Differential Revision
583 """Return a dict mapping each rev in `revs` to their Differential Revision
584 ID or None.
584 ID or None.
585 """
585 """
586 result = {}
586 result = {}
587 for rev in revs:
587 for rev in revs:
588 result[rev] = None
588 result[rev] = None
589 ctx = repo[rev]
589 ctx = repo[rev]
590 # Check commit message
590 # Check commit message
591 m = _differentialrevisiondescre.search(ctx.description())
591 m = _differentialrevisiondescre.search(ctx.description())
592 if m:
592 if m:
593 result[rev] = int(m.group('id'))
593 result[rev] = int(m.group('id'))
594 continue
594 continue
595 # Check tags
595 # Check tags
596 for tag in repo.nodetags(ctx.node()):
596 for tag in repo.nodetags(ctx.node()):
597 m = _differentialrevisiontagre.match(tag)
597 m = _differentialrevisiontagre.match(tag)
598 if m:
598 if m:
599 result[rev] = int(m.group(1))
599 result[rev] = int(m.group(1))
600 break
600 break
601
601
602 return result
602 return result
603
603
604
604
605 def getdiff(basectx, ctx, diffopts):
605 def getdiff(basectx, ctx, diffopts):
606 """plain-text diff without header (user, commit message, etc)"""
606 """plain-text diff without header (user, commit message, etc)"""
607 output = util.stringio()
607 output = util.stringio()
608 for chunk, _label in patch.diffui(
608 for chunk, _label in patch.diffui(
609 ctx.repo(), basectx.p1().node(), ctx.node(), None, opts=diffopts
609 ctx.repo(), basectx.p1().node(), ctx.node(), None, opts=diffopts
610 ):
610 ):
611 output.write(chunk)
611 output.write(chunk)
612 return output.getvalue()
612 return output.getvalue()
613
613
614
614
615 class DiffChangeType(object):
615 class DiffChangeType(object):
616 ADD = 1
616 ADD = 1
617 CHANGE = 2
617 CHANGE = 2
618 DELETE = 3
618 DELETE = 3
619 MOVE_AWAY = 4
619 MOVE_AWAY = 4
620 COPY_AWAY = 5
620 COPY_AWAY = 5
621 MOVE_HERE = 6
621 MOVE_HERE = 6
622 COPY_HERE = 7
622 COPY_HERE = 7
623 MULTICOPY = 8
623 MULTICOPY = 8
624
624
625
625
626 class DiffFileType(object):
626 class DiffFileType(object):
627 TEXT = 1
627 TEXT = 1
628 IMAGE = 2
628 IMAGE = 2
629 BINARY = 3
629 BINARY = 3
630
630
631
631
632 @attr.s
632 @attr.s
633 class phabhunk(dict):
633 class phabhunk(dict):
634 """Represents a Differential hunk, which is owned by a Differential change
634 """Represents a Differential hunk, which is owned by a Differential change
635 """
635 """
636
636
637 oldOffset = attr.ib(default=0) # camelcase-required
637 oldOffset = attr.ib(default=0) # camelcase-required
638 oldLength = attr.ib(default=0) # camelcase-required
638 oldLength = attr.ib(default=0) # camelcase-required
639 newOffset = attr.ib(default=0) # camelcase-required
639 newOffset = attr.ib(default=0) # camelcase-required
640 newLength = attr.ib(default=0) # camelcase-required
640 newLength = attr.ib(default=0) # camelcase-required
641 corpus = attr.ib(default='')
641 corpus = attr.ib(default='')
642 # These get added to the phabchange's equivalents
642 # These get added to the phabchange's equivalents
643 addLines = attr.ib(default=0) # camelcase-required
643 addLines = attr.ib(default=0) # camelcase-required
644 delLines = attr.ib(default=0) # camelcase-required
644 delLines = attr.ib(default=0) # camelcase-required
645
645
646
646
647 @attr.s
647 @attr.s
648 class phabchange(object):
648 class phabchange(object):
649 """Represents a Differential change, owns Differential hunks and owned by a
649 """Represents a Differential change, owns Differential hunks and owned by a
650 Differential diff. Each one represents one file in a diff.
650 Differential diff. Each one represents one file in a diff.
651 """
651 """
652
652
653 currentPath = attr.ib(default=None) # camelcase-required
653 currentPath = attr.ib(default=None) # camelcase-required
654 oldPath = attr.ib(default=None) # camelcase-required
654 oldPath = attr.ib(default=None) # camelcase-required
655 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
655 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
656 metadata = attr.ib(default=attr.Factory(dict))
656 metadata = attr.ib(default=attr.Factory(dict))
657 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
657 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
658 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
658 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
659 type = attr.ib(default=DiffChangeType.CHANGE)
659 type = attr.ib(default=DiffChangeType.CHANGE)
660 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
660 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
661 commitHash = attr.ib(default=None) # camelcase-required
661 commitHash = attr.ib(default=None) # camelcase-required
662 addLines = attr.ib(default=0) # camelcase-required
662 addLines = attr.ib(default=0) # camelcase-required
663 delLines = attr.ib(default=0) # camelcase-required
663 delLines = attr.ib(default=0) # camelcase-required
664 hunks = attr.ib(default=attr.Factory(list))
664 hunks = attr.ib(default=attr.Factory(list))
665
665
666 def copynewmetadatatoold(self):
666 def copynewmetadatatoold(self):
667 for key in list(self.metadata.keys()):
667 for key in list(self.metadata.keys()):
668 newkey = key.replace(b'new:', b'old:')
668 newkey = key.replace(b'new:', b'old:')
669 self.metadata[newkey] = self.metadata[key]
669 self.metadata[newkey] = self.metadata[key]
670
670
671 def addoldmode(self, value):
671 def addoldmode(self, value):
672 self.oldProperties[b'unix:filemode'] = value
672 self.oldProperties[b'unix:filemode'] = value
673
673
674 def addnewmode(self, value):
674 def addnewmode(self, value):
675 self.newProperties[b'unix:filemode'] = value
675 self.newProperties[b'unix:filemode'] = value
676
676
677 def addhunk(self, hunk):
677 def addhunk(self, hunk):
678 if not isinstance(hunk, phabhunk):
678 if not isinstance(hunk, phabhunk):
679 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
679 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
680 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
680 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
681 # It's useful to include these stats since the Phab web UI shows them,
681 # It's useful to include these stats since the Phab web UI shows them,
682 # and uses them to estimate how large a change a Revision is. Also used
682 # and uses them to estimate how large a change a Revision is. Also used
683 # in email subjects for the [+++--] bit.
683 # in email subjects for the [+++--] bit.
684 self.addLines += hunk.addLines
684 self.addLines += hunk.addLines
685 self.delLines += hunk.delLines
685 self.delLines += hunk.delLines
686
686
687
687
688 @attr.s
688 @attr.s
689 class phabdiff(object):
689 class phabdiff(object):
690 """Represents a Differential diff, owns Differential changes. Corresponds
690 """Represents a Differential diff, owns Differential changes. Corresponds
691 to a commit.
691 to a commit.
692 """
692 """
693
693
694 # Doesn't seem to be any reason to send this (output of uname -n)
694 # Doesn't seem to be any reason to send this (output of uname -n)
695 sourceMachine = attr.ib(default=b'') # camelcase-required
695 sourceMachine = attr.ib(default=b'') # camelcase-required
696 sourcePath = attr.ib(default=b'/') # camelcase-required
696 sourcePath = attr.ib(default=b'/') # camelcase-required
697 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
697 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
698 sourceControlPath = attr.ib(default=b'/') # camelcase-required
698 sourceControlPath = attr.ib(default=b'/') # camelcase-required
699 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
699 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
700 branch = attr.ib(default=b'default')
700 branch = attr.ib(default=b'default')
701 bookmark = attr.ib(default=None)
701 bookmark = attr.ib(default=None)
702 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
702 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
703 lintStatus = attr.ib(default=b'none') # camelcase-required
703 lintStatus = attr.ib(default=b'none') # camelcase-required
704 unitStatus = attr.ib(default=b'none') # camelcase-required
704 unitStatus = attr.ib(default=b'none') # camelcase-required
705 changes = attr.ib(default=attr.Factory(dict))
705 changes = attr.ib(default=attr.Factory(dict))
706 repositoryPHID = attr.ib(default=None) # camelcase-required
706 repositoryPHID = attr.ib(default=None) # camelcase-required
707
707
708 def addchange(self, change):
708 def addchange(self, change):
709 if not isinstance(change, phabchange):
709 if not isinstance(change, phabchange):
710 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
710 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
711 self.changes[change.currentPath] = pycompat.byteskwargs(
711 self.changes[change.currentPath] = pycompat.byteskwargs(
712 attr.asdict(change)
712 attr.asdict(change)
713 )
713 )
714
714
715
715
716 def maketext(pchange, basectx, ctx, fname):
716 def maketext(pchange, basectx, ctx, fname):
717 """populate the phabchange for a text file"""
717 """populate the phabchange for a text file"""
718 repo = ctx.repo()
718 repo = ctx.repo()
719 fmatcher = match.exact([fname])
719 fmatcher = match.exact([fname])
720 diffopts = mdiff.diffopts(git=True, context=32767)
720 diffopts = mdiff.diffopts(git=True, context=32767)
721 _pfctx, _fctx, header, fhunks = next(
721 _pfctx, _fctx, header, fhunks = next(
722 patch.diffhunks(repo, basectx.p1(), ctx, fmatcher, opts=diffopts)
722 patch.diffhunks(repo, basectx.p1(), ctx, fmatcher, opts=diffopts)
723 )
723 )
724
724
725 for fhunk in fhunks:
725 for fhunk in fhunks:
726 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
726 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
727 corpus = b''.join(lines[1:])
727 corpus = b''.join(lines[1:])
728 shunk = list(header)
728 shunk = list(header)
729 shunk.extend(lines)
729 shunk.extend(lines)
730 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
730 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
731 patch.diffstatdata(util.iterlines(shunk))
731 patch.diffstatdata(util.iterlines(shunk))
732 )
732 )
733 pchange.addhunk(
733 pchange.addhunk(
734 phabhunk(
734 phabhunk(
735 oldOffset,
735 oldOffset,
736 oldLength,
736 oldLength,
737 newOffset,
737 newOffset,
738 newLength,
738 newLength,
739 corpus,
739 corpus,
740 addLines,
740 addLines,
741 delLines,
741 delLines,
742 )
742 )
743 )
743 )
744
744
745
745
746 def uploadchunks(fctx, fphid):
746 def uploadchunks(fctx, fphid):
747 """upload large binary files as separate chunks.
747 """upload large binary files as separate chunks.
748 Phab requests chunking over 8MiB, and splits into 4MiB chunks
748 Phab requests chunking over 8MiB, and splits into 4MiB chunks
749 """
749 """
750 ui = fctx.repo().ui
750 ui = fctx.repo().ui
751 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
751 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
752 with ui.makeprogress(
752 with ui.makeprogress(
753 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
753 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
754 ) as progress:
754 ) as progress:
755 for chunk in chunks:
755 for chunk in chunks:
756 progress.increment()
756 progress.increment()
757 if chunk[b'complete']:
757 if chunk[b'complete']:
758 continue
758 continue
759 bstart = int(chunk[b'byteStart'])
759 bstart = int(chunk[b'byteStart'])
760 bend = int(chunk[b'byteEnd'])
760 bend = int(chunk[b'byteEnd'])
761 callconduit(
761 callconduit(
762 ui,
762 ui,
763 b'file.uploadchunk',
763 b'file.uploadchunk',
764 {
764 {
765 b'filePHID': fphid,
765 b'filePHID': fphid,
766 b'byteStart': bstart,
766 b'byteStart': bstart,
767 b'data': base64.b64encode(fctx.data()[bstart:bend]),
767 b'data': base64.b64encode(fctx.data()[bstart:bend]),
768 b'dataEncoding': b'base64',
768 b'dataEncoding': b'base64',
769 },
769 },
770 )
770 )
771
771
772
772
773 def uploadfile(fctx):
773 def uploadfile(fctx):
774 """upload binary files to Phabricator"""
774 """upload binary files to Phabricator"""
775 repo = fctx.repo()
775 repo = fctx.repo()
776 ui = repo.ui
776 ui = repo.ui
777 fname = fctx.path()
777 fname = fctx.path()
778 size = fctx.size()
778 size = fctx.size()
779 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
779 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
780
780
781 # an allocate call is required first to see if an upload is even required
781 # an allocate call is required first to see if an upload is even required
782 # (Phab might already have it) and to determine if chunking is needed
782 # (Phab might already have it) and to determine if chunking is needed
783 allocateparams = {
783 allocateparams = {
784 b'name': fname,
784 b'name': fname,
785 b'contentLength': size,
785 b'contentLength': size,
786 b'contentHash': fhash,
786 b'contentHash': fhash,
787 }
787 }
788 filealloc = callconduit(ui, b'file.allocate', allocateparams)
788 filealloc = callconduit(ui, b'file.allocate', allocateparams)
789 fphid = filealloc[b'filePHID']
789 fphid = filealloc[b'filePHID']
790
790
791 if filealloc[b'upload']:
791 if filealloc[b'upload']:
792 ui.write(_(b'uploading %s\n') % bytes(fctx))
792 ui.write(_(b'uploading %s\n') % bytes(fctx))
793 if not fphid:
793 if not fphid:
794 uploadparams = {
794 uploadparams = {
795 b'name': fname,
795 b'name': fname,
796 b'data_base64': base64.b64encode(fctx.data()),
796 b'data_base64': base64.b64encode(fctx.data()),
797 }
797 }
798 fphid = callconduit(ui, b'file.upload', uploadparams)
798 fphid = callconduit(ui, b'file.upload', uploadparams)
799 else:
799 else:
800 uploadchunks(fctx, fphid)
800 uploadchunks(fctx, fphid)
801 else:
801 else:
802 ui.debug(b'server already has %s\n' % bytes(fctx))
802 ui.debug(b'server already has %s\n' % bytes(fctx))
803
803
804 if not fphid:
804 if not fphid:
805 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
805 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
806
806
807 return fphid
807 return fphid
808
808
809
809
810 def addoldbinary(pchange, oldfctx, fctx):
810 def addoldbinary(pchange, oldfctx, fctx):
811 """add the metadata for the previous version of a binary file to the
811 """add the metadata for the previous version of a binary file to the
812 phabchange for the new version
812 phabchange for the new version
813
813
814 ``oldfctx`` is the previous version of the file; ``fctx`` is the new
814 ``oldfctx`` is the previous version of the file; ``fctx`` is the new
815 version of the file, or None if the file is being removed.
815 version of the file, or None if the file is being removed.
816 """
816 """
817 if not fctx or fctx.cmp(oldfctx):
817 if not fctx or fctx.cmp(oldfctx):
818 # Files differ, add the old one
818 # Files differ, add the old one
819 pchange.metadata[b'old:file:size'] = oldfctx.size()
819 pchange.metadata[b'old:file:size'] = oldfctx.size()
820 mimeguess, _enc = mimetypes.guess_type(
820 mimeguess, _enc = mimetypes.guess_type(
821 encoding.unifromlocal(oldfctx.path())
821 encoding.unifromlocal(oldfctx.path())
822 )
822 )
823 if mimeguess:
823 if mimeguess:
824 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
824 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
825 mimeguess
825 mimeguess
826 )
826 )
827 fphid = uploadfile(oldfctx)
827 fphid = uploadfile(oldfctx)
828 pchange.metadata[b'old:binary-phid'] = fphid
828 pchange.metadata[b'old:binary-phid'] = fphid
829 else:
829 else:
830 # If it's left as IMAGE/BINARY web UI might try to display it
830 # If it's left as IMAGE/BINARY web UI might try to display it
831 pchange.fileType = DiffFileType.TEXT
831 pchange.fileType = DiffFileType.TEXT
832 pchange.copynewmetadatatoold()
832 pchange.copynewmetadatatoold()
833
833
834
834
835 def makebinary(pchange, fctx):
835 def makebinary(pchange, fctx):
836 """populate the phabchange for a binary file"""
836 """populate the phabchange for a binary file"""
837 pchange.fileType = DiffFileType.BINARY
837 pchange.fileType = DiffFileType.BINARY
838 fphid = uploadfile(fctx)
838 fphid = uploadfile(fctx)
839 pchange.metadata[b'new:binary-phid'] = fphid
839 pchange.metadata[b'new:binary-phid'] = fphid
840 pchange.metadata[b'new:file:size'] = fctx.size()
840 pchange.metadata[b'new:file:size'] = fctx.size()
841 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
841 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
842 if mimeguess:
842 if mimeguess:
843 mimeguess = pycompat.bytestr(mimeguess)
843 mimeguess = pycompat.bytestr(mimeguess)
844 pchange.metadata[b'new:file:mime-type'] = mimeguess
844 pchange.metadata[b'new:file:mime-type'] = mimeguess
845 if mimeguess.startswith(b'image/'):
845 if mimeguess.startswith(b'image/'):
846 pchange.fileType = DiffFileType.IMAGE
846 pchange.fileType = DiffFileType.IMAGE
847
847
848
848
849 # Copied from mercurial/patch.py
849 # Copied from mercurial/patch.py
850 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
850 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
851
851
852
852
853 def notutf8(fctx):
853 def notutf8(fctx):
854 """detect non-UTF-8 text files since Phabricator requires them to be marked
854 """detect non-UTF-8 text files since Phabricator requires them to be marked
855 as binary
855 as binary
856 """
856 """
857 try:
857 try:
858 fctx.data().decode('utf-8')
858 fctx.data().decode('utf-8')
859 return False
859 return False
860 except UnicodeDecodeError:
860 except UnicodeDecodeError:
861 fctx.repo().ui.write(
861 fctx.repo().ui.write(
862 _(b'file %s detected as non-UTF-8, marked as binary\n')
862 _(b'file %s detected as non-UTF-8, marked as binary\n')
863 % fctx.path()
863 % fctx.path()
864 )
864 )
865 return True
865 return True
866
866
867
867
868 def addremoved(pdiff, basectx, ctx, removed):
868 def addremoved(pdiff, basectx, ctx, removed):
869 """add removed files to the phabdiff. Shouldn't include moves"""
869 """add removed files to the phabdiff. Shouldn't include moves"""
870 for fname in removed:
870 for fname in removed:
871 pchange = phabchange(
871 pchange = phabchange(
872 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
872 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
873 )
873 )
874 oldfctx = basectx.p1()[fname]
874 oldfctx = basectx.p1()[fname]
875 pchange.addoldmode(gitmode[oldfctx.flags()])
875 pchange.addoldmode(gitmode[oldfctx.flags()])
876 if not (oldfctx.isbinary() or notutf8(oldfctx)):
876 if not (oldfctx.isbinary() or notutf8(oldfctx)):
877 maketext(pchange, basectx, ctx, fname)
877 maketext(pchange, basectx, ctx, fname)
878
878
879 pdiff.addchange(pchange)
879 pdiff.addchange(pchange)
880
880
881
881
882 def addmodified(pdiff, basectx, ctx, modified):
882 def addmodified(pdiff, basectx, ctx, modified):
883 """add modified files to the phabdiff"""
883 """add modified files to the phabdiff"""
884 for fname in modified:
884 for fname in modified:
885 fctx = ctx[fname]
885 fctx = ctx[fname]
886 oldfctx = basectx.p1()[fname]
886 oldfctx = basectx.p1()[fname]
887 pchange = phabchange(currentPath=fname, oldPath=fname)
887 pchange = phabchange(currentPath=fname, oldPath=fname)
888 filemode = gitmode[fctx.flags()]
888 filemode = gitmode[fctx.flags()]
889 originalmode = gitmode[oldfctx.flags()]
889 originalmode = gitmode[oldfctx.flags()]
890 if filemode != originalmode:
890 if filemode != originalmode:
891 pchange.addoldmode(originalmode)
891 pchange.addoldmode(originalmode)
892 pchange.addnewmode(filemode)
892 pchange.addnewmode(filemode)
893
893
894 if (
894 if (
895 fctx.isbinary()
895 fctx.isbinary()
896 or notutf8(fctx)
896 or notutf8(fctx)
897 or oldfctx.isbinary()
897 or oldfctx.isbinary()
898 or notutf8(oldfctx)
898 or notutf8(oldfctx)
899 ):
899 ):
900 makebinary(pchange, fctx)
900 makebinary(pchange, fctx)
901 addoldbinary(pchange, oldfctx, fctx)
901 addoldbinary(pchange, oldfctx, fctx)
902 else:
902 else:
903 maketext(pchange, basectx, ctx, fname)
903 maketext(pchange, basectx, ctx, fname)
904
904
905 pdiff.addchange(pchange)
905 pdiff.addchange(pchange)
906
906
907
907
908 def addadded(pdiff, basectx, ctx, added, removed):
908 def addadded(pdiff, basectx, ctx, added, removed):
909 """add file adds to the phabdiff, both new files and copies/moves"""
909 """add file adds to the phabdiff, both new files and copies/moves"""
910 # Keep track of files that've been recorded as moved/copied, so if there are
910 # Keep track of files that've been recorded as moved/copied, so if there are
911 # additional copies we can mark them (moves get removed from removed)
911 # additional copies we can mark them (moves get removed from removed)
912 copiedchanges = {}
912 copiedchanges = {}
913 movedchanges = {}
913 movedchanges = {}
914
914
915 copy = {}
915 copy = {}
916 if basectx != ctx:
916 if basectx != ctx:
917 copy = copies.pathcopies(basectx.p1(), ctx)
917 copy = copies.pathcopies(basectx.p1(), ctx)
918
918
919 for fname in added:
919 for fname in added:
920 fctx = ctx[fname]
920 fctx = ctx[fname]
921 oldfctx = None
921 oldfctx = None
922 pchange = phabchange(currentPath=fname)
922 pchange = phabchange(currentPath=fname)
923
923
924 filemode = gitmode[fctx.flags()]
924 filemode = gitmode[fctx.flags()]
925
925
926 if copy:
926 if copy:
927 originalfname = copy.get(fname, fname)
927 originalfname = copy.get(fname, fname)
928 else:
928 else:
929 originalfname = fname
929 originalfname = fname
930 if fctx.renamed():
930 if fctx.renamed():
931 originalfname = fctx.renamed()[0]
931 originalfname = fctx.renamed()[0]
932
932
933 renamed = fname != originalfname
933 renamed = fname != originalfname
934
934
935 if renamed:
935 if renamed:
936 oldfctx = basectx.p1()[originalfname]
936 oldfctx = basectx.p1()[originalfname]
937 originalmode = gitmode[oldfctx.flags()]
937 originalmode = gitmode[oldfctx.flags()]
938 pchange.oldPath = originalfname
938 pchange.oldPath = originalfname
939
939
940 if originalfname in removed:
940 if originalfname in removed:
941 origpchange = phabchange(
941 origpchange = phabchange(
942 currentPath=originalfname,
942 currentPath=originalfname,
943 oldPath=originalfname,
943 oldPath=originalfname,
944 type=DiffChangeType.MOVE_AWAY,
944 type=DiffChangeType.MOVE_AWAY,
945 awayPaths=[fname],
945 awayPaths=[fname],
946 )
946 )
947 movedchanges[originalfname] = origpchange
947 movedchanges[originalfname] = origpchange
948 removed.remove(originalfname)
948 removed.remove(originalfname)
949 pchange.type = DiffChangeType.MOVE_HERE
949 pchange.type = DiffChangeType.MOVE_HERE
950 elif originalfname in movedchanges:
950 elif originalfname in movedchanges:
951 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
951 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
952 movedchanges[originalfname].awayPaths.append(fname)
952 movedchanges[originalfname].awayPaths.append(fname)
953 pchange.type = DiffChangeType.COPY_HERE
953 pchange.type = DiffChangeType.COPY_HERE
954 else: # pure copy
954 else: # pure copy
955 if originalfname not in copiedchanges:
955 if originalfname not in copiedchanges:
956 origpchange = phabchange(
956 origpchange = phabchange(
957 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
957 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
958 )
958 )
959 copiedchanges[originalfname] = origpchange
959 copiedchanges[originalfname] = origpchange
960 else:
960 else:
961 origpchange = copiedchanges[originalfname]
961 origpchange = copiedchanges[originalfname]
962 origpchange.awayPaths.append(fname)
962 origpchange.awayPaths.append(fname)
963 pchange.type = DiffChangeType.COPY_HERE
963 pchange.type = DiffChangeType.COPY_HERE
964
964
965 if filemode != originalmode:
965 if filemode != originalmode:
966 pchange.addoldmode(originalmode)
966 pchange.addoldmode(originalmode)
967 pchange.addnewmode(filemode)
967 pchange.addnewmode(filemode)
968 else: # Brand-new file
968 else: # Brand-new file
969 pchange.addnewmode(gitmode[fctx.flags()])
969 pchange.addnewmode(gitmode[fctx.flags()])
970 pchange.type = DiffChangeType.ADD
970 pchange.type = DiffChangeType.ADD
971
971
972 if (
972 if (
973 fctx.isbinary()
973 fctx.isbinary()
974 or notutf8(fctx)
974 or notutf8(fctx)
975 or (oldfctx and (oldfctx.isbinary() or notutf8(oldfctx)))
975 or (oldfctx and (oldfctx.isbinary() or notutf8(oldfctx)))
976 ):
976 ):
977 makebinary(pchange, fctx)
977 makebinary(pchange, fctx)
978 if renamed:
978 if renamed:
979 addoldbinary(pchange, oldfctx, fctx)
979 addoldbinary(pchange, oldfctx, fctx)
980 else:
980 else:
981 maketext(pchange, basectx, ctx, fname)
981 maketext(pchange, basectx, ctx, fname)
982
982
983 pdiff.addchange(pchange)
983 pdiff.addchange(pchange)
984
984
985 for _path, copiedchange in copiedchanges.items():
985 for _path, copiedchange in copiedchanges.items():
986 pdiff.addchange(copiedchange)
986 pdiff.addchange(copiedchange)
987 for _path, movedchange in movedchanges.items():
987 for _path, movedchange in movedchanges.items():
988 pdiff.addchange(movedchange)
988 pdiff.addchange(movedchange)
989
989
990
990
991 def creatediff(basectx, ctx):
991 def creatediff(basectx, ctx):
992 """create a Differential Diff"""
992 """create a Differential Diff"""
993 repo = ctx.repo()
993 repo = ctx.repo()
994 repophid = getrepophid(repo)
994 repophid = getrepophid(repo)
995 # Create a "Differential Diff" via "differential.creatediff" API
995 # Create a "Differential Diff" via "differential.creatediff" API
996 pdiff = phabdiff(
996 pdiff = phabdiff(
997 sourceControlBaseRevision=b'%s' % basectx.p1().hex(),
997 sourceControlBaseRevision=b'%s' % basectx.p1().hex(),
998 branch=b'%s' % ctx.branch(),
998 branch=b'%s' % ctx.branch(),
999 )
999 )
1000 modified, added, removed, _d, _u, _i, _c = basectx.p1().status(ctx)
1000 modified, added, removed, _d, _u, _i, _c = basectx.p1().status(ctx)
1001 # addadded will remove moved files from removed, so addremoved won't get
1001 # addadded will remove moved files from removed, so addremoved won't get
1002 # them
1002 # them
1003 addadded(pdiff, basectx, ctx, added, removed)
1003 addadded(pdiff, basectx, ctx, added, removed)
1004 addmodified(pdiff, basectx, ctx, modified)
1004 addmodified(pdiff, basectx, ctx, modified)
1005 addremoved(pdiff, basectx, ctx, removed)
1005 addremoved(pdiff, basectx, ctx, removed)
1006 if repophid:
1006 if repophid:
1007 pdiff.repositoryPHID = repophid
1007 pdiff.repositoryPHID = repophid
1008 diff = callconduit(
1008 diff = callconduit(
1009 repo.ui,
1009 repo.ui,
1010 b'differential.creatediff',
1010 b'differential.creatediff',
1011 pycompat.byteskwargs(attr.asdict(pdiff)),
1011 pycompat.byteskwargs(attr.asdict(pdiff)),
1012 )
1012 )
1013 if not diff:
1013 if not diff:
1014 if basectx != ctx:
1014 if basectx != ctx:
1015 msg = _(b'cannot create diff for %s::%s') % (basectx, ctx)
1015 msg = _(b'cannot create diff for %s::%s') % (basectx, ctx)
1016 else:
1016 else:
1017 msg = _(b'cannot create diff for %s') % ctx
1017 msg = _(b'cannot create diff for %s') % ctx
1018 raise error.Abort(msg)
1018 raise error.Abort(msg)
1019 return diff
1019 return diff
1020
1020
1021
1021
1022 def writediffproperties(ctxs, diff):
1022 def writediffproperties(ctxs, diff):
1023 """write metadata to diff so patches could be applied losslessly
1023 """write metadata to diff so patches could be applied losslessly
1024
1024
1025 ``ctxs`` is the list of commits that created the diff, in ascending order.
1025 ``ctxs`` is the list of commits that created the diff, in ascending order.
1026 The list is generally a single commit, but may be several when using
1026 The list is generally a single commit, but may be several when using
1027 ``phabsend --fold``.
1027 ``phabsend --fold``.
1028 """
1028 """
1029 # creatediff returns with a diffid but query returns with an id
1029 # creatediff returns with a diffid but query returns with an id
1030 diffid = diff.get(b'diffid', diff.get(b'id'))
1030 diffid = diff.get(b'diffid', diff.get(b'id'))
1031 basectx = ctxs[0]
1031 basectx = ctxs[0]
1032 tipctx = ctxs[-1]
1032 tipctx = ctxs[-1]
1033
1033
1034 params = {
1034 params = {
1035 b'diff_id': diffid,
1035 b'diff_id': diffid,
1036 b'name': b'hg:meta',
1036 b'name': b'hg:meta',
1037 b'data': templatefilters.json(
1037 b'data': templatefilters.json(
1038 {
1038 {
1039 b'user': tipctx.user(),
1039 b'user': tipctx.user(),
1040 b'date': b'%d %d' % tipctx.date(),
1040 b'date': b'%d %d' % tipctx.date(),
1041 b'branch': tipctx.branch(),
1041 b'branch': tipctx.branch(),
1042 b'node': tipctx.hex(),
1042 b'node': tipctx.hex(),
1043 b'parent': basectx.p1().hex(),
1043 b'parent': basectx.p1().hex(),
1044 }
1044 }
1045 ),
1045 ),
1046 }
1046 }
1047 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1047 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1048
1048
1049 commits = {}
1049 commits = {}
1050 for ctx in ctxs:
1050 for ctx in ctxs:
1051 commits[ctx.hex()] = {
1051 commits[ctx.hex()] = {
1052 b'author': stringutil.person(ctx.user()),
1052 b'author': stringutil.person(ctx.user()),
1053 b'authorEmail': stringutil.email(ctx.user()),
1053 b'authorEmail': stringutil.email(ctx.user()),
1054 b'time': int(ctx.date()[0]),
1054 b'time': int(ctx.date()[0]),
1055 b'commit': ctx.hex(),
1055 b'commit': ctx.hex(),
1056 b'parents': [ctx.p1().hex()],
1056 b'parents': [ctx.p1().hex()],
1057 b'branch': ctx.branch(),
1057 b'branch': ctx.branch(),
1058 }
1058 }
1059 params = {
1059 params = {
1060 b'diff_id': diffid,
1060 b'diff_id': diffid,
1061 b'name': b'local:commits',
1061 b'name': b'local:commits',
1062 b'data': templatefilters.json(commits),
1062 b'data': templatefilters.json(commits),
1063 }
1063 }
1064 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1064 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1065
1065
1066
1066
1067 def createdifferentialrevision(
1067 def createdifferentialrevision(
1068 ctxs,
1068 ctxs,
1069 revid=None,
1069 revid=None,
1070 parentrevphid=None,
1070 parentrevphid=None,
1071 oldbasenode=None,
1071 oldbasenode=None,
1072 oldnode=None,
1072 oldnode=None,
1073 olddiff=None,
1073 olddiff=None,
1074 actions=None,
1074 actions=None,
1075 comment=None,
1075 comment=None,
1076 ):
1076 ):
1077 """create or update a Differential Revision
1077 """create or update a Differential Revision
1078
1078
1079 If revid is None, create a new Differential Revision, otherwise update
1079 If revid is None, create a new Differential Revision, otherwise update
1080 revid. If parentrevphid is not None, set it as a dependency.
1080 revid. If parentrevphid is not None, set it as a dependency.
1081
1081
1082 If there is a single commit for the new Differential Revision, ``ctxs`` will
1082 If there is a single commit for the new Differential Revision, ``ctxs`` will
1083 be a list of that single context. Otherwise, it is a list that covers the
1083 be a list of that single context. Otherwise, it is a list that covers the
1084 range of changes for the differential, where ``ctxs[0]`` is the first change
1084 range of changes for the differential, where ``ctxs[0]`` is the first change
1085 to include and ``ctxs[-1]`` is the last.
1085 to include and ``ctxs[-1]`` is the last.
1086
1086
1087 If oldnode is not None, check if the patch content (without commit message
1087 If oldnode is not None, check if the patch content (without commit message
1088 and metadata) has changed before creating another diff. For a Revision with
1088 and metadata) has changed before creating another diff. For a Revision with
1089 a single commit, ``oldbasenode`` and ``oldnode`` have the same value. For a
1089 a single commit, ``oldbasenode`` and ``oldnode`` have the same value. For a
1090 Revision covering multiple commits, ``oldbasenode`` corresponds to
1090 Revision covering multiple commits, ``oldbasenode`` corresponds to
1091 ``ctxs[0]`` the previous time this Revision was posted, and ``oldnode``
1091 ``ctxs[0]`` the previous time this Revision was posted, and ``oldnode``
1092 corresponds to ``ctxs[-1]``.
1092 corresponds to ``ctxs[-1]``.
1093
1093
1094 If actions is not None, they will be appended to the transaction.
1094 If actions is not None, they will be appended to the transaction.
1095 """
1095 """
1096 ctx = ctxs[-1]
1096 ctx = ctxs[-1]
1097 basectx = ctxs[0]
1097 basectx = ctxs[0]
1098
1098
1099 repo = ctx.repo()
1099 repo = ctx.repo()
1100 if oldnode:
1100 if oldnode:
1101 diffopts = mdiff.diffopts(git=True, context=32767)
1101 diffopts = mdiff.diffopts(git=True, context=32767)
1102 unfi = repo.unfiltered()
1102 unfi = repo.unfiltered()
1103 oldctx = unfi[oldnode]
1103 oldctx = unfi[oldnode]
1104 oldbasectx = unfi[oldbasenode]
1104 oldbasectx = unfi[oldbasenode]
1105 neednewdiff = getdiff(basectx, ctx, diffopts) != getdiff(
1105 neednewdiff = getdiff(basectx, ctx, diffopts) != getdiff(
1106 oldbasectx, oldctx, diffopts
1106 oldbasectx, oldctx, diffopts
1107 )
1107 )
1108 else:
1108 else:
1109 neednewdiff = True
1109 neednewdiff = True
1110
1110
1111 transactions = []
1111 transactions = []
1112 if neednewdiff:
1112 if neednewdiff:
1113 diff = creatediff(basectx, ctx)
1113 diff = creatediff(basectx, ctx)
1114 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1114 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1115 if comment:
1115 if comment:
1116 transactions.append({b'type': b'comment', b'value': comment})
1116 transactions.append({b'type': b'comment', b'value': comment})
1117 else:
1117 else:
1118 # Even if we don't need to upload a new diff because the patch content
1118 # Even if we don't need to upload a new diff because the patch content
1119 # does not change. We might still need to update its metadata so
1119 # does not change. We might still need to update its metadata so
1120 # pushers could know the correct node metadata.
1120 # pushers could know the correct node metadata.
1121 assert olddiff
1121 assert olddiff
1122 diff = olddiff
1122 diff = olddiff
1123 writediffproperties(ctxs, diff)
1123 writediffproperties(ctxs, diff)
1124
1124
1125 # Set the parent Revision every time, so commit re-ordering is picked-up
1125 # Set the parent Revision every time, so commit re-ordering is picked-up
1126 if parentrevphid:
1126 if parentrevphid:
1127 transactions.append(
1127 transactions.append(
1128 {b'type': b'parents.set', b'value': [parentrevphid]}
1128 {b'type': b'parents.set', b'value': [parentrevphid]}
1129 )
1129 )
1130
1130
1131 if actions:
1131 if actions:
1132 transactions += actions
1132 transactions += actions
1133
1133
1134 # When folding multiple local commits into a single review, arcanist will
1134 # When folding multiple local commits into a single review, arcanist will
1135 # take the summary line of the first commit as the title, and then
1135 # take the summary line of the first commit as the title, and then
1136 # concatenate the rest of the remaining messages (including each of their
1136 # concatenate the rest of the remaining messages (including each of their
1137 # first lines) to the rest of the first commit message (each separated by
1137 # first lines) to the rest of the first commit message (each separated by
1138 # an empty line), and use that as the summary field. Do the same here.
1138 # an empty line), and use that as the summary field. Do the same here.
1139 # For commits with only a one line message, there is no summary field, as
1139 # For commits with only a one line message, there is no summary field, as
1140 # this gets assigned to the title.
1140 # this gets assigned to the title.
1141 fields = util.sortdict() # sorted for stable wire protocol in tests
1141 fields = util.sortdict() # sorted for stable wire protocol in tests
1142
1142
1143 for i, _ctx in enumerate(ctxs):
1143 for i, _ctx in enumerate(ctxs):
1144 # Parse commit message and update related fields.
1144 # Parse commit message and update related fields.
1145 desc = _ctx.description()
1145 desc = _ctx.description()
1146 info = callconduit(
1146 info = callconduit(
1147 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1147 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1148 )
1148 )
1149
1149
1150 for k in [b'title', b'summary', b'testPlan']:
1150 for k in [b'title', b'summary', b'testPlan']:
1151 v = info[b'fields'].get(k)
1151 v = info[b'fields'].get(k)
1152 if not v:
1152 if not v:
1153 continue
1153 continue
1154
1154
1155 if i == 0:
1155 if i == 0:
1156 # Title, summary and test plan (if present) are taken verbatim
1156 # Title, summary and test plan (if present) are taken verbatim
1157 # for the first commit.
1157 # for the first commit.
1158 fields[k] = v.rstrip()
1158 fields[k] = v.rstrip()
1159 continue
1159 continue
1160 elif k == b'title':
1160 elif k == b'title':
1161 # Add subsequent titles (i.e. the first line of the commit
1161 # Add subsequent titles (i.e. the first line of the commit
1162 # message) back to the summary.
1162 # message) back to the summary.
1163 k = b'summary'
1163 k = b'summary'
1164
1164
1165 # Append any current field to the existing composite field
1165 # Append any current field to the existing composite field
1166 fields[k] = b'\n\n'.join(filter(None, [fields.get(k), v.rstrip()]))
1166 fields[k] = b'\n\n'.join(filter(None, [fields.get(k), v.rstrip()]))
1167
1167
1168 for k, v in fields.items():
1168 for k, v in fields.items():
1169 transactions.append({b'type': k, b'value': v})
1169 transactions.append({b'type': k, b'value': v})
1170
1170
1171 params = {b'transactions': transactions}
1171 params = {b'transactions': transactions}
1172 if revid is not None:
1172 if revid is not None:
1173 # Update an existing Differential Revision
1173 # Update an existing Differential Revision
1174 params[b'objectIdentifier'] = revid
1174 params[b'objectIdentifier'] = revid
1175
1175
1176 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1176 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1177 if not revision:
1177 if not revision:
1178 if len(ctxs) == 1:
1178 if len(ctxs) == 1:
1179 msg = _(b'cannot create revision for %s') % ctx
1179 msg = _(b'cannot create revision for %s') % ctx
1180 else:
1180 else:
1181 msg = _(b'cannot create revision for %s::%s') % (basectx, ctx)
1181 msg = _(b'cannot create revision for %s::%s') % (basectx, ctx)
1182 raise error.Abort(msg)
1182 raise error.Abort(msg)
1183
1183
1184 return revision, diff
1184 return revision, diff
1185
1185
1186
1186
1187 def userphids(ui, names):
1187 def userphids(ui, names):
1188 """convert user names to PHIDs"""
1188 """convert user names to PHIDs"""
1189 names = [name.lower() for name in names]
1189 names = [name.lower() for name in names]
1190 query = {b'constraints': {b'usernames': names}}
1190 query = {b'constraints': {b'usernames': names}}
1191 result = callconduit(ui, b'user.search', query)
1191 result = callconduit(ui, b'user.search', query)
1192 # username not found is not an error of the API. So check if we have missed
1192 # username not found is not an error of the API. So check if we have missed
1193 # some names here.
1193 # some names here.
1194 data = result[b'data']
1194 data = result[b'data']
1195 resolved = {entry[b'fields'][b'username'].lower() for entry in data}
1195 resolved = {entry[b'fields'][b'username'].lower() for entry in data}
1196 unresolved = set(names) - resolved
1196 unresolved = set(names) - resolved
1197 if unresolved:
1197 if unresolved:
1198 raise error.Abort(
1198 raise error.Abort(
1199 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1199 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1200 )
1200 )
1201 return [entry[b'phid'] for entry in data]
1201 return [entry[b'phid'] for entry in data]
1202
1202
1203
1203
1204 def _print_phabsend_action(ui, ctx, newrevid, action):
1204 def _print_phabsend_action(ui, ctx, newrevid, action):
1205 """print the ``action`` that occurred when posting ``ctx`` for review
1205 """print the ``action`` that occurred when posting ``ctx`` for review
1206
1206
1207 This is a utility function for the sending phase of ``phabsend``, which
1207 This is a utility function for the sending phase of ``phabsend``, which
1208 makes it easier to show a status for all local commits with `--fold``.
1208 makes it easier to show a status for all local commits with `--fold``.
1209 """
1209 """
1210 actiondesc = ui.label(
1210 actiondesc = ui.label(
1211 {
1211 {
1212 b'created': _(b'created'),
1212 b'created': _(b'created'),
1213 b'skipped': _(b'skipped'),
1213 b'skipped': _(b'skipped'),
1214 b'updated': _(b'updated'),
1214 b'updated': _(b'updated'),
1215 }[action],
1215 }[action],
1216 b'phabricator.action.%s' % action,
1216 b'phabricator.action.%s' % action,
1217 )
1217 )
1218 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1218 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1219 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
1219 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
1220 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
1220 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
1221 ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc))
1221 ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc))
1222
1222
1223
1223
1224 def _amend_diff_properties(unfi, drevid, newnodes, diff):
1224 def _amend_diff_properties(unfi, drevid, newnodes, diff):
1225 """update the local commit list for the ``diff`` associated with ``drevid``
1225 """update the local commit list for the ``diff`` associated with ``drevid``
1226
1226
1227 This is a utility function for the amend phase of ``phabsend``, which
1227 This is a utility function for the amend phase of ``phabsend``, which
1228 converts failures to warning messages.
1228 converts failures to warning messages.
1229 """
1229 """
1230 _debug(
1230 _debug(
1231 unfi.ui,
1231 unfi.ui,
1232 b"new commits: %s\n" % stringutil.pprint([short(n) for n in newnodes]),
1232 b"new commits: %s\n" % stringutil.pprint([short(n) for n in newnodes]),
1233 )
1233 )
1234
1234
1235 try:
1235 try:
1236 writediffproperties([unfi[newnode] for newnode in newnodes], diff)
1236 writediffproperties([unfi[newnode] for newnode in newnodes], diff)
1237 except util.urlerr.urlerror:
1237 except util.urlerr.urlerror:
1238 # If it fails just warn and keep going, otherwise the DREV
1238 # If it fails just warn and keep going, otherwise the DREV
1239 # associations will be lost
1239 # associations will be lost
1240 unfi.ui.warnnoi18n(b'Failed to update metadata for D%d\n' % drevid)
1240 unfi.ui.warnnoi18n(b'Failed to update metadata for D%d\n' % drevid)
1241
1241
1242
1242
1243 @vcrcommand(
1243 @vcrcommand(
1244 b'phabsend',
1244 b'phabsend',
1245 [
1245 [
1246 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1246 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1247 (b'', b'amend', True, _(b'update commit messages')),
1247 (b'', b'amend', True, _(b'update commit messages')),
1248 (b'', b'reviewer', [], _(b'specify reviewers')),
1248 (b'', b'reviewer', [], _(b'specify reviewers')),
1249 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1249 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1250 (
1250 (
1251 b'm',
1251 b'm',
1252 b'comment',
1252 b'comment',
1253 b'',
1253 b'',
1254 _(b'add a comment to Revisions with new/updated Diffs'),
1254 _(b'add a comment to Revisions with new/updated Diffs'),
1255 ),
1255 ),
1256 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1256 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1257 (b'', b'fold', False, _(b'combine the revisions into one review')),
1257 (b'', b'fold', False, _(b'combine the revisions into one review')),
1258 ],
1258 ],
1259 _(b'REV [OPTIONS]'),
1259 _(b'REV [OPTIONS]'),
1260 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1260 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1261 )
1261 )
1262 def phabsend(ui, repo, *revs, **opts):
1262 def phabsend(ui, repo, *revs, **opts):
1263 """upload changesets to Phabricator
1263 """upload changesets to Phabricator
1264
1264
1265 If there are multiple revisions specified, they will be send as a stack
1265 If there are multiple revisions specified, they will be send as a stack
1266 with a linear dependencies relationship using the order specified by the
1266 with a linear dependencies relationship using the order specified by the
1267 revset.
1267 revset.
1268
1268
1269 For the first time uploading changesets, local tags will be created to
1269 For the first time uploading changesets, local tags will be created to
1270 maintain the association. After the first time, phabsend will check
1270 maintain the association. After the first time, phabsend will check
1271 obsstore and tags information so it can figure out whether to update an
1271 obsstore and tags information so it can figure out whether to update an
1272 existing Differential Revision, or create a new one.
1272 existing Differential Revision, or create a new one.
1273
1273
1274 If --amend is set, update commit messages so they have the
1274 If --amend is set, update commit messages so they have the
1275 ``Differential Revision`` URL, remove related tags. This is similar to what
1275 ``Differential Revision`` URL, remove related tags. This is similar to what
1276 arcanist will do, and is more desired in author-push workflows. Otherwise,
1276 arcanist will do, and is more desired in author-push workflows. Otherwise,
1277 use local tags to record the ``Differential Revision`` association.
1277 use local tags to record the ``Differential Revision`` association.
1278
1278
1279 The --confirm option lets you confirm changesets before sending them. You
1279 The --confirm option lets you confirm changesets before sending them. You
1280 can also add following to your configuration file to make it default
1280 can also add following to your configuration file to make it default
1281 behaviour::
1281 behaviour::
1282
1282
1283 [phabsend]
1283 [phabsend]
1284 confirm = true
1284 confirm = true
1285
1285
1286 By default, a separate review will be created for each commit that is
1286 By default, a separate review will be created for each commit that is
1287 selected, and will have the same parent/child relationship in Phabricator.
1287 selected, and will have the same parent/child relationship in Phabricator.
1288 If ``--fold`` is set, multiple commits are rolled up into a single review
1288 If ``--fold`` is set, multiple commits are rolled up into a single review
1289 as if diffed from the parent of the first revision to the last. The commit
1289 as if diffed from the parent of the first revision to the last. The commit
1290 messages are concatenated in the summary field on Phabricator.
1290 messages are concatenated in the summary field on Phabricator.
1291
1291
1292 phabsend will check obsstore and the above association to decide whether to
1292 phabsend will check obsstore and the above association to decide whether to
1293 update an existing Differential Revision, or create a new one.
1293 update an existing Differential Revision, or create a new one.
1294 """
1294 """
1295 opts = pycompat.byteskwargs(opts)
1295 opts = pycompat.byteskwargs(opts)
1296 revs = list(revs) + opts.get(b'rev', [])
1296 revs = list(revs) + opts.get(b'rev', [])
1297 revs = scmutil.revrange(repo, revs)
1297 revs = scmutil.revrange(repo, revs)
1298 revs.sort() # ascending order to preserve topological parent/child in phab
1298 revs.sort() # ascending order to preserve topological parent/child in phab
1299
1299
1300 if not revs:
1300 if not revs:
1301 raise error.Abort(_(b'phabsend requires at least one changeset'))
1301 raise error.Abort(_(b'phabsend requires at least one changeset'))
1302 if opts.get(b'amend'):
1302 if opts.get(b'amend'):
1303 cmdutil.checkunfinished(repo)
1303 cmdutil.checkunfinished(repo)
1304
1304
1305 ctxs = [repo[rev] for rev in revs]
1305 ctxs = [repo[rev] for rev in revs]
1306
1306
1307 fold = opts.get(b'fold')
1307 fold = opts.get(b'fold')
1308 if fold:
1308 if fold:
1309 if len(revs) == 1:
1309 if len(revs) == 1:
1310 # TODO: just switch to --no-fold instead?
1310 # TODO: just switch to --no-fold instead?
1311 raise error.Abort(_(b"cannot fold a single revision"))
1311 raise error.Abort(_(b"cannot fold a single revision"))
1312
1312
1313 # There's no clear way to manage multiple commits with a Dxxx tag, so
1313 # There's no clear way to manage multiple commits with a Dxxx tag, so
1314 # require the amend option. (We could append "_nnn", but then it
1314 # require the amend option. (We could append "_nnn", but then it
1315 # becomes jumbled if earlier commits are added to an update.) It should
1315 # becomes jumbled if earlier commits are added to an update.) It should
1316 # lock the repo and ensure that the range is editable, but that would
1316 # lock the repo and ensure that the range is editable, but that would
1317 # make the code pretty convoluted. The default behavior of `arc` is to
1317 # make the code pretty convoluted. The default behavior of `arc` is to
1318 # create a new review anyway.
1318 # create a new review anyway.
1319 if not opts.get(b"amend"):
1319 if not opts.get(b"amend"):
1320 raise error.Abort(_(b"cannot fold with --no-amend"))
1320 raise error.Abort(_(b"cannot fold with --no-amend"))
1321
1321
1322 # Ensure the local commits are an unbroken range
1322 # Ensure the local commits are an unbroken range
1323 revrange = repo.revs(b'(first(%ld)::last(%ld))', revs, revs)
1323 revrange = repo.revs(b'(first(%ld)::last(%ld))', revs, revs)
1324 if any(r for r in revs if r not in revrange) or any(
1324 if any(r for r in revs if r not in revrange) or any(
1325 r for r in revrange if r not in revs
1325 r for r in revrange if r not in revs
1326 ):
1326 ):
1327 raise error.Abort(_(b"cannot fold non-linear revisions"))
1327 raise error.Abort(_(b"cannot fold non-linear revisions"))
1328
1328
1329 # It might be possible to bucketize the revisions by the DREV value, and
1329 # It might be possible to bucketize the revisions by the DREV value, and
1330 # iterate over those groups when posting, and then again when amending.
1330 # iterate over those groups when posting, and then again when amending.
1331 # But for simplicity, require all selected revisions to be for the same
1331 # But for simplicity, require all selected revisions to be for the same
1332 # DREV (if present). Adding local revisions to an existing DREV is
1332 # DREV (if present). Adding local revisions to an existing DREV is
1333 # acceptable.
1333 # acceptable.
1334 drevmatchers = [
1334 drevmatchers = [
1335 _differentialrevisiondescre.search(ctx.description())
1335 _differentialrevisiondescre.search(ctx.description())
1336 for ctx in ctxs
1336 for ctx in ctxs
1337 ]
1337 ]
1338 if len({m.group('url') for m in drevmatchers if m}) > 1:
1338 if len({m.group('url') for m in drevmatchers if m}) > 1:
1339 raise error.Abort(
1339 raise error.Abort(
1340 _(b"cannot fold revisions with different DREV values")
1340 _(b"cannot fold revisions with different DREV values")
1341 )
1341 )
1342
1342
1343 # {newnode: (oldnode, olddiff, olddrev}
1343 # {newnode: (oldnode, olddiff, olddrev}
1344 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1344 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1345
1345
1346 confirm = ui.configbool(b'phabsend', b'confirm')
1346 confirm = ui.configbool(b'phabsend', b'confirm')
1347 confirm |= bool(opts.get(b'confirm'))
1347 confirm |= bool(opts.get(b'confirm'))
1348 if confirm:
1348 if confirm:
1349 confirmed = _confirmbeforesend(repo, revs, oldmap)
1349 confirmed = _confirmbeforesend(repo, revs, oldmap)
1350 if not confirmed:
1350 if not confirmed:
1351 raise error.Abort(_(b'phabsend cancelled'))
1351 raise error.Abort(_(b'phabsend cancelled'))
1352
1352
1353 actions = []
1353 actions = []
1354 reviewers = opts.get(b'reviewer', [])
1354 reviewers = opts.get(b'reviewer', [])
1355 blockers = opts.get(b'blocker', [])
1355 blockers = opts.get(b'blocker', [])
1356 phids = []
1356 phids = []
1357 if reviewers:
1357 if reviewers:
1358 phids.extend(userphids(repo.ui, reviewers))
1358 phids.extend(userphids(repo.ui, reviewers))
1359 if blockers:
1359 if blockers:
1360 phids.extend(
1360 phids.extend(
1361 map(
1361 map(
1362 lambda phid: b'blocking(%s)' % phid,
1362 lambda phid: b'blocking(%s)' % phid,
1363 userphids(repo.ui, blockers),
1363 userphids(repo.ui, blockers),
1364 )
1364 )
1365 )
1365 )
1366 if phids:
1366 if phids:
1367 actions.append({b'type': b'reviewers.add', b'value': phids})
1367 actions.append({b'type': b'reviewers.add', b'value': phids})
1368
1368
1369 drevids = [] # [int]
1369 drevids = [] # [int]
1370 diffmap = {} # {newnode: diff}
1370 diffmap = {} # {newnode: diff}
1371
1371
1372 # Send patches one by one so we know their Differential Revision PHIDs and
1372 # Send patches one by one so we know their Differential Revision PHIDs and
1373 # can provide dependency relationship
1373 # can provide dependency relationship
1374 lastrevphid = None
1374 lastrevphid = None
1375 for ctx in ctxs:
1375 for ctx in ctxs:
1376 if fold:
1376 if fold:
1377 ui.debug(b'sending rev %d::%d\n' % (ctx.rev(), ctxs[-1].rev()))
1377 ui.debug(b'sending rev %d::%d\n' % (ctx.rev(), ctxs[-1].rev()))
1378 else:
1378 else:
1379 ui.debug(b'sending rev %d\n' % ctx.rev())
1379 ui.debug(b'sending rev %d\n' % ctx.rev())
1380
1380
1381 # Get Differential Revision ID
1381 # Get Differential Revision ID
1382 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1382 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1383 oldbasenode, oldbasediff, oldbaserevid = oldnode, olddiff, revid
1383 oldbasenode, oldbasediff, oldbaserevid = oldnode, olddiff, revid
1384
1384
1385 if fold:
1385 if fold:
1386 oldbasenode, oldbasediff, oldbaserevid = oldmap.get(
1386 oldbasenode, oldbasediff, oldbaserevid = oldmap.get(
1387 ctxs[-1].node(), (None, None, None)
1387 ctxs[-1].node(), (None, None, None)
1388 )
1388 )
1389
1389
1390 if oldnode != ctx.node() or opts.get(b'amend'):
1390 if oldnode != ctx.node() or opts.get(b'amend'):
1391 # Create or update Differential Revision
1391 # Create or update Differential Revision
1392 revision, diff = createdifferentialrevision(
1392 revision, diff = createdifferentialrevision(
1393 ctxs if fold else [ctx],
1393 ctxs if fold else [ctx],
1394 revid,
1394 revid,
1395 lastrevphid,
1395 lastrevphid,
1396 oldbasenode,
1396 oldbasenode,
1397 oldnode,
1397 oldnode,
1398 olddiff,
1398 olddiff,
1399 actions,
1399 actions,
1400 opts.get(b'comment'),
1400 opts.get(b'comment'),
1401 )
1401 )
1402
1402
1403 if fold:
1403 if fold:
1404 for ctx in ctxs:
1404 for ctx in ctxs:
1405 diffmap[ctx.node()] = diff
1405 diffmap[ctx.node()] = diff
1406 else:
1406 else:
1407 diffmap[ctx.node()] = diff
1407 diffmap[ctx.node()] = diff
1408
1408
1409 newrevid = int(revision[b'object'][b'id'])
1409 newrevid = int(revision[b'object'][b'id'])
1410 newrevphid = revision[b'object'][b'phid']
1410 newrevphid = revision[b'object'][b'phid']
1411 if revid:
1411 if revid:
1412 action = b'updated'
1412 action = b'updated'
1413 else:
1413 else:
1414 action = b'created'
1414 action = b'created'
1415
1415
1416 # Create a local tag to note the association, if commit message
1416 # Create a local tag to note the association, if commit message
1417 # does not have it already
1417 # does not have it already
1418 if not fold:
1418 if not fold:
1419 m = _differentialrevisiondescre.search(ctx.description())
1419 m = _differentialrevisiondescre.search(ctx.description())
1420 if not m or int(m.group('id')) != newrevid:
1420 if not m or int(m.group('id')) != newrevid:
1421 tagname = b'D%d' % newrevid
1421 tagname = b'D%d' % newrevid
1422 tags.tag(
1422 tags.tag(
1423 repo,
1423 repo,
1424 tagname,
1424 tagname,
1425 ctx.node(),
1425 ctx.node(),
1426 message=None,
1426 message=None,
1427 user=None,
1427 user=None,
1428 date=None,
1428 date=None,
1429 local=True,
1429 local=True,
1430 )
1430 )
1431 else:
1431 else:
1432 # Nothing changed. But still set "newrevphid" so the next revision
1432 # Nothing changed. But still set "newrevphid" so the next revision
1433 # could depend on this one and "newrevid" for the summary line.
1433 # could depend on this one and "newrevid" for the summary line.
1434 newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']
1434 newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']
1435 newrevid = revid
1435 newrevid = revid
1436 action = b'skipped'
1436 action = b'skipped'
1437
1437
1438 drevids.append(newrevid)
1438 drevids.append(newrevid)
1439 lastrevphid = newrevphid
1439 lastrevphid = newrevphid
1440
1440
1441 if fold:
1441 if fold:
1442 for c in ctxs:
1442 for c in ctxs:
1443 if oldmap.get(c.node(), (None, None, None))[2]:
1443 if oldmap.get(c.node(), (None, None, None))[2]:
1444 action = b'updated'
1444 action = b'updated'
1445 else:
1445 else:
1446 action = b'created'
1446 action = b'created'
1447 _print_phabsend_action(ui, c, newrevid, action)
1447 _print_phabsend_action(ui, c, newrevid, action)
1448 break
1448 break
1449
1449
1450 _print_phabsend_action(ui, ctx, newrevid, action)
1450 _print_phabsend_action(ui, ctx, newrevid, action)
1451
1451
1452 # Update commit messages and remove tags
1452 # Update commit messages and remove tags
1453 if opts.get(b'amend'):
1453 if opts.get(b'amend'):
1454 unfi = repo.unfiltered()
1454 unfi = repo.unfiltered()
1455 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1455 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1456 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1456 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1457 wnode = unfi[b'.'].node()
1457 wnode = unfi[b'.'].node()
1458 mapping = {} # {oldnode: [newnode]}
1458 mapping = {} # {oldnode: [newnode]}
1459 newnodes = []
1459 newnodes = []
1460
1460
1461 drevid = drevids[0]
1461 drevid = drevids[0]
1462
1462
1463 for i, rev in enumerate(revs):
1463 for i, rev in enumerate(revs):
1464 old = unfi[rev]
1464 old = unfi[rev]
1465 if not fold:
1465 if not fold:
1466 drevid = drevids[i]
1466 drevid = drevids[i]
1467 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1467 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1468
1468
1469 newdesc = get_amended_desc(drev, old, fold)
1469 newdesc = get_amended_desc(drev, old, fold)
1470 # Make sure commit message contain "Differential Revision"
1470 # Make sure commit message contain "Differential Revision"
1471 if old.description() != newdesc:
1471 if (
1472 old.description() != newdesc
1473 or old.p1().node() in mapping
1474 or old.p2().node() in mapping
1475 ):
1472 if old.phase() == phases.public:
1476 if old.phase() == phases.public:
1473 ui.warn(
1477 ui.warn(
1474 _(b"warning: not updating public commit %s\n")
1478 _(b"warning: not updating public commit %s\n")
1475 % scmutil.formatchangeid(old)
1479 % scmutil.formatchangeid(old)
1476 )
1480 )
1477 continue
1481 continue
1478 parents = [
1482 parents = [
1479 mapping.get(old.p1().node(), (old.p1(),))[0],
1483 mapping.get(old.p1().node(), (old.p1(),))[0],
1480 mapping.get(old.p2().node(), (old.p2(),))[0],
1484 mapping.get(old.p2().node(), (old.p2(),))[0],
1481 ]
1485 ]
1482 new = context.metadataonlyctx(
1486 new = context.metadataonlyctx(
1483 repo,
1487 repo,
1484 old,
1488 old,
1485 parents=parents,
1489 parents=parents,
1486 text=newdesc,
1490 text=newdesc,
1487 user=old.user(),
1491 user=old.user(),
1488 date=old.date(),
1492 date=old.date(),
1489 extra=old.extra(),
1493 extra=old.extra(),
1490 )
1494 )
1491
1495
1492 newnode = new.commit()
1496 newnode = new.commit()
1493
1497
1494 mapping[old.node()] = [newnode]
1498 mapping[old.node()] = [newnode]
1495
1499
1496 if fold:
1500 if fold:
1497 # Defer updating the (single) Diff until all nodes are
1501 # Defer updating the (single) Diff until all nodes are
1498 # collected. No tags were created, so none need to be
1502 # collected. No tags were created, so none need to be
1499 # removed.
1503 # removed.
1500 newnodes.append(newnode)
1504 newnodes.append(newnode)
1501 continue
1505 continue
1502
1506
1503 _amend_diff_properties(
1507 _amend_diff_properties(
1504 unfi, drevid, [newnode], diffmap[old.node()]
1508 unfi, drevid, [newnode], diffmap[old.node()]
1505 )
1509 )
1506
1510
1507 # Remove local tags since it's no longer necessary
1511 # Remove local tags since it's no longer necessary
1508 tagname = b'D%d' % drevid
1512 tagname = b'D%d' % drevid
1509 if tagname in repo.tags():
1513 if tagname in repo.tags():
1510 tags.tag(
1514 tags.tag(
1511 repo,
1515 repo,
1512 tagname,
1516 tagname,
1513 nullid,
1517 nullid,
1514 message=None,
1518 message=None,
1515 user=None,
1519 user=None,
1516 date=None,
1520 date=None,
1517 local=True,
1521 local=True,
1518 )
1522 )
1519 elif fold:
1523 elif fold:
1520 # When folding multiple commits into one review with
1524 # When folding multiple commits into one review with
1521 # --fold, track even the commits that weren't amended, so
1525 # --fold, track even the commits that weren't amended, so
1522 # that their association isn't lost if the properties are
1526 # that their association isn't lost if the properties are
1523 # rewritten below.
1527 # rewritten below.
1524 newnodes.append(old.node())
1528 newnodes.append(old.node())
1525
1529
1526 # If the submitted commits are public, no amend takes place so
1530 # If the submitted commits are public, no amend takes place so
1527 # there are no newnodes and therefore no diff update to do.
1531 # there are no newnodes and therefore no diff update to do.
1528 if fold and newnodes:
1532 if fold and newnodes:
1529 diff = diffmap[old.node()]
1533 diff = diffmap[old.node()]
1530
1534
1531 # The diff object in diffmap doesn't have the local commits
1535 # The diff object in diffmap doesn't have the local commits
1532 # because that could be returned from differential.creatediff,
1536 # because that could be returned from differential.creatediff,
1533 # not differential.querydiffs. So use the queried diff (if
1537 # not differential.querydiffs. So use the queried diff (if
1534 # present), or force the amend (a new revision is being posted.)
1538 # present), or force the amend (a new revision is being posted.)
1535 if not olddiff or set(newnodes) != getlocalcommits(olddiff):
1539 if not olddiff or set(newnodes) != getlocalcommits(olddiff):
1536 _debug(ui, b"updating local commit list for D%d\n" % drevid)
1540 _debug(ui, b"updating local commit list for D%d\n" % drevid)
1537 _amend_diff_properties(unfi, drevid, newnodes, diff)
1541 _amend_diff_properties(unfi, drevid, newnodes, diff)
1538 else:
1542 else:
1539 _debug(
1543 _debug(
1540 ui,
1544 ui,
1541 b"local commit list for D%d is already up-to-date\n"
1545 b"local commit list for D%d is already up-to-date\n"
1542 % drevid,
1546 % drevid,
1543 )
1547 )
1544 elif fold:
1548 elif fold:
1545 _debug(ui, b"no newnodes to update\n")
1549 _debug(ui, b"no newnodes to update\n")
1546
1550
1547 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1551 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1548 if wnode in mapping:
1552 if wnode in mapping:
1549 unfi.setparents(mapping[wnode][0])
1553 unfi.setparents(mapping[wnode][0])
1550
1554
1551
1555
1552 # Map from "hg:meta" keys to header understood by "hg import". The order is
1556 # Map from "hg:meta" keys to header understood by "hg import". The order is
1553 # consistent with "hg export" output.
1557 # consistent with "hg export" output.
1554 _metanamemap = util.sortdict(
1558 _metanamemap = util.sortdict(
1555 [
1559 [
1556 (b'user', b'User'),
1560 (b'user', b'User'),
1557 (b'date', b'Date'),
1561 (b'date', b'Date'),
1558 (b'branch', b'Branch'),
1562 (b'branch', b'Branch'),
1559 (b'node', b'Node ID'),
1563 (b'node', b'Node ID'),
1560 (b'parent', b'Parent '),
1564 (b'parent', b'Parent '),
1561 ]
1565 ]
1562 )
1566 )
1563
1567
1564
1568
1565 def _confirmbeforesend(repo, revs, oldmap):
1569 def _confirmbeforesend(repo, revs, oldmap):
1566 url, token = readurltoken(repo.ui)
1570 url, token = readurltoken(repo.ui)
1567 ui = repo.ui
1571 ui = repo.ui
1568 for rev in revs:
1572 for rev in revs:
1569 ctx = repo[rev]
1573 ctx = repo[rev]
1570 desc = ctx.description().splitlines()[0]
1574 desc = ctx.description().splitlines()[0]
1571 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1575 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1572 if drevid:
1576 if drevid:
1573 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1577 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1574 else:
1578 else:
1575 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1579 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1576
1580
1577 ui.write(
1581 ui.write(
1578 _(b'%s - %s: %s\n')
1582 _(b'%s - %s: %s\n')
1579 % (
1583 % (
1580 drevdesc,
1584 drevdesc,
1581 ui.label(bytes(ctx), b'phabricator.node'),
1585 ui.label(bytes(ctx), b'phabricator.node'),
1582 ui.label(desc, b'phabricator.desc'),
1586 ui.label(desc, b'phabricator.desc'),
1583 )
1587 )
1584 )
1588 )
1585
1589
1586 if ui.promptchoice(
1590 if ui.promptchoice(
1587 _(b'Send the above changes to %s (yn)?$$ &Yes $$ &No') % url
1591 _(b'Send the above changes to %s (yn)?$$ &Yes $$ &No') % url
1588 ):
1592 ):
1589 return False
1593 return False
1590
1594
1591 return True
1595 return True
1592
1596
1593
1597
1594 _knownstatusnames = {
1598 _knownstatusnames = {
1595 b'accepted',
1599 b'accepted',
1596 b'needsreview',
1600 b'needsreview',
1597 b'needsrevision',
1601 b'needsrevision',
1598 b'closed',
1602 b'closed',
1599 b'abandoned',
1603 b'abandoned',
1600 b'changesplanned',
1604 b'changesplanned',
1601 }
1605 }
1602
1606
1603
1607
1604 def _getstatusname(drev):
1608 def _getstatusname(drev):
1605 """get normalized status name from a Differential Revision"""
1609 """get normalized status name from a Differential Revision"""
1606 return drev[b'statusName'].replace(b' ', b'').lower()
1610 return drev[b'statusName'].replace(b' ', b'').lower()
1607
1611
1608
1612
1609 # Small language to specify differential revisions. Support symbols: (), :X,
1613 # Small language to specify differential revisions. Support symbols: (), :X,
1610 # +, and -.
1614 # +, and -.
1611
1615
1612 _elements = {
1616 _elements = {
1613 # token-type: binding-strength, primary, prefix, infix, suffix
1617 # token-type: binding-strength, primary, prefix, infix, suffix
1614 b'(': (12, None, (b'group', 1, b')'), None, None),
1618 b'(': (12, None, (b'group', 1, b')'), None, None),
1615 b':': (8, None, (b'ancestors', 8), None, None),
1619 b':': (8, None, (b'ancestors', 8), None, None),
1616 b'&': (5, None, None, (b'and_', 5), None),
1620 b'&': (5, None, None, (b'and_', 5), None),
1617 b'+': (4, None, None, (b'add', 4), None),
1621 b'+': (4, None, None, (b'add', 4), None),
1618 b'-': (4, None, None, (b'sub', 4), None),
1622 b'-': (4, None, None, (b'sub', 4), None),
1619 b')': (0, None, None, None, None),
1623 b')': (0, None, None, None, None),
1620 b'symbol': (0, b'symbol', None, None, None),
1624 b'symbol': (0, b'symbol', None, None, None),
1621 b'end': (0, None, None, None, None),
1625 b'end': (0, None, None, None, None),
1622 }
1626 }
1623
1627
1624
1628
1625 def _tokenize(text):
1629 def _tokenize(text):
1626 view = memoryview(text) # zero-copy slice
1630 view = memoryview(text) # zero-copy slice
1627 special = b'():+-& '
1631 special = b'():+-& '
1628 pos = 0
1632 pos = 0
1629 length = len(text)
1633 length = len(text)
1630 while pos < length:
1634 while pos < length:
1631 symbol = b''.join(
1635 symbol = b''.join(
1632 itertools.takewhile(
1636 itertools.takewhile(
1633 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1637 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1634 )
1638 )
1635 )
1639 )
1636 if symbol:
1640 if symbol:
1637 yield (b'symbol', symbol, pos)
1641 yield (b'symbol', symbol, pos)
1638 pos += len(symbol)
1642 pos += len(symbol)
1639 else: # special char, ignore space
1643 else: # special char, ignore space
1640 if text[pos : pos + 1] != b' ':
1644 if text[pos : pos + 1] != b' ':
1641 yield (text[pos : pos + 1], None, pos)
1645 yield (text[pos : pos + 1], None, pos)
1642 pos += 1
1646 pos += 1
1643 yield (b'end', None, pos)
1647 yield (b'end', None, pos)
1644
1648
1645
1649
1646 def _parse(text):
1650 def _parse(text):
1647 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1651 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1648 if pos != len(text):
1652 if pos != len(text):
1649 raise error.ParseError(b'invalid token', pos)
1653 raise error.ParseError(b'invalid token', pos)
1650 return tree
1654 return tree
1651
1655
1652
1656
1653 def _parsedrev(symbol):
1657 def _parsedrev(symbol):
1654 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1658 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1655 if symbol.startswith(b'D') and symbol[1:].isdigit():
1659 if symbol.startswith(b'D') and symbol[1:].isdigit():
1656 return int(symbol[1:])
1660 return int(symbol[1:])
1657 if symbol.isdigit():
1661 if symbol.isdigit():
1658 return int(symbol)
1662 return int(symbol)
1659
1663
1660
1664
1661 def _prefetchdrevs(tree):
1665 def _prefetchdrevs(tree):
1662 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1666 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1663 drevs = set()
1667 drevs = set()
1664 ancestordrevs = set()
1668 ancestordrevs = set()
1665 op = tree[0]
1669 op = tree[0]
1666 if op == b'symbol':
1670 if op == b'symbol':
1667 r = _parsedrev(tree[1])
1671 r = _parsedrev(tree[1])
1668 if r:
1672 if r:
1669 drevs.add(r)
1673 drevs.add(r)
1670 elif op == b'ancestors':
1674 elif op == b'ancestors':
1671 r, a = _prefetchdrevs(tree[1])
1675 r, a = _prefetchdrevs(tree[1])
1672 drevs.update(r)
1676 drevs.update(r)
1673 ancestordrevs.update(r)
1677 ancestordrevs.update(r)
1674 ancestordrevs.update(a)
1678 ancestordrevs.update(a)
1675 else:
1679 else:
1676 for t in tree[1:]:
1680 for t in tree[1:]:
1677 r, a = _prefetchdrevs(t)
1681 r, a = _prefetchdrevs(t)
1678 drevs.update(r)
1682 drevs.update(r)
1679 ancestordrevs.update(a)
1683 ancestordrevs.update(a)
1680 return drevs, ancestordrevs
1684 return drevs, ancestordrevs
1681
1685
1682
1686
1683 def querydrev(ui, spec):
1687 def querydrev(ui, spec):
1684 """return a list of "Differential Revision" dicts
1688 """return a list of "Differential Revision" dicts
1685
1689
1686 spec is a string using a simple query language, see docstring in phabread
1690 spec is a string using a simple query language, see docstring in phabread
1687 for details.
1691 for details.
1688
1692
1689 A "Differential Revision dict" looks like:
1693 A "Differential Revision dict" looks like:
1690
1694
1691 {
1695 {
1692 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1696 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1693 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1697 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1694 "auxiliary": {
1698 "auxiliary": {
1695 "phabricator:depends-on": [
1699 "phabricator:depends-on": [
1696 "PHID-DREV-gbapp366kutjebt7agcd"
1700 "PHID-DREV-gbapp366kutjebt7agcd"
1697 ]
1701 ]
1698 "phabricator:projects": [],
1702 "phabricator:projects": [],
1699 },
1703 },
1700 "branch": "default",
1704 "branch": "default",
1701 "ccs": [],
1705 "ccs": [],
1702 "commits": [],
1706 "commits": [],
1703 "dateCreated": "1499181406",
1707 "dateCreated": "1499181406",
1704 "dateModified": "1499182103",
1708 "dateModified": "1499182103",
1705 "diffs": [
1709 "diffs": [
1706 "3",
1710 "3",
1707 "4",
1711 "4",
1708 ],
1712 ],
1709 "hashes": [],
1713 "hashes": [],
1710 "id": "2",
1714 "id": "2",
1711 "lineCount": "2",
1715 "lineCount": "2",
1712 "phid": "PHID-DREV-672qvysjcczopag46qty",
1716 "phid": "PHID-DREV-672qvysjcczopag46qty",
1713 "properties": {},
1717 "properties": {},
1714 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1718 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1715 "reviewers": [],
1719 "reviewers": [],
1716 "sourcePath": null
1720 "sourcePath": null
1717 "status": "0",
1721 "status": "0",
1718 "statusName": "Needs Review",
1722 "statusName": "Needs Review",
1719 "summary": "",
1723 "summary": "",
1720 "testPlan": "",
1724 "testPlan": "",
1721 "title": "example",
1725 "title": "example",
1722 "uri": "https://phab.example.com/D2",
1726 "uri": "https://phab.example.com/D2",
1723 }
1727 }
1724 """
1728 """
1725 # TODO: replace differential.query and differential.querydiffs with
1729 # TODO: replace differential.query and differential.querydiffs with
1726 # differential.diff.search because the former (and their output) are
1730 # differential.diff.search because the former (and their output) are
1727 # frozen, and planned to be deprecated and removed.
1731 # frozen, and planned to be deprecated and removed.
1728
1732
1729 def fetch(params):
1733 def fetch(params):
1730 """params -> single drev or None"""
1734 """params -> single drev or None"""
1731 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1735 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1732 if key in prefetched:
1736 if key in prefetched:
1733 return prefetched[key]
1737 return prefetched[key]
1734 drevs = callconduit(ui, b'differential.query', params)
1738 drevs = callconduit(ui, b'differential.query', params)
1735 # Fill prefetched with the result
1739 # Fill prefetched with the result
1736 for drev in drevs:
1740 for drev in drevs:
1737 prefetched[drev[b'phid']] = drev
1741 prefetched[drev[b'phid']] = drev
1738 prefetched[int(drev[b'id'])] = drev
1742 prefetched[int(drev[b'id'])] = drev
1739 if key not in prefetched:
1743 if key not in prefetched:
1740 raise error.Abort(
1744 raise error.Abort(
1741 _(b'cannot get Differential Revision %r') % params
1745 _(b'cannot get Differential Revision %r') % params
1742 )
1746 )
1743 return prefetched[key]
1747 return prefetched[key]
1744
1748
1745 def getstack(topdrevids):
1749 def getstack(topdrevids):
1746 """given a top, get a stack from the bottom, [id] -> [id]"""
1750 """given a top, get a stack from the bottom, [id] -> [id]"""
1747 visited = set()
1751 visited = set()
1748 result = []
1752 result = []
1749 queue = [{b'ids': [i]} for i in topdrevids]
1753 queue = [{b'ids': [i]} for i in topdrevids]
1750 while queue:
1754 while queue:
1751 params = queue.pop()
1755 params = queue.pop()
1752 drev = fetch(params)
1756 drev = fetch(params)
1753 if drev[b'id'] in visited:
1757 if drev[b'id'] in visited:
1754 continue
1758 continue
1755 visited.add(drev[b'id'])
1759 visited.add(drev[b'id'])
1756 result.append(int(drev[b'id']))
1760 result.append(int(drev[b'id']))
1757 auxiliary = drev.get(b'auxiliary', {})
1761 auxiliary = drev.get(b'auxiliary', {})
1758 depends = auxiliary.get(b'phabricator:depends-on', [])
1762 depends = auxiliary.get(b'phabricator:depends-on', [])
1759 for phid in depends:
1763 for phid in depends:
1760 queue.append({b'phids': [phid]})
1764 queue.append({b'phids': [phid]})
1761 result.reverse()
1765 result.reverse()
1762 return smartset.baseset(result)
1766 return smartset.baseset(result)
1763
1767
1764 # Initialize prefetch cache
1768 # Initialize prefetch cache
1765 prefetched = {} # {id or phid: drev}
1769 prefetched = {} # {id or phid: drev}
1766
1770
1767 tree = _parse(spec)
1771 tree = _parse(spec)
1768 drevs, ancestordrevs = _prefetchdrevs(tree)
1772 drevs, ancestordrevs = _prefetchdrevs(tree)
1769
1773
1770 # developer config: phabricator.batchsize
1774 # developer config: phabricator.batchsize
1771 batchsize = ui.configint(b'phabricator', b'batchsize')
1775 batchsize = ui.configint(b'phabricator', b'batchsize')
1772
1776
1773 # Prefetch Differential Revisions in batch
1777 # Prefetch Differential Revisions in batch
1774 tofetch = set(drevs)
1778 tofetch = set(drevs)
1775 for r in ancestordrevs:
1779 for r in ancestordrevs:
1776 tofetch.update(range(max(1, r - batchsize), r + 1))
1780 tofetch.update(range(max(1, r - batchsize), r + 1))
1777 if drevs:
1781 if drevs:
1778 fetch({b'ids': list(tofetch)})
1782 fetch({b'ids': list(tofetch)})
1779 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1783 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1780
1784
1781 # Walk through the tree, return smartsets
1785 # Walk through the tree, return smartsets
1782 def walk(tree):
1786 def walk(tree):
1783 op = tree[0]
1787 op = tree[0]
1784 if op == b'symbol':
1788 if op == b'symbol':
1785 drev = _parsedrev(tree[1])
1789 drev = _parsedrev(tree[1])
1786 if drev:
1790 if drev:
1787 return smartset.baseset([drev])
1791 return smartset.baseset([drev])
1788 elif tree[1] in _knownstatusnames:
1792 elif tree[1] in _knownstatusnames:
1789 drevs = [
1793 drevs = [
1790 r
1794 r
1791 for r in validids
1795 for r in validids
1792 if _getstatusname(prefetched[r]) == tree[1]
1796 if _getstatusname(prefetched[r]) == tree[1]
1793 ]
1797 ]
1794 return smartset.baseset(drevs)
1798 return smartset.baseset(drevs)
1795 else:
1799 else:
1796 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1800 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1797 elif op in {b'and_', b'add', b'sub'}:
1801 elif op in {b'and_', b'add', b'sub'}:
1798 assert len(tree) == 3
1802 assert len(tree) == 3
1799 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1803 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1800 elif op == b'group':
1804 elif op == b'group':
1801 return walk(tree[1])
1805 return walk(tree[1])
1802 elif op == b'ancestors':
1806 elif op == b'ancestors':
1803 return getstack(walk(tree[1]))
1807 return getstack(walk(tree[1]))
1804 else:
1808 else:
1805 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1809 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1806
1810
1807 return [prefetched[r] for r in walk(tree)]
1811 return [prefetched[r] for r in walk(tree)]
1808
1812
1809
1813
1810 def getdescfromdrev(drev):
1814 def getdescfromdrev(drev):
1811 """get description (commit message) from "Differential Revision"
1815 """get description (commit message) from "Differential Revision"
1812
1816
1813 This is similar to differential.getcommitmessage API. But we only care
1817 This is similar to differential.getcommitmessage API. But we only care
1814 about limited fields: title, summary, test plan, and URL.
1818 about limited fields: title, summary, test plan, and URL.
1815 """
1819 """
1816 title = drev[b'title']
1820 title = drev[b'title']
1817 summary = drev[b'summary'].rstrip()
1821 summary = drev[b'summary'].rstrip()
1818 testplan = drev[b'testPlan'].rstrip()
1822 testplan = drev[b'testPlan'].rstrip()
1819 if testplan:
1823 if testplan:
1820 testplan = b'Test Plan:\n%s' % testplan
1824 testplan = b'Test Plan:\n%s' % testplan
1821 uri = b'Differential Revision: %s' % drev[b'uri']
1825 uri = b'Differential Revision: %s' % drev[b'uri']
1822 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1826 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1823
1827
1824
1828
1825 def get_amended_desc(drev, ctx, folded):
1829 def get_amended_desc(drev, ctx, folded):
1826 """similar to ``getdescfromdrev``, but supports a folded series of commits
1830 """similar to ``getdescfromdrev``, but supports a folded series of commits
1827
1831
1828 This is used when determining if an individual commit needs to have its
1832 This is used when determining if an individual commit needs to have its
1829 message amended after posting it for review. The determination is made for
1833 message amended after posting it for review. The determination is made for
1830 each individual commit, even when they were folded into one review.
1834 each individual commit, even when they were folded into one review.
1831 """
1835 """
1832 if not folded:
1836 if not folded:
1833 return getdescfromdrev(drev)
1837 return getdescfromdrev(drev)
1834
1838
1835 uri = b'Differential Revision: %s' % drev[b'uri']
1839 uri = b'Differential Revision: %s' % drev[b'uri']
1836
1840
1837 # Since the commit messages were combined when posting multiple commits
1841 # Since the commit messages were combined when posting multiple commits
1838 # with --fold, the fields can't be read from Phabricator here, or *all*
1842 # with --fold, the fields can't be read from Phabricator here, or *all*
1839 # affected local revisions will end up with the same commit message after
1843 # affected local revisions will end up with the same commit message after
1840 # the URI is amended in. Append in the DREV line, or update it if it
1844 # the URI is amended in. Append in the DREV line, or update it if it
1841 # exists. At worst, this means commit message or test plan updates on
1845 # exists. At worst, this means commit message or test plan updates on
1842 # Phabricator aren't propagated back to the repository, but that seems
1846 # Phabricator aren't propagated back to the repository, but that seems
1843 # reasonable for the case where local commits are effectively combined
1847 # reasonable for the case where local commits are effectively combined
1844 # in Phabricator.
1848 # in Phabricator.
1845 m = _differentialrevisiondescre.search(ctx.description())
1849 m = _differentialrevisiondescre.search(ctx.description())
1846 if not m:
1850 if not m:
1847 return b'\n\n'.join([ctx.description(), uri])
1851 return b'\n\n'.join([ctx.description(), uri])
1848
1852
1849 return _differentialrevisiondescre.sub(uri, ctx.description())
1853 return _differentialrevisiondescre.sub(uri, ctx.description())
1850
1854
1851
1855
1852 def getlocalcommits(diff):
1856 def getlocalcommits(diff):
1853 """get the set of local commits from a diff object
1857 """get the set of local commits from a diff object
1854
1858
1855 See ``getdiffmeta()`` for an example diff object.
1859 See ``getdiffmeta()`` for an example diff object.
1856 """
1860 """
1857 props = diff.get(b'properties') or {}
1861 props = diff.get(b'properties') or {}
1858 commits = props.get(b'local:commits') or {}
1862 commits = props.get(b'local:commits') or {}
1859 if len(commits) > 1:
1863 if len(commits) > 1:
1860 return {bin(c) for c in commits.keys()}
1864 return {bin(c) for c in commits.keys()}
1861
1865
1862 # Storing the diff metadata predates storing `local:commits`, so continue
1866 # Storing the diff metadata predates storing `local:commits`, so continue
1863 # to use that in the --no-fold case.
1867 # to use that in the --no-fold case.
1864 return {bin(getdiffmeta(diff).get(b'node', b'')) or None}
1868 return {bin(getdiffmeta(diff).get(b'node', b'')) or None}
1865
1869
1866
1870
1867 def getdiffmeta(diff):
1871 def getdiffmeta(diff):
1868 """get commit metadata (date, node, user, p1) from a diff object
1872 """get commit metadata (date, node, user, p1) from a diff object
1869
1873
1870 The metadata could be "hg:meta", sent by phabsend, like:
1874 The metadata could be "hg:meta", sent by phabsend, like:
1871
1875
1872 "properties": {
1876 "properties": {
1873 "hg:meta": {
1877 "hg:meta": {
1874 "branch": "default",
1878 "branch": "default",
1875 "date": "1499571514 25200",
1879 "date": "1499571514 25200",
1876 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
1880 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
1877 "user": "Foo Bar <foo@example.com>",
1881 "user": "Foo Bar <foo@example.com>",
1878 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
1882 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
1879 }
1883 }
1880 }
1884 }
1881
1885
1882 Or converted from "local:commits", sent by "arc", like:
1886 Or converted from "local:commits", sent by "arc", like:
1883
1887
1884 "properties": {
1888 "properties": {
1885 "local:commits": {
1889 "local:commits": {
1886 "98c08acae292b2faf60a279b4189beb6cff1414d": {
1890 "98c08acae292b2faf60a279b4189beb6cff1414d": {
1887 "author": "Foo Bar",
1891 "author": "Foo Bar",
1888 "authorEmail": "foo@example.com"
1892 "authorEmail": "foo@example.com"
1889 "branch": "default",
1893 "branch": "default",
1890 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
1894 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
1891 "local": "1000",
1895 "local": "1000",
1892 "message": "...",
1896 "message": "...",
1893 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
1897 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
1894 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
1898 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
1895 "summary": "...",
1899 "summary": "...",
1896 "tag": "",
1900 "tag": "",
1897 "time": 1499546314,
1901 "time": 1499546314,
1898 }
1902 }
1899 }
1903 }
1900 }
1904 }
1901
1905
1902 Note: metadata extracted from "local:commits" will lose time zone
1906 Note: metadata extracted from "local:commits" will lose time zone
1903 information.
1907 information.
1904 """
1908 """
1905 props = diff.get(b'properties') or {}
1909 props = diff.get(b'properties') or {}
1906 meta = props.get(b'hg:meta')
1910 meta = props.get(b'hg:meta')
1907 if not meta:
1911 if not meta:
1908 if props.get(b'local:commits'):
1912 if props.get(b'local:commits'):
1909 commit = sorted(props[b'local:commits'].values())[0]
1913 commit = sorted(props[b'local:commits'].values())[0]
1910 meta = {}
1914 meta = {}
1911 if b'author' in commit and b'authorEmail' in commit:
1915 if b'author' in commit and b'authorEmail' in commit:
1912 meta[b'user'] = b'%s <%s>' % (
1916 meta[b'user'] = b'%s <%s>' % (
1913 commit[b'author'],
1917 commit[b'author'],
1914 commit[b'authorEmail'],
1918 commit[b'authorEmail'],
1915 )
1919 )
1916 if b'time' in commit:
1920 if b'time' in commit:
1917 meta[b'date'] = b'%d 0' % int(commit[b'time'])
1921 meta[b'date'] = b'%d 0' % int(commit[b'time'])
1918 if b'branch' in commit:
1922 if b'branch' in commit:
1919 meta[b'branch'] = commit[b'branch']
1923 meta[b'branch'] = commit[b'branch']
1920 node = commit.get(b'commit', commit.get(b'rev'))
1924 node = commit.get(b'commit', commit.get(b'rev'))
1921 if node:
1925 if node:
1922 meta[b'node'] = node
1926 meta[b'node'] = node
1923 if len(commit.get(b'parents', ())) >= 1:
1927 if len(commit.get(b'parents', ())) >= 1:
1924 meta[b'parent'] = commit[b'parents'][0]
1928 meta[b'parent'] = commit[b'parents'][0]
1925 else:
1929 else:
1926 meta = {}
1930 meta = {}
1927 if b'date' not in meta and b'dateCreated' in diff:
1931 if b'date' not in meta and b'dateCreated' in diff:
1928 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
1932 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
1929 if b'branch' not in meta and diff.get(b'branch'):
1933 if b'branch' not in meta and diff.get(b'branch'):
1930 meta[b'branch'] = diff[b'branch']
1934 meta[b'branch'] = diff[b'branch']
1931 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
1935 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
1932 meta[b'parent'] = diff[b'sourceControlBaseRevision']
1936 meta[b'parent'] = diff[b'sourceControlBaseRevision']
1933 return meta
1937 return meta
1934
1938
1935
1939
1936 def _getdrevs(ui, stack, specs):
1940 def _getdrevs(ui, stack, specs):
1937 """convert user supplied DREVSPECs into "Differential Revision" dicts
1941 """convert user supplied DREVSPECs into "Differential Revision" dicts
1938
1942
1939 See ``hg help phabread`` for how to specify each DREVSPEC.
1943 See ``hg help phabread`` for how to specify each DREVSPEC.
1940 """
1944 """
1941 if len(specs) > 0:
1945 if len(specs) > 0:
1942
1946
1943 def _formatspec(s):
1947 def _formatspec(s):
1944 if stack:
1948 if stack:
1945 s = b':(%s)' % s
1949 s = b':(%s)' % s
1946 return b'(%s)' % s
1950 return b'(%s)' % s
1947
1951
1948 spec = b'+'.join(pycompat.maplist(_formatspec, specs))
1952 spec = b'+'.join(pycompat.maplist(_formatspec, specs))
1949
1953
1950 drevs = querydrev(ui, spec)
1954 drevs = querydrev(ui, spec)
1951 if drevs:
1955 if drevs:
1952 return drevs
1956 return drevs
1953
1957
1954 raise error.Abort(_(b"empty DREVSPEC set"))
1958 raise error.Abort(_(b"empty DREVSPEC set"))
1955
1959
1956
1960
1957 def readpatch(ui, drevs, write):
1961 def readpatch(ui, drevs, write):
1958 """generate plain-text patch readable by 'hg import'
1962 """generate plain-text patch readable by 'hg import'
1959
1963
1960 write takes a list of (DREV, bytes), where DREV is the differential number
1964 write takes a list of (DREV, bytes), where DREV is the differential number
1961 (as bytes, without the "D" prefix) and the bytes are the text of a patch
1965 (as bytes, without the "D" prefix) and the bytes are the text of a patch
1962 to be imported. drevs is what "querydrev" returns, results of
1966 to be imported. drevs is what "querydrev" returns, results of
1963 "differential.query".
1967 "differential.query".
1964 """
1968 """
1965 # Prefetch hg:meta property for all diffs
1969 # Prefetch hg:meta property for all diffs
1966 diffids = sorted({max(int(v) for v in drev[b'diffs']) for drev in drevs})
1970 diffids = sorted({max(int(v) for v in drev[b'diffs']) for drev in drevs})
1967 diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})
1971 diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})
1968
1972
1969 patches = []
1973 patches = []
1970
1974
1971 # Generate patch for each drev
1975 # Generate patch for each drev
1972 for drev in drevs:
1976 for drev in drevs:
1973 ui.note(_(b'reading D%s\n') % drev[b'id'])
1977 ui.note(_(b'reading D%s\n') % drev[b'id'])
1974
1978
1975 diffid = max(int(v) for v in drev[b'diffs'])
1979 diffid = max(int(v) for v in drev[b'diffs'])
1976 body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})
1980 body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})
1977 desc = getdescfromdrev(drev)
1981 desc = getdescfromdrev(drev)
1978 header = b'# HG changeset patch\n'
1982 header = b'# HG changeset patch\n'
1979
1983
1980 # Try to preserve metadata from hg:meta property. Write hg patch
1984 # Try to preserve metadata from hg:meta property. Write hg patch
1981 # headers that can be read by the "import" command. See patchheadermap
1985 # headers that can be read by the "import" command. See patchheadermap
1982 # and extract in mercurial/patch.py for supported headers.
1986 # and extract in mercurial/patch.py for supported headers.
1983 meta = getdiffmeta(diffs[b'%d' % diffid])
1987 meta = getdiffmeta(diffs[b'%d' % diffid])
1984 for k in _metanamemap.keys():
1988 for k in _metanamemap.keys():
1985 if k in meta:
1989 if k in meta:
1986 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
1990 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
1987
1991
1988 content = b'%s%s\n%s' % (header, desc, body)
1992 content = b'%s%s\n%s' % (header, desc, body)
1989 patches.append((drev[b'id'], content))
1993 patches.append((drev[b'id'], content))
1990
1994
1991 # Write patches to the supplied callback
1995 # Write patches to the supplied callback
1992 write(patches)
1996 write(patches)
1993
1997
1994
1998
1995 @vcrcommand(
1999 @vcrcommand(
1996 b'phabread',
2000 b'phabread',
1997 [(b'', b'stack', False, _(b'read dependencies'))],
2001 [(b'', b'stack', False, _(b'read dependencies'))],
1998 _(b'DREVSPEC... [OPTIONS]'),
2002 _(b'DREVSPEC... [OPTIONS]'),
1999 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2003 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2000 optionalrepo=True,
2004 optionalrepo=True,
2001 )
2005 )
2002 def phabread(ui, repo, *specs, **opts):
2006 def phabread(ui, repo, *specs, **opts):
2003 """print patches from Phabricator suitable for importing
2007 """print patches from Phabricator suitable for importing
2004
2008
2005 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
2009 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
2006 the number ``123``. It could also have common operators like ``+``, ``-``,
2010 the number ``123``. It could also have common operators like ``+``, ``-``,
2007 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
2011 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
2008 select a stack. If multiple DREVSPEC values are given, the result is the
2012 select a stack. If multiple DREVSPEC values are given, the result is the
2009 union of each individually evaluated value. No attempt is currently made
2013 union of each individually evaluated value. No attempt is currently made
2010 to reorder the values to run from parent to child.
2014 to reorder the values to run from parent to child.
2011
2015
2012 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
2016 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
2013 could be used to filter patches by status. For performance reason, they
2017 could be used to filter patches by status. For performance reason, they
2014 only represent a subset of non-status selections and cannot be used alone.
2018 only represent a subset of non-status selections and cannot be used alone.
2015
2019
2016 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
2020 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
2017 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
2021 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
2018 stack up to D9.
2022 stack up to D9.
2019
2023
2020 If --stack is given, follow dependencies information and read all patches.
2024 If --stack is given, follow dependencies information and read all patches.
2021 It is equivalent to the ``:`` operator.
2025 It is equivalent to the ``:`` operator.
2022 """
2026 """
2023 opts = pycompat.byteskwargs(opts)
2027 opts = pycompat.byteskwargs(opts)
2024 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2028 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2025
2029
2026 def _write(patches):
2030 def _write(patches):
2027 for drev, content in patches:
2031 for drev, content in patches:
2028 ui.write(content)
2032 ui.write(content)
2029
2033
2030 readpatch(ui, drevs, _write)
2034 readpatch(ui, drevs, _write)
2031
2035
2032
2036
2033 @vcrcommand(
2037 @vcrcommand(
2034 b'phabimport',
2038 b'phabimport',
2035 [(b'', b'stack', False, _(b'import dependencies as well'))],
2039 [(b'', b'stack', False, _(b'import dependencies as well'))],
2036 _(b'DREVSPEC... [OPTIONS]'),
2040 _(b'DREVSPEC... [OPTIONS]'),
2037 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2041 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2038 )
2042 )
2039 def phabimport(ui, repo, *specs, **opts):
2043 def phabimport(ui, repo, *specs, **opts):
2040 """import patches from Phabricator for the specified Differential Revisions
2044 """import patches from Phabricator for the specified Differential Revisions
2041
2045
2042 The patches are read and applied starting at the parent of the working
2046 The patches are read and applied starting at the parent of the working
2043 directory.
2047 directory.
2044
2048
2045 See ``hg help phabread`` for how to specify DREVSPEC.
2049 See ``hg help phabread`` for how to specify DREVSPEC.
2046 """
2050 """
2047 opts = pycompat.byteskwargs(opts)
2051 opts = pycompat.byteskwargs(opts)
2048
2052
2049 # --bypass avoids losing exec and symlink bits when importing on Windows,
2053 # --bypass avoids losing exec and symlink bits when importing on Windows,
2050 # and allows importing with a dirty wdir. It also aborts instead of leaving
2054 # and allows importing with a dirty wdir. It also aborts instead of leaving
2051 # rejects.
2055 # rejects.
2052 opts[b'bypass'] = True
2056 opts[b'bypass'] = True
2053
2057
2054 # Mandatory default values, synced with commands.import
2058 # Mandatory default values, synced with commands.import
2055 opts[b'strip'] = 1
2059 opts[b'strip'] = 1
2056 opts[b'prefix'] = b''
2060 opts[b'prefix'] = b''
2057 # Evolve 9.3.0 assumes this key is present in cmdutil.tryimportone()
2061 # Evolve 9.3.0 assumes this key is present in cmdutil.tryimportone()
2058 opts[b'obsolete'] = False
2062 opts[b'obsolete'] = False
2059
2063
2060 if ui.configbool(b'phabimport', b'secret'):
2064 if ui.configbool(b'phabimport', b'secret'):
2061 opts[b'secret'] = True
2065 opts[b'secret'] = True
2062 if ui.configbool(b'phabimport', b'obsolete'):
2066 if ui.configbool(b'phabimport', b'obsolete'):
2063 opts[b'obsolete'] = True # Handled by evolve wrapping tryimportone()
2067 opts[b'obsolete'] = True # Handled by evolve wrapping tryimportone()
2064
2068
2065 def _write(patches):
2069 def _write(patches):
2066 parents = repo[None].parents()
2070 parents = repo[None].parents()
2067
2071
2068 with repo.wlock(), repo.lock(), repo.transaction(b'phabimport'):
2072 with repo.wlock(), repo.lock(), repo.transaction(b'phabimport'):
2069 for drev, contents in patches:
2073 for drev, contents in patches:
2070 ui.status(_(b'applying patch from D%s\n') % drev)
2074 ui.status(_(b'applying patch from D%s\n') % drev)
2071
2075
2072 with patch.extract(ui, pycompat.bytesio(contents)) as patchdata:
2076 with patch.extract(ui, pycompat.bytesio(contents)) as patchdata:
2073 msg, node, rej = cmdutil.tryimportone(
2077 msg, node, rej = cmdutil.tryimportone(
2074 ui,
2078 ui,
2075 repo,
2079 repo,
2076 patchdata,
2080 patchdata,
2077 parents,
2081 parents,
2078 opts,
2082 opts,
2079 [],
2083 [],
2080 None, # Never update wdir to another revision
2084 None, # Never update wdir to another revision
2081 )
2085 )
2082
2086
2083 if not node:
2087 if not node:
2084 raise error.Abort(_(b'D%s: no diffs found') % drev)
2088 raise error.Abort(_(b'D%s: no diffs found') % drev)
2085
2089
2086 ui.note(msg + b'\n')
2090 ui.note(msg + b'\n')
2087 parents = [repo[node]]
2091 parents = [repo[node]]
2088
2092
2089 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2093 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2090
2094
2091 readpatch(repo.ui, drevs, _write)
2095 readpatch(repo.ui, drevs, _write)
2092
2096
2093
2097
2094 @vcrcommand(
2098 @vcrcommand(
2095 b'phabupdate',
2099 b'phabupdate',
2096 [
2100 [
2097 (b'', b'accept', False, _(b'accept revisions')),
2101 (b'', b'accept', False, _(b'accept revisions')),
2098 (b'', b'reject', False, _(b'reject revisions')),
2102 (b'', b'reject', False, _(b'reject revisions')),
2099 (b'', b'abandon', False, _(b'abandon revisions')),
2103 (b'', b'abandon', False, _(b'abandon revisions')),
2100 (b'', b'reclaim', False, _(b'reclaim revisions')),
2104 (b'', b'reclaim', False, _(b'reclaim revisions')),
2101 (b'm', b'comment', b'', _(b'comment on the last revision')),
2105 (b'm', b'comment', b'', _(b'comment on the last revision')),
2102 ],
2106 ],
2103 _(b'DREVSPEC... [OPTIONS]'),
2107 _(b'DREVSPEC... [OPTIONS]'),
2104 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2108 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2105 optionalrepo=True,
2109 optionalrepo=True,
2106 )
2110 )
2107 def phabupdate(ui, repo, *specs, **opts):
2111 def phabupdate(ui, repo, *specs, **opts):
2108 """update Differential Revision in batch
2112 """update Differential Revision in batch
2109
2113
2110 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
2114 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
2111 """
2115 """
2112 opts = pycompat.byteskwargs(opts)
2116 opts = pycompat.byteskwargs(opts)
2113 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
2117 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
2114 if len(flags) > 1:
2118 if len(flags) > 1:
2115 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
2119 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
2116
2120
2117 actions = []
2121 actions = []
2118 for f in flags:
2122 for f in flags:
2119 actions.append({b'type': f, b'value': True})
2123 actions.append({b'type': f, b'value': True})
2120
2124
2121 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2125 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2122 for i, drev in enumerate(drevs):
2126 for i, drev in enumerate(drevs):
2123 if i + 1 == len(drevs) and opts.get(b'comment'):
2127 if i + 1 == len(drevs) and opts.get(b'comment'):
2124 actions.append({b'type': b'comment', b'value': opts[b'comment']})
2128 actions.append({b'type': b'comment', b'value': opts[b'comment']})
2125 if actions:
2129 if actions:
2126 params = {
2130 params = {
2127 b'objectIdentifier': drev[b'phid'],
2131 b'objectIdentifier': drev[b'phid'],
2128 b'transactions': actions,
2132 b'transactions': actions,
2129 }
2133 }
2130 callconduit(ui, b'differential.revision.edit', params)
2134 callconduit(ui, b'differential.revision.edit', params)
2131
2135
2132
2136
2133 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
2137 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
2134 def template_review(context, mapping):
2138 def template_review(context, mapping):
2135 """:phabreview: Object describing the review for this changeset.
2139 """:phabreview: Object describing the review for this changeset.
2136 Has attributes `url` and `id`.
2140 Has attributes `url` and `id`.
2137 """
2141 """
2138 ctx = context.resource(mapping, b'ctx')
2142 ctx = context.resource(mapping, b'ctx')
2139 m = _differentialrevisiondescre.search(ctx.description())
2143 m = _differentialrevisiondescre.search(ctx.description())
2140 if m:
2144 if m:
2141 return templateutil.hybriddict(
2145 return templateutil.hybriddict(
2142 {b'url': m.group('url'), b'id': b"D%s" % m.group('id'),}
2146 {b'url': m.group('url'), b'id': b"D%s" % m.group('id'),}
2143 )
2147 )
2144 else:
2148 else:
2145 tags = ctx.repo().nodetags(ctx.node())
2149 tags = ctx.repo().nodetags(ctx.node())
2146 for t in tags:
2150 for t in tags:
2147 if _differentialrevisiontagre.match(t):
2151 if _differentialrevisiontagre.match(t):
2148 url = ctx.repo().ui.config(b'phabricator', b'url')
2152 url = ctx.repo().ui.config(b'phabricator', b'url')
2149 if not url.endswith(b'/'):
2153 if not url.endswith(b'/'):
2150 url += b'/'
2154 url += b'/'
2151 url += t
2155 url += t
2152
2156
2153 return templateutil.hybriddict({b'url': url, b'id': t,})
2157 return templateutil.hybriddict({b'url': url, b'id': t,})
2154 return None
2158 return None
2155
2159
2156
2160
2157 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
2161 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
2158 def template_status(context, mapping):
2162 def template_status(context, mapping):
2159 """:phabstatus: String. Status of Phabricator differential.
2163 """:phabstatus: String. Status of Phabricator differential.
2160 """
2164 """
2161 ctx = context.resource(mapping, b'ctx')
2165 ctx = context.resource(mapping, b'ctx')
2162 repo = context.resource(mapping, b'repo')
2166 repo = context.resource(mapping, b'repo')
2163 ui = context.resource(mapping, b'ui')
2167 ui = context.resource(mapping, b'ui')
2164
2168
2165 rev = ctx.rev()
2169 rev = ctx.rev()
2166 try:
2170 try:
2167 drevid = getdrevmap(repo, [rev])[rev]
2171 drevid = getdrevmap(repo, [rev])[rev]
2168 except KeyError:
2172 except KeyError:
2169 return None
2173 return None
2170 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
2174 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
2171 for drev in drevs:
2175 for drev in drevs:
2172 if int(drev[b'id']) == drevid:
2176 if int(drev[b'id']) == drevid:
2173 return templateutil.hybriddict(
2177 return templateutil.hybriddict(
2174 {b'url': drev[b'uri'], b'status': drev[b'statusName'],}
2178 {b'url': drev[b'uri'], b'status': drev[b'statusName'],}
2175 )
2179 )
2176 return None
2180 return None
2177
2181
2178
2182
2179 @show.showview(b'phabstatus', csettopic=b'work')
2183 @show.showview(b'phabstatus', csettopic=b'work')
2180 def phabstatusshowview(ui, repo, displayer):
2184 def phabstatusshowview(ui, repo, displayer):
2181 """Phabricator differiential status"""
2185 """Phabricator differiential status"""
2182 revs = repo.revs('sort(_underway(), topo)')
2186 revs = repo.revs('sort(_underway(), topo)')
2183 drevmap = getdrevmap(repo, revs)
2187 drevmap = getdrevmap(repo, revs)
2184 unknownrevs, drevids, revsbydrevid = [], set(), {}
2188 unknownrevs, drevids, revsbydrevid = [], set(), {}
2185 for rev, drevid in pycompat.iteritems(drevmap):
2189 for rev, drevid in pycompat.iteritems(drevmap):
2186 if drevid is not None:
2190 if drevid is not None:
2187 drevids.add(drevid)
2191 drevids.add(drevid)
2188 revsbydrevid.setdefault(drevid, set()).add(rev)
2192 revsbydrevid.setdefault(drevid, set()).add(rev)
2189 else:
2193 else:
2190 unknownrevs.append(rev)
2194 unknownrevs.append(rev)
2191
2195
2192 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
2196 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
2193 drevsbyrev = {}
2197 drevsbyrev = {}
2194 for drev in drevs:
2198 for drev in drevs:
2195 for rev in revsbydrevid[int(drev[b'id'])]:
2199 for rev in revsbydrevid[int(drev[b'id'])]:
2196 drevsbyrev[rev] = drev
2200 drevsbyrev[rev] = drev
2197
2201
2198 def phabstatus(ctx):
2202 def phabstatus(ctx):
2199 drev = drevsbyrev[ctx.rev()]
2203 drev = drevsbyrev[ctx.rev()]
2200 status = ui.label(
2204 status = ui.label(
2201 b'%(statusName)s' % drev,
2205 b'%(statusName)s' % drev,
2202 b'phabricator.status.%s' % _getstatusname(drev),
2206 b'phabricator.status.%s' % _getstatusname(drev),
2203 )
2207 )
2204 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
2208 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
2205
2209
2206 revs -= smartset.baseset(unknownrevs)
2210 revs -= smartset.baseset(unknownrevs)
2207 revdag = graphmod.dagwalker(repo, revs)
2211 revdag = graphmod.dagwalker(repo, revs)
2208
2212
2209 ui.setconfig(b'experimental', b'graphshorten', True)
2213 ui.setconfig(b'experimental', b'graphshorten', True)
2210 displayer._exthook = phabstatus
2214 displayer._exthook = phabstatus
2211 nodelen = show.longestshortest(repo, revs)
2215 nodelen = show.longestshortest(repo, revs)
2212 logcmdutil.displaygraph(
2216 logcmdutil.displaygraph(
2213 ui,
2217 ui,
2214 repo,
2218 repo,
2215 revdag,
2219 revdag,
2216 displayer,
2220 displayer,
2217 graphmod.asciiedges,
2221 graphmod.asciiedges,
2218 props={b'nodelen': nodelen},
2222 props={b'nodelen': nodelen},
2219 )
2223 )
@@ -1,788 +1,838 b''
1 #require vcr
1 #require vcr
2 $ cat >> $HGRCPATH <<EOF
2 $ cat >> $HGRCPATH <<EOF
3 > [extensions]
3 > [extensions]
4 > phabricator =
4 > phabricator =
5 >
5 >
6 > [auth]
6 > [auth]
7 > hgphab.schemes = https
7 > hgphab.schemes = https
8 > hgphab.prefix = phab.mercurial-scm.org
8 > hgphab.prefix = phab.mercurial-scm.org
9 > # When working on the extension and making phabricator interaction
9 > # When working on the extension and making phabricator interaction
10 > # changes, edit this to be a real phabricator token. When done, edit
10 > # changes, edit this to be a real phabricator token. When done, edit
11 > # it back. The VCR transcripts will be auto-sanitised to replace your real
11 > # it back. The VCR transcripts will be auto-sanitised to replace your real
12 > # token with this value.
12 > # token with this value.
13 > hgphab.phabtoken = cli-hahayouwish
13 > hgphab.phabtoken = cli-hahayouwish
14 >
14 >
15 > [phabricator]
15 > [phabricator]
16 > debug = True
16 > debug = True
17 > EOF
17 > EOF
18 $ hg init repo
18 $ hg init repo
19 $ cd repo
19 $ cd repo
20 $ cat >> .hg/hgrc <<EOF
20 $ cat >> .hg/hgrc <<EOF
21 > [phabricator]
21 > [phabricator]
22 > url = https://phab.mercurial-scm.org/
22 > url = https://phab.mercurial-scm.org/
23 > callsign = HG
23 > callsign = HG
24 > EOF
24 > EOF
25 $ VCR="$TESTDIR/phabricator"
25 $ VCR="$TESTDIR/phabricator"
26
26
27 Error is handled reasonably. We override the phabtoken here so that
27 Error is handled reasonably. We override the phabtoken here so that
28 when you're developing changes to phabricator.py you can edit the
28 when you're developing changes to phabricator.py you can edit the
29 above config and have a real token in the test but not have to edit
29 above config and have a real token in the test but not have to edit
30 this test.
30 this test.
31 $ hg phabread --config auth.hgphab.phabtoken=cli-notavalidtoken \
31 $ hg phabread --config auth.hgphab.phabtoken=cli-notavalidtoken \
32 > --test-vcr "$VCR/phabread-conduit-error.json" D4480 | head
32 > --test-vcr "$VCR/phabread-conduit-error.json" D4480 | head
33 abort: Conduit Error (ERR-INVALID-AUTH): API token "cli-notavalidtoken" has the wrong length. API tokens should be 32 characters long.
33 abort: Conduit Error (ERR-INVALID-AUTH): API token "cli-notavalidtoken" has the wrong length. API tokens should be 32 characters long.
34
34
35 Missing arguments don't crash, and may print the command help
35 Missing arguments don't crash, and may print the command help
36
36
37 $ hg debugcallconduit
37 $ hg debugcallconduit
38 hg debugcallconduit: invalid arguments
38 hg debugcallconduit: invalid arguments
39 hg debugcallconduit METHOD
39 hg debugcallconduit METHOD
40
40
41 call Conduit API
41 call Conduit API
42
42
43 options:
43 options:
44
44
45 (use 'hg debugcallconduit -h' to show more help)
45 (use 'hg debugcallconduit -h' to show more help)
46 [255]
46 [255]
47 $ hg phabread
47 $ hg phabread
48 abort: empty DREVSPEC set
48 abort: empty DREVSPEC set
49 [255]
49 [255]
50
50
51 Basic phabread:
51 Basic phabread:
52 $ hg phabread --test-vcr "$VCR/phabread-4480.json" D4480 | head
52 $ hg phabread --test-vcr "$VCR/phabread-4480.json" D4480 | head
53 # HG changeset patch
53 # HG changeset patch
54 # Date 1536771503 0
54 # Date 1536771503 0
55 # Parent a5de21c9e3703f8e8eb064bd7d893ff2f703c66a
55 # Parent a5de21c9e3703f8e8eb064bd7d893ff2f703c66a
56 exchangev2: start to implement pull with wire protocol v2
56 exchangev2: start to implement pull with wire protocol v2
57
57
58 Wire protocol version 2 will take a substantially different
58 Wire protocol version 2 will take a substantially different
59 approach to exchange than version 1 (at least as far as pulling
59 approach to exchange than version 1 (at least as far as pulling
60 is concerned).
60 is concerned).
61
61
62 This commit establishes a new exchangev2 module for holding
62 This commit establishes a new exchangev2 module for holding
63
63
64 Phabread with multiple DREVSPEC
64 Phabread with multiple DREVSPEC
65
65
66 TODO: attempt to order related revisions like --stack?
66 TODO: attempt to order related revisions like --stack?
67 $ hg phabread --test-vcr "$VCR/phabread-multi-drev.json" D8205 8206 D8207 \
67 $ hg phabread --test-vcr "$VCR/phabread-multi-drev.json" D8205 8206 D8207 \
68 > | grep '^Differential Revision'
68 > | grep '^Differential Revision'
69 Differential Revision: https://phab.mercurial-scm.org/D8205
69 Differential Revision: https://phab.mercurial-scm.org/D8205
70 Differential Revision: https://phab.mercurial-scm.org/D8206
70 Differential Revision: https://phab.mercurial-scm.org/D8206
71 Differential Revision: https://phab.mercurial-scm.org/D8207
71 Differential Revision: https://phab.mercurial-scm.org/D8207
72
72
73 Empty DREVSPECs don't crash
73 Empty DREVSPECs don't crash
74
74
75 $ hg phabread --test-vcr "$VCR/phabread-empty-drev.json" D7917-D7917
75 $ hg phabread --test-vcr "$VCR/phabread-empty-drev.json" D7917-D7917
76 abort: empty DREVSPEC set
76 abort: empty DREVSPEC set
77 [255]
77 [255]
78
78
79
79
80 phabupdate with an accept:
80 phabupdate with an accept:
81 $ hg phabupdate --accept D4564 \
81 $ hg phabupdate --accept D4564 \
82 > -m 'I think I like where this is headed. Will read rest of series later.'\
82 > -m 'I think I like where this is headed. Will read rest of series later.'\
83 > --test-vcr "$VCR/accept-4564.json"
83 > --test-vcr "$VCR/accept-4564.json"
84 abort: Conduit Error (ERR-CONDUIT-CORE): Validation errors:
84 abort: Conduit Error (ERR-CONDUIT-CORE): Validation errors:
85 - You can not accept this revision because it has already been closed. Only open revisions can be accepted.
85 - You can not accept this revision because it has already been closed. Only open revisions can be accepted.
86 [255]
86 [255]
87 $ hg phabupdate --accept D7913 -m 'LGTM' --test-vcr "$VCR/accept-7913.json"
87 $ hg phabupdate --accept D7913 -m 'LGTM' --test-vcr "$VCR/accept-7913.json"
88
88
89 Create a differential diff:
89 Create a differential diff:
90 $ HGENCODING=utf-8; export HGENCODING
90 $ HGENCODING=utf-8; export HGENCODING
91 $ echo alpha > alpha
91 $ echo alpha > alpha
92 $ hg ci --addremove -m 'create alpha for phabricator test €'
92 $ hg ci --addremove -m 'create alpha for phabricator test €'
93 adding alpha
93 adding alpha
94 $ hg phabsend -r . --test-vcr "$VCR/phabsend-create-alpha.json"
94 $ hg phabsend -r . --test-vcr "$VCR/phabsend-create-alpha.json"
95 D7915 - created - d386117f30e6: create alpha for phabricator test \xe2\x82\xac (esc)
95 D7915 - created - d386117f30e6: create alpha for phabricator test \xe2\x82\xac (esc)
96 new commits: ['347bf67801e5']
96 new commits: ['347bf67801e5']
97 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d386117f30e6-24ffe649-phabsend.hg
97 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d386117f30e6-24ffe649-phabsend.hg
98 $ echo more >> alpha
98 $ echo more >> alpha
99 $ HGEDITOR=true hg ci --amend
99 $ HGEDITOR=true hg ci --amend
100 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/347bf67801e5-3bf313e4-amend.hg
100 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/347bf67801e5-3bf313e4-amend.hg
101 $ echo beta > beta
101 $ echo beta > beta
102 $ hg ci --addremove -m 'create beta for phabricator test'
102 $ hg ci --addremove -m 'create beta for phabricator test'
103 adding beta
103 adding beta
104 $ hg phabsend -r ".^::" --test-vcr "$VCR/phabsend-update-alpha-create-beta.json"
104 $ hg phabsend -r ".^::" --test-vcr "$VCR/phabsend-update-alpha-create-beta.json"
105 c44b38f24a45 mapped to old nodes []
105 c44b38f24a45 mapped to old nodes []
106 D7915 - updated - c44b38f24a45: create alpha for phabricator test \xe2\x82\xac (esc)
106 D7915 - updated - c44b38f24a45: create alpha for phabricator test \xe2\x82\xac (esc)
107 D7916 - created - 9e6901f21d5b: create beta for phabricator test
107 D7916 - created - 9e6901f21d5b: create beta for phabricator test
108 new commits: ['a692622e6937']
108 new commits: ['a692622e6937']
109 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9e6901f21d5b-1fcd4f0e-phabsend.hg
109 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9e6901f21d5b-1fcd4f0e-phabsend.hg
110 $ unset HGENCODING
110 $ unset HGENCODING
111
111
112 The amend won't explode after posting a public commit. The local tag is left
112 The amend won't explode after posting a public commit. The local tag is left
113 behind to identify it.
113 behind to identify it.
114
114
115 $ echo 'public change' > beta
115 $ echo 'public change' > beta
116 $ hg ci -m 'create public change for phabricator testing'
116 $ hg ci -m 'create public change for phabricator testing'
117 $ hg phase --public .
117 $ hg phase --public .
118 $ echo 'draft change' > alpha
118 $ echo 'draft change' > alpha
119 $ hg ci -m 'create draft change for phabricator testing'
119 $ hg ci -m 'create draft change for phabricator testing'
120 $ hg phabsend --amend -r '.^::' --test-vcr "$VCR/phabsend-create-public.json"
120 $ hg phabsend --amend -r '.^::' --test-vcr "$VCR/phabsend-create-public.json"
121 D7917 - created - 7b4185ab5d16: create public change for phabricator testing
121 D7917 - created - 7b4185ab5d16: create public change for phabricator testing
122 D7918 - created - 251c1c333fc6: create draft change for phabricator testing
122 D7918 - created - 251c1c333fc6: create draft change for phabricator testing
123 warning: not updating public commit 2:7b4185ab5d16
123 warning: not updating public commit 2:7b4185ab5d16
124 new commits: ['3244dc4a3334']
124 new commits: ['3244dc4a3334']
125 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/251c1c333fc6-41cb7c3b-phabsend.hg
125 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/251c1c333fc6-41cb7c3b-phabsend.hg
126 $ hg tags -v
126 $ hg tags -v
127 tip 3:3244dc4a3334
127 tip 3:3244dc4a3334
128 D7917 2:7b4185ab5d16 local
128 D7917 2:7b4185ab5d16 local
129
129
130 $ hg debugcallconduit user.search --test-vcr "$VCR/phab-conduit.json" <<EOF
130 $ hg debugcallconduit user.search --test-vcr "$VCR/phab-conduit.json" <<EOF
131 > {
131 > {
132 > "constraints": {
132 > "constraints": {
133 > "isBot": true
133 > "isBot": true
134 > }
134 > }
135 > }
135 > }
136 > EOF
136 > EOF
137 {
137 {
138 "cursor": {
138 "cursor": {
139 "after": null,
139 "after": null,
140 "before": null,
140 "before": null,
141 "limit": 100,
141 "limit": 100,
142 "order": null
142 "order": null
143 },
143 },
144 "data": [],
144 "data": [],
145 "maps": {},
145 "maps": {},
146 "query": {
146 "query": {
147 "queryKey": null
147 "queryKey": null
148 }
148 }
149 }
149 }
150
150
151 Template keywords
151 Template keywords
152 $ hg log -T'{rev} {phabreview|json}\n'
152 $ hg log -T'{rev} {phabreview|json}\n'
153 3 {"id": "D7918", "url": "https://phab.mercurial-scm.org/D7918"}
153 3 {"id": "D7918", "url": "https://phab.mercurial-scm.org/D7918"}
154 2 {"id": "D7917", "url": "https://phab.mercurial-scm.org/D7917"}
154 2 {"id": "D7917", "url": "https://phab.mercurial-scm.org/D7917"}
155 1 {"id": "D7916", "url": "https://phab.mercurial-scm.org/D7916"}
155 1 {"id": "D7916", "url": "https://phab.mercurial-scm.org/D7916"}
156 0 {"id": "D7915", "url": "https://phab.mercurial-scm.org/D7915"}
156 0 {"id": "D7915", "url": "https://phab.mercurial-scm.org/D7915"}
157
157
158 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n'
158 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n'
159 3 https://phab.mercurial-scm.org/D7918 D7918
159 3 https://phab.mercurial-scm.org/D7918 D7918
160 2 https://phab.mercurial-scm.org/D7917 D7917
160 2 https://phab.mercurial-scm.org/D7917 D7917
161 1 https://phab.mercurial-scm.org/D7916 D7916
161 1 https://phab.mercurial-scm.org/D7916 D7916
162 0 https://phab.mercurial-scm.org/D7915 D7915
162 0 https://phab.mercurial-scm.org/D7915 D7915
163
163
164 Commenting when phabsending:
164 Commenting when phabsending:
165 $ echo comment > comment
165 $ echo comment > comment
166 $ hg ci --addremove -m "create comment for phabricator test"
166 $ hg ci --addremove -m "create comment for phabricator test"
167 adding comment
167 adding comment
168 $ hg phabsend -r . -m "For default branch" --test-vcr "$VCR/phabsend-comment-created.json"
168 $ hg phabsend -r . -m "For default branch" --test-vcr "$VCR/phabsend-comment-created.json"
169 D7919 - created - d5dddca9023d: create comment for phabricator test
169 D7919 - created - d5dddca9023d: create comment for phabricator test
170 new commits: ['f7db812bbe1d']
170 new commits: ['f7db812bbe1d']
171 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d5dddca9023d-adf673ba-phabsend.hg
171 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d5dddca9023d-adf673ba-phabsend.hg
172 $ echo comment2 >> comment
172 $ echo comment2 >> comment
173 $ hg ci --amend
173 $ hg ci --amend
174 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f7db812bbe1d-8fcded77-amend.hg
174 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f7db812bbe1d-8fcded77-amend.hg
175 $ hg phabsend -r . -m "Address review comments" --test-vcr "$VCR/phabsend-comment-updated.json"
175 $ hg phabsend -r . -m "Address review comments" --test-vcr "$VCR/phabsend-comment-updated.json"
176 1849d7828727 mapped to old nodes []
176 1849d7828727 mapped to old nodes []
177 D7919 - updated - 1849d7828727: create comment for phabricator test
177 D7919 - updated - 1849d7828727: create comment for phabricator test
178
178
179 Phabsending a skipped commit:
179 Phabsending a skipped commit:
180 $ hg phabsend --no-amend -r . --test-vcr "$VCR/phabsend-skipped.json"
180 $ hg phabsend --no-amend -r . --test-vcr "$VCR/phabsend-skipped.json"
181 1849d7828727 mapped to old nodes ['1849d7828727']
181 1849d7828727 mapped to old nodes ['1849d7828727']
182 D7919 - skipped - 1849d7828727: create comment for phabricator test
182 D7919 - skipped - 1849d7828727: create comment for phabricator test
183
183
184 Phabsend doesn't create an instability when rebasing existing revisions on top
185 of new revisions.
186
187 $ hg init reorder
188 $ cd reorder
189 $ cat >> .hg/hgrc <<EOF
190 > [phabricator]
191 > url = https://phab.mercurial-scm.org/
192 > callsign = HG
193 > [experimental]
194 > evolution = all
195 > EOF
196
197 $ echo "add" > file1.txt
198 $ hg ci -Aqm 'added'
199 $ echo "mod1" > file1.txt
200 $ hg ci -m 'modified 1'
201 $ echo "mod2" > file1.txt
202 $ hg ci -m 'modified 2'
203 $ hg phabsend -r . --test-vcr "$VCR/phabsend-add-parent-setup.json"
204 D8433 - created - 5d3959e20d1d: modified 2
205 new commits: ['2b4aa8a88d61']
206 $ hg log -G -T compact
207 @ 3[tip]:1 2b4aa8a88d61 1970-01-01 00:00 +0000 test
208 | modified 2
209 |
210 o 1 d549263bcb2d 1970-01-01 00:00 +0000 test
211 | modified 1
212 |
213 o 0 5cbade24e0fa 1970-01-01 00:00 +0000 test
214 added
215
216 $ hg phabsend -r ".^ + ." --test-vcr "$VCR/phabsend-add-parent.json"
217 2b4aa8a88d61 mapped to old nodes ['2b4aa8a88d61']
218 D8434 - created - d549263bcb2d: modified 1
219 D8433 - updated - 2b4aa8a88d61: modified 2
220 new commits: ['876a60d024de']
221 new commits: ['0c6523cb1d0f']
222 $ hg log -G -T compact
223 @ 5[tip] 1dff6b051abf 1970-01-01 00:00 +0000 test
224 | modified 2
225 |
226 o 4:0 eb3752621d45 1970-01-01 00:00 +0000 test
227 | modified 1
228 |
229 o 0 5cbade24e0fa 1970-01-01 00:00 +0000 test
230 added
231
232 $ cd ..
233
184 Phabesending a new binary, a modified binary, and a removed binary
234 Phabesending a new binary, a modified binary, and a removed binary
185
235
186 >>> open('bin', 'wb').write(b'\0a') and None
236 >>> open('bin', 'wb').write(b'\0a') and None
187 $ hg ci -Am 'add binary'
237 $ hg ci -Am 'add binary'
188 adding bin
238 adding bin
189 >>> open('bin', 'wb').write(b'\0b') and None
239 >>> open('bin', 'wb').write(b'\0b') and None
190 $ hg ci -m 'modify binary'
240 $ hg ci -m 'modify binary'
191 $ hg rm bin
241 $ hg rm bin
192 $ hg ci -m 'remove binary'
242 $ hg ci -m 'remove binary'
193 $ hg phabsend -r .~2:: --test-vcr "$VCR/phabsend-binary.json"
243 $ hg phabsend -r .~2:: --test-vcr "$VCR/phabsend-binary.json"
194 uploading bin@aa24a81f55de
244 uploading bin@aa24a81f55de
195 D8007 - created - aa24a81f55de: add binary
245 D8007 - created - aa24a81f55de: add binary
196 uploading bin@d8d62a881b54
246 uploading bin@d8d62a881b54
197 D8008 - created - d8d62a881b54: modify binary
247 D8008 - created - d8d62a881b54: modify binary
198 D8009 - created - af55645b2e29: remove binary
248 D8009 - created - af55645b2e29: remove binary
199 new commits: ['b8139fbb4a57']
249 new commits: ['b8139fbb4a57']
200 new commits: ['c88ce4c2d2ad']
250 new commits: ['c88ce4c2d2ad']
201 new commits: ['75dbbc901145']
251 new commits: ['75dbbc901145']
202 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/aa24a81f55de-a3a0cf24-phabsend.hg
252 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/aa24a81f55de-a3a0cf24-phabsend.hg
203
253
204 Phabsend a renamed binary and a copied binary, with and without content changes
254 Phabsend a renamed binary and a copied binary, with and without content changes
205 to src and dest
255 to src and dest
206
256
207 >>> open('bin2', 'wb').write(b'\0c') and None
257 >>> open('bin2', 'wb').write(b'\0c') and None
208 $ hg ci -Am 'add another binary'
258 $ hg ci -Am 'add another binary'
209 adding bin2
259 adding bin2
210
260
211 TODO: "bin2" can't be viewed in this commit (left or right side), and the URL
261 TODO: "bin2" can't be viewed in this commit (left or right side), and the URL
212 looks much different than when viewing "bin2_moved". No idea if this is a phab
262 looks much different than when viewing "bin2_moved". No idea if this is a phab
213 bug, or phabsend bug. The patch (as printed by phabread) look reasonable
263 bug, or phabsend bug. The patch (as printed by phabread) look reasonable
214 though.
264 though.
215
265
216 $ hg mv bin2 bin2_moved
266 $ hg mv bin2 bin2_moved
217 $ hg ci -m "moved binary"
267 $ hg ci -m "moved binary"
218
268
219 Note: "bin2_moved" is also not viewable in phabricator with this review
269 Note: "bin2_moved" is also not viewable in phabricator with this review
220
270
221 $ hg cp bin2_moved bin2_copied
271 $ hg cp bin2_moved bin2_copied
222 $ hg ci -m "copied binary"
272 $ hg ci -m "copied binary"
223
273
224 Note: "bin2_moved_again" is marked binary in phabricator, and both sides of it
274 Note: "bin2_moved_again" is marked binary in phabricator, and both sides of it
225 are viewable in their proper state. "bin2_copied" is not viewable, and not
275 are viewable in their proper state. "bin2_copied" is not viewable, and not
226 listed as binary in phabricator.
276 listed as binary in phabricator.
227
277
228 >>> open('bin2_copied', 'wb').write(b'\0move+mod') and None
278 >>> open('bin2_copied', 'wb').write(b'\0move+mod') and None
229 $ hg mv bin2_copied bin2_moved_again
279 $ hg mv bin2_copied bin2_moved_again
230 $ hg ci -m "move+mod copied binary"
280 $ hg ci -m "move+mod copied binary"
231
281
232 Note: "bin2_moved" and "bin2_moved_copy" are both marked binary, and both
282 Note: "bin2_moved" and "bin2_moved_copy" are both marked binary, and both
233 viewable on each side.
283 viewable on each side.
234
284
235 >>> open('bin2_moved', 'wb').write(b'\0precopy mod') and None
285 >>> open('bin2_moved', 'wb').write(b'\0precopy mod') and None
236 $ hg cp bin2_moved bin2_moved_copied
286 $ hg cp bin2_moved bin2_moved_copied
237 >>> open('bin2_moved', 'wb').write(b'\0copy src+mod') and None
287 >>> open('bin2_moved', 'wb').write(b'\0copy src+mod') and None
238 $ hg ci -m "copy+mod moved binary"
288 $ hg ci -m "copy+mod moved binary"
239
289
240 $ hg phabsend -r .~4:: --test-vcr "$VCR/phabsend-binary-renames.json"
290 $ hg phabsend -r .~4:: --test-vcr "$VCR/phabsend-binary-renames.json"
241 uploading bin2@f42f9195e00c
291 uploading bin2@f42f9195e00c
242 D8128 - created - f42f9195e00c: add another binary
292 D8128 - created - f42f9195e00c: add another binary
243 D8129 - created - 834ab31d80ae: moved binary
293 D8129 - created - 834ab31d80ae: moved binary
244 D8130 - created - 494b750e5194: copied binary
294 D8130 - created - 494b750e5194: copied binary
245 uploading bin2_moved_again@25f766b50cc2
295 uploading bin2_moved_again@25f766b50cc2
246 D8131 - created - 25f766b50cc2: move+mod copied binary
296 D8131 - created - 25f766b50cc2: move+mod copied binary
247 uploading bin2_moved_copied@1b87b363a5e4
297 uploading bin2_moved_copied@1b87b363a5e4
248 uploading bin2_moved@1b87b363a5e4
298 uploading bin2_moved@1b87b363a5e4
249 D8132 - created - 1b87b363a5e4: copy+mod moved binary
299 D8132 - created - 1b87b363a5e4: copy+mod moved binary
250 new commits: ['90437c20312a']
300 new commits: ['90437c20312a']
251 new commits: ['f391f4da4c61']
301 new commits: ['f391f4da4c61']
252 new commits: ['da86a9f3268c']
302 new commits: ['da86a9f3268c']
253 new commits: ['003ffc16ba66']
303 new commits: ['003ffc16ba66']
254 new commits: ['13bd750c36fa']
304 new commits: ['13bd750c36fa']
255 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f42f9195e00c-e82a0769-phabsend.hg
305 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f42f9195e00c-e82a0769-phabsend.hg
256
306
257 Phabreading a DREV with a local:commits time as a string:
307 Phabreading a DREV with a local:commits time as a string:
258 $ hg phabread --test-vcr "$VCR/phabread-str-time.json" D1285
308 $ hg phabread --test-vcr "$VCR/phabread-str-time.json" D1285
259 # HG changeset patch
309 # HG changeset patch
260 # User Pulkit Goyal <7895pulkit@gmail.com>
310 # User Pulkit Goyal <7895pulkit@gmail.com>
261 # Date 1509404054 -19800
311 # Date 1509404054 -19800
262 # Node ID 44fc1c1f1774a76423b9c732af6938435099bcc5
312 # Node ID 44fc1c1f1774a76423b9c732af6938435099bcc5
263 # Parent 8feef8ef8389a3b544e0a74624f1efc3a8d85d35
313 # Parent 8feef8ef8389a3b544e0a74624f1efc3a8d85d35
264 repoview: add a new attribute _visibilityexceptions and related API
314 repoview: add a new attribute _visibilityexceptions and related API
265
315
266 Currently we don't have a defined way in core to make some hidden revisions
316 Currently we don't have a defined way in core to make some hidden revisions
267 visible in filtered repo. Extensions to achieve the purpose of unhiding some
317 visible in filtered repo. Extensions to achieve the purpose of unhiding some
268 hidden commits, wrap repoview.pinnedrevs() function.
318 hidden commits, wrap repoview.pinnedrevs() function.
269
319
270 To make the above task simple and have well defined API, this patch adds a new
320 To make the above task simple and have well defined API, this patch adds a new
271 attribute '_visibilityexceptions' to repoview class which will contains
321 attribute '_visibilityexceptions' to repoview class which will contains
272 the hidden revs which should be exception.
322 the hidden revs which should be exception.
273 This will allow to set different exceptions for different repoview objects
323 This will allow to set different exceptions for different repoview objects
274 backed by the same unfiltered repo.
324 backed by the same unfiltered repo.
275
325
276 This patch also adds API to add revs to the attribute set and get them.
326 This patch also adds API to add revs to the attribute set and get them.
277
327
278 Thanks to Jun for suggesting the use of repoview class instead of localrepo.
328 Thanks to Jun for suggesting the use of repoview class instead of localrepo.
279
329
280 Differential Revision: https://phab.mercurial-scm.org/D1285
330 Differential Revision: https://phab.mercurial-scm.org/D1285
281 diff --git a/mercurial/repoview.py b/mercurial/repoview.py
331 diff --git a/mercurial/repoview.py b/mercurial/repoview.py
282 --- a/mercurial/repoview.py
332 --- a/mercurial/repoview.py
283 +++ b/mercurial/repoview.py
333 +++ b/mercurial/repoview.py
284 @@ * @@ (glob)
334 @@ * @@ (glob)
285 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
335 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
286 """
336 """
287
337
288 + # hidden revs which should be visible
338 + # hidden revs which should be visible
289 + _visibilityexceptions = set()
339 + _visibilityexceptions = set()
290 +
340 +
291 def __init__(self, repo, filtername):
341 def __init__(self, repo, filtername):
292 object.__setattr__(self, r'_unfilteredrepo', repo)
342 object.__setattr__(self, r'_unfilteredrepo', repo)
293 object.__setattr__(self, r'filtername', filtername)
343 object.__setattr__(self, r'filtername', filtername)
294 @@ -231,6 +234,14 @@
344 @@ -231,6 +234,14 @@
295 return self
345 return self
296 return self.unfiltered().filtered(name)
346 return self.unfiltered().filtered(name)
297
347
298 + def addvisibilityexceptions(self, revs):
348 + def addvisibilityexceptions(self, revs):
299 + """adds hidden revs which should be visible to set of exceptions"""
349 + """adds hidden revs which should be visible to set of exceptions"""
300 + self._visibilityexceptions.update(revs)
350 + self._visibilityexceptions.update(revs)
301 +
351 +
302 + def getvisibilityexceptions(self):
352 + def getvisibilityexceptions(self):
303 + """returns the set of hidden revs which should be visible"""
353 + """returns the set of hidden revs which should be visible"""
304 + return self._visibilityexceptions
354 + return self._visibilityexceptions
305 +
355 +
306 # everything access are forwarded to the proxied repo
356 # everything access are forwarded to the proxied repo
307 def __getattr__(self, attr):
357 def __getattr__(self, attr):
308 return getattr(self._unfilteredrepo, attr)
358 return getattr(self._unfilteredrepo, attr)
309 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
359 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
310 --- a/mercurial/localrepo.py
360 --- a/mercurial/localrepo.py
311 +++ b/mercurial/localrepo.py
361 +++ b/mercurial/localrepo.py
312 @@ -570,6 +570,14 @@
362 @@ -570,6 +570,14 @@
313 def close(self):
363 def close(self):
314 self._writecaches()
364 self._writecaches()
315
365
316 + def addvisibilityexceptions(self, exceptions):
366 + def addvisibilityexceptions(self, exceptions):
317 + # should be called on a filtered repository
367 + # should be called on a filtered repository
318 + pass
368 + pass
319 +
369 +
320 + def getvisibilityexceptions(self):
370 + def getvisibilityexceptions(self):
321 + # should be called on a filtered repository
371 + # should be called on a filtered repository
322 + return set()
372 + return set()
323 +
373 +
324 def _loadextensions(self):
374 def _loadextensions(self):
325 extensions.loadall(self.ui)
375 extensions.loadall(self.ui)
326
376
327
377
328 A bad .arcconfig doesn't error out
378 A bad .arcconfig doesn't error out
329 $ echo 'garbage' > .arcconfig
379 $ echo 'garbage' > .arcconfig
330 $ hg config phabricator --debug
380 $ hg config phabricator --debug
331 invalid JSON in $TESTTMP/repo/.arcconfig
381 invalid JSON in $TESTTMP/repo/.arcconfig
332 read config from: */.hgrc (glob)
382 read config from: */.hgrc (glob)
333 */.hgrc:*: phabricator.debug=True (glob)
383 */.hgrc:*: phabricator.debug=True (glob)
334 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=https://phab.mercurial-scm.org/ (glob)
384 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=https://phab.mercurial-scm.org/ (glob)
335 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=HG (glob)
385 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=HG (glob)
336
386
337 The .arcconfig content overrides global config
387 The .arcconfig content overrides global config
338 $ cat >> $HGRCPATH << EOF
388 $ cat >> $HGRCPATH << EOF
339 > [phabricator]
389 > [phabricator]
340 > url = global
390 > url = global
341 > callsign = global
391 > callsign = global
342 > EOF
392 > EOF
343 $ cp $TESTDIR/../.arcconfig .
393 $ cp $TESTDIR/../.arcconfig .
344 $ mv .hg/hgrc .hg/hgrc.bak
394 $ mv .hg/hgrc .hg/hgrc.bak
345 $ hg config phabricator --debug
395 $ hg config phabricator --debug
346 read config from: */.hgrc (glob)
396 read config from: */.hgrc (glob)
347 */.hgrc:*: phabricator.debug=True (glob)
397 */.hgrc:*: phabricator.debug=True (glob)
348 $TESTTMP/repo/.arcconfig: phabricator.callsign=HG
398 $TESTTMP/repo/.arcconfig: phabricator.callsign=HG
349 $TESTTMP/repo/.arcconfig: phabricator.url=https://phab.mercurial-scm.org/
399 $TESTTMP/repo/.arcconfig: phabricator.url=https://phab.mercurial-scm.org/
350
400
351 But it doesn't override local config
401 But it doesn't override local config
352 $ cat >> .hg/hgrc << EOF
402 $ cat >> .hg/hgrc << EOF
353 > [phabricator]
403 > [phabricator]
354 > url = local
404 > url = local
355 > callsign = local
405 > callsign = local
356 > EOF
406 > EOF
357 $ hg config phabricator --debug
407 $ hg config phabricator --debug
358 read config from: */.hgrc (glob)
408 read config from: */.hgrc (glob)
359 */.hgrc:*: phabricator.debug=True (glob)
409 */.hgrc:*: phabricator.debug=True (glob)
360 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=local (glob)
410 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=local (glob)
361 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=local (glob)
411 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=local (glob)
362 $ mv .hg/hgrc.bak .hg/hgrc
412 $ mv .hg/hgrc.bak .hg/hgrc
363
413
364 Phabimport works with a stack
414 Phabimport works with a stack
365
415
366 $ cd ..
416 $ cd ..
367 $ hg clone repo repo2 -qr 1
417 $ hg clone repo repo2 -qr 1
368 $ cp repo/.hg/hgrc repo2/.hg/
418 $ cp repo/.hg/hgrc repo2/.hg/
369 $ cd repo2
419 $ cd repo2
370 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json"
420 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json"
371 applying patch from D7917
421 applying patch from D7917
372 applying patch from D7918
422 applying patch from D7918
373 $ hg log -r .: -G -Tcompact
423 $ hg log -r .: -G -Tcompact
374 o 3[tip] aaef04066140 1970-01-01 00:00 +0000 test
424 o 3[tip] aaef04066140 1970-01-01 00:00 +0000 test
375 | create draft change for phabricator testing
425 | create draft change for phabricator testing
376 |
426 |
377 o 2 8de3712202d1 1970-01-01 00:00 +0000 test
427 o 2 8de3712202d1 1970-01-01 00:00 +0000 test
378 | create public change for phabricator testing
428 | create public change for phabricator testing
379 |
429 |
380 @ 1 a692622e6937 1970-01-01 00:00 +0000 test
430 @ 1 a692622e6937 1970-01-01 00:00 +0000 test
381 | create beta for phabricator test
431 | create beta for phabricator test
382 ~
432 ~
383 Phabimport can create secret commits
433 Phabimport can create secret commits
384
434
385 $ hg rollback --config ui.rollback=True
435 $ hg rollback --config ui.rollback=True
386 repository tip rolled back to revision 1 (undo phabimport)
436 repository tip rolled back to revision 1 (undo phabimport)
387 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json" \
437 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json" \
388 > --config phabimport.secret=True
438 > --config phabimport.secret=True
389 applying patch from D7917
439 applying patch from D7917
390 applying patch from D7918
440 applying patch from D7918
391 $ hg log -r 'reverse(.:)' -T phases
441 $ hg log -r 'reverse(.:)' -T phases
392 changeset: 3:aaef04066140
442 changeset: 3:aaef04066140
393 tag: tip
443 tag: tip
394 phase: secret
444 phase: secret
395 user: test
445 user: test
396 date: Thu Jan 01 00:00:00 1970 +0000
446 date: Thu Jan 01 00:00:00 1970 +0000
397 summary: create draft change for phabricator testing
447 summary: create draft change for phabricator testing
398
448
399 changeset: 2:8de3712202d1
449 changeset: 2:8de3712202d1
400 phase: secret
450 phase: secret
401 user: test
451 user: test
402 date: Thu Jan 01 00:00:00 1970 +0000
452 date: Thu Jan 01 00:00:00 1970 +0000
403 summary: create public change for phabricator testing
453 summary: create public change for phabricator testing
404
454
405 changeset: 1:a692622e6937
455 changeset: 1:a692622e6937
406 phase: public
456 phase: public
407 user: test
457 user: test
408 date: Thu Jan 01 00:00:00 1970 +0000
458 date: Thu Jan 01 00:00:00 1970 +0000
409 summary: create beta for phabricator test
459 summary: create beta for phabricator test
410
460
411 Phabimport accepts multiple DREVSPECs
461 Phabimport accepts multiple DREVSPECs
412
462
413 $ hg rollback --config ui.rollback=True
463 $ hg rollback --config ui.rollback=True
414 repository tip rolled back to revision 1 (undo phabimport)
464 repository tip rolled back to revision 1 (undo phabimport)
415 $ hg phabimport --no-stack D7917 D7918 --test-vcr "$VCR/phabimport-multi-drev.json"
465 $ hg phabimport --no-stack D7917 D7918 --test-vcr "$VCR/phabimport-multi-drev.json"
416 applying patch from D7917
466 applying patch from D7917
417 applying patch from D7918
467 applying patch from D7918
418
468
419 Validate arguments with --fold
469 Validate arguments with --fold
420
470
421 $ hg phabsend --fold -r 1
471 $ hg phabsend --fold -r 1
422 abort: cannot fold a single revision
472 abort: cannot fold a single revision
423 [255]
473 [255]
424 $ hg phabsend --fold --no-amend -r 1::
474 $ hg phabsend --fold --no-amend -r 1::
425 abort: cannot fold with --no-amend
475 abort: cannot fold with --no-amend
426 [255]
476 [255]
427 $ hg phabsend --fold -r 0+3
477 $ hg phabsend --fold -r 0+3
428 abort: cannot fold non-linear revisions
478 abort: cannot fold non-linear revisions
429 [255]
479 [255]
430 $ hg phabsend --fold -r 1::
480 $ hg phabsend --fold -r 1::
431 abort: cannot fold revisions with different DREV values
481 abort: cannot fold revisions with different DREV values
432 [255]
482 [255]
433
483
434 Setup a series of commits to be folded, and include the Test Plan field multiple
484 Setup a series of commits to be folded, and include the Test Plan field multiple
435 times to test the concatenation logic. No Test Plan field in the last one to
485 times to test the concatenation logic. No Test Plan field in the last one to
436 ensure missing fields are skipped.
486 ensure missing fields are skipped.
437
487
438 $ hg init ../folded
488 $ hg init ../folded
439 $ cd ../folded
489 $ cd ../folded
440 $ cat >> .hg/hgrc <<EOF
490 $ cat >> .hg/hgrc <<EOF
441 > [phabricator]
491 > [phabricator]
442 > url = https://phab.mercurial-scm.org/
492 > url = https://phab.mercurial-scm.org/
443 > callsign = HG
493 > callsign = HG
444 > EOF
494 > EOF
445
495
446 $ echo 'added' > file.txt
496 $ echo 'added' > file.txt
447 $ hg ci -Aqm 'added file'
497 $ hg ci -Aqm 'added file'
448
498
449 $ cat > log.txt <<EOF
499 $ cat > log.txt <<EOF
450 > one: first commit to review
500 > one: first commit to review
451 >
501 >
452 > This file was modified with 'mod1' as its contents.
502 > This file was modified with 'mod1' as its contents.
453 >
503 >
454 > Test Plan:
504 > Test Plan:
455 > LOL! What testing?!
505 > LOL! What testing?!
456 > EOF
506 > EOF
457 $ echo mod1 > file.txt
507 $ echo mod1 > file.txt
458 $ hg ci -l log.txt
508 $ hg ci -l log.txt
459
509
460 $ cat > log.txt <<EOF
510 $ cat > log.txt <<EOF
461 > two: second commit to review
511 > two: second commit to review
462 >
512 >
463 > This file was modified with 'mod2' as its contents.
513 > This file was modified with 'mod2' as its contents.
464 >
514 >
465 > Test Plan:
515 > Test Plan:
466 > Haha! yeah, right.
516 > Haha! yeah, right.
467 >
517 >
468 > EOF
518 > EOF
469 $ echo mod2 > file.txt
519 $ echo mod2 > file.txt
470 $ hg ci -l log.txt
520 $ hg ci -l log.txt
471
521
472 $ echo mod3 > file.txt
522 $ echo mod3 > file.txt
473 $ hg ci -m '3: a commit with no detailed message'
523 $ hg ci -m '3: a commit with no detailed message'
474
524
475 The folding of immutable commits works...
525 The folding of immutable commits works...
476
526
477 $ hg phase -r tip --public
527 $ hg phase -r tip --public
478 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-immutable.json"
528 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-immutable.json"
479 D8386 - created - a959a3f69d8d: one: first commit to review
529 D8386 - created - a959a3f69d8d: one: first commit to review
480 D8386 - created - 24a4438154ba: two: second commit to review
530 D8386 - created - 24a4438154ba: two: second commit to review
481 D8386 - created - d235829e802c: 3: a commit with no detailed message
531 D8386 - created - d235829e802c: 3: a commit with no detailed message
482 warning: not updating public commit 1:a959a3f69d8d
532 warning: not updating public commit 1:a959a3f69d8d
483 warning: not updating public commit 2:24a4438154ba
533 warning: not updating public commit 2:24a4438154ba
484 warning: not updating public commit 3:d235829e802c
534 warning: not updating public commit 3:d235829e802c
485 no newnodes to update
535 no newnodes to update
486
536
487 $ hg phase -r 0 --draft --force
537 $ hg phase -r 0 --draft --force
488
538
489 ... as does the initial mutable fold...
539 ... as does the initial mutable fold...
490
540
491 $ echo y | hg phabsend --fold --confirm -r 1:: \
541 $ echo y | hg phabsend --fold --confirm -r 1:: \
492 > --test-vcr "$VCR/phabsend-fold-initial.json"
542 > --test-vcr "$VCR/phabsend-fold-initial.json"
493 NEW - a959a3f69d8d: one: first commit to review
543 NEW - a959a3f69d8d: one: first commit to review
494 NEW - 24a4438154ba: two: second commit to review
544 NEW - 24a4438154ba: two: second commit to review
495 NEW - d235829e802c: 3: a commit with no detailed message
545 NEW - d235829e802c: 3: a commit with no detailed message
496 Send the above changes to https://phab.mercurial-scm.org/ (yn)? y
546 Send the above changes to https://phab.mercurial-scm.org/ (yn)? y
497 D8387 - created - a959a3f69d8d: one: first commit to review
547 D8387 - created - a959a3f69d8d: one: first commit to review
498 D8387 - created - 24a4438154ba: two: second commit to review
548 D8387 - created - 24a4438154ba: two: second commit to review
499 D8387 - created - d235829e802c: 3: a commit with no detailed message
549 D8387 - created - d235829e802c: 3: a commit with no detailed message
500 updating local commit list for D8387
550 updating local commit list for D8387
501 new commits: ['602c4e738243', '832553266fe8', '921f8265efbd']
551 new commits: ['602c4e738243', '832553266fe8', '921f8265efbd']
502 saved backup bundle to $TESTTMP/folded/.hg/strip-backup/a959a3f69d8d-a4a24136-phabsend.hg
552 saved backup bundle to $TESTTMP/folded/.hg/strip-backup/a959a3f69d8d-a4a24136-phabsend.hg
503
553
504 ... and doesn't mangle the local commits.
554 ... and doesn't mangle the local commits.
505
555
506 $ hg log -T '{rev}:{node|short}\n{indent(desc, " ")}\n'
556 $ hg log -T '{rev}:{node|short}\n{indent(desc, " ")}\n'
507 3:921f8265efbd
557 3:921f8265efbd
508 3: a commit with no detailed message
558 3: a commit with no detailed message
509
559
510 Differential Revision: https://phab.mercurial-scm.org/D8387
560 Differential Revision: https://phab.mercurial-scm.org/D8387
511 2:832553266fe8
561 2:832553266fe8
512 two: second commit to review
562 two: second commit to review
513
563
514 This file was modified with 'mod2' as its contents.
564 This file was modified with 'mod2' as its contents.
515
565
516 Test Plan:
566 Test Plan:
517 Haha! yeah, right.
567 Haha! yeah, right.
518
568
519 Differential Revision: https://phab.mercurial-scm.org/D8387
569 Differential Revision: https://phab.mercurial-scm.org/D8387
520 1:602c4e738243
570 1:602c4e738243
521 one: first commit to review
571 one: first commit to review
522
572
523 This file was modified with 'mod1' as its contents.
573 This file was modified with 'mod1' as its contents.
524
574
525 Test Plan:
575 Test Plan:
526 LOL! What testing?!
576 LOL! What testing?!
527
577
528 Differential Revision: https://phab.mercurial-scm.org/D8387
578 Differential Revision: https://phab.mercurial-scm.org/D8387
529 0:98d480e0d494
579 0:98d480e0d494
530 added file
580 added file
531
581
532 Setup some obsmarkers by adding a file to the middle commit. This stress tests
582 Setup some obsmarkers by adding a file to the middle commit. This stress tests
533 getoldnodedrevmap() in later phabsends.
583 getoldnodedrevmap() in later phabsends.
534
584
535 $ hg up '.^'
585 $ hg up '.^'
536 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
586 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
537 $ echo 'modified' > file2.txt
587 $ echo 'modified' > file2.txt
538 $ hg add file2.txt
588 $ hg add file2.txt
539 $ hg amend --config experimental.evolution=all --config extensions.amend=
589 $ hg amend --config experimental.evolution=all --config extensions.amend=
540 1 new orphan changesets
590 1 new orphan changesets
541 $ hg up 3
591 $ hg up 3
542 obsolete feature not enabled but 1 markers found!
592 obsolete feature not enabled but 1 markers found!
543 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
593 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
544 $ hg rebase --config experimental.evolution=all --config extensions.rebase=
594 $ hg rebase --config experimental.evolution=all --config extensions.rebase=
545 note: not rebasing 2:832553266fe8 "two: second commit to review", already in destination as 4:0124e5474c88 "two: second commit to review" (tip)
595 note: not rebasing 2:832553266fe8 "two: second commit to review", already in destination as 4:0124e5474c88 "two: second commit to review" (tip)
546 rebasing 3:921f8265efbd "3: a commit with no detailed message"
596 rebasing 3:921f8265efbd "3: a commit with no detailed message"
547
597
548 When commits have changed locally, the local commit list on Phabricator is
598 When commits have changed locally, the local commit list on Phabricator is
549 updated.
599 updated.
550
600
551 $ echo y | hg phabsend --fold --confirm -r 1:: \
601 $ echo y | hg phabsend --fold --confirm -r 1:: \
552 > --test-vcr "$VCR/phabsend-fold-updated.json"
602 > --test-vcr "$VCR/phabsend-fold-updated.json"
553 obsolete feature not enabled but 2 markers found!
603 obsolete feature not enabled but 2 markers found!
554 602c4e738243 mapped to old nodes ['602c4e738243']
604 602c4e738243 mapped to old nodes ['602c4e738243']
555 0124e5474c88 mapped to old nodes ['832553266fe8']
605 0124e5474c88 mapped to old nodes ['832553266fe8']
556 e4edb1fe3565 mapped to old nodes ['921f8265efbd']
606 e4edb1fe3565 mapped to old nodes ['921f8265efbd']
557 D8387 - 602c4e738243: one: first commit to review
607 D8387 - 602c4e738243: one: first commit to review
558 D8387 - 0124e5474c88: two: second commit to review
608 D8387 - 0124e5474c88: two: second commit to review
559 D8387 - e4edb1fe3565: 3: a commit with no detailed message
609 D8387 - e4edb1fe3565: 3: a commit with no detailed message
560 Send the above changes to https://phab.mercurial-scm.org/ (yn)? y
610 Send the above changes to https://phab.mercurial-scm.org/ (yn)? y
561 D8387 - updated - 602c4e738243: one: first commit to review
611 D8387 - updated - 602c4e738243: one: first commit to review
562 D8387 - updated - 0124e5474c88: two: second commit to review
612 D8387 - updated - 0124e5474c88: two: second commit to review
563 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
613 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
564 obsolete feature not enabled but 2 markers found! (?)
614 obsolete feature not enabled but 2 markers found! (?)
565 updating local commit list for D8387
615 updating local commit list for D8387
566 new commits: ['602c4e738243', '0124e5474c88', 'e4edb1fe3565']
616 new commits: ['602c4e738243', '0124e5474c88', 'e4edb1fe3565']
567 $ hg log -Tcompact
617 $ hg log -Tcompact
568 obsolete feature not enabled but 2 markers found!
618 obsolete feature not enabled but 2 markers found!
569 5[tip] e4edb1fe3565 1970-01-01 00:00 +0000 test
619 5[tip] e4edb1fe3565 1970-01-01 00:00 +0000 test
570 3: a commit with no detailed message
620 3: a commit with no detailed message
571
621
572 4:1 0124e5474c88 1970-01-01 00:00 +0000 test
622 4:1 0124e5474c88 1970-01-01 00:00 +0000 test
573 two: second commit to review
623 two: second commit to review
574
624
575 1 602c4e738243 1970-01-01 00:00 +0000 test
625 1 602c4e738243 1970-01-01 00:00 +0000 test
576 one: first commit to review
626 one: first commit to review
577
627
578 0 98d480e0d494 1970-01-01 00:00 +0000 test
628 0 98d480e0d494 1970-01-01 00:00 +0000 test
579 added file
629 added file
580
630
581 When nothing has changed locally since the last phabsend, the commit list isn't
631 When nothing has changed locally since the last phabsend, the commit list isn't
582 updated, and nothing is changed locally afterward.
632 updated, and nothing is changed locally afterward.
583
633
584 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-no-changes.json"
634 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-no-changes.json"
585 obsolete feature not enabled but 2 markers found!
635 obsolete feature not enabled but 2 markers found!
586 602c4e738243 mapped to old nodes ['602c4e738243']
636 602c4e738243 mapped to old nodes ['602c4e738243']
587 0124e5474c88 mapped to old nodes ['0124e5474c88']
637 0124e5474c88 mapped to old nodes ['0124e5474c88']
588 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
638 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
589 D8387 - updated - 602c4e738243: one: first commit to review
639 D8387 - updated - 602c4e738243: one: first commit to review
590 D8387 - updated - 0124e5474c88: two: second commit to review
640 D8387 - updated - 0124e5474c88: two: second commit to review
591 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
641 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
592 obsolete feature not enabled but 2 markers found! (?)
642 obsolete feature not enabled but 2 markers found! (?)
593 local commit list for D8387 is already up-to-date
643 local commit list for D8387 is already up-to-date
594 $ hg log -Tcompact
644 $ hg log -Tcompact
595 obsolete feature not enabled but 2 markers found!
645 obsolete feature not enabled but 2 markers found!
596 5[tip] e4edb1fe3565 1970-01-01 00:00 +0000 test
646 5[tip] e4edb1fe3565 1970-01-01 00:00 +0000 test
597 3: a commit with no detailed message
647 3: a commit with no detailed message
598
648
599 4:1 0124e5474c88 1970-01-01 00:00 +0000 test
649 4:1 0124e5474c88 1970-01-01 00:00 +0000 test
600 two: second commit to review
650 two: second commit to review
601
651
602 1 602c4e738243 1970-01-01 00:00 +0000 test
652 1 602c4e738243 1970-01-01 00:00 +0000 test
603 one: first commit to review
653 one: first commit to review
604
654
605 0 98d480e0d494 1970-01-01 00:00 +0000 test
655 0 98d480e0d494 1970-01-01 00:00 +0000 test
606 added file
656 added file
607
657
608 Fold will accept new revisions at the end...
658 Fold will accept new revisions at the end...
609
659
610 $ echo 'another mod' > file2.txt
660 $ echo 'another mod' > file2.txt
611 $ hg ci -m 'four: extend the fold range'
661 $ hg ci -m 'four: extend the fold range'
612 obsolete feature not enabled but 2 markers found!
662 obsolete feature not enabled but 2 markers found!
613 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-extend-end.json" \
663 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-extend-end.json" \
614 > --config experimental.evolution=all
664 > --config experimental.evolution=all
615 602c4e738243 mapped to old nodes ['602c4e738243']
665 602c4e738243 mapped to old nodes ['602c4e738243']
616 0124e5474c88 mapped to old nodes ['0124e5474c88']
666 0124e5474c88 mapped to old nodes ['0124e5474c88']
617 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
667 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
618 D8387 - updated - 602c4e738243: one: first commit to review
668 D8387 - updated - 602c4e738243: one: first commit to review
619 D8387 - updated - 0124e5474c88: two: second commit to review
669 D8387 - updated - 0124e5474c88: two: second commit to review
620 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
670 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
621 D8387 - created - 94aaae213b23: four: extend the fold range
671 D8387 - created - 94aaae213b23: four: extend the fold range
622 updating local commit list for D8387
672 updating local commit list for D8387
623 new commits: ['602c4e738243', '0124e5474c88', 'e4edb1fe3565', '51a04fea8707']
673 new commits: ['602c4e738243', '0124e5474c88', 'e4edb1fe3565', '51a04fea8707']
624 $ hg log -r . -T '{desc}\n'
674 $ hg log -r . -T '{desc}\n'
625 four: extend the fold range
675 four: extend the fold range
626
676
627 Differential Revision: https://phab.mercurial-scm.org/D8387
677 Differential Revision: https://phab.mercurial-scm.org/D8387
628 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n' -r 1::
678 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n' -r 1::
629 obsolete feature not enabled but 3 markers found!
679 obsolete feature not enabled but 3 markers found!
630 1 https://phab.mercurial-scm.org/D8387 D8387
680 1 https://phab.mercurial-scm.org/D8387 D8387
631 4 https://phab.mercurial-scm.org/D8387 D8387
681 4 https://phab.mercurial-scm.org/D8387 D8387
632 5 https://phab.mercurial-scm.org/D8387 D8387
682 5 https://phab.mercurial-scm.org/D8387 D8387
633 7 https://phab.mercurial-scm.org/D8387 D8387
683 7 https://phab.mercurial-scm.org/D8387 D8387
634
684
635 ... and also accepts new revisions at the beginning of the range
685 ... and also accepts new revisions at the beginning of the range
636
686
637 It's a bit unfortunate that not having a Differential URL on the first commit
687 It's a bit unfortunate that not having a Differential URL on the first commit
638 causes a new Differential Revision to be created, though it isn't *entirely*
688 causes a new Differential Revision to be created, though it isn't *entirely*
639 unreasonable. At least this updates the subsequent commits.
689 unreasonable. At least this updates the subsequent commits.
640
690
641 TODO: See if it can reuse the existing Differential.
691 TODO: See if it can reuse the existing Differential.
642
692
643 $ hg phabsend --fold -r 0:: --test-vcr "$VCR/phabsend-fold-extend-front.json" \
693 $ hg phabsend --fold -r 0:: --test-vcr "$VCR/phabsend-fold-extend-front.json" \
644 > --config experimental.evolution=all
694 > --config experimental.evolution=all
645 602c4e738243 mapped to old nodes ['602c4e738243']
695 602c4e738243 mapped to old nodes ['602c4e738243']
646 0124e5474c88 mapped to old nodes ['0124e5474c88']
696 0124e5474c88 mapped to old nodes ['0124e5474c88']
647 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
697 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
648 51a04fea8707 mapped to old nodes ['51a04fea8707']
698 51a04fea8707 mapped to old nodes ['51a04fea8707']
649 D8388 - created - 98d480e0d494: added file
699 D8388 - created - 98d480e0d494: added file
650 D8388 - updated - 602c4e738243: one: first commit to review
700 D8388 - updated - 602c4e738243: one: first commit to review
651 D8388 - updated - 0124e5474c88: two: second commit to review
701 D8388 - updated - 0124e5474c88: two: second commit to review
652 D8388 - updated - e4edb1fe3565: 3: a commit with no detailed message
702 D8388 - updated - e4edb1fe3565: 3: a commit with no detailed message
653 D8388 - updated - 51a04fea8707: four: extend the fold range
703 D8388 - updated - 51a04fea8707: four: extend the fold range
654 updating local commit list for D8388
704 updating local commit list for D8388
655 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', 'ac7db67f0991']
705 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', 'ac7db67f0991']
656
706
657 $ hg log -T '{rev}:{node|short}\n{indent(desc, " ")}\n'
707 $ hg log -T '{rev}:{node|short}\n{indent(desc, " ")}\n'
658 obsolete feature not enabled but 8 markers found!
708 obsolete feature not enabled but 8 markers found!
659 12:ac7db67f0991
709 12:ac7db67f0991
660 four: extend the fold range
710 four: extend the fold range
661
711
662 Differential Revision: https://phab.mercurial-scm.org/D8388
712 Differential Revision: https://phab.mercurial-scm.org/D8388
663 11:30682b960804
713 11:30682b960804
664 3: a commit with no detailed message
714 3: a commit with no detailed message
665
715
666 Differential Revision: https://phab.mercurial-scm.org/D8388
716 Differential Revision: https://phab.mercurial-scm.org/D8388
667 10:3ee132d41dbc
717 10:3ee132d41dbc
668 two: second commit to review
718 two: second commit to review
669
719
670 This file was modified with 'mod2' as its contents.
720 This file was modified with 'mod2' as its contents.
671
721
672 Test Plan:
722 Test Plan:
673 Haha! yeah, right.
723 Haha! yeah, right.
674
724
675 Differential Revision: https://phab.mercurial-scm.org/D8388
725 Differential Revision: https://phab.mercurial-scm.org/D8388
676 9:6320b7d714cf
726 9:6320b7d714cf
677 one: first commit to review
727 one: first commit to review
678
728
679 This file was modified with 'mod1' as its contents.
729 This file was modified with 'mod1' as its contents.
680
730
681 Test Plan:
731 Test Plan:
682 LOL! What testing?!
732 LOL! What testing?!
683
733
684 Differential Revision: https://phab.mercurial-scm.org/D8388
734 Differential Revision: https://phab.mercurial-scm.org/D8388
685 8:15e9b14b4b4c
735 8:15e9b14b4b4c
686 added file
736 added file
687
737
688 Differential Revision: https://phab.mercurial-scm.org/D8388
738 Differential Revision: https://phab.mercurial-scm.org/D8388
689
739
690 Test phabsend --fold with an `hg split` at the end of the range
740 Test phabsend --fold with an `hg split` at the end of the range
691
741
692 $ echo foo > file3.txt
742 $ echo foo > file3.txt
693 $ hg add file3.txt
743 $ hg add file3.txt
694
744
695 $ hg log -r . -T '{desc}' > log.txt
745 $ hg log -r . -T '{desc}' > log.txt
696 $ echo 'amended mod' > file2.txt
746 $ echo 'amended mod' > file2.txt
697 $ hg ci --amend -l log.txt --config experimental.evolution=all
747 $ hg ci --amend -l log.txt --config experimental.evolution=all
698
748
699 $ cat <<EOF | hg --config extensions.split= --config ui.interactive=True \
749 $ cat <<EOF | hg --config extensions.split= --config ui.interactive=True \
700 > --config experimental.evolution=all split -r .
750 > --config experimental.evolution=all split -r .
701 > n
751 > n
702 > y
752 > y
703 > y
753 > y
704 > y
754 > y
705 > y
755 > y
706 > EOF
756 > EOF
707 diff --git a/file2.txt b/file2.txt
757 diff --git a/file2.txt b/file2.txt
708 1 hunks, 1 lines changed
758 1 hunks, 1 lines changed
709 examine changes to 'file2.txt'?
759 examine changes to 'file2.txt'?
710 (enter ? for help) [Ynesfdaq?] n
760 (enter ? for help) [Ynesfdaq?] n
711
761
712 diff --git a/file3.txt b/file3.txt
762 diff --git a/file3.txt b/file3.txt
713 new file mode 100644
763 new file mode 100644
714 examine changes to 'file3.txt'?
764 examine changes to 'file3.txt'?
715 (enter ? for help) [Ynesfdaq?] y
765 (enter ? for help) [Ynesfdaq?] y
716
766
717 @@ -0,0 +1,1 @@
767 @@ -0,0 +1,1 @@
718 +foo
768 +foo
719 record change 2/2 to 'file3.txt'?
769 record change 2/2 to 'file3.txt'?
720 (enter ? for help) [Ynesfdaq?] y
770 (enter ? for help) [Ynesfdaq?] y
721
771
722 created new head
772 created new head
723 diff --git a/file2.txt b/file2.txt
773 diff --git a/file2.txt b/file2.txt
724 1 hunks, 1 lines changed
774 1 hunks, 1 lines changed
725 examine changes to 'file2.txt'?
775 examine changes to 'file2.txt'?
726 (enter ? for help) [Ynesfdaq?] y
776 (enter ? for help) [Ynesfdaq?] y
727
777
728 @@ -1,1 +1,1 @@
778 @@ -1,1 +1,1 @@
729 -modified
779 -modified
730 +amended mod
780 +amended mod
731 record this change to 'file2.txt'?
781 record this change to 'file2.txt'?
732 (enter ? for help) [Ynesfdaq?] y
782 (enter ? for help) [Ynesfdaq?] y
733
783
734 $ hg phabsend --fold -r 8:: --test-vcr "$VCR/phabsend-fold-split-end.json" \
784 $ hg phabsend --fold -r 8:: --test-vcr "$VCR/phabsend-fold-split-end.json" \
735 > --config experimental.evolution=all
785 > --config experimental.evolution=all
736 15e9b14b4b4c mapped to old nodes ['15e9b14b4b4c']
786 15e9b14b4b4c mapped to old nodes ['15e9b14b4b4c']
737 6320b7d714cf mapped to old nodes ['6320b7d714cf']
787 6320b7d714cf mapped to old nodes ['6320b7d714cf']
738 3ee132d41dbc mapped to old nodes ['3ee132d41dbc']
788 3ee132d41dbc mapped to old nodes ['3ee132d41dbc']
739 30682b960804 mapped to old nodes ['30682b960804']
789 30682b960804 mapped to old nodes ['30682b960804']
740 6bc15dc99efd mapped to old nodes ['ac7db67f0991']
790 6bc15dc99efd mapped to old nodes ['ac7db67f0991']
741 b50946d5e490 mapped to old nodes ['ac7db67f0991']
791 b50946d5e490 mapped to old nodes ['ac7db67f0991']
742 D8388 - updated - 15e9b14b4b4c: added file
792 D8388 - updated - 15e9b14b4b4c: added file
743 D8388 - updated - 6320b7d714cf: one: first commit to review
793 D8388 - updated - 6320b7d714cf: one: first commit to review
744 D8388 - updated - 3ee132d41dbc: two: second commit to review
794 D8388 - updated - 3ee132d41dbc: two: second commit to review
745 D8388 - updated - 30682b960804: 3: a commit with no detailed message
795 D8388 - updated - 30682b960804: 3: a commit with no detailed message
746 D8388 - updated - 6bc15dc99efd: four: extend the fold range
796 D8388 - updated - 6bc15dc99efd: four: extend the fold range
747 D8388 - updated - b50946d5e490: four: extend the fold range
797 D8388 - updated - b50946d5e490: four: extend the fold range
748 updating local commit list for D8388
798 updating local commit list for D8388
749 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', '6bc15dc99efd', 'b50946d5e490']
799 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', '6bc15dc99efd', 'b50946d5e490']
750
800
751 Test phabsend --fold with an `hg fold` at the end of the range
801 Test phabsend --fold with an `hg fold` at the end of the range
752
802
753 $ hg --config experimental.evolution=all --config extensions.rebase= \
803 $ hg --config experimental.evolution=all --config extensions.rebase= \
754 > rebase -r '.^' -r . -d '.^^' --collapse -l log.txt
804 > rebase -r '.^' -r . -d '.^^' --collapse -l log.txt
755 rebasing 14:6bc15dc99efd "four: extend the fold range"
805 rebasing 14:6bc15dc99efd "four: extend the fold range"
756 rebasing 15:b50946d5e490 "four: extend the fold range" (tip)
806 rebasing 15:b50946d5e490 "four: extend the fold range" (tip)
757
807
758 $ hg phabsend --fold -r 8:: --test-vcr "$VCR/phabsend-fold-fold-end.json" \
808 $ hg phabsend --fold -r 8:: --test-vcr "$VCR/phabsend-fold-fold-end.json" \
759 > --config experimental.evolution=all
809 > --config experimental.evolution=all
760 15e9b14b4b4c mapped to old nodes ['15e9b14b4b4c']
810 15e9b14b4b4c mapped to old nodes ['15e9b14b4b4c']
761 6320b7d714cf mapped to old nodes ['6320b7d714cf']
811 6320b7d714cf mapped to old nodes ['6320b7d714cf']
762 3ee132d41dbc mapped to old nodes ['3ee132d41dbc']
812 3ee132d41dbc mapped to old nodes ['3ee132d41dbc']
763 30682b960804 mapped to old nodes ['30682b960804']
813 30682b960804 mapped to old nodes ['30682b960804']
764 e919cdf3d4fe mapped to old nodes ['6bc15dc99efd', 'b50946d5e490']
814 e919cdf3d4fe mapped to old nodes ['6bc15dc99efd', 'b50946d5e490']
765 D8388 - updated - 15e9b14b4b4c: added file
815 D8388 - updated - 15e9b14b4b4c: added file
766 D8388 - updated - 6320b7d714cf: one: first commit to review
816 D8388 - updated - 6320b7d714cf: one: first commit to review
767 D8388 - updated - 3ee132d41dbc: two: second commit to review
817 D8388 - updated - 3ee132d41dbc: two: second commit to review
768 D8388 - updated - 30682b960804: 3: a commit with no detailed message
818 D8388 - updated - 30682b960804: 3: a commit with no detailed message
769 D8388 - updated - e919cdf3d4fe: four: extend the fold range
819 D8388 - updated - e919cdf3d4fe: four: extend the fold range
770 updating local commit list for D8388
820 updating local commit list for D8388
771 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', 'e919cdf3d4fe']
821 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', 'e919cdf3d4fe']
772
822
773 $ hg log -r tip -v
823 $ hg log -r tip -v
774 obsolete feature not enabled but 12 markers found!
824 obsolete feature not enabled but 12 markers found!
775 changeset: 16:e919cdf3d4fe
825 changeset: 16:e919cdf3d4fe
776 tag: tip
826 tag: tip
777 parent: 11:30682b960804
827 parent: 11:30682b960804
778 user: test
828 user: test
779 date: Thu Jan 01 00:00:00 1970 +0000
829 date: Thu Jan 01 00:00:00 1970 +0000
780 files: file2.txt file3.txt
830 files: file2.txt file3.txt
781 description:
831 description:
782 four: extend the fold range
832 four: extend the fold range
783
833
784 Differential Revision: https://phab.mercurial-scm.org/D8388
834 Differential Revision: https://phab.mercurial-scm.org/D8388
785
835
786
836
787
837
788 $ cd ..
838 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now