##// END OF EJS Templates
phabricator: add node and p1 to hg:meta property...
Jun Wu -
r33264:26632157 default
parent child Browse files
Show More
@@ -1,353 +1,363 b''
1 # phabricator.py - simple Phabricator integration
1 # phabricator.py - simple Phabricator integration
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """simple Phabricator integration
7 """simple Phabricator integration
8
8
9 This extension provides a ``phabsend`` command which sends a stack of
9 This extension provides a ``phabsend`` command which sends a stack of
10 changesets to Phabricator without amending commit messages, and a ``phabread``
10 changesets to Phabricator without amending commit messages, and a ``phabread``
11 command which prints a stack of revisions in a format suitable
11 command which prints a stack of revisions in a format suitable
12 for :hg:`import`.
12 for :hg:`import`.
13
13
14 By default, Phabricator requires ``Test Plan`` which might prevent some
14 By default, Phabricator requires ``Test Plan`` which might prevent some
15 changeset from being sent. The requirement could be disabled by changing
15 changeset from being sent. The requirement could be disabled by changing
16 ``differential.require-test-plan-field`` config server side.
16 ``differential.require-test-plan-field`` config server side.
17
17
18 Config::
18 Config::
19
19
20 [phabricator]
20 [phabricator]
21 # Phabricator URL
21 # Phabricator URL
22 url = https://phab.example.com/
22 url = https://phab.example.com/
23
23
24 # API token. Get it from https://$HOST/conduit/login/
24 # API token. Get it from https://$HOST/conduit/login/
25 token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
25 token = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
26
26
27 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
27 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
28 # callsign is "FOO".
28 # callsign is "FOO".
29 callsign = FOO
29 callsign = FOO
30
30
31 """
31 """
32
32
33 from __future__ import absolute_import
33 from __future__ import absolute_import
34
34
35 import json
35 import json
36 import re
36 import re
37
37
38 from mercurial.i18n import _
38 from mercurial.i18n import _
39 from mercurial import (
39 from mercurial import (
40 encoding,
40 encoding,
41 error,
41 error,
42 mdiff,
42 mdiff,
43 obsolete,
43 obsolete,
44 patch,
44 patch,
45 registrar,
45 registrar,
46 scmutil,
46 scmutil,
47 tags,
47 tags,
48 url as urlmod,
48 url as urlmod,
49 util,
49 util,
50 )
50 )
51
51
52 cmdtable = {}
52 cmdtable = {}
53 command = registrar.command(cmdtable)
53 command = registrar.command(cmdtable)
54
54
55 def urlencodenested(params):
55 def urlencodenested(params):
56 """like urlencode, but works with nested parameters.
56 """like urlencode, but works with nested parameters.
57
57
58 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
58 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
59 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
59 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
60 urlencode. Note: the encoding is consistent with PHP's http_build_query.
60 urlencode. Note: the encoding is consistent with PHP's http_build_query.
61 """
61 """
62 flatparams = util.sortdict()
62 flatparams = util.sortdict()
63 def process(prefix, obj):
63 def process(prefix, obj):
64 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
64 items = {list: enumerate, dict: lambda x: x.items()}.get(type(obj))
65 if items is None:
65 if items is None:
66 flatparams[prefix] = obj
66 flatparams[prefix] = obj
67 else:
67 else:
68 for k, v in items(obj):
68 for k, v in items(obj):
69 if prefix:
69 if prefix:
70 process('%s[%s]' % (prefix, k), v)
70 process('%s[%s]' % (prefix, k), v)
71 else:
71 else:
72 process(k, v)
72 process(k, v)
73 process('', params)
73 process('', params)
74 return util.urlreq.urlencode(flatparams)
74 return util.urlreq.urlencode(flatparams)
75
75
76 def readurltoken(repo):
76 def readurltoken(repo):
77 """return conduit url, token and make sure they exist
77 """return conduit url, token and make sure they exist
78
78
79 Currently read from [phabricator] config section. In the future, it might
79 Currently read from [phabricator] config section. In the future, it might
80 make sense to read from .arcconfig and .arcrc as well.
80 make sense to read from .arcconfig and .arcrc as well.
81 """
81 """
82 values = []
82 values = []
83 section = 'phabricator'
83 section = 'phabricator'
84 for name in ['url', 'token']:
84 for name in ['url', 'token']:
85 value = repo.ui.config(section, name)
85 value = repo.ui.config(section, name)
86 if not value:
86 if not value:
87 raise error.Abort(_('config %s.%s is required') % (section, name))
87 raise error.Abort(_('config %s.%s is required') % (section, name))
88 values.append(value)
88 values.append(value)
89 return values
89 return values
90
90
91 def callconduit(repo, name, params):
91 def callconduit(repo, name, params):
92 """call Conduit API, params is a dict. return json.loads result, or None"""
92 """call Conduit API, params is a dict. return json.loads result, or None"""
93 host, token = readurltoken(repo)
93 host, token = readurltoken(repo)
94 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
94 url, authinfo = util.url('/'.join([host, 'api', name])).authinfo()
95 urlopener = urlmod.opener(repo.ui, authinfo)
95 urlopener = urlmod.opener(repo.ui, authinfo)
96 repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
96 repo.ui.debug('Conduit Call: %s %s\n' % (url, params))
97 params = params.copy()
97 params = params.copy()
98 params['api.token'] = token
98 params['api.token'] = token
99 request = util.urlreq.request(url, data=urlencodenested(params))
99 request = util.urlreq.request(url, data=urlencodenested(params))
100 body = urlopener.open(request).read()
100 body = urlopener.open(request).read()
101 repo.ui.debug('Conduit Response: %s\n' % body)
101 repo.ui.debug('Conduit Response: %s\n' % body)
102 parsed = json.loads(body)
102 parsed = json.loads(body)
103 if parsed.get(r'error_code'):
103 if parsed.get(r'error_code'):
104 msg = (_('Conduit Error (%s): %s')
104 msg = (_('Conduit Error (%s): %s')
105 % (parsed[r'error_code'], parsed[r'error_info']))
105 % (parsed[r'error_code'], parsed[r'error_info']))
106 raise error.Abort(msg)
106 raise error.Abort(msg)
107 return parsed[r'result']
107 return parsed[r'result']
108
108
109 @command('debugcallconduit', [], _('METHOD'))
109 @command('debugcallconduit', [], _('METHOD'))
110 def debugcallconduit(ui, repo, name):
110 def debugcallconduit(ui, repo, name):
111 """call Conduit API
111 """call Conduit API
112
112
113 Call parameters are read from stdin as a JSON blob. Result will be written
113 Call parameters are read from stdin as a JSON blob. Result will be written
114 to stdout as a JSON blob.
114 to stdout as a JSON blob.
115 """
115 """
116 params = json.loads(ui.fin.read())
116 params = json.loads(ui.fin.read())
117 result = callconduit(repo, name, params)
117 result = callconduit(repo, name, params)
118 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
118 s = json.dumps(result, sort_keys=True, indent=2, separators=(',', ': '))
119 ui.write('%s\n' % s)
119 ui.write('%s\n' % s)
120
120
121 def getrepophid(repo):
121 def getrepophid(repo):
122 """given callsign, return repository PHID or None"""
122 """given callsign, return repository PHID or None"""
123 # developer config: phabricator.repophid
123 # developer config: phabricator.repophid
124 repophid = repo.ui.config('phabricator', 'repophid')
124 repophid = repo.ui.config('phabricator', 'repophid')
125 if repophid:
125 if repophid:
126 return repophid
126 return repophid
127 callsign = repo.ui.config('phabricator', 'callsign')
127 callsign = repo.ui.config('phabricator', 'callsign')
128 if not callsign:
128 if not callsign:
129 return None
129 return None
130 query = callconduit(repo, 'diffusion.repository.search',
130 query = callconduit(repo, 'diffusion.repository.search',
131 {'constraints': {'callsigns': [callsign]}})
131 {'constraints': {'callsigns': [callsign]}})
132 if len(query[r'data']) == 0:
132 if len(query[r'data']) == 0:
133 return None
133 return None
134 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
134 repophid = encoding.strtolocal(query[r'data'][0][r'phid'])
135 repo.ui.setconfig('phabricator', 'repophid', repophid)
135 repo.ui.setconfig('phabricator', 'repophid', repophid)
136 return repophid
136 return repophid
137
137
138 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
138 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
139 _differentialrevisiondescre = re.compile(
139 _differentialrevisiondescre = re.compile(
140 '^Differential Revision:.*D([1-9][0-9]*)$', re.M)
140 '^Differential Revision:.*D([1-9][0-9]*)$', re.M)
141
141
142 def getmapping(ctx):
142 def getmapping(ctx):
143 """return (node, associated Differential Revision ID) or (None, None)
143 """return (node, associated Differential Revision ID) or (None, None)
144
144
145 Examines all precursors and their tags. Tags with format like "D1234" are
145 Examines all precursors and their tags. Tags with format like "D1234" are
146 considered a match and the node with that tag, and the number after "D"
146 considered a match and the node with that tag, and the number after "D"
147 (ex. 1234) will be returned.
147 (ex. 1234) will be returned.
148
148
149 If tags are not found, examine commit message. The "Differential Revision:"
149 If tags are not found, examine commit message. The "Differential Revision:"
150 line could associate this changeset to a Differential Revision.
150 line could associate this changeset to a Differential Revision.
151 """
151 """
152 unfi = ctx.repo().unfiltered()
152 unfi = ctx.repo().unfiltered()
153 nodemap = unfi.changelog.nodemap
153 nodemap = unfi.changelog.nodemap
154
154
155 # Check tags like "D123"
155 # Check tags like "D123"
156 for n in obsolete.allprecursors(unfi.obsstore, [ctx.node()]):
156 for n in obsolete.allprecursors(unfi.obsstore, [ctx.node()]):
157 if n in nodemap:
157 if n in nodemap:
158 for tag in unfi.nodetags(n):
158 for tag in unfi.nodetags(n):
159 m = _differentialrevisiontagre.match(tag)
159 m = _differentialrevisiontagre.match(tag)
160 if m:
160 if m:
161 return n, int(m.group(1))
161 return n, int(m.group(1))
162
162
163 # Check commit message
163 # Check commit message
164 m = _differentialrevisiondescre.search(ctx.description())
164 m = _differentialrevisiondescre.search(ctx.description())
165 if m:
165 if m:
166 return None, int(m.group(1))
166 return None, int(m.group(1))
167
167
168 return None, None
168 return None, None
169
169
170 def getdiff(ctx, diffopts):
170 def getdiff(ctx, diffopts):
171 """plain-text diff without header (user, commit message, etc)"""
171 """plain-text diff without header (user, commit message, etc)"""
172 output = util.stringio()
172 output = util.stringio()
173 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
173 for chunk, _label in patch.diffui(ctx.repo(), ctx.p1().node(), ctx.node(),
174 None, opts=diffopts):
174 None, opts=diffopts):
175 output.write(chunk)
175 output.write(chunk)
176 return output.getvalue()
176 return output.getvalue()
177
177
178 def creatediff(ctx):
178 def creatediff(ctx):
179 """create a Differential Diff"""
179 """create a Differential Diff"""
180 repo = ctx.repo()
180 repo = ctx.repo()
181 repophid = getrepophid(repo)
181 repophid = getrepophid(repo)
182 # Create a "Differential Diff" via "differential.createrawdiff" API
182 # Create a "Differential Diff" via "differential.createrawdiff" API
183 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
183 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
184 if repophid:
184 if repophid:
185 params['repositoryPHID'] = repophid
185 params['repositoryPHID'] = repophid
186 diff = callconduit(repo, 'differential.createrawdiff', params)
186 diff = callconduit(repo, 'differential.createrawdiff', params)
187 if not diff:
187 if not diff:
188 raise error.Abort(_('cannot create diff for %s') % ctx)
188 raise error.Abort(_('cannot create diff for %s') % ctx)
189 return diff
189 return diff
190
190
191 def writediffproperties(ctx, diff):
191 def writediffproperties(ctx, diff):
192 """write metadata to diff so patches could be applied losslessly"""
192 """write metadata to diff so patches could be applied losslessly"""
193 params = {
193 params = {
194 'diff_id': diff[r'id'],
194 'diff_id': diff[r'id'],
195 'name': 'hg:meta',
195 'name': 'hg:meta',
196 'data': json.dumps({
196 'data': json.dumps({
197 'user': ctx.user(),
197 'user': ctx.user(),
198 'date': '%d %d' % ctx.date(),
198 'date': '%d %d' % ctx.date(),
199 'node': ctx.hex(),
200 'parent': ctx.p1().hex(),
199 }),
201 }),
200 }
202 }
201 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
203 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
202
204
203 def createdifferentialrevision(ctx, revid=None, parentrevid=None):
205 def createdifferentialrevision(ctx, revid=None, parentrevid=None):
204 """create or update a Differential Revision
206 """create or update a Differential Revision
205
207
206 If revid is None, create a new Differential Revision, otherwise update
208 If revid is None, create a new Differential Revision, otherwise update
207 revid. If parentrevid is not None, set it as a dependency.
209 revid. If parentrevid is not None, set it as a dependency.
208 """
210 """
209 repo = ctx.repo()
211 repo = ctx.repo()
210 diff = creatediff(ctx)
212 diff = creatediff(ctx)
211 writediffproperties(ctx, diff)
213 writediffproperties(ctx, diff)
212
214
213 transactions = [{'type': 'update', 'value': diff[r'phid']}]
215 transactions = [{'type': 'update', 'value': diff[r'phid']}]
214
216
215 # Use a temporary summary to set dependency. There might be better ways but
217 # Use a temporary summary to set dependency. There might be better ways but
216 # I cannot find them for now. But do not do that if we are updating an
218 # I cannot find them for now. But do not do that if we are updating an
217 # existing revision (revid is not None) since that introduces visible
219 # existing revision (revid is not None) since that introduces visible
218 # churns (someone edited "Summary" twice) on the web page.
220 # churns (someone edited "Summary" twice) on the web page.
219 if parentrevid and revid is None:
221 if parentrevid and revid is None:
220 summary = 'Depends on D%s' % parentrevid
222 summary = 'Depends on D%s' % parentrevid
221 transactions += [{'type': 'summary', 'value': summary},
223 transactions += [{'type': 'summary', 'value': summary},
222 {'type': 'summary', 'value': ' '}]
224 {'type': 'summary', 'value': ' '}]
223
225
224 # Parse commit message and update related fields.
226 # Parse commit message and update related fields.
225 desc = ctx.description()
227 desc = ctx.description()
226 info = callconduit(repo, 'differential.parsecommitmessage',
228 info = callconduit(repo, 'differential.parsecommitmessage',
227 {'corpus': desc})
229 {'corpus': desc})
228 for k, v in info[r'fields'].items():
230 for k, v in info[r'fields'].items():
229 if k in ['title', 'summary', 'testPlan']:
231 if k in ['title', 'summary', 'testPlan']:
230 transactions.append({'type': k, 'value': v})
232 transactions.append({'type': k, 'value': v})
231
233
232 params = {'transactions': transactions}
234 params = {'transactions': transactions}
233 if revid is not None:
235 if revid is not None:
234 # Update an existing Differential Revision
236 # Update an existing Differential Revision
235 params['objectIdentifier'] = revid
237 params['objectIdentifier'] = revid
236
238
237 revision = callconduit(repo, 'differential.revision.edit', params)
239 revision = callconduit(repo, 'differential.revision.edit', params)
238 if not revision:
240 if not revision:
239 raise error.Abort(_('cannot create revision for %s') % ctx)
241 raise error.Abort(_('cannot create revision for %s') % ctx)
240
242
241 return revision
243 return revision
242
244
243 @command('phabsend',
245 @command('phabsend',
244 [('r', 'rev', [], _('revisions to send'), _('REV'))],
246 [('r', 'rev', [], _('revisions to send'), _('REV'))],
245 _('REV [OPTIONS]'))
247 _('REV [OPTIONS]'))
246 def phabsend(ui, repo, *revs, **opts):
248 def phabsend(ui, repo, *revs, **opts):
247 """upload changesets to Phabricator
249 """upload changesets to Phabricator
248
250
249 If there are multiple revisions specified, they will be send as a stack
251 If there are multiple revisions specified, they will be send as a stack
250 with a linear dependencies relationship using the order specified by the
252 with a linear dependencies relationship using the order specified by the
251 revset.
253 revset.
252
254
253 For the first time uploading changesets, local tags will be created to
255 For the first time uploading changesets, local tags will be created to
254 maintain the association. After the first time, phabsend will check
256 maintain the association. After the first time, phabsend will check
255 obsstore and tags information so it can figure out whether to update an
257 obsstore and tags information so it can figure out whether to update an
256 existing Differential Revision, or create a new one.
258 existing Differential Revision, or create a new one.
257 """
259 """
258 revs = list(revs) + opts.get('rev', [])
260 revs = list(revs) + opts.get('rev', [])
259 revs = scmutil.revrange(repo, revs)
261 revs = scmutil.revrange(repo, revs)
260
262
261 # Send patches one by one so we know their Differential Revision IDs and
263 # Send patches one by one so we know their Differential Revision IDs and
262 # can provide dependency relationship
264 # can provide dependency relationship
263 lastrevid = None
265 lastrevid = None
264 for rev in revs:
266 for rev in revs:
265 ui.debug('sending rev %d\n' % rev)
267 ui.debug('sending rev %d\n' % rev)
266 ctx = repo[rev]
268 ctx = repo[rev]
267
269
268 # Get Differential Revision ID
270 # Get Differential Revision ID
269 oldnode, revid = getmapping(ctx)
271 oldnode, revid = getmapping(ctx)
270 if oldnode != ctx.node():
272 if oldnode != ctx.node():
271 # Create or update Differential Revision
273 # Create or update Differential Revision
272 revision = createdifferentialrevision(ctx, revid, lastrevid)
274 revision = createdifferentialrevision(ctx, revid, lastrevid)
273 newrevid = int(revision[r'object'][r'id'])
275 newrevid = int(revision[r'object'][r'id'])
274 if revid:
276 if revid:
275 action = _('updated')
277 action = _('updated')
276 else:
278 else:
277 action = _('created')
279 action = _('created')
278
280
279 # Create a local tag to note the association
281 # Create a local tag to note the association
280 tagname = 'D%d' % newrevid
282 tagname = 'D%d' % newrevid
281 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
283 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
282 date=None, local=True)
284 date=None, local=True)
283 else:
285 else:
284 # Nothing changed. But still set "newrevid" so the next revision
286 # Nothing changed. But still set "newrevid" so the next revision
285 # could depend on this one.
287 # could depend on this one.
286 newrevid = revid
288 newrevid = revid
287 action = _('skipped')
289 action = _('skipped')
288
290
289 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
291 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
290 ctx.description().split('\n')[0]))
292 ctx.description().split('\n')[0]))
291 lastrevid = newrevid
293 lastrevid = newrevid
292
294
293 _summaryre = re.compile('^Summary:\s*', re.M)
295 _summaryre = re.compile('^Summary:\s*', re.M)
294
296
297 # Map from "hg:meta" keys to header understood by "hg import". The order is
298 # consistent with "hg export" output.
299 _metanamemap = util.sortdict([(r'user', 'User'), (r'date', 'Date'),
300 (r'node', 'Node ID'), (r'parent', 'Parent ')])
301
295 def readpatch(repo, params, recursive=False):
302 def readpatch(repo, params, recursive=False):
296 """generate plain-text patch readable by 'hg import'
303 """generate plain-text patch readable by 'hg import'
297
304
298 params is passed to "differential.query". If recursive is True, also return
305 params is passed to "differential.query". If recursive is True, also return
299 dependent patches.
306 dependent patches.
300 """
307 """
301 # Differential Revisions
308 # Differential Revisions
302 drevs = callconduit(repo, 'differential.query', params)
309 drevs = callconduit(repo, 'differential.query', params)
303 if len(drevs) == 1:
310 if len(drevs) == 1:
304 drev = drevs[0]
311 drev = drevs[0]
305 else:
312 else:
306 raise error.Abort(_('cannot get Differential Revision %r') % params)
313 raise error.Abort(_('cannot get Differential Revision %r') % params)
307
314
308 repo.ui.note(_('reading D%s\n') % drev[r'id'])
315 repo.ui.note(_('reading D%s\n') % drev[r'id'])
309
316
310 diffid = max(int(v) for v in drev[r'diffs'])
317 diffid = max(int(v) for v in drev[r'diffs'])
311 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
318 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
312 desc = callconduit(repo, 'differential.getcommitmessage',
319 desc = callconduit(repo, 'differential.getcommitmessage',
313 {'revision_id': drev[r'id']})
320 {'revision_id': drev[r'id']})
314 header = '# HG changeset patch\n'
321 header = '# HG changeset patch\n'
315
322
316 # Remove potential empty "Summary:"
323 # Remove potential empty "Summary:"
317 desc = _summaryre.sub('', desc)
324 desc = _summaryre.sub('', desc)
318
325
319 # Try to preserve metadata (user, date) from hg:meta property
326 # Try to preserve metadata from hg:meta property. Write hg patch headers
327 # that can be read by the "import" command. See patchheadermap and extract
328 # in mercurial/patch.py for supported headers.
320 diffs = callconduit(repo, 'differential.querydiffs', {'ids': [diffid]})
329 diffs = callconduit(repo, 'differential.querydiffs', {'ids': [diffid]})
321 props = diffs[str(diffid)][r'properties'] # could be empty list or dict
330 props = diffs[str(diffid)][r'properties'] # could be empty list or dict
322 if props and r'hg:meta' in props:
331 if props and r'hg:meta' in props:
323 meta = props[r'hg:meta']
332 meta = props[r'hg:meta']
324 for k, v in meta.items():
333 for k in _metanamemap.keys():
325 header += '# %s %s\n' % (k.capitalize(), v)
334 if k in meta:
335 header += '# %s %s\n' % (_metanamemap[k], meta[k])
326
336
327 patch = ('%s%s\n%s') % (header, desc, body)
337 patch = ('%s%s\n%s') % (header, desc, body)
328
338
329 # Check dependencies
339 # Check dependencies
330 if recursive:
340 if recursive:
331 auxiliary = drev.get(r'auxiliary', {})
341 auxiliary = drev.get(r'auxiliary', {})
332 depends = auxiliary.get(r'phabricator:depends-on', [])
342 depends = auxiliary.get(r'phabricator:depends-on', [])
333 for phid in depends:
343 for phid in depends:
334 patch = readpatch(repo, {'phids': [phid]}, recursive=True) + patch
344 patch = readpatch(repo, {'phids': [phid]}, recursive=True) + patch
335 return patch
345 return patch
336
346
337 @command('phabread',
347 @command('phabread',
338 [('', 'stack', False, _('read dependencies'))],
348 [('', 'stack', False, _('read dependencies'))],
339 _('REVID [OPTIONS]'))
349 _('REVID [OPTIONS]'))
340 def phabread(ui, repo, revid, **opts):
350 def phabread(ui, repo, revid, **opts):
341 """print patches from Phabricator suitable for importing
351 """print patches from Phabricator suitable for importing
342
352
343 REVID could be a Differential Revision identity, like ``D123``, or just the
353 REVID could be a Differential Revision identity, like ``D123``, or just the
344 number ``123``, or a full URL like ``https://phab.example.com/D123``.
354 number ``123``, or a full URL like ``https://phab.example.com/D123``.
345
355
346 If --stack is given, follow dependencies information and read all patches.
356 If --stack is given, follow dependencies information and read all patches.
347 """
357 """
348 try:
358 try:
349 revid = int(revid.split('/')[-1].replace('D', ''))
359 revid = int(revid.split('/')[-1].replace('D', ''))
350 except ValueError:
360 except ValueError:
351 raise error.Abort(_('invalid Revision ID: %s') % revid)
361 raise error.Abort(_('invalid Revision ID: %s') % revid)
352 patch = readpatch(repo, {'ids': [revid]}, recursive=opts.get('stack'))
362 patch = readpatch(repo, {'ids': [revid]}, recursive=opts.get('stack'))
353 ui.write(patch)
363 ui.write(patch)
General Comments 0
You need to be logged in to leave comments. Login now