##// END OF EJS Templates
phabricator: respect metadata sent by arc...
Jun Wu -
r33441:de7c6ec2 default
parent child Browse files
Show More
@@ -1,465 +1,513 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 getmapping(ctx):
143 """return (node, associated Differential Revision ID) or (None, None)
143 """return (node, associated Differential Revision ID) or (None, None)
144
144
145 Examines all precursors and their tags. Tags with format like "D1234" are
145 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"
146 considered a match and the node with that tag, and the number after "D"
147 (ex. 1234) will be returned.
147 (ex. 1234) will be returned.
148
148
149 If tags are not found, examine commit message. The "Differential Revision:"
149 If tags are not found, examine commit message. The "Differential Revision:"
150 line could associate this changeset to a Differential Revision.
150 line could associate this changeset to a Differential Revision.
151 """
151 """
152 unfi = ctx.repo().unfiltered()
152 unfi = ctx.repo().unfiltered()
153 nodemap = unfi.changelog.nodemap
153 nodemap = unfi.changelog.nodemap
154
154
155 # Check tags like "D123"
155 # Check tags like "D123"
156 for n in obsolete.allprecursors(unfi.obsstore, [ctx.node()]):
156 for n in obsolete.allprecursors(unfi.obsstore, [ctx.node()]):
157 if n in nodemap:
157 if n in nodemap:
158 for tag in unfi.nodetags(n):
158 for tag in unfi.nodetags(n):
159 m = _differentialrevisiontagre.match(tag)
159 m = _differentialrevisiontagre.match(tag)
160 if m:
160 if m:
161 return n, int(m.group(1))
161 return n, int(m.group(1))
162
162
163 # Check commit message
163 # Check commit message
164 m = _differentialrevisiondescre.search(ctx.description())
164 m = _differentialrevisiondescre.search(ctx.description())
165 if m:
165 if m:
166 return None, int(m.group(1))
166 return None, int(m.group(1))
167
167
168 return None, None
168 return None, None
169
169
170 def getdiff(ctx, diffopts):
170 def getdiff(ctx, diffopts):
171 """plain-text diff without header (user, commit message, etc)"""
171 """plain-text diff without header (user, commit message, etc)"""
172 output = util.stringio()
172 output = util.stringio()
173 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
173 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
174 None, opts=diffopts):
174 None, opts=diffopts):
175 output.write(chunk)
175 output.write(chunk)
176 return output.getvalue()
176 return output.getvalue()
177
177
178 def creatediff(ctx):
178 def creatediff(ctx):
179 """create a Differential Diff"""
179 """create a Differential Diff"""
180 repo = ctx.repo()
180 repo = ctx.repo()
181 repophid = getrepophid(repo)
181 repophid = getrepophid(repo)
182 # Create a "Differential Diff" via "differential.createrawdiff" API
182 # Create a "Differential Diff" via "differential.createrawdiff" API
183 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
183 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
184 if repophid:
184 if repophid:
185 params['repositoryPHID'] = repophid
185 params['repositoryPHID'] = repophid
186 diff = callconduit(repo, 'differential.createrawdiff', params)
186 diff = callconduit(repo, 'differential.createrawdiff', params)
187 if not diff:
187 if not diff:
188 raise error.Abort(_('cannot create diff for %s') % ctx)
188 raise error.Abort(_('cannot create diff for %s') % ctx)
189 return diff
189 return diff
190
190
191 def writediffproperties(ctx, diff):
191 def writediffproperties(ctx, diff):
192 """write metadata to diff so patches could be applied losslessly"""
192 """write metadata to diff so patches could be applied losslessly"""
193 params = {
193 params = {
194 'diff_id': diff[r'id'],
194 'diff_id': diff[r'id'],
195 'name': 'hg:meta',
195 'name': 'hg:meta',
196 'data': json.dumps({
196 'data': json.dumps({
197 'user': ctx.user(),
197 'user': ctx.user(),
198 'date': '%d %d' % ctx.date(),
198 'date': '%d %d' % ctx.date(),
199 'node': ctx.hex(),
199 'node': ctx.hex(),
200 'parent': ctx.p1().hex(),
200 'parent': ctx.p1().hex(),
201 }),
201 }),
202 }
202 }
203 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
203 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
204
204
205 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None):
205 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None):
206 """create or update a Differential Revision
206 """create or update a Differential Revision
207
207
208 If revid is None, create a new Differential Revision, otherwise update
208 If revid is None, create a new Differential Revision, otherwise update
209 revid. If parentrevid is not None, set it as a dependency.
209 revid. If parentrevid is not None, set it as a dependency.
210
210
211 If oldnode is not None, check if the patch content (without commit message
211 If oldnode is not None, check if the patch content (without commit message
212 and metadata) has changed before creating another diff.
212 and metadata) has changed before creating another diff.
213 """
213 """
214 repo = ctx.repo()
214 repo = ctx.repo()
215 if oldnode:
215 if oldnode:
216 diffopts = mdiff.diffopts(git=True, context=1)
216 diffopts = mdiff.diffopts(git=True, context=1)
217 oldctx = repo.unfiltered()[oldnode]
217 oldctx = repo.unfiltered()[oldnode]
218 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
218 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
219 else:
219 else:
220 neednewdiff = True
220 neednewdiff = True
221
221
222 transactions = []
222 transactions = []
223 if neednewdiff:
223 if neednewdiff:
224 diff = creatediff(ctx)
224 diff = creatediff(ctx)
225 writediffproperties(ctx, diff)
225 writediffproperties(ctx, diff)
226 transactions.append({'type': 'update', 'value': diff[r'phid']})
226 transactions.append({'type': 'update', 'value': diff[r'phid']})
227
227
228 # Use a temporary summary to set dependency. There might be better ways but
228 # 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
229 # 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
230 # existing revision (revid is not None) since that introduces visible
231 # churns (someone edited "Summary" twice) on the web page.
231 # churns (someone edited "Summary" twice) on the web page.
232 if parentrevid and revid is None:
232 if parentrevid and revid is None:
233 summary = 'Depends on D%s' % parentrevid
233 summary = 'Depends on D%s' % parentrevid
234 transactions += [{'type': 'summary', 'value': summary},
234 transactions += [{'type': 'summary', 'value': summary},
235 {'type': 'summary', 'value': ' '}]
235 {'type': 'summary', 'value': ' '}]
236
236
237 # Parse commit message and update related fields.
237 # Parse commit message and update related fields.
238 desc = ctx.description()
238 desc = ctx.description()
239 info = callconduit(repo, 'differential.parsecommitmessage',
239 info = callconduit(repo, 'differential.parsecommitmessage',
240 {'corpus': desc})
240 {'corpus': desc})
241 for k, v in info[r'fields'].items():
241 for k, v in info[r'fields'].items():
242 if k in ['title', 'summary', 'testPlan']:
242 if k in ['title', 'summary', 'testPlan']:
243 transactions.append({'type': k, 'value': v})
243 transactions.append({'type': k, 'value': v})
244
244
245 params = {'transactions': transactions}
245 params = {'transactions': transactions}
246 if revid is not None:
246 if revid is not None:
247 # Update an existing Differential Revision
247 # Update an existing Differential Revision
248 params['objectIdentifier'] = revid
248 params['objectIdentifier'] = revid
249
249
250 revision = callconduit(repo, 'differential.revision.edit', params)
250 revision = callconduit(repo, 'differential.revision.edit', params)
251 if not revision:
251 if not revision:
252 raise error.Abort(_('cannot create revision for %s') % ctx)
252 raise error.Abort(_('cannot create revision for %s') % ctx)
253
253
254 return revision
254 return revision
255
255
256 @command('phabsend',
256 @command('phabsend',
257 [('r', 'rev', [], _('revisions to send'), _('REV'))],
257 [('r', 'rev', [], _('revisions to send'), _('REV'))],
258 _('REV [OPTIONS]'))
258 _('REV [OPTIONS]'))
259 def phabsend(ui, repo, *revs, **opts):
259 def phabsend(ui, repo, *revs, **opts):
260 """upload changesets to Phabricator
260 """upload changesets to Phabricator
261
261
262 If there are multiple revisions specified, they will be send as a stack
262 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
263 with a linear dependencies relationship using the order specified by the
264 revset.
264 revset.
265
265
266 For the first time uploading changesets, local tags will be created to
266 For the first time uploading changesets, local tags will be created to
267 maintain the association. After the first time, phabsend will check
267 maintain the association. After the first time, phabsend will check
268 obsstore and tags information so it can figure out whether to update an
268 obsstore and tags information so it can figure out whether to update an
269 existing Differential Revision, or create a new one.
269 existing Differential Revision, or create a new one.
270 """
270 """
271 revs = list(revs) + opts.get('rev', [])
271 revs = list(revs) + opts.get('rev', [])
272 revs = scmutil.revrange(repo, revs)
272 revs = scmutil.revrange(repo, revs)
273
273
274 if not revs:
274 if not revs:
275 raise error.Abort(_('phabsend requires at least one changeset'))
275 raise error.Abort(_('phabsend requires at least one changeset'))
276
276
277 # Send patches one by one so we know their Differential Revision IDs and
277 # Send patches one by one so we know their Differential Revision IDs and
278 # can provide dependency relationship
278 # can provide dependency relationship
279 lastrevid = None
279 lastrevid = None
280 for rev in revs:
280 for rev in revs:
281 ui.debug('sending rev %d\n' % rev)
281 ui.debug('sending rev %d\n' % rev)
282 ctx = repo[rev]
282 ctx = repo[rev]
283
283
284 # Get Differential Revision ID
284 # Get Differential Revision ID
285 oldnode, revid = getmapping(ctx)
285 oldnode, revid = getmapping(ctx)
286 if oldnode != ctx.node():
286 if oldnode != ctx.node():
287 # Create or update Differential Revision
287 # Create or update Differential Revision
288 revision = createdifferentialrevision(ctx, revid, lastrevid,
288 revision = createdifferentialrevision(ctx, revid, lastrevid,
289 oldnode)
289 oldnode)
290 newrevid = int(revision[r'object'][r'id'])
290 newrevid = int(revision[r'object'][r'id'])
291 if revid:
291 if revid:
292 action = _('updated')
292 action = _('updated')
293 else:
293 else:
294 action = _('created')
294 action = _('created')
295
295
296 # Create a local tag to note the association
296 # Create a local tag to note the association
297 tagname = 'D%d' % newrevid
297 tagname = 'D%d' % newrevid
298 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
298 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
299 date=None, local=True)
299 date=None, local=True)
300 else:
300 else:
301 # Nothing changed. But still set "newrevid" so the next revision
301 # Nothing changed. But still set "newrevid" so the next revision
302 # could depend on this one.
302 # could depend on this one.
303 newrevid = revid
303 newrevid = revid
304 action = _('skipped')
304 action = _('skipped')
305
305
306 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
306 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
307 ctx.description().split('\n')[0]))
307 ctx.description().split('\n')[0]))
308 lastrevid = newrevid
308 lastrevid = newrevid
309
309
310 # Map from "hg:meta" keys to header understood by "hg import". The order is
310 # Map from "hg:meta" keys to header understood by "hg import". The order is
311 # consistent with "hg export" output.
311 # consistent with "hg export" output.
312 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
312 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
313 (r'node', 'Node ID'), (r'parent', 'Parent ')])
313 (r'node', 'Node ID'), (r'parent', 'Parent ')])
314
314
315 def querydrev(repo, params, stack=False):
315 def querydrev(repo, params, stack=False):
316 """return a list of "Differential Revision" dicts
316 """return a list of "Differential Revision" dicts
317
317
318 params is the input of "differential.query" API, and is expected to match
318 params is the input of "differential.query" API, and is expected to match
319 just a single Differential Revision.
319 just a single Differential Revision.
320
320
321 A "Differential Revision dict" looks like:
321 A "Differential Revision dict" looks like:
322
322
323 {
323 {
324 "id": "2",
324 "id": "2",
325 "phid": "PHID-DREV-672qvysjcczopag46qty",
325 "phid": "PHID-DREV-672qvysjcczopag46qty",
326 "title": "example",
326 "title": "example",
327 "uri": "https://phab.example.com/D2",
327 "uri": "https://phab.example.com/D2",
328 "dateCreated": "1499181406",
328 "dateCreated": "1499181406",
329 "dateModified": "1499182103",
329 "dateModified": "1499182103",
330 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
330 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
331 "status": "0",
331 "status": "0",
332 "statusName": "Needs Review",
332 "statusName": "Needs Review",
333 "properties": [],
333 "properties": [],
334 "branch": null,
334 "branch": null,
335 "summary": "",
335 "summary": "",
336 "testPlan": "",
336 "testPlan": "",
337 "lineCount": "2",
337 "lineCount": "2",
338 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
338 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
339 "diffs": [
339 "diffs": [
340 "3",
340 "3",
341 "4",
341 "4",
342 ],
342 ],
343 "commits": [],
343 "commits": [],
344 "reviewers": [],
344 "reviewers": [],
345 "ccs": [],
345 "ccs": [],
346 "hashes": [],
346 "hashes": [],
347 "auxiliary": {
347 "auxiliary": {
348 "phabricator:projects": [],
348 "phabricator:projects": [],
349 "phabricator:depends-on": [
349 "phabricator:depends-on": [
350 "PHID-DREV-gbapp366kutjebt7agcd"
350 "PHID-DREV-gbapp366kutjebt7agcd"
351 ]
351 ]
352 },
352 },
353 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
353 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
354 "sourcePath": null
354 "sourcePath": null
355 }
355 }
356
356
357 If stack is True, return a list of "Differential Revision dict"s in an
357 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
358 order that the latter ones depend on the former ones. Otherwise, return a
359 list of a unique "Differential Revision dict".
359 list of a unique "Differential Revision dict".
360 """
360 """
361 prefetched = {} # {id or phid: drev}
361 prefetched = {} # {id or phid: drev}
362 def fetch(params):
362 def fetch(params):
363 """params -> single drev or None"""
363 """params -> single drev or None"""
364 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
364 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
365 if key in prefetched:
365 if key in prefetched:
366 return prefetched[key]
366 return prefetched[key]
367 # Otherwise, send the request. If we're fetching a stack, be smarter
367 # 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.
368 # and fetch more ids in one batch, even if it could be unnecessary.
369 batchparams = params
369 batchparams = params
370 if stack and len(params.get(r'ids', [])) == 1:
370 if stack and len(params.get(r'ids', [])) == 1:
371 i = int(params[r'ids'][0])
371 i = int(params[r'ids'][0])
372 # developer config: phabricator.batchsize
372 # developer config: phabricator.batchsize
373 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
373 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
374 batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
374 batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
375 drevs = callconduit(repo, 'differential.query', batchparams)
375 drevs = callconduit(repo, 'differential.query', batchparams)
376 # Fill prefetched with the result
376 # Fill prefetched with the result
377 for drev in drevs:
377 for drev in drevs:
378 prefetched[drev[r'phid']] = drev
378 prefetched[drev[r'phid']] = drev
379 prefetched[int(drev[r'id'])] = drev
379 prefetched[int(drev[r'id'])] = drev
380 if key not in prefetched:
380 if key not in prefetched:
381 raise error.Abort(_('cannot get Differential Revision %r') % params)
381 raise error.Abort(_('cannot get Differential Revision %r') % params)
382 return prefetched[key]
382 return prefetched[key]
383
383
384 visited = set()
384 visited = set()
385 result = []
385 result = []
386 queue = [params]
386 queue = [params]
387 while queue:
387 while queue:
388 params = queue.pop()
388 params = queue.pop()
389 drev = fetch(params)
389 drev = fetch(params)
390 if drev[r'id'] in visited:
390 if drev[r'id'] in visited:
391 continue
391 continue
392 visited.add(drev[r'id'])
392 visited.add(drev[r'id'])
393 result.append(drev)
393 result.append(drev)
394 if stack:
394 if stack:
395 auxiliary = drev.get(r'auxiliary', {})
395 auxiliary = drev.get(r'auxiliary', {})
396 depends = auxiliary.get(r'phabricator:depends-on', [])
396 depends = auxiliary.get(r'phabricator:depends-on', [])
397 for phid in depends:
397 for phid in depends:
398 queue.append({'phids': [phid]})
398 queue.append({'phids': [phid]})
399 result.reverse()
399 result.reverse()
400 return result
400 return result
401
401
402 def getdescfromdrev(drev):
402 def getdescfromdrev(drev):
403 """get description (commit message) from "Differential Revision"
403 """get description (commit message) from "Differential Revision"
404
404
405 This is similar to differential.getcommitmessage API. But we only care
405 This is similar to differential.getcommitmessage API. But we only care
406 about limited fields: title, summary, test plan, and URL.
406 about limited fields: title, summary, test plan, and URL.
407 """
407 """
408 title = drev[r'title']
408 title = drev[r'title']
409 summary = drev[r'summary'].rstrip()
409 summary = drev[r'summary'].rstrip()
410 testplan = drev[r'testPlan'].rstrip()
410 testplan = drev[r'testPlan'].rstrip()
411 if testplan:
411 if testplan:
412 testplan = 'Test Plan:\n%s' % testplan
412 testplan = 'Test Plan:\n%s' % testplan
413 uri = 'Differential Revision: %s' % drev[r'uri']
413 uri = 'Differential Revision: %s' % drev[r'uri']
414 return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
414 return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
415
415
416 def getdiffmeta(diff):
417 """get commit metadata (date, node, user, p1) from a diff object
418
419 The metadata could be "hg:meta", sent by phabsend, like:
420
421 "properties": {
422 "hg:meta": {
423 "date": "1499571514 25200",
424 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
425 "user": "Foo Bar <foo@example.com>",
426 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
427 }
428 }
429
430 Or converted from "local:commits", sent by "arc", like:
431
432 "properties": {
433 "local:commits": {
434 "98c08acae292b2faf60a279b4189beb6cff1414d": {
435 "author": "Foo Bar",
436 "time": 1499546314,
437 "branch": "default",
438 "tag": "",
439 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
440 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
441 "local": "1000",
442 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
443 "summary": "...",
444 "message": "...",
445 "authorEmail": "foo@example.com"
446 }
447 }
448 }
449
450 Note: metadata extracted from "local:commits" will lose time zone
451 information.
452 """
453 props = diff.get(r'properties') or {}
454 meta = props.get(r'hg:meta')
455 if not meta and props.get(r'local:commits'):
456 commit = sorted(props[r'local:commits'].values())[0]
457 meta = {
458 r'date': r'%d 0' % commit[r'time'],
459 r'node': commit[r'rev'],
460 r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
461 }
462 if len(commit.get(r'parents', ())) >= 1:
463 meta[r'parent'] = commit[r'parents'][0]
464 return meta or {}
465
416 def readpatch(repo, params, write, stack=False):
466 def readpatch(repo, params, write, stack=False):
417 """generate plain-text patch readable by 'hg import'
467 """generate plain-text patch readable by 'hg import'
418
468
419 write is usually ui.write. params is passed to "differential.query". If
469 write is usually ui.write. params is passed to "differential.query". If
420 stack is True, also write dependent patches.
470 stack is True, also write dependent patches.
421 """
471 """
422 # Differential Revisions
472 # Differential Revisions
423 drevs = querydrev(repo, params, stack)
473 drevs = querydrev(repo, params, stack)
424
474
425 # Prefetch hg:meta property for all diffs
475 # Prefetch hg:meta property for all diffs
426 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
476 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
427 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
477 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
428
478
429 # Generate patch for each drev
479 # Generate patch for each drev
430 for drev in drevs:
480 for drev in drevs:
431 repo.ui.note(_('reading D%s\n') % drev[r'id'])
481 repo.ui.note(_('reading D%s\n') % drev[r'id'])
432
482
433 diffid = max(int(v) for v in drev[r'diffs'])
483 diffid = max(int(v) for v in drev[r'diffs'])
434 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
484 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
435 desc = getdescfromdrev(drev)
485 desc = getdescfromdrev(drev)
436 header = '# HG changeset patch\n'
486 header = '# HG changeset patch\n'
437
487
438 # Try to preserve metadata from hg:meta property. Write hg patch
488 # Try to preserve metadata from hg:meta property. Write hg patch
439 # headers that can be read by the "import" command. See patchheadermap
489 # headers that can be read by the "import" command. See patchheadermap
440 # and extract in mercurial/patch.py for supported headers.
490 # and extract in mercurial/patch.py for supported headers.
441 props = diffs[str(diffid)][r'properties'] # could be empty list or dict
491 meta = getdiffmeta(diffs[str(diffid)])
442 if props and r'hg:meta' in props:
492 for k in _metanamemap.keys():
443 meta = props[r'hg:meta']
493 if k in meta:
444 for k in _metanamemap.keys():
494 header += '# %s %s\n' % (_metanamemap[k], meta[k])
445 if k in meta:
446 header += '# %s %s\n' % (_metanamemap[k], meta[k])
447
495
448 write(('%s%s\n%s') % (header, desc, body))
496 write(('%s%s\n%s') % (header, desc, body))
449
497
450 @command('phabread',
498 @command('phabread',
451 [('', 'stack', False, _('read dependencies'))],
499 [('', 'stack', False, _('read dependencies'))],
452 _('REVID [OPTIONS]'))
500 _('REVID [OPTIONS]'))
453 def phabread(ui, repo, revid, **opts):
501 def phabread(ui, repo, revid, **opts):
454 """print patches from Phabricator suitable for importing
502 """print patches from Phabricator suitable for importing
455
503
456 REVID could be a Differential Revision identity, like ``D123``, or just the
504 REVID could be a Differential Revision identity, like ``D123``, or just the
457 number ``123``, or a full URL like ``https://phab.example.com/D123``.
505 number ``123``, or a full URL like ``https://phab.example.com/D123``.
458
506
459 If --stack is given, follow dependencies information and read all patches.
507 If --stack is given, follow dependencies information and read all patches.
460 """
508 """
461 try:
509 try:
462 revid = int(revid.split('/')[-1].replace('D', ''))
510 revid = int(revid.split('/')[-1].replace('D', ''))
463 except ValueError:
511 except ValueError:
464 raise error.Abort(_('invalid Revision ID: %s') % revid)
512 raise error.Abort(_('invalid Revision ID: %s') % revid)
465 readpatch(repo, {'ids': [revid]}, ui.write, opts.get('stack'))
513 readpatch(repo, {'ids': [revid]}, ui.write, opts.get('stack'))
General Comments 0
You need to be logged in to leave comments. Login now