comparison contrib/phabricator.py @ 38392:81a4be7099fa

py3: byte-stringify literals in contrib/phabricator.py as example Transformed by contrib/byteify-strings.py and adjusted exceeded lines manually. Some of b''s would be wrong as the phabriactor extension has to work with JSON data. # skip-blame just many b prefixes
author Yuya Nishihara <yuya@tcha.org>
date Sun, 03 Jun 2018 18:18:36 +0900
parents bb7e3c6ef592
children 32fba6fe893d
comparison
equal deleted inserted replaced
38391:f77bbd34a1df 38392:81a4be7099fa
75 75
76 configtable = {} 76 configtable = {}
77 configitem = registrar.configitem(configtable) 77 configitem = registrar.configitem(configtable)
78 78
79 # developer config: phabricator.batchsize 79 # developer config: phabricator.batchsize
80 configitem('phabricator', 'batchsize', 80 configitem(b'phabricator', b'batchsize',
81 default=12, 81 default=12,
82 ) 82 )
83 configitem('phabricator', 'callsign', 83 configitem(b'phabricator', b'callsign',
84 default=None, 84 default=None,
85 ) 85 )
86 configitem('phabricator', 'curlcmd', 86 configitem(b'phabricator', b'curlcmd',
87 default=None, 87 default=None,
88 ) 88 )
89 # developer config: phabricator.repophid 89 # developer config: phabricator.repophid
90 configitem('phabricator', 'repophid', 90 configitem(b'phabricator', b'repophid',
91 default=None, 91 default=None,
92 ) 92 )
93 configitem('phabricator', 'url', 93 configitem(b'phabricator', b'url',
94 default=None, 94 default=None,
95 ) 95 )
96 configitem('phabsend', 'confirm', 96 configitem(b'phabsend', b'confirm',
97 default=False, 97 default=False,
98 ) 98 )
99 99
100 colortable = { 100 colortable = {
101 'phabricator.action.created': 'green', 101 b'phabricator.action.created': b'green',
102 'phabricator.action.skipped': 'magenta', 102 b'phabricator.action.skipped': b'magenta',
103 'phabricator.action.updated': 'magenta', 103 b'phabricator.action.updated': b'magenta',
104 'phabricator.desc': '', 104 b'phabricator.desc': b'',
105 'phabricator.drev': 'bold', 105 b'phabricator.drev': b'bold',
106 'phabricator.node': '', 106 b'phabricator.node': b'',
107 } 107 }
108 108
109 def urlencodenested(params): 109 def urlencodenested(params):
110 """like urlencode, but works with nested parameters. 110 """like urlencode, but works with nested parameters.
111 111
119 if items is None: 119 if items is None:
120 flatparams[prefix] = obj 120 flatparams[prefix] = obj
121 else: 121 else:
122 for k, v in items(obj): 122 for k, v in items(obj):
123 if prefix: 123 if prefix:
124 process('%s[%s]' % (prefix, k), v) 124 process(b'%s[%s]' % (prefix, k), v)
125 else: 125 else:
126 process(k, v) 126 process(k, v)
127 process('', params) 127 process(b'', params)
128 return util.urlreq.urlencode(flatparams) 128 return util.urlreq.urlencode(flatparams)
129 129
130 printed_token_warning = False 130 printed_token_warning = False
131 131
132 def readlegacytoken(repo, url): 132 def readlegacytoken(repo, url):
133 """Transitional support for old phabricator tokens. 133 """Transitional support for old phabricator tokens.
134 134
135 Remove before the 4.7 release. 135 Remove before the 4.7 release.
136 """ 136 """
137 groups = {} 137 groups = {}
138 for key, val in repo.ui.configitems('phabricator.auth'): 138 for key, val in repo.ui.configitems(b'phabricator.auth'):
139 if '.' not in key: 139 if b'.' not in key:
140 repo.ui.warn(_("ignoring invalid [phabricator.auth] key '%s'\n") 140 repo.ui.warn(_(b"ignoring invalid [phabricator.auth] key '%s'\n")
141 % key) 141 % key)
142 continue 142 continue
143 group, setting = key.rsplit('.', 1) 143 group, setting = key.rsplit(b'.', 1)
144 groups.setdefault(group, {})[setting] = val 144 groups.setdefault(group, {})[setting] = val
145 145
146 token = None 146 token = None
147 for group, auth in groups.iteritems(): 147 for group, auth in groups.iteritems():
148 if url != auth.get('url'): 148 if url != auth.get(b'url'):
149 continue 149 continue
150 token = auth.get('token') 150 token = auth.get(b'token')
151 if token: 151 if token:
152 break 152 break
153 153
154 global printed_token_warning 154 global printed_token_warning
155 155
156 if token and not printed_token_warning: 156 if token and not printed_token_warning:
157 printed_token_warning = True 157 printed_token_warning = True
158 repo.ui.warn(_('phabricator.auth.token is deprecated - please ' 158 repo.ui.warn(_(b'phabricator.auth.token is deprecated - please '
159 'migrate to auth.phabtoken.\n')) 159 b'migrate to auth.phabtoken.\n'))
160 return token 160 return token
161 161
162 def readurltoken(repo): 162 def readurltoken(repo):
163 """return conduit url, token and make sure they exist 163 """return conduit url, token and make sure they exist
164 164
165 Currently read from [auth] config section. In the future, it might 165 Currently read from [auth] config section. In the future, it might
166 make sense to read from .arcconfig and .arcrc as well. 166 make sense to read from .arcconfig and .arcrc as well.
167 """ 167 """
168 url = repo.ui.config('phabricator', 'url') 168 url = repo.ui.config(b'phabricator', b'url')
169 if not url: 169 if not url:
170 raise error.Abort(_('config %s.%s is required') 170 raise error.Abort(_(b'config %s.%s is required')
171 % ('phabricator', 'url')) 171 % (b'phabricator', b'url'))
172 172
173 res = httpconnectionmod.readauthforuri(repo.ui, url, util.url(url).user) 173 res = httpconnectionmod.readauthforuri(repo.ui, url, util.url(url).user)
174 token = None 174 token = None
175 175
176 if res: 176 if res:
177 group, auth = res 177 group, auth = res
178 178
179 repo.ui.debug("using auth.%s.* for authentication\n" % group) 179 repo.ui.debug(b"using auth.%s.* for authentication\n" % group)
180 180
181 token = auth.get('phabtoken') 181 token = auth.get(b'phabtoken')
182 182
183 if not token: 183 if not token:
184 token = readlegacytoken(repo, url) 184 token = readlegacytoken(repo, url)
185 if not token: 185 if not token:
186 raise error.Abort(_('Can\'t find conduit token associated to %s') 186 raise error.Abort(_(b'Can\'t find conduit token associated to %s')
187 % (url,)) 187 % (url,))
188 188
189 return url, token 189 return url, token
190 190
191 def callconduit(repo, name, params): 191 def callconduit(repo, name, params):
192 """call Conduit API, params is a dict. return json.loads result, or None""" 192 """call Conduit API, params is a dict. return json.loads result, or None"""
193 host, token = readurltoken(repo) 193 host, token = readurltoken(repo)
194 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo() 194 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
195 repo.ui.debug('Conduit Call: %s %s\n' % (url, params)) 195 repo.ui.debug(b'Conduit Call: %s %s\n' % (url, params))
196 params = params.copy() 196 params = params.copy()
197 params['api.token'] = token 197 params[b'api.token'] = token
198 data = urlencodenested(params) 198 data = urlencodenested(params)
199 curlcmd = repo.ui.config('phabricator', 'curlcmd') 199 curlcmd = repo.ui.config(b'phabricator', b'curlcmd')
200 if curlcmd: 200 if curlcmd:
201 sin, sout = procutil.popen2('%s -d @- %s' 201 sin, sout = procutil.popen2(b'%s -d @- %s'
202 % (curlcmd, procutil.shellquote(url))) 202 % (curlcmd, procutil.shellquote(url)))
203 sin.write(data) 203 sin.write(data)
204 sin.close() 204 sin.close()
205 body = sout.read() 205 body = sout.read()
206 else: 206 else:
207 urlopener = urlmod.opener(repo.ui, authinfo) 207 urlopener = urlmod.opener(repo.ui, authinfo)
208 request = util.urlreq.request(url, data=data) 208 request = util.urlreq.request(url, data=data)
209 body = urlopener.open(request).read() 209 body = urlopener.open(request).read()
210 repo.ui.debug('Conduit Response: %s\n' % body) 210 repo.ui.debug(b'Conduit Response: %s\n' % body)
211 parsed = json.loads(body) 211 parsed = json.loads(body)
212 if parsed.get(r'error_code'): 212 if parsed.get(r'error_code'):
213 msg = (_('Conduit Error (%s): %s') 213 msg = (_(b'Conduit Error (%s): %s')
214 % (parsed[r'error_code'], parsed[r'error_info'])) 214 % (parsed[r'error_code'], parsed[r'error_info']))
215 raise error.Abort(msg) 215 raise error.Abort(msg)
216 return parsed[r'result'] 216 return parsed[r'result']
217 217
218 @command('debugcallconduit', [], _('METHOD')) 218 @command(b'debugcallconduit', [], _(b'METHOD'))
219 def debugcallconduit(ui, repo, name): 219 def debugcallconduit(ui, repo, name):
220 """call Conduit API 220 """call Conduit API
221 221
222 Call parameters are read from stdin as a JSON blob. Result will be written 222 Call parameters are read from stdin as a JSON blob. Result will be written
223 to stdout as a JSON blob. 223 to stdout as a JSON blob.
224 """ 224 """
225 params = json.loads(ui.fin.read()) 225 params = json.loads(ui.fin.read())
226 result = callconduit(repo, name, params) 226 result = callconduit(repo, name, params)
227 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': ')) 227 s = json.dumps(result, sort_keys=True, indent=2, separators=(b',', b': '))
228 ui.write('%s\n' % s) 228 ui.write(b'%s\n' % s)
229 229
230 def getrepophid(repo): 230 def getrepophid(repo):
231 """given callsign, return repository PHID or None""" 231 """given callsign, return repository PHID or None"""
232 # developer config: phabricator.repophid 232 # developer config: phabricator.repophid
233 repophid = repo.ui.config('phabricator', 'repophid') 233 repophid = repo.ui.config(b'phabricator', b'repophid')
234 if repophid: 234 if repophid:
235 return repophid 235 return repophid
236 callsign = repo.ui.config('phabricator', 'callsign') 236 callsign = repo.ui.config(b'phabricator', b'callsign')
237 if not callsign: 237 if not callsign:
238 return None 238 return None
239 query = callconduit(repo, 'diffusion.repository.search', 239 query = callconduit(repo, b'diffusion.repository.search',
240 {'constraints': {'callsigns': [callsign]}}) 240 {b'constraints': {b'callsigns': [callsign]}})
241 if len(query[r'data']) == 0: 241 if len(query[r'data']) == 0:
242 return None 242 return None
243 repophid = encoding.strtolocal(query[r'data'][0][r'phid']) 243 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
244 repo.ui.setconfig('phabricator', 'repophid', repophid) 244 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
245 return repophid 245 return repophid
246 246
247 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z') 247 _differentialrevisiontagre = re.compile(b'\AD([1-9][0-9]*)\Z')
248 _differentialrevisiondescre = re.compile( 248 _differentialrevisiondescre = re.compile(
249 '^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M) 249 b'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M)
250 250
251 def getoldnodedrevmap(repo, nodelist): 251 def getoldnodedrevmap(repo, nodelist):
252 """find previous nodes that has been sent to Phabricator 252 """find previous nodes that has been sent to Phabricator
253 253
254 return {node: (oldnode, Differential diff, Differential Revision ID)} 254 return {node: (oldnode, Differential diff, Differential Revision ID)}
285 continue 285 continue
286 286
287 # Check commit message 287 # Check commit message
288 m = _differentialrevisiondescre.search(ctx.description()) 288 m = _differentialrevisiondescre.search(ctx.description())
289 if m: 289 if m:
290 toconfirm[node] = (1, set(precnodes), int(m.group('id'))) 290 toconfirm[node] = (1, set(precnodes), int(m.group(b'id')))
291 291
292 # Double check if tags are genuine by collecting all old nodes from 292 # Double check if tags are genuine by collecting all old nodes from
293 # Phabricator, and expect precursors overlap with it. 293 # Phabricator, and expect precursors overlap with it.
294 if toconfirm: 294 if toconfirm:
295 drevs = [drev for force, precs, drev in toconfirm.values()] 295 drevs = [drev for force, precs, drev in toconfirm.values()]
296 alldiffs = callconduit(unfi, 'differential.querydiffs', 296 alldiffs = callconduit(unfi, b'differential.querydiffs',
297 {'revisionIDs': drevs}) 297 {b'revisionIDs': drevs})
298 getnode = lambda d: bin(encoding.unitolocal( 298 getnode = lambda d: bin(encoding.unitolocal(
299 getdiffmeta(d).get(r'node', ''))) or None 299 getdiffmeta(d).get(r'node', b''))) or None
300 for newnode, (force, precset, drev) in toconfirm.items(): 300 for newnode, (force, precset, drev) in toconfirm.items():
301 diffs = [d for d in alldiffs.values() 301 diffs = [d for d in alldiffs.values()
302 if int(d[r'revisionID']) == drev] 302 if int(d[r'revisionID']) == drev]
303 303
304 # "precursors" as known by Phabricator 304 # "precursors" as known by Phabricator
305 phprecset = set(getnode(d) for d in diffs) 305 phprecset = set(getnode(d) for d in diffs)
306 306
307 # Ignore if precursors (Phabricator and local repo) do not overlap, 307 # Ignore if precursors (Phabricator and local repo) do not overlap,
308 # and force is not set (when commit message says nothing) 308 # and force is not set (when commit message says nothing)
309 if not force and not bool(phprecset & precset): 309 if not force and not bool(phprecset & precset):
310 tagname = 'D%d' % drev 310 tagname = b'D%d' % drev
311 tags.tag(repo, tagname, nullid, message=None, user=None, 311 tags.tag(repo, tagname, nullid, message=None, user=None,
312 date=None, local=True) 312 date=None, local=True)
313 unfi.ui.warn(_('D%s: local tag removed - does not match ' 313 unfi.ui.warn(_(b'D%s: local tag removed - does not match '
314 'Differential history\n') % drev) 314 b'Differential history\n') % drev)
315 continue 315 continue
316 316
317 # Find the last node using Phabricator metadata, and make sure it 317 # Find the last node using Phabricator metadata, and make sure it
318 # exists in the repo 318 # exists in the repo
319 oldnode = lastdiff = None 319 oldnode = lastdiff = None
338 def creatediff(ctx): 338 def creatediff(ctx):
339 """create a Differential Diff""" 339 """create a Differential Diff"""
340 repo = ctx.repo() 340 repo = ctx.repo()
341 repophid = getrepophid(repo) 341 repophid = getrepophid(repo)
342 # Create a "Differential Diff" via "differential.createrawdiff" API 342 # Create a "Differential Diff" via "differential.createrawdiff" API
343 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))} 343 params = {b'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
344 if repophid: 344 if repophid:
345 params['repositoryPHID'] = repophid 345 params[b'repositoryPHID'] = repophid
346 diff = callconduit(repo, 'differential.createrawdiff', params) 346 diff = callconduit(repo, b'differential.createrawdiff', params)
347 if not diff: 347 if not diff:
348 raise error.Abort(_('cannot create diff for %s') % ctx) 348 raise error.Abort(_(b'cannot create diff for %s') % ctx)
349 return diff 349 return diff
350 350
351 def writediffproperties(ctx, diff): 351 def writediffproperties(ctx, diff):
352 """write metadata to diff so patches could be applied losslessly""" 352 """write metadata to diff so patches could be applied losslessly"""
353 params = { 353 params = {
354 'diff_id': diff[r'id'], 354 b'diff_id': diff[r'id'],
355 'name': 'hg:meta', 355 b'name': b'hg:meta',
356 'data': json.dumps({ 356 b'data': json.dumps({
357 'user': ctx.user(), 357 b'user': ctx.user(),
358 'date': '%d %d' % ctx.date(), 358 b'date': b'%d %d' % ctx.date(),
359 'node': ctx.hex(), 359 b'node': ctx.hex(),
360 'parent': ctx.p1().hex(), 360 b'parent': ctx.p1().hex(),
361 }), 361 }),
362 } 362 }
363 callconduit(ctx.repo(), 'differential.setdiffproperty', params) 363 callconduit(ctx.repo(), b'differential.setdiffproperty', params)
364 364
365 params = { 365 params = {
366 'diff_id': diff[r'id'], 366 b'diff_id': diff[r'id'],
367 'name': 'local:commits', 367 b'name': b'local:commits',
368 'data': json.dumps({ 368 b'data': json.dumps({
369 ctx.hex(): { 369 ctx.hex(): {
370 'author': stringutil.person(ctx.user()), 370 b'author': stringutil.person(ctx.user()),
371 'authorEmail': stringutil.email(ctx.user()), 371 b'authorEmail': stringutil.email(ctx.user()),
372 'time': ctx.date()[0], 372 b'time': ctx.date()[0],
373 }, 373 },
374 }), 374 }),
375 } 375 }
376 callconduit(ctx.repo(), 'differential.setdiffproperty', params) 376 callconduit(ctx.repo(), b'differential.setdiffproperty', params)
377 377
378 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None, 378 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
379 olddiff=None, actions=None): 379 olddiff=None, actions=None):
380 """create or update a Differential Revision 380 """create or update a Differential Revision
381 381
396 neednewdiff = True 396 neednewdiff = True
397 397
398 transactions = [] 398 transactions = []
399 if neednewdiff: 399 if neednewdiff:
400 diff = creatediff(ctx) 400 diff = creatediff(ctx)
401 transactions.append({'type': 'update', 'value': diff[r'phid']}) 401 transactions.append({b'type': b'update', b'value': diff[r'phid']})
402 else: 402 else:
403 # Even if we don't need to upload a new diff because the patch content 403 # Even if we don't need to upload a new diff because the patch content
404 # does not change. We might still need to update its metadata so 404 # does not change. We might still need to update its metadata so
405 # pushers could know the correct node metadata. 405 # pushers could know the correct node metadata.
406 assert olddiff 406 assert olddiff
410 # Use a temporary summary to set dependency. There might be better ways but 410 # Use a temporary summary to set dependency. There might be better ways but
411 # I cannot find them for now. But do not do that if we are updating an 411 # I cannot find them for now. But do not do that if we are updating an
412 # existing revision (revid is not None) since that introduces visible 412 # existing revision (revid is not None) since that introduces visible
413 # churns (someone edited "Summary" twice) on the web page. 413 # churns (someone edited "Summary" twice) on the web page.
414 if parentrevid and revid is None: 414 if parentrevid and revid is None:
415 summary = 'Depends on D%s' % parentrevid 415 summary = b'Depends on D%s' % parentrevid
416 transactions += [{'type': 'summary', 'value': summary}, 416 transactions += [{b'type': b'summary', b'value': summary},
417 {'type': 'summary', 'value': ' '}] 417 {b'type': b'summary', b'value': b' '}]
418 418
419 if actions: 419 if actions:
420 transactions += actions 420 transactions += actions
421 421
422 # Parse commit message and update related fields. 422 # Parse commit message and update related fields.
423 desc = ctx.description() 423 desc = ctx.description()
424 info = callconduit(repo, 'differential.parsecommitmessage', 424 info = callconduit(repo, b'differential.parsecommitmessage',
425 {'corpus': desc}) 425 {b'corpus': desc})
426 for k, v in info[r'fields'].items(): 426 for k, v in info[r'fields'].items():
427 if k in ['title', 'summary', 'testPlan']: 427 if k in [b'title', b'summary', b'testPlan']:
428 transactions.append({'type': k, 'value': v}) 428 transactions.append({b'type': k, b'value': v})
429 429
430 params = {'transactions': transactions} 430 params = {b'transactions': transactions}
431 if revid is not None: 431 if revid is not None:
432 # Update an existing Differential Revision 432 # Update an existing Differential Revision
433 params['objectIdentifier'] = revid 433 params[b'objectIdentifier'] = revid
434 434
435 revision = callconduit(repo, 'differential.revision.edit', params) 435 revision = callconduit(repo, b'differential.revision.edit', params)
436 if not revision: 436 if not revision:
437 raise error.Abort(_('cannot create revision for %s') % ctx) 437 raise error.Abort(_(b'cannot create revision for %s') % ctx)
438 438
439 return revision, diff 439 return revision, diff
440 440
441 def userphids(repo, names): 441 def userphids(repo, names):
442 """convert user names to PHIDs""" 442 """convert user names to PHIDs"""
443 query = {'constraints': {'usernames': names}} 443 query = {b'constraints': {b'usernames': names}}
444 result = callconduit(repo, 'user.search', query) 444 result = callconduit(repo, b'user.search', query)
445 # username not found is not an error of the API. So check if we have missed 445 # username not found is not an error of the API. So check if we have missed
446 # some names here. 446 # some names here.
447 data = result[r'data'] 447 data = result[r'data']
448 resolved = set(entry[r'fields'][r'username'] for entry in data) 448 resolved = set(entry[r'fields'][r'username'] for entry in data)
449 unresolved = set(names) - resolved 449 unresolved = set(names) - resolved
450 if unresolved: 450 if unresolved:
451 raise error.Abort(_('unknown username: %s') 451 raise error.Abort(_(b'unknown username: %s')
452 % ' '.join(sorted(unresolved))) 452 % b' '.join(sorted(unresolved)))
453 return [entry[r'phid'] for entry in data] 453 return [entry[r'phid'] for entry in data]
454 454
455 @command('phabsend', 455 @command(b'phabsend',
456 [('r', 'rev', [], _('revisions to send'), _('REV')), 456 [(b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
457 ('', 'amend', True, _('update commit messages')), 457 (b'', b'amend', True, _(b'update commit messages')),
458 ('', 'reviewer', [], _('specify reviewers')), 458 (b'', b'reviewer', [], _(b'specify reviewers')),
459 ('', 'confirm', None, _('ask for confirmation before sending'))], 459 (b'', b'confirm', None, _(b'ask for confirmation before sending'))],
460 _('REV [OPTIONS]')) 460 _(b'REV [OPTIONS]'))
461 def phabsend(ui, repo, *revs, **opts): 461 def phabsend(ui, repo, *revs, **opts):
462 """upload changesets to Phabricator 462 """upload changesets to Phabricator
463 463
464 If there are multiple revisions specified, they will be send as a stack 464 If there are multiple revisions specified, they will be send as a stack
465 with a linear dependencies relationship using the order specified by the 465 with a linear dependencies relationship using the order specified by the
483 confirm = true 483 confirm = true
484 484
485 phabsend will check obsstore and the above association to decide whether to 485 phabsend will check obsstore and the above association to decide whether to
486 update an existing Differential Revision, or create a new one. 486 update an existing Differential Revision, or create a new one.
487 """ 487 """
488 revs = list(revs) + opts.get('rev', []) 488 revs = list(revs) + opts.get(b'rev', [])
489 revs = scmutil.revrange(repo, revs) 489 revs = scmutil.revrange(repo, revs)
490 490
491 if not revs: 491 if not revs:
492 raise error.Abort(_('phabsend requires at least one changeset')) 492 raise error.Abort(_(b'phabsend requires at least one changeset'))
493 if opts.get('amend'): 493 if opts.get(b'amend'):
494 cmdutil.checkunfinished(repo) 494 cmdutil.checkunfinished(repo)
495 495
496 # {newnode: (oldnode, olddiff, olddrev} 496 # {newnode: (oldnode, olddiff, olddrev}
497 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs]) 497 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
498 498
499 confirm = ui.configbool('phabsend', 'confirm') 499 confirm = ui.configbool(b'phabsend', b'confirm')
500 confirm |= bool(opts.get('confirm')) 500 confirm |= bool(opts.get(b'confirm'))
501 if confirm: 501 if confirm:
502 confirmed = _confirmbeforesend(repo, revs, oldmap) 502 confirmed = _confirmbeforesend(repo, revs, oldmap)
503 if not confirmed: 503 if not confirmed:
504 raise error.Abort(_('phabsend cancelled')) 504 raise error.Abort(_(b'phabsend cancelled'))
505 505
506 actions = [] 506 actions = []
507 reviewers = opts.get('reviewer', []) 507 reviewers = opts.get(b'reviewer', [])
508 if reviewers: 508 if reviewers:
509 phids = userphids(repo, reviewers) 509 phids = userphids(repo, reviewers)
510 actions.append({'type': 'reviewers.add', 'value': phids}) 510 actions.append({b'type': b'reviewers.add', b'value': phids})
511 511
512 drevids = [] # [int] 512 drevids = [] # [int]
513 diffmap = {} # {newnode: diff} 513 diffmap = {} # {newnode: diff}
514 514
515 # Send patches one by one so we know their Differential Revision IDs and 515 # Send patches one by one so we know their Differential Revision IDs and
516 # can provide dependency relationship 516 # can provide dependency relationship
517 lastrevid = None 517 lastrevid = None
518 for rev in revs: 518 for rev in revs:
519 ui.debug('sending rev %d\n' % rev) 519 ui.debug(b'sending rev %d\n' % rev)
520 ctx = repo[rev] 520 ctx = repo[rev]
521 521
522 # Get Differential Revision ID 522 # Get Differential Revision ID
523 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None)) 523 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
524 if oldnode != ctx.node() or opts.get('amend'): 524 if oldnode != ctx.node() or opts.get(b'amend'):
525 # Create or update Differential Revision 525 # Create or update Differential Revision
526 revision, diff = createdifferentialrevision( 526 revision, diff = createdifferentialrevision(
527 ctx, revid, lastrevid, oldnode, olddiff, actions) 527 ctx, revid, lastrevid, oldnode, olddiff, actions)
528 diffmap[ctx.node()] = diff 528 diffmap[ctx.node()] = diff
529 newrevid = int(revision[r'object'][r'id']) 529 newrevid = int(revision[r'object'][r'id'])
530 if revid: 530 if revid:
531 action = 'updated' 531 action = b'updated'
532 else: 532 else:
533 action = 'created' 533 action = b'created'
534 534
535 # Create a local tag to note the association, if commit message 535 # Create a local tag to note the association, if commit message
536 # does not have it already 536 # does not have it already
537 m = _differentialrevisiondescre.search(ctx.description()) 537 m = _differentialrevisiondescre.search(ctx.description())
538 if not m or int(m.group('id')) != newrevid: 538 if not m or int(m.group(b'id')) != newrevid:
539 tagname = 'D%d' % newrevid 539 tagname = b'D%d' % newrevid
540 tags.tag(repo, tagname, ctx.node(), message=None, user=None, 540 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
541 date=None, local=True) 541 date=None, local=True)
542 else: 542 else:
543 # Nothing changed. But still set "newrevid" so the next revision 543 # Nothing changed. But still set "newrevid" so the next revision
544 # could depend on this one. 544 # could depend on this one.
545 newrevid = revid 545 newrevid = revid
546 action = 'skipped' 546 action = b'skipped'
547 547
548 actiondesc = ui.label( 548 actiondesc = ui.label(
549 {'created': _('created'), 549 {b'created': _(b'created'),
550 'skipped': _('skipped'), 550 b'skipped': _(b'skipped'),
551 'updated': _('updated')}[action], 551 b'updated': _(b'updated')}[action],
552 'phabricator.action.%s' % action) 552 b'phabricator.action.%s' % action)
553 drevdesc = ui.label('D%s' % newrevid, 'phabricator.drev') 553 drevdesc = ui.label(b'D%s' % newrevid, b'phabricator.drev')
554 nodedesc = ui.label(bytes(ctx), 'phabricator.node') 554 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
555 desc = ui.label(ctx.description().split('\n')[0], 'phabricator.desc') 555 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
556 ui.write(_('%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, 556 ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc,
557 desc)) 557 desc))
558 drevids.append(newrevid) 558 drevids.append(newrevid)
559 lastrevid = newrevid 559 lastrevid = newrevid
560 560
561 # Update commit messages and remove tags 561 # Update commit messages and remove tags
562 if opts.get('amend'): 562 if opts.get(b'amend'):
563 unfi = repo.unfiltered() 563 unfi = repo.unfiltered()
564 drevs = callconduit(repo, 'differential.query', {'ids': drevids}) 564 drevs = callconduit(repo, b'differential.query', {b'ids': drevids})
565 with repo.wlock(), repo.lock(), repo.transaction('phabsend'): 565 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
566 wnode = unfi['.'].node() 566 wnode = unfi[b'.'].node()
567 mapping = {} # {oldnode: [newnode]} 567 mapping = {} # {oldnode: [newnode]}
568 for i, rev in enumerate(revs): 568 for i, rev in enumerate(revs):
569 old = unfi[rev] 569 old = unfi[rev]
570 drevid = drevids[i] 570 drevid = drevids[i]
571 drev = [d for d in drevs if int(d[r'id']) == drevid][0] 571 drev = [d for d in drevs if int(d[r'id']) == drevid][0]
578 ] 578 ]
579 new = context.metadataonlyctx( 579 new = context.metadataonlyctx(
580 repo, old, parents=parents, text=newdesc, 580 repo, old, parents=parents, text=newdesc,
581 user=old.user(), date=old.date(), extra=old.extra()) 581 user=old.user(), date=old.date(), extra=old.extra())
582 582
583 overrides = {('phases', 'new-commit'): old.phase()} 583 overrides = {(b'phases', b'new-commit'): old.phase()}
584 with ui.configoverride(overrides, 'phabsend'): 584 with ui.configoverride(overrides, b'phabsend'):
585 newnode = new.commit() 585 newnode = new.commit()
586 586
587 mapping[old.node()] = [newnode] 587 mapping[old.node()] = [newnode]
588 # Update diff property 588 # Update diff property
589 writediffproperties(unfi[newnode], diffmap[old.node()]) 589 writediffproperties(unfi[newnode], diffmap[old.node()])
590 # Remove local tags since it's no longer necessary 590 # Remove local tags since it's no longer necessary
591 tagname = 'D%d' % drevid 591 tagname = b'D%d' % drevid
592 if tagname in repo.tags(): 592 if tagname in repo.tags():
593 tags.tag(repo, tagname, nullid, message=None, user=None, 593 tags.tag(repo, tagname, nullid, message=None, user=None,
594 date=None, local=True) 594 date=None, local=True)
595 scmutil.cleanupnodes(repo, mapping, 'phabsend') 595 scmutil.cleanupnodes(repo, mapping, b'phabsend')
596 if wnode in mapping: 596 if wnode in mapping:
597 unfi.setparents(mapping[wnode][0]) 597 unfi.setparents(mapping[wnode][0])
598 598
599 # Map from "hg:meta" keys to header understood by "hg import". The order is 599 # Map from "hg:meta" keys to header understood by "hg import". The order is
600 # consistent with "hg export" output. 600 # consistent with "hg export" output.
601 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'), 601 _metanamemap = util.sortdict([(r'user', b'User'), (r'date', b'Date'),
602 (r'node', 'Node ID'), (r'parent', 'Parent ')]) 602 (r'node', b'Node ID'), (r'parent', b'Parent ')])
603 603
604 def _confirmbeforesend(repo, revs, oldmap): 604 def _confirmbeforesend(repo, revs, oldmap):
605 url, token = readurltoken(repo) 605 url, token = readurltoken(repo)
606 ui = repo.ui 606 ui = repo.ui
607 for rev in revs: 607 for rev in revs:
608 ctx = repo[rev] 608 ctx = repo[rev]
609 desc = ctx.description().splitlines()[0] 609 desc = ctx.description().splitlines()[0]
610 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None)) 610 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
611 if drevid: 611 if drevid:
612 drevdesc = ui.label('D%s' % drevid, 'phabricator.drev') 612 drevdesc = ui.label(b'D%s' % drevid, b'phabricator.drev')
613 else: 613 else:
614 drevdesc = ui.label(_('NEW'), 'phabricator.drev') 614 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
615 615
616 ui.write(_('%s - %s: %s\n') % (drevdesc, 616 ui.write(_(b'%s - %s: %s\n')
617 ui.label(bytes(ctx), 'phabricator.node'), 617 % (drevdesc,
618 ui.label(desc, 'phabricator.desc'))) 618 ui.label(bytes(ctx), b'phabricator.node'),
619 619 ui.label(desc, b'phabricator.desc')))
620 if ui.promptchoice(_('Send the above changes to %s (yn)?' 620
621 '$$ &Yes $$ &No') % url): 621 if ui.promptchoice(_(b'Send the above changes to %s (yn)?'
622 b'$$ &Yes $$ &No') % url):
622 return False 623 return False
623 624
624 return True 625 return True
625 626
626 _knownstatusnames = {'accepted', 'needsreview', 'needsrevision', 'closed', 627 _knownstatusnames = {b'accepted', b'needsreview', b'needsrevision', b'closed',
627 'abandoned'} 628 b'abandoned'}
628 629
629 def _getstatusname(drev): 630 def _getstatusname(drev):
630 """get normalized status name from a Differential Revision""" 631 """get normalized status name from a Differential Revision"""
631 return drev[r'statusName'].replace(' ', '').lower() 632 return drev[r'statusName'].replace(b' ', b'').lower()
632 633
633 # Small language to specify differential revisions. Support symbols: (), :X, 634 # Small language to specify differential revisions. Support symbols: (), :X,
634 # +, and -. 635 # +, and -.
635 636
636 _elements = { 637 _elements = {
637 # token-type: binding-strength, primary, prefix, infix, suffix 638 # token-type: binding-strength, primary, prefix, infix, suffix
638 '(': (12, None, ('group', 1, ')'), None, None), 639 b'(': (12, None, (b'group', 1, b')'), None, None),
639 ':': (8, None, ('ancestors', 8), None, None), 640 b':': (8, None, (b'ancestors', 8), None, None),
640 '&': (5, None, None, ('and_', 5), None), 641 b'&': (5, None, None, (b'and_', 5), None),
641 '+': (4, None, None, ('add', 4), None), 642 b'+': (4, None, None, (b'add', 4), None),
642 '-': (4, None, None, ('sub', 4), None), 643 b'-': (4, None, None, (b'sub', 4), None),
643 ')': (0, None, None, None, None), 644 b')': (0, None, None, None, None),
644 'symbol': (0, 'symbol', None, None, None), 645 b'symbol': (0, b'symbol', None, None, None),
645 'end': (0, None, None, None, None), 646 b'end': (0, None, None, None, None),
646 } 647 }
647 648
648 def _tokenize(text): 649 def _tokenize(text):
649 view = memoryview(text) # zero-copy slice 650 view = memoryview(text) # zero-copy slice
650 special = '():+-& ' 651 special = b'():+-& '
651 pos = 0 652 pos = 0
652 length = len(text) 653 length = len(text)
653 while pos < length: 654 while pos < length:
654 symbol = ''.join(itertools.takewhile(lambda ch: ch not in special, 655 symbol = b''.join(itertools.takewhile(lambda ch: ch not in special,
655 view[pos:])) 656 view[pos:]))
656 if symbol: 657 if symbol:
657 yield ('symbol', symbol, pos) 658 yield (b'symbol', symbol, pos)
658 pos += len(symbol) 659 pos += len(symbol)
659 else: # special char, ignore space 660 else: # special char, ignore space
660 if text[pos] != ' ': 661 if text[pos] != b' ':
661 yield (text[pos], None, pos) 662 yield (text[pos], None, pos)
662 pos += 1 663 pos += 1
663 yield ('end', None, pos) 664 yield (b'end', None, pos)
664 665
665 def _parse(text): 666 def _parse(text):
666 tree, pos = parser.parser(_elements).parse(_tokenize(text)) 667 tree, pos = parser.parser(_elements).parse(_tokenize(text))
667 if pos != len(text): 668 if pos != len(text):
668 raise error.ParseError('invalid token', pos) 669 raise error.ParseError(b'invalid token', pos)
669 return tree 670 return tree
670 671
671 def _parsedrev(symbol): 672 def _parsedrev(symbol):
672 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None""" 673 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
673 if symbol.startswith('D') and symbol[1:].isdigit(): 674 if symbol.startswith(b'D') and symbol[1:].isdigit():
674 return int(symbol[1:]) 675 return int(symbol[1:])
675 if symbol.isdigit(): 676 if symbol.isdigit():
676 return int(symbol) 677 return int(symbol)
677 678
678 def _prefetchdrevs(tree): 679 def _prefetchdrevs(tree):
679 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch""" 680 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
680 drevs = set() 681 drevs = set()
681 ancestordrevs = set() 682 ancestordrevs = set()
682 op = tree[0] 683 op = tree[0]
683 if op == 'symbol': 684 if op == b'symbol':
684 r = _parsedrev(tree[1]) 685 r = _parsedrev(tree[1])
685 if r: 686 if r:
686 drevs.add(r) 687 drevs.add(r)
687 elif op == 'ancestors': 688 elif op == b'ancestors':
688 r, a = _prefetchdrevs(tree[1]) 689 r, a = _prefetchdrevs(tree[1])
689 drevs.update(r) 690 drevs.update(r)
690 ancestordrevs.update(r) 691 ancestordrevs.update(r)
691 ancestordrevs.update(a) 692 ancestordrevs.update(a)
692 else: 693 else:
741 def fetch(params): 742 def fetch(params):
742 """params -> single drev or None""" 743 """params -> single drev or None"""
743 key = (params.get(r'ids') or params.get(r'phids') or [None])[0] 744 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
744 if key in prefetched: 745 if key in prefetched:
745 return prefetched[key] 746 return prefetched[key]
746 drevs = callconduit(repo, 'differential.query', params) 747 drevs = callconduit(repo, b'differential.query', params)
747 # Fill prefetched with the result 748 # Fill prefetched with the result
748 for drev in drevs: 749 for drev in drevs:
749 prefetched[drev[r'phid']] = drev 750 prefetched[drev[r'phid']] = drev
750 prefetched[int(drev[r'id'])] = drev 751 prefetched[int(drev[r'id'])] = drev
751 if key not in prefetched: 752 if key not in prefetched:
752 raise error.Abort(_('cannot get Differential Revision %r') % params) 753 raise error.Abort(_(b'cannot get Differential Revision %r')
754 % params)
753 return prefetched[key] 755 return prefetched[key]
754 756
755 def getstack(topdrevids): 757 def getstack(topdrevids):
756 """given a top, get a stack from the bottom, [id] -> [id]""" 758 """given a top, get a stack from the bottom, [id] -> [id]"""
757 visited = set() 759 visited = set()
765 visited.add(drev[r'id']) 767 visited.add(drev[r'id'])
766 result.append(int(drev[r'id'])) 768 result.append(int(drev[r'id']))
767 auxiliary = drev.get(r'auxiliary', {}) 769 auxiliary = drev.get(r'auxiliary', {})
768 depends = auxiliary.get(r'phabricator:depends-on', []) 770 depends = auxiliary.get(r'phabricator:depends-on', [])
769 for phid in depends: 771 for phid in depends:
770 queue.append({'phids': [phid]}) 772 queue.append({b'phids': [phid]})
771 result.reverse() 773 result.reverse()
772 return smartset.baseset(result) 774 return smartset.baseset(result)
773 775
774 # Initialize prefetch cache 776 # Initialize prefetch cache
775 prefetched = {} # {id or phid: drev} 777 prefetched = {} # {id or phid: drev}
776 778
777 tree = _parse(spec) 779 tree = _parse(spec)
778 drevs, ancestordrevs = _prefetchdrevs(tree) 780 drevs, ancestordrevs = _prefetchdrevs(tree)
779 781
780 # developer config: phabricator.batchsize 782 # developer config: phabricator.batchsize
781 batchsize = repo.ui.configint('phabricator', 'batchsize') 783 batchsize = repo.ui.configint(b'phabricator', b'batchsize')
782 784
783 # Prefetch Differential Revisions in batch 785 # Prefetch Differential Revisions in batch
784 tofetch = set(drevs) 786 tofetch = set(drevs)
785 for r in ancestordrevs: 787 for r in ancestordrevs:
786 tofetch.update(range(max(1, r - batchsize), r + 1)) 788 tofetch.update(range(max(1, r - batchsize), r + 1))
789 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs)) 791 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
790 792
791 # Walk through the tree, return smartsets 793 # Walk through the tree, return smartsets
792 def walk(tree): 794 def walk(tree):
793 op = tree[0] 795 op = tree[0]
794 if op == 'symbol': 796 if op == b'symbol':
795 drev = _parsedrev(tree[1]) 797 drev = _parsedrev(tree[1])
796 if drev: 798 if drev:
797 return smartset.baseset([drev]) 799 return smartset.baseset([drev])
798 elif tree[1] in _knownstatusnames: 800 elif tree[1] in _knownstatusnames:
799 drevs = [r for r in validids 801 drevs = [r for r in validids
800 if _getstatusname(prefetched[r]) == tree[1]] 802 if _getstatusname(prefetched[r]) == tree[1]]
801 return smartset.baseset(drevs) 803 return smartset.baseset(drevs)
802 else: 804 else:
803 raise error.Abort(_('unknown symbol: %s') % tree[1]) 805 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
804 elif op in {'and_', 'add', 'sub'}: 806 elif op in {b'and_', b'add', b'sub'}:
805 assert len(tree) == 3 807 assert len(tree) == 3
806 return getattr(operator, op)(walk(tree[1]), walk(tree[2])) 808 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
807 elif op == 'group': 809 elif op == b'group':
808 return walk(tree[1]) 810 return walk(tree[1])
809 elif op == 'ancestors': 811 elif op == b'ancestors':
810 return getstack(walk(tree[1])) 812 return getstack(walk(tree[1]))
811 else: 813 else:
812 raise error.ProgrammingError('illegal tree: %r' % tree) 814 raise error.ProgrammingError(b'illegal tree: %r' % tree)
813 815
814 return [prefetched[r] for r in walk(tree)] 816 return [prefetched[r] for r in walk(tree)]
815 817
816 def getdescfromdrev(drev): 818 def getdescfromdrev(drev):
817 """get description (commit message) from "Differential Revision" 819 """get description (commit message) from "Differential Revision"
821 """ 823 """
822 title = drev[r'title'] 824 title = drev[r'title']
823 summary = drev[r'summary'].rstrip() 825 summary = drev[r'summary'].rstrip()
824 testplan = drev[r'testPlan'].rstrip() 826 testplan = drev[r'testPlan'].rstrip()
825 if testplan: 827 if testplan:
826 testplan = 'Test Plan:\n%s' % testplan 828 testplan = b'Test Plan:\n%s' % testplan
827 uri = 'Differential Revision: %s' % drev[r'uri'] 829 uri = b'Differential Revision: %s' % drev[r'uri']
828 return '\n\n'.join(filter(None, [title, summary, testplan, uri])) 830 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
829 831
830 def getdiffmeta(diff): 832 def getdiffmeta(diff):
831 """get commit metadata (date, node, user, p1) from a diff object 833 """get commit metadata (date, node, user, p1) from a diff object
832 834
833 The metadata could be "hg:meta", sent by phabsend, like: 835 The metadata could be "hg:meta", sent by phabsend, like:
883 write is usually ui.write. drevs is what "querydrev" returns, results of 885 write is usually ui.write. drevs is what "querydrev" returns, results of
884 "differential.query". 886 "differential.query".
885 """ 887 """
886 # Prefetch hg:meta property for all diffs 888 # Prefetch hg:meta property for all diffs
887 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs)) 889 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
888 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids}) 890 diffs = callconduit(repo, b'differential.querydiffs', {b'ids': diffids})
889 891
890 # Generate patch for each drev 892 # Generate patch for each drev
891 for drev in drevs: 893 for drev in drevs:
892 repo.ui.note(_('reading D%s\n') % drev[r'id']) 894 repo.ui.note(_(b'reading D%s\n') % drev[r'id'])
893 895
894 diffid = max(int(v) for v in drev[r'diffs']) 896 diffid = max(int(v) for v in drev[r'diffs'])
895 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid}) 897 body = callconduit(repo, b'differential.getrawdiff',
898 {b'diffID': diffid})
896 desc = getdescfromdrev(drev) 899 desc = getdescfromdrev(drev)
897 header = '# HG changeset patch\n' 900 header = b'# HG changeset patch\n'
898 901
899 # Try to preserve metadata from hg:meta property. Write hg patch 902 # Try to preserve metadata from hg:meta property. Write hg patch
900 # headers that can be read by the "import" command. See patchheadermap 903 # headers that can be read by the "import" command. See patchheadermap
901 # and extract in mercurial/patch.py for supported headers. 904 # and extract in mercurial/patch.py for supported headers.
902 meta = getdiffmeta(diffs[str(diffid)]) 905 meta = getdiffmeta(diffs[str(diffid)])
903 for k in _metanamemap.keys(): 906 for k in _metanamemap.keys():
904 if k in meta: 907 if k in meta:
905 header += '# %s %s\n' % (_metanamemap[k], meta[k]) 908 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
906 909
907 content = '%s%s\n%s' % (header, desc, body) 910 content = b'%s%s\n%s' % (header, desc, body)
908 write(encoding.unitolocal(content)) 911 write(encoding.unitolocal(content))
909 912
910 @command('phabread', 913 @command(b'phabread',
911 [('', 'stack', False, _('read dependencies'))], 914 [(b'', b'stack', False, _(b'read dependencies'))],
912 _('DREVSPEC [OPTIONS]')) 915 _(b'DREVSPEC [OPTIONS]'))
913 def phabread(ui, repo, spec, **opts): 916 def phabread(ui, repo, spec, **opts):
914 """print patches from Phabricator suitable for importing 917 """print patches from Phabricator suitable for importing
915 918
916 DREVSPEC could be a Differential Revision identity, like ``D123``, or just 919 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
917 the number ``123``. It could also have common operators like ``+``, ``-``, 920 the number ``123``. It could also have common operators like ``+``, ``-``,
927 stack up to D9. 930 stack up to D9.
928 931
929 If --stack is given, follow dependencies information and read all patches. 932 If --stack is given, follow dependencies information and read all patches.
930 It is equivalent to the ``:`` operator. 933 It is equivalent to the ``:`` operator.
931 """ 934 """
932 if opts.get('stack'): 935 if opts.get(b'stack'):
933 spec = ':(%s)' % spec 936 spec = b':(%s)' % spec
934 drevs = querydrev(repo, spec) 937 drevs = querydrev(repo, spec)
935 readpatch(repo, drevs, ui.write) 938 readpatch(repo, drevs, ui.write)
936 939
937 @command('phabupdate', 940 @command(b'phabupdate',
938 [('', 'accept', False, _('accept revisions')), 941 [(b'', b'accept', False, _(b'accept revisions')),
939 ('', 'reject', False, _('reject revisions')), 942 (b'', b'reject', False, _(b'reject revisions')),
940 ('', 'abandon', False, _('abandon revisions')), 943 (b'', b'abandon', False, _(b'abandon revisions')),
941 ('', 'reclaim', False, _('reclaim revisions')), 944 (b'', b'reclaim', False, _(b'reclaim revisions')),
942 ('m', 'comment', '', _('comment on the last revision')), 945 (b'm', b'comment', b'', _(b'comment on the last revision')),
943 ], _('DREVSPEC [OPTIONS]')) 946 ], _(b'DREVSPEC [OPTIONS]'))
944 def phabupdate(ui, repo, spec, **opts): 947 def phabupdate(ui, repo, spec, **opts):
945 """update Differential Revision in batch 948 """update Differential Revision in batch
946 949
947 DREVSPEC selects revisions. See :hg:`help phabread` for its usage. 950 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
948 """ 951 """
949 flags = [n for n in 'accept reject abandon reclaim'.split() if opts.get(n)] 952 flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)]
950 if len(flags) > 1: 953 if len(flags) > 1:
951 raise error.Abort(_('%s cannot be used together') % ', '.join(flags)) 954 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
952 955
953 actions = [] 956 actions = []
954 for f in flags: 957 for f in flags:
955 actions.append({'type': f, 'value': 'true'}) 958 actions.append({b'type': f, b'value': b'true'})
956 959
957 drevs = querydrev(repo, spec) 960 drevs = querydrev(repo, spec)
958 for i, drev in enumerate(drevs): 961 for i, drev in enumerate(drevs):
959 if i + 1 == len(drevs) and opts.get('comment'): 962 if i + 1 == len(drevs) and opts.get(b'comment'):
960 actions.append({'type': 'comment', 'value': opts['comment']}) 963 actions.append({b'type': b'comment', b'value': opts[b'comment']})
961 if actions: 964 if actions:
962 params = {'objectIdentifier': drev[r'phid'], 965 params = {b'objectIdentifier': drev[r'phid'],
963 'transactions': actions} 966 b'transactions': actions}
964 callconduit(repo, 'differential.revision.edit', params) 967 callconduit(repo, b'differential.revision.edit', params)
965 968
966 templatekeyword = registrar.templatekeyword() 969 templatekeyword = registrar.templatekeyword()
967 970
968 @templatekeyword('phabreview', requires={'ctx'}) 971 @templatekeyword(b'phabreview', requires={b'ctx'})
969 def template_review(context, mapping): 972 def template_review(context, mapping):
970 """:phabreview: Object describing the review for this changeset. 973 """:phabreview: Object describing the review for this changeset.
971 Has attributes `url` and `id`. 974 Has attributes `url` and `id`.
972 """ 975 """
973 ctx = context.resource(mapping, 'ctx') 976 ctx = context.resource(mapping, b'ctx')
974 m = _differentialrevisiondescre.search(ctx.description()) 977 m = _differentialrevisiondescre.search(ctx.description())
975 if m: 978 if m:
976 return { 979 return {
977 'url': m.group('url'), 980 b'url': m.group(b'url'),
978 'id': "D{}".format(m.group('id')), 981 b'id': b"D{}".format(m.group(b'id')),
979 } 982 }