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¶ms=%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¶ms=%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¶ms=%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¶ms=%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¶ms=%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 |
|
|
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 |
|
|
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 = |
|
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 |
|
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