##// END OF EJS Templates
phabricator: change "readpatch" to be more flexible...
Jun Wu -
r33830:75fdaf85 default
parent child Browse files
Show More
@@ -1,686 +1,684 b''
1 # phabricator.py - simple Phabricator integration
1 # phabricator.py - simple Phabricator integration
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """simple Phabricator integration
7 """simple Phabricator integration
8
8
9 This extension provides a ``phabsend`` command which sends a stack of
9 This extension provides a ``phabsend`` command which sends a stack of
10 changesets to Phabricator without amending commit messages, and a ``phabread``
10 changesets to Phabricator without amending commit messages, and a ``phabread``
11 command which prints a stack of revisions in a format suitable
11 command which prints a stack of revisions in a format suitable
12 for :hg:`import`.
12 for :hg:`import`.
13
13
14 By default, Phabricator requires ``Test Plan`` which might prevent some
14 By default, Phabricator requires ``Test Plan`` which might prevent some
15 changeset from being sent. The requirement could be disabled by changing
15 changeset from being sent. The requirement could be disabled by changing
16 ``differential.require-test-plan-field`` config server side.
16 ``differential.require-test-plan-field`` config server side.
17
17
18 Config::
18 Config::
19
19
20 [phabricator]
20 [phabricator]
21 # Phabricator URL
21 # Phabricator URL
22 url = https://phab.example.com/
22 url = https://phab.example.com/
23
23
24 # API token. Get it from https://$HOST/conduit/login/
24 # API token. Get it from https://$HOST/conduit/login/
25 token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
25 token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
26
26
27 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
27 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
28 # callsign is "FOO".
28 # callsign is "FOO".
29 callsign = FOO
29 callsign = FOO
30
30
31 """
31 """
32
32
33 from __future__ import absolute_import
33 from __future__ import absolute_import
34
34
35 import json
35 import json
36 import re
36 import re
37
37
38 from mercurial.node import bin, nullid
38 from mercurial.node import bin, nullid
39 from mercurial.i18n import _
39 from mercurial.i18n import _
40 from mercurial import (
40 from mercurial import (
41 cmdutil,
41 cmdutil,
42 context,
42 context,
43 encoding,
43 encoding,
44 error,
44 error,
45 mdiff,
45 mdiff,
46 obsutil,
46 obsutil,
47 patch,
47 patch,
48 registrar,
48 registrar,
49 scmutil,
49 scmutil,
50 tags,
50 tags,
51 url as urlmod,
51 url as urlmod,
52 util,
52 util,
53 )
53 )
54
54
55 cmdtable = {}
55 cmdtable = {}
56 command = registrar.command(cmdtable)
56 command = registrar.command(cmdtable)
57
57
58 def urlencodenested(params):
58 def urlencodenested(params):
59 """like urlencode, but works with nested parameters.
59 """like urlencode, but works with nested parameters.
60
60
61 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
61 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
62 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
62 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
63 urlencode. Note: the encoding is consistent with PHP's http_build_query.
63 urlencode. Note: the encoding is consistent with PHP's http_build_query.
64 """
64 """
65 flatparams = util.sortdict()
65 flatparams = util.sortdict()
66 def process(prefix, obj):
66 def process(prefix, obj):
67 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
67 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
68 if items is None:
68 if items is None:
69 flatparams[prefix] = obj
69 flatparams[prefix] = obj
70 else:
70 else:
71 for k, v in items(obj):
71 for k, v in items(obj):
72 if prefix:
72 if prefix:
73 process('%s[%s]' % (prefix, k), v)
73 process('%s[%s]' % (prefix, k), v)
74 else:
74 else:
75 process(k, v)
75 process(k, v)
76 process('', params)
76 process('', params)
77 return util.urlreq.urlencode(flatparams)
77 return util.urlreq.urlencode(flatparams)
78
78
79 def readurltoken(repo):
79 def readurltoken(repo):
80 """return conduit url, token and make sure they exist
80 """return conduit url, token and make sure they exist
81
81
82 Currently read from [phabricator] config section. In the future, it might
82 Currently read from [phabricator] config section. In the future, it might
83 make sense to read from .arcconfig and .arcrc as well.
83 make sense to read from .arcconfig and .arcrc as well.
84 """
84 """
85 values = []
85 values = []
86 section = 'phabricator'
86 section = 'phabricator'
87 for name in ['url', 'token']:
87 for name in ['url', 'token']:
88 value = repo.ui.config(section, name)
88 value = repo.ui.config(section, name)
89 if not value:
89 if not value:
90 raise error.Abort(_('config %s.%s is required') % (section, name))
90 raise error.Abort(_('config %s.%s is required') % (section, name))
91 values.append(value)
91 values.append(value)
92 return values
92 return values
93
93
94 def callconduit(repo, name, params):
94 def callconduit(repo, name, params):
95 """call Conduit API, params is a dict. return json.loads result, or None"""
95 """call Conduit API, params is a dict. return json.loads result, or None"""
96 host, token = readurltoken(repo)
96 host, token = readurltoken(repo)
97 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
97 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
98 urlopener = urlmod.opener(repo.ui, authinfo)
98 urlopener = urlmod.opener(repo.ui, authinfo)
99 repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
99 repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
100 params = params.copy()
100 params = params.copy()
101 params['api.token'] = token
101 params['api.token'] = token
102 request = util.urlreq.request(url, data=urlencodenested(params))
102 request = util.urlreq.request(url, data=urlencodenested(params))
103 body = urlopener.open(request).read()
103 body = urlopener.open(request).read()
104 repo.ui.debug('Conduit Response: %s\n' % body)
104 repo.ui.debug('Conduit Response: %s\n' % body)
105 parsed = json.loads(body)
105 parsed = json.loads(body)
106 if parsed.get(r'error_code'):
106 if parsed.get(r'error_code'):
107 msg = (_('Conduit Error (%s): %s')
107 msg = (_('Conduit Error (%s): %s')
108 % (parsed[r'error_code'], parsed[r'error_info']))
108 % (parsed[r'error_code'], parsed[r'error_info']))
109 raise error.Abort(msg)
109 raise error.Abort(msg)
110 return parsed[r'result']
110 return parsed[r'result']
111
111
112 @command('debugcallconduit', [], _('METHOD'))
112 @command('debugcallconduit', [], _('METHOD'))
113 def debugcallconduit(ui, repo, name):
113 def debugcallconduit(ui, repo, name):
114 """call Conduit API
114 """call Conduit API
115
115
116 Call parameters are read from stdin as a JSON blob. Result will be written
116 Call parameters are read from stdin as a JSON blob. Result will be written
117 to stdout as a JSON blob.
117 to stdout as a JSON blob.
118 """
118 """
119 params = json.loads(ui.fin.read())
119 params = json.loads(ui.fin.read())
120 result = callconduit(repo, name, params)
120 result = callconduit(repo, name, params)
121 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
121 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
122 ui.write('%s\n' % s)
122 ui.write('%s\n' % s)
123
123
124 def getrepophid(repo):
124 def getrepophid(repo):
125 """given callsign, return repository PHID or None"""
125 """given callsign, return repository PHID or None"""
126 # developer config: phabricator.repophid
126 # developer config: phabricator.repophid
127 repophid = repo.ui.config('phabricator', 'repophid')
127 repophid = repo.ui.config('phabricator', 'repophid')
128 if repophid:
128 if repophid:
129 return repophid
129 return repophid
130 callsign = repo.ui.config('phabricator', 'callsign')
130 callsign = repo.ui.config('phabricator', 'callsign')
131 if not callsign:
131 if not callsign:
132 return None
132 return None
133 query = callconduit(repo, 'diffusion.repository.search',
133 query = callconduit(repo, 'diffusion.repository.search',
134 {'constraints': {'callsigns': [callsign]}})
134 {'constraints': {'callsigns': [callsign]}})
135 if len(query[r'data']) == 0:
135 if len(query[r'data']) == 0:
136 return None
136 return None
137 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
137 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
138 repo.ui.setconfig('phabricator', 'repophid', repophid)
138 repo.ui.setconfig('phabricator', 'repophid', repophid)
139 return repophid
139 return repophid
140
140
141 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
141 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
142 _differentialrevisiondescre = re.compile(
142 _differentialrevisiondescre = re.compile(
143 '^Differential Revision:\s*(?:.*)D([1-9][0-9]*)$', re.M)
143 '^Differential Revision:\s*(?:.*)D([1-9][0-9]*)$', re.M)
144
144
145 def getoldnodedrevmap(repo, nodelist):
145 def getoldnodedrevmap(repo, nodelist):
146 """find previous nodes that has been sent to Phabricator
146 """find previous nodes that has been sent to Phabricator
147
147
148 return {node: (oldnode, Differential diff, Differential Revision ID)}
148 return {node: (oldnode, Differential diff, Differential Revision ID)}
149 for node in nodelist with known previous sent versions, or associated
149 for node in nodelist with known previous sent versions, or associated
150 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
150 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
151 be ``None``.
151 be ``None``.
152
152
153 Examines commit messages like "Differential Revision:" to get the
153 Examines commit messages like "Differential Revision:" to get the
154 association information.
154 association information.
155
155
156 If such commit message line is not found, examines all precursors and their
156 If such commit message line is not found, examines all precursors and their
157 tags. Tags with format like "D1234" are considered a match and the node
157 tags. Tags with format like "D1234" are considered a match and the node
158 with that tag, and the number after "D" (ex. 1234) will be returned.
158 with that tag, and the number after "D" (ex. 1234) will be returned.
159
159
160 The ``old node``, if not None, is guaranteed to be the last diff of
160 The ``old node``, if not None, is guaranteed to be the last diff of
161 corresponding Differential Revision, and exist in the repo.
161 corresponding Differential Revision, and exist in the repo.
162 """
162 """
163 url, token = readurltoken(repo)
163 url, token = readurltoken(repo)
164 unfi = repo.unfiltered()
164 unfi = repo.unfiltered()
165 nodemap = unfi.changelog.nodemap
165 nodemap = unfi.changelog.nodemap
166
166
167 result = {} # {node: (oldnode?, lastdiff?, drev)}
167 result = {} # {node: (oldnode?, lastdiff?, drev)}
168 toconfirm = {} # {node: (force, {precnode}, drev)}
168 toconfirm = {} # {node: (force, {precnode}, drev)}
169 for node in nodelist:
169 for node in nodelist:
170 ctx = unfi[node]
170 ctx = unfi[node]
171 # For tags like "D123", put them into "toconfirm" to verify later
171 # For tags like "D123", put them into "toconfirm" to verify later
172 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
172 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
173 for n in precnodes:
173 for n in precnodes:
174 if n in nodemap:
174 if n in nodemap:
175 for tag in unfi.nodetags(n):
175 for tag in unfi.nodetags(n):
176 m = _differentialrevisiontagre.match(tag)
176 m = _differentialrevisiontagre.match(tag)
177 if m:
177 if m:
178 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
178 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
179 continue
179 continue
180
180
181 # Check commit message
181 # Check commit message
182 m = _differentialrevisiondescre.search(ctx.description())
182 m = _differentialrevisiondescre.search(ctx.description())
183 if m:
183 if m:
184 toconfirm[node] = (1, set(precnodes), int(m.group(1)))
184 toconfirm[node] = (1, set(precnodes), int(m.group(1)))
185
185
186 # Double check if tags are genuine by collecting all old nodes from
186 # Double check if tags are genuine by collecting all old nodes from
187 # Phabricator, and expect precursors overlap with it.
187 # Phabricator, and expect precursors overlap with it.
188 if toconfirm:
188 if toconfirm:
189 drevs = [drev for force, precs, drev in toconfirm.values()]
189 drevs = [drev for force, precs, drev in toconfirm.values()]
190 alldiffs = callconduit(unfi, 'differential.querydiffs',
190 alldiffs = callconduit(unfi, 'differential.querydiffs',
191 {'revisionIDs': drevs})
191 {'revisionIDs': drevs})
192 getnode = lambda d: bin(encoding.unitolocal(
192 getnode = lambda d: bin(encoding.unitolocal(
193 getdiffmeta(d).get(r'node', ''))) or None
193 getdiffmeta(d).get(r'node', ''))) or None
194 for newnode, (force, precset, drev) in toconfirm.items():
194 for newnode, (force, precset, drev) in toconfirm.items():
195 diffs = [d for d in alldiffs.values()
195 diffs = [d for d in alldiffs.values()
196 if int(d[r'revisionID']) == drev]
196 if int(d[r'revisionID']) == drev]
197
197
198 # "precursors" as known by Phabricator
198 # "precursors" as known by Phabricator
199 phprecset = set(getnode(d) for d in diffs)
199 phprecset = set(getnode(d) for d in diffs)
200
200
201 # Ignore if precursors (Phabricator and local repo) do not overlap,
201 # Ignore if precursors (Phabricator and local repo) do not overlap,
202 # and force is not set (when commit message says nothing)
202 # and force is not set (when commit message says nothing)
203 if not force and not bool(phprecset & precset):
203 if not force and not bool(phprecset & precset):
204 tagname = 'D%d' % drev
204 tagname = 'D%d' % drev
205 tags.tag(repo, tagname, nullid, message=None, user=None,
205 tags.tag(repo, tagname, nullid, message=None, user=None,
206 date=None, local=True)
206 date=None, local=True)
207 unfi.ui.warn(_('D%s: local tag removed - does not match '
207 unfi.ui.warn(_('D%s: local tag removed - does not match '
208 'Differential history\n') % drev)
208 'Differential history\n') % drev)
209 continue
209 continue
210
210
211 # Find the last node using Phabricator metadata, and make sure it
211 # Find the last node using Phabricator metadata, and make sure it
212 # exists in the repo
212 # exists in the repo
213 oldnode = lastdiff = None
213 oldnode = lastdiff = None
214 if diffs:
214 if diffs:
215 lastdiff = max(diffs, key=lambda d: int(d[r'id']))
215 lastdiff = max(diffs, key=lambda d: int(d[r'id']))
216 oldnode = getnode(lastdiff)
216 oldnode = getnode(lastdiff)
217 if oldnode and oldnode not in nodemap:
217 if oldnode and oldnode not in nodemap:
218 oldnode = None
218 oldnode = None
219
219
220 result[newnode] = (oldnode, lastdiff, drev)
220 result[newnode] = (oldnode, lastdiff, drev)
221
221
222 return result
222 return result
223
223
224 def getdiff(ctx, diffopts):
224 def getdiff(ctx, diffopts):
225 """plain-text diff without header (user, commit message, etc)"""
225 """plain-text diff without header (user, commit message, etc)"""
226 output = util.stringio()
226 output = util.stringio()
227 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
227 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
228 None, opts=diffopts):
228 None, opts=diffopts):
229 output.write(chunk)
229 output.write(chunk)
230 return output.getvalue()
230 return output.getvalue()
231
231
232 def creatediff(ctx):
232 def creatediff(ctx):
233 """create a Differential Diff"""
233 """create a Differential Diff"""
234 repo = ctx.repo()
234 repo = ctx.repo()
235 repophid = getrepophid(repo)
235 repophid = getrepophid(repo)
236 # Create a "Differential Diff" via "differential.createrawdiff" API
236 # Create a "Differential Diff" via "differential.createrawdiff" API
237 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
237 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
238 if repophid:
238 if repophid:
239 params['repositoryPHID'] = repophid
239 params['repositoryPHID'] = repophid
240 diff = callconduit(repo, 'differential.createrawdiff', params)
240 diff = callconduit(repo, 'differential.createrawdiff', params)
241 if not diff:
241 if not diff:
242 raise error.Abort(_('cannot create diff for %s') % ctx)
242 raise error.Abort(_('cannot create diff for %s') % ctx)
243 return diff
243 return diff
244
244
245 def writediffproperties(ctx, diff):
245 def writediffproperties(ctx, diff):
246 """write metadata to diff so patches could be applied losslessly"""
246 """write metadata to diff so patches could be applied losslessly"""
247 params = {
247 params = {
248 'diff_id': diff[r'id'],
248 'diff_id': diff[r'id'],
249 'name': 'hg:meta',
249 'name': 'hg:meta',
250 'data': json.dumps({
250 'data': json.dumps({
251 'user': ctx.user(),
251 'user': ctx.user(),
252 'date': '%d %d' % ctx.date(),
252 'date': '%d %d' % ctx.date(),
253 'node': ctx.hex(),
253 'node': ctx.hex(),
254 'parent': ctx.p1().hex(),
254 'parent': ctx.p1().hex(),
255 }),
255 }),
256 }
256 }
257 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
257 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
258
258
259 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
259 def createdifferentialrevision(ctx, revid=None, parentrevid=None, oldnode=None,
260 olddiff=None, actions=None):
260 olddiff=None, actions=None):
261 """create or update a Differential Revision
261 """create or update a Differential Revision
262
262
263 If revid is None, create a new Differential Revision, otherwise update
263 If revid is None, create a new Differential Revision, otherwise update
264 revid. If parentrevid is not None, set it as a dependency.
264 revid. If parentrevid is not None, set it as a dependency.
265
265
266 If oldnode is not None, check if the patch content (without commit message
266 If oldnode is not None, check if the patch content (without commit message
267 and metadata) has changed before creating another diff.
267 and metadata) has changed before creating another diff.
268
268
269 If actions is not None, they will be appended to the transaction.
269 If actions is not None, they will be appended to the transaction.
270 """
270 """
271 repo = ctx.repo()
271 repo = ctx.repo()
272 if oldnode:
272 if oldnode:
273 diffopts = mdiff.diffopts(git=True, context=1)
273 diffopts = mdiff.diffopts(git=True, context=1)
274 oldctx = repo.unfiltered()[oldnode]
274 oldctx = repo.unfiltered()[oldnode]
275 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
275 neednewdiff = (getdiff(ctx, diffopts) != getdiff(oldctx, diffopts))
276 else:
276 else:
277 neednewdiff = True
277 neednewdiff = True
278
278
279 transactions = []
279 transactions = []
280 if neednewdiff:
280 if neednewdiff:
281 diff = creatediff(ctx)
281 diff = creatediff(ctx)
282 transactions.append({'type': 'update', 'value': diff[r'phid']})
282 transactions.append({'type': 'update', 'value': diff[r'phid']})
283 else:
283 else:
284 # Even if we don't need to upload a new diff because the patch content
284 # Even if we don't need to upload a new diff because the patch content
285 # does not change. We might still need to update its metadata so
285 # does not change. We might still need to update its metadata so
286 # pushers could know the correct node metadata.
286 # pushers could know the correct node metadata.
287 assert olddiff
287 assert olddiff
288 diff = olddiff
288 diff = olddiff
289 writediffproperties(ctx, diff)
289 writediffproperties(ctx, diff)
290
290
291 # Use a temporary summary to set dependency. There might be better ways but
291 # Use a temporary summary to set dependency. There might be better ways but
292 # I cannot find them for now. But do not do that if we are updating an
292 # I cannot find them for now. But do not do that if we are updating an
293 # existing revision (revid is not None) since that introduces visible
293 # existing revision (revid is not None) since that introduces visible
294 # churns (someone edited "Summary" twice) on the web page.
294 # churns (someone edited "Summary" twice) on the web page.
295 if parentrevid and revid is None:
295 if parentrevid and revid is None:
296 summary = 'Depends on D%s' % parentrevid
296 summary = 'Depends on D%s' % parentrevid
297 transactions += [{'type': 'summary', 'value': summary},
297 transactions += [{'type': 'summary', 'value': summary},
298 {'type': 'summary', 'value': ' '}]
298 {'type': 'summary', 'value': ' '}]
299
299
300 if actions:
300 if actions:
301 transactions += actions
301 transactions += actions
302
302
303 # Parse commit message and update related fields.
303 # Parse commit message and update related fields.
304 desc = ctx.description()
304 desc = ctx.description()
305 info = callconduit(repo, 'differential.parsecommitmessage',
305 info = callconduit(repo, 'differential.parsecommitmessage',
306 {'corpus': desc})
306 {'corpus': desc})
307 for k, v in info[r'fields'].items():
307 for k, v in info[r'fields'].items():
308 if k in ['title', 'summary', 'testPlan']:
308 if k in ['title', 'summary', 'testPlan']:
309 transactions.append({'type': k, 'value': v})
309 transactions.append({'type': k, 'value': v})
310
310
311 params = {'transactions': transactions}
311 params = {'transactions': transactions}
312 if revid is not None:
312 if revid is not None:
313 # Update an existing Differential Revision
313 # Update an existing Differential Revision
314 params['objectIdentifier'] = revid
314 params['objectIdentifier'] = revid
315
315
316 revision = callconduit(repo, 'differential.revision.edit', params)
316 revision = callconduit(repo, 'differential.revision.edit', params)
317 if not revision:
317 if not revision:
318 raise error.Abort(_('cannot create revision for %s') % ctx)
318 raise error.Abort(_('cannot create revision for %s') % ctx)
319
319
320 return revision, diff
320 return revision, diff
321
321
322 def userphids(repo, names):
322 def userphids(repo, names):
323 """convert user names to PHIDs"""
323 """convert user names to PHIDs"""
324 query = {'constraints': {'usernames': names}}
324 query = {'constraints': {'usernames': names}}
325 result = callconduit(repo, 'user.search', query)
325 result = callconduit(repo, 'user.search', query)
326 # username not found is not an error of the API. So check if we have missed
326 # username not found is not an error of the API. So check if we have missed
327 # some names here.
327 # some names here.
328 data = result[r'data']
328 data = result[r'data']
329 resolved = set(entry[r'fields'][r'username'] for entry in data)
329 resolved = set(entry[r'fields'][r'username'] for entry in data)
330 unresolved = set(names) - resolved
330 unresolved = set(names) - resolved
331 if unresolved:
331 if unresolved:
332 raise error.Abort(_('unknown username: %s')
332 raise error.Abort(_('unknown username: %s')
333 % ' '.join(sorted(unresolved)))
333 % ' '.join(sorted(unresolved)))
334 return [entry[r'phid'] for entry in data]
334 return [entry[r'phid'] for entry in data]
335
335
336 @command('phabsend',
336 @command('phabsend',
337 [('r', 'rev', [], _('revisions to send'), _('REV')),
337 [('r', 'rev', [], _('revisions to send'), _('REV')),
338 ('', 'amend', False, _('update commit messages')),
338 ('', 'amend', False, _('update commit messages')),
339 ('', 'reviewer', [], _('specify reviewers')),
339 ('', 'reviewer', [], _('specify reviewers')),
340 ('', 'confirm', None, _('ask for confirmation before sending'))],
340 ('', 'confirm', None, _('ask for confirmation before sending'))],
341 _('REV [OPTIONS]'))
341 _('REV [OPTIONS]'))
342 def phabsend(ui, repo, *revs, **opts):
342 def phabsend(ui, repo, *revs, **opts):
343 """upload changesets to Phabricator
343 """upload changesets to Phabricator
344
344
345 If there are multiple revisions specified, they will be send as a stack
345 If there are multiple revisions specified, they will be send as a stack
346 with a linear dependencies relationship using the order specified by the
346 with a linear dependencies relationship using the order specified by the
347 revset.
347 revset.
348
348
349 For the first time uploading changesets, local tags will be created to
349 For the first time uploading changesets, local tags will be created to
350 maintain the association. After the first time, phabsend will check
350 maintain the association. After the first time, phabsend will check
351 obsstore and tags information so it can figure out whether to update an
351 obsstore and tags information so it can figure out whether to update an
352 existing Differential Revision, or create a new one.
352 existing Differential Revision, or create a new one.
353
353
354 If --amend is set, update commit messages so they have the
354 If --amend is set, update commit messages so they have the
355 ``Differential Revision`` URL, remove related tags. This is similar to what
355 ``Differential Revision`` URL, remove related tags. This is similar to what
356 arcanist will do, and is more desired in author-push workflows. Otherwise,
356 arcanist will do, and is more desired in author-push workflows. Otherwise,
357 use local tags to record the ``Differential Revision`` association.
357 use local tags to record the ``Differential Revision`` association.
358
358
359 The --confirm option lets you confirm changesets before sending them. You
359 The --confirm option lets you confirm changesets before sending them. You
360 can also add following to your configuration file to make it default
360 can also add following to your configuration file to make it default
361 behaviour.
361 behaviour.
362
362
363 [phabsend]
363 [phabsend]
364 confirm = true
364 confirm = true
365
365
366 phabsend will check obsstore and the above association to decide whether to
366 phabsend will check obsstore and the above association to decide whether to
367 update an existing Differential Revision, or create a new one.
367 update an existing Differential Revision, or create a new one.
368 """
368 """
369 revs = list(revs) + opts.get('rev', [])
369 revs = list(revs) + opts.get('rev', [])
370 revs = scmutil.revrange(repo, revs)
370 revs = scmutil.revrange(repo, revs)
371
371
372 if not revs:
372 if not revs:
373 raise error.Abort(_('phabsend requires at least one changeset'))
373 raise error.Abort(_('phabsend requires at least one changeset'))
374 if opts.get('amend'):
374 if opts.get('amend'):
375 cmdutil.checkunfinished(repo)
375 cmdutil.checkunfinished(repo)
376
376
377 confirm = ui.configbool('phabsend', 'confirm')
377 confirm = ui.configbool('phabsend', 'confirm')
378 confirm |= bool(opts.get('confirm'))
378 confirm |= bool(opts.get('confirm'))
379 if confirm:
379 if confirm:
380 confirmed = _confirmbeforesend(repo, revs)
380 confirmed = _confirmbeforesend(repo, revs)
381 if not confirmed:
381 if not confirmed:
382 raise error.Abort(_('phabsend cancelled'))
382 raise error.Abort(_('phabsend cancelled'))
383
383
384 actions = []
384 actions = []
385 reviewers = opts.get('reviewer', [])
385 reviewers = opts.get('reviewer', [])
386 if reviewers:
386 if reviewers:
387 phids = userphids(repo, reviewers)
387 phids = userphids(repo, reviewers)
388 actions.append({'type': 'reviewers.add', 'value': phids})
388 actions.append({'type': 'reviewers.add', 'value': phids})
389
389
390 # {newnode: (oldnode, olddiff, olddrev}
390 # {newnode: (oldnode, olddiff, olddrev}
391 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
391 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
392
392
393 drevids = [] # [int]
393 drevids = [] # [int]
394 diffmap = {} # {newnode: diff}
394 diffmap = {} # {newnode: diff}
395
395
396 # Send patches one by one so we know their Differential Revision IDs and
396 # Send patches one by one so we know their Differential Revision IDs and
397 # can provide dependency relationship
397 # can provide dependency relationship
398 lastrevid = None
398 lastrevid = None
399 for rev in revs:
399 for rev in revs:
400 ui.debug('sending rev %d\n' % rev)
400 ui.debug('sending rev %d\n' % rev)
401 ctx = repo[rev]
401 ctx = repo[rev]
402
402
403 # Get Differential Revision ID
403 # Get Differential Revision ID
404 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
404 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
405 if oldnode != ctx.node() or opts.get('amend'):
405 if oldnode != ctx.node() or opts.get('amend'):
406 # Create or update Differential Revision
406 # Create or update Differential Revision
407 revision, diff = createdifferentialrevision(
407 revision, diff = createdifferentialrevision(
408 ctx, revid, lastrevid, oldnode, olddiff, actions)
408 ctx, revid, lastrevid, oldnode, olddiff, actions)
409 diffmap[ctx.node()] = diff
409 diffmap[ctx.node()] = diff
410 newrevid = int(revision[r'object'][r'id'])
410 newrevid = int(revision[r'object'][r'id'])
411 if revid:
411 if revid:
412 action = _('updated')
412 action = _('updated')
413 else:
413 else:
414 action = _('created')
414 action = _('created')
415
415
416 # Create a local tag to note the association, if commit message
416 # Create a local tag to note the association, if commit message
417 # does not have it already
417 # does not have it already
418 m = _differentialrevisiondescre.search(ctx.description())
418 m = _differentialrevisiondescre.search(ctx.description())
419 if not m or int(m.group(1)) != newrevid:
419 if not m or int(m.group(1)) != newrevid:
420 tagname = 'D%d' % newrevid
420 tagname = 'D%d' % newrevid
421 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
421 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
422 date=None, local=True)
422 date=None, local=True)
423 else:
423 else:
424 # Nothing changed. But still set "newrevid" so the next revision
424 # Nothing changed. But still set "newrevid" so the next revision
425 # could depend on this one.
425 # could depend on this one.
426 newrevid = revid
426 newrevid = revid
427 action = _('skipped')
427 action = _('skipped')
428
428
429 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
429 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
430 ctx.description().split('\n')[0]))
430 ctx.description().split('\n')[0]))
431 drevids.append(newrevid)
431 drevids.append(newrevid)
432 lastrevid = newrevid
432 lastrevid = newrevid
433
433
434 # Update commit messages and remove tags
434 # Update commit messages and remove tags
435 if opts.get('amend'):
435 if opts.get('amend'):
436 unfi = repo.unfiltered()
436 unfi = repo.unfiltered()
437 drevs = callconduit(repo, 'differential.query', {'ids': drevids})
437 drevs = callconduit(repo, 'differential.query', {'ids': drevids})
438 with repo.wlock(), repo.lock(), repo.transaction('phabsend'):
438 with repo.wlock(), repo.lock(), repo.transaction('phabsend'):
439 wnode = unfi['.'].node()
439 wnode = unfi['.'].node()
440 mapping = {} # {oldnode: [newnode]}
440 mapping = {} # {oldnode: [newnode]}
441 for i, rev in enumerate(revs):
441 for i, rev in enumerate(revs):
442 old = unfi[rev]
442 old = unfi[rev]
443 drevid = drevids[i]
443 drevid = drevids[i]
444 drev = [d for d in drevs if int(d[r'id']) == drevid][0]
444 drev = [d for d in drevs if int(d[r'id']) == drevid][0]
445 newdesc = getdescfromdrev(drev)
445 newdesc = getdescfromdrev(drev)
446 # Make sure commit message contain "Differential Revision"
446 # Make sure commit message contain "Differential Revision"
447 if old.description() != newdesc:
447 if old.description() != newdesc:
448 parents = [
448 parents = [
449 mapping.get(old.p1().node(), (old.p1(),))[0],
449 mapping.get(old.p1().node(), (old.p1(),))[0],
450 mapping.get(old.p2().node(), (old.p2(),))[0],
450 mapping.get(old.p2().node(), (old.p2(),))[0],
451 ]
451 ]
452 new = context.metadataonlyctx(
452 new = context.metadataonlyctx(
453 repo, old, parents=parents, text=newdesc,
453 repo, old, parents=parents, text=newdesc,
454 user=old.user(), date=old.date(), extra=old.extra())
454 user=old.user(), date=old.date(), extra=old.extra())
455 newnode = new.commit()
455 newnode = new.commit()
456 mapping[old.node()] = [newnode]
456 mapping[old.node()] = [newnode]
457 # Update diff property
457 # Update diff property
458 writediffproperties(unfi[newnode], diffmap[old.node()])
458 writediffproperties(unfi[newnode], diffmap[old.node()])
459 # Remove local tags since it's no longer necessary
459 # Remove local tags since it's no longer necessary
460 tagname = 'D%d' % drevid
460 tagname = 'D%d' % drevid
461 if tagname in repo.tags():
461 if tagname in repo.tags():
462 tags.tag(repo, tagname, nullid, message=None, user=None,
462 tags.tag(repo, tagname, nullid, message=None, user=None,
463 date=None, local=True)
463 date=None, local=True)
464 scmutil.cleanupnodes(repo, mapping, 'phabsend')
464 scmutil.cleanupnodes(repo, mapping, 'phabsend')
465 if wnode in mapping:
465 if wnode in mapping:
466 unfi.setparents(mapping[wnode][0])
466 unfi.setparents(mapping[wnode][0])
467
467
468 # Map from "hg:meta" keys to header understood by "hg import". The order is
468 # Map from "hg:meta" keys to header understood by "hg import". The order is
469 # consistent with "hg export" output.
469 # consistent with "hg export" output.
470 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
470 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
471 (r'node', 'Node ID'), (r'parent', 'Parent ')])
471 (r'node', 'Node ID'), (r'parent', 'Parent ')])
472
472
473 def _confirmbeforesend(repo, revs):
473 def _confirmbeforesend(repo, revs):
474 ui = repo.ui
474 ui = repo.ui
475 for rev in revs:
475 for rev in revs:
476 ctx = repo[rev]
476 ctx = repo[rev]
477 desc = ctx.description().splitlines()[0]
477 desc = ctx.description().splitlines()[0]
478 ui.write(('%d: ' % rev), label='phabsend.revnumber')
478 ui.write(('%d: ' % rev), label='phabsend.revnumber')
479 ui.write(('%s\n' % desc), label='phabsend.desc')
479 ui.write(('%s\n' % desc), label='phabsend.desc')
480
480
481 if ui.promptchoice(_('Phabsend the above changes (yn)?'
481 if ui.promptchoice(_('Phabsend the above changes (yn)?'
482 '$$ &Yes $$ &No')):
482 '$$ &Yes $$ &No')):
483 return False
483 return False
484
484
485 return True
485 return True
486
486
487 def querydrev(repo, params, stack=False):
487 def querydrev(repo, params, stack=False):
488 """return a list of "Differential Revision" dicts
488 """return a list of "Differential Revision" dicts
489
489
490 params is the input of "differential.query" API, and is expected to match
490 params is the input of "differential.query" API, and is expected to match
491 just a single Differential Revision.
491 just a single Differential Revision.
492
492
493 A "Differential Revision dict" looks like:
493 A "Differential Revision dict" looks like:
494
494
495 {
495 {
496 "id": "2",
496 "id": "2",
497 "phid": "PHID-DREV-672qvysjcczopag46qty",
497 "phid": "PHID-DREV-672qvysjcczopag46qty",
498 "title": "example",
498 "title": "example",
499 "uri": "https://phab.example.com/D2",
499 "uri": "https://phab.example.com/D2",
500 "dateCreated": "1499181406",
500 "dateCreated": "1499181406",
501 "dateModified": "1499182103",
501 "dateModified": "1499182103",
502 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
502 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
503 "status": "0",
503 "status": "0",
504 "statusName": "Needs Review",
504 "statusName": "Needs Review",
505 "properties": [],
505 "properties": [],
506 "branch": null,
506 "branch": null,
507 "summary": "",
507 "summary": "",
508 "testPlan": "",
508 "testPlan": "",
509 "lineCount": "2",
509 "lineCount": "2",
510 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
510 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
511 "diffs": [
511 "diffs": [
512 "3",
512 "3",
513 "4",
513 "4",
514 ],
514 ],
515 "commits": [],
515 "commits": [],
516 "reviewers": [],
516 "reviewers": [],
517 "ccs": [],
517 "ccs": [],
518 "hashes": [],
518 "hashes": [],
519 "auxiliary": {
519 "auxiliary": {
520 "phabricator:projects": [],
520 "phabricator:projects": [],
521 "phabricator:depends-on": [
521 "phabricator:depends-on": [
522 "PHID-DREV-gbapp366kutjebt7agcd"
522 "PHID-DREV-gbapp366kutjebt7agcd"
523 ]
523 ]
524 },
524 },
525 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
525 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
526 "sourcePath": null
526 "sourcePath": null
527 }
527 }
528
528
529 If stack is True, return a list of "Differential Revision dict"s in an
529 If stack is True, return a list of "Differential Revision dict"s in an
530 order that the latter ones depend on the former ones. Otherwise, return a
530 order that the latter ones depend on the former ones. Otherwise, return a
531 list of a unique "Differential Revision dict".
531 list of a unique "Differential Revision dict".
532 """
532 """
533 prefetched = {} # {id or phid: drev}
533 prefetched = {} # {id or phid: drev}
534 def fetch(params):
534 def fetch(params):
535 """params -> single drev or None"""
535 """params -> single drev or None"""
536 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
536 key = (params.get(r'ids') or params.get(r'phids') or [None])[0]
537 if key in prefetched:
537 if key in prefetched:
538 return prefetched[key]
538 return prefetched[key]
539 # Otherwise, send the request. If we're fetching a stack, be smarter
539 # Otherwise, send the request. If we're fetching a stack, be smarter
540 # and fetch more ids in one batch, even if it could be unnecessary.
540 # and fetch more ids in one batch, even if it could be unnecessary.
541 batchparams = params
541 batchparams = params
542 if stack and len(params.get(r'ids', [])) == 1:
542 if stack and len(params.get(r'ids', [])) == 1:
543 i = int(params[r'ids'][0])
543 i = int(params[r'ids'][0])
544 # developer config: phabricator.batchsize
544 # developer config: phabricator.batchsize
545 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
545 batchsize = repo.ui.configint('phabricator', 'batchsize', 12)
546 batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
546 batchparams = {'ids': range(max(1, i - batchsize), i + 1)}
547 drevs = callconduit(repo, 'differential.query', batchparams)
547 drevs = callconduit(repo, 'differential.query', batchparams)
548 # Fill prefetched with the result
548 # Fill prefetched with the result
549 for drev in drevs:
549 for drev in drevs:
550 prefetched[drev[r'phid']] = drev
550 prefetched[drev[r'phid']] = drev
551 prefetched[int(drev[r'id'])] = drev
551 prefetched[int(drev[r'id'])] = drev
552 if key not in prefetched:
552 if key not in prefetched:
553 raise error.Abort(_('cannot get Differential Revision %r') % params)
553 raise error.Abort(_('cannot get Differential Revision %r') % params)
554 return prefetched[key]
554 return prefetched[key]
555
555
556 visited = set()
556 visited = set()
557 result = []
557 result = []
558 queue = [params]
558 queue = [params]
559 while queue:
559 while queue:
560 params = queue.pop()
560 params = queue.pop()
561 drev = fetch(params)
561 drev = fetch(params)
562 if drev[r'id'] in visited:
562 if drev[r'id'] in visited:
563 continue
563 continue
564 visited.add(drev[r'id'])
564 visited.add(drev[r'id'])
565 result.append(drev)
565 result.append(drev)
566 if stack:
566 if stack:
567 auxiliary = drev.get(r'auxiliary', {})
567 auxiliary = drev.get(r'auxiliary', {})
568 depends = auxiliary.get(r'phabricator:depends-on', [])
568 depends = auxiliary.get(r'phabricator:depends-on', [])
569 for phid in depends:
569 for phid in depends:
570 queue.append({'phids': [phid]})
570 queue.append({'phids': [phid]})
571 result.reverse()
571 result.reverse()
572 return result
572 return result
573
573
574 def getdescfromdrev(drev):
574 def getdescfromdrev(drev):
575 """get description (commit message) from "Differential Revision"
575 """get description (commit message) from "Differential Revision"
576
576
577 This is similar to differential.getcommitmessage API. But we only care
577 This is similar to differential.getcommitmessage API. But we only care
578 about limited fields: title, summary, test plan, and URL.
578 about limited fields: title, summary, test plan, and URL.
579 """
579 """
580 title = drev[r'title']
580 title = drev[r'title']
581 summary = drev[r'summary'].rstrip()
581 summary = drev[r'summary'].rstrip()
582 testplan = drev[r'testPlan'].rstrip()
582 testplan = drev[r'testPlan'].rstrip()
583 if testplan:
583 if testplan:
584 testplan = 'Test Plan:\n%s' % testplan
584 testplan = 'Test Plan:\n%s' % testplan
585 uri = 'Differential Revision: %s' % drev[r'uri']
585 uri = 'Differential Revision: %s' % drev[r'uri']
586 return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
586 return '\n\n'.join(filter(None, [title, summary, testplan, uri]))
587
587
588 def getdiffmeta(diff):
588 def getdiffmeta(diff):
589 """get commit metadata (date, node, user, p1) from a diff object
589 """get commit metadata (date, node, user, p1) from a diff object
590
590
591 The metadata could be "hg:meta", sent by phabsend, like:
591 The metadata could be "hg:meta", sent by phabsend, like:
592
592
593 "properties": {
593 "properties": {
594 "hg:meta": {
594 "hg:meta": {
595 "date": "1499571514 25200",
595 "date": "1499571514 25200",
596 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
596 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
597 "user": "Foo Bar <foo@example.com>",
597 "user": "Foo Bar <foo@example.com>",
598 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
598 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
599 }
599 }
600 }
600 }
601
601
602 Or converted from "local:commits", sent by "arc", like:
602 Or converted from "local:commits", sent by "arc", like:
603
603
604 "properties": {
604 "properties": {
605 "local:commits": {
605 "local:commits": {
606 "98c08acae292b2faf60a279b4189beb6cff1414d": {
606 "98c08acae292b2faf60a279b4189beb6cff1414d": {
607 "author": "Foo Bar",
607 "author": "Foo Bar",
608 "time": 1499546314,
608 "time": 1499546314,
609 "branch": "default",
609 "branch": "default",
610 "tag": "",
610 "tag": "",
611 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
611 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
612 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
612 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
613 "local": "1000",
613 "local": "1000",
614 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
614 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
615 "summary": "...",
615 "summary": "...",
616 "message": "...",
616 "message": "...",
617 "authorEmail": "foo@example.com"
617 "authorEmail": "foo@example.com"
618 }
618 }
619 }
619 }
620 }
620 }
621
621
622 Note: metadata extracted from "local:commits" will lose time zone
622 Note: metadata extracted from "local:commits" will lose time zone
623 information.
623 information.
624 """
624 """
625 props = diff.get(r'properties') or {}
625 props = diff.get(r'properties') or {}
626 meta = props.get(r'hg:meta')
626 meta = props.get(r'hg:meta')
627 if not meta and props.get(r'local:commits'):
627 if not meta and props.get(r'local:commits'):
628 commit = sorted(props[r'local:commits'].values())[0]
628 commit = sorted(props[r'local:commits'].values())[0]
629 meta = {
629 meta = {
630 r'date': r'%d 0' % commit[r'time'],
630 r'date': r'%d 0' % commit[r'time'],
631 r'node': commit[r'rev'],
631 r'node': commit[r'rev'],
632 r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
632 r'user': r'%s <%s>' % (commit[r'author'], commit[r'authorEmail']),
633 }
633 }
634 if len(commit.get(r'parents', ())) >= 1:
634 if len(commit.get(r'parents', ())) >= 1:
635 meta[r'parent'] = commit[r'parents'][0]
635 meta[r'parent'] = commit[r'parents'][0]
636 return meta or {}
636 return meta or {}
637
637
638 def readpatch(repo, params, write, stack=False):
638 def readpatch(repo, drevs, write):
639 """generate plain-text patch readable by 'hg import'
639 """generate plain-text patch readable by 'hg import'
640
640
641 write is usually ui.write. params is passed to "differential.query". If
641 write is usually ui.write. drevs is what "querydrev" returns, results of
642 stack is True, also write dependent patches.
642 "differential.query".
643 """
643 """
644 # Differential Revisions
645 drevs = querydrev(repo, params, stack)
646
647 # Prefetch hg:meta property for all diffs
644 # Prefetch hg:meta property for all diffs
648 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
645 diffids = sorted(set(max(int(v) for v in drev[r'diffs']) for drev in drevs))
649 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
646 diffs = callconduit(repo, 'differential.querydiffs', {'ids': diffids})
650
647
651 # Generate patch for each drev
648 # Generate patch for each drev
652 for drev in drevs:
649 for drev in drevs:
653 repo.ui.note(_('reading D%s\n') % drev[r'id'])
650 repo.ui.note(_('reading D%s\n') % drev[r'id'])
654
651
655 diffid = max(int(v) for v in drev[r'diffs'])
652 diffid = max(int(v) for v in drev[r'diffs'])
656 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
653 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
657 desc = getdescfromdrev(drev)
654 desc = getdescfromdrev(drev)
658 header = '# HG changeset patch\n'
655 header = '# HG changeset patch\n'
659
656
660 # Try to preserve metadata from hg:meta property. Write hg patch
657 # Try to preserve metadata from hg:meta property. Write hg patch
661 # headers that can be read by the "import" command. See patchheadermap
658 # headers that can be read by the "import" command. See patchheadermap
662 # and extract in mercurial/patch.py for supported headers.
659 # and extract in mercurial/patch.py for supported headers.
663 meta = getdiffmeta(diffs[str(diffid)])
660 meta = getdiffmeta(diffs[str(diffid)])
664 for k in _metanamemap.keys():
661 for k in _metanamemap.keys():
665 if k in meta:
662 if k in meta:
666 header += '# %s %s\n' % (_metanamemap[k], meta[k])
663 header += '# %s %s\n' % (_metanamemap[k], meta[k])
667
664
668 content = '%s%s\n%s' % (header, desc, body)
665 content = '%s%s\n%s' % (header, desc, body)
669 write(encoding.unitolocal(content))
666 write(encoding.unitolocal(content))
670
667
671 @command('phabread',
668 @command('phabread',
672 [('', 'stack', False, _('read dependencies'))],
669 [('', 'stack', False, _('read dependencies'))],
673 _('REVID [OPTIONS]'))
670 _('REVID [OPTIONS]'))
674 def phabread(ui, repo, revid, **opts):
671 def phabread(ui, repo, revid, **opts):
675 """print patches from Phabricator suitable for importing
672 """print patches from Phabricator suitable for importing
676
673
677 REVID could be a Differential Revision identity, like ``D123``, or just the
674 REVID could be a Differential Revision identity, like ``D123``, or just the
678 number ``123``, or a full URL like ``https://phab.example.com/D123``.
675 number ``123``, or a full URL like ``https://phab.example.com/D123``.
679
676
680 If --stack is given, follow dependencies information and read all patches.
677 If --stack is given, follow dependencies information and read all patches.
681 """
678 """
682 try:
679 try:
683 revid = int(revid.split('/')[-1].replace('D', ''))
680 revid = int(revid.split('/')[-1].replace('D', ''))
684 except ValueError:
681 except ValueError:
685 raise error.Abort(_('invalid Revision ID: %s') % revid)
682 raise error.Abort(_('invalid Revision ID: %s') % revid)
686 readpatch(repo, {'ids': [revid]}, ui.write, opts.get('stack'))
683 drevs = querydrev(repo, {'ids': [revid]}, opts.get('stack'))
684 readpatch(repo, drevs, ui.write)
General Comments 0
You need to be logged in to leave comments. Login now