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