##// END OF EJS Templates
phabricator: allow multiple DREVSPEC args to phabread|phabimport|phabupdate...
Matt Harbison -
r45071:a7f8c657 default
parent child Browse files
Show More
@@ -0,0 +1,277 b''
1 {
2 "version": 1,
3 "interactions": [
4 {
5 "response": {
6 "status": {
7 "message": "OK",
8 "code": 200
9 },
10 "body": {
11 "string": "{\"result\":[{\"id\":\"7918\",\"phid\":\"PHID-DREV-sfsckrwrwc77rdl3k5rz\",\"title\":\"create draft change for phabricator testing\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D7918\",\"dateCreated\":\"1579221164\",\"dateModified\":\"1579222305\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"4\",\"statusName\":\"Abandoned\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":1,\"lines.removed\":2},\"branch\":\"default\",\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"3\",\"activeDiffPHID\":\"PHID-DIFF-pqdlhei24n47fzeofjph\",\"diffs\":[\"19394\"],\"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-yhl3yvijs4jploa5iqm4\"]},\"repositoryPHID\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"sourcePath\":\"\\/\"},{\"id\":\"7917\",\"phid\":\"PHID-DREV-yhl3yvijs4jploa5iqm4\",\"title\":\"create public change for phabricator testing\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D7917\",\"dateCreated\":\"1579221160\",\"dateModified\":\"1579222286\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"4\",\"statusName\":\"Abandoned\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":1,\"lines.removed\":1},\"branch\":\"default\",\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"2\",\"activeDiffPHID\":\"PHID-DIFF-e64weyerxtutv2jvj2dt\",\"diffs\":[\"19393\"],\"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}"
12 },
13 "headers": {
14 "expires": [
15 "Sat, 01 Jan 2000 00:00:00 GMT"
16 ],
17 "transfer-encoding": [
18 "chunked"
19 ],
20 "strict-transport-security": [
21 "max-age=0; includeSubdomains; preload"
22 ],
23 "x-xss-protection": [
24 "1; mode=block"
25 ],
26 "date": [
27 "Wed, 04 Mar 2020 22:52:46 GMT"
28 ],
29 "referrer-policy": [
30 "no-referrer"
31 ],
32 "content-type": [
33 "application/json"
34 ],
35 "x-frame-options": [
36 "Deny"
37 ],
38 "x-content-type-options": [
39 "nosniff"
40 ],
41 "cache-control": [
42 "no-store"
43 ],
44 "server": [
45 "Apache/2.4.10 (Debian)"
46 ]
47 }
48 },
49 "request": {
50 "method": "POST",
51 "uri": "https://phab.mercurial-scm.org//api/differential.query",
52 "body": "output=json&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22ids%22%3A+%5B7917%2C+7918%5D%7D&__conduit__=1",
53 "headers": {
54 "user-agent": [
55 "mercurial/proto-1.0 (Mercurial 5.3+33-bb58931d0c4f)"
56 ],
57 "host": [
58 "phab.mercurial-scm.org"
59 ],
60 "content-type": [
61 "application/x-www-form-urlencoded"
62 ],
63 "accept": [
64 "application/mercurial-0.1"
65 ],
66 "content-length": [
67 "154"
68 ]
69 }
70 }
71 },
72 {
73 "response": {
74 "status": {
75 "message": "OK",
76 "code": 200
77 },
78 "body": {
79 "string": "{\"result\":{\"19394\":{\"id\":\"19394\",\"revisionID\":\"7918\",\"dateCreated\":\"1579221161\",\"dateModified\":\"1579221164\",\"sourceControlBaseRevision\":\"7b4185ab5d16acf98e41d566be38c5dbea10878d\",\"sourceControlPath\":\"\\/\",\"sourceControlSystem\":\"hg\",\"branch\":\"default\",\"bookmark\":null,\"creationMethod\":\"phabsend\",\"description\":null,\"unitStatus\":\"0\",\"lintStatus\":\"0\",\"changes\":[{\"id\":\"52927\",\"metadata\":{\"line:first\":1,\"hash.effect\":\"sjHKTvwwqRoW\"},\"oldPath\":\"alpha\",\"currentPath\":\"alpha\",\"awayPaths\":[],\"oldProperties\":[],\"newProperties\":[],\"type\":\"2\",\"fileType\":\"1\",\"commitHash\":null,\"addLines\":\"1\",\"delLines\":\"2\",\"hunks\":[{\"oldOffset\":\"1\",\"newOffset\":\"1\",\"oldLength\":\"2\",\"newLength\":\"1\",\"addLines\":null,\"delLines\":null,\"isMissingOldNewline\":null,\"isMissingNewNewline\":null,\"corpus\":\"-alpha\\n-more\\n+draft change\\n\"}]}],\"properties\":{\"hg:meta\":{\"branch\":\"default\",\"date\":\"0 0\",\"node\":\"3244dc4a33342b4d91ad534ae091685244ac5ed4\",\"parent\":\"7b4185ab5d16acf98e41d566be38c5dbea10878d\",\"user\":\"test\"},\"local:commits\":{\"3244dc4a33342b4d91ad534ae091685244ac5ed4\":{\"author\":\"test\",\"authorEmail\":\"test\",\"branch\":\"default\",\"commit\":\"3244dc4a33342b4d91ad534ae091685244ac5ed4\",\"parents\":[\"7b4185ab5d16acf98e41d566be38c5dbea10878d\"],\"time\":0}}},\"authorName\":\"test\",\"authorEmail\":\"test\"},\"19393\":{\"id\":\"19393\",\"revisionID\":\"7917\",\"dateCreated\":\"1579221158\",\"dateModified\":\"1579221160\",\"sourceControlBaseRevision\":\"a692622e693757674f85ff481c7ff77057a7f82a\",\"sourceControlPath\":\"\\/\",\"sourceControlSystem\":\"hg\",\"branch\":\"default\",\"bookmark\":null,\"creationMethod\":\"phabsend\",\"description\":null,\"unitStatus\":\"0\",\"lintStatus\":\"0\",\"changes\":[{\"id\":\"52926\",\"metadata\":{\"line:first\":1,\"hash.effect\":\"uKa4JPWhh2di\"},\"oldPath\":\"beta\",\"currentPath\":\"beta\",\"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\":\"-beta\\n+public change\\n\"}]}],\"properties\":{\"hg:meta\":{\"branch\":\"default\",\"date\":\"0 0\",\"node\":\"7b4185ab5d16acf98e41d566be38c5dbea10878d\",\"parent\":\"a692622e693757674f85ff481c7ff77057a7f82a\",\"user\":\"test\"},\"local:commits\":{\"7b4185ab5d16acf98e41d566be38c5dbea10878d\":{\"author\":\"test\",\"authorEmail\":\"test\",\"branch\":\"default\",\"commit\":\"7b4185ab5d16acf98e41d566be38c5dbea10878d\",\"parents\":[\"a692622e693757674f85ff481c7ff77057a7f82a\"],\"time\":0}}},\"authorName\":\"test\",\"authorEmail\":\"test\"}},\"error_code\":null,\"error_info\":null}"
80 },
81 "headers": {
82 "expires": [
83 "Sat, 01 Jan 2000 00:00:00 GMT"
84 ],
85 "transfer-encoding": [
86 "chunked"
87 ],
88 "strict-transport-security": [
89 "max-age=0; includeSubdomains; preload"
90 ],
91 "x-xss-protection": [
92 "1; mode=block"
93 ],
94 "date": [
95 "Wed, 04 Mar 2020 22:52:46 GMT"
96 ],
97 "referrer-policy": [
98 "no-referrer"
99 ],
100 "content-type": [
101 "application/json"
102 ],
103 "x-frame-options": [
104 "Deny"
105 ],
106 "x-content-type-options": [
107 "nosniff"
108 ],
109 "cache-control": [
110 "no-store"
111 ],
112 "server": [
113 "Apache/2.4.10 (Debian)"
114 ]
115 }
116 },
117 "request": {
118 "method": "POST",
119 "uri": "https://phab.mercurial-scm.org//api/differential.querydiffs",
120 "body": "output=json&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22ids%22%3A+%5B19393%2C+19394%5D%7D&__conduit__=1",
121 "headers": {
122 "user-agent": [
123 "mercurial/proto-1.0 (Mercurial 5.3+33-bb58931d0c4f)"
124 ],
125 "host": [
126 "phab.mercurial-scm.org"
127 ],
128 "content-type": [
129 "application/x-www-form-urlencoded"
130 ],
131 "accept": [
132 "application/mercurial-0.1"
133 ],
134 "content-length": [
135 "156"
136 ]
137 }
138 }
139 },
140 {
141 "response": {
142 "status": {
143 "message": "OK",
144 "code": 200
145 },
146 "body": {
147 "string": "{\"result\":\"diff --git a\\/beta b\\/beta\\n--- a\\/beta\\n+++ b\\/beta\\n@@ -1 +1 @@\\n-beta\\n+public change\\n\\n\",\"error_code\":null,\"error_info\":null}"
148 },
149 "headers": {
150 "expires": [
151 "Sat, 01 Jan 2000 00:00:00 GMT"
152 ],
153 "transfer-encoding": [
154 "chunked"
155 ],
156 "strict-transport-security": [
157 "max-age=0; includeSubdomains; preload"
158 ],
159 "x-xss-protection": [
160 "1; mode=block"
161 ],
162 "date": [
163 "Wed, 04 Mar 2020 22:52:47 GMT"
164 ],
165 "referrer-policy": [
166 "no-referrer"
167 ],
168 "content-type": [
169 "application/json"
170 ],
171 "x-frame-options": [
172 "Deny"
173 ],
174 "x-content-type-options": [
175 "nosniff"
176 ],
177 "cache-control": [
178 "no-store"
179 ],
180 "server": [
181 "Apache/2.4.10 (Debian)"
182 ]
183 }
184 },
185 "request": {
186 "method": "POST",
187 "uri": "https://phab.mercurial-scm.org//api/differential.getrawdiff",
188 "body": "output=json&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22diffID%22%3A+19393%7D&__conduit__=1",
189 "headers": {
190 "user-agent": [
191 "mercurial/proto-1.0 (Mercurial 5.3+33-bb58931d0c4f)"
192 ],
193 "host": [
194 "phab.mercurial-scm.org"
195 ],
196 "content-type": [
197 "application/x-www-form-urlencoded"
198 ],
199 "accept": [
200 "application/mercurial-0.1"
201 ],
202 "content-length": [
203 "144"
204 ]
205 }
206 }
207 },
208 {
209 "response": {
210 "status": {
211 "message": "OK",
212 "code": 200
213 },
214 "body": {
215 "string": "{\"result\":\"diff --git a\\/alpha b\\/alpha\\n--- a\\/alpha\\n+++ b\\/alpha\\n@@ -1,2 +1 @@\\n-alpha\\n-more\\n+draft change\\n\\n\",\"error_code\":null,\"error_info\":null}"
216 },
217 "headers": {
218 "expires": [
219 "Sat, 01 Jan 2000 00:00:00 GMT"
220 ],
221 "transfer-encoding": [
222 "chunked"
223 ],
224 "strict-transport-security": [
225 "max-age=0; includeSubdomains; preload"
226 ],
227 "x-xss-protection": [
228 "1; mode=block"
229 ],
230 "date": [
231 "Wed, 04 Mar 2020 22:52:47 GMT"
232 ],
233 "referrer-policy": [
234 "no-referrer"
235 ],
236 "content-type": [
237 "application/json"
238 ],
239 "x-frame-options": [
240 "Deny"
241 ],
242 "x-content-type-options": [
243 "nosniff"
244 ],
245 "cache-control": [
246 "no-store"
247 ],
248 "server": [
249 "Apache/2.4.10 (Debian)"
250 ]
251 }
252 },
253 "request": {
254 "method": "POST",
255 "uri": "https://phab.mercurial-scm.org//api/differential.getrawdiff",
256 "body": "output=json&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22diffID%22%3A+19394%7D&__conduit__=1",
257 "headers": {
258 "user-agent": [
259 "mercurial/proto-1.0 (Mercurial 5.3+33-bb58931d0c4f)"
260 ],
261 "host": [
262 "phab.mercurial-scm.org"
263 ],
264 "content-type": [
265 "application/x-www-form-urlencoded"
266 ],
267 "accept": [
268 "application/mercurial-0.1"
269 ],
270 "content-length": [
271 "144"
272 ]
273 }
274 }
275 }
276 ]
277 } No newline at end of file
@@ -0,0 +1,73 b''
1 {
2 "version": 1,
3 "interactions": [
4 {
5 "request": {
6 "uri": "https://phab.mercurial-scm.org//api/differential.query",
7 "method": "POST",
8 "headers": {
9 "accept": [
10 "application/mercurial-0.1"
11 ],
12 "host": [
13 "phab.mercurial-scm.org"
14 ],
15 "content-length": [
16 "146"
17 ],
18 "user-agent": [
19 "mercurial/proto-1.0 (Mercurial 5.3+33-bb58931d0c4f)"
20 ],
21 "content-type": [
22 "application/x-www-form-urlencoded"
23 ]
24 },
25 "body": "__conduit__=1&params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22ids%22%3A+%5B7917%5D%7D&output=json"
26 },
27 "response": {
28 "status": {
29 "code": 200,
30 "message": "OK"
31 },
32 "headers": {
33 "x-content-type-options": [
34 "nosniff"
35 ],
36 "x-frame-options": [
37 "Deny"
38 ],
39 "referrer-policy": [
40 "no-referrer"
41 ],
42 "server": [
43 "Apache/2.4.10 (Debian)"
44 ],
45 "cache-control": [
46 "no-store"
47 ],
48 "content-type": [
49 "application/json"
50 ],
51 "transfer-encoding": [
52 "chunked"
53 ],
54 "expires": [
55 "Sat, 01 Jan 2000 00:00:00 GMT"
56 ],
57 "x-xss-protection": [
58 "1; mode=block"
59 ],
60 "date": [
61 "Thu, 05 Mar 2020 16:19:23 GMT"
62 ],
63 "strict-transport-security": [
64 "max-age=0; includeSubdomains; preload"
65 ]
66 },
67 "body": {
68 "string": "{\"result\":[{\"id\":\"7917\",\"phid\":\"PHID-DREV-yhl3yvijs4jploa5iqm4\",\"title\":\"create public change for phabricator testing\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D7917\",\"dateCreated\":\"1579221160\",\"dateModified\":\"1579222286\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"4\",\"statusName\":\"Abandoned\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":1,\"lines.removed\":1},\"branch\":\"default\",\"summary\":\"\",\"testPlan\":\"\",\"lineCount\":\"2\",\"activeDiffPHID\":\"PHID-DIFF-e64weyerxtutv2jvj2dt\",\"diffs\":[\"19393\"],\"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}"
69 }
70 }
71 }
72 ]
73 } No newline at end of file
@@ -0,0 +1,345 b''
1 {
2 "version": 1,
3 "interactions": [
4 {
5 "response": {
6 "body": {
7 "string": "{\"result\":[{\"id\":\"8207\",\"phid\":\"PHID-DREV-2cgovej5wkjco3xjcqta\",\"title\":\"phabricator: pass ui instead of repo to `userphids()`\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D8207\",\"dateCreated\":\"1583259903\",\"dateModified\":\"1583348836\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"3\",\"statusName\":\"Closed\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":7,\"lines.removed\":4,\"wasAcceptedBeforeClose\":true},\"branch\":null,\"summary\":\"Also not a repository operation.\",\"testPlan\":\"\",\"lineCount\":\"11\",\"activeDiffPHID\":\"PHID-DIFF-wzbsydozxy3nv2k6q4nd\",\"diffs\":[\"20443\",\"20423\"],\"commits\":[\"PHID-CMIT-o75v5xkiwt7t4qsjdhhw\",\"PHID-CMIT-4od7afhqygglq77yjjbr\"],\"reviewers\":{\"PHID-PROJ-3dvcxzznrjru2xmmses3\":\"PHID-PROJ-3dvcxzznrjru2xmmses3\",\"PHID-USER-34jnztnonbr4lhwuybwl\":\"PHID-USER-34jnztnonbr4lhwuybwl\"},\"ccs\":[\"PHID-USER-5iy6mkoveguhm2zthvww\",\"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-gtbyd4t7mjnm4i3erun5\"]},\"repositoryPHID\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"sourcePath\":null},{\"id\":\"8206\",\"phid\":\"PHID-DREV-gtbyd4t7mjnm4i3erun5\",\"title\":\"phabricator: pass ui instead of repo to `querydrev()`\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D8206\",\"dateCreated\":\"1583259900\",\"dateModified\":\"1583348835\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"3\",\"statusName\":\"Closed\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":6,\"lines.removed\":6,\"wasAcceptedBeforeClose\":true},\"branch\":null,\"summary\":\"Also not a repository operation.\",\"testPlan\":\"\",\"lineCount\":\"12\",\"activeDiffPHID\":\"PHID-DIFF-shmhfs2exdg7ituxbt22\",\"diffs\":[\"20442\",\"20422\"],\"commits\":[\"PHID-CMIT-66dzbf7lma7m2ri62tfl\",\"PHID-CMIT-2su6m35fsf32mblyi2ad\"],\"reviewers\":{\"PHID-PROJ-3dvcxzznrjru2xmmses3\":\"PHID-PROJ-3dvcxzznrjru2xmmses3\",\"PHID-USER-34jnztnonbr4lhwuybwl\":\"PHID-USER-34jnztnonbr4lhwuybwl\"},\"ccs\":[\"PHID-USER-5iy6mkoveguhm2zthvww\",\"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-42xnmk3odcdz2lwckiym\"]},\"repositoryPHID\":\"PHID-REPO-bvunnehri4u2isyr7bc3\",\"sourcePath\":null},{\"id\":\"8205\",\"phid\":\"PHID-DREV-42xnmk3odcdz2lwckiym\",\"title\":\"phabricator: pass ui instead of repo to `readpatch()`\",\"uri\":\"https:\\/\\/phab.mercurial-scm.org\\/D8205\",\"dateCreated\":\"1583259897\",\"dateModified\":\"1583348832\",\"authorPHID\":\"PHID-USER-tzhaient733lwrlbcag5\",\"status\":\"3\",\"statusName\":\"Closed\",\"properties\":{\"draft.broadcast\":true,\"lines.added\":5,\"lines.removed\":7,\"wasAcceptedBeforeClose\":true},\"branch\":null,\"summary\":\"This makes it a little clearer that it isn't a repository operation.\",\"testPlan\":\"\",\"lineCount\":\"12\",\"activeDiffPHID\":\"PHID-DIFF-atzhtzu6avavi6uevt3n\",\"diffs\":[\"20441\",\"20421\"],\"commits\":[\"PHID-CMIT-wtocju4a33qnh7jwy7on\",\"PHID-CMIT-e3dyltz277hhalnoum4m\"],\"reviewers\":{\"PHID-PROJ-3dvcxzznrjru2xmmses3\":\"PHID-PROJ-3dvcxzznrjru2xmmses3\",\"PHID-USER-34jnztnonbr4lhwuybwl\":\"PHID-USER-34jnztnonbr4lhwuybwl\"},\"ccs\":[\"PHID-USER-5iy6mkoveguhm2zthvww\",\"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\":null}],\"error_code\":null,\"error_info\":null}"
8 },
9 "headers": {
10 "x-frame-options": [
11 "Deny"
12 ],
13 "strict-transport-security": [
14 "max-age=0; includeSubdomains; preload"
15 ],
16 "transfer-encoding": [
17 "chunked"
18 ],
19 "cache-control": [
20 "no-store"
21 ],
22 "referrer-policy": [
23 "no-referrer"
24 ],
25 "x-content-type-options": [
26 "nosniff"
27 ],
28 "x-xss-protection": [
29 "1; mode=block"
30 ],
31 "server": [
32 "Apache/2.4.10 (Debian)"
33 ],
34 "date": [
35 "Wed, 04 Mar 2020 22:05:21 GMT"
36 ],
37 "content-type": [
38 "application/json"
39 ],
40 "expires": [
41 "Sat, 01 Jan 2000 00:00:00 GMT"
42 ]
43 },
44 "status": {
45 "message": "OK",
46 "code": 200
47 }
48 },
49 "request": {
50 "body": "params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22ids%22%3A+%5B8205%2C+8206%2C+8207%5D%7D&output=json&__conduit__=1",
51 "uri": "https://phab.mercurial-scm.org//api/differential.query",
52 "headers": {
53 "content-length": [
54 "162"
55 ],
56 "accept": [
57 "application/mercurial-0.1"
58 ],
59 "host": [
60 "phab.mercurial-scm.org"
61 ],
62 "user-agent": [
63 "mercurial/proto-1.0 (Mercurial 5.3+33-bb58931d0c4f)"
64 ],
65 "content-type": [
66 "application/x-www-form-urlencoded"
67 ]
68 },
69 "method": "POST"
70 }
71 },
72 {
73 "response": {
74 "body": {
75 "string": "{\"result\":{\"20443\":{\"id\":\"20443\",\"revisionID\":\"8207\",\"dateCreated\":\"1581964120\",\"dateModified\":\"1583327828\",\"sourceControlBaseRevision\":\"9b46270917348950e3fb1e73a5c9e46038065622\",\"sourceControlPath\":null,\"sourceControlSystem\":\"hg\",\"branch\":null,\"bookmark\":null,\"creationMethod\":\"commit\",\"description\":\"rHGa271ef1de08664a9ee4a286711681377875ca2a2\",\"unitStatus\":\"6\",\"lintStatus\":\"6\",\"changes\":[{\"id\":\"55598\",\"metadata\":{\"line:first\":1043,\"hash.effect\":\".HmDk8vnow9e\"},\"oldPath\":\"hgext\\/phabricator.py\",\"currentPath\":\"hgext\\/phabricator.py\",\"awayPaths\":[],\"oldProperties\":[],\"newProperties\":[],\"type\":\"2\",\"fileType\":\"1\",\"commitHash\":null,\"addLines\":\"7\",\"delLines\":\"4\",\"hunks\":[{\"oldOffset\":\"1\",\"newOffset\":\"1\",\"oldLength\":\"1799\",\"newLength\":\"1802\",\"addLines\":null,\"delLines\":null,\"isMissingOldNewline\":null,\"isMissingNewNewline\":null,\"corpus\":\" # phabricator.py - simple Phabricator integration\\n #\\n # Copyright 2017 Facebook, Inc.\\n #\\n # This software may be used and distributed according to the terms of the\\n # GNU General Public License version 2 or any later version.\\n \\\"\\\"\\\"simple Phabricator integration (EXPERIMENTAL)\\n \\n This extension provides a ``phabsend`` command which sends a stack of\\n changesets to Phabricator, and a ``phabread`` command which prints a stack of\\n revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command\\n to update statuses in batch.\\n \\n A \\\"phabstatus\\\" view for :hg:`show` is also provided; it displays status\\n information of Phabricator differentials associated with unfinished\\n changesets.\\n \\n By default, Phabricator requires ``Test Plan`` which might prevent some\\n changeset from being sent. The requirement could be disabled by changing\\n ``differential.require-test-plan-field`` config server side.\\n \\n Config::\\n \\n [phabricator]\\n # Phabricator URL\\n url = https:\\/\\/phab.example.com\\/\\n \\n # Repo callsign. If a repo has a URL https:\\/\\/$HOST\\/diffusion\\/FOO, then its\\n # callsign is \\\"FOO\\\".\\n callsign = FOO\\n \\n # curl command to use. If not set (default), use builtin HTTP library to\\n # communicate. If set, use the specified curl command. This could be useful\\n # if you need to specify advanced options that is not easily supported by\\n # the internal library.\\n curlcmd = curl --connect-timeout 2 --retry 3 --silent\\n \\n [auth]\\n example.schemes = https\\n example.prefix = phab.example.com\\n \\n # API token. Get it from https:\\/\\/$HOST\\/conduit\\/login\\/\\n example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx\\n \\\"\\\"\\\"\\n \\n from __future__ import absolute_import\\n \\n import base64\\n import contextlib\\n import hashlib\\n import itertools\\n import json\\n import mimetypes\\n import operator\\n import re\\n \\n from mercurial.node import bin, nullid\\n from mercurial.i18n import _\\n from mercurial.pycompat import getattr\\n from mercurial.thirdparty import attr\\n from mercurial import (\\n cmdutil,\\n context,\\n encoding,\\n error,\\n exthelper,\\n graphmod,\\n httpconnection as httpconnectionmod,\\n localrepo,\\n logcmdutil,\\n match,\\n mdiff,\\n obsutil,\\n parser,\\n patch,\\n phases,\\n pycompat,\\n scmutil,\\n smartset,\\n tags,\\n templatefilters,\\n templateutil,\\n url as urlmod,\\n util,\\n )\\n from mercurial.utils import (\\n procutil,\\n stringutil,\\n )\\n from . import show\\n \\n \\n # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for\\n # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should\\n # be specifying the version(s) of Mercurial they are tested with, or\\n # leave the attribute unspecified.\\n testedwith = b'ships-with-hg-core'\\n \\n eh = exthelper.exthelper()\\n \\n cmdtable = eh.cmdtable\\n command = eh.command\\n configtable = eh.configtable\\n templatekeyword = eh.templatekeyword\\n uisetup = eh.finaluisetup\\n \\n # developer config: phabricator.batchsize\\n eh.configitem(\\n b'phabricator', b'batchsize', default=12,\\n )\\n eh.configitem(\\n b'phabricator', b'callsign', default=None,\\n )\\n eh.configitem(\\n b'phabricator', b'curlcmd', default=None,\\n )\\n # developer config: phabricator.repophid\\n eh.configitem(\\n b'phabricator', b'repophid', default=None,\\n )\\n eh.configitem(\\n b'phabricator', b'url', default=None,\\n )\\n eh.configitem(\\n b'phabsend', b'confirm', default=False,\\n )\\n \\n colortable = {\\n b'phabricator.action.created': b'green',\\n b'phabricator.action.skipped': b'magenta',\\n b'phabricator.action.updated': b'magenta',\\n b'phabricator.desc': b'',\\n b'phabricator.drev': b'bold',\\n b'phabricator.node': b'',\\n b'phabricator.status.abandoned': b'magenta dim',\\n b'phabricator.status.accepted': b'green bold',\\n b'phabricator.status.closed': b'green',\\n b'phabricator.status.needsreview': b'yellow',\\n b'phabricator.status.needsrevision': b'red',\\n b'phabricator.status.changesplanned': b'red',\\n }\\n \\n _VCR_FLAGS = [\\n (\\n b'',\\n b'test-vcr',\\n b'',\\n _(\\n b'Path to a vcr file. If nonexistent, will record a new vcr transcript'\\n b', otherwise will mock all http requests using the specified vcr file.'\\n b' (ADVANCED)'\\n ),\\n ),\\n ]\\n \\n \\n @eh.wrapfunction(localrepo, \\\"loadhgrc\\\")\\n def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):\\n \\\"\\\"\\\"Load ``.arcconfig`` content into a ui instance on repository open.\\n \\\"\\\"\\\"\\n result = False\\n arcconfig = {}\\n \\n try:\\n # json.loads only accepts bytes from 3.6+\\n rawparams = encoding.unifromlocal(wdirvfs.read(b\\\".arcconfig\\\"))\\n # json.loads only returns unicode strings\\n arcconfig = pycompat.rapply(\\n lambda x: encoding.unitolocal(x)\\n if isinstance(x, pycompat.unicode)\\n else x,\\n pycompat.json_loads(rawparams),\\n )\\n \\n result = True\\n except ValueError:\\n ui.warn(_(b\\\"invalid JSON in %s\\\\n\\\") % wdirvfs.join(b\\\".arcconfig\\\"))\\n except IOError:\\n pass\\n \\n cfg = util.sortdict()\\n \\n if b\\\"repository.callsign\\\" in arcconfig:\\n cfg[(b\\\"phabricator\\\", b\\\"callsign\\\")] = arcconfig[b\\\"repository.callsign\\\"]\\n \\n if b\\\"phabricator.uri\\\" in arcconfig:\\n cfg[(b\\\"phabricator\\\", b\\\"url\\\")] = arcconfig[b\\\"phabricator.uri\\\"]\\n \\n if cfg:\\n ui.applyconfig(cfg, source=wdirvfs.join(b\\\".arcconfig\\\"))\\n \\n return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg\\/hgrc\\n \\n \\n def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):\\n fullflags = flags + _VCR_FLAGS\\n \\n def hgmatcher(r1, r2):\\n if r1.uri != r2.uri or r1.method != r2.method:\\n return False\\n r1params = util.urlreq.parseqs(r1.body)\\n r2params = util.urlreq.parseqs(r2.body)\\n for key in r1params:\\n if key not in r2params:\\n return False\\n value = r1params[key][0]\\n # we want to compare json payloads without worrying about ordering\\n if value.startswith(b'{') and value.endswith(b'}'):\\n r1json = pycompat.json_loads(value)\\n r2json = pycompat.json_loads(r2params[key][0])\\n if r1json != r2json:\\n return False\\n elif r2params[key][0] != value:\\n return False\\n return True\\n \\n def sanitiserequest(request):\\n request.body = re.sub(\\n br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body\\n )\\n return request\\n \\n def sanitiseresponse(response):\\n if 'set-cookie' in response['headers']:\\n del response['headers']['set-cookie']\\n return response\\n \\n def decorate(fn):\\n def inner(*args, **kwargs):\\n cassette = pycompat.fsdecode(kwargs.pop('test_vcr', None))\\n if cassette:\\n import hgdemandimport\\n \\n with hgdemandimport.deactivated():\\n import vcr as vcrmod\\n import vcr.stubs as stubs\\n \\n vcr = vcrmod.VCR(\\n serializer='json',\\n before_record_request=sanitiserequest,\\n before_record_response=sanitiseresponse,\\n custom_patches=[\\n (\\n urlmod,\\n 'httpconnection',\\n stubs.VCRHTTPConnection,\\n ),\\n (\\n urlmod,\\n 'httpsconnection',\\n stubs.VCRHTTPSConnection,\\n ),\\n ],\\n )\\n vcr.register_matcher('hgmatcher', hgmatcher)\\n with vcr.use_cassette(cassette, match_on=['hgmatcher']):\\n return fn(*args, **kwargs)\\n return fn(*args, **kwargs)\\n \\n inner.__name__ = fn.__name__\\n inner.__doc__ = fn.__doc__\\n return command(\\n name,\\n fullflags,\\n spec,\\n helpcategory=helpcategory,\\n optionalrepo=optionalrepo,\\n )(inner)\\n \\n return decorate\\n \\n \\n def urlencodenested(params):\\n \\\"\\\"\\\"like urlencode, but works with nested parameters.\\n \\n For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be\\n flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to\\n urlencode. Note: the encoding is consistent with PHP's http_build_query.\\n \\\"\\\"\\\"\\n flatparams = util.sortdict()\\n \\n def process(prefix, obj):\\n if isinstance(obj, bool):\\n obj = {True: b'true', False: b'false'}[obj] # Python -\\u003e PHP form\\n lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]\\n items = {list: lister, dict: lambda x: x.items()}.get(type(obj))\\n if items is None:\\n flatparams[prefix] = obj\\n else:\\n for k, v in items(obj):\\n if prefix:\\n process(b'%s[%s]' % (prefix, k), v)\\n else:\\n process(k, v)\\n \\n process(b'', params)\\n return util.urlreq.urlencode(flatparams)\\n \\n \\n def readurltoken(ui):\\n \\\"\\\"\\\"return conduit url, token and make sure they exist\\n \\n Currently read from [auth] config section. In the future, it might\\n make sense to read from .arcconfig and .arcrc as well.\\n \\\"\\\"\\\"\\n url = ui.config(b'phabricator', b'url')\\n if not url:\\n raise error.Abort(\\n _(b'config %s.%s is required') % (b'phabricator', b'url')\\n )\\n \\n res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)\\n token = None\\n \\n if res:\\n group, auth = res\\n \\n ui.debug(b\\\"using auth.%s.* for authentication\\\\n\\\" % group)\\n \\n token = auth.get(b'phabtoken')\\n \\n if not token:\\n raise error.Abort(\\n _(b'Can\\\\'t find conduit token associated to %s') % (url,)\\n )\\n \\n return url, token\\n \\n \\n def callconduit(ui, name, params):\\n \\\"\\\"\\\"call Conduit API, params is a dict. return json.loads result, or None\\\"\\\"\\\"\\n host, token = readurltoken(ui)\\n url, authinfo = util.url(b'\\/'.join([host, b'api', name])).authinfo()\\n ui.debug(b'Conduit Call: %s %s\\\\n' % (url, pycompat.byterepr(params)))\\n params = params.copy()\\n params[b'__conduit__'] = {\\n b'token': token,\\n }\\n rawdata = {\\n b'params': templatefilters.json(params),\\n b'output': b'json',\\n b'__conduit__': 1,\\n }\\n data = urlencodenested(rawdata)\\n curlcmd = ui.config(b'phabricator', b'curlcmd')\\n if curlcmd:\\n sin, sout = procutil.popen2(\\n b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))\\n )\\n sin.write(data)\\n sin.close()\\n body = sout.read()\\n else:\\n urlopener = urlmod.opener(ui, authinfo)\\n request = util.urlreq.request(pycompat.strurl(url), data=data)\\n with contextlib.closing(urlopener.open(request)) as rsp:\\n body = rsp.read()\\n ui.debug(b'Conduit Response: %s\\\\n' % body)\\n parsed = pycompat.rapply(\\n lambda x: encoding.unitolocal(x)\\n if isinstance(x, pycompat.unicode)\\n else x,\\n # json.loads only accepts bytes from py3.6+\\n pycompat.json_loads(encoding.unifromlocal(body)),\\n )\\n if parsed.get(b'error_code'):\\n msg = _(b'Conduit Error (%s): %s') % (\\n parsed[b'error_code'],\\n parsed[b'error_info'],\\n )\\n raise error.Abort(msg)\\n return parsed[b'result']\\n \\n \\n @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)\\n def debugcallconduit(ui, repo, name):\\n \\\"\\\"\\\"call Conduit API\\n \\n Call parameters are read from stdin as a JSON blob. Result will be written\\n to stdout as a JSON blob.\\n \\\"\\\"\\\"\\n # json.loads only accepts bytes from 3.6+\\n rawparams = encoding.unifromlocal(ui.fin.read())\\n # json.loads only returns unicode strings\\n params = pycompat.rapply(\\n lambda x: encoding.unitolocal(x)\\n if isinstance(x, pycompat.unicode)\\n else x,\\n pycompat.json_loads(rawparams),\\n )\\n # json.dumps only accepts unicode strings\\n result = pycompat.rapply(\\n lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,\\n callconduit(ui, name, params),\\n )\\n s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))\\n ui.write(b'%s\\\\n' % encoding.unitolocal(s))\\n \\n \\n def getrepophid(repo):\\n \\\"\\\"\\\"given callsign, return repository PHID or None\\\"\\\"\\\"\\n # developer config: phabricator.repophid\\n repophid = repo.ui.config(b'phabricator', b'repophid')\\n if repophid:\\n return repophid\\n callsign = repo.ui.config(b'phabricator', b'callsign')\\n if not callsign:\\n return None\\n query = callconduit(\\n repo.ui,\\n b'diffusion.repository.search',\\n {b'constraints': {b'callsigns': [callsign]}},\\n )\\n if len(query[b'data']) == 0:\\n return None\\n repophid = query[b'data'][0][b'phid']\\n repo.ui.setconfig(b'phabricator', b'repophid', repophid)\\n return repophid\\n \\n \\n _differentialrevisiontagre = re.compile(br'\\\\AD([1-9][0-9]*)\\\\Z')\\n _differentialrevisiondescre = re.compile(\\n br'^Differential Revision:\\\\s*(?P\\u003curl\\u003e(?:.*)D(?P\\u003cid\\u003e[1-9][0-9]*))$', re.M\\n )\\n \\n \\n def getoldnodedrevmap(repo, nodelist):\\n \\\"\\\"\\\"find previous nodes that has been sent to Phabricator\\n \\n return {node: (oldnode, Differential diff, Differential Revision ID)}\\n for node in nodelist with known previous sent versions, or associated\\n Differential Revision IDs. ``oldnode`` and ``Differential diff`` could\\n be ``None``.\\n \\n Examines commit messages like \\\"Differential Revision:\\\" to get the\\n association information.\\n \\n If such commit message line is not found, examines all precursors and their\\n tags. Tags with format like \\\"D1234\\\" are considered a match and the node\\n with that tag, and the number after \\\"D\\\" (ex. 1234) will be returned.\\n \\n The ``old node``, if not None, is guaranteed to be the last diff of\\n corresponding Differential Revision, and exist in the repo.\\n \\\"\\\"\\\"\\n unfi = repo.unfiltered()\\n has_node = unfi.changelog.index.has_node\\n \\n result = {} # {node: (oldnode?, lastdiff?, drev)}\\n toconfirm = {} # {node: (force, {precnode}, drev)}\\n for node in nodelist:\\n ctx = unfi[node]\\n # For tags like \\\"D123\\\", put them into \\\"toconfirm\\\" to verify later\\n precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))\\n for n in precnodes:\\n if has_node(n):\\n for tag in unfi.nodetags(n):\\n m = _differentialrevisiontagre.match(tag)\\n if m:\\n toconfirm[node] = (0, set(precnodes), int(m.group(1)))\\n break\\n else:\\n continue # move to next predecessor\\n break # found a tag, stop\\n else:\\n # Check commit message\\n m = _differentialrevisiondescre.search(ctx.description())\\n if m:\\n toconfirm[node] = (1, set(precnodes), int(m.group('id')))\\n \\n # Double check if tags are genuine by collecting all old nodes from\\n # Phabricator, and expect precursors overlap with it.\\n if toconfirm:\\n drevs = [drev for force, precs, drev in toconfirm.values()]\\n alldiffs = callconduit(\\n unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}\\n )\\n getnode = lambda d: bin(getdiffmeta(d).get(b'node', b'')) or None\\n for newnode, (force, precset, drev) in toconfirm.items():\\n diffs = [\\n d for d in alldiffs.values() if int(d[b'revisionID']) == drev\\n ]\\n \\n # \\\"precursors\\\" as known by Phabricator\\n phprecset = set(getnode(d) for d in diffs)\\n \\n # Ignore if precursors (Phabricator and local repo) do not overlap,\\n # and force is not set (when commit message says nothing)\\n if not force and not bool(phprecset & precset):\\n tagname = b'D%d' % drev\\n tags.tag(\\n repo,\\n tagname,\\n nullid,\\n message=None,\\n user=None,\\n date=None,\\n local=True,\\n )\\n unfi.ui.warn(\\n _(\\n b'D%d: local tag removed - does not match '\\n b'Differential history\\\\n'\\n )\\n % drev\\n )\\n continue\\n \\n # Find the last node using Phabricator metadata, and make sure it\\n # exists in the repo\\n oldnode = lastdiff = None\\n if diffs:\\n lastdiff = max(diffs, key=lambda d: int(d[b'id']))\\n oldnode = getnode(lastdiff)\\n if oldnode and not has_node(oldnode):\\n oldnode = None\\n \\n result[newnode] = (oldnode, lastdiff, drev)\\n \\n return result\\n \\n \\n def getdrevmap(repo, revs):\\n \\\"\\\"\\\"Return a dict mapping each rev in `revs` to their Differential Revision\\n ID or None.\\n \\\"\\\"\\\"\\n result = {}\\n for rev in revs:\\n result[rev] = None\\n ctx = repo[rev]\\n # Check commit message\\n m = _differentialrevisiondescre.search(ctx.description())\\n if m:\\n result[rev] = int(m.group('id'))\\n continue\\n # Check tags\\n for tag in repo.nodetags(ctx.node()):\\n m = _differentialrevisiontagre.match(tag)\\n if m:\\n result[rev] = int(m.group(1))\\n break\\n \\n return result\\n \\n \\n def getdiff(ctx, diffopts):\\n \\\"\\\"\\\"plain-text diff without header (user, commit message, etc)\\\"\\\"\\\"\\n output = util.stringio()\\n for chunk, _label in patch.diffui(\\n ctx.repo(), ctx.p1().node(), ctx.node(), None, opts=diffopts\\n ):\\n output.write(chunk)\\n return output.getvalue()\\n \\n \\n class DiffChangeType(object):\\n ADD = 1\\n CHANGE = 2\\n DELETE = 3\\n MOVE_AWAY = 4\\n COPY_AWAY = 5\\n MOVE_HERE = 6\\n COPY_HERE = 7\\n MULTICOPY = 8\\n \\n \\n class DiffFileType(object):\\n TEXT = 1\\n IMAGE = 2\\n BINARY = 3\\n \\n \\n @attr.s\\n class phabhunk(dict):\\n \\\"\\\"\\\"Represents a Differential hunk, which is owned by a Differential change\\n \\\"\\\"\\\"\\n \\n oldOffset = attr.ib(default=0) # camelcase-required\\n oldLength = attr.ib(default=0) # camelcase-required\\n newOffset = attr.ib(default=0) # camelcase-required\\n newLength = attr.ib(default=0) # camelcase-required\\n corpus = attr.ib(default='')\\n # These get added to the phabchange's equivalents\\n addLines = attr.ib(default=0) # camelcase-required\\n delLines = attr.ib(default=0) # camelcase-required\\n \\n \\n @attr.s\\n class phabchange(object):\\n \\\"\\\"\\\"Represents a Differential change, owns Differential hunks and owned by a\\n Differential diff. Each one represents one file in a diff.\\n \\\"\\\"\\\"\\n \\n currentPath = attr.ib(default=None) # camelcase-required\\n oldPath = attr.ib(default=None) # camelcase-required\\n awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required\\n metadata = attr.ib(default=attr.Factory(dict))\\n oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required\\n newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required\\n type = attr.ib(default=DiffChangeType.CHANGE)\\n fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required\\n commitHash = attr.ib(default=None) # camelcase-required\\n addLines = attr.ib(default=0) # camelcase-required\\n delLines = attr.ib(default=0) # camelcase-required\\n hunks = attr.ib(default=attr.Factory(list))\\n \\n def copynewmetadatatoold(self):\\n for key in list(self.metadata.keys()):\\n newkey = key.replace(b'new:', b'old:')\\n self.metadata[newkey] = self.metadata[key]\\n \\n def addoldmode(self, value):\\n self.oldProperties[b'unix:filemode'] = value\\n \\n def addnewmode(self, value):\\n self.newProperties[b'unix:filemode'] = value\\n \\n def addhunk(self, hunk):\\n if not isinstance(hunk, phabhunk):\\n raise error.Abort(b'phabchange.addhunk only takes phabhunks')\\n self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))\\n # It's useful to include these stats since the Phab web UI shows them,\\n # and uses them to estimate how large a change a Revision is. Also used\\n # in email subjects for the [+++--] bit.\\n self.addLines += hunk.addLines\\n self.delLines += hunk.delLines\\n \\n \\n @attr.s\\n class phabdiff(object):\\n \\\"\\\"\\\"Represents a Differential diff, owns Differential changes. Corresponds\\n to a commit.\\n \\\"\\\"\\\"\\n \\n # Doesn't seem to be any reason to send this (output of uname -n)\\n sourceMachine = attr.ib(default=b'') # camelcase-required\\n sourcePath = attr.ib(default=b'\\/') # camelcase-required\\n sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required\\n sourceControlPath = attr.ib(default=b'\\/') # camelcase-required\\n sourceControlSystem = attr.ib(default=b'hg') # camelcase-required\\n branch = attr.ib(default=b'default')\\n bookmark = attr.ib(default=None)\\n creationMethod = attr.ib(default=b'phabsend') # camelcase-required\\n lintStatus = attr.ib(default=b'none') # camelcase-required\\n unitStatus = attr.ib(default=b'none') # camelcase-required\\n changes = attr.ib(default=attr.Factory(dict))\\n repositoryPHID = attr.ib(default=None) # camelcase-required\\n \\n def addchange(self, change):\\n if not isinstance(change, phabchange):\\n raise error.Abort(b'phabdiff.addchange only takes phabchanges')\\n self.changes[change.currentPath] = pycompat.byteskwargs(\\n attr.asdict(change)\\n )\\n \\n \\n def maketext(pchange, ctx, fname):\\n \\\"\\\"\\\"populate the phabchange for a text file\\\"\\\"\\\"\\n repo = ctx.repo()\\n fmatcher = match.exact([fname])\\n diffopts = mdiff.diffopts(git=True, context=32767)\\n _pfctx, _fctx, header, fhunks = next(\\n patch.diffhunks(repo, ctx.p1(), ctx, fmatcher, opts=diffopts)\\n )\\n \\n for fhunk in fhunks:\\n (oldOffset, oldLength, newOffset, newLength), lines = fhunk\\n corpus = b''.join(lines[1:])\\n shunk = list(header)\\n shunk.extend(lines)\\n _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(\\n patch.diffstatdata(util.iterlines(shunk))\\n )\\n pchange.addhunk(\\n phabhunk(\\n oldOffset,\\n oldLength,\\n newOffset,\\n newLength,\\n corpus,\\n addLines,\\n delLines,\\n )\\n )\\n \\n \\n def uploadchunks(fctx, fphid):\\n \\\"\\\"\\\"upload large binary files as separate chunks.\\n Phab requests chunking over 8MiB, and splits into 4MiB chunks\\n \\\"\\\"\\\"\\n ui = fctx.repo().ui\\n chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})\\n with ui.makeprogress(\\n _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)\\n ) as progress:\\n for chunk in chunks:\\n progress.increment()\\n if chunk[b'complete']:\\n continue\\n bstart = int(chunk[b'byteStart'])\\n bend = int(chunk[b'byteEnd'])\\n callconduit(\\n ui,\\n b'file.uploadchunk',\\n {\\n b'filePHID': fphid,\\n b'byteStart': bstart,\\n b'data': base64.b64encode(fctx.data()[bstart:bend]),\\n b'dataEncoding': b'base64',\\n },\\n )\\n \\n \\n def uploadfile(fctx):\\n \\\"\\\"\\\"upload binary files to Phabricator\\\"\\\"\\\"\\n repo = fctx.repo()\\n ui = repo.ui\\n fname = fctx.path()\\n size = fctx.size()\\n fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())\\n \\n # an allocate call is required first to see if an upload is even required\\n # (Phab might already have it) and to determine if chunking is needed\\n allocateparams = {\\n b'name': fname,\\n b'contentLength': size,\\n b'contentHash': fhash,\\n }\\n filealloc = callconduit(ui, b'file.allocate', allocateparams)\\n fphid = filealloc[b'filePHID']\\n \\n if filealloc[b'upload']:\\n ui.write(_(b'uploading %s\\\\n') % bytes(fctx))\\n if not fphid:\\n uploadparams = {\\n b'name': fname,\\n b'data_base64': base64.b64encode(fctx.data()),\\n }\\n fphid = callconduit(ui, b'file.upload', uploadparams)\\n else:\\n uploadchunks(fctx, fphid)\\n else:\\n ui.debug(b'server already has %s\\\\n' % bytes(fctx))\\n \\n if not fphid:\\n raise error.Abort(b'Upload of %s failed.' % bytes(fctx))\\n \\n return fphid\\n \\n \\n def addoldbinary(pchange, fctx):\\n \\\"\\\"\\\"add the metadata for the previous version of a binary file to the\\n phabchange for the new version\\n \\\"\\\"\\\"\\n oldfctx = fctx.p1()\\n if fctx.cmp(oldfctx):\\n # Files differ, add the old one\\n pchange.metadata[b'old:file:size'] = oldfctx.size()\\n mimeguess, _enc = mimetypes.guess_type(\\n encoding.unifromlocal(oldfctx.path())\\n )\\n if mimeguess:\\n pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(\\n mimeguess\\n )\\n fphid = uploadfile(oldfctx)\\n pchange.metadata[b'old:binary-phid'] = fphid\\n else:\\n # If it's left as IMAGE\\/BINARY web UI might try to display it\\n pchange.fileType = DiffFileType.TEXT\\n pchange.copynewmetadatatoold()\\n \\n \\n def makebinary(pchange, fctx):\\n \\\"\\\"\\\"populate the phabchange for a binary file\\\"\\\"\\\"\\n pchange.fileType = DiffFileType.BINARY\\n fphid = uploadfile(fctx)\\n pchange.metadata[b'new:binary-phid'] = fphid\\n pchange.metadata[b'new:file:size'] = fctx.size()\\n mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))\\n if mimeguess:\\n mimeguess = pycompat.bytestr(mimeguess)\\n pchange.metadata[b'new:file:mime-type'] = mimeguess\\n if mimeguess.startswith(b'image\\/'):\\n pchange.fileType = DiffFileType.IMAGE\\n \\n \\n # Copied from mercurial\\/patch.py\\n gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}\\n \\n \\n def notutf8(fctx):\\n \\\"\\\"\\\"detect non-UTF-8 text files since Phabricator requires them to be marked\\n as binary\\n \\\"\\\"\\\"\\n try:\\n fctx.data().decode('utf-8')\\n if fctx.parents():\\n fctx.p1().data().decode('utf-8')\\n return False\\n except UnicodeDecodeError:\\n fctx.repo().ui.write(\\n _(b'file %s detected as non-UTF-8, marked as binary\\\\n')\\n % fctx.path()\\n )\\n return True\\n \\n \\n def addremoved(pdiff, ctx, removed):\\n \\\"\\\"\\\"add removed files to the phabdiff. Shouldn't include moves\\\"\\\"\\\"\\n for fname in removed:\\n pchange = phabchange(\\n currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE\\n )\\n pchange.addoldmode(gitmode[ctx.p1()[fname].flags()])\\n fctx = ctx.p1()[fname]\\n if not (fctx.isbinary() or notutf8(fctx)):\\n maketext(pchange, ctx, fname)\\n \\n pdiff.addchange(pchange)\\n \\n \\n def addmodified(pdiff, ctx, modified):\\n \\\"\\\"\\\"add modified files to the phabdiff\\\"\\\"\\\"\\n for fname in modified:\\n fctx = ctx[fname]\\n pchange = phabchange(currentPath=fname, oldPath=fname)\\n filemode = gitmode[ctx[fname].flags()]\\n originalmode = gitmode[ctx.p1()[fname].flags()]\\n if filemode != originalmode:\\n pchange.addoldmode(originalmode)\\n pchange.addnewmode(filemode)\\n \\n if fctx.isbinary() or notutf8(fctx):\\n makebinary(pchange, fctx)\\n addoldbinary(pchange, fctx)\\n else:\\n maketext(pchange, ctx, fname)\\n \\n pdiff.addchange(pchange)\\n \\n \\n def addadded(pdiff, ctx, added, removed):\\n \\\"\\\"\\\"add file adds to the phabdiff, both new files and copies\\/moves\\\"\\\"\\\"\\n # Keep track of files that've been recorded as moved\\/copied, so if there are\\n # additional copies we can mark them (moves get removed from removed)\\n copiedchanges = {}\\n movedchanges = {}\\n for fname in added:\\n fctx = ctx[fname]\\n pchange = phabchange(currentPath=fname)\\n \\n filemode = gitmode[ctx[fname].flags()]\\n renamed = fctx.renamed()\\n \\n if renamed:\\n originalfname = renamed[0]\\n originalmode = gitmode[ctx.p1()[originalfname].flags()]\\n pchange.oldPath = originalfname\\n \\n if originalfname in removed:\\n origpchange = phabchange(\\n currentPath=originalfname,\\n oldPath=originalfname,\\n type=DiffChangeType.MOVE_AWAY,\\n awayPaths=[fname],\\n )\\n movedchanges[originalfname] = origpchange\\n removed.remove(originalfname)\\n pchange.type = DiffChangeType.MOVE_HERE\\n elif originalfname in movedchanges:\\n movedchanges[originalfname].type = DiffChangeType.MULTICOPY\\n movedchanges[originalfname].awayPaths.append(fname)\\n pchange.type = DiffChangeType.COPY_HERE\\n else: # pure copy\\n if originalfname not in copiedchanges:\\n origpchange = phabchange(\\n currentPath=originalfname, type=DiffChangeType.COPY_AWAY\\n )\\n copiedchanges[originalfname] = origpchange\\n else:\\n origpchange = copiedchanges[originalfname]\\n origpchange.awayPaths.append(fname)\\n pchange.type = DiffChangeType.COPY_HERE\\n \\n if filemode != originalmode:\\n pchange.addoldmode(originalmode)\\n pchange.addnewmode(filemode)\\n else: # Brand-new file\\n pchange.addnewmode(gitmode[fctx.flags()])\\n pchange.type = DiffChangeType.ADD\\n \\n if fctx.isbinary() or notutf8(fctx):\\n makebinary(pchange, fctx)\\n if renamed:\\n addoldbinary(pchange, fctx)\\n else:\\n maketext(pchange, ctx, fname)\\n \\n pdiff.addchange(pchange)\\n \\n for _path, copiedchange in copiedchanges.items():\\n pdiff.addchange(copiedchange)\\n for _path, movedchange in movedchanges.items():\\n pdiff.addchange(movedchange)\\n \\n \\n def creatediff(ctx):\\n \\\"\\\"\\\"create a Differential Diff\\\"\\\"\\\"\\n repo = ctx.repo()\\n repophid = getrepophid(repo)\\n # Create a \\\"Differential Diff\\\" via \\\"differential.creatediff\\\" API\\n pdiff = phabdiff(\\n sourceControlBaseRevision=b'%s' % ctx.p1().hex(),\\n branch=b'%s' % ctx.branch(),\\n )\\n modified, added, removed, _d, _u, _i, _c = ctx.p1().status(ctx)\\n # addadded will remove moved files from removed, so addremoved won't get\\n # them\\n addadded(pdiff, ctx, added, removed)\\n addmodified(pdiff, ctx, modified)\\n addremoved(pdiff, ctx, removed)\\n if repophid:\\n pdiff.repositoryPHID = repophid\\n diff = callconduit(\\n repo.ui,\\n b'differential.creatediff',\\n pycompat.byteskwargs(attr.asdict(pdiff)),\\n )\\n if not diff:\\n raise error.Abort(_(b'cannot create diff for %s') % ctx)\\n return diff\\n \\n \\n def writediffproperties(ctx, diff):\\n \\\"\\\"\\\"write metadata to diff so patches could be applied losslessly\\\"\\\"\\\"\\n # creatediff returns with a diffid but query returns with an id\\n diffid = diff.get(b'diffid', diff.get(b'id'))\\n params = {\\n b'diff_id': diffid,\\n b'name': b'hg:meta',\\n b'data': templatefilters.json(\\n {\\n b'user': ctx.user(),\\n b'date': b'%d %d' % ctx.date(),\\n b'branch': ctx.branch(),\\n b'node': ctx.hex(),\\n b'parent': ctx.p1().hex(),\\n }\\n ),\\n }\\n callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)\\n \\n params = {\\n b'diff_id': diffid,\\n b'name': b'local:commits',\\n b'data': templatefilters.json(\\n {\\n ctx.hex(): {\\n b'author': stringutil.person(ctx.user()),\\n b'authorEmail': stringutil.email(ctx.user()),\\n b'time': int(ctx.date()[0]),\\n b'commit': ctx.hex(),\\n b'parents': [ctx.p1().hex()],\\n b'branch': ctx.branch(),\\n },\\n }\\n ),\\n }\\n callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)\\n \\n \\n def createdifferentialrevision(\\n ctx,\\n revid=None,\\n parentrevphid=None,\\n oldnode=None,\\n olddiff=None,\\n actions=None,\\n comment=None,\\n ):\\n \\\"\\\"\\\"create or update a Differential Revision\\n \\n If revid is None, create a new Differential Revision, otherwise update\\n revid. If parentrevphid is not None, set it as a dependency.\\n \\n If oldnode is not None, check if the patch content (without commit message\\n and metadata) has changed before creating another diff.\\n \\n If actions is not None, they will be appended to the transaction.\\n \\\"\\\"\\\"\\n repo = ctx.repo()\\n if oldnode:\\n diffopts = mdiff.diffopts(git=True, context=32767)\\n oldctx = repo.unfiltered()[oldnode]\\n neednewdiff = getdiff(ctx, diffopts) != getdiff(oldctx, diffopts)\\n else:\\n neednewdiff = True\\n \\n transactions = []\\n if neednewdiff:\\n diff = creatediff(ctx)\\n transactions.append({b'type': b'update', b'value': diff[b'phid']})\\n if comment:\\n transactions.append({b'type': b'comment', b'value': comment})\\n else:\\n # Even if we don't need to upload a new diff because the patch content\\n # does not change. We might still need to update its metadata so\\n # pushers could know the correct node metadata.\\n assert olddiff\\n diff = olddiff\\n writediffproperties(ctx, diff)\\n \\n # Set the parent Revision every time, so commit re-ordering is picked-up\\n if parentrevphid:\\n transactions.append(\\n {b'type': b'parents.set', b'value': [parentrevphid]}\\n )\\n \\n if actions:\\n transactions += actions\\n \\n # Parse commit message and update related fields.\\n desc = ctx.description()\\n info = callconduit(\\n repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}\\n )\\n for k, v in info[b'fields'].items():\\n if k in [b'title', b'summary', b'testPlan']:\\n transactions.append({b'type': k, b'value': v})\\n \\n params = {b'transactions': transactions}\\n if revid is not None:\\n # Update an existing Differential Revision\\n params[b'objectIdentifier'] = revid\\n \\n revision = callconduit(repo.ui, b'differential.revision.edit', params)\\n if not revision:\\n raise error.Abort(_(b'cannot create revision for %s') % ctx)\\n \\n return revision, diff\\n \\n \\n-def userphids(repo, names):\\n+def userphids(ui, names):\\n \\\"\\\"\\\"convert user names to PHIDs\\\"\\\"\\\"\\n names = [name.lower() for name in names]\\n query = {b'constraints': {b'usernames': names}}\\n- result = callconduit(repo.ui, b'user.search', query)\\n+ result = callconduit(ui, b'user.search', query)\\n # username not found is not an error of the API. So check if we have missed\\n # some names here.\\n data = result[b'data']\\n resolved = set(entry[b'fields'][b'username'].lower() for entry in data)\\n unresolved = set(names) - resolved\\n if unresolved:\\n raise error.Abort(\\n _(b'unknown username: %s') % b' '.join(sorted(unresolved))\\n )\\n return [entry[b'phid'] for entry in data]\\n \\n \\n @vcrcommand(\\n b'phabsend',\\n [\\n (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),\\n (b'', b'amend', True, _(b'update commit messages')),\\n (b'', b'reviewer', [], _(b'specify reviewers')),\\n (b'', b'blocker', [], _(b'specify blocking reviewers')),\\n (\\n b'm',\\n b'comment',\\n b'',\\n _(b'add a comment to Revisions with new\\/updated Diffs'),\\n ),\\n (b'', b'confirm', None, _(b'ask for confirmation before sending')),\\n ],\\n _(b'REV [OPTIONS]'),\\n helpcategory=command.CATEGORY_IMPORT_EXPORT,\\n )\\n def phabsend(ui, repo, *revs, **opts):\\n \\\"\\\"\\\"upload changesets to Phabricator\\n \\n If there are multiple revisions specified, they will be send as a stack\\n with a linear dependencies relationship using the order specified by the\\n revset.\\n \\n For the first time uploading changesets, local tags will be created to\\n maintain the association. After the first time, phabsend will check\\n obsstore and tags information so it can figure out whether to update an\\n existing Differential Revision, or create a new one.\\n \\n If --amend is set, update commit messages so they have the\\n ``Differential Revision`` URL, remove related tags. This is similar to what\\n arcanist will do, and is more desired in author-push workflows. Otherwise,\\n use local tags to record the ``Differential Revision`` association.\\n \\n The --confirm option lets you confirm changesets before sending them. You\\n can also add following to your configuration file to make it default\\n behaviour::\\n \\n [phabsend]\\n confirm = true\\n \\n phabsend will check obsstore and the above association to decide whether to\\n update an existing Differential Revision, or create a new one.\\n \\\"\\\"\\\"\\n opts = pycompat.byteskwargs(opts)\\n revs = list(revs) + opts.get(b'rev', [])\\n revs = scmutil.revrange(repo, revs)\\n revs.sort() # ascending order to preserve topological parent\\/child in phab\\n \\n if not revs:\\n raise error.Abort(_(b'phabsend requires at least one changeset'))\\n if opts.get(b'amend'):\\n cmdutil.checkunfinished(repo)\\n \\n # {newnode: (oldnode, olddiff, olddrev}\\n oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])\\n \\n confirm = ui.configbool(b'phabsend', b'confirm')\\n confirm |= bool(opts.get(b'confirm'))\\n if confirm:\\n confirmed = _confirmbeforesend(repo, revs, oldmap)\\n if not confirmed:\\n raise error.Abort(_(b'phabsend cancelled'))\\n \\n actions = []\\n reviewers = opts.get(b'reviewer', [])\\n blockers = opts.get(b'blocker', [])\\n phids = []\\n if reviewers:\\n- phids.extend(userphids(repo, reviewers))\\n+ phids.extend(userphids(repo.ui, reviewers))\\n if blockers:\\n phids.extend(\\n- map(lambda phid: b'blocking(%s)' % phid, userphids(repo, blockers))\\n+ map(\\n+ lambda phid: b'blocking(%s)' % phid,\\n+ userphids(repo.ui, blockers),\\n+ )\\n )\\n if phids:\\n actions.append({b'type': b'reviewers.add', b'value': phids})\\n \\n drevids = [] # [int]\\n diffmap = {} # {newnode: diff}\\n \\n # Send patches one by one so we know their Differential Revision PHIDs and\\n # can provide dependency relationship\\n lastrevphid = None\\n for rev in revs:\\n ui.debug(b'sending rev %d\\\\n' % rev)\\n ctx = repo[rev]\\n \\n # Get Differential Revision ID\\n oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))\\n if oldnode != ctx.node() or opts.get(b'amend'):\\n # Create or update Differential Revision\\n revision, diff = createdifferentialrevision(\\n ctx,\\n revid,\\n lastrevphid,\\n oldnode,\\n olddiff,\\n actions,\\n opts.get(b'comment'),\\n )\\n diffmap[ctx.node()] = diff\\n newrevid = int(revision[b'object'][b'id'])\\n newrevphid = revision[b'object'][b'phid']\\n if revid:\\n action = b'updated'\\n else:\\n action = b'created'\\n \\n # Create a local tag to note the association, if commit message\\n # does not have it already\\n m = _differentialrevisiondescre.search(ctx.description())\\n if not m or int(m.group('id')) != newrevid:\\n tagname = b'D%d' % newrevid\\n tags.tag(\\n repo,\\n tagname,\\n ctx.node(),\\n message=None,\\n user=None,\\n date=None,\\n local=True,\\n )\\n else:\\n # Nothing changed. But still set \\\"newrevphid\\\" so the next revision\\n # could depend on this one and \\\"newrevid\\\" for the summary line.\\n newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']\\n newrevid = revid\\n action = b'skipped'\\n \\n actiondesc = ui.label(\\n {\\n b'created': _(b'created'),\\n b'skipped': _(b'skipped'),\\n b'updated': _(b'updated'),\\n }[action],\\n b'phabricator.action.%s' % action,\\n )\\n drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')\\n nodedesc = ui.label(bytes(ctx), b'phabricator.node')\\n desc = ui.label(ctx.description().split(b'\\\\n')[0], b'phabricator.desc')\\n ui.write(\\n _(b'%s - %s - %s: %s\\\\n') % (drevdesc, actiondesc, nodedesc, desc)\\n )\\n drevids.append(newrevid)\\n lastrevphid = newrevphid\\n \\n # Update commit messages and remove tags\\n if opts.get(b'amend'):\\n unfi = repo.unfiltered()\\n drevs = callconduit(ui, b'differential.query', {b'ids': drevids})\\n with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):\\n wnode = unfi[b'.'].node()\\n mapping = {} # {oldnode: [newnode]}\\n for i, rev in enumerate(revs):\\n old = unfi[rev]\\n drevid = drevids[i]\\n drev = [d for d in drevs if int(d[b'id']) == drevid][0]\\n newdesc = getdescfromdrev(drev)\\n # Make sure commit message contain \\\"Differential Revision\\\"\\n if old.description() != newdesc:\\n if old.phase() == phases.public:\\n ui.warn(\\n _(b\\\"warning: not updating public commit %s\\\\n\\\")\\n % scmutil.formatchangeid(old)\\n )\\n continue\\n parents = [\\n mapping.get(old.p1().node(), (old.p1(),))[0],\\n mapping.get(old.p2().node(), (old.p2(),))[0],\\n ]\\n new = context.metadataonlyctx(\\n repo,\\n old,\\n parents=parents,\\n text=newdesc,\\n user=old.user(),\\n date=old.date(),\\n extra=old.extra(),\\n )\\n \\n newnode = new.commit()\\n \\n mapping[old.node()] = [newnode]\\n # Update diff property\\n # If it fails just warn and keep going, otherwise the DREV\\n # associations will be lost\\n try:\\n writediffproperties(unfi[newnode], diffmap[old.node()])\\n except util.urlerr.urlerror:\\n ui.warnnoi18n(\\n b'Failed to update metadata for D%d\\\\n' % drevid\\n )\\n # Remove local tags since it's no longer necessary\\n tagname = b'D%d' % drevid\\n if tagname in repo.tags():\\n tags.tag(\\n repo,\\n tagname,\\n nullid,\\n message=None,\\n user=None,\\n date=None,\\n local=True,\\n )\\n scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)\\n if wnode in mapping:\\n unfi.setparents(mapping[wnode][0])\\n \\n \\n # Map from \\\"hg:meta\\\" keys to header understood by \\\"hg import\\\". The order is\\n # consistent with \\\"hg export\\\" output.\\n _metanamemap = util.sortdict(\\n [\\n (b'user', b'User'),\\n (b'date', b'Date'),\\n (b'branch', b'Branch'),\\n (b'node', b'Node ID'),\\n (b'parent', b'Parent '),\\n ]\\n )\\n \\n \\n def _confirmbeforesend(repo, revs, oldmap):\\n url, token = readurltoken(repo.ui)\\n ui = repo.ui\\n for rev in revs:\\n ctx = repo[rev]\\n desc = ctx.description().splitlines()[0]\\n oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))\\n if drevid:\\n drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')\\n else:\\n drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')\\n \\n ui.write(\\n _(b'%s - %s: %s\\\\n')\\n % (\\n drevdesc,\\n ui.label(bytes(ctx), b'phabricator.node'),\\n ui.label(desc, b'phabricator.desc'),\\n )\\n )\\n \\n if ui.promptchoice(\\n _(b'Send the above changes to %s (yn)?$$ &Yes $$ &No') % url\\n ):\\n return False\\n \\n return True\\n \\n \\n _knownstatusnames = {\\n b'accepted',\\n b'needsreview',\\n b'needsrevision',\\n b'closed',\\n b'abandoned',\\n b'changesplanned',\\n }\\n \\n \\n def _getstatusname(drev):\\n \\\"\\\"\\\"get normalized status name from a Differential Revision\\\"\\\"\\\"\\n return drev[b'statusName'].replace(b' ', b'').lower()\\n \\n \\n # Small language to specify differential revisions. Support symbols: (), :X,\\n # +, and -.\\n \\n _elements = {\\n # token-type: binding-strength, primary, prefix, infix, suffix\\n b'(': (12, None, (b'group', 1, b')'), None, None),\\n b':': (8, None, (b'ancestors', 8), None, None),\\n b'&': (5, None, None, (b'and_', 5), None),\\n b'+': (4, None, None, (b'add', 4), None),\\n b'-': (4, None, None, (b'sub', 4), None),\\n b')': (0, None, None, None, None),\\n b'symbol': (0, b'symbol', None, None, None),\\n b'end': (0, None, None, None, None),\\n }\\n \\n \\n def _tokenize(text):\\n view = memoryview(text) # zero-copy slice\\n special = b'():+-& '\\n pos = 0\\n length = len(text)\\n while pos \\u003c length:\\n symbol = b''.join(\\n itertools.takewhile(\\n lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])\\n )\\n )\\n if symbol:\\n yield (b'symbol', symbol, pos)\\n pos += len(symbol)\\n else: # special char, ignore space\\n if text[pos : pos + 1] != b' ':\\n yield (text[pos : pos + 1], None, pos)\\n pos += 1\\n yield (b'end', None, pos)\\n \\n \\n def _parse(text):\\n tree, pos = parser.parser(_elements).parse(_tokenize(text))\\n if pos != len(text):\\n raise error.ParseError(b'invalid token', pos)\\n return tree\\n \\n \\n def _parsedrev(symbol):\\n \\\"\\\"\\\"str -\\u003e int or None, ex. 'D45' -\\u003e 45; '12' -\\u003e 12; 'x' -\\u003e None\\\"\\\"\\\"\\n if symbol.startswith(b'D') and symbol[1:].isdigit():\\n return int(symbol[1:])\\n if symbol.isdigit():\\n return int(symbol)\\n \\n \\n def _prefetchdrevs(tree):\\n \\\"\\\"\\\"return ({single-drev-id}, {ancestor-drev-id}) to prefetch\\\"\\\"\\\"\\n drevs = set()\\n ancestordrevs = set()\\n op = tree[0]\\n if op == b'symbol':\\n r = _parsedrev(tree[1])\\n if r:\\n drevs.add(r)\\n elif op == b'ancestors':\\n r, a = _prefetchdrevs(tree[1])\\n drevs.update(r)\\n ancestordrevs.update(r)\\n ancestordrevs.update(a)\\n else:\\n for t in tree[1:]:\\n r, a = _prefetchdrevs(t)\\n drevs.update(r)\\n ancestordrevs.update(a)\\n return drevs, ancestordrevs\\n \\n \\n def querydrev(ui, spec):\\n \\\"\\\"\\\"return a list of \\\"Differential Revision\\\" dicts\\n \\n spec is a string using a simple query language, see docstring in phabread\\n for details.\\n \\n A \\\"Differential Revision dict\\\" looks like:\\n \\n {\\n \\\"activeDiffPHID\\\": \\\"PHID-DIFF-xoqnjkobbm6k4dk6hi72\\\",\\n \\\"authorPHID\\\": \\\"PHID-USER-tv3ohwc4v4jeu34otlye\\\",\\n \\\"auxiliary\\\": {\\n \\\"phabricator:depends-on\\\": [\\n \\\"PHID-DREV-gbapp366kutjebt7agcd\\\"\\n ]\\n \\\"phabricator:projects\\\": [],\\n },\\n \\\"branch\\\": \\\"default\\\",\\n \\\"ccs\\\": [],\\n \\\"commits\\\": [],\\n \\\"dateCreated\\\": \\\"1499181406\\\",\\n \\\"dateModified\\\": \\\"1499182103\\\",\\n \\\"diffs\\\": [\\n \\\"3\\\",\\n \\\"4\\\",\\n ],\\n \\\"hashes\\\": [],\\n \\\"id\\\": \\\"2\\\",\\n \\\"lineCount\\\": \\\"2\\\",\\n \\\"phid\\\": \\\"PHID-DREV-672qvysjcczopag46qty\\\",\\n \\\"properties\\\": {},\\n \\\"repositoryPHID\\\": \\\"PHID-REPO-hub2hx62ieuqeheznasv\\\",\\n \\\"reviewers\\\": [],\\n \\\"sourcePath\\\": null\\n \\\"status\\\": \\\"0\\\",\\n \\\"statusName\\\": \\\"Needs Review\\\",\\n \\\"summary\\\": \\\"\\\",\\n \\\"testPlan\\\": \\\"\\\",\\n \\\"title\\\": \\\"example\\\",\\n \\\"uri\\\": \\\"https:\\/\\/phab.example.com\\/D2\\\",\\n }\\n \\\"\\\"\\\"\\n # TODO: replace differential.query and differential.querydiffs with\\n # differential.diff.search because the former (and their output) are\\n # frozen, and planned to be deprecated and removed.\\n \\n def fetch(params):\\n \\\"\\\"\\\"params -\\u003e single drev or None\\\"\\\"\\\"\\n key = (params.get(b'ids') or params.get(b'phids') or [None])[0]\\n if key in prefetched:\\n return prefetched[key]\\n drevs = callconduit(ui, b'differential.query', params)\\n # Fill prefetched with the result\\n for drev in drevs:\\n prefetched[drev[b'phid']] = drev\\n prefetched[int(drev[b'id'])] = drev\\n if key not in prefetched:\\n raise error.Abort(\\n _(b'cannot get Differential Revision %r') % params\\n )\\n return prefetched[key]\\n \\n def getstack(topdrevids):\\n \\\"\\\"\\\"given a top, get a stack from the bottom, [id] -\\u003e [id]\\\"\\\"\\\"\\n visited = set()\\n result = []\\n queue = [{b'ids': [i]} for i in topdrevids]\\n while queue:\\n params = queue.pop()\\n drev = fetch(params)\\n if drev[b'id'] in visited:\\n continue\\n visited.add(drev[b'id'])\\n result.append(int(drev[b'id']))\\n auxiliary = drev.get(b'auxiliary', {})\\n depends = auxiliary.get(b'phabricator:depends-on', [])\\n for phid in depends:\\n queue.append({b'phids': [phid]})\\n result.reverse()\\n return smartset.baseset(result)\\n \\n # Initialize prefetch cache\\n prefetched = {} # {id or phid: drev}\\n \\n tree = _parse(spec)\\n drevs, ancestordrevs = _prefetchdrevs(tree)\\n \\n # developer config: phabricator.batchsize\\n batchsize = ui.configint(b'phabricator', b'batchsize')\\n \\n # Prefetch Differential Revisions in batch\\n tofetch = set(drevs)\\n for r in ancestordrevs:\\n tofetch.update(range(max(1, r - batchsize), r + 1))\\n if drevs:\\n fetch({b'ids': list(tofetch)})\\n validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))\\n \\n # Walk through the tree, return smartsets\\n def walk(tree):\\n op = tree[0]\\n if op == b'symbol':\\n drev = _parsedrev(tree[1])\\n if drev:\\n return smartset.baseset([drev])\\n elif tree[1] in _knownstatusnames:\\n drevs = [\\n r\\n for r in validids\\n if _getstatusname(prefetched[r]) == tree[1]\\n ]\\n return smartset.baseset(drevs)\\n else:\\n raise error.Abort(_(b'unknown symbol: %s') % tree[1])\\n elif op in {b'and_', b'add', b'sub'}:\\n assert len(tree) == 3\\n return getattr(operator, op)(walk(tree[1]), walk(tree[2]))\\n elif op == b'group':\\n return walk(tree[1])\\n elif op == b'ancestors':\\n return getstack(walk(tree[1]))\\n else:\\n raise error.ProgrammingError(b'illegal tree: %r' % tree)\\n \\n return [prefetched[r] for r in walk(tree)]\\n \\n \\n def getdescfromdrev(drev):\\n \\\"\\\"\\\"get description (commit message) from \\\"Differential Revision\\\"\\n \\n This is similar to differential.getcommitmessage API. But we only care\\n about limited fields: title, summary, test plan, and URL.\\n \\\"\\\"\\\"\\n title = drev[b'title']\\n summary = drev[b'summary'].rstrip()\\n testplan = drev[b'testPlan'].rstrip()\\n if testplan:\\n testplan = b'Test Plan:\\\\n%s' % testplan\\n uri = b'Differential Revision: %s' % drev[b'uri']\\n return b'\\\\n\\\\n'.join(filter(None, [title, summary, testplan, uri]))\\n \\n \\n def getdiffmeta(diff):\\n \\\"\\\"\\\"get commit metadata (date, node, user, p1) from a diff object\\n \\n The metadata could be \\\"hg:meta\\\", sent by phabsend, like:\\n \\n \\\"properties\\\": {\\n \\\"hg:meta\\\": {\\n \\\"branch\\\": \\\"default\\\",\\n \\\"date\\\": \\\"1499571514 25200\\\",\\n \\\"node\\\": \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\",\\n \\\"user\\\": \\\"Foo Bar \\u003cfoo@example.com\\u003e\\\",\\n \\\"parent\\\": \\\"6d0abad76b30e4724a37ab8721d630394070fe16\\\"\\n }\\n }\\n \\n Or converted from \\\"local:commits\\\", sent by \\\"arc\\\", like:\\n \\n \\\"properties\\\": {\\n \\\"local:commits\\\": {\\n \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\": {\\n \\\"author\\\": \\\"Foo Bar\\\",\\n \\\"authorEmail\\\": \\\"foo@example.com\\\"\\n \\\"branch\\\": \\\"default\\\",\\n \\\"commit\\\": \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\",\\n \\\"local\\\": \\\"1000\\\",\\n \\\"message\\\": \\\"...\\\",\\n \\\"parents\\\": [\\\"6d0abad76b30e4724a37ab8721d630394070fe16\\\"],\\n \\\"rev\\\": \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\",\\n \\\"summary\\\": \\\"...\\\",\\n \\\"tag\\\": \\\"\\\",\\n \\\"time\\\": 1499546314,\\n }\\n }\\n }\\n \\n Note: metadata extracted from \\\"local:commits\\\" will lose time zone\\n information.\\n \\\"\\\"\\\"\\n props = diff.get(b'properties') or {}\\n meta = props.get(b'hg:meta')\\n if not meta:\\n if props.get(b'local:commits'):\\n commit = sorted(props[b'local:commits'].values())[0]\\n meta = {}\\n if b'author' in commit and b'authorEmail' in commit:\\n meta[b'user'] = b'%s \\u003c%s\\u003e' % (\\n commit[b'author'],\\n commit[b'authorEmail'],\\n )\\n if b'time' in commit:\\n meta[b'date'] = b'%d 0' % int(commit[b'time'])\\n if b'branch' in commit:\\n meta[b'branch'] = commit[b'branch']\\n node = commit.get(b'commit', commit.get(b'rev'))\\n if node:\\n meta[b'node'] = node\\n if len(commit.get(b'parents', ())) \\u003e= 1:\\n meta[b'parent'] = commit[b'parents'][0]\\n else:\\n meta = {}\\n if b'date' not in meta and b'dateCreated' in diff:\\n meta[b'date'] = b'%s 0' % diff[b'dateCreated']\\n if b'branch' not in meta and diff.get(b'branch'):\\n meta[b'branch'] = diff[b'branch']\\n if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):\\n meta[b'parent'] = diff[b'sourceControlBaseRevision']\\n return meta\\n \\n \\n def readpatch(ui, drevs, write):\\n \\\"\\\"\\\"generate plain-text patch readable by 'hg import'\\n \\n write is usually ui.write. drevs is what \\\"querydrev\\\" returns, results of\\n \\\"differential.query\\\".\\n \\\"\\\"\\\"\\n # Prefetch hg:meta property for all diffs\\n diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))\\n diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})\\n \\n # Generate patch for each drev\\n for drev in drevs:\\n ui.note(_(b'reading D%s\\\\n') % drev[b'id'])\\n \\n diffid = max(int(v) for v in drev[b'diffs'])\\n body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})\\n desc = getdescfromdrev(drev)\\n header = b'# HG changeset patch\\\\n'\\n \\n # Try to preserve metadata from hg:meta property. Write hg patch\\n # headers that can be read by the \\\"import\\\" command. See patchheadermap\\n # and extract in mercurial\\/patch.py for supported headers.\\n meta = getdiffmeta(diffs[b'%d' % diffid])\\n for k in _metanamemap.keys():\\n if k in meta:\\n header += b'# %s %s\\\\n' % (_metanamemap[k], meta[k])\\n \\n content = b'%s%s\\\\n%s' % (header, desc, body)\\n write(content)\\n \\n \\n @vcrcommand(\\n b'phabread',\\n [(b'', b'stack', False, _(b'read dependencies'))],\\n _(b'DREVSPEC [OPTIONS]'),\\n helpcategory=command.CATEGORY_IMPORT_EXPORT,\\n )\\n def phabread(ui, repo, spec, **opts):\\n \\\"\\\"\\\"print patches from Phabricator suitable for importing\\n \\n DREVSPEC could be a Differential Revision identity, like ``D123``, or just\\n the number ``123``. It could also have common operators like ``+``, ``-``,\\n ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to\\n select a stack.\\n \\n ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``\\n could be used to filter patches by status. For performance reason, they\\n only represent a subset of non-status selections and cannot be used alone.\\n \\n For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude\\n D2 and D4. ``:D9 & needsreview`` selects \\\"Needs Review\\\" revisions in a\\n stack up to D9.\\n \\n If --stack is given, follow dependencies information and read all patches.\\n It is equivalent to the ``:`` operator.\\n \\\"\\\"\\\"\\n opts = pycompat.byteskwargs(opts)\\n if opts.get(b'stack'):\\n spec = b':(%s)' % spec\\n drevs = querydrev(repo.ui, spec)\\n readpatch(repo.ui, drevs, ui.write)\\n \\n \\n @vcrcommand(\\n b'phabupdate',\\n [\\n (b'', b'accept', False, _(b'accept revisions')),\\n (b'', b'reject', False, _(b'reject revisions')),\\n (b'', b'abandon', False, _(b'abandon revisions')),\\n (b'', b'reclaim', False, _(b'reclaim revisions')),\\n (b'm', b'comment', b'', _(b'comment on the last revision')),\\n ],\\n _(b'DREVSPEC [OPTIONS]'),\\n helpcategory=command.CATEGORY_IMPORT_EXPORT,\\n )\\n def phabupdate(ui, repo, spec, **opts):\\n \\\"\\\"\\\"update Differential Revision in batch\\n \\n DREVSPEC selects revisions. See :hg:`help phabread` for its usage.\\n \\\"\\\"\\\"\\n opts = pycompat.byteskwargs(opts)\\n flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]\\n if len(flags) \\u003e 1:\\n raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))\\n \\n actions = []\\n for f in flags:\\n actions.append({b'type': f, b'value': True})\\n \\n drevs = querydrev(repo.ui, spec)\\n for i, drev in enumerate(drevs):\\n if i + 1 == len(drevs) and opts.get(b'comment'):\\n actions.append({b'type': b'comment', b'value': opts[b'comment']})\\n if actions:\\n params = {\\n b'objectIdentifier': drev[b'phid'],\\n b'transactions': actions,\\n }\\n callconduit(ui, b'differential.revision.edit', params)\\n \\n \\n @eh.templatekeyword(b'phabreview', requires={b'ctx'})\\n def template_review(context, mapping):\\n \\\"\\\"\\\":phabreview: Object describing the review for this changeset.\\n Has attributes `url` and `id`.\\n \\\"\\\"\\\"\\n ctx = context.resource(mapping, b'ctx')\\n m = _differentialrevisiondescre.search(ctx.description())\\n if m:\\n return templateutil.hybriddict(\\n {b'url': m.group('url'), b'id': b\\\"D%s\\\" % m.group('id'),}\\n )\\n else:\\n tags = ctx.repo().nodetags(ctx.node())\\n for t in tags:\\n if _differentialrevisiontagre.match(t):\\n url = ctx.repo().ui.config(b'phabricator', b'url')\\n if not url.endswith(b'\\/'):\\n url += b'\\/'\\n url += t\\n \\n return templateutil.hybriddict({b'url': url, b'id': t,})\\n return None\\n \\n \\n @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})\\n def template_status(context, mapping):\\n \\\"\\\"\\\":phabstatus: String. Status of Phabricator differential.\\n \\\"\\\"\\\"\\n ctx = context.resource(mapping, b'ctx')\\n repo = context.resource(mapping, b'repo')\\n ui = context.resource(mapping, b'ui')\\n \\n rev = ctx.rev()\\n try:\\n drevid = getdrevmap(repo, [rev])[rev]\\n except KeyError:\\n return None\\n drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})\\n for drev in drevs:\\n if int(drev[b'id']) == drevid:\\n return templateutil.hybriddict(\\n {b'url': drev[b'uri'], b'status': drev[b'statusName'],}\\n )\\n return None\\n \\n \\n @show.showview(b'phabstatus', csettopic=b'work')\\n def phabstatusshowview(ui, repo, displayer):\\n \\\"\\\"\\\"Phabricator differiential status\\\"\\\"\\\"\\n revs = repo.revs('sort(_underway(), topo)')\\n drevmap = getdrevmap(repo, revs)\\n unknownrevs, drevids, revsbydrevid = [], set([]), {}\\n for rev, drevid in pycompat.iteritems(drevmap):\\n if drevid is not None:\\n drevids.add(drevid)\\n revsbydrevid.setdefault(drevid, set([])).add(rev)\\n else:\\n unknownrevs.append(rev)\\n \\n drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})\\n drevsbyrev = {}\\n for drev in drevs:\\n for rev in revsbydrevid[int(drev[b'id'])]:\\n drevsbyrev[rev] = drev\\n \\n def phabstatus(ctx):\\n drev = drevsbyrev[ctx.rev()]\\n status = ui.label(\\n b'%(statusName)s' % drev,\\n b'phabricator.status.%s' % _getstatusname(drev),\\n )\\n ui.write(b\\\"\\\\n%s %s\\\\n\\\" % (drev[b'uri'], status))\\n \\n revs -= smartset.baseset(unknownrevs)\\n revdag = graphmod.dagwalker(repo, revs)\\n \\n ui.setconfig(b'experimental', b'graphshorten', True)\\n displayer._exthook = phabstatus\\n nodelen = show.longestshortest(repo, revs)\\n logcmdutil.displaygraph(\\n ui,\\n repo,\\n revdag,\\n displayer,\\n graphmod.asciiedges,\\n props={b'nodelen': nodelen},\\n )\\n\"}]}],\"properties\":[]},\"20442\":{\"id\":\"20442\",\"revisionID\":\"8206\",\"dateCreated\":\"1581962881\",\"dateModified\":\"1583327825\",\"sourceControlBaseRevision\":\"72c6190de577bb2bb448eb2b14121e4ef85d08ff\",\"sourceControlPath\":null,\"sourceControlSystem\":\"hg\",\"branch\":null,\"bookmark\":null,\"creationMethod\":\"commit\",\"description\":\"rHG9b46270917348950e3fb1e73a5c9e46038065622\",\"unitStatus\":\"6\",\"lintStatus\":\"6\",\"changes\":[{\"id\":\"55597\",\"metadata\":{\"line:first\":1186,\"hash.effect\":\"QoC7Ipbh4yf0\"},\"oldPath\":\"hgext\\/phabricator.py\",\"currentPath\":\"hgext\\/phabricator.py\",\"awayPaths\":[],\"oldProperties\":[],\"newProperties\":[],\"type\":\"2\",\"fileType\":\"1\",\"commitHash\":null,\"addLines\":\"6\",\"delLines\":\"6\",\"hunks\":[{\"oldOffset\":\"1\",\"newOffset\":\"1\",\"oldLength\":\"1799\",\"newLength\":\"1799\",\"addLines\":null,\"delLines\":null,\"isMissingOldNewline\":null,\"isMissingNewNewline\":null,\"corpus\":\" # phabricator.py - simple Phabricator integration\\n #\\n # Copyright 2017 Facebook, Inc.\\n #\\n # This software may be used and distributed according to the terms of the\\n # GNU General Public License version 2 or any later version.\\n \\\"\\\"\\\"simple Phabricator integration (EXPERIMENTAL)\\n \\n This extension provides a ``phabsend`` command which sends a stack of\\n changesets to Phabricator, and a ``phabread`` command which prints a stack of\\n revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command\\n to update statuses in batch.\\n \\n A \\\"phabstatus\\\" view for :hg:`show` is also provided; it displays status\\n information of Phabricator differentials associated with unfinished\\n changesets.\\n \\n By default, Phabricator requires ``Test Plan`` which might prevent some\\n changeset from being sent. The requirement could be disabled by changing\\n ``differential.require-test-plan-field`` config server side.\\n \\n Config::\\n \\n [phabricator]\\n # Phabricator URL\\n url = https:\\/\\/phab.example.com\\/\\n \\n # Repo callsign. If a repo has a URL https:\\/\\/$HOST\\/diffusion\\/FOO, then its\\n # callsign is \\\"FOO\\\".\\n callsign = FOO\\n \\n # curl command to use. If not set (default), use builtin HTTP library to\\n # communicate. If set, use the specified curl command. This could be useful\\n # if you need to specify advanced options that is not easily supported by\\n # the internal library.\\n curlcmd = curl --connect-timeout 2 --retry 3 --silent\\n \\n [auth]\\n example.schemes = https\\n example.prefix = phab.example.com\\n \\n # API token. Get it from https:\\/\\/$HOST\\/conduit\\/login\\/\\n example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx\\n \\\"\\\"\\\"\\n \\n from __future__ import absolute_import\\n \\n import base64\\n import contextlib\\n import hashlib\\n import itertools\\n import json\\n import mimetypes\\n import operator\\n import re\\n \\n from mercurial.node import bin, nullid\\n from mercurial.i18n import _\\n from mercurial.pycompat import getattr\\n from mercurial.thirdparty import attr\\n from mercurial import (\\n cmdutil,\\n context,\\n encoding,\\n error,\\n exthelper,\\n graphmod,\\n httpconnection as httpconnectionmod,\\n localrepo,\\n logcmdutil,\\n match,\\n mdiff,\\n obsutil,\\n parser,\\n patch,\\n phases,\\n pycompat,\\n scmutil,\\n smartset,\\n tags,\\n templatefilters,\\n templateutil,\\n url as urlmod,\\n util,\\n )\\n from mercurial.utils import (\\n procutil,\\n stringutil,\\n )\\n from . import show\\n \\n \\n # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for\\n # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should\\n # be specifying the version(s) of Mercurial they are tested with, or\\n # leave the attribute unspecified.\\n testedwith = b'ships-with-hg-core'\\n \\n eh = exthelper.exthelper()\\n \\n cmdtable = eh.cmdtable\\n command = eh.command\\n configtable = eh.configtable\\n templatekeyword = eh.templatekeyword\\n uisetup = eh.finaluisetup\\n \\n # developer config: phabricator.batchsize\\n eh.configitem(\\n b'phabricator', b'batchsize', default=12,\\n )\\n eh.configitem(\\n b'phabricator', b'callsign', default=None,\\n )\\n eh.configitem(\\n b'phabricator', b'curlcmd', default=None,\\n )\\n # developer config: phabricator.repophid\\n eh.configitem(\\n b'phabricator', b'repophid', default=None,\\n )\\n eh.configitem(\\n b'phabricator', b'url', default=None,\\n )\\n eh.configitem(\\n b'phabsend', b'confirm', default=False,\\n )\\n \\n colortable = {\\n b'phabricator.action.created': b'green',\\n b'phabricator.action.skipped': b'magenta',\\n b'phabricator.action.updated': b'magenta',\\n b'phabricator.desc': b'',\\n b'phabricator.drev': b'bold',\\n b'phabricator.node': b'',\\n b'phabricator.status.abandoned': b'magenta dim',\\n b'phabricator.status.accepted': b'green bold',\\n b'phabricator.status.closed': b'green',\\n b'phabricator.status.needsreview': b'yellow',\\n b'phabricator.status.needsrevision': b'red',\\n b'phabricator.status.changesplanned': b'red',\\n }\\n \\n _VCR_FLAGS = [\\n (\\n b'',\\n b'test-vcr',\\n b'',\\n _(\\n b'Path to a vcr file. If nonexistent, will record a new vcr transcript'\\n b', otherwise will mock all http requests using the specified vcr file.'\\n b' (ADVANCED)'\\n ),\\n ),\\n ]\\n \\n \\n @eh.wrapfunction(localrepo, \\\"loadhgrc\\\")\\n def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):\\n \\\"\\\"\\\"Load ``.arcconfig`` content into a ui instance on repository open.\\n \\\"\\\"\\\"\\n result = False\\n arcconfig = {}\\n \\n try:\\n # json.loads only accepts bytes from 3.6+\\n rawparams = encoding.unifromlocal(wdirvfs.read(b\\\".arcconfig\\\"))\\n # json.loads only returns unicode strings\\n arcconfig = pycompat.rapply(\\n lambda x: encoding.unitolocal(x)\\n if isinstance(x, pycompat.unicode)\\n else x,\\n pycompat.json_loads(rawparams),\\n )\\n \\n result = True\\n except ValueError:\\n ui.warn(_(b\\\"invalid JSON in %s\\\\n\\\") % wdirvfs.join(b\\\".arcconfig\\\"))\\n except IOError:\\n pass\\n \\n cfg = util.sortdict()\\n \\n if b\\\"repository.callsign\\\" in arcconfig:\\n cfg[(b\\\"phabricator\\\", b\\\"callsign\\\")] = arcconfig[b\\\"repository.callsign\\\"]\\n \\n if b\\\"phabricator.uri\\\" in arcconfig:\\n cfg[(b\\\"phabricator\\\", b\\\"url\\\")] = arcconfig[b\\\"phabricator.uri\\\"]\\n \\n if cfg:\\n ui.applyconfig(cfg, source=wdirvfs.join(b\\\".arcconfig\\\"))\\n \\n return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg\\/hgrc\\n \\n \\n def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):\\n fullflags = flags + _VCR_FLAGS\\n \\n def hgmatcher(r1, r2):\\n if r1.uri != r2.uri or r1.method != r2.method:\\n return False\\n r1params = util.urlreq.parseqs(r1.body)\\n r2params = util.urlreq.parseqs(r2.body)\\n for key in r1params:\\n if key not in r2params:\\n return False\\n value = r1params[key][0]\\n # we want to compare json payloads without worrying about ordering\\n if value.startswith(b'{') and value.endswith(b'}'):\\n r1json = pycompat.json_loads(value)\\n r2json = pycompat.json_loads(r2params[key][0])\\n if r1json != r2json:\\n return False\\n elif r2params[key][0] != value:\\n return False\\n return True\\n \\n def sanitiserequest(request):\\n request.body = re.sub(\\n br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body\\n )\\n return request\\n \\n def sanitiseresponse(response):\\n if 'set-cookie' in response['headers']:\\n del response['headers']['set-cookie']\\n return response\\n \\n def decorate(fn):\\n def inner(*args, **kwargs):\\n cassette = pycompat.fsdecode(kwargs.pop('test_vcr', None))\\n if cassette:\\n import hgdemandimport\\n \\n with hgdemandimport.deactivated():\\n import vcr as vcrmod\\n import vcr.stubs as stubs\\n \\n vcr = vcrmod.VCR(\\n serializer='json',\\n before_record_request=sanitiserequest,\\n before_record_response=sanitiseresponse,\\n custom_patches=[\\n (\\n urlmod,\\n 'httpconnection',\\n stubs.VCRHTTPConnection,\\n ),\\n (\\n urlmod,\\n 'httpsconnection',\\n stubs.VCRHTTPSConnection,\\n ),\\n ],\\n )\\n vcr.register_matcher('hgmatcher', hgmatcher)\\n with vcr.use_cassette(cassette, match_on=['hgmatcher']):\\n return fn(*args, **kwargs)\\n return fn(*args, **kwargs)\\n \\n inner.__name__ = fn.__name__\\n inner.__doc__ = fn.__doc__\\n return command(\\n name,\\n fullflags,\\n spec,\\n helpcategory=helpcategory,\\n optionalrepo=optionalrepo,\\n )(inner)\\n \\n return decorate\\n \\n \\n def urlencodenested(params):\\n \\\"\\\"\\\"like urlencode, but works with nested parameters.\\n \\n For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be\\n flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to\\n urlencode. Note: the encoding is consistent with PHP's http_build_query.\\n \\\"\\\"\\\"\\n flatparams = util.sortdict()\\n \\n def process(prefix, obj):\\n if isinstance(obj, bool):\\n obj = {True: b'true', False: b'false'}[obj] # Python -\\u003e PHP form\\n lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]\\n items = {list: lister, dict: lambda x: x.items()}.get(type(obj))\\n if items is None:\\n flatparams[prefix] = obj\\n else:\\n for k, v in items(obj):\\n if prefix:\\n process(b'%s[%s]' % (prefix, k), v)\\n else:\\n process(k, v)\\n \\n process(b'', params)\\n return util.urlreq.urlencode(flatparams)\\n \\n \\n def readurltoken(ui):\\n \\\"\\\"\\\"return conduit url, token and make sure they exist\\n \\n Currently read from [auth] config section. In the future, it might\\n make sense to read from .arcconfig and .arcrc as well.\\n \\\"\\\"\\\"\\n url = ui.config(b'phabricator', b'url')\\n if not url:\\n raise error.Abort(\\n _(b'config %s.%s is required') % (b'phabricator', b'url')\\n )\\n \\n res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)\\n token = None\\n \\n if res:\\n group, auth = res\\n \\n ui.debug(b\\\"using auth.%s.* for authentication\\\\n\\\" % group)\\n \\n token = auth.get(b'phabtoken')\\n \\n if not token:\\n raise error.Abort(\\n _(b'Can\\\\'t find conduit token associated to %s') % (url,)\\n )\\n \\n return url, token\\n \\n \\n def callconduit(ui, name, params):\\n \\\"\\\"\\\"call Conduit API, params is a dict. return json.loads result, or None\\\"\\\"\\\"\\n host, token = readurltoken(ui)\\n url, authinfo = util.url(b'\\/'.join([host, b'api', name])).authinfo()\\n ui.debug(b'Conduit Call: %s %s\\\\n' % (url, pycompat.byterepr(params)))\\n params = params.copy()\\n params[b'__conduit__'] = {\\n b'token': token,\\n }\\n rawdata = {\\n b'params': templatefilters.json(params),\\n b'output': b'json',\\n b'__conduit__': 1,\\n }\\n data = urlencodenested(rawdata)\\n curlcmd = ui.config(b'phabricator', b'curlcmd')\\n if curlcmd:\\n sin, sout = procutil.popen2(\\n b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))\\n )\\n sin.write(data)\\n sin.close()\\n body = sout.read()\\n else:\\n urlopener = urlmod.opener(ui, authinfo)\\n request = util.urlreq.request(pycompat.strurl(url), data=data)\\n with contextlib.closing(urlopener.open(request)) as rsp:\\n body = rsp.read()\\n ui.debug(b'Conduit Response: %s\\\\n' % body)\\n parsed = pycompat.rapply(\\n lambda x: encoding.unitolocal(x)\\n if isinstance(x, pycompat.unicode)\\n else x,\\n # json.loads only accepts bytes from py3.6+\\n pycompat.json_loads(encoding.unifromlocal(body)),\\n )\\n if parsed.get(b'error_code'):\\n msg = _(b'Conduit Error (%s): %s') % (\\n parsed[b'error_code'],\\n parsed[b'error_info'],\\n )\\n raise error.Abort(msg)\\n return parsed[b'result']\\n \\n \\n @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)\\n def debugcallconduit(ui, repo, name):\\n \\\"\\\"\\\"call Conduit API\\n \\n Call parameters are read from stdin as a JSON blob. Result will be written\\n to stdout as a JSON blob.\\n \\\"\\\"\\\"\\n # json.loads only accepts bytes from 3.6+\\n rawparams = encoding.unifromlocal(ui.fin.read())\\n # json.loads only returns unicode strings\\n params = pycompat.rapply(\\n lambda x: encoding.unitolocal(x)\\n if isinstance(x, pycompat.unicode)\\n else x,\\n pycompat.json_loads(rawparams),\\n )\\n # json.dumps only accepts unicode strings\\n result = pycompat.rapply(\\n lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,\\n callconduit(ui, name, params),\\n )\\n s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))\\n ui.write(b'%s\\\\n' % encoding.unitolocal(s))\\n \\n \\n def getrepophid(repo):\\n \\\"\\\"\\\"given callsign, return repository PHID or None\\\"\\\"\\\"\\n # developer config: phabricator.repophid\\n repophid = repo.ui.config(b'phabricator', b'repophid')\\n if repophid:\\n return repophid\\n callsign = repo.ui.config(b'phabricator', b'callsign')\\n if not callsign:\\n return None\\n query = callconduit(\\n repo.ui,\\n b'diffusion.repository.search',\\n {b'constraints': {b'callsigns': [callsign]}},\\n )\\n if len(query[b'data']) == 0:\\n return None\\n repophid = query[b'data'][0][b'phid']\\n repo.ui.setconfig(b'phabricator', b'repophid', repophid)\\n return repophid\\n \\n \\n _differentialrevisiontagre = re.compile(br'\\\\AD([1-9][0-9]*)\\\\Z')\\n _differentialrevisiondescre = re.compile(\\n br'^Differential Revision:\\\\s*(?P\\u003curl\\u003e(?:.*)D(?P\\u003cid\\u003e[1-9][0-9]*))$', re.M\\n )\\n \\n \\n def getoldnodedrevmap(repo, nodelist):\\n \\\"\\\"\\\"find previous nodes that has been sent to Phabricator\\n \\n return {node: (oldnode, Differential diff, Differential Revision ID)}\\n for node in nodelist with known previous sent versions, or associated\\n Differential Revision IDs. ``oldnode`` and ``Differential diff`` could\\n be ``None``.\\n \\n Examines commit messages like \\\"Differential Revision:\\\" to get the\\n association information.\\n \\n If such commit message line is not found, examines all precursors and their\\n tags. Tags with format like \\\"D1234\\\" are considered a match and the node\\n with that tag, and the number after \\\"D\\\" (ex. 1234) will be returned.\\n \\n The ``old node``, if not None, is guaranteed to be the last diff of\\n corresponding Differential Revision, and exist in the repo.\\n \\\"\\\"\\\"\\n unfi = repo.unfiltered()\\n has_node = unfi.changelog.index.has_node\\n \\n result = {} # {node: (oldnode?, lastdiff?, drev)}\\n toconfirm = {} # {node: (force, {precnode}, drev)}\\n for node in nodelist:\\n ctx = unfi[node]\\n # For tags like \\\"D123\\\", put them into \\\"toconfirm\\\" to verify later\\n precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))\\n for n in precnodes:\\n if has_node(n):\\n for tag in unfi.nodetags(n):\\n m = _differentialrevisiontagre.match(tag)\\n if m:\\n toconfirm[node] = (0, set(precnodes), int(m.group(1)))\\n break\\n else:\\n continue # move to next predecessor\\n break # found a tag, stop\\n else:\\n # Check commit message\\n m = _differentialrevisiondescre.search(ctx.description())\\n if m:\\n toconfirm[node] = (1, set(precnodes), int(m.group('id')))\\n \\n # Double check if tags are genuine by collecting all old nodes from\\n # Phabricator, and expect precursors overlap with it.\\n if toconfirm:\\n drevs = [drev for force, precs, drev in toconfirm.values()]\\n alldiffs = callconduit(\\n unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}\\n )\\n getnode = lambda d: bin(getdiffmeta(d).get(b'node', b'')) or None\\n for newnode, (force, precset, drev) in toconfirm.items():\\n diffs = [\\n d for d in alldiffs.values() if int(d[b'revisionID']) == drev\\n ]\\n \\n # \\\"precursors\\\" as known by Phabricator\\n phprecset = set(getnode(d) for d in diffs)\\n \\n # Ignore if precursors (Phabricator and local repo) do not overlap,\\n # and force is not set (when commit message says nothing)\\n if not force and not bool(phprecset & precset):\\n tagname = b'D%d' % drev\\n tags.tag(\\n repo,\\n tagname,\\n nullid,\\n message=None,\\n user=None,\\n date=None,\\n local=True,\\n )\\n unfi.ui.warn(\\n _(\\n b'D%d: local tag removed - does not match '\\n b'Differential history\\\\n'\\n )\\n % drev\\n )\\n continue\\n \\n # Find the last node using Phabricator metadata, and make sure it\\n # exists in the repo\\n oldnode = lastdiff = None\\n if diffs:\\n lastdiff = max(diffs, key=lambda d: int(d[b'id']))\\n oldnode = getnode(lastdiff)\\n if oldnode and not has_node(oldnode):\\n oldnode = None\\n \\n result[newnode] = (oldnode, lastdiff, drev)\\n \\n return result\\n \\n \\n def getdrevmap(repo, revs):\\n \\\"\\\"\\\"Return a dict mapping each rev in `revs` to their Differential Revision\\n ID or None.\\n \\\"\\\"\\\"\\n result = {}\\n for rev in revs:\\n result[rev] = None\\n ctx = repo[rev]\\n # Check commit message\\n m = _differentialrevisiondescre.search(ctx.description())\\n if m:\\n result[rev] = int(m.group('id'))\\n continue\\n # Check tags\\n for tag in repo.nodetags(ctx.node()):\\n m = _differentialrevisiontagre.match(tag)\\n if m:\\n result[rev] = int(m.group(1))\\n break\\n \\n return result\\n \\n \\n def getdiff(ctx, diffopts):\\n \\\"\\\"\\\"plain-text diff without header (user, commit message, etc)\\\"\\\"\\\"\\n output = util.stringio()\\n for chunk, _label in patch.diffui(\\n ctx.repo(), ctx.p1().node(), ctx.node(), None, opts=diffopts\\n ):\\n output.write(chunk)\\n return output.getvalue()\\n \\n \\n class DiffChangeType(object):\\n ADD = 1\\n CHANGE = 2\\n DELETE = 3\\n MOVE_AWAY = 4\\n COPY_AWAY = 5\\n MOVE_HERE = 6\\n COPY_HERE = 7\\n MULTICOPY = 8\\n \\n \\n class DiffFileType(object):\\n TEXT = 1\\n IMAGE = 2\\n BINARY = 3\\n \\n \\n @attr.s\\n class phabhunk(dict):\\n \\\"\\\"\\\"Represents a Differential hunk, which is owned by a Differential change\\n \\\"\\\"\\\"\\n \\n oldOffset = attr.ib(default=0) # camelcase-required\\n oldLength = attr.ib(default=0) # camelcase-required\\n newOffset = attr.ib(default=0) # camelcase-required\\n newLength = attr.ib(default=0) # camelcase-required\\n corpus = attr.ib(default='')\\n # These get added to the phabchange's equivalents\\n addLines = attr.ib(default=0) # camelcase-required\\n delLines = attr.ib(default=0) # camelcase-required\\n \\n \\n @attr.s\\n class phabchange(object):\\n \\\"\\\"\\\"Represents a Differential change, owns Differential hunks and owned by a\\n Differential diff. Each one represents one file in a diff.\\n \\\"\\\"\\\"\\n \\n currentPath = attr.ib(default=None) # camelcase-required\\n oldPath = attr.ib(default=None) # camelcase-required\\n awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required\\n metadata = attr.ib(default=attr.Factory(dict))\\n oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required\\n newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required\\n type = attr.ib(default=DiffChangeType.CHANGE)\\n fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required\\n commitHash = attr.ib(default=None) # camelcase-required\\n addLines = attr.ib(default=0) # camelcase-required\\n delLines = attr.ib(default=0) # camelcase-required\\n hunks = attr.ib(default=attr.Factory(list))\\n \\n def copynewmetadatatoold(self):\\n for key in list(self.metadata.keys()):\\n newkey = key.replace(b'new:', b'old:')\\n self.metadata[newkey] = self.metadata[key]\\n \\n def addoldmode(self, value):\\n self.oldProperties[b'unix:filemode'] = value\\n \\n def addnewmode(self, value):\\n self.newProperties[b'unix:filemode'] = value\\n \\n def addhunk(self, hunk):\\n if not isinstance(hunk, phabhunk):\\n raise error.Abort(b'phabchange.addhunk only takes phabhunks')\\n self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))\\n # It's useful to include these stats since the Phab web UI shows them,\\n # and uses them to estimate how large a change a Revision is. Also used\\n # in email subjects for the [+++--] bit.\\n self.addLines += hunk.addLines\\n self.delLines += hunk.delLines\\n \\n \\n @attr.s\\n class phabdiff(object):\\n \\\"\\\"\\\"Represents a Differential diff, owns Differential changes. Corresponds\\n to a commit.\\n \\\"\\\"\\\"\\n \\n # Doesn't seem to be any reason to send this (output of uname -n)\\n sourceMachine = attr.ib(default=b'') # camelcase-required\\n sourcePath = attr.ib(default=b'\\/') # camelcase-required\\n sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required\\n sourceControlPath = attr.ib(default=b'\\/') # camelcase-required\\n sourceControlSystem = attr.ib(default=b'hg') # camelcase-required\\n branch = attr.ib(default=b'default')\\n bookmark = attr.ib(default=None)\\n creationMethod = attr.ib(default=b'phabsend') # camelcase-required\\n lintStatus = attr.ib(default=b'none') # camelcase-required\\n unitStatus = attr.ib(default=b'none') # camelcase-required\\n changes = attr.ib(default=attr.Factory(dict))\\n repositoryPHID = attr.ib(default=None) # camelcase-required\\n \\n def addchange(self, change):\\n if not isinstance(change, phabchange):\\n raise error.Abort(b'phabdiff.addchange only takes phabchanges')\\n self.changes[change.currentPath] = pycompat.byteskwargs(\\n attr.asdict(change)\\n )\\n \\n \\n def maketext(pchange, ctx, fname):\\n \\\"\\\"\\\"populate the phabchange for a text file\\\"\\\"\\\"\\n repo = ctx.repo()\\n fmatcher = match.exact([fname])\\n diffopts = mdiff.diffopts(git=True, context=32767)\\n _pfctx, _fctx, header, fhunks = next(\\n patch.diffhunks(repo, ctx.p1(), ctx, fmatcher, opts=diffopts)\\n )\\n \\n for fhunk in fhunks:\\n (oldOffset, oldLength, newOffset, newLength), lines = fhunk\\n corpus = b''.join(lines[1:])\\n shunk = list(header)\\n shunk.extend(lines)\\n _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(\\n patch.diffstatdata(util.iterlines(shunk))\\n )\\n pchange.addhunk(\\n phabhunk(\\n oldOffset,\\n oldLength,\\n newOffset,\\n newLength,\\n corpus,\\n addLines,\\n delLines,\\n )\\n )\\n \\n \\n def uploadchunks(fctx, fphid):\\n \\\"\\\"\\\"upload large binary files as separate chunks.\\n Phab requests chunking over 8MiB, and splits into 4MiB chunks\\n \\\"\\\"\\\"\\n ui = fctx.repo().ui\\n chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})\\n with ui.makeprogress(\\n _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)\\n ) as progress:\\n for chunk in chunks:\\n progress.increment()\\n if chunk[b'complete']:\\n continue\\n bstart = int(chunk[b'byteStart'])\\n bend = int(chunk[b'byteEnd'])\\n callconduit(\\n ui,\\n b'file.uploadchunk',\\n {\\n b'filePHID': fphid,\\n b'byteStart': bstart,\\n b'data': base64.b64encode(fctx.data()[bstart:bend]),\\n b'dataEncoding': b'base64',\\n },\\n )\\n \\n \\n def uploadfile(fctx):\\n \\\"\\\"\\\"upload binary files to Phabricator\\\"\\\"\\\"\\n repo = fctx.repo()\\n ui = repo.ui\\n fname = fctx.path()\\n size = fctx.size()\\n fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())\\n \\n # an allocate call is required first to see if an upload is even required\\n # (Phab might already have it) and to determine if chunking is needed\\n allocateparams = {\\n b'name': fname,\\n b'contentLength': size,\\n b'contentHash': fhash,\\n }\\n filealloc = callconduit(ui, b'file.allocate', allocateparams)\\n fphid = filealloc[b'filePHID']\\n \\n if filealloc[b'upload']:\\n ui.write(_(b'uploading %s\\\\n') % bytes(fctx))\\n if not fphid:\\n uploadparams = {\\n b'name': fname,\\n b'data_base64': base64.b64encode(fctx.data()),\\n }\\n fphid = callconduit(ui, b'file.upload', uploadparams)\\n else:\\n uploadchunks(fctx, fphid)\\n else:\\n ui.debug(b'server already has %s\\\\n' % bytes(fctx))\\n \\n if not fphid:\\n raise error.Abort(b'Upload of %s failed.' % bytes(fctx))\\n \\n return fphid\\n \\n \\n def addoldbinary(pchange, fctx):\\n \\\"\\\"\\\"add the metadata for the previous version of a binary file to the\\n phabchange for the new version\\n \\\"\\\"\\\"\\n oldfctx = fctx.p1()\\n if fctx.cmp(oldfctx):\\n # Files differ, add the old one\\n pchange.metadata[b'old:file:size'] = oldfctx.size()\\n mimeguess, _enc = mimetypes.guess_type(\\n encoding.unifromlocal(oldfctx.path())\\n )\\n if mimeguess:\\n pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(\\n mimeguess\\n )\\n fphid = uploadfile(oldfctx)\\n pchange.metadata[b'old:binary-phid'] = fphid\\n else:\\n # If it's left as IMAGE\\/BINARY web UI might try to display it\\n pchange.fileType = DiffFileType.TEXT\\n pchange.copynewmetadatatoold()\\n \\n \\n def makebinary(pchange, fctx):\\n \\\"\\\"\\\"populate the phabchange for a binary file\\\"\\\"\\\"\\n pchange.fileType = DiffFileType.BINARY\\n fphid = uploadfile(fctx)\\n pchange.metadata[b'new:binary-phid'] = fphid\\n pchange.metadata[b'new:file:size'] = fctx.size()\\n mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))\\n if mimeguess:\\n mimeguess = pycompat.bytestr(mimeguess)\\n pchange.metadata[b'new:file:mime-type'] = mimeguess\\n if mimeguess.startswith(b'image\\/'):\\n pchange.fileType = DiffFileType.IMAGE\\n \\n \\n # Copied from mercurial\\/patch.py\\n gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}\\n \\n \\n def notutf8(fctx):\\n \\\"\\\"\\\"detect non-UTF-8 text files since Phabricator requires them to be marked\\n as binary\\n \\\"\\\"\\\"\\n try:\\n fctx.data().decode('utf-8')\\n if fctx.parents():\\n fctx.p1().data().decode('utf-8')\\n return False\\n except UnicodeDecodeError:\\n fctx.repo().ui.write(\\n _(b'file %s detected as non-UTF-8, marked as binary\\\\n')\\n % fctx.path()\\n )\\n return True\\n \\n \\n def addremoved(pdiff, ctx, removed):\\n \\\"\\\"\\\"add removed files to the phabdiff. Shouldn't include moves\\\"\\\"\\\"\\n for fname in removed:\\n pchange = phabchange(\\n currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE\\n )\\n pchange.addoldmode(gitmode[ctx.p1()[fname].flags()])\\n fctx = ctx.p1()[fname]\\n if not (fctx.isbinary() or notutf8(fctx)):\\n maketext(pchange, ctx, fname)\\n \\n pdiff.addchange(pchange)\\n \\n \\n def addmodified(pdiff, ctx, modified):\\n \\\"\\\"\\\"add modified files to the phabdiff\\\"\\\"\\\"\\n for fname in modified:\\n fctx = ctx[fname]\\n pchange = phabchange(currentPath=fname, oldPath=fname)\\n filemode = gitmode[ctx[fname].flags()]\\n originalmode = gitmode[ctx.p1()[fname].flags()]\\n if filemode != originalmode:\\n pchange.addoldmode(originalmode)\\n pchange.addnewmode(filemode)\\n \\n if fctx.isbinary() or notutf8(fctx):\\n makebinary(pchange, fctx)\\n addoldbinary(pchange, fctx)\\n else:\\n maketext(pchange, ctx, fname)\\n \\n pdiff.addchange(pchange)\\n \\n \\n def addadded(pdiff, ctx, added, removed):\\n \\\"\\\"\\\"add file adds to the phabdiff, both new files and copies\\/moves\\\"\\\"\\\"\\n # Keep track of files that've been recorded as moved\\/copied, so if there are\\n # additional copies we can mark them (moves get removed from removed)\\n copiedchanges = {}\\n movedchanges = {}\\n for fname in added:\\n fctx = ctx[fname]\\n pchange = phabchange(currentPath=fname)\\n \\n filemode = gitmode[ctx[fname].flags()]\\n renamed = fctx.renamed()\\n \\n if renamed:\\n originalfname = renamed[0]\\n originalmode = gitmode[ctx.p1()[originalfname].flags()]\\n pchange.oldPath = originalfname\\n \\n if originalfname in removed:\\n origpchange = phabchange(\\n currentPath=originalfname,\\n oldPath=originalfname,\\n type=DiffChangeType.MOVE_AWAY,\\n awayPaths=[fname],\\n )\\n movedchanges[originalfname] = origpchange\\n removed.remove(originalfname)\\n pchange.type = DiffChangeType.MOVE_HERE\\n elif originalfname in movedchanges:\\n movedchanges[originalfname].type = DiffChangeType.MULTICOPY\\n movedchanges[originalfname].awayPaths.append(fname)\\n pchange.type = DiffChangeType.COPY_HERE\\n else: # pure copy\\n if originalfname not in copiedchanges:\\n origpchange = phabchange(\\n currentPath=originalfname, type=DiffChangeType.COPY_AWAY\\n )\\n copiedchanges[originalfname] = origpchange\\n else:\\n origpchange = copiedchanges[originalfname]\\n origpchange.awayPaths.append(fname)\\n pchange.type = DiffChangeType.COPY_HERE\\n \\n if filemode != originalmode:\\n pchange.addoldmode(originalmode)\\n pchange.addnewmode(filemode)\\n else: # Brand-new file\\n pchange.addnewmode(gitmode[fctx.flags()])\\n pchange.type = DiffChangeType.ADD\\n \\n if fctx.isbinary() or notutf8(fctx):\\n makebinary(pchange, fctx)\\n if renamed:\\n addoldbinary(pchange, fctx)\\n else:\\n maketext(pchange, ctx, fname)\\n \\n pdiff.addchange(pchange)\\n \\n for _path, copiedchange in copiedchanges.items():\\n pdiff.addchange(copiedchange)\\n for _path, movedchange in movedchanges.items():\\n pdiff.addchange(movedchange)\\n \\n \\n def creatediff(ctx):\\n \\\"\\\"\\\"create a Differential Diff\\\"\\\"\\\"\\n repo = ctx.repo()\\n repophid = getrepophid(repo)\\n # Create a \\\"Differential Diff\\\" via \\\"differential.creatediff\\\" API\\n pdiff = phabdiff(\\n sourceControlBaseRevision=b'%s' % ctx.p1().hex(),\\n branch=b'%s' % ctx.branch(),\\n )\\n modified, added, removed, _d, _u, _i, _c = ctx.p1().status(ctx)\\n # addadded will remove moved files from removed, so addremoved won't get\\n # them\\n addadded(pdiff, ctx, added, removed)\\n addmodified(pdiff, ctx, modified)\\n addremoved(pdiff, ctx, removed)\\n if repophid:\\n pdiff.repositoryPHID = repophid\\n diff = callconduit(\\n repo.ui,\\n b'differential.creatediff',\\n pycompat.byteskwargs(attr.asdict(pdiff)),\\n )\\n if not diff:\\n raise error.Abort(_(b'cannot create diff for %s') % ctx)\\n return diff\\n \\n \\n def writediffproperties(ctx, diff):\\n \\\"\\\"\\\"write metadata to diff so patches could be applied losslessly\\\"\\\"\\\"\\n # creatediff returns with a diffid but query returns with an id\\n diffid = diff.get(b'diffid', diff.get(b'id'))\\n params = {\\n b'diff_id': diffid,\\n b'name': b'hg:meta',\\n b'data': templatefilters.json(\\n {\\n b'user': ctx.user(),\\n b'date': b'%d %d' % ctx.date(),\\n b'branch': ctx.branch(),\\n b'node': ctx.hex(),\\n b'parent': ctx.p1().hex(),\\n }\\n ),\\n }\\n callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)\\n \\n params = {\\n b'diff_id': diffid,\\n b'name': b'local:commits',\\n b'data': templatefilters.json(\\n {\\n ctx.hex(): {\\n b'author': stringutil.person(ctx.user()),\\n b'authorEmail': stringutil.email(ctx.user()),\\n b'time': int(ctx.date()[0]),\\n b'commit': ctx.hex(),\\n b'parents': [ctx.p1().hex()],\\n b'branch': ctx.branch(),\\n },\\n }\\n ),\\n }\\n callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)\\n \\n \\n def createdifferentialrevision(\\n ctx,\\n revid=None,\\n parentrevphid=None,\\n oldnode=None,\\n olddiff=None,\\n actions=None,\\n comment=None,\\n ):\\n \\\"\\\"\\\"create or update a Differential Revision\\n \\n If revid is None, create a new Differential Revision, otherwise update\\n revid. If parentrevphid is not None, set it as a dependency.\\n \\n If oldnode is not None, check if the patch content (without commit message\\n and metadata) has changed before creating another diff.\\n \\n If actions is not None, they will be appended to the transaction.\\n \\\"\\\"\\\"\\n repo = ctx.repo()\\n if oldnode:\\n diffopts = mdiff.diffopts(git=True, context=32767)\\n oldctx = repo.unfiltered()[oldnode]\\n neednewdiff = getdiff(ctx, diffopts) != getdiff(oldctx, diffopts)\\n else:\\n neednewdiff = True\\n \\n transactions = []\\n if neednewdiff:\\n diff = creatediff(ctx)\\n transactions.append({b'type': b'update', b'value': diff[b'phid']})\\n if comment:\\n transactions.append({b'type': b'comment', b'value': comment})\\n else:\\n # Even if we don't need to upload a new diff because the patch content\\n # does not change. We might still need to update its metadata so\\n # pushers could know the correct node metadata.\\n assert olddiff\\n diff = olddiff\\n writediffproperties(ctx, diff)\\n \\n # Set the parent Revision every time, so commit re-ordering is picked-up\\n if parentrevphid:\\n transactions.append(\\n {b'type': b'parents.set', b'value': [parentrevphid]}\\n )\\n \\n if actions:\\n transactions += actions\\n \\n # Parse commit message and update related fields.\\n desc = ctx.description()\\n info = callconduit(\\n repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}\\n )\\n for k, v in info[b'fields'].items():\\n if k in [b'title', b'summary', b'testPlan']:\\n transactions.append({b'type': k, b'value': v})\\n \\n params = {b'transactions': transactions}\\n if revid is not None:\\n # Update an existing Differential Revision\\n params[b'objectIdentifier'] = revid\\n \\n revision = callconduit(repo.ui, b'differential.revision.edit', params)\\n if not revision:\\n raise error.Abort(_(b'cannot create revision for %s') % ctx)\\n \\n return revision, diff\\n \\n \\n def userphids(repo, names):\\n \\\"\\\"\\\"convert user names to PHIDs\\\"\\\"\\\"\\n names = [name.lower() for name in names]\\n query = {b'constraints': {b'usernames': names}}\\n result = callconduit(repo.ui, b'user.search', query)\\n # username not found is not an error of the API. So check if we have missed\\n # some names here.\\n data = result[b'data']\\n resolved = set(entry[b'fields'][b'username'].lower() for entry in data)\\n unresolved = set(names) - resolved\\n if unresolved:\\n raise error.Abort(\\n _(b'unknown username: %s') % b' '.join(sorted(unresolved))\\n )\\n return [entry[b'phid'] for entry in data]\\n \\n \\n @vcrcommand(\\n b'phabsend',\\n [\\n (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),\\n (b'', b'amend', True, _(b'update commit messages')),\\n (b'', b'reviewer', [], _(b'specify reviewers')),\\n (b'', b'blocker', [], _(b'specify blocking reviewers')),\\n (\\n b'm',\\n b'comment',\\n b'',\\n _(b'add a comment to Revisions with new\\/updated Diffs'),\\n ),\\n (b'', b'confirm', None, _(b'ask for confirmation before sending')),\\n ],\\n _(b'REV [OPTIONS]'),\\n helpcategory=command.CATEGORY_IMPORT_EXPORT,\\n )\\n def phabsend(ui, repo, *revs, **opts):\\n \\\"\\\"\\\"upload changesets to Phabricator\\n \\n If there are multiple revisions specified, they will be send as a stack\\n with a linear dependencies relationship using the order specified by the\\n revset.\\n \\n For the first time uploading changesets, local tags will be created to\\n maintain the association. After the first time, phabsend will check\\n obsstore and tags information so it can figure out whether to update an\\n existing Differential Revision, or create a new one.\\n \\n If --amend is set, update commit messages so they have the\\n ``Differential Revision`` URL, remove related tags. This is similar to what\\n arcanist will do, and is more desired in author-push workflows. Otherwise,\\n use local tags to record the ``Differential Revision`` association.\\n \\n The --confirm option lets you confirm changesets before sending them. You\\n can also add following to your configuration file to make it default\\n behaviour::\\n \\n [phabsend]\\n confirm = true\\n \\n phabsend will check obsstore and the above association to decide whether to\\n update an existing Differential Revision, or create a new one.\\n \\\"\\\"\\\"\\n opts = pycompat.byteskwargs(opts)\\n revs = list(revs) + opts.get(b'rev', [])\\n revs = scmutil.revrange(repo, revs)\\n revs.sort() # ascending order to preserve topological parent\\/child in phab\\n \\n if not revs:\\n raise error.Abort(_(b'phabsend requires at least one changeset'))\\n if opts.get(b'amend'):\\n cmdutil.checkunfinished(repo)\\n \\n # {newnode: (oldnode, olddiff, olddrev}\\n oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])\\n \\n confirm = ui.configbool(b'phabsend', b'confirm')\\n confirm |= bool(opts.get(b'confirm'))\\n if confirm:\\n confirmed = _confirmbeforesend(repo, revs, oldmap)\\n if not confirmed:\\n raise error.Abort(_(b'phabsend cancelled'))\\n \\n actions = []\\n reviewers = opts.get(b'reviewer', [])\\n blockers = opts.get(b'blocker', [])\\n phids = []\\n if reviewers:\\n phids.extend(userphids(repo, reviewers))\\n if blockers:\\n phids.extend(\\n map(lambda phid: b'blocking(%s)' % phid, userphids(repo, blockers))\\n )\\n if phids:\\n actions.append({b'type': b'reviewers.add', b'value': phids})\\n \\n drevids = [] # [int]\\n diffmap = {} # {newnode: diff}\\n \\n # Send patches one by one so we know their Differential Revision PHIDs and\\n # can provide dependency relationship\\n lastrevphid = None\\n for rev in revs:\\n ui.debug(b'sending rev %d\\\\n' % rev)\\n ctx = repo[rev]\\n \\n # Get Differential Revision ID\\n oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))\\n if oldnode != ctx.node() or opts.get(b'amend'):\\n # Create or update Differential Revision\\n revision, diff = createdifferentialrevision(\\n ctx,\\n revid,\\n lastrevphid,\\n oldnode,\\n olddiff,\\n actions,\\n opts.get(b'comment'),\\n )\\n diffmap[ctx.node()] = diff\\n newrevid = int(revision[b'object'][b'id'])\\n newrevphid = revision[b'object'][b'phid']\\n if revid:\\n action = b'updated'\\n else:\\n action = b'created'\\n \\n # Create a local tag to note the association, if commit message\\n # does not have it already\\n m = _differentialrevisiondescre.search(ctx.description())\\n if not m or int(m.group('id')) != newrevid:\\n tagname = b'D%d' % newrevid\\n tags.tag(\\n repo,\\n tagname,\\n ctx.node(),\\n message=None,\\n user=None,\\n date=None,\\n local=True,\\n )\\n else:\\n # Nothing changed. But still set \\\"newrevphid\\\" so the next revision\\n # could depend on this one and \\\"newrevid\\\" for the summary line.\\n- newrevphid = querydrev(repo, b'%d' % revid)[0][b'phid']\\n+ newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']\\n newrevid = revid\\n action = b'skipped'\\n \\n actiondesc = ui.label(\\n {\\n b'created': _(b'created'),\\n b'skipped': _(b'skipped'),\\n b'updated': _(b'updated'),\\n }[action],\\n b'phabricator.action.%s' % action,\\n )\\n drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')\\n nodedesc = ui.label(bytes(ctx), b'phabricator.node')\\n desc = ui.label(ctx.description().split(b'\\\\n')[0], b'phabricator.desc')\\n ui.write(\\n _(b'%s - %s - %s: %s\\\\n') % (drevdesc, actiondesc, nodedesc, desc)\\n )\\n drevids.append(newrevid)\\n lastrevphid = newrevphid\\n \\n # Update commit messages and remove tags\\n if opts.get(b'amend'):\\n unfi = repo.unfiltered()\\n drevs = callconduit(ui, b'differential.query', {b'ids': drevids})\\n with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):\\n wnode = unfi[b'.'].node()\\n mapping = {} # {oldnode: [newnode]}\\n for i, rev in enumerate(revs):\\n old = unfi[rev]\\n drevid = drevids[i]\\n drev = [d for d in drevs if int(d[b'id']) == drevid][0]\\n newdesc = getdescfromdrev(drev)\\n # Make sure commit message contain \\\"Differential Revision\\\"\\n if old.description() != newdesc:\\n if old.phase() == phases.public:\\n ui.warn(\\n _(b\\\"warning: not updating public commit %s\\\\n\\\")\\n % scmutil.formatchangeid(old)\\n )\\n continue\\n parents = [\\n mapping.get(old.p1().node(), (old.p1(),))[0],\\n mapping.get(old.p2().node(), (old.p2(),))[0],\\n ]\\n new = context.metadataonlyctx(\\n repo,\\n old,\\n parents=parents,\\n text=newdesc,\\n user=old.user(),\\n date=old.date(),\\n extra=old.extra(),\\n )\\n \\n newnode = new.commit()\\n \\n mapping[old.node()] = [newnode]\\n # Update diff property\\n # If it fails just warn and keep going, otherwise the DREV\\n # associations will be lost\\n try:\\n writediffproperties(unfi[newnode], diffmap[old.node()])\\n except util.urlerr.urlerror:\\n ui.warnnoi18n(\\n b'Failed to update metadata for D%d\\\\n' % drevid\\n )\\n # Remove local tags since it's no longer necessary\\n tagname = b'D%d' % drevid\\n if tagname in repo.tags():\\n tags.tag(\\n repo,\\n tagname,\\n nullid,\\n message=None,\\n user=None,\\n date=None,\\n local=True,\\n )\\n scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)\\n if wnode in mapping:\\n unfi.setparents(mapping[wnode][0])\\n \\n \\n # Map from \\\"hg:meta\\\" keys to header understood by \\\"hg import\\\". The order is\\n # consistent with \\\"hg export\\\" output.\\n _metanamemap = util.sortdict(\\n [\\n (b'user', b'User'),\\n (b'date', b'Date'),\\n (b'branch', b'Branch'),\\n (b'node', b'Node ID'),\\n (b'parent', b'Parent '),\\n ]\\n )\\n \\n \\n def _confirmbeforesend(repo, revs, oldmap):\\n url, token = readurltoken(repo.ui)\\n ui = repo.ui\\n for rev in revs:\\n ctx = repo[rev]\\n desc = ctx.description().splitlines()[0]\\n oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))\\n if drevid:\\n drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')\\n else:\\n drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')\\n \\n ui.write(\\n _(b'%s - %s: %s\\\\n')\\n % (\\n drevdesc,\\n ui.label(bytes(ctx), b'phabricator.node'),\\n ui.label(desc, b'phabricator.desc'),\\n )\\n )\\n \\n if ui.promptchoice(\\n _(b'Send the above changes to %s (yn)?$$ &Yes $$ &No') % url\\n ):\\n return False\\n \\n return True\\n \\n \\n _knownstatusnames = {\\n b'accepted',\\n b'needsreview',\\n b'needsrevision',\\n b'closed',\\n b'abandoned',\\n b'changesplanned',\\n }\\n \\n \\n def _getstatusname(drev):\\n \\\"\\\"\\\"get normalized status name from a Differential Revision\\\"\\\"\\\"\\n return drev[b'statusName'].replace(b' ', b'').lower()\\n \\n \\n # Small language to specify differential revisions. Support symbols: (), :X,\\n # +, and -.\\n \\n _elements = {\\n # token-type: binding-strength, primary, prefix, infix, suffix\\n b'(': (12, None, (b'group', 1, b')'), None, None),\\n b':': (8, None, (b'ancestors', 8), None, None),\\n b'&': (5, None, None, (b'and_', 5), None),\\n b'+': (4, None, None, (b'add', 4), None),\\n b'-': (4, None, None, (b'sub', 4), None),\\n b')': (0, None, None, None, None),\\n b'symbol': (0, b'symbol', None, None, None),\\n b'end': (0, None, None, None, None),\\n }\\n \\n \\n def _tokenize(text):\\n view = memoryview(text) # zero-copy slice\\n special = b'():+-& '\\n pos = 0\\n length = len(text)\\n while pos \\u003c length:\\n symbol = b''.join(\\n itertools.takewhile(\\n lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])\\n )\\n )\\n if symbol:\\n yield (b'symbol', symbol, pos)\\n pos += len(symbol)\\n else: # special char, ignore space\\n if text[pos : pos + 1] != b' ':\\n yield (text[pos : pos + 1], None, pos)\\n pos += 1\\n yield (b'end', None, pos)\\n \\n \\n def _parse(text):\\n tree, pos = parser.parser(_elements).parse(_tokenize(text))\\n if pos != len(text):\\n raise error.ParseError(b'invalid token', pos)\\n return tree\\n \\n \\n def _parsedrev(symbol):\\n \\\"\\\"\\\"str -\\u003e int or None, ex. 'D45' -\\u003e 45; '12' -\\u003e 12; 'x' -\\u003e None\\\"\\\"\\\"\\n if symbol.startswith(b'D') and symbol[1:].isdigit():\\n return int(symbol[1:])\\n if symbol.isdigit():\\n return int(symbol)\\n \\n \\n def _prefetchdrevs(tree):\\n \\\"\\\"\\\"return ({single-drev-id}, {ancestor-drev-id}) to prefetch\\\"\\\"\\\"\\n drevs = set()\\n ancestordrevs = set()\\n op = tree[0]\\n if op == b'symbol':\\n r = _parsedrev(tree[1])\\n if r:\\n drevs.add(r)\\n elif op == b'ancestors':\\n r, a = _prefetchdrevs(tree[1])\\n drevs.update(r)\\n ancestordrevs.update(r)\\n ancestordrevs.update(a)\\n else:\\n for t in tree[1:]:\\n r, a = _prefetchdrevs(t)\\n drevs.update(r)\\n ancestordrevs.update(a)\\n return drevs, ancestordrevs\\n \\n \\n-def querydrev(repo, spec):\\n+def querydrev(ui, spec):\\n \\\"\\\"\\\"return a list of \\\"Differential Revision\\\" dicts\\n \\n spec is a string using a simple query language, see docstring in phabread\\n for details.\\n \\n A \\\"Differential Revision dict\\\" looks like:\\n \\n {\\n \\\"activeDiffPHID\\\": \\\"PHID-DIFF-xoqnjkobbm6k4dk6hi72\\\",\\n \\\"authorPHID\\\": \\\"PHID-USER-tv3ohwc4v4jeu34otlye\\\",\\n \\\"auxiliary\\\": {\\n \\\"phabricator:depends-on\\\": [\\n \\\"PHID-DREV-gbapp366kutjebt7agcd\\\"\\n ]\\n \\\"phabricator:projects\\\": [],\\n },\\n \\\"branch\\\": \\\"default\\\",\\n \\\"ccs\\\": [],\\n \\\"commits\\\": [],\\n \\\"dateCreated\\\": \\\"1499181406\\\",\\n \\\"dateModified\\\": \\\"1499182103\\\",\\n \\\"diffs\\\": [\\n \\\"3\\\",\\n \\\"4\\\",\\n ],\\n \\\"hashes\\\": [],\\n \\\"id\\\": \\\"2\\\",\\n \\\"lineCount\\\": \\\"2\\\",\\n \\\"phid\\\": \\\"PHID-DREV-672qvysjcczopag46qty\\\",\\n \\\"properties\\\": {},\\n \\\"repositoryPHID\\\": \\\"PHID-REPO-hub2hx62ieuqeheznasv\\\",\\n \\\"reviewers\\\": [],\\n \\\"sourcePath\\\": null\\n \\\"status\\\": \\\"0\\\",\\n \\\"statusName\\\": \\\"Needs Review\\\",\\n \\\"summary\\\": \\\"\\\",\\n \\\"testPlan\\\": \\\"\\\",\\n \\\"title\\\": \\\"example\\\",\\n \\\"uri\\\": \\\"https:\\/\\/phab.example.com\\/D2\\\",\\n }\\n \\\"\\\"\\\"\\n # TODO: replace differential.query and differential.querydiffs with\\n # differential.diff.search because the former (and their output) are\\n # frozen, and planned to be deprecated and removed.\\n \\n def fetch(params):\\n \\\"\\\"\\\"params -\\u003e single drev or None\\\"\\\"\\\"\\n key = (params.get(b'ids') or params.get(b'phids') or [None])[0]\\n if key in prefetched:\\n return prefetched[key]\\n- drevs = callconduit(repo.ui, b'differential.query', params)\\n+ drevs = callconduit(ui, b'differential.query', params)\\n # Fill prefetched with the result\\n for drev in drevs:\\n prefetched[drev[b'phid']] = drev\\n prefetched[int(drev[b'id'])] = drev\\n if key not in prefetched:\\n raise error.Abort(\\n _(b'cannot get Differential Revision %r') % params\\n )\\n return prefetched[key]\\n \\n def getstack(topdrevids):\\n \\\"\\\"\\\"given a top, get a stack from the bottom, [id] -\\u003e [id]\\\"\\\"\\\"\\n visited = set()\\n result = []\\n queue = [{b'ids': [i]} for i in topdrevids]\\n while queue:\\n params = queue.pop()\\n drev = fetch(params)\\n if drev[b'id'] in visited:\\n continue\\n visited.add(drev[b'id'])\\n result.append(int(drev[b'id']))\\n auxiliary = drev.get(b'auxiliary', {})\\n depends = auxiliary.get(b'phabricator:depends-on', [])\\n for phid in depends:\\n queue.append({b'phids': [phid]})\\n result.reverse()\\n return smartset.baseset(result)\\n \\n # Initialize prefetch cache\\n prefetched = {} # {id or phid: drev}\\n \\n tree = _parse(spec)\\n drevs, ancestordrevs = _prefetchdrevs(tree)\\n \\n # developer config: phabricator.batchsize\\n- batchsize = repo.ui.configint(b'phabricator', b'batchsize')\\n+ batchsize = ui.configint(b'phabricator', b'batchsize')\\n \\n # Prefetch Differential Revisions in batch\\n tofetch = set(drevs)\\n for r in ancestordrevs:\\n tofetch.update(range(max(1, r - batchsize), r + 1))\\n if drevs:\\n fetch({b'ids': list(tofetch)})\\n validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))\\n \\n # Walk through the tree, return smartsets\\n def walk(tree):\\n op = tree[0]\\n if op == b'symbol':\\n drev = _parsedrev(tree[1])\\n if drev:\\n return smartset.baseset([drev])\\n elif tree[1] in _knownstatusnames:\\n drevs = [\\n r\\n for r in validids\\n if _getstatusname(prefetched[r]) == tree[1]\\n ]\\n return smartset.baseset(drevs)\\n else:\\n raise error.Abort(_(b'unknown symbol: %s') % tree[1])\\n elif op in {b'and_', b'add', b'sub'}:\\n assert len(tree) == 3\\n return getattr(operator, op)(walk(tree[1]), walk(tree[2]))\\n elif op == b'group':\\n return walk(tree[1])\\n elif op == b'ancestors':\\n return getstack(walk(tree[1]))\\n else:\\n raise error.ProgrammingError(b'illegal tree: %r' % tree)\\n \\n return [prefetched[r] for r in walk(tree)]\\n \\n \\n def getdescfromdrev(drev):\\n \\\"\\\"\\\"get description (commit message) from \\\"Differential Revision\\\"\\n \\n This is similar to differential.getcommitmessage API. But we only care\\n about limited fields: title, summary, test plan, and URL.\\n \\\"\\\"\\\"\\n title = drev[b'title']\\n summary = drev[b'summary'].rstrip()\\n testplan = drev[b'testPlan'].rstrip()\\n if testplan:\\n testplan = b'Test Plan:\\\\n%s' % testplan\\n uri = b'Differential Revision: %s' % drev[b'uri']\\n return b'\\\\n\\\\n'.join(filter(None, [title, summary, testplan, uri]))\\n \\n \\n def getdiffmeta(diff):\\n \\\"\\\"\\\"get commit metadata (date, node, user, p1) from a diff object\\n \\n The metadata could be \\\"hg:meta\\\", sent by phabsend, like:\\n \\n \\\"properties\\\": {\\n \\\"hg:meta\\\": {\\n \\\"branch\\\": \\\"default\\\",\\n \\\"date\\\": \\\"1499571514 25200\\\",\\n \\\"node\\\": \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\",\\n \\\"user\\\": \\\"Foo Bar \\u003cfoo@example.com\\u003e\\\",\\n \\\"parent\\\": \\\"6d0abad76b30e4724a37ab8721d630394070fe16\\\"\\n }\\n }\\n \\n Or converted from \\\"local:commits\\\", sent by \\\"arc\\\", like:\\n \\n \\\"properties\\\": {\\n \\\"local:commits\\\": {\\n \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\": {\\n \\\"author\\\": \\\"Foo Bar\\\",\\n \\\"authorEmail\\\": \\\"foo@example.com\\\"\\n \\\"branch\\\": \\\"default\\\",\\n \\\"commit\\\": \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\",\\n \\\"local\\\": \\\"1000\\\",\\n \\\"message\\\": \\\"...\\\",\\n \\\"parents\\\": [\\\"6d0abad76b30e4724a37ab8721d630394070fe16\\\"],\\n \\\"rev\\\": \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\",\\n \\\"summary\\\": \\\"...\\\",\\n \\\"tag\\\": \\\"\\\",\\n \\\"time\\\": 1499546314,\\n }\\n }\\n }\\n \\n Note: metadata extracted from \\\"local:commits\\\" will lose time zone\\n information.\\n \\\"\\\"\\\"\\n props = diff.get(b'properties') or {}\\n meta = props.get(b'hg:meta')\\n if not meta:\\n if props.get(b'local:commits'):\\n commit = sorted(props[b'local:commits'].values())[0]\\n meta = {}\\n if b'author' in commit and b'authorEmail' in commit:\\n meta[b'user'] = b'%s \\u003c%s\\u003e' % (\\n commit[b'author'],\\n commit[b'authorEmail'],\\n )\\n if b'time' in commit:\\n meta[b'date'] = b'%d 0' % int(commit[b'time'])\\n if b'branch' in commit:\\n meta[b'branch'] = commit[b'branch']\\n node = commit.get(b'commit', commit.get(b'rev'))\\n if node:\\n meta[b'node'] = node\\n if len(commit.get(b'parents', ())) \\u003e= 1:\\n meta[b'parent'] = commit[b'parents'][0]\\n else:\\n meta = {}\\n if b'date' not in meta and b'dateCreated' in diff:\\n meta[b'date'] = b'%s 0' % diff[b'dateCreated']\\n if b'branch' not in meta and diff.get(b'branch'):\\n meta[b'branch'] = diff[b'branch']\\n if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):\\n meta[b'parent'] = diff[b'sourceControlBaseRevision']\\n return meta\\n \\n \\n def readpatch(ui, drevs, write):\\n \\\"\\\"\\\"generate plain-text patch readable by 'hg import'\\n \\n write is usually ui.write. drevs is what \\\"querydrev\\\" returns, results of\\n \\\"differential.query\\\".\\n \\\"\\\"\\\"\\n # Prefetch hg:meta property for all diffs\\n diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))\\n diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})\\n \\n # Generate patch for each drev\\n for drev in drevs:\\n ui.note(_(b'reading D%s\\\\n') % drev[b'id'])\\n \\n diffid = max(int(v) for v in drev[b'diffs'])\\n body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})\\n desc = getdescfromdrev(drev)\\n header = b'# HG changeset patch\\\\n'\\n \\n # Try to preserve metadata from hg:meta property. Write hg patch\\n # headers that can be read by the \\\"import\\\" command. See patchheadermap\\n # and extract in mercurial\\/patch.py for supported headers.\\n meta = getdiffmeta(diffs[b'%d' % diffid])\\n for k in _metanamemap.keys():\\n if k in meta:\\n header += b'# %s %s\\\\n' % (_metanamemap[k], meta[k])\\n \\n content = b'%s%s\\\\n%s' % (header, desc, body)\\n write(content)\\n \\n \\n @vcrcommand(\\n b'phabread',\\n [(b'', b'stack', False, _(b'read dependencies'))],\\n _(b'DREVSPEC [OPTIONS]'),\\n helpcategory=command.CATEGORY_IMPORT_EXPORT,\\n )\\n def phabread(ui, repo, spec, **opts):\\n \\\"\\\"\\\"print patches from Phabricator suitable for importing\\n \\n DREVSPEC could be a Differential Revision identity, like ``D123``, or just\\n the number ``123``. It could also have common operators like ``+``, ``-``,\\n ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to\\n select a stack.\\n \\n ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``\\n could be used to filter patches by status. For performance reason, they\\n only represent a subset of non-status selections and cannot be used alone.\\n \\n For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude\\n D2 and D4. ``:D9 & needsreview`` selects \\\"Needs Review\\\" revisions in a\\n stack up to D9.\\n \\n If --stack is given, follow dependencies information and read all patches.\\n It is equivalent to the ``:`` operator.\\n \\\"\\\"\\\"\\n opts = pycompat.byteskwargs(opts)\\n if opts.get(b'stack'):\\n spec = b':(%s)' % spec\\n- drevs = querydrev(repo, spec)\\n+ drevs = querydrev(repo.ui, spec)\\n readpatch(repo.ui, drevs, ui.write)\\n \\n \\n @vcrcommand(\\n b'phabupdate',\\n [\\n (b'', b'accept', False, _(b'accept revisions')),\\n (b'', b'reject', False, _(b'reject revisions')),\\n (b'', b'abandon', False, _(b'abandon revisions')),\\n (b'', b'reclaim', False, _(b'reclaim revisions')),\\n (b'm', b'comment', b'', _(b'comment on the last revision')),\\n ],\\n _(b'DREVSPEC [OPTIONS]'),\\n helpcategory=command.CATEGORY_IMPORT_EXPORT,\\n )\\n def phabupdate(ui, repo, spec, **opts):\\n \\\"\\\"\\\"update Differential Revision in batch\\n \\n DREVSPEC selects revisions. See :hg:`help phabread` for its usage.\\n \\\"\\\"\\\"\\n opts = pycompat.byteskwargs(opts)\\n flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]\\n if len(flags) \\u003e 1:\\n raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))\\n \\n actions = []\\n for f in flags:\\n actions.append({b'type': f, b'value': True})\\n \\n- drevs = querydrev(repo, spec)\\n+ drevs = querydrev(repo.ui, spec)\\n for i, drev in enumerate(drevs):\\n if i + 1 == len(drevs) and opts.get(b'comment'):\\n actions.append({b'type': b'comment', b'value': opts[b'comment']})\\n if actions:\\n params = {\\n b'objectIdentifier': drev[b'phid'],\\n b'transactions': actions,\\n }\\n callconduit(ui, b'differential.revision.edit', params)\\n \\n \\n @eh.templatekeyword(b'phabreview', requires={b'ctx'})\\n def template_review(context, mapping):\\n \\\"\\\"\\\":phabreview: Object describing the review for this changeset.\\n Has attributes `url` and `id`.\\n \\\"\\\"\\\"\\n ctx = context.resource(mapping, b'ctx')\\n m = _differentialrevisiondescre.search(ctx.description())\\n if m:\\n return templateutil.hybriddict(\\n {b'url': m.group('url'), b'id': b\\\"D%s\\\" % m.group('id'),}\\n )\\n else:\\n tags = ctx.repo().nodetags(ctx.node())\\n for t in tags:\\n if _differentialrevisiontagre.match(t):\\n url = ctx.repo().ui.config(b'phabricator', b'url')\\n if not url.endswith(b'\\/'):\\n url += b'\\/'\\n url += t\\n \\n return templateutil.hybriddict({b'url': url, b'id': t,})\\n return None\\n \\n \\n @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})\\n def template_status(context, mapping):\\n \\\"\\\"\\\":phabstatus: String. Status of Phabricator differential.\\n \\\"\\\"\\\"\\n ctx = context.resource(mapping, b'ctx')\\n repo = context.resource(mapping, b'repo')\\n ui = context.resource(mapping, b'ui')\\n \\n rev = ctx.rev()\\n try:\\n drevid = getdrevmap(repo, [rev])[rev]\\n except KeyError:\\n return None\\n drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})\\n for drev in drevs:\\n if int(drev[b'id']) == drevid:\\n return templateutil.hybriddict(\\n {b'url': drev[b'uri'], b'status': drev[b'statusName'],}\\n )\\n return None\\n \\n \\n @show.showview(b'phabstatus', csettopic=b'work')\\n def phabstatusshowview(ui, repo, displayer):\\n \\\"\\\"\\\"Phabricator differiential status\\\"\\\"\\\"\\n revs = repo.revs('sort(_underway(), topo)')\\n drevmap = getdrevmap(repo, revs)\\n unknownrevs, drevids, revsbydrevid = [], set([]), {}\\n for rev, drevid in pycompat.iteritems(drevmap):\\n if drevid is not None:\\n drevids.add(drevid)\\n revsbydrevid.setdefault(drevid, set([])).add(rev)\\n else:\\n unknownrevs.append(rev)\\n \\n drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})\\n drevsbyrev = {}\\n for drev in drevs:\\n for rev in revsbydrevid[int(drev[b'id'])]:\\n drevsbyrev[rev] = drev\\n \\n def phabstatus(ctx):\\n drev = drevsbyrev[ctx.rev()]\\n status = ui.label(\\n b'%(statusName)s' % drev,\\n b'phabricator.status.%s' % _getstatusname(drev),\\n )\\n ui.write(b\\\"\\\\n%s %s\\\\n\\\" % (drev[b'uri'], status))\\n \\n revs -= smartset.baseset(unknownrevs)\\n revdag = graphmod.dagwalker(repo, revs)\\n \\n ui.setconfig(b'experimental', b'graphshorten', True)\\n displayer._exthook = phabstatus\\n nodelen = show.longestshortest(repo, revs)\\n logcmdutil.displaygraph(\\n ui,\\n repo,\\n revdag,\\n displayer,\\n graphmod.asciiedges,\\n props={b'nodelen': nodelen},\\n )\\n\"}]}],\"properties\":[]},\"20441\":{\"id\":\"20441\",\"revisionID\":\"8205\",\"dateCreated\":\"1581962476\",\"dateModified\":\"1583327820\",\"sourceControlBaseRevision\":\"69392460f7b1adf37a2b1206af8a5eb8ca0828f1\",\"sourceControlPath\":null,\"sourceControlSystem\":\"hg\",\"branch\":null,\"bookmark\":null,\"creationMethod\":\"commit\",\"description\":\"rHG72c6190de577bb2bb448eb2b14121e4ef85d08ff\",\"unitStatus\":\"6\",\"lintStatus\":\"6\",\"changes\":[{\"id\":\"55596\",\"metadata\":{\"line:first\":1612,\"hash.effect\":\"8g80k6H5tv6i\"},\"oldPath\":\"hgext\\/phabricator.py\",\"currentPath\":\"hgext\\/phabricator.py\",\"awayPaths\":[],\"oldProperties\":[],\"newProperties\":[],\"type\":\"2\",\"fileType\":\"1\",\"commitHash\":null,\"addLines\":\"5\",\"delLines\":\"7\",\"hunks\":[{\"oldOffset\":\"1\",\"newOffset\":\"1\",\"oldLength\":\"1801\",\"newLength\":\"1799\",\"addLines\":null,\"delLines\":null,\"isMissingOldNewline\":null,\"isMissingNewNewline\":null,\"corpus\":\" # phabricator.py - simple Phabricator integration\\n #\\n # Copyright 2017 Facebook, Inc.\\n #\\n # This software may be used and distributed according to the terms of the\\n # GNU General Public License version 2 or any later version.\\n \\\"\\\"\\\"simple Phabricator integration (EXPERIMENTAL)\\n \\n This extension provides a ``phabsend`` command which sends a stack of\\n changesets to Phabricator, and a ``phabread`` command which prints a stack of\\n revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command\\n to update statuses in batch.\\n \\n A \\\"phabstatus\\\" view for :hg:`show` is also provided; it displays status\\n information of Phabricator differentials associated with unfinished\\n changesets.\\n \\n By default, Phabricator requires ``Test Plan`` which might prevent some\\n changeset from being sent. The requirement could be disabled by changing\\n ``differential.require-test-plan-field`` config server side.\\n \\n Config::\\n \\n [phabricator]\\n # Phabricator URL\\n url = https:\\/\\/phab.example.com\\/\\n \\n # Repo callsign. If a repo has a URL https:\\/\\/$HOST\\/diffusion\\/FOO, then its\\n # callsign is \\\"FOO\\\".\\n callsign = FOO\\n \\n # curl command to use. If not set (default), use builtin HTTP library to\\n # communicate. If set, use the specified curl command. This could be useful\\n # if you need to specify advanced options that is not easily supported by\\n # the internal library.\\n curlcmd = curl --connect-timeout 2 --retry 3 --silent\\n \\n [auth]\\n example.schemes = https\\n example.prefix = phab.example.com\\n \\n # API token. Get it from https:\\/\\/$HOST\\/conduit\\/login\\/\\n example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx\\n \\\"\\\"\\\"\\n \\n from __future__ import absolute_import\\n \\n import base64\\n import contextlib\\n import hashlib\\n import itertools\\n import json\\n import mimetypes\\n import operator\\n import re\\n \\n from mercurial.node import bin, nullid\\n from mercurial.i18n import _\\n from mercurial.pycompat import getattr\\n from mercurial.thirdparty import attr\\n from mercurial import (\\n cmdutil,\\n context,\\n encoding,\\n error,\\n exthelper,\\n graphmod,\\n httpconnection as httpconnectionmod,\\n localrepo,\\n logcmdutil,\\n match,\\n mdiff,\\n obsutil,\\n parser,\\n patch,\\n phases,\\n pycompat,\\n scmutil,\\n smartset,\\n tags,\\n templatefilters,\\n templateutil,\\n url as urlmod,\\n util,\\n )\\n from mercurial.utils import (\\n procutil,\\n stringutil,\\n )\\n from . import show\\n \\n \\n # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for\\n # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should\\n # be specifying the version(s) of Mercurial they are tested with, or\\n # leave the attribute unspecified.\\n testedwith = b'ships-with-hg-core'\\n \\n eh = exthelper.exthelper()\\n \\n cmdtable = eh.cmdtable\\n command = eh.command\\n configtable = eh.configtable\\n templatekeyword = eh.templatekeyword\\n uisetup = eh.finaluisetup\\n \\n # developer config: phabricator.batchsize\\n eh.configitem(\\n b'phabricator', b'batchsize', default=12,\\n )\\n eh.configitem(\\n b'phabricator', b'callsign', default=None,\\n )\\n eh.configitem(\\n b'phabricator', b'curlcmd', default=None,\\n )\\n # developer config: phabricator.repophid\\n eh.configitem(\\n b'phabricator', b'repophid', default=None,\\n )\\n eh.configitem(\\n b'phabricator', b'url', default=None,\\n )\\n eh.configitem(\\n b'phabsend', b'confirm', default=False,\\n )\\n \\n colortable = {\\n b'phabricator.action.created': b'green',\\n b'phabricator.action.skipped': b'magenta',\\n b'phabricator.action.updated': b'magenta',\\n b'phabricator.desc': b'',\\n b'phabricator.drev': b'bold',\\n b'phabricator.node': b'',\\n b'phabricator.status.abandoned': b'magenta dim',\\n b'phabricator.status.accepted': b'green bold',\\n b'phabricator.status.closed': b'green',\\n b'phabricator.status.needsreview': b'yellow',\\n b'phabricator.status.needsrevision': b'red',\\n b'phabricator.status.changesplanned': b'red',\\n }\\n \\n _VCR_FLAGS = [\\n (\\n b'',\\n b'test-vcr',\\n b'',\\n _(\\n b'Path to a vcr file. If nonexistent, will record a new vcr transcript'\\n b', otherwise will mock all http requests using the specified vcr file.'\\n b' (ADVANCED)'\\n ),\\n ),\\n ]\\n \\n \\n @eh.wrapfunction(localrepo, \\\"loadhgrc\\\")\\n def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):\\n \\\"\\\"\\\"Load ``.arcconfig`` content into a ui instance on repository open.\\n \\\"\\\"\\\"\\n result = False\\n arcconfig = {}\\n \\n try:\\n # json.loads only accepts bytes from 3.6+\\n rawparams = encoding.unifromlocal(wdirvfs.read(b\\\".arcconfig\\\"))\\n # json.loads only returns unicode strings\\n arcconfig = pycompat.rapply(\\n lambda x: encoding.unitolocal(x)\\n if isinstance(x, pycompat.unicode)\\n else x,\\n pycompat.json_loads(rawparams),\\n )\\n \\n result = True\\n except ValueError:\\n ui.warn(_(b\\\"invalid JSON in %s\\\\n\\\") % wdirvfs.join(b\\\".arcconfig\\\"))\\n except IOError:\\n pass\\n \\n cfg = util.sortdict()\\n \\n if b\\\"repository.callsign\\\" in arcconfig:\\n cfg[(b\\\"phabricator\\\", b\\\"callsign\\\")] = arcconfig[b\\\"repository.callsign\\\"]\\n \\n if b\\\"phabricator.uri\\\" in arcconfig:\\n cfg[(b\\\"phabricator\\\", b\\\"url\\\")] = arcconfig[b\\\"phabricator.uri\\\"]\\n \\n if cfg:\\n ui.applyconfig(cfg, source=wdirvfs.join(b\\\".arcconfig\\\"))\\n \\n return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg\\/hgrc\\n \\n \\n def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):\\n fullflags = flags + _VCR_FLAGS\\n \\n def hgmatcher(r1, r2):\\n if r1.uri != r2.uri or r1.method != r2.method:\\n return False\\n r1params = util.urlreq.parseqs(r1.body)\\n r2params = util.urlreq.parseqs(r2.body)\\n for key in r1params:\\n if key not in r2params:\\n return False\\n value = r1params[key][0]\\n # we want to compare json payloads without worrying about ordering\\n if value.startswith(b'{') and value.endswith(b'}'):\\n r1json = pycompat.json_loads(value)\\n r2json = pycompat.json_loads(r2params[key][0])\\n if r1json != r2json:\\n return False\\n elif r2params[key][0] != value:\\n return False\\n return True\\n \\n def sanitiserequest(request):\\n request.body = re.sub(\\n br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body\\n )\\n return request\\n \\n def sanitiseresponse(response):\\n if 'set-cookie' in response['headers']:\\n del response['headers']['set-cookie']\\n return response\\n \\n def decorate(fn):\\n def inner(*args, **kwargs):\\n cassette = pycompat.fsdecode(kwargs.pop('test_vcr', None))\\n if cassette:\\n import hgdemandimport\\n \\n with hgdemandimport.deactivated():\\n import vcr as vcrmod\\n import vcr.stubs as stubs\\n \\n vcr = vcrmod.VCR(\\n serializer='json',\\n before_record_request=sanitiserequest,\\n before_record_response=sanitiseresponse,\\n custom_patches=[\\n (\\n urlmod,\\n 'httpconnection',\\n stubs.VCRHTTPConnection,\\n ),\\n (\\n urlmod,\\n 'httpsconnection',\\n stubs.VCRHTTPSConnection,\\n ),\\n ],\\n )\\n vcr.register_matcher('hgmatcher', hgmatcher)\\n with vcr.use_cassette(cassette, match_on=['hgmatcher']):\\n return fn(*args, **kwargs)\\n return fn(*args, **kwargs)\\n \\n inner.__name__ = fn.__name__\\n inner.__doc__ = fn.__doc__\\n return command(\\n name,\\n fullflags,\\n spec,\\n helpcategory=helpcategory,\\n optionalrepo=optionalrepo,\\n )(inner)\\n \\n return decorate\\n \\n \\n def urlencodenested(params):\\n \\\"\\\"\\\"like urlencode, but works with nested parameters.\\n \\n For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be\\n flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to\\n urlencode. Note: the encoding is consistent with PHP's http_build_query.\\n \\\"\\\"\\\"\\n flatparams = util.sortdict()\\n \\n def process(prefix, obj):\\n if isinstance(obj, bool):\\n obj = {True: b'true', False: b'false'}[obj] # Python -\\u003e PHP form\\n lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]\\n items = {list: lister, dict: lambda x: x.items()}.get(type(obj))\\n if items is None:\\n flatparams[prefix] = obj\\n else:\\n for k, v in items(obj):\\n if prefix:\\n process(b'%s[%s]' % (prefix, k), v)\\n else:\\n process(k, v)\\n \\n process(b'', params)\\n return util.urlreq.urlencode(flatparams)\\n \\n \\n def readurltoken(ui):\\n \\\"\\\"\\\"return conduit url, token and make sure they exist\\n \\n Currently read from [auth] config section. In the future, it might\\n make sense to read from .arcconfig and .arcrc as well.\\n \\\"\\\"\\\"\\n url = ui.config(b'phabricator', b'url')\\n if not url:\\n raise error.Abort(\\n _(b'config %s.%s is required') % (b'phabricator', b'url')\\n )\\n \\n res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)\\n token = None\\n \\n if res:\\n group, auth = res\\n \\n ui.debug(b\\\"using auth.%s.* for authentication\\\\n\\\" % group)\\n \\n token = auth.get(b'phabtoken')\\n \\n if not token:\\n raise error.Abort(\\n _(b'Can\\\\'t find conduit token associated to %s') % (url,)\\n )\\n \\n return url, token\\n \\n \\n def callconduit(ui, name, params):\\n \\\"\\\"\\\"call Conduit API, params is a dict. return json.loads result, or None\\\"\\\"\\\"\\n host, token = readurltoken(ui)\\n url, authinfo = util.url(b'\\/'.join([host, b'api', name])).authinfo()\\n ui.debug(b'Conduit Call: %s %s\\\\n' % (url, pycompat.byterepr(params)))\\n params = params.copy()\\n params[b'__conduit__'] = {\\n b'token': token,\\n }\\n rawdata = {\\n b'params': templatefilters.json(params),\\n b'output': b'json',\\n b'__conduit__': 1,\\n }\\n data = urlencodenested(rawdata)\\n curlcmd = ui.config(b'phabricator', b'curlcmd')\\n if curlcmd:\\n sin, sout = procutil.popen2(\\n b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))\\n )\\n sin.write(data)\\n sin.close()\\n body = sout.read()\\n else:\\n urlopener = urlmod.opener(ui, authinfo)\\n request = util.urlreq.request(pycompat.strurl(url), data=data)\\n with contextlib.closing(urlopener.open(request)) as rsp:\\n body = rsp.read()\\n ui.debug(b'Conduit Response: %s\\\\n' % body)\\n parsed = pycompat.rapply(\\n lambda x: encoding.unitolocal(x)\\n if isinstance(x, pycompat.unicode)\\n else x,\\n # json.loads only accepts bytes from py3.6+\\n pycompat.json_loads(encoding.unifromlocal(body)),\\n )\\n if parsed.get(b'error_code'):\\n msg = _(b'Conduit Error (%s): %s') % (\\n parsed[b'error_code'],\\n parsed[b'error_info'],\\n )\\n raise error.Abort(msg)\\n return parsed[b'result']\\n \\n \\n @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)\\n def debugcallconduit(ui, repo, name):\\n \\\"\\\"\\\"call Conduit API\\n \\n Call parameters are read from stdin as a JSON blob. Result will be written\\n to stdout as a JSON blob.\\n \\\"\\\"\\\"\\n # json.loads only accepts bytes from 3.6+\\n rawparams = encoding.unifromlocal(ui.fin.read())\\n # json.loads only returns unicode strings\\n params = pycompat.rapply(\\n lambda x: encoding.unitolocal(x)\\n if isinstance(x, pycompat.unicode)\\n else x,\\n pycompat.json_loads(rawparams),\\n )\\n # json.dumps only accepts unicode strings\\n result = pycompat.rapply(\\n lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,\\n callconduit(ui, name, params),\\n )\\n s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))\\n ui.write(b'%s\\\\n' % encoding.unitolocal(s))\\n \\n \\n def getrepophid(repo):\\n \\\"\\\"\\\"given callsign, return repository PHID or None\\\"\\\"\\\"\\n # developer config: phabricator.repophid\\n repophid = repo.ui.config(b'phabricator', b'repophid')\\n if repophid:\\n return repophid\\n callsign = repo.ui.config(b'phabricator', b'callsign')\\n if not callsign:\\n return None\\n query = callconduit(\\n repo.ui,\\n b'diffusion.repository.search',\\n {b'constraints': {b'callsigns': [callsign]}},\\n )\\n if len(query[b'data']) == 0:\\n return None\\n repophid = query[b'data'][0][b'phid']\\n repo.ui.setconfig(b'phabricator', b'repophid', repophid)\\n return repophid\\n \\n \\n _differentialrevisiontagre = re.compile(br'\\\\AD([1-9][0-9]*)\\\\Z')\\n _differentialrevisiondescre = re.compile(\\n br'^Differential Revision:\\\\s*(?P\\u003curl\\u003e(?:.*)D(?P\\u003cid\\u003e[1-9][0-9]*))$', re.M\\n )\\n \\n \\n def getoldnodedrevmap(repo, nodelist):\\n \\\"\\\"\\\"find previous nodes that has been sent to Phabricator\\n \\n return {node: (oldnode, Differential diff, Differential Revision ID)}\\n for node in nodelist with known previous sent versions, or associated\\n Differential Revision IDs. ``oldnode`` and ``Differential diff`` could\\n be ``None``.\\n \\n Examines commit messages like \\\"Differential Revision:\\\" to get the\\n association information.\\n \\n If such commit message line is not found, examines all precursors and their\\n tags. Tags with format like \\\"D1234\\\" are considered a match and the node\\n with that tag, and the number after \\\"D\\\" (ex. 1234) will be returned.\\n \\n The ``old node``, if not None, is guaranteed to be the last diff of\\n corresponding Differential Revision, and exist in the repo.\\n \\\"\\\"\\\"\\n unfi = repo.unfiltered()\\n has_node = unfi.changelog.index.has_node\\n \\n result = {} # {node: (oldnode?, lastdiff?, drev)}\\n toconfirm = {} # {node: (force, {precnode}, drev)}\\n for node in nodelist:\\n ctx = unfi[node]\\n # For tags like \\\"D123\\\", put them into \\\"toconfirm\\\" to verify later\\n precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))\\n for n in precnodes:\\n if has_node(n):\\n for tag in unfi.nodetags(n):\\n m = _differentialrevisiontagre.match(tag)\\n if m:\\n toconfirm[node] = (0, set(precnodes), int(m.group(1)))\\n break\\n else:\\n continue # move to next predecessor\\n break # found a tag, stop\\n else:\\n # Check commit message\\n m = _differentialrevisiondescre.search(ctx.description())\\n if m:\\n toconfirm[node] = (1, set(precnodes), int(m.group('id')))\\n \\n # Double check if tags are genuine by collecting all old nodes from\\n # Phabricator, and expect precursors overlap with it.\\n if toconfirm:\\n drevs = [drev for force, precs, drev in toconfirm.values()]\\n alldiffs = callconduit(\\n unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}\\n )\\n getnode = lambda d: bin(getdiffmeta(d).get(b'node', b'')) or None\\n for newnode, (force, precset, drev) in toconfirm.items():\\n diffs = [\\n d for d in alldiffs.values() if int(d[b'revisionID']) == drev\\n ]\\n \\n # \\\"precursors\\\" as known by Phabricator\\n phprecset = set(getnode(d) for d in diffs)\\n \\n # Ignore if precursors (Phabricator and local repo) do not overlap,\\n # and force is not set (when commit message says nothing)\\n if not force and not bool(phprecset & precset):\\n tagname = b'D%d' % drev\\n tags.tag(\\n repo,\\n tagname,\\n nullid,\\n message=None,\\n user=None,\\n date=None,\\n local=True,\\n )\\n unfi.ui.warn(\\n _(\\n b'D%d: local tag removed - does not match '\\n b'Differential history\\\\n'\\n )\\n % drev\\n )\\n continue\\n \\n # Find the last node using Phabricator metadata, and make sure it\\n # exists in the repo\\n oldnode = lastdiff = None\\n if diffs:\\n lastdiff = max(diffs, key=lambda d: int(d[b'id']))\\n oldnode = getnode(lastdiff)\\n if oldnode and not has_node(oldnode):\\n oldnode = None\\n \\n result[newnode] = (oldnode, lastdiff, drev)\\n \\n return result\\n \\n \\n def getdrevmap(repo, revs):\\n \\\"\\\"\\\"Return a dict mapping each rev in `revs` to their Differential Revision\\n ID or None.\\n \\\"\\\"\\\"\\n result = {}\\n for rev in revs:\\n result[rev] = None\\n ctx = repo[rev]\\n # Check commit message\\n m = _differentialrevisiondescre.search(ctx.description())\\n if m:\\n result[rev] = int(m.group('id'))\\n continue\\n # Check tags\\n for tag in repo.nodetags(ctx.node()):\\n m = _differentialrevisiontagre.match(tag)\\n if m:\\n result[rev] = int(m.group(1))\\n break\\n \\n return result\\n \\n \\n def getdiff(ctx, diffopts):\\n \\\"\\\"\\\"plain-text diff without header (user, commit message, etc)\\\"\\\"\\\"\\n output = util.stringio()\\n for chunk, _label in patch.diffui(\\n ctx.repo(), ctx.p1().node(), ctx.node(), None, opts=diffopts\\n ):\\n output.write(chunk)\\n return output.getvalue()\\n \\n \\n class DiffChangeType(object):\\n ADD = 1\\n CHANGE = 2\\n DELETE = 3\\n MOVE_AWAY = 4\\n COPY_AWAY = 5\\n MOVE_HERE = 6\\n COPY_HERE = 7\\n MULTICOPY = 8\\n \\n \\n class DiffFileType(object):\\n TEXT = 1\\n IMAGE = 2\\n BINARY = 3\\n \\n \\n @attr.s\\n class phabhunk(dict):\\n \\\"\\\"\\\"Represents a Differential hunk, which is owned by a Differential change\\n \\\"\\\"\\\"\\n \\n oldOffset = attr.ib(default=0) # camelcase-required\\n oldLength = attr.ib(default=0) # camelcase-required\\n newOffset = attr.ib(default=0) # camelcase-required\\n newLength = attr.ib(default=0) # camelcase-required\\n corpus = attr.ib(default='')\\n # These get added to the phabchange's equivalents\\n addLines = attr.ib(default=0) # camelcase-required\\n delLines = attr.ib(default=0) # camelcase-required\\n \\n \\n @attr.s\\n class phabchange(object):\\n \\\"\\\"\\\"Represents a Differential change, owns Differential hunks and owned by a\\n Differential diff. Each one represents one file in a diff.\\n \\\"\\\"\\\"\\n \\n currentPath = attr.ib(default=None) # camelcase-required\\n oldPath = attr.ib(default=None) # camelcase-required\\n awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required\\n metadata = attr.ib(default=attr.Factory(dict))\\n oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required\\n newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required\\n type = attr.ib(default=DiffChangeType.CHANGE)\\n fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required\\n commitHash = attr.ib(default=None) # camelcase-required\\n addLines = attr.ib(default=0) # camelcase-required\\n delLines = attr.ib(default=0) # camelcase-required\\n hunks = attr.ib(default=attr.Factory(list))\\n \\n def copynewmetadatatoold(self):\\n for key in list(self.metadata.keys()):\\n newkey = key.replace(b'new:', b'old:')\\n self.metadata[newkey] = self.metadata[key]\\n \\n def addoldmode(self, value):\\n self.oldProperties[b'unix:filemode'] = value\\n \\n def addnewmode(self, value):\\n self.newProperties[b'unix:filemode'] = value\\n \\n def addhunk(self, hunk):\\n if not isinstance(hunk, phabhunk):\\n raise error.Abort(b'phabchange.addhunk only takes phabhunks')\\n self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))\\n # It's useful to include these stats since the Phab web UI shows them,\\n # and uses them to estimate how large a change a Revision is. Also used\\n # in email subjects for the [+++--] bit.\\n self.addLines += hunk.addLines\\n self.delLines += hunk.delLines\\n \\n \\n @attr.s\\n class phabdiff(object):\\n \\\"\\\"\\\"Represents a Differential diff, owns Differential changes. Corresponds\\n to a commit.\\n \\\"\\\"\\\"\\n \\n # Doesn't seem to be any reason to send this (output of uname -n)\\n sourceMachine = attr.ib(default=b'') # camelcase-required\\n sourcePath = attr.ib(default=b'\\/') # camelcase-required\\n sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required\\n sourceControlPath = attr.ib(default=b'\\/') # camelcase-required\\n sourceControlSystem = attr.ib(default=b'hg') # camelcase-required\\n branch = attr.ib(default=b'default')\\n bookmark = attr.ib(default=None)\\n creationMethod = attr.ib(default=b'phabsend') # camelcase-required\\n lintStatus = attr.ib(default=b'none') # camelcase-required\\n unitStatus = attr.ib(default=b'none') # camelcase-required\\n changes = attr.ib(default=attr.Factory(dict))\\n repositoryPHID = attr.ib(default=None) # camelcase-required\\n \\n def addchange(self, change):\\n if not isinstance(change, phabchange):\\n raise error.Abort(b'phabdiff.addchange only takes phabchanges')\\n self.changes[change.currentPath] = pycompat.byteskwargs(\\n attr.asdict(change)\\n )\\n \\n \\n def maketext(pchange, ctx, fname):\\n \\\"\\\"\\\"populate the phabchange for a text file\\\"\\\"\\\"\\n repo = ctx.repo()\\n fmatcher = match.exact([fname])\\n diffopts = mdiff.diffopts(git=True, context=32767)\\n _pfctx, _fctx, header, fhunks = next(\\n patch.diffhunks(repo, ctx.p1(), ctx, fmatcher, opts=diffopts)\\n )\\n \\n for fhunk in fhunks:\\n (oldOffset, oldLength, newOffset, newLength), lines = fhunk\\n corpus = b''.join(lines[1:])\\n shunk = list(header)\\n shunk.extend(lines)\\n _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(\\n patch.diffstatdata(util.iterlines(shunk))\\n )\\n pchange.addhunk(\\n phabhunk(\\n oldOffset,\\n oldLength,\\n newOffset,\\n newLength,\\n corpus,\\n addLines,\\n delLines,\\n )\\n )\\n \\n \\n def uploadchunks(fctx, fphid):\\n \\\"\\\"\\\"upload large binary files as separate chunks.\\n Phab requests chunking over 8MiB, and splits into 4MiB chunks\\n \\\"\\\"\\\"\\n ui = fctx.repo().ui\\n chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})\\n with ui.makeprogress(\\n _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)\\n ) as progress:\\n for chunk in chunks:\\n progress.increment()\\n if chunk[b'complete']:\\n continue\\n bstart = int(chunk[b'byteStart'])\\n bend = int(chunk[b'byteEnd'])\\n callconduit(\\n ui,\\n b'file.uploadchunk',\\n {\\n b'filePHID': fphid,\\n b'byteStart': bstart,\\n b'data': base64.b64encode(fctx.data()[bstart:bend]),\\n b'dataEncoding': b'base64',\\n },\\n )\\n \\n \\n def uploadfile(fctx):\\n \\\"\\\"\\\"upload binary files to Phabricator\\\"\\\"\\\"\\n repo = fctx.repo()\\n ui = repo.ui\\n fname = fctx.path()\\n size = fctx.size()\\n fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())\\n \\n # an allocate call is required first to see if an upload is even required\\n # (Phab might already have it) and to determine if chunking is needed\\n allocateparams = {\\n b'name': fname,\\n b'contentLength': size,\\n b'contentHash': fhash,\\n }\\n filealloc = callconduit(ui, b'file.allocate', allocateparams)\\n fphid = filealloc[b'filePHID']\\n \\n if filealloc[b'upload']:\\n ui.write(_(b'uploading %s\\\\n') % bytes(fctx))\\n if not fphid:\\n uploadparams = {\\n b'name': fname,\\n b'data_base64': base64.b64encode(fctx.data()),\\n }\\n fphid = callconduit(ui, b'file.upload', uploadparams)\\n else:\\n uploadchunks(fctx, fphid)\\n else:\\n ui.debug(b'server already has %s\\\\n' % bytes(fctx))\\n \\n if not fphid:\\n raise error.Abort(b'Upload of %s failed.' % bytes(fctx))\\n \\n return fphid\\n \\n \\n def addoldbinary(pchange, fctx):\\n \\\"\\\"\\\"add the metadata for the previous version of a binary file to the\\n phabchange for the new version\\n \\\"\\\"\\\"\\n oldfctx = fctx.p1()\\n if fctx.cmp(oldfctx):\\n # Files differ, add the old one\\n pchange.metadata[b'old:file:size'] = oldfctx.size()\\n mimeguess, _enc = mimetypes.guess_type(\\n encoding.unifromlocal(oldfctx.path())\\n )\\n if mimeguess:\\n pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(\\n mimeguess\\n )\\n fphid = uploadfile(oldfctx)\\n pchange.metadata[b'old:binary-phid'] = fphid\\n else:\\n # If it's left as IMAGE\\/BINARY web UI might try to display it\\n pchange.fileType = DiffFileType.TEXT\\n pchange.copynewmetadatatoold()\\n \\n \\n def makebinary(pchange, fctx):\\n \\\"\\\"\\\"populate the phabchange for a binary file\\\"\\\"\\\"\\n pchange.fileType = DiffFileType.BINARY\\n fphid = uploadfile(fctx)\\n pchange.metadata[b'new:binary-phid'] = fphid\\n pchange.metadata[b'new:file:size'] = fctx.size()\\n mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))\\n if mimeguess:\\n mimeguess = pycompat.bytestr(mimeguess)\\n pchange.metadata[b'new:file:mime-type'] = mimeguess\\n if mimeguess.startswith(b'image\\/'):\\n pchange.fileType = DiffFileType.IMAGE\\n \\n \\n # Copied from mercurial\\/patch.py\\n gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}\\n \\n \\n def notutf8(fctx):\\n \\\"\\\"\\\"detect non-UTF-8 text files since Phabricator requires them to be marked\\n as binary\\n \\\"\\\"\\\"\\n try:\\n fctx.data().decode('utf-8')\\n if fctx.parents():\\n fctx.p1().data().decode('utf-8')\\n return False\\n except UnicodeDecodeError:\\n fctx.repo().ui.write(\\n _(b'file %s detected as non-UTF-8, marked as binary\\\\n')\\n % fctx.path()\\n )\\n return True\\n \\n \\n def addremoved(pdiff, ctx, removed):\\n \\\"\\\"\\\"add removed files to the phabdiff. Shouldn't include moves\\\"\\\"\\\"\\n for fname in removed:\\n pchange = phabchange(\\n currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE\\n )\\n pchange.addoldmode(gitmode[ctx.p1()[fname].flags()])\\n fctx = ctx.p1()[fname]\\n if not (fctx.isbinary() or notutf8(fctx)):\\n maketext(pchange, ctx, fname)\\n \\n pdiff.addchange(pchange)\\n \\n \\n def addmodified(pdiff, ctx, modified):\\n \\\"\\\"\\\"add modified files to the phabdiff\\\"\\\"\\\"\\n for fname in modified:\\n fctx = ctx[fname]\\n pchange = phabchange(currentPath=fname, oldPath=fname)\\n filemode = gitmode[ctx[fname].flags()]\\n originalmode = gitmode[ctx.p1()[fname].flags()]\\n if filemode != originalmode:\\n pchange.addoldmode(originalmode)\\n pchange.addnewmode(filemode)\\n \\n if fctx.isbinary() or notutf8(fctx):\\n makebinary(pchange, fctx)\\n addoldbinary(pchange, fctx)\\n else:\\n maketext(pchange, ctx, fname)\\n \\n pdiff.addchange(pchange)\\n \\n \\n def addadded(pdiff, ctx, added, removed):\\n \\\"\\\"\\\"add file adds to the phabdiff, both new files and copies\\/moves\\\"\\\"\\\"\\n # Keep track of files that've been recorded as moved\\/copied, so if there are\\n # additional copies we can mark them (moves get removed from removed)\\n copiedchanges = {}\\n movedchanges = {}\\n for fname in added:\\n fctx = ctx[fname]\\n pchange = phabchange(currentPath=fname)\\n \\n filemode = gitmode[ctx[fname].flags()]\\n renamed = fctx.renamed()\\n \\n if renamed:\\n originalfname = renamed[0]\\n originalmode = gitmode[ctx.p1()[originalfname].flags()]\\n pchange.oldPath = originalfname\\n \\n if originalfname in removed:\\n origpchange = phabchange(\\n currentPath=originalfname,\\n oldPath=originalfname,\\n type=DiffChangeType.MOVE_AWAY,\\n awayPaths=[fname],\\n )\\n movedchanges[originalfname] = origpchange\\n removed.remove(originalfname)\\n pchange.type = DiffChangeType.MOVE_HERE\\n elif originalfname in movedchanges:\\n movedchanges[originalfname].type = DiffChangeType.MULTICOPY\\n movedchanges[originalfname].awayPaths.append(fname)\\n pchange.type = DiffChangeType.COPY_HERE\\n else: # pure copy\\n if originalfname not in copiedchanges:\\n origpchange = phabchange(\\n currentPath=originalfname, type=DiffChangeType.COPY_AWAY\\n )\\n copiedchanges[originalfname] = origpchange\\n else:\\n origpchange = copiedchanges[originalfname]\\n origpchange.awayPaths.append(fname)\\n pchange.type = DiffChangeType.COPY_HERE\\n \\n if filemode != originalmode:\\n pchange.addoldmode(originalmode)\\n pchange.addnewmode(filemode)\\n else: # Brand-new file\\n pchange.addnewmode(gitmode[fctx.flags()])\\n pchange.type = DiffChangeType.ADD\\n \\n if fctx.isbinary() or notutf8(fctx):\\n makebinary(pchange, fctx)\\n if renamed:\\n addoldbinary(pchange, fctx)\\n else:\\n maketext(pchange, ctx, fname)\\n \\n pdiff.addchange(pchange)\\n \\n for _path, copiedchange in copiedchanges.items():\\n pdiff.addchange(copiedchange)\\n for _path, movedchange in movedchanges.items():\\n pdiff.addchange(movedchange)\\n \\n \\n def creatediff(ctx):\\n \\\"\\\"\\\"create a Differential Diff\\\"\\\"\\\"\\n repo = ctx.repo()\\n repophid = getrepophid(repo)\\n # Create a \\\"Differential Diff\\\" via \\\"differential.creatediff\\\" API\\n pdiff = phabdiff(\\n sourceControlBaseRevision=b'%s' % ctx.p1().hex(),\\n branch=b'%s' % ctx.branch(),\\n )\\n modified, added, removed, _d, _u, _i, _c = ctx.p1().status(ctx)\\n # addadded will remove moved files from removed, so addremoved won't get\\n # them\\n addadded(pdiff, ctx, added, removed)\\n addmodified(pdiff, ctx, modified)\\n addremoved(pdiff, ctx, removed)\\n if repophid:\\n pdiff.repositoryPHID = repophid\\n diff = callconduit(\\n repo.ui,\\n b'differential.creatediff',\\n pycompat.byteskwargs(attr.asdict(pdiff)),\\n )\\n if not diff:\\n raise error.Abort(_(b'cannot create diff for %s') % ctx)\\n return diff\\n \\n \\n def writediffproperties(ctx, diff):\\n \\\"\\\"\\\"write metadata to diff so patches could be applied losslessly\\\"\\\"\\\"\\n # creatediff returns with a diffid but query returns with an id\\n diffid = diff.get(b'diffid', diff.get(b'id'))\\n params = {\\n b'diff_id': diffid,\\n b'name': b'hg:meta',\\n b'data': templatefilters.json(\\n {\\n b'user': ctx.user(),\\n b'date': b'%d %d' % ctx.date(),\\n b'branch': ctx.branch(),\\n b'node': ctx.hex(),\\n b'parent': ctx.p1().hex(),\\n }\\n ),\\n }\\n callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)\\n \\n params = {\\n b'diff_id': diffid,\\n b'name': b'local:commits',\\n b'data': templatefilters.json(\\n {\\n ctx.hex(): {\\n b'author': stringutil.person(ctx.user()),\\n b'authorEmail': stringutil.email(ctx.user()),\\n b'time': int(ctx.date()[0]),\\n b'commit': ctx.hex(),\\n b'parents': [ctx.p1().hex()],\\n b'branch': ctx.branch(),\\n },\\n }\\n ),\\n }\\n callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)\\n \\n \\n def createdifferentialrevision(\\n ctx,\\n revid=None,\\n parentrevphid=None,\\n oldnode=None,\\n olddiff=None,\\n actions=None,\\n comment=None,\\n ):\\n \\\"\\\"\\\"create or update a Differential Revision\\n \\n If revid is None, create a new Differential Revision, otherwise update\\n revid. If parentrevphid is not None, set it as a dependency.\\n \\n If oldnode is not None, check if the patch content (without commit message\\n and metadata) has changed before creating another diff.\\n \\n If actions is not None, they will be appended to the transaction.\\n \\\"\\\"\\\"\\n repo = ctx.repo()\\n if oldnode:\\n diffopts = mdiff.diffopts(git=True, context=32767)\\n oldctx = repo.unfiltered()[oldnode]\\n neednewdiff = getdiff(ctx, diffopts) != getdiff(oldctx, diffopts)\\n else:\\n neednewdiff = True\\n \\n transactions = []\\n if neednewdiff:\\n diff = creatediff(ctx)\\n transactions.append({b'type': b'update', b'value': diff[b'phid']})\\n if comment:\\n transactions.append({b'type': b'comment', b'value': comment})\\n else:\\n # Even if we don't need to upload a new diff because the patch content\\n # does not change. We might still need to update its metadata so\\n # pushers could know the correct node metadata.\\n assert olddiff\\n diff = olddiff\\n writediffproperties(ctx, diff)\\n \\n # Set the parent Revision every time, so commit re-ordering is picked-up\\n if parentrevphid:\\n transactions.append(\\n {b'type': b'parents.set', b'value': [parentrevphid]}\\n )\\n \\n if actions:\\n transactions += actions\\n \\n # Parse commit message and update related fields.\\n desc = ctx.description()\\n info = callconduit(\\n repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}\\n )\\n for k, v in info[b'fields'].items():\\n if k in [b'title', b'summary', b'testPlan']:\\n transactions.append({b'type': k, b'value': v})\\n \\n params = {b'transactions': transactions}\\n if revid is not None:\\n # Update an existing Differential Revision\\n params[b'objectIdentifier'] = revid\\n \\n revision = callconduit(repo.ui, b'differential.revision.edit', params)\\n if not revision:\\n raise error.Abort(_(b'cannot create revision for %s') % ctx)\\n \\n return revision, diff\\n \\n \\n def userphids(repo, names):\\n \\\"\\\"\\\"convert user names to PHIDs\\\"\\\"\\\"\\n names = [name.lower() for name in names]\\n query = {b'constraints': {b'usernames': names}}\\n result = callconduit(repo.ui, b'user.search', query)\\n # username not found is not an error of the API. So check if we have missed\\n # some names here.\\n data = result[b'data']\\n resolved = set(entry[b'fields'][b'username'].lower() for entry in data)\\n unresolved = set(names) - resolved\\n if unresolved:\\n raise error.Abort(\\n _(b'unknown username: %s') % b' '.join(sorted(unresolved))\\n )\\n return [entry[b'phid'] for entry in data]\\n \\n \\n @vcrcommand(\\n b'phabsend',\\n [\\n (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),\\n (b'', b'amend', True, _(b'update commit messages')),\\n (b'', b'reviewer', [], _(b'specify reviewers')),\\n (b'', b'blocker', [], _(b'specify blocking reviewers')),\\n (\\n b'm',\\n b'comment',\\n b'',\\n _(b'add a comment to Revisions with new\\/updated Diffs'),\\n ),\\n (b'', b'confirm', None, _(b'ask for confirmation before sending')),\\n ],\\n _(b'REV [OPTIONS]'),\\n helpcategory=command.CATEGORY_IMPORT_EXPORT,\\n )\\n def phabsend(ui, repo, *revs, **opts):\\n \\\"\\\"\\\"upload changesets to Phabricator\\n \\n If there are multiple revisions specified, they will be send as a stack\\n with a linear dependencies relationship using the order specified by the\\n revset.\\n \\n For the first time uploading changesets, local tags will be created to\\n maintain the association. After the first time, phabsend will check\\n obsstore and tags information so it can figure out whether to update an\\n existing Differential Revision, or create a new one.\\n \\n If --amend is set, update commit messages so they have the\\n ``Differential Revision`` URL, remove related tags. This is similar to what\\n arcanist will do, and is more desired in author-push workflows. Otherwise,\\n use local tags to record the ``Differential Revision`` association.\\n \\n The --confirm option lets you confirm changesets before sending them. You\\n can also add following to your configuration file to make it default\\n behaviour::\\n \\n [phabsend]\\n confirm = true\\n \\n phabsend will check obsstore and the above association to decide whether to\\n update an existing Differential Revision, or create a new one.\\n \\\"\\\"\\\"\\n opts = pycompat.byteskwargs(opts)\\n revs = list(revs) + opts.get(b'rev', [])\\n revs = scmutil.revrange(repo, revs)\\n revs.sort() # ascending order to preserve topological parent\\/child in phab\\n \\n if not revs:\\n raise error.Abort(_(b'phabsend requires at least one changeset'))\\n if opts.get(b'amend'):\\n cmdutil.checkunfinished(repo)\\n \\n # {newnode: (oldnode, olddiff, olddrev}\\n oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])\\n \\n confirm = ui.configbool(b'phabsend', b'confirm')\\n confirm |= bool(opts.get(b'confirm'))\\n if confirm:\\n confirmed = _confirmbeforesend(repo, revs, oldmap)\\n if not confirmed:\\n raise error.Abort(_(b'phabsend cancelled'))\\n \\n actions = []\\n reviewers = opts.get(b'reviewer', [])\\n blockers = opts.get(b'blocker', [])\\n phids = []\\n if reviewers:\\n phids.extend(userphids(repo, reviewers))\\n if blockers:\\n phids.extend(\\n map(lambda phid: b'blocking(%s)' % phid, userphids(repo, blockers))\\n )\\n if phids:\\n actions.append({b'type': b'reviewers.add', b'value': phids})\\n \\n drevids = [] # [int]\\n diffmap = {} # {newnode: diff}\\n \\n # Send patches one by one so we know their Differential Revision PHIDs and\\n # can provide dependency relationship\\n lastrevphid = None\\n for rev in revs:\\n ui.debug(b'sending rev %d\\\\n' % rev)\\n ctx = repo[rev]\\n \\n # Get Differential Revision ID\\n oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))\\n if oldnode != ctx.node() or opts.get(b'amend'):\\n # Create or update Differential Revision\\n revision, diff = createdifferentialrevision(\\n ctx,\\n revid,\\n lastrevphid,\\n oldnode,\\n olddiff,\\n actions,\\n opts.get(b'comment'),\\n )\\n diffmap[ctx.node()] = diff\\n newrevid = int(revision[b'object'][b'id'])\\n newrevphid = revision[b'object'][b'phid']\\n if revid:\\n action = b'updated'\\n else:\\n action = b'created'\\n \\n # Create a local tag to note the association, if commit message\\n # does not have it already\\n m = _differentialrevisiondescre.search(ctx.description())\\n if not m or int(m.group('id')) != newrevid:\\n tagname = b'D%d' % newrevid\\n tags.tag(\\n repo,\\n tagname,\\n ctx.node(),\\n message=None,\\n user=None,\\n date=None,\\n local=True,\\n )\\n else:\\n # Nothing changed. But still set \\\"newrevphid\\\" so the next revision\\n # could depend on this one and \\\"newrevid\\\" for the summary line.\\n newrevphid = querydrev(repo, b'%d' % revid)[0][b'phid']\\n newrevid = revid\\n action = b'skipped'\\n \\n actiondesc = ui.label(\\n {\\n b'created': _(b'created'),\\n b'skipped': _(b'skipped'),\\n b'updated': _(b'updated'),\\n }[action],\\n b'phabricator.action.%s' % action,\\n )\\n drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')\\n nodedesc = ui.label(bytes(ctx), b'phabricator.node')\\n desc = ui.label(ctx.description().split(b'\\\\n')[0], b'phabricator.desc')\\n ui.write(\\n _(b'%s - %s - %s: %s\\\\n') % (drevdesc, actiondesc, nodedesc, desc)\\n )\\n drevids.append(newrevid)\\n lastrevphid = newrevphid\\n \\n # Update commit messages and remove tags\\n if opts.get(b'amend'):\\n unfi = repo.unfiltered()\\n drevs = callconduit(ui, b'differential.query', {b'ids': drevids})\\n with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):\\n wnode = unfi[b'.'].node()\\n mapping = {} # {oldnode: [newnode]}\\n for i, rev in enumerate(revs):\\n old = unfi[rev]\\n drevid = drevids[i]\\n drev = [d for d in drevs if int(d[b'id']) == drevid][0]\\n newdesc = getdescfromdrev(drev)\\n # Make sure commit message contain \\\"Differential Revision\\\"\\n if old.description() != newdesc:\\n if old.phase() == phases.public:\\n ui.warn(\\n _(b\\\"warning: not updating public commit %s\\\\n\\\")\\n % scmutil.formatchangeid(old)\\n )\\n continue\\n parents = [\\n mapping.get(old.p1().node(), (old.p1(),))[0],\\n mapping.get(old.p2().node(), (old.p2(),))[0],\\n ]\\n new = context.metadataonlyctx(\\n repo,\\n old,\\n parents=parents,\\n text=newdesc,\\n user=old.user(),\\n date=old.date(),\\n extra=old.extra(),\\n )\\n \\n newnode = new.commit()\\n \\n mapping[old.node()] = [newnode]\\n # Update diff property\\n # If it fails just warn and keep going, otherwise the DREV\\n # associations will be lost\\n try:\\n writediffproperties(unfi[newnode], diffmap[old.node()])\\n except util.urlerr.urlerror:\\n ui.warnnoi18n(\\n b'Failed to update metadata for D%d\\\\n' % drevid\\n )\\n # Remove local tags since it's no longer necessary\\n tagname = b'D%d' % drevid\\n if tagname in repo.tags():\\n tags.tag(\\n repo,\\n tagname,\\n nullid,\\n message=None,\\n user=None,\\n date=None,\\n local=True,\\n )\\n scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)\\n if wnode in mapping:\\n unfi.setparents(mapping[wnode][0])\\n \\n \\n # Map from \\\"hg:meta\\\" keys to header understood by \\\"hg import\\\". The order is\\n # consistent with \\\"hg export\\\" output.\\n _metanamemap = util.sortdict(\\n [\\n (b'user', b'User'),\\n (b'date', b'Date'),\\n (b'branch', b'Branch'),\\n (b'node', b'Node ID'),\\n (b'parent', b'Parent '),\\n ]\\n )\\n \\n \\n def _confirmbeforesend(repo, revs, oldmap):\\n url, token = readurltoken(repo.ui)\\n ui = repo.ui\\n for rev in revs:\\n ctx = repo[rev]\\n desc = ctx.description().splitlines()[0]\\n oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))\\n if drevid:\\n drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')\\n else:\\n drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')\\n \\n ui.write(\\n _(b'%s - %s: %s\\\\n')\\n % (\\n drevdesc,\\n ui.label(bytes(ctx), b'phabricator.node'),\\n ui.label(desc, b'phabricator.desc'),\\n )\\n )\\n \\n if ui.promptchoice(\\n _(b'Send the above changes to %s (yn)?$$ &Yes $$ &No') % url\\n ):\\n return False\\n \\n return True\\n \\n \\n _knownstatusnames = {\\n b'accepted',\\n b'needsreview',\\n b'needsrevision',\\n b'closed',\\n b'abandoned',\\n b'changesplanned',\\n }\\n \\n \\n def _getstatusname(drev):\\n \\\"\\\"\\\"get normalized status name from a Differential Revision\\\"\\\"\\\"\\n return drev[b'statusName'].replace(b' ', b'').lower()\\n \\n \\n # Small language to specify differential revisions. Support symbols: (), :X,\\n # +, and -.\\n \\n _elements = {\\n # token-type: binding-strength, primary, prefix, infix, suffix\\n b'(': (12, None, (b'group', 1, b')'), None, None),\\n b':': (8, None, (b'ancestors', 8), None, None),\\n b'&': (5, None, None, (b'and_', 5), None),\\n b'+': (4, None, None, (b'add', 4), None),\\n b'-': (4, None, None, (b'sub', 4), None),\\n b')': (0, None, None, None, None),\\n b'symbol': (0, b'symbol', None, None, None),\\n b'end': (0, None, None, None, None),\\n }\\n \\n \\n def _tokenize(text):\\n view = memoryview(text) # zero-copy slice\\n special = b'():+-& '\\n pos = 0\\n length = len(text)\\n while pos \\u003c length:\\n symbol = b''.join(\\n itertools.takewhile(\\n lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])\\n )\\n )\\n if symbol:\\n yield (b'symbol', symbol, pos)\\n pos += len(symbol)\\n else: # special char, ignore space\\n if text[pos : pos + 1] != b' ':\\n yield (text[pos : pos + 1], None, pos)\\n pos += 1\\n yield (b'end', None, pos)\\n \\n \\n def _parse(text):\\n tree, pos = parser.parser(_elements).parse(_tokenize(text))\\n if pos != len(text):\\n raise error.ParseError(b'invalid token', pos)\\n return tree\\n \\n \\n def _parsedrev(symbol):\\n \\\"\\\"\\\"str -\\u003e int or None, ex. 'D45' -\\u003e 45; '12' -\\u003e 12; 'x' -\\u003e None\\\"\\\"\\\"\\n if symbol.startswith(b'D') and symbol[1:].isdigit():\\n return int(symbol[1:])\\n if symbol.isdigit():\\n return int(symbol)\\n \\n \\n def _prefetchdrevs(tree):\\n \\\"\\\"\\\"return ({single-drev-id}, {ancestor-drev-id}) to prefetch\\\"\\\"\\\"\\n drevs = set()\\n ancestordrevs = set()\\n op = tree[0]\\n if op == b'symbol':\\n r = _parsedrev(tree[1])\\n if r:\\n drevs.add(r)\\n elif op == b'ancestors':\\n r, a = _prefetchdrevs(tree[1])\\n drevs.update(r)\\n ancestordrevs.update(r)\\n ancestordrevs.update(a)\\n else:\\n for t in tree[1:]:\\n r, a = _prefetchdrevs(t)\\n drevs.update(r)\\n ancestordrevs.update(a)\\n return drevs, ancestordrevs\\n \\n \\n def querydrev(repo, spec):\\n \\\"\\\"\\\"return a list of \\\"Differential Revision\\\" dicts\\n \\n spec is a string using a simple query language, see docstring in phabread\\n for details.\\n \\n A \\\"Differential Revision dict\\\" looks like:\\n \\n {\\n \\\"activeDiffPHID\\\": \\\"PHID-DIFF-xoqnjkobbm6k4dk6hi72\\\",\\n \\\"authorPHID\\\": \\\"PHID-USER-tv3ohwc4v4jeu34otlye\\\",\\n \\\"auxiliary\\\": {\\n \\\"phabricator:depends-on\\\": [\\n \\\"PHID-DREV-gbapp366kutjebt7agcd\\\"\\n ]\\n \\\"phabricator:projects\\\": [],\\n },\\n \\\"branch\\\": \\\"default\\\",\\n \\\"ccs\\\": [],\\n \\\"commits\\\": [],\\n \\\"dateCreated\\\": \\\"1499181406\\\",\\n \\\"dateModified\\\": \\\"1499182103\\\",\\n \\\"diffs\\\": [\\n \\\"3\\\",\\n \\\"4\\\",\\n ],\\n \\\"hashes\\\": [],\\n \\\"id\\\": \\\"2\\\",\\n \\\"lineCount\\\": \\\"2\\\",\\n \\\"phid\\\": \\\"PHID-DREV-672qvysjcczopag46qty\\\",\\n \\\"properties\\\": {},\\n \\\"repositoryPHID\\\": \\\"PHID-REPO-hub2hx62ieuqeheznasv\\\",\\n \\\"reviewers\\\": [],\\n \\\"sourcePath\\\": null\\n \\\"status\\\": \\\"0\\\",\\n \\\"statusName\\\": \\\"Needs Review\\\",\\n \\\"summary\\\": \\\"\\\",\\n \\\"testPlan\\\": \\\"\\\",\\n \\\"title\\\": \\\"example\\\",\\n \\\"uri\\\": \\\"https:\\/\\/phab.example.com\\/D2\\\",\\n }\\n \\\"\\\"\\\"\\n # TODO: replace differential.query and differential.querydiffs with\\n # differential.diff.search because the former (and their output) are\\n # frozen, and planned to be deprecated and removed.\\n \\n def fetch(params):\\n \\\"\\\"\\\"params -\\u003e single drev or None\\\"\\\"\\\"\\n key = (params.get(b'ids') or params.get(b'phids') or [None])[0]\\n if key in prefetched:\\n return prefetched[key]\\n drevs = callconduit(repo.ui, b'differential.query', params)\\n # Fill prefetched with the result\\n for drev in drevs:\\n prefetched[drev[b'phid']] = drev\\n prefetched[int(drev[b'id'])] = drev\\n if key not in prefetched:\\n raise error.Abort(\\n _(b'cannot get Differential Revision %r') % params\\n )\\n return prefetched[key]\\n \\n def getstack(topdrevids):\\n \\\"\\\"\\\"given a top, get a stack from the bottom, [id] -\\u003e [id]\\\"\\\"\\\"\\n visited = set()\\n result = []\\n queue = [{b'ids': [i]} for i in topdrevids]\\n while queue:\\n params = queue.pop()\\n drev = fetch(params)\\n if drev[b'id'] in visited:\\n continue\\n visited.add(drev[b'id'])\\n result.append(int(drev[b'id']))\\n auxiliary = drev.get(b'auxiliary', {})\\n depends = auxiliary.get(b'phabricator:depends-on', [])\\n for phid in depends:\\n queue.append({b'phids': [phid]})\\n result.reverse()\\n return smartset.baseset(result)\\n \\n # Initialize prefetch cache\\n prefetched = {} # {id or phid: drev}\\n \\n tree = _parse(spec)\\n drevs, ancestordrevs = _prefetchdrevs(tree)\\n \\n # developer config: phabricator.batchsize\\n batchsize = repo.ui.configint(b'phabricator', b'batchsize')\\n \\n # Prefetch Differential Revisions in batch\\n tofetch = set(drevs)\\n for r in ancestordrevs:\\n tofetch.update(range(max(1, r - batchsize), r + 1))\\n if drevs:\\n fetch({b'ids': list(tofetch)})\\n validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))\\n \\n # Walk through the tree, return smartsets\\n def walk(tree):\\n op = tree[0]\\n if op == b'symbol':\\n drev = _parsedrev(tree[1])\\n if drev:\\n return smartset.baseset([drev])\\n elif tree[1] in _knownstatusnames:\\n drevs = [\\n r\\n for r in validids\\n if _getstatusname(prefetched[r]) == tree[1]\\n ]\\n return smartset.baseset(drevs)\\n else:\\n raise error.Abort(_(b'unknown symbol: %s') % tree[1])\\n elif op in {b'and_', b'add', b'sub'}:\\n assert len(tree) == 3\\n return getattr(operator, op)(walk(tree[1]), walk(tree[2]))\\n elif op == b'group':\\n return walk(tree[1])\\n elif op == b'ancestors':\\n return getstack(walk(tree[1]))\\n else:\\n raise error.ProgrammingError(b'illegal tree: %r' % tree)\\n \\n return [prefetched[r] for r in walk(tree)]\\n \\n \\n def getdescfromdrev(drev):\\n \\\"\\\"\\\"get description (commit message) from \\\"Differential Revision\\\"\\n \\n This is similar to differential.getcommitmessage API. But we only care\\n about limited fields: title, summary, test plan, and URL.\\n \\\"\\\"\\\"\\n title = drev[b'title']\\n summary = drev[b'summary'].rstrip()\\n testplan = drev[b'testPlan'].rstrip()\\n if testplan:\\n testplan = b'Test Plan:\\\\n%s' % testplan\\n uri = b'Differential Revision: %s' % drev[b'uri']\\n return b'\\\\n\\\\n'.join(filter(None, [title, summary, testplan, uri]))\\n \\n \\n def getdiffmeta(diff):\\n \\\"\\\"\\\"get commit metadata (date, node, user, p1) from a diff object\\n \\n The metadata could be \\\"hg:meta\\\", sent by phabsend, like:\\n \\n \\\"properties\\\": {\\n \\\"hg:meta\\\": {\\n \\\"branch\\\": \\\"default\\\",\\n \\\"date\\\": \\\"1499571514 25200\\\",\\n \\\"node\\\": \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\",\\n \\\"user\\\": \\\"Foo Bar \\u003cfoo@example.com\\u003e\\\",\\n \\\"parent\\\": \\\"6d0abad76b30e4724a37ab8721d630394070fe16\\\"\\n }\\n }\\n \\n Or converted from \\\"local:commits\\\", sent by \\\"arc\\\", like:\\n \\n \\\"properties\\\": {\\n \\\"local:commits\\\": {\\n \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\": {\\n \\\"author\\\": \\\"Foo Bar\\\",\\n \\\"authorEmail\\\": \\\"foo@example.com\\\"\\n \\\"branch\\\": \\\"default\\\",\\n \\\"commit\\\": \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\",\\n \\\"local\\\": \\\"1000\\\",\\n \\\"message\\\": \\\"...\\\",\\n \\\"parents\\\": [\\\"6d0abad76b30e4724a37ab8721d630394070fe16\\\"],\\n \\\"rev\\\": \\\"98c08acae292b2faf60a279b4189beb6cff1414d\\\",\\n \\\"summary\\\": \\\"...\\\",\\n \\\"tag\\\": \\\"\\\",\\n \\\"time\\\": 1499546314,\\n }\\n }\\n }\\n \\n Note: metadata extracted from \\\"local:commits\\\" will lose time zone\\n information.\\n \\\"\\\"\\\"\\n props = diff.get(b'properties') or {}\\n meta = props.get(b'hg:meta')\\n if not meta:\\n if props.get(b'local:commits'):\\n commit = sorted(props[b'local:commits'].values())[0]\\n meta = {}\\n if b'author' in commit and b'authorEmail' in commit:\\n meta[b'user'] = b'%s \\u003c%s\\u003e' % (\\n commit[b'author'],\\n commit[b'authorEmail'],\\n )\\n if b'time' in commit:\\n meta[b'date'] = b'%d 0' % int(commit[b'time'])\\n if b'branch' in commit:\\n meta[b'branch'] = commit[b'branch']\\n node = commit.get(b'commit', commit.get(b'rev'))\\n if node:\\n meta[b'node'] = node\\n if len(commit.get(b'parents', ())) \\u003e= 1:\\n meta[b'parent'] = commit[b'parents'][0]\\n else:\\n meta = {}\\n if b'date' not in meta and b'dateCreated' in diff:\\n meta[b'date'] = b'%s 0' % diff[b'dateCreated']\\n if b'branch' not in meta and diff.get(b'branch'):\\n meta[b'branch'] = diff[b'branch']\\n if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):\\n meta[b'parent'] = diff[b'sourceControlBaseRevision']\\n return meta\\n \\n \\n-def readpatch(repo, drevs, write):\\n+def readpatch(ui, drevs, write):\\n \\\"\\\"\\\"generate plain-text patch readable by 'hg import'\\n \\n write is usually ui.write. drevs is what \\\"querydrev\\\" returns, results of\\n \\\"differential.query\\\".\\n \\\"\\\"\\\"\\n # Prefetch hg:meta property for all diffs\\n diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))\\n- diffs = callconduit(repo.ui, b'differential.querydiffs', {b'ids': diffids})\\n+ diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})\\n \\n # Generate patch for each drev\\n for drev in drevs:\\n- repo.ui.note(_(b'reading D%s\\\\n') % drev[b'id'])\\n+ ui.note(_(b'reading D%s\\\\n') % drev[b'id'])\\n \\n diffid = max(int(v) for v in drev[b'diffs'])\\n- body = callconduit(\\n- repo.ui, b'differential.getrawdiff', {b'diffID': diffid}\\n- )\\n+ body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})\\n desc = getdescfromdrev(drev)\\n header = b'# HG changeset patch\\\\n'\\n \\n # Try to preserve metadata from hg:meta property. Write hg patch\\n # headers that can be read by the \\\"import\\\" command. See patchheadermap\\n # and extract in mercurial\\/patch.py for supported headers.\\n meta = getdiffmeta(diffs[b'%d' % diffid])\\n for k in _metanamemap.keys():\\n if k in meta:\\n header += b'# %s %s\\\\n' % (_metanamemap[k], meta[k])\\n \\n content = b'%s%s\\\\n%s' % (header, desc, body)\\n write(content)\\n \\n \\n @vcrcommand(\\n b'phabread',\\n [(b'', b'stack', False, _(b'read dependencies'))],\\n _(b'DREVSPEC [OPTIONS]'),\\n helpcategory=command.CATEGORY_IMPORT_EXPORT,\\n )\\n def phabread(ui, repo, spec, **opts):\\n \\\"\\\"\\\"print patches from Phabricator suitable for importing\\n \\n DREVSPEC could be a Differential Revision identity, like ``D123``, or just\\n the number ``123``. It could also have common operators like ``+``, ``-``,\\n ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to\\n select a stack.\\n \\n ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``\\n could be used to filter patches by status. For performance reason, they\\n only represent a subset of non-status selections and cannot be used alone.\\n \\n For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude\\n D2 and D4. ``:D9 & needsreview`` selects \\\"Needs Review\\\" revisions in a\\n stack up to D9.\\n \\n If --stack is given, follow dependencies information and read all patches.\\n It is equivalent to the ``:`` operator.\\n \\\"\\\"\\\"\\n opts = pycompat.byteskwargs(opts)\\n if opts.get(b'stack'):\\n spec = b':(%s)' % spec\\n drevs = querydrev(repo, spec)\\n- readpatch(repo, drevs, ui.write)\\n+ readpatch(repo.ui, drevs, ui.write)\\n \\n \\n @vcrcommand(\\n b'phabupdate',\\n [\\n (b'', b'accept', False, _(b'accept revisions')),\\n (b'', b'reject', False, _(b'reject revisions')),\\n (b'', b'abandon', False, _(b'abandon revisions')),\\n (b'', b'reclaim', False, _(b'reclaim revisions')),\\n (b'm', b'comment', b'', _(b'comment on the last revision')),\\n ],\\n _(b'DREVSPEC [OPTIONS]'),\\n helpcategory=command.CATEGORY_IMPORT_EXPORT,\\n )\\n def phabupdate(ui, repo, spec, **opts):\\n \\\"\\\"\\\"update Differential Revision in batch\\n \\n DREVSPEC selects revisions. See :hg:`help phabread` for its usage.\\n \\\"\\\"\\\"\\n opts = pycompat.byteskwargs(opts)\\n flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]\\n if len(flags) \\u003e 1:\\n raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))\\n \\n actions = []\\n for f in flags:\\n actions.append({b'type': f, b'value': True})\\n \\n drevs = querydrev(repo, spec)\\n for i, drev in enumerate(drevs):\\n if i + 1 == len(drevs) and opts.get(b'comment'):\\n actions.append({b'type': b'comment', b'value': opts[b'comment']})\\n if actions:\\n params = {\\n b'objectIdentifier': drev[b'phid'],\\n b'transactions': actions,\\n }\\n callconduit(ui, b'differential.revision.edit', params)\\n \\n \\n @eh.templatekeyword(b'phabreview', requires={b'ctx'})\\n def template_review(context, mapping):\\n \\\"\\\"\\\":phabreview: Object describing the review for this changeset.\\n Has attributes `url` and `id`.\\n \\\"\\\"\\\"\\n ctx = context.resource(mapping, b'ctx')\\n m = _differentialrevisiondescre.search(ctx.description())\\n if m:\\n return templateutil.hybriddict(\\n {b'url': m.group('url'), b'id': b\\\"D%s\\\" % m.group('id'),}\\n )\\n else:\\n tags = ctx.repo().nodetags(ctx.node())\\n for t in tags:\\n if _differentialrevisiontagre.match(t):\\n url = ctx.repo().ui.config(b'phabricator', b'url')\\n if not url.endswith(b'\\/'):\\n url += b'\\/'\\n url += t\\n \\n return templateutil.hybriddict({b'url': url, b'id': t,})\\n return None\\n \\n \\n @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})\\n def template_status(context, mapping):\\n \\\"\\\"\\\":phabstatus: String. Status of Phabricator differential.\\n \\\"\\\"\\\"\\n ctx = context.resource(mapping, b'ctx')\\n repo = context.resource(mapping, b'repo')\\n ui = context.resource(mapping, b'ui')\\n \\n rev = ctx.rev()\\n try:\\n drevid = getdrevmap(repo, [rev])[rev]\\n except KeyError:\\n return None\\n drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})\\n for drev in drevs:\\n if int(drev[b'id']) == drevid:\\n return templateutil.hybriddict(\\n {b'url': drev[b'uri'], b'status': drev[b'statusName'],}\\n )\\n return None\\n \\n \\n @show.showview(b'phabstatus', csettopic=b'work')\\n def phabstatusshowview(ui, repo, displayer):\\n \\\"\\\"\\\"Phabricator differiential status\\\"\\\"\\\"\\n revs = repo.revs('sort(_underway(), topo)')\\n drevmap = getdrevmap(repo, revs)\\n unknownrevs, drevids, revsbydrevid = [], set([]), {}\\n for rev, drevid in pycompat.iteritems(drevmap):\\n if drevid is not None:\\n drevids.add(drevid)\\n revsbydrevid.setdefault(drevid, set([])).add(rev)\\n else:\\n unknownrevs.append(rev)\\n \\n drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})\\n drevsbyrev = {}\\n for drev in drevs:\\n for rev in revsbydrevid[int(drev[b'id'])]:\\n drevsbyrev[rev] = drev\\n \\n def phabstatus(ctx):\\n drev = drevsbyrev[ctx.rev()]\\n status = ui.label(\\n b'%(statusName)s' % drev,\\n b'phabricator.status.%s' % _getstatusname(drev),\\n )\\n ui.write(b\\\"\\\\n%s %s\\\\n\\\" % (drev[b'uri'], status))\\n \\n revs -= smartset.baseset(unknownrevs)\\n revdag = graphmod.dagwalker(repo, revs)\\n \\n ui.setconfig(b'experimental', b'graphshorten', True)\\n displayer._exthook = phabstatus\\n nodelen = show.longestshortest(repo, revs)\\n logcmdutil.displaygraph(\\n ui,\\n repo,\\n revdag,\\n displayer,\\n graphmod.asciiedges,\\n props={b'nodelen': nodelen},\\n )\\n\"}]}],\"properties\":[]}},\"error_code\":null,\"error_info\":null}"
76 },
77 "headers": {
78 "x-frame-options": [
79 "Deny"
80 ],
81 "strict-transport-security": [
82 "max-age=0; includeSubdomains; preload"
83 ],
84 "transfer-encoding": [
85 "chunked"
86 ],
87 "cache-control": [
88 "no-store"
89 ],
90 "referrer-policy": [
91 "no-referrer"
92 ],
93 "x-content-type-options": [
94 "nosniff"
95 ],
96 "x-xss-protection": [
97 "1; mode=block"
98 ],
99 "server": [
100 "Apache/2.4.10 (Debian)"
101 ],
102 "date": [
103 "Wed, 04 Mar 2020 22:05:22 GMT"
104 ],
105 "content-type": [
106 "application/json"
107 ],
108 "expires": [
109 "Sat, 01 Jan 2000 00:00:00 GMT"
110 ]
111 },
112 "status": {
113 "message": "OK",
114 "code": 200
115 }
116 },
117 "request": {
118 "body": "params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22ids%22%3A+%5B20441%2C+20442%2C+20443%5D%7D&output=json&__conduit__=1",
119 "uri": "https://phab.mercurial-scm.org//api/differential.querydiffs",
120 "headers": {
121 "content-length": [
122 "165"
123 ],
124 "accept": [
125 "application/mercurial-0.1"
126 ],
127 "host": [
128 "phab.mercurial-scm.org"
129 ],
130 "user-agent": [
131 "mercurial/proto-1.0 (Mercurial 5.3+33-bb58931d0c4f)"
132 ],
133 "content-type": [
134 "application/x-www-form-urlencoded"
135 ]
136 },
137 "method": "POST"
138 }
139 },
140 {
141 "response": {
142 "body": {
143 "string": "{\"result\":\"diff --git a\\/hgext\\/phabricator.py b\\/hgext\\/phabricator.py\\n--- a\\/hgext\\/phabricator.py\\n+++ b\\/hgext\\/phabricator.py\\n@@ -1609,7 +1609,7 @@\\n return meta\\n \\n \\n-def readpatch(repo, drevs, write):\\n+def readpatch(ui, drevs, write):\\n \\\"\\\"\\\"generate plain-text patch readable by 'hg import'\\n \\n write is usually ui.write. drevs is what \\\"querydrev\\\" returns, results of\\n@@ -1617,16 +1617,14 @@\\n \\\"\\\"\\\"\\n # Prefetch hg:meta property for all diffs\\n diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs))\\n- diffs = callconduit(repo.ui, b'differential.querydiffs', {b'ids': diffids})\\n+ diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})\\n \\n # Generate patch for each drev\\n for drev in drevs:\\n- repo.ui.note(_(b'reading D%s\\\\n') % drev[b'id'])\\n+ ui.note(_(b'reading D%s\\\\n') % drev[b'id'])\\n \\n diffid = max(int(v) for v in drev[b'diffs'])\\n- body = callconduit(\\n- repo.ui, b'differential.getrawdiff', {b'diffID': diffid}\\n- )\\n+ body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})\\n desc = getdescfromdrev(drev)\\n header = b'# HG changeset patch\\\\n'\\n \\n@@ -1671,7 +1669,7 @@\\n if opts.get(b'stack'):\\n spec = b':(%s)' % spec\\n drevs = querydrev(repo, spec)\\n- readpatch(repo, drevs, ui.write)\\n+ readpatch(repo.ui, drevs, ui.write)\\n \\n \\n @vcrcommand(\\n\\n\",\"error_code\":null,\"error_info\":null}"
144 },
145 "headers": {
146 "x-frame-options": [
147 "Deny"
148 ],
149 "strict-transport-security": [
150 "max-age=0; includeSubdomains; preload"
151 ],
152 "transfer-encoding": [
153 "chunked"
154 ],
155 "cache-control": [
156 "no-store"
157 ],
158 "referrer-policy": [
159 "no-referrer"
160 ],
161 "x-content-type-options": [
162 "nosniff"
163 ],
164 "x-xss-protection": [
165 "1; mode=block"
166 ],
167 "server": [
168 "Apache/2.4.10 (Debian)"
169 ],
170 "date": [
171 "Wed, 04 Mar 2020 22:05:23 GMT"
172 ],
173 "content-type": [
174 "application/json"
175 ],
176 "expires": [
177 "Sat, 01 Jan 2000 00:00:00 GMT"
178 ]
179 },
180 "status": {
181 "message": "OK",
182 "code": 200
183 }
184 },
185 "request": {
186 "body": "params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22diffID%22%3A+20441%7D&output=json&__conduit__=1",
187 "uri": "https://phab.mercurial-scm.org//api/differential.getrawdiff",
188 "headers": {
189 "content-length": [
190 "144"
191 ],
192 "accept": [
193 "application/mercurial-0.1"
194 ],
195 "host": [
196 "phab.mercurial-scm.org"
197 ],
198 "user-agent": [
199 "mercurial/proto-1.0 (Mercurial 5.3+33-bb58931d0c4f)"
200 ],
201 "content-type": [
202 "application/x-www-form-urlencoded"
203 ]
204 },
205 "method": "POST"
206 }
207 },
208 {
209 "response": {
210 "body": {
211 "string": "{\"result\":\"diff --git a\\/hgext\\/phabricator.py b\\/hgext\\/phabricator.py\\n--- a\\/hgext\\/phabricator.py\\n+++ b\\/hgext\\/phabricator.py\\n@@ -1040,11 +1040,11 @@\\n return revision, diff\\n \\n \\n-def userphids(repo, names):\\n+def userphids(ui, names):\\n \\\"\\\"\\\"convert user names to PHIDs\\\"\\\"\\\"\\n names = [name.lower() for name in names]\\n query = {b'constraints': {b'usernames': names}}\\n- result = callconduit(repo.ui, b'user.search', query)\\n+ result = callconduit(ui, b'user.search', query)\\n # username not found is not an error of the API. So check if we have missed\\n # some names here.\\n data = result[b'data']\\n@@ -1127,10 +1127,13 @@\\n blockers = opts.get(b'blocker', [])\\n phids = []\\n if reviewers:\\n- phids.extend(userphids(repo, reviewers))\\n+ phids.extend(userphids(repo.ui, reviewers))\\n if blockers:\\n phids.extend(\\n- map(lambda phid: b'blocking(%s)' % phid, userphids(repo, blockers))\\n+ map(\\n+ lambda phid: b'blocking(%s)' % phid,\\n+ userphids(repo.ui, blockers),\\n+ )\\n )\\n if phids:\\n actions.append({b'type': b'reviewers.add', b'value': phids})\\n\\n\",\"error_code\":null,\"error_info\":null}"
212 },
213 "headers": {
214 "x-frame-options": [
215 "Deny"
216 ],
217 "strict-transport-security": [
218 "max-age=0; includeSubdomains; preload"
219 ],
220 "transfer-encoding": [
221 "chunked"
222 ],
223 "cache-control": [
224 "no-store"
225 ],
226 "referrer-policy": [
227 "no-referrer"
228 ],
229 "x-content-type-options": [
230 "nosniff"
231 ],
232 "x-xss-protection": [
233 "1; mode=block"
234 ],
235 "server": [
236 "Apache/2.4.10 (Debian)"
237 ],
238 "date": [
239 "Wed, 04 Mar 2020 22:05:23 GMT"
240 ],
241 "content-type": [
242 "application/json"
243 ],
244 "expires": [
245 "Sat, 01 Jan 2000 00:00:00 GMT"
246 ]
247 },
248 "status": {
249 "message": "OK",
250 "code": 200
251 }
252 },
253 "request": {
254 "body": "params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22diffID%22%3A+20443%7D&output=json&__conduit__=1",
255 "uri": "https://phab.mercurial-scm.org//api/differential.getrawdiff",
256 "headers": {
257 "content-length": [
258 "144"
259 ],
260 "accept": [
261 "application/mercurial-0.1"
262 ],
263 "host": [
264 "phab.mercurial-scm.org"
265 ],
266 "user-agent": [
267 "mercurial/proto-1.0 (Mercurial 5.3+33-bb58931d0c4f)"
268 ],
269 "content-type": [
270 "application/x-www-form-urlencoded"
271 ]
272 },
273 "method": "POST"
274 }
275 },
276 {
277 "response": {
278 "body": {
279 "string": "{\"result\":\"diff --git a\\/hgext\\/phabricator.py b\\/hgext\\/phabricator.py\\n--- a\\/hgext\\/phabricator.py\\n+++ b\\/hgext\\/phabricator.py\\n@@ -1183,7 +1183,7 @@\\n else:\\n # Nothing changed. But still set \\\"newrevphid\\\" so the next revision\\n # could depend on this one and \\\"newrevid\\\" for the summary line.\\n- newrevphid = querydrev(repo, b'%d' % revid)[0][b'phid']\\n+ newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']\\n newrevid = revid\\n action = b'skipped'\\n \\n@@ -1398,7 +1398,7 @@\\n return drevs, ancestordrevs\\n \\n \\n-def querydrev(repo, spec):\\n+def querydrev(ui, spec):\\n \\\"\\\"\\\"return a list of \\\"Differential Revision\\\" dicts\\n \\n spec is a string using a simple query language, see docstring in phabread\\n@@ -1449,7 +1449,7 @@\\n key = (params.get(b'ids') or params.get(b'phids') or [None])[0]\\n if key in prefetched:\\n return prefetched[key]\\n- drevs = callconduit(repo.ui, b'differential.query', params)\\n+ drevs = callconduit(ui, b'differential.query', params)\\n # Fill prefetched with the result\\n for drev in drevs:\\n prefetched[drev[b'phid']] = drev\\n@@ -1486,7 +1486,7 @@\\n drevs, ancestordrevs = _prefetchdrevs(tree)\\n \\n # developer config: phabricator.batchsize\\n- batchsize = repo.ui.configint(b'phabricator', b'batchsize')\\n+ batchsize = ui.configint(b'phabricator', b'batchsize')\\n \\n # Prefetch Differential Revisions in batch\\n tofetch = set(drevs)\\n@@ -1668,7 +1668,7 @@\\n opts = pycompat.byteskwargs(opts)\\n if opts.get(b'stack'):\\n spec = b':(%s)' % spec\\n- drevs = querydrev(repo, spec)\\n+ drevs = querydrev(repo.ui, spec)\\n readpatch(repo.ui, drevs, ui.write)\\n \\n \\n@@ -1698,7 +1698,7 @@\\n for f in flags:\\n actions.append({b'type': f, b'value': True})\\n \\n- drevs = querydrev(repo, spec)\\n+ drevs = querydrev(repo.ui, spec)\\n for i, drev in enumerate(drevs):\\n if i + 1 == len(drevs) and opts.get(b'comment'):\\n actions.append({b'type': b'comment', b'value': opts[b'comment']})\\n\\n\",\"error_code\":null,\"error_info\":null}"
280 },
281 "headers": {
282 "x-frame-options": [
283 "Deny"
284 ],
285 "strict-transport-security": [
286 "max-age=0; includeSubdomains; preload"
287 ],
288 "transfer-encoding": [
289 "chunked"
290 ],
291 "cache-control": [
292 "no-store"
293 ],
294 "referrer-policy": [
295 "no-referrer"
296 ],
297 "x-content-type-options": [
298 "nosniff"
299 ],
300 "x-xss-protection": [
301 "1; mode=block"
302 ],
303 "server": [
304 "Apache/2.4.10 (Debian)"
305 ],
306 "date": [
307 "Wed, 04 Mar 2020 22:05:23 GMT"
308 ],
309 "content-type": [
310 "application/json"
311 ],
312 "expires": [
313 "Sat, 01 Jan 2000 00:00:00 GMT"
314 ]
315 },
316 "status": {
317 "message": "OK",
318 "code": 200
319 }
320 },
321 "request": {
322 "body": "params=%7B%22__conduit__%22%3A+%7B%22token%22%3A+%22cli-hahayouwish%22%7D%2C+%22diffID%22%3A+20442%7D&output=json&__conduit__=1",
323 "uri": "https://phab.mercurial-scm.org//api/differential.getrawdiff",
324 "headers": {
325 "content-length": [
326 "144"
327 ],
328 "accept": [
329 "application/mercurial-0.1"
330 ],
331 "host": [
332 "phab.mercurial-scm.org"
333 ],
334 "user-agent": [
335 "mercurial/proto-1.0 (Mercurial 5.3+33-bb58931d0c4f)"
336 ],
337 "content-type": [
338 "application/x-www-form-urlencoded"
339 ]
340 },
341 "method": "POST"
342 }
343 }
344 ]
345 } No newline at end of file
@@ -1,1920 +1,1918 b''
1 # phabricator.py - simple Phabricator integration
1 # phabricator.py - simple Phabricator integration
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """simple Phabricator integration (EXPERIMENTAL)
7 """simple Phabricator integration (EXPERIMENTAL)
8
8
9 This extension provides a ``phabsend`` command which sends a stack of
9 This extension provides a ``phabsend`` command which sends a stack of
10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
12 to update statuses in batch.
12 to update statuses in batch.
13
13
14 A "phabstatus" view for :hg:`show` is also provided; it displays status
14 A "phabstatus" view for :hg:`show` is also provided; it displays status
15 information of Phabricator differentials associated with unfinished
15 information of Phabricator differentials associated with unfinished
16 changesets.
16 changesets.
17
17
18 By default, Phabricator requires ``Test Plan`` which might prevent some
18 By default, Phabricator requires ``Test Plan`` which might prevent some
19 changeset from being sent. The requirement could be disabled by changing
19 changeset from being sent. The requirement could be disabled by changing
20 ``differential.require-test-plan-field`` config server side.
20 ``differential.require-test-plan-field`` config server side.
21
21
22 Config::
22 Config::
23
23
24 [phabricator]
24 [phabricator]
25 # Phabricator URL
25 # Phabricator URL
26 url = https://phab.example.com/
26 url = https://phab.example.com/
27
27
28 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
28 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
29 # callsign is "FOO".
29 # callsign is "FOO".
30 callsign = FOO
30 callsign = FOO
31
31
32 # curl command to use. If not set (default), use builtin HTTP library to
32 # curl command to use. If not set (default), use builtin HTTP library to
33 # communicate. If set, use the specified curl command. This could be useful
33 # communicate. If set, use the specified curl command. This could be useful
34 # if you need to specify advanced options that is not easily supported by
34 # if you need to specify advanced options that is not easily supported by
35 # the internal library.
35 # the internal library.
36 curlcmd = curl --connect-timeout 2 --retry 3 --silent
36 curlcmd = curl --connect-timeout 2 --retry 3 --silent
37
37
38 [auth]
38 [auth]
39 example.schemes = https
39 example.schemes = https
40 example.prefix = phab.example.com
40 example.prefix = phab.example.com
41
41
42 # API token. Get it from https://$HOST/conduit/login/
42 # API token. Get it from https://$HOST/conduit/login/
43 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
43 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
44 """
44 """
45
45
46 from __future__ import absolute_import
46 from __future__ import absolute_import
47
47
48 import base64
48 import base64
49 import contextlib
49 import contextlib
50 import hashlib
50 import hashlib
51 import itertools
51 import itertools
52 import json
52 import json
53 import mimetypes
53 import mimetypes
54 import operator
54 import operator
55 import re
55 import re
56
56
57 from mercurial.node import bin, nullid
57 from mercurial.node import bin, nullid
58 from mercurial.i18n import _
58 from mercurial.i18n import _
59 from mercurial.pycompat import getattr
59 from mercurial.pycompat import getattr
60 from mercurial.thirdparty import attr
60 from mercurial.thirdparty import attr
61 from mercurial import (
61 from mercurial import (
62 cmdutil,
62 cmdutil,
63 context,
63 context,
64 encoding,
64 encoding,
65 error,
65 error,
66 exthelper,
66 exthelper,
67 graphmod,
67 graphmod,
68 httpconnection as httpconnectionmod,
68 httpconnection as httpconnectionmod,
69 localrepo,
69 localrepo,
70 logcmdutil,
70 logcmdutil,
71 match,
71 match,
72 mdiff,
72 mdiff,
73 obsutil,
73 obsutil,
74 parser,
74 parser,
75 patch,
75 patch,
76 phases,
76 phases,
77 pycompat,
77 pycompat,
78 scmutil,
78 scmutil,
79 smartset,
79 smartset,
80 tags,
80 tags,
81 templatefilters,
81 templatefilters,
82 templateutil,
82 templateutil,
83 url as urlmod,
83 url as urlmod,
84 util,
84 util,
85 )
85 )
86 from mercurial.utils import (
86 from mercurial.utils import (
87 procutil,
87 procutil,
88 stringutil,
88 stringutil,
89 )
89 )
90 from . import show
90 from . import show
91
91
92
92
93 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
93 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
94 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
94 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
95 # be specifying the version(s) of Mercurial they are tested with, or
95 # be specifying the version(s) of Mercurial they are tested with, or
96 # leave the attribute unspecified.
96 # leave the attribute unspecified.
97 testedwith = b'ships-with-hg-core'
97 testedwith = b'ships-with-hg-core'
98
98
99 eh = exthelper.exthelper()
99 eh = exthelper.exthelper()
100
100
101 cmdtable = eh.cmdtable
101 cmdtable = eh.cmdtable
102 command = eh.command
102 command = eh.command
103 configtable = eh.configtable
103 configtable = eh.configtable
104 templatekeyword = eh.templatekeyword
104 templatekeyword = eh.templatekeyword
105 uisetup = eh.finaluisetup
105 uisetup = eh.finaluisetup
106
106
107 # developer config: phabricator.batchsize
107 # developer config: phabricator.batchsize
108 eh.configitem(
108 eh.configitem(
109 b'phabricator', b'batchsize', default=12,
109 b'phabricator', b'batchsize', default=12,
110 )
110 )
111 eh.configitem(
111 eh.configitem(
112 b'phabricator', b'callsign', default=None,
112 b'phabricator', b'callsign', default=None,
113 )
113 )
114 eh.configitem(
114 eh.configitem(
115 b'phabricator', b'curlcmd', default=None,
115 b'phabricator', b'curlcmd', default=None,
116 )
116 )
117 # developer config: phabricator.repophid
117 # developer config: phabricator.repophid
118 eh.configitem(
118 eh.configitem(
119 b'phabricator', b'repophid', default=None,
119 b'phabricator', b'repophid', default=None,
120 )
120 )
121 eh.configitem(
121 eh.configitem(
122 b'phabricator', b'url', default=None,
122 b'phabricator', b'url', default=None,
123 )
123 )
124 eh.configitem(
124 eh.configitem(
125 b'phabsend', b'confirm', default=False,
125 b'phabsend', b'confirm', default=False,
126 )
126 )
127 eh.configitem(
127 eh.configitem(
128 b'phabimport', b'secret', default=False,
128 b'phabimport', b'secret', default=False,
129 )
129 )
130 eh.configitem(
130 eh.configitem(
131 b'phabimport', b'obsolete', default=False,
131 b'phabimport', b'obsolete', default=False,
132 )
132 )
133
133
134 colortable = {
134 colortable = {
135 b'phabricator.action.created': b'green',
135 b'phabricator.action.created': b'green',
136 b'phabricator.action.skipped': b'magenta',
136 b'phabricator.action.skipped': b'magenta',
137 b'phabricator.action.updated': b'magenta',
137 b'phabricator.action.updated': b'magenta',
138 b'phabricator.desc': b'',
138 b'phabricator.desc': b'',
139 b'phabricator.drev': b'bold',
139 b'phabricator.drev': b'bold',
140 b'phabricator.node': b'',
140 b'phabricator.node': b'',
141 b'phabricator.status.abandoned': b'magenta dim',
141 b'phabricator.status.abandoned': b'magenta dim',
142 b'phabricator.status.accepted': b'green bold',
142 b'phabricator.status.accepted': b'green bold',
143 b'phabricator.status.closed': b'green',
143 b'phabricator.status.closed': b'green',
144 b'phabricator.status.needsreview': b'yellow',
144 b'phabricator.status.needsreview': b'yellow',
145 b'phabricator.status.needsrevision': b'red',
145 b'phabricator.status.needsrevision': b'red',
146 b'phabricator.status.changesplanned': b'red',
146 b'phabricator.status.changesplanned': b'red',
147 }
147 }
148
148
149 _VCR_FLAGS = [
149 _VCR_FLAGS = [
150 (
150 (
151 b'',
151 b'',
152 b'test-vcr',
152 b'test-vcr',
153 b'',
153 b'',
154 _(
154 _(
155 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
155 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
156 b', otherwise will mock all http requests using the specified vcr file.'
156 b', otherwise will mock all http requests using the specified vcr file.'
157 b' (ADVANCED)'
157 b' (ADVANCED)'
158 ),
158 ),
159 ),
159 ),
160 ]
160 ]
161
161
162
162
163 @eh.wrapfunction(localrepo, "loadhgrc")
163 @eh.wrapfunction(localrepo, "loadhgrc")
164 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):
164 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):
165 """Load ``.arcconfig`` content into a ui instance on repository open.
165 """Load ``.arcconfig`` content into a ui instance on repository open.
166 """
166 """
167 result = False
167 result = False
168 arcconfig = {}
168 arcconfig = {}
169
169
170 try:
170 try:
171 # json.loads only accepts bytes from 3.6+
171 # json.loads only accepts bytes from 3.6+
172 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
172 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
173 # json.loads only returns unicode strings
173 # json.loads only returns unicode strings
174 arcconfig = pycompat.rapply(
174 arcconfig = pycompat.rapply(
175 lambda x: encoding.unitolocal(x)
175 lambda x: encoding.unitolocal(x)
176 if isinstance(x, pycompat.unicode)
176 if isinstance(x, pycompat.unicode)
177 else x,
177 else x,
178 pycompat.json_loads(rawparams),
178 pycompat.json_loads(rawparams),
179 )
179 )
180
180
181 result = True
181 result = True
182 except ValueError:
182 except ValueError:
183 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
183 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
184 except IOError:
184 except IOError:
185 pass
185 pass
186
186
187 cfg = util.sortdict()
187 cfg = util.sortdict()
188
188
189 if b"repository.callsign" in arcconfig:
189 if b"repository.callsign" in arcconfig:
190 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
190 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
191
191
192 if b"phabricator.uri" in arcconfig:
192 if b"phabricator.uri" in arcconfig:
193 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
193 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
194
194
195 if cfg:
195 if cfg:
196 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
196 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
197
197
198 return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg/hgrc
198 return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg/hgrc
199
199
200
200
201 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
201 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
202 fullflags = flags + _VCR_FLAGS
202 fullflags = flags + _VCR_FLAGS
203
203
204 def hgmatcher(r1, r2):
204 def hgmatcher(r1, r2):
205 if r1.uri != r2.uri or r1.method != r2.method:
205 if r1.uri != r2.uri or r1.method != r2.method:
206 return False
206 return False
207 r1params = util.urlreq.parseqs(r1.body)
207 r1params = util.urlreq.parseqs(r1.body)
208 r2params = util.urlreq.parseqs(r2.body)
208 r2params = util.urlreq.parseqs(r2.body)
209 for key in r1params:
209 for key in r1params:
210 if key not in r2params:
210 if key not in r2params:
211 return False
211 return False
212 value = r1params[key][0]
212 value = r1params[key][0]
213 # we want to compare json payloads without worrying about ordering
213 # we want to compare json payloads without worrying about ordering
214 if value.startswith(b'{') and value.endswith(b'}'):
214 if value.startswith(b'{') and value.endswith(b'}'):
215 r1json = pycompat.json_loads(value)
215 r1json = pycompat.json_loads(value)
216 r2json = pycompat.json_loads(r2params[key][0])
216 r2json = pycompat.json_loads(r2params[key][0])
217 if r1json != r2json:
217 if r1json != r2json:
218 return False
218 return False
219 elif r2params[key][0] != value:
219 elif r2params[key][0] != value:
220 return False
220 return False
221 return True
221 return True
222
222
223 def sanitiserequest(request):
223 def sanitiserequest(request):
224 request.body = re.sub(
224 request.body = re.sub(
225 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
225 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
226 )
226 )
227 return request
227 return request
228
228
229 def sanitiseresponse(response):
229 def sanitiseresponse(response):
230 if 'set-cookie' in response['headers']:
230 if 'set-cookie' in response['headers']:
231 del response['headers']['set-cookie']
231 del response['headers']['set-cookie']
232 return response
232 return response
233
233
234 def decorate(fn):
234 def decorate(fn):
235 def inner(*args, **kwargs):
235 def inner(*args, **kwargs):
236 cassette = pycompat.fsdecode(kwargs.pop('test_vcr', None))
236 cassette = pycompat.fsdecode(kwargs.pop('test_vcr', None))
237 if cassette:
237 if cassette:
238 import hgdemandimport
238 import hgdemandimport
239
239
240 with hgdemandimport.deactivated():
240 with hgdemandimport.deactivated():
241 import vcr as vcrmod
241 import vcr as vcrmod
242 import vcr.stubs as stubs
242 import vcr.stubs as stubs
243
243
244 vcr = vcrmod.VCR(
244 vcr = vcrmod.VCR(
245 serializer='json',
245 serializer='json',
246 before_record_request=sanitiserequest,
246 before_record_request=sanitiserequest,
247 before_record_response=sanitiseresponse,
247 before_record_response=sanitiseresponse,
248 custom_patches=[
248 custom_patches=[
249 (
249 (
250 urlmod,
250 urlmod,
251 'httpconnection',
251 'httpconnection',
252 stubs.VCRHTTPConnection,
252 stubs.VCRHTTPConnection,
253 ),
253 ),
254 (
254 (
255 urlmod,
255 urlmod,
256 'httpsconnection',
256 'httpsconnection',
257 stubs.VCRHTTPSConnection,
257 stubs.VCRHTTPSConnection,
258 ),
258 ),
259 ],
259 ],
260 )
260 )
261 vcr.register_matcher('hgmatcher', hgmatcher)
261 vcr.register_matcher('hgmatcher', hgmatcher)
262 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
262 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
263 return fn(*args, **kwargs)
263 return fn(*args, **kwargs)
264 return fn(*args, **kwargs)
264 return fn(*args, **kwargs)
265
265
266 cmd = util.checksignature(inner, depth=2)
266 cmd = util.checksignature(inner, depth=2)
267 cmd.__name__ = fn.__name__
267 cmd.__name__ = fn.__name__
268 cmd.__doc__ = fn.__doc__
268 cmd.__doc__ = fn.__doc__
269
269
270 return command(
270 return command(
271 name,
271 name,
272 fullflags,
272 fullflags,
273 spec,
273 spec,
274 helpcategory=helpcategory,
274 helpcategory=helpcategory,
275 optionalrepo=optionalrepo,
275 optionalrepo=optionalrepo,
276 )(cmd)
276 )(cmd)
277
277
278 return decorate
278 return decorate
279
279
280
280
281 def urlencodenested(params):
281 def urlencodenested(params):
282 """like urlencode, but works with nested parameters.
282 """like urlencode, but works with nested parameters.
283
283
284 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
284 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
285 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
285 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
286 urlencode. Note: the encoding is consistent with PHP's http_build_query.
286 urlencode. Note: the encoding is consistent with PHP's http_build_query.
287 """
287 """
288 flatparams = util.sortdict()
288 flatparams = util.sortdict()
289
289
290 def process(prefix, obj):
290 def process(prefix, obj):
291 if isinstance(obj, bool):
291 if isinstance(obj, bool):
292 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
292 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
293 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
293 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
294 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
294 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
295 if items is None:
295 if items is None:
296 flatparams[prefix] = obj
296 flatparams[prefix] = obj
297 else:
297 else:
298 for k, v in items(obj):
298 for k, v in items(obj):
299 if prefix:
299 if prefix:
300 process(b'%s[%s]' % (prefix, k), v)
300 process(b'%s[%s]' % (prefix, k), v)
301 else:
301 else:
302 process(k, v)
302 process(k, v)
303
303
304 process(b'', params)
304 process(b'', params)
305 return util.urlreq.urlencode(flatparams)
305 return util.urlreq.urlencode(flatparams)
306
306
307
307
308 def readurltoken(ui):
308 def readurltoken(ui):
309 """return conduit url, token and make sure they exist
309 """return conduit url, token and make sure they exist
310
310
311 Currently read from [auth] config section. In the future, it might
311 Currently read from [auth] config section. In the future, it might
312 make sense to read from .arcconfig and .arcrc as well.
312 make sense to read from .arcconfig and .arcrc as well.
313 """
313 """
314 url = ui.config(b'phabricator', b'url')
314 url = ui.config(b'phabricator', b'url')
315 if not url:
315 if not url:
316 raise error.Abort(
316 raise error.Abort(
317 _(b'config %s.%s is required') % (b'phabricator', b'url')
317 _(b'config %s.%s is required') % (b'phabricator', b'url')
318 )
318 )
319
319
320 res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)
320 res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)
321 token = None
321 token = None
322
322
323 if res:
323 if res:
324 group, auth = res
324 group, auth = res
325
325
326 ui.debug(b"using auth.%s.* for authentication\n" % group)
326 ui.debug(b"using auth.%s.* for authentication\n" % group)
327
327
328 token = auth.get(b'phabtoken')
328 token = auth.get(b'phabtoken')
329
329
330 if not token:
330 if not token:
331 raise error.Abort(
331 raise error.Abort(
332 _(b'Can\'t find conduit token associated to %s') % (url,)
332 _(b'Can\'t find conduit token associated to %s') % (url,)
333 )
333 )
334
334
335 return url, token
335 return url, token
336
336
337
337
338 def callconduit(ui, name, params):
338 def callconduit(ui, name, params):
339 """call Conduit API, params is a dict. return json.loads result, or None"""
339 """call Conduit API, params is a dict. return json.loads result, or None"""
340 host, token = readurltoken(ui)
340 host, token = readurltoken(ui)
341 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
341 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
342 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
342 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
343 params = params.copy()
343 params = params.copy()
344 params[b'__conduit__'] = {
344 params[b'__conduit__'] = {
345 b'token': token,
345 b'token': token,
346 }
346 }
347 rawdata = {
347 rawdata = {
348 b'params': templatefilters.json(params),
348 b'params': templatefilters.json(params),
349 b'output': b'json',
349 b'output': b'json',
350 b'__conduit__': 1,
350 b'__conduit__': 1,
351 }
351 }
352 data = urlencodenested(rawdata)
352 data = urlencodenested(rawdata)
353 curlcmd = ui.config(b'phabricator', b'curlcmd')
353 curlcmd = ui.config(b'phabricator', b'curlcmd')
354 if curlcmd:
354 if curlcmd:
355 sin, sout = procutil.popen2(
355 sin, sout = procutil.popen2(
356 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
356 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
357 )
357 )
358 sin.write(data)
358 sin.write(data)
359 sin.close()
359 sin.close()
360 body = sout.read()
360 body = sout.read()
361 else:
361 else:
362 urlopener = urlmod.opener(ui, authinfo)
362 urlopener = urlmod.opener(ui, authinfo)
363 request = util.urlreq.request(pycompat.strurl(url), data=data)
363 request = util.urlreq.request(pycompat.strurl(url), data=data)
364 with contextlib.closing(urlopener.open(request)) as rsp:
364 with contextlib.closing(urlopener.open(request)) as rsp:
365 body = rsp.read()
365 body = rsp.read()
366 ui.debug(b'Conduit Response: %s\n' % body)
366 ui.debug(b'Conduit Response: %s\n' % body)
367 parsed = pycompat.rapply(
367 parsed = pycompat.rapply(
368 lambda x: encoding.unitolocal(x)
368 lambda x: encoding.unitolocal(x)
369 if isinstance(x, pycompat.unicode)
369 if isinstance(x, pycompat.unicode)
370 else x,
370 else x,
371 # json.loads only accepts bytes from py3.6+
371 # json.loads only accepts bytes from py3.6+
372 pycompat.json_loads(encoding.unifromlocal(body)),
372 pycompat.json_loads(encoding.unifromlocal(body)),
373 )
373 )
374 if parsed.get(b'error_code'):
374 if parsed.get(b'error_code'):
375 msg = _(b'Conduit Error (%s): %s') % (
375 msg = _(b'Conduit Error (%s): %s') % (
376 parsed[b'error_code'],
376 parsed[b'error_code'],
377 parsed[b'error_info'],
377 parsed[b'error_info'],
378 )
378 )
379 raise error.Abort(msg)
379 raise error.Abort(msg)
380 return parsed[b'result']
380 return parsed[b'result']
381
381
382
382
383 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
383 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
384 def debugcallconduit(ui, repo, name):
384 def debugcallconduit(ui, repo, name):
385 """call Conduit API
385 """call Conduit API
386
386
387 Call parameters are read from stdin as a JSON blob. Result will be written
387 Call parameters are read from stdin as a JSON blob. Result will be written
388 to stdout as a JSON blob.
388 to stdout as a JSON blob.
389 """
389 """
390 # json.loads only accepts bytes from 3.6+
390 # json.loads only accepts bytes from 3.6+
391 rawparams = encoding.unifromlocal(ui.fin.read())
391 rawparams = encoding.unifromlocal(ui.fin.read())
392 # json.loads only returns unicode strings
392 # json.loads only returns unicode strings
393 params = pycompat.rapply(
393 params = pycompat.rapply(
394 lambda x: encoding.unitolocal(x)
394 lambda x: encoding.unitolocal(x)
395 if isinstance(x, pycompat.unicode)
395 if isinstance(x, pycompat.unicode)
396 else x,
396 else x,
397 pycompat.json_loads(rawparams),
397 pycompat.json_loads(rawparams),
398 )
398 )
399 # json.dumps only accepts unicode strings
399 # json.dumps only accepts unicode strings
400 result = pycompat.rapply(
400 result = pycompat.rapply(
401 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
401 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
402 callconduit(ui, name, params),
402 callconduit(ui, name, params),
403 )
403 )
404 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
404 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
405 ui.write(b'%s\n' % encoding.unitolocal(s))
405 ui.write(b'%s\n' % encoding.unitolocal(s))
406
406
407
407
408 def getrepophid(repo):
408 def getrepophid(repo):
409 """given callsign, return repository PHID or None"""
409 """given callsign, return repository PHID or None"""
410 # developer config: phabricator.repophid
410 # developer config: phabricator.repophid
411 repophid = repo.ui.config(b'phabricator', b'repophid')
411 repophid = repo.ui.config(b'phabricator', b'repophid')
412 if repophid:
412 if repophid:
413 return repophid
413 return repophid
414 callsign = repo.ui.config(b'phabricator', b'callsign')
414 callsign = repo.ui.config(b'phabricator', b'callsign')
415 if not callsign:
415 if not callsign:
416 return None
416 return None
417 query = callconduit(
417 query = callconduit(
418 repo.ui,
418 repo.ui,
419 b'diffusion.repository.search',
419 b'diffusion.repository.search',
420 {b'constraints': {b'callsigns': [callsign]}},
420 {b'constraints': {b'callsigns': [callsign]}},
421 )
421 )
422 if len(query[b'data']) == 0:
422 if len(query[b'data']) == 0:
423 return None
423 return None
424 repophid = query[b'data'][0][b'phid']
424 repophid = query[b'data'][0][b'phid']
425 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
425 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
426 return repophid
426 return repophid
427
427
428
428
429 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
429 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
430 _differentialrevisiondescre = re.compile(
430 _differentialrevisiondescre = re.compile(
431 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
431 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
432 )
432 )
433
433
434
434
435 def getoldnodedrevmap(repo, nodelist):
435 def getoldnodedrevmap(repo, nodelist):
436 """find previous nodes that has been sent to Phabricator
436 """find previous nodes that has been sent to Phabricator
437
437
438 return {node: (oldnode, Differential diff, Differential Revision ID)}
438 return {node: (oldnode, Differential diff, Differential Revision ID)}
439 for node in nodelist with known previous sent versions, or associated
439 for node in nodelist with known previous sent versions, or associated
440 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
440 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
441 be ``None``.
441 be ``None``.
442
442
443 Examines commit messages like "Differential Revision:" to get the
443 Examines commit messages like "Differential Revision:" to get the
444 association information.
444 association information.
445
445
446 If such commit message line is not found, examines all precursors and their
446 If such commit message line is not found, examines all precursors and their
447 tags. Tags with format like "D1234" are considered a match and the node
447 tags. Tags with format like "D1234" are considered a match and the node
448 with that tag, and the number after "D" (ex. 1234) will be returned.
448 with that tag, and the number after "D" (ex. 1234) will be returned.
449
449
450 The ``old node``, if not None, is guaranteed to be the last diff of
450 The ``old node``, if not None, is guaranteed to be the last diff of
451 corresponding Differential Revision, and exist in the repo.
451 corresponding Differential Revision, and exist in the repo.
452 """
452 """
453 unfi = repo.unfiltered()
453 unfi = repo.unfiltered()
454 has_node = unfi.changelog.index.has_node
454 has_node = unfi.changelog.index.has_node
455
455
456 result = {} # {node: (oldnode?, lastdiff?, drev)}
456 result = {} # {node: (oldnode?, lastdiff?, drev)}
457 toconfirm = {} # {node: (force, {precnode}, drev)}
457 toconfirm = {} # {node: (force, {precnode}, drev)}
458 for node in nodelist:
458 for node in nodelist:
459 ctx = unfi[node]
459 ctx = unfi[node]
460 # For tags like "D123", put them into "toconfirm" to verify later
460 # For tags like "D123", put them into "toconfirm" to verify later
461 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
461 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
462 for n in precnodes:
462 for n in precnodes:
463 if has_node(n):
463 if has_node(n):
464 for tag in unfi.nodetags(n):
464 for tag in unfi.nodetags(n):
465 m = _differentialrevisiontagre.match(tag)
465 m = _differentialrevisiontagre.match(tag)
466 if m:
466 if m:
467 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
467 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
468 break
468 break
469 else:
469 else:
470 continue # move to next predecessor
470 continue # move to next predecessor
471 break # found a tag, stop
471 break # found a tag, stop
472 else:
472 else:
473 # Check commit message
473 # Check commit message
474 m = _differentialrevisiondescre.search(ctx.description())
474 m = _differentialrevisiondescre.search(ctx.description())
475 if m:
475 if m:
476 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
476 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
477
477
478 # Double check if tags are genuine by collecting all old nodes from
478 # Double check if tags are genuine by collecting all old nodes from
479 # Phabricator, and expect precursors overlap with it.
479 # Phabricator, and expect precursors overlap with it.
480 if toconfirm:
480 if toconfirm:
481 drevs = [drev for force, precs, drev in toconfirm.values()]
481 drevs = [drev for force, precs, drev in toconfirm.values()]
482 alldiffs = callconduit(
482 alldiffs = callconduit(
483 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
483 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
484 )
484 )
485 getnode = lambda d: bin(getdiffmeta(d).get(b'node', b'')) or None
485 getnode = lambda d: bin(getdiffmeta(d).get(b'node', b'')) or None
486 for newnode, (force, precset, drev) in toconfirm.items():
486 for newnode, (force, precset, drev) in toconfirm.items():
487 diffs = [
487 diffs = [
488 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
488 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
489 ]
489 ]
490
490
491 # "precursors" as known by Phabricator
491 # "precursors" as known by Phabricator
492 phprecset = {getnode(d) for d in diffs}
492 phprecset = {getnode(d) for d in diffs}
493
493
494 # Ignore if precursors (Phabricator and local repo) do not overlap,
494 # Ignore if precursors (Phabricator and local repo) do not overlap,
495 # and force is not set (when commit message says nothing)
495 # and force is not set (when commit message says nothing)
496 if not force and not bool(phprecset & precset):
496 if not force and not bool(phprecset & precset):
497 tagname = b'D%d' % drev
497 tagname = b'D%d' % drev
498 tags.tag(
498 tags.tag(
499 repo,
499 repo,
500 tagname,
500 tagname,
501 nullid,
501 nullid,
502 message=None,
502 message=None,
503 user=None,
503 user=None,
504 date=None,
504 date=None,
505 local=True,
505 local=True,
506 )
506 )
507 unfi.ui.warn(
507 unfi.ui.warn(
508 _(
508 _(
509 b'D%d: local tag removed - does not match '
509 b'D%d: local tag removed - does not match '
510 b'Differential history\n'
510 b'Differential history\n'
511 )
511 )
512 % drev
512 % drev
513 )
513 )
514 continue
514 continue
515
515
516 # Find the last node using Phabricator metadata, and make sure it
516 # Find the last node using Phabricator metadata, and make sure it
517 # exists in the repo
517 # exists in the repo
518 oldnode = lastdiff = None
518 oldnode = lastdiff = None
519 if diffs:
519 if diffs:
520 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
520 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
521 oldnode = getnode(lastdiff)
521 oldnode = getnode(lastdiff)
522 if oldnode and not has_node(oldnode):
522 if oldnode and not has_node(oldnode):
523 oldnode = None
523 oldnode = None
524
524
525 result[newnode] = (oldnode, lastdiff, drev)
525 result[newnode] = (oldnode, lastdiff, drev)
526
526
527 return result
527 return result
528
528
529
529
530 def getdrevmap(repo, revs):
530 def getdrevmap(repo, revs):
531 """Return a dict mapping each rev in `revs` to their Differential Revision
531 """Return a dict mapping each rev in `revs` to their Differential Revision
532 ID or None.
532 ID or None.
533 """
533 """
534 result = {}
534 result = {}
535 for rev in revs:
535 for rev in revs:
536 result[rev] = None
536 result[rev] = None
537 ctx = repo[rev]
537 ctx = repo[rev]
538 # Check commit message
538 # Check commit message
539 m = _differentialrevisiondescre.search(ctx.description())
539 m = _differentialrevisiondescre.search(ctx.description())
540 if m:
540 if m:
541 result[rev] = int(m.group('id'))
541 result[rev] = int(m.group('id'))
542 continue
542 continue
543 # Check tags
543 # Check tags
544 for tag in repo.nodetags(ctx.node()):
544 for tag in repo.nodetags(ctx.node()):
545 m = _differentialrevisiontagre.match(tag)
545 m = _differentialrevisiontagre.match(tag)
546 if m:
546 if m:
547 result[rev] = int(m.group(1))
547 result[rev] = int(m.group(1))
548 break
548 break
549
549
550 return result
550 return result
551
551
552
552
553 def getdiff(ctx, diffopts):
553 def getdiff(ctx, diffopts):
554 """plain-text diff without header (user, commit message, etc)"""
554 """plain-text diff without header (user, commit message, etc)"""
555 output = util.stringio()
555 output = util.stringio()
556 for chunk, _label in patch.diffui(
556 for chunk, _label in patch.diffui(
557 ctx.repo(), ctx.p1().node(), ctx.node(), None, opts=diffopts
557 ctx.repo(), ctx.p1().node(), ctx.node(), None, opts=diffopts
558 ):
558 ):
559 output.write(chunk)
559 output.write(chunk)
560 return output.getvalue()
560 return output.getvalue()
561
561
562
562
563 class DiffChangeType(object):
563 class DiffChangeType(object):
564 ADD = 1
564 ADD = 1
565 CHANGE = 2
565 CHANGE = 2
566 DELETE = 3
566 DELETE = 3
567 MOVE_AWAY = 4
567 MOVE_AWAY = 4
568 COPY_AWAY = 5
568 COPY_AWAY = 5
569 MOVE_HERE = 6
569 MOVE_HERE = 6
570 COPY_HERE = 7
570 COPY_HERE = 7
571 MULTICOPY = 8
571 MULTICOPY = 8
572
572
573
573
574 class DiffFileType(object):
574 class DiffFileType(object):
575 TEXT = 1
575 TEXT = 1
576 IMAGE = 2
576 IMAGE = 2
577 BINARY = 3
577 BINARY = 3
578
578
579
579
580 @attr.s
580 @attr.s
581 class phabhunk(dict):
581 class phabhunk(dict):
582 """Represents a Differential hunk, which is owned by a Differential change
582 """Represents a Differential hunk, which is owned by a Differential change
583 """
583 """
584
584
585 oldOffset = attr.ib(default=0) # camelcase-required
585 oldOffset = attr.ib(default=0) # camelcase-required
586 oldLength = attr.ib(default=0) # camelcase-required
586 oldLength = attr.ib(default=0) # camelcase-required
587 newOffset = attr.ib(default=0) # camelcase-required
587 newOffset = attr.ib(default=0) # camelcase-required
588 newLength = attr.ib(default=0) # camelcase-required
588 newLength = attr.ib(default=0) # camelcase-required
589 corpus = attr.ib(default='')
589 corpus = attr.ib(default='')
590 # These get added to the phabchange's equivalents
590 # These get added to the phabchange's equivalents
591 addLines = attr.ib(default=0) # camelcase-required
591 addLines = attr.ib(default=0) # camelcase-required
592 delLines = attr.ib(default=0) # camelcase-required
592 delLines = attr.ib(default=0) # camelcase-required
593
593
594
594
595 @attr.s
595 @attr.s
596 class phabchange(object):
596 class phabchange(object):
597 """Represents a Differential change, owns Differential hunks and owned by a
597 """Represents a Differential change, owns Differential hunks and owned by a
598 Differential diff. Each one represents one file in a diff.
598 Differential diff. Each one represents one file in a diff.
599 """
599 """
600
600
601 currentPath = attr.ib(default=None) # camelcase-required
601 currentPath = attr.ib(default=None) # camelcase-required
602 oldPath = attr.ib(default=None) # camelcase-required
602 oldPath = attr.ib(default=None) # camelcase-required
603 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
603 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
604 metadata = attr.ib(default=attr.Factory(dict))
604 metadata = attr.ib(default=attr.Factory(dict))
605 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
605 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
606 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
606 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
607 type = attr.ib(default=DiffChangeType.CHANGE)
607 type = attr.ib(default=DiffChangeType.CHANGE)
608 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
608 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
609 commitHash = attr.ib(default=None) # camelcase-required
609 commitHash = attr.ib(default=None) # camelcase-required
610 addLines = attr.ib(default=0) # camelcase-required
610 addLines = attr.ib(default=0) # camelcase-required
611 delLines = attr.ib(default=0) # camelcase-required
611 delLines = attr.ib(default=0) # camelcase-required
612 hunks = attr.ib(default=attr.Factory(list))
612 hunks = attr.ib(default=attr.Factory(list))
613
613
614 def copynewmetadatatoold(self):
614 def copynewmetadatatoold(self):
615 for key in list(self.metadata.keys()):
615 for key in list(self.metadata.keys()):
616 newkey = key.replace(b'new:', b'old:')
616 newkey = key.replace(b'new:', b'old:')
617 self.metadata[newkey] = self.metadata[key]
617 self.metadata[newkey] = self.metadata[key]
618
618
619 def addoldmode(self, value):
619 def addoldmode(self, value):
620 self.oldProperties[b'unix:filemode'] = value
620 self.oldProperties[b'unix:filemode'] = value
621
621
622 def addnewmode(self, value):
622 def addnewmode(self, value):
623 self.newProperties[b'unix:filemode'] = value
623 self.newProperties[b'unix:filemode'] = value
624
624
625 def addhunk(self, hunk):
625 def addhunk(self, hunk):
626 if not isinstance(hunk, phabhunk):
626 if not isinstance(hunk, phabhunk):
627 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
627 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
628 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
628 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
629 # It's useful to include these stats since the Phab web UI shows them,
629 # It's useful to include these stats since the Phab web UI shows them,
630 # and uses them to estimate how large a change a Revision is. Also used
630 # and uses them to estimate how large a change a Revision is. Also used
631 # in email subjects for the [+++--] bit.
631 # in email subjects for the [+++--] bit.
632 self.addLines += hunk.addLines
632 self.addLines += hunk.addLines
633 self.delLines += hunk.delLines
633 self.delLines += hunk.delLines
634
634
635
635
636 @attr.s
636 @attr.s
637 class phabdiff(object):
637 class phabdiff(object):
638 """Represents a Differential diff, owns Differential changes. Corresponds
638 """Represents a Differential diff, owns Differential changes. Corresponds
639 to a commit.
639 to a commit.
640 """
640 """
641
641
642 # Doesn't seem to be any reason to send this (output of uname -n)
642 # Doesn't seem to be any reason to send this (output of uname -n)
643 sourceMachine = attr.ib(default=b'') # camelcase-required
643 sourceMachine = attr.ib(default=b'') # camelcase-required
644 sourcePath = attr.ib(default=b'/') # camelcase-required
644 sourcePath = attr.ib(default=b'/') # camelcase-required
645 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
645 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
646 sourceControlPath = attr.ib(default=b'/') # camelcase-required
646 sourceControlPath = attr.ib(default=b'/') # camelcase-required
647 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
647 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
648 branch = attr.ib(default=b'default')
648 branch = attr.ib(default=b'default')
649 bookmark = attr.ib(default=None)
649 bookmark = attr.ib(default=None)
650 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
650 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
651 lintStatus = attr.ib(default=b'none') # camelcase-required
651 lintStatus = attr.ib(default=b'none') # camelcase-required
652 unitStatus = attr.ib(default=b'none') # camelcase-required
652 unitStatus = attr.ib(default=b'none') # camelcase-required
653 changes = attr.ib(default=attr.Factory(dict))
653 changes = attr.ib(default=attr.Factory(dict))
654 repositoryPHID = attr.ib(default=None) # camelcase-required
654 repositoryPHID = attr.ib(default=None) # camelcase-required
655
655
656 def addchange(self, change):
656 def addchange(self, change):
657 if not isinstance(change, phabchange):
657 if not isinstance(change, phabchange):
658 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
658 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
659 self.changes[change.currentPath] = pycompat.byteskwargs(
659 self.changes[change.currentPath] = pycompat.byteskwargs(
660 attr.asdict(change)
660 attr.asdict(change)
661 )
661 )
662
662
663
663
664 def maketext(pchange, ctx, fname):
664 def maketext(pchange, ctx, fname):
665 """populate the phabchange for a text file"""
665 """populate the phabchange for a text file"""
666 repo = ctx.repo()
666 repo = ctx.repo()
667 fmatcher = match.exact([fname])
667 fmatcher = match.exact([fname])
668 diffopts = mdiff.diffopts(git=True, context=32767)
668 diffopts = mdiff.diffopts(git=True, context=32767)
669 _pfctx, _fctx, header, fhunks = next(
669 _pfctx, _fctx, header, fhunks = next(
670 patch.diffhunks(repo, ctx.p1(), ctx, fmatcher, opts=diffopts)
670 patch.diffhunks(repo, ctx.p1(), ctx, fmatcher, opts=diffopts)
671 )
671 )
672
672
673 for fhunk in fhunks:
673 for fhunk in fhunks:
674 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
674 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
675 corpus = b''.join(lines[1:])
675 corpus = b''.join(lines[1:])
676 shunk = list(header)
676 shunk = list(header)
677 shunk.extend(lines)
677 shunk.extend(lines)
678 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
678 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
679 patch.diffstatdata(util.iterlines(shunk))
679 patch.diffstatdata(util.iterlines(shunk))
680 )
680 )
681 pchange.addhunk(
681 pchange.addhunk(
682 phabhunk(
682 phabhunk(
683 oldOffset,
683 oldOffset,
684 oldLength,
684 oldLength,
685 newOffset,
685 newOffset,
686 newLength,
686 newLength,
687 corpus,
687 corpus,
688 addLines,
688 addLines,
689 delLines,
689 delLines,
690 )
690 )
691 )
691 )
692
692
693
693
694 def uploadchunks(fctx, fphid):
694 def uploadchunks(fctx, fphid):
695 """upload large binary files as separate chunks.
695 """upload large binary files as separate chunks.
696 Phab requests chunking over 8MiB, and splits into 4MiB chunks
696 Phab requests chunking over 8MiB, and splits into 4MiB chunks
697 """
697 """
698 ui = fctx.repo().ui
698 ui = fctx.repo().ui
699 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
699 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
700 with ui.makeprogress(
700 with ui.makeprogress(
701 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
701 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
702 ) as progress:
702 ) as progress:
703 for chunk in chunks:
703 for chunk in chunks:
704 progress.increment()
704 progress.increment()
705 if chunk[b'complete']:
705 if chunk[b'complete']:
706 continue
706 continue
707 bstart = int(chunk[b'byteStart'])
707 bstart = int(chunk[b'byteStart'])
708 bend = int(chunk[b'byteEnd'])
708 bend = int(chunk[b'byteEnd'])
709 callconduit(
709 callconduit(
710 ui,
710 ui,
711 b'file.uploadchunk',
711 b'file.uploadchunk',
712 {
712 {
713 b'filePHID': fphid,
713 b'filePHID': fphid,
714 b'byteStart': bstart,
714 b'byteStart': bstart,
715 b'data': base64.b64encode(fctx.data()[bstart:bend]),
715 b'data': base64.b64encode(fctx.data()[bstart:bend]),
716 b'dataEncoding': b'base64',
716 b'dataEncoding': b'base64',
717 },
717 },
718 )
718 )
719
719
720
720
721 def uploadfile(fctx):
721 def uploadfile(fctx):
722 """upload binary files to Phabricator"""
722 """upload binary files to Phabricator"""
723 repo = fctx.repo()
723 repo = fctx.repo()
724 ui = repo.ui
724 ui = repo.ui
725 fname = fctx.path()
725 fname = fctx.path()
726 size = fctx.size()
726 size = fctx.size()
727 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
727 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
728
728
729 # an allocate call is required first to see if an upload is even required
729 # an allocate call is required first to see if an upload is even required
730 # (Phab might already have it) and to determine if chunking is needed
730 # (Phab might already have it) and to determine if chunking is needed
731 allocateparams = {
731 allocateparams = {
732 b'name': fname,
732 b'name': fname,
733 b'contentLength': size,
733 b'contentLength': size,
734 b'contentHash': fhash,
734 b'contentHash': fhash,
735 }
735 }
736 filealloc = callconduit(ui, b'file.allocate', allocateparams)
736 filealloc = callconduit(ui, b'file.allocate', allocateparams)
737 fphid = filealloc[b'filePHID']
737 fphid = filealloc[b'filePHID']
738
738
739 if filealloc[b'upload']:
739 if filealloc[b'upload']:
740 ui.write(_(b'uploading %s\n') % bytes(fctx))
740 ui.write(_(b'uploading %s\n') % bytes(fctx))
741 if not fphid:
741 if not fphid:
742 uploadparams = {
742 uploadparams = {
743 b'name': fname,
743 b'name': fname,
744 b'data_base64': base64.b64encode(fctx.data()),
744 b'data_base64': base64.b64encode(fctx.data()),
745 }
745 }
746 fphid = callconduit(ui, b'file.upload', uploadparams)
746 fphid = callconduit(ui, b'file.upload', uploadparams)
747 else:
747 else:
748 uploadchunks(fctx, fphid)
748 uploadchunks(fctx, fphid)
749 else:
749 else:
750 ui.debug(b'server already has %s\n' % bytes(fctx))
750 ui.debug(b'server already has %s\n' % bytes(fctx))
751
751
752 if not fphid:
752 if not fphid:
753 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
753 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
754
754
755 return fphid
755 return fphid
756
756
757
757
758 def addoldbinary(pchange, oldfctx, fctx):
758 def addoldbinary(pchange, oldfctx, fctx):
759 """add the metadata for the previous version of a binary file to the
759 """add the metadata for the previous version of a binary file to the
760 phabchange for the new version
760 phabchange for the new version
761
761
762 ``oldfctx`` is the previous version of the file; ``fctx`` is the new
762 ``oldfctx`` is the previous version of the file; ``fctx`` is the new
763 version of the file, or None if the file is being removed.
763 version of the file, or None if the file is being removed.
764 """
764 """
765 if not fctx or fctx.cmp(oldfctx):
765 if not fctx or fctx.cmp(oldfctx):
766 # Files differ, add the old one
766 # Files differ, add the old one
767 pchange.metadata[b'old:file:size'] = oldfctx.size()
767 pchange.metadata[b'old:file:size'] = oldfctx.size()
768 mimeguess, _enc = mimetypes.guess_type(
768 mimeguess, _enc = mimetypes.guess_type(
769 encoding.unifromlocal(oldfctx.path())
769 encoding.unifromlocal(oldfctx.path())
770 )
770 )
771 if mimeguess:
771 if mimeguess:
772 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
772 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
773 mimeguess
773 mimeguess
774 )
774 )
775 fphid = uploadfile(oldfctx)
775 fphid = uploadfile(oldfctx)
776 pchange.metadata[b'old:binary-phid'] = fphid
776 pchange.metadata[b'old:binary-phid'] = fphid
777 else:
777 else:
778 # If it's left as IMAGE/BINARY web UI might try to display it
778 # If it's left as IMAGE/BINARY web UI might try to display it
779 pchange.fileType = DiffFileType.TEXT
779 pchange.fileType = DiffFileType.TEXT
780 pchange.copynewmetadatatoold()
780 pchange.copynewmetadatatoold()
781
781
782
782
783 def makebinary(pchange, fctx):
783 def makebinary(pchange, fctx):
784 """populate the phabchange for a binary file"""
784 """populate the phabchange for a binary file"""
785 pchange.fileType = DiffFileType.BINARY
785 pchange.fileType = DiffFileType.BINARY
786 fphid = uploadfile(fctx)
786 fphid = uploadfile(fctx)
787 pchange.metadata[b'new:binary-phid'] = fphid
787 pchange.metadata[b'new:binary-phid'] = fphid
788 pchange.metadata[b'new:file:size'] = fctx.size()
788 pchange.metadata[b'new:file:size'] = fctx.size()
789 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
789 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
790 if mimeguess:
790 if mimeguess:
791 mimeguess = pycompat.bytestr(mimeguess)
791 mimeguess = pycompat.bytestr(mimeguess)
792 pchange.metadata[b'new:file:mime-type'] = mimeguess
792 pchange.metadata[b'new:file:mime-type'] = mimeguess
793 if mimeguess.startswith(b'image/'):
793 if mimeguess.startswith(b'image/'):
794 pchange.fileType = DiffFileType.IMAGE
794 pchange.fileType = DiffFileType.IMAGE
795
795
796
796
797 # Copied from mercurial/patch.py
797 # Copied from mercurial/patch.py
798 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
798 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
799
799
800
800
801 def notutf8(fctx):
801 def notutf8(fctx):
802 """detect non-UTF-8 text files since Phabricator requires them to be marked
802 """detect non-UTF-8 text files since Phabricator requires them to be marked
803 as binary
803 as binary
804 """
804 """
805 try:
805 try:
806 fctx.data().decode('utf-8')
806 fctx.data().decode('utf-8')
807 return False
807 return False
808 except UnicodeDecodeError:
808 except UnicodeDecodeError:
809 fctx.repo().ui.write(
809 fctx.repo().ui.write(
810 _(b'file %s detected as non-UTF-8, marked as binary\n')
810 _(b'file %s detected as non-UTF-8, marked as binary\n')
811 % fctx.path()
811 % fctx.path()
812 )
812 )
813 return True
813 return True
814
814
815
815
816 def addremoved(pdiff, ctx, removed):
816 def addremoved(pdiff, ctx, removed):
817 """add removed files to the phabdiff. Shouldn't include moves"""
817 """add removed files to the phabdiff. Shouldn't include moves"""
818 for fname in removed:
818 for fname in removed:
819 pchange = phabchange(
819 pchange = phabchange(
820 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
820 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
821 )
821 )
822 pchange.addoldmode(gitmode[ctx.p1()[fname].flags()])
822 pchange.addoldmode(gitmode[ctx.p1()[fname].flags()])
823 oldfctx = ctx.p1()[fname]
823 oldfctx = ctx.p1()[fname]
824 if not (oldfctx.isbinary() or notutf8(oldfctx)):
824 if not (oldfctx.isbinary() or notutf8(oldfctx)):
825 maketext(pchange, ctx, fname)
825 maketext(pchange, ctx, fname)
826
826
827 pdiff.addchange(pchange)
827 pdiff.addchange(pchange)
828
828
829
829
830 def addmodified(pdiff, ctx, modified):
830 def addmodified(pdiff, ctx, modified):
831 """add modified files to the phabdiff"""
831 """add modified files to the phabdiff"""
832 for fname in modified:
832 for fname in modified:
833 fctx = ctx[fname]
833 fctx = ctx[fname]
834 oldfctx = fctx.p1()
834 oldfctx = fctx.p1()
835 pchange = phabchange(currentPath=fname, oldPath=fname)
835 pchange = phabchange(currentPath=fname, oldPath=fname)
836 filemode = gitmode[ctx[fname].flags()]
836 filemode = gitmode[ctx[fname].flags()]
837 originalmode = gitmode[ctx.p1()[fname].flags()]
837 originalmode = gitmode[ctx.p1()[fname].flags()]
838 if filemode != originalmode:
838 if filemode != originalmode:
839 pchange.addoldmode(originalmode)
839 pchange.addoldmode(originalmode)
840 pchange.addnewmode(filemode)
840 pchange.addnewmode(filemode)
841
841
842 if (
842 if (
843 fctx.isbinary()
843 fctx.isbinary()
844 or notutf8(fctx)
844 or notutf8(fctx)
845 or oldfctx.isbinary()
845 or oldfctx.isbinary()
846 or notutf8(oldfctx)
846 or notutf8(oldfctx)
847 ):
847 ):
848 makebinary(pchange, fctx)
848 makebinary(pchange, fctx)
849 addoldbinary(pchange, fctx.p1(), fctx)
849 addoldbinary(pchange, fctx.p1(), fctx)
850 else:
850 else:
851 maketext(pchange, ctx, fname)
851 maketext(pchange, ctx, fname)
852
852
853 pdiff.addchange(pchange)
853 pdiff.addchange(pchange)
854
854
855
855
856 def addadded(pdiff, ctx, added, removed):
856 def addadded(pdiff, ctx, added, removed):
857 """add file adds to the phabdiff, both new files and copies/moves"""
857 """add file adds to the phabdiff, both new files and copies/moves"""
858 # Keep track of files that've been recorded as moved/copied, so if there are
858 # Keep track of files that've been recorded as moved/copied, so if there are
859 # additional copies we can mark them (moves get removed from removed)
859 # additional copies we can mark them (moves get removed from removed)
860 copiedchanges = {}
860 copiedchanges = {}
861 movedchanges = {}
861 movedchanges = {}
862 for fname in added:
862 for fname in added:
863 fctx = ctx[fname]
863 fctx = ctx[fname]
864 oldfctx = None
864 oldfctx = None
865 pchange = phabchange(currentPath=fname)
865 pchange = phabchange(currentPath=fname)
866
866
867 filemode = gitmode[ctx[fname].flags()]
867 filemode = gitmode[ctx[fname].flags()]
868 renamed = fctx.renamed()
868 renamed = fctx.renamed()
869
869
870 if renamed:
870 if renamed:
871 originalfname = renamed[0]
871 originalfname = renamed[0]
872 oldfctx = ctx.p1()[originalfname]
872 oldfctx = ctx.p1()[originalfname]
873 originalmode = gitmode[oldfctx.flags()]
873 originalmode = gitmode[oldfctx.flags()]
874 pchange.oldPath = originalfname
874 pchange.oldPath = originalfname
875
875
876 if originalfname in removed:
876 if originalfname in removed:
877 origpchange = phabchange(
877 origpchange = phabchange(
878 currentPath=originalfname,
878 currentPath=originalfname,
879 oldPath=originalfname,
879 oldPath=originalfname,
880 type=DiffChangeType.MOVE_AWAY,
880 type=DiffChangeType.MOVE_AWAY,
881 awayPaths=[fname],
881 awayPaths=[fname],
882 )
882 )
883 movedchanges[originalfname] = origpchange
883 movedchanges[originalfname] = origpchange
884 removed.remove(originalfname)
884 removed.remove(originalfname)
885 pchange.type = DiffChangeType.MOVE_HERE
885 pchange.type = DiffChangeType.MOVE_HERE
886 elif originalfname in movedchanges:
886 elif originalfname in movedchanges:
887 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
887 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
888 movedchanges[originalfname].awayPaths.append(fname)
888 movedchanges[originalfname].awayPaths.append(fname)
889 pchange.type = DiffChangeType.COPY_HERE
889 pchange.type = DiffChangeType.COPY_HERE
890 else: # pure copy
890 else: # pure copy
891 if originalfname not in copiedchanges:
891 if originalfname not in copiedchanges:
892 origpchange = phabchange(
892 origpchange = phabchange(
893 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
893 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
894 )
894 )
895 copiedchanges[originalfname] = origpchange
895 copiedchanges[originalfname] = origpchange
896 else:
896 else:
897 origpchange = copiedchanges[originalfname]
897 origpchange = copiedchanges[originalfname]
898 origpchange.awayPaths.append(fname)
898 origpchange.awayPaths.append(fname)
899 pchange.type = DiffChangeType.COPY_HERE
899 pchange.type = DiffChangeType.COPY_HERE
900
900
901 if filemode != originalmode:
901 if filemode != originalmode:
902 pchange.addoldmode(originalmode)
902 pchange.addoldmode(originalmode)
903 pchange.addnewmode(filemode)
903 pchange.addnewmode(filemode)
904 else: # Brand-new file
904 else: # Brand-new file
905 pchange.addnewmode(gitmode[fctx.flags()])
905 pchange.addnewmode(gitmode[fctx.flags()])
906 pchange.type = DiffChangeType.ADD
906 pchange.type = DiffChangeType.ADD
907
907
908 if (
908 if (
909 fctx.isbinary()
909 fctx.isbinary()
910 or notutf8(fctx)
910 or notutf8(fctx)
911 or (oldfctx and (oldfctx.isbinary() or notutf8(oldfctx)))
911 or (oldfctx and (oldfctx.isbinary() or notutf8(oldfctx)))
912 ):
912 ):
913 makebinary(pchange, fctx)
913 makebinary(pchange, fctx)
914 if renamed:
914 if renamed:
915 addoldbinary(pchange, oldfctx, fctx)
915 addoldbinary(pchange, oldfctx, fctx)
916 else:
916 else:
917 maketext(pchange, ctx, fname)
917 maketext(pchange, ctx, fname)
918
918
919 pdiff.addchange(pchange)
919 pdiff.addchange(pchange)
920
920
921 for _path, copiedchange in copiedchanges.items():
921 for _path, copiedchange in copiedchanges.items():
922 pdiff.addchange(copiedchange)
922 pdiff.addchange(copiedchange)
923 for _path, movedchange in movedchanges.items():
923 for _path, movedchange in movedchanges.items():
924 pdiff.addchange(movedchange)
924 pdiff.addchange(movedchange)
925
925
926
926
927 def creatediff(ctx):
927 def creatediff(ctx):
928 """create a Differential Diff"""
928 """create a Differential Diff"""
929 repo = ctx.repo()
929 repo = ctx.repo()
930 repophid = getrepophid(repo)
930 repophid = getrepophid(repo)
931 # Create a "Differential Diff" via "differential.creatediff" API
931 # Create a "Differential Diff" via "differential.creatediff" API
932 pdiff = phabdiff(
932 pdiff = phabdiff(
933 sourceControlBaseRevision=b'%s' % ctx.p1().hex(),
933 sourceControlBaseRevision=b'%s' % ctx.p1().hex(),
934 branch=b'%s' % ctx.branch(),
934 branch=b'%s' % ctx.branch(),
935 )
935 )
936 modified, added, removed, _d, _u, _i, _c = ctx.p1().status(ctx)
936 modified, added, removed, _d, _u, _i, _c = ctx.p1().status(ctx)
937 # addadded will remove moved files from removed, so addremoved won't get
937 # addadded will remove moved files from removed, so addremoved won't get
938 # them
938 # them
939 addadded(pdiff, ctx, added, removed)
939 addadded(pdiff, ctx, added, removed)
940 addmodified(pdiff, ctx, modified)
940 addmodified(pdiff, ctx, modified)
941 addremoved(pdiff, ctx, removed)
941 addremoved(pdiff, ctx, removed)
942 if repophid:
942 if repophid:
943 pdiff.repositoryPHID = repophid
943 pdiff.repositoryPHID = repophid
944 diff = callconduit(
944 diff = callconduit(
945 repo.ui,
945 repo.ui,
946 b'differential.creatediff',
946 b'differential.creatediff',
947 pycompat.byteskwargs(attr.asdict(pdiff)),
947 pycompat.byteskwargs(attr.asdict(pdiff)),
948 )
948 )
949 if not diff:
949 if not diff:
950 raise error.Abort(_(b'cannot create diff for %s') % ctx)
950 raise error.Abort(_(b'cannot create diff for %s') % ctx)
951 return diff
951 return diff
952
952
953
953
954 def writediffproperties(ctx, diff):
954 def writediffproperties(ctx, diff):
955 """write metadata to diff so patches could be applied losslessly"""
955 """write metadata to diff so patches could be applied losslessly"""
956 # creatediff returns with a diffid but query returns with an id
956 # creatediff returns with a diffid but query returns with an id
957 diffid = diff.get(b'diffid', diff.get(b'id'))
957 diffid = diff.get(b'diffid', diff.get(b'id'))
958 params = {
958 params = {
959 b'diff_id': diffid,
959 b'diff_id': diffid,
960 b'name': b'hg:meta',
960 b'name': b'hg:meta',
961 b'data': templatefilters.json(
961 b'data': templatefilters.json(
962 {
962 {
963 b'user': ctx.user(),
963 b'user': ctx.user(),
964 b'date': b'%d %d' % ctx.date(),
964 b'date': b'%d %d' % ctx.date(),
965 b'branch': ctx.branch(),
965 b'branch': ctx.branch(),
966 b'node': ctx.hex(),
966 b'node': ctx.hex(),
967 b'parent': ctx.p1().hex(),
967 b'parent': ctx.p1().hex(),
968 }
968 }
969 ),
969 ),
970 }
970 }
971 callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)
971 callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)
972
972
973 params = {
973 params = {
974 b'diff_id': diffid,
974 b'diff_id': diffid,
975 b'name': b'local:commits',
975 b'name': b'local:commits',
976 b'data': templatefilters.json(
976 b'data': templatefilters.json(
977 {
977 {
978 ctx.hex(): {
978 ctx.hex(): {
979 b'author': stringutil.person(ctx.user()),
979 b'author': stringutil.person(ctx.user()),
980 b'authorEmail': stringutil.email(ctx.user()),
980 b'authorEmail': stringutil.email(ctx.user()),
981 b'time': int(ctx.date()[0]),
981 b'time': int(ctx.date()[0]),
982 b'commit': ctx.hex(),
982 b'commit': ctx.hex(),
983 b'parents': [ctx.p1().hex()],
983 b'parents': [ctx.p1().hex()],
984 b'branch': ctx.branch(),
984 b'branch': ctx.branch(),
985 },
985 },
986 }
986 }
987 ),
987 ),
988 }
988 }
989 callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)
989 callconduit(ctx.repo().ui, b'differential.setdiffproperty', params)
990
990
991
991
992 def createdifferentialrevision(
992 def createdifferentialrevision(
993 ctx,
993 ctx,
994 revid=None,
994 revid=None,
995 parentrevphid=None,
995 parentrevphid=None,
996 oldnode=None,
996 oldnode=None,
997 olddiff=None,
997 olddiff=None,
998 actions=None,
998 actions=None,
999 comment=None,
999 comment=None,
1000 ):
1000 ):
1001 """create or update a Differential Revision
1001 """create or update a Differential Revision
1002
1002
1003 If revid is None, create a new Differential Revision, otherwise update
1003 If revid is None, create a new Differential Revision, otherwise update
1004 revid. If parentrevphid is not None, set it as a dependency.
1004 revid. If parentrevphid is not None, set it as a dependency.
1005
1005
1006 If oldnode is not None, check if the patch content (without commit message
1006 If oldnode is not None, check if the patch content (without commit message
1007 and metadata) has changed before creating another diff.
1007 and metadata) has changed before creating another diff.
1008
1008
1009 If actions is not None, they will be appended to the transaction.
1009 If actions is not None, they will be appended to the transaction.
1010 """
1010 """
1011 repo = ctx.repo()
1011 repo = ctx.repo()
1012 if oldnode:
1012 if oldnode:
1013 diffopts = mdiff.diffopts(git=True, context=32767)
1013 diffopts = mdiff.diffopts(git=True, context=32767)
1014 oldctx = repo.unfiltered()[oldnode]
1014 oldctx = repo.unfiltered()[oldnode]
1015 neednewdiff = getdiff(ctx, diffopts) != getdiff(oldctx, diffopts)
1015 neednewdiff = getdiff(ctx, diffopts) != getdiff(oldctx, diffopts)
1016 else:
1016 else:
1017 neednewdiff = True
1017 neednewdiff = True
1018
1018
1019 transactions = []
1019 transactions = []
1020 if neednewdiff:
1020 if neednewdiff:
1021 diff = creatediff(ctx)
1021 diff = creatediff(ctx)
1022 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1022 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1023 if comment:
1023 if comment:
1024 transactions.append({b'type': b'comment', b'value': comment})
1024 transactions.append({b'type': b'comment', b'value': comment})
1025 else:
1025 else:
1026 # Even if we don't need to upload a new diff because the patch content
1026 # Even if we don't need to upload a new diff because the patch content
1027 # does not change. We might still need to update its metadata so
1027 # does not change. We might still need to update its metadata so
1028 # pushers could know the correct node metadata.
1028 # pushers could know the correct node metadata.
1029 assert olddiff
1029 assert olddiff
1030 diff = olddiff
1030 diff = olddiff
1031 writediffproperties(ctx, diff)
1031 writediffproperties(ctx, diff)
1032
1032
1033 # Set the parent Revision every time, so commit re-ordering is picked-up
1033 # Set the parent Revision every time, so commit re-ordering is picked-up
1034 if parentrevphid:
1034 if parentrevphid:
1035 transactions.append(
1035 transactions.append(
1036 {b'type': b'parents.set', b'value': [parentrevphid]}
1036 {b'type': b'parents.set', b'value': [parentrevphid]}
1037 )
1037 )
1038
1038
1039 if actions:
1039 if actions:
1040 transactions += actions
1040 transactions += actions
1041
1041
1042 # Parse commit message and update related fields.
1042 # Parse commit message and update related fields.
1043 desc = ctx.description()
1043 desc = ctx.description()
1044 info = callconduit(
1044 info = callconduit(
1045 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1045 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1046 )
1046 )
1047 for k, v in info[b'fields'].items():
1047 for k, v in info[b'fields'].items():
1048 if k in [b'title', b'summary', b'testPlan']:
1048 if k in [b'title', b'summary', b'testPlan']:
1049 transactions.append({b'type': k, b'value': v})
1049 transactions.append({b'type': k, b'value': v})
1050
1050
1051 params = {b'transactions': transactions}
1051 params = {b'transactions': transactions}
1052 if revid is not None:
1052 if revid is not None:
1053 # Update an existing Differential Revision
1053 # Update an existing Differential Revision
1054 params[b'objectIdentifier'] = revid
1054 params[b'objectIdentifier'] = revid
1055
1055
1056 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1056 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1057 if not revision:
1057 if not revision:
1058 raise error.Abort(_(b'cannot create revision for %s') % ctx)
1058 raise error.Abort(_(b'cannot create revision for %s') % ctx)
1059
1059
1060 return revision, diff
1060 return revision, diff
1061
1061
1062
1062
1063 def userphids(ui, names):
1063 def userphids(ui, names):
1064 """convert user names to PHIDs"""
1064 """convert user names to PHIDs"""
1065 names = [name.lower() for name in names]
1065 names = [name.lower() for name in names]
1066 query = {b'constraints': {b'usernames': names}}
1066 query = {b'constraints': {b'usernames': names}}
1067 result = callconduit(ui, b'user.search', query)
1067 result = callconduit(ui, b'user.search', query)
1068 # username not found is not an error of the API. So check if we have missed
1068 # username not found is not an error of the API. So check if we have missed
1069 # some names here.
1069 # some names here.
1070 data = result[b'data']
1070 data = result[b'data']
1071 resolved = {entry[b'fields'][b'username'].lower() for entry in data}
1071 resolved = {entry[b'fields'][b'username'].lower() for entry in data}
1072 unresolved = set(names) - resolved
1072 unresolved = set(names) - resolved
1073 if unresolved:
1073 if unresolved:
1074 raise error.Abort(
1074 raise error.Abort(
1075 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1075 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1076 )
1076 )
1077 return [entry[b'phid'] for entry in data]
1077 return [entry[b'phid'] for entry in data]
1078
1078
1079
1079
1080 @vcrcommand(
1080 @vcrcommand(
1081 b'phabsend',
1081 b'phabsend',
1082 [
1082 [
1083 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1083 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1084 (b'', b'amend', True, _(b'update commit messages')),
1084 (b'', b'amend', True, _(b'update commit messages')),
1085 (b'', b'reviewer', [], _(b'specify reviewers')),
1085 (b'', b'reviewer', [], _(b'specify reviewers')),
1086 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1086 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1087 (
1087 (
1088 b'm',
1088 b'm',
1089 b'comment',
1089 b'comment',
1090 b'',
1090 b'',
1091 _(b'add a comment to Revisions with new/updated Diffs'),
1091 _(b'add a comment to Revisions with new/updated Diffs'),
1092 ),
1092 ),
1093 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1093 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1094 ],
1094 ],
1095 _(b'REV [OPTIONS]'),
1095 _(b'REV [OPTIONS]'),
1096 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1096 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1097 )
1097 )
1098 def phabsend(ui, repo, *revs, **opts):
1098 def phabsend(ui, repo, *revs, **opts):
1099 """upload changesets to Phabricator
1099 """upload changesets to Phabricator
1100
1100
1101 If there are multiple revisions specified, they will be send as a stack
1101 If there are multiple revisions specified, they will be send as a stack
1102 with a linear dependencies relationship using the order specified by the
1102 with a linear dependencies relationship using the order specified by the
1103 revset.
1103 revset.
1104
1104
1105 For the first time uploading changesets, local tags will be created to
1105 For the first time uploading changesets, local tags will be created to
1106 maintain the association. After the first time, phabsend will check
1106 maintain the association. After the first time, phabsend will check
1107 obsstore and tags information so it can figure out whether to update an
1107 obsstore and tags information so it can figure out whether to update an
1108 existing Differential Revision, or create a new one.
1108 existing Differential Revision, or create a new one.
1109
1109
1110 If --amend is set, update commit messages so they have the
1110 If --amend is set, update commit messages so they have the
1111 ``Differential Revision`` URL, remove related tags. This is similar to what
1111 ``Differential Revision`` URL, remove related tags. This is similar to what
1112 arcanist will do, and is more desired in author-push workflows. Otherwise,
1112 arcanist will do, and is more desired in author-push workflows. Otherwise,
1113 use local tags to record the ``Differential Revision`` association.
1113 use local tags to record the ``Differential Revision`` association.
1114
1114
1115 The --confirm option lets you confirm changesets before sending them. You
1115 The --confirm option lets you confirm changesets before sending them. You
1116 can also add following to your configuration file to make it default
1116 can also add following to your configuration file to make it default
1117 behaviour::
1117 behaviour::
1118
1118
1119 [phabsend]
1119 [phabsend]
1120 confirm = true
1120 confirm = true
1121
1121
1122 phabsend will check obsstore and the above association to decide whether to
1122 phabsend will check obsstore and the above association to decide whether to
1123 update an existing Differential Revision, or create a new one.
1123 update an existing Differential Revision, or create a new one.
1124 """
1124 """
1125 opts = pycompat.byteskwargs(opts)
1125 opts = pycompat.byteskwargs(opts)
1126 revs = list(revs) + opts.get(b'rev', [])
1126 revs = list(revs) + opts.get(b'rev', [])
1127 revs = scmutil.revrange(repo, revs)
1127 revs = scmutil.revrange(repo, revs)
1128 revs.sort() # ascending order to preserve topological parent/child in phab
1128 revs.sort() # ascending order to preserve topological parent/child in phab
1129
1129
1130 if not revs:
1130 if not revs:
1131 raise error.Abort(_(b'phabsend requires at least one changeset'))
1131 raise error.Abort(_(b'phabsend requires at least one changeset'))
1132 if opts.get(b'amend'):
1132 if opts.get(b'amend'):
1133 cmdutil.checkunfinished(repo)
1133 cmdutil.checkunfinished(repo)
1134
1134
1135 # {newnode: (oldnode, olddiff, olddrev}
1135 # {newnode: (oldnode, olddiff, olddrev}
1136 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1136 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1137
1137
1138 confirm = ui.configbool(b'phabsend', b'confirm')
1138 confirm = ui.configbool(b'phabsend', b'confirm')
1139 confirm |= bool(opts.get(b'confirm'))
1139 confirm |= bool(opts.get(b'confirm'))
1140 if confirm:
1140 if confirm:
1141 confirmed = _confirmbeforesend(repo, revs, oldmap)
1141 confirmed = _confirmbeforesend(repo, revs, oldmap)
1142 if not confirmed:
1142 if not confirmed:
1143 raise error.Abort(_(b'phabsend cancelled'))
1143 raise error.Abort(_(b'phabsend cancelled'))
1144
1144
1145 actions = []
1145 actions = []
1146 reviewers = opts.get(b'reviewer', [])
1146 reviewers = opts.get(b'reviewer', [])
1147 blockers = opts.get(b'blocker', [])
1147 blockers = opts.get(b'blocker', [])
1148 phids = []
1148 phids = []
1149 if reviewers:
1149 if reviewers:
1150 phids.extend(userphids(repo.ui, reviewers))
1150 phids.extend(userphids(repo.ui, reviewers))
1151 if blockers:
1151 if blockers:
1152 phids.extend(
1152 phids.extend(
1153 map(
1153 map(
1154 lambda phid: b'blocking(%s)' % phid,
1154 lambda phid: b'blocking(%s)' % phid,
1155 userphids(repo.ui, blockers),
1155 userphids(repo.ui, blockers),
1156 )
1156 )
1157 )
1157 )
1158 if phids:
1158 if phids:
1159 actions.append({b'type': b'reviewers.add', b'value': phids})
1159 actions.append({b'type': b'reviewers.add', b'value': phids})
1160
1160
1161 drevids = [] # [int]
1161 drevids = [] # [int]
1162 diffmap = {} # {newnode: diff}
1162 diffmap = {} # {newnode: diff}
1163
1163
1164 # Send patches one by one so we know their Differential Revision PHIDs and
1164 # Send patches one by one so we know their Differential Revision PHIDs and
1165 # can provide dependency relationship
1165 # can provide dependency relationship
1166 lastrevphid = None
1166 lastrevphid = None
1167 for rev in revs:
1167 for rev in revs:
1168 ui.debug(b'sending rev %d\n' % rev)
1168 ui.debug(b'sending rev %d\n' % rev)
1169 ctx = repo[rev]
1169 ctx = repo[rev]
1170
1170
1171 # Get Differential Revision ID
1171 # Get Differential Revision ID
1172 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1172 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1173 if oldnode != ctx.node() or opts.get(b'amend'):
1173 if oldnode != ctx.node() or opts.get(b'amend'):
1174 # Create or update Differential Revision
1174 # Create or update Differential Revision
1175 revision, diff = createdifferentialrevision(
1175 revision, diff = createdifferentialrevision(
1176 ctx,
1176 ctx,
1177 revid,
1177 revid,
1178 lastrevphid,
1178 lastrevphid,
1179 oldnode,
1179 oldnode,
1180 olddiff,
1180 olddiff,
1181 actions,
1181 actions,
1182 opts.get(b'comment'),
1182 opts.get(b'comment'),
1183 )
1183 )
1184 diffmap[ctx.node()] = diff
1184 diffmap[ctx.node()] = diff
1185 newrevid = int(revision[b'object'][b'id'])
1185 newrevid = int(revision[b'object'][b'id'])
1186 newrevphid = revision[b'object'][b'phid']
1186 newrevphid = revision[b'object'][b'phid']
1187 if revid:
1187 if revid:
1188 action = b'updated'
1188 action = b'updated'
1189 else:
1189 else:
1190 action = b'created'
1190 action = b'created'
1191
1191
1192 # Create a local tag to note the association, if commit message
1192 # Create a local tag to note the association, if commit message
1193 # does not have it already
1193 # does not have it already
1194 m = _differentialrevisiondescre.search(ctx.description())
1194 m = _differentialrevisiondescre.search(ctx.description())
1195 if not m or int(m.group('id')) != newrevid:
1195 if not m or int(m.group('id')) != newrevid:
1196 tagname = b'D%d' % newrevid
1196 tagname = b'D%d' % newrevid
1197 tags.tag(
1197 tags.tag(
1198 repo,
1198 repo,
1199 tagname,
1199 tagname,
1200 ctx.node(),
1200 ctx.node(),
1201 message=None,
1201 message=None,
1202 user=None,
1202 user=None,
1203 date=None,
1203 date=None,
1204 local=True,
1204 local=True,
1205 )
1205 )
1206 else:
1206 else:
1207 # Nothing changed. But still set "newrevphid" so the next revision
1207 # Nothing changed. But still set "newrevphid" so the next revision
1208 # could depend on this one and "newrevid" for the summary line.
1208 # could depend on this one and "newrevid" for the summary line.
1209 newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']
1209 newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']
1210 newrevid = revid
1210 newrevid = revid
1211 action = b'skipped'
1211 action = b'skipped'
1212
1212
1213 actiondesc = ui.label(
1213 actiondesc = ui.label(
1214 {
1214 {
1215 b'created': _(b'created'),
1215 b'created': _(b'created'),
1216 b'skipped': _(b'skipped'),
1216 b'skipped': _(b'skipped'),
1217 b'updated': _(b'updated'),
1217 b'updated': _(b'updated'),
1218 }[action],
1218 }[action],
1219 b'phabricator.action.%s' % action,
1219 b'phabricator.action.%s' % action,
1220 )
1220 )
1221 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1221 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1222 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
1222 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
1223 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
1223 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
1224 ui.write(
1224 ui.write(
1225 _(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc)
1225 _(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc)
1226 )
1226 )
1227 drevids.append(newrevid)
1227 drevids.append(newrevid)
1228 lastrevphid = newrevphid
1228 lastrevphid = newrevphid
1229
1229
1230 # Update commit messages and remove tags
1230 # Update commit messages and remove tags
1231 if opts.get(b'amend'):
1231 if opts.get(b'amend'):
1232 unfi = repo.unfiltered()
1232 unfi = repo.unfiltered()
1233 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1233 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1234 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1234 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1235 wnode = unfi[b'.'].node()
1235 wnode = unfi[b'.'].node()
1236 mapping = {} # {oldnode: [newnode]}
1236 mapping = {} # {oldnode: [newnode]}
1237 for i, rev in enumerate(revs):
1237 for i, rev in enumerate(revs):
1238 old = unfi[rev]
1238 old = unfi[rev]
1239 drevid = drevids[i]
1239 drevid = drevids[i]
1240 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1240 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1241 newdesc = getdescfromdrev(drev)
1241 newdesc = getdescfromdrev(drev)
1242 # Make sure commit message contain "Differential Revision"
1242 # Make sure commit message contain "Differential Revision"
1243 if old.description() != newdesc:
1243 if old.description() != newdesc:
1244 if old.phase() == phases.public:
1244 if old.phase() == phases.public:
1245 ui.warn(
1245 ui.warn(
1246 _(b"warning: not updating public commit %s\n")
1246 _(b"warning: not updating public commit %s\n")
1247 % scmutil.formatchangeid(old)
1247 % scmutil.formatchangeid(old)
1248 )
1248 )
1249 continue
1249 continue
1250 parents = [
1250 parents = [
1251 mapping.get(old.p1().node(), (old.p1(),))[0],
1251 mapping.get(old.p1().node(), (old.p1(),))[0],
1252 mapping.get(old.p2().node(), (old.p2(),))[0],
1252 mapping.get(old.p2().node(), (old.p2(),))[0],
1253 ]
1253 ]
1254 new = context.metadataonlyctx(
1254 new = context.metadataonlyctx(
1255 repo,
1255 repo,
1256 old,
1256 old,
1257 parents=parents,
1257 parents=parents,
1258 text=newdesc,
1258 text=newdesc,
1259 user=old.user(),
1259 user=old.user(),
1260 date=old.date(),
1260 date=old.date(),
1261 extra=old.extra(),
1261 extra=old.extra(),
1262 )
1262 )
1263
1263
1264 newnode = new.commit()
1264 newnode = new.commit()
1265
1265
1266 mapping[old.node()] = [newnode]
1266 mapping[old.node()] = [newnode]
1267 # Update diff property
1267 # Update diff property
1268 # If it fails just warn and keep going, otherwise the DREV
1268 # If it fails just warn and keep going, otherwise the DREV
1269 # associations will be lost
1269 # associations will be lost
1270 try:
1270 try:
1271 writediffproperties(unfi[newnode], diffmap[old.node()])
1271 writediffproperties(unfi[newnode], diffmap[old.node()])
1272 except util.urlerr.urlerror:
1272 except util.urlerr.urlerror:
1273 ui.warnnoi18n(
1273 ui.warnnoi18n(
1274 b'Failed to update metadata for D%d\n' % drevid
1274 b'Failed to update metadata for D%d\n' % drevid
1275 )
1275 )
1276 # Remove local tags since it's no longer necessary
1276 # Remove local tags since it's no longer necessary
1277 tagname = b'D%d' % drevid
1277 tagname = b'D%d' % drevid
1278 if tagname in repo.tags():
1278 if tagname in repo.tags():
1279 tags.tag(
1279 tags.tag(
1280 repo,
1280 repo,
1281 tagname,
1281 tagname,
1282 nullid,
1282 nullid,
1283 message=None,
1283 message=None,
1284 user=None,
1284 user=None,
1285 date=None,
1285 date=None,
1286 local=True,
1286 local=True,
1287 )
1287 )
1288 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1288 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1289 if wnode in mapping:
1289 if wnode in mapping:
1290 unfi.setparents(mapping[wnode][0])
1290 unfi.setparents(mapping[wnode][0])
1291
1291
1292
1292
1293 # Map from "hg:meta" keys to header understood by "hg import". The order is
1293 # Map from "hg:meta" keys to header understood by "hg import". The order is
1294 # consistent with "hg export" output.
1294 # consistent with "hg export" output.
1295 _metanamemap = util.sortdict(
1295 _metanamemap = util.sortdict(
1296 [
1296 [
1297 (b'user', b'User'),
1297 (b'user', b'User'),
1298 (b'date', b'Date'),
1298 (b'date', b'Date'),
1299 (b'branch', b'Branch'),
1299 (b'branch', b'Branch'),
1300 (b'node', b'Node ID'),
1300 (b'node', b'Node ID'),
1301 (b'parent', b'Parent '),
1301 (b'parent', b'Parent '),
1302 ]
1302 ]
1303 )
1303 )
1304
1304
1305
1305
1306 def _confirmbeforesend(repo, revs, oldmap):
1306 def _confirmbeforesend(repo, revs, oldmap):
1307 url, token = readurltoken(repo.ui)
1307 url, token = readurltoken(repo.ui)
1308 ui = repo.ui
1308 ui = repo.ui
1309 for rev in revs:
1309 for rev in revs:
1310 ctx = repo[rev]
1310 ctx = repo[rev]
1311 desc = ctx.description().splitlines()[0]
1311 desc = ctx.description().splitlines()[0]
1312 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1312 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1313 if drevid:
1313 if drevid:
1314 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1314 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1315 else:
1315 else:
1316 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1316 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1317
1317
1318 ui.write(
1318 ui.write(
1319 _(b'%s - %s: %s\n')
1319 _(b'%s - %s: %s\n')
1320 % (
1320 % (
1321 drevdesc,
1321 drevdesc,
1322 ui.label(bytes(ctx), b'phabricator.node'),
1322 ui.label(bytes(ctx), b'phabricator.node'),
1323 ui.label(desc, b'phabricator.desc'),
1323 ui.label(desc, b'phabricator.desc'),
1324 )
1324 )
1325 )
1325 )
1326
1326
1327 if ui.promptchoice(
1327 if ui.promptchoice(
1328 _(b'Send the above changes to %s (yn)?$$ &Yes $$ &No') % url
1328 _(b'Send the above changes to %s (yn)?$$ &Yes $$ &No') % url
1329 ):
1329 ):
1330 return False
1330 return False
1331
1331
1332 return True
1332 return True
1333
1333
1334
1334
1335 _knownstatusnames = {
1335 _knownstatusnames = {
1336 b'accepted',
1336 b'accepted',
1337 b'needsreview',
1337 b'needsreview',
1338 b'needsrevision',
1338 b'needsrevision',
1339 b'closed',
1339 b'closed',
1340 b'abandoned',
1340 b'abandoned',
1341 b'changesplanned',
1341 b'changesplanned',
1342 }
1342 }
1343
1343
1344
1344
1345 def _getstatusname(drev):
1345 def _getstatusname(drev):
1346 """get normalized status name from a Differential Revision"""
1346 """get normalized status name from a Differential Revision"""
1347 return drev[b'statusName'].replace(b' ', b'').lower()
1347 return drev[b'statusName'].replace(b' ', b'').lower()
1348
1348
1349
1349
1350 # Small language to specify differential revisions. Support symbols: (), :X,
1350 # Small language to specify differential revisions. Support symbols: (), :X,
1351 # +, and -.
1351 # +, and -.
1352
1352
1353 _elements = {
1353 _elements = {
1354 # token-type: binding-strength, primary, prefix, infix, suffix
1354 # token-type: binding-strength, primary, prefix, infix, suffix
1355 b'(': (12, None, (b'group', 1, b')'), None, None),
1355 b'(': (12, None, (b'group', 1, b')'), None, None),
1356 b':': (8, None, (b'ancestors', 8), None, None),
1356 b':': (8, None, (b'ancestors', 8), None, None),
1357 b'&': (5, None, None, (b'and_', 5), None),
1357 b'&': (5, None, None, (b'and_', 5), None),
1358 b'+': (4, None, None, (b'add', 4), None),
1358 b'+': (4, None, None, (b'add', 4), None),
1359 b'-': (4, None, None, (b'sub', 4), None),
1359 b'-': (4, None, None, (b'sub', 4), None),
1360 b')': (0, None, None, None, None),
1360 b')': (0, None, None, None, None),
1361 b'symbol': (0, b'symbol', None, None, None),
1361 b'symbol': (0, b'symbol', None, None, None),
1362 b'end': (0, None, None, None, None),
1362 b'end': (0, None, None, None, None),
1363 }
1363 }
1364
1364
1365
1365
1366 def _tokenize(text):
1366 def _tokenize(text):
1367 view = memoryview(text) # zero-copy slice
1367 view = memoryview(text) # zero-copy slice
1368 special = b'():+-& '
1368 special = b'():+-& '
1369 pos = 0
1369 pos = 0
1370 length = len(text)
1370 length = len(text)
1371 while pos < length:
1371 while pos < length:
1372 symbol = b''.join(
1372 symbol = b''.join(
1373 itertools.takewhile(
1373 itertools.takewhile(
1374 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1374 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1375 )
1375 )
1376 )
1376 )
1377 if symbol:
1377 if symbol:
1378 yield (b'symbol', symbol, pos)
1378 yield (b'symbol', symbol, pos)
1379 pos += len(symbol)
1379 pos += len(symbol)
1380 else: # special char, ignore space
1380 else: # special char, ignore space
1381 if text[pos : pos + 1] != b' ':
1381 if text[pos : pos + 1] != b' ':
1382 yield (text[pos : pos + 1], None, pos)
1382 yield (text[pos : pos + 1], None, pos)
1383 pos += 1
1383 pos += 1
1384 yield (b'end', None, pos)
1384 yield (b'end', None, pos)
1385
1385
1386
1386
1387 def _parse(text):
1387 def _parse(text):
1388 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1388 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1389 if pos != len(text):
1389 if pos != len(text):
1390 raise error.ParseError(b'invalid token', pos)
1390 raise error.ParseError(b'invalid token', pos)
1391 return tree
1391 return tree
1392
1392
1393
1393
1394 def _parsedrev(symbol):
1394 def _parsedrev(symbol):
1395 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1395 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1396 if symbol.startswith(b'D') and symbol[1:].isdigit():
1396 if symbol.startswith(b'D') and symbol[1:].isdigit():
1397 return int(symbol[1:])
1397 return int(symbol[1:])
1398 if symbol.isdigit():
1398 if symbol.isdigit():
1399 return int(symbol)
1399 return int(symbol)
1400
1400
1401
1401
1402 def _prefetchdrevs(tree):
1402 def _prefetchdrevs(tree):
1403 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1403 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1404 drevs = set()
1404 drevs = set()
1405 ancestordrevs = set()
1405 ancestordrevs = set()
1406 op = tree[0]
1406 op = tree[0]
1407 if op == b'symbol':
1407 if op == b'symbol':
1408 r = _parsedrev(tree[1])
1408 r = _parsedrev(tree[1])
1409 if r:
1409 if r:
1410 drevs.add(r)
1410 drevs.add(r)
1411 elif op == b'ancestors':
1411 elif op == b'ancestors':
1412 r, a = _prefetchdrevs(tree[1])
1412 r, a = _prefetchdrevs(tree[1])
1413 drevs.update(r)
1413 drevs.update(r)
1414 ancestordrevs.update(r)
1414 ancestordrevs.update(r)
1415 ancestordrevs.update(a)
1415 ancestordrevs.update(a)
1416 else:
1416 else:
1417 for t in tree[1:]:
1417 for t in tree[1:]:
1418 r, a = _prefetchdrevs(t)
1418 r, a = _prefetchdrevs(t)
1419 drevs.update(r)
1419 drevs.update(r)
1420 ancestordrevs.update(a)
1420 ancestordrevs.update(a)
1421 return drevs, ancestordrevs
1421 return drevs, ancestordrevs
1422
1422
1423
1423
1424 def querydrev(ui, spec):
1424 def querydrev(ui, spec):
1425 """return a list of "Differential Revision" dicts
1425 """return a list of "Differential Revision" dicts
1426
1426
1427 spec is a string using a simple query language, see docstring in phabread
1427 spec is a string using a simple query language, see docstring in phabread
1428 for details.
1428 for details.
1429
1429
1430 A "Differential Revision dict" looks like:
1430 A "Differential Revision dict" looks like:
1431
1431
1432 {
1432 {
1433 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1433 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1434 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1434 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1435 "auxiliary": {
1435 "auxiliary": {
1436 "phabricator:depends-on": [
1436 "phabricator:depends-on": [
1437 "PHID-DREV-gbapp366kutjebt7agcd"
1437 "PHID-DREV-gbapp366kutjebt7agcd"
1438 ]
1438 ]
1439 "phabricator:projects": [],
1439 "phabricator:projects": [],
1440 },
1440 },
1441 "branch": "default",
1441 "branch": "default",
1442 "ccs": [],
1442 "ccs": [],
1443 "commits": [],
1443 "commits": [],
1444 "dateCreated": "1499181406",
1444 "dateCreated": "1499181406",
1445 "dateModified": "1499182103",
1445 "dateModified": "1499182103",
1446 "diffs": [
1446 "diffs": [
1447 "3",
1447 "3",
1448 "4",
1448 "4",
1449 ],
1449 ],
1450 "hashes": [],
1450 "hashes": [],
1451 "id": "2",
1451 "id": "2",
1452 "lineCount": "2",
1452 "lineCount": "2",
1453 "phid": "PHID-DREV-672qvysjcczopag46qty",
1453 "phid": "PHID-DREV-672qvysjcczopag46qty",
1454 "properties": {},
1454 "properties": {},
1455 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1455 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1456 "reviewers": [],
1456 "reviewers": [],
1457 "sourcePath": null
1457 "sourcePath": null
1458 "status": "0",
1458 "status": "0",
1459 "statusName": "Needs Review",
1459 "statusName": "Needs Review",
1460 "summary": "",
1460 "summary": "",
1461 "testPlan": "",
1461 "testPlan": "",
1462 "title": "example",
1462 "title": "example",
1463 "uri": "https://phab.example.com/D2",
1463 "uri": "https://phab.example.com/D2",
1464 }
1464 }
1465 """
1465 """
1466 # TODO: replace differential.query and differential.querydiffs with
1466 # TODO: replace differential.query and differential.querydiffs with
1467 # differential.diff.search because the former (and their output) are
1467 # differential.diff.search because the former (and their output) are
1468 # frozen, and planned to be deprecated and removed.
1468 # frozen, and planned to be deprecated and removed.
1469
1469
1470 def fetch(params):
1470 def fetch(params):
1471 """params -> single drev or None"""
1471 """params -> single drev or None"""
1472 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1472 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1473 if key in prefetched:
1473 if key in prefetched:
1474 return prefetched[key]
1474 return prefetched[key]
1475 drevs = callconduit(ui, b'differential.query', params)
1475 drevs = callconduit(ui, b'differential.query', params)
1476 # Fill prefetched with the result
1476 # Fill prefetched with the result
1477 for drev in drevs:
1477 for drev in drevs:
1478 prefetched[drev[b'phid']] = drev
1478 prefetched[drev[b'phid']] = drev
1479 prefetched[int(drev[b'id'])] = drev
1479 prefetched[int(drev[b'id'])] = drev
1480 if key not in prefetched:
1480 if key not in prefetched:
1481 raise error.Abort(
1481 raise error.Abort(
1482 _(b'cannot get Differential Revision %r') % params
1482 _(b'cannot get Differential Revision %r') % params
1483 )
1483 )
1484 return prefetched[key]
1484 return prefetched[key]
1485
1485
1486 def getstack(topdrevids):
1486 def getstack(topdrevids):
1487 """given a top, get a stack from the bottom, [id] -> [id]"""
1487 """given a top, get a stack from the bottom, [id] -> [id]"""
1488 visited = set()
1488 visited = set()
1489 result = []
1489 result = []
1490 queue = [{b'ids': [i]} for i in topdrevids]
1490 queue = [{b'ids': [i]} for i in topdrevids]
1491 while queue:
1491 while queue:
1492 params = queue.pop()
1492 params = queue.pop()
1493 drev = fetch(params)
1493 drev = fetch(params)
1494 if drev[b'id'] in visited:
1494 if drev[b'id'] in visited:
1495 continue
1495 continue
1496 visited.add(drev[b'id'])
1496 visited.add(drev[b'id'])
1497 result.append(int(drev[b'id']))
1497 result.append(int(drev[b'id']))
1498 auxiliary = drev.get(b'auxiliary', {})
1498 auxiliary = drev.get(b'auxiliary', {})
1499 depends = auxiliary.get(b'phabricator:depends-on', [])
1499 depends = auxiliary.get(b'phabricator:depends-on', [])
1500 for phid in depends:
1500 for phid in depends:
1501 queue.append({b'phids': [phid]})
1501 queue.append({b'phids': [phid]})
1502 result.reverse()
1502 result.reverse()
1503 return smartset.baseset(result)
1503 return smartset.baseset(result)
1504
1504
1505 # Initialize prefetch cache
1505 # Initialize prefetch cache
1506 prefetched = {} # {id or phid: drev}
1506 prefetched = {} # {id or phid: drev}
1507
1507
1508 tree = _parse(spec)
1508 tree = _parse(spec)
1509 drevs, ancestordrevs = _prefetchdrevs(tree)
1509 drevs, ancestordrevs = _prefetchdrevs(tree)
1510
1510
1511 # developer config: phabricator.batchsize
1511 # developer config: phabricator.batchsize
1512 batchsize = ui.configint(b'phabricator', b'batchsize')
1512 batchsize = ui.configint(b'phabricator', b'batchsize')
1513
1513
1514 # Prefetch Differential Revisions in batch
1514 # Prefetch Differential Revisions in batch
1515 tofetch = set(drevs)
1515 tofetch = set(drevs)
1516 for r in ancestordrevs:
1516 for r in ancestordrevs:
1517 tofetch.update(range(max(1, r - batchsize), r + 1))
1517 tofetch.update(range(max(1, r - batchsize), r + 1))
1518 if drevs:
1518 if drevs:
1519 fetch({b'ids': list(tofetch)})
1519 fetch({b'ids': list(tofetch)})
1520 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1520 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1521
1521
1522 # Walk through the tree, return smartsets
1522 # Walk through the tree, return smartsets
1523 def walk(tree):
1523 def walk(tree):
1524 op = tree[0]
1524 op = tree[0]
1525 if op == b'symbol':
1525 if op == b'symbol':
1526 drev = _parsedrev(tree[1])
1526 drev = _parsedrev(tree[1])
1527 if drev:
1527 if drev:
1528 return smartset.baseset([drev])
1528 return smartset.baseset([drev])
1529 elif tree[1] in _knownstatusnames:
1529 elif tree[1] in _knownstatusnames:
1530 drevs = [
1530 drevs = [
1531 r
1531 r
1532 for r in validids
1532 for r in validids
1533 if _getstatusname(prefetched[r]) == tree[1]
1533 if _getstatusname(prefetched[r]) == tree[1]
1534 ]
1534 ]
1535 return smartset.baseset(drevs)
1535 return smartset.baseset(drevs)
1536 else:
1536 else:
1537 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1537 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1538 elif op in {b'and_', b'add', b'sub'}:
1538 elif op in {b'and_', b'add', b'sub'}:
1539 assert len(tree) == 3
1539 assert len(tree) == 3
1540 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1540 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1541 elif op == b'group':
1541 elif op == b'group':
1542 return walk(tree[1])
1542 return walk(tree[1])
1543 elif op == b'ancestors':
1543 elif op == b'ancestors':
1544 return getstack(walk(tree[1]))
1544 return getstack(walk(tree[1]))
1545 else:
1545 else:
1546 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1546 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1547
1547
1548 return [prefetched[r] for r in walk(tree)]
1548 return [prefetched[r] for r in walk(tree)]
1549
1549
1550
1550
1551 def getdescfromdrev(drev):
1551 def getdescfromdrev(drev):
1552 """get description (commit message) from "Differential Revision"
1552 """get description (commit message) from "Differential Revision"
1553
1553
1554 This is similar to differential.getcommitmessage API. But we only care
1554 This is similar to differential.getcommitmessage API. But we only care
1555 about limited fields: title, summary, test plan, and URL.
1555 about limited fields: title, summary, test plan, and URL.
1556 """
1556 """
1557 title = drev[b'title']
1557 title = drev[b'title']
1558 summary = drev[b'summary'].rstrip()
1558 summary = drev[b'summary'].rstrip()
1559 testplan = drev[b'testPlan'].rstrip()
1559 testplan = drev[b'testPlan'].rstrip()
1560 if testplan:
1560 if testplan:
1561 testplan = b'Test Plan:\n%s' % testplan
1561 testplan = b'Test Plan:\n%s' % testplan
1562 uri = b'Differential Revision: %s' % drev[b'uri']
1562 uri = b'Differential Revision: %s' % drev[b'uri']
1563 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1563 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1564
1564
1565
1565
1566 def getdiffmeta(diff):
1566 def getdiffmeta(diff):
1567 """get commit metadata (date, node, user, p1) from a diff object
1567 """get commit metadata (date, node, user, p1) from a diff object
1568
1568
1569 The metadata could be "hg:meta", sent by phabsend, like:
1569 The metadata could be "hg:meta", sent by phabsend, like:
1570
1570
1571 "properties": {
1571 "properties": {
1572 "hg:meta": {
1572 "hg:meta": {
1573 "branch": "default",
1573 "branch": "default",
1574 "date": "1499571514 25200",
1574 "date": "1499571514 25200",
1575 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
1575 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
1576 "user": "Foo Bar <foo@example.com>",
1576 "user": "Foo Bar <foo@example.com>",
1577 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
1577 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
1578 }
1578 }
1579 }
1579 }
1580
1580
1581 Or converted from "local:commits", sent by "arc", like:
1581 Or converted from "local:commits", sent by "arc", like:
1582
1582
1583 "properties": {
1583 "properties": {
1584 "local:commits": {
1584 "local:commits": {
1585 "98c08acae292b2faf60a279b4189beb6cff1414d": {
1585 "98c08acae292b2faf60a279b4189beb6cff1414d": {
1586 "author": "Foo Bar",
1586 "author": "Foo Bar",
1587 "authorEmail": "foo@example.com"
1587 "authorEmail": "foo@example.com"
1588 "branch": "default",
1588 "branch": "default",
1589 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
1589 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
1590 "local": "1000",
1590 "local": "1000",
1591 "message": "...",
1591 "message": "...",
1592 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
1592 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
1593 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
1593 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
1594 "summary": "...",
1594 "summary": "...",
1595 "tag": "",
1595 "tag": "",
1596 "time": 1499546314,
1596 "time": 1499546314,
1597 }
1597 }
1598 }
1598 }
1599 }
1599 }
1600
1600
1601 Note: metadata extracted from "local:commits" will lose time zone
1601 Note: metadata extracted from "local:commits" will lose time zone
1602 information.
1602 information.
1603 """
1603 """
1604 props = diff.get(b'properties') or {}
1604 props = diff.get(b'properties') or {}
1605 meta = props.get(b'hg:meta')
1605 meta = props.get(b'hg:meta')
1606 if not meta:
1606 if not meta:
1607 if props.get(b'local:commits'):
1607 if props.get(b'local:commits'):
1608 commit = sorted(props[b'local:commits'].values())[0]
1608 commit = sorted(props[b'local:commits'].values())[0]
1609 meta = {}
1609 meta = {}
1610 if b'author' in commit and b'authorEmail' in commit:
1610 if b'author' in commit and b'authorEmail' in commit:
1611 meta[b'user'] = b'%s <%s>' % (
1611 meta[b'user'] = b'%s <%s>' % (
1612 commit[b'author'],
1612 commit[b'author'],
1613 commit[b'authorEmail'],
1613 commit[b'authorEmail'],
1614 )
1614 )
1615 if b'time' in commit:
1615 if b'time' in commit:
1616 meta[b'date'] = b'%d 0' % int(commit[b'time'])
1616 meta[b'date'] = b'%d 0' % int(commit[b'time'])
1617 if b'branch' in commit:
1617 if b'branch' in commit:
1618 meta[b'branch'] = commit[b'branch']
1618 meta[b'branch'] = commit[b'branch']
1619 node = commit.get(b'commit', commit.get(b'rev'))
1619 node = commit.get(b'commit', commit.get(b'rev'))
1620 if node:
1620 if node:
1621 meta[b'node'] = node
1621 meta[b'node'] = node
1622 if len(commit.get(b'parents', ())) >= 1:
1622 if len(commit.get(b'parents', ())) >= 1:
1623 meta[b'parent'] = commit[b'parents'][0]
1623 meta[b'parent'] = commit[b'parents'][0]
1624 else:
1624 else:
1625 meta = {}
1625 meta = {}
1626 if b'date' not in meta and b'dateCreated' in diff:
1626 if b'date' not in meta and b'dateCreated' in diff:
1627 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
1627 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
1628 if b'branch' not in meta and diff.get(b'branch'):
1628 if b'branch' not in meta and diff.get(b'branch'):
1629 meta[b'branch'] = diff[b'branch']
1629 meta[b'branch'] = diff[b'branch']
1630 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
1630 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
1631 meta[b'parent'] = diff[b'sourceControlBaseRevision']
1631 meta[b'parent'] = diff[b'sourceControlBaseRevision']
1632 return meta
1632 return meta
1633
1633
1634
1634
1635 def _getdrevs(ui, stack, *specs):
1635 def _getdrevs(ui, stack, *specs):
1636 """convert user supplied DREVSPECs into "Differential Revision" dicts
1636 """convert user supplied DREVSPECs into "Differential Revision" dicts
1637
1637
1638 See ``hg help phabread`` for how to specify each DREVSPEC.
1638 See ``hg help phabread`` for how to specify each DREVSPEC.
1639 """
1639 """
1640 if len(*specs) > 0:
1640 if len(*specs) > 0:
1641
1641
1642 def _formatspec(s):
1642 def _formatspec(s):
1643 if stack:
1643 if stack:
1644 s = b':(%s)' % s
1644 s = b':(%s)' % s
1645 return b'(%s)' % s
1645 return b'(%s)' % s
1646
1646
1647 spec = b'+'.join(pycompat.maplist(_formatspec, *specs))
1647 spec = b'+'.join(pycompat.maplist(_formatspec, *specs))
1648
1648
1649 drevs = querydrev(ui, spec)
1649 drevs = querydrev(ui, spec)
1650 if drevs:
1650 if drevs:
1651 return drevs
1651 return drevs
1652
1652
1653 raise error.Abort(_(b"empty DREVSPEC set"))
1653 raise error.Abort(_(b"empty DREVSPEC set"))
1654
1654
1655
1655
1656 def readpatch(ui, drevs, write):
1656 def readpatch(ui, drevs, write):
1657 """generate plain-text patch readable by 'hg import'
1657 """generate plain-text patch readable by 'hg import'
1658
1658
1659 write takes a list of (DREV, bytes), where DREV is the differential number
1659 write takes a list of (DREV, bytes), where DREV is the differential number
1660 (as bytes, without the "D" prefix) and the bytes are the text of a patch
1660 (as bytes, without the "D" prefix) and the bytes are the text of a patch
1661 to be imported. drevs is what "querydrev" returns, results of
1661 to be imported. drevs is what "querydrev" returns, results of
1662 "differential.query".
1662 "differential.query".
1663 """
1663 """
1664 # Prefetch hg:meta property for all diffs
1664 # Prefetch hg:meta property for all diffs
1665 diffids = sorted({max(int(v) for v in drev[b'diffs']) for drev in drevs})
1665 diffids = sorted({max(int(v) for v in drev[b'diffs']) for drev in drevs})
1666 diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})
1666 diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})
1667
1667
1668 patches = []
1668 patches = []
1669
1669
1670 # Generate patch for each drev
1670 # Generate patch for each drev
1671 for drev in drevs:
1671 for drev in drevs:
1672 ui.note(_(b'reading D%s\n') % drev[b'id'])
1672 ui.note(_(b'reading D%s\n') % drev[b'id'])
1673
1673
1674 diffid = max(int(v) for v in drev[b'diffs'])
1674 diffid = max(int(v) for v in drev[b'diffs'])
1675 body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})
1675 body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})
1676 desc = getdescfromdrev(drev)
1676 desc = getdescfromdrev(drev)
1677 header = b'# HG changeset patch\n'
1677 header = b'# HG changeset patch\n'
1678
1678
1679 # Try to preserve metadata from hg:meta property. Write hg patch
1679 # Try to preserve metadata from hg:meta property. Write hg patch
1680 # headers that can be read by the "import" command. See patchheadermap
1680 # headers that can be read by the "import" command. See patchheadermap
1681 # and extract in mercurial/patch.py for supported headers.
1681 # and extract in mercurial/patch.py for supported headers.
1682 meta = getdiffmeta(diffs[b'%d' % diffid])
1682 meta = getdiffmeta(diffs[b'%d' % diffid])
1683 for k in _metanamemap.keys():
1683 for k in _metanamemap.keys():
1684 if k in meta:
1684 if k in meta:
1685 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
1685 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
1686
1686
1687 content = b'%s%s\n%s' % (header, desc, body)
1687 content = b'%s%s\n%s' % (header, desc, body)
1688 patches.append((drev[b'id'], content))
1688 patches.append((drev[b'id'], content))
1689
1689
1690 # Write patches to the supplied callback
1690 # Write patches to the supplied callback
1691 write(patches)
1691 write(patches)
1692
1692
1693
1693
1694 @vcrcommand(
1694 @vcrcommand(
1695 b'phabread',
1695 b'phabread',
1696 [(b'', b'stack', False, _(b'read dependencies'))],
1696 [(b'', b'stack', False, _(b'read dependencies'))],
1697 _(b'DREVSPEC [OPTIONS]'),
1697 _(b'DREVSPEC... [OPTIONS]'),
1698 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1698 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1699 optionalrepo=True,
1699 optionalrepo=True,
1700 )
1700 )
1701 def phabread(ui, repo, spec, **opts):
1701 def phabread(ui, repo, *specs, **opts):
1702 """print patches from Phabricator suitable for importing
1702 """print patches from Phabricator suitable for importing
1703
1703
1704 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
1704 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
1705 the number ``123``. It could also have common operators like ``+``, ``-``,
1705 the number ``123``. It could also have common operators like ``+``, ``-``,
1706 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
1706 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
1707 select a stack.
1707 select a stack. If multiple DREVSPEC values are given, the result is the
1708 union of each individually evaluated value. No attempt is currently made
1709 to reorder the values to run from parent to child.
1708
1710
1709 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
1711 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
1710 could be used to filter patches by status. For performance reason, they
1712 could be used to filter patches by status. For performance reason, they
1711 only represent a subset of non-status selections and cannot be used alone.
1713 only represent a subset of non-status selections and cannot be used alone.
1712
1714
1713 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
1715 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
1714 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
1716 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
1715 stack up to D9.
1717 stack up to D9.
1716
1718
1717 If --stack is given, follow dependencies information and read all patches.
1719 If --stack is given, follow dependencies information and read all patches.
1718 It is equivalent to the ``:`` operator.
1720 It is equivalent to the ``:`` operator.
1719 """
1721 """
1720 opts = pycompat.byteskwargs(opts)
1722 opts = pycompat.byteskwargs(opts)
1721 if opts.get(b'stack'):
1723 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
1722 spec = b':(%s)' % spec
1723 drevs = querydrev(ui, spec)
1724
1724
1725 def _write(patches):
1725 def _write(patches):
1726 for drev, content in patches:
1726 for drev, content in patches:
1727 ui.write(content)
1727 ui.write(content)
1728
1728
1729 readpatch(ui, drevs, _write)
1729 readpatch(ui, drevs, _write)
1730
1730
1731
1731
1732 @vcrcommand(
1732 @vcrcommand(
1733 b'phabimport',
1733 b'phabimport',
1734 [(b'', b'stack', False, _(b'import dependencies as well'))],
1734 [(b'', b'stack', False, _(b'import dependencies as well'))],
1735 _(b'DREVSPEC [OPTIONS]'),
1735 _(b'DREVSPEC... [OPTIONS]'),
1736 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1736 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1737 )
1737 )
1738 def phabimport(ui, repo, spec, **opts):
1738 def phabimport(ui, repo, *specs, **opts):
1739 """import patches from Phabricator for the specified Differential Revisions
1739 """import patches from Phabricator for the specified Differential Revisions
1740
1740
1741 The patches are read and applied starting at the parent of the working
1741 The patches are read and applied starting at the parent of the working
1742 directory.
1742 directory.
1743
1743
1744 See ``hg help phabread`` for how to specify DREVSPEC.
1744 See ``hg help phabread`` for how to specify DREVSPEC.
1745 """
1745 """
1746 opts = pycompat.byteskwargs(opts)
1746 opts = pycompat.byteskwargs(opts)
1747
1747
1748 # --bypass avoids losing exec and symlink bits when importing on Windows,
1748 # --bypass avoids losing exec and symlink bits when importing on Windows,
1749 # and allows importing with a dirty wdir. It also aborts instead of leaving
1749 # and allows importing with a dirty wdir. It also aborts instead of leaving
1750 # rejects.
1750 # rejects.
1751 opts[b'bypass'] = True
1751 opts[b'bypass'] = True
1752
1752
1753 # Mandatory default values, synced with commands.import
1753 # Mandatory default values, synced with commands.import
1754 opts[b'strip'] = 1
1754 opts[b'strip'] = 1
1755 opts[b'prefix'] = b''
1755 opts[b'prefix'] = b''
1756 # Evolve 9.3.0 assumes this key is present in cmdutil.tryimportone()
1756 # Evolve 9.3.0 assumes this key is present in cmdutil.tryimportone()
1757 opts[b'obsolete'] = False
1757 opts[b'obsolete'] = False
1758
1758
1759 if ui.configbool(b'phabimport', b'secret'):
1759 if ui.configbool(b'phabimport', b'secret'):
1760 opts[b'secret'] = True
1760 opts[b'secret'] = True
1761 if ui.configbool(b'phabimport', b'obsolete'):
1761 if ui.configbool(b'phabimport', b'obsolete'):
1762 opts[b'obsolete'] = True # Handled by evolve wrapping tryimportone()
1762 opts[b'obsolete'] = True # Handled by evolve wrapping tryimportone()
1763
1763
1764 def _write(patches):
1764 def _write(patches):
1765 parents = repo[None].parents()
1765 parents = repo[None].parents()
1766
1766
1767 with repo.wlock(), repo.lock(), repo.transaction(b'phabimport'):
1767 with repo.wlock(), repo.lock(), repo.transaction(b'phabimport'):
1768 for drev, contents in patches:
1768 for drev, contents in patches:
1769 ui.status(_(b'applying patch from D%s\n') % drev)
1769 ui.status(_(b'applying patch from D%s\n') % drev)
1770
1770
1771 with patch.extract(ui, pycompat.bytesio(contents)) as patchdata:
1771 with patch.extract(ui, pycompat.bytesio(contents)) as patchdata:
1772 msg, node, rej = cmdutil.tryimportone(
1772 msg, node, rej = cmdutil.tryimportone(
1773 ui,
1773 ui,
1774 repo,
1774 repo,
1775 patchdata,
1775 patchdata,
1776 parents,
1776 parents,
1777 opts,
1777 opts,
1778 [],
1778 [],
1779 None, # Never update wdir to another revision
1779 None, # Never update wdir to another revision
1780 )
1780 )
1781
1781
1782 if not node:
1782 if not node:
1783 raise error.Abort(_(b'D%s: no diffs found') % drev)
1783 raise error.Abort(_(b'D%s: no diffs found') % drev)
1784
1784
1785 ui.note(msg + b'\n')
1785 ui.note(msg + b'\n')
1786 parents = [repo[node]]
1786 parents = [repo[node]]
1787
1787
1788 if opts.get(b'stack'):
1788 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
1789 spec = b':(%s)' % spec
1790 drevs = querydrev(repo.ui, spec)
1791
1789
1792 readpatch(repo.ui, drevs, _write)
1790 readpatch(repo.ui, drevs, _write)
1793
1791
1794
1792
1795 @vcrcommand(
1793 @vcrcommand(
1796 b'phabupdate',
1794 b'phabupdate',
1797 [
1795 [
1798 (b'', b'accept', False, _(b'accept revisions')),
1796 (b'', b'accept', False, _(b'accept revisions')),
1799 (b'', b'reject', False, _(b'reject revisions')),
1797 (b'', b'reject', False, _(b'reject revisions')),
1800 (b'', b'abandon', False, _(b'abandon revisions')),
1798 (b'', b'abandon', False, _(b'abandon revisions')),
1801 (b'', b'reclaim', False, _(b'reclaim revisions')),
1799 (b'', b'reclaim', False, _(b'reclaim revisions')),
1802 (b'm', b'comment', b'', _(b'comment on the last revision')),
1800 (b'm', b'comment', b'', _(b'comment on the last revision')),
1803 ],
1801 ],
1804 _(b'DREVSPEC [OPTIONS]'),
1802 _(b'DREVSPEC... [OPTIONS]'),
1805 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1803 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1806 optionalrepo=True,
1804 optionalrepo=True,
1807 )
1805 )
1808 def phabupdate(ui, repo, spec, **opts):
1806 def phabupdate(ui, repo, *specs, **opts):
1809 """update Differential Revision in batch
1807 """update Differential Revision in batch
1810
1808
1811 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
1809 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
1812 """
1810 """
1813 opts = pycompat.byteskwargs(opts)
1811 opts = pycompat.byteskwargs(opts)
1814 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
1812 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
1815 if len(flags) > 1:
1813 if len(flags) > 1:
1816 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
1814 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
1817
1815
1818 actions = []
1816 actions = []
1819 for f in flags:
1817 for f in flags:
1820 actions.append({b'type': f, b'value': True})
1818 actions.append({b'type': f, b'value': True})
1821
1819
1822 drevs = querydrev(ui, spec)
1820 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
1823 for i, drev in enumerate(drevs):
1821 for i, drev in enumerate(drevs):
1824 if i + 1 == len(drevs) and opts.get(b'comment'):
1822 if i + 1 == len(drevs) and opts.get(b'comment'):
1825 actions.append({b'type': b'comment', b'value': opts[b'comment']})
1823 actions.append({b'type': b'comment', b'value': opts[b'comment']})
1826 if actions:
1824 if actions:
1827 params = {
1825 params = {
1828 b'objectIdentifier': drev[b'phid'],
1826 b'objectIdentifier': drev[b'phid'],
1829 b'transactions': actions,
1827 b'transactions': actions,
1830 }
1828 }
1831 callconduit(ui, b'differential.revision.edit', params)
1829 callconduit(ui, b'differential.revision.edit', params)
1832
1830
1833
1831
1834 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
1832 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
1835 def template_review(context, mapping):
1833 def template_review(context, mapping):
1836 """:phabreview: Object describing the review for this changeset.
1834 """:phabreview: Object describing the review for this changeset.
1837 Has attributes `url` and `id`.
1835 Has attributes `url` and `id`.
1838 """
1836 """
1839 ctx = context.resource(mapping, b'ctx')
1837 ctx = context.resource(mapping, b'ctx')
1840 m = _differentialrevisiondescre.search(ctx.description())
1838 m = _differentialrevisiondescre.search(ctx.description())
1841 if m:
1839 if m:
1842 return templateutil.hybriddict(
1840 return templateutil.hybriddict(
1843 {b'url': m.group('url'), b'id': b"D%s" % m.group('id'),}
1841 {b'url': m.group('url'), b'id': b"D%s" % m.group('id'),}
1844 )
1842 )
1845 else:
1843 else:
1846 tags = ctx.repo().nodetags(ctx.node())
1844 tags = ctx.repo().nodetags(ctx.node())
1847 for t in tags:
1845 for t in tags:
1848 if _differentialrevisiontagre.match(t):
1846 if _differentialrevisiontagre.match(t):
1849 url = ctx.repo().ui.config(b'phabricator', b'url')
1847 url = ctx.repo().ui.config(b'phabricator', b'url')
1850 if not url.endswith(b'/'):
1848 if not url.endswith(b'/'):
1851 url += b'/'
1849 url += b'/'
1852 url += t
1850 url += t
1853
1851
1854 return templateutil.hybriddict({b'url': url, b'id': t,})
1852 return templateutil.hybriddict({b'url': url, b'id': t,})
1855 return None
1853 return None
1856
1854
1857
1855
1858 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
1856 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
1859 def template_status(context, mapping):
1857 def template_status(context, mapping):
1860 """:phabstatus: String. Status of Phabricator differential.
1858 """:phabstatus: String. Status of Phabricator differential.
1861 """
1859 """
1862 ctx = context.resource(mapping, b'ctx')
1860 ctx = context.resource(mapping, b'ctx')
1863 repo = context.resource(mapping, b'repo')
1861 repo = context.resource(mapping, b'repo')
1864 ui = context.resource(mapping, b'ui')
1862 ui = context.resource(mapping, b'ui')
1865
1863
1866 rev = ctx.rev()
1864 rev = ctx.rev()
1867 try:
1865 try:
1868 drevid = getdrevmap(repo, [rev])[rev]
1866 drevid = getdrevmap(repo, [rev])[rev]
1869 except KeyError:
1867 except KeyError:
1870 return None
1868 return None
1871 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
1869 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
1872 for drev in drevs:
1870 for drev in drevs:
1873 if int(drev[b'id']) == drevid:
1871 if int(drev[b'id']) == drevid:
1874 return templateutil.hybriddict(
1872 return templateutil.hybriddict(
1875 {b'url': drev[b'uri'], b'status': drev[b'statusName'],}
1873 {b'url': drev[b'uri'], b'status': drev[b'statusName'],}
1876 )
1874 )
1877 return None
1875 return None
1878
1876
1879
1877
1880 @show.showview(b'phabstatus', csettopic=b'work')
1878 @show.showview(b'phabstatus', csettopic=b'work')
1881 def phabstatusshowview(ui, repo, displayer):
1879 def phabstatusshowview(ui, repo, displayer):
1882 """Phabricator differiential status"""
1880 """Phabricator differiential status"""
1883 revs = repo.revs('sort(_underway(), topo)')
1881 revs = repo.revs('sort(_underway(), topo)')
1884 drevmap = getdrevmap(repo, revs)
1882 drevmap = getdrevmap(repo, revs)
1885 unknownrevs, drevids, revsbydrevid = [], set(), {}
1883 unknownrevs, drevids, revsbydrevid = [], set(), {}
1886 for rev, drevid in pycompat.iteritems(drevmap):
1884 for rev, drevid in pycompat.iteritems(drevmap):
1887 if drevid is not None:
1885 if drevid is not None:
1888 drevids.add(drevid)
1886 drevids.add(drevid)
1889 revsbydrevid.setdefault(drevid, set()).add(rev)
1887 revsbydrevid.setdefault(drevid, set()).add(rev)
1890 else:
1888 else:
1891 unknownrevs.append(rev)
1889 unknownrevs.append(rev)
1892
1890
1893 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
1891 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
1894 drevsbyrev = {}
1892 drevsbyrev = {}
1895 for drev in drevs:
1893 for drev in drevs:
1896 for rev in revsbydrevid[int(drev[b'id'])]:
1894 for rev in revsbydrevid[int(drev[b'id'])]:
1897 drevsbyrev[rev] = drev
1895 drevsbyrev[rev] = drev
1898
1896
1899 def phabstatus(ctx):
1897 def phabstatus(ctx):
1900 drev = drevsbyrev[ctx.rev()]
1898 drev = drevsbyrev[ctx.rev()]
1901 status = ui.label(
1899 status = ui.label(
1902 b'%(statusName)s' % drev,
1900 b'%(statusName)s' % drev,
1903 b'phabricator.status.%s' % _getstatusname(drev),
1901 b'phabricator.status.%s' % _getstatusname(drev),
1904 )
1902 )
1905 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
1903 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
1906
1904
1907 revs -= smartset.baseset(unknownrevs)
1905 revs -= smartset.baseset(unknownrevs)
1908 revdag = graphmod.dagwalker(repo, revs)
1906 revdag = graphmod.dagwalker(repo, revs)
1909
1907
1910 ui.setconfig(b'experimental', b'graphshorten', True)
1908 ui.setconfig(b'experimental', b'graphshorten', True)
1911 displayer._exthook = phabstatus
1909 displayer._exthook = phabstatus
1912 nodelen = show.longestshortest(repo, revs)
1910 nodelen = show.longestshortest(repo, revs)
1913 logcmdutil.displaygraph(
1911 logcmdutil.displaygraph(
1914 ui,
1912 ui,
1915 repo,
1913 repo,
1916 revdag,
1914 revdag,
1917 displayer,
1915 displayer,
1918 graphmod.asciiedges,
1916 graphmod.asciiedges,
1919 props={b'nodelen': nodelen},
1917 props={b'nodelen': nodelen},
1920 )
1918 )
@@ -1,374 +1,398 b''
1 #require vcr
1 #require vcr
2 $ cat >> $HGRCPATH <<EOF
2 $ cat >> $HGRCPATH <<EOF
3 > [extensions]
3 > [extensions]
4 > phabricator =
4 > phabricator =
5 > EOF
5 > EOF
6 $ hg init repo
6 $ hg init repo
7 $ cd repo
7 $ cd repo
8 $ cat >> .hg/hgrc <<EOF
8 $ cat >> .hg/hgrc <<EOF
9 > [phabricator]
9 > [phabricator]
10 > url = https://phab.mercurial-scm.org/
10 > url = https://phab.mercurial-scm.org/
11 > callsign = HG
11 > callsign = HG
12 >
12 >
13 > [auth]
13 > [auth]
14 > hgphab.schemes = https
14 > hgphab.schemes = https
15 > hgphab.prefix = phab.mercurial-scm.org
15 > hgphab.prefix = phab.mercurial-scm.org
16 > # When working on the extension and making phabricator interaction
16 > # When working on the extension and making phabricator interaction
17 > # changes, edit this to be a real phabricator token. When done, edit
17 > # changes, edit this to be a real phabricator token. When done, edit
18 > # it back. The VCR transcripts will be auto-sanitised to replace your real
18 > # it back. The VCR transcripts will be auto-sanitised to replace your real
19 > # token with this value.
19 > # token with this value.
20 > hgphab.phabtoken = cli-hahayouwish
20 > hgphab.phabtoken = cli-hahayouwish
21 > EOF
21 > EOF
22 $ VCR="$TESTDIR/phabricator"
22 $ VCR="$TESTDIR/phabricator"
23
23
24 Error is handled reasonably. We override the phabtoken here so that
24 Error is handled reasonably. We override the phabtoken here so that
25 when you're developing changes to phabricator.py you can edit the
25 when you're developing changes to phabricator.py you can edit the
26 above config and have a real token in the test but not have to edit
26 above config and have a real token in the test but not have to edit
27 this test.
27 this test.
28 $ hg phabread --config auth.hgphab.phabtoken=cli-notavalidtoken \
28 $ hg phabread --config auth.hgphab.phabtoken=cli-notavalidtoken \
29 > --test-vcr "$VCR/phabread-conduit-error.json" D4480 | head
29 > --test-vcr "$VCR/phabread-conduit-error.json" D4480 | head
30 abort: Conduit Error (ERR-INVALID-AUTH): API token "cli-notavalidtoken" has the wrong length. API tokens should be 32 characters long.
30 abort: Conduit Error (ERR-INVALID-AUTH): API token "cli-notavalidtoken" has the wrong length. API tokens should be 32 characters long.
31
31
32 Missing arguments print the command help
32 Missing arguments don't crash, and may print the command help
33
33
34 $ hg phabread
34 $ hg debugcallconduit
35 hg phabread: invalid arguments
35 hg debugcallconduit: invalid arguments
36 hg phabread DREVSPEC [OPTIONS]
36 hg debugcallconduit METHOD
37
37
38 print patches from Phabricator suitable for importing
38 call Conduit API
39
39
40 options:
40 options:
41
41
42 --stack read dependencies
42 (use 'hg debugcallconduit -h' to show more help)
43
43 [255]
44 (use 'hg phabread -h' to show more help)
44 $ hg phabread
45 abort: empty DREVSPEC set
45 [255]
46 [255]
46
47
47 Basic phabread:
48 Basic phabread:
48 $ hg phabread --test-vcr "$VCR/phabread-4480.json" D4480 | head
49 $ hg phabread --test-vcr "$VCR/phabread-4480.json" D4480 | head
49 # HG changeset patch
50 # HG changeset patch
50 # Date 1536771503 0
51 # Date 1536771503 0
51 # Parent a5de21c9e3703f8e8eb064bd7d893ff2f703c66a
52 # Parent a5de21c9e3703f8e8eb064bd7d893ff2f703c66a
52 exchangev2: start to implement pull with wire protocol v2
53 exchangev2: start to implement pull with wire protocol v2
53
54
54 Wire protocol version 2 will take a substantially different
55 Wire protocol version 2 will take a substantially different
55 approach to exchange than version 1 (at least as far as pulling
56 approach to exchange than version 1 (at least as far as pulling
56 is concerned).
57 is concerned).
57
58
58 This commit establishes a new exchangev2 module for holding
59 This commit establishes a new exchangev2 module for holding
59
60
61 Phabread with multiple DREVSPEC
62
63 TODO: attempt to order related revisions like --stack?
64 $ hg phabread --test-vcr "$VCR/phabread-multi-drev.json" D8205 8206 D8207 \
65 > | grep '^Differential Revision'
66 Differential Revision: https://phab.mercurial-scm.org/D8205
67 Differential Revision: https://phab.mercurial-scm.org/D8206
68 Differential Revision: https://phab.mercurial-scm.org/D8207
69
70 Empty DREVSPECs don't crash
71
72 $ hg phabread --test-vcr "$VCR/phabread-empty-drev.json" D7917-D7917
73 abort: empty DREVSPEC set
74 [255]
75
76
60 phabupdate with an accept:
77 phabupdate with an accept:
61 $ hg phabupdate --accept D4564 \
78 $ hg phabupdate --accept D4564 \
62 > -m 'I think I like where this is headed. Will read rest of series later.'\
79 > -m 'I think I like where this is headed. Will read rest of series later.'\
63 > --test-vcr "$VCR/accept-4564.json"
80 > --test-vcr "$VCR/accept-4564.json"
64 abort: Conduit Error (ERR-CONDUIT-CORE): Validation errors:
81 abort: Conduit Error (ERR-CONDUIT-CORE): Validation errors:
65 - You can not accept this revision because it has already been closed. Only open revisions can be accepted.
82 - You can not accept this revision because it has already been closed. Only open revisions can be accepted.
66 [255]
83 [255]
67 $ hg phabupdate --accept D7913 -m 'LGTM' --test-vcr "$VCR/accept-7913.json"
84 $ hg phabupdate --accept D7913 -m 'LGTM' --test-vcr "$VCR/accept-7913.json"
68
85
69 Create a differential diff:
86 Create a differential diff:
70 $ HGENCODING=utf-8; export HGENCODING
87 $ HGENCODING=utf-8; export HGENCODING
71 $ echo alpha > alpha
88 $ echo alpha > alpha
72 $ hg ci --addremove -m 'create alpha for phabricator test €'
89 $ hg ci --addremove -m 'create alpha for phabricator test €'
73 adding alpha
90 adding alpha
74 $ hg phabsend -r . --test-vcr "$VCR/phabsend-create-alpha.json"
91 $ hg phabsend -r . --test-vcr "$VCR/phabsend-create-alpha.json"
75 D7915 - created - d386117f30e6: create alpha for phabricator test \xe2\x82\xac (esc)
92 D7915 - created - d386117f30e6: create alpha for phabricator test \xe2\x82\xac (esc)
76 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d386117f30e6-24ffe649-phabsend.hg
93 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d386117f30e6-24ffe649-phabsend.hg
77 $ echo more >> alpha
94 $ echo more >> alpha
78 $ HGEDITOR=true hg ci --amend
95 $ HGEDITOR=true hg ci --amend
79 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/347bf67801e5-3bf313e4-amend.hg
96 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/347bf67801e5-3bf313e4-amend.hg
80 $ echo beta > beta
97 $ echo beta > beta
81 $ hg ci --addremove -m 'create beta for phabricator test'
98 $ hg ci --addremove -m 'create beta for phabricator test'
82 adding beta
99 adding beta
83 $ hg phabsend -r ".^::" --test-vcr "$VCR/phabsend-update-alpha-create-beta.json"
100 $ hg phabsend -r ".^::" --test-vcr "$VCR/phabsend-update-alpha-create-beta.json"
84 D7915 - updated - c44b38f24a45: create alpha for phabricator test \xe2\x82\xac (esc)
101 D7915 - updated - c44b38f24a45: create alpha for phabricator test \xe2\x82\xac (esc)
85 D7916 - created - 9e6901f21d5b: create beta for phabricator test
102 D7916 - created - 9e6901f21d5b: create beta for phabricator test
86 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9e6901f21d5b-1fcd4f0e-phabsend.hg
103 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9e6901f21d5b-1fcd4f0e-phabsend.hg
87 $ unset HGENCODING
104 $ unset HGENCODING
88
105
89 The amend won't explode after posting a public commit. The local tag is left
106 The amend won't explode after posting a public commit. The local tag is left
90 behind to identify it.
107 behind to identify it.
91
108
92 $ echo 'public change' > beta
109 $ echo 'public change' > beta
93 $ hg ci -m 'create public change for phabricator testing'
110 $ hg ci -m 'create public change for phabricator testing'
94 $ hg phase --public .
111 $ hg phase --public .
95 $ echo 'draft change' > alpha
112 $ echo 'draft change' > alpha
96 $ hg ci -m 'create draft change for phabricator testing'
113 $ hg ci -m 'create draft change for phabricator testing'
97 $ hg phabsend --amend -r '.^::' --test-vcr "$VCR/phabsend-create-public.json"
114 $ hg phabsend --amend -r '.^::' --test-vcr "$VCR/phabsend-create-public.json"
98 D7917 - created - 7b4185ab5d16: create public change for phabricator testing
115 D7917 - created - 7b4185ab5d16: create public change for phabricator testing
99 D7918 - created - 251c1c333fc6: create draft change for phabricator testing
116 D7918 - created - 251c1c333fc6: create draft change for phabricator testing
100 warning: not updating public commit 2:7b4185ab5d16
117 warning: not updating public commit 2:7b4185ab5d16
101 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/251c1c333fc6-41cb7c3b-phabsend.hg
118 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/251c1c333fc6-41cb7c3b-phabsend.hg
102 $ hg tags -v
119 $ hg tags -v
103 tip 3:3244dc4a3334
120 tip 3:3244dc4a3334
104 D7917 2:7b4185ab5d16 local
121 D7917 2:7b4185ab5d16 local
105
122
106 $ hg debugcallconduit user.search --test-vcr "$VCR/phab-conduit.json" <<EOF
123 $ hg debugcallconduit user.search --test-vcr "$VCR/phab-conduit.json" <<EOF
107 > {
124 > {
108 > "constraints": {
125 > "constraints": {
109 > "isBot": true
126 > "isBot": true
110 > }
127 > }
111 > }
128 > }
112 > EOF
129 > EOF
113 {
130 {
114 "cursor": {
131 "cursor": {
115 "after": null,
132 "after": null,
116 "before": null,
133 "before": null,
117 "limit": 100,
134 "limit": 100,
118 "order": null
135 "order": null
119 },
136 },
120 "data": [],
137 "data": [],
121 "maps": {},
138 "maps": {},
122 "query": {
139 "query": {
123 "queryKey": null
140 "queryKey": null
124 }
141 }
125 }
142 }
126
143
127 Template keywords
144 Template keywords
128 $ hg log -T'{rev} {phabreview|json}\n'
145 $ hg log -T'{rev} {phabreview|json}\n'
129 3 {"id": "D7918", "url": "https://phab.mercurial-scm.org/D7918"}
146 3 {"id": "D7918", "url": "https://phab.mercurial-scm.org/D7918"}
130 2 {"id": "D7917", "url": "https://phab.mercurial-scm.org/D7917"}
147 2 {"id": "D7917", "url": "https://phab.mercurial-scm.org/D7917"}
131 1 {"id": "D7916", "url": "https://phab.mercurial-scm.org/D7916"}
148 1 {"id": "D7916", "url": "https://phab.mercurial-scm.org/D7916"}
132 0 {"id": "D7915", "url": "https://phab.mercurial-scm.org/D7915"}
149 0 {"id": "D7915", "url": "https://phab.mercurial-scm.org/D7915"}
133
150
134 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n'
151 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n'
135 3 https://phab.mercurial-scm.org/D7918 D7918
152 3 https://phab.mercurial-scm.org/D7918 D7918
136 2 https://phab.mercurial-scm.org/D7917 D7917
153 2 https://phab.mercurial-scm.org/D7917 D7917
137 1 https://phab.mercurial-scm.org/D7916 D7916
154 1 https://phab.mercurial-scm.org/D7916 D7916
138 0 https://phab.mercurial-scm.org/D7915 D7915
155 0 https://phab.mercurial-scm.org/D7915 D7915
139
156
140 Commenting when phabsending:
157 Commenting when phabsending:
141 $ echo comment > comment
158 $ echo comment > comment
142 $ hg ci --addremove -m "create comment for phabricator test"
159 $ hg ci --addremove -m "create comment for phabricator test"
143 adding comment
160 adding comment
144 $ hg phabsend -r . -m "For default branch" --test-vcr "$VCR/phabsend-comment-created.json"
161 $ hg phabsend -r . -m "For default branch" --test-vcr "$VCR/phabsend-comment-created.json"
145 D7919 - created - d5dddca9023d: create comment for phabricator test
162 D7919 - created - d5dddca9023d: create comment for phabricator test
146 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d5dddca9023d-adf673ba-phabsend.hg
163 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d5dddca9023d-adf673ba-phabsend.hg
147 $ echo comment2 >> comment
164 $ echo comment2 >> comment
148 $ hg ci --amend
165 $ hg ci --amend
149 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f7db812bbe1d-8fcded77-amend.hg
166 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f7db812bbe1d-8fcded77-amend.hg
150 $ hg phabsend -r . -m "Address review comments" --test-vcr "$VCR/phabsend-comment-updated.json"
167 $ hg phabsend -r . -m "Address review comments" --test-vcr "$VCR/phabsend-comment-updated.json"
151 D7919 - updated - 1849d7828727: create comment for phabricator test
168 D7919 - updated - 1849d7828727: create comment for phabricator test
152
169
153 Phabsending a skipped commit:
170 Phabsending a skipped commit:
154 $ hg phabsend --no-amend -r . --test-vcr "$VCR/phabsend-skipped.json"
171 $ hg phabsend --no-amend -r . --test-vcr "$VCR/phabsend-skipped.json"
155 D7919 - skipped - 1849d7828727: create comment for phabricator test
172 D7919 - skipped - 1849d7828727: create comment for phabricator test
156
173
157 Phabesending a new binary, a modified binary, and a removed binary
174 Phabesending a new binary, a modified binary, and a removed binary
158
175
159 >>> open('bin', 'wb').write(b'\0a') and None
176 >>> open('bin', 'wb').write(b'\0a') and None
160 $ hg ci -Am 'add binary'
177 $ hg ci -Am 'add binary'
161 adding bin
178 adding bin
162 >>> open('bin', 'wb').write(b'\0b') and None
179 >>> open('bin', 'wb').write(b'\0b') and None
163 $ hg ci -m 'modify binary'
180 $ hg ci -m 'modify binary'
164 $ hg rm bin
181 $ hg rm bin
165 $ hg ci -m 'remove binary'
182 $ hg ci -m 'remove binary'
166 $ hg phabsend -r .~2:: --test-vcr "$VCR/phabsend-binary.json"
183 $ hg phabsend -r .~2:: --test-vcr "$VCR/phabsend-binary.json"
167 uploading bin@aa24a81f55de
184 uploading bin@aa24a81f55de
168 D8007 - created - aa24a81f55de: add binary
185 D8007 - created - aa24a81f55de: add binary
169 uploading bin@d8d62a881b54
186 uploading bin@d8d62a881b54
170 D8008 - created - d8d62a881b54: modify binary
187 D8008 - created - d8d62a881b54: modify binary
171 D8009 - created - af55645b2e29: remove binary
188 D8009 - created - af55645b2e29: remove binary
172 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/aa24a81f55de-a3a0cf24-phabsend.hg
189 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/aa24a81f55de-a3a0cf24-phabsend.hg
173
190
174 Phabsend a renamed binary and a copied binary, with and without content changes
191 Phabsend a renamed binary and a copied binary, with and without content changes
175 to src and dest
192 to src and dest
176
193
177 >>> open('bin2', 'wb').write(b'\0c') and None
194 >>> open('bin2', 'wb').write(b'\0c') and None
178 $ hg ci -Am 'add another binary'
195 $ hg ci -Am 'add another binary'
179 adding bin2
196 adding bin2
180
197
181 TODO: "bin2" can't be viewed in this commit (left or right side), and the URL
198 TODO: "bin2" can't be viewed in this commit (left or right side), and the URL
182 looks much different than when viewing "bin2_moved". No idea if this is a phab
199 looks much different than when viewing "bin2_moved". No idea if this is a phab
183 bug, or phabsend bug. The patch (as printed by phabread) look reasonable
200 bug, or phabsend bug. The patch (as printed by phabread) look reasonable
184 though.
201 though.
185
202
186 $ hg mv bin2 bin2_moved
203 $ hg mv bin2 bin2_moved
187 $ hg ci -m "moved binary"
204 $ hg ci -m "moved binary"
188
205
189 Note: "bin2_moved" is also not viewable in phabricator with this review
206 Note: "bin2_moved" is also not viewable in phabricator with this review
190
207
191 $ hg cp bin2_moved bin2_copied
208 $ hg cp bin2_moved bin2_copied
192 $ hg ci -m "copied binary"
209 $ hg ci -m "copied binary"
193
210
194 Note: "bin2_moved_again" is marked binary in phabricator, and both sides of it
211 Note: "bin2_moved_again" is marked binary in phabricator, and both sides of it
195 are viewable in their proper state. "bin2_copied" is not viewable, and not
212 are viewable in their proper state. "bin2_copied" is not viewable, and not
196 listed as binary in phabricator.
213 listed as binary in phabricator.
197
214
198 >>> open('bin2_copied', 'wb').write(b'\0move+mod') and None
215 >>> open('bin2_copied', 'wb').write(b'\0move+mod') and None
199 $ hg mv bin2_copied bin2_moved_again
216 $ hg mv bin2_copied bin2_moved_again
200 $ hg ci -m "move+mod copied binary"
217 $ hg ci -m "move+mod copied binary"
201
218
202 Note: "bin2_moved" and "bin2_moved_copy" are both marked binary, and both
219 Note: "bin2_moved" and "bin2_moved_copy" are both marked binary, and both
203 viewable on each side.
220 viewable on each side.
204
221
205 >>> open('bin2_moved', 'wb').write(b'\0precopy mod') and None
222 >>> open('bin2_moved', 'wb').write(b'\0precopy mod') and None
206 $ hg cp bin2_moved bin2_moved_copied
223 $ hg cp bin2_moved bin2_moved_copied
207 >>> open('bin2_moved', 'wb').write(b'\0copy src+mod') and None
224 >>> open('bin2_moved', 'wb').write(b'\0copy src+mod') and None
208 $ hg ci -m "copy+mod moved binary"
225 $ hg ci -m "copy+mod moved binary"
209
226
210 $ hg phabsend -r .~4:: --test-vcr "$VCR/phabsend-binary-renames.json"
227 $ hg phabsend -r .~4:: --test-vcr "$VCR/phabsend-binary-renames.json"
211 uploading bin2@f42f9195e00c
228 uploading bin2@f42f9195e00c
212 D8128 - created - f42f9195e00c: add another binary
229 D8128 - created - f42f9195e00c: add another binary
213 D8129 - created - 834ab31d80ae: moved binary
230 D8129 - created - 834ab31d80ae: moved binary
214 D8130 - created - 494b750e5194: copied binary
231 D8130 - created - 494b750e5194: copied binary
215 uploading bin2_moved_again@25f766b50cc2
232 uploading bin2_moved_again@25f766b50cc2
216 D8131 - created - 25f766b50cc2: move+mod copied binary
233 D8131 - created - 25f766b50cc2: move+mod copied binary
217 uploading bin2_moved_copied@1b87b363a5e4
234 uploading bin2_moved_copied@1b87b363a5e4
218 uploading bin2_moved@1b87b363a5e4
235 uploading bin2_moved@1b87b363a5e4
219 D8132 - created - 1b87b363a5e4: copy+mod moved binary
236 D8132 - created - 1b87b363a5e4: copy+mod moved binary
220 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f42f9195e00c-e82a0769-phabsend.hg
237 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f42f9195e00c-e82a0769-phabsend.hg
221
238
222 Phabreading a DREV with a local:commits time as a string:
239 Phabreading a DREV with a local:commits time as a string:
223 $ hg phabread --test-vcr "$VCR/phabread-str-time.json" D1285
240 $ hg phabread --test-vcr "$VCR/phabread-str-time.json" D1285
224 # HG changeset patch
241 # HG changeset patch
225 # User Pulkit Goyal <7895pulkit@gmail.com>
242 # User Pulkit Goyal <7895pulkit@gmail.com>
226 # Date 1509404054 -19800
243 # Date 1509404054 -19800
227 # Node ID 44fc1c1f1774a76423b9c732af6938435099bcc5
244 # Node ID 44fc1c1f1774a76423b9c732af6938435099bcc5
228 # Parent 8feef8ef8389a3b544e0a74624f1efc3a8d85d35
245 # Parent 8feef8ef8389a3b544e0a74624f1efc3a8d85d35
229 repoview: add a new attribute _visibilityexceptions and related API
246 repoview: add a new attribute _visibilityexceptions and related API
230
247
231 Currently we don't have a defined way in core to make some hidden revisions
248 Currently we don't have a defined way in core to make some hidden revisions
232 visible in filtered repo. Extensions to achieve the purpose of unhiding some
249 visible in filtered repo. Extensions to achieve the purpose of unhiding some
233 hidden commits, wrap repoview.pinnedrevs() function.
250 hidden commits, wrap repoview.pinnedrevs() function.
234
251
235 To make the above task simple and have well defined API, this patch adds a new
252 To make the above task simple and have well defined API, this patch adds a new
236 attribute '_visibilityexceptions' to repoview class which will contains
253 attribute '_visibilityexceptions' to repoview class which will contains
237 the hidden revs which should be exception.
254 the hidden revs which should be exception.
238 This will allow to set different exceptions for different repoview objects
255 This will allow to set different exceptions for different repoview objects
239 backed by the same unfiltered repo.
256 backed by the same unfiltered repo.
240
257
241 This patch also adds API to add revs to the attribute set and get them.
258 This patch also adds API to add revs to the attribute set and get them.
242
259
243 Thanks to Jun for suggesting the use of repoview class instead of localrepo.
260 Thanks to Jun for suggesting the use of repoview class instead of localrepo.
244
261
245 Differential Revision: https://phab.mercurial-scm.org/D1285
262 Differential Revision: https://phab.mercurial-scm.org/D1285
246 diff --git a/mercurial/repoview.py b/mercurial/repoview.py
263 diff --git a/mercurial/repoview.py b/mercurial/repoview.py
247 --- a/mercurial/repoview.py
264 --- a/mercurial/repoview.py
248 +++ b/mercurial/repoview.py
265 +++ b/mercurial/repoview.py
249 @@ * @@ (glob)
266 @@ * @@ (glob)
250 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
267 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
251 """
268 """
252
269
253 + # hidden revs which should be visible
270 + # hidden revs which should be visible
254 + _visibilityexceptions = set()
271 + _visibilityexceptions = set()
255 +
272 +
256 def __init__(self, repo, filtername):
273 def __init__(self, repo, filtername):
257 object.__setattr__(self, r'_unfilteredrepo', repo)
274 object.__setattr__(self, r'_unfilteredrepo', repo)
258 object.__setattr__(self, r'filtername', filtername)
275 object.__setattr__(self, r'filtername', filtername)
259 @@ -231,6 +234,14 @@
276 @@ -231,6 +234,14 @@
260 return self
277 return self
261 return self.unfiltered().filtered(name)
278 return self.unfiltered().filtered(name)
262
279
263 + def addvisibilityexceptions(self, revs):
280 + def addvisibilityexceptions(self, revs):
264 + """adds hidden revs which should be visible to set of exceptions"""
281 + """adds hidden revs which should be visible to set of exceptions"""
265 + self._visibilityexceptions.update(revs)
282 + self._visibilityexceptions.update(revs)
266 +
283 +
267 + def getvisibilityexceptions(self):
284 + def getvisibilityexceptions(self):
268 + """returns the set of hidden revs which should be visible"""
285 + """returns the set of hidden revs which should be visible"""
269 + return self._visibilityexceptions
286 + return self._visibilityexceptions
270 +
287 +
271 # everything access are forwarded to the proxied repo
288 # everything access are forwarded to the proxied repo
272 def __getattr__(self, attr):
289 def __getattr__(self, attr):
273 return getattr(self._unfilteredrepo, attr)
290 return getattr(self._unfilteredrepo, attr)
274 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
291 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
275 --- a/mercurial/localrepo.py
292 --- a/mercurial/localrepo.py
276 +++ b/mercurial/localrepo.py
293 +++ b/mercurial/localrepo.py
277 @@ -570,6 +570,14 @@
294 @@ -570,6 +570,14 @@
278 def close(self):
295 def close(self):
279 self._writecaches()
296 self._writecaches()
280
297
281 + def addvisibilityexceptions(self, exceptions):
298 + def addvisibilityexceptions(self, exceptions):
282 + # should be called on a filtered repository
299 + # should be called on a filtered repository
283 + pass
300 + pass
284 +
301 +
285 + def getvisibilityexceptions(self):
302 + def getvisibilityexceptions(self):
286 + # should be called on a filtered repository
303 + # should be called on a filtered repository
287 + return set()
304 + return set()
288 +
305 +
289 def _loadextensions(self):
306 def _loadextensions(self):
290 extensions.loadall(self.ui)
307 extensions.loadall(self.ui)
291
308
292
309
293 A bad .arcconfig doesn't error out
310 A bad .arcconfig doesn't error out
294 $ echo 'garbage' > .arcconfig
311 $ echo 'garbage' > .arcconfig
295 $ hg config phabricator --debug
312 $ hg config phabricator --debug
296 invalid JSON in $TESTTMP/repo/.arcconfig
313 invalid JSON in $TESTTMP/repo/.arcconfig
297 read config from: */.hgrc (glob)
314 read config from: */.hgrc (glob)
298 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=https://phab.mercurial-scm.org/ (glob)
315 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=https://phab.mercurial-scm.org/ (glob)
299 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=HG (glob)
316 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=HG (glob)
300
317
301 The .arcconfig content overrides global config
318 The .arcconfig content overrides global config
302 $ cat >> $HGRCPATH << EOF
319 $ cat >> $HGRCPATH << EOF
303 > [phabricator]
320 > [phabricator]
304 > url = global
321 > url = global
305 > callsign = global
322 > callsign = global
306 > EOF
323 > EOF
307 $ cp $TESTDIR/../.arcconfig .
324 $ cp $TESTDIR/../.arcconfig .
308 $ mv .hg/hgrc .hg/hgrc.bak
325 $ mv .hg/hgrc .hg/hgrc.bak
309 $ hg config phabricator --debug
326 $ hg config phabricator --debug
310 read config from: */.hgrc (glob)
327 read config from: */.hgrc (glob)
311 $TESTTMP/repo/.arcconfig: phabricator.callsign=HG
328 $TESTTMP/repo/.arcconfig: phabricator.callsign=HG
312 $TESTTMP/repo/.arcconfig: phabricator.url=https://phab.mercurial-scm.org/
329 $TESTTMP/repo/.arcconfig: phabricator.url=https://phab.mercurial-scm.org/
313
330
314 But it doesn't override local config
331 But it doesn't override local config
315 $ cat >> .hg/hgrc << EOF
332 $ cat >> .hg/hgrc << EOF
316 > [phabricator]
333 > [phabricator]
317 > url = local
334 > url = local
318 > callsign = local
335 > callsign = local
319 > EOF
336 > EOF
320 $ hg config phabricator --debug
337 $ hg config phabricator --debug
321 read config from: */.hgrc (glob)
338 read config from: */.hgrc (glob)
322 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=local (glob)
339 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=local (glob)
323 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=local (glob)
340 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=local (glob)
324 $ mv .hg/hgrc.bak .hg/hgrc
341 $ mv .hg/hgrc.bak .hg/hgrc
325
342
326 Phabimport works with a stack
343 Phabimport works with a stack
327
344
328 $ cd ..
345 $ cd ..
329 $ hg clone repo repo2 -qr 1
346 $ hg clone repo repo2 -qr 1
330 $ cp repo/.hg/hgrc repo2/.hg/
347 $ cp repo/.hg/hgrc repo2/.hg/
331 $ cd repo2
348 $ cd repo2
332 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json"
349 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json"
333 applying patch from D7917
350 applying patch from D7917
334 applying patch from D7918
351 applying patch from D7918
335 $ hg log -r .: -G -Tcompact
352 $ hg log -r .: -G -Tcompact
336 o 3[tip] aaef04066140 1970-01-01 00:00 +0000 test
353 o 3[tip] aaef04066140 1970-01-01 00:00 +0000 test
337 | create draft change for phabricator testing
354 | create draft change for phabricator testing
338 |
355 |
339 o 2 8de3712202d1 1970-01-01 00:00 +0000 test
356 o 2 8de3712202d1 1970-01-01 00:00 +0000 test
340 | create public change for phabricator testing
357 | create public change for phabricator testing
341 |
358 |
342 @ 1 a692622e6937 1970-01-01 00:00 +0000 test
359 @ 1 a692622e6937 1970-01-01 00:00 +0000 test
343 | create beta for phabricator test
360 | create beta for phabricator test
344 ~
361 ~
345 Phabimport can create secret commits
362 Phabimport can create secret commits
346
363
347 $ hg rollback --config ui.rollback=True
364 $ hg rollback --config ui.rollback=True
348 repository tip rolled back to revision 1 (undo phabimport)
365 repository tip rolled back to revision 1 (undo phabimport)
349 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json" \
366 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json" \
350 > --config phabimport.secret=True
367 > --config phabimport.secret=True
351 applying patch from D7917
368 applying patch from D7917
352 applying patch from D7918
369 applying patch from D7918
353 $ hg log -r 'reverse(.:)' -T phases
370 $ hg log -r 'reverse(.:)' -T phases
354 changeset: 3:aaef04066140
371 changeset: 3:aaef04066140
355 tag: tip
372 tag: tip
356 phase: secret
373 phase: secret
357 user: test
374 user: test
358 date: Thu Jan 01 00:00:00 1970 +0000
375 date: Thu Jan 01 00:00:00 1970 +0000
359 summary: create draft change for phabricator testing
376 summary: create draft change for phabricator testing
360
377
361 changeset: 2:8de3712202d1
378 changeset: 2:8de3712202d1
362 phase: secret
379 phase: secret
363 user: test
380 user: test
364 date: Thu Jan 01 00:00:00 1970 +0000
381 date: Thu Jan 01 00:00:00 1970 +0000
365 summary: create public change for phabricator testing
382 summary: create public change for phabricator testing
366
383
367 changeset: 1:a692622e6937
384 changeset: 1:a692622e6937
368 phase: public
385 phase: public
369 user: test
386 user: test
370 date: Thu Jan 01 00:00:00 1970 +0000
387 date: Thu Jan 01 00:00:00 1970 +0000
371 summary: create beta for phabricator test
388 summary: create beta for phabricator test
372
389
390 Phabimport accepts multiple DREVSPECs
391
392 $ hg rollback --config ui.rollback=True
393 repository tip rolled back to revision 1 (undo phabimport)
394 $ hg phabimport --no-stack D7917 D7918 --test-vcr "$VCR/phabimport-multi-drev.json"
395 applying patch from D7917
396 applying patch from D7918
373
397
374 $ cd ..
398 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now