##// END OF EJS Templates
phabricator: finding old nodes in batch...
Jun Wu -
r33442:3ab0d576 default
parent child Browse files
Show More
@@ -1,513 +1,524 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
7 """simple Phabricator integration
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 without amending commit messages, and a ``phabread``
10 changesets to Phabricator without amending commit messages, and a ``phabread``
11 command which prints a stack of revisions in a format suitable
11 command which prints a stack of revisions in a format suitable
12 for :hg:`import`.
12 for :hg:`import`.
13
13
14 By default, Phabricator requires ``Test Plan`` which might prevent some
14 By default, Phabricator requires ``Test Plan`` which might prevent some
15 changeset from being sent. The requirement could be disabled by changing
15 changeset from being sent. The requirement could be disabled by changing
16 ``differential.require-test-plan-field`` config server side.
16 ``differential.require-test-plan-field`` config server side.
17
17
18 Config::
18 Config::
19
19
20 [phabricator]
20 [phabricator]
21 # Phabricator URL
21 # Phabricator URL
22 url = https://phab.example.com/
22 url = https://phab.example.com/
23
23
24 # API token. Get it from https://$HOST/conduit/login/
24 # API token. Get it from https://$HOST/conduit/login/
25 token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
25 token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
26
26
27 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
27 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
28 # callsign is "FOO".
28 # callsign is "FOO".
29 callsign = FOO
29 callsign = FOO
30
30
31 """
31 """
32
32
33 from __future__ import absolute_import
33 from __future__ import absolute_import
34
34
35 import json
35 import json
36 import re
36 import re
37
37
38 from mercurial.i18n import _
38 from mercurial.i18n import _
39 from mercurial import (
39 from mercurial import (
40 encoding,
40 encoding,
41 error,
41 error,
42 mdiff,
42 mdiff,
43 obsolete,
43 obsolete,
44 patch,
44 patch,
45 registrar,
45 registrar,
46 scmutil,
46 scmutil,
47 tags,
47 tags,
48 url as urlmod,
48 url as urlmod,
49 util,
49 util,
50 )
50 )
51
51
52 cmdtable = {}
52 cmdtable = {}
53 command = registrar.command(cmdtable)
53 command = registrar.command(cmdtable)
54
54
55 def urlencodenested(params):
55 def urlencodenested(params):
56 """like urlencode, but works with nested parameters.
56 """like urlencode, but works with nested parameters.
57
57
58 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
58 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
59 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
59 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
60 urlencode. Note: the encoding is consistent with PHP's http_build_query.
60 urlencode. Note: the encoding is consistent with PHP's http_build_query.
61 """
61 """
62 flatparams = util.sortdict()
62 flatparams = util.sortdict()
63 def process(prefix, obj):
63 def process(prefix, obj):
64 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
64 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
65 if items is None:
65 if items is None:
66 flatparams[prefix] = obj
66 flatparams[prefix] = obj
67 else:
67 else:
68 for k, v in items(obj):
68 for k, v in items(obj):
69 if prefix:
69 if prefix:
70 process('%s[%s]' % (prefix, k), v)
70 process('%s[%s]' % (prefix, k), v)
71 else:
71 else:
72 process(k, v)
72 process(k, v)
73 process('', params)
73 process('', params)
74 return util.urlreq.urlencode(flatparams)
74 return util.urlreq.urlencode(flatparams)
75
75
76 def readurltoken(repo):
76 def readurltoken(repo):
77 """return conduit url, token and make sure they exist
77 """return conduit url, token and make sure they exist
78
78
79 Currently read from [phabricator] config section. In the future, it might
79 Currently read from [phabricator] config section. In the future, it might
80 make sense to read from .arcconfig and .arcrc as well.
80 make sense to read from .arcconfig and .arcrc as well.
81 """
81 """
82 values = []
82 values = []
83 section = 'phabricator'
83 section = 'phabricator'
84 for name in ['url', 'token']:
84 for name in ['url', 'token']:
85 value = repo.ui.config(section, name)
85 value = repo.ui.config(section, name)
86 if not value:
86 if not value:
87 raise error.Abort(_('config %s.%s is required') % (section, name))
87 raise error.Abort(_('config %s.%s is required') % (section, name))
88 values.append(value)
88 values.append(value)
89 return values
89 return values
90
90
91 def callconduit(repo, name, params):
91 def callconduit(repo, name, params):
92 """call Conduit API, params is a dict. return json.loads result, or None"""
92 """call Conduit API, params is a dict. return json.loads result, or None"""
93 host, token = readurltoken(repo)
93 host, token = readurltoken(repo)
94 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
94 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
95 urlopener = urlmod.opener(repo.ui, authinfo)
95 urlopener = urlmod.opener(repo.ui, authinfo)
96 repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
96 repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
97 params = params.copy()
97 params = params.copy()
98 params['api.token'] = token
98 params['api.token'] = token
99 request = util.urlreq.request(url, data=urlencodenested(params))
99 request = util.urlreq.request(url, data=urlencodenested(params))
100 body = urlopener.open(request).read()
100 body = urlopener.open(request).read()
101 repo.ui.debug('Conduit Response: %s\n' % body)
101 repo.ui.debug('Conduit Response: %s\n' % body)
102 parsed = json.loads(body)
102 parsed = json.loads(body)
103 if parsed.get(r'error_code'):
103 if parsed.get(r'error_code'):
104 msg = (_('Conduit Error (%s): %s')
104 msg = (_('Conduit Error (%s): %s')
105 % (parsed[r'error_code'], parsed[r'error_info']))
105 % (parsed[r'error_code'], parsed[r'error_info']))
106 raise error.Abort(msg)
106 raise error.Abort(msg)
107 return parsed[r'result']
107 return parsed[r'result']
108
108
109 @command('debugcallconduit', [], _('METHOD'))
109 @command('debugcallconduit', [], _('METHOD'))
110 def debugcallconduit(ui, repo, name):
110 def debugcallconduit(ui, repo, name):
111 """call Conduit API
111 """call Conduit API
112
112
113 Call parameters are read from stdin as a JSON blob. Result will be written
113 Call parameters are read from stdin as a JSON blob. Result will be written
114 to stdout as a JSON blob.
114 to stdout as a JSON blob.
115 """
115 """
116 params = json.loads(ui.fin.read())
116 params = json.loads(ui.fin.read())
117 result = callconduit(repo, name, params)
117 result = callconduit(repo, name, params)
118 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
118 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
119 ui.write('%s\n' % s)
119 ui.write('%s\n' % s)
120
120
121 def getrepophid(repo):
121 def getrepophid(repo):
122 """given callsign, return repository PHID or None"""
122 """given callsign, return repository PHID or None"""
123 # developer config: phabricator.repophid
123 # developer config: phabricator.repophid
124 repophid = repo.ui.config('phabricator', 'repophid')
124 repophid = repo.ui.config('phabricator', 'repophid')
125 if repophid:
125 if repophid:
126 return repophid
126 return repophid
127 callsign = repo.ui.config('phabricator', 'callsign')
127 callsign = repo.ui.config('phabricator', 'callsign')
128 if not callsign:
128 if not callsign:
129 return None
129 return None
130 query = callconduit(repo, 'diffusion.repository.search',
130 query = callconduit(repo, 'diffusion.repository.search',
131 {'constraints': {'callsigns': [callsign]}})
131 {'constraints': {'callsigns': [callsign]}})
132 if len(query[r'data']) == 0:
132 if len(query[r'data']) == 0:
133 return None
133 return None
134 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
134 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
135 repo.ui.setconfig('phabricator', 'repophid', repophid)
135 repo.ui.setconfig('phabricator', 'repophid', repophid)
136 return repophid
136 return repophid
137
137
138 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
138 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
139 _differentialrevisiondescre = re.compile(
139 _differentialrevisiondescre = re.compile(
140 '^Differential Revision:.*D([1-9][0-9]*)$', re.M)
140 '^Differential Revision:.*D([1-9][0-9]*)$', re.M)
141
141
142 def getmapping(ctx):
142 def getoldnodedrevmap(repo, nodelist):
143 """return (node, associated Differential Revision ID) or (None, None)
143 """find previous nodes that has been sent to Phabricator
144
145 return {node: (oldnode or None, Differential Revision ID)}
146 for node in nodelist with known previous sent versions, or associated
147 Differential Revision IDs.
144
148
145 Examines all precursors and their tags. Tags with format like "D1234" are
149 Examines all precursors and their tags. Tags with format like "D1234" are
146 considered a match and the node with that tag, and the number after "D"
150 considered a match and the node with that tag, and the number after "D"
147 (ex. 1234) will be returned.
151 (ex. 1234) will be returned.
148
152
149 If tags are not found, examine commit message. The "Differential Revision:"
153 If tags are not found, examine commit message. The "Differential Revision:"
150 line could associate this changeset to a Differential Revision.
154 line could associate this changeset to a Differential Revision.
151 """
155 """
152 unfi = ctx.repo().unfiltered()
156 url, token = readurltoken(repo)
157 unfi = repo.unfiltered()
153 nodemap = unfi.changelog.nodemap
158 nodemap = unfi.changelog.nodemap
154
159
155 # Check tags like "D123"
160 result = {} # {node: (oldnode or None, drev)}
156 for n in obsolete.allprecursors(unfi.obsstore, [ctx.node()]):
161 for node in nodelist:
157 if n in nodemap:
162 ctx = unfi[node]
158 for tag in unfi.nodetags(n):
163 # Check tags like "D123"
159 m = _differentialrevisiontagre.match(tag)
164 for n in obsolete.allprecursors(unfi.obsstore, [node]):
160 if m:
165 if n in nodemap:
161 return n, int(m.group(1))
166 for tag in unfi.nodetags(n):
167 m = _differentialrevisiontagre.match(tag)
168 if m:
169 result[node] = (n, int(m.group(1)))
170 continue
162
171
163 # Check commit message
172 # Check commit message
164 m = _differentialrevisiondescre.search(ctx.description())
173 m = _differentialrevisiondescre.search(ctx.description())
165 if m:
174 if m:
166 return None, int(m.group(1))
175 result[node] = (None, int(m.group(1)))
167
176
168 return None, None
177 return result
169
178
170 def getdiff(ctx, diffopts):
179 def getdiff(ctx, diffopts):
171 """plain-text diff without header (user, commit message, etc)"""
180 """plain-text diff without header (user, commit message, etc)"""
172 output = util.stringio()
181 output = util.stringio()
173 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
182 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
174 None, opts=diffopts):
183 None, opts=diffopts):
175 output.write(chunk)
184 output.write(chunk)
176 return output.getvalue()
185 return output.getvalue()
177
186
178 def creatediff(ctx):
187 def creatediff(ctx):
179 """create a Differential Diff"""
188 """create a Differential Diff"""
180 repo = ctx.repo()
189 repo = ctx.repo()
181 repophid = getrepophid(repo)
190 repophid = getrepophid(repo)
182 # Create a "Differential Diff" via "differential.createrawdiff" API
191 # Create a "Differential Diff" via "differential.createrawdiff" API
183 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
192 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
184 if repophid:
193 if repophid:
185 params['repositoryPHID'] = repophid
194 params['repositoryPHID'] = repophid
186 diff = callconduit(repo, 'differential.createrawdiff', params)
195 diff = callconduit(repo, 'differential.createrawdiff', params)
187 if not diff:
196 if not diff:
188 raise error.Abort(_('cannot create diff for %s') % ctx)
197 raise error.Abort(_('cannot create diff for %s') % ctx)
189 return diff
198 return diff
190
199
191 def writediffproperties(ctx, diff):
200 def writediffproperties(ctx, diff):
192 """write metadata to diff so patches could be applied losslessly"""
201 """write metadata to diff so patches could be applied losslessly"""
193 params = {
202 params = {
194 'diff_id': diff[r'id'],
203 'diff_id': diff[r'id'],
195 'name': 'hg:meta',
204 'name': 'hg:meta',
196 'data': json.dumps({
205 'data': json.dumps({
197 'user': ctx.user(),
206 'user': ctx.user(),
198 'date': '%d %d' % ctx.date(),
207 'date': '%d %d' % ctx.date(),
199 'node': ctx.hex(),
208 'node': ctx.hex(),
200 'parent': ctx.p1().hex(),
209 'parent': ctx.p1().hex(),
201 }),
210 }),
202 }
211 }
203 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
212 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
204
213
205 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None):
214 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None):
206 """create or update a Differential Revision
215 """create or update a Differential Revision
207
216
208 If revid is None, create a new Differential Revision, otherwise update
217 If revid is None, create a new Differential Revision, otherwise update
209 revid. If parentrevid is not None, set it as a dependency.
218 revid. If parentrevid is not None, set it as a dependency.
210
219
211 If oldnode is not None, check if the patch content (without commit message
220 If oldnode is not None, check if the patch content (without commit message
212 and metadata) has changed before creating another diff.
221 and metadata) has changed before creating another diff.
213 """
222 """
214 repo = ctx.repo()
223 repo = ctx.repo()
215 if oldnode:
224 if oldnode:
216 diffopts = mdiff.diffopts(git=True, context=1)
225 diffopts = mdiff.diffopts(git=True, context=1)
217 oldctx = repo.unfiltered()[oldnode]
226 oldctx = repo.unfiltered()[oldnode]
218 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
227 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
219 else:
228 else:
220 neednewdiff = True
229 neednewdiff = True
221
230
222 transactions = []
231 transactions = []
223 if neednewdiff:
232 if neednewdiff:
224 diff = creatediff(ctx)
233 diff = creatediff(ctx)
225 writediffproperties(ctx, diff)
234 writediffproperties(ctx, diff)
226 transactions.append({'type': 'update', 'value': diff[r'phid']})
235 transactions.append({'type': 'update', 'value': diff[r'phid']})
227
236
228 # Use a temporary summary to set dependency. There might be better ways but
237 # Use a temporary summary to set dependency. There might be better ways but
229 # I cannot find them for now. But do not do that if we are updating an
238 # I cannot find them for now. But do not do that if we are updating an
230 # existing revision (revid is not None) since that introduces visible
239 # existing revision (revid is not None) since that introduces visible
231 # churns (someone edited "Summary" twice) on the web page.
240 # churns (someone edited "Summary" twice) on the web page.
232 if parentrevid and revid is None:
241 if parentrevid and revid is None:
233 summary = 'Depends on D%s' % parentrevid
242 summary = 'Depends on D%s' % parentrevid
234 transactions += [{'type': 'summary', 'value': summary},
243 transactions += [{'type': 'summary', 'value': summary},
235 {'type': 'summary', 'value': ' '}]
244 {'type': 'summary', 'value': ' '}]
236
245
237 # Parse commit message and update related fields.
246 # Parse commit message and update related fields.
238 desc = ctx.description()
247 desc = ctx.description()
239 info = callconduit(repo, 'differential.parsecommitmessage',
248 info = callconduit(repo, 'differential.parsecommitmessage',
240 {'corpus': desc})
249 {'corpus': desc})
241 for k, v in info[r'fields'].items():
250 for k, v in info[r'fields'].items():
242 if k in ['title', 'summary', 'testPlan']:
251 if k in ['title', 'summary', 'testPlan']:
243 transactions.append({'type': k, 'value': v})
252 transactions.append({'type': k, 'value': v})
244
253
245 params = {'transactions': transactions}
254 params = {'transactions': transactions}
246 if revid is not None:
255 if revid is not None:
247 # Update an existing Differential Revision
256 # Update an existing Differential Revision
248 params['objectIdentifier'] = revid
257 params['objectIdentifier'] = revid
249
258
250 revision = callconduit(repo, 'differential.revision.edit', params)
259 revision = callconduit(repo, 'differential.revision.edit', params)
251 if not revision:
260 if not revision:
252 raise error.Abort(_('cannot create revision for %s') % ctx)
261 raise error.Abort(_('cannot create revision for %s') % ctx)
253
262
254 return revision
263 return revision
255
264
256 @command('phabsend',
265 @command('phabsend',
257 [('r', 'rev', [], _('revisions to send'), _('REV'))],
266 [('r', 'rev', [], _('revisions to send'), _('REV'))],
258 _('REV [OPTIONS]'))
267 _('REV [OPTIONS]'))
259 def phabsend(ui, repo, *revs, **opts):
268 def phabsend(ui, repo, *revs, **opts):
260 """upload changesets to Phabricator
269 """upload changesets to Phabricator
261
270
262 If there are multiple revisions specified, they will be send as a stack
271 If there are multiple revisions specified, they will be send as a stack
263 with a linear dependencies relationship using the order specified by the
272 with a linear dependencies relationship using the order specified by the
264 revset.
273 revset.
265
274
266 For the first time uploading changesets, local tags will be created to
275 For the first time uploading changesets, local tags will be created to
267 maintain the association. After the first time, phabsend will check
276 maintain the association. After the first time, phabsend will check
268 obsstore and tags information so it can figure out whether to update an
277 obsstore and tags information so it can figure out whether to update an
269 existing Differential Revision, or create a new one.
278 existing Differential Revision, or create a new one.
270 """
279 """
271 revs = list(revs) + opts.get('rev', [])
280 revs = list(revs) + opts.get('rev', [])
272 revs = scmutil.revrange(repo, revs)
281 revs = scmutil.revrange(repo, revs)
273
282
274 if not revs:
283 if not revs:
275 raise error.Abort(_('phabsend requires at least one changeset'))
284 raise error.Abort(_('phabsend requires at least one changeset'))
276
285
286 oldnodedrev = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
287
277 # Send patches one by one so we know their Differential Revision IDs and
288 # Send patches one by one so we know their Differential Revision IDs and
278 # can provide dependency relationship
289 # can provide dependency relationship
279 lastrevid = None
290 lastrevid = None
280 for rev in revs:
291 for rev in revs:
281 ui.debug('sending rev %d\n' % rev)
292 ui.debug('sending rev %d\n' % rev)
282 ctx = repo[rev]
293 ctx = repo[rev]
283
294
284 # Get Differential Revision ID
295 # Get Differential Revision ID
285 oldnode, revid = getmapping(ctx)
296 oldnode, revid = oldnodedrev.get(ctx.node(), (None, None))
286 if oldnode != ctx.node():
297 if oldnode != ctx.node():
287 # Create or update Differential Revision
298 # Create or update Differential Revision
288 revision = createdifferentialrevision(ctx, revid, lastrevid,
299 revision = createdifferentialrevision(ctx, revid, lastrevid,
289 oldnode)
300 oldnode)
290 newrevid = int(revision[r'object'][r'id'])
301 newrevid = int(revision[r'object'][r'id'])
291 if revid:
302 if revid:
292 action = _('updated')
303 action = _('updated')
293 else:
304 else:
294 action = _('created')
305 action = _('created')
295
306
296 # Create a local tag to note the association
307 # Create a local tag to note the association
297 tagname = 'D%d' % newrevid
308 tagname = 'D%d' % newrevid
298 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
309 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
299 date=None, local=True)
310 date=None, local=True)
300 else:
311 else:
301 # Nothing changed. But still set "newrevid" so the next revision
312 # Nothing changed. But still set "newrevid" so the next revision
302 # could depend on this one.
313 # could depend on this one.
303 newrevid = revid
314 newrevid = revid
304 action = _('skipped')
315 action = _('skipped')
305
316
306 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
317 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
307 ctx.description().split('\n')[0]))
318 ctx.description().split('\n')[0]))
308 lastrevid = newrevid
319 lastrevid = newrevid
309
320
310 # Map from "hg:meta" keys to header understood by "hg import". The order is
321 # Map from "hg:meta" keys to header understood by "hg import". The order is
311 # consistent with "hg export" output.
322 # consistent with "hg export" output.
312 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
323 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
313 (r'node', 'Node ID'), (r'parent', 'Parent ')])
324 (r'node', 'Node ID'), (r'parent', 'Parent ')])
314
325
315 def querydrev(repo, params, stack=False):
326 def querydrev(repo, params, stack=False):
316 """return a list of "Differential Revision" dicts
327 """return a list of "Differential Revision" dicts
317
328
318 params is the input of "differential.query" API, and is expected to match
329 params is the input of "differential.query" API, and is expected to match
319 just a single Differential Revision.
330 just a single Differential Revision.
320
331
321 A "Differential Revision dict" looks like:
332 A "Differential Revision dict" looks like:
322
333
323 {
334 {
324 "id": "2",
335 "id": "2",
325 "phid": "PHID-DREV-672qvysjcczopag46qty",
336 "phid": "PHID-DREV-672qvysjcczopag46qty",
326 "title": "example",
337 "title": "example",
327 "uri": "https://phab.example.com/D2",
338 "uri": "https://phab.example.com/D2",
328 "dateCreated": "1499181406",
339 "dateCreated": "1499181406",
329 "dateModified": "1499182103",
340 "dateModified": "1499182103",
330 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
341 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
331 "status": "0",
342 "status": "0",
332 "statusName": "Needs Review",
343 "statusName": "Needs Review",
333 "properties": [],
344 "properties": [],
334 "branch": null,
345 "branch": null,
335 "summary": "",
346 "summary": "",
336 "testPlan": "",
347 "testPlan": "",
337 "lineCount": "2",
348 "lineCount": "2",
338 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
349 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
339 "diffs": [
350 "diffs": [
340 "3",
351 "3",
341 "4",
352 "4",
342 ],
353 ],
343 "commits": [],
354 "commits": [],
344 "reviewers": [],
355 "reviewers": [],
345 "ccs": [],
356 "ccs": [],
346 "hashes": [],
357 "hashes": [],
347 "auxiliary": {
358 "auxiliary": {
348 "phabricator:projects": [],
359 "phabricator:projects": [],
349 "phabricator:depends-on": [
360 "phabricator:depends-on": [
350 "PHID-DREV-gbapp366kutjebt7agcd"
361 "PHID-DREV-gbapp366kutjebt7agcd"
351 ]
362 ]
352 },
363 },
353 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
364 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
354 "sourcePath": null
365 "sourcePath": null
355 }
366 }
356
367
357 If stack is True, return a list of "Differential Revision dict"s in an
368 If stack is True, return a list of "Differential Revision dict"s in an
358 order that the latter ones depend on the former ones. Otherwise, return a
369 order that the latter ones depend on the former ones. Otherwise, return a
359 list of a unique "Differential Revision dict".
370 list of a unique "Differential Revision dict".
360 """
371 """
361 prefetched = {} # {id or phid: drev}
372 prefetched = {} # {id or phid: drev}
362 def fetch(params):
373 def fetch(params):
363 """params -> single drev or None"""
374 """params -> single drev or None"""
364 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
375 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
365 if key in prefetched:
376 if key in prefetched:
366 return prefetched[key]
377 return prefetched[key]
367 # Otherwise, send the request. If we're fetching a stack, be smarter
378 # Otherwise, send the request. If we're fetching a stack, be smarter
368 # and fetch more ids in one batch, even if it could be unnecessary.
379 # and fetch more ids in one batch, even if it could be unnecessary.
369 batchparams = params
380 batchparams = params
370 if stack and len(params.get(r'ids', [])) == 1:
381 if stack and len(params.get(r'ids', [])) == 1:
371 i = int(params[r'ids'][0])
382 i = int(params[r'ids'][0])
372 # developer config: phabricator.batchsize
383 # developer config: phabricator.batchsize
373 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
384 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
374 batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
385 batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
375 drevs = callconduit(repo, 'differential.query', batchparams)
386 drevs = callconduit(repo, 'differential.query', batchparams)
376 # Fill prefetched with the result
387 # Fill prefetched with the result
377 for drev in drevs:
388 for drev in drevs:
378 prefetched[drev[r'phid']] = drev
389 prefetched[drev[r'phid']] = drev
379 prefetched[int(drev[r'id'])] = drev
390 prefetched[int(drev[r'id'])] = drev
380 if key not in prefetched:
391 if key not in prefetched:
381 raise error.Abort(_('cannot get Differential Revision %r') % params)
392 raise error.Abort(_('cannot get Differential Revision %r') % params)
382 return prefetched[key]
393 return prefetched[key]
383
394
384 visited = set()
395 visited = set()
385 result = []
396 result = []
386 queue = [params]
397 queue = [params]
387 while queue:
398 while queue:
388 params = queue.pop()
399 params = queue.pop()
389 drev = fetch(params)
400 drev = fetch(params)
390 if drev[r'id'] in visited:
401 if drev[r'id'] in visited:
391 continue
402 continue
392 visited.add(drev[r'id'])
403 visited.add(drev[r'id'])
393 result.append(drev)
404 result.append(drev)
394 if stack:
405 if stack:
395 auxiliary = drev.get(r'auxiliary', {})
406 auxiliary = drev.get(r'auxiliary', {})
396 depends = auxiliary.get(r'phabricator:depends-on', [])
407 depends = auxiliary.get(r'phabricator:depends-on', [])
397 for phid in depends:
408 for phid in depends:
398 queue.append({'phids': [phid]})
409 queue.append({'phids': [phid]})
399 result.reverse()
410 result.reverse()
400 return result
411 return result
401
412
402 def getdescfromdrev(drev):
413 def getdescfromdrev(drev):
403 """get description (commit message) from "Differential Revision"
414 """get description (commit message) from "Differential Revision"
404
415
405 This is similar to differential.getcommitmessage API. But we only care
416 This is similar to differential.getcommitmessage API. But we only care
406 about limited fields: title, summary, test plan, and URL.
417 about limited fields: title, summary, test plan, and URL.
407 """
418 """
408 title = drev[r'title']
419 title = drev[r'title']
409 summary = drev[r'summary'].rstrip()
420 summary = drev[r'summary'].rstrip()
410 testplan = drev[r'testPlan'].rstrip()
421 testplan = drev[r'testPlan'].rstrip()
411 if testplan:
422 if testplan:
412 testplan = 'Test Plan:\n%s' % testplan
423 testplan = 'Test Plan:\n%s' % testplan
413 uri = 'Differential Revision: %s' % drev[r'uri']
424 uri = 'Differential Revision: %s' % drev[r'uri']
414 return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
425 return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
415
426
416 def getdiffmeta(diff):
427 def getdiffmeta(diff):
417 """get commit metadata (date, node, user, p1) from a diff object
428 """get commit metadata (date, node, user, p1) from a diff object
418
429
419 The metadata could be "hg:meta", sent by phabsend, like:
430 The metadata could be "hg:meta", sent by phabsend, like:
420
431
421 "properties": {
432 "properties": {
422 "hg:meta": {
433 "hg:meta": {
423 "date": "1499571514 25200",
434 "date": "1499571514 25200",
424 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
435 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
425 "user": "Foo Bar <foo@example.com>",
436 "user": "Foo Bar <foo@example.com>",
426 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
437 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
427 }
438 }
428 }
439 }
429
440
430 Or converted from "local:commits", sent by "arc", like:
441 Or converted from "local:commits", sent by "arc", like:
431
442
432 "properties": {
443 "properties": {
433 "local:commits": {
444 "local:commits": {
434 "98c08acae292b2faf60a279b4189beb6cff1414d": {
445 "98c08acae292b2faf60a279b4189beb6cff1414d": {
435 "author": "Foo Bar",
446 "author": "Foo Bar",
436 "time": 1499546314,
447 "time": 1499546314,
437 "branch": "default",
448 "branch": "default",
438 "tag": "",
449 "tag": "",
439 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
450 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
440 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
451 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
441 "local": "1000",
452 "local": "1000",
442 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
453 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
443 "summary": "...",
454 "summary": "...",
444 "message": "...",
455 "message": "...",
445 "authorEmail": "foo@example.com"
456 "authorEmail": "foo@example.com"
446 }
457 }
447 }
458 }
448 }
459 }
449
460
450 Note: metadata extracted from "local:commits" will lose time zone
461 Note: metadata extracted from "local:commits" will lose time zone
451 information.
462 information.
452 """
463 """
453 props = diff.get(r'properties') or {}
464 props = diff.get(r'properties') or {}
454 meta = props.get(r'hg:meta')
465 meta = props.get(r'hg:meta')
455 if not meta and props.get(r'local:commits'):
466 if not meta and props.get(r'local:commits'):
456 commit = sorted(props[r'local:commits'].values())[0]
467 commit = sorted(props[r'local:commits'].values())[0]
457 meta = {
468 meta = {
458 r'date': r'%d 0' % commit[r'time'],
469 r'date': r'%d 0' % commit[r'time'],
459 r'node': commit[r'rev'],
470 r'node': commit[r'rev'],
460 r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
471 r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
461 }
472 }
462 if len(commit.get(r'parents', ())) >= 1:
473 if len(commit.get(r'parents', ())) >= 1:
463 meta[r'parent'] = commit[r'parents'][0]
474 meta[r'parent'] = commit[r'parents'][0]
464 return meta or {}
475 return meta or {}
465
476
466 def readpatch(repo, params, write, stack=False):
477 def readpatch(repo, params, write, stack=False):
467 """generate plain-text patch readable by 'hg import'
478 """generate plain-text patch readable by 'hg import'
468
479
469 write is usually ui.write. params is passed to "differential.query". If
480 write is usually ui.write. params is passed to "differential.query". If
470 stack is True, also write dependent patches.
481 stack is True, also write dependent patches.
471 """
482 """
472 # Differential Revisions
483 # Differential Revisions
473 drevs = querydrev(repo, params, stack)
484 drevs = querydrev(repo, params, stack)
474
485
475 # Prefetch hg:meta property for all diffs
486 # Prefetch hg:meta property for all diffs
476 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
487 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
477 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
488 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
478
489
479 # Generate patch for each drev
490 # Generate patch for each drev
480 for drev in drevs:
491 for drev in drevs:
481 repo.ui.note(_('reading D%s\n') % drev[r'id'])
492 repo.ui.note(_('reading D%s\n') % drev[r'id'])
482
493
483 diffid = max(int(v) for v in drev[r'diffs'])
494 diffid = max(int(v) for v in drev[r'diffs'])
484 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
495 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
485 desc = getdescfromdrev(drev)
496 desc = getdescfromdrev(drev)
486 header = '# HG changeset patch\n'
497 header = '# HG changeset patch\n'
487
498
488 # Try to preserve metadata from hg:meta property. Write hg patch
499 # Try to preserve metadata from hg:meta property. Write hg patch
489 # headers that can be read by the "import" command. See patchheadermap
500 # headers that can be read by the "import" command. See patchheadermap
490 # and extract in mercurial/patch.py for supported headers.
501 # and extract in mercurial/patch.py for supported headers.
491 meta = getdiffmeta(diffs[str(diffid)])
502 meta = getdiffmeta(diffs[str(diffid)])
492 for k in _metanamemap.keys():
503 for k in _metanamemap.keys():
493 if k in meta:
504 if k in meta:
494 header += '# %s %s\n' % (_metanamemap[k], meta[k])
505 header += '# %s %s\n' % (_metanamemap[k], meta[k])
495
506
496 write(('%s%s\n%s') % (header, desc, body))
507 write(('%s%s\n%s') % (header, desc, body))
497
508
498 @command('phabread',
509 @command('phabread',
499 [('', 'stack', False, _('read dependencies'))],
510 [('', 'stack', False, _('read dependencies'))],
500 _('REVID [OPTIONS]'))
511 _('REVID [OPTIONS]'))
501 def phabread(ui, repo, revid, **opts):
512 def phabread(ui, repo, revid, **opts):
502 """print patches from Phabricator suitable for importing
513 """print patches from Phabricator suitable for importing
503
514
504 REVID could be a Differential Revision identity, like ``D123``, or just the
515 REVID could be a Differential Revision identity, like ``D123``, or just the
505 number ``123``, or a full URL like ``https://phab.example.com/D123``.
516 number ``123``, or a full URL like ``https://phab.example.com/D123``.
506
517
507 If --stack is given, follow dependencies information and read all patches.
518 If --stack is given, follow dependencies information and read all patches.
508 """
519 """
509 try:
520 try:
510 revid = int(revid.split('/')[-1].replace('D', ''))
521 revid = int(revid.split('/')[-1].replace('D', ''))
511 except ValueError:
522 except ValueError:
512 raise error.Abort(_('invalid Revision ID: %s') % revid)
523 raise error.Abort(_('invalid Revision ID: %s') % revid)
513 readpatch(repo, {'ids': [revid]}, ui.write, opts.get('stack'))
524 readpatch(repo, {'ids': [revid]}, ui.write, opts.get('stack'))
General Comments 0
You need to be logged in to leave comments. Login now