##// END OF EJS Templates
phabricator: unconditionally pop `test_vcr` to fix debugcallconduit...
Ian Moody -
r45806:b1f2659c default
parent child Browse files
Show More
@@ -1,2303 +1,2304 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 (EXPERIMENTAL)
7 """simple Phabricator integration (EXPERIMENTAL)
8
8
9 This extension provides a ``phabsend`` command which sends a stack of
9 This extension provides a ``phabsend`` command which sends a stack of
10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
12 to update statuses in batch.
12 to update statuses in batch.
13
13
14 A "phabstatus" view for :hg:`show` is also provided; it displays status
14 A "phabstatus" view for :hg:`show` is also provided; it displays status
15 information of Phabricator differentials associated with unfinished
15 information of Phabricator differentials associated with unfinished
16 changesets.
16 changesets.
17
17
18 By default, Phabricator requires ``Test Plan`` which might prevent some
18 By default, Phabricator requires ``Test Plan`` which might prevent some
19 changeset from being sent. The requirement could be disabled by changing
19 changeset from being sent. The requirement could be disabled by changing
20 ``differential.require-test-plan-field`` config server side.
20 ``differential.require-test-plan-field`` config server side.
21
21
22 Config::
22 Config::
23
23
24 [phabricator]
24 [phabricator]
25 # Phabricator URL
25 # Phabricator URL
26 url = https://phab.example.com/
26 url = https://phab.example.com/
27
27
28 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
28 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
29 # callsign is "FOO".
29 # callsign is "FOO".
30 callsign = FOO
30 callsign = FOO
31
31
32 # curl command to use. If not set (default), use builtin HTTP library to
32 # curl command to use. If not set (default), use builtin HTTP library to
33 # communicate. If set, use the specified curl command. This could be useful
33 # communicate. If set, use the specified curl command. This could be useful
34 # if you need to specify advanced options that is not easily supported by
34 # if you need to specify advanced options that is not easily supported by
35 # the internal library.
35 # the internal library.
36 curlcmd = curl --connect-timeout 2 --retry 3 --silent
36 curlcmd = curl --connect-timeout 2 --retry 3 --silent
37
37
38 [auth]
38 [auth]
39 example.schemes = https
39 example.schemes = https
40 example.prefix = phab.example.com
40 example.prefix = phab.example.com
41
41
42 # API token. Get it from https://$HOST/conduit/login/
42 # API token. Get it from https://$HOST/conduit/login/
43 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
43 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
44 """
44 """
45
45
46 from __future__ import absolute_import
46 from __future__ import absolute_import
47
47
48 import base64
48 import base64
49 import contextlib
49 import contextlib
50 import hashlib
50 import hashlib
51 import itertools
51 import itertools
52 import json
52 import json
53 import mimetypes
53 import mimetypes
54 import operator
54 import operator
55 import re
55 import re
56
56
57 from mercurial.node import bin, nullid, short
57 from mercurial.node import bin, nullid, short
58 from mercurial.i18n import _
58 from mercurial.i18n import _
59 from mercurial.pycompat import getattr
59 from mercurial.pycompat import getattr
60 from mercurial.thirdparty import attr
60 from mercurial.thirdparty import attr
61 from mercurial import (
61 from mercurial import (
62 cmdutil,
62 cmdutil,
63 context,
63 context,
64 copies,
64 copies,
65 encoding,
65 encoding,
66 error,
66 error,
67 exthelper,
67 exthelper,
68 graphmod,
68 graphmod,
69 httpconnection as httpconnectionmod,
69 httpconnection as httpconnectionmod,
70 localrepo,
70 localrepo,
71 logcmdutil,
71 logcmdutil,
72 match,
72 match,
73 mdiff,
73 mdiff,
74 obsutil,
74 obsutil,
75 parser,
75 parser,
76 patch,
76 patch,
77 phases,
77 phases,
78 pycompat,
78 pycompat,
79 scmutil,
79 scmutil,
80 smartset,
80 smartset,
81 tags,
81 tags,
82 templatefilters,
82 templatefilters,
83 templateutil,
83 templateutil,
84 url as urlmod,
84 url as urlmod,
85 util,
85 util,
86 )
86 )
87 from mercurial.utils import (
87 from mercurial.utils import (
88 procutil,
88 procutil,
89 stringutil,
89 stringutil,
90 )
90 )
91 from . import show
91 from . import show
92
92
93
93
94 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
94 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
95 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
95 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
96 # be specifying the version(s) of Mercurial they are tested with, or
96 # be specifying the version(s) of Mercurial they are tested with, or
97 # leave the attribute unspecified.
97 # leave the attribute unspecified.
98 testedwith = b'ships-with-hg-core'
98 testedwith = b'ships-with-hg-core'
99
99
100 eh = exthelper.exthelper()
100 eh = exthelper.exthelper()
101
101
102 cmdtable = eh.cmdtable
102 cmdtable = eh.cmdtable
103 command = eh.command
103 command = eh.command
104 configtable = eh.configtable
104 configtable = eh.configtable
105 templatekeyword = eh.templatekeyword
105 templatekeyword = eh.templatekeyword
106 uisetup = eh.finaluisetup
106 uisetup = eh.finaluisetup
107
107
108 # developer config: phabricator.batchsize
108 # developer config: phabricator.batchsize
109 eh.configitem(
109 eh.configitem(
110 b'phabricator', b'batchsize', default=12,
110 b'phabricator', b'batchsize', default=12,
111 )
111 )
112 eh.configitem(
112 eh.configitem(
113 b'phabricator', b'callsign', default=None,
113 b'phabricator', b'callsign', default=None,
114 )
114 )
115 eh.configitem(
115 eh.configitem(
116 b'phabricator', b'curlcmd', default=None,
116 b'phabricator', b'curlcmd', default=None,
117 )
117 )
118 # developer config: phabricator.debug
118 # developer config: phabricator.debug
119 eh.configitem(
119 eh.configitem(
120 b'phabricator', b'debug', default=False,
120 b'phabricator', b'debug', default=False,
121 )
121 )
122 # developer config: phabricator.repophid
122 # developer config: phabricator.repophid
123 eh.configitem(
123 eh.configitem(
124 b'phabricator', b'repophid', default=None,
124 b'phabricator', b'repophid', default=None,
125 )
125 )
126 eh.configitem(
126 eh.configitem(
127 b'phabricator', b'url', default=None,
127 b'phabricator', b'url', default=None,
128 )
128 )
129 eh.configitem(
129 eh.configitem(
130 b'phabsend', b'confirm', default=False,
130 b'phabsend', b'confirm', default=False,
131 )
131 )
132 eh.configitem(
132 eh.configitem(
133 b'phabimport', b'secret', default=False,
133 b'phabimport', b'secret', default=False,
134 )
134 )
135 eh.configitem(
135 eh.configitem(
136 b'phabimport', b'obsolete', default=False,
136 b'phabimport', b'obsolete', default=False,
137 )
137 )
138
138
139 colortable = {
139 colortable = {
140 b'phabricator.action.created': b'green',
140 b'phabricator.action.created': b'green',
141 b'phabricator.action.skipped': b'magenta',
141 b'phabricator.action.skipped': b'magenta',
142 b'phabricator.action.updated': b'magenta',
142 b'phabricator.action.updated': b'magenta',
143 b'phabricator.desc': b'',
143 b'phabricator.desc': b'',
144 b'phabricator.drev': b'bold',
144 b'phabricator.drev': b'bold',
145 b'phabricator.node': b'',
145 b'phabricator.node': b'',
146 b'phabricator.status.abandoned': b'magenta dim',
146 b'phabricator.status.abandoned': b'magenta dim',
147 b'phabricator.status.accepted': b'green bold',
147 b'phabricator.status.accepted': b'green bold',
148 b'phabricator.status.closed': b'green',
148 b'phabricator.status.closed': b'green',
149 b'phabricator.status.needsreview': b'yellow',
149 b'phabricator.status.needsreview': b'yellow',
150 b'phabricator.status.needsrevision': b'red',
150 b'phabricator.status.needsrevision': b'red',
151 b'phabricator.status.changesplanned': b'red',
151 b'phabricator.status.changesplanned': b'red',
152 }
152 }
153
153
154 _VCR_FLAGS = [
154 _VCR_FLAGS = [
155 (
155 (
156 b'',
156 b'',
157 b'test-vcr',
157 b'test-vcr',
158 b'',
158 b'',
159 _(
159 _(
160 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
160 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
161 b', otherwise will mock all http requests using the specified vcr file.'
161 b', otherwise will mock all http requests using the specified vcr file.'
162 b' (ADVANCED)'
162 b' (ADVANCED)'
163 ),
163 ),
164 ),
164 ),
165 ]
165 ]
166
166
167
167
168 @eh.wrapfunction(localrepo, "loadhgrc")
168 @eh.wrapfunction(localrepo, "loadhgrc")
169 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):
169 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements):
170 """Load ``.arcconfig`` content into a ui instance on repository open.
170 """Load ``.arcconfig`` content into a ui instance on repository open.
171 """
171 """
172 result = False
172 result = False
173 arcconfig = {}
173 arcconfig = {}
174
174
175 try:
175 try:
176 # json.loads only accepts bytes from 3.6+
176 # json.loads only accepts bytes from 3.6+
177 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
177 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
178 # json.loads only returns unicode strings
178 # json.loads only returns unicode strings
179 arcconfig = pycompat.rapply(
179 arcconfig = pycompat.rapply(
180 lambda x: encoding.unitolocal(x)
180 lambda x: encoding.unitolocal(x)
181 if isinstance(x, pycompat.unicode)
181 if isinstance(x, pycompat.unicode)
182 else x,
182 else x,
183 pycompat.json_loads(rawparams),
183 pycompat.json_loads(rawparams),
184 )
184 )
185
185
186 result = True
186 result = True
187 except ValueError:
187 except ValueError:
188 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
188 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
189 except IOError:
189 except IOError:
190 pass
190 pass
191
191
192 cfg = util.sortdict()
192 cfg = util.sortdict()
193
193
194 if b"repository.callsign" in arcconfig:
194 if b"repository.callsign" in arcconfig:
195 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
195 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
196
196
197 if b"phabricator.uri" in arcconfig:
197 if b"phabricator.uri" in arcconfig:
198 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
198 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
199
199
200 if cfg:
200 if cfg:
201 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
201 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
202
202
203 return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg/hgrc
203 return orig(ui, wdirvfs, hgvfs, requirements) or result # Load .hg/hgrc
204
204
205
205
206 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
206 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
207 fullflags = flags + _VCR_FLAGS
207 fullflags = flags + _VCR_FLAGS
208
208
209 def hgmatcher(r1, r2):
209 def hgmatcher(r1, r2):
210 if r1.uri != r2.uri or r1.method != r2.method:
210 if r1.uri != r2.uri or r1.method != r2.method:
211 return False
211 return False
212 r1params = util.urlreq.parseqs(r1.body)
212 r1params = util.urlreq.parseqs(r1.body)
213 r2params = util.urlreq.parseqs(r2.body)
213 r2params = util.urlreq.parseqs(r2.body)
214 for key in r1params:
214 for key in r1params:
215 if key not in r2params:
215 if key not in r2params:
216 return False
216 return False
217 value = r1params[key][0]
217 value = r1params[key][0]
218 # we want to compare json payloads without worrying about ordering
218 # we want to compare json payloads without worrying about ordering
219 if value.startswith(b'{') and value.endswith(b'}'):
219 if value.startswith(b'{') and value.endswith(b'}'):
220 r1json = pycompat.json_loads(value)
220 r1json = pycompat.json_loads(value)
221 r2json = pycompat.json_loads(r2params[key][0])
221 r2json = pycompat.json_loads(r2params[key][0])
222 if r1json != r2json:
222 if r1json != r2json:
223 return False
223 return False
224 elif r2params[key][0] != value:
224 elif r2params[key][0] != value:
225 return False
225 return False
226 return True
226 return True
227
227
228 def sanitiserequest(request):
228 def sanitiserequest(request):
229 request.body = re.sub(
229 request.body = re.sub(
230 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
230 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
231 )
231 )
232 return request
232 return request
233
233
234 def sanitiseresponse(response):
234 def sanitiseresponse(response):
235 if 'set-cookie' in response['headers']:
235 if 'set-cookie' in response['headers']:
236 del response['headers']['set-cookie']
236 del response['headers']['set-cookie']
237 return response
237 return response
238
238
239 def decorate(fn):
239 def decorate(fn):
240 def inner(*args, **kwargs):
240 def inner(*args, **kwargs):
241 if kwargs.get('test_vcr'):
241 vcr = kwargs.pop('test_vcr')
242 cassette = pycompat.fsdecode(kwargs.pop('test_vcr'))
242 if vcr:
243 cassette = pycompat.fsdecode(vcr)
243 import hgdemandimport
244 import hgdemandimport
244
245
245 with hgdemandimport.deactivated():
246 with hgdemandimport.deactivated():
246 import vcr as vcrmod
247 import vcr as vcrmod
247 import vcr.stubs as stubs
248 import vcr.stubs as stubs
248
249
249 vcr = vcrmod.VCR(
250 vcr = vcrmod.VCR(
250 serializer='json',
251 serializer='json',
251 before_record_request=sanitiserequest,
252 before_record_request=sanitiserequest,
252 before_record_response=sanitiseresponse,
253 before_record_response=sanitiseresponse,
253 custom_patches=[
254 custom_patches=[
254 (
255 (
255 urlmod,
256 urlmod,
256 'httpconnection',
257 'httpconnection',
257 stubs.VCRHTTPConnection,
258 stubs.VCRHTTPConnection,
258 ),
259 ),
259 (
260 (
260 urlmod,
261 urlmod,
261 'httpsconnection',
262 'httpsconnection',
262 stubs.VCRHTTPSConnection,
263 stubs.VCRHTTPSConnection,
263 ),
264 ),
264 ],
265 ],
265 )
266 )
266 vcr.register_matcher('hgmatcher', hgmatcher)
267 vcr.register_matcher('hgmatcher', hgmatcher)
267 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
268 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
268 return fn(*args, **kwargs)
269 return fn(*args, **kwargs)
269 return fn(*args, **kwargs)
270 return fn(*args, **kwargs)
270
271
271 cmd = util.checksignature(inner, depth=2)
272 cmd = util.checksignature(inner, depth=2)
272 cmd.__name__ = fn.__name__
273 cmd.__name__ = fn.__name__
273 cmd.__doc__ = fn.__doc__
274 cmd.__doc__ = fn.__doc__
274
275
275 return command(
276 return command(
276 name,
277 name,
277 fullflags,
278 fullflags,
278 spec,
279 spec,
279 helpcategory=helpcategory,
280 helpcategory=helpcategory,
280 optionalrepo=optionalrepo,
281 optionalrepo=optionalrepo,
281 )(cmd)
282 )(cmd)
282
283
283 return decorate
284 return decorate
284
285
285
286
286 def _debug(ui, *msg, **opts):
287 def _debug(ui, *msg, **opts):
287 """write debug output for Phabricator if ``phabricator.debug`` is set
288 """write debug output for Phabricator if ``phabricator.debug`` is set
288
289
289 Specifically, this avoids dumping Conduit and HTTP auth chatter that is
290 Specifically, this avoids dumping Conduit and HTTP auth chatter that is
290 printed with the --debug argument.
291 printed with the --debug argument.
291 """
292 """
292 if ui.configbool(b"phabricator", b"debug"):
293 if ui.configbool(b"phabricator", b"debug"):
293 flag = ui.debugflag
294 flag = ui.debugflag
294 try:
295 try:
295 ui.debugflag = True
296 ui.debugflag = True
296 ui.write(*msg, **opts)
297 ui.write(*msg, **opts)
297 finally:
298 finally:
298 ui.debugflag = flag
299 ui.debugflag = flag
299
300
300
301
301 def urlencodenested(params):
302 def urlencodenested(params):
302 """like urlencode, but works with nested parameters.
303 """like urlencode, but works with nested parameters.
303
304
304 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
305 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
305 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
306 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
306 urlencode. Note: the encoding is consistent with PHP's http_build_query.
307 urlencode. Note: the encoding is consistent with PHP's http_build_query.
307 """
308 """
308 flatparams = util.sortdict()
309 flatparams = util.sortdict()
309
310
310 def process(prefix, obj):
311 def process(prefix, obj):
311 if isinstance(obj, bool):
312 if isinstance(obj, bool):
312 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
313 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
313 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
314 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
314 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
315 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
315 if items is None:
316 if items is None:
316 flatparams[prefix] = obj
317 flatparams[prefix] = obj
317 else:
318 else:
318 for k, v in items(obj):
319 for k, v in items(obj):
319 if prefix:
320 if prefix:
320 process(b'%s[%s]' % (prefix, k), v)
321 process(b'%s[%s]' % (prefix, k), v)
321 else:
322 else:
322 process(k, v)
323 process(k, v)
323
324
324 process(b'', params)
325 process(b'', params)
325 return util.urlreq.urlencode(flatparams)
326 return util.urlreq.urlencode(flatparams)
326
327
327
328
328 def readurltoken(ui):
329 def readurltoken(ui):
329 """return conduit url, token and make sure they exist
330 """return conduit url, token and make sure they exist
330
331
331 Currently read from [auth] config section. In the future, it might
332 Currently read from [auth] config section. In the future, it might
332 make sense to read from .arcconfig and .arcrc as well.
333 make sense to read from .arcconfig and .arcrc as well.
333 """
334 """
334 url = ui.config(b'phabricator', b'url')
335 url = ui.config(b'phabricator', b'url')
335 if not url:
336 if not url:
336 raise error.Abort(
337 raise error.Abort(
337 _(b'config %s.%s is required') % (b'phabricator', b'url')
338 _(b'config %s.%s is required') % (b'phabricator', b'url')
338 )
339 )
339
340
340 res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)
341 res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user)
341 token = None
342 token = None
342
343
343 if res:
344 if res:
344 group, auth = res
345 group, auth = res
345
346
346 ui.debug(b"using auth.%s.* for authentication\n" % group)
347 ui.debug(b"using auth.%s.* for authentication\n" % group)
347
348
348 token = auth.get(b'phabtoken')
349 token = auth.get(b'phabtoken')
349
350
350 if not token:
351 if not token:
351 raise error.Abort(
352 raise error.Abort(
352 _(b'Can\'t find conduit token associated to %s') % (url,)
353 _(b'Can\'t find conduit token associated to %s') % (url,)
353 )
354 )
354
355
355 return url, token
356 return url, token
356
357
357
358
358 def callconduit(ui, name, params):
359 def callconduit(ui, name, params):
359 """call Conduit API, params is a dict. return json.loads result, or None"""
360 """call Conduit API, params is a dict. return json.loads result, or None"""
360 host, token = readurltoken(ui)
361 host, token = readurltoken(ui)
361 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
362 url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo()
362 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
363 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
363 params = params.copy()
364 params = params.copy()
364 params[b'__conduit__'] = {
365 params[b'__conduit__'] = {
365 b'token': token,
366 b'token': token,
366 }
367 }
367 rawdata = {
368 rawdata = {
368 b'params': templatefilters.json(params),
369 b'params': templatefilters.json(params),
369 b'output': b'json',
370 b'output': b'json',
370 b'__conduit__': 1,
371 b'__conduit__': 1,
371 }
372 }
372 data = urlencodenested(rawdata)
373 data = urlencodenested(rawdata)
373 curlcmd = ui.config(b'phabricator', b'curlcmd')
374 curlcmd = ui.config(b'phabricator', b'curlcmd')
374 if curlcmd:
375 if curlcmd:
375 sin, sout = procutil.popen2(
376 sin, sout = procutil.popen2(
376 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
377 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
377 )
378 )
378 sin.write(data)
379 sin.write(data)
379 sin.close()
380 sin.close()
380 body = sout.read()
381 body = sout.read()
381 else:
382 else:
382 urlopener = urlmod.opener(ui, authinfo)
383 urlopener = urlmod.opener(ui, authinfo)
383 request = util.urlreq.request(pycompat.strurl(url), data=data)
384 request = util.urlreq.request(pycompat.strurl(url), data=data)
384 with contextlib.closing(urlopener.open(request)) as rsp:
385 with contextlib.closing(urlopener.open(request)) as rsp:
385 body = rsp.read()
386 body = rsp.read()
386 ui.debug(b'Conduit Response: %s\n' % body)
387 ui.debug(b'Conduit Response: %s\n' % body)
387 parsed = pycompat.rapply(
388 parsed = pycompat.rapply(
388 lambda x: encoding.unitolocal(x)
389 lambda x: encoding.unitolocal(x)
389 if isinstance(x, pycompat.unicode)
390 if isinstance(x, pycompat.unicode)
390 else x,
391 else x,
391 # json.loads only accepts bytes from py3.6+
392 # json.loads only accepts bytes from py3.6+
392 pycompat.json_loads(encoding.unifromlocal(body)),
393 pycompat.json_loads(encoding.unifromlocal(body)),
393 )
394 )
394 if parsed.get(b'error_code'):
395 if parsed.get(b'error_code'):
395 msg = _(b'Conduit Error (%s): %s') % (
396 msg = _(b'Conduit Error (%s): %s') % (
396 parsed[b'error_code'],
397 parsed[b'error_code'],
397 parsed[b'error_info'],
398 parsed[b'error_info'],
398 )
399 )
399 raise error.Abort(msg)
400 raise error.Abort(msg)
400 return parsed[b'result']
401 return parsed[b'result']
401
402
402
403
403 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
404 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
404 def debugcallconduit(ui, repo, name):
405 def debugcallconduit(ui, repo, name):
405 """call Conduit API
406 """call Conduit API
406
407
407 Call parameters are read from stdin as a JSON blob. Result will be written
408 Call parameters are read from stdin as a JSON blob. Result will be written
408 to stdout as a JSON blob.
409 to stdout as a JSON blob.
409 """
410 """
410 # json.loads only accepts bytes from 3.6+
411 # json.loads only accepts bytes from 3.6+
411 rawparams = encoding.unifromlocal(ui.fin.read())
412 rawparams = encoding.unifromlocal(ui.fin.read())
412 # json.loads only returns unicode strings
413 # json.loads only returns unicode strings
413 params = pycompat.rapply(
414 params = pycompat.rapply(
414 lambda x: encoding.unitolocal(x)
415 lambda x: encoding.unitolocal(x)
415 if isinstance(x, pycompat.unicode)
416 if isinstance(x, pycompat.unicode)
416 else x,
417 else x,
417 pycompat.json_loads(rawparams),
418 pycompat.json_loads(rawparams),
418 )
419 )
419 # json.dumps only accepts unicode strings
420 # json.dumps only accepts unicode strings
420 result = pycompat.rapply(
421 result = pycompat.rapply(
421 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
422 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
422 callconduit(ui, name, params),
423 callconduit(ui, name, params),
423 )
424 )
424 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
425 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
425 ui.write(b'%s\n' % encoding.unitolocal(s))
426 ui.write(b'%s\n' % encoding.unitolocal(s))
426
427
427
428
428 def getrepophid(repo):
429 def getrepophid(repo):
429 """given callsign, return repository PHID or None"""
430 """given callsign, return repository PHID or None"""
430 # developer config: phabricator.repophid
431 # developer config: phabricator.repophid
431 repophid = repo.ui.config(b'phabricator', b'repophid')
432 repophid = repo.ui.config(b'phabricator', b'repophid')
432 if repophid:
433 if repophid:
433 return repophid
434 return repophid
434 callsign = repo.ui.config(b'phabricator', b'callsign')
435 callsign = repo.ui.config(b'phabricator', b'callsign')
435 if not callsign:
436 if not callsign:
436 return None
437 return None
437 query = callconduit(
438 query = callconduit(
438 repo.ui,
439 repo.ui,
439 b'diffusion.repository.search',
440 b'diffusion.repository.search',
440 {b'constraints': {b'callsigns': [callsign]}},
441 {b'constraints': {b'callsigns': [callsign]}},
441 )
442 )
442 if len(query[b'data']) == 0:
443 if len(query[b'data']) == 0:
443 return None
444 return None
444 repophid = query[b'data'][0][b'phid']
445 repophid = query[b'data'][0][b'phid']
445 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
446 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
446 return repophid
447 return repophid
447
448
448
449
449 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
450 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
450 _differentialrevisiondescre = re.compile(
451 _differentialrevisiondescre = re.compile(
451 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
452 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
452 )
453 )
453
454
454
455
455 def getoldnodedrevmap(repo, nodelist):
456 def getoldnodedrevmap(repo, nodelist):
456 """find previous nodes that has been sent to Phabricator
457 """find previous nodes that has been sent to Phabricator
457
458
458 return {node: (oldnode, Differential diff, Differential Revision ID)}
459 return {node: (oldnode, Differential diff, Differential Revision ID)}
459 for node in nodelist with known previous sent versions, or associated
460 for node in nodelist with known previous sent versions, or associated
460 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
461 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
461 be ``None``.
462 be ``None``.
462
463
463 Examines commit messages like "Differential Revision:" to get the
464 Examines commit messages like "Differential Revision:" to get the
464 association information.
465 association information.
465
466
466 If such commit message line is not found, examines all precursors and their
467 If such commit message line is not found, examines all precursors and their
467 tags. Tags with format like "D1234" are considered a match and the node
468 tags. Tags with format like "D1234" are considered a match and the node
468 with that tag, and the number after "D" (ex. 1234) will be returned.
469 with that tag, and the number after "D" (ex. 1234) will be returned.
469
470
470 The ``old node``, if not None, is guaranteed to be the last diff of
471 The ``old node``, if not None, is guaranteed to be the last diff of
471 corresponding Differential Revision, and exist in the repo.
472 corresponding Differential Revision, and exist in the repo.
472 """
473 """
473 unfi = repo.unfiltered()
474 unfi = repo.unfiltered()
474 has_node = unfi.changelog.index.has_node
475 has_node = unfi.changelog.index.has_node
475
476
476 result = {} # {node: (oldnode?, lastdiff?, drev)}
477 result = {} # {node: (oldnode?, lastdiff?, drev)}
477 # ordered for test stability when printing new -> old mapping below
478 # ordered for test stability when printing new -> old mapping below
478 toconfirm = util.sortdict() # {node: (force, {precnode}, drev)}
479 toconfirm = util.sortdict() # {node: (force, {precnode}, drev)}
479 for node in nodelist:
480 for node in nodelist:
480 ctx = unfi[node]
481 ctx = unfi[node]
481 # For tags like "D123", put them into "toconfirm" to verify later
482 # For tags like "D123", put them into "toconfirm" to verify later
482 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
483 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
483 for n in precnodes:
484 for n in precnodes:
484 if has_node(n):
485 if has_node(n):
485 for tag in unfi.nodetags(n):
486 for tag in unfi.nodetags(n):
486 m = _differentialrevisiontagre.match(tag)
487 m = _differentialrevisiontagre.match(tag)
487 if m:
488 if m:
488 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
489 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
489 break
490 break
490 else:
491 else:
491 continue # move to next predecessor
492 continue # move to next predecessor
492 break # found a tag, stop
493 break # found a tag, stop
493 else:
494 else:
494 # Check commit message
495 # Check commit message
495 m = _differentialrevisiondescre.search(ctx.description())
496 m = _differentialrevisiondescre.search(ctx.description())
496 if m:
497 if m:
497 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
498 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
498
499
499 # Double check if tags are genuine by collecting all old nodes from
500 # Double check if tags are genuine by collecting all old nodes from
500 # Phabricator, and expect precursors overlap with it.
501 # Phabricator, and expect precursors overlap with it.
501 if toconfirm:
502 if toconfirm:
502 drevs = [drev for force, precs, drev in toconfirm.values()]
503 drevs = [drev for force, precs, drev in toconfirm.values()]
503 alldiffs = callconduit(
504 alldiffs = callconduit(
504 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
505 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
505 )
506 )
506
507
507 def getnodes(d, precset):
508 def getnodes(d, precset):
508 # Ignore other nodes that were combined into the Differential
509 # Ignore other nodes that were combined into the Differential
509 # that aren't predecessors of the current local node.
510 # that aren't predecessors of the current local node.
510 return [n for n in getlocalcommits(d) if n in precset]
511 return [n for n in getlocalcommits(d) if n in precset]
511
512
512 for newnode, (force, precset, drev) in toconfirm.items():
513 for newnode, (force, precset, drev) in toconfirm.items():
513 diffs = [
514 diffs = [
514 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
515 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
515 ]
516 ]
516
517
517 # local predecessors known by Phabricator
518 # local predecessors known by Phabricator
518 phprecset = {n for d in diffs for n in getnodes(d, precset)}
519 phprecset = {n for d in diffs for n in getnodes(d, precset)}
519
520
520 # Ignore if precursors (Phabricator and local repo) do not overlap,
521 # Ignore if precursors (Phabricator and local repo) do not overlap,
521 # and force is not set (when commit message says nothing)
522 # and force is not set (when commit message says nothing)
522 if not force and not phprecset:
523 if not force and not phprecset:
523 tagname = b'D%d' % drev
524 tagname = b'D%d' % drev
524 tags.tag(
525 tags.tag(
525 repo,
526 repo,
526 tagname,
527 tagname,
527 nullid,
528 nullid,
528 message=None,
529 message=None,
529 user=None,
530 user=None,
530 date=None,
531 date=None,
531 local=True,
532 local=True,
532 )
533 )
533 unfi.ui.warn(
534 unfi.ui.warn(
534 _(
535 _(
535 b'D%d: local tag removed - does not match '
536 b'D%d: local tag removed - does not match '
536 b'Differential history\n'
537 b'Differential history\n'
537 )
538 )
538 % drev
539 % drev
539 )
540 )
540 continue
541 continue
541
542
542 # Find the last node using Phabricator metadata, and make sure it
543 # Find the last node using Phabricator metadata, and make sure it
543 # exists in the repo
544 # exists in the repo
544 oldnode = lastdiff = None
545 oldnode = lastdiff = None
545 if diffs:
546 if diffs:
546 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
547 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
547 oldnodes = getnodes(lastdiff, precset)
548 oldnodes = getnodes(lastdiff, precset)
548
549
549 _debug(
550 _debug(
550 unfi.ui,
551 unfi.ui,
551 b"%s mapped to old nodes %s\n"
552 b"%s mapped to old nodes %s\n"
552 % (
553 % (
553 short(newnode),
554 short(newnode),
554 stringutil.pprint([short(n) for n in sorted(oldnodes)]),
555 stringutil.pprint([short(n) for n in sorted(oldnodes)]),
555 ),
556 ),
556 )
557 )
557
558
558 # If this commit was the result of `hg fold` after submission,
559 # If this commit was the result of `hg fold` after submission,
559 # and now resubmitted with --fold, the easiest thing to do is
560 # and now resubmitted with --fold, the easiest thing to do is
560 # to leave the node clear. This only results in creating a new
561 # to leave the node clear. This only results in creating a new
561 # diff for the _same_ Differential Revision if this commit is
562 # diff for the _same_ Differential Revision if this commit is
562 # the first or last in the selected range. If we picked a node
563 # the first or last in the selected range. If we picked a node
563 # from the list instead, it would have to be the lowest if at
564 # from the list instead, it would have to be the lowest if at
564 # the beginning of the --fold range, or the highest at the end.
565 # the beginning of the --fold range, or the highest at the end.
565 # Otherwise, one or more of the nodes wouldn't be considered in
566 # Otherwise, one or more of the nodes wouldn't be considered in
566 # the diff, and the Differential wouldn't be properly updated.
567 # the diff, and the Differential wouldn't be properly updated.
567 # If this commit is the result of `hg split` in the same
568 # If this commit is the result of `hg split` in the same
568 # scenario, there is a single oldnode here (and multiple
569 # scenario, there is a single oldnode here (and multiple
569 # newnodes mapped to it). That makes it the same as the normal
570 # newnodes mapped to it). That makes it the same as the normal
570 # case, as the edges of the newnode range cleanly maps to one
571 # case, as the edges of the newnode range cleanly maps to one
571 # oldnode each.
572 # oldnode each.
572 if len(oldnodes) == 1:
573 if len(oldnodes) == 1:
573 oldnode = oldnodes[0]
574 oldnode = oldnodes[0]
574 if oldnode and not has_node(oldnode):
575 if oldnode and not has_node(oldnode):
575 oldnode = None
576 oldnode = None
576
577
577 result[newnode] = (oldnode, lastdiff, drev)
578 result[newnode] = (oldnode, lastdiff, drev)
578
579
579 return result
580 return result
580
581
581
582
582 def getdrevmap(repo, revs):
583 def getdrevmap(repo, revs):
583 """Return a dict mapping each rev in `revs` to their Differential Revision
584 """Return a dict mapping each rev in `revs` to their Differential Revision
584 ID or None.
585 ID or None.
585 """
586 """
586 result = {}
587 result = {}
587 for rev in revs:
588 for rev in revs:
588 result[rev] = None
589 result[rev] = None
589 ctx = repo[rev]
590 ctx = repo[rev]
590 # Check commit message
591 # Check commit message
591 m = _differentialrevisiondescre.search(ctx.description())
592 m = _differentialrevisiondescre.search(ctx.description())
592 if m:
593 if m:
593 result[rev] = int(m.group('id'))
594 result[rev] = int(m.group('id'))
594 continue
595 continue
595 # Check tags
596 # Check tags
596 for tag in repo.nodetags(ctx.node()):
597 for tag in repo.nodetags(ctx.node()):
597 m = _differentialrevisiontagre.match(tag)
598 m = _differentialrevisiontagre.match(tag)
598 if m:
599 if m:
599 result[rev] = int(m.group(1))
600 result[rev] = int(m.group(1))
600 break
601 break
601
602
602 return result
603 return result
603
604
604
605
605 def getdiff(basectx, ctx, diffopts):
606 def getdiff(basectx, ctx, diffopts):
606 """plain-text diff without header (user, commit message, etc)"""
607 """plain-text diff without header (user, commit message, etc)"""
607 output = util.stringio()
608 output = util.stringio()
608 for chunk, _label in patch.diffui(
609 for chunk, _label in patch.diffui(
609 ctx.repo(), basectx.p1().node(), ctx.node(), None, opts=diffopts
610 ctx.repo(), basectx.p1().node(), ctx.node(), None, opts=diffopts
610 ):
611 ):
611 output.write(chunk)
612 output.write(chunk)
612 return output.getvalue()
613 return output.getvalue()
613
614
614
615
615 class DiffChangeType(object):
616 class DiffChangeType(object):
616 ADD = 1
617 ADD = 1
617 CHANGE = 2
618 CHANGE = 2
618 DELETE = 3
619 DELETE = 3
619 MOVE_AWAY = 4
620 MOVE_AWAY = 4
620 COPY_AWAY = 5
621 COPY_AWAY = 5
621 MOVE_HERE = 6
622 MOVE_HERE = 6
622 COPY_HERE = 7
623 COPY_HERE = 7
623 MULTICOPY = 8
624 MULTICOPY = 8
624
625
625
626
626 class DiffFileType(object):
627 class DiffFileType(object):
627 TEXT = 1
628 TEXT = 1
628 IMAGE = 2
629 IMAGE = 2
629 BINARY = 3
630 BINARY = 3
630
631
631
632
632 @attr.s
633 @attr.s
633 class phabhunk(dict):
634 class phabhunk(dict):
634 """Represents a Differential hunk, which is owned by a Differential change
635 """Represents a Differential hunk, which is owned by a Differential change
635 """
636 """
636
637
637 oldOffset = attr.ib(default=0) # camelcase-required
638 oldOffset = attr.ib(default=0) # camelcase-required
638 oldLength = attr.ib(default=0) # camelcase-required
639 oldLength = attr.ib(default=0) # camelcase-required
639 newOffset = attr.ib(default=0) # camelcase-required
640 newOffset = attr.ib(default=0) # camelcase-required
640 newLength = attr.ib(default=0) # camelcase-required
641 newLength = attr.ib(default=0) # camelcase-required
641 corpus = attr.ib(default='')
642 corpus = attr.ib(default='')
642 # These get added to the phabchange's equivalents
643 # These get added to the phabchange's equivalents
643 addLines = attr.ib(default=0) # camelcase-required
644 addLines = attr.ib(default=0) # camelcase-required
644 delLines = attr.ib(default=0) # camelcase-required
645 delLines = attr.ib(default=0) # camelcase-required
645
646
646
647
647 @attr.s
648 @attr.s
648 class phabchange(object):
649 class phabchange(object):
649 """Represents a Differential change, owns Differential hunks and owned by a
650 """Represents a Differential change, owns Differential hunks and owned by a
650 Differential diff. Each one represents one file in a diff.
651 Differential diff. Each one represents one file in a diff.
651 """
652 """
652
653
653 currentPath = attr.ib(default=None) # camelcase-required
654 currentPath = attr.ib(default=None) # camelcase-required
654 oldPath = attr.ib(default=None) # camelcase-required
655 oldPath = attr.ib(default=None) # camelcase-required
655 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
656 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
656 metadata = attr.ib(default=attr.Factory(dict))
657 metadata = attr.ib(default=attr.Factory(dict))
657 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
658 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
658 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
659 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
659 type = attr.ib(default=DiffChangeType.CHANGE)
660 type = attr.ib(default=DiffChangeType.CHANGE)
660 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
661 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
661 commitHash = attr.ib(default=None) # camelcase-required
662 commitHash = attr.ib(default=None) # camelcase-required
662 addLines = attr.ib(default=0) # camelcase-required
663 addLines = attr.ib(default=0) # camelcase-required
663 delLines = attr.ib(default=0) # camelcase-required
664 delLines = attr.ib(default=0) # camelcase-required
664 hunks = attr.ib(default=attr.Factory(list))
665 hunks = attr.ib(default=attr.Factory(list))
665
666
666 def copynewmetadatatoold(self):
667 def copynewmetadatatoold(self):
667 for key in list(self.metadata.keys()):
668 for key in list(self.metadata.keys()):
668 newkey = key.replace(b'new:', b'old:')
669 newkey = key.replace(b'new:', b'old:')
669 self.metadata[newkey] = self.metadata[key]
670 self.metadata[newkey] = self.metadata[key]
670
671
671 def addoldmode(self, value):
672 def addoldmode(self, value):
672 self.oldProperties[b'unix:filemode'] = value
673 self.oldProperties[b'unix:filemode'] = value
673
674
674 def addnewmode(self, value):
675 def addnewmode(self, value):
675 self.newProperties[b'unix:filemode'] = value
676 self.newProperties[b'unix:filemode'] = value
676
677
677 def addhunk(self, hunk):
678 def addhunk(self, hunk):
678 if not isinstance(hunk, phabhunk):
679 if not isinstance(hunk, phabhunk):
679 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
680 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
680 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
681 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
681 # It's useful to include these stats since the Phab web UI shows them,
682 # It's useful to include these stats since the Phab web UI shows them,
682 # and uses them to estimate how large a change a Revision is. Also used
683 # and uses them to estimate how large a change a Revision is. Also used
683 # in email subjects for the [+++--] bit.
684 # in email subjects for the [+++--] bit.
684 self.addLines += hunk.addLines
685 self.addLines += hunk.addLines
685 self.delLines += hunk.delLines
686 self.delLines += hunk.delLines
686
687
687
688
688 @attr.s
689 @attr.s
689 class phabdiff(object):
690 class phabdiff(object):
690 """Represents a Differential diff, owns Differential changes. Corresponds
691 """Represents a Differential diff, owns Differential changes. Corresponds
691 to a commit.
692 to a commit.
692 """
693 """
693
694
694 # Doesn't seem to be any reason to send this (output of uname -n)
695 # Doesn't seem to be any reason to send this (output of uname -n)
695 sourceMachine = attr.ib(default=b'') # camelcase-required
696 sourceMachine = attr.ib(default=b'') # camelcase-required
696 sourcePath = attr.ib(default=b'/') # camelcase-required
697 sourcePath = attr.ib(default=b'/') # camelcase-required
697 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
698 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
698 sourceControlPath = attr.ib(default=b'/') # camelcase-required
699 sourceControlPath = attr.ib(default=b'/') # camelcase-required
699 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
700 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
700 branch = attr.ib(default=b'default')
701 branch = attr.ib(default=b'default')
701 bookmark = attr.ib(default=None)
702 bookmark = attr.ib(default=None)
702 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
703 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
703 lintStatus = attr.ib(default=b'none') # camelcase-required
704 lintStatus = attr.ib(default=b'none') # camelcase-required
704 unitStatus = attr.ib(default=b'none') # camelcase-required
705 unitStatus = attr.ib(default=b'none') # camelcase-required
705 changes = attr.ib(default=attr.Factory(dict))
706 changes = attr.ib(default=attr.Factory(dict))
706 repositoryPHID = attr.ib(default=None) # camelcase-required
707 repositoryPHID = attr.ib(default=None) # camelcase-required
707
708
708 def addchange(self, change):
709 def addchange(self, change):
709 if not isinstance(change, phabchange):
710 if not isinstance(change, phabchange):
710 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
711 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
711 self.changes[change.currentPath] = pycompat.byteskwargs(
712 self.changes[change.currentPath] = pycompat.byteskwargs(
712 attr.asdict(change)
713 attr.asdict(change)
713 )
714 )
714
715
715
716
716 def maketext(pchange, basectx, ctx, fname):
717 def maketext(pchange, basectx, ctx, fname):
717 """populate the phabchange for a text file"""
718 """populate the phabchange for a text file"""
718 repo = ctx.repo()
719 repo = ctx.repo()
719 fmatcher = match.exact([fname])
720 fmatcher = match.exact([fname])
720 diffopts = mdiff.diffopts(git=True, context=32767)
721 diffopts = mdiff.diffopts(git=True, context=32767)
721 _pfctx, _fctx, header, fhunks = next(
722 _pfctx, _fctx, header, fhunks = next(
722 patch.diffhunks(repo, basectx.p1(), ctx, fmatcher, opts=diffopts)
723 patch.diffhunks(repo, basectx.p1(), ctx, fmatcher, opts=diffopts)
723 )
724 )
724
725
725 for fhunk in fhunks:
726 for fhunk in fhunks:
726 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
727 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
727 corpus = b''.join(lines[1:])
728 corpus = b''.join(lines[1:])
728 shunk = list(header)
729 shunk = list(header)
729 shunk.extend(lines)
730 shunk.extend(lines)
730 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
731 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
731 patch.diffstatdata(util.iterlines(shunk))
732 patch.diffstatdata(util.iterlines(shunk))
732 )
733 )
733 pchange.addhunk(
734 pchange.addhunk(
734 phabhunk(
735 phabhunk(
735 oldOffset,
736 oldOffset,
736 oldLength,
737 oldLength,
737 newOffset,
738 newOffset,
738 newLength,
739 newLength,
739 corpus,
740 corpus,
740 addLines,
741 addLines,
741 delLines,
742 delLines,
742 )
743 )
743 )
744 )
744
745
745
746
746 def uploadchunks(fctx, fphid):
747 def uploadchunks(fctx, fphid):
747 """upload large binary files as separate chunks.
748 """upload large binary files as separate chunks.
748 Phab requests chunking over 8MiB, and splits into 4MiB chunks
749 Phab requests chunking over 8MiB, and splits into 4MiB chunks
749 """
750 """
750 ui = fctx.repo().ui
751 ui = fctx.repo().ui
751 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
752 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
752 with ui.makeprogress(
753 with ui.makeprogress(
753 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
754 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
754 ) as progress:
755 ) as progress:
755 for chunk in chunks:
756 for chunk in chunks:
756 progress.increment()
757 progress.increment()
757 if chunk[b'complete']:
758 if chunk[b'complete']:
758 continue
759 continue
759 bstart = int(chunk[b'byteStart'])
760 bstart = int(chunk[b'byteStart'])
760 bend = int(chunk[b'byteEnd'])
761 bend = int(chunk[b'byteEnd'])
761 callconduit(
762 callconduit(
762 ui,
763 ui,
763 b'file.uploadchunk',
764 b'file.uploadchunk',
764 {
765 {
765 b'filePHID': fphid,
766 b'filePHID': fphid,
766 b'byteStart': bstart,
767 b'byteStart': bstart,
767 b'data': base64.b64encode(fctx.data()[bstart:bend]),
768 b'data': base64.b64encode(fctx.data()[bstart:bend]),
768 b'dataEncoding': b'base64',
769 b'dataEncoding': b'base64',
769 },
770 },
770 )
771 )
771
772
772
773
773 def uploadfile(fctx):
774 def uploadfile(fctx):
774 """upload binary files to Phabricator"""
775 """upload binary files to Phabricator"""
775 repo = fctx.repo()
776 repo = fctx.repo()
776 ui = repo.ui
777 ui = repo.ui
777 fname = fctx.path()
778 fname = fctx.path()
778 size = fctx.size()
779 size = fctx.size()
779 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
780 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
780
781
781 # an allocate call is required first to see if an upload is even required
782 # an allocate call is required first to see if an upload is even required
782 # (Phab might already have it) and to determine if chunking is needed
783 # (Phab might already have it) and to determine if chunking is needed
783 allocateparams = {
784 allocateparams = {
784 b'name': fname,
785 b'name': fname,
785 b'contentLength': size,
786 b'contentLength': size,
786 b'contentHash': fhash,
787 b'contentHash': fhash,
787 }
788 }
788 filealloc = callconduit(ui, b'file.allocate', allocateparams)
789 filealloc = callconduit(ui, b'file.allocate', allocateparams)
789 fphid = filealloc[b'filePHID']
790 fphid = filealloc[b'filePHID']
790
791
791 if filealloc[b'upload']:
792 if filealloc[b'upload']:
792 ui.write(_(b'uploading %s\n') % bytes(fctx))
793 ui.write(_(b'uploading %s\n') % bytes(fctx))
793 if not fphid:
794 if not fphid:
794 uploadparams = {
795 uploadparams = {
795 b'name': fname,
796 b'name': fname,
796 b'data_base64': base64.b64encode(fctx.data()),
797 b'data_base64': base64.b64encode(fctx.data()),
797 }
798 }
798 fphid = callconduit(ui, b'file.upload', uploadparams)
799 fphid = callconduit(ui, b'file.upload', uploadparams)
799 else:
800 else:
800 uploadchunks(fctx, fphid)
801 uploadchunks(fctx, fphid)
801 else:
802 else:
802 ui.debug(b'server already has %s\n' % bytes(fctx))
803 ui.debug(b'server already has %s\n' % bytes(fctx))
803
804
804 if not fphid:
805 if not fphid:
805 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
806 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
806
807
807 return fphid
808 return fphid
808
809
809
810
810 def addoldbinary(pchange, oldfctx, fctx):
811 def addoldbinary(pchange, oldfctx, fctx):
811 """add the metadata for the previous version of a binary file to the
812 """add the metadata for the previous version of a binary file to the
812 phabchange for the new version
813 phabchange for the new version
813
814
814 ``oldfctx`` is the previous version of the file; ``fctx`` is the new
815 ``oldfctx`` is the previous version of the file; ``fctx`` is the new
815 version of the file, or None if the file is being removed.
816 version of the file, or None if the file is being removed.
816 """
817 """
817 if not fctx or fctx.cmp(oldfctx):
818 if not fctx or fctx.cmp(oldfctx):
818 # Files differ, add the old one
819 # Files differ, add the old one
819 pchange.metadata[b'old:file:size'] = oldfctx.size()
820 pchange.metadata[b'old:file:size'] = oldfctx.size()
820 mimeguess, _enc = mimetypes.guess_type(
821 mimeguess, _enc = mimetypes.guess_type(
821 encoding.unifromlocal(oldfctx.path())
822 encoding.unifromlocal(oldfctx.path())
822 )
823 )
823 if mimeguess:
824 if mimeguess:
824 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
825 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
825 mimeguess
826 mimeguess
826 )
827 )
827 fphid = uploadfile(oldfctx)
828 fphid = uploadfile(oldfctx)
828 pchange.metadata[b'old:binary-phid'] = fphid
829 pchange.metadata[b'old:binary-phid'] = fphid
829 else:
830 else:
830 # If it's left as IMAGE/BINARY web UI might try to display it
831 # If it's left as IMAGE/BINARY web UI might try to display it
831 pchange.fileType = DiffFileType.TEXT
832 pchange.fileType = DiffFileType.TEXT
832 pchange.copynewmetadatatoold()
833 pchange.copynewmetadatatoold()
833
834
834
835
835 def makebinary(pchange, fctx):
836 def makebinary(pchange, fctx):
836 """populate the phabchange for a binary file"""
837 """populate the phabchange for a binary file"""
837 pchange.fileType = DiffFileType.BINARY
838 pchange.fileType = DiffFileType.BINARY
838 fphid = uploadfile(fctx)
839 fphid = uploadfile(fctx)
839 pchange.metadata[b'new:binary-phid'] = fphid
840 pchange.metadata[b'new:binary-phid'] = fphid
840 pchange.metadata[b'new:file:size'] = fctx.size()
841 pchange.metadata[b'new:file:size'] = fctx.size()
841 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
842 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
842 if mimeguess:
843 if mimeguess:
843 mimeguess = pycompat.bytestr(mimeguess)
844 mimeguess = pycompat.bytestr(mimeguess)
844 pchange.metadata[b'new:file:mime-type'] = mimeguess
845 pchange.metadata[b'new:file:mime-type'] = mimeguess
845 if mimeguess.startswith(b'image/'):
846 if mimeguess.startswith(b'image/'):
846 pchange.fileType = DiffFileType.IMAGE
847 pchange.fileType = DiffFileType.IMAGE
847
848
848
849
849 # Copied from mercurial/patch.py
850 # Copied from mercurial/patch.py
850 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
851 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
851
852
852
853
853 def notutf8(fctx):
854 def notutf8(fctx):
854 """detect non-UTF-8 text files since Phabricator requires them to be marked
855 """detect non-UTF-8 text files since Phabricator requires them to be marked
855 as binary
856 as binary
856 """
857 """
857 try:
858 try:
858 fctx.data().decode('utf-8')
859 fctx.data().decode('utf-8')
859 return False
860 return False
860 except UnicodeDecodeError:
861 except UnicodeDecodeError:
861 fctx.repo().ui.write(
862 fctx.repo().ui.write(
862 _(b'file %s detected as non-UTF-8, marked as binary\n')
863 _(b'file %s detected as non-UTF-8, marked as binary\n')
863 % fctx.path()
864 % fctx.path()
864 )
865 )
865 return True
866 return True
866
867
867
868
868 def addremoved(pdiff, basectx, ctx, removed):
869 def addremoved(pdiff, basectx, ctx, removed):
869 """add removed files to the phabdiff. Shouldn't include moves"""
870 """add removed files to the phabdiff. Shouldn't include moves"""
870 for fname in removed:
871 for fname in removed:
871 pchange = phabchange(
872 pchange = phabchange(
872 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
873 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
873 )
874 )
874 oldfctx = basectx.p1()[fname]
875 oldfctx = basectx.p1()[fname]
875 pchange.addoldmode(gitmode[oldfctx.flags()])
876 pchange.addoldmode(gitmode[oldfctx.flags()])
876 if not (oldfctx.isbinary() or notutf8(oldfctx)):
877 if not (oldfctx.isbinary() or notutf8(oldfctx)):
877 maketext(pchange, basectx, ctx, fname)
878 maketext(pchange, basectx, ctx, fname)
878
879
879 pdiff.addchange(pchange)
880 pdiff.addchange(pchange)
880
881
881
882
882 def addmodified(pdiff, basectx, ctx, modified):
883 def addmodified(pdiff, basectx, ctx, modified):
883 """add modified files to the phabdiff"""
884 """add modified files to the phabdiff"""
884 for fname in modified:
885 for fname in modified:
885 fctx = ctx[fname]
886 fctx = ctx[fname]
886 oldfctx = basectx.p1()[fname]
887 oldfctx = basectx.p1()[fname]
887 pchange = phabchange(currentPath=fname, oldPath=fname)
888 pchange = phabchange(currentPath=fname, oldPath=fname)
888 filemode = gitmode[fctx.flags()]
889 filemode = gitmode[fctx.flags()]
889 originalmode = gitmode[oldfctx.flags()]
890 originalmode = gitmode[oldfctx.flags()]
890 if filemode != originalmode:
891 if filemode != originalmode:
891 pchange.addoldmode(originalmode)
892 pchange.addoldmode(originalmode)
892 pchange.addnewmode(filemode)
893 pchange.addnewmode(filemode)
893
894
894 if (
895 if (
895 fctx.isbinary()
896 fctx.isbinary()
896 or notutf8(fctx)
897 or notutf8(fctx)
897 or oldfctx.isbinary()
898 or oldfctx.isbinary()
898 or notutf8(oldfctx)
899 or notutf8(oldfctx)
899 ):
900 ):
900 makebinary(pchange, fctx)
901 makebinary(pchange, fctx)
901 addoldbinary(pchange, oldfctx, fctx)
902 addoldbinary(pchange, oldfctx, fctx)
902 else:
903 else:
903 maketext(pchange, basectx, ctx, fname)
904 maketext(pchange, basectx, ctx, fname)
904
905
905 pdiff.addchange(pchange)
906 pdiff.addchange(pchange)
906
907
907
908
908 def addadded(pdiff, basectx, ctx, added, removed):
909 def addadded(pdiff, basectx, ctx, added, removed):
909 """add file adds to the phabdiff, both new files and copies/moves"""
910 """add file adds to the phabdiff, both new files and copies/moves"""
910 # Keep track of files that've been recorded as moved/copied, so if there are
911 # Keep track of files that've been recorded as moved/copied, so if there are
911 # additional copies we can mark them (moves get removed from removed)
912 # additional copies we can mark them (moves get removed from removed)
912 copiedchanges = {}
913 copiedchanges = {}
913 movedchanges = {}
914 movedchanges = {}
914
915
915 copy = {}
916 copy = {}
916 if basectx != ctx:
917 if basectx != ctx:
917 copy = copies.pathcopies(basectx.p1(), ctx)
918 copy = copies.pathcopies(basectx.p1(), ctx)
918
919
919 for fname in added:
920 for fname in added:
920 fctx = ctx[fname]
921 fctx = ctx[fname]
921 oldfctx = None
922 oldfctx = None
922 pchange = phabchange(currentPath=fname)
923 pchange = phabchange(currentPath=fname)
923
924
924 filemode = gitmode[fctx.flags()]
925 filemode = gitmode[fctx.flags()]
925
926
926 if copy:
927 if copy:
927 originalfname = copy.get(fname, fname)
928 originalfname = copy.get(fname, fname)
928 else:
929 else:
929 originalfname = fname
930 originalfname = fname
930 if fctx.renamed():
931 if fctx.renamed():
931 originalfname = fctx.renamed()[0]
932 originalfname = fctx.renamed()[0]
932
933
933 renamed = fname != originalfname
934 renamed = fname != originalfname
934
935
935 if renamed:
936 if renamed:
936 oldfctx = basectx.p1()[originalfname]
937 oldfctx = basectx.p1()[originalfname]
937 originalmode = gitmode[oldfctx.flags()]
938 originalmode = gitmode[oldfctx.flags()]
938 pchange.oldPath = originalfname
939 pchange.oldPath = originalfname
939
940
940 if originalfname in removed:
941 if originalfname in removed:
941 origpchange = phabchange(
942 origpchange = phabchange(
942 currentPath=originalfname,
943 currentPath=originalfname,
943 oldPath=originalfname,
944 oldPath=originalfname,
944 type=DiffChangeType.MOVE_AWAY,
945 type=DiffChangeType.MOVE_AWAY,
945 awayPaths=[fname],
946 awayPaths=[fname],
946 )
947 )
947 movedchanges[originalfname] = origpchange
948 movedchanges[originalfname] = origpchange
948 removed.remove(originalfname)
949 removed.remove(originalfname)
949 pchange.type = DiffChangeType.MOVE_HERE
950 pchange.type = DiffChangeType.MOVE_HERE
950 elif originalfname in movedchanges:
951 elif originalfname in movedchanges:
951 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
952 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
952 movedchanges[originalfname].awayPaths.append(fname)
953 movedchanges[originalfname].awayPaths.append(fname)
953 pchange.type = DiffChangeType.COPY_HERE
954 pchange.type = DiffChangeType.COPY_HERE
954 else: # pure copy
955 else: # pure copy
955 if originalfname not in copiedchanges:
956 if originalfname not in copiedchanges:
956 origpchange = phabchange(
957 origpchange = phabchange(
957 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
958 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
958 )
959 )
959 copiedchanges[originalfname] = origpchange
960 copiedchanges[originalfname] = origpchange
960 else:
961 else:
961 origpchange = copiedchanges[originalfname]
962 origpchange = copiedchanges[originalfname]
962 origpchange.awayPaths.append(fname)
963 origpchange.awayPaths.append(fname)
963 pchange.type = DiffChangeType.COPY_HERE
964 pchange.type = DiffChangeType.COPY_HERE
964
965
965 if filemode != originalmode:
966 if filemode != originalmode:
966 pchange.addoldmode(originalmode)
967 pchange.addoldmode(originalmode)
967 pchange.addnewmode(filemode)
968 pchange.addnewmode(filemode)
968 else: # Brand-new file
969 else: # Brand-new file
969 pchange.addnewmode(gitmode[fctx.flags()])
970 pchange.addnewmode(gitmode[fctx.flags()])
970 pchange.type = DiffChangeType.ADD
971 pchange.type = DiffChangeType.ADD
971
972
972 if (
973 if (
973 fctx.isbinary()
974 fctx.isbinary()
974 or notutf8(fctx)
975 or notutf8(fctx)
975 or (oldfctx and (oldfctx.isbinary() or notutf8(oldfctx)))
976 or (oldfctx and (oldfctx.isbinary() or notutf8(oldfctx)))
976 ):
977 ):
977 makebinary(pchange, fctx)
978 makebinary(pchange, fctx)
978 if renamed:
979 if renamed:
979 addoldbinary(pchange, oldfctx, fctx)
980 addoldbinary(pchange, oldfctx, fctx)
980 else:
981 else:
981 maketext(pchange, basectx, ctx, fname)
982 maketext(pchange, basectx, ctx, fname)
982
983
983 pdiff.addchange(pchange)
984 pdiff.addchange(pchange)
984
985
985 for _path, copiedchange in copiedchanges.items():
986 for _path, copiedchange in copiedchanges.items():
986 pdiff.addchange(copiedchange)
987 pdiff.addchange(copiedchange)
987 for _path, movedchange in movedchanges.items():
988 for _path, movedchange in movedchanges.items():
988 pdiff.addchange(movedchange)
989 pdiff.addchange(movedchange)
989
990
990
991
991 def creatediff(basectx, ctx):
992 def creatediff(basectx, ctx):
992 """create a Differential Diff"""
993 """create a Differential Diff"""
993 repo = ctx.repo()
994 repo = ctx.repo()
994 repophid = getrepophid(repo)
995 repophid = getrepophid(repo)
995 # Create a "Differential Diff" via "differential.creatediff" API
996 # Create a "Differential Diff" via "differential.creatediff" API
996 pdiff = phabdiff(
997 pdiff = phabdiff(
997 sourceControlBaseRevision=b'%s' % basectx.p1().hex(),
998 sourceControlBaseRevision=b'%s' % basectx.p1().hex(),
998 branch=b'%s' % ctx.branch(),
999 branch=b'%s' % ctx.branch(),
999 )
1000 )
1000 modified, added, removed, _d, _u, _i, _c = basectx.p1().status(ctx)
1001 modified, added, removed, _d, _u, _i, _c = basectx.p1().status(ctx)
1001 # addadded will remove moved files from removed, so addremoved won't get
1002 # addadded will remove moved files from removed, so addremoved won't get
1002 # them
1003 # them
1003 addadded(pdiff, basectx, ctx, added, removed)
1004 addadded(pdiff, basectx, ctx, added, removed)
1004 addmodified(pdiff, basectx, ctx, modified)
1005 addmodified(pdiff, basectx, ctx, modified)
1005 addremoved(pdiff, basectx, ctx, removed)
1006 addremoved(pdiff, basectx, ctx, removed)
1006 if repophid:
1007 if repophid:
1007 pdiff.repositoryPHID = repophid
1008 pdiff.repositoryPHID = repophid
1008 diff = callconduit(
1009 diff = callconduit(
1009 repo.ui,
1010 repo.ui,
1010 b'differential.creatediff',
1011 b'differential.creatediff',
1011 pycompat.byteskwargs(attr.asdict(pdiff)),
1012 pycompat.byteskwargs(attr.asdict(pdiff)),
1012 )
1013 )
1013 if not diff:
1014 if not diff:
1014 if basectx != ctx:
1015 if basectx != ctx:
1015 msg = _(b'cannot create diff for %s::%s') % (basectx, ctx)
1016 msg = _(b'cannot create diff for %s::%s') % (basectx, ctx)
1016 else:
1017 else:
1017 msg = _(b'cannot create diff for %s') % ctx
1018 msg = _(b'cannot create diff for %s') % ctx
1018 raise error.Abort(msg)
1019 raise error.Abort(msg)
1019 return diff
1020 return diff
1020
1021
1021
1022
1022 def writediffproperties(ctxs, diff):
1023 def writediffproperties(ctxs, diff):
1023 """write metadata to diff so patches could be applied losslessly
1024 """write metadata to diff so patches could be applied losslessly
1024
1025
1025 ``ctxs`` is the list of commits that created the diff, in ascending order.
1026 ``ctxs`` is the list of commits that created the diff, in ascending order.
1026 The list is generally a single commit, but may be several when using
1027 The list is generally a single commit, but may be several when using
1027 ``phabsend --fold``.
1028 ``phabsend --fold``.
1028 """
1029 """
1029 # creatediff returns with a diffid but query returns with an id
1030 # creatediff returns with a diffid but query returns with an id
1030 diffid = diff.get(b'diffid', diff.get(b'id'))
1031 diffid = diff.get(b'diffid', diff.get(b'id'))
1031 basectx = ctxs[0]
1032 basectx = ctxs[0]
1032 tipctx = ctxs[-1]
1033 tipctx = ctxs[-1]
1033
1034
1034 params = {
1035 params = {
1035 b'diff_id': diffid,
1036 b'diff_id': diffid,
1036 b'name': b'hg:meta',
1037 b'name': b'hg:meta',
1037 b'data': templatefilters.json(
1038 b'data': templatefilters.json(
1038 {
1039 {
1039 b'user': tipctx.user(),
1040 b'user': tipctx.user(),
1040 b'date': b'%d %d' % tipctx.date(),
1041 b'date': b'%d %d' % tipctx.date(),
1041 b'branch': tipctx.branch(),
1042 b'branch': tipctx.branch(),
1042 b'node': tipctx.hex(),
1043 b'node': tipctx.hex(),
1043 b'parent': basectx.p1().hex(),
1044 b'parent': basectx.p1().hex(),
1044 }
1045 }
1045 ),
1046 ),
1046 }
1047 }
1047 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1048 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1048
1049
1049 commits = {}
1050 commits = {}
1050 for ctx in ctxs:
1051 for ctx in ctxs:
1051 commits[ctx.hex()] = {
1052 commits[ctx.hex()] = {
1052 b'author': stringutil.person(ctx.user()),
1053 b'author': stringutil.person(ctx.user()),
1053 b'authorEmail': stringutil.email(ctx.user()),
1054 b'authorEmail': stringutil.email(ctx.user()),
1054 b'time': int(ctx.date()[0]),
1055 b'time': int(ctx.date()[0]),
1055 b'commit': ctx.hex(),
1056 b'commit': ctx.hex(),
1056 b'parents': [ctx.p1().hex()],
1057 b'parents': [ctx.p1().hex()],
1057 b'branch': ctx.branch(),
1058 b'branch': ctx.branch(),
1058 }
1059 }
1059 params = {
1060 params = {
1060 b'diff_id': diffid,
1061 b'diff_id': diffid,
1061 b'name': b'local:commits',
1062 b'name': b'local:commits',
1062 b'data': templatefilters.json(commits),
1063 b'data': templatefilters.json(commits),
1063 }
1064 }
1064 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1065 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1065
1066
1066
1067
1067 def createdifferentialrevision(
1068 def createdifferentialrevision(
1068 ctxs,
1069 ctxs,
1069 revid=None,
1070 revid=None,
1070 parentrevphid=None,
1071 parentrevphid=None,
1071 oldbasenode=None,
1072 oldbasenode=None,
1072 oldnode=None,
1073 oldnode=None,
1073 olddiff=None,
1074 olddiff=None,
1074 actions=None,
1075 actions=None,
1075 comment=None,
1076 comment=None,
1076 ):
1077 ):
1077 """create or update a Differential Revision
1078 """create or update a Differential Revision
1078
1079
1079 If revid is None, create a new Differential Revision, otherwise update
1080 If revid is None, create a new Differential Revision, otherwise update
1080 revid. If parentrevphid is not None, set it as a dependency.
1081 revid. If parentrevphid is not None, set it as a dependency.
1081
1082
1082 If there is a single commit for the new Differential Revision, ``ctxs`` will
1083 If there is a single commit for the new Differential Revision, ``ctxs`` will
1083 be a list of that single context. Otherwise, it is a list that covers the
1084 be a list of that single context. Otherwise, it is a list that covers the
1084 range of changes for the differential, where ``ctxs[0]`` is the first change
1085 range of changes for the differential, where ``ctxs[0]`` is the first change
1085 to include and ``ctxs[-1]`` is the last.
1086 to include and ``ctxs[-1]`` is the last.
1086
1087
1087 If oldnode is not None, check if the patch content (without commit message
1088 If oldnode is not None, check if the patch content (without commit message
1088 and metadata) has changed before creating another diff. For a Revision with
1089 and metadata) has changed before creating another diff. For a Revision with
1089 a single commit, ``oldbasenode`` and ``oldnode`` have the same value. For a
1090 a single commit, ``oldbasenode`` and ``oldnode`` have the same value. For a
1090 Revision covering multiple commits, ``oldbasenode`` corresponds to
1091 Revision covering multiple commits, ``oldbasenode`` corresponds to
1091 ``ctxs[0]`` the previous time this Revision was posted, and ``oldnode``
1092 ``ctxs[0]`` the previous time this Revision was posted, and ``oldnode``
1092 corresponds to ``ctxs[-1]``.
1093 corresponds to ``ctxs[-1]``.
1093
1094
1094 If actions is not None, they will be appended to the transaction.
1095 If actions is not None, they will be appended to the transaction.
1095 """
1096 """
1096 ctx = ctxs[-1]
1097 ctx = ctxs[-1]
1097 basectx = ctxs[0]
1098 basectx = ctxs[0]
1098
1099
1099 repo = ctx.repo()
1100 repo = ctx.repo()
1100 if oldnode:
1101 if oldnode:
1101 diffopts = mdiff.diffopts(git=True, context=32767)
1102 diffopts = mdiff.diffopts(git=True, context=32767)
1102 unfi = repo.unfiltered()
1103 unfi = repo.unfiltered()
1103 oldctx = unfi[oldnode]
1104 oldctx = unfi[oldnode]
1104 oldbasectx = unfi[oldbasenode]
1105 oldbasectx = unfi[oldbasenode]
1105 neednewdiff = getdiff(basectx, ctx, diffopts) != getdiff(
1106 neednewdiff = getdiff(basectx, ctx, diffopts) != getdiff(
1106 oldbasectx, oldctx, diffopts
1107 oldbasectx, oldctx, diffopts
1107 )
1108 )
1108 else:
1109 else:
1109 neednewdiff = True
1110 neednewdiff = True
1110
1111
1111 transactions = []
1112 transactions = []
1112 if neednewdiff:
1113 if neednewdiff:
1113 diff = creatediff(basectx, ctx)
1114 diff = creatediff(basectx, ctx)
1114 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1115 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1115 if comment:
1116 if comment:
1116 transactions.append({b'type': b'comment', b'value': comment})
1117 transactions.append({b'type': b'comment', b'value': comment})
1117 else:
1118 else:
1118 # Even if we don't need to upload a new diff because the patch content
1119 # Even if we don't need to upload a new diff because the patch content
1119 # does not change. We might still need to update its metadata so
1120 # does not change. We might still need to update its metadata so
1120 # pushers could know the correct node metadata.
1121 # pushers could know the correct node metadata.
1121 assert olddiff
1122 assert olddiff
1122 diff = olddiff
1123 diff = olddiff
1123 writediffproperties(ctxs, diff)
1124 writediffproperties(ctxs, diff)
1124
1125
1125 # Set the parent Revision every time, so commit re-ordering is picked-up
1126 # Set the parent Revision every time, so commit re-ordering is picked-up
1126 if parentrevphid:
1127 if parentrevphid:
1127 transactions.append(
1128 transactions.append(
1128 {b'type': b'parents.set', b'value': [parentrevphid]}
1129 {b'type': b'parents.set', b'value': [parentrevphid]}
1129 )
1130 )
1130
1131
1131 if actions:
1132 if actions:
1132 transactions += actions
1133 transactions += actions
1133
1134
1134 # When folding multiple local commits into a single review, arcanist will
1135 # When folding multiple local commits into a single review, arcanist will
1135 # take the summary line of the first commit as the title, and then
1136 # take the summary line of the first commit as the title, and then
1136 # concatenate the rest of the remaining messages (including each of their
1137 # concatenate the rest of the remaining messages (including each of their
1137 # first lines) to the rest of the first commit message (each separated by
1138 # first lines) to the rest of the first commit message (each separated by
1138 # an empty line), and use that as the summary field. Do the same here.
1139 # an empty line), and use that as the summary field. Do the same here.
1139 # For commits with only a one line message, there is no summary field, as
1140 # For commits with only a one line message, there is no summary field, as
1140 # this gets assigned to the title.
1141 # this gets assigned to the title.
1141 fields = util.sortdict() # sorted for stable wire protocol in tests
1142 fields = util.sortdict() # sorted for stable wire protocol in tests
1142
1143
1143 for i, _ctx in enumerate(ctxs):
1144 for i, _ctx in enumerate(ctxs):
1144 # Parse commit message and update related fields.
1145 # Parse commit message and update related fields.
1145 desc = _ctx.description()
1146 desc = _ctx.description()
1146 info = callconduit(
1147 info = callconduit(
1147 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1148 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1148 )
1149 )
1149
1150
1150 for k in [b'title', b'summary', b'testPlan']:
1151 for k in [b'title', b'summary', b'testPlan']:
1151 v = info[b'fields'].get(k)
1152 v = info[b'fields'].get(k)
1152 if not v:
1153 if not v:
1153 continue
1154 continue
1154
1155
1155 if i == 0:
1156 if i == 0:
1156 # Title, summary and test plan (if present) are taken verbatim
1157 # Title, summary and test plan (if present) are taken verbatim
1157 # for the first commit.
1158 # for the first commit.
1158 fields[k] = v.rstrip()
1159 fields[k] = v.rstrip()
1159 continue
1160 continue
1160 elif k == b'title':
1161 elif k == b'title':
1161 # Add subsequent titles (i.e. the first line of the commit
1162 # Add subsequent titles (i.e. the first line of the commit
1162 # message) back to the summary.
1163 # message) back to the summary.
1163 k = b'summary'
1164 k = b'summary'
1164
1165
1165 # Append any current field to the existing composite field
1166 # Append any current field to the existing composite field
1166 fields[k] = b'\n\n'.join(filter(None, [fields.get(k), v.rstrip()]))
1167 fields[k] = b'\n\n'.join(filter(None, [fields.get(k), v.rstrip()]))
1167
1168
1168 for k, v in fields.items():
1169 for k, v in fields.items():
1169 transactions.append({b'type': k, b'value': v})
1170 transactions.append({b'type': k, b'value': v})
1170
1171
1171 params = {b'transactions': transactions}
1172 params = {b'transactions': transactions}
1172 if revid is not None:
1173 if revid is not None:
1173 # Update an existing Differential Revision
1174 # Update an existing Differential Revision
1174 params[b'objectIdentifier'] = revid
1175 params[b'objectIdentifier'] = revid
1175
1176
1176 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1177 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1177 if not revision:
1178 if not revision:
1178 if len(ctxs) == 1:
1179 if len(ctxs) == 1:
1179 msg = _(b'cannot create revision for %s') % ctx
1180 msg = _(b'cannot create revision for %s') % ctx
1180 else:
1181 else:
1181 msg = _(b'cannot create revision for %s::%s') % (basectx, ctx)
1182 msg = _(b'cannot create revision for %s::%s') % (basectx, ctx)
1182 raise error.Abort(msg)
1183 raise error.Abort(msg)
1183
1184
1184 return revision, diff
1185 return revision, diff
1185
1186
1186
1187
1187 def userphids(ui, names):
1188 def userphids(ui, names):
1188 """convert user names to PHIDs"""
1189 """convert user names to PHIDs"""
1189 names = [name.lower() for name in names]
1190 names = [name.lower() for name in names]
1190 query = {b'constraints': {b'usernames': names}}
1191 query = {b'constraints': {b'usernames': names}}
1191 result = callconduit(ui, b'user.search', query)
1192 result = callconduit(ui, b'user.search', query)
1192 # username not found is not an error of the API. So check if we have missed
1193 # username not found is not an error of the API. So check if we have missed
1193 # some names here.
1194 # some names here.
1194 data = result[b'data']
1195 data = result[b'data']
1195 resolved = {entry[b'fields'][b'username'].lower() for entry in data}
1196 resolved = {entry[b'fields'][b'username'].lower() for entry in data}
1196 unresolved = set(names) - resolved
1197 unresolved = set(names) - resolved
1197 if unresolved:
1198 if unresolved:
1198 raise error.Abort(
1199 raise error.Abort(
1199 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1200 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1200 )
1201 )
1201 return [entry[b'phid'] for entry in data]
1202 return [entry[b'phid'] for entry in data]
1202
1203
1203
1204
1204 def _print_phabsend_action(ui, ctx, newrevid, action):
1205 def _print_phabsend_action(ui, ctx, newrevid, action):
1205 """print the ``action`` that occurred when posting ``ctx`` for review
1206 """print the ``action`` that occurred when posting ``ctx`` for review
1206
1207
1207 This is a utility function for the sending phase of ``phabsend``, which
1208 This is a utility function for the sending phase of ``phabsend``, which
1208 makes it easier to show a status for all local commits with `--fold``.
1209 makes it easier to show a status for all local commits with `--fold``.
1209 """
1210 """
1210 actiondesc = ui.label(
1211 actiondesc = ui.label(
1211 {
1212 {
1212 b'created': _(b'created'),
1213 b'created': _(b'created'),
1213 b'skipped': _(b'skipped'),
1214 b'skipped': _(b'skipped'),
1214 b'updated': _(b'updated'),
1215 b'updated': _(b'updated'),
1215 }[action],
1216 }[action],
1216 b'phabricator.action.%s' % action,
1217 b'phabricator.action.%s' % action,
1217 )
1218 )
1218 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1219 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1219 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
1220 nodedesc = ui.label(bytes(ctx), b'phabricator.node')
1220 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
1221 desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc')
1221 ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc))
1222 ui.write(_(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc))
1222
1223
1223
1224
1224 def _amend_diff_properties(unfi, drevid, newnodes, diff):
1225 def _amend_diff_properties(unfi, drevid, newnodes, diff):
1225 """update the local commit list for the ``diff`` associated with ``drevid``
1226 """update the local commit list for the ``diff`` associated with ``drevid``
1226
1227
1227 This is a utility function for the amend phase of ``phabsend``, which
1228 This is a utility function for the amend phase of ``phabsend``, which
1228 converts failures to warning messages.
1229 converts failures to warning messages.
1229 """
1230 """
1230 _debug(
1231 _debug(
1231 unfi.ui,
1232 unfi.ui,
1232 b"new commits: %s\n" % stringutil.pprint([short(n) for n in newnodes]),
1233 b"new commits: %s\n" % stringutil.pprint([short(n) for n in newnodes]),
1233 )
1234 )
1234
1235
1235 try:
1236 try:
1236 writediffproperties([unfi[newnode] for newnode in newnodes], diff)
1237 writediffproperties([unfi[newnode] for newnode in newnodes], diff)
1237 except util.urlerr.urlerror:
1238 except util.urlerr.urlerror:
1238 # If it fails just warn and keep going, otherwise the DREV
1239 # If it fails just warn and keep going, otherwise the DREV
1239 # associations will be lost
1240 # associations will be lost
1240 unfi.ui.warnnoi18n(b'Failed to update metadata for D%d\n' % drevid)
1241 unfi.ui.warnnoi18n(b'Failed to update metadata for D%d\n' % drevid)
1241
1242
1242
1243
1243 @vcrcommand(
1244 @vcrcommand(
1244 b'phabsend',
1245 b'phabsend',
1245 [
1246 [
1246 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1247 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1247 (b'', b'amend', True, _(b'update commit messages')),
1248 (b'', b'amend', True, _(b'update commit messages')),
1248 (b'', b'reviewer', [], _(b'specify reviewers')),
1249 (b'', b'reviewer', [], _(b'specify reviewers')),
1249 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1250 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1250 (
1251 (
1251 b'm',
1252 b'm',
1252 b'comment',
1253 b'comment',
1253 b'',
1254 b'',
1254 _(b'add a comment to Revisions with new/updated Diffs'),
1255 _(b'add a comment to Revisions with new/updated Diffs'),
1255 ),
1256 ),
1256 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1257 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1257 (b'', b'fold', False, _(b'combine the revisions into one review')),
1258 (b'', b'fold', False, _(b'combine the revisions into one review')),
1258 ],
1259 ],
1259 _(b'REV [OPTIONS]'),
1260 _(b'REV [OPTIONS]'),
1260 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1261 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1261 )
1262 )
1262 def phabsend(ui, repo, *revs, **opts):
1263 def phabsend(ui, repo, *revs, **opts):
1263 """upload changesets to Phabricator
1264 """upload changesets to Phabricator
1264
1265
1265 If there are multiple revisions specified, they will be send as a stack
1266 If there are multiple revisions specified, they will be send as a stack
1266 with a linear dependencies relationship using the order specified by the
1267 with a linear dependencies relationship using the order specified by the
1267 revset.
1268 revset.
1268
1269
1269 For the first time uploading changesets, local tags will be created to
1270 For the first time uploading changesets, local tags will be created to
1270 maintain the association. After the first time, phabsend will check
1271 maintain the association. After the first time, phabsend will check
1271 obsstore and tags information so it can figure out whether to update an
1272 obsstore and tags information so it can figure out whether to update an
1272 existing Differential Revision, or create a new one.
1273 existing Differential Revision, or create a new one.
1273
1274
1274 If --amend is set, update commit messages so they have the
1275 If --amend is set, update commit messages so they have the
1275 ``Differential Revision`` URL, remove related tags. This is similar to what
1276 ``Differential Revision`` URL, remove related tags. This is similar to what
1276 arcanist will do, and is more desired in author-push workflows. Otherwise,
1277 arcanist will do, and is more desired in author-push workflows. Otherwise,
1277 use local tags to record the ``Differential Revision`` association.
1278 use local tags to record the ``Differential Revision`` association.
1278
1279
1279 The --confirm option lets you confirm changesets before sending them. You
1280 The --confirm option lets you confirm changesets before sending them. You
1280 can also add following to your configuration file to make it default
1281 can also add following to your configuration file to make it default
1281 behaviour::
1282 behaviour::
1282
1283
1283 [phabsend]
1284 [phabsend]
1284 confirm = true
1285 confirm = true
1285
1286
1286 By default, a separate review will be created for each commit that is
1287 By default, a separate review will be created for each commit that is
1287 selected, and will have the same parent/child relationship in Phabricator.
1288 selected, and will have the same parent/child relationship in Phabricator.
1288 If ``--fold`` is set, multiple commits are rolled up into a single review
1289 If ``--fold`` is set, multiple commits are rolled up into a single review
1289 as if diffed from the parent of the first revision to the last. The commit
1290 as if diffed from the parent of the first revision to the last. The commit
1290 messages are concatenated in the summary field on Phabricator.
1291 messages are concatenated in the summary field on Phabricator.
1291
1292
1292 phabsend will check obsstore and the above association to decide whether to
1293 phabsend will check obsstore and the above association to decide whether to
1293 update an existing Differential Revision, or create a new one.
1294 update an existing Differential Revision, or create a new one.
1294 """
1295 """
1295 opts = pycompat.byteskwargs(opts)
1296 opts = pycompat.byteskwargs(opts)
1296 revs = list(revs) + opts.get(b'rev', [])
1297 revs = list(revs) + opts.get(b'rev', [])
1297 revs = scmutil.revrange(repo, revs)
1298 revs = scmutil.revrange(repo, revs)
1298 revs.sort() # ascending order to preserve topological parent/child in phab
1299 revs.sort() # ascending order to preserve topological parent/child in phab
1299
1300
1300 if not revs:
1301 if not revs:
1301 raise error.Abort(_(b'phabsend requires at least one changeset'))
1302 raise error.Abort(_(b'phabsend requires at least one changeset'))
1302 if opts.get(b'amend'):
1303 if opts.get(b'amend'):
1303 cmdutil.checkunfinished(repo)
1304 cmdutil.checkunfinished(repo)
1304
1305
1305 ctxs = [repo[rev] for rev in revs]
1306 ctxs = [repo[rev] for rev in revs]
1306
1307
1307 if any(c for c in ctxs if c.obsolete()):
1308 if any(c for c in ctxs if c.obsolete()):
1308 raise error.Abort(_(b"obsolete commits cannot be posted for review"))
1309 raise error.Abort(_(b"obsolete commits cannot be posted for review"))
1309
1310
1310 # Ensure the local commits are an unbroken range. The semantics of the
1311 # Ensure the local commits are an unbroken range. The semantics of the
1311 # --fold option implies this, and the auto restacking of orphans requires
1312 # --fold option implies this, and the auto restacking of orphans requires
1312 # it. Otherwise A+C in A->B->C will cause B to be orphaned, and C' to
1313 # it. Otherwise A+C in A->B->C will cause B to be orphaned, and C' to
1313 # get A' as a parent.
1314 # get A' as a parent.
1314 def _fail_nonlinear_revs(revs, revtype):
1315 def _fail_nonlinear_revs(revs, revtype):
1315 badnodes = [repo[r].node() for r in revs]
1316 badnodes = [repo[r].node() for r in revs]
1316 raise error.Abort(
1317 raise error.Abort(
1317 _(b"cannot phabsend multiple %s revisions: %s")
1318 _(b"cannot phabsend multiple %s revisions: %s")
1318 % (revtype, scmutil.nodesummaries(repo, badnodes)),
1319 % (revtype, scmutil.nodesummaries(repo, badnodes)),
1319 hint=_(b"the revisions must form a linear chain"),
1320 hint=_(b"the revisions must form a linear chain"),
1320 )
1321 )
1321
1322
1322 heads = repo.revs(b'heads(%ld)', revs)
1323 heads = repo.revs(b'heads(%ld)', revs)
1323 if len(heads) > 1:
1324 if len(heads) > 1:
1324 _fail_nonlinear_revs(heads, b"head")
1325 _fail_nonlinear_revs(heads, b"head")
1325
1326
1326 roots = repo.revs(b'roots(%ld)', revs)
1327 roots = repo.revs(b'roots(%ld)', revs)
1327 if len(roots) > 1:
1328 if len(roots) > 1:
1328 _fail_nonlinear_revs(roots, b"root")
1329 _fail_nonlinear_revs(roots, b"root")
1329
1330
1330 fold = opts.get(b'fold')
1331 fold = opts.get(b'fold')
1331 if fold:
1332 if fold:
1332 if len(revs) == 1:
1333 if len(revs) == 1:
1333 # TODO: just switch to --no-fold instead?
1334 # TODO: just switch to --no-fold instead?
1334 raise error.Abort(_(b"cannot fold a single revision"))
1335 raise error.Abort(_(b"cannot fold a single revision"))
1335
1336
1336 # There's no clear way to manage multiple commits with a Dxxx tag, so
1337 # There's no clear way to manage multiple commits with a Dxxx tag, so
1337 # require the amend option. (We could append "_nnn", but then it
1338 # require the amend option. (We could append "_nnn", but then it
1338 # becomes jumbled if earlier commits are added to an update.) It should
1339 # becomes jumbled if earlier commits are added to an update.) It should
1339 # lock the repo and ensure that the range is editable, but that would
1340 # lock the repo and ensure that the range is editable, but that would
1340 # make the code pretty convoluted. The default behavior of `arc` is to
1341 # make the code pretty convoluted. The default behavior of `arc` is to
1341 # create a new review anyway.
1342 # create a new review anyway.
1342 if not opts.get(b"amend"):
1343 if not opts.get(b"amend"):
1343 raise error.Abort(_(b"cannot fold with --no-amend"))
1344 raise error.Abort(_(b"cannot fold with --no-amend"))
1344
1345
1345 # It might be possible to bucketize the revisions by the DREV value, and
1346 # It might be possible to bucketize the revisions by the DREV value, and
1346 # iterate over those groups when posting, and then again when amending.
1347 # iterate over those groups when posting, and then again when amending.
1347 # But for simplicity, require all selected revisions to be for the same
1348 # But for simplicity, require all selected revisions to be for the same
1348 # DREV (if present). Adding local revisions to an existing DREV is
1349 # DREV (if present). Adding local revisions to an existing DREV is
1349 # acceptable.
1350 # acceptable.
1350 drevmatchers = [
1351 drevmatchers = [
1351 _differentialrevisiondescre.search(ctx.description())
1352 _differentialrevisiondescre.search(ctx.description())
1352 for ctx in ctxs
1353 for ctx in ctxs
1353 ]
1354 ]
1354 if len({m.group('url') for m in drevmatchers if m}) > 1:
1355 if len({m.group('url') for m in drevmatchers if m}) > 1:
1355 raise error.Abort(
1356 raise error.Abort(
1356 _(b"cannot fold revisions with different DREV values")
1357 _(b"cannot fold revisions with different DREV values")
1357 )
1358 )
1358
1359
1359 # {newnode: (oldnode, olddiff, olddrev}
1360 # {newnode: (oldnode, olddiff, olddrev}
1360 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1361 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1361
1362
1362 confirm = ui.configbool(b'phabsend', b'confirm')
1363 confirm = ui.configbool(b'phabsend', b'confirm')
1363 confirm |= bool(opts.get(b'confirm'))
1364 confirm |= bool(opts.get(b'confirm'))
1364 if confirm:
1365 if confirm:
1365 confirmed = _confirmbeforesend(repo, revs, oldmap)
1366 confirmed = _confirmbeforesend(repo, revs, oldmap)
1366 if not confirmed:
1367 if not confirmed:
1367 raise error.Abort(_(b'phabsend cancelled'))
1368 raise error.Abort(_(b'phabsend cancelled'))
1368
1369
1369 actions = []
1370 actions = []
1370 reviewers = opts.get(b'reviewer', [])
1371 reviewers = opts.get(b'reviewer', [])
1371 blockers = opts.get(b'blocker', [])
1372 blockers = opts.get(b'blocker', [])
1372 phids = []
1373 phids = []
1373 if reviewers:
1374 if reviewers:
1374 phids.extend(userphids(repo.ui, reviewers))
1375 phids.extend(userphids(repo.ui, reviewers))
1375 if blockers:
1376 if blockers:
1376 phids.extend(
1377 phids.extend(
1377 map(
1378 map(
1378 lambda phid: b'blocking(%s)' % phid,
1379 lambda phid: b'blocking(%s)' % phid,
1379 userphids(repo.ui, blockers),
1380 userphids(repo.ui, blockers),
1380 )
1381 )
1381 )
1382 )
1382 if phids:
1383 if phids:
1383 actions.append({b'type': b'reviewers.add', b'value': phids})
1384 actions.append({b'type': b'reviewers.add', b'value': phids})
1384
1385
1385 drevids = [] # [int]
1386 drevids = [] # [int]
1386 diffmap = {} # {newnode: diff}
1387 diffmap = {} # {newnode: diff}
1387
1388
1388 # Send patches one by one so we know their Differential Revision PHIDs and
1389 # Send patches one by one so we know their Differential Revision PHIDs and
1389 # can provide dependency relationship
1390 # can provide dependency relationship
1390 lastrevphid = None
1391 lastrevphid = None
1391 for ctx in ctxs:
1392 for ctx in ctxs:
1392 if fold:
1393 if fold:
1393 ui.debug(b'sending rev %d::%d\n' % (ctx.rev(), ctxs[-1].rev()))
1394 ui.debug(b'sending rev %d::%d\n' % (ctx.rev(), ctxs[-1].rev()))
1394 else:
1395 else:
1395 ui.debug(b'sending rev %d\n' % ctx.rev())
1396 ui.debug(b'sending rev %d\n' % ctx.rev())
1396
1397
1397 # Get Differential Revision ID
1398 # Get Differential Revision ID
1398 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1399 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1399 oldbasenode, oldbasediff, oldbaserevid = oldnode, olddiff, revid
1400 oldbasenode, oldbasediff, oldbaserevid = oldnode, olddiff, revid
1400
1401
1401 if fold:
1402 if fold:
1402 oldbasenode, oldbasediff, oldbaserevid = oldmap.get(
1403 oldbasenode, oldbasediff, oldbaserevid = oldmap.get(
1403 ctxs[-1].node(), (None, None, None)
1404 ctxs[-1].node(), (None, None, None)
1404 )
1405 )
1405
1406
1406 if oldnode != ctx.node() or opts.get(b'amend'):
1407 if oldnode != ctx.node() or opts.get(b'amend'):
1407 # Create or update Differential Revision
1408 # Create or update Differential Revision
1408 revision, diff = createdifferentialrevision(
1409 revision, diff = createdifferentialrevision(
1409 ctxs if fold else [ctx],
1410 ctxs if fold else [ctx],
1410 revid,
1411 revid,
1411 lastrevphid,
1412 lastrevphid,
1412 oldbasenode,
1413 oldbasenode,
1413 oldnode,
1414 oldnode,
1414 olddiff,
1415 olddiff,
1415 actions,
1416 actions,
1416 opts.get(b'comment'),
1417 opts.get(b'comment'),
1417 )
1418 )
1418
1419
1419 if fold:
1420 if fold:
1420 for ctx in ctxs:
1421 for ctx in ctxs:
1421 diffmap[ctx.node()] = diff
1422 diffmap[ctx.node()] = diff
1422 else:
1423 else:
1423 diffmap[ctx.node()] = diff
1424 diffmap[ctx.node()] = diff
1424
1425
1425 newrevid = int(revision[b'object'][b'id'])
1426 newrevid = int(revision[b'object'][b'id'])
1426 newrevphid = revision[b'object'][b'phid']
1427 newrevphid = revision[b'object'][b'phid']
1427 if revid:
1428 if revid:
1428 action = b'updated'
1429 action = b'updated'
1429 else:
1430 else:
1430 action = b'created'
1431 action = b'created'
1431
1432
1432 # Create a local tag to note the association, if commit message
1433 # Create a local tag to note the association, if commit message
1433 # does not have it already
1434 # does not have it already
1434 if not fold:
1435 if not fold:
1435 m = _differentialrevisiondescre.search(ctx.description())
1436 m = _differentialrevisiondescre.search(ctx.description())
1436 if not m or int(m.group('id')) != newrevid:
1437 if not m or int(m.group('id')) != newrevid:
1437 tagname = b'D%d' % newrevid
1438 tagname = b'D%d' % newrevid
1438 tags.tag(
1439 tags.tag(
1439 repo,
1440 repo,
1440 tagname,
1441 tagname,
1441 ctx.node(),
1442 ctx.node(),
1442 message=None,
1443 message=None,
1443 user=None,
1444 user=None,
1444 date=None,
1445 date=None,
1445 local=True,
1446 local=True,
1446 )
1447 )
1447 else:
1448 else:
1448 # Nothing changed. But still set "newrevphid" so the next revision
1449 # Nothing changed. But still set "newrevphid" so the next revision
1449 # could depend on this one and "newrevid" for the summary line.
1450 # could depend on this one and "newrevid" for the summary line.
1450 newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']
1451 newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']
1451 newrevid = revid
1452 newrevid = revid
1452 action = b'skipped'
1453 action = b'skipped'
1453
1454
1454 drevids.append(newrevid)
1455 drevids.append(newrevid)
1455 lastrevphid = newrevphid
1456 lastrevphid = newrevphid
1456
1457
1457 if fold:
1458 if fold:
1458 for c in ctxs:
1459 for c in ctxs:
1459 if oldmap.get(c.node(), (None, None, None))[2]:
1460 if oldmap.get(c.node(), (None, None, None))[2]:
1460 action = b'updated'
1461 action = b'updated'
1461 else:
1462 else:
1462 action = b'created'
1463 action = b'created'
1463 _print_phabsend_action(ui, c, newrevid, action)
1464 _print_phabsend_action(ui, c, newrevid, action)
1464 break
1465 break
1465
1466
1466 _print_phabsend_action(ui, ctx, newrevid, action)
1467 _print_phabsend_action(ui, ctx, newrevid, action)
1467
1468
1468 # Update commit messages and remove tags
1469 # Update commit messages and remove tags
1469 if opts.get(b'amend'):
1470 if opts.get(b'amend'):
1470 unfi = repo.unfiltered()
1471 unfi = repo.unfiltered()
1471 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1472 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1472 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1473 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1473 # Eagerly evaluate commits to restabilize before creating new
1474 # Eagerly evaluate commits to restabilize before creating new
1474 # commits. The selected revisions are excluded because they are
1475 # commits. The selected revisions are excluded because they are
1475 # automatically restacked as part of the submission process.
1476 # automatically restacked as part of the submission process.
1476 restack = [
1477 restack = [
1477 c
1478 c
1478 for c in repo.set(
1479 for c in repo.set(
1479 b"(%ld::) - (%ld) - unstable() - obsolete() - public()",
1480 b"(%ld::) - (%ld) - unstable() - obsolete() - public()",
1480 revs,
1481 revs,
1481 revs,
1482 revs,
1482 )
1483 )
1483 ]
1484 ]
1484 wnode = unfi[b'.'].node()
1485 wnode = unfi[b'.'].node()
1485 mapping = {} # {oldnode: [newnode]}
1486 mapping = {} # {oldnode: [newnode]}
1486 newnodes = []
1487 newnodes = []
1487
1488
1488 drevid = drevids[0]
1489 drevid = drevids[0]
1489
1490
1490 for i, rev in enumerate(revs):
1491 for i, rev in enumerate(revs):
1491 old = unfi[rev]
1492 old = unfi[rev]
1492 if not fold:
1493 if not fold:
1493 drevid = drevids[i]
1494 drevid = drevids[i]
1494 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1495 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1495
1496
1496 newdesc = get_amended_desc(drev, old, fold)
1497 newdesc = get_amended_desc(drev, old, fold)
1497 # Make sure commit message contain "Differential Revision"
1498 # Make sure commit message contain "Differential Revision"
1498 if (
1499 if (
1499 old.description() != newdesc
1500 old.description() != newdesc
1500 or old.p1().node() in mapping
1501 or old.p1().node() in mapping
1501 or old.p2().node() in mapping
1502 or old.p2().node() in mapping
1502 ):
1503 ):
1503 if old.phase() == phases.public:
1504 if old.phase() == phases.public:
1504 ui.warn(
1505 ui.warn(
1505 _(b"warning: not updating public commit %s\n")
1506 _(b"warning: not updating public commit %s\n")
1506 % scmutil.formatchangeid(old)
1507 % scmutil.formatchangeid(old)
1507 )
1508 )
1508 continue
1509 continue
1509 parents = [
1510 parents = [
1510 mapping.get(old.p1().node(), (old.p1(),))[0],
1511 mapping.get(old.p1().node(), (old.p1(),))[0],
1511 mapping.get(old.p2().node(), (old.p2(),))[0],
1512 mapping.get(old.p2().node(), (old.p2(),))[0],
1512 ]
1513 ]
1513 new = context.metadataonlyctx(
1514 new = context.metadataonlyctx(
1514 repo,
1515 repo,
1515 old,
1516 old,
1516 parents=parents,
1517 parents=parents,
1517 text=newdesc,
1518 text=newdesc,
1518 user=old.user(),
1519 user=old.user(),
1519 date=old.date(),
1520 date=old.date(),
1520 extra=old.extra(),
1521 extra=old.extra(),
1521 )
1522 )
1522
1523
1523 newnode = new.commit()
1524 newnode = new.commit()
1524
1525
1525 mapping[old.node()] = [newnode]
1526 mapping[old.node()] = [newnode]
1526
1527
1527 if fold:
1528 if fold:
1528 # Defer updating the (single) Diff until all nodes are
1529 # Defer updating the (single) Diff until all nodes are
1529 # collected. No tags were created, so none need to be
1530 # collected. No tags were created, so none need to be
1530 # removed.
1531 # removed.
1531 newnodes.append(newnode)
1532 newnodes.append(newnode)
1532 continue
1533 continue
1533
1534
1534 _amend_diff_properties(
1535 _amend_diff_properties(
1535 unfi, drevid, [newnode], diffmap[old.node()]
1536 unfi, drevid, [newnode], diffmap[old.node()]
1536 )
1537 )
1537
1538
1538 # Remove local tags since it's no longer necessary
1539 # Remove local tags since it's no longer necessary
1539 tagname = b'D%d' % drevid
1540 tagname = b'D%d' % drevid
1540 if tagname in repo.tags():
1541 if tagname in repo.tags():
1541 tags.tag(
1542 tags.tag(
1542 repo,
1543 repo,
1543 tagname,
1544 tagname,
1544 nullid,
1545 nullid,
1545 message=None,
1546 message=None,
1546 user=None,
1547 user=None,
1547 date=None,
1548 date=None,
1548 local=True,
1549 local=True,
1549 )
1550 )
1550 elif fold:
1551 elif fold:
1551 # When folding multiple commits into one review with
1552 # When folding multiple commits into one review with
1552 # --fold, track even the commits that weren't amended, so
1553 # --fold, track even the commits that weren't amended, so
1553 # that their association isn't lost if the properties are
1554 # that their association isn't lost if the properties are
1554 # rewritten below.
1555 # rewritten below.
1555 newnodes.append(old.node())
1556 newnodes.append(old.node())
1556
1557
1557 # If the submitted commits are public, no amend takes place so
1558 # If the submitted commits are public, no amend takes place so
1558 # there are no newnodes and therefore no diff update to do.
1559 # there are no newnodes and therefore no diff update to do.
1559 if fold and newnodes:
1560 if fold and newnodes:
1560 diff = diffmap[old.node()]
1561 diff = diffmap[old.node()]
1561
1562
1562 # The diff object in diffmap doesn't have the local commits
1563 # The diff object in diffmap doesn't have the local commits
1563 # because that could be returned from differential.creatediff,
1564 # because that could be returned from differential.creatediff,
1564 # not differential.querydiffs. So use the queried diff (if
1565 # not differential.querydiffs. So use the queried diff (if
1565 # present), or force the amend (a new revision is being posted.)
1566 # present), or force the amend (a new revision is being posted.)
1566 if not olddiff or set(newnodes) != getlocalcommits(olddiff):
1567 if not olddiff or set(newnodes) != getlocalcommits(olddiff):
1567 _debug(ui, b"updating local commit list for D%d\n" % drevid)
1568 _debug(ui, b"updating local commit list for D%d\n" % drevid)
1568 _amend_diff_properties(unfi, drevid, newnodes, diff)
1569 _amend_diff_properties(unfi, drevid, newnodes, diff)
1569 else:
1570 else:
1570 _debug(
1571 _debug(
1571 ui,
1572 ui,
1572 b"local commit list for D%d is already up-to-date\n"
1573 b"local commit list for D%d is already up-to-date\n"
1573 % drevid,
1574 % drevid,
1574 )
1575 )
1575 elif fold:
1576 elif fold:
1576 _debug(ui, b"no newnodes to update\n")
1577 _debug(ui, b"no newnodes to update\n")
1577
1578
1578 # Restack any children of first-time submissions that were orphaned
1579 # Restack any children of first-time submissions that were orphaned
1579 # in the process. The ctx won't report that it is an orphan until
1580 # in the process. The ctx won't report that it is an orphan until
1580 # the cleanup takes place below.
1581 # the cleanup takes place below.
1581 for old in restack:
1582 for old in restack:
1582 parents = [
1583 parents = [
1583 mapping.get(old.p1().node(), (old.p1(),))[0],
1584 mapping.get(old.p1().node(), (old.p1(),))[0],
1584 mapping.get(old.p2().node(), (old.p2(),))[0],
1585 mapping.get(old.p2().node(), (old.p2(),))[0],
1585 ]
1586 ]
1586 new = context.metadataonlyctx(
1587 new = context.metadataonlyctx(
1587 repo,
1588 repo,
1588 old,
1589 old,
1589 parents=parents,
1590 parents=parents,
1590 text=old.description(),
1591 text=old.description(),
1591 user=old.user(),
1592 user=old.user(),
1592 date=old.date(),
1593 date=old.date(),
1593 extra=old.extra(),
1594 extra=old.extra(),
1594 )
1595 )
1595
1596
1596 newnode = new.commit()
1597 newnode = new.commit()
1597
1598
1598 # Don't obsolete unselected descendants of nodes that have not
1599 # Don't obsolete unselected descendants of nodes that have not
1599 # been changed in this transaction- that results in an error.
1600 # been changed in this transaction- that results in an error.
1600 if newnode != old.node():
1601 if newnode != old.node():
1601 mapping[old.node()] = [newnode]
1602 mapping[old.node()] = [newnode]
1602 _debug(
1603 _debug(
1603 ui,
1604 ui,
1604 b"restabilizing %s as %s\n"
1605 b"restabilizing %s as %s\n"
1605 % (short(old.node()), short(newnode)),
1606 % (short(old.node()), short(newnode)),
1606 )
1607 )
1607 else:
1608 else:
1608 _debug(
1609 _debug(
1609 ui,
1610 ui,
1610 b"not restabilizing unchanged %s\n" % short(old.node()),
1611 b"not restabilizing unchanged %s\n" % short(old.node()),
1611 )
1612 )
1612
1613
1613 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1614 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1614 if wnode in mapping:
1615 if wnode in mapping:
1615 unfi.setparents(mapping[wnode][0])
1616 unfi.setparents(mapping[wnode][0])
1616
1617
1617
1618
1618 # Map from "hg:meta" keys to header understood by "hg import". The order is
1619 # Map from "hg:meta" keys to header understood by "hg import". The order is
1619 # consistent with "hg export" output.
1620 # consistent with "hg export" output.
1620 _metanamemap = util.sortdict(
1621 _metanamemap = util.sortdict(
1621 [
1622 [
1622 (b'user', b'User'),
1623 (b'user', b'User'),
1623 (b'date', b'Date'),
1624 (b'date', b'Date'),
1624 (b'branch', b'Branch'),
1625 (b'branch', b'Branch'),
1625 (b'node', b'Node ID'),
1626 (b'node', b'Node ID'),
1626 (b'parent', b'Parent '),
1627 (b'parent', b'Parent '),
1627 ]
1628 ]
1628 )
1629 )
1629
1630
1630
1631
1631 def _confirmbeforesend(repo, revs, oldmap):
1632 def _confirmbeforesend(repo, revs, oldmap):
1632 url, token = readurltoken(repo.ui)
1633 url, token = readurltoken(repo.ui)
1633 ui = repo.ui
1634 ui = repo.ui
1634 for rev in revs:
1635 for rev in revs:
1635 ctx = repo[rev]
1636 ctx = repo[rev]
1636 desc = ctx.description().splitlines()[0]
1637 desc = ctx.description().splitlines()[0]
1637 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1638 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1638 if drevid:
1639 if drevid:
1639 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1640 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1640 else:
1641 else:
1641 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1642 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1642
1643
1643 ui.write(
1644 ui.write(
1644 _(b'%s - %s: %s\n')
1645 _(b'%s - %s: %s\n')
1645 % (
1646 % (
1646 drevdesc,
1647 drevdesc,
1647 ui.label(bytes(ctx), b'phabricator.node'),
1648 ui.label(bytes(ctx), b'phabricator.node'),
1648 ui.label(desc, b'phabricator.desc'),
1649 ui.label(desc, b'phabricator.desc'),
1649 )
1650 )
1650 )
1651 )
1651
1652
1652 if ui.promptchoice(
1653 if ui.promptchoice(
1653 _(b'Send the above changes to %s (Y/n)?$$ &Yes $$ &No') % url
1654 _(b'Send the above changes to %s (Y/n)?$$ &Yes $$ &No') % url
1654 ):
1655 ):
1655 return False
1656 return False
1656
1657
1657 return True
1658 return True
1658
1659
1659
1660
1660 _knownstatusnames = {
1661 _knownstatusnames = {
1661 b'accepted',
1662 b'accepted',
1662 b'needsreview',
1663 b'needsreview',
1663 b'needsrevision',
1664 b'needsrevision',
1664 b'closed',
1665 b'closed',
1665 b'abandoned',
1666 b'abandoned',
1666 b'changesplanned',
1667 b'changesplanned',
1667 }
1668 }
1668
1669
1669
1670
1670 def _getstatusname(drev):
1671 def _getstatusname(drev):
1671 """get normalized status name from a Differential Revision"""
1672 """get normalized status name from a Differential Revision"""
1672 return drev[b'statusName'].replace(b' ', b'').lower()
1673 return drev[b'statusName'].replace(b' ', b'').lower()
1673
1674
1674
1675
1675 # Small language to specify differential revisions. Support symbols: (), :X,
1676 # Small language to specify differential revisions. Support symbols: (), :X,
1676 # +, and -.
1677 # +, and -.
1677
1678
1678 _elements = {
1679 _elements = {
1679 # token-type: binding-strength, primary, prefix, infix, suffix
1680 # token-type: binding-strength, primary, prefix, infix, suffix
1680 b'(': (12, None, (b'group', 1, b')'), None, None),
1681 b'(': (12, None, (b'group', 1, b')'), None, None),
1681 b':': (8, None, (b'ancestors', 8), None, None),
1682 b':': (8, None, (b'ancestors', 8), None, None),
1682 b'&': (5, None, None, (b'and_', 5), None),
1683 b'&': (5, None, None, (b'and_', 5), None),
1683 b'+': (4, None, None, (b'add', 4), None),
1684 b'+': (4, None, None, (b'add', 4), None),
1684 b'-': (4, None, None, (b'sub', 4), None),
1685 b'-': (4, None, None, (b'sub', 4), None),
1685 b')': (0, None, None, None, None),
1686 b')': (0, None, None, None, None),
1686 b'symbol': (0, b'symbol', None, None, None),
1687 b'symbol': (0, b'symbol', None, None, None),
1687 b'end': (0, None, None, None, None),
1688 b'end': (0, None, None, None, None),
1688 }
1689 }
1689
1690
1690
1691
1691 def _tokenize(text):
1692 def _tokenize(text):
1692 view = memoryview(text) # zero-copy slice
1693 view = memoryview(text) # zero-copy slice
1693 special = b'():+-& '
1694 special = b'():+-& '
1694 pos = 0
1695 pos = 0
1695 length = len(text)
1696 length = len(text)
1696 while pos < length:
1697 while pos < length:
1697 symbol = b''.join(
1698 symbol = b''.join(
1698 itertools.takewhile(
1699 itertools.takewhile(
1699 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1700 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1700 )
1701 )
1701 )
1702 )
1702 if symbol:
1703 if symbol:
1703 yield (b'symbol', symbol, pos)
1704 yield (b'symbol', symbol, pos)
1704 pos += len(symbol)
1705 pos += len(symbol)
1705 else: # special char, ignore space
1706 else: # special char, ignore space
1706 if text[pos : pos + 1] != b' ':
1707 if text[pos : pos + 1] != b' ':
1707 yield (text[pos : pos + 1], None, pos)
1708 yield (text[pos : pos + 1], None, pos)
1708 pos += 1
1709 pos += 1
1709 yield (b'end', None, pos)
1710 yield (b'end', None, pos)
1710
1711
1711
1712
1712 def _parse(text):
1713 def _parse(text):
1713 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1714 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1714 if pos != len(text):
1715 if pos != len(text):
1715 raise error.ParseError(b'invalid token', pos)
1716 raise error.ParseError(b'invalid token', pos)
1716 return tree
1717 return tree
1717
1718
1718
1719
1719 def _parsedrev(symbol):
1720 def _parsedrev(symbol):
1720 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1721 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1721 if symbol.startswith(b'D') and symbol[1:].isdigit():
1722 if symbol.startswith(b'D') and symbol[1:].isdigit():
1722 return int(symbol[1:])
1723 return int(symbol[1:])
1723 if symbol.isdigit():
1724 if symbol.isdigit():
1724 return int(symbol)
1725 return int(symbol)
1725
1726
1726
1727
1727 def _prefetchdrevs(tree):
1728 def _prefetchdrevs(tree):
1728 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1729 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1729 drevs = set()
1730 drevs = set()
1730 ancestordrevs = set()
1731 ancestordrevs = set()
1731 op = tree[0]
1732 op = tree[0]
1732 if op == b'symbol':
1733 if op == b'symbol':
1733 r = _parsedrev(tree[1])
1734 r = _parsedrev(tree[1])
1734 if r:
1735 if r:
1735 drevs.add(r)
1736 drevs.add(r)
1736 elif op == b'ancestors':
1737 elif op == b'ancestors':
1737 r, a = _prefetchdrevs(tree[1])
1738 r, a = _prefetchdrevs(tree[1])
1738 drevs.update(r)
1739 drevs.update(r)
1739 ancestordrevs.update(r)
1740 ancestordrevs.update(r)
1740 ancestordrevs.update(a)
1741 ancestordrevs.update(a)
1741 else:
1742 else:
1742 for t in tree[1:]:
1743 for t in tree[1:]:
1743 r, a = _prefetchdrevs(t)
1744 r, a = _prefetchdrevs(t)
1744 drevs.update(r)
1745 drevs.update(r)
1745 ancestordrevs.update(a)
1746 ancestordrevs.update(a)
1746 return drevs, ancestordrevs
1747 return drevs, ancestordrevs
1747
1748
1748
1749
1749 def querydrev(ui, spec):
1750 def querydrev(ui, spec):
1750 """return a list of "Differential Revision" dicts
1751 """return a list of "Differential Revision" dicts
1751
1752
1752 spec is a string using a simple query language, see docstring in phabread
1753 spec is a string using a simple query language, see docstring in phabread
1753 for details.
1754 for details.
1754
1755
1755 A "Differential Revision dict" looks like:
1756 A "Differential Revision dict" looks like:
1756
1757
1757 {
1758 {
1758 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1759 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1759 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1760 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1760 "auxiliary": {
1761 "auxiliary": {
1761 "phabricator:depends-on": [
1762 "phabricator:depends-on": [
1762 "PHID-DREV-gbapp366kutjebt7agcd"
1763 "PHID-DREV-gbapp366kutjebt7agcd"
1763 ]
1764 ]
1764 "phabricator:projects": [],
1765 "phabricator:projects": [],
1765 },
1766 },
1766 "branch": "default",
1767 "branch": "default",
1767 "ccs": [],
1768 "ccs": [],
1768 "commits": [],
1769 "commits": [],
1769 "dateCreated": "1499181406",
1770 "dateCreated": "1499181406",
1770 "dateModified": "1499182103",
1771 "dateModified": "1499182103",
1771 "diffs": [
1772 "diffs": [
1772 "3",
1773 "3",
1773 "4",
1774 "4",
1774 ],
1775 ],
1775 "hashes": [],
1776 "hashes": [],
1776 "id": "2",
1777 "id": "2",
1777 "lineCount": "2",
1778 "lineCount": "2",
1778 "phid": "PHID-DREV-672qvysjcczopag46qty",
1779 "phid": "PHID-DREV-672qvysjcczopag46qty",
1779 "properties": {},
1780 "properties": {},
1780 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1781 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1781 "reviewers": [],
1782 "reviewers": [],
1782 "sourcePath": null
1783 "sourcePath": null
1783 "status": "0",
1784 "status": "0",
1784 "statusName": "Needs Review",
1785 "statusName": "Needs Review",
1785 "summary": "",
1786 "summary": "",
1786 "testPlan": "",
1787 "testPlan": "",
1787 "title": "example",
1788 "title": "example",
1788 "uri": "https://phab.example.com/D2",
1789 "uri": "https://phab.example.com/D2",
1789 }
1790 }
1790 """
1791 """
1791 # TODO: replace differential.query and differential.querydiffs with
1792 # TODO: replace differential.query and differential.querydiffs with
1792 # differential.diff.search because the former (and their output) are
1793 # differential.diff.search because the former (and their output) are
1793 # frozen, and planned to be deprecated and removed.
1794 # frozen, and planned to be deprecated and removed.
1794
1795
1795 def fetch(params):
1796 def fetch(params):
1796 """params -> single drev or None"""
1797 """params -> single drev or None"""
1797 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1798 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1798 if key in prefetched:
1799 if key in prefetched:
1799 return prefetched[key]
1800 return prefetched[key]
1800 drevs = callconduit(ui, b'differential.query', params)
1801 drevs = callconduit(ui, b'differential.query', params)
1801 # Fill prefetched with the result
1802 # Fill prefetched with the result
1802 for drev in drevs:
1803 for drev in drevs:
1803 prefetched[drev[b'phid']] = drev
1804 prefetched[drev[b'phid']] = drev
1804 prefetched[int(drev[b'id'])] = drev
1805 prefetched[int(drev[b'id'])] = drev
1805 if key not in prefetched:
1806 if key not in prefetched:
1806 raise error.Abort(
1807 raise error.Abort(
1807 _(b'cannot get Differential Revision %r') % params
1808 _(b'cannot get Differential Revision %r') % params
1808 )
1809 )
1809 return prefetched[key]
1810 return prefetched[key]
1810
1811
1811 def getstack(topdrevids):
1812 def getstack(topdrevids):
1812 """given a top, get a stack from the bottom, [id] -> [id]"""
1813 """given a top, get a stack from the bottom, [id] -> [id]"""
1813 visited = set()
1814 visited = set()
1814 result = []
1815 result = []
1815 queue = [{b'ids': [i]} for i in topdrevids]
1816 queue = [{b'ids': [i]} for i in topdrevids]
1816 while queue:
1817 while queue:
1817 params = queue.pop()
1818 params = queue.pop()
1818 drev = fetch(params)
1819 drev = fetch(params)
1819 if drev[b'id'] in visited:
1820 if drev[b'id'] in visited:
1820 continue
1821 continue
1821 visited.add(drev[b'id'])
1822 visited.add(drev[b'id'])
1822 result.append(int(drev[b'id']))
1823 result.append(int(drev[b'id']))
1823 auxiliary = drev.get(b'auxiliary', {})
1824 auxiliary = drev.get(b'auxiliary', {})
1824 depends = auxiliary.get(b'phabricator:depends-on', [])
1825 depends = auxiliary.get(b'phabricator:depends-on', [])
1825 for phid in depends:
1826 for phid in depends:
1826 queue.append({b'phids': [phid]})
1827 queue.append({b'phids': [phid]})
1827 result.reverse()
1828 result.reverse()
1828 return smartset.baseset(result)
1829 return smartset.baseset(result)
1829
1830
1830 # Initialize prefetch cache
1831 # Initialize prefetch cache
1831 prefetched = {} # {id or phid: drev}
1832 prefetched = {} # {id or phid: drev}
1832
1833
1833 tree = _parse(spec)
1834 tree = _parse(spec)
1834 drevs, ancestordrevs = _prefetchdrevs(tree)
1835 drevs, ancestordrevs = _prefetchdrevs(tree)
1835
1836
1836 # developer config: phabricator.batchsize
1837 # developer config: phabricator.batchsize
1837 batchsize = ui.configint(b'phabricator', b'batchsize')
1838 batchsize = ui.configint(b'phabricator', b'batchsize')
1838
1839
1839 # Prefetch Differential Revisions in batch
1840 # Prefetch Differential Revisions in batch
1840 tofetch = set(drevs)
1841 tofetch = set(drevs)
1841 for r in ancestordrevs:
1842 for r in ancestordrevs:
1842 tofetch.update(range(max(1, r - batchsize), r + 1))
1843 tofetch.update(range(max(1, r - batchsize), r + 1))
1843 if drevs:
1844 if drevs:
1844 fetch({b'ids': list(tofetch)})
1845 fetch({b'ids': list(tofetch)})
1845 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1846 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1846
1847
1847 # Walk through the tree, return smartsets
1848 # Walk through the tree, return smartsets
1848 def walk(tree):
1849 def walk(tree):
1849 op = tree[0]
1850 op = tree[0]
1850 if op == b'symbol':
1851 if op == b'symbol':
1851 drev = _parsedrev(tree[1])
1852 drev = _parsedrev(tree[1])
1852 if drev:
1853 if drev:
1853 return smartset.baseset([drev])
1854 return smartset.baseset([drev])
1854 elif tree[1] in _knownstatusnames:
1855 elif tree[1] in _knownstatusnames:
1855 drevs = [
1856 drevs = [
1856 r
1857 r
1857 for r in validids
1858 for r in validids
1858 if _getstatusname(prefetched[r]) == tree[1]
1859 if _getstatusname(prefetched[r]) == tree[1]
1859 ]
1860 ]
1860 return smartset.baseset(drevs)
1861 return smartset.baseset(drevs)
1861 else:
1862 else:
1862 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1863 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1863 elif op in {b'and_', b'add', b'sub'}:
1864 elif op in {b'and_', b'add', b'sub'}:
1864 assert len(tree) == 3
1865 assert len(tree) == 3
1865 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1866 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1866 elif op == b'group':
1867 elif op == b'group':
1867 return walk(tree[1])
1868 return walk(tree[1])
1868 elif op == b'ancestors':
1869 elif op == b'ancestors':
1869 return getstack(walk(tree[1]))
1870 return getstack(walk(tree[1]))
1870 else:
1871 else:
1871 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1872 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1872
1873
1873 return [prefetched[r] for r in walk(tree)]
1874 return [prefetched[r] for r in walk(tree)]
1874
1875
1875
1876
1876 def getdescfromdrev(drev):
1877 def getdescfromdrev(drev):
1877 """get description (commit message) from "Differential Revision"
1878 """get description (commit message) from "Differential Revision"
1878
1879
1879 This is similar to differential.getcommitmessage API. But we only care
1880 This is similar to differential.getcommitmessage API. But we only care
1880 about limited fields: title, summary, test plan, and URL.
1881 about limited fields: title, summary, test plan, and URL.
1881 """
1882 """
1882 title = drev[b'title']
1883 title = drev[b'title']
1883 summary = drev[b'summary'].rstrip()
1884 summary = drev[b'summary'].rstrip()
1884 testplan = drev[b'testPlan'].rstrip()
1885 testplan = drev[b'testPlan'].rstrip()
1885 if testplan:
1886 if testplan:
1886 testplan = b'Test Plan:\n%s' % testplan
1887 testplan = b'Test Plan:\n%s' % testplan
1887 uri = b'Differential Revision: %s' % drev[b'uri']
1888 uri = b'Differential Revision: %s' % drev[b'uri']
1888 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1889 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1889
1890
1890
1891
1891 def get_amended_desc(drev, ctx, folded):
1892 def get_amended_desc(drev, ctx, folded):
1892 """similar to ``getdescfromdrev``, but supports a folded series of commits
1893 """similar to ``getdescfromdrev``, but supports a folded series of commits
1893
1894
1894 This is used when determining if an individual commit needs to have its
1895 This is used when determining if an individual commit needs to have its
1895 message amended after posting it for review. The determination is made for
1896 message amended after posting it for review. The determination is made for
1896 each individual commit, even when they were folded into one review.
1897 each individual commit, even when they were folded into one review.
1897 """
1898 """
1898 if not folded:
1899 if not folded:
1899 return getdescfromdrev(drev)
1900 return getdescfromdrev(drev)
1900
1901
1901 uri = b'Differential Revision: %s' % drev[b'uri']
1902 uri = b'Differential Revision: %s' % drev[b'uri']
1902
1903
1903 # Since the commit messages were combined when posting multiple commits
1904 # Since the commit messages were combined when posting multiple commits
1904 # with --fold, the fields can't be read from Phabricator here, or *all*
1905 # with --fold, the fields can't be read from Phabricator here, or *all*
1905 # affected local revisions will end up with the same commit message after
1906 # affected local revisions will end up with the same commit message after
1906 # the URI is amended in. Append in the DREV line, or update it if it
1907 # the URI is amended in. Append in the DREV line, or update it if it
1907 # exists. At worst, this means commit message or test plan updates on
1908 # exists. At worst, this means commit message or test plan updates on
1908 # Phabricator aren't propagated back to the repository, but that seems
1909 # Phabricator aren't propagated back to the repository, but that seems
1909 # reasonable for the case where local commits are effectively combined
1910 # reasonable for the case where local commits are effectively combined
1910 # in Phabricator.
1911 # in Phabricator.
1911 m = _differentialrevisiondescre.search(ctx.description())
1912 m = _differentialrevisiondescre.search(ctx.description())
1912 if not m:
1913 if not m:
1913 return b'\n\n'.join([ctx.description(), uri])
1914 return b'\n\n'.join([ctx.description(), uri])
1914
1915
1915 return _differentialrevisiondescre.sub(uri, ctx.description())
1916 return _differentialrevisiondescre.sub(uri, ctx.description())
1916
1917
1917
1918
1918 def getlocalcommits(diff):
1919 def getlocalcommits(diff):
1919 """get the set of local commits from a diff object
1920 """get the set of local commits from a diff object
1920
1921
1921 See ``getdiffmeta()`` for an example diff object.
1922 See ``getdiffmeta()`` for an example diff object.
1922 """
1923 """
1923 props = diff.get(b'properties') or {}
1924 props = diff.get(b'properties') or {}
1924 commits = props.get(b'local:commits') or {}
1925 commits = props.get(b'local:commits') or {}
1925 if len(commits) > 1:
1926 if len(commits) > 1:
1926 return {bin(c) for c in commits.keys()}
1927 return {bin(c) for c in commits.keys()}
1927
1928
1928 # Storing the diff metadata predates storing `local:commits`, so continue
1929 # Storing the diff metadata predates storing `local:commits`, so continue
1929 # to use that in the --no-fold case.
1930 # to use that in the --no-fold case.
1930 return {bin(getdiffmeta(diff).get(b'node', b'')) or None}
1931 return {bin(getdiffmeta(diff).get(b'node', b'')) or None}
1931
1932
1932
1933
1933 def getdiffmeta(diff):
1934 def getdiffmeta(diff):
1934 """get commit metadata (date, node, user, p1) from a diff object
1935 """get commit metadata (date, node, user, p1) from a diff object
1935
1936
1936 The metadata could be "hg:meta", sent by phabsend, like:
1937 The metadata could be "hg:meta", sent by phabsend, like:
1937
1938
1938 "properties": {
1939 "properties": {
1939 "hg:meta": {
1940 "hg:meta": {
1940 "branch": "default",
1941 "branch": "default",
1941 "date": "1499571514 25200",
1942 "date": "1499571514 25200",
1942 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
1943 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
1943 "user": "Foo Bar <foo@example.com>",
1944 "user": "Foo Bar <foo@example.com>",
1944 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
1945 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
1945 }
1946 }
1946 }
1947 }
1947
1948
1948 Or converted from "local:commits", sent by "arc", like:
1949 Or converted from "local:commits", sent by "arc", like:
1949
1950
1950 "properties": {
1951 "properties": {
1951 "local:commits": {
1952 "local:commits": {
1952 "98c08acae292b2faf60a279b4189beb6cff1414d": {
1953 "98c08acae292b2faf60a279b4189beb6cff1414d": {
1953 "author": "Foo Bar",
1954 "author": "Foo Bar",
1954 "authorEmail": "foo@example.com"
1955 "authorEmail": "foo@example.com"
1955 "branch": "default",
1956 "branch": "default",
1956 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
1957 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
1957 "local": "1000",
1958 "local": "1000",
1958 "message": "...",
1959 "message": "...",
1959 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
1960 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
1960 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
1961 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
1961 "summary": "...",
1962 "summary": "...",
1962 "tag": "",
1963 "tag": "",
1963 "time": 1499546314,
1964 "time": 1499546314,
1964 }
1965 }
1965 }
1966 }
1966 }
1967 }
1967
1968
1968 Note: metadata extracted from "local:commits" will lose time zone
1969 Note: metadata extracted from "local:commits" will lose time zone
1969 information.
1970 information.
1970 """
1971 """
1971 props = diff.get(b'properties') or {}
1972 props = diff.get(b'properties') or {}
1972 meta = props.get(b'hg:meta')
1973 meta = props.get(b'hg:meta')
1973 if not meta:
1974 if not meta:
1974 if props.get(b'local:commits'):
1975 if props.get(b'local:commits'):
1975 commit = sorted(props[b'local:commits'].values())[0]
1976 commit = sorted(props[b'local:commits'].values())[0]
1976 meta = {}
1977 meta = {}
1977 if b'author' in commit and b'authorEmail' in commit:
1978 if b'author' in commit and b'authorEmail' in commit:
1978 meta[b'user'] = b'%s <%s>' % (
1979 meta[b'user'] = b'%s <%s>' % (
1979 commit[b'author'],
1980 commit[b'author'],
1980 commit[b'authorEmail'],
1981 commit[b'authorEmail'],
1981 )
1982 )
1982 if b'time' in commit:
1983 if b'time' in commit:
1983 meta[b'date'] = b'%d 0' % int(commit[b'time'])
1984 meta[b'date'] = b'%d 0' % int(commit[b'time'])
1984 if b'branch' in commit:
1985 if b'branch' in commit:
1985 meta[b'branch'] = commit[b'branch']
1986 meta[b'branch'] = commit[b'branch']
1986 node = commit.get(b'commit', commit.get(b'rev'))
1987 node = commit.get(b'commit', commit.get(b'rev'))
1987 if node:
1988 if node:
1988 meta[b'node'] = node
1989 meta[b'node'] = node
1989 if len(commit.get(b'parents', ())) >= 1:
1990 if len(commit.get(b'parents', ())) >= 1:
1990 meta[b'parent'] = commit[b'parents'][0]
1991 meta[b'parent'] = commit[b'parents'][0]
1991 else:
1992 else:
1992 meta = {}
1993 meta = {}
1993 if b'date' not in meta and b'dateCreated' in diff:
1994 if b'date' not in meta and b'dateCreated' in diff:
1994 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
1995 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
1995 if b'branch' not in meta and diff.get(b'branch'):
1996 if b'branch' not in meta and diff.get(b'branch'):
1996 meta[b'branch'] = diff[b'branch']
1997 meta[b'branch'] = diff[b'branch']
1997 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
1998 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
1998 meta[b'parent'] = diff[b'sourceControlBaseRevision']
1999 meta[b'parent'] = diff[b'sourceControlBaseRevision']
1999 return meta
2000 return meta
2000
2001
2001
2002
2002 def _getdrevs(ui, stack, specs):
2003 def _getdrevs(ui, stack, specs):
2003 """convert user supplied DREVSPECs into "Differential Revision" dicts
2004 """convert user supplied DREVSPECs into "Differential Revision" dicts
2004
2005
2005 See ``hg help phabread`` for how to specify each DREVSPEC.
2006 See ``hg help phabread`` for how to specify each DREVSPEC.
2006 """
2007 """
2007 if len(specs) > 0:
2008 if len(specs) > 0:
2008
2009
2009 def _formatspec(s):
2010 def _formatspec(s):
2010 if stack:
2011 if stack:
2011 s = b':(%s)' % s
2012 s = b':(%s)' % s
2012 return b'(%s)' % s
2013 return b'(%s)' % s
2013
2014
2014 spec = b'+'.join(pycompat.maplist(_formatspec, specs))
2015 spec = b'+'.join(pycompat.maplist(_formatspec, specs))
2015
2016
2016 drevs = querydrev(ui, spec)
2017 drevs = querydrev(ui, spec)
2017 if drevs:
2018 if drevs:
2018 return drevs
2019 return drevs
2019
2020
2020 raise error.Abort(_(b"empty DREVSPEC set"))
2021 raise error.Abort(_(b"empty DREVSPEC set"))
2021
2022
2022
2023
2023 def readpatch(ui, drevs, write):
2024 def readpatch(ui, drevs, write):
2024 """generate plain-text patch readable by 'hg import'
2025 """generate plain-text patch readable by 'hg import'
2025
2026
2026 write takes a list of (DREV, bytes), where DREV is the differential number
2027 write takes a list of (DREV, bytes), where DREV is the differential number
2027 (as bytes, without the "D" prefix) and the bytes are the text of a patch
2028 (as bytes, without the "D" prefix) and the bytes are the text of a patch
2028 to be imported. drevs is what "querydrev" returns, results of
2029 to be imported. drevs is what "querydrev" returns, results of
2029 "differential.query".
2030 "differential.query".
2030 """
2031 """
2031 # Prefetch hg:meta property for all diffs
2032 # Prefetch hg:meta property for all diffs
2032 diffids = sorted({max(int(v) for v in drev[b'diffs']) for drev in drevs})
2033 diffids = sorted({max(int(v) for v in drev[b'diffs']) for drev in drevs})
2033 diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})
2034 diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})
2034
2035
2035 patches = []
2036 patches = []
2036
2037
2037 # Generate patch for each drev
2038 # Generate patch for each drev
2038 for drev in drevs:
2039 for drev in drevs:
2039 ui.note(_(b'reading D%s\n') % drev[b'id'])
2040 ui.note(_(b'reading D%s\n') % drev[b'id'])
2040
2041
2041 diffid = max(int(v) for v in drev[b'diffs'])
2042 diffid = max(int(v) for v in drev[b'diffs'])
2042 body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})
2043 body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})
2043 desc = getdescfromdrev(drev)
2044 desc = getdescfromdrev(drev)
2044 header = b'# HG changeset patch\n'
2045 header = b'# HG changeset patch\n'
2045
2046
2046 # Try to preserve metadata from hg:meta property. Write hg patch
2047 # Try to preserve metadata from hg:meta property. Write hg patch
2047 # headers that can be read by the "import" command. See patchheadermap
2048 # headers that can be read by the "import" command. See patchheadermap
2048 # and extract in mercurial/patch.py for supported headers.
2049 # and extract in mercurial/patch.py for supported headers.
2049 meta = getdiffmeta(diffs[b'%d' % diffid])
2050 meta = getdiffmeta(diffs[b'%d' % diffid])
2050 for k in _metanamemap.keys():
2051 for k in _metanamemap.keys():
2051 if k in meta:
2052 if k in meta:
2052 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
2053 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
2053
2054
2054 content = b'%s%s\n%s' % (header, desc, body)
2055 content = b'%s%s\n%s' % (header, desc, body)
2055 patches.append((drev[b'id'], content))
2056 patches.append((drev[b'id'], content))
2056
2057
2057 # Write patches to the supplied callback
2058 # Write patches to the supplied callback
2058 write(patches)
2059 write(patches)
2059
2060
2060
2061
2061 @vcrcommand(
2062 @vcrcommand(
2062 b'phabread',
2063 b'phabread',
2063 [(b'', b'stack', False, _(b'read dependencies'))],
2064 [(b'', b'stack', False, _(b'read dependencies'))],
2064 _(b'DREVSPEC... [OPTIONS]'),
2065 _(b'DREVSPEC... [OPTIONS]'),
2065 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2066 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2066 optionalrepo=True,
2067 optionalrepo=True,
2067 )
2068 )
2068 def phabread(ui, repo, *specs, **opts):
2069 def phabread(ui, repo, *specs, **opts):
2069 """print patches from Phabricator suitable for importing
2070 """print patches from Phabricator suitable for importing
2070
2071
2071 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
2072 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
2072 the number ``123``. It could also have common operators like ``+``, ``-``,
2073 the number ``123``. It could also have common operators like ``+``, ``-``,
2073 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
2074 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
2074 select a stack. If multiple DREVSPEC values are given, the result is the
2075 select a stack. If multiple DREVSPEC values are given, the result is the
2075 union of each individually evaluated value. No attempt is currently made
2076 union of each individually evaluated value. No attempt is currently made
2076 to reorder the values to run from parent to child.
2077 to reorder the values to run from parent to child.
2077
2078
2078 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
2079 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
2079 could be used to filter patches by status. For performance reason, they
2080 could be used to filter patches by status. For performance reason, they
2080 only represent a subset of non-status selections and cannot be used alone.
2081 only represent a subset of non-status selections and cannot be used alone.
2081
2082
2082 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
2083 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
2083 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
2084 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
2084 stack up to D9.
2085 stack up to D9.
2085
2086
2086 If --stack is given, follow dependencies information and read all patches.
2087 If --stack is given, follow dependencies information and read all patches.
2087 It is equivalent to the ``:`` operator.
2088 It is equivalent to the ``:`` operator.
2088 """
2089 """
2089 opts = pycompat.byteskwargs(opts)
2090 opts = pycompat.byteskwargs(opts)
2090 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2091 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2091
2092
2092 def _write(patches):
2093 def _write(patches):
2093 for drev, content in patches:
2094 for drev, content in patches:
2094 ui.write(content)
2095 ui.write(content)
2095
2096
2096 readpatch(ui, drevs, _write)
2097 readpatch(ui, drevs, _write)
2097
2098
2098
2099
2099 @vcrcommand(
2100 @vcrcommand(
2100 b'phabimport',
2101 b'phabimport',
2101 [(b'', b'stack', False, _(b'import dependencies as well'))],
2102 [(b'', b'stack', False, _(b'import dependencies as well'))],
2102 _(b'DREVSPEC... [OPTIONS]'),
2103 _(b'DREVSPEC... [OPTIONS]'),
2103 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2104 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2104 )
2105 )
2105 def phabimport(ui, repo, *specs, **opts):
2106 def phabimport(ui, repo, *specs, **opts):
2106 """import patches from Phabricator for the specified Differential Revisions
2107 """import patches from Phabricator for the specified Differential Revisions
2107
2108
2108 The patches are read and applied starting at the parent of the working
2109 The patches are read and applied starting at the parent of the working
2109 directory.
2110 directory.
2110
2111
2111 See ``hg help phabread`` for how to specify DREVSPEC.
2112 See ``hg help phabread`` for how to specify DREVSPEC.
2112 """
2113 """
2113 opts = pycompat.byteskwargs(opts)
2114 opts = pycompat.byteskwargs(opts)
2114
2115
2115 # --bypass avoids losing exec and symlink bits when importing on Windows,
2116 # --bypass avoids losing exec and symlink bits when importing on Windows,
2116 # and allows importing with a dirty wdir. It also aborts instead of leaving
2117 # and allows importing with a dirty wdir. It also aborts instead of leaving
2117 # rejects.
2118 # rejects.
2118 opts[b'bypass'] = True
2119 opts[b'bypass'] = True
2119
2120
2120 # Mandatory default values, synced with commands.import
2121 # Mandatory default values, synced with commands.import
2121 opts[b'strip'] = 1
2122 opts[b'strip'] = 1
2122 opts[b'prefix'] = b''
2123 opts[b'prefix'] = b''
2123 # Evolve 9.3.0 assumes this key is present in cmdutil.tryimportone()
2124 # Evolve 9.3.0 assumes this key is present in cmdutil.tryimportone()
2124 opts[b'obsolete'] = False
2125 opts[b'obsolete'] = False
2125
2126
2126 if ui.configbool(b'phabimport', b'secret'):
2127 if ui.configbool(b'phabimport', b'secret'):
2127 opts[b'secret'] = True
2128 opts[b'secret'] = True
2128 if ui.configbool(b'phabimport', b'obsolete'):
2129 if ui.configbool(b'phabimport', b'obsolete'):
2129 opts[b'obsolete'] = True # Handled by evolve wrapping tryimportone()
2130 opts[b'obsolete'] = True # Handled by evolve wrapping tryimportone()
2130
2131
2131 def _write(patches):
2132 def _write(patches):
2132 parents = repo[None].parents()
2133 parents = repo[None].parents()
2133
2134
2134 with repo.wlock(), repo.lock(), repo.transaction(b'phabimport'):
2135 with repo.wlock(), repo.lock(), repo.transaction(b'phabimport'):
2135 for drev, contents in patches:
2136 for drev, contents in patches:
2136 ui.status(_(b'applying patch from D%s\n') % drev)
2137 ui.status(_(b'applying patch from D%s\n') % drev)
2137
2138
2138 with patch.extract(ui, pycompat.bytesio(contents)) as patchdata:
2139 with patch.extract(ui, pycompat.bytesio(contents)) as patchdata:
2139 msg, node, rej = cmdutil.tryimportone(
2140 msg, node, rej = cmdutil.tryimportone(
2140 ui,
2141 ui,
2141 repo,
2142 repo,
2142 patchdata,
2143 patchdata,
2143 parents,
2144 parents,
2144 opts,
2145 opts,
2145 [],
2146 [],
2146 None, # Never update wdir to another revision
2147 None, # Never update wdir to another revision
2147 )
2148 )
2148
2149
2149 if not node:
2150 if not node:
2150 raise error.Abort(_(b'D%s: no diffs found') % drev)
2151 raise error.Abort(_(b'D%s: no diffs found') % drev)
2151
2152
2152 ui.note(msg + b'\n')
2153 ui.note(msg + b'\n')
2153 parents = [repo[node]]
2154 parents = [repo[node]]
2154
2155
2155 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2156 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2156
2157
2157 readpatch(repo.ui, drevs, _write)
2158 readpatch(repo.ui, drevs, _write)
2158
2159
2159
2160
2160 @vcrcommand(
2161 @vcrcommand(
2161 b'phabupdate',
2162 b'phabupdate',
2162 [
2163 [
2163 (b'', b'accept', False, _(b'accept revisions')),
2164 (b'', b'accept', False, _(b'accept revisions')),
2164 (b'', b'reject', False, _(b'reject revisions')),
2165 (b'', b'reject', False, _(b'reject revisions')),
2165 (b'', b'request-review', False, _(b'request review on revisions')),
2166 (b'', b'request-review', False, _(b'request review on revisions')),
2166 (b'', b'abandon', False, _(b'abandon revisions')),
2167 (b'', b'abandon', False, _(b'abandon revisions')),
2167 (b'', b'reclaim', False, _(b'reclaim revisions')),
2168 (b'', b'reclaim', False, _(b'reclaim revisions')),
2168 (b'', b'close', False, _(b'close revisions')),
2169 (b'', b'close', False, _(b'close revisions')),
2169 (b'', b'reopen', False, _(b'reopen revisions')),
2170 (b'', b'reopen', False, _(b'reopen revisions')),
2170 (b'', b'plan-changes', False, _(b'plan changes for revisions')),
2171 (b'', b'plan-changes', False, _(b'plan changes for revisions')),
2171 (b'', b'resign', False, _(b'resign as a reviewer from revisions')),
2172 (b'', b'resign', False, _(b'resign as a reviewer from revisions')),
2172 (b'', b'commandeer', False, _(b'commandeer revisions')),
2173 (b'', b'commandeer', False, _(b'commandeer revisions')),
2173 (b'm', b'comment', b'', _(b'comment on the last revision')),
2174 (b'm', b'comment', b'', _(b'comment on the last revision')),
2174 ],
2175 ],
2175 _(b'DREVSPEC... [OPTIONS]'),
2176 _(b'DREVSPEC... [OPTIONS]'),
2176 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2177 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2177 optionalrepo=True,
2178 optionalrepo=True,
2178 )
2179 )
2179 def phabupdate(ui, repo, *specs, **opts):
2180 def phabupdate(ui, repo, *specs, **opts):
2180 """update Differential Revision in batch
2181 """update Differential Revision in batch
2181
2182
2182 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
2183 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
2183 """
2184 """
2184 opts = pycompat.byteskwargs(opts)
2185 opts = pycompat.byteskwargs(opts)
2185 transactions = [
2186 transactions = [
2186 b'abandon',
2187 b'abandon',
2187 b'accept',
2188 b'accept',
2188 b'close',
2189 b'close',
2189 b'commandeer',
2190 b'commandeer',
2190 b'plan-changes',
2191 b'plan-changes',
2191 b'reclaim',
2192 b'reclaim',
2192 b'reject',
2193 b'reject',
2193 b'reopen',
2194 b'reopen',
2194 b'request-review',
2195 b'request-review',
2195 b'resign',
2196 b'resign',
2196 ]
2197 ]
2197 flags = [n for n in transactions if opts.get(n.replace(b'-', b'_'))]
2198 flags = [n for n in transactions if opts.get(n.replace(b'-', b'_'))]
2198 if len(flags) > 1:
2199 if len(flags) > 1:
2199 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
2200 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
2200
2201
2201 actions = []
2202 actions = []
2202 for f in flags:
2203 for f in flags:
2203 actions.append({b'type': f, b'value': True})
2204 actions.append({b'type': f, b'value': True})
2204
2205
2205 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2206 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2206 for i, drev in enumerate(drevs):
2207 for i, drev in enumerate(drevs):
2207 if i + 1 == len(drevs) and opts.get(b'comment'):
2208 if i + 1 == len(drevs) and opts.get(b'comment'):
2208 actions.append({b'type': b'comment', b'value': opts[b'comment']})
2209 actions.append({b'type': b'comment', b'value': opts[b'comment']})
2209 if actions:
2210 if actions:
2210 params = {
2211 params = {
2211 b'objectIdentifier': drev[b'phid'],
2212 b'objectIdentifier': drev[b'phid'],
2212 b'transactions': actions,
2213 b'transactions': actions,
2213 }
2214 }
2214 callconduit(ui, b'differential.revision.edit', params)
2215 callconduit(ui, b'differential.revision.edit', params)
2215
2216
2216
2217
2217 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
2218 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
2218 def template_review(context, mapping):
2219 def template_review(context, mapping):
2219 """:phabreview: Object describing the review for this changeset.
2220 """:phabreview: Object describing the review for this changeset.
2220 Has attributes `url` and `id`.
2221 Has attributes `url` and `id`.
2221 """
2222 """
2222 ctx = context.resource(mapping, b'ctx')
2223 ctx = context.resource(mapping, b'ctx')
2223 m = _differentialrevisiondescre.search(ctx.description())
2224 m = _differentialrevisiondescre.search(ctx.description())
2224 if m:
2225 if m:
2225 return templateutil.hybriddict(
2226 return templateutil.hybriddict(
2226 {b'url': m.group('url'), b'id': b"D%s" % m.group('id'),}
2227 {b'url': m.group('url'), b'id': b"D%s" % m.group('id'),}
2227 )
2228 )
2228 else:
2229 else:
2229 tags = ctx.repo().nodetags(ctx.node())
2230 tags = ctx.repo().nodetags(ctx.node())
2230 for t in tags:
2231 for t in tags:
2231 if _differentialrevisiontagre.match(t):
2232 if _differentialrevisiontagre.match(t):
2232 url = ctx.repo().ui.config(b'phabricator', b'url')
2233 url = ctx.repo().ui.config(b'phabricator', b'url')
2233 if not url.endswith(b'/'):
2234 if not url.endswith(b'/'):
2234 url += b'/'
2235 url += b'/'
2235 url += t
2236 url += t
2236
2237
2237 return templateutil.hybriddict({b'url': url, b'id': t,})
2238 return templateutil.hybriddict({b'url': url, b'id': t,})
2238 return None
2239 return None
2239
2240
2240
2241
2241 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
2242 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
2242 def template_status(context, mapping):
2243 def template_status(context, mapping):
2243 """:phabstatus: String. Status of Phabricator differential.
2244 """:phabstatus: String. Status of Phabricator differential.
2244 """
2245 """
2245 ctx = context.resource(mapping, b'ctx')
2246 ctx = context.resource(mapping, b'ctx')
2246 repo = context.resource(mapping, b'repo')
2247 repo = context.resource(mapping, b'repo')
2247 ui = context.resource(mapping, b'ui')
2248 ui = context.resource(mapping, b'ui')
2248
2249
2249 rev = ctx.rev()
2250 rev = ctx.rev()
2250 try:
2251 try:
2251 drevid = getdrevmap(repo, [rev])[rev]
2252 drevid = getdrevmap(repo, [rev])[rev]
2252 except KeyError:
2253 except KeyError:
2253 return None
2254 return None
2254 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
2255 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
2255 for drev in drevs:
2256 for drev in drevs:
2256 if int(drev[b'id']) == drevid:
2257 if int(drev[b'id']) == drevid:
2257 return templateutil.hybriddict(
2258 return templateutil.hybriddict(
2258 {b'url': drev[b'uri'], b'status': drev[b'statusName'],}
2259 {b'url': drev[b'uri'], b'status': drev[b'statusName'],}
2259 )
2260 )
2260 return None
2261 return None
2261
2262
2262
2263
2263 @show.showview(b'phabstatus', csettopic=b'work')
2264 @show.showview(b'phabstatus', csettopic=b'work')
2264 def phabstatusshowview(ui, repo, displayer):
2265 def phabstatusshowview(ui, repo, displayer):
2265 """Phabricator differiential status"""
2266 """Phabricator differiential status"""
2266 revs = repo.revs('sort(_underway(), topo)')
2267 revs = repo.revs('sort(_underway(), topo)')
2267 drevmap = getdrevmap(repo, revs)
2268 drevmap = getdrevmap(repo, revs)
2268 unknownrevs, drevids, revsbydrevid = [], set(), {}
2269 unknownrevs, drevids, revsbydrevid = [], set(), {}
2269 for rev, drevid in pycompat.iteritems(drevmap):
2270 for rev, drevid in pycompat.iteritems(drevmap):
2270 if drevid is not None:
2271 if drevid is not None:
2271 drevids.add(drevid)
2272 drevids.add(drevid)
2272 revsbydrevid.setdefault(drevid, set()).add(rev)
2273 revsbydrevid.setdefault(drevid, set()).add(rev)
2273 else:
2274 else:
2274 unknownrevs.append(rev)
2275 unknownrevs.append(rev)
2275
2276
2276 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
2277 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
2277 drevsbyrev = {}
2278 drevsbyrev = {}
2278 for drev in drevs:
2279 for drev in drevs:
2279 for rev in revsbydrevid[int(drev[b'id'])]:
2280 for rev in revsbydrevid[int(drev[b'id'])]:
2280 drevsbyrev[rev] = drev
2281 drevsbyrev[rev] = drev
2281
2282
2282 def phabstatus(ctx):
2283 def phabstatus(ctx):
2283 drev = drevsbyrev[ctx.rev()]
2284 drev = drevsbyrev[ctx.rev()]
2284 status = ui.label(
2285 status = ui.label(
2285 b'%(statusName)s' % drev,
2286 b'%(statusName)s' % drev,
2286 b'phabricator.status.%s' % _getstatusname(drev),
2287 b'phabricator.status.%s' % _getstatusname(drev),
2287 )
2288 )
2288 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
2289 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
2289
2290
2290 revs -= smartset.baseset(unknownrevs)
2291 revs -= smartset.baseset(unknownrevs)
2291 revdag = graphmod.dagwalker(repo, revs)
2292 revdag = graphmod.dagwalker(repo, revs)
2292
2293
2293 ui.setconfig(b'experimental', b'graphshorten', True)
2294 ui.setconfig(b'experimental', b'graphshorten', True)
2294 displayer._exthook = phabstatus
2295 displayer._exthook = phabstatus
2295 nodelen = show.longestshortest(repo, revs)
2296 nodelen = show.longestshortest(repo, revs)
2296 logcmdutil.displaygraph(
2297 logcmdutil.displaygraph(
2297 ui,
2298 ui,
2298 repo,
2299 repo,
2299 revdag,
2300 revdag,
2300 displayer,
2301 displayer,
2301 graphmod.asciiedges,
2302 graphmod.asciiedges,
2302 props={b'nodelen': nodelen},
2303 props={b'nodelen': nodelen},
2303 )
2304 )
@@ -1,981 +1,974 b''
1 #require vcr
1 #require vcr
2 $ cat >> $HGRCPATH <<EOF
2 $ cat >> $HGRCPATH <<EOF
3 > [extensions]
3 > [extensions]
4 > phabricator =
4 > phabricator =
5 >
5 >
6 > [auth]
6 > [auth]
7 > hgphab.schemes = https
7 > hgphab.schemes = https
8 > hgphab.prefix = phab.mercurial-scm.org
8 > hgphab.prefix = phab.mercurial-scm.org
9 > # When working on the extension and making phabricator interaction
9 > # When working on the extension and making phabricator interaction
10 > # changes, edit this to be a real phabricator token. When done, edit
10 > # changes, edit this to be a real phabricator token. When done, edit
11 > # it back. The VCR transcripts will be auto-sanitised to replace your real
11 > # it back. The VCR transcripts will be auto-sanitised to replace your real
12 > # token with this value.
12 > # token with this value.
13 > hgphab.phabtoken = cli-hahayouwish
13 > hgphab.phabtoken = cli-hahayouwish
14 >
14 >
15 > [phabricator]
15 > [phabricator]
16 > debug = True
16 > debug = True
17 > EOF
17 > EOF
18 $ hg init repo
18 $ hg init repo
19 $ cd repo
19 $ cd repo
20 $ cat >> .hg/hgrc <<EOF
20 $ cat >> .hg/hgrc <<EOF
21 > [phabricator]
21 > [phabricator]
22 > url = https://phab.mercurial-scm.org/
22 > url = https://phab.mercurial-scm.org/
23 > callsign = HG
23 > callsign = HG
24 > EOF
24 > EOF
25 $ VCR="$TESTDIR/phabricator"
25 $ VCR="$TESTDIR/phabricator"
26
26
27 BROKEN: debugcallconduit fails without --test-vcr:
27 debugcallconduit doesn't claim invalid arguments without --test-vcr:
28 $ echo '{}' | HGRCSKIPREPO= hg debugcallconduit 'conduit.ping'
28 $ echo '{}' | HGRCSKIPREPO= hg debugcallconduit 'conduit.ping'
29 hg debugcallconduit: invalid arguments
29 abort: config phabricator.url is required
30 hg debugcallconduit METHOD
31
32 call Conduit API
33
34 options:
35
36 (use 'hg debugcallconduit -h' to show more help)
37 [255]
30 [255]
38
31
39 Error is handled reasonably. We override the phabtoken here so that
32 Error is handled reasonably. We override the phabtoken here so that
40 when you're developing changes to phabricator.py you can edit the
33 when you're developing changes to phabricator.py you can edit the
41 above config and have a real token in the test but not have to edit
34 above config and have a real token in the test but not have to edit
42 this test.
35 this test.
43 $ hg phabread --config auth.hgphab.phabtoken=cli-notavalidtoken \
36 $ hg phabread --config auth.hgphab.phabtoken=cli-notavalidtoken \
44 > --test-vcr "$VCR/phabread-conduit-error.json" D4480 | head
37 > --test-vcr "$VCR/phabread-conduit-error.json" D4480 | head
45 abort: Conduit Error (ERR-INVALID-AUTH): API token "cli-notavalidtoken" has the wrong length. API tokens should be 32 characters long.
38 abort: Conduit Error (ERR-INVALID-AUTH): API token "cli-notavalidtoken" has the wrong length. API tokens should be 32 characters long.
46
39
47 Missing arguments don't crash, and may print the command help
40 Missing arguments don't crash, and may print the command help
48
41
49 $ hg debugcallconduit
42 $ hg debugcallconduit
50 hg debugcallconduit: invalid arguments
43 hg debugcallconduit: invalid arguments
51 hg debugcallconduit METHOD
44 hg debugcallconduit METHOD
52
45
53 call Conduit API
46 call Conduit API
54
47
55 options:
48 options:
56
49
57 (use 'hg debugcallconduit -h' to show more help)
50 (use 'hg debugcallconduit -h' to show more help)
58 [255]
51 [255]
59 $ hg phabread
52 $ hg phabread
60 abort: empty DREVSPEC set
53 abort: empty DREVSPEC set
61 [255]
54 [255]
62
55
63 Basic phabread:
56 Basic phabread:
64 $ hg phabread --test-vcr "$VCR/phabread-4480.json" D4480 | head
57 $ hg phabread --test-vcr "$VCR/phabread-4480.json" D4480 | head
65 # HG changeset patch
58 # HG changeset patch
66 # Date 1536771503 0
59 # Date 1536771503 0
67 # Parent a5de21c9e3703f8e8eb064bd7d893ff2f703c66a
60 # Parent a5de21c9e3703f8e8eb064bd7d893ff2f703c66a
68 exchangev2: start to implement pull with wire protocol v2
61 exchangev2: start to implement pull with wire protocol v2
69
62
70 Wire protocol version 2 will take a substantially different
63 Wire protocol version 2 will take a substantially different
71 approach to exchange than version 1 (at least as far as pulling
64 approach to exchange than version 1 (at least as far as pulling
72 is concerned).
65 is concerned).
73
66
74 This commit establishes a new exchangev2 module for holding
67 This commit establishes a new exchangev2 module for holding
75
68
76 Phabread with multiple DREVSPEC
69 Phabread with multiple DREVSPEC
77
70
78 TODO: attempt to order related revisions like --stack?
71 TODO: attempt to order related revisions like --stack?
79 $ hg phabread --test-vcr "$VCR/phabread-multi-drev.json" D8205 8206 D8207 \
72 $ hg phabread --test-vcr "$VCR/phabread-multi-drev.json" D8205 8206 D8207 \
80 > | grep '^Differential Revision'
73 > | grep '^Differential Revision'
81 Differential Revision: https://phab.mercurial-scm.org/D8205
74 Differential Revision: https://phab.mercurial-scm.org/D8205
82 Differential Revision: https://phab.mercurial-scm.org/D8206
75 Differential Revision: https://phab.mercurial-scm.org/D8206
83 Differential Revision: https://phab.mercurial-scm.org/D8207
76 Differential Revision: https://phab.mercurial-scm.org/D8207
84
77
85 Empty DREVSPECs don't crash
78 Empty DREVSPECs don't crash
86
79
87 $ hg phabread --test-vcr "$VCR/phabread-empty-drev.json" D7917-D7917
80 $ hg phabread --test-vcr "$VCR/phabread-empty-drev.json" D7917-D7917
88 abort: empty DREVSPEC set
81 abort: empty DREVSPEC set
89 [255]
82 [255]
90
83
91
84
92 phabupdate with an accept:
85 phabupdate with an accept:
93 $ hg phabupdate --accept D4564 \
86 $ hg phabupdate --accept D4564 \
94 > -m 'I think I like where this is headed. Will read rest of series later.'\
87 > -m 'I think I like where this is headed. Will read rest of series later.'\
95 > --test-vcr "$VCR/accept-4564.json"
88 > --test-vcr "$VCR/accept-4564.json"
96 abort: Conduit Error (ERR-CONDUIT-CORE): Validation errors:
89 abort: Conduit Error (ERR-CONDUIT-CORE): Validation errors:
97 - You can not accept this revision because it has already been closed. Only open revisions can be accepted.
90 - You can not accept this revision because it has already been closed. Only open revisions can be accepted.
98 [255]
91 [255]
99 $ hg phabupdate --accept D7913 -m 'LGTM' --test-vcr "$VCR/accept-7913.json"
92 $ hg phabupdate --accept D7913 -m 'LGTM' --test-vcr "$VCR/accept-7913.json"
100
93
101 phabupdate with --plan-changes:
94 phabupdate with --plan-changes:
102
95
103 $ hg phabupdate --plan-changes D6876 --test-vcr "$VCR/phabupdate-change-6876.json"
96 $ hg phabupdate --plan-changes D6876 --test-vcr "$VCR/phabupdate-change-6876.json"
104
97
105 Create a differential diff:
98 Create a differential diff:
106 $ HGENCODING=utf-8; export HGENCODING
99 $ HGENCODING=utf-8; export HGENCODING
107 $ echo alpha > alpha
100 $ echo alpha > alpha
108 $ hg ci --addremove -m 'create alpha for phabricator test €'
101 $ hg ci --addremove -m 'create alpha for phabricator test €'
109 adding alpha
102 adding alpha
110 $ hg phabsend -r . --test-vcr "$VCR/phabsend-create-alpha.json"
103 $ hg phabsend -r . --test-vcr "$VCR/phabsend-create-alpha.json"
111 D7915 - created - d386117f30e6: create alpha for phabricator test \xe2\x82\xac (esc)
104 D7915 - created - d386117f30e6: create alpha for phabricator test \xe2\x82\xac (esc)
112 new commits: ['347bf67801e5']
105 new commits: ['347bf67801e5']
113 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d386117f30e6-24ffe649-phabsend.hg
106 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d386117f30e6-24ffe649-phabsend.hg
114 $ echo more >> alpha
107 $ echo more >> alpha
115 $ HGEDITOR=true hg ci --amend
108 $ HGEDITOR=true hg ci --amend
116 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/347bf67801e5-3bf313e4-amend.hg
109 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/347bf67801e5-3bf313e4-amend.hg
117 $ echo beta > beta
110 $ echo beta > beta
118 $ hg ci --addremove -m 'create beta for phabricator test'
111 $ hg ci --addremove -m 'create beta for phabricator test'
119 adding beta
112 adding beta
120 $ hg phabsend -r ".^::" --test-vcr "$VCR/phabsend-update-alpha-create-beta.json"
113 $ hg phabsend -r ".^::" --test-vcr "$VCR/phabsend-update-alpha-create-beta.json"
121 c44b38f24a45 mapped to old nodes []
114 c44b38f24a45 mapped to old nodes []
122 D7915 - updated - c44b38f24a45: create alpha for phabricator test \xe2\x82\xac (esc)
115 D7915 - updated - c44b38f24a45: create alpha for phabricator test \xe2\x82\xac (esc)
123 D7916 - created - 9e6901f21d5b: create beta for phabricator test
116 D7916 - created - 9e6901f21d5b: create beta for phabricator test
124 new commits: ['a692622e6937']
117 new commits: ['a692622e6937']
125 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9e6901f21d5b-1fcd4f0e-phabsend.hg
118 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9e6901f21d5b-1fcd4f0e-phabsend.hg
126 $ unset HGENCODING
119 $ unset HGENCODING
127
120
128 The amend won't explode after posting a public commit. The local tag is left
121 The amend won't explode after posting a public commit. The local tag is left
129 behind to identify it.
122 behind to identify it.
130
123
131 $ echo 'public change' > beta
124 $ echo 'public change' > beta
132 $ hg ci -m 'create public change for phabricator testing'
125 $ hg ci -m 'create public change for phabricator testing'
133 $ hg phase --public .
126 $ hg phase --public .
134 $ echo 'draft change' > alpha
127 $ echo 'draft change' > alpha
135 $ hg ci -m 'create draft change for phabricator testing'
128 $ hg ci -m 'create draft change for phabricator testing'
136 $ hg phabsend --amend -r '.^::' --test-vcr "$VCR/phabsend-create-public.json"
129 $ hg phabsend --amend -r '.^::' --test-vcr "$VCR/phabsend-create-public.json"
137 D7917 - created - 7b4185ab5d16: create public change for phabricator testing
130 D7917 - created - 7b4185ab5d16: create public change for phabricator testing
138 D7918 - created - 251c1c333fc6: create draft change for phabricator testing
131 D7918 - created - 251c1c333fc6: create draft change for phabricator testing
139 warning: not updating public commit 2:7b4185ab5d16
132 warning: not updating public commit 2:7b4185ab5d16
140 new commits: ['3244dc4a3334']
133 new commits: ['3244dc4a3334']
141 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/251c1c333fc6-41cb7c3b-phabsend.hg
134 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/251c1c333fc6-41cb7c3b-phabsend.hg
142 $ hg tags -v
135 $ hg tags -v
143 tip 3:3244dc4a3334
136 tip 3:3244dc4a3334
144 D7917 2:7b4185ab5d16 local
137 D7917 2:7b4185ab5d16 local
145
138
146 $ hg debugcallconduit user.search --test-vcr "$VCR/phab-conduit.json" <<EOF
139 $ hg debugcallconduit user.search --test-vcr "$VCR/phab-conduit.json" <<EOF
147 > {
140 > {
148 > "constraints": {
141 > "constraints": {
149 > "isBot": true
142 > "isBot": true
150 > }
143 > }
151 > }
144 > }
152 > EOF
145 > EOF
153 {
146 {
154 "cursor": {
147 "cursor": {
155 "after": null,
148 "after": null,
156 "before": null,
149 "before": null,
157 "limit": 100,
150 "limit": 100,
158 "order": null
151 "order": null
159 },
152 },
160 "data": [],
153 "data": [],
161 "maps": {},
154 "maps": {},
162 "query": {
155 "query": {
163 "queryKey": null
156 "queryKey": null
164 }
157 }
165 }
158 }
166
159
167 Template keywords
160 Template keywords
168 $ hg log -T'{rev} {phabreview|json}\n'
161 $ hg log -T'{rev} {phabreview|json}\n'
169 3 {"id": "D7918", "url": "https://phab.mercurial-scm.org/D7918"}
162 3 {"id": "D7918", "url": "https://phab.mercurial-scm.org/D7918"}
170 2 {"id": "D7917", "url": "https://phab.mercurial-scm.org/D7917"}
163 2 {"id": "D7917", "url": "https://phab.mercurial-scm.org/D7917"}
171 1 {"id": "D7916", "url": "https://phab.mercurial-scm.org/D7916"}
164 1 {"id": "D7916", "url": "https://phab.mercurial-scm.org/D7916"}
172 0 {"id": "D7915", "url": "https://phab.mercurial-scm.org/D7915"}
165 0 {"id": "D7915", "url": "https://phab.mercurial-scm.org/D7915"}
173
166
174 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n'
167 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n'
175 3 https://phab.mercurial-scm.org/D7918 D7918
168 3 https://phab.mercurial-scm.org/D7918 D7918
176 2 https://phab.mercurial-scm.org/D7917 D7917
169 2 https://phab.mercurial-scm.org/D7917 D7917
177 1 https://phab.mercurial-scm.org/D7916 D7916
170 1 https://phab.mercurial-scm.org/D7916 D7916
178 0 https://phab.mercurial-scm.org/D7915 D7915
171 0 https://phab.mercurial-scm.org/D7915 D7915
179
172
180 Commenting when phabsending:
173 Commenting when phabsending:
181 $ echo comment > comment
174 $ echo comment > comment
182 $ hg ci --addremove -m "create comment for phabricator test"
175 $ hg ci --addremove -m "create comment for phabricator test"
183 adding comment
176 adding comment
184 $ hg phabsend -r . -m "For default branch" --test-vcr "$VCR/phabsend-comment-created.json"
177 $ hg phabsend -r . -m "For default branch" --test-vcr "$VCR/phabsend-comment-created.json"
185 D7919 - created - d5dddca9023d: create comment for phabricator test
178 D7919 - created - d5dddca9023d: create comment for phabricator test
186 new commits: ['f7db812bbe1d']
179 new commits: ['f7db812bbe1d']
187 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d5dddca9023d-adf673ba-phabsend.hg
180 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d5dddca9023d-adf673ba-phabsend.hg
188 $ echo comment2 >> comment
181 $ echo comment2 >> comment
189 $ hg ci --amend
182 $ hg ci --amend
190 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f7db812bbe1d-8fcded77-amend.hg
183 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f7db812bbe1d-8fcded77-amend.hg
191 $ hg phabsend -r . -m "Address review comments" --test-vcr "$VCR/phabsend-comment-updated.json"
184 $ hg phabsend -r . -m "Address review comments" --test-vcr "$VCR/phabsend-comment-updated.json"
192 1849d7828727 mapped to old nodes []
185 1849d7828727 mapped to old nodes []
193 D7919 - updated - 1849d7828727: create comment for phabricator test
186 D7919 - updated - 1849d7828727: create comment for phabricator test
194
187
195 Phabsending a skipped commit:
188 Phabsending a skipped commit:
196 $ hg phabsend --no-amend -r . --test-vcr "$VCR/phabsend-skipped.json"
189 $ hg phabsend --no-amend -r . --test-vcr "$VCR/phabsend-skipped.json"
197 1849d7828727 mapped to old nodes ['1849d7828727']
190 1849d7828727 mapped to old nodes ['1849d7828727']
198 D7919 - skipped - 1849d7828727: create comment for phabricator test
191 D7919 - skipped - 1849d7828727: create comment for phabricator test
199
192
200 Phabsend doesn't create an instability when restacking existing revisions on top
193 Phabsend doesn't create an instability when restacking existing revisions on top
201 of new revisions.
194 of new revisions.
202
195
203 $ hg init reorder
196 $ hg init reorder
204 $ cd reorder
197 $ cd reorder
205 $ cat >> .hg/hgrc <<EOF
198 $ cat >> .hg/hgrc <<EOF
206 > [phabricator]
199 > [phabricator]
207 > url = https://phab.mercurial-scm.org/
200 > url = https://phab.mercurial-scm.org/
208 > callsign = HG
201 > callsign = HG
209 > [experimental]
202 > [experimental]
210 > evolution = all
203 > evolution = all
211 > EOF
204 > EOF
212
205
213 $ echo "add" > file1.txt
206 $ echo "add" > file1.txt
214 $ hg ci -Aqm 'added'
207 $ hg ci -Aqm 'added'
215 $ echo "mod1" > file1.txt
208 $ echo "mod1" > file1.txt
216 $ hg ci -m 'modified 1'
209 $ hg ci -m 'modified 1'
217 $ echo "mod2" > file1.txt
210 $ echo "mod2" > file1.txt
218 $ hg ci -m 'modified 2'
211 $ hg ci -m 'modified 2'
219 $ hg phabsend -r . --test-vcr "$VCR/phabsend-add-parent-setup.json"
212 $ hg phabsend -r . --test-vcr "$VCR/phabsend-add-parent-setup.json"
220 D8433 - created - 5d3959e20d1d: modified 2
213 D8433 - created - 5d3959e20d1d: modified 2
221 new commits: ['2b4aa8a88d61']
214 new commits: ['2b4aa8a88d61']
222 $ hg log -G -T compact
215 $ hg log -G -T compact
223 @ 3[tip]:1 2b4aa8a88d61 1970-01-01 00:00 +0000 test
216 @ 3[tip]:1 2b4aa8a88d61 1970-01-01 00:00 +0000 test
224 | modified 2
217 | modified 2
225 |
218 |
226 o 1 d549263bcb2d 1970-01-01 00:00 +0000 test
219 o 1 d549263bcb2d 1970-01-01 00:00 +0000 test
227 | modified 1
220 | modified 1
228 |
221 |
229 o 0 5cbade24e0fa 1970-01-01 00:00 +0000 test
222 o 0 5cbade24e0fa 1970-01-01 00:00 +0000 test
230 added
223 added
231
224
232 Also check that it doesn't create more orphans outside of the stack
225 Also check that it doesn't create more orphans outside of the stack
233
226
234 $ hg up -q 1
227 $ hg up -q 1
235 $ echo "mod3" > file1.txt
228 $ echo "mod3" > file1.txt
236 $ hg ci -m 'modified 3'
229 $ hg ci -m 'modified 3'
237 created new head
230 created new head
238 $ hg up -q 3
231 $ hg up -q 3
239 $ hg phabsend -r ".^ + ." --test-vcr "$VCR/phabsend-add-parent.json"
232 $ hg phabsend -r ".^ + ." --test-vcr "$VCR/phabsend-add-parent.json"
240 2b4aa8a88d61 mapped to old nodes ['2b4aa8a88d61']
233 2b4aa8a88d61 mapped to old nodes ['2b4aa8a88d61']
241 D8434 - created - d549263bcb2d: modified 1
234 D8434 - created - d549263bcb2d: modified 1
242 D8433 - updated - 2b4aa8a88d61: modified 2
235 D8433 - updated - 2b4aa8a88d61: modified 2
243 new commits: ['876a60d024de']
236 new commits: ['876a60d024de']
244 new commits: ['0c6523cb1d0f']
237 new commits: ['0c6523cb1d0f']
245 restabilizing 1eda4bf55021 as d2c78c3a3e01
238 restabilizing 1eda4bf55021 as d2c78c3a3e01
246 $ hg log -G -T compact
239 $ hg log -G -T compact
247 o 7[tip]:5 d2c78c3a3e01 1970-01-01 00:00 +0000 test
240 o 7[tip]:5 d2c78c3a3e01 1970-01-01 00:00 +0000 test
248 | modified 3
241 | modified 3
249 |
242 |
250 | @ 6 0c6523cb1d0f 1970-01-01 00:00 +0000 test
243 | @ 6 0c6523cb1d0f 1970-01-01 00:00 +0000 test
251 |/ modified 2
244 |/ modified 2
252 |
245 |
253 o 5:0 876a60d024de 1970-01-01 00:00 +0000 test
246 o 5:0 876a60d024de 1970-01-01 00:00 +0000 test
254 | modified 1
247 | modified 1
255 |
248 |
256 o 0 5cbade24e0fa 1970-01-01 00:00 +0000 test
249 o 0 5cbade24e0fa 1970-01-01 00:00 +0000 test
257 added
250 added
258
251
259 Posting obsolete commits is disallowed
252 Posting obsolete commits is disallowed
260
253
261 $ echo "mod3" > file1.txt
254 $ echo "mod3" > file1.txt
262 $ hg ci -m 'modified A'
255 $ hg ci -m 'modified A'
263 $ echo "mod4" > file1.txt
256 $ echo "mod4" > file1.txt
264 $ hg ci -m 'modified B'
257 $ hg ci -m 'modified B'
265
258
266 $ hg up '.^'
259 $ hg up '.^'
267 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
260 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
268 $ echo 'obsolete' > file1.txt
261 $ echo 'obsolete' > file1.txt
269 $ hg amend --config extensions.amend=
262 $ hg amend --config extensions.amend=
270 1 new orphan changesets
263 1 new orphan changesets
271 $ hg log -G
264 $ hg log -G
272 @ changeset: 10:082be6c94150
265 @ changeset: 10:082be6c94150
273 | tag: tip
266 | tag: tip
274 | parent: 6:0c6523cb1d0f
267 | parent: 6:0c6523cb1d0f
275 | user: test
268 | user: test
276 | date: Thu Jan 01 00:00:00 1970 +0000
269 | date: Thu Jan 01 00:00:00 1970 +0000
277 | summary: modified A
270 | summary: modified A
278 |
271 |
279 | * changeset: 9:a67643f48146
272 | * changeset: 9:a67643f48146
280 | | user: test
273 | | user: test
281 | | date: Thu Jan 01 00:00:00 1970 +0000
274 | | date: Thu Jan 01 00:00:00 1970 +0000
282 | | instability: orphan
275 | | instability: orphan
283 | | summary: modified B
276 | | summary: modified B
284 | |
277 | |
285 | x changeset: 8:db79727cb2f7
278 | x changeset: 8:db79727cb2f7
286 |/ parent: 6:0c6523cb1d0f
279 |/ parent: 6:0c6523cb1d0f
287 | user: test
280 | user: test
288 | date: Thu Jan 01 00:00:00 1970 +0000
281 | date: Thu Jan 01 00:00:00 1970 +0000
289 | obsolete: rewritten using amend as 10:082be6c94150
282 | obsolete: rewritten using amend as 10:082be6c94150
290 | summary: modified A
283 | summary: modified A
291 |
284 |
292 | o changeset: 7:d2c78c3a3e01
285 | o changeset: 7:d2c78c3a3e01
293 | | parent: 5:876a60d024de
286 | | parent: 5:876a60d024de
294 | | user: test
287 | | user: test
295 | | date: Thu Jan 01 00:00:00 1970 +0000
288 | | date: Thu Jan 01 00:00:00 1970 +0000
296 | | summary: modified 3
289 | | summary: modified 3
297 | |
290 | |
298 o | changeset: 6:0c6523cb1d0f
291 o | changeset: 6:0c6523cb1d0f
299 |/ user: test
292 |/ user: test
300 | date: Thu Jan 01 00:00:00 1970 +0000
293 | date: Thu Jan 01 00:00:00 1970 +0000
301 | summary: modified 2
294 | summary: modified 2
302 |
295 |
303 o changeset: 5:876a60d024de
296 o changeset: 5:876a60d024de
304 | parent: 0:5cbade24e0fa
297 | parent: 0:5cbade24e0fa
305 | user: test
298 | user: test
306 | date: Thu Jan 01 00:00:00 1970 +0000
299 | date: Thu Jan 01 00:00:00 1970 +0000
307 | summary: modified 1
300 | summary: modified 1
308 |
301 |
309 o changeset: 0:5cbade24e0fa
302 o changeset: 0:5cbade24e0fa
310 user: test
303 user: test
311 date: Thu Jan 01 00:00:00 1970 +0000
304 date: Thu Jan 01 00:00:00 1970 +0000
312 summary: added
305 summary: added
313
306
314 $ hg phabsend -r 5::
307 $ hg phabsend -r 5::
315 abort: obsolete commits cannot be posted for review
308 abort: obsolete commits cannot be posted for review
316 [255]
309 [255]
317
310
318 Don't restack existing orphans
311 Don't restack existing orphans
319
312
320 $ hg phabsend -r 5::tip --test-vcr "$VCR/phabsend-no-restack-orphan.json"
313 $ hg phabsend -r 5::tip --test-vcr "$VCR/phabsend-no-restack-orphan.json"
321 876a60d024de mapped to old nodes ['876a60d024de']
314 876a60d024de mapped to old nodes ['876a60d024de']
322 0c6523cb1d0f mapped to old nodes ['0c6523cb1d0f']
315 0c6523cb1d0f mapped to old nodes ['0c6523cb1d0f']
323 D8434 - updated - 876a60d024de: modified 1
316 D8434 - updated - 876a60d024de: modified 1
324 D8433 - updated - 0c6523cb1d0f: modified 2
317 D8433 - updated - 0c6523cb1d0f: modified 2
325 D8435 - created - 082be6c94150: modified A
318 D8435 - created - 082be6c94150: modified A
326 new commits: ['b5913193c805']
319 new commits: ['b5913193c805']
327 not restabilizing unchanged d2c78c3a3e01
320 not restabilizing unchanged d2c78c3a3e01
328 $ hg log -G
321 $ hg log -G
329 @ changeset: 11:b5913193c805
322 @ changeset: 11:b5913193c805
330 | tag: tip
323 | tag: tip
331 | parent: 6:0c6523cb1d0f
324 | parent: 6:0c6523cb1d0f
332 | user: test
325 | user: test
333 | date: Thu Jan 01 00:00:00 1970 +0000
326 | date: Thu Jan 01 00:00:00 1970 +0000
334 | summary: modified A
327 | summary: modified A
335 |
328 |
336 | * changeset: 9:a67643f48146
329 | * changeset: 9:a67643f48146
337 | | user: test
330 | | user: test
338 | | date: Thu Jan 01 00:00:00 1970 +0000
331 | | date: Thu Jan 01 00:00:00 1970 +0000
339 | | instability: orphan
332 | | instability: orphan
340 | | summary: modified B
333 | | summary: modified B
341 | |
334 | |
342 | x changeset: 8:db79727cb2f7
335 | x changeset: 8:db79727cb2f7
343 |/ parent: 6:0c6523cb1d0f
336 |/ parent: 6:0c6523cb1d0f
344 | user: test
337 | user: test
345 | date: Thu Jan 01 00:00:00 1970 +0000
338 | date: Thu Jan 01 00:00:00 1970 +0000
346 | obsolete: rewritten using amend, phabsend as 11:b5913193c805
339 | obsolete: rewritten using amend, phabsend as 11:b5913193c805
347 | summary: modified A
340 | summary: modified A
348 |
341 |
349 | o changeset: 7:d2c78c3a3e01
342 | o changeset: 7:d2c78c3a3e01
350 | | parent: 5:876a60d024de
343 | | parent: 5:876a60d024de
351 | | user: test
344 | | user: test
352 | | date: Thu Jan 01 00:00:00 1970 +0000
345 | | date: Thu Jan 01 00:00:00 1970 +0000
353 | | summary: modified 3
346 | | summary: modified 3
354 | |
347 | |
355 o | changeset: 6:0c6523cb1d0f
348 o | changeset: 6:0c6523cb1d0f
356 |/ user: test
349 |/ user: test
357 | date: Thu Jan 01 00:00:00 1970 +0000
350 | date: Thu Jan 01 00:00:00 1970 +0000
358 | summary: modified 2
351 | summary: modified 2
359 |
352 |
360 o changeset: 5:876a60d024de
353 o changeset: 5:876a60d024de
361 | parent: 0:5cbade24e0fa
354 | parent: 0:5cbade24e0fa
362 | user: test
355 | user: test
363 | date: Thu Jan 01 00:00:00 1970 +0000
356 | date: Thu Jan 01 00:00:00 1970 +0000
364 | summary: modified 1
357 | summary: modified 1
365 |
358 |
366 o changeset: 0:5cbade24e0fa
359 o changeset: 0:5cbade24e0fa
367 user: test
360 user: test
368 date: Thu Jan 01 00:00:00 1970 +0000
361 date: Thu Jan 01 00:00:00 1970 +0000
369 summary: added
362 summary: added
370
363
371 $ cd ..
364 $ cd ..
372
365
373 Phabesending a new binary, a modified binary, and a removed binary
366 Phabesending a new binary, a modified binary, and a removed binary
374
367
375 >>> open('bin', 'wb').write(b'\0a') and None
368 >>> open('bin', 'wb').write(b'\0a') and None
376 $ hg ci -Am 'add binary'
369 $ hg ci -Am 'add binary'
377 adding bin
370 adding bin
378 >>> open('bin', 'wb').write(b'\0b') and None
371 >>> open('bin', 'wb').write(b'\0b') and None
379 $ hg ci -m 'modify binary'
372 $ hg ci -m 'modify binary'
380 $ hg rm bin
373 $ hg rm bin
381 $ hg ci -m 'remove binary'
374 $ hg ci -m 'remove binary'
382 $ hg phabsend -r .~2:: --test-vcr "$VCR/phabsend-binary.json"
375 $ hg phabsend -r .~2:: --test-vcr "$VCR/phabsend-binary.json"
383 uploading bin@aa24a81f55de
376 uploading bin@aa24a81f55de
384 D8007 - created - aa24a81f55de: add binary
377 D8007 - created - aa24a81f55de: add binary
385 uploading bin@d8d62a881b54
378 uploading bin@d8d62a881b54
386 D8008 - created - d8d62a881b54: modify binary
379 D8008 - created - d8d62a881b54: modify binary
387 D8009 - created - af55645b2e29: remove binary
380 D8009 - created - af55645b2e29: remove binary
388 new commits: ['b8139fbb4a57']
381 new commits: ['b8139fbb4a57']
389 new commits: ['c88ce4c2d2ad']
382 new commits: ['c88ce4c2d2ad']
390 new commits: ['75dbbc901145']
383 new commits: ['75dbbc901145']
391 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/aa24a81f55de-a3a0cf24-phabsend.hg
384 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/aa24a81f55de-a3a0cf24-phabsend.hg
392
385
393 Phabsend a renamed binary and a copied binary, with and without content changes
386 Phabsend a renamed binary and a copied binary, with and without content changes
394 to src and dest
387 to src and dest
395
388
396 >>> open('bin2', 'wb').write(b'\0c') and None
389 >>> open('bin2', 'wb').write(b'\0c') and None
397 $ hg ci -Am 'add another binary'
390 $ hg ci -Am 'add another binary'
398 adding bin2
391 adding bin2
399
392
400 TODO: "bin2" can't be viewed in this commit (left or right side), and the URL
393 TODO: "bin2" can't be viewed in this commit (left or right side), and the URL
401 looks much different than when viewing "bin2_moved". No idea if this is a phab
394 looks much different than when viewing "bin2_moved". No idea if this is a phab
402 bug, or phabsend bug. The patch (as printed by phabread) look reasonable
395 bug, or phabsend bug. The patch (as printed by phabread) look reasonable
403 though.
396 though.
404
397
405 $ hg mv bin2 bin2_moved
398 $ hg mv bin2 bin2_moved
406 $ hg ci -m "moved binary"
399 $ hg ci -m "moved binary"
407
400
408 Note: "bin2_moved" is also not viewable in phabricator with this review
401 Note: "bin2_moved" is also not viewable in phabricator with this review
409
402
410 $ hg cp bin2_moved bin2_copied
403 $ hg cp bin2_moved bin2_copied
411 $ hg ci -m "copied binary"
404 $ hg ci -m "copied binary"
412
405
413 Note: "bin2_moved_again" is marked binary in phabricator, and both sides of it
406 Note: "bin2_moved_again" is marked binary in phabricator, and both sides of it
414 are viewable in their proper state. "bin2_copied" is not viewable, and not
407 are viewable in their proper state. "bin2_copied" is not viewable, and not
415 listed as binary in phabricator.
408 listed as binary in phabricator.
416
409
417 >>> open('bin2_copied', 'wb').write(b'\0move+mod') and None
410 >>> open('bin2_copied', 'wb').write(b'\0move+mod') and None
418 $ hg mv bin2_copied bin2_moved_again
411 $ hg mv bin2_copied bin2_moved_again
419 $ hg ci -m "move+mod copied binary"
412 $ hg ci -m "move+mod copied binary"
420
413
421 Note: "bin2_moved" and "bin2_moved_copy" are both marked binary, and both
414 Note: "bin2_moved" and "bin2_moved_copy" are both marked binary, and both
422 viewable on each side.
415 viewable on each side.
423
416
424 >>> open('bin2_moved', 'wb').write(b'\0precopy mod') and None
417 >>> open('bin2_moved', 'wb').write(b'\0precopy mod') and None
425 $ hg cp bin2_moved bin2_moved_copied
418 $ hg cp bin2_moved bin2_moved_copied
426 >>> open('bin2_moved', 'wb').write(b'\0copy src+mod') and None
419 >>> open('bin2_moved', 'wb').write(b'\0copy src+mod') and None
427 $ hg ci -m "copy+mod moved binary"
420 $ hg ci -m "copy+mod moved binary"
428
421
429 $ hg phabsend -r .~4:: --test-vcr "$VCR/phabsend-binary-renames.json"
422 $ hg phabsend -r .~4:: --test-vcr "$VCR/phabsend-binary-renames.json"
430 uploading bin2@f42f9195e00c
423 uploading bin2@f42f9195e00c
431 D8128 - created - f42f9195e00c: add another binary
424 D8128 - created - f42f9195e00c: add another binary
432 D8129 - created - 834ab31d80ae: moved binary
425 D8129 - created - 834ab31d80ae: moved binary
433 D8130 - created - 494b750e5194: copied binary
426 D8130 - created - 494b750e5194: copied binary
434 uploading bin2_moved_again@25f766b50cc2
427 uploading bin2_moved_again@25f766b50cc2
435 D8131 - created - 25f766b50cc2: move+mod copied binary
428 D8131 - created - 25f766b50cc2: move+mod copied binary
436 uploading bin2_moved_copied@1b87b363a5e4
429 uploading bin2_moved_copied@1b87b363a5e4
437 uploading bin2_moved@1b87b363a5e4
430 uploading bin2_moved@1b87b363a5e4
438 D8132 - created - 1b87b363a5e4: copy+mod moved binary
431 D8132 - created - 1b87b363a5e4: copy+mod moved binary
439 new commits: ['90437c20312a']
432 new commits: ['90437c20312a']
440 new commits: ['f391f4da4c61']
433 new commits: ['f391f4da4c61']
441 new commits: ['da86a9f3268c']
434 new commits: ['da86a9f3268c']
442 new commits: ['003ffc16ba66']
435 new commits: ['003ffc16ba66']
443 new commits: ['13bd750c36fa']
436 new commits: ['13bd750c36fa']
444 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f42f9195e00c-e82a0769-phabsend.hg
437 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/f42f9195e00c-e82a0769-phabsend.hg
445
438
446 Phabreading a DREV with a local:commits time as a string:
439 Phabreading a DREV with a local:commits time as a string:
447 $ hg phabread --test-vcr "$VCR/phabread-str-time.json" D1285
440 $ hg phabread --test-vcr "$VCR/phabread-str-time.json" D1285
448 # HG changeset patch
441 # HG changeset patch
449 # User Pulkit Goyal <7895pulkit@gmail.com>
442 # User Pulkit Goyal <7895pulkit@gmail.com>
450 # Date 1509404054 -19800
443 # Date 1509404054 -19800
451 # Node ID 44fc1c1f1774a76423b9c732af6938435099bcc5
444 # Node ID 44fc1c1f1774a76423b9c732af6938435099bcc5
452 # Parent 8feef8ef8389a3b544e0a74624f1efc3a8d85d35
445 # Parent 8feef8ef8389a3b544e0a74624f1efc3a8d85d35
453 repoview: add a new attribute _visibilityexceptions and related API
446 repoview: add a new attribute _visibilityexceptions and related API
454
447
455 Currently we don't have a defined way in core to make some hidden revisions
448 Currently we don't have a defined way in core to make some hidden revisions
456 visible in filtered repo. Extensions to achieve the purpose of unhiding some
449 visible in filtered repo. Extensions to achieve the purpose of unhiding some
457 hidden commits, wrap repoview.pinnedrevs() function.
450 hidden commits, wrap repoview.pinnedrevs() function.
458
451
459 To make the above task simple and have well defined API, this patch adds a new
452 To make the above task simple and have well defined API, this patch adds a new
460 attribute '_visibilityexceptions' to repoview class which will contains
453 attribute '_visibilityexceptions' to repoview class which will contains
461 the hidden revs which should be exception.
454 the hidden revs which should be exception.
462 This will allow to set different exceptions for different repoview objects
455 This will allow to set different exceptions for different repoview objects
463 backed by the same unfiltered repo.
456 backed by the same unfiltered repo.
464
457
465 This patch also adds API to add revs to the attribute set and get them.
458 This patch also adds API to add revs to the attribute set and get them.
466
459
467 Thanks to Jun for suggesting the use of repoview class instead of localrepo.
460 Thanks to Jun for suggesting the use of repoview class instead of localrepo.
468
461
469 Differential Revision: https://phab.mercurial-scm.org/D1285
462 Differential Revision: https://phab.mercurial-scm.org/D1285
470 diff --git a/mercurial/repoview.py b/mercurial/repoview.py
463 diff --git a/mercurial/repoview.py b/mercurial/repoview.py
471 --- a/mercurial/repoview.py
464 --- a/mercurial/repoview.py
472 +++ b/mercurial/repoview.py
465 +++ b/mercurial/repoview.py
473 @@ * @@ (glob)
466 @@ * @@ (glob)
474 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
467 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
475 """
468 """
476
469
477 + # hidden revs which should be visible
470 + # hidden revs which should be visible
478 + _visibilityexceptions = set()
471 + _visibilityexceptions = set()
479 +
472 +
480 def __init__(self, repo, filtername):
473 def __init__(self, repo, filtername):
481 object.__setattr__(self, r'_unfilteredrepo', repo)
474 object.__setattr__(self, r'_unfilteredrepo', repo)
482 object.__setattr__(self, r'filtername', filtername)
475 object.__setattr__(self, r'filtername', filtername)
483 @@ -231,6 +234,14 @@
476 @@ -231,6 +234,14 @@
484 return self
477 return self
485 return self.unfiltered().filtered(name)
478 return self.unfiltered().filtered(name)
486
479
487 + def addvisibilityexceptions(self, revs):
480 + def addvisibilityexceptions(self, revs):
488 + """adds hidden revs which should be visible to set of exceptions"""
481 + """adds hidden revs which should be visible to set of exceptions"""
489 + self._visibilityexceptions.update(revs)
482 + self._visibilityexceptions.update(revs)
490 +
483 +
491 + def getvisibilityexceptions(self):
484 + def getvisibilityexceptions(self):
492 + """returns the set of hidden revs which should be visible"""
485 + """returns the set of hidden revs which should be visible"""
493 + return self._visibilityexceptions
486 + return self._visibilityexceptions
494 +
487 +
495 # everything access are forwarded to the proxied repo
488 # everything access are forwarded to the proxied repo
496 def __getattr__(self, attr):
489 def __getattr__(self, attr):
497 return getattr(self._unfilteredrepo, attr)
490 return getattr(self._unfilteredrepo, attr)
498 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
491 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
499 --- a/mercurial/localrepo.py
492 --- a/mercurial/localrepo.py
500 +++ b/mercurial/localrepo.py
493 +++ b/mercurial/localrepo.py
501 @@ -570,6 +570,14 @@
494 @@ -570,6 +570,14 @@
502 def close(self):
495 def close(self):
503 self._writecaches()
496 self._writecaches()
504
497
505 + def addvisibilityexceptions(self, exceptions):
498 + def addvisibilityexceptions(self, exceptions):
506 + # should be called on a filtered repository
499 + # should be called on a filtered repository
507 + pass
500 + pass
508 +
501 +
509 + def getvisibilityexceptions(self):
502 + def getvisibilityexceptions(self):
510 + # should be called on a filtered repository
503 + # should be called on a filtered repository
511 + return set()
504 + return set()
512 +
505 +
513 def _loadextensions(self):
506 def _loadextensions(self):
514 extensions.loadall(self.ui)
507 extensions.loadall(self.ui)
515
508
516
509
517 A bad .arcconfig doesn't error out
510 A bad .arcconfig doesn't error out
518 $ echo 'garbage' > .arcconfig
511 $ echo 'garbage' > .arcconfig
519 $ hg config phabricator --debug
512 $ hg config phabricator --debug
520 invalid JSON in $TESTTMP/repo/.arcconfig
513 invalid JSON in $TESTTMP/repo/.arcconfig
521 read config from: */.hgrc (glob)
514 read config from: */.hgrc (glob)
522 */.hgrc:*: phabricator.debug=True (glob)
515 */.hgrc:*: phabricator.debug=True (glob)
523 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=https://phab.mercurial-scm.org/ (glob)
516 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=https://phab.mercurial-scm.org/ (glob)
524 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=HG (glob)
517 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=HG (glob)
525
518
526 The .arcconfig content overrides global config
519 The .arcconfig content overrides global config
527 $ cat >> $HGRCPATH << EOF
520 $ cat >> $HGRCPATH << EOF
528 > [phabricator]
521 > [phabricator]
529 > url = global
522 > url = global
530 > callsign = global
523 > callsign = global
531 > EOF
524 > EOF
532 $ cp $TESTDIR/../.arcconfig .
525 $ cp $TESTDIR/../.arcconfig .
533 $ mv .hg/hgrc .hg/hgrc.bak
526 $ mv .hg/hgrc .hg/hgrc.bak
534 $ hg config phabricator --debug
527 $ hg config phabricator --debug
535 read config from: */.hgrc (glob)
528 read config from: */.hgrc (glob)
536 */.hgrc:*: phabricator.debug=True (glob)
529 */.hgrc:*: phabricator.debug=True (glob)
537 $TESTTMP/repo/.arcconfig: phabricator.callsign=HG
530 $TESTTMP/repo/.arcconfig: phabricator.callsign=HG
538 $TESTTMP/repo/.arcconfig: phabricator.url=https://phab.mercurial-scm.org/
531 $TESTTMP/repo/.arcconfig: phabricator.url=https://phab.mercurial-scm.org/
539
532
540 But it doesn't override local config
533 But it doesn't override local config
541 $ cat >> .hg/hgrc << EOF
534 $ cat >> .hg/hgrc << EOF
542 > [phabricator]
535 > [phabricator]
543 > url = local
536 > url = local
544 > callsign = local
537 > callsign = local
545 > EOF
538 > EOF
546 $ hg config phabricator --debug
539 $ hg config phabricator --debug
547 read config from: */.hgrc (glob)
540 read config from: */.hgrc (glob)
548 */.hgrc:*: phabricator.debug=True (glob)
541 */.hgrc:*: phabricator.debug=True (glob)
549 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=local (glob)
542 $TESTTMP/repo/.hg/hgrc:*: phabricator.url=local (glob)
550 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=local (glob)
543 $TESTTMP/repo/.hg/hgrc:*: phabricator.callsign=local (glob)
551 $ mv .hg/hgrc.bak .hg/hgrc
544 $ mv .hg/hgrc.bak .hg/hgrc
552
545
553 Phabimport works with a stack
546 Phabimport works with a stack
554
547
555 $ cd ..
548 $ cd ..
556 $ hg clone repo repo2 -qr 1
549 $ hg clone repo repo2 -qr 1
557 $ cp repo/.hg/hgrc repo2/.hg/
550 $ cp repo/.hg/hgrc repo2/.hg/
558 $ cd repo2
551 $ cd repo2
559 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json"
552 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json"
560 applying patch from D7917
553 applying patch from D7917
561 applying patch from D7918
554 applying patch from D7918
562 $ hg log -r .: -G -Tcompact
555 $ hg log -r .: -G -Tcompact
563 o 3[tip] aaef04066140 1970-01-01 00:00 +0000 test
556 o 3[tip] aaef04066140 1970-01-01 00:00 +0000 test
564 | create draft change for phabricator testing
557 | create draft change for phabricator testing
565 |
558 |
566 o 2 8de3712202d1 1970-01-01 00:00 +0000 test
559 o 2 8de3712202d1 1970-01-01 00:00 +0000 test
567 | create public change for phabricator testing
560 | create public change for phabricator testing
568 |
561 |
569 @ 1 a692622e6937 1970-01-01 00:00 +0000 test
562 @ 1 a692622e6937 1970-01-01 00:00 +0000 test
570 | create beta for phabricator test
563 | create beta for phabricator test
571 ~
564 ~
572 Phabimport can create secret commits
565 Phabimport can create secret commits
573
566
574 $ hg rollback --config ui.rollback=True
567 $ hg rollback --config ui.rollback=True
575 repository tip rolled back to revision 1 (undo phabimport)
568 repository tip rolled back to revision 1 (undo phabimport)
576 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json" \
569 $ hg phabimport --stack 'D7918' --test-vcr "$VCR/phabimport-stack.json" \
577 > --config phabimport.secret=True
570 > --config phabimport.secret=True
578 applying patch from D7917
571 applying patch from D7917
579 applying patch from D7918
572 applying patch from D7918
580 $ hg log -r 'reverse(.:)' -T phases
573 $ hg log -r 'reverse(.:)' -T phases
581 changeset: 3:aaef04066140
574 changeset: 3:aaef04066140
582 tag: tip
575 tag: tip
583 phase: secret
576 phase: secret
584 user: test
577 user: test
585 date: Thu Jan 01 00:00:00 1970 +0000
578 date: Thu Jan 01 00:00:00 1970 +0000
586 summary: create draft change for phabricator testing
579 summary: create draft change for phabricator testing
587
580
588 changeset: 2:8de3712202d1
581 changeset: 2:8de3712202d1
589 phase: secret
582 phase: secret
590 user: test
583 user: test
591 date: Thu Jan 01 00:00:00 1970 +0000
584 date: Thu Jan 01 00:00:00 1970 +0000
592 summary: create public change for phabricator testing
585 summary: create public change for phabricator testing
593
586
594 changeset: 1:a692622e6937
587 changeset: 1:a692622e6937
595 phase: public
588 phase: public
596 user: test
589 user: test
597 date: Thu Jan 01 00:00:00 1970 +0000
590 date: Thu Jan 01 00:00:00 1970 +0000
598 summary: create beta for phabricator test
591 summary: create beta for phabricator test
599
592
600 Phabimport accepts multiple DREVSPECs
593 Phabimport accepts multiple DREVSPECs
601
594
602 $ hg rollback --config ui.rollback=True
595 $ hg rollback --config ui.rollback=True
603 repository tip rolled back to revision 1 (undo phabimport)
596 repository tip rolled back to revision 1 (undo phabimport)
604 $ hg phabimport --no-stack D7917 D7918 --test-vcr "$VCR/phabimport-multi-drev.json"
597 $ hg phabimport --no-stack D7917 D7918 --test-vcr "$VCR/phabimport-multi-drev.json"
605 applying patch from D7917
598 applying patch from D7917
606 applying patch from D7918
599 applying patch from D7918
607
600
608 Phabsend requires a linear range of commits
601 Phabsend requires a linear range of commits
609
602
610 $ hg phabsend -r 0+2+3
603 $ hg phabsend -r 0+2+3
611 abort: cannot phabsend multiple head revisions: c44b38f24a45 aaef04066140
604 abort: cannot phabsend multiple head revisions: c44b38f24a45 aaef04066140
612 (the revisions must form a linear chain)
605 (the revisions must form a linear chain)
613 [255]
606 [255]
614
607
615 Validate arguments with --fold
608 Validate arguments with --fold
616
609
617 $ hg phabsend --fold -r 1
610 $ hg phabsend --fold -r 1
618 abort: cannot fold a single revision
611 abort: cannot fold a single revision
619 [255]
612 [255]
620 $ hg phabsend --fold --no-amend -r 1::
613 $ hg phabsend --fold --no-amend -r 1::
621 abort: cannot fold with --no-amend
614 abort: cannot fold with --no-amend
622 [255]
615 [255]
623 $ hg phabsend --fold -r 1::
616 $ hg phabsend --fold -r 1::
624 abort: cannot fold revisions with different DREV values
617 abort: cannot fold revisions with different DREV values
625 [255]
618 [255]
626
619
627 Setup a series of commits to be folded, and include the Test Plan field multiple
620 Setup a series of commits to be folded, and include the Test Plan field multiple
628 times to test the concatenation logic. No Test Plan field in the last one to
621 times to test the concatenation logic. No Test Plan field in the last one to
629 ensure missing fields are skipped.
622 ensure missing fields are skipped.
630
623
631 $ hg init ../folded
624 $ hg init ../folded
632 $ cd ../folded
625 $ cd ../folded
633 $ cat >> .hg/hgrc <<EOF
626 $ cat >> .hg/hgrc <<EOF
634 > [phabricator]
627 > [phabricator]
635 > url = https://phab.mercurial-scm.org/
628 > url = https://phab.mercurial-scm.org/
636 > callsign = HG
629 > callsign = HG
637 > EOF
630 > EOF
638
631
639 $ echo 'added' > file.txt
632 $ echo 'added' > file.txt
640 $ hg ci -Aqm 'added file'
633 $ hg ci -Aqm 'added file'
641
634
642 $ cat > log.txt <<EOF
635 $ cat > log.txt <<EOF
643 > one: first commit to review
636 > one: first commit to review
644 >
637 >
645 > This file was modified with 'mod1' as its contents.
638 > This file was modified with 'mod1' as its contents.
646 >
639 >
647 > Test Plan:
640 > Test Plan:
648 > LOL! What testing?!
641 > LOL! What testing?!
649 > EOF
642 > EOF
650 $ echo mod1 > file.txt
643 $ echo mod1 > file.txt
651 $ hg ci -l log.txt
644 $ hg ci -l log.txt
652
645
653 $ cat > log.txt <<EOF
646 $ cat > log.txt <<EOF
654 > two: second commit to review
647 > two: second commit to review
655 >
648 >
656 > This file was modified with 'mod2' as its contents.
649 > This file was modified with 'mod2' as its contents.
657 >
650 >
658 > Test Plan:
651 > Test Plan:
659 > Haha! yeah, right.
652 > Haha! yeah, right.
660 >
653 >
661 > EOF
654 > EOF
662 $ echo mod2 > file.txt
655 $ echo mod2 > file.txt
663 $ hg ci -l log.txt
656 $ hg ci -l log.txt
664
657
665 $ echo mod3 > file.txt
658 $ echo mod3 > file.txt
666 $ hg ci -m '3: a commit with no detailed message'
659 $ hg ci -m '3: a commit with no detailed message'
667
660
668 The folding of immutable commits works...
661 The folding of immutable commits works...
669
662
670 $ hg phase -r tip --public
663 $ hg phase -r tip --public
671 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-immutable.json"
664 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-immutable.json"
672 D8386 - created - a959a3f69d8d: one: first commit to review
665 D8386 - created - a959a3f69d8d: one: first commit to review
673 D8386 - created - 24a4438154ba: two: second commit to review
666 D8386 - created - 24a4438154ba: two: second commit to review
674 D8386 - created - d235829e802c: 3: a commit with no detailed message
667 D8386 - created - d235829e802c: 3: a commit with no detailed message
675 warning: not updating public commit 1:a959a3f69d8d
668 warning: not updating public commit 1:a959a3f69d8d
676 warning: not updating public commit 2:24a4438154ba
669 warning: not updating public commit 2:24a4438154ba
677 warning: not updating public commit 3:d235829e802c
670 warning: not updating public commit 3:d235829e802c
678 no newnodes to update
671 no newnodes to update
679
672
680 $ hg phase -r 0 --draft --force
673 $ hg phase -r 0 --draft --force
681
674
682 ... as does the initial mutable fold...
675 ... as does the initial mutable fold...
683
676
684 $ echo y | hg phabsend --fold --confirm -r 1:: \
677 $ echo y | hg phabsend --fold --confirm -r 1:: \
685 > --test-vcr "$VCR/phabsend-fold-initial.json"
678 > --test-vcr "$VCR/phabsend-fold-initial.json"
686 NEW - a959a3f69d8d: one: first commit to review
679 NEW - a959a3f69d8d: one: first commit to review
687 NEW - 24a4438154ba: two: second commit to review
680 NEW - 24a4438154ba: two: second commit to review
688 NEW - d235829e802c: 3: a commit with no detailed message
681 NEW - d235829e802c: 3: a commit with no detailed message
689 Send the above changes to https://phab.mercurial-scm.org/ (Y/n)? y
682 Send the above changes to https://phab.mercurial-scm.org/ (Y/n)? y
690 D8387 - created - a959a3f69d8d: one: first commit to review
683 D8387 - created - a959a3f69d8d: one: first commit to review
691 D8387 - created - 24a4438154ba: two: second commit to review
684 D8387 - created - 24a4438154ba: two: second commit to review
692 D8387 - created - d235829e802c: 3: a commit with no detailed message
685 D8387 - created - d235829e802c: 3: a commit with no detailed message
693 updating local commit list for D8387
686 updating local commit list for D8387
694 new commits: ['602c4e738243', '832553266fe8', '921f8265efbd']
687 new commits: ['602c4e738243', '832553266fe8', '921f8265efbd']
695 saved backup bundle to $TESTTMP/folded/.hg/strip-backup/a959a3f69d8d-a4a24136-phabsend.hg
688 saved backup bundle to $TESTTMP/folded/.hg/strip-backup/a959a3f69d8d-a4a24136-phabsend.hg
696
689
697 ... and doesn't mangle the local commits.
690 ... and doesn't mangle the local commits.
698
691
699 $ hg log -T '{rev}:{node|short}\n{indent(desc, " ")}\n'
692 $ hg log -T '{rev}:{node|short}\n{indent(desc, " ")}\n'
700 3:921f8265efbd
693 3:921f8265efbd
701 3: a commit with no detailed message
694 3: a commit with no detailed message
702
695
703 Differential Revision: https://phab.mercurial-scm.org/D8387
696 Differential Revision: https://phab.mercurial-scm.org/D8387
704 2:832553266fe8
697 2:832553266fe8
705 two: second commit to review
698 two: second commit to review
706
699
707 This file was modified with 'mod2' as its contents.
700 This file was modified with 'mod2' as its contents.
708
701
709 Test Plan:
702 Test Plan:
710 Haha! yeah, right.
703 Haha! yeah, right.
711
704
712 Differential Revision: https://phab.mercurial-scm.org/D8387
705 Differential Revision: https://phab.mercurial-scm.org/D8387
713 1:602c4e738243
706 1:602c4e738243
714 one: first commit to review
707 one: first commit to review
715
708
716 This file was modified with 'mod1' as its contents.
709 This file was modified with 'mod1' as its contents.
717
710
718 Test Plan:
711 Test Plan:
719 LOL! What testing?!
712 LOL! What testing?!
720
713
721 Differential Revision: https://phab.mercurial-scm.org/D8387
714 Differential Revision: https://phab.mercurial-scm.org/D8387
722 0:98d480e0d494
715 0:98d480e0d494
723 added file
716 added file
724
717
725 Setup some obsmarkers by adding a file to the middle commit. This stress tests
718 Setup some obsmarkers by adding a file to the middle commit. This stress tests
726 getoldnodedrevmap() in later phabsends.
719 getoldnodedrevmap() in later phabsends.
727
720
728 $ hg up '.^'
721 $ hg up '.^'
729 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
722 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
730 $ echo 'modified' > file2.txt
723 $ echo 'modified' > file2.txt
731 $ hg add file2.txt
724 $ hg add file2.txt
732 $ hg amend --config experimental.evolution=all --config extensions.amend=
725 $ hg amend --config experimental.evolution=all --config extensions.amend=
733 1 new orphan changesets
726 1 new orphan changesets
734 $ hg up 3
727 $ hg up 3
735 obsolete feature not enabled but 1 markers found!
728 obsolete feature not enabled but 1 markers found!
736 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
729 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
737 $ hg rebase --config experimental.evolution=all --config extensions.rebase=
730 $ hg rebase --config experimental.evolution=all --config extensions.rebase=
738 note: not rebasing 2:832553266fe8 "two: second commit to review", already in destination as 4:0124e5474c88 "two: second commit to review" (tip)
731 note: not rebasing 2:832553266fe8 "two: second commit to review", already in destination as 4:0124e5474c88 "two: second commit to review" (tip)
739 rebasing 3:921f8265efbd "3: a commit with no detailed message"
732 rebasing 3:921f8265efbd "3: a commit with no detailed message"
740
733
741 When commits have changed locally, the local commit list on Phabricator is
734 When commits have changed locally, the local commit list on Phabricator is
742 updated.
735 updated.
743
736
744 $ echo y | hg phabsend --fold --confirm -r 1:: \
737 $ echo y | hg phabsend --fold --confirm -r 1:: \
745 > --test-vcr "$VCR/phabsend-fold-updated.json"
738 > --test-vcr "$VCR/phabsend-fold-updated.json"
746 obsolete feature not enabled but 2 markers found!
739 obsolete feature not enabled but 2 markers found!
747 602c4e738243 mapped to old nodes ['602c4e738243']
740 602c4e738243 mapped to old nodes ['602c4e738243']
748 0124e5474c88 mapped to old nodes ['832553266fe8']
741 0124e5474c88 mapped to old nodes ['832553266fe8']
749 e4edb1fe3565 mapped to old nodes ['921f8265efbd']
742 e4edb1fe3565 mapped to old nodes ['921f8265efbd']
750 D8387 - 602c4e738243: one: first commit to review
743 D8387 - 602c4e738243: one: first commit to review
751 D8387 - 0124e5474c88: two: second commit to review
744 D8387 - 0124e5474c88: two: second commit to review
752 D8387 - e4edb1fe3565: 3: a commit with no detailed message
745 D8387 - e4edb1fe3565: 3: a commit with no detailed message
753 Send the above changes to https://phab.mercurial-scm.org/ (Y/n)? y
746 Send the above changes to https://phab.mercurial-scm.org/ (Y/n)? y
754 D8387 - updated - 602c4e738243: one: first commit to review
747 D8387 - updated - 602c4e738243: one: first commit to review
755 D8387 - updated - 0124e5474c88: two: second commit to review
748 D8387 - updated - 0124e5474c88: two: second commit to review
756 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
749 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
757 obsolete feature not enabled but 2 markers found! (?)
750 obsolete feature not enabled but 2 markers found! (?)
758 updating local commit list for D8387
751 updating local commit list for D8387
759 new commits: ['602c4e738243', '0124e5474c88', 'e4edb1fe3565']
752 new commits: ['602c4e738243', '0124e5474c88', 'e4edb1fe3565']
760 $ hg log -Tcompact
753 $ hg log -Tcompact
761 obsolete feature not enabled but 2 markers found!
754 obsolete feature not enabled but 2 markers found!
762 5[tip] e4edb1fe3565 1970-01-01 00:00 +0000 test
755 5[tip] e4edb1fe3565 1970-01-01 00:00 +0000 test
763 3: a commit with no detailed message
756 3: a commit with no detailed message
764
757
765 4:1 0124e5474c88 1970-01-01 00:00 +0000 test
758 4:1 0124e5474c88 1970-01-01 00:00 +0000 test
766 two: second commit to review
759 two: second commit to review
767
760
768 1 602c4e738243 1970-01-01 00:00 +0000 test
761 1 602c4e738243 1970-01-01 00:00 +0000 test
769 one: first commit to review
762 one: first commit to review
770
763
771 0 98d480e0d494 1970-01-01 00:00 +0000 test
764 0 98d480e0d494 1970-01-01 00:00 +0000 test
772 added file
765 added file
773
766
774 When nothing has changed locally since the last phabsend, the commit list isn't
767 When nothing has changed locally since the last phabsend, the commit list isn't
775 updated, and nothing is changed locally afterward.
768 updated, and nothing is changed locally afterward.
776
769
777 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-no-changes.json"
770 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-no-changes.json"
778 obsolete feature not enabled but 2 markers found!
771 obsolete feature not enabled but 2 markers found!
779 602c4e738243 mapped to old nodes ['602c4e738243']
772 602c4e738243 mapped to old nodes ['602c4e738243']
780 0124e5474c88 mapped to old nodes ['0124e5474c88']
773 0124e5474c88 mapped to old nodes ['0124e5474c88']
781 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
774 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
782 D8387 - updated - 602c4e738243: one: first commit to review
775 D8387 - updated - 602c4e738243: one: first commit to review
783 D8387 - updated - 0124e5474c88: two: second commit to review
776 D8387 - updated - 0124e5474c88: two: second commit to review
784 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
777 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
785 obsolete feature not enabled but 2 markers found! (?)
778 obsolete feature not enabled but 2 markers found! (?)
786 local commit list for D8387 is already up-to-date
779 local commit list for D8387 is already up-to-date
787 $ hg log -Tcompact
780 $ hg log -Tcompact
788 obsolete feature not enabled but 2 markers found!
781 obsolete feature not enabled but 2 markers found!
789 5[tip] e4edb1fe3565 1970-01-01 00:00 +0000 test
782 5[tip] e4edb1fe3565 1970-01-01 00:00 +0000 test
790 3: a commit with no detailed message
783 3: a commit with no detailed message
791
784
792 4:1 0124e5474c88 1970-01-01 00:00 +0000 test
785 4:1 0124e5474c88 1970-01-01 00:00 +0000 test
793 two: second commit to review
786 two: second commit to review
794
787
795 1 602c4e738243 1970-01-01 00:00 +0000 test
788 1 602c4e738243 1970-01-01 00:00 +0000 test
796 one: first commit to review
789 one: first commit to review
797
790
798 0 98d480e0d494 1970-01-01 00:00 +0000 test
791 0 98d480e0d494 1970-01-01 00:00 +0000 test
799 added file
792 added file
800
793
801 Fold will accept new revisions at the end...
794 Fold will accept new revisions at the end...
802
795
803 $ echo 'another mod' > file2.txt
796 $ echo 'another mod' > file2.txt
804 $ hg ci -m 'four: extend the fold range'
797 $ hg ci -m 'four: extend the fold range'
805 obsolete feature not enabled but 2 markers found!
798 obsolete feature not enabled but 2 markers found!
806 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-extend-end.json" \
799 $ hg phabsend --fold -r 1:: --test-vcr "$VCR/phabsend-fold-extend-end.json" \
807 > --config experimental.evolution=all
800 > --config experimental.evolution=all
808 602c4e738243 mapped to old nodes ['602c4e738243']
801 602c4e738243 mapped to old nodes ['602c4e738243']
809 0124e5474c88 mapped to old nodes ['0124e5474c88']
802 0124e5474c88 mapped to old nodes ['0124e5474c88']
810 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
803 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
811 D8387 - updated - 602c4e738243: one: first commit to review
804 D8387 - updated - 602c4e738243: one: first commit to review
812 D8387 - updated - 0124e5474c88: two: second commit to review
805 D8387 - updated - 0124e5474c88: two: second commit to review
813 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
806 D8387 - updated - e4edb1fe3565: 3: a commit with no detailed message
814 D8387 - created - 94aaae213b23: four: extend the fold range
807 D8387 - created - 94aaae213b23: four: extend the fold range
815 updating local commit list for D8387
808 updating local commit list for D8387
816 new commits: ['602c4e738243', '0124e5474c88', 'e4edb1fe3565', '51a04fea8707']
809 new commits: ['602c4e738243', '0124e5474c88', 'e4edb1fe3565', '51a04fea8707']
817 $ hg log -r . -T '{desc}\n'
810 $ hg log -r . -T '{desc}\n'
818 four: extend the fold range
811 four: extend the fold range
819
812
820 Differential Revision: https://phab.mercurial-scm.org/D8387
813 Differential Revision: https://phab.mercurial-scm.org/D8387
821 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n' -r 1::
814 $ hg log -T'{rev} {if(phabreview, "{phabreview.url} {phabreview.id}")}\n' -r 1::
822 obsolete feature not enabled but 3 markers found!
815 obsolete feature not enabled but 3 markers found!
823 1 https://phab.mercurial-scm.org/D8387 D8387
816 1 https://phab.mercurial-scm.org/D8387 D8387
824 4 https://phab.mercurial-scm.org/D8387 D8387
817 4 https://phab.mercurial-scm.org/D8387 D8387
825 5 https://phab.mercurial-scm.org/D8387 D8387
818 5 https://phab.mercurial-scm.org/D8387 D8387
826 7 https://phab.mercurial-scm.org/D8387 D8387
819 7 https://phab.mercurial-scm.org/D8387 D8387
827
820
828 ... and also accepts new revisions at the beginning of the range
821 ... and also accepts new revisions at the beginning of the range
829
822
830 It's a bit unfortunate that not having a Differential URL on the first commit
823 It's a bit unfortunate that not having a Differential URL on the first commit
831 causes a new Differential Revision to be created, though it isn't *entirely*
824 causes a new Differential Revision to be created, though it isn't *entirely*
832 unreasonable. At least this updates the subsequent commits.
825 unreasonable. At least this updates the subsequent commits.
833
826
834 TODO: See if it can reuse the existing Differential.
827 TODO: See if it can reuse the existing Differential.
835
828
836 $ hg phabsend --fold -r 0:: --test-vcr "$VCR/phabsend-fold-extend-front.json" \
829 $ hg phabsend --fold -r 0:: --test-vcr "$VCR/phabsend-fold-extend-front.json" \
837 > --config experimental.evolution=all
830 > --config experimental.evolution=all
838 602c4e738243 mapped to old nodes ['602c4e738243']
831 602c4e738243 mapped to old nodes ['602c4e738243']
839 0124e5474c88 mapped to old nodes ['0124e5474c88']
832 0124e5474c88 mapped to old nodes ['0124e5474c88']
840 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
833 e4edb1fe3565 mapped to old nodes ['e4edb1fe3565']
841 51a04fea8707 mapped to old nodes ['51a04fea8707']
834 51a04fea8707 mapped to old nodes ['51a04fea8707']
842 D8388 - created - 98d480e0d494: added file
835 D8388 - created - 98d480e0d494: added file
843 D8388 - updated - 602c4e738243: one: first commit to review
836 D8388 - updated - 602c4e738243: one: first commit to review
844 D8388 - updated - 0124e5474c88: two: second commit to review
837 D8388 - updated - 0124e5474c88: two: second commit to review
845 D8388 - updated - e4edb1fe3565: 3: a commit with no detailed message
838 D8388 - updated - e4edb1fe3565: 3: a commit with no detailed message
846 D8388 - updated - 51a04fea8707: four: extend the fold range
839 D8388 - updated - 51a04fea8707: four: extend the fold range
847 updating local commit list for D8388
840 updating local commit list for D8388
848 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', 'ac7db67f0991']
841 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', 'ac7db67f0991']
849
842
850 $ hg log -T '{rev}:{node|short}\n{indent(desc, " ")}\n'
843 $ hg log -T '{rev}:{node|short}\n{indent(desc, " ")}\n'
851 obsolete feature not enabled but 8 markers found!
844 obsolete feature not enabled but 8 markers found!
852 12:ac7db67f0991
845 12:ac7db67f0991
853 four: extend the fold range
846 four: extend the fold range
854
847
855 Differential Revision: https://phab.mercurial-scm.org/D8388
848 Differential Revision: https://phab.mercurial-scm.org/D8388
856 11:30682b960804
849 11:30682b960804
857 3: a commit with no detailed message
850 3: a commit with no detailed message
858
851
859 Differential Revision: https://phab.mercurial-scm.org/D8388
852 Differential Revision: https://phab.mercurial-scm.org/D8388
860 10:3ee132d41dbc
853 10:3ee132d41dbc
861 two: second commit to review
854 two: second commit to review
862
855
863 This file was modified with 'mod2' as its contents.
856 This file was modified with 'mod2' as its contents.
864
857
865 Test Plan:
858 Test Plan:
866 Haha! yeah, right.
859 Haha! yeah, right.
867
860
868 Differential Revision: https://phab.mercurial-scm.org/D8388
861 Differential Revision: https://phab.mercurial-scm.org/D8388
869 9:6320b7d714cf
862 9:6320b7d714cf
870 one: first commit to review
863 one: first commit to review
871
864
872 This file was modified with 'mod1' as its contents.
865 This file was modified with 'mod1' as its contents.
873
866
874 Test Plan:
867 Test Plan:
875 LOL! What testing?!
868 LOL! What testing?!
876
869
877 Differential Revision: https://phab.mercurial-scm.org/D8388
870 Differential Revision: https://phab.mercurial-scm.org/D8388
878 8:15e9b14b4b4c
871 8:15e9b14b4b4c
879 added file
872 added file
880
873
881 Differential Revision: https://phab.mercurial-scm.org/D8388
874 Differential Revision: https://phab.mercurial-scm.org/D8388
882
875
883 Test phabsend --fold with an `hg split` at the end of the range
876 Test phabsend --fold with an `hg split` at the end of the range
884
877
885 $ echo foo > file3.txt
878 $ echo foo > file3.txt
886 $ hg add file3.txt
879 $ hg add file3.txt
887
880
888 $ hg log -r . -T '{desc}' > log.txt
881 $ hg log -r . -T '{desc}' > log.txt
889 $ echo 'amended mod' > file2.txt
882 $ echo 'amended mod' > file2.txt
890 $ hg ci --amend -l log.txt --config experimental.evolution=all
883 $ hg ci --amend -l log.txt --config experimental.evolution=all
891
884
892 $ cat <<EOF | hg --config extensions.split= --config ui.interactive=True \
885 $ cat <<EOF | hg --config extensions.split= --config ui.interactive=True \
893 > --config experimental.evolution=all split -r .
886 > --config experimental.evolution=all split -r .
894 > n
887 > n
895 > y
888 > y
896 > y
889 > y
897 > y
890 > y
898 > y
891 > y
899 > EOF
892 > EOF
900 diff --git a/file2.txt b/file2.txt
893 diff --git a/file2.txt b/file2.txt
901 1 hunks, 1 lines changed
894 1 hunks, 1 lines changed
902 examine changes to 'file2.txt'?
895 examine changes to 'file2.txt'?
903 (enter ? for help) [Ynesfdaq?] n
896 (enter ? for help) [Ynesfdaq?] n
904
897
905 diff --git a/file3.txt b/file3.txt
898 diff --git a/file3.txt b/file3.txt
906 new file mode 100644
899 new file mode 100644
907 examine changes to 'file3.txt'?
900 examine changes to 'file3.txt'?
908 (enter ? for help) [Ynesfdaq?] y
901 (enter ? for help) [Ynesfdaq?] y
909
902
910 @@ -0,0 +1,1 @@
903 @@ -0,0 +1,1 @@
911 +foo
904 +foo
912 record change 2/2 to 'file3.txt'?
905 record change 2/2 to 'file3.txt'?
913 (enter ? for help) [Ynesfdaq?] y
906 (enter ? for help) [Ynesfdaq?] y
914
907
915 created new head
908 created new head
916 diff --git a/file2.txt b/file2.txt
909 diff --git a/file2.txt b/file2.txt
917 1 hunks, 1 lines changed
910 1 hunks, 1 lines changed
918 examine changes to 'file2.txt'?
911 examine changes to 'file2.txt'?
919 (enter ? for help) [Ynesfdaq?] y
912 (enter ? for help) [Ynesfdaq?] y
920
913
921 @@ -1,1 +1,1 @@
914 @@ -1,1 +1,1 @@
922 -modified
915 -modified
923 +amended mod
916 +amended mod
924 record this change to 'file2.txt'?
917 record this change to 'file2.txt'?
925 (enter ? for help) [Ynesfdaq?] y
918 (enter ? for help) [Ynesfdaq?] y
926
919
927 $ hg phabsend --fold -r 8:: --test-vcr "$VCR/phabsend-fold-split-end.json" \
920 $ hg phabsend --fold -r 8:: --test-vcr "$VCR/phabsend-fold-split-end.json" \
928 > --config experimental.evolution=all
921 > --config experimental.evolution=all
929 15e9b14b4b4c mapped to old nodes ['15e9b14b4b4c']
922 15e9b14b4b4c mapped to old nodes ['15e9b14b4b4c']
930 6320b7d714cf mapped to old nodes ['6320b7d714cf']
923 6320b7d714cf mapped to old nodes ['6320b7d714cf']
931 3ee132d41dbc mapped to old nodes ['3ee132d41dbc']
924 3ee132d41dbc mapped to old nodes ['3ee132d41dbc']
932 30682b960804 mapped to old nodes ['30682b960804']
925 30682b960804 mapped to old nodes ['30682b960804']
933 6bc15dc99efd mapped to old nodes ['ac7db67f0991']
926 6bc15dc99efd mapped to old nodes ['ac7db67f0991']
934 b50946d5e490 mapped to old nodes ['ac7db67f0991']
927 b50946d5e490 mapped to old nodes ['ac7db67f0991']
935 D8388 - updated - 15e9b14b4b4c: added file
928 D8388 - updated - 15e9b14b4b4c: added file
936 D8388 - updated - 6320b7d714cf: one: first commit to review
929 D8388 - updated - 6320b7d714cf: one: first commit to review
937 D8388 - updated - 3ee132d41dbc: two: second commit to review
930 D8388 - updated - 3ee132d41dbc: two: second commit to review
938 D8388 - updated - 30682b960804: 3: a commit with no detailed message
931 D8388 - updated - 30682b960804: 3: a commit with no detailed message
939 D8388 - updated - 6bc15dc99efd: four: extend the fold range
932 D8388 - updated - 6bc15dc99efd: four: extend the fold range
940 D8388 - updated - b50946d5e490: four: extend the fold range
933 D8388 - updated - b50946d5e490: four: extend the fold range
941 updating local commit list for D8388
934 updating local commit list for D8388
942 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', '6bc15dc99efd', 'b50946d5e490']
935 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', '6bc15dc99efd', 'b50946d5e490']
943
936
944 Test phabsend --fold with an `hg fold` at the end of the range
937 Test phabsend --fold with an `hg fold` at the end of the range
945
938
946 $ hg --config experimental.evolution=all --config extensions.rebase= \
939 $ hg --config experimental.evolution=all --config extensions.rebase= \
947 > rebase -r '.^' -r . -d '.^^' --collapse -l log.txt
940 > rebase -r '.^' -r . -d '.^^' --collapse -l log.txt
948 rebasing 14:6bc15dc99efd "four: extend the fold range"
941 rebasing 14:6bc15dc99efd "four: extend the fold range"
949 rebasing 15:b50946d5e490 "four: extend the fold range" (tip)
942 rebasing 15:b50946d5e490 "four: extend the fold range" (tip)
950
943
951 $ hg phabsend --fold -r 8:: --test-vcr "$VCR/phabsend-fold-fold-end.json" \
944 $ hg phabsend --fold -r 8:: --test-vcr "$VCR/phabsend-fold-fold-end.json" \
952 > --config experimental.evolution=all
945 > --config experimental.evolution=all
953 15e9b14b4b4c mapped to old nodes ['15e9b14b4b4c']
946 15e9b14b4b4c mapped to old nodes ['15e9b14b4b4c']
954 6320b7d714cf mapped to old nodes ['6320b7d714cf']
947 6320b7d714cf mapped to old nodes ['6320b7d714cf']
955 3ee132d41dbc mapped to old nodes ['3ee132d41dbc']
948 3ee132d41dbc mapped to old nodes ['3ee132d41dbc']
956 30682b960804 mapped to old nodes ['30682b960804']
949 30682b960804 mapped to old nodes ['30682b960804']
957 e919cdf3d4fe mapped to old nodes ['6bc15dc99efd', 'b50946d5e490']
950 e919cdf3d4fe mapped to old nodes ['6bc15dc99efd', 'b50946d5e490']
958 D8388 - updated - 15e9b14b4b4c: added file
951 D8388 - updated - 15e9b14b4b4c: added file
959 D8388 - updated - 6320b7d714cf: one: first commit to review
952 D8388 - updated - 6320b7d714cf: one: first commit to review
960 D8388 - updated - 3ee132d41dbc: two: second commit to review
953 D8388 - updated - 3ee132d41dbc: two: second commit to review
961 D8388 - updated - 30682b960804: 3: a commit with no detailed message
954 D8388 - updated - 30682b960804: 3: a commit with no detailed message
962 D8388 - updated - e919cdf3d4fe: four: extend the fold range
955 D8388 - updated - e919cdf3d4fe: four: extend the fold range
963 updating local commit list for D8388
956 updating local commit list for D8388
964 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', 'e919cdf3d4fe']
957 new commits: ['15e9b14b4b4c', '6320b7d714cf', '3ee132d41dbc', '30682b960804', 'e919cdf3d4fe']
965
958
966 $ hg log -r tip -v
959 $ hg log -r tip -v
967 obsolete feature not enabled but 12 markers found!
960 obsolete feature not enabled but 12 markers found!
968 changeset: 16:e919cdf3d4fe
961 changeset: 16:e919cdf3d4fe
969 tag: tip
962 tag: tip
970 parent: 11:30682b960804
963 parent: 11:30682b960804
971 user: test
964 user: test
972 date: Thu Jan 01 00:00:00 1970 +0000
965 date: Thu Jan 01 00:00:00 1970 +0000
973 files: file2.txt file3.txt
966 files: file2.txt file3.txt
974 description:
967 description:
975 four: extend the fold range
968 four: extend the fold range
976
969
977 Differential Revision: https://phab.mercurial-scm.org/D8388
970 Differential Revision: https://phab.mercurial-scm.org/D8388
978
971
979
972
980
973
981 $ cd ..
974 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now