##// END OF EJS Templates
phabricator: standardize colors...
Jun Wu -
r34065:941c33cf default
parent child Browse files
Show More
@@ -1,837 +1,854 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, and a ``phabread`` command which prints a stack of
10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
12 to update statuses in batch.
12 to update statuses in batch.
13
13
14 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 itertools
35 import itertools
36 import json
36 import json
37 import operator
37 import operator
38 import re
38 import re
39
39
40 from mercurial.node import bin, nullid
40 from mercurial.node import bin, nullid
41 from mercurial.i18n import _
41 from mercurial.i18n import _
42 from mercurial import (
42 from mercurial import (
43 cmdutil,
43 cmdutil,
44 context,
44 context,
45 encoding,
45 encoding,
46 error,
46 error,
47 mdiff,
47 mdiff,
48 obsutil,
48 obsutil,
49 parser,
49 parser,
50 patch,
50 patch,
51 registrar,
51 registrar,
52 scmutil,
52 scmutil,
53 smartset,
53 smartset,
54 tags,
54 tags,
55 url as urlmod,
55 url as urlmod,
56 util,
56 util,
57 )
57 )
58
58
59 cmdtable = {}
59 cmdtable = {}
60 command = registrar.command(cmdtable)
60 command = registrar.command(cmdtable)
61
61
62 colortable = {
63 'phabricator.action.created': 'green',
64 'phabricator.action.skipped': 'magenta',
65 'phabricator.action.updated': 'magenta',
66 'phabricator.desc': '',
67 'phabricator.drev': 'bold',
68 'phabricator.node': '',
69 }
70
62 def urlencodenested(params):
71 def urlencodenested(params):
63 """like urlencode, but works with nested parameters.
72 """like urlencode, but works with nested parameters.
64
73
65 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
74 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
66 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
75 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
67 urlencode. Note: the encoding is consistent with PHP's http_build_query.
76 urlencode. Note: the encoding is consistent with PHP's http_build_query.
68 """
77 """
69 flatparams = util.sortdict()
78 flatparams = util.sortdict()
70 def process(prefix, obj):
79 def process(prefix, obj):
71 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
80 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
72 if items is None:
81 if items is None:
73 flatparams[prefix] = obj
82 flatparams[prefix] = obj
74 else:
83 else:
75 for k, v in items(obj):
84 for k, v in items(obj):
76 if prefix:
85 if prefix:
77 process('%s[%s]' % (prefix, k), v)
86 process('%s[%s]' % (prefix, k), v)
78 else:
87 else:
79 process(k, v)
88 process(k, v)
80 process('', params)
89 process('', params)
81 return util.urlreq.urlencode(flatparams)
90 return util.urlreq.urlencode(flatparams)
82
91
83 def readurltoken(repo):
92 def readurltoken(repo):
84 """return conduit url, token and make sure they exist
93 """return conduit url, token and make sure they exist
85
94
86 Currently read from [phabricator] config section. In the future, it might
95 Currently read from [phabricator] config section. In the future, it might
87 make sense to read from .arcconfig and .arcrc as well.
96 make sense to read from .arcconfig and .arcrc as well.
88 """
97 """
89 values = []
98 values = []
90 section = 'phabricator'
99 section = 'phabricator'
91 for name in ['url', 'token']:
100 for name in ['url', 'token']:
92 value = repo.ui.config(section, name)
101 value = repo.ui.config(section, name)
93 if not value:
102 if not value:
94 raise error.Abort(_('config %s.%s is required') % (section, name))
103 raise error.Abort(_('config %s.%s is required') % (section, name))
95 values.append(value)
104 values.append(value)
96 return values
105 return values
97
106
98 def callconduit(repo, name, params):
107 def callconduit(repo, name, params):
99 """call Conduit API, params is a dict. return json.loads result, or None"""
108 """call Conduit API, params is a dict. return json.loads result, or None"""
100 host, token = readurltoken(repo)
109 host, token = readurltoken(repo)
101 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
110 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
102 urlopener = urlmod.opener(repo.ui, authinfo)
111 urlopener = urlmod.opener(repo.ui, authinfo)
103 repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
112 repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
104 params = params.copy()
113 params = params.copy()
105 params['api.token'] = token
114 params['api.token'] = token
106 request = util.urlreq.request(url, data=urlencodenested(params))
115 request = util.urlreq.request(url, data=urlencodenested(params))
107 body = urlopener.open(request).read()
116 body = urlopener.open(request).read()
108 repo.ui.debug('Conduit Response: %s\n' % body)
117 repo.ui.debug('Conduit Response: %s\n' % body)
109 parsed = json.loads(body)
118 parsed = json.loads(body)
110 if parsed.get(r'error_code'):
119 if parsed.get(r'error_code'):
111 msg = (_('Conduit Error (%s): %s')
120 msg = (_('Conduit Error (%s): %s')
112 % (parsed[r'error_code'], parsed[r'error_info']))
121 % (parsed[r'error_code'], parsed[r'error_info']))
113 raise error.Abort(msg)
122 raise error.Abort(msg)
114 return parsed[r'result']
123 return parsed[r'result']
115
124
116 @command('debugcallconduit', [], _('METHOD'))
125 @command('debugcallconduit', [], _('METHOD'))
117 def debugcallconduit(ui, repo, name):
126 def debugcallconduit(ui, repo, name):
118 """call Conduit API
127 """call Conduit API
119
128
120 Call parameters are read from stdin as a JSON blob. Result will be written
129 Call parameters are read from stdin as a JSON blob. Result will be written
121 to stdout as a JSON blob.
130 to stdout as a JSON blob.
122 """
131 """
123 params = json.loads(ui.fin.read())
132 params = json.loads(ui.fin.read())
124 result = callconduit(repo, name, params)
133 result = callconduit(repo, name, params)
125 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
134 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
126 ui.write('%s\n' % s)
135 ui.write('%s\n' % s)
127
136
128 def getrepophid(repo):
137 def getrepophid(repo):
129 """given callsign, return repository PHID or None"""
138 """given callsign, return repository PHID or None"""
130 # developer config: phabricator.repophid
139 # developer config: phabricator.repophid
131 repophid = repo.ui.config('phabricator', 'repophid')
140 repophid = repo.ui.config('phabricator', 'repophid')
132 if repophid:
141 if repophid:
133 return repophid
142 return repophid
134 callsign = repo.ui.config('phabricator', 'callsign')
143 callsign = repo.ui.config('phabricator', 'callsign')
135 if not callsign:
144 if not callsign:
136 return None
145 return None
137 query = callconduit(repo, 'diffusion.repository.search',
146 query = callconduit(repo, 'diffusion.repository.search',
138 {'constraints': {'callsigns': [callsign]}})
147 {'constraints': {'callsigns': [callsign]}})
139 if len(query[r'data']) == 0:
148 if len(query[r'data']) == 0:
140 return None
149 return None
141 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
150 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
142 repo.ui.setconfig('phabricator', 'repophid', repophid)
151 repo.ui.setconfig('phabricator', 'repophid', repophid)
143 return repophid
152 return repophid
144
153
145 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
154 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
146 _differentialrevisiondescre = re.compile(
155 _differentialrevisiondescre = re.compile(
147 '^Differential Revision:\s*(?:.*)D([1-9][0-9]*)$', re.M)
156 '^Differential Revision:\s*(?:.*)D([1-9][0-9]*)$', re.M)
148
157
149 def getoldnodedrevmap(repo, nodelist):
158 def getoldnodedrevmap(repo, nodelist):
150 """find previous nodes that has been sent to Phabricator
159 """find previous nodes that has been sent to Phabricator
151
160
152 return {node: (oldnode, Differential diff, Differential Revision ID)}
161 return {node: (oldnode, Differential diff, Differential Revision ID)}
153 for node in nodelist with known previous sent versions, or associated
162 for node in nodelist with known previous sent versions, or associated
154 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
163 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
155 be ``None``.
164 be ``None``.
156
165
157 Examines commit messages like "Differential Revision:" to get the
166 Examines commit messages like "Differential Revision:" to get the
158 association information.
167 association information.
159
168
160 If such commit message line is not found, examines all precursors and their
169 If such commit message line is not found, examines all precursors and their
161 tags. Tags with format like "D1234" are considered a match and the node
170 tags. Tags with format like "D1234" are considered a match and the node
162 with that tag, and the number after "D" (ex. 1234) will be returned.
171 with that tag, and the number after "D" (ex. 1234) will be returned.
163
172
164 The ``old node``, if not None, is guaranteed to be the last diff of
173 The ``old node``, if not None, is guaranteed to be the last diff of
165 corresponding Differential Revision, and exist in the repo.
174 corresponding Differential Revision, and exist in the repo.
166 """
175 """
167 url, token = readurltoken(repo)
176 url, token = readurltoken(repo)
168 unfi = repo.unfiltered()
177 unfi = repo.unfiltered()
169 nodemap = unfi.changelog.nodemap
178 nodemap = unfi.changelog.nodemap
170
179
171 result = {} # {node: (oldnode?, lastdiff?, drev)}
180 result = {} # {node: (oldnode?, lastdiff?, drev)}
172 toconfirm = {} # {node: (force, {precnode}, drev)}
181 toconfirm = {} # {node: (force, {precnode}, drev)}
173 for node in nodelist:
182 for node in nodelist:
174 ctx = unfi[node]
183 ctx = unfi[node]
175 # For tags like "D123", put them into "toconfirm" to verify later
184 # For tags like "D123", put them into "toconfirm" to verify later
176 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
185 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
177 for n in precnodes:
186 for n in precnodes:
178 if n in nodemap:
187 if n in nodemap:
179 for tag in unfi.nodetags(n):
188 for tag in unfi.nodetags(n):
180 m = _differentialrevisiontagre.match(tag)
189 m = _differentialrevisiontagre.match(tag)
181 if m:
190 if m:
182 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
191 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
183 continue
192 continue
184
193
185 # Check commit message
194 # Check commit message
186 m = _differentialrevisiondescre.search(ctx.description())
195 m = _differentialrevisiondescre.search(ctx.description())
187 if m:
196 if m:
188 toconfirm[node] = (1, set(precnodes), int(m.group(1)))
197 toconfirm[node] = (1, set(precnodes), int(m.group(1)))
189
198
190 # Double check if tags are genuine by collecting all old nodes from
199 # Double check if tags are genuine by collecting all old nodes from
191 # Phabricator, and expect precursors overlap with it.
200 # Phabricator, and expect precursors overlap with it.
192 if toconfirm:
201 if toconfirm:
193 drevs = [drev for force, precs, drev in toconfirm.values()]
202 drevs = [drev for force, precs, drev in toconfirm.values()]
194 alldiffs = callconduit(unfi, 'differential.querydiffs',
203 alldiffs = callconduit(unfi, 'differential.querydiffs',
195 {'revisionIDs': drevs})
204 {'revisionIDs': drevs})
196 getnode = lambda d: bin(encoding.unitolocal(
205 getnode = lambda d: bin(encoding.unitolocal(
197 getdiffmeta(d).get(r'node', ''))) or None
206 getdiffmeta(d).get(r'node', ''))) or None
198 for newnode, (force, precset, drev) in toconfirm.items():
207 for newnode, (force, precset, drev) in toconfirm.items():
199 diffs = [d for d in alldiffs.values()
208 diffs = [d for d in alldiffs.values()
200 if int(d[r'revisionID']) == drev]
209 if int(d[r'revisionID']) == drev]
201
210
202 # "precursors" as known by Phabricator
211 # "precursors" as known by Phabricator
203 phprecset = set(getnode(d) for d in diffs)
212 phprecset = set(getnode(d) for d in diffs)
204
213
205 # Ignore if precursors (Phabricator and local repo) do not overlap,
214 # Ignore if precursors (Phabricator and local repo) do not overlap,
206 # and force is not set (when commit message says nothing)
215 # and force is not set (when commit message says nothing)
207 if not force and not bool(phprecset & precset):
216 if not force and not bool(phprecset & precset):
208 tagname = 'D%d' % drev
217 tagname = 'D%d' % drev
209 tags.tag(repo, tagname, nullid, message=None, user=None,
218 tags.tag(repo, tagname, nullid, message=None, user=None,
210 date=None, local=True)
219 date=None, local=True)
211 unfi.ui.warn(_('D%s: local tag removed - does not match '
220 unfi.ui.warn(_('D%s: local tag removed - does not match '
212 'Differential history\n') % drev)
221 'Differential history\n') % drev)
213 continue
222 continue
214
223
215 # Find the last node using Phabricator metadata, and make sure it
224 # Find the last node using Phabricator metadata, and make sure it
216 # exists in the repo
225 # exists in the repo
217 oldnode = lastdiff = None
226 oldnode = lastdiff = None
218 if diffs:
227 if diffs:
219 lastdiff = max(diffs, key=lambda d: int(d[r'id']))
228 lastdiff = max(diffs, key=lambda d: int(d[r'id']))
220 oldnode = getnode(lastdiff)
229 oldnode = getnode(lastdiff)
221 if oldnode and oldnode not in nodemap:
230 if oldnode and oldnode not in nodemap:
222 oldnode = None
231 oldnode = None
223
232
224 result[newnode] = (oldnode, lastdiff, drev)
233 result[newnode] = (oldnode, lastdiff, drev)
225
234
226 return result
235 return result
227
236
228 def getdiff(ctx, diffopts):
237 def getdiff(ctx, diffopts):
229 """plain-text diff without header (user, commit message, etc)"""
238 """plain-text diff without header (user, commit message, etc)"""
230 output = util.stringio()
239 output = util.stringio()
231 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
240 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
232 None, opts=diffopts):
241 None, opts=diffopts):
233 output.write(chunk)
242 output.write(chunk)
234 return output.getvalue()
243 return output.getvalue()
235
244
236 def creatediff(ctx):
245 def creatediff(ctx):
237 """create a Differential Diff"""
246 """create a Differential Diff"""
238 repo = ctx.repo()
247 repo = ctx.repo()
239 repophid = getrepophid(repo)
248 repophid = getrepophid(repo)
240 # Create a "Differential Diff" via "differential.createrawdiff" API
249 # Create a "Differential Diff" via "differential.createrawdiff" API
241 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
250 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
242 if repophid:
251 if repophid:
243 params['repositoryPHID'] = repophid
252 params['repositoryPHID'] = repophid
244 diff = callconduit(repo, 'differential.createrawdiff', params)
253 diff = callconduit(repo, 'differential.createrawdiff', params)
245 if not diff:
254 if not diff:
246 raise error.Abort(_('cannot create diff for %s') % ctx)
255 raise error.Abort(_('cannot create diff for %s') % ctx)
247 return diff
256 return diff
248
257
249 def writediffproperties(ctx, diff):
258 def writediffproperties(ctx, diff):
250 """write metadata to diff so patches could be applied losslessly"""
259 """write metadata to diff so patches could be applied losslessly"""
251 params = {
260 params = {
252 'diff_id': diff[r'id'],
261 'diff_id': diff[r'id'],
253 'name': 'hg:meta',
262 'name': 'hg:meta',
254 'data': json.dumps({
263 'data': json.dumps({
255 'user': ctx.user(),
264 'user': ctx.user(),
256 'date': '%d %d' % ctx.date(),
265 'date': '%d %d' % ctx.date(),
257 'node': ctx.hex(),
266 'node': ctx.hex(),
258 'parent': ctx.p1().hex(),
267 'parent': ctx.p1().hex(),
259 }),
268 }),
260 }
269 }
261 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
270 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
262
271
263 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
272 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
264 olddiff=None, actions=None):
273 olddiff=None, actions=None):
265 """create or update a Differential Revision
274 """create or update a Differential Revision
266
275
267 If revid is None, create a new Differential Revision, otherwise update
276 If revid is None, create a new Differential Revision, otherwise update
268 revid. If parentrevid is not None, set it as a dependency.
277 revid. If parentrevid is not None, set it as a dependency.
269
278
270 If oldnode is not None, check if the patch content (without commit message
279 If oldnode is not None, check if the patch content (without commit message
271 and metadata) has changed before creating another diff.
280 and metadata) has changed before creating another diff.
272
281
273 If actions is not None, they will be appended to the transaction.
282 If actions is not None, they will be appended to the transaction.
274 """
283 """
275 repo = ctx.repo()
284 repo = ctx.repo()
276 if oldnode:
285 if oldnode:
277 diffopts = mdiff.diffopts(git=True, context=32767)
286 diffopts = mdiff.diffopts(git=True, context=32767)
278 oldctx = repo.unfiltered()[oldnode]
287 oldctx = repo.unfiltered()[oldnode]
279 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
288 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
280 else:
289 else:
281 neednewdiff = True
290 neednewdiff = True
282
291
283 transactions = []
292 transactions = []
284 if neednewdiff:
293 if neednewdiff:
285 diff = creatediff(ctx)
294 diff = creatediff(ctx)
286 transactions.append({'type': 'update', 'value': diff[r'phid']})
295 transactions.append({'type': 'update', 'value': diff[r'phid']})
287 else:
296 else:
288 # Even if we don't need to upload a new diff because the patch content
297 # Even if we don't need to upload a new diff because the patch content
289 # does not change. We might still need to update its metadata so
298 # does not change. We might still need to update its metadata so
290 # pushers could know the correct node metadata.
299 # pushers could know the correct node metadata.
291 assert olddiff
300 assert olddiff
292 diff = olddiff
301 diff = olddiff
293 writediffproperties(ctx, diff)
302 writediffproperties(ctx, diff)
294
303
295 # Use a temporary summary to set dependency. There might be better ways but
304 # Use a temporary summary to set dependency. There might be better ways but
296 # I cannot find them for now. But do not do that if we are updating an
305 # I cannot find them for now. But do not do that if we are updating an
297 # existing revision (revid is not None) since that introduces visible
306 # existing revision (revid is not None) since that introduces visible
298 # churns (someone edited "Summary" twice) on the web page.
307 # churns (someone edited "Summary" twice) on the web page.
299 if parentrevid and revid is None:
308 if parentrevid and revid is None:
300 summary = 'Depends on D%s' % parentrevid
309 summary = 'Depends on D%s' % parentrevid
301 transactions += [{'type': 'summary', 'value': summary},
310 transactions += [{'type': 'summary', 'value': summary},
302 {'type': 'summary', 'value': ' '}]
311 {'type': 'summary', 'value': ' '}]
303
312
304 if actions:
313 if actions:
305 transactions += actions
314 transactions += actions
306
315
307 # Parse commit message and update related fields.
316 # Parse commit message and update related fields.
308 desc = ctx.description()
317 desc = ctx.description()
309 info = callconduit(repo, 'differential.parsecommitmessage',
318 info = callconduit(repo, 'differential.parsecommitmessage',
310 {'corpus': desc})
319 {'corpus': desc})
311 for k, v in info[r'fields'].items():
320 for k, v in info[r'fields'].items():
312 if k in ['title', 'summary', 'testPlan']:
321 if k in ['title', 'summary', 'testPlan']:
313 transactions.append({'type': k, 'value': v})
322 transactions.append({'type': k, 'value': v})
314
323
315 params = {'transactions': transactions}
324 params = {'transactions': transactions}
316 if revid is not None:
325 if revid is not None:
317 # Update an existing Differential Revision
326 # Update an existing Differential Revision
318 params['objectIdentifier'] = revid
327 params['objectIdentifier'] = revid
319
328
320 revision = callconduit(repo, 'differential.revision.edit', params)
329 revision = callconduit(repo, 'differential.revision.edit', params)
321 if not revision:
330 if not revision:
322 raise error.Abort(_('cannot create revision for %s') % ctx)
331 raise error.Abort(_('cannot create revision for %s') % ctx)
323
332
324 return revision, diff
333 return revision, diff
325
334
326 def userphids(repo, names):
335 def userphids(repo, names):
327 """convert user names to PHIDs"""
336 """convert user names to PHIDs"""
328 query = {'constraints': {'usernames': names}}
337 query = {'constraints': {'usernames': names}}
329 result = callconduit(repo, 'user.search', query)
338 result = callconduit(repo, 'user.search', query)
330 # username not found is not an error of the API. So check if we have missed
339 # username not found is not an error of the API. So check if we have missed
331 # some names here.
340 # some names here.
332 data = result[r'data']
341 data = result[r'data']
333 resolved = set(entry[r'fields'][r'username'] for entry in data)
342 resolved = set(entry[r'fields'][r'username'] for entry in data)
334 unresolved = set(names) - resolved
343 unresolved = set(names) - resolved
335 if unresolved:
344 if unresolved:
336 raise error.Abort(_('unknown username: %s')
345 raise error.Abort(_('unknown username: %s')
337 % ' '.join(sorted(unresolved)))
346 % ' '.join(sorted(unresolved)))
338 return [entry[r'phid'] for entry in data]
347 return [entry[r'phid'] for entry in data]
339
348
340 @command('phabsend',
349 @command('phabsend',
341 [('r', 'rev', [], _('revisions to send'), _('REV')),
350 [('r', 'rev', [], _('revisions to send'), _('REV')),
342 ('', 'amend', True, _('update commit messages')),
351 ('', 'amend', True, _('update commit messages')),
343 ('', 'reviewer', [], _('specify reviewers')),
352 ('', 'reviewer', [], _('specify reviewers')),
344 ('', 'confirm', None, _('ask for confirmation before sending'))],
353 ('', 'confirm', None, _('ask for confirmation before sending'))],
345 _('REV [OPTIONS]'))
354 _('REV [OPTIONS]'))
346 def phabsend(ui, repo, *revs, **opts):
355 def phabsend(ui, repo, *revs, **opts):
347 """upload changesets to Phabricator
356 """upload changesets to Phabricator
348
357
349 If there are multiple revisions specified, they will be send as a stack
358 If there are multiple revisions specified, they will be send as a stack
350 with a linear dependencies relationship using the order specified by the
359 with a linear dependencies relationship using the order specified by the
351 revset.
360 revset.
352
361
353 For the first time uploading changesets, local tags will be created to
362 For the first time uploading changesets, local tags will be created to
354 maintain the association. After the first time, phabsend will check
363 maintain the association. After the first time, phabsend will check
355 obsstore and tags information so it can figure out whether to update an
364 obsstore and tags information so it can figure out whether to update an
356 existing Differential Revision, or create a new one.
365 existing Differential Revision, or create a new one.
357
366
358 If --amend is set, update commit messages so they have the
367 If --amend is set, update commit messages so they have the
359 ``Differential Revision`` URL, remove related tags. This is similar to what
368 ``Differential Revision`` URL, remove related tags. This is similar to what
360 arcanist will do, and is more desired in author-push workflows. Otherwise,
369 arcanist will do, and is more desired in author-push workflows. Otherwise,
361 use local tags to record the ``Differential Revision`` association.
370 use local tags to record the ``Differential Revision`` association.
362
371
363 The --confirm option lets you confirm changesets before sending them. You
372 The --confirm option lets you confirm changesets before sending them. You
364 can also add following to your configuration file to make it default
373 can also add following to your configuration file to make it default
365 behaviour::
374 behaviour::
366
375
367 [phabsend]
376 [phabsend]
368 confirm = true
377 confirm = true
369
378
370 phabsend will check obsstore and the above association to decide whether to
379 phabsend will check obsstore and the above association to decide whether to
371 update an existing Differential Revision, or create a new one.
380 update an existing Differential Revision, or create a new one.
372 """
381 """
373 revs = list(revs) + opts.get('rev', [])
382 revs = list(revs) + opts.get('rev', [])
374 revs = scmutil.revrange(repo, revs)
383 revs = scmutil.revrange(repo, revs)
375
384
376 if not revs:
385 if not revs:
377 raise error.Abort(_('phabsend requires at least one changeset'))
386 raise error.Abort(_('phabsend requires at least one changeset'))
378 if opts.get('amend'):
387 if opts.get('amend'):
379 cmdutil.checkunfinished(repo)
388 cmdutil.checkunfinished(repo)
380
389
381 # {newnode: (oldnode, olddiff, olddrev}
390 # {newnode: (oldnode, olddiff, olddrev}
382 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
391 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
383
392
384 confirm = ui.configbool('phabsend', 'confirm')
393 confirm = ui.configbool('phabsend', 'confirm')
385 confirm |= bool(opts.get('confirm'))
394 confirm |= bool(opts.get('confirm'))
386 if confirm:
395 if confirm:
387 confirmed = _confirmbeforesend(repo, revs, oldmap)
396 confirmed = _confirmbeforesend(repo, revs, oldmap)
388 if not confirmed:
397 if not confirmed:
389 raise error.Abort(_('phabsend cancelled'))
398 raise error.Abort(_('phabsend cancelled'))
390
399
391 actions = []
400 actions = []
392 reviewers = opts.get('reviewer', [])
401 reviewers = opts.get('reviewer', [])
393 if reviewers:
402 if reviewers:
394 phids = userphids(repo, reviewers)
403 phids = userphids(repo, reviewers)
395 actions.append({'type': 'reviewers.add', 'value': phids})
404 actions.append({'type': 'reviewers.add', 'value': phids})
396
405
397 drevids = [] # [int]
406 drevids = [] # [int]
398 diffmap = {} # {newnode: diff}
407 diffmap = {} # {newnode: diff}
399
408
400 # Send patches one by one so we know their Differential Revision IDs and
409 # Send patches one by one so we know their Differential Revision IDs and
401 # can provide dependency relationship
410 # can provide dependency relationship
402 lastrevid = None
411 lastrevid = None
403 for rev in revs:
412 for rev in revs:
404 ui.debug('sending rev %d\n' % rev)
413 ui.debug('sending rev %d\n' % rev)
405 ctx = repo[rev]
414 ctx = repo[rev]
406
415
407 # Get Differential Revision ID
416 # Get Differential Revision ID
408 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
417 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
409 if oldnode != ctx.node() or opts.get('amend'):
418 if oldnode != ctx.node() or opts.get('amend'):
410 # Create or update Differential Revision
419 # Create or update Differential Revision
411 revision, diff = createdifferentialrevision(
420 revision, diff = createdifferentialrevision(
412 ctx, revid, lastrevid, oldnode, olddiff, actions)
421 ctx, revid, lastrevid, oldnode, olddiff, actions)
413 diffmap[ctx.node()] = diff
422 diffmap[ctx.node()] = diff
414 newrevid = int(revision[r'object'][r'id'])
423 newrevid = int(revision[r'object'][r'id'])
415 if revid:
424 if revid:
416 action = _('updated')
425 action = 'updated'
417 else:
426 else:
418 action = _('created')
427 action = 'created'
419
428
420 # Create a local tag to note the association, if commit message
429 # Create a local tag to note the association, if commit message
421 # does not have it already
430 # does not have it already
422 m = _differentialrevisiondescre.search(ctx.description())
431 m = _differentialrevisiondescre.search(ctx.description())
423 if not m or int(m.group(1)) != newrevid:
432 if not m or int(m.group(1)) != newrevid:
424 tagname = 'D%d' % newrevid
433 tagname = 'D%d' % newrevid
425 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
434 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
426 date=None, local=True)
435 date=None, local=True)
427 else:
436 else:
428 # Nothing changed. But still set "newrevid" so the next revision
437 # Nothing changed. But still set "newrevid" so the next revision
429 # could depend on this one.
438 # could depend on this one.
430 newrevid = revid
439 newrevid = revid
431 action = _('skipped')
440 action = 'skipped'
432
441
433 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
442 actiondesc = ui.label(
434 ctx.description().split('\n')[0]))
443 {'created': _('created'),
444 'skipped': _('skipped'),
445 'updated': _('updated')}[action],
446 'phabricator.action.%s' % action)
447 drevdesc = ui.label('D%s' % newrevid, 'phabricator.drev')
448 nodedesc = ui.label(bytes(ctx), 'phabricator.node')
449 desc = ui.label(ctx.description().split('\n')[0], 'phabricator.desc')
450 ui.write(_('%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc,
451 desc))
435 drevids.append(newrevid)
452 drevids.append(newrevid)
436 lastrevid = newrevid
453 lastrevid = newrevid
437
454
438 # Update commit messages and remove tags
455 # Update commit messages and remove tags
439 if opts.get('amend'):
456 if opts.get('amend'):
440 unfi = repo.unfiltered()
457 unfi = repo.unfiltered()
441 drevs = callconduit(repo, 'differential.query', {'ids': drevids})
458 drevs = callconduit(repo, 'differential.query', {'ids': drevids})
442 with repo.wlock(), repo.lock(), repo.transaction('phabsend'):
459 with repo.wlock(), repo.lock(), repo.transaction('phabsend'):
443 wnode = unfi['.'].node()
460 wnode = unfi['.'].node()
444 mapping = {} # {oldnode: [newnode]}
461 mapping = {} # {oldnode: [newnode]}
445 for i, rev in enumerate(revs):
462 for i, rev in enumerate(revs):
446 old = unfi[rev]
463 old = unfi[rev]
447 drevid = drevids[i]
464 drevid = drevids[i]
448 drev = [d for d in drevs if int(d[r'id']) == drevid][0]
465 drev = [d for d in drevs if int(d[r'id']) == drevid][0]
449 newdesc = getdescfromdrev(drev)
466 newdesc = getdescfromdrev(drev)
450 # Make sure commit message contain "Differential Revision"
467 # Make sure commit message contain "Differential Revision"
451 if old.description() != newdesc:
468 if old.description() != newdesc:
452 parents = [
469 parents = [
453 mapping.get(old.p1().node(), (old.p1(),))[0],
470 mapping.get(old.p1().node(), (old.p1(),))[0],
454 mapping.get(old.p2().node(), (old.p2(),))[0],
471 mapping.get(old.p2().node(), (old.p2(),))[0],
455 ]
472 ]
456 new = context.metadataonlyctx(
473 new = context.metadataonlyctx(
457 repo, old, parents=parents, text=newdesc,
474 repo, old, parents=parents, text=newdesc,
458 user=old.user(), date=old.date(), extra=old.extra())
475 user=old.user(), date=old.date(), extra=old.extra())
459 newnode = new.commit()
476 newnode = new.commit()
460 mapping[old.node()] = [newnode]
477 mapping[old.node()] = [newnode]
461 # Update diff property
478 # Update diff property
462 writediffproperties(unfi[newnode], diffmap[old.node()])
479 writediffproperties(unfi[newnode], diffmap[old.node()])
463 # Remove local tags since it's no longer necessary
480 # Remove local tags since it's no longer necessary
464 tagname = 'D%d' % drevid
481 tagname = 'D%d' % drevid
465 if tagname in repo.tags():
482 if tagname in repo.tags():
466 tags.tag(repo, tagname, nullid, message=None, user=None,
483 tags.tag(repo, tagname, nullid, message=None, user=None,
467 date=None, local=True)
484 date=None, local=True)
468 scmutil.cleanupnodes(repo, mapping, 'phabsend')
485 scmutil.cleanupnodes(repo, mapping, 'phabsend')
469 if wnode in mapping:
486 if wnode in mapping:
470 unfi.setparents(mapping[wnode][0])
487 unfi.setparents(mapping[wnode][0])
471
488
472 # Map from "hg:meta" keys to header understood by "hg import". The order is
489 # Map from "hg:meta" keys to header understood by "hg import". The order is
473 # consistent with "hg export" output.
490 # consistent with "hg export" output.
474 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
491 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
475 (r'node', 'Node ID'), (r'parent', 'Parent ')])
492 (r'node', 'Node ID'), (r'parent', 'Parent ')])
476
493
477 def _confirmbeforesend(repo, revs, oldmap):
494 def _confirmbeforesend(repo, revs, oldmap):
478 url, token = readurltoken(repo)
495 url, token = readurltoken(repo)
479 ui = repo.ui
496 ui = repo.ui
480 for rev in revs:
497 for rev in revs:
481 ctx = repo[rev]
498 ctx = repo[rev]
482 desc = ctx.description().splitlines()[0]
499 desc = ctx.description().splitlines()[0]
483 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
500 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
484 if drevid:
501 if drevid:
485 drevdesc = ui.label('D%s' % drevid, 'phabricator.drev')
502 drevdesc = ui.label('D%s' % drevid, 'phabricator.drev')
486 else:
503 else:
487 drevdesc = ui.label(_('NEW'), 'phabricator.drev')
504 drevdesc = ui.label(_('NEW'), 'phabricator.drev')
488
505
489 ui.write(_('%s - %s: %s\n') % (drevdesc,
506 ui.write(_('%s - %s: %s\n') % (drevdesc,
490 ui.label(bytes(ctx), 'phabricator.node'),
507 ui.label(bytes(ctx), 'phabricator.node'),
491 ui.label(desc, 'phabricator.desc')))
508 ui.label(desc, 'phabricator.desc')))
492
509
493 if ui.promptchoice(_('Send the above changes to %s (yn)?'
510 if ui.promptchoice(_('Send the above changes to %s (yn)?'
494 '$$ &Yes $$ &No') % url):
511 '$$ &Yes $$ &No') % url):
495 return False
512 return False
496
513
497 return True
514 return True
498
515
499 _knownstatusnames = {'accepted', 'needsreview', 'needsrevision', 'closed',
516 _knownstatusnames = {'accepted', 'needsreview', 'needsrevision', 'closed',
500 'abandoned'}
517 'abandoned'}
501
518
502 def _getstatusname(drev):
519 def _getstatusname(drev):
503 """get normalized status name from a Differential Revision"""
520 """get normalized status name from a Differential Revision"""
504 return drev[r'statusName'].replace(' ', '').lower()
521 return drev[r'statusName'].replace(' ', '').lower()
505
522
506 # Small language to specify differential revisions. Support symbols: (), :X,
523 # Small language to specify differential revisions. Support symbols: (), :X,
507 # +, and -.
524 # +, and -.
508
525
509 _elements = {
526 _elements = {
510 # token-type: binding-strength, primary, prefix, infix, suffix
527 # token-type: binding-strength, primary, prefix, infix, suffix
511 '(': (12, None, ('group', 1, ')'), None, None),
528 '(': (12, None, ('group', 1, ')'), None, None),
512 ':': (8, None, ('ancestors', 8), None, None),
529 ':': (8, None, ('ancestors', 8), None, None),
513 '&': (5, None, None, ('and_', 5), None),
530 '&': (5, None, None, ('and_', 5), None),
514 '+': (4, None, None, ('add', 4), None),
531 '+': (4, None, None, ('add', 4), None),
515 '-': (4, None, None, ('sub', 4), None),
532 '-': (4, None, None, ('sub', 4), None),
516 ')': (0, None, None, None, None),
533 ')': (0, None, None, None, None),
517 'symbol': (0, 'symbol', None, None, None),
534 'symbol': (0, 'symbol', None, None, None),
518 'end': (0, None, None, None, None),
535 'end': (0, None, None, None, None),
519 }
536 }
520
537
521 def _tokenize(text):
538 def _tokenize(text):
522 view = memoryview(text) # zero-copy slice
539 view = memoryview(text) # zero-copy slice
523 special = '():+-& '
540 special = '():+-& '
524 pos = 0
541 pos = 0
525 length = len(text)
542 length = len(text)
526 while pos < length:
543 while pos < length:
527 symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
544 symbol = ''.join(itertools.takewhile(lambda ch: ch not in special,
528 view[pos:]))
545 view[pos:]))
529 if symbol:
546 if symbol:
530 yield ('symbol', symbol, pos)
547 yield ('symbol', symbol, pos)
531 pos += len(symbol)
548 pos += len(symbol)
532 else: # special char, ignore space
549 else: # special char, ignore space
533 if text[pos] != ' ':
550 if text[pos] != ' ':
534 yield (text[pos], None, pos)
551 yield (text[pos], None, pos)
535 pos += 1
552 pos += 1
536 yield ('end', None, pos)
553 yield ('end', None, pos)
537
554
538 def _parse(text):
555 def _parse(text):
539 tree, pos = parser.parser(_elements).parse(_tokenize(text))
556 tree, pos = parser.parser(_elements).parse(_tokenize(text))
540 if pos != len(text):
557 if pos != len(text):
541 raise error.ParseError('invalid token', pos)
558 raise error.ParseError('invalid token', pos)
542 return tree
559 return tree
543
560
544 def _parsedrev(symbol):
561 def _parsedrev(symbol):
545 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
562 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
546 if symbol.startswith('D') and symbol[1:].isdigit():
563 if symbol.startswith('D') and symbol[1:].isdigit():
547 return int(symbol[1:])
564 return int(symbol[1:])
548 if symbol.isdigit():
565 if symbol.isdigit():
549 return int(symbol)
566 return int(symbol)
550
567
551 def _prefetchdrevs(tree):
568 def _prefetchdrevs(tree):
552 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
569 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
553 drevs = set()
570 drevs = set()
554 ancestordrevs = set()
571 ancestordrevs = set()
555 op = tree[0]
572 op = tree[0]
556 if op == 'symbol':
573 if op == 'symbol':
557 r = _parsedrev(tree[1])
574 r = _parsedrev(tree[1])
558 if r:
575 if r:
559 drevs.add(r)
576 drevs.add(r)
560 elif op == 'ancestors':
577 elif op == 'ancestors':
561 r, a = _prefetchdrevs(tree[1])
578 r, a = _prefetchdrevs(tree[1])
562 drevs.update(r)
579 drevs.update(r)
563 ancestordrevs.update(r)
580 ancestordrevs.update(r)
564 ancestordrevs.update(a)
581 ancestordrevs.update(a)
565 else:
582 else:
566 for t in tree[1:]:
583 for t in tree[1:]:
567 r, a = _prefetchdrevs(t)
584 r, a = _prefetchdrevs(t)
568 drevs.update(r)
585 drevs.update(r)
569 ancestordrevs.update(a)
586 ancestordrevs.update(a)
570 return drevs, ancestordrevs
587 return drevs, ancestordrevs
571
588
572 def querydrev(repo, spec):
589 def querydrev(repo, spec):
573 """return a list of "Differential Revision" dicts
590 """return a list of "Differential Revision" dicts
574
591
575 spec is a string using a simple query language, see docstring in phabread
592 spec is a string using a simple query language, see docstring in phabread
576 for details.
593 for details.
577
594
578 A "Differential Revision dict" looks like:
595 A "Differential Revision dict" looks like:
579
596
580 {
597 {
581 "id": "2",
598 "id": "2",
582 "phid": "PHID-DREV-672qvysjcczopag46qty",
599 "phid": "PHID-DREV-672qvysjcczopag46qty",
583 "title": "example",
600 "title": "example",
584 "uri": "https://phab.example.com/D2",
601 "uri": "https://phab.example.com/D2",
585 "dateCreated": "1499181406",
602 "dateCreated": "1499181406",
586 "dateModified": "1499182103",
603 "dateModified": "1499182103",
587 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
604 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
588 "status": "0",
605 "status": "0",
589 "statusName": "Needs Review",
606 "statusName": "Needs Review",
590 "properties": [],
607 "properties": [],
591 "branch": null,
608 "branch": null,
592 "summary": "",
609 "summary": "",
593 "testPlan": "",
610 "testPlan": "",
594 "lineCount": "2",
611 "lineCount": "2",
595 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
612 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
596 "diffs": [
613 "diffs": [
597 "3",
614 "3",
598 "4",
615 "4",
599 ],
616 ],
600 "commits": [],
617 "commits": [],
601 "reviewers": [],
618 "reviewers": [],
602 "ccs": [],
619 "ccs": [],
603 "hashes": [],
620 "hashes": [],
604 "auxiliary": {
621 "auxiliary": {
605 "phabricator:projects": [],
622 "phabricator:projects": [],
606 "phabricator:depends-on": [
623 "phabricator:depends-on": [
607 "PHID-DREV-gbapp366kutjebt7agcd"
624 "PHID-DREV-gbapp366kutjebt7agcd"
608 ]
625 ]
609 },
626 },
610 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
627 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
611 "sourcePath": null
628 "sourcePath": null
612 }
629 }
613 """
630 """
614 def fetch(params):
631 def fetch(params):
615 """params -> single drev or None"""
632 """params -> single drev or None"""
616 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
633 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
617 if key in prefetched:
634 if key in prefetched:
618 return prefetched[key]
635 return prefetched[key]
619 drevs = callconduit(repo, 'differential.query', params)
636 drevs = callconduit(repo, 'differential.query', params)
620 # Fill prefetched with the result
637 # Fill prefetched with the result
621 for drev in drevs:
638 for drev in drevs:
622 prefetched[drev[r'phid']] = drev
639 prefetched[drev[r'phid']] = drev
623 prefetched[int(drev[r'id'])] = drev
640 prefetched[int(drev[r'id'])] = drev
624 if key not in prefetched:
641 if key not in prefetched:
625 raise error.Abort(_('cannot get Differential Revision %r') % params)
642 raise error.Abort(_('cannot get Differential Revision %r') % params)
626 return prefetched[key]
643 return prefetched[key]
627
644
628 def getstack(topdrevids):
645 def getstack(topdrevids):
629 """given a top, get a stack from the bottom, [id] -> [id]"""
646 """given a top, get a stack from the bottom, [id] -> [id]"""
630 visited = set()
647 visited = set()
631 result = []
648 result = []
632 queue = [{r'ids': [i]} for i in topdrevids]
649 queue = [{r'ids': [i]} for i in topdrevids]
633 while queue:
650 while queue:
634 params = queue.pop()
651 params = queue.pop()
635 drev = fetch(params)
652 drev = fetch(params)
636 if drev[r'id'] in visited:
653 if drev[r'id'] in visited:
637 continue
654 continue
638 visited.add(drev[r'id'])
655 visited.add(drev[r'id'])
639 result.append(int(drev[r'id']))
656 result.append(int(drev[r'id']))
640 auxiliary = drev.get(r'auxiliary', {})
657 auxiliary = drev.get(r'auxiliary', {})
641 depends = auxiliary.get(r'phabricator:depends-on', [])
658 depends = auxiliary.get(r'phabricator:depends-on', [])
642 for phid in depends:
659 for phid in depends:
643 queue.append({'phids': [phid]})
660 queue.append({'phids': [phid]})
644 result.reverse()
661 result.reverse()
645 return smartset.baseset(result)
662 return smartset.baseset(result)
646
663
647 # Initialize prefetch cache
664 # Initialize prefetch cache
648 prefetched = {} # {id or phid: drev}
665 prefetched = {} # {id or phid: drev}
649
666
650 tree = _parse(spec)
667 tree = _parse(spec)
651 drevs, ancestordrevs = _prefetchdrevs(tree)
668 drevs, ancestordrevs = _prefetchdrevs(tree)
652
669
653 # developer config: phabricator.batchsize
670 # developer config: phabricator.batchsize
654 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
671 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
655
672
656 # Prefetch Differential Revisions in batch
673 # Prefetch Differential Revisions in batch
657 tofetch = set(drevs)
674 tofetch = set(drevs)
658 for r in ancestordrevs:
675 for r in ancestordrevs:
659 tofetch.update(range(max(1, r - batchsize), r + 1))
676 tofetch.update(range(max(1, r - batchsize), r + 1))
660 if drevs:
677 if drevs:
661 fetch({r'ids': list(tofetch)})
678 fetch({r'ids': list(tofetch)})
662 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
679 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
663
680
664 # Walk through the tree, return smartsets
681 # Walk through the tree, return smartsets
665 def walk(tree):
682 def walk(tree):
666 op = tree[0]
683 op = tree[0]
667 if op == 'symbol':
684 if op == 'symbol':
668 drev = _parsedrev(tree[1])
685 drev = _parsedrev(tree[1])
669 if drev:
686 if drev:
670 return smartset.baseset([drev])
687 return smartset.baseset([drev])
671 elif tree[1] in _knownstatusnames:
688 elif tree[1] in _knownstatusnames:
672 drevs = [r for r in validids
689 drevs = [r for r in validids
673 if _getstatusname(prefetched[r]) == tree[1]]
690 if _getstatusname(prefetched[r]) == tree[1]]
674 return smartset.baseset(drevs)
691 return smartset.baseset(drevs)
675 else:
692 else:
676 raise error.Abort(_('unknown symbol: %s') % tree[1])
693 raise error.Abort(_('unknown symbol: %s') % tree[1])
677 elif op in {'and_', 'add', 'sub'}:
694 elif op in {'and_', 'add', 'sub'}:
678 assert len(tree) == 3
695 assert len(tree) == 3
679 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
696 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
680 elif op == 'group':
697 elif op == 'group':
681 return walk(tree[1])
698 return walk(tree[1])
682 elif op == 'ancestors':
699 elif op == 'ancestors':
683 return getstack(walk(tree[1]))
700 return getstack(walk(tree[1]))
684 else:
701 else:
685 raise error.ProgrammingError('illegal tree: %r' % tree)
702 raise error.ProgrammingError('illegal tree: %r' % tree)
686
703
687 return [prefetched[r] for r in walk(tree)]
704 return [prefetched[r] for r in walk(tree)]
688
705
689 def getdescfromdrev(drev):
706 def getdescfromdrev(drev):
690 """get description (commit message) from "Differential Revision"
707 """get description (commit message) from "Differential Revision"
691
708
692 This is similar to differential.getcommitmessage API. But we only care
709 This is similar to differential.getcommitmessage API. But we only care
693 about limited fields: title, summary, test plan, and URL.
710 about limited fields: title, summary, test plan, and URL.
694 """
711 """
695 title = drev[r'title']
712 title = drev[r'title']
696 summary = drev[r'summary'].rstrip()
713 summary = drev[r'summary'].rstrip()
697 testplan = drev[r'testPlan'].rstrip()
714 testplan = drev[r'testPlan'].rstrip()
698 if testplan:
715 if testplan:
699 testplan = 'Test Plan:\n%s' % testplan
716 testplan = 'Test Plan:\n%s' % testplan
700 uri = 'Differential Revision: %s' % drev[r'uri']
717 uri = 'Differential Revision: %s' % drev[r'uri']
701 return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
718 return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
702
719
703 def getdiffmeta(diff):
720 def getdiffmeta(diff):
704 """get commit metadata (date, node, user, p1) from a diff object
721 """get commit metadata (date, node, user, p1) from a diff object
705
722
706 The metadata could be "hg:meta", sent by phabsend, like:
723 The metadata could be "hg:meta", sent by phabsend, like:
707
724
708 "properties": {
725 "properties": {
709 "hg:meta": {
726 "hg:meta": {
710 "date": "1499571514 25200",
727 "date": "1499571514 25200",
711 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
728 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
712 "user": "Foo Bar <foo@example.com>",
729 "user": "Foo Bar <foo@example.com>",
713 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
730 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
714 }
731 }
715 }
732 }
716
733
717 Or converted from "local:commits", sent by "arc", like:
734 Or converted from "local:commits", sent by "arc", like:
718
735
719 "properties": {
736 "properties": {
720 "local:commits": {
737 "local:commits": {
721 "98c08acae292b2faf60a279b4189beb6cff1414d": {
738 "98c08acae292b2faf60a279b4189beb6cff1414d": {
722 "author": "Foo Bar",
739 "author": "Foo Bar",
723 "time": 1499546314,
740 "time": 1499546314,
724 "branch": "default",
741 "branch": "default",
725 "tag": "",
742 "tag": "",
726 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
743 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
727 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
744 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
728 "local": "1000",
745 "local": "1000",
729 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
746 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
730 "summary": "...",
747 "summary": "...",
731 "message": "...",
748 "message": "...",
732 "authorEmail": "foo@example.com"
749 "authorEmail": "foo@example.com"
733 }
750 }
734 }
751 }
735 }
752 }
736
753
737 Note: metadata extracted from "local:commits" will lose time zone
754 Note: metadata extracted from "local:commits" will lose time zone
738 information.
755 information.
739 """
756 """
740 props = diff.get(r'properties') or {}
757 props = diff.get(r'properties') or {}
741 meta = props.get(r'hg:meta')
758 meta = props.get(r'hg:meta')
742 if not meta and props.get(r'local:commits'):
759 if not meta and props.get(r'local:commits'):
743 commit = sorted(props[r'local:commits'].values())[0]
760 commit = sorted(props[r'local:commits'].values())[0]
744 meta = {
761 meta = {
745 r'date': r'%d 0' % commit[r'time'],
762 r'date': r'%d 0' % commit[r'time'],
746 r'node': commit[r'rev'],
763 r'node': commit[r'rev'],
747 r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
764 r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
748 }
765 }
749 if len(commit.get(r'parents', ())) >= 1:
766 if len(commit.get(r'parents', ())) >= 1:
750 meta[r'parent'] = commit[r'parents'][0]
767 meta[r'parent'] = commit[r'parents'][0]
751 return meta or {}
768 return meta or {}
752
769
753 def readpatch(repo, drevs, write):
770 def readpatch(repo, drevs, write):
754 """generate plain-text patch readable by 'hg import'
771 """generate plain-text patch readable by 'hg import'
755
772
756 write is usually ui.write. drevs is what "querydrev" returns, results of
773 write is usually ui.write. drevs is what "querydrev" returns, results of
757 "differential.query".
774 "differential.query".
758 """
775 """
759 # Prefetch hg:meta property for all diffs
776 # Prefetch hg:meta property for all diffs
760 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
777 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
761 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
778 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
762
779
763 # Generate patch for each drev
780 # Generate patch for each drev
764 for drev in drevs:
781 for drev in drevs:
765 repo.ui.note(_('reading D%s\n') % drev[r'id'])
782 repo.ui.note(_('reading D%s\n') % drev[r'id'])
766
783
767 diffid = max(int(v) for v in drev[r'diffs'])
784 diffid = max(int(v) for v in drev[r'diffs'])
768 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
785 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
769 desc = getdescfromdrev(drev)
786 desc = getdescfromdrev(drev)
770 header = '# HG changeset patch\n'
787 header = '# HG changeset patch\n'
771
788
772 # Try to preserve metadata from hg:meta property. Write hg patch
789 # Try to preserve metadata from hg:meta property. Write hg patch
773 # headers that can be read by the "import" command. See patchheadermap
790 # headers that can be read by the "import" command. See patchheadermap
774 # and extract in mercurial/patch.py for supported headers.
791 # and extract in mercurial/patch.py for supported headers.
775 meta = getdiffmeta(diffs[str(diffid)])
792 meta = getdiffmeta(diffs[str(diffid)])
776 for k in _metanamemap.keys():
793 for k in _metanamemap.keys():
777 if k in meta:
794 if k in meta:
778 header += '# %s %s\n' % (_metanamemap[k], meta[k])
795 header += '# %s %s\n' % (_metanamemap[k], meta[k])
779
796
780 content = '%s%s\n%s' % (header, desc, body)
797 content = '%s%s\n%s' % (header, desc, body)
781 write(encoding.unitolocal(content))
798 write(encoding.unitolocal(content))
782
799
783 @command('phabread',
800 @command('phabread',
784 [('', 'stack', False, _('read dependencies'))],
801 [('', 'stack', False, _('read dependencies'))],
785 _('DREVSPEC [OPTIONS]'))
802 _('DREVSPEC [OPTIONS]'))
786 def phabread(ui, repo, spec, **opts):
803 def phabread(ui, repo, spec, **opts):
787 """print patches from Phabricator suitable for importing
804 """print patches from Phabricator suitable for importing
788
805
789 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
806 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
790 the number ``123``. It could also have common operators like ``+``, ``-``,
807 the number ``123``. It could also have common operators like ``+``, ``-``,
791 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
808 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
792 select a stack.
809 select a stack.
793
810
794 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
811 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
795 could be used to filter patches by status. For performance reason, they
812 could be used to filter patches by status. For performance reason, they
796 only represent a subset of non-status selections and cannot be used alone.
813 only represent a subset of non-status selections and cannot be used alone.
797
814
798 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
815 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
799 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
816 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
800 stack up to D9.
817 stack up to D9.
801
818
802 If --stack is given, follow dependencies information and read all patches.
819 If --stack is given, follow dependencies information and read all patches.
803 It is equivalent to the ``:`` operator.
820 It is equivalent to the ``:`` operator.
804 """
821 """
805 if opts.get('stack'):
822 if opts.get('stack'):
806 spec = ':(%s)' % spec
823 spec = ':(%s)' % spec
807 drevs = querydrev(repo, spec)
824 drevs = querydrev(repo, spec)
808 readpatch(repo, drevs, ui.write)
825 readpatch(repo, drevs, ui.write)
809
826
810 @command('phabupdate',
827 @command('phabupdate',
811 [('', 'accept', False, _('accept revisions')),
828 [('', 'accept', False, _('accept revisions')),
812 ('', 'reject', False, _('reject revisions')),
829 ('', 'reject', False, _('reject revisions')),
813 ('', 'abandon', False, _('abandon revisions')),
830 ('', 'abandon', False, _('abandon revisions')),
814 ('', 'reclaim', False, _('reclaim revisions')),
831 ('', 'reclaim', False, _('reclaim revisions')),
815 ('m', 'comment', '', _('comment on the last revision')),
832 ('m', 'comment', '', _('comment on the last revision')),
816 ], _('DREVSPEC [OPTIONS]'))
833 ], _('DREVSPEC [OPTIONS]'))
817 def phabupdate(ui, repo, spec, **opts):
834 def phabupdate(ui, repo, spec, **opts):
818 """update Differential Revision in batch
835 """update Differential Revision in batch
819
836
820 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
837 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
821 """
838 """
822 flags = [n for n in 'accept reject abandon reclaim'.split() if opts.get(n)]
839 flags = [n for n in 'accept reject abandon reclaim'.split() if opts.get(n)]
823 if len(flags) > 1:
840 if len(flags) > 1:
824 raise error.Abort(_('%s cannot be used together') % ', '.join(flags))
841 raise error.Abort(_('%s cannot be used together') % ', '.join(flags))
825
842
826 actions = []
843 actions = []
827 for f in flags:
844 for f in flags:
828 actions.append({'type': f, 'value': 'true'})
845 actions.append({'type': f, 'value': 'true'})
829
846
830 drevs = querydrev(repo, spec)
847 drevs = querydrev(repo, spec)
831 for i, drev in enumerate(drevs):
848 for i, drev in enumerate(drevs):
832 if i + 1 == len(drevs) and opts.get('comment'):
849 if i + 1 == len(drevs) and opts.get('comment'):
833 actions.append({'type': 'comment', 'value': opts['comment']})
850 actions.append({'type': 'comment', 'value': opts['comment']})
834 if actions:
851 if actions:
835 params = {'objectIdentifier': drev[r'phid'],
852 params = {'objectIdentifier': drev[r'phid'],
836 'transactions': actions}
853 'transactions': actions}
837 callconduit(repo, 'differential.revision.edit', params)
854 callconduit(repo, 'differential.revision.edit', params)
General Comments 0
You need to be logged in to leave comments. Login now