##// END OF EJS Templates
phabricator: check associated Differential Revision from commit message...
Jun Wu -
r33263:ed611897 default
parent child Browse files
Show More
@@ -1,340 +1,353 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 _differentialrevisionre = re.compile('\AD([1-9][0-9]*)\Z')
138 _differentialrevisiontagre = re.compile('\AD([1-9][0-9]*)\Z')
139 _differentialrevisiondescre = re.compile(
140 '^Differential Revision:.*D([1-9][0-9]*)$', re.M)
139
141
140 def getmapping(ctx):
142 def getmapping(ctx):
141 """return (node, associated Differential Revision ID) or (None, None)
143 """return (node, associated Differential Revision ID) or (None, None)
142
144
143 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
144 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"
145 (ex. 1234) will be returned.
147 (ex. 1234) will be returned.
148
149 If tags are not found, examine commit message. The "Differential Revision:"
150 line could associate this changeset to a Differential Revision.
146 """
151 """
147 unfi = ctx.repo().unfiltered()
152 unfi = ctx.repo().unfiltered()
148 nodemap = unfi.changelog.nodemap
153 nodemap = unfi.changelog.nodemap
154
155 # Check tags like "D123"
149 for n in obsolete.allprecursors(unfi.obsstore, [ctx.node()]):
156 for n in obsolete.allprecursors(unfi.obsstore, [ctx.node()]):
150 if n in nodemap:
157 if n in nodemap:
151 for tag in unfi.nodetags(n):
158 for tag in unfi.nodetags(n):
152 m = _differentialrevisionre.match(tag)
159 m = _differentialrevisiontagre.match(tag)
153 if m:
160 if m:
154 return n, int(m.group(1))
161 return n, int(m.group(1))
162
163 # Check commit message
164 m = _differentialrevisiondescre.search(ctx.description())
165 if m:
166 return None, int(m.group(1))
167
155 return None, None
168 return None, None
156
169
157 def getdiff(ctx, diffopts):
170 def getdiff(ctx, diffopts):
158 """plain-text diff without header (user, commit message, etc)"""
171 """plain-text diff without header (user, commit message, etc)"""
159 output = util.stringio()
172 output = util.stringio()
160 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(),
161 None, opts=diffopts):
174 None, opts=diffopts):
162 output.write(chunk)
175 output.write(chunk)
163 return output.getvalue()
176 return output.getvalue()
164
177
165 def creatediff(ctx):
178 def creatediff(ctx):
166 """create a Differential Diff"""
179 """create a Differential Diff"""
167 repo = ctx.repo()
180 repo = ctx.repo()
168 repophid = getrepophid(repo)
181 repophid = getrepophid(repo)
169 # Create a "Differential Diff" via "differential.createrawdiff" API
182 # Create a "Differential Diff" via "differential.createrawdiff" API
170 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
183 params = {'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))}
171 if repophid:
184 if repophid:
172 params['repositoryPHID'] = repophid
185 params['repositoryPHID'] = repophid
173 diff = callconduit(repo, 'differential.createrawdiff', params)
186 diff = callconduit(repo, 'differential.createrawdiff', params)
174 if not diff:
187 if not diff:
175 raise error.Abort(_('cannot create diff for %s') % ctx)
188 raise error.Abort(_('cannot create diff for %s') % ctx)
176 return diff
189 return diff
177
190
178 def writediffproperties(ctx, diff):
191 def writediffproperties(ctx, diff):
179 """write metadata to diff so patches could be applied losslessly"""
192 """write metadata to diff so patches could be applied losslessly"""
180 params = {
193 params = {
181 'diff_id': diff[r'id'],
194 'diff_id': diff[r'id'],
182 'name': 'hg:meta',
195 'name': 'hg:meta',
183 'data': json.dumps({
196 'data': json.dumps({
184 'user': ctx.user(),
197 'user': ctx.user(),
185 'date': '%d %d' % ctx.date(),
198 'date': '%d %d' % ctx.date(),
186 }),
199 }),
187 }
200 }
188 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
201 callconduit(ctx.repo(), 'differential.setdiffproperty', params)
189
202
190 def createdifferentialrevision(ctx, revid=None, parentrevid=None):
203 def createdifferentialrevision(ctx, revid=None, parentrevid=None):
191 """create or update a Differential Revision
204 """create or update a Differential Revision
192
205
193 If revid is None, create a new Differential Revision, otherwise update
206 If revid is None, create a new Differential Revision, otherwise update
194 revid. If parentrevid is not None, set it as a dependency.
207 revid. If parentrevid is not None, set it as a dependency.
195 """
208 """
196 repo = ctx.repo()
209 repo = ctx.repo()
197 diff = creatediff(ctx)
210 diff = creatediff(ctx)
198 writediffproperties(ctx, diff)
211 writediffproperties(ctx, diff)
199
212
200 transactions = [{'type': 'update', 'value': diff[r'phid']}]
213 transactions = [{'type': 'update', 'value': diff[r'phid']}]
201
214
202 # Use a temporary summary to set dependency. There might be better ways but
215 # Use a temporary summary to set dependency. There might be better ways but
203 # I cannot find them for now. But do not do that if we are updating an
216 # I cannot find them for now. But do not do that if we are updating an
204 # existing revision (revid is not None) since that introduces visible
217 # existing revision (revid is not None) since that introduces visible
205 # churns (someone edited "Summary" twice) on the web page.
218 # churns (someone edited "Summary" twice) on the web page.
206 if parentrevid and revid is None:
219 if parentrevid and revid is None:
207 summary = 'Depends on D%s' % parentrevid
220 summary = 'Depends on D%s' % parentrevid
208 transactions += [{'type': 'summary', 'value': summary},
221 transactions += [{'type': 'summary', 'value': summary},
209 {'type': 'summary', 'value': ' '}]
222 {'type': 'summary', 'value': ' '}]
210
223
211 # Parse commit message and update related fields.
224 # Parse commit message and update related fields.
212 desc = ctx.description()
225 desc = ctx.description()
213 info = callconduit(repo, 'differential.parsecommitmessage',
226 info = callconduit(repo, 'differential.parsecommitmessage',
214 {'corpus': desc})
227 {'corpus': desc})
215 for k, v in info[r'fields'].items():
228 for k, v in info[r'fields'].items():
216 if k in ['title', 'summary', 'testPlan']:
229 if k in ['title', 'summary', 'testPlan']:
217 transactions.append({'type': k, 'value': v})
230 transactions.append({'type': k, 'value': v})
218
231
219 params = {'transactions': transactions}
232 params = {'transactions': transactions}
220 if revid is not None:
233 if revid is not None:
221 # Update an existing Differential Revision
234 # Update an existing Differential Revision
222 params['objectIdentifier'] = revid
235 params['objectIdentifier'] = revid
223
236
224 revision = callconduit(repo, 'differential.revision.edit', params)
237 revision = callconduit(repo, 'differential.revision.edit', params)
225 if not revision:
238 if not revision:
226 raise error.Abort(_('cannot create revision for %s') % ctx)
239 raise error.Abort(_('cannot create revision for %s') % ctx)
227
240
228 return revision
241 return revision
229
242
230 @command('phabsend',
243 @command('phabsend',
231 [('r', 'rev', [], _('revisions to send'), _('REV'))],
244 [('r', 'rev', [], _('revisions to send'), _('REV'))],
232 _('REV [OPTIONS]'))
245 _('REV [OPTIONS]'))
233 def phabsend(ui, repo, *revs, **opts):
246 def phabsend(ui, repo, *revs, **opts):
234 """upload changesets to Phabricator
247 """upload changesets to Phabricator
235
248
236 If there are multiple revisions specified, they will be send as a stack
249 If there are multiple revisions specified, they will be send as a stack
237 with a linear dependencies relationship using the order specified by the
250 with a linear dependencies relationship using the order specified by the
238 revset.
251 revset.
239
252
240 For the first time uploading changesets, local tags will be created to
253 For the first time uploading changesets, local tags will be created to
241 maintain the association. After the first time, phabsend will check
254 maintain the association. After the first time, phabsend will check
242 obsstore and tags information so it can figure out whether to update an
255 obsstore and tags information so it can figure out whether to update an
243 existing Differential Revision, or create a new one.
256 existing Differential Revision, or create a new one.
244 """
257 """
245 revs = list(revs) + opts.get('rev', [])
258 revs = list(revs) + opts.get('rev', [])
246 revs = scmutil.revrange(repo, revs)
259 revs = scmutil.revrange(repo, revs)
247
260
248 # Send patches one by one so we know their Differential Revision IDs and
261 # Send patches one by one so we know their Differential Revision IDs and
249 # can provide dependency relationship
262 # can provide dependency relationship
250 lastrevid = None
263 lastrevid = None
251 for rev in revs:
264 for rev in revs:
252 ui.debug('sending rev %d\n' % rev)
265 ui.debug('sending rev %d\n' % rev)
253 ctx = repo[rev]
266 ctx = repo[rev]
254
267
255 # Get Differential Revision ID
268 # Get Differential Revision ID
256 oldnode, revid = getmapping(ctx)
269 oldnode, revid = getmapping(ctx)
257 if oldnode != ctx.node():
270 if oldnode != ctx.node():
258 # Create or update Differential Revision
271 # Create or update Differential Revision
259 revision = createdifferentialrevision(ctx, revid, lastrevid)
272 revision = createdifferentialrevision(ctx, revid, lastrevid)
260 newrevid = int(revision[r'object'][r'id'])
273 newrevid = int(revision[r'object'][r'id'])
261 if revid:
274 if revid:
262 action = _('updated')
275 action = _('updated')
263 else:
276 else:
264 action = _('created')
277 action = _('created')
265
278
266 # Create a local tag to note the association
279 # Create a local tag to note the association
267 tagname = 'D%d' % newrevid
280 tagname = 'D%d' % newrevid
268 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
281 tags.tag(repo, tagname, ctx.node(), message=None, user=None,
269 date=None, local=True)
282 date=None, local=True)
270 else:
283 else:
271 # Nothing changed. But still set "newrevid" so the next revision
284 # Nothing changed. But still set "newrevid" so the next revision
272 # could depend on this one.
285 # could depend on this one.
273 newrevid = revid
286 newrevid = revid
274 action = _('skipped')
287 action = _('skipped')
275
288
276 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
289 ui.write(_('D%s: %s - %s: %s\n') % (newrevid, action, ctx,
277 ctx.description().split('\n')[0]))
290 ctx.description().split('\n')[0]))
278 lastrevid = newrevid
291 lastrevid = newrevid
279
292
280 _summaryre = re.compile('^Summary:\s*', re.M)
293 _summaryre = re.compile('^Summary:\s*', re.M)
281
294
282 def readpatch(repo, params, recursive=False):
295 def readpatch(repo, params, recursive=False):
283 """generate plain-text patch readable by 'hg import'
296 """generate plain-text patch readable by 'hg import'
284
297
285 params is passed to "differential.query". If recursive is True, also return
298 params is passed to "differential.query". If recursive is True, also return
286 dependent patches.
299 dependent patches.
287 """
300 """
288 # Differential Revisions
301 # Differential Revisions
289 drevs = callconduit(repo, 'differential.query', params)
302 drevs = callconduit(repo, 'differential.query', params)
290 if len(drevs) == 1:
303 if len(drevs) == 1:
291 drev = drevs[0]
304 drev = drevs[0]
292 else:
305 else:
293 raise error.Abort(_('cannot get Differential Revision %r') % params)
306 raise error.Abort(_('cannot get Differential Revision %r') % params)
294
307
295 repo.ui.note(_('reading D%s\n') % drev[r'id'])
308 repo.ui.note(_('reading D%s\n') % drev[r'id'])
296
309
297 diffid = max(int(v) for v in drev[r'diffs'])
310 diffid = max(int(v) for v in drev[r'diffs'])
298 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
311 body = callconduit(repo, 'differential.getrawdiff', {'diffID': diffid})
299 desc = callconduit(repo, 'differential.getcommitmessage',
312 desc = callconduit(repo, 'differential.getcommitmessage',
300 {'revision_id': drev[r'id']})
313 {'revision_id': drev[r'id']})
301 header = '# HG changeset patch\n'
314 header = '# HG changeset patch\n'
302
315
303 # Remove potential empty "Summary:"
316 # Remove potential empty "Summary:"
304 desc = _summaryre.sub('', desc)
317 desc = _summaryre.sub('', desc)
305
318
306 # Try to preserve metadata (user, date) from hg:meta property
319 # Try to preserve metadata (user, date) from hg:meta property
307 diffs = callconduit(repo, 'differential.querydiffs', {'ids': [diffid]})
320 diffs = callconduit(repo, 'differential.querydiffs', {'ids': [diffid]})
308 props = diffs[str(diffid)][r'properties'] # could be empty list or dict
321 props = diffs[str(diffid)][r'properties'] # could be empty list or dict
309 if props and r'hg:meta' in props:
322 if props and r'hg:meta' in props:
310 meta = props[r'hg:meta']
323 meta = props[r'hg:meta']
311 for k, v in meta.items():
324 for k, v in meta.items():
312 header += '# %s %s\n' % (k.capitalize(), v)
325 header += '# %s %s\n' % (k.capitalize(), v)
313
326
314 patch = ('%s%s\n%s') % (header, desc, body)
327 patch = ('%s%s\n%s') % (header, desc, body)
315
328
316 # Check dependencies
329 # Check dependencies
317 if recursive:
330 if recursive:
318 auxiliary = drev.get(r'auxiliary', {})
331 auxiliary = drev.get(r'auxiliary', {})
319 depends = auxiliary.get(r'phabricator:depends-on', [])
332 depends = auxiliary.get(r'phabricator:depends-on', [])
320 for phid in depends:
333 for phid in depends:
321 patch = readpatch(repo, {'phids': [phid]}, recursive=True) + patch
334 patch = readpatch(repo, {'phids': [phid]}, recursive=True) + patch
322 return patch
335 return patch
323
336
324 @command('phabread',
337 @command('phabread',
325 [('', 'stack', False, _('read dependencies'))],
338 [('', 'stack', False, _('read dependencies'))],
326 _('REVID [OPTIONS]'))
339 _('REVID [OPTIONS]'))
327 def phabread(ui, repo, revid, **opts):
340 def phabread(ui, repo, revid, **opts):
328 """print patches from Phabricator suitable for importing
341 """print patches from Phabricator suitable for importing
329
342
330 REVID could be a Differential Revision identity, like ``D123``, or just the
343 REVID could be a Differential Revision identity, like ``D123``, or just the
331 number ``123``, or a full URL like ``https://phab.example.com/D123``.
344 number ``123``, or a full URL like ``https://phab.example.com/D123``.
332
345
333 If --stack is given, follow dependencies information and read all patches.
346 If --stack is given, follow dependencies information and read all patches.
334 """
347 """
335 try:
348 try:
336 revid = int(revid.split('/')[-1].replace('D', ''))
349 revid = int(revid.split('/')[-1].replace('D', ''))
337 except ValueError:
350 except ValueError:
338 raise error.Abort(_('invalid Revision ID: %s') % revid)
351 raise error.Abort(_('invalid Revision ID: %s') % revid)
339 patch = readpatch(repo, {'ids': [revid]}, recursive=opts.get('stack'))
352 patch = readpatch(repo, {'ids': [revid]}, recursive=opts.get('stack'))
340 ui.write(patch)
353 ui.write(patch)
General Comments 0
You need to be logged in to leave comments. Login now