##// END OF EJS Templates
formatting: blacken the codebase...
Augie Fackler -
r43346:2372284d default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -36,8 +36,13 b' def bootstrap():'
36 pip = venv_bin / 'pip'
36 pip = venv_bin / 'pip'
37 python = venv_bin / 'python'
37 python = venv_bin / 'python'
38
38
39 args = [str(pip), 'install', '-r', str(REQUIREMENTS_TXT),
39 args = [
40 '--disable-pip-version-check']
40 str(pip),
41 'install',
42 '-r',
43 str(REQUIREMENTS_TXT),
44 '--disable-pip-version-check',
45 ]
41
46
42 if not venv_created:
47 if not venv_created:
43 args.append('-q')
48 args.append('-q')
@@ -45,8 +50,7 b' def bootstrap():'
45 subprocess.run(args, check=True)
50 subprocess.run(args, check=True)
46
51
47 os.environ['HGAUTOMATION_BOOTSTRAPPED'] = '1'
52 os.environ['HGAUTOMATION_BOOTSTRAPPED'] = '1'
48 os.environ['PATH'] = '%s%s%s' % (
53 os.environ['PATH'] = '%s%s%s' % (venv_bin, os.pathsep, os.environ['PATH'])
49 venv_bin, os.pathsep, os.environ['PATH'])
50
54
51 subprocess.run([str(python), __file__] + sys.argv[1:], check=True)
55 subprocess.run([str(python), __file__] + sys.argv[1:], check=True)
52
56
@@ -10,9 +10,7 b''
10 import pathlib
10 import pathlib
11 import secrets
11 import secrets
12
12
13 from .aws import (
13 from .aws import AWSConnection
14 AWSConnection,
15 )
16
14
17
15
18 class HGAutomation:
16 class HGAutomation:
@@ -53,7 +51,7 b' class HGAutomation:'
53
51
54 return password
52 return password
55
53
56 def aws_connection(self, region: str, ensure_ec2_state: bool=True):
54 def aws_connection(self, region: str, ensure_ec2_state: bool = True):
57 """Obtain an AWSConnection instance bound to a specific region."""
55 """Obtain an AWSConnection instance bound to a specific region."""
58
56
59 return AWSConnection(self, region, ensure_ec2_state=ensure_ec2_state)
57 return AWSConnection(self, region, ensure_ec2_state=ensure_ec2_state)
@@ -19,9 +19,7 b' import time'
19 import boto3
19 import boto3
20 import botocore.exceptions
20 import botocore.exceptions
21
21
22 from .linux import (
22 from .linux import BOOTSTRAP_DEBIAN
23 BOOTSTRAP_DEBIAN,
24 )
25 from .ssh import (
23 from .ssh import (
26 exec_command as ssh_exec_command,
24 exec_command as ssh_exec_command,
27 wait_for_ssh,
25 wait_for_ssh,
@@ -32,10 +30,13 b' from .winrm import ('
32 )
30 )
33
31
34
32
35 SOURCE_ROOT = pathlib.Path(os.path.abspath(__file__)).parent.parent.parent.parent
33 SOURCE_ROOT = pathlib.Path(
34 os.path.abspath(__file__)
35 ).parent.parent.parent.parent
36
36
37 INSTALL_WINDOWS_DEPENDENCIES = (SOURCE_ROOT / 'contrib' /
37 INSTALL_WINDOWS_DEPENDENCIES = (
38 'install-windows-dependencies.ps1')
38 SOURCE_ROOT / 'contrib' / 'install-windows-dependencies.ps1'
39 )
39
40
40
41
41 INSTANCE_TYPES_WITH_STORAGE = {
42 INSTANCE_TYPES_WITH_STORAGE = {
@@ -107,7 +108,6 b' SECURITY_GROUPS = {'
107 'Description': 'RDP from entire Internet',
108 'Description': 'RDP from entire Internet',
108 },
109 },
109 ],
110 ],
110
111 },
111 },
112 {
112 {
113 'FromPort': 5985,
113 'FromPort': 5985,
@@ -119,7 +119,7 b' SECURITY_GROUPS = {'
119 'Description': 'PowerShell Remoting (Windows Remote Management)',
119 'Description': 'PowerShell Remoting (Windows Remote Management)',
120 },
120 },
121 ],
121 ],
122 }
122 },
123 ],
123 ],
124 },
124 },
125 }
125 }
@@ -152,11 +152,7 b" ASSUME_ROLE_POLICY_DOCUMENT = '''"
152
152
153
153
154 IAM_INSTANCE_PROFILES = {
154 IAM_INSTANCE_PROFILES = {
155 'ephemeral-ec2-1': {
155 'ephemeral-ec2-1': {'roles': ['ephemeral-ec2-role-1',],}
156 'roles': [
157 'ephemeral-ec2-role-1',
158 ],
159 }
160 }
156 }
161
157
162
158
@@ -226,7 +222,7 b' Install-WindowsFeature -Name Net-Framewo'
226 class AWSConnection:
222 class AWSConnection:
227 """Manages the state of a connection with AWS."""
223 """Manages the state of a connection with AWS."""
228
224
229 def __init__(self, automation, region: str, ensure_ec2_state: bool=True):
225 def __init__(self, automation, region: str, ensure_ec2_state: bool = True):
230 self.automation = automation
226 self.automation = automation
231 self.local_state_path = automation.state_path
227 self.local_state_path = automation.state_path
232
228
@@ -257,10 +253,19 b' def rsa_key_fingerprint(p: pathlib.Path)'
257
253
258 # TODO use rsa package.
254 # TODO use rsa package.
259 res = subprocess.run(
255 res = subprocess.run(
260 ['openssl', 'pkcs8', '-in', str(p), '-nocrypt', '-topk8',
256 [
261 '-outform', 'DER'],
257 'openssl',
258 'pkcs8',
259 '-in',
260 str(p),
261 '-nocrypt',
262 '-topk8',
263 '-outform',
264 'DER',
265 ],
262 capture_output=True,
266 capture_output=True,
263 check=True)
267 check=True,
268 )
264
269
265 sha1 = hashlib.sha1(res.stdout).hexdigest()
270 sha1 = hashlib.sha1(res.stdout).hexdigest()
266 return ':'.join(a + b for a, b in zip(sha1[::2], sha1[1::2]))
271 return ':'.join(a + b for a, b in zip(sha1[::2], sha1[1::2]))
@@ -271,7 +276,7 b' def ensure_key_pairs(state_path: pathlib'
271
276
272 for kpi in ec2resource.key_pairs.all():
277 for kpi in ec2resource.key_pairs.all():
273 if kpi.name.startswith(prefix):
278 if kpi.name.startswith(prefix):
274 remote_existing[kpi.name[len(prefix):]] = kpi.key_fingerprint
279 remote_existing[kpi.name[len(prefix) :]] = kpi.key_fingerprint
275
280
276 # Validate that we have these keys locally.
281 # Validate that we have these keys locally.
277 key_path = state_path / 'keys'
282 key_path = state_path / 'keys'
@@ -297,7 +302,7 b' def ensure_key_pairs(state_path: pathlib'
297 if not f.startswith('keypair-') or not f.endswith('.pub'):
302 if not f.startswith('keypair-') or not f.endswith('.pub'):
298 continue
303 continue
299
304
300 name = f[len('keypair-'):-len('.pub')]
305 name = f[len('keypair-') : -len('.pub')]
301
306
302 pub_full = key_path / f
307 pub_full = key_path / f
303 priv_full = key_path / ('keypair-%s' % name)
308 priv_full = key_path / ('keypair-%s' % name)
@@ -306,8 +311,9 b' def ensure_key_pairs(state_path: pathlib'
306 data = fh.read()
311 data = fh.read()
307
312
308 if not data.startswith('ssh-rsa '):
313 if not data.startswith('ssh-rsa '):
309 print('unexpected format for key pair file: %s; removing' %
314 print(
310 pub_full)
315 'unexpected format for key pair file: %s; removing' % pub_full
316 )
311 pub_full.unlink()
317 pub_full.unlink()
312 priv_full.unlink()
318 priv_full.unlink()
313 continue
319 continue
@@ -327,8 +333,10 b' def ensure_key_pairs(state_path: pathlib'
327 del local_existing[name]
333 del local_existing[name]
328
334
329 elif remote_existing[name] != local_existing[name]:
335 elif remote_existing[name] != local_existing[name]:
330 print('key fingerprint mismatch for %s; '
336 print(
331 'removing from local and remote' % name)
337 'key fingerprint mismatch for %s; '
338 'removing from local and remote' % name
339 )
332 remove_local(name)
340 remove_local(name)
333 remove_remote('%s%s' % (prefix, name))
341 remove_remote('%s%s' % (prefix, name))
334 del local_existing[name]
342 del local_existing[name]
@@ -356,15 +364,18 b' def ensure_key_pairs(state_path: pathlib'
356 subprocess.run(
364 subprocess.run(
357 ['ssh-keygen', '-y', '-f', str(priv_full)],
365 ['ssh-keygen', '-y', '-f', str(priv_full)],
358 stdout=fh,
366 stdout=fh,
359 check=True)
367 check=True,
368 )
360
369
361 pub_full.chmod(0o0600)
370 pub_full.chmod(0o0600)
362
371
363
372
364 def delete_instance_profile(profile):
373 def delete_instance_profile(profile):
365 for role in profile.roles:
374 for role in profile.roles:
366 print('removing role %s from instance profile %s' % (role.name,
375 print(
367 profile.name))
376 'removing role %s from instance profile %s'
377 % (role.name, profile.name)
378 )
368 profile.remove_role(RoleName=role.name)
379 profile.remove_role(RoleName=role.name)
369
380
370 print('deleting instance profile %s' % profile.name)
381 print('deleting instance profile %s' % profile.name)
@@ -378,7 +389,7 b' def ensure_iam_state(iamclient, iamresou'
378
389
379 for profile in iamresource.instance_profiles.all():
390 for profile in iamresource.instance_profiles.all():
380 if profile.name.startswith(prefix):
391 if profile.name.startswith(prefix):
381 remote_profiles[profile.name[len(prefix):]] = profile
392 remote_profiles[profile.name[len(prefix) :]] = profile
382
393
383 for name in sorted(set(remote_profiles) - set(IAM_INSTANCE_PROFILES)):
394 for name in sorted(set(remote_profiles) - set(IAM_INSTANCE_PROFILES)):
384 delete_instance_profile(remote_profiles[name])
395 delete_instance_profile(remote_profiles[name])
@@ -388,7 +399,7 b' def ensure_iam_state(iamclient, iamresou'
388
399
389 for role in iamresource.roles.all():
400 for role in iamresource.roles.all():
390 if role.name.startswith(prefix):
401 if role.name.startswith(prefix):
391 remote_roles[role.name[len(prefix):]] = role
402 remote_roles[role.name[len(prefix) :]] = role
392
403
393 for name in sorted(set(remote_roles) - set(IAM_ROLES)):
404 for name in sorted(set(remote_roles) - set(IAM_ROLES)):
394 role = remote_roles[name]
405 role = remote_roles[name]
@@ -404,7 +415,8 b' def ensure_iam_state(iamclient, iamresou'
404 print('creating IAM instance profile %s' % actual)
415 print('creating IAM instance profile %s' % actual)
405
416
406 profile = iamresource.create_instance_profile(
417 profile = iamresource.create_instance_profile(
407 InstanceProfileName=actual)
418 InstanceProfileName=actual
419 )
408 remote_profiles[name] = profile
420 remote_profiles[name] = profile
409
421
410 waiter = iamclient.get_waiter('instance_profile_exists')
422 waiter = iamclient.get_waiter('instance_profile_exists')
@@ -453,23 +465,12 b' def find_image(ec2resource, owner_id, na'
453
465
454 images = ec2resource.images.filter(
466 images = ec2resource.images.filter(
455 Filters=[
467 Filters=[
456 {
468 {'Name': 'owner-id', 'Values': [owner_id],},
457 'Name': 'owner-id',
469 {'Name': 'state', 'Values': ['available'],},
458 'Values': [owner_id],
470 {'Name': 'image-type', 'Values': ['machine'],},
459 },
471 {'Name': 'name', 'Values': [name],},
460 {
472 ]
461 'Name': 'state',
473 )
462 'Values': ['available'],
463 },
464 {
465 'Name': 'image-type',
466 'Values': ['machine'],
467 },
468 {
469 'Name': 'name',
470 'Values': [name],
471 },
472 ])
473
474
474 for image in images:
475 for image in images:
475 return image
476 return image
@@ -487,7 +488,7 b' def ensure_security_groups(ec2resource, '
487
488
488 for group in ec2resource.security_groups.all():
489 for group in ec2resource.security_groups.all():
489 if group.group_name.startswith(prefix):
490 if group.group_name.startswith(prefix):
490 existing[group.group_name[len(prefix):]] = group
491 existing[group.group_name[len(prefix) :]] = group
491
492
492 purge = set(existing) - set(SECURITY_GROUPS)
493 purge = set(existing) - set(SECURITY_GROUPS)
493
494
@@ -507,13 +508,10 b' def ensure_security_groups(ec2resource, '
507 print('adding security group %s' % actual)
508 print('adding security group %s' % actual)
508
509
509 group_res = ec2resource.create_security_group(
510 group_res = ec2resource.create_security_group(
510 Description=group['description'],
511 Description=group['description'], GroupName=actual,
511 GroupName=actual,
512 )
512 )
513
513
514 group_res.authorize_ingress(
514 group_res.authorize_ingress(IpPermissions=group['ingress'],)
515 IpPermissions=group['ingress'],
516 )
517
515
518 security_groups[name] = group_res
516 security_groups[name] = group_res
519
517
@@ -577,8 +575,10 b' def wait_for_ip_addresses(instances):'
577 instance.reload()
575 instance.reload()
578 continue
576 continue
579
577
580 print('public IP address for %s: %s' % (
578 print(
581 instance.id, instance.public_ip_address))
579 'public IP address for %s: %s'
580 % (instance.id, instance.public_ip_address)
581 )
582 break
582 break
583
583
584
584
@@ -603,10 +603,7 b' def wait_for_ssm(ssmclient, instances):'
603 while True:
603 while True:
604 res = ssmclient.describe_instance_information(
604 res = ssmclient.describe_instance_information(
605 Filters=[
605 Filters=[
606 {
606 {'Key': 'InstanceIds', 'Values': [i.id for i in instances],},
607 'Key': 'InstanceIds',
608 'Values': [i.id for i in instances],
609 },
610 ],
607 ],
611 )
608 )
612
609
@@ -628,9 +625,7 b' def run_ssm_command(ssmclient, instances'
628 InstanceIds=[i.id for i in instances],
625 InstanceIds=[i.id for i in instances],
629 DocumentName=document_name,
626 DocumentName=document_name,
630 Parameters=parameters,
627 Parameters=parameters,
631 CloudWatchOutputConfig={
628 CloudWatchOutputConfig={'CloudWatchOutputEnabled': True,},
632 'CloudWatchOutputEnabled': True,
633 },
634 )
629 )
635
630
636 command_id = res['Command']['CommandId']
631 command_id = res['Command']['CommandId']
@@ -639,8 +634,7 b' def run_ssm_command(ssmclient, instances'
639 while True:
634 while True:
640 try:
635 try:
641 res = ssmclient.get_command_invocation(
636 res = ssmclient.get_command_invocation(
642 CommandId=command_id,
637 CommandId=command_id, InstanceId=instance.id,
643 InstanceId=instance.id,
644 )
638 )
645 except botocore.exceptions.ClientError as e:
639 except botocore.exceptions.ClientError as e:
646 if e.response['Error']['Code'] == 'InvocationDoesNotExist':
640 if e.response['Error']['Code'] == 'InvocationDoesNotExist':
@@ -655,8 +649,9 b' def run_ssm_command(ssmclient, instances'
655 elif res['Status'] in ('Pending', 'InProgress', 'Delayed'):
649 elif res['Status'] in ('Pending', 'InProgress', 'Delayed'):
656 time.sleep(2)
650 time.sleep(2)
657 else:
651 else:
658 raise Exception('command failed on %s: %s' % (
652 raise Exception(
659 instance.id, res['Status']))
653 'command failed on %s: %s' % (instance.id, res['Status'])
654 )
660
655
661
656
662 @contextlib.contextmanager
657 @contextlib.contextmanager
@@ -711,10 +706,12 b' def create_temp_windows_ec2_instances(c:'
711 config['IamInstanceProfile'] = {
706 config['IamInstanceProfile'] = {
712 'Name': 'hg-ephemeral-ec2-1',
707 'Name': 'hg-ephemeral-ec2-1',
713 }
708 }
714 config.setdefault('TagSpecifications', []).append({
709 config.setdefault('TagSpecifications', []).append(
715 'ResourceType': 'instance',
710 {
716 'Tags': [{'Key': 'Name', 'Value': 'hg-temp-windows'}],
711 'ResourceType': 'instance',
717 })
712 'Tags': [{'Key': 'Name', 'Value': 'hg-temp-windows'}],
713 }
714 )
718 config['UserData'] = WINDOWS_USER_DATA % password
715 config['UserData'] = WINDOWS_USER_DATA % password
719
716
720 with temporary_ec2_instances(c.ec2resource, config) as instances:
717 with temporary_ec2_instances(c.ec2resource, config) as instances:
@@ -723,7 +720,9 b' def create_temp_windows_ec2_instances(c:'
723 print('waiting for Windows Remote Management service...')
720 print('waiting for Windows Remote Management service...')
724
721
725 for instance in instances:
722 for instance in instances:
726 client = wait_for_winrm(instance.public_ip_address, 'Administrator', password)
723 client = wait_for_winrm(
724 instance.public_ip_address, 'Administrator', password
725 )
727 print('established WinRM connection to %s' % instance.id)
726 print('established WinRM connection to %s' % instance.id)
728 instance.winrm_client = client
727 instance.winrm_client = client
729
728
@@ -748,14 +747,17 b' def find_and_reconcile_image(ec2resource'
748 # Store a reference to a good image so it can be returned one the
747 # Store a reference to a good image so it can be returned one the
749 # image state is reconciled.
748 # image state is reconciled.
750 images = ec2resource.images.filter(
749 images = ec2resource.images.filter(
751 Filters=[{'Name': 'name', 'Values': [name]}])
750 Filters=[{'Name': 'name', 'Values': [name]}]
751 )
752
752
753 existing_image = None
753 existing_image = None
754
754
755 for image in images:
755 for image in images:
756 if image.tags is None:
756 if image.tags is None:
757 print('image %s for %s lacks required tags; removing' % (
757 print(
758 image.id, image.name))
758 'image %s for %s lacks required tags; removing'
759 % (image.id, image.name)
760 )
759 remove_ami(ec2resource, image)
761 remove_ami(ec2resource, image)
760 else:
762 else:
761 tags = {t['Key']: t['Value'] for t in image.tags}
763 tags = {t['Key']: t['Value'] for t in image.tags}
@@ -763,15 +765,18 b' def find_and_reconcile_image(ec2resource'
763 if tags.get('HGIMAGEFINGERPRINT') == fingerprint:
765 if tags.get('HGIMAGEFINGERPRINT') == fingerprint:
764 existing_image = image
766 existing_image = image
765 else:
767 else:
766 print('image %s for %s has wrong fingerprint; removing' % (
768 print(
767 image.id, image.name))
769 'image %s for %s has wrong fingerprint; removing'
770 % (image.id, image.name)
771 )
768 remove_ami(ec2resource, image)
772 remove_ami(ec2resource, image)
769
773
770 return existing_image
774 return existing_image
771
775
772
776
773 def create_ami_from_instance(ec2client, instance, name, description,
777 def create_ami_from_instance(
774 fingerprint):
778 ec2client, instance, name, description, fingerprint
779 ):
775 """Create an AMI from a running instance.
780 """Create an AMI from a running instance.
776
781
777 Returns the ``ec2resource.Image`` representing the created AMI.
782 Returns the ``ec2resource.Image`` representing the created AMI.
@@ -779,29 +784,19 b' def create_ami_from_instance(ec2client, '
779 instance.stop()
784 instance.stop()
780
785
781 ec2client.get_waiter('instance_stopped').wait(
786 ec2client.get_waiter('instance_stopped').wait(
782 InstanceIds=[instance.id],
787 InstanceIds=[instance.id], WaiterConfig={'Delay': 5,}
783 WaiterConfig={
788 )
784 'Delay': 5,
785 })
786 print('%s is stopped' % instance.id)
789 print('%s is stopped' % instance.id)
787
790
788 image = instance.create_image(
791 image = instance.create_image(Name=name, Description=description,)
789 Name=name,
790 Description=description,
791 )
792
792
793 image.create_tags(Tags=[
793 image.create_tags(
794 {
794 Tags=[{'Key': 'HGIMAGEFINGERPRINT', 'Value': fingerprint,},]
795 'Key': 'HGIMAGEFINGERPRINT',
795 )
796 'Value': fingerprint,
797 },
798 ])
799
796
800 print('waiting for image %s' % image.id)
797 print('waiting for image %s' % image.id)
801
798
802 ec2client.get_waiter('image_available').wait(
799 ec2client.get_waiter('image_available').wait(ImageIds=[image.id],)
803 ImageIds=[image.id],
804 )
805
800
806 print('image %s available as %s' % (image.id, image.name))
801 print('image %s available as %s' % (image.id, image.name))
807
802
@@ -827,9 +822,7 b' def ensure_linux_dev_ami(c: AWSConnectio'
827 ssh_username = 'admin'
822 ssh_username = 'admin'
828 elif distro == 'debian10':
823 elif distro == 'debian10':
829 image = find_image(
824 image = find_image(
830 ec2resource,
825 ec2resource, DEBIAN_ACCOUNT_ID_2, 'debian-10-amd64-20190909-10',
831 DEBIAN_ACCOUNT_ID_2,
832 'debian-10-amd64-20190909-10',
833 )
826 )
834 ssh_username = 'admin'
827 ssh_username = 'admin'
835 elif distro == 'ubuntu18.04':
828 elif distro == 'ubuntu18.04':
@@ -871,10 +864,12 b' def ensure_linux_dev_ami(c: AWSConnectio'
871 'SecurityGroupIds': [c.security_groups['linux-dev-1'].id],
864 'SecurityGroupIds': [c.security_groups['linux-dev-1'].id],
872 }
865 }
873
866
874 requirements2_path = (pathlib.Path(__file__).parent.parent /
867 requirements2_path = (
875 'linux-requirements-py2.txt')
868 pathlib.Path(__file__).parent.parent / 'linux-requirements-py2.txt'
876 requirements3_path = (pathlib.Path(__file__).parent.parent /
869 )
877 'linux-requirements-py3.txt')
870 requirements3_path = (
871 pathlib.Path(__file__).parent.parent / 'linux-requirements-py3.txt'
872 )
878 with requirements2_path.open('r', encoding='utf-8') as fh:
873 with requirements2_path.open('r', encoding='utf-8') as fh:
879 requirements2 = fh.read()
874 requirements2 = fh.read()
880 with requirements3_path.open('r', encoding='utf-8') as fh:
875 with requirements3_path.open('r', encoding='utf-8') as fh:
@@ -882,12 +877,14 b' def ensure_linux_dev_ami(c: AWSConnectio'
882
877
883 # Compute a deterministic fingerprint to determine whether image needs to
878 # Compute a deterministic fingerprint to determine whether image needs to
884 # be regenerated.
879 # be regenerated.
885 fingerprint = resolve_fingerprint({
880 fingerprint = resolve_fingerprint(
886 'instance_config': config,
881 {
887 'bootstrap_script': BOOTSTRAP_DEBIAN,
882 'instance_config': config,
888 'requirements_py2': requirements2,
883 'bootstrap_script': BOOTSTRAP_DEBIAN,
889 'requirements_py3': requirements3,
884 'requirements_py2': requirements2,
890 })
885 'requirements_py3': requirements3,
886 }
887 )
891
888
892 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint)
889 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint)
893
890
@@ -902,9 +899,11 b' def ensure_linux_dev_ami(c: AWSConnectio'
902 instance = instances[0]
899 instance = instances[0]
903
900
904 client = wait_for_ssh(
901 client = wait_for_ssh(
905 instance.public_ip_address, 22,
902 instance.public_ip_address,
903 22,
906 username=ssh_username,
904 username=ssh_username,
907 key_filename=str(c.key_pair_path_private('automation')))
905 key_filename=str(c.key_pair_path_private('automation')),
906 )
908
907
909 home = '/home/%s' % ssh_username
908 home = '/home/%s' % ssh_username
910
909
@@ -926,8 +925,9 b' def ensure_linux_dev_ami(c: AWSConnectio'
926 fh.chmod(0o0700)
925 fh.chmod(0o0700)
927
926
928 print('executing bootstrap')
927 print('executing bootstrap')
929 chan, stdin, stdout = ssh_exec_command(client,
928 chan, stdin, stdout = ssh_exec_command(
930 '%s/bootstrap' % home)
929 client, '%s/bootstrap' % home
930 )
931 stdin.close()
931 stdin.close()
932
932
933 for line in stdout:
933 for line in stdout:
@@ -937,17 +937,28 b' def ensure_linux_dev_ami(c: AWSConnectio'
937 if res:
937 if res:
938 raise Exception('non-0 exit from bootstrap: %d' % res)
938 raise Exception('non-0 exit from bootstrap: %d' % res)
939
939
940 print('bootstrap completed; stopping %s to create %s' % (
940 print(
941 instance.id, name))
941 'bootstrap completed; stopping %s to create %s'
942 % (instance.id, name)
943 )
942
944
943 return create_ami_from_instance(ec2client, instance, name,
945 return create_ami_from_instance(
944 'Mercurial Linux development environment',
946 ec2client,
945 fingerprint)
947 instance,
948 name,
949 'Mercurial Linux development environment',
950 fingerprint,
951 )
946
952
947
953
948 @contextlib.contextmanager
954 @contextlib.contextmanager
949 def temporary_linux_dev_instances(c: AWSConnection, image, instance_type,
955 def temporary_linux_dev_instances(
950 prefix='hg-', ensure_extra_volume=False):
956 c: AWSConnection,
957 image,
958 instance_type,
959 prefix='hg-',
960 ensure_extra_volume=False,
961 ):
951 """Create temporary Linux development EC2 instances.
962 """Create temporary Linux development EC2 instances.
952
963
953 Context manager resolves to a list of ``ec2.Instance`` that were created
964 Context manager resolves to a list of ``ec2.Instance`` that were created
@@ -979,8 +990,9 b' def temporary_linux_dev_instances(c: AWS'
979
990
980 # This is not an exhaustive list of instance types having instance storage.
991 # This is not an exhaustive list of instance types having instance storage.
981 # But
992 # But
982 if (ensure_extra_volume
993 if ensure_extra_volume and not instance_type.startswith(
983 and not instance_type.startswith(tuple(INSTANCE_TYPES_WITH_STORAGE))):
994 tuple(INSTANCE_TYPES_WITH_STORAGE)
995 ):
984 main_device = block_device_mappings[0]['DeviceName']
996 main_device = block_device_mappings[0]['DeviceName']
985
997
986 if main_device == 'xvda':
998 if main_device == 'xvda':
@@ -988,17 +1000,20 b' def temporary_linux_dev_instances(c: AWS'
988 elif main_device == '/dev/sda1':
1000 elif main_device == '/dev/sda1':
989 second_device = '/dev/sdb'
1001 second_device = '/dev/sdb'
990 else:
1002 else:
991 raise ValueError('unhandled primary EBS device name: %s' %
1003 raise ValueError(
992 main_device)
1004 'unhandled primary EBS device name: %s' % main_device
1005 )
993
1006
994 block_device_mappings.append({
1007 block_device_mappings.append(
995 'DeviceName': second_device,
1008 {
996 'Ebs': {
1009 'DeviceName': second_device,
997 'DeleteOnTermination': True,
1010 'Ebs': {
998 'VolumeSize': 8,
1011 'DeleteOnTermination': True,
999 'VolumeType': 'gp2',
1012 'VolumeSize': 8,
1013 'VolumeType': 'gp2',
1014 },
1000 }
1015 }
1001 })
1016 )
1002
1017
1003 config = {
1018 config = {
1004 'BlockDeviceMappings': block_device_mappings,
1019 'BlockDeviceMappings': block_device_mappings,
@@ -1019,9 +1034,11 b' def temporary_linux_dev_instances(c: AWS'
1019
1034
1020 for instance in instances:
1035 for instance in instances:
1021 client = wait_for_ssh(
1036 client = wait_for_ssh(
1022 instance.public_ip_address, 22,
1037 instance.public_ip_address,
1038 22,
1023 username='hg',
1039 username='hg',
1024 key_filename=ssh_private_key_path)
1040 key_filename=ssh_private_key_path,
1041 )
1025
1042
1026 instance.ssh_client = client
1043 instance.ssh_client = client
1027 instance.ssh_private_key_path = ssh_private_key_path
1044 instance.ssh_private_key_path = ssh_private_key_path
@@ -1033,8 +1050,9 b' def temporary_linux_dev_instances(c: AWS'
1033 instance.ssh_client.close()
1050 instance.ssh_client.close()
1034
1051
1035
1052
1036 def ensure_windows_dev_ami(c: AWSConnection, prefix='hg-',
1053 def ensure_windows_dev_ami(
1037 base_image_name=WINDOWS_BASE_IMAGE_NAME):
1054 c: AWSConnection, prefix='hg-', base_image_name=WINDOWS_BASE_IMAGE_NAME
1055 ):
1038 """Ensure Windows Development AMI is available and up-to-date.
1056 """Ensure Windows Development AMI is available and up-to-date.
1039
1057
1040 If necessary, a modern AMI will be built by starting a temporary EC2
1058 If necessary, a modern AMI will be built by starting a temporary EC2
@@ -1100,13 +1118,15 b' def ensure_windows_dev_ami(c: AWSConnect'
1100
1118
1101 # Compute a deterministic fingerprint to determine whether image needs
1119 # Compute a deterministic fingerprint to determine whether image needs
1102 # to be regenerated.
1120 # to be regenerated.
1103 fingerprint = resolve_fingerprint({
1121 fingerprint = resolve_fingerprint(
1104 'instance_config': config,
1122 {
1105 'user_data': WINDOWS_USER_DATA,
1123 'instance_config': config,
1106 'initial_bootstrap': WINDOWS_BOOTSTRAP_POWERSHELL,
1124 'user_data': WINDOWS_USER_DATA,
1107 'bootstrap_commands': commands,
1125 'initial_bootstrap': WINDOWS_BOOTSTRAP_POWERSHELL,
1108 'base_image_name': base_image_name,
1126 'bootstrap_commands': commands,
1109 })
1127 'base_image_name': base_image_name,
1128 }
1129 )
1110
1130
1111 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint)
1131 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint)
1112
1132
@@ -1131,9 +1151,7 b' def ensure_windows_dev_ami(c: AWSConnect'
1131 ssmclient,
1151 ssmclient,
1132 [instance],
1152 [instance],
1133 'AWS-RunPowerShellScript',
1153 'AWS-RunPowerShellScript',
1134 {
1154 {'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'),},
1135 'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'),
1136 },
1137 )
1155 )
1138
1156
1139 # Reboot so all updates are fully applied.
1157 # Reboot so all updates are fully applied.
@@ -1145,10 +1163,8 b' def ensure_windows_dev_ami(c: AWSConnect'
1145 print('rebooting instance %s' % instance.id)
1163 print('rebooting instance %s' % instance.id)
1146 instance.stop()
1164 instance.stop()
1147 ec2client.get_waiter('instance_stopped').wait(
1165 ec2client.get_waiter('instance_stopped').wait(
1148 InstanceIds=[instance.id],
1166 InstanceIds=[instance.id], WaiterConfig={'Delay': 5,}
1149 WaiterConfig={
1167 )
1150 'Delay': 5,
1151 })
1152
1168
1153 instance.start()
1169 instance.start()
1154 wait_for_ip_addresses([instance])
1170 wait_for_ip_addresses([instance])
@@ -1159,8 +1175,11 b' def ensure_windows_dev_ami(c: AWSConnect'
1159 # TODO figure out a workaround.
1175 # TODO figure out a workaround.
1160
1176
1161 print('waiting for Windows Remote Management to come back...')
1177 print('waiting for Windows Remote Management to come back...')
1162 client = wait_for_winrm(instance.public_ip_address, 'Administrator',
1178 client = wait_for_winrm(
1163 c.automation.default_password())
1179 instance.public_ip_address,
1180 'Administrator',
1181 c.automation.default_password(),
1182 )
1164 print('established WinRM connection to %s' % instance.id)
1183 print('established WinRM connection to %s' % instance.id)
1165 instance.winrm_client = client
1184 instance.winrm_client = client
1166
1185
@@ -1168,14 +1187,23 b' def ensure_windows_dev_ami(c: AWSConnect'
1168 run_powershell(instance.winrm_client, '\n'.join(commands))
1187 run_powershell(instance.winrm_client, '\n'.join(commands))
1169
1188
1170 print('bootstrap completed; stopping %s to create image' % instance.id)
1189 print('bootstrap completed; stopping %s to create image' % instance.id)
1171 return create_ami_from_instance(ec2client, instance, name,
1190 return create_ami_from_instance(
1172 'Mercurial Windows development environment',
1191 ec2client,
1173 fingerprint)
1192 instance,
1193 name,
1194 'Mercurial Windows development environment',
1195 fingerprint,
1196 )
1174
1197
1175
1198
1176 @contextlib.contextmanager
1199 @contextlib.contextmanager
1177 def temporary_windows_dev_instances(c: AWSConnection, image, instance_type,
1200 def temporary_windows_dev_instances(
1178 prefix='hg-', disable_antivirus=False):
1201 c: AWSConnection,
1202 image,
1203 instance_type,
1204 prefix='hg-',
1205 disable_antivirus=False,
1206 ):
1179 """Create a temporary Windows development EC2 instance.
1207 """Create a temporary Windows development EC2 instance.
1180
1208
1181 Context manager resolves to the list of ``EC2.Instance`` that were created.
1209 Context manager resolves to the list of ``EC2.Instance`` that were created.
@@ -1205,6 +1233,7 b' def temporary_windows_dev_instances(c: A'
1205 for instance in instances:
1233 for instance in instances:
1206 run_powershell(
1234 run_powershell(
1207 instance.winrm_client,
1235 instance.winrm_client,
1208 'Set-MpPreference -DisableRealtimeMonitoring $true')
1236 'Set-MpPreference -DisableRealtimeMonitoring $true',
1237 )
1209
1238
1210 yield instances
1239 yield instances
@@ -22,12 +22,15 b' from . import ('
22 )
22 )
23
23
24
24
25 SOURCE_ROOT = pathlib.Path(os.path.abspath(__file__)).parent.parent.parent.parent
25 SOURCE_ROOT = pathlib.Path(
26 os.path.abspath(__file__)
27 ).parent.parent.parent.parent
26 DIST_PATH = SOURCE_ROOT / 'dist'
28 DIST_PATH = SOURCE_ROOT / 'dist'
27
29
28
30
29 def bootstrap_linux_dev(hga: HGAutomation, aws_region, distros=None,
31 def bootstrap_linux_dev(
30 parallel=False):
32 hga: HGAutomation, aws_region, distros=None, parallel=False
33 ):
31 c = hga.aws_connection(aws_region)
34 c = hga.aws_connection(aws_region)
32
35
33 if distros:
36 if distros:
@@ -59,8 +62,9 b' def bootstrap_windows_dev(hga: HGAutomat'
59 print('Windows development AMI available as %s' % image.id)
62 print('Windows development AMI available as %s' % image.id)
60
63
61
64
62 def build_inno(hga: HGAutomation, aws_region, arch, revision, version,
65 def build_inno(
63 base_image_name):
66 hga: HGAutomation, aws_region, arch, revision, version, base_image_name
67 ):
64 c = hga.aws_connection(aws_region)
68 c = hga.aws_connection(aws_region)
65 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
69 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
66 DIST_PATH.mkdir(exist_ok=True)
70 DIST_PATH.mkdir(exist_ok=True)
@@ -71,13 +75,14 b' def build_inno(hga: HGAutomation, aws_re'
71 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
75 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
72
76
73 for a in arch:
77 for a in arch:
74 windows.build_inno_installer(instance.winrm_client, a,
78 windows.build_inno_installer(
75 DIST_PATH,
79 instance.winrm_client, a, DIST_PATH, version=version
76 version=version)
80 )
77
81
78
82
79 def build_wix(hga: HGAutomation, aws_region, arch, revision, version,
83 def build_wix(
80 base_image_name):
84 hga: HGAutomation, aws_region, arch, revision, version, base_image_name
85 ):
81 c = hga.aws_connection(aws_region)
86 c = hga.aws_connection(aws_region)
82 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
87 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
83 DIST_PATH.mkdir(exist_ok=True)
88 DIST_PATH.mkdir(exist_ok=True)
@@ -88,12 +93,14 b' def build_wix(hga: HGAutomation, aws_reg'
88 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
93 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
89
94
90 for a in arch:
95 for a in arch:
91 windows.build_wix_installer(instance.winrm_client, a,
96 windows.build_wix_installer(
92 DIST_PATH, version=version)
97 instance.winrm_client, a, DIST_PATH, version=version
98 )
93
99
94
100
95 def build_windows_wheel(hga: HGAutomation, aws_region, arch, revision,
101 def build_windows_wheel(
96 base_image_name):
102 hga: HGAutomation, aws_region, arch, revision, base_image_name
103 ):
97 c = hga.aws_connection(aws_region)
104 c = hga.aws_connection(aws_region)
98 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
105 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
99 DIST_PATH.mkdir(exist_ok=True)
106 DIST_PATH.mkdir(exist_ok=True)
@@ -107,8 +114,9 b' def build_windows_wheel(hga: HGAutomatio'
107 windows.build_wheel(instance.winrm_client, a, DIST_PATH)
114 windows.build_wheel(instance.winrm_client, a, DIST_PATH)
108
115
109
116
110 def build_all_windows_packages(hga: HGAutomation, aws_region, revision,
117 def build_all_windows_packages(
111 version, base_image_name):
118 hga: HGAutomation, aws_region, revision, version, base_image_name
119 ):
112 c = hga.aws_connection(aws_region)
120 c = hga.aws_connection(aws_region)
113 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
121 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
114 DIST_PATH.mkdir(exist_ok=True)
122 DIST_PATH.mkdir(exist_ok=True)
@@ -124,11 +132,13 b' def build_all_windows_packages(hga: HGAu'
124 windows.purge_hg(winrm_client)
132 windows.purge_hg(winrm_client)
125 windows.build_wheel(winrm_client, arch, DIST_PATH)
133 windows.build_wheel(winrm_client, arch, DIST_PATH)
126 windows.purge_hg(winrm_client)
134 windows.purge_hg(winrm_client)
127 windows.build_inno_installer(winrm_client, arch, DIST_PATH,
135 windows.build_inno_installer(
128 version=version)
136 winrm_client, arch, DIST_PATH, version=version
137 )
129 windows.purge_hg(winrm_client)
138 windows.purge_hg(winrm_client)
130 windows.build_wix_installer(winrm_client, arch, DIST_PATH,
139 windows.build_wix_installer(
131 version=version)
140 winrm_client, arch, DIST_PATH, version=version
141 )
132
142
133
143
134 def terminate_ec2_instances(hga: HGAutomation, aws_region):
144 def terminate_ec2_instances(hga: HGAutomation, aws_region):
@@ -141,8 +151,15 b' def purge_ec2_resources(hga: HGAutomatio'
141 aws.remove_resources(c)
151 aws.remove_resources(c)
142
152
143
153
144 def run_tests_linux(hga: HGAutomation, aws_region, instance_type,
154 def run_tests_linux(
145 python_version, test_flags, distro, filesystem):
155 hga: HGAutomation,
156 aws_region,
157 instance_type,
158 python_version,
159 test_flags,
160 distro,
161 filesystem,
162 ):
146 c = hga.aws_connection(aws_region)
163 c = hga.aws_connection(aws_region)
147 image = aws.ensure_linux_dev_ami(c, distro=distro)
164 image = aws.ensure_linux_dev_ami(c, distro=distro)
148
165
@@ -151,17 +168,17 b' def run_tests_linux(hga: HGAutomation, a'
151 ensure_extra_volume = filesystem not in ('default', 'tmpfs')
168 ensure_extra_volume = filesystem not in ('default', 'tmpfs')
152
169
153 with aws.temporary_linux_dev_instances(
170 with aws.temporary_linux_dev_instances(
154 c, image, instance_type,
171 c, image, instance_type, ensure_extra_volume=ensure_extra_volume
155 ensure_extra_volume=ensure_extra_volume) as insts:
172 ) as insts:
156
173
157 instance = insts[0]
174 instance = insts[0]
158
175
159 linux.prepare_exec_environment(instance.ssh_client,
176 linux.prepare_exec_environment(
160 filesystem=filesystem)
177 instance.ssh_client, filesystem=filesystem
178 )
161 linux.synchronize_hg(SOURCE_ROOT, instance, '.')
179 linux.synchronize_hg(SOURCE_ROOT, instance, '.')
162 t_prepared = time.time()
180 t_prepared = time.time()
163 linux.run_tests(instance.ssh_client, python_version,
181 linux.run_tests(instance.ssh_client, python_version, test_flags)
164 test_flags)
165 t_done = time.time()
182 t_done = time.time()
166
183
167 t_setup = t_prepared - t_start
184 t_setup = t_prepared - t_start
@@ -169,29 +186,48 b' def run_tests_linux(hga: HGAutomation, a'
169
186
170 print(
187 print(
171 'total time: %.1fs; setup: %.1fs; tests: %.1fs; setup overhead: %.1f%%'
188 'total time: %.1fs; setup: %.1fs; tests: %.1fs; setup overhead: %.1f%%'
172 % (t_all, t_setup, t_done - t_prepared, t_setup / t_all * 100.0))
189 % (t_all, t_setup, t_done - t_prepared, t_setup / t_all * 100.0)
190 )
173
191
174
192
175 def run_tests_windows(hga: HGAutomation, aws_region, instance_type,
193 def run_tests_windows(
176 python_version, arch, test_flags, base_image_name):
194 hga: HGAutomation,
195 aws_region,
196 instance_type,
197 python_version,
198 arch,
199 test_flags,
200 base_image_name,
201 ):
177 c = hga.aws_connection(aws_region)
202 c = hga.aws_connection(aws_region)
178 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
203 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
179
204
180 with aws.temporary_windows_dev_instances(c, image, instance_type,
205 with aws.temporary_windows_dev_instances(
181 disable_antivirus=True) as insts:
206 c, image, instance_type, disable_antivirus=True
207 ) as insts:
182 instance = insts[0]
208 instance = insts[0]
183
209
184 windows.synchronize_hg(SOURCE_ROOT, '.', instance)
210 windows.synchronize_hg(SOURCE_ROOT, '.', instance)
185 windows.run_tests(instance.winrm_client, python_version, arch,
211 windows.run_tests(
186 test_flags)
212 instance.winrm_client, python_version, arch, test_flags
213 )
187
214
188
215
189 def publish_windows_artifacts(hg: HGAutomation, aws_region, version: str,
216 def publish_windows_artifacts(
190 pypi: bool, mercurial_scm_org: bool,
217 hg: HGAutomation,
191 ssh_username: str):
218 aws_region,
192 windows.publish_artifacts(DIST_PATH, version,
219 version: str,
193 pypi=pypi, mercurial_scm_org=mercurial_scm_org,
220 pypi: bool,
194 ssh_username=ssh_username)
221 mercurial_scm_org: bool,
222 ssh_username: str,
223 ):
224 windows.publish_artifacts(
225 DIST_PATH,
226 version,
227 pypi=pypi,
228 mercurial_scm_org=mercurial_scm_org,
229 ssh_username=ssh_username,
230 )
195
231
196
232
197 def run_try(hga: HGAutomation, aws_region: str, rev: str):
233 def run_try(hga: HGAutomation, aws_region: str, rev: str):
@@ -208,25 +244,21 b' def get_parser():'
208 help='Path for local state files',
244 help='Path for local state files',
209 )
245 )
210 parser.add_argument(
246 parser.add_argument(
211 '--aws-region',
247 '--aws-region', help='AWS region to use', default='us-west-2',
212 help='AWS region to use',
213 default='us-west-2',
214 )
248 )
215
249
216 subparsers = parser.add_subparsers()
250 subparsers = parser.add_subparsers()
217
251
218 sp = subparsers.add_parser(
252 sp = subparsers.add_parser(
219 'bootstrap-linux-dev',
253 'bootstrap-linux-dev', help='Bootstrap Linux development environments',
220 help='Bootstrap Linux development environments',
221 )
254 )
222 sp.add_argument(
255 sp.add_argument(
223 '--distros',
256 '--distros', help='Comma delimited list of distros to bootstrap',
224 help='Comma delimited list of distros to bootstrap',
225 )
257 )
226 sp.add_argument(
258 sp.add_argument(
227 '--parallel',
259 '--parallel',
228 action='store_true',
260 action='store_true',
229 help='Generate AMIs in parallel (not CTRL-c safe)'
261 help='Generate AMIs in parallel (not CTRL-c safe)',
230 )
262 )
231 sp.set_defaults(func=bootstrap_linux_dev)
263 sp.set_defaults(func=bootstrap_linux_dev)
232
264
@@ -242,17 +274,13 b' def get_parser():'
242 sp.set_defaults(func=bootstrap_windows_dev)
274 sp.set_defaults(func=bootstrap_windows_dev)
243
275
244 sp = subparsers.add_parser(
276 sp = subparsers.add_parser(
245 'build-all-windows-packages',
277 'build-all-windows-packages', help='Build all Windows packages',
246 help='Build all Windows packages',
247 )
278 )
248 sp.add_argument(
279 sp.add_argument(
249 '--revision',
280 '--revision', help='Mercurial revision to build', default='.',
250 help='Mercurial revision to build',
251 default='.',
252 )
281 )
253 sp.add_argument(
282 sp.add_argument(
254 '--version',
283 '--version', help='Mercurial version string to use',
255 help='Mercurial version string to use',
256 )
284 )
257 sp.add_argument(
285 sp.add_argument(
258 '--base-image-name',
286 '--base-image-name',
@@ -262,8 +290,7 b' def get_parser():'
262 sp.set_defaults(func=build_all_windows_packages)
290 sp.set_defaults(func=build_all_windows_packages)
263
291
264 sp = subparsers.add_parser(
292 sp = subparsers.add_parser(
265 'build-inno',
293 'build-inno', help='Build Inno Setup installer(s)',
266 help='Build Inno Setup installer(s)',
267 )
294 )
268 sp.add_argument(
295 sp.add_argument(
269 '--arch',
296 '--arch',
@@ -273,13 +300,10 b' def get_parser():'
273 default=['x64'],
300 default=['x64'],
274 )
301 )
275 sp.add_argument(
302 sp.add_argument(
276 '--revision',
303 '--revision', help='Mercurial revision to build', default='.',
277 help='Mercurial revision to build',
278 default='.',
279 )
304 )
280 sp.add_argument(
305 sp.add_argument(
281 '--version',
306 '--version', help='Mercurial version string to use in installer',
282 help='Mercurial version string to use in installer',
283 )
307 )
284 sp.add_argument(
308 sp.add_argument(
285 '--base-image-name',
309 '--base-image-name',
@@ -289,8 +313,7 b' def get_parser():'
289 sp.set_defaults(func=build_inno)
313 sp.set_defaults(func=build_inno)
290
314
291 sp = subparsers.add_parser(
315 sp = subparsers.add_parser(
292 'build-windows-wheel',
316 'build-windows-wheel', help='Build Windows wheel(s)',
293 help='Build Windows wheel(s)',
294 )
317 )
295 sp.add_argument(
318 sp.add_argument(
296 '--arch',
319 '--arch',
@@ -300,9 +323,7 b' def get_parser():'
300 default=['x64'],
323 default=['x64'],
301 )
324 )
302 sp.add_argument(
325 sp.add_argument(
303 '--revision',
326 '--revision', help='Mercurial revision to build', default='.',
304 help='Mercurial revision to build',
305 default='.',
306 )
327 )
307 sp.add_argument(
328 sp.add_argument(
308 '--base-image-name',
329 '--base-image-name',
@@ -311,10 +332,7 b' def get_parser():'
311 )
332 )
312 sp.set_defaults(func=build_windows_wheel)
333 sp.set_defaults(func=build_windows_wheel)
313
334
314 sp = subparsers.add_parser(
335 sp = subparsers.add_parser('build-wix', help='Build WiX installer(s)')
315 'build-wix',
316 help='Build WiX installer(s)'
317 )
318 sp.add_argument(
336 sp.add_argument(
319 '--arch',
337 '--arch',
320 help='Architecture to build for',
338 help='Architecture to build for',
@@ -323,13 +341,10 b' def get_parser():'
323 default=['x64'],
341 default=['x64'],
324 )
342 )
325 sp.add_argument(
343 sp.add_argument(
326 '--revision',
344 '--revision', help='Mercurial revision to build', default='.',
327 help='Mercurial revision to build',
328 default='.',
329 )
345 )
330 sp.add_argument(
346 sp.add_argument(
331 '--version',
347 '--version', help='Mercurial version string to use in installer',
332 help='Mercurial version string to use in installer',
333 )
348 )
334 sp.add_argument(
349 sp.add_argument(
335 '--base-image-name',
350 '--base-image-name',
@@ -345,15 +360,11 b' def get_parser():'
345 sp.set_defaults(func=terminate_ec2_instances)
360 sp.set_defaults(func=terminate_ec2_instances)
346
361
347 sp = subparsers.add_parser(
362 sp = subparsers.add_parser(
348 'purge-ec2-resources',
363 'purge-ec2-resources', help='Purge all EC2 resources managed by us',
349 help='Purge all EC2 resources managed by us',
350 )
364 )
351 sp.set_defaults(func=purge_ec2_resources)
365 sp.set_defaults(func=purge_ec2_resources)
352
366
353 sp = subparsers.add_parser(
367 sp = subparsers.add_parser('run-tests-linux', help='Run tests on Linux',)
354 'run-tests-linux',
355 help='Run tests on Linux',
356 )
357 sp.add_argument(
368 sp.add_argument(
358 '--distro',
369 '--distro',
359 help='Linux distribution to run tests on',
370 help='Linux distribution to run tests on',
@@ -374,8 +385,18 b' def get_parser():'
374 sp.add_argument(
385 sp.add_argument(
375 '--python-version',
386 '--python-version',
376 help='Python version to use',
387 help='Python version to use',
377 choices={'system2', 'system3', '2.7', '3.5', '3.6', '3.7', '3.8',
388 choices={
378 'pypy', 'pypy3.5', 'pypy3.6'},
389 'system2',
390 'system3',
391 '2.7',
392 '3.5',
393 '3.6',
394 '3.7',
395 '3.8',
396 'pypy',
397 'pypy3.5',
398 'pypy3.6',
399 },
379 default='system2',
400 default='system2',
380 )
401 )
381 sp.add_argument(
402 sp.add_argument(
@@ -386,13 +407,10 b' def get_parser():'
386 sp.set_defaults(func=run_tests_linux)
407 sp.set_defaults(func=run_tests_linux)
387
408
388 sp = subparsers.add_parser(
409 sp = subparsers.add_parser(
389 'run-tests-windows',
410 'run-tests-windows', help='Run tests on Windows',
390 help='Run tests on Windows',
391 )
411 )
392 sp.add_argument(
412 sp.add_argument(
393 '--instance-type',
413 '--instance-type', help='EC2 instance type to use', default='t3.medium',
394 help='EC2 instance type to use',
395 default='t3.medium',
396 )
414 )
397 sp.add_argument(
415 sp.add_argument(
398 '--python-version',
416 '--python-version',
@@ -407,8 +425,7 b' def get_parser():'
407 default='x64',
425 default='x64',
408 )
426 )
409 sp.add_argument(
427 sp.add_argument(
410 '--test-flags',
428 '--test-flags', help='Extra command line flags to pass to run-tests.py',
411 help='Extra command line flags to pass to run-tests.py',
412 )
429 )
413 sp.add_argument(
430 sp.add_argument(
414 '--base-image-name',
431 '--base-image-name',
@@ -419,7 +436,7 b' def get_parser():'
419
436
420 sp = subparsers.add_parser(
437 sp = subparsers.add_parser(
421 'publish-windows-artifacts',
438 'publish-windows-artifacts',
422 help='Publish built Windows artifacts (wheels, installers, etc)'
439 help='Publish built Windows artifacts (wheels, installers, etc)',
423 )
440 )
424 sp.add_argument(
441 sp.add_argument(
425 '--no-pypi',
442 '--no-pypi',
@@ -436,22 +453,17 b' def get_parser():'
436 help='Skip uploading to www.mercurial-scm.org',
453 help='Skip uploading to www.mercurial-scm.org',
437 )
454 )
438 sp.add_argument(
455 sp.add_argument(
439 '--ssh-username',
456 '--ssh-username', help='SSH username for mercurial-scm.org',
440 help='SSH username for mercurial-scm.org',
441 )
457 )
442 sp.add_argument(
458 sp.add_argument(
443 'version',
459 'version', help='Mercurial version string to locate local packages',
444 help='Mercurial version string to locate local packages',
445 )
460 )
446 sp.set_defaults(func=publish_windows_artifacts)
461 sp.set_defaults(func=publish_windows_artifacts)
447
462
448 sp = subparsers.add_parser(
463 sp = subparsers.add_parser(
449 'try',
464 'try', help='Run CI automation against a custom changeset'
450 help='Run CI automation against a custom changeset'
451 )
465 )
452 sp.add_argument('-r', '--rev',
466 sp.add_argument('-r', '--rev', default='.', help='Revision to run CI on')
453 default='.',
454 help='Revision to run CI on')
455 sp.set_defaults(func=run_try)
467 sp.set_defaults(func=run_try)
456
468
457 return parser
469 return parser
@@ -13,9 +13,7 b' import shlex'
13 import subprocess
13 import subprocess
14 import tempfile
14 import tempfile
15
15
16 from .ssh import (
16 from .ssh import exec_command
17 exec_command,
18 )
19
17
20
18
21 # Linux distributions that are supported.
19 # Linux distributions that are supported.
@@ -62,7 +60,9 b' for v in ${PYENV3_VERSIONS}; do'
62 done
60 done
63
61
64 pyenv global ${PYENV2_VERSIONS} ${PYENV3_VERSIONS} system
62 pyenv global ${PYENV2_VERSIONS} ${PYENV3_VERSIONS} system
65 '''.lstrip().replace('\r\n', '\n')
63 '''.lstrip().replace(
64 '\r\n', '\n'
65 )
66
66
67
67
68 INSTALL_RUST = r'''
68 INSTALL_RUST = r'''
@@ -87,10 +87,13 b' wget -O ${HG_TARBALL} --progress dot:meg'
87 echo "${HG_SHA256} ${HG_TARBALL}" | sha256sum --check -
87 echo "${HG_SHA256} ${HG_TARBALL}" | sha256sum --check -
88
88
89 /hgdev/venv-bootstrap/bin/pip install ${HG_TARBALL}
89 /hgdev/venv-bootstrap/bin/pip install ${HG_TARBALL}
90 '''.lstrip().replace('\r\n', '\n')
90 '''.lstrip().replace(
91 '\r\n', '\n'
92 )
91
93
92
94
93 BOOTSTRAP_DEBIAN = r'''
95 BOOTSTRAP_DEBIAN = (
96 r'''
94 #!/bin/bash
97 #!/bin/bash
95
98
96 set -ex
99 set -ex
@@ -323,11 +326,14 b' publish = false'
323 EOF
326 EOF
324
327
325 sudo chown -R hg:hg /hgdev
328 sudo chown -R hg:hg /hgdev
326 '''.lstrip().format(
329 '''.lstrip()
327 install_rust=INSTALL_RUST,
330 .format(
328 install_pythons=INSTALL_PYTHONS,
331 install_rust=INSTALL_RUST,
329 bootstrap_virtualenv=BOOTSTRAP_VIRTUALENV
332 install_pythons=INSTALL_PYTHONS,
330 ).replace('\r\n', '\n')
333 bootstrap_virtualenv=BOOTSTRAP_VIRTUALENV,
334 )
335 .replace('\r\n', '\n')
336 )
331
337
332
338
333 # Prepares /hgdev for operations.
339 # Prepares /hgdev for operations.
@@ -409,7 +415,9 b' mkdir /hgwork/tmp'
409 chown hg:hg /hgwork/tmp
415 chown hg:hg /hgwork/tmp
410
416
411 rsync -a /hgdev/src /hgwork/
417 rsync -a /hgdev/src /hgwork/
412 '''.lstrip().replace('\r\n', '\n')
418 '''.lstrip().replace(
419 '\r\n', '\n'
420 )
413
421
414
422
415 HG_UPDATE_CLEAN = '''
423 HG_UPDATE_CLEAN = '''
@@ -421,7 +429,9 b' cd /hgwork/src'
421 ${HG} --config extensions.purge= purge --all
429 ${HG} --config extensions.purge= purge --all
422 ${HG} update -C $1
430 ${HG} update -C $1
423 ${HG} log -r .
431 ${HG} log -r .
424 '''.lstrip().replace('\r\n', '\n')
432 '''.lstrip().replace(
433 '\r\n', '\n'
434 )
425
435
426
436
427 def prepare_exec_environment(ssh_client, filesystem='default'):
437 def prepare_exec_environment(ssh_client, filesystem='default'):
@@ -456,11 +466,12 b' def prepare_exec_environment(ssh_client,'
456 res = chan.recv_exit_status()
466 res = chan.recv_exit_status()
457
467
458 if res:
468 if res:
459 raise Exception('non-0 exit code updating working directory; %d'
469 raise Exception('non-0 exit code updating working directory; %d' % res)
460 % res)
461
470
462
471
463 def synchronize_hg(source_path: pathlib.Path, ec2_instance, revision: str=None):
472 def synchronize_hg(
473 source_path: pathlib.Path, ec2_instance, revision: str = None
474 ):
464 """Synchronize a local Mercurial source path to remote EC2 instance."""
475 """Synchronize a local Mercurial source path to remote EC2 instance."""
465
476
466 with tempfile.TemporaryDirectory() as temp_dir:
477 with tempfile.TemporaryDirectory() as temp_dir:
@@ -482,8 +493,10 b' def synchronize_hg(source_path: pathlib.'
482 fh.write(' IdentityFile %s\n' % ec2_instance.ssh_private_key_path)
493 fh.write(' IdentityFile %s\n' % ec2_instance.ssh_private_key_path)
483
494
484 if not (source_path / '.hg').is_dir():
495 if not (source_path / '.hg').is_dir():
485 raise Exception('%s is not a Mercurial repository; synchronization '
496 raise Exception(
486 'not yet supported' % source_path)
497 '%s is not a Mercurial repository; synchronization '
498 'not yet supported' % source_path
499 )
487
500
488 env = dict(os.environ)
501 env = dict(os.environ)
489 env['HGPLAIN'] = '1'
502 env['HGPLAIN'] = '1'
@@ -493,17 +506,29 b' def synchronize_hg(source_path: pathlib.'
493
506
494 res = subprocess.run(
507 res = subprocess.run(
495 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
508 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
496 cwd=str(source_path), env=env, check=True, capture_output=True)
509 cwd=str(source_path),
510 env=env,
511 check=True,
512 capture_output=True,
513 )
497
514
498 full_revision = res.stdout.decode('ascii')
515 full_revision = res.stdout.decode('ascii')
499
516
500 args = [
517 args = [
501 'python2.7', str(hg_bin),
518 'python2.7',
502 '--config', 'ui.ssh=ssh -F %s' % ssh_config,
519 str(hg_bin),
503 '--config', 'ui.remotecmd=/hgdev/venv-bootstrap/bin/hg',
520 '--config',
521 'ui.ssh=ssh -F %s' % ssh_config,
522 '--config',
523 'ui.remotecmd=/hgdev/venv-bootstrap/bin/hg',
504 # Also ensure .hgtags changes are present so auto version
524 # Also ensure .hgtags changes are present so auto version
505 # calculation works.
525 # calculation works.
506 'push', '-f', '-r', full_revision, '-r', 'file(.hgtags)',
526 'push',
527 '-f',
528 '-r',
529 full_revision,
530 '-r',
531 'file(.hgtags)',
507 'ssh://%s//hgwork/src' % public_ip,
532 'ssh://%s//hgwork/src' % public_ip,
508 ]
533 ]
509
534
@@ -522,7 +547,8 b' def synchronize_hg(source_path: pathlib.'
522 fh.chmod(0o0700)
547 fh.chmod(0o0700)
523
548
524 chan, stdin, stdout = exec_command(
549 chan, stdin, stdout = exec_command(
525 ec2_instance.ssh_client, '/hgdev/hgup %s' % full_revision)
550 ec2_instance.ssh_client, '/hgdev/hgup %s' % full_revision
551 )
526 stdin.close()
552 stdin.close()
527
553
528 for line in stdout:
554 for line in stdout:
@@ -531,8 +557,9 b' def synchronize_hg(source_path: pathlib.'
531 res = chan.recv_exit_status()
557 res = chan.recv_exit_status()
532
558
533 if res:
559 if res:
534 raise Exception('non-0 exit code updating working directory; %d'
560 raise Exception(
535 % res)
561 'non-0 exit code updating working directory; %d' % res
562 )
536
563
537
564
538 def run_tests(ssh_client, python_version, test_flags=None):
565 def run_tests(ssh_client, python_version, test_flags=None):
@@ -554,8 +581,8 b' def run_tests(ssh_client, python_version'
554
581
555 command = (
582 command = (
556 '/bin/sh -c "export TMPDIR=/hgwork/tmp; '
583 '/bin/sh -c "export TMPDIR=/hgwork/tmp; '
557 'cd /hgwork/src/tests && %s run-tests.py %s"' % (
584 'cd /hgwork/src/tests && %s run-tests.py %s"' % (python, test_flags)
558 python, test_flags))
585 )
559
586
560 chan, stdin, stdout = exec_command(ssh_client, command)
587 chan, stdin, stdout = exec_command(ssh_client, command)
561
588
@@ -7,12 +7,8 b''
7
7
8 # no-check-code because Python 3 native.
8 # no-check-code because Python 3 native.
9
9
10 from twine.commands.upload import (
10 from twine.commands.upload import upload as twine_upload
11 upload as twine_upload,
11 from twine.settings import Settings
12 )
13 from twine.settings import (
14 Settings,
15 )
16
12
17
13
18 def upload(paths):
14 def upload(paths):
@@ -11,14 +11,13 b' import socket'
11 import time
11 import time
12 import warnings
12 import warnings
13
13
14 from cryptography.utils import (
14 from cryptography.utils import CryptographyDeprecationWarning
15 CryptographyDeprecationWarning,
16 )
17 import paramiko
15 import paramiko
18
16
19
17
20 def wait_for_ssh(hostname, port, timeout=60, username=None, key_filename=None):
18 def wait_for_ssh(hostname, port, timeout=60, username=None, key_filename=None):
21 """Wait for an SSH server to start on the specified host and port."""
19 """Wait for an SSH server to start on the specified host and port."""
20
22 class IgnoreHostKeyPolicy(paramiko.MissingHostKeyPolicy):
21 class IgnoreHostKeyPolicy(paramiko.MissingHostKeyPolicy):
23 def missing_host_key(self, client, hostname, key):
22 def missing_host_key(self, client, hostname, key):
24 return
23 return
@@ -28,17 +27,23 b' def wait_for_ssh(hostname, port, timeout'
28 # paramiko triggers a CryptographyDeprecationWarning in the cryptography
27 # paramiko triggers a CryptographyDeprecationWarning in the cryptography
29 # package. Let's suppress
28 # package. Let's suppress
30 with warnings.catch_warnings():
29 with warnings.catch_warnings():
31 warnings.filterwarnings('ignore',
30 warnings.filterwarnings(
32 category=CryptographyDeprecationWarning)
31 'ignore', category=CryptographyDeprecationWarning
32 )
33
33
34 while True:
34 while True:
35 client = paramiko.SSHClient()
35 client = paramiko.SSHClient()
36 client.set_missing_host_key_policy(IgnoreHostKeyPolicy())
36 client.set_missing_host_key_policy(IgnoreHostKeyPolicy())
37 try:
37 try:
38 client.connect(hostname, port=port, username=username,
38 client.connect(
39 key_filename=key_filename,
39 hostname,
40 timeout=5.0, allow_agent=False,
40 port=port,
41 look_for_keys=False)
41 username=username,
42 key_filename=key_filename,
43 timeout=5.0,
44 allow_agent=False,
45 look_for_keys=False,
46 )
42
47
43 return client
48 return client
44 except socket.error:
49 except socket.error:
@@ -15,12 +15,8 b' import re'
15 import subprocess
15 import subprocess
16 import tempfile
16 import tempfile
17
17
18 from .pypi import (
18 from .pypi import upload as pypi_upload
19 upload as pypi_upload,
19 from .winrm import run_powershell
20 )
21 from .winrm import (
22 run_powershell,
23 )
24
20
25
21
26 # PowerShell commands to activate a Visual Studio 2008 environment.
22 # PowerShell commands to activate a Visual Studio 2008 environment.
@@ -117,14 +113,21 b" MERCURIAL_SCM_BASE_URL = 'https://mercur"
117 X86_USER_AGENT_PATTERN = '.*Windows.*'
113 X86_USER_AGENT_PATTERN = '.*Windows.*'
118 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
114 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
119
115
120 X86_EXE_DESCRIPTION = ('Mercurial {version} Inno Setup installer - x86 Windows '
116 X86_EXE_DESCRIPTION = (
121 '- does not require admin rights')
117 'Mercurial {version} Inno Setup installer - x86 Windows '
122 X64_EXE_DESCRIPTION = ('Mercurial {version} Inno Setup installer - x64 Windows '
118 '- does not require admin rights'
123 '- does not require admin rights')
119 )
124 X86_MSI_DESCRIPTION = ('Mercurial {version} MSI installer - x86 Windows '
120 X64_EXE_DESCRIPTION = (
125 '- requires admin rights')
121 'Mercurial {version} Inno Setup installer - x64 Windows '
126 X64_MSI_DESCRIPTION = ('Mercurial {version} MSI installer - x64 Windows '
122 '- does not require admin rights'
127 '- requires admin rights')
123 )
124 X86_MSI_DESCRIPTION = (
125 'Mercurial {version} MSI installer - x86 Windows ' '- requires admin rights'
126 )
127 X64_MSI_DESCRIPTION = (
128 'Mercurial {version} MSI installer - x64 Windows ' '- requires admin rights'
129 )
130
128
131
129 def get_vc_prefix(arch):
132 def get_vc_prefix(arch):
130 if arch == 'x86':
133 if arch == 'x86':
@@ -158,10 +161,21 b' def synchronize_hg(hg_repo: pathlib.Path'
158 ssh_dir.chmod(0o0700)
161 ssh_dir.chmod(0o0700)
159
162
160 # Generate SSH key to use for communication.
163 # Generate SSH key to use for communication.
161 subprocess.run([
164 subprocess.run(
162 'ssh-keygen', '-t', 'rsa', '-b', '4096', '-N', '',
165 [
163 '-f', str(ssh_dir / 'id_rsa')],
166 'ssh-keygen',
164 check=True, capture_output=True)
167 '-t',
168 'rsa',
169 '-b',
170 '4096',
171 '-N',
172 '',
173 '-f',
174 str(ssh_dir / 'id_rsa'),
175 ],
176 check=True,
177 capture_output=True,
178 )
165
179
166 # Add it to ~/.ssh/authorized_keys on remote.
180 # Add it to ~/.ssh/authorized_keys on remote.
167 # This assumes the file doesn't already exist.
181 # This assumes the file doesn't already exist.
@@ -182,8 +196,10 b' def synchronize_hg(hg_repo: pathlib.Path'
182 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
196 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
183
197
184 if not (hg_repo / '.hg').is_dir():
198 if not (hg_repo / '.hg').is_dir():
185 raise Exception('%s is not a Mercurial repository; '
199 raise Exception(
186 'synchronization not yet supported' % hg_repo)
200 '%s is not a Mercurial repository; '
201 'synchronization not yet supported' % hg_repo
202 )
187
203
188 env = dict(os.environ)
204 env = dict(os.environ)
189 env['HGPLAIN'] = '1'
205 env['HGPLAIN'] = '1'
@@ -193,17 +209,29 b' def synchronize_hg(hg_repo: pathlib.Path'
193
209
194 res = subprocess.run(
210 res = subprocess.run(
195 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
211 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
196 cwd=str(hg_repo), env=env, check=True, capture_output=True)
212 cwd=str(hg_repo),
213 env=env,
214 check=True,
215 capture_output=True,
216 )
197
217
198 full_revision = res.stdout.decode('ascii')
218 full_revision = res.stdout.decode('ascii')
199
219
200 args = [
220 args = [
201 'python2.7', hg_bin,
221 'python2.7',
202 '--config', 'ui.ssh=ssh -F %s' % ssh_config,
222 hg_bin,
203 '--config', 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
223 '--config',
224 'ui.ssh=ssh -F %s' % ssh_config,
225 '--config',
226 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
204 # Also ensure .hgtags changes are present so auto version
227 # Also ensure .hgtags changes are present so auto version
205 # calculation works.
228 # calculation works.
206 'push', '-f', '-r', full_revision, '-r', 'file(.hgtags)',
229 'push',
230 '-f',
231 '-r',
232 full_revision,
233 '-r',
234 'file(.hgtags)',
207 'ssh://%s/c:/hgdev/src' % public_ip,
235 'ssh://%s/c:/hgdev/src' % public_ip,
208 ]
236 ]
209
237
@@ -213,8 +241,9 b' def synchronize_hg(hg_repo: pathlib.Path'
213 if res.returncode not in (0, 1):
241 if res.returncode not in (0, 1):
214 res.check_returncode()
242 res.check_returncode()
215
243
216 run_powershell(winrm_client,
244 run_powershell(
217 HG_UPDATE_CLEAN.format(revision=full_revision))
245 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
246 )
218
247
219 # TODO detect dirty local working directory and synchronize accordingly.
248 # TODO detect dirty local working directory and synchronize accordingly.
220
249
@@ -250,8 +279,9 b' def copy_latest_dist(winrm_client, patte'
250 winrm_client.fetch(source, str(dest))
279 winrm_client.fetch(source, str(dest))
251
280
252
281
253 def build_inno_installer(winrm_client, arch: str, dest_path: pathlib.Path,
282 def build_inno_installer(
254 version=None):
283 winrm_client, arch: str, dest_path: pathlib.Path, version=None
284 ):
255 """Build the Inno Setup installer on a remote machine.
285 """Build the Inno Setup installer on a remote machine.
256
286
257 Using a WinRM client, remote commands are executed to build
287 Using a WinRM client, remote commands are executed to build
@@ -263,8 +293,9 b' def build_inno_installer(winrm_client, a'
263 if version:
293 if version:
264 extra_args.extend(['--version', version])
294 extra_args.extend(['--version', version])
265
295
266 ps = get_vc_prefix(arch) + BUILD_INNO.format(arch=arch,
296 ps = get_vc_prefix(arch) + BUILD_INNO.format(
267 extra_args=' '.join(extra_args))
297 arch=arch, extra_args=' '.join(extra_args)
298 )
268 run_powershell(winrm_client, ps)
299 run_powershell(winrm_client, ps)
269 copy_latest_dist(winrm_client, '*.exe', dest_path)
300 copy_latest_dist(winrm_client, '*.exe', dest_path)
270
301
@@ -281,8 +312,9 b' def build_wheel(winrm_client, arch: str,'
281 copy_latest_dist(winrm_client, '*.whl', dest_path)
312 copy_latest_dist(winrm_client, '*.whl', dest_path)
282
313
283
314
284 def build_wix_installer(winrm_client, arch: str, dest_path: pathlib.Path,
315 def build_wix_installer(
285 version=None):
316 winrm_client, arch: str, dest_path: pathlib.Path, version=None
317 ):
286 """Build the WiX installer on a remote machine.
318 """Build the WiX installer on a remote machine.
287
319
288 Using a WinRM client, remote commands are executed to build a WiX installer.
320 Using a WinRM client, remote commands are executed to build a WiX installer.
@@ -292,8 +324,9 b' def build_wix_installer(winrm_client, ar'
292 if version:
324 if version:
293 extra_args.extend(['--version', version])
325 extra_args.extend(['--version', version])
294
326
295 ps = get_vc_prefix(arch) + BUILD_WIX.format(arch=arch,
327 ps = get_vc_prefix(arch) + BUILD_WIX.format(
296 extra_args=' '.join(extra_args))
328 arch=arch, extra_args=' '.join(extra_args)
329 )
297 run_powershell(winrm_client, ps)
330 run_powershell(winrm_client, ps)
298 copy_latest_dist(winrm_client, '*.msi', dest_path)
331 copy_latest_dist(winrm_client, '*.msi', dest_path)
299
332
@@ -307,18 +340,16 b' def run_tests(winrm_client, python_versi'
307 ``run-tests.py``.
340 ``run-tests.py``.
308 """
341 """
309 if not re.match(r'\d\.\d', python_version):
342 if not re.match(r'\d\.\d', python_version):
310 raise ValueError(r'python_version must be \d.\d; got %s' %
343 raise ValueError(
311 python_version)
344 r'python_version must be \d.\d; got %s' % python_version
345 )
312
346
313 if arch not in ('x86', 'x64'):
347 if arch not in ('x86', 'x64'):
314 raise ValueError('arch must be x86 or x64; got %s' % arch)
348 raise ValueError('arch must be x86 or x64; got %s' % arch)
315
349
316 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
350 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
317
351
318 ps = RUN_TESTS.format(
352 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
319 python_path=python_path,
320 test_flags=test_flags or '',
321 )
322
353
323 run_powershell(winrm_client, ps)
354 run_powershell(winrm_client, ps)
324
355
@@ -374,8 +405,8 b' def generate_latest_dat(version: str):'
374 version,
405 version,
375 X64_USER_AGENT_PATTERN,
406 X64_USER_AGENT_PATTERN,
376 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
407 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
377 X64_MSI_DESCRIPTION.format(version=version)
408 X64_MSI_DESCRIPTION.format(version=version),
378 )
409 ),
379 )
410 )
380
411
381 lines = ['\t'.join(e) for e in entries]
412 lines = ['\t'.join(e) for e in entries]
@@ -396,8 +427,9 b' def publish_artifacts_pypi(dist_path: pa'
396 pypi_upload(wheel_paths)
427 pypi_upload(wheel_paths)
397
428
398
429
399 def publish_artifacts_mercurial_scm_org(dist_path: pathlib.Path, version: str,
430 def publish_artifacts_mercurial_scm_org(
400 ssh_username=None):
431 dist_path: pathlib.Path, version: str, ssh_username=None
432 ):
401 """Publish Windows release artifacts to mercurial-scm.org."""
433 """Publish Windows release artifacts to mercurial-scm.org."""
402 all_paths = resolve_all_artifacts(dist_path, version)
434 all_paths = resolve_all_artifacts(dist_path, version)
403
435
@@ -436,7 +468,8 b' def publish_artifacts_mercurial_scm_org('
436
468
437 now = datetime.datetime.utcnow()
469 now = datetime.datetime.utcnow()
438 backup_path = dist_path / (
470 backup_path = dist_path / (
439 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S'))
471 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S')
472 )
440 print('backing up %s to %s' % (latest_dat_path, backup_path))
473 print('backing up %s to %s' % (latest_dat_path, backup_path))
441
474
442 with sftp.open(latest_dat_path, 'rb') as fh:
475 with sftp.open(latest_dat_path, 'rb') as fh:
@@ -453,9 +486,13 b' def publish_artifacts_mercurial_scm_org('
453 fh.write(latest_dat_content.encode('ascii'))
486 fh.write(latest_dat_content.encode('ascii'))
454
487
455
488
456 def publish_artifacts(dist_path: pathlib.Path, version: str,
489 def publish_artifacts(
457 pypi=True, mercurial_scm_org=True,
490 dist_path: pathlib.Path,
458 ssh_username=None):
491 version: str,
492 pypi=True,
493 mercurial_scm_org=True,
494 ssh_username=None,
495 ):
459 """Publish Windows release artifacts.
496 """Publish Windows release artifacts.
460
497
461 Files are found in `dist_path`. We will look for files with version string
498 Files are found in `dist_path`. We will look for files with version string
@@ -468,5 +505,6 b' def publish_artifacts(dist_path: pathlib'
468 publish_artifacts_pypi(dist_path, version)
505 publish_artifacts_pypi(dist_path, version)
469
506
470 if mercurial_scm_org:
507 if mercurial_scm_org:
471 publish_artifacts_mercurial_scm_org(dist_path, version,
508 publish_artifacts_mercurial_scm_org(
472 ssh_username=ssh_username)
509 dist_path, version, ssh_username=ssh_username
510 )
@@ -11,9 +11,7 b' import logging'
11 import pprint
11 import pprint
12 import time
12 import time
13
13
14 from pypsrp.client import (
14 from pypsrp.client import Client
15 Client,
16 )
17 from pypsrp.powershell import (
15 from pypsrp.powershell import (
18 PowerShell,
16 PowerShell,
19 PSInvocationState,
17 PSInvocationState,
@@ -35,8 +33,13 b' def wait_for_winrm(host, username, passw'
35
33
36 while True:
34 while True:
37 try:
35 try:
38 client = Client(host, username=username, password=password,
36 client = Client(
39 ssl=ssl, connection_timeout=5)
37 host,
38 username=username,
39 password=password,
40 ssl=ssl,
41 connection_timeout=5,
42 )
40 client.execute_ps("Write-Host 'Hello, World!'")
43 client.execute_ps("Write-Host 'Hello, World!'")
41 return client
44 return client
42 except requests.exceptions.ConnectionError:
45 except requests.exceptions.ConnectionError:
@@ -78,5 +81,7 b' def run_powershell(client, script):'
78 print(format_object(o))
81 print(format_object(o))
79
82
80 if ps.state == PSInvocationState.FAILED:
83 if ps.state == PSInvocationState.FAILED:
81 raise Exception('PowerShell execution failed: %s' %
84 raise Exception(
82 ' '.join(map(format_object, ps.streams.error)))
85 'PowerShell execution failed: %s'
86 % ' '.join(map(format_object, ps.streams.error))
87 )
@@ -9,15 +9,20 b' from mercurial import ('
9 pycompat,
9 pycompat,
10 )
10 )
11
11
12
12 def reducetest(a, b):
13 def reducetest(a, b):
13 tries = 0
14 tries = 0
14 reductions = 0
15 reductions = 0
15 print("reducing...")
16 print("reducing...")
16 while tries < 1000:
17 while tries < 1000:
17 a2 = "\n".join(l for l in a.splitlines()
18 a2 = (
18 if random.randint(0, 100) > 0) + "\n"
19 "\n".join(l for l in a.splitlines() if random.randint(0, 100) > 0)
19 b2 = "\n".join(l for l in b.splitlines()
20 + "\n"
20 if random.randint(0, 100) > 0) + "\n"
21 )
22 b2 = (
23 "\n".join(l for l in b.splitlines() if random.randint(0, 100) > 0)
24 + "\n"
25 )
21 if a2 == a and b2 == b:
26 if a2 == a and b2 == b:
22 continue
27 continue
23 if a2 == b2:
28 if a2 == b2:
@@ -32,8 +37,7 b' def reducetest(a, b):'
32 a = a2
37 a = a2
33 b = b2
38 b = b2
34
39
35 print("reduced:", reductions, len(a) + len(b),
40 print("reduced:", reductions, len(a) + len(b), repr(a), repr(b))
36 repr(a), repr(b))
37 try:
41 try:
38 test1(a, b)
42 test1(a, b)
39 except Exception as inst:
43 except Exception as inst:
@@ -41,6 +45,7 b' def reducetest(a, b):'
41
45
42 sys.exit(0)
46 sys.exit(0)
43
47
48
44 def test1(a, b):
49 def test1(a, b):
45 d = mdiff.textdiff(a, b)
50 d = mdiff.textdiff(a, b)
46 if not d:
51 if not d:
@@ -49,6 +54,7 b' def test1(a, b):'
49 if c != b:
54 if c != b:
50 raise ValueError("bad")
55 raise ValueError("bad")
51
56
57
52 def testwrap(a, b):
58 def testwrap(a, b):
53 try:
59 try:
54 test1(a, b)
60 test1(a, b)
@@ -57,10 +63,12 b' def testwrap(a, b):'
57 print("exception:", inst)
63 print("exception:", inst)
58 reducetest(a, b)
64 reducetest(a, b)
59
65
66
60 def test(a, b):
67 def test(a, b):
61 testwrap(a, b)
68 testwrap(a, b)
62 testwrap(b, a)
69 testwrap(b, a)
63
70
71
64 def rndtest(size, noise):
72 def rndtest(size, noise):
65 a = []
73 a = []
66 src = " aaaaaaaabbbbccd"
74 src = " aaaaaaaabbbbccd"
@@ -82,6 +90,7 b' def rndtest(size, noise):'
82
90
83 test(a, b)
91 test(a, b)
84
92
93
85 maxvol = 10000
94 maxvol = 10000
86 startsize = 2
95 startsize = 2
87 while True:
96 while True:
@@ -44,15 +44,24 b' from mercurial import ('
44 util,
44 util,
45 )
45 )
46
46
47 basedir = os.path.abspath(os.path.join(os.path.dirname(__file__),
47 basedir = os.path.abspath(
48 os.path.pardir, os.path.pardir))
48 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir)
49 )
49 reposdir = os.environ['REPOS_DIR']
50 reposdir = os.environ['REPOS_DIR']
50 reposnames = [name for name in os.listdir(reposdir)
51 reposnames = [
51 if os.path.isdir(os.path.join(reposdir, name, ".hg"))]
52 name
53 for name in os.listdir(reposdir)
54 if os.path.isdir(os.path.join(reposdir, name, ".hg"))
55 ]
52 if not reposnames:
56 if not reposnames:
53 raise ValueError("No repositories found in $REPO_DIR")
57 raise ValueError("No repositories found in $REPO_DIR")
54 outputre = re.compile((r'! wall (\d+.\d+) comb \d+.\d+ user \d+.\d+ sys '
58 outputre = re.compile(
55 r'\d+.\d+ \(best of \d+\)'))
59 (
60 r'! wall (\d+.\d+) comb \d+.\d+ user \d+.\d+ sys '
61 r'\d+.\d+ \(best of \d+\)'
62 )
63 )
64
56
65
57 def runperfcommand(reponame, command, *args, **kwargs):
66 def runperfcommand(reponame, command, *args, **kwargs):
58 os.environ["HGRCPATH"] = os.environ.get("ASVHGRCPATH", "")
67 os.environ["HGRCPATH"] = os.environ.get("ASVHGRCPATH", "")
@@ -63,8 +72,9 b' def runperfcommand(reponame, command, *a'
63 else:
72 else:
64 ui = uimod.ui()
73 ui = uimod.ui()
65 repo = hg.repository(ui, os.path.join(reposdir, reponame))
74 repo = hg.repository(ui, os.path.join(reposdir, reponame))
66 perfext = extensions.load(ui, 'perfext',
75 perfext = extensions.load(
67 os.path.join(basedir, 'contrib', 'perf.py'))
76 ui, 'perfext', os.path.join(basedir, 'contrib', 'perf.py')
77 )
68 cmd = getattr(perfext, command)
78 cmd = getattr(perfext, command)
69 ui.pushbuffer()
79 ui.pushbuffer()
70 cmd(ui, repo, *args, **kwargs)
80 cmd(ui, repo, *args, **kwargs)
@@ -74,6 +84,7 b' def runperfcommand(reponame, command, *a'
74 raise ValueError("Invalid output {0}".format(output))
84 raise ValueError("Invalid output {0}".format(output))
75 return float(match.group(1))
85 return float(match.group(1))
76
86
87
77 def perfbench(repos=reposnames, name=None, params=None):
88 def perfbench(repos=reposnames, name=None, params=None):
78 """decorator to declare ASV benchmark based on contrib/perf.py extension
89 """decorator to declare ASV benchmark based on contrib/perf.py extension
79
90
@@ -104,10 +115,12 b' def perfbench(repos=reposnames, name=Non'
104 def wrapped(repo, *args):
115 def wrapped(repo, *args):
105 def perf(command, *a, **kw):
116 def perf(command, *a, **kw):
106 return runperfcommand(repo, command, *a, **kw)
117 return runperfcommand(repo, command, *a, **kw)
118
107 return func(perf, *args)
119 return func(perf, *args)
108
120
109 wrapped.params = [p[1] for p in params]
121 wrapped.params = [p[1] for p in params]
110 wrapped.param_names = [p[0] for p in params]
122 wrapped.param_names = [p[0] for p in params]
111 wrapped.pretty_name = name
123 wrapped.pretty_name = name
112 return wrapped
124 return wrapped
125
113 return decorator
126 return decorator
@@ -9,18 +9,22 b' from __future__ import absolute_import'
9
9
10 from . import perfbench
10 from . import perfbench
11
11
12
12 @perfbench()
13 @perfbench()
13 def track_tags(perf):
14 def track_tags(perf):
14 return perf("perftags")
15 return perf("perftags")
15
16
17
16 @perfbench()
18 @perfbench()
17 def track_status(perf):
19 def track_status(perf):
18 return perf("perfstatus", unknown=False)
20 return perf("perfstatus", unknown=False)
19
21
22
20 @perfbench(params=[('rev', ['1000', '10000', 'tip'])])
23 @perfbench(params=[('rev', ['1000', '10000', 'tip'])])
21 def track_manifest(perf, rev):
24 def track_manifest(perf, rev):
22 return perf("perfmanifest", rev)
25 return perf("perfmanifest", rev)
23
26
27
24 @perfbench()
28 @perfbench()
25 def track_heads(perf):
29 def track_heads(perf):
26 return perf("perfheads")
30 return perf("perfheads")
@@ -18,15 +18,16 b' import sys'
18
18
19 from . import basedir, perfbench
19 from . import basedir, perfbench
20
20
21
21 def createrevsetbenchmark(baseset, variants=None):
22 def createrevsetbenchmark(baseset, variants=None):
22 if variants is None:
23 if variants is None:
23 # Default variants
24 # Default variants
24 variants = ["plain", "first", "last", "sort", "sort+first",
25 variants = ["plain", "first", "last", "sort", "sort+first", "sort+last"]
25 "sort+last"]
26 fname = "track_" + "_".join(
26 fname = "track_" + "_".join("".join([
27 "".join(
27 c if c in string.digits + string.letters else " "
28 [c if c in string.digits + string.letters else " " for c in baseset]
28 for c in baseset
29 ).split()
29 ]).split())
30 )
30
31
31 def wrap(fname, baseset):
32 def wrap(fname, baseset):
32 @perfbench(name=baseset, params=[("variant", variants)])
33 @perfbench(name=baseset, params=[("variant", variants)])
@@ -36,18 +37,21 b' def createrevsetbenchmark(baseset, varia'
36 for var in variant.split("+"):
37 for var in variant.split("+"):
37 revset = "%s(%s)" % (var, revset)
38 revset = "%s(%s)" % (var, revset)
38 return perf("perfrevset", revset)
39 return perf("perfrevset", revset)
40
39 f.__name__ = fname
41 f.__name__ = fname
40 return f
42 return f
43
41 return wrap(fname, baseset)
44 return wrap(fname, baseset)
42
45
46
43 def initializerevsetbenchmarks():
47 def initializerevsetbenchmarks():
44 mod = sys.modules[__name__]
48 mod = sys.modules[__name__]
45 with open(os.path.join(basedir, 'contrib', 'base-revsets.txt'),
49 with open(os.path.join(basedir, 'contrib', 'base-revsets.txt'), 'rb') as fh:
46 'rb') as fh:
47 for line in fh:
50 for line in fh:
48 baseset = line.strip()
51 baseset = line.strip()
49 if baseset and not baseset.startswith('#'):
52 if baseset and not baseset.startswith('#'):
50 func = createrevsetbenchmark(baseset)
53 func = createrevsetbenchmark(baseset)
51 setattr(mod, func.__name__, func)
54 setattr(mod, func.__name__, func)
52
55
56
53 initializerevsetbenchmarks()
57 initializerevsetbenchmarks()
@@ -18,10 +18,13 b' import tempfile'
18 import token
18 import token
19 import tokenize
19 import tokenize
20
20
21
21 def adjusttokenpos(t, ofs):
22 def adjusttokenpos(t, ofs):
22 """Adjust start/end column of the given token"""
23 """Adjust start/end column of the given token"""
23 return t._replace(start=(t.start[0], t.start[1] + ofs),
24 return t._replace(
24 end=(t.end[0], t.end[1] + ofs))
25 start=(t.start[0], t.start[1] + ofs), end=(t.end[0], t.end[1] + ofs)
26 )
27
25
28
26 def replacetokens(tokens, opts):
29 def replacetokens(tokens, opts):
27 """Transform a stream of tokens from raw to Python 3.
30 """Transform a stream of tokens from raw to Python 3.
@@ -82,9 +85,8 b' def replacetokens(tokens, opts):'
82 currtoken = tokens[k]
85 currtoken = tokens[k]
83 while currtoken.type in (token.STRING, token.NEWLINE, tokenize.NL):
86 while currtoken.type in (token.STRING, token.NEWLINE, tokenize.NL):
84 k += 1
87 k += 1
85 if (
88 if currtoken.type == token.STRING and currtoken.string.startswith(
86 currtoken.type == token.STRING
89 ("'", '"')
87 and currtoken.string.startswith(("'", '"'))
88 ):
90 ):
89 sysstrtokens.add(currtoken)
91 sysstrtokens.add(currtoken)
90 try:
92 try:
@@ -126,7 +128,7 b' def replacetokens(tokens, opts):'
126 coloffset = -1 # column offset for the current line (-1: TBD)
128 coloffset = -1 # column offset for the current line (-1: TBD)
127 parens = [(0, 0, 0, -1)] # stack of (line, end-column, column-offset, type)
129 parens = [(0, 0, 0, -1)] # stack of (line, end-column, column-offset, type)
128 ignorenextline = False # don't transform the next line
130 ignorenextline = False # don't transform the next line
129 insideignoreblock = False # don't transform until turned off
131 insideignoreblock = False # don't transform until turned off
130 for i, t in enumerate(tokens):
132 for i, t in enumerate(tokens):
131 # Compute the column offset for the current line, such that
133 # Compute the column offset for the current line, such that
132 # the current line will be aligned to the last opening paren
134 # the current line will be aligned to the last opening paren
@@ -135,9 +137,9 b' def replacetokens(tokens, opts):'
135 lastparen = parens[-1]
137 lastparen = parens[-1]
136 if t.start[1] == lastparen[1]:
138 if t.start[1] == lastparen[1]:
137 coloffset = lastparen[2]
139 coloffset = lastparen[2]
138 elif (
140 elif t.start[1] + 1 == lastparen[1] and lastparen[3] not in (
139 t.start[1] + 1 == lastparen[1]
141 token.NEWLINE,
140 and lastparen[3] not in (token.NEWLINE, tokenize.NL)
142 tokenize.NL,
141 ):
143 ):
142 # fix misaligned indent of s/util.Abort/error.Abort/
144 # fix misaligned indent of s/util.Abort/error.Abort/
143 coloffset = lastparen[2] + (lastparen[1] - t.start[1])
145 coloffset = lastparen[2] + (lastparen[1] - t.start[1])
@@ -202,8 +204,7 b' def replacetokens(tokens, opts):'
202 continue
204 continue
203
205
204 # String literal. Prefix to make a b'' string.
206 # String literal. Prefix to make a b'' string.
205 yield adjusttokenpos(t._replace(string='b%s' % t.string),
207 yield adjusttokenpos(t._replace(string='b%s' % t.string), coloffset)
206 coloffset)
207 coldelta += 1
208 coldelta += 1
208 continue
209 continue
209
210
@@ -213,8 +214,13 b' def replacetokens(tokens, opts):'
213
214
214 # *attr() builtins don't accept byte strings to 2nd argument.
215 # *attr() builtins don't accept byte strings to 2nd argument.
215 if fn in (
216 if fn in (
216 'getattr', 'setattr', 'hasattr', 'safehasattr', 'wrapfunction',
217 'getattr',
217 'wrapclass', 'addattr'
218 'setattr',
219 'hasattr',
220 'safehasattr',
221 'wrapfunction',
222 'wrapclass',
223 'addattr',
218 ) and (opts['allow-attr-methods'] or not _isop(i - 1, '.')):
224 ) and (opts['allow-attr-methods'] or not _isop(i - 1, '.')):
219 arg1idx = _findargnofcall(1)
225 arg1idx = _findargnofcall(1)
220 if arg1idx is not None:
226 if arg1idx is not None:
@@ -241,18 +247,23 b' def replacetokens(tokens, opts):'
241 _ensuresysstr(i + 4)
247 _ensuresysstr(i + 4)
242
248
243 # Looks like "if __name__ == '__main__'".
249 # Looks like "if __name__ == '__main__'".
244 if (t.type == token.NAME and t.string == '__name__'
250 if (
245 and _isop(i + 1, '==')):
251 t.type == token.NAME
252 and t.string == '__name__'
253 and _isop(i + 1, '==')
254 ):
246 _ensuresysstr(i + 2)
255 _ensuresysstr(i + 2)
247
256
248 # Emit unmodified token.
257 # Emit unmodified token.
249 yield adjusttokenpos(t, coloffset)
258 yield adjusttokenpos(t, coloffset)
250
259
260
251 def process(fin, fout, opts):
261 def process(fin, fout, opts):
252 tokens = tokenize.tokenize(fin.readline)
262 tokens = tokenize.tokenize(fin.readline)
253 tokens = replacetokens(list(tokens), opts)
263 tokens = replacetokens(list(tokens), opts)
254 fout.write(tokenize.untokenize(tokens))
264 fout.write(tokenize.untokenize(tokens))
255
265
266
256 def tryunlink(fname):
267 def tryunlink(fname):
257 try:
268 try:
258 os.unlink(fname)
269 os.unlink(fname)
@@ -260,12 +271,14 b' def tryunlink(fname):'
260 if err.errno != errno.ENOENT:
271 if err.errno != errno.ENOENT:
261 raise
272 raise
262
273
274
263 @contextlib.contextmanager
275 @contextlib.contextmanager
264 def editinplace(fname):
276 def editinplace(fname):
265 n = os.path.basename(fname)
277 n = os.path.basename(fname)
266 d = os.path.dirname(fname)
278 d = os.path.dirname(fname)
267 fp = tempfile.NamedTemporaryFile(prefix='.%s-' % n, suffix='~', dir=d,
279 fp = tempfile.NamedTemporaryFile(
268 delete=False)
280 prefix='.%s-' % n, suffix='~', dir=d, delete=False
281 )
269 try:
282 try:
270 yield fp
283 yield fp
271 fp.close()
284 fp.close()
@@ -276,19 +289,37 b' def editinplace(fname):'
276 fp.close()
289 fp.close()
277 tryunlink(fp.name)
290 tryunlink(fp.name)
278
291
292
279 def main():
293 def main():
280 ap = argparse.ArgumentParser()
294 ap = argparse.ArgumentParser()
281 ap.add_argument('--version', action='version',
295 ap.add_argument(
282 version='Byteify strings 1.0')
296 '--version', action='version', version='Byteify strings 1.0'
283 ap.add_argument('-i', '--inplace', action='store_true', default=False,
297 )
284 help='edit files in place')
298 ap.add_argument(
285 ap.add_argument('--dictiter', action='store_true', default=False,
299 '-i',
286 help='rewrite iteritems() and itervalues()'),
300 '--inplace',
287 ap.add_argument('--allow-attr-methods', action='store_true',
301 action='store_true',
288 default=False,
302 default=False,
289 help='also handle attr*() when they are methods'),
303 help='edit files in place',
290 ap.add_argument('--treat-as-kwargs', nargs="+", default=[],
304 )
291 help="ignore kwargs-like objects"),
305 ap.add_argument(
306 '--dictiter',
307 action='store_true',
308 default=False,
309 help='rewrite iteritems() and itervalues()',
310 ),
311 ap.add_argument(
312 '--allow-attr-methods',
313 action='store_true',
314 default=False,
315 help='also handle attr*() when they are methods',
316 ),
317 ap.add_argument(
318 '--treat-as-kwargs',
319 nargs="+",
320 default=[],
321 help="ignore kwargs-like objects",
322 ),
292 ap.add_argument('files', metavar='FILE', nargs='+', help='source file')
323 ap.add_argument('files', metavar='FILE', nargs='+', help='source file')
293 args = ap.parse_args()
324 args = ap.parse_args()
294 opts = {
325 opts = {
@@ -306,6 +337,7 b' def main():'
306 fout = sys.stdout.buffer
337 fout = sys.stdout.buffer
307 process(fin, fout, opts)
338 process(fin, fout, opts)
308
339
340
309 if __name__ == '__main__':
341 if __name__ == '__main__':
310 if sys.version_info.major < 3:
342 if sys.version_info.major < 3:
311 print('This script must be run under Python 3.')
343 print('This script must be run under Python 3.')
@@ -1,12 +1,12 b''
1 from __future__ import absolute_import
1 from __future__ import absolute_import
2 import __builtin__
2 import __builtin__
3 import os
3 import os
4 from mercurial import (
4 from mercurial import util
5 util,
5
6 )
7
6
8 def lowerwrap(scope, funcname):
7 def lowerwrap(scope, funcname):
9 f = getattr(scope, funcname)
8 f = getattr(scope, funcname)
9
10 def wrap(fname, *args, **kwargs):
10 def wrap(fname, *args, **kwargs):
11 d, base = os.path.split(fname)
11 d, base = os.path.split(fname)
12 try:
12 try:
@@ -19,11 +19,14 b' def lowerwrap(scope, funcname):'
19 if fn.lower() == base.lower():
19 if fn.lower() == base.lower():
20 return f(os.path.join(d, fn), *args, **kwargs)
20 return f(os.path.join(d, fn), *args, **kwargs)
21 return f(fname, *args, **kwargs)
21 return f(fname, *args, **kwargs)
22
22 scope.__dict__[funcname] = wrap
23 scope.__dict__[funcname] = wrap
23
24
25
24 def normcase(path):
26 def normcase(path):
25 return path.lower()
27 return path.lower()
26
28
29
27 os.path.normcase = normcase
30 os.path.normcase = normcase
28
31
29 for f in 'file open'.split():
32 for f in 'file open'.split():
@@ -53,15 +53,28 b' import timeit'
53 # Python version and OS
53 # Python version and OS
54 timer = timeit.default_timer
54 timer = timeit.default_timer
55
55
56
56 def main():
57 def main():
57 parser = argparse.ArgumentParser()
58 parser = argparse.ArgumentParser()
58 parser.add_argument('pipe', type=str, nargs=1,
59 parser.add_argument(
59 help='Path of named pipe to create and listen on.')
60 'pipe',
60 parser.add_argument('output', default='trace.json', type=str, nargs='?',
61 type=str,
61 help='Path of json file to create where the traces '
62 nargs=1,
62 'will be stored.')
63 help='Path of named pipe to create and listen on.',
63 parser.add_argument('--debug', default=False, action='store_true',
64 )
64 help='Print useful debug messages')
65 parser.add_argument(
66 'output',
67 default='trace.json',
68 type=str,
69 nargs='?',
70 help='Path of json file to create where the traces ' 'will be stored.',
71 )
72 parser.add_argument(
73 '--debug',
74 default=False,
75 action='store_true',
76 help='Print useful debug messages',
77 )
65 args = parser.parse_args()
78 args = parser.parse_args()
66 fn = args.pipe[0]
79 fn = args.pipe[0]
67 os.mkfifo(fn)
80 os.mkfifo(fn)
@@ -86,19 +99,23 b' def main():'
86 payload_args = {}
99 payload_args = {}
87 pid = _threadmap[session]
100 pid = _threadmap[session]
88 ts_micros = (now - start) * 1000000
101 ts_micros = (now - start) * 1000000
89 out.write(json.dumps(
102 out.write(
90 {
103 json.dumps(
91 "name": label,
104 {
92 "cat": "misc",
105 "name": label,
93 "ph": _TYPEMAP[verb],
106 "cat": "misc",
94 "ts": ts_micros,
107 "ph": _TYPEMAP[verb],
95 "pid": pid,
108 "ts": ts_micros,
96 "tid": 1,
109 "pid": pid,
97 "args": payload_args,
110 "tid": 1,
98 }))
111 "args": payload_args,
112 }
113 )
114 )
99 out.write(',\n')
115 out.write(',\n')
100 finally:
116 finally:
101 os.unlink(fn)
117 os.unlink(fn)
102
118
119
103 if __name__ == '__main__':
120 if __name__ == '__main__':
104 main()
121 main()
This diff has been collapsed as it changes many lines, (994 lines changed) Show them Hide them
@@ -26,11 +26,15 b' import optparse'
26 import os
26 import os
27 import re
27 import re
28 import sys
28 import sys
29
29 if sys.version_info[0] < 3:
30 if sys.version_info[0] < 3:
30 opentext = open
31 opentext = open
31 else:
32 else:
33
32 def opentext(f):
34 def opentext(f):
33 return open(f, encoding='latin1')
35 return open(f, encoding='latin1')
36
37
34 try:
38 try:
35 xrange
39 xrange
36 except NameError:
40 except NameError:
@@ -42,6 +46,7 b' except ImportError:'
42
46
43 import testparseutil
47 import testparseutil
44
48
49
45 def compilere(pat, multiline=False):
50 def compilere(pat, multiline=False):
46 if multiline:
51 if multiline:
47 pat = '(?m)' + pat
52 pat = '(?m)' + pat
@@ -52,10 +57,22 b' def compilere(pat, multiline=False):'
52 pass
57 pass
53 return re.compile(pat)
58 return re.compile(pat)
54
59
60
55 # check "rules depending on implementation of repquote()" in each
61 # check "rules depending on implementation of repquote()" in each
56 # patterns (especially pypats), before changing around repquote()
62 # patterns (especially pypats), before changing around repquote()
57 _repquotefixedmap = {' ': ' ', '\n': '\n', '.': 'p', ':': 'q',
63 _repquotefixedmap = {
58 '%': '%', '\\': 'b', '*': 'A', '+': 'P', '-': 'M'}
64 ' ': ' ',
65 '\n': '\n',
66 '.': 'p',
67 ':': 'q',
68 '%': '%',
69 '\\': 'b',
70 '*': 'A',
71 '+': 'P',
72 '-': 'M',
73 }
74
75
59 def _repquoteencodechr(i):
76 def _repquoteencodechr(i):
60 if i > 255:
77 if i > 255:
61 return 'u'
78 return 'u'
@@ -67,13 +84,17 b' def _repquoteencodechr(i):'
67 if c.isdigit():
84 if c.isdigit():
68 return 'n'
85 return 'n'
69 return 'o'
86 return 'o'
87
88
70 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
89 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
71
90
91
72 def repquote(m):
92 def repquote(m):
73 t = m.group('text')
93 t = m.group('text')
74 t = t.translate(_repquotett)
94 t = t.translate(_repquotett)
75 return m.group('quote') + t + m.group('quote')
95 return m.group('quote') + t + m.group('quote')
76
96
97
77 def reppython(m):
98 def reppython(m):
78 comment = m.group('comment')
99 comment = m.group('comment')
79 if comment:
100 if comment:
@@ -81,86 +102,103 b' def reppython(m):'
81 return "#" * l + comment[l:]
102 return "#" * l + comment[l:]
82 return repquote(m)
103 return repquote(m)
83
104
105
84 def repcomment(m):
106 def repcomment(m):
85 return m.group(1) + "#" * len(m.group(2))
107 return m.group(1) + "#" * len(m.group(2))
86
108
109
87 def repccomment(m):
110 def repccomment(m):
88 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
111 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
89 return m.group(1) + t + "*/"
112 return m.group(1) + t + "*/"
90
113
114
91 def repcallspaces(m):
115 def repcallspaces(m):
92 t = re.sub(r"\n\s+", "\n", m.group(2))
116 t = re.sub(r"\n\s+", "\n", m.group(2))
93 return m.group(1) + t
117 return m.group(1) + t
94
118
119
95 def repinclude(m):
120 def repinclude(m):
96 return m.group(1) + "<foo>"
121 return m.group(1) + "<foo>"
97
122
123
98 def rephere(m):
124 def rephere(m):
99 t = re.sub(r"\S", "x", m.group(2))
125 t = re.sub(r"\S", "x", m.group(2))
100 return m.group(1) + t
126 return m.group(1) + t
101
127
102
128
103 testpats = [
129 testpats = [
104 [
130 [
105 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
131 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
106 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
132 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
107 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
133 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
108 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
134 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
109 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
135 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
110 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
136 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
111 (r'echo -n', "don't use 'echo -n', use printf"),
137 (r'echo -n', "don't use 'echo -n', use printf"),
112 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
138 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
113 (r'head -c', "don't use 'head -c', use 'dd'"),
139 (r'head -c', "don't use 'head -c', use 'dd'"),
114 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
140 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
115 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
141 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
116 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
142 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
117 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
143 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
118 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
144 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
119 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
145 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
120 (r'\[[^\]]+==', '[ foo == bar ] is a bashism, use [ foo = bar ] instead'),
146 (
121 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
147 r'\[[^\]]+==',
122 "use egrep for extended grep syntax"),
148 '[ foo == bar ] is a bashism, use [ foo = bar ] instead',
123 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
149 ),
124 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
150 (
125 (r'#!.*/bash', "don't use bash in shebang, use sh"),
151 r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
126 (r'[^\n]\Z', "no trailing newline"),
152 "use egrep for extended grep syntax",
127 (r'export .*=', "don't export and assign at once"),
153 ),
128 (r'^source\b', "don't use 'source', use '.'"),
154 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
129 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
155 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
130 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
156 (r'#!.*/bash', "don't use bash in shebang, use sh"),
131 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
157 (r'[^\n]\Z', "no trailing newline"),
132 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
158 (r'export .*=', "don't export and assign at once"),
133 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
159 (r'^source\b', "don't use 'source', use '.'"),
134 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
160 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
135 (r'^alias\b.*=', "don't use alias, use a function"),
161 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
136 (r'if\s*!', "don't use '!' to negate exit status"),
162 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
137 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
163 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
138 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
164 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
139 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
165 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
140 "put a backslash-escaped newline after sed 'i' command"),
166 (r'^alias\b.*=', "don't use alias, use a function"),
141 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
167 (r'if\s*!', "don't use '!' to negate exit status"),
142 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
168 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
143 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
169 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
144 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
170 (
145 (r'\butil\.Abort\b', "directly use error.Abort"),
171 r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
146 (r'\|&', "don't use |&, use 2>&1"),
172 "put a backslash-escaped newline after sed 'i' command",
147 (r'\w = +\w', "only one space after = allowed"),
173 ),
148 (r'\bsed\b.*[^\\]\\n', "don't use 'sed ... \\n', use a \\ and a newline"),
174 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
149 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
175 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
150 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
176 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
151 (r'grep.* -[ABC]', "don't use grep's context flags"),
177 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
152 (r'find.*-printf',
178 (r'\butil\.Abort\b', "directly use error.Abort"),
153 "don't use 'find -printf', it doesn't exist on BSD find(1)"),
179 (r'\|&', "don't use |&, use 2>&1"),
154 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
180 (r'\w = +\w', "only one space after = allowed"),
155 ],
181 (
156 # warnings
182 r'\bsed\b.*[^\\]\\n',
157 [
183 "don't use 'sed ... \\n', use a \\ and a newline",
158 (r'^function', "don't use 'function', use old style"),
184 ),
159 (r'^diff.*-\w*N', "don't use 'diff -N'"),
185 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
160 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
186 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
161 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
187 (r'grep.* -[ABC]', "don't use grep's context flags"),
162 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
188 (
163 ]
189 r'find.*-printf',
190 "don't use 'find -printf', it doesn't exist on BSD find(1)",
191 ),
192 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
193 ],
194 # warnings
195 [
196 (r'^function', "don't use 'function', use old style"),
197 (r'^diff.*-\w*N', "don't use 'diff -N'"),
198 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
199 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
200 (r'kill (`|\$\()', "don't use kill, use killdaemons.py"),
201 ],
164 ]
202 ]
165
203
166 testfilters = [
204 testfilters = [
@@ -170,45 +208,72 b' testfilters = ['
170
208
171 uprefix = r"^ \$ "
209 uprefix = r"^ \$ "
172 utestpats = [
210 utestpats = [
173 [
211 [
174 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
212 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
175 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
213 (
176 "use regex test output patterns instead of sed"),
214 uprefix + r'.*\|\s*sed[^|>\n]*\n',
177 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
215 "use regex test output patterns instead of sed",
178 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
216 ),
179 (uprefix + r'.*\|\| echo.*(fail|error)',
217 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
180 "explicit exit code checks unnecessary"),
218 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
181 (uprefix + r'set -e', "don't use set -e"),
219 (
182 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
220 uprefix + r'.*\|\| echo.*(fail|error)',
183 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
221 "explicit exit code checks unnecessary",
184 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
222 ),
185 '# no-msys'), # in test-pull.t which is skipped on windows
223 (uprefix + r'set -e', "don't use set -e"),
186 (r'^ [^$>].*27\.0\.0\.1',
224 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
187 'use $LOCALIP not an explicit loopback address'),
225 (
188 (r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
226 uprefix + r'.*:\.\S*/',
189 'mark $LOCALIP output lines with (glob) to help tests in BSD jails'),
227 "x:.y in a path does not work on msys, rewrite "
190 (r'^ (cat|find): .*: \$ENOENT\$',
228 "as x://.y, or see `hg log -k msys` for alternatives",
191 'use test -f to test for file existence'),
229 r'-\S+:\.|' '# no-msys', # -Rxxx
192 (r'^ diff -[^ -]*p',
230 ), # in test-pull.t which is skipped on windows
193 "don't use (external) diff with -p for portability"),
231 (
194 (r' readlink ', 'use readlink.py instead of readlink'),
232 r'^ [^$>].*27\.0\.0\.1',
195 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
233 'use $LOCALIP not an explicit loopback address',
196 "glob timezone field in diff output for portability"),
234 ),
197 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
235 (
198 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability"),
236 r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
199 (r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
237 'mark $LOCALIP output lines with (glob) to help tests in BSD jails',
200 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability"),
238 ),
201 (r'^ @@ -[0-9]+ [+][0-9]+ @@',
239 (
202 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
240 r'^ (cat|find): .*: \$ENOENT\$',
203 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
241 'use test -f to test for file existence',
204 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
242 ),
205 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
243 (
206 ],
244 r'^ diff -[^ -]*p',
207 # warnings
245 "don't use (external) diff with -p for portability",
208 [
246 ),
209 (r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
247 (r' readlink ', 'use readlink.py instead of readlink'),
210 "glob match with no glob string (?, *, /, and $LOCALIP)"),
248 (
211 ]
249 r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
250 "glob timezone field in diff output for portability",
251 ),
252 (
253 r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
254 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability",
255 ),
256 (
257 r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
258 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability",
259 ),
260 (
261 r'^ @@ -[0-9]+ [+][0-9]+ @@',
262 "use '@@ -N* +N* @@ (glob)' style chunk header for portability",
263 ),
264 (
265 uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
266 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
267 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)",
268 ),
269 ],
270 # warnings
271 [
272 (
273 r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
274 "glob match with no glob string (?, *, /, and $LOCALIP)",
275 ),
276 ],
212 ]
277 ]
213
278
214 # transform plain test rules to unified test's
279 # transform plain test rules to unified test's
@@ -234,148 +299,212 b' utestfilters = ['
234
299
235 # common patterns to check *.py
300 # common patterns to check *.py
236 commonpypats = [
301 commonpypats = [
237 [
302 [
238 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
303 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
239 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
304 (
240 "tuple parameter unpacking not available in Python 3+"),
305 r'^\s*def\s*\w+\s*\(.*,\s*\(',
241 (r'lambda\s*\(.*,.*\)',
306 "tuple parameter unpacking not available in Python 3+",
242 "tuple parameter unpacking not available in Python 3+"),
307 ),
243 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
308 (
244 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
309 r'lambda\s*\(.*,.*\)',
245 (r'\bdict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
310 "tuple parameter unpacking not available in Python 3+",
246 'dict-from-generator'),
311 ),
247 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
312 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
248 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
313 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
249 (r'^\s*\t', "don't use tabs"),
314 (
250 (r'\S;\s*\n', "semicolon"),
315 r'\bdict\(.*=',
251 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
316 'dict() is different in Py2 and 3 and is slower than {}',
252 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
317 'dict-from-generator',
253 (r'(\w|\)),\w', "missing whitespace after ,"),
318 ),
254 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
319 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
255 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
320 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
256 ((
321 (r'^\s*\t', "don't use tabs"),
257 # a line ending with a colon, potentially with trailing comments
322 (r'\S;\s*\n', "semicolon"),
258 r':([ \t]*#[^\n]*)?\n'
323 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
259 # one that is not a pass and not only a comment
324 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
260 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
325 (r'(\w|\)),\w', "missing whitespace after ,"),
261 # more lines at the same indent level
326 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
262 r'((?P=indent)[^\n]+\n)*'
327 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
263 # a pass at the same indent level, which is bogus
328 (
264 r'(?P=indent)pass[ \t\n#]'
329 (
265 ), 'omit superfluous pass'),
330 # a line ending with a colon, potentially with trailing comments
266 (r'[^\n]\Z', "no trailing newline"),
331 r':([ \t]*#[^\n]*)?\n'
267 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
332 # one that is not a pass and not only a comment
268 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
333 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
269 # "don't use underbars in identifiers"),
334 # more lines at the same indent level
270 (r'^\s+(self\.)?[A-Za-z][a-z0-9]+[A-Z]\w* = ',
335 r'((?P=indent)[^\n]+\n)*'
271 "don't use camelcase in identifiers", r'#.*camelcase-required'),
336 # a pass at the same indent level, which is bogus
272 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
337 r'(?P=indent)pass[ \t\n#]'
273 "linebreak after :"),
338 ),
274 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
339 'omit superfluous pass',
275 r'#.*old-style'),
340 ),
276 (r'class\s[^( \n]+\(\):',
341 (r'[^\n]\Z', "no trailing newline"),
277 "class foo() creates old style object, use class foo(object)",
342 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
278 r'#.*old-style'),
343 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
279 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
344 # "don't use underbars in identifiers"),
280 if k not in ('print', 'exec')),
345 (
281 "Python keyword is not a function"),
346 r'^\s+(self\.)?[A-Za-z][a-z0-9]+[A-Z]\w* = ',
282 # (r'class\s[A-Z][^\(]*\((?!Exception)',
347 "don't use camelcase in identifiers",
283 # "don't capitalize non-exception classes"),
348 r'#.*camelcase-required',
284 # (r'in range\(', "use xrange"),
349 ),
285 # (r'^\s*print\s+', "avoid using print in core and extensions"),
350 (
286 (r'[\x80-\xff]', "non-ASCII character literal"),
351 r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
287 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
352 "linebreak after :",
288 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
353 ),
289 # (r'\s\s=', "gratuitous whitespace before ="),
354 (
290 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
355 r'class\s[^( \n]+:',
291 "missing whitespace around operator"),
356 "old-style class, use class foo(object)",
292 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
357 r'#.*old-style',
293 "missing whitespace around operator"),
358 ),
294 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
359 (
295 "missing whitespace around operator"),
360 r'class\s[^( \n]+\(\):',
296 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
361 "class foo() creates old style object, use class foo(object)",
297 "wrong whitespace around ="),
362 r'#.*old-style',
298 (r'\([^()]*( =[^=]|[^<>!=]= )',
363 ),
299 "no whitespace around = for named parameters"),
364 (
300 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
365 r'\b(%s)\('
301 "don't use old-style two-argument raise, use Exception(message)"),
366 % '|'.join(k for k in keyword.kwlist if k not in ('print', 'exec')),
302 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
367 "Python keyword is not a function",
303 (r' [=!]=\s+(True|False|None)',
368 ),
304 "comparison with singleton, use 'is' or 'is not' instead"),
369 # (r'class\s[A-Z][^\(]*\((?!Exception)',
305 (r'^\s*(while|if) [01]:',
370 # "don't capitalize non-exception classes"),
306 "use True/False for constant Boolean expression"),
371 # (r'in range\(', "use xrange"),
307 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
372 # (r'^\s*print\s+', "avoid using print in core and extensions"),
308 (r'(?:(?<!def)\s+|\()hasattr\(',
373 (r'[\x80-\xff]', "non-ASCII character literal"),
309 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
374 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
310 'instead', r'#.*hasattr-py3-only'),
375 (
311 (r'opener\([^)]*\).read\(',
376 r'([\(\[][ \t]\S)|(\S[ \t][\)\]])',
312 "use opener.read() instead"),
377 "gratuitous whitespace in () or []",
313 (r'opener\([^)]*\).write\(',
378 ),
314 "use opener.write() instead"),
379 # (r'\s\s=', "gratuitous whitespace before ="),
315 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
380 (
316 (r'\.debug\(\_', "don't mark debug messages for translation"),
381 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
317 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
382 "missing whitespace around operator",
318 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
383 ),
319 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
384 (
320 'legacy exception syntax; use "as" instead of ","'),
385 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
321 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
386 "missing whitespace around operator",
322 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
387 ),
323 (r'os\.path\.join\(.*, *(""|\'\')\)',
388 (
324 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
389 r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
325 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
390 "missing whitespace around operator",
326 # XXX only catch mutable arguments on the first line of the definition
391 ),
327 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
392 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]', "wrong whitespace around ="),
328 (r'\butil\.Abort\b', "directly use error.Abort"),
393 (
329 (r'^@(\w*\.)?cachefunc', "module-level @cachefunc is risky, please avoid"),
394 r'\([^()]*( =[^=]|[^<>!=]= )',
330 (r'^import Queue', "don't use Queue, use pycompat.queue.Queue + "
395 "no whitespace around = for named parameters",
331 "pycompat.queue.Empty"),
396 ),
332 (r'^import cStringIO', "don't use cStringIO.StringIO, use util.stringio"),
397 (
333 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
398 r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
334 (r'^import SocketServer', "don't use SockerServer, use util.socketserver"),
399 "don't use old-style two-argument raise, use Exception(message)",
335 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
400 ),
336 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
401 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
337 (r'^import cPickle', "don't use cPickle, use util.pickle"),
402 (
338 (r'^import pickle', "don't use pickle, use util.pickle"),
403 r' [=!]=\s+(True|False|None)',
339 (r'^import httplib', "don't use httplib, use util.httplib"),
404 "comparison with singleton, use 'is' or 'is not' instead",
340 (r'^import BaseHTTPServer', "use util.httpserver instead"),
405 ),
341 (r'^(from|import) mercurial\.(cext|pure|cffi)',
406 (
342 "use mercurial.policy.importmod instead"),
407 r'^\s*(while|if) [01]:',
343 (r'\.next\(\)', "don't use .next(), use next(...)"),
408 "use True/False for constant Boolean expression",
344 (r'([a-z]*).revision\(\1\.node\(',
409 ),
345 "don't convert rev to node before passing to revision(nodeorrev)"),
410 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
346 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
411 (
347
412 r'(?:(?<!def)\s+|\()hasattr\(',
348 ],
413 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
349 # warnings
414 'instead',
350 [
415 r'#.*hasattr-py3-only',
351 ]
416 ),
417 (r'opener\([^)]*\).read\(', "use opener.read() instead"),
418 (r'opener\([^)]*\).write\(', "use opener.write() instead"),
419 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
420 (r'\.debug\(\_', "don't mark debug messages for translation"),
421 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
422 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
423 (
424 r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
425 'legacy exception syntax; use "as" instead of ","',
426 ),
427 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
428 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
429 (
430 r'os\.path\.join\(.*, *(""|\'\')\)',
431 "use pathutil.normasprefix(path) instead of os.path.join(path, '')",
432 ),
433 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
434 # XXX only catch mutable arguments on the first line of the definition
435 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
436 (r'\butil\.Abort\b', "directly use error.Abort"),
437 (
438 r'^@(\w*\.)?cachefunc',
439 "module-level @cachefunc is risky, please avoid",
440 ),
441 (
442 r'^import Queue',
443 "don't use Queue, use pycompat.queue.Queue + "
444 "pycompat.queue.Empty",
445 ),
446 (
447 r'^import cStringIO',
448 "don't use cStringIO.StringIO, use util.stringio",
449 ),
450 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
451 (
452 r'^import SocketServer',
453 "don't use SockerServer, use util.socketserver",
454 ),
455 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
456 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
457 (r'^import cPickle', "don't use cPickle, use util.pickle"),
458 (r'^import pickle', "don't use pickle, use util.pickle"),
459 (r'^import httplib', "don't use httplib, use util.httplib"),
460 (r'^import BaseHTTPServer', "use util.httpserver instead"),
461 (
462 r'^(from|import) mercurial\.(cext|pure|cffi)',
463 "use mercurial.policy.importmod instead",
464 ),
465 (r'\.next\(\)', "don't use .next(), use next(...)"),
466 (
467 r'([a-z]*).revision\(\1\.node\(',
468 "don't convert rev to node before passing to revision(nodeorrev)",
469 ),
470 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
471 ],
472 # warnings
473 [],
352 ]
474 ]
353
475
354 # patterns to check normal *.py files
476 # patterns to check normal *.py files
355 pypats = [
477 pypats = [
356 [
478 [
357 # Ideally, these should be placed in "commonpypats" for
479 # Ideally, these should be placed in "commonpypats" for
358 # consistency of coding rules in Mercurial source tree.
480 # consistency of coding rules in Mercurial source tree.
359 # But on the other hand, these are not so seriously required for
481 # But on the other hand, these are not so seriously required for
360 # python code fragments embedded in test scripts. Fixing test
482 # python code fragments embedded in test scripts. Fixing test
361 # scripts for these patterns requires many changes, and has less
483 # scripts for these patterns requires many changes, and has less
362 # profit than effort.
484 # profit than effort.
363 (r'raise Exception', "don't raise generic exceptions"),
485 (r'raise Exception', "don't raise generic exceptions"),
364 (r'[\s\(](open|file)\([^)]*\)\.read\(',
486 (r'[\s\(](open|file)\([^)]*\)\.read\(', "use util.readfile() instead"),
365 "use util.readfile() instead"),
487 (
366 (r'[\s\(](open|file)\([^)]*\)\.write\(',
488 r'[\s\(](open|file)\([^)]*\)\.write\(',
367 "use util.writefile() instead"),
489 "use util.writefile() instead",
368 (r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
490 ),
369 "always assign an opened file to a variable, and close it afterwards"),
491 (
370 (r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
492 r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
371 "always assign an opened file to a variable, and close it afterwards"),
493 "always assign an opened file to a variable, and close it afterwards",
372 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
494 ),
373 (r'^import atexit', "don't use atexit, use ui.atexit"),
495 (
374
496 r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
375 # rules depending on implementation of repquote()
497 "always assign an opened file to a variable, and close it afterwards",
376 (r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
498 ),
377 'string join across lines with no space'),
499 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
378 (r'''(?x)ui\.(status|progress|write|note|warn)\(
500 (r'^import atexit', "don't use atexit, use ui.atexit"),
501 # rules depending on implementation of repquote()
502 (
503 r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
504 'string join across lines with no space',
505 ),
506 (
507 r'''(?x)ui\.(status|progress|write|note|warn)\(
379 [ \t\n#]*
508 [ \t\n#]*
380 (?# any strings/comments might precede a string, which
509 (?# any strings/comments might precede a string, which
381 # contains translatable message)
510 # contains translatable message)
@@ -389,51 +518,55 b' pypats = ['
389 (?# this regexp can't use [^...] style,
518 (?# this regexp can't use [^...] style,
390 # because _preparepats forcibly adds "\n" into [^...],
519 # because _preparepats forcibly adds "\n" into [^...],
391 # even though this regexp wants match it against "\n")''',
520 # even though this regexp wants match it against "\n")''',
392 "missing _() in ui message (use () to hide false-positives)"),
521 "missing _() in ui message (use () to hide false-positives)",
393 ] + commonpypats[0],
522 ),
394 # warnings
523 ]
395 [
524 + commonpypats[0],
396 # rules depending on implementation of repquote()
525 # warnings
397 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
526 [
398 ] + commonpypats[1]
527 # rules depending on implementation of repquote()
528 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
529 ]
530 + commonpypats[1],
399 ]
531 ]
400
532
401 # patterns to check *.py for embedded ones in test script
533 # patterns to check *.py for embedded ones in test script
402 embeddedpypats = [
534 embeddedpypats = [
403 [
535 [] + commonpypats[0],
404 ] + commonpypats[0],
536 # warnings
405 # warnings
537 [] + commonpypats[1],
406 [
407 ] + commonpypats[1]
408 ]
538 ]
409
539
410 # common filters to convert *.py
540 # common filters to convert *.py
411 commonpyfilters = [
541 commonpyfilters = [
412 (r"""(?msx)(?P<comment>\#.*?$)|
542 (
543 r"""(?msx)(?P<comment>\#.*?$)|
413 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
544 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
414 (?P<text>(([^\\]|\\.)*?))
545 (?P<text>(([^\\]|\\.)*?))
415 (?P=quote))""", reppython),
546 (?P=quote))""",
547 reppython,
548 ),
416 ]
549 ]
417
550
418 # filters to convert normal *.py files
551 # filters to convert normal *.py files
419 pyfilters = [
552 pyfilters = [] + commonpyfilters
420 ] + commonpyfilters
421
553
422 # non-filter patterns
554 # non-filter patterns
423 pynfpats = [
555 pynfpats = [
424 [
556 [
425 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
557 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
426 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
558 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
427 (r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
559 (
428 "use pycompat.isdarwin"),
560 r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
561 "use pycompat.isdarwin",
562 ),
429 ],
563 ],
430 # warnings
564 # warnings
431 [],
565 [],
432 ]
566 ]
433
567
434 # filters to convert *.py for embedded ones in test script
568 # filters to convert *.py for embedded ones in test script
435 embeddedpyfilters = [
569 embeddedpyfilters = [] + commonpyfilters
436 ] + commonpyfilters
437
570
438 # extension non-filter patterns
571 # extension non-filter patterns
439 pyextnfpats = [
572 pyextnfpats = [
@@ -445,41 +578,40 b' pyextnfpats = ['
445 txtfilters = []
578 txtfilters = []
446
579
447 txtpats = [
580 txtpats = [
448 [
581 [
449 (r'\s$', 'trailing whitespace'),
582 (r'\s$', 'trailing whitespace'),
450 ('.. note::[ \n][^\n]', 'add two newlines after note::')
583 ('.. note::[ \n][^\n]', 'add two newlines after note::'),
451 ],
584 ],
452 []
585 [],
453 ]
586 ]
454
587
455 cpats = [
588 cpats = [
456 [
589 [
457 (r'//', "don't use //-style comments"),
590 (r'//', "don't use //-style comments"),
458 (r'\S\t', "don't use tabs except for indent"),
591 (r'\S\t', "don't use tabs except for indent"),
459 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
592 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
460 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
593 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
461 (r'return\(', "return is not a function"),
594 (r'return\(', "return is not a function"),
462 (r' ;', "no space before ;"),
595 (r' ;', "no space before ;"),
463 (r'[^;] \)', "no space before )"),
596 (r'[^;] \)', "no space before )"),
464 (r'[)][{]', "space between ) and {"),
597 (r'[)][{]', "space between ) and {"),
465 (r'\w+\* \w+', "use int *foo, not int* foo"),
598 (r'\w+\* \w+', "use int *foo, not int* foo"),
466 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
599 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
467 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
600 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
468 (r'\w,\w', "missing whitespace after ,"),
601 (r'\w,\w', "missing whitespace after ,"),
469 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
602 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
470 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
603 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
471 (r'^#\s+\w', "use #foo, not # foo"),
604 (r'^#\s+\w', "use #foo, not # foo"),
472 (r'[^\n]\Z', "no trailing newline"),
605 (r'[^\n]\Z', "no trailing newline"),
473 (r'^\s*#import\b', "use only #include in standard C code"),
606 (r'^\s*#import\b', "use only #include in standard C code"),
474 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
607 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
475 (r'strcat\(', "don't use strcat"),
608 (r'strcat\(', "don't use strcat"),
476
609 # rules depending on implementation of repquote()
477 # rules depending on implementation of repquote()
610 ],
478 ],
611 # warnings
479 # warnings
612 [
480 [
613 # rules depending on implementation of repquote()
481 # rules depending on implementation of repquote()
614 ],
482 ]
483 ]
615 ]
484
616
485 cfilters = [
617 cfilters = [
@@ -490,82 +622,109 b' cfilters = ['
490 ]
622 ]
491
623
492 inutilpats = [
624 inutilpats = [
493 [
625 [(r'\bui\.', "don't use ui in util"),],
494 (r'\bui\.', "don't use ui in util"),
626 # warnings
495 ],
627 [],
496 # warnings
497 []
498 ]
628 ]
499
629
500 inrevlogpats = [
630 inrevlogpats = [
501 [
631 [(r'\brepo\.', "don't use repo in revlog"),],
502 (r'\brepo\.', "don't use repo in revlog"),
632 # warnings
503 ],
633 [],
504 # warnings
505 []
506 ]
634 ]
507
635
508 webtemplatefilters = []
636 webtemplatefilters = []
509
637
510 webtemplatepats = [
638 webtemplatepats = [
511 [],
639 [],
512 [
640 [
513 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
641 (
514 'follow desc keyword with either firstline or websub'),
642 r'{desc(\|(?!websub|firstline)[^\|]*)+}',
515 ]
643 'follow desc keyword with either firstline or websub',
644 ),
645 ],
516 ]
646 ]
517
647
518 allfilesfilters = []
648 allfilesfilters = []
519
649
520 allfilespats = [
650 allfilespats = [
521 [
651 [
522 (r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
652 (
523 'use mercurial-scm.org domain URL'),
653 r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
524 (r'mercurial@selenic\.com',
654 'use mercurial-scm.org domain URL',
525 'use mercurial-scm.org domain for mercurial ML address'),
655 ),
526 (r'mercurial-devel@selenic\.com',
656 (
527 'use mercurial-scm.org domain for mercurial-devel ML address'),
657 r'mercurial@selenic\.com',
528 ],
658 'use mercurial-scm.org domain for mercurial ML address',
529 # warnings
659 ),
530 [],
660 (
661 r'mercurial-devel@selenic\.com',
662 'use mercurial-scm.org domain for mercurial-devel ML address',
663 ),
664 ],
665 # warnings
666 [],
531 ]
667 ]
532
668
533 py3pats = [
669 py3pats = [
534 [
670 [
535 (r'os\.environ', "use encoding.environ instead (py3)", r'#.*re-exports'),
671 (
536 (r'os\.name', "use pycompat.osname instead (py3)"),
672 r'os\.environ',
537 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
673 "use encoding.environ instead (py3)",
538 (r'os\.sep', "use pycompat.ossep instead (py3)"),
674 r'#.*re-exports',
539 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
675 ),
540 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
676 (r'os\.name', "use pycompat.osname instead (py3)"),
541 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
677 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
542 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
678 (r'os\.sep', "use pycompat.ossep instead (py3)"),
543 (r'os\.getenv', "use encoding.environ.get instead"),
679 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
544 (r'os\.setenv', "modifying the environ dict is not preferred"),
680 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
545 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
681 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
546 ],
682 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
547 # warnings
683 (r'os\.getenv', "use encoding.environ.get instead"),
548 [],
684 (r'os\.setenv', "modifying the environ dict is not preferred"),
685 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
686 ],
687 # warnings
688 [],
549 ]
689 ]
550
690
551 checks = [
691 checks = [
552 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
692 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
553 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
693 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
554 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
694 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
555 ('python 3', r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
695 (
556 '', pyfilters, py3pats),
696 'python 3',
697 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
698 '',
699 pyfilters,
700 py3pats,
701 ),
557 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
702 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
558 ('c', r'.*\.[ch]$', '', cfilters, cpats),
703 ('c', r'.*\.[ch]$', '', cfilters, cpats),
559 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
704 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
560 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
705 (
561 pyfilters, inrevlogpats),
706 'layering violation repo in revlog',
562 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
707 r'mercurial/revlog\.py',
563 inutilpats),
708 '',
709 pyfilters,
710 inrevlogpats,
711 ),
712 (
713 'layering violation ui in util',
714 r'mercurial/util\.py',
715 '',
716 pyfilters,
717 inutilpats,
718 ),
564 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
719 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
565 ('web template', r'mercurial/templates/.*\.tmpl', '',
720 (
566 webtemplatefilters, webtemplatepats),
721 'web template',
567 ('all except for .po', r'.*(?<!\.po)$', '',
722 r'mercurial/templates/.*\.tmpl',
568 allfilesfilters, allfilespats),
723 '',
724 webtemplatefilters,
725 webtemplatepats,
726 ),
727 ('all except for .po', r'.*(?<!\.po)$', '', allfilesfilters, allfilespats),
569 ]
728 ]
570
729
571 # (desc,
730 # (desc,
@@ -573,10 +732,15 b' checks = ['
573 # list of patterns to convert target files
732 # list of patterns to convert target files
574 # list of patterns to detect errors/warnings)
733 # list of patterns to detect errors/warnings)
575 embeddedchecks = [
734 embeddedchecks = [
576 ('embedded python',
735 (
577 testparseutil.pyembedded, embeddedpyfilters, embeddedpypats)
736 'embedded python',
737 testparseutil.pyembedded,
738 embeddedpyfilters,
739 embeddedpypats,
740 )
578 ]
741 ]
579
742
743
580 def _preparepats():
744 def _preparepats():
581 def preparefailandwarn(failandwarn):
745 def preparefailandwarn(failandwarn):
582 for pats in failandwarn:
746 for pats in failandwarn:
@@ -605,6 +769,7 b' def _preparepats():'
605 filters = c[-2]
769 filters = c[-2]
606 preparefilters(filters)
770 preparefilters(filters)
607
771
772
608 class norepeatlogger(object):
773 class norepeatlogger(object):
609 def __init__(self):
774 def __init__(self):
610 self._lastseen = None
775 self._lastseen = None
@@ -630,8 +795,10 b' class norepeatlogger(object):'
630 self._lastseen = msgid
795 self._lastseen = msgid
631 print(" " + msg)
796 print(" " + msg)
632
797
798
633 _defaultlogger = norepeatlogger()
799 _defaultlogger = norepeatlogger()
634
800
801
635 def getblame(f):
802 def getblame(f):
636 lines = []
803 lines = []
637 for l in os.popen('hg annotate -un %s' % f):
804 for l in os.popen('hg annotate -un %s' % f):
@@ -640,8 +807,16 b' def getblame(f):'
640 lines.append((line[1:-1], user, rev))
807 lines.append((line[1:-1], user, rev))
641 return lines
808 return lines
642
809
643 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
810
644 blame=False, debug=False, lineno=True):
811 def checkfile(
812 f,
813 logfunc=_defaultlogger.log,
814 maxerr=None,
815 warnings=False,
816 blame=False,
817 debug=False,
818 lineno=True,
819 ):
645 """checks style and portability of a given file
820 """checks style and portability of a given file
646
821
647 :f: filepath
822 :f: filepath
@@ -673,8 +848,9 b' def checkfile(f, logfunc=_defaultlogger.'
673 print(name, f)
848 print(name, f)
674 if not (re.match(match, f) or (magic and re.search(magic, pre))):
849 if not (re.match(match, f) or (magic and re.search(magic, pre))):
675 if debug:
850 if debug:
676 print("Skipping %s for %s it doesn't match %s" % (
851 print(
677 name, match, f))
852 "Skipping %s for %s it doesn't match %s" % (name, match, f)
853 )
678 continue
854 continue
679 if "no-" "check-code" in pre:
855 if "no-" "check-code" in pre:
680 # If you're looking at this line, it's because a file has:
856 # If you're looking at this line, it's because a file has:
@@ -684,16 +860,28 b' def checkfile(f, logfunc=_defaultlogger.'
684 # spelling, we write it with the expected spelling from
860 # spelling, we write it with the expected spelling from
685 # tests/test-check-code.t
861 # tests/test-check-code.t
686 print("Skipping %s it has no-che?k-code (glob)" % f)
862 print("Skipping %s it has no-che?k-code (glob)" % f)
687 return "Skip" # skip checking this file
863 return "Skip" # skip checking this file
688
864
689 fc = _checkfiledata(name, f, pre, filters, pats, context,
865 fc = _checkfiledata(
690 logfunc, maxerr, warnings, blame, debug, lineno)
866 name,
867 f,
868 pre,
869 filters,
870 pats,
871 context,
872 logfunc,
873 maxerr,
874 warnings,
875 blame,
876 debug,
877 lineno,
878 )
691 if fc:
879 if fc:
692 result = False
880 result = False
693
881
694 if f.endswith('.t') and "no-" "check-code" not in pre:
882 if f.endswith('.t') and "no-" "check-code" not in pre:
695 if debug:
883 if debug:
696 print("Checking embedded code in %s" % (f))
884 print("Checking embedded code in %s" % f)
697
885
698 prelines = pre.splitlines()
886 prelines = pre.splitlines()
699 embeddederros = []
887 embeddederros = []
@@ -705,9 +893,21 b' def checkfile(f, logfunc=_defaultlogger.'
705
893
706 for found in embedded(f, prelines, embeddederros):
894 for found in embedded(f, prelines, embeddederros):
707 filename, starts, ends, code = found
895 filename, starts, ends, code = found
708 fc = _checkfiledata(name, f, code, filters, pats, context,
896 fc = _checkfiledata(
709 logfunc, curmaxerr, warnings, blame, debug,
897 name,
710 lineno, offset=starts - 1)
898 f,
899 code,
900 filters,
901 pats,
902 context,
903 logfunc,
904 curmaxerr,
905 warnings,
906 blame,
907 debug,
908 lineno,
909 offset=starts - 1,
910 )
711 if fc:
911 if fc:
712 result = False
912 result = False
713 if curmaxerr:
913 if curmaxerr:
@@ -717,9 +917,22 b' def checkfile(f, logfunc=_defaultlogger.'
717
917
718 return result
918 return result
719
919
720 def _checkfiledata(name, f, filedata, filters, pats, context,
920
721 logfunc, maxerr, warnings, blame, debug, lineno,
921 def _checkfiledata(
722 offset=None):
922 name,
923 f,
924 filedata,
925 filters,
926 pats,
927 context,
928 logfunc,
929 maxerr,
930 warnings,
931 blame,
932 debug,
933 lineno,
934 offset=None,
935 ):
723 """Execute actual error check for file data
936 """Execute actual error check for file data
724
937
725 :name: of the checking category
938 :name: of the checking category
@@ -752,10 +965,10 b' def _checkfiledata(name, f, filedata, fi'
752 fc = 0
965 fc = 0
753 pre = post = filedata
966 pre = post = filedata
754
967
755 if True: # TODO: get rid of this redundant 'if' block
968 if True: # TODO: get rid of this redundant 'if' block
756 for p, r in filters:
969 for p, r in filters:
757 post = re.sub(p, r, post)
970 post = re.sub(p, r, post)
758 nerrs = len(pats[0]) # nerr elements are errors
971 nerrs = len(pats[0]) # nerr elements are errors
759 if warnings:
972 if warnings:
760 pats = pats[0] + pats[1]
973 pats = pats[0] + pats[1]
761 else:
974 else:
@@ -794,8 +1007,10 b' def _checkfiledata(name, f, filedata, fi'
794
1007
795 if ignore and re.search(ignore, l, re.MULTILINE):
1008 if ignore and re.search(ignore, l, re.MULTILINE):
796 if debug:
1009 if debug:
797 print("Skipping %s for %s:%s (ignore pattern)" % (
1010 print(
798 name, f, (n + lineoffset)))
1011 "Skipping %s for %s:%s (ignore pattern)"
1012 % (name, f, (n + lineoffset))
1013 )
799 continue
1014 continue
800 bd = ""
1015 bd = ""
801 if blame:
1016 if blame:
@@ -830,21 +1045,38 b' def _checkfiledata(name, f, filedata, fi'
830
1045
831 return fc
1046 return fc
832
1047
1048
833 def main():
1049 def main():
834 parser = optparse.OptionParser("%prog [options] [files | -]")
1050 parser = optparse.OptionParser("%prog [options] [files | -]")
835 parser.add_option("-w", "--warnings", action="store_true",
1051 parser.add_option(
836 help="include warning-level checks")
1052 "-w",
837 parser.add_option("-p", "--per-file", type="int",
1053 "--warnings",
838 help="max warnings per file")
1054 action="store_true",
839 parser.add_option("-b", "--blame", action="store_true",
1055 help="include warning-level checks",
840 help="use annotate to generate blame info")
1056 )
841 parser.add_option("", "--debug", action="store_true",
1057 parser.add_option(
842 help="show debug information")
1058 "-p", "--per-file", type="int", help="max warnings per file"
843 parser.add_option("", "--nolineno", action="store_false",
1059 )
844 dest='lineno', help="don't show line numbers")
1060 parser.add_option(
1061 "-b",
1062 "--blame",
1063 action="store_true",
1064 help="use annotate to generate blame info",
1065 )
1066 parser.add_option(
1067 "", "--debug", action="store_true", help="show debug information"
1068 )
1069 parser.add_option(
1070 "",
1071 "--nolineno",
1072 action="store_false",
1073 dest='lineno',
1074 help="don't show line numbers",
1075 )
845
1076
846 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
1077 parser.set_defaults(
847 lineno=True)
1078 per_file=15, warnings=False, blame=False, debug=False, lineno=True
1079 )
848 (options, args) = parser.parse_args()
1080 (options, args) = parser.parse_args()
849
1081
850 if len(args) == 0:
1082 if len(args) == 0:
@@ -859,11 +1091,17 b' def main():'
859
1091
860 ret = 0
1092 ret = 0
861 for f in check:
1093 for f in check:
862 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
1094 if not checkfile(
863 blame=options.blame, debug=options.debug,
1095 f,
864 lineno=options.lineno):
1096 maxerr=options.per_file,
1097 warnings=options.warnings,
1098 blame=options.blame,
1099 debug=options.debug,
1100 lineno=options.lineno,
1101 ):
865 ret = 1
1102 ret = 1
866 return ret
1103 return ret
867
1104
1105
868 if __name__ == "__main__":
1106 if __name__ == "__main__":
869 sys.exit(main())
1107 sys.exit(main())
@@ -15,7 +15,8 b' foundopts = {}'
15 documented = {}
15 documented = {}
16 allowinconsistent = set()
16 allowinconsistent = set()
17
17
18 configre = re.compile(br'''
18 configre = re.compile(
19 br'''
19 # Function call
20 # Function call
20 ui\.config(?P<ctype>|int|bool|list)\(
21 ui\.config(?P<ctype>|int|bool|list)\(
21 # First argument.
22 # First argument.
@@ -23,9 +24,12 b" configre = re.compile(br'''"
23 # Second argument
24 # Second argument
24 ['"](?P<option>\S+)['"](,\s+
25 ['"](?P<option>\S+)['"](,\s+
25 (?:default=)?(?P<default>\S+?))?
26 (?:default=)?(?P<default>\S+?))?
26 \)''', re.VERBOSE | re.MULTILINE)
27 \)''',
28 re.VERBOSE | re.MULTILINE,
29 )
27
30
28 configwithre = re.compile(br'''
31 configwithre = re.compile(
32 br'''
29 ui\.config(?P<ctype>with)\(
33 ui\.config(?P<ctype>with)\(
30 # First argument is callback function. This doesn't parse robustly
34 # First argument is callback function. This doesn't parse robustly
31 # if it is e.g. a function call.
35 # if it is e.g. a function call.
@@ -33,23 +37,32 b" configwithre = re.compile(br'''"
33 ['"](?P<section>\S+)['"],\s*
37 ['"](?P<section>\S+)['"],\s*
34 ['"](?P<option>\S+)['"](,\s+
38 ['"](?P<option>\S+)['"](,\s+
35 (?:default=)?(?P<default>\S+?))?
39 (?:default=)?(?P<default>\S+?))?
36 \)''', re.VERBOSE | re.MULTILINE)
40 \)''',
41 re.VERBOSE | re.MULTILINE,
42 )
37
43
38 configpartialre = (br"""ui\.config""")
44 configpartialre = br"""ui\.config"""
39
45
40 ignorere = re.compile(br'''
46 ignorere = re.compile(
47 br'''
41 \#\s(?P<reason>internal|experimental|deprecated|developer|inconsistent)\s
48 \#\s(?P<reason>internal|experimental|deprecated|developer|inconsistent)\s
42 config:\s(?P<config>\S+\.\S+)$
49 config:\s(?P<config>\S+\.\S+)$
43 ''', re.VERBOSE | re.MULTILINE)
50 ''',
51 re.VERBOSE | re.MULTILINE,
52 )
44
53
45 if sys.version_info[0] > 2:
54 if sys.version_info[0] > 2:
55
46 def mkstr(b):
56 def mkstr(b):
47 if isinstance(b, str):
57 if isinstance(b, str):
48 return b
58 return b
49 return b.decode('utf8')
59 return b.decode('utf8')
60
61
50 else:
62 else:
51 mkstr = lambda x: x
63 mkstr = lambda x: x
52
64
65
53 def main(args):
66 def main(args):
54 for f in args:
67 for f in args:
55 sect = b''
68 sect = b''
@@ -115,18 +128,32 b' def main(args):'
115 name = m.group('section') + b"." + m.group('option')
128 name = m.group('section') + b"." + m.group('option')
116 default = m.group('default')
129 default = m.group('default')
117 if default in (
130 if default in (
118 None, b'False', b'None', b'0', b'[]', b'""', b"''"):
131 None,
132 b'False',
133 b'None',
134 b'0',
135 b'[]',
136 b'""',
137 b"''",
138 ):
119 default = b''
139 default = b''
120 if re.match(b'[a-z.]+$', default):
140 if re.match(b'[a-z.]+$', default):
121 default = b'<variable>'
141 default = b'<variable>'
122 if (name in foundopts and (ctype, default) != foundopts[name]
142 if (
123 and name not in allowinconsistent):
143 name in foundopts
144 and (ctype, default) != foundopts[name]
145 and name not in allowinconsistent
146 ):
124 print(mkstr(l.rstrip()))
147 print(mkstr(l.rstrip()))
125 fctype, fdefault = foundopts[name]
148 fctype, fdefault = foundopts[name]
126 print("conflict on %s: %r != %r" % (
149 print(
127 mkstr(name),
150 "conflict on %s: %r != %r"
128 (mkstr(ctype), mkstr(default)),
151 % (
129 (mkstr(fctype), mkstr(fdefault))))
152 mkstr(name),
153 (mkstr(ctype), mkstr(default)),
154 (mkstr(fctype), mkstr(fdefault)),
155 )
156 )
130 print("at %s:%d:" % (mkstr(f), linenum))
157 print("at %s:%d:" % (mkstr(f), linenum))
131 foundopts[name] = (ctype, default)
158 foundopts[name] = (ctype, default)
132 carryover = b''
159 carryover = b''
@@ -139,9 +166,11 b' def main(args):'
139
166
140 for name in sorted(foundopts):
167 for name in sorted(foundopts):
141 if name not in documented:
168 if name not in documented:
142 if not (name.startswith(b"devel.") or
169 if not (
143 name.startswith(b"experimental.") or
170 name.startswith(b"devel.")
144 name.startswith(b"debug.")):
171 or name.startswith(b"experimental.")
172 or name.startswith(b"debug.")
173 ):
145 ctype, default = foundopts[name]
174 ctype, default = foundopts[name]
146 if default:
175 if default:
147 if isinstance(default, bytes):
176 if isinstance(default, bytes):
@@ -149,8 +178,11 b' def main(args):'
149 default = ' [%s]' % default
178 default = ' [%s]' % default
150 elif isinstance(default, bytes):
179 elif isinstance(default, bytes):
151 default = mkstr(default)
180 default = mkstr(default)
152 print("undocumented: %s (%s)%s" % (
181 print(
153 mkstr(name), mkstr(ctype), default))
182 "undocumented: %s (%s)%s"
183 % (mkstr(name), mkstr(ctype), default)
184 )
185
154
186
155 if __name__ == "__main__":
187 if __name__ == "__main__":
156 if len(sys.argv) > 1:
188 if len(sys.argv) > 1:
@@ -16,6 +16,7 b' import sys'
16 import traceback
16 import traceback
17 import warnings
17 import warnings
18
18
19
19 def check_compat_py2(f):
20 def check_compat_py2(f):
20 """Check Python 3 compatibility for a file with Python 2"""
21 """Check Python 3 compatibility for a file with Python 2"""
21 with open(f, 'rb') as fh:
22 with open(f, 'rb') as fh:
@@ -40,6 +41,7 b' def check_compat_py2(f):'
40 if haveprint and 'print_function' not in futures:
41 if haveprint and 'print_function' not in futures:
41 print('%s requires print_function' % f)
42 print('%s requires print_function' % f)
42
43
44
43 def check_compat_py3(f):
45 def check_compat_py3(f):
44 """Check Python 3 compatibility of a file with Python 3."""
46 """Check Python 3 compatibility of a file with Python 3."""
45 with open(f, 'rb') as fh:
47 with open(f, 'rb') as fh:
@@ -54,8 +56,9 b' def check_compat_py3(f):'
54 # Try to import the module.
56 # Try to import the module.
55 # For now we only support modules in packages because figuring out module
57 # For now we only support modules in packages because figuring out module
56 # paths for things not in a package can be confusing.
58 # paths for things not in a package can be confusing.
57 if (f.startswith(('hgdemandimport/', 'hgext/', 'mercurial/'))
59 if f.startswith(
58 and not f.endswith('__init__.py')):
60 ('hgdemandimport/', 'hgext/', 'mercurial/')
61 ) and not f.endswith('__init__.py'):
59 assert f.endswith('.py')
62 assert f.endswith('.py')
60 name = f.replace('/', '.')[:-3]
63 name = f.replace('/', '.')[:-3]
61 try:
64 try:
@@ -79,11 +82,16 b' def check_compat_py3(f):'
79
82
80 if frame.filename:
83 if frame.filename:
81 filename = os.path.basename(frame.filename)
84 filename = os.path.basename(frame.filename)
82 print('%s: error importing: <%s> %s (error at %s:%d)' % (
85 print(
83 f, type(e).__name__, e, filename, frame.lineno))
86 '%s: error importing: <%s> %s (error at %s:%d)'
87 % (f, type(e).__name__, e, filename, frame.lineno)
88 )
84 else:
89 else:
85 print('%s: error importing module: <%s> %s (line %d)' % (
90 print(
86 f, type(e).__name__, e, frame.lineno))
91 '%s: error importing module: <%s> %s (line %d)'
92 % (f, type(e).__name__, e, frame.lineno)
93 )
94
87
95
88 if __name__ == '__main__':
96 if __name__ == '__main__':
89 if sys.version_info[0] == 2:
97 if sys.version_info[0] == 2:
@@ -96,7 +104,10 b" if __name__ == '__main__':"
96 fn(f)
104 fn(f)
97
105
98 for w in warns:
106 for w in warns:
99 print(warnings.formatwarning(w.message, w.category,
107 print(
100 w.filename, w.lineno).rstrip())
108 warnings.formatwarning(
109 w.message, w.category, w.filename, w.lineno
110 ).rstrip()
111 )
101
112
102 sys.exit(0)
113 sys.exit(0)
@@ -23,6 +23,7 b" if sys.argv[1] == '-':"
23 else:
23 else:
24 log = open(sys.argv[1], 'a')
24 log = open(sys.argv[1], 'a')
25
25
26
26 def read(size):
27 def read(size):
27 data = sys.stdin.read(size)
28 data = sys.stdin.read(size)
28 if not data:
29 if not data:
@@ -31,6 +32,7 b' def read(size):'
31 sys.stdout.flush()
32 sys.stdout.flush()
32 return data
33 return data
33
34
35
34 try:
36 try:
35 while True:
37 while True:
36 header = read(outputfmtsize)
38 header = read(outputfmtsize)
@@ -14,6 +14,7 b' from mercurial import ('
14 cmdtable = {}
14 cmdtable = {}
15 command = registrar.command(cmdtable)
15 command = registrar.command(cmdtable)
16
16
17
17 def pdb(ui, repo, msg, **opts):
18 def pdb(ui, repo, msg, **opts):
18 objects = {
19 objects = {
19 'mercurial': mercurial,
20 'mercurial': mercurial,
@@ -24,25 +25,25 b' def pdb(ui, repo, msg, **opts):'
24
25
25 code.interact(msg, local=objects)
26 code.interact(msg, local=objects)
26
27
28
27 def ipdb(ui, repo, msg, **opts):
29 def ipdb(ui, repo, msg, **opts):
28 import IPython
30 import IPython
29
31
30 cl = repo.changelog
32 cl = repo.changelog
31 mf = repo.manifestlog
33 mf = repo.manifestlog
32 cl, mf # use variables to appease pyflakes
34 cl, mf # use variables to appease pyflakes
33
35
34 IPython.embed()
36 IPython.embed()
35
37
38
36 @command(b'debugshell|dbsh', [])
39 @command(b'debugshell|dbsh', [])
37 def debugshell(ui, repo, **opts):
40 def debugshell(ui, repo, **opts):
38 bannermsg = ("loaded repo : %s\n"
41 bannermsg = "loaded repo : %s\n" "using source: %s" % (
39 "using source: %s" % (pycompat.sysstr(repo.root),
42 pycompat.sysstr(repo.root),
40 mercurial.__path__[0]))
43 mercurial.__path__[0],
44 )
41
45
42 pdbmap = {
46 pdbmap = {'pdb': 'code', 'ipdb': 'IPython'}
43 'pdb' : 'code',
44 'ipdb' : 'IPython'
45 }
46
47
47 debugger = ui.config(b"ui", b"debugger")
48 debugger = ui.config(b"ui", b"debugger")
48 if not debugger:
49 if not debugger:
@@ -55,8 +56,10 b' def debugshell(ui, repo, **opts):'
55 with demandimport.deactivated():
56 with demandimport.deactivated():
56 __import__(pdbmap[debugger])
57 __import__(pdbmap[debugger])
57 except ImportError:
58 except ImportError:
58 ui.warn((b"%s debugger specified but %s module was not found\n")
59 ui.warn(
59 % (debugger, pdbmap[debugger]))
60 b"%s debugger specified but %s module was not found\n"
61 % (debugger, pdbmap[debugger])
62 )
60 debugger = b'pdb'
63 debugger = b'pdb'
61
64
62 getattr(sys.modules[__name__], debugger)(ui, repo, bannermsg, **opts)
65 getattr(sys.modules[__name__], debugger)(ui, repo, bannermsg, **opts)
@@ -13,6 +13,7 b' from mercurial import ('
13 extensions,
13 extensions,
14 )
14 )
15
15
16
16 def nonnormalentries(dmap):
17 def nonnormalentries(dmap):
17 """Compute nonnormal entries from dirstate's dmap"""
18 """Compute nonnormal entries from dirstate's dmap"""
18 res = set()
19 res = set()
@@ -21,6 +22,7 b' def nonnormalentries(dmap):'
21 res.add(f)
22 res.add(f)
22 return res
23 return res
23
24
25
24 def checkconsistency(ui, orig, dmap, _nonnormalset, label):
26 def checkconsistency(ui, orig, dmap, _nonnormalset, label):
25 """Compute nonnormalset from dmap, check that it matches _nonnormalset"""
27 """Compute nonnormalset from dmap, check that it matches _nonnormalset"""
26 nonnormalcomputedmap = nonnormalentries(dmap)
28 nonnormalcomputedmap = nonnormalentries(dmap)
@@ -30,15 +32,19 b' def checkconsistency(ui, orig, dmap, _no'
30 ui.develwarn(b"[nonnormalset] %s\n" % _nonnormalset, config=b'dirstate')
32 ui.develwarn(b"[nonnormalset] %s\n" % _nonnormalset, config=b'dirstate')
31 ui.develwarn(b"[map] %s\n" % nonnormalcomputedmap, config=b'dirstate')
33 ui.develwarn(b"[map] %s\n" % nonnormalcomputedmap, config=b'dirstate')
32
34
35
33 def _checkdirstate(orig, self, arg):
36 def _checkdirstate(orig, self, arg):
34 """Check nonnormal set consistency before and after the call to orig"""
37 """Check nonnormal set consistency before and after the call to orig"""
35 checkconsistency(self._ui, orig, self._map, self._map.nonnormalset,
38 checkconsistency(
36 b"before")
39 self._ui, orig, self._map, self._map.nonnormalset, b"before"
40 )
37 r = orig(self, arg)
41 r = orig(self, arg)
38 checkconsistency(self._ui, orig, self._map, self._map.nonnormalset,
42 checkconsistency(
39 b"after")
43 self._ui, orig, self._map, self._map.nonnormalset, b"after"
44 )
40 return r
45 return r
41
46
47
42 def extsetup(ui):
48 def extsetup(ui):
43 """Wrap functions modifying dirstate to check nonnormalset consistency"""
49 """Wrap functions modifying dirstate to check nonnormalset consistency"""
44 dirstatecl = dirstate.dirstate
50 dirstatecl = dirstate.dirstate
@@ -8,8 +8,7 b' ap = argparse.ArgumentParser()'
8 ap.add_argument("out", metavar="some.zip", type=str, nargs=1)
8 ap.add_argument("out", metavar="some.zip", type=str, nargs=1)
9 args = ap.parse_args()
9 args = ap.parse_args()
10
10
11 reporoot = os.path.normpath(os.path.join(os.path.dirname(__file__),
11 reporoot = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..'))
12 '..', '..'))
13 dirstate = os.path.join(reporoot, '.hg', 'dirstate')
12 dirstate = os.path.join(reporoot, '.hg', 'dirstate')
14
13
15 with zipfile.ZipFile(args.out[0], "w", zipfile.ZIP_STORED) as zf:
14 with zipfile.ZipFile(args.out[0], "w", zipfile.ZIP_STORED) as zf:
@@ -33,4 +33,6 b' with zipfile.ZipFile(args.out[0], "w", z'
33 'nhistedituserAugie Fackler <raf@durin42.com>\x00\x00\x00yA\xd7\x02'
33 'nhistedituserAugie Fackler <raf@durin42.com>\x00\x00\x00yA\xd7\x02'
34 'MtA\xd4\xe1\x01,\x00\x00\x01\x03\x03"\xa5\xcb\x86\xb6\xf4\xbaO\xa0'
34 'MtA\xd4\xe1\x01,\x00\x00\x01\x03\x03"\xa5\xcb\x86\xb6\xf4\xbaO\xa0'
35 'sH\xe7?\xcb\x9b\xc2n\xcfI\x9e\x14\xf0D\xf0!\x18DN\xcd\x97\x016\xa5'
35 'sH\xe7?\xcb\x9b\xc2n\xcfI\x9e\x14\xf0D\xf0!\x18DN\xcd\x97\x016\xa5'
36 '\xef\xa06\xcb\x884\x8a\x03\x01\t\x08\x04\x1fef14operationhisted'))
36 '\xef\xa06\xcb\x884\x8a\x03\x01\t\x08\x04\x1fef14operationhisted'
37 ),
38 )
@@ -8,8 +8,9 b' ap.add_argument("out", metavar="some.zip'
8 args = ap.parse_args()
8 args = ap.parse_args()
9
9
10 with zipfile.ZipFile(args.out[0], "w", zipfile.ZIP_STORED) as zf:
10 with zipfile.ZipFile(args.out[0], "w", zipfile.ZIP_STORED) as zf:
11 zf.writestr("manifest_zero",
11 zf.writestr(
12 '''PKG-INFO\09b3ed8f2b81095a13064402e930565f083346e9a
12 "manifest_zero",
13 '''PKG-INFO\09b3ed8f2b81095a13064402e930565f083346e9a
13 README\080b6e76643dcb44d4bc729e932fc464b3e36dbe3
14 README\080b6e76643dcb44d4bc729e932fc464b3e36dbe3
14 hg\0b6444347c629cc058d478023905cfb83b7f5bb9d
15 hg\0b6444347c629cc058d478023905cfb83b7f5bb9d
15 mercurial/__init__.py\0b80de5d138758541c5f05265ad144ab9fa86d1db
16 mercurial/__init__.py\0b80de5d138758541c5f05265ad144ab9fa86d1db
@@ -22,9 +23,11 b' mercurial/transaction.py\\09d180df101dc14'
22 notes.txt\0703afcec5edb749cf5cec67831f554d6da13f2fb
23 notes.txt\0703afcec5edb749cf5cec67831f554d6da13f2fb
23 setup.py\0ccf3f6daf0f13101ca73631f7a1769e328b472c9
24 setup.py\0ccf3f6daf0f13101ca73631f7a1769e328b472c9
24 tkmerge\03c922edb43a9c143682f7bc7b00f98b3c756ebe7
25 tkmerge\03c922edb43a9c143682f7bc7b00f98b3c756ebe7
25 ''')
26 ''',
26 zf.writestr("badmanifest_shorthashes",
27 )
27 "narf\0aa\nnarf2\0aaa\n")
28 zf.writestr("badmanifest_shorthashes", "narf\0aa\nnarf2\0aaa\n")
28 zf.writestr("badmanifest_nonull",
29 zf.writestr(
29 "narf\0cccccccccccccccccccccccccccccccccccccccc\n"
30 "badmanifest_nonull",
30 "narf2aaaaaaaaaaaaaaaaaaaa\n")
31 "narf\0cccccccccccccccccccccccccccccccccccccccc\n"
32 "narf2aaaaaaaaaaaaaaaaaaaa\n",
33 )
@@ -13,6 +13,7 b' ap = argparse.ArgumentParser()'
13 ap.add_argument("out", metavar="some.zip", type=str, nargs=1)
13 ap.add_argument("out", metavar="some.zip", type=str, nargs=1)
14 args = ap.parse_args()
14 args = ap.parse_args()
15
15
16
16 class deltafrag(object):
17 class deltafrag(object):
17 def __init__(self, start, end, data):
18 def __init__(self, start, end, data):
18 self.start = start
19 self.start = start
@@ -20,8 +21,11 b' class deltafrag(object):'
20 self.data = data
21 self.data = data
21
22
22 def __str__(self):
23 def __str__(self):
23 return struct.pack(
24 return (
24 ">lll", self.start, self.end, len(self.data)) + self.data
25 struct.pack(">lll", self.start, self.end, len(self.data))
26 + self.data
27 )
28
25
29
26 class delta(object):
30 class delta(object):
27 def __init__(self, frags):
31 def __init__(self, frags):
@@ -30,8 +34,8 b' class delta(object):'
30 def __str__(self):
34 def __str__(self):
31 return ''.join(str(f) for f in self.frags)
35 return ''.join(str(f) for f in self.frags)
32
36
37
33 class corpus(object):
38 class corpus(object):
34
35 def __init__(self, base, deltas):
39 def __init__(self, base, deltas):
36 self.base = base
40 self.base = base
37 self.deltas = deltas
41 self.deltas = deltas
@@ -49,19 +53,19 b' class corpus(object):'
49 )
53 )
50 return "".join(parts)
54 return "".join(parts)
51
55
56
52 with zipfile.ZipFile(args.out[0], "w", zipfile.ZIP_STORED) as zf:
57 with zipfile.ZipFile(args.out[0], "w", zipfile.ZIP_STORED) as zf:
53 # Manually constructed entries
58 # Manually constructed entries
54 zf.writestr(
59 zf.writestr(
55 "one_delta_applies",
60 "one_delta_applies", str(corpus('a', [delta([deltafrag(0, 1, 'b')])]))
56 str(corpus('a', [delta([deltafrag(0, 1, 'b')])]))
57 )
61 )
58 zf.writestr(
62 zf.writestr(
59 "one_delta_starts_late",
63 "one_delta_starts_late",
60 str(corpus('a', [delta([deltafrag(3, 1, 'b')])]))
64 str(corpus('a', [delta([deltafrag(3, 1, 'b')])])),
61 )
65 )
62 zf.writestr(
66 zf.writestr(
63 "one_delta_ends_late",
67 "one_delta_ends_late",
64 str(corpus('a', [delta([deltafrag(0, 20, 'b')])]))
68 str(corpus('a', [delta([deltafrag(0, 20, 'b')])])),
65 )
69 )
66
70
67 try:
71 try:
@@ -70,9 +74,8 b' with zipfile.ZipFile(args.out[0], "w", z'
70 fl = r.file('mercurial/manifest.py')
74 fl = r.file('mercurial/manifest.py')
71 rl = getattr(fl, '_revlog', fl)
75 rl = getattr(fl, '_revlog', fl)
72 bins = rl._chunks(rl._deltachain(10)[0])
76 bins = rl._chunks(rl._deltachain(10)[0])
73 zf.writestr('manifest_py_rev_10',
77 zf.writestr('manifest_py_rev_10', str(corpus(bins[0], bins[1:])))
74 str(corpus(bins[0], bins[1:])))
78 except: # skip this, so no re-raises
75 except: # skip this, so no re-raises
76 print('skipping seed file from repo data')
79 print('skipping seed file from repo data')
77 # Automatically discovered by running the fuzzer
80 # Automatically discovered by running the fuzzer
78 zf.writestr(
81 zf.writestr(
@@ -81,7 +84,8 b' with zipfile.ZipFile(args.out[0], "w", z'
81 # https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=8876
84 # https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=8876
82 zf.writestr(
85 zf.writestr(
83 "mpatch_ossfuzz_getbe32_ubsan",
86 "mpatch_ossfuzz_getbe32_ubsan",
84 "\x02\x00\x00\x00\x0c \xff\xff\xff\xff ")
87 "\x02\x00\x00\x00\x0c \xff\xff\xff\xff ",
88 )
85 zf.writestr(
89 zf.writestr(
86 "mpatch_apply_over_memcpy",
90 "mpatch_apply_over_memcpy",
87 '\x13\x01\x00\x05\xd0\x00\x00\x00\x00\x00\x00\x00\x00\n \x00\x00\x00'
91 '\x13\x01\x00\x05\xd0\x00\x00\x00\x00\x00\x00\x00\x00\n \x00\x00\x00'
@@ -342,4 +346,5 b' with zipfile.ZipFile(args.out[0], "w", z'
342 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
346 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
343 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00se\x00\x00'
347 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00se\x00\x00'
344 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
348 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
345 '\x00\x00\x00\x00')
349 '\x00\x00\x00\x00',
350 )
@@ -8,13 +8,13 b' ap = argparse.ArgumentParser()'
8 ap.add_argument("out", metavar="some.zip", type=str, nargs=1)
8 ap.add_argument("out", metavar="some.zip", type=str, nargs=1)
9 args = ap.parse_args()
9 args = ap.parse_args()
10
10
11 reporoot = os.path.normpath(os.path.join(os.path.dirname(__file__),
11 reporoot = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..'))
12 '..', '..'))
13 # typically a standalone index
12 # typically a standalone index
14 changelog = os.path.join(reporoot, '.hg', 'store', '00changelog.i')
13 changelog = os.path.join(reporoot, '.hg', 'store', '00changelog.i')
15 # an inline revlog with only a few revisions
14 # an inline revlog with only a few revisions
16 contributing = os.path.join(
15 contributing = os.path.join(
17 reporoot, '.hg', 'store', 'data', 'contrib', 'fuzz', 'mpatch.cc.i')
16 reporoot, '.hg', 'store', 'data', 'contrib', 'fuzz', 'mpatch.cc.i'
17 )
18
18
19 print(changelog, os.path.exists(changelog))
19 print(changelog, os.path.exists(changelog))
20 print(contributing, os.path.exists(contributing))
20 print(contributing, os.path.exists(contributing))
@@ -7,21 +7,25 b' import subprocess'
7 import sys
7 import sys
8
8
9 # Always load hg libraries from the hg we can find on $PATH.
9 # Always load hg libraries from the hg we can find on $PATH.
10 hglib = subprocess.check_output(
10 hglib = subprocess.check_output(['hg', 'debuginstall', '-T', '{hgmodules}'])
11 ['hg', 'debuginstall', '-T', '{hgmodules}'])
12 sys.path.insert(0, os.path.dirname(hglib))
11 sys.path.insert(0, os.path.dirname(hglib))
13
12
14 from mercurial import util
13 from mercurial import util
15
14
16 ap = argparse.ArgumentParser()
15 ap = argparse.ArgumentParser()
17 ap.add_argument('--paranoid',
16 ap.add_argument(
18 action='store_true',
17 '--paranoid',
19 help=("Be paranoid about how version numbers compare and "
18 action='store_true',
20 "produce something that's more likely to sort "
19 help=(
21 "reasonably."))
20 "Be paranoid about how version numbers compare and "
21 "produce something that's more likely to sort "
22 "reasonably."
23 ),
24 )
22 ap.add_argument('--selftest', action='store_true', help='Run self-tests.')
25 ap.add_argument('--selftest', action='store_true', help='Run self-tests.')
23 ap.add_argument('versionfile', help='Path to a valid mercurial __version__.py')
26 ap.add_argument('versionfile', help='Path to a valid mercurial __version__.py')
24
27
28
25 def paranoidver(ver):
29 def paranoidver(ver):
26 """Given an hg version produce something that distutils can sort.
30 """Given an hg version produce something that distutils can sort.
27
31
@@ -108,22 +112,25 b' def paranoidver(ver):'
108 extra = ''
112 extra = ''
109 return '%d.%d.%d%s' % (major, minor, micro, extra)
113 return '%d.%d.%d%s' % (major, minor, micro, extra)
110
114
115
111 def main(argv):
116 def main(argv):
112 opts = ap.parse_args(argv[1:])
117 opts = ap.parse_args(argv[1:])
113 if opts.selftest:
118 if opts.selftest:
114 import doctest
119 import doctest
120
115 doctest.testmod()
121 doctest.testmod()
116 return
122 return
117 with open(opts.versionfile) as f:
123 with open(opts.versionfile) as f:
118 for l in f:
124 for l in f:
119 if l.startswith('version = b'):
125 if l.startswith('version = b'):
120 # version number is entire line minus the quotes
126 # version number is entire line minus the quotes
121 ver = l[len('version = b') + 1:-2]
127 ver = l[len('version = b') + 1 : -2]
122 break
128 break
123 if opts.paranoid:
129 if opts.paranoid:
124 print(paranoidver(ver))
130 print(paranoidver(ver))
125 else:
131 else:
126 print(ver)
132 print(ver)
127
133
134
128 if __name__ == '__main__':
135 if __name__ == '__main__':
129 main(sys.argv)
136 main(sys.argv)
@@ -16,17 +16,22 b' if sys.version_info[0] >= 3:'
16 stdout = sys.stdout.buffer
16 stdout = sys.stdout.buffer
17 stderr = sys.stderr.buffer
17 stderr = sys.stderr.buffer
18 stringio = io.BytesIO
18 stringio = io.BytesIO
19
19 def bprint(*args):
20 def bprint(*args):
20 # remove b'' as well for ease of test migration
21 # remove b'' as well for ease of test migration
21 pargs = [re.sub(br'''\bb(['"])''', br'\1', b'%s' % a) for a in args]
22 pargs = [re.sub(br'''\bb(['"])''', br'\1', b'%s' % a) for a in args]
22 stdout.write(b' '.join(pargs) + b'\n')
23 stdout.write(b' '.join(pargs) + b'\n')
24
25
23 else:
26 else:
24 import cStringIO
27 import cStringIO
28
25 stdout = sys.stdout
29 stdout = sys.stdout
26 stderr = sys.stderr
30 stderr = sys.stderr
27 stringio = cStringIO.StringIO
31 stringio = cStringIO.StringIO
28 bprint = print
32 bprint = print
29
33
34
30 def connectpipe(path=None, extraargs=()):
35 def connectpipe(path=None, extraargs=()):
31 cmdline = [b'hg', b'serve', b'--cmdserver', b'pipe']
36 cmdline = [b'hg', b'serve', b'--cmdserver', b'pipe']
32 if path:
37 if path:
@@ -38,11 +43,13 b' def connectpipe(path=None, extraargs=())'
38 return cmdline
43 return cmdline
39 return [arg.decode("utf-8") for arg in cmdline]
44 return [arg.decode("utf-8") for arg in cmdline]
40
45
41 server = subprocess.Popen(tonative(cmdline), stdin=subprocess.PIPE,
46 server = subprocess.Popen(
42 stdout=subprocess.PIPE)
47 tonative(cmdline), stdin=subprocess.PIPE, stdout=subprocess.PIPE
48 )
43
49
44 return server
50 return server
45
51
52
46 class unixconnection(object):
53 class unixconnection(object):
47 def __init__(self, sockpath):
54 def __init__(self, sockpath):
48 self.sock = sock = socket.socket(socket.AF_UNIX)
55 self.sock = sock = socket.socket(socket.AF_UNIX)
@@ -55,6 +62,7 b' class unixconnection(object):'
55 self.stdout.close()
62 self.stdout.close()
56 self.sock.close()
63 self.sock.close()
57
64
65
58 class unixserver(object):
66 class unixserver(object):
59 def __init__(self, sockpath, logpath=None, repopath=None):
67 def __init__(self, sockpath, logpath=None, repopath=None):
60 self.sockpath = sockpath
68 self.sockpath = sockpath
@@ -80,11 +88,13 b' class unixserver(object):'
80 os.kill(self.server.pid, signal.SIGTERM)
88 os.kill(self.server.pid, signal.SIGTERM)
81 self.server.wait()
89 self.server.wait()
82
90
91
83 def writeblock(server, data):
92 def writeblock(server, data):
84 server.stdin.write(struct.pack(b'>I', len(data)))
93 server.stdin.write(struct.pack(b'>I', len(data)))
85 server.stdin.write(data)
94 server.stdin.write(data)
86 server.stdin.flush()
95 server.stdin.flush()
87
96
97
88 def readchannel(server):
98 def readchannel(server):
89 data = server.stdout.read(5)
99 data = server.stdout.read(5)
90 if not data:
100 if not data:
@@ -95,11 +105,14 b' def readchannel(server):'
95 else:
105 else:
96 return channel, server.stdout.read(length)
106 return channel, server.stdout.read(length)
97
107
108
98 def sep(text):
109 def sep(text):
99 return text.replace(b'\\', b'/')
110 return text.replace(b'\\', b'/')
100
111
101 def runcommand(server, args, output=stdout, error=stderr, input=None,
112
102 outfilter=lambda x: x):
113 def runcommand(
114 server, args, output=stdout, error=stderr, input=None, outfilter=lambda x: x
115 ):
103 bprint(b'*** runcommand', b' '.join(args))
116 bprint(b'*** runcommand', b' '.join(args))
104 stdout.flush()
117 stdout.flush()
105 server.stdin.write(b'runcommand\n')
118 server.stdin.write(b'runcommand\n')
@@ -123,7 +136,7 b' def runcommand(server, args, output=stdo'
123 elif ch == b'm':
136 elif ch == b'm':
124 bprint(b"message: %r" % data)
137 bprint(b"message: %r" % data)
125 elif ch == b'r':
138 elif ch == b'r':
126 ret, = struct.unpack('>i', data)
139 (ret,) = struct.unpack('>i', data)
127 if ret != 0:
140 if ret != 0:
128 bprint(b' [%d]' % ret)
141 bprint(b' [%d]' % ret)
129 return ret
142 return ret
@@ -132,6 +145,7 b' def runcommand(server, args, output=stdo'
132 if ch.isupper():
145 if ch.isupper():
133 return
146 return
134
147
148
135 def check(func, connect=connectpipe):
149 def check(func, connect=connectpipe):
136 stdout.flush()
150 stdout.flush()
137 server = connect()
151 server = connect()
@@ -141,7 +155,9 b' def check(func, connect=connectpipe):'
141 server.stdin.close()
155 server.stdin.close()
142 server.wait()
156 server.wait()
143
157
158
144 def checkwith(connect=connectpipe, **kwargs):
159 def checkwith(connect=connectpipe, **kwargs):
145 def wrap(func):
160 def wrap(func):
146 return check(func, lambda: connect(**kwargs))
161 return check(func, lambda: connect(**kwargs))
162
147 return wrap
163 return wrap
@@ -13,6 +13,7 b' prints it to ``stderr`` on exit.'
13
13
14 from __future__ import absolute_import
14 from __future__ import absolute_import
15
15
16
16 def memusage(ui):
17 def memusage(ui):
17 """Report memory usage of the current process."""
18 """Report memory usage of the current process."""
18 result = {'peak': 0, 'rss': 0}
19 result = {'peak': 0, 'rss': 0}
@@ -24,8 +25,13 b' def memusage(ui):'
24 key = parts[0][2:-1].lower()
25 key = parts[0][2:-1].lower()
25 if key in result:
26 if key in result:
26 result[key] = int(parts[1])
27 result[key] = int(parts[1])
27 ui.write_err(", ".join(["%s: %.1f MiB" % (k, v / 1024.0)
28 ui.write_err(
28 for k, v in result.iteritems()]) + "\n")
29 ", ".join(
30 ["%s: %.1f MiB" % (k, v / 1024.0) for k, v in result.iteritems()]
31 )
32 + "\n"
33 )
34
29
35
30 def extsetup(ui):
36 def extsetup(ui):
31 ui.atexit(memusage, ui)
37 ui.atexit(memusage, ui)
@@ -98,7 +98,10 b' def secure_download_stream(url, size, sh'
98 length = 0
98 length = 0
99
99
100 with urllib.request.urlopen(url) as fh:
100 with urllib.request.urlopen(url) as fh:
101 if not url.endswith('.gz') and fh.info().get('Content-Encoding') == 'gzip':
101 if (
102 not url.endswith('.gz')
103 and fh.info().get('Content-Encoding') == 'gzip'
104 ):
102 fh = gzip.GzipFile(fileobj=fh)
105 fh = gzip.GzipFile(fileobj=fh)
103
106
104 while True:
107 while True:
@@ -114,12 +117,14 b' def secure_download_stream(url, size, sh'
114 digest = h.hexdigest()
117 digest = h.hexdigest()
115
118
116 if length != size:
119 if length != size:
117 raise IntegrityError('size mismatch on %s: wanted %d; got %d' % (
120 raise IntegrityError(
118 url, size, length))
121 'size mismatch on %s: wanted %d; got %d' % (url, size, length)
122 )
119
123
120 if digest != sha256:
124 if digest != sha256:
121 raise IntegrityError('sha256 mismatch on %s: wanted %s; got %s' % (
125 raise IntegrityError(
122 url, sha256, digest))
126 'sha256 mismatch on %s: wanted %s; got %s' % (url, sha256, digest)
127 )
123
128
124
129
125 def download_to_path(url: str, path: pathlib.Path, size: int, sha256: str):
130 def download_to_path(url: str, path: pathlib.Path, size: int, sha256: str):
@@ -162,12 +167,14 b' def download_to_path(url: str, path: pat'
162 print('successfully downloaded %s' % url)
167 print('successfully downloaded %s' % url)
163
168
164
169
165 def download_entry(name: dict, dest_path: pathlib.Path, local_name=None) -> pathlib.Path:
170 def download_entry(
171 name: dict, dest_path: pathlib.Path, local_name=None
172 ) -> pathlib.Path:
166 entry = DOWNLOADS[name]
173 entry = DOWNLOADS[name]
167
174
168 url = entry['url']
175 url = entry['url']
169
176
170 local_name = local_name or url[url.rindex('/') + 1:]
177 local_name = local_name or url[url.rindex('/') + 1 :]
171
178
172 local_path = dest_path / local_name
179 local_path = dest_path / local_name
173 download_to_path(url, local_path, entry['size'], entry['sha256'])
180 download_to_path(url, local_path, entry['size'], entry['sha256'])
@@ -12,12 +12,8 b' import pathlib'
12 import shutil
12 import shutil
13 import subprocess
13 import subprocess
14
14
15 from .py2exe import (
15 from .py2exe import build_py2exe
16 build_py2exe,
16 from .util import find_vc_runtime_files
17 )
18 from .util import (
19 find_vc_runtime_files,
20 )
21
17
22
18
23 EXTRA_PACKAGES = {
19 EXTRA_PACKAGES = {
@@ -28,9 +24,13 b' EXTRA_PACKAGES = {'
28 }
24 }
29
25
30
26
31 def build(source_dir: pathlib.Path, build_dir: pathlib.Path,
27 def build(
32 python_exe: pathlib.Path, iscc_exe: pathlib.Path,
28 source_dir: pathlib.Path,
33 version=None):
29 build_dir: pathlib.Path,
30 python_exe: pathlib.Path,
31 iscc_exe: pathlib.Path,
32 version=None,
33 ):
34 """Build the Inno installer.
34 """Build the Inno installer.
35
35
36 Build files will be placed in ``build_dir``.
36 Build files will be placed in ``build_dir``.
@@ -44,11 +44,18 b' def build(source_dir: pathlib.Path, buil'
44
44
45 vc_x64 = r'\x64' in os.environ.get('LIB', '')
45 vc_x64 = r'\x64' in os.environ.get('LIB', '')
46
46
47 requirements_txt = (source_dir / 'contrib' / 'packaging' /
47 requirements_txt = (
48 'inno' / 'requirements.txt')
48 source_dir / 'contrib' / 'packaging' / 'inno' / 'requirements.txt'
49 )
49
50
50 build_py2exe(source_dir, build_dir, python_exe, 'inno',
51 build_py2exe(
51 requirements_txt, extra_packages=EXTRA_PACKAGES)
52 source_dir,
53 build_dir,
54 python_exe,
55 'inno',
56 requirements_txt,
57 extra_packages=EXTRA_PACKAGES,
58 )
52
59
53 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
60 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
54 for f in find_vc_runtime_files(vc_x64):
61 for f in find_vc_runtime_files(vc_x64):
@@ -11,9 +11,7 b' import os'
11 import pathlib
11 import pathlib
12 import subprocess
12 import subprocess
13
13
14 from .downloads import (
14 from .downloads import download_entry
15 download_entry,
16 )
17 from .util import (
15 from .util import (
18 extract_tar_to_directory,
16 extract_tar_to_directory,
19 extract_zip_to_directory,
17 extract_zip_to_directory,
@@ -21,12 +19,17 b' from .util import ('
21 )
19 )
22
20
23
21
24 def build_py2exe(source_dir: pathlib.Path, build_dir: pathlib.Path,
22 def build_py2exe(
25 python_exe: pathlib.Path, build_name: str,
23 source_dir: pathlib.Path,
26 venv_requirements_txt: pathlib.Path,
24 build_dir: pathlib.Path,
27 extra_packages=None, extra_excludes=None,
25 python_exe: pathlib.Path,
28 extra_dll_excludes=None,
26 build_name: str,
29 extra_packages_script=None):
27 venv_requirements_txt: pathlib.Path,
28 extra_packages=None,
29 extra_excludes=None,
30 extra_dll_excludes=None,
31 extra_packages_script=None,
32 ):
30 """Build Mercurial with py2exe.
33 """Build Mercurial with py2exe.
31
34
32 Build files will be placed in ``build_dir``.
35 Build files will be placed in ``build_dir``.
@@ -36,9 +39,11 b' def build_py2exe(source_dir: pathlib.Pat'
36 to already be configured with an active toolchain.
39 to already be configured with an active toolchain.
37 """
40 """
38 if 'VCINSTALLDIR' not in os.environ:
41 if 'VCINSTALLDIR' not in os.environ:
39 raise Exception('not running from a Visual C++ build environment; '
42 raise Exception(
40 'execute the "Visual C++ <version> Command Prompt" '
43 'not running from a Visual C++ build environment; '
41 'application shortcut or a vcsvarsall.bat file')
44 'execute the "Visual C++ <version> Command Prompt" '
45 'application shortcut or a vcsvarsall.bat file'
46 )
42
47
43 # Identity x86/x64 and validate the environment matches the Python
48 # Identity x86/x64 and validate the environment matches the Python
44 # architecture.
49 # architecture.
@@ -48,12 +53,16 b' def build_py2exe(source_dir: pathlib.Pat'
48
53
49 if vc_x64:
54 if vc_x64:
50 if py_info['arch'] != '64bit':
55 if py_info['arch'] != '64bit':
51 raise Exception('architecture mismatch: Visual C++ environment '
56 raise Exception(
52 'is configured for 64-bit but Python is 32-bit')
57 'architecture mismatch: Visual C++ environment '
58 'is configured for 64-bit but Python is 32-bit'
59 )
53 else:
60 else:
54 if py_info['arch'] != '32bit':
61 if py_info['arch'] != '32bit':
55 raise Exception('architecture mismatch: Visual C++ environment '
62 raise Exception(
56 'is configured for 32-bit but Python is 64-bit')
63 'architecture mismatch: Visual C++ environment '
64 'is configured for 32-bit but Python is 64-bit'
65 )
57
66
58 if py_info['py3']:
67 if py_info['py3']:
59 raise Exception('Only Python 2 is currently supported')
68 raise Exception('Only Python 2 is currently supported')
@@ -65,11 +74,11 b' def build_py2exe(source_dir: pathlib.Pat'
65 virtualenv_pkg, virtualenv_entry = download_entry('virtualenv', build_dir)
74 virtualenv_pkg, virtualenv_entry = download_entry('virtualenv', build_dir)
66 py2exe_pkg, py2exe_entry = download_entry('py2exe', build_dir)
75 py2exe_pkg, py2exe_entry = download_entry('py2exe', build_dir)
67
76
68 venv_path = build_dir / ('venv-%s-%s' % (build_name,
77 venv_path = build_dir / (
69 'x64' if vc_x64 else 'x86'))
78 'venv-%s-%s' % (build_name, 'x64' if vc_x64 else 'x86')
79 )
70
80
71 gettext_root = build_dir / (
81 gettext_root = build_dir / ('gettext-win-%s' % gettext_entry['version'])
72 'gettext-win-%s' % gettext_entry['version'])
73
82
74 if not gettext_root.exists():
83 if not gettext_root.exists():
75 extract_zip_to_directory(gettext_pkg, gettext_root)
84 extract_zip_to_directory(gettext_pkg, gettext_root)
@@ -77,7 +86,8 b' def build_py2exe(source_dir: pathlib.Pat'
77
86
78 # This assumes Python 2. We don't need virtualenv on Python 3.
87 # This assumes Python 2. We don't need virtualenv on Python 3.
79 virtualenv_src_path = build_dir / (
88 virtualenv_src_path = build_dir / (
80 'virtualenv-%s' % virtualenv_entry['version'])
89 'virtualenv-%s' % virtualenv_entry['version']
90 )
81 virtualenv_py = virtualenv_src_path / 'virtualenv.py'
91 virtualenv_py = virtualenv_src_path / 'virtualenv.py'
82
92
83 if not virtualenv_src_path.exists():
93 if not virtualenv_src_path.exists():
@@ -91,14 +101,15 b' def build_py2exe(source_dir: pathlib.Pat'
91 if not venv_path.exists():
101 if not venv_path.exists():
92 print('creating virtualenv with dependencies')
102 print('creating virtualenv with dependencies')
93 subprocess.run(
103 subprocess.run(
94 [str(python_exe), str(virtualenv_py), str(venv_path)],
104 [str(python_exe), str(virtualenv_py), str(venv_path)], check=True
95 check=True)
105 )
96
106
97 venv_python = venv_path / 'Scripts' / 'python.exe'
107 venv_python = venv_path / 'Scripts' / 'python.exe'
98 venv_pip = venv_path / 'Scripts' / 'pip.exe'
108 venv_pip = venv_path / 'Scripts' / 'pip.exe'
99
109
100 subprocess.run([str(venv_pip), 'install', '-r', str(venv_requirements_txt)],
110 subprocess.run(
101 check=True)
111 [str(venv_pip), 'install', '-r', str(venv_requirements_txt)], check=True
112 )
102
113
103 # Force distutils to use VC++ settings from environment, which was
114 # Force distutils to use VC++ settings from environment, which was
104 # validated above.
115 # validated above.
@@ -107,9 +118,13 b' def build_py2exe(source_dir: pathlib.Pat'
107 env['MSSdk'] = '1'
118 env['MSSdk'] = '1'
108
119
109 if extra_packages_script:
120 if extra_packages_script:
110 more_packages = set(subprocess.check_output(
121 more_packages = set(
111 extra_packages_script,
122 subprocess.check_output(extra_packages_script, cwd=build_dir)
112 cwd=build_dir).split(b'\0')[-1].strip().decode('utf-8').splitlines())
123 .split(b'\0')[-1]
124 .strip()
125 .decode('utf-8')
126 .splitlines()
127 )
113 if more_packages:
128 if more_packages:
114 if not extra_packages:
129 if not extra_packages:
115 extra_packages = more_packages
130 extra_packages = more_packages
@@ -119,32 +134,38 b' def build_py2exe(source_dir: pathlib.Pat'
119 if extra_packages:
134 if extra_packages:
120 env['HG_PY2EXE_EXTRA_PACKAGES'] = ' '.join(sorted(extra_packages))
135 env['HG_PY2EXE_EXTRA_PACKAGES'] = ' '.join(sorted(extra_packages))
121 hgext3rd_extras = sorted(
136 hgext3rd_extras = sorted(
122 e for e in extra_packages if e.startswith('hgext3rd.'))
137 e for e in extra_packages if e.startswith('hgext3rd.')
138 )
123 if hgext3rd_extras:
139 if hgext3rd_extras:
124 env['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'] = ' '.join(hgext3rd_extras)
140 env['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'] = ' '.join(hgext3rd_extras)
125 if extra_excludes:
141 if extra_excludes:
126 env['HG_PY2EXE_EXTRA_EXCLUDES'] = ' '.join(sorted(extra_excludes))
142 env['HG_PY2EXE_EXTRA_EXCLUDES'] = ' '.join(sorted(extra_excludes))
127 if extra_dll_excludes:
143 if extra_dll_excludes:
128 env['HG_PY2EXE_EXTRA_DLL_EXCLUDES'] = ' '.join(
144 env['HG_PY2EXE_EXTRA_DLL_EXCLUDES'] = ' '.join(
129 sorted(extra_dll_excludes))
145 sorted(extra_dll_excludes)
146 )
130
147
131 py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe'
148 py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe'
132 if not py2exe_py_path.exists():
149 if not py2exe_py_path.exists():
133 print('building py2exe')
150 print('building py2exe')
134 subprocess.run([str(venv_python), 'setup.py', 'install'],
151 subprocess.run(
135 cwd=py2exe_source_path,
152 [str(venv_python), 'setup.py', 'install'],
136 env=env,
153 cwd=py2exe_source_path,
137 check=True)
154 env=env,
155 check=True,
156 )
138
157
139 # Register location of msgfmt and other binaries.
158 # Register location of msgfmt and other binaries.
140 env['PATH'] = '%s%s%s' % (
159 env['PATH'] = '%s%s%s' % (
141 env['PATH'], os.pathsep, str(gettext_root / 'bin'))
160 env['PATH'],
161 os.pathsep,
162 str(gettext_root / 'bin'),
163 )
142
164
143 print('building Mercurial')
165 print('building Mercurial')
144 subprocess.run(
166 subprocess.run(
145 [str(venv_python), 'setup.py',
167 [str(venv_python), 'setup.py', 'py2exe', 'build_doc', '--html'],
146 'py2exe',
147 'build_doc', '--html'],
148 cwd=str(source_dir),
168 cwd=str(source_dir),
149 env=env,
169 env=env,
150 check=True)
170 check=True,
171 )
@@ -32,8 +32,11 b' def find_vc_runtime_files(x64=False):'
32
32
33 prefix = 'amd64' if x64 else 'x86'
33 prefix = 'amd64' if x64 else 'x86'
34
34
35 candidates = sorted(p for p in os.listdir(winsxs)
35 candidates = sorted(
36 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix))
36 p
37 for p in os.listdir(winsxs)
38 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix)
39 )
37
40
38 for p in candidates:
41 for p in candidates:
39 print('found candidate VC runtime: %s' % p)
42 print('found candidate VC runtime: %s' % p)
@@ -72,7 +75,7 b' def windows_10_sdk_info():'
72 'version': version,
75 'version': version,
73 'bin_root': bin_version,
76 'bin_root': bin_version,
74 'bin_x86': bin_version / 'x86',
77 'bin_x86': bin_version / 'x86',
75 'bin_x64': bin_version / 'x64'
78 'bin_x64': bin_version / 'x64',
76 }
79 }
77
80
78
81
@@ -89,9 +92,14 b' def find_signtool():'
89 raise Exception('could not find signtool.exe in Windows 10 SDK')
92 raise Exception('could not find signtool.exe in Windows 10 SDK')
90
93
91
94
92 def sign_with_signtool(file_path, description, subject_name=None,
95 def sign_with_signtool(
93 cert_path=None, cert_password=None,
96 file_path,
94 timestamp_url=None):
97 description,
98 subject_name=None,
99 cert_path=None,
100 cert_password=None,
101 timestamp_url=None,
102 ):
95 """Digitally sign a file with signtool.exe.
103 """Digitally sign a file with signtool.exe.
96
104
97 ``file_path`` is file to sign.
105 ``file_path`` is file to sign.
@@ -114,10 +122,13 b' def sign_with_signtool(file_path, descri'
114 cert_password = getpass.getpass('password for %s: ' % cert_path)
122 cert_password = getpass.getpass('password for %s: ' % cert_path)
115
123
116 args = [
124 args = [
117 str(find_signtool()), 'sign',
125 str(find_signtool()),
126 'sign',
118 '/v',
127 '/v',
119 '/fd', 'sha256',
128 '/fd',
120 '/d', description,
129 'sha256',
130 '/d',
131 description,
121 ]
132 ]
122
133
123 if cert_path:
134 if cert_path:
@@ -15,12 +15,8 b' import tempfile'
15 import typing
15 import typing
16 import xml.dom.minidom
16 import xml.dom.minidom
17
17
18 from .downloads import (
18 from .downloads import download_entry
19 download_entry,
19 from .py2exe import build_py2exe
20 )
21 from .py2exe import (
22 build_py2exe,
23 )
24 from .util import (
20 from .util import (
25 extract_zip_to_directory,
21 extract_zip_to_directory,
26 sign_with_signtool,
22 sign_with_signtool,
@@ -84,17 +80,29 b' def normalize_version(version):'
84
80
85 def ensure_vc90_merge_modules(build_dir):
81 def ensure_vc90_merge_modules(build_dir):
86 x86 = (
82 x86 = (
87 download_entry('vc9-crt-x86-msm', build_dir,
83 download_entry(
88 local_name='microsoft.vcxx.crt.x86_msm.msm')[0],
84 'vc9-crt-x86-msm',
89 download_entry('vc9-crt-x86-msm-policy', build_dir,
85 build_dir,
90 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0]
86 local_name='microsoft.vcxx.crt.x86_msm.msm',
87 )[0],
88 download_entry(
89 'vc9-crt-x86-msm-policy',
90 build_dir,
91 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
92 )[0],
91 )
93 )
92
94
93 x64 = (
95 x64 = (
94 download_entry('vc9-crt-x64-msm', build_dir,
96 download_entry(
95 local_name='microsoft.vcxx.crt.x64_msm.msm')[0],
97 'vc9-crt-x64-msm',
96 download_entry('vc9-crt-x64-msm-policy', build_dir,
98 build_dir,
97 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0]
99 local_name='microsoft.vcxx.crt.x64_msm.msm',
100 )[0],
101 download_entry(
102 'vc9-crt-x64-msm-policy',
103 build_dir,
104 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
105 )[0],
98 )
106 )
99 return {
107 return {
100 'x86': x86,
108 'x86': x86,
@@ -116,17 +124,26 b' def run_candle(wix, cwd, wxs, source_dir'
116 subprocess.run(args, cwd=str(cwd), check=True)
124 subprocess.run(args, cwd=str(cwd), check=True)
117
125
118
126
119 def make_post_build_signing_fn(name, subject_name=None, cert_path=None,
127 def make_post_build_signing_fn(
120 cert_password=None, timestamp_url=None):
128 name,
129 subject_name=None,
130 cert_path=None,
131 cert_password=None,
132 timestamp_url=None,
133 ):
121 """Create a callable that will use signtool to sign hg.exe."""
134 """Create a callable that will use signtool to sign hg.exe."""
122
135
123 def post_build_sign(source_dir, build_dir, dist_dir, version):
136 def post_build_sign(source_dir, build_dir, dist_dir, version):
124 description = '%s %s' % (name, version)
137 description = '%s %s' % (name, version)
125
138
126 sign_with_signtool(dist_dir / 'hg.exe', description,
139 sign_with_signtool(
127 subject_name=subject_name, cert_path=cert_path,
140 dist_dir / 'hg.exe',
128 cert_password=cert_password,
141 description,
129 timestamp_url=timestamp_url)
142 subject_name=subject_name,
143 cert_path=cert_path,
144 cert_password=cert_password,
145 timestamp_url=timestamp_url,
146 )
130
147
131 return post_build_sign
148 return post_build_sign
132
149
@@ -155,7 +172,8 b' def make_libraries_xml(wix_dir: pathlib.'
155 # We can't use ElementTree because it doesn't handle the
172 # We can't use ElementTree because it doesn't handle the
156 # <?include ?> directives.
173 # <?include ?> directives.
157 doc = xml.dom.minidom.parseString(
174 doc = xml.dom.minidom.parseString(
158 LIBRARIES_XML.format(wix_dir=str(wix_dir)))
175 LIBRARIES_XML.format(wix_dir=str(wix_dir))
176 )
159
177
160 component = doc.getElementsByTagName('Component')[0]
178 component = doc.getElementsByTagName('Component')[0]
161
179
@@ -177,11 +195,16 b' def make_libraries_xml(wix_dir: pathlib.'
177 return doc.toprettyxml()
195 return doc.toprettyxml()
178
196
179
197
180 def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
198 def build_installer(
181 msi_name='mercurial', version=None, post_build_fn=None,
199 source_dir: pathlib.Path,
182 extra_packages_script=None,
200 python_exe: pathlib.Path,
183 extra_wxs:typing.Optional[typing.Dict[str,str]]=None,
201 msi_name='mercurial',
184 extra_features:typing.Optional[typing.List[str]]=None):
202 version=None,
203 post_build_fn=None,
204 extra_packages_script=None,
205 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
206 extra_features: typing.Optional[typing.List[str]] = None,
207 ):
185 """Build a WiX MSI installer.
208 """Build a WiX MSI installer.
186
209
187 ``source_dir`` is the path to the Mercurial source tree to use.
210 ``source_dir`` is the path to the Mercurial source tree to use.
@@ -209,10 +232,15 b' def build_installer(source_dir: pathlib.'
209
232
210 requirements_txt = wix_dir / 'requirements.txt'
233 requirements_txt = wix_dir / 'requirements.txt'
211
234
212 build_py2exe(source_dir, hg_build_dir,
235 build_py2exe(
213 python_exe, 'wix', requirements_txt,
236 source_dir,
214 extra_packages=EXTRA_PACKAGES,
237 hg_build_dir,
215 extra_packages_script=extra_packages_script)
238 python_exe,
239 'wix',
240 requirements_txt,
241 extra_packages=EXTRA_PACKAGES,
242 extra_packages_script=extra_packages_script,
243 )
216
244
217 version = version or normalize_version(find_version(source_dir))
245 version = version or normalize_version(find_version(source_dir))
218 print('using version string: %s' % version)
246 print('using version string: %s' % version)
@@ -265,16 +293,19 b' def build_installer(source_dir: pathlib.'
265
293
266 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
294 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
267
295
268 msi_path = source_dir / 'dist' / (
296 msi_path = (
269 '%s-%s-%s.msi' % (msi_name, version, arch))
297 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, version, arch))
298 )
270
299
271 args = [
300 args = [
272 str(wix_path / 'light.exe'),
301 str(wix_path / 'light.exe'),
273 '-nologo',
302 '-nologo',
274 '-ext', 'WixUIExtension',
303 '-ext',
304 'WixUIExtension',
275 '-sw1076',
305 '-sw1076',
276 '-spdb',
306 '-spdb',
277 '-o', str(msi_path),
307 '-o',
308 str(msi_path),
278 ]
309 ]
279
310
280 for source, rel_path in SUPPORT_WXS:
311 for source, rel_path in SUPPORT_WXS:
@@ -286,10 +317,12 b' def build_installer(source_dir: pathlib.'
286 source = os.path.basename(source)
317 source = os.path.basename(source)
287 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
318 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
288
319
289 args.extend([
320 args.extend(
290 str(build_dir / 'library.wixobj'),
321 [
291 str(build_dir / 'mercurial.wixobj'),
322 str(build_dir / 'library.wixobj'),
292 ])
323 str(build_dir / 'mercurial.wixobj'),
324 ]
325 )
293
326
294 subprocess.run(args, cwd=str(source_dir), check=True)
327 subprocess.run(args, cwd=str(source_dir), check=True)
295
328
@@ -300,11 +333,19 b' def build_installer(source_dir: pathlib.'
300 }
333 }
301
334
302
335
303 def build_signed_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
336 def build_signed_installer(
304 name: str, version=None, subject_name=None,
337 source_dir: pathlib.Path,
305 cert_path=None, cert_password=None,
338 python_exe: pathlib.Path,
306 timestamp_url=None, extra_packages_script=None,
339 name: str,
307 extra_wxs=None, extra_features=None):
340 version=None,
341 subject_name=None,
342 cert_path=None,
343 cert_password=None,
344 timestamp_url=None,
345 extra_packages_script=None,
346 extra_wxs=None,
347 extra_features=None,
348 ):
308 """Build an installer with signed executables."""
349 """Build an installer with signed executables."""
309
350
310 post_build_fn = make_post_build_signing_fn(
351 post_build_fn = make_post_build_signing_fn(
@@ -312,16 +353,27 b' def build_signed_installer(source_dir: p'
312 subject_name=subject_name,
353 subject_name=subject_name,
313 cert_path=cert_path,
354 cert_path=cert_path,
314 cert_password=cert_password,
355 cert_password=cert_password,
315 timestamp_url=timestamp_url)
356 timestamp_url=timestamp_url,
357 )
316
358
317 info = build_installer(source_dir, python_exe=python_exe,
359 info = build_installer(
318 msi_name=name.lower(), version=version,
360 source_dir,
319 post_build_fn=post_build_fn,
361 python_exe=python_exe,
320 extra_packages_script=extra_packages_script,
362 msi_name=name.lower(),
321 extra_wxs=extra_wxs, extra_features=extra_features)
363 version=version,
364 post_build_fn=post_build_fn,
365 extra_packages_script=extra_packages_script,
366 extra_wxs=extra_wxs,
367 extra_features=extra_features,
368 )
322
369
323 description = '%s %s' % (name, version)
370 description = '%s %s' % (name, version)
324
371
325 sign_with_signtool(info['msi_path'], description,
372 sign_with_signtool(
326 subject_name=subject_name, cert_path=cert_path,
373 info['msi_path'],
327 cert_password=cert_password, timestamp_url=timestamp_url)
374 description,
375 subject_name=subject_name,
376 cert_path=cert_path,
377 cert_password=cert_password,
378 timestamp_url=timestamp_url,
379 )
@@ -19,14 +19,15 b' import sys'
19 if __name__ == '__main__':
19 if __name__ == '__main__':
20 parser = argparse.ArgumentParser()
20 parser = argparse.ArgumentParser()
21
21
22 parser.add_argument('--python',
22 parser.add_argument(
23 required=True,
23 '--python', required=True, help='path to python.exe to use'
24 help='path to python.exe to use')
24 )
25 parser.add_argument('--iscc',
25 parser.add_argument('--iscc', help='path to iscc.exe to use')
26 help='path to iscc.exe to use')
26 parser.add_argument(
27 parser.add_argument('--version',
27 '--version',
28 help='Mercurial version string to use '
28 help='Mercurial version string to use '
29 '(detected from __version__.py if not defined')
29 '(detected from __version__.py if not defined',
30 )
30
31
31 args = parser.parse_args()
32 args = parser.parse_args()
32
33
@@ -36,8 +37,11 b" if __name__ == '__main__':"
36 if args.iscc:
37 if args.iscc:
37 iscc = pathlib.Path(args.iscc)
38 iscc = pathlib.Path(args.iscc)
38 else:
39 else:
39 iscc = (pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Inno Setup 5' /
40 iscc = (
40 'ISCC.exe')
41 pathlib.Path(os.environ['ProgramFiles(x86)'])
42 / 'Inno Setup 5'
43 / 'ISCC.exe'
44 )
41
45
42 here = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
46 here = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
43 source_dir = here.parent.parent.parent
47 source_dir = here.parent.parent.parent
@@ -47,5 +51,10 b" if __name__ == '__main__':"
47
51
48 from hgpackaging.inno import build
52 from hgpackaging.inno import build
49
53
50 build(source_dir, build_dir, pathlib.Path(args.python), iscc,
54 build(
51 version=args.version)
55 source_dir,
56 build_dir,
57 pathlib.Path(args.python),
58 iscc,
59 version=args.version,
60 )
@@ -17,31 +17,42 b' import sys'
17 if __name__ == '__main__':
17 if __name__ == '__main__':
18 parser = argparse.ArgumentParser()
18 parser = argparse.ArgumentParser()
19
19
20 parser.add_argument('--name',
20 parser.add_argument('--name', help='Application name', default='Mercurial')
21 help='Application name',
21 parser.add_argument(
22 default='Mercurial')
22 '--python', help='Path to Python executable to use', required=True
23 parser.add_argument('--python',
23 )
24 help='Path to Python executable to use',
24 parser.add_argument(
25 required=True)
25 '--sign-sn',
26 parser.add_argument('--sign-sn',
26 help='Subject name (or fragment thereof) of certificate '
27 help='Subject name (or fragment thereof) of certificate '
27 'to use for signing',
28 'to use for signing')
28 )
29 parser.add_argument('--sign-cert',
29 parser.add_argument(
30 help='Path to certificate to use for signing')
30 '--sign-cert', help='Path to certificate to use for signing'
31 parser.add_argument('--sign-password',
31 )
32 help='Password for signing certificate')
32 parser.add_argument(
33 parser.add_argument('--sign-timestamp-url',
33 '--sign-password', help='Password for signing certificate'
34 help='URL of timestamp server to use for signing')
34 )
35 parser.add_argument('--version',
35 parser.add_argument(
36 help='Version string to use')
36 '--sign-timestamp-url',
37 parser.add_argument('--extra-packages-script',
37 help='URL of timestamp server to use for signing',
38 help=('Script to execute to include extra packages in '
38 )
39 'py2exe binary.'))
39 parser.add_argument('--version', help='Version string to use')
40 parser.add_argument('--extra-wxs',
40 parser.add_argument(
41 help='CSV of path_to_wxs_file=working_dir_for_wxs_file')
41 '--extra-packages-script',
42 parser.add_argument('--extra-features',
42 help=(
43 help=('CSV of extra feature names to include '
43 'Script to execute to include extra packages in ' 'py2exe binary.'
44 'in the installer from the extra wxs files'))
44 ),
45 )
46 parser.add_argument(
47 '--extra-wxs', help='CSV of path_to_wxs_file=working_dir_for_wxs_file'
48 )
49 parser.add_argument(
50 '--extra-features',
51 help=(
52 'CSV of extra feature names to include '
53 'in the installer from the extra wxs files'
54 ),
55 )
45
56
46 args = parser.parse_args()
57 args = parser.parse_args()
47
58
@@ -69,7 +80,8 b" if __name__ == '__main__':"
69 kwargs['extra_packages_script'] = args.extra_packages_script
80 kwargs['extra_packages_script'] = args.extra_packages_script
70 if args.extra_wxs:
81 if args.extra_wxs:
71 kwargs['extra_wxs'] = dict(
82 kwargs['extra_wxs'] = dict(
72 thing.split("=") for thing in args.extra_wxs.split(','))
83 thing.split("=") for thing in args.extra_wxs.split(',')
84 )
73 if args.extra_features:
85 if args.extra_features:
74 kwargs['extra_features'] = args.extra_features.split(',')
86 kwargs['extra_features'] = args.extra_features.split(',')
75
87
@@ -44,18 +44,12 b' def plot(data, title=None):'
44 comb_plt = fig.add_subplot(211)
44 comb_plt = fig.add_subplot(211)
45 other_plt = fig.add_subplot(212)
45 other_plt = fig.add_subplot(212)
46
46
47 comb_plt.plot(ary[0],
47 comb_plt.plot(
48 np.cumsum(ary[1]),
48 ary[0], np.cumsum(ary[1]), color='red', linewidth=1, label='comb'
49 color='red',
49 )
50 linewidth=1,
51 label='comb')
52
50
53 plots = []
51 plots = []
54 p = other_plt.plot(ary[0],
52 p = other_plt.plot(ary[0], ary[1], color='red', linewidth=1, label='wall')
55 ary[1],
56 color='red',
57 linewidth=1,
58 label='wall')
59 plots.append(p)
53 plots.append(p)
60
54
61 colors = {
55 colors = {
@@ -64,20 +58,24 b' def plot(data, title=None):'
64 1000: ('purple', 'xkcd:dark pink'),
58 1000: ('purple', 'xkcd:dark pink'),
65 }
59 }
66 for n, color in colors.items():
60 for n, color in colors.items():
67 avg_n = np.convolve(ary[1], np.full(n, 1. / n), 'valid')
61 avg_n = np.convolve(ary[1], np.full(n, 1.0 / n), 'valid')
68 p = other_plt.plot(ary[0][n - 1:],
62 p = other_plt.plot(
69 avg_n,
63 ary[0][n - 1 :],
70 color=color[0],
64 avg_n,
71 linewidth=1,
65 color=color[0],
72 label='avg time last %d' % n)
66 linewidth=1,
67 label='avg time last %d' % n,
68 )
73 plots.append(p)
69 plots.append(p)
74
70
75 med_n = scipy.signal.medfilt(ary[1], n + 1)
71 med_n = scipy.signal.medfilt(ary[1], n + 1)
76 p = other_plt.plot(ary[0],
72 p = other_plt.plot(
77 med_n,
73 ary[0],
78 color=color[1],
74 med_n,
79 linewidth=1,
75 color=color[1],
80 label='median time last %d' % n)
76 linewidth=1,
77 label='median time last %d' % n,
78 )
81 plots.append(p)
79 plots.append(p)
82
80
83 formatter = mticker.ScalarFormatter()
81 formatter = mticker.ScalarFormatter()
@@ -108,6 +106,7 b' def plot(data, title=None):'
108 else:
106 else:
109 legline.set_alpha(0.2)
107 legline.set_alpha(0.2)
110 fig.canvas.draw()
108 fig.canvas.draw()
109
111 if title is not None:
110 if title is not None:
112 fig.canvas.set_window_title(title)
111 fig.canvas.set_window_title(title)
113 fig.canvas.mpl_connect('pick_event', onpick)
112 fig.canvas.mpl_connect('pick_event', onpick)
This diff has been collapsed as it changes many lines, (1348 lines changed) Show them Hide them
@@ -84,32 +84,33 b' from mercurial import ('
84 # try to import modules separately (in dict order), and ignore
84 # try to import modules separately (in dict order), and ignore
85 # failure, because these aren't available with early Mercurial
85 # failure, because these aren't available with early Mercurial
86 try:
86 try:
87 from mercurial import branchmap # since 2.5 (or bcee63733aad)
87 from mercurial import branchmap # since 2.5 (or bcee63733aad)
88 except ImportError:
88 except ImportError:
89 pass
89 pass
90 try:
90 try:
91 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
91 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
92 except ImportError:
92 except ImportError:
93 pass
93 pass
94 try:
94 try:
95 from mercurial import registrar # since 3.7 (or 37d50250b696)
95 from mercurial import registrar # since 3.7 (or 37d50250b696)
96 dir(registrar) # forcibly load it
96
97 dir(registrar) # forcibly load it
97 except ImportError:
98 except ImportError:
98 registrar = None
99 registrar = None
99 try:
100 try:
100 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
101 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
101 except ImportError:
102 except ImportError:
102 pass
103 pass
103 try:
104 try:
104 from mercurial.utils import repoviewutil # since 5.0
105 from mercurial.utils import repoviewutil # since 5.0
105 except ImportError:
106 except ImportError:
106 repoviewutil = None
107 repoviewutil = None
107 try:
108 try:
108 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
109 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
109 except ImportError:
110 except ImportError:
110 pass
111 pass
111 try:
112 try:
112 from mercurial import setdiscovery # since 1.9 (or cb98fed52495)
113 from mercurial import setdiscovery # since 1.9 (or cb98fed52495)
113 except ImportError:
114 except ImportError:
114 pass
115 pass
115
116
@@ -118,29 +119,33 b' try:'
118 except ImportError:
119 except ImportError:
119 profiling = None
120 profiling = None
120
121
122
121 def identity(a):
123 def identity(a):
122 return a
124 return a
123
125
126
124 try:
127 try:
125 from mercurial import pycompat
128 from mercurial import pycompat
129
126 getargspec = pycompat.getargspec # added to module after 4.5
130 getargspec = pycompat.getargspec # added to module after 4.5
127 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
131 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
128 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
132 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
129 _bytestr = pycompat.bytestr # since 4.2 (or b70407bd84d5)
133 _bytestr = pycompat.bytestr # since 4.2 (or b70407bd84d5)
130 _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b)
134 _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b)
131 fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e)
135 fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e)
132 if pycompat.ispy3:
136 if pycompat.ispy3:
133 _maxint = sys.maxsize # per py3 docs for replacing maxint
137 _maxint = sys.maxsize # per py3 docs for replacing maxint
134 else:
138 else:
135 _maxint = sys.maxint
139 _maxint = sys.maxint
136 except (NameError, ImportError, AttributeError):
140 except (NameError, ImportError, AttributeError):
137 import inspect
141 import inspect
142
138 getargspec = inspect.getargspec
143 getargspec = inspect.getargspec
139 _byteskwargs = identity
144 _byteskwargs = identity
140 _bytestr = str
145 _bytestr = str
141 fsencode = identity # no py3 support
146 fsencode = identity # no py3 support
142 _maxint = sys.maxint # no py3 support
147 _maxint = sys.maxint # no py3 support
143 _sysstr = lambda x: x # no py3 support
148 _sysstr = lambda x: x # no py3 support
144 _xrange = xrange
149 _xrange = xrange
145
150
146 try:
151 try:
@@ -155,6 +160,7 b' except (NameError, AttributeError, Impor'
155
160
156 try:
161 try:
157 from mercurial import logcmdutil
162 from mercurial import logcmdutil
163
158 makelogtemplater = logcmdutil.maketemplater
164 makelogtemplater = logcmdutil.maketemplater
159 except (AttributeError, ImportError):
165 except (AttributeError, ImportError):
160 try:
166 try:
@@ -166,8 +172,12 b' except (AttributeError, ImportError):'
166 # define util.safehasattr forcibly, because util.safehasattr has been
172 # define util.safehasattr forcibly, because util.safehasattr has been
167 # available since 1.9.3 (or 94b200a11cf7)
173 # available since 1.9.3 (or 94b200a11cf7)
168 _undefined = object()
174 _undefined = object()
175
176
169 def safehasattr(thing, attr):
177 def safehasattr(thing, attr):
170 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
178 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
179
180
171 setattr(util, 'safehasattr', safehasattr)
181 setattr(util, 'safehasattr', safehasattr)
172
182
173 # for "historical portability":
183 # for "historical portability":
@@ -185,20 +195,28 b' else:'
185 # available, because commands.formatteropts has been available since
195 # available, because commands.formatteropts has been available since
186 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
196 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
187 # available since 2.2 (or ae5f92e154d3)
197 # available since 2.2 (or ae5f92e154d3)
188 formatteropts = getattr(cmdutil, "formatteropts",
198 formatteropts = getattr(
189 getattr(commands, "formatteropts", []))
199 cmdutil, "formatteropts", getattr(commands, "formatteropts", [])
200 )
190
201
191 # for "historical portability":
202 # for "historical portability":
192 # use locally defined option list, if debugrevlogopts isn't available,
203 # use locally defined option list, if debugrevlogopts isn't available,
193 # because commands.debugrevlogopts has been available since 3.7 (or
204 # because commands.debugrevlogopts has been available since 3.7 (or
194 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
205 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
195 # since 1.9 (or a79fea6b3e77).
206 # since 1.9 (or a79fea6b3e77).
196 revlogopts = getattr(cmdutil, "debugrevlogopts",
207 revlogopts = getattr(
197 getattr(commands, "debugrevlogopts", [
208 cmdutil,
198 (b'c', b'changelog', False, (b'open changelog')),
209 "debugrevlogopts",
199 (b'm', b'manifest', False, (b'open manifest')),
210 getattr(
200 (b'', b'dir', False, (b'open directory manifest')),
211 commands,
201 ]))
212 "debugrevlogopts",
213 [
214 (b'c', b'changelog', False, b'open changelog'),
215 (b'm', b'manifest', False, b'open manifest'),
216 (b'', b'dir', False, b'open directory manifest'),
217 ],
218 ),
219 )
202
220
203 cmdtable = {}
221 cmdtable = {}
204
222
@@ -208,6 +226,7 b' cmdtable = {}'
208 def parsealiases(cmd):
226 def parsealiases(cmd):
209 return cmd.split(b"|")
227 return cmd.split(b"|")
210
228
229
211 if safehasattr(registrar, 'command'):
230 if safehasattr(registrar, 'command'):
212 command = registrar.command(cmdtable)
231 command = registrar.command(cmdtable)
213 elif safehasattr(cmdutil, 'command'):
232 elif safehasattr(cmdutil, 'command'):
@@ -217,10 +236,13 b" elif safehasattr(cmdutil, 'command'):"
217 # wrap original cmdutil.command, because "norepo" option has
236 # wrap original cmdutil.command, because "norepo" option has
218 # been available since 3.1 (or 75a96326cecb)
237 # been available since 3.1 (or 75a96326cecb)
219 _command = command
238 _command = command
239
220 def command(name, options=(), synopsis=None, norepo=False):
240 def command(name, options=(), synopsis=None, norepo=False):
221 if norepo:
241 if norepo:
222 commands.norepo += b' %s' % b' '.join(parsealiases(name))
242 commands.norepo += b' %s' % b' '.join(parsealiases(name))
223 return _command(name, list(options), synopsis)
243 return _command(name, list(options), synopsis)
244
245
224 else:
246 else:
225 # for "historical portability":
247 # for "historical portability":
226 # define "@command" annotation locally, because cmdutil.command
248 # define "@command" annotation locally, because cmdutil.command
@@ -234,36 +256,51 b' else:'
234 if norepo:
256 if norepo:
235 commands.norepo += b' %s' % b' '.join(parsealiases(name))
257 commands.norepo += b' %s' % b' '.join(parsealiases(name))
236 return func
258 return func
259
237 return decorator
260 return decorator
238
261
262
239 try:
263 try:
240 import mercurial.registrar
264 import mercurial.registrar
241 import mercurial.configitems
265 import mercurial.configitems
266
242 configtable = {}
267 configtable = {}
243 configitem = mercurial.registrar.configitem(configtable)
268 configitem = mercurial.registrar.configitem(configtable)
244 configitem(b'perf', b'presleep',
269 configitem(
270 b'perf',
271 b'presleep',
245 default=mercurial.configitems.dynamicdefault,
272 default=mercurial.configitems.dynamicdefault,
246 experimental=True,
273 experimental=True,
247 )
274 )
248 configitem(b'perf', b'stub',
275 configitem(
276 b'perf',
277 b'stub',
249 default=mercurial.configitems.dynamicdefault,
278 default=mercurial.configitems.dynamicdefault,
250 experimental=True,
279 experimental=True,
251 )
280 )
252 configitem(b'perf', b'parentscount',
281 configitem(
282 b'perf',
283 b'parentscount',
253 default=mercurial.configitems.dynamicdefault,
284 default=mercurial.configitems.dynamicdefault,
254 experimental=True,
285 experimental=True,
255 )
286 )
256 configitem(b'perf', b'all-timing',
287 configitem(
288 b'perf',
289 b'all-timing',
257 default=mercurial.configitems.dynamicdefault,
290 default=mercurial.configitems.dynamicdefault,
258 experimental=True,
291 experimental=True,
259 )
292 )
260 configitem(b'perf', b'pre-run',
293 configitem(
294 b'perf', b'pre-run', default=mercurial.configitems.dynamicdefault,
295 )
296 configitem(
297 b'perf',
298 b'profile-benchmark',
261 default=mercurial.configitems.dynamicdefault,
299 default=mercurial.configitems.dynamicdefault,
262 )
300 )
263 configitem(b'perf', b'profile-benchmark',
301 configitem(
264 default=mercurial.configitems.dynamicdefault,
302 b'perf',
265 )
303 b'run-limits',
266 configitem(b'perf', b'run-limits',
267 default=mercurial.configitems.dynamicdefault,
304 default=mercurial.configitems.dynamicdefault,
268 experimental=True,
305 experimental=True,
269 )
306 )
@@ -272,42 +309,50 b' except (ImportError, AttributeError):'
272 except TypeError:
309 except TypeError:
273 # compatibility fix for a11fd395e83f
310 # compatibility fix for a11fd395e83f
274 # hg version: 5.2
311 # hg version: 5.2
275 configitem(b'perf', b'presleep',
312 configitem(
276 default=mercurial.configitems.dynamicdefault,
313 b'perf', b'presleep', default=mercurial.configitems.dynamicdefault,
314 )
315 configitem(
316 b'perf', b'stub', default=mercurial.configitems.dynamicdefault,
317 )
318 configitem(
319 b'perf', b'parentscount', default=mercurial.configitems.dynamicdefault,
277 )
320 )
278 configitem(b'perf', b'stub',
321 configitem(
279 default=mercurial.configitems.dynamicdefault,
322 b'perf', b'all-timing', default=mercurial.configitems.dynamicdefault,
280 )
323 )
281 configitem(b'perf', b'parentscount',
324 configitem(
325 b'perf', b'pre-run', default=mercurial.configitems.dynamicdefault,
326 )
327 configitem(
328 b'perf',
329 b'profile-benchmark',
282 default=mercurial.configitems.dynamicdefault,
330 default=mercurial.configitems.dynamicdefault,
283 )
331 )
284 configitem(b'perf', b'all-timing',
332 configitem(
285 default=mercurial.configitems.dynamicdefault,
333 b'perf', b'run-limits', default=mercurial.configitems.dynamicdefault,
286 )
287 configitem(b'perf', b'pre-run',
288 default=mercurial.configitems.dynamicdefault,
289 )
334 )
290 configitem(b'perf', b'profile-benchmark',
335
291 default=mercurial.configitems.dynamicdefault,
292 )
293 configitem(b'perf', b'run-limits',
294 default=mercurial.configitems.dynamicdefault,
295 )
296
336
297 def getlen(ui):
337 def getlen(ui):
298 if ui.configbool(b"perf", b"stub", False):
338 if ui.configbool(b"perf", b"stub", False):
299 return lambda x: 1
339 return lambda x: 1
300 return len
340 return len
301
341
342
302 class noop(object):
343 class noop(object):
303 """dummy context manager"""
344 """dummy context manager"""
345
304 def __enter__(self):
346 def __enter__(self):
305 pass
347 pass
348
306 def __exit__(self, *args):
349 def __exit__(self, *args):
307 pass
350 pass
308
351
352
309 NOOPCTX = noop()
353 NOOPCTX = noop()
310
354
355
311 def gettimer(ui, opts=None):
356 def gettimer(ui, opts=None):
312 """return a timer function and formatter: (timer, formatter)
357 """return a timer function and formatter: (timer, formatter)
313
358
@@ -338,31 +383,42 b' def gettimer(ui, opts=None):'
338 # define formatter locally, because ui.formatter has been
383 # define formatter locally, because ui.formatter has been
339 # available since 2.2 (or ae5f92e154d3)
384 # available since 2.2 (or ae5f92e154d3)
340 from mercurial import node
385 from mercurial import node
386
341 class defaultformatter(object):
387 class defaultformatter(object):
342 """Minimized composition of baseformatter and plainformatter
388 """Minimized composition of baseformatter and plainformatter
343 """
389 """
390
344 def __init__(self, ui, topic, opts):
391 def __init__(self, ui, topic, opts):
345 self._ui = ui
392 self._ui = ui
346 if ui.debugflag:
393 if ui.debugflag:
347 self.hexfunc = node.hex
394 self.hexfunc = node.hex
348 else:
395 else:
349 self.hexfunc = node.short
396 self.hexfunc = node.short
397
350 def __nonzero__(self):
398 def __nonzero__(self):
351 return False
399 return False
400
352 __bool__ = __nonzero__
401 __bool__ = __nonzero__
402
353 def startitem(self):
403 def startitem(self):
354 pass
404 pass
405
355 def data(self, **data):
406 def data(self, **data):
356 pass
407 pass
408
357 def write(self, fields, deftext, *fielddata, **opts):
409 def write(self, fields, deftext, *fielddata, **opts):
358 self._ui.write(deftext % fielddata, **opts)
410 self._ui.write(deftext % fielddata, **opts)
411
359 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
412 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
360 if cond:
413 if cond:
361 self._ui.write(deftext % fielddata, **opts)
414 self._ui.write(deftext % fielddata, **opts)
415
362 def plain(self, text, **opts):
416 def plain(self, text, **opts):
363 self._ui.write(text, **opts)
417 self._ui.write(text, **opts)
418
364 def end(self):
419 def end(self):
365 pass
420 pass
421
366 fm = defaultformatter(ui, b'perf', opts)
422 fm = defaultformatter(ui, b'perf', opts)
367
423
368 # stub function, runs code only once instead of in a loop
424 # stub function, runs code only once instead of in a loop
@@ -379,20 +435,27 b' def gettimer(ui, opts=None):'
379 for item in limitspec:
435 for item in limitspec:
380 parts = item.split(b'-', 1)
436 parts = item.split(b'-', 1)
381 if len(parts) < 2:
437 if len(parts) < 2:
382 ui.warn((b'malformatted run limit entry, missing "-": %s\n'
438 ui.warn((b'malformatted run limit entry, missing "-": %s\n' % item))
383 % item))
384 continue
439 continue
385 try:
440 try:
386 time_limit = float(_sysstr(parts[0]))
441 time_limit = float(_sysstr(parts[0]))
387 except ValueError as e:
442 except ValueError as e:
388 ui.warn((b'malformatted run limit entry, %s: %s\n'
443 ui.warn(
389 % (_bytestr(e), item)))
444 (
445 b'malformatted run limit entry, %s: %s\n'
446 % (_bytestr(e), item)
447 )
448 )
390 continue
449 continue
391 try:
450 try:
392 run_limit = int(_sysstr(parts[1]))
451 run_limit = int(_sysstr(parts[1]))
393 except ValueError as e:
452 except ValueError as e:
394 ui.warn((b'malformatted run limit entry, %s: %s\n'
453 ui.warn(
395 % (_bytestr(e), item)))
454 (
455 b'malformatted run limit entry, %s: %s\n'
456 % (_bytestr(e), item)
457 )
458 )
396 continue
459 continue
397 limits.append((time_limit, run_limit))
460 limits.append((time_limit, run_limit))
398 if not limits:
461 if not limits:
@@ -404,15 +467,23 b' def gettimer(ui, opts=None):'
404 profiler = profiling.profile(ui)
467 profiler = profiling.profile(ui)
405
468
406 prerun = getint(ui, b"perf", b"pre-run", 0)
469 prerun = getint(ui, b"perf", b"pre-run", 0)
407 t = functools.partial(_timer, fm, displayall=displayall, limits=limits,
470 t = functools.partial(
408 prerun=prerun, profiler=profiler)
471 _timer,
472 fm,
473 displayall=displayall,
474 limits=limits,
475 prerun=prerun,
476 profiler=profiler,
477 )
409 return t, fm
478 return t, fm
410
479
480
411 def stub_timer(fm, func, setup=None, title=None):
481 def stub_timer(fm, func, setup=None, title=None):
412 if setup is not None:
482 if setup is not None:
413 setup()
483 setup()
414 func()
484 func()
415
485
486
416 @contextlib.contextmanager
487 @contextlib.contextmanager
417 def timeone():
488 def timeone():
418 r = []
489 r = []
@@ -422,7 +493,7 b' def timeone():'
422 cstop = util.timer()
493 cstop = util.timer()
423 ostop = os.times()
494 ostop = os.times()
424 a, b = ostart, ostop
495 a, b = ostart, ostop
425 r.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
496 r.append((cstop - cstart, b[0] - a[0], b[1] - a[1]))
426
497
427
498
428 # list of stop condition (elapsed time, minimal run count)
499 # list of stop condition (elapsed time, minimal run count)
@@ -431,8 +502,17 b' DEFAULTLIMITS = ('
431 (10.0, 3),
502 (10.0, 3),
432 )
503 )
433
504
434 def _timer(fm, func, setup=None, title=None, displayall=False,
505
435 limits=DEFAULTLIMITS, prerun=0, profiler=None):
506 def _timer(
507 fm,
508 func,
509 setup=None,
510 title=None,
511 displayall=False,
512 limits=DEFAULTLIMITS,
513 prerun=0,
514 profiler=None,
515 ):
436 gc.collect()
516 gc.collect()
437 results = []
517 results = []
438 begin = util.timer()
518 begin = util.timer()
@@ -461,8 +541,8 b' def _timer(fm, func, setup=None, title=N'
461 keepgoing = False
541 keepgoing = False
462 break
542 break
463
543
464 formatone(fm, results, title=title, result=r,
544 formatone(fm, results, title=title, result=r, displayall=displayall)
465 displayall=displayall)
545
466
546
467 def formatone(fm, timings, title=None, result=None, displayall=False):
547 def formatone(fm, timings, title=None, result=None, displayall=False):
468
548
@@ -474,6 +554,7 b' def formatone(fm, timings, title=None, r'
474 fm.write(b'title', b'! %s\n', title)
554 fm.write(b'title', b'! %s\n', title)
475 if result:
555 if result:
476 fm.write(b'result', b'! result: %s\n', result)
556 fm.write(b'result', b'! result: %s\n', result)
557
477 def display(role, entry):
558 def display(role, entry):
478 prefix = b''
559 prefix = b''
479 if role != b'best':
560 if role != b'best':
@@ -482,9 +563,10 b' def formatone(fm, timings, title=None, r'
482 fm.write(prefix + b'wall', b' wall %f', entry[0])
563 fm.write(prefix + b'wall', b' wall %f', entry[0])
483 fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2])
564 fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2])
484 fm.write(prefix + b'user', b' user %f', entry[1])
565 fm.write(prefix + b'user', b' user %f', entry[1])
485 fm.write(prefix + b'sys', b' sys %f', entry[2])
566 fm.write(prefix + b'sys', b' sys %f', entry[2])
486 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
567 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
487 fm.plain(b'\n')
568 fm.plain(b'\n')
569
488 timings.sort()
570 timings.sort()
489 min_val = timings[0]
571 min_val = timings[0]
490 display(b'best', min_val)
572 display(b'best', min_val)
@@ -496,8 +578,10 b' def formatone(fm, timings, title=None, r'
496 median = timings[len(timings) // 2]
578 median = timings[len(timings) // 2]
497 display(b'median', median)
579 display(b'median', median)
498
580
581
499 # utilities for historical portability
582 # utilities for historical portability
500
583
584
501 def getint(ui, section, name, default):
585 def getint(ui, section, name, default):
502 # for "historical portability":
586 # for "historical portability":
503 # ui.configint has been available since 1.9 (or fa2b596db182)
587 # ui.configint has been available since 1.9 (or fa2b596db182)
@@ -507,8 +591,10 b' def getint(ui, section, name, default):'
507 try:
591 try:
508 return int(v)
592 return int(v)
509 except ValueError:
593 except ValueError:
510 raise error.ConfigError((b"%s.%s is not an integer ('%s')")
594 raise error.ConfigError(
511 % (section, name, v))
595 b"%s.%s is not an integer ('%s')" % (section, name, v)
596 )
597
512
598
513 def safeattrsetter(obj, name, ignoremissing=False):
599 def safeattrsetter(obj, name, ignoremissing=False):
514 """Ensure that 'obj' has 'name' attribute before subsequent setattr
600 """Ensure that 'obj' has 'name' attribute before subsequent setattr
@@ -528,20 +614,29 b' def safeattrsetter(obj, name, ignoremiss'
528 if not util.safehasattr(obj, name):
614 if not util.safehasattr(obj, name):
529 if ignoremissing:
615 if ignoremissing:
530 return None
616 return None
531 raise error.Abort((b"missing attribute %s of %s might break assumption"
617 raise error.Abort(
532 b" of performance measurement") % (name, obj))
618 (
619 b"missing attribute %s of %s might break assumption"
620 b" of performance measurement"
621 )
622 % (name, obj)
623 )
533
624
534 origvalue = getattr(obj, _sysstr(name))
625 origvalue = getattr(obj, _sysstr(name))
626
535 class attrutil(object):
627 class attrutil(object):
536 def set(self, newvalue):
628 def set(self, newvalue):
537 setattr(obj, _sysstr(name), newvalue)
629 setattr(obj, _sysstr(name), newvalue)
630
538 def restore(self):
631 def restore(self):
539 setattr(obj, _sysstr(name), origvalue)
632 setattr(obj, _sysstr(name), origvalue)
540
633
541 return attrutil()
634 return attrutil()
542
635
636
543 # utilities to examine each internal API changes
637 # utilities to examine each internal API changes
544
638
639
545 def getbranchmapsubsettable():
640 def getbranchmapsubsettable():
546 # for "historical portability":
641 # for "historical portability":
547 # subsettable is defined in:
642 # subsettable is defined in:
@@ -556,8 +651,11 b' def getbranchmapsubsettable():'
556 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
651 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
557 # branchmap and repoview modules exist, but subsettable attribute
652 # branchmap and repoview modules exist, but subsettable attribute
558 # doesn't)
653 # doesn't)
559 raise error.Abort((b"perfbranchmap not available with this Mercurial"),
654 raise error.Abort(
560 hint=b"use 2.5 or later")
655 b"perfbranchmap not available with this Mercurial",
656 hint=b"use 2.5 or later",
657 )
658
561
659
562 def getsvfs(repo):
660 def getsvfs(repo):
563 """Return appropriate object to access files under .hg/store
661 """Return appropriate object to access files under .hg/store
@@ -570,6 +668,7 b' def getsvfs(repo):'
570 else:
668 else:
571 return getattr(repo, 'sopener')
669 return getattr(repo, 'sopener')
572
670
671
573 def getvfs(repo):
672 def getvfs(repo):
574 """Return appropriate object to access files under .hg
673 """Return appropriate object to access files under .hg
575 """
674 """
@@ -581,10 +680,11 b' def getvfs(repo):'
581 else:
680 else:
582 return getattr(repo, 'opener')
681 return getattr(repo, 'opener')
583
682
683
584 def repocleartagscachefunc(repo):
684 def repocleartagscachefunc(repo):
585 """Return the function to clear tags cache according to repo internal API
685 """Return the function to clear tags cache according to repo internal API
586 """
686 """
587 if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525)
687 if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525)
588 # in this case, setattr(repo, '_tagscache', None) or so isn't
688 # in this case, setattr(repo, '_tagscache', None) or so isn't
589 # correct way to clear tags cache, because existing code paths
689 # correct way to clear tags cache, because existing code paths
590 # expect _tagscache to be a structured object.
690 # expect _tagscache to be a structured object.
@@ -593,25 +693,28 b' def repocleartagscachefunc(repo):'
593 # 98c867ac1330), and delattr() can't work in such case
693 # 98c867ac1330), and delattr() can't work in such case
594 if b'_tagscache' in vars(repo):
694 if b'_tagscache' in vars(repo):
595 del repo.__dict__[b'_tagscache']
695 del repo.__dict__[b'_tagscache']
696
596 return clearcache
697 return clearcache
597
698
598 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
699 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
599 if repotags: # since 1.4 (or 5614a628d173)
700 if repotags: # since 1.4 (or 5614a628d173)
600 return lambda : repotags.set(None)
701 return lambda: repotags.set(None)
601
702
602 repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True)
703 repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True)
603 if repotagscache: # since 0.6 (or d7df759d0e97)
704 if repotagscache: # since 0.6 (or d7df759d0e97)
604 return lambda : repotagscache.set(None)
705 return lambda: repotagscache.set(None)
605
706
606 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
707 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
607 # this point, but it isn't so problematic, because:
708 # this point, but it isn't so problematic, because:
608 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
709 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
609 # in perftags() causes failure soon
710 # in perftags() causes failure soon
610 # - perf.py itself has been available since 1.1 (or eb240755386d)
711 # - perf.py itself has been available since 1.1 (or eb240755386d)
611 raise error.Abort((b"tags API of this hg command is unknown"))
712 raise error.Abort(b"tags API of this hg command is unknown")
713
612
714
613 # utilities to clear cache
715 # utilities to clear cache
614
716
717
615 def clearfilecache(obj, attrname):
718 def clearfilecache(obj, attrname):
616 unfiltered = getattr(obj, 'unfiltered', None)
719 unfiltered = getattr(obj, 'unfiltered', None)
617 if unfiltered is not None:
720 if unfiltered is not None:
@@ -620,23 +723,32 b' def clearfilecache(obj, attrname):'
620 delattr(obj, attrname)
723 delattr(obj, attrname)
621 obj._filecache.pop(attrname, None)
724 obj._filecache.pop(attrname, None)
622
725
726
623 def clearchangelog(repo):
727 def clearchangelog(repo):
624 if repo is not repo.unfiltered():
728 if repo is not repo.unfiltered():
625 object.__setattr__(repo, r'_clcachekey', None)
729 object.__setattr__(repo, r'_clcachekey', None)
626 object.__setattr__(repo, r'_clcache', None)
730 object.__setattr__(repo, r'_clcache', None)
627 clearfilecache(repo.unfiltered(), 'changelog')
731 clearfilecache(repo.unfiltered(), 'changelog')
628
732
733
629 # perf commands
734 # perf commands
630
735
736
631 @command(b'perfwalk', formatteropts)
737 @command(b'perfwalk', formatteropts)
632 def perfwalk(ui, repo, *pats, **opts):
738 def perfwalk(ui, repo, *pats, **opts):
633 opts = _byteskwargs(opts)
739 opts = _byteskwargs(opts)
634 timer, fm = gettimer(ui, opts)
740 timer, fm = gettimer(ui, opts)
635 m = scmutil.match(repo[None], pats, {})
741 m = scmutil.match(repo[None], pats, {})
636 timer(lambda: len(list(repo.dirstate.walk(m, subrepos=[], unknown=True,
742 timer(
637 ignored=False))))
743 lambda: len(
744 list(
745 repo.dirstate.walk(m, subrepos=[], unknown=True, ignored=False)
746 )
747 )
748 )
638 fm.end()
749 fm.end()
639
750
751
640 @command(b'perfannotate', formatteropts)
752 @command(b'perfannotate', formatteropts)
641 def perfannotate(ui, repo, f, **opts):
753 def perfannotate(ui, repo, f, **opts):
642 opts = _byteskwargs(opts)
754 opts = _byteskwargs(opts)
@@ -645,18 +757,22 b' def perfannotate(ui, repo, f, **opts):'
645 timer(lambda: len(fc.annotate(True)))
757 timer(lambda: len(fc.annotate(True)))
646 fm.end()
758 fm.end()
647
759
648 @command(b'perfstatus',
760
649 [(b'u', b'unknown', False,
761 @command(
650 b'ask status to look for unknown files')] + formatteropts)
762 b'perfstatus',
763 [(b'u', b'unknown', False, b'ask status to look for unknown files')]
764 + formatteropts,
765 )
651 def perfstatus(ui, repo, **opts):
766 def perfstatus(ui, repo, **opts):
652 opts = _byteskwargs(opts)
767 opts = _byteskwargs(opts)
653 #m = match.always(repo.root, repo.getcwd())
768 # m = match.always(repo.root, repo.getcwd())
654 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
769 # timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
655 # False))))
770 # False))))
656 timer, fm = gettimer(ui, opts)
771 timer, fm = gettimer(ui, opts)
657 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
772 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
658 fm.end()
773 fm.end()
659
774
775
660 @command(b'perfaddremove', formatteropts)
776 @command(b'perfaddremove', formatteropts)
661 def perfaddremove(ui, repo, **opts):
777 def perfaddremove(ui, repo, **opts):
662 opts = _byteskwargs(opts)
778 opts = _byteskwargs(opts)
@@ -675,71 +791,89 b' def perfaddremove(ui, repo, **opts):'
675 repo.ui.quiet = oldquiet
791 repo.ui.quiet = oldquiet
676 fm.end()
792 fm.end()
677
793
794
678 def clearcaches(cl):
795 def clearcaches(cl):
679 # behave somewhat consistently across internal API changes
796 # behave somewhat consistently across internal API changes
680 if util.safehasattr(cl, b'clearcaches'):
797 if util.safehasattr(cl, b'clearcaches'):
681 cl.clearcaches()
798 cl.clearcaches()
682 elif util.safehasattr(cl, b'_nodecache'):
799 elif util.safehasattr(cl, b'_nodecache'):
683 from mercurial.node import nullid, nullrev
800 from mercurial.node import nullid, nullrev
801
684 cl._nodecache = {nullid: nullrev}
802 cl._nodecache = {nullid: nullrev}
685 cl._nodepos = None
803 cl._nodepos = None
686
804
805
687 @command(b'perfheads', formatteropts)
806 @command(b'perfheads', formatteropts)
688 def perfheads(ui, repo, **opts):
807 def perfheads(ui, repo, **opts):
689 """benchmark the computation of a changelog heads"""
808 """benchmark the computation of a changelog heads"""
690 opts = _byteskwargs(opts)
809 opts = _byteskwargs(opts)
691 timer, fm = gettimer(ui, opts)
810 timer, fm = gettimer(ui, opts)
692 cl = repo.changelog
811 cl = repo.changelog
812
693 def s():
813 def s():
694 clearcaches(cl)
814 clearcaches(cl)
815
695 def d():
816 def d():
696 len(cl.headrevs())
817 len(cl.headrevs())
818
697 timer(d, setup=s)
819 timer(d, setup=s)
698 fm.end()
820 fm.end()
699
821
700 @command(b'perftags', formatteropts+
822
701 [
823 @command(
702 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
824 b'perftags',
703 ])
825 formatteropts
826 + [(b'', b'clear-revlogs', False, b'refresh changelog and manifest'),],
827 )
704 def perftags(ui, repo, **opts):
828 def perftags(ui, repo, **opts):
705 opts = _byteskwargs(opts)
829 opts = _byteskwargs(opts)
706 timer, fm = gettimer(ui, opts)
830 timer, fm = gettimer(ui, opts)
707 repocleartagscache = repocleartagscachefunc(repo)
831 repocleartagscache = repocleartagscachefunc(repo)
708 clearrevlogs = opts[b'clear_revlogs']
832 clearrevlogs = opts[b'clear_revlogs']
833
709 def s():
834 def s():
710 if clearrevlogs:
835 if clearrevlogs:
711 clearchangelog(repo)
836 clearchangelog(repo)
712 clearfilecache(repo.unfiltered(), 'manifest')
837 clearfilecache(repo.unfiltered(), 'manifest')
713 repocleartagscache()
838 repocleartagscache()
839
714 def t():
840 def t():
715 return len(repo.tags())
841 return len(repo.tags())
842
716 timer(t, setup=s)
843 timer(t, setup=s)
717 fm.end()
844 fm.end()
718
845
846
719 @command(b'perfancestors', formatteropts)
847 @command(b'perfancestors', formatteropts)
720 def perfancestors(ui, repo, **opts):
848 def perfancestors(ui, repo, **opts):
721 opts = _byteskwargs(opts)
849 opts = _byteskwargs(opts)
722 timer, fm = gettimer(ui, opts)
850 timer, fm = gettimer(ui, opts)
723 heads = repo.changelog.headrevs()
851 heads = repo.changelog.headrevs()
852
724 def d():
853 def d():
725 for a in repo.changelog.ancestors(heads):
854 for a in repo.changelog.ancestors(heads):
726 pass
855 pass
856
727 timer(d)
857 timer(d)
728 fm.end()
858 fm.end()
729
859
860
730 @command(b'perfancestorset', formatteropts)
861 @command(b'perfancestorset', formatteropts)
731 def perfancestorset(ui, repo, revset, **opts):
862 def perfancestorset(ui, repo, revset, **opts):
732 opts = _byteskwargs(opts)
863 opts = _byteskwargs(opts)
733 timer, fm = gettimer(ui, opts)
864 timer, fm = gettimer(ui, opts)
734 revs = repo.revs(revset)
865 revs = repo.revs(revset)
735 heads = repo.changelog.headrevs()
866 heads = repo.changelog.headrevs()
867
736 def d():
868 def d():
737 s = repo.changelog.ancestors(heads)
869 s = repo.changelog.ancestors(heads)
738 for rev in revs:
870 for rev in revs:
739 rev in s
871 rev in s
872
740 timer(d)
873 timer(d)
741 fm.end()
874 fm.end()
742
875
876
743 @command(b'perfdiscovery', formatteropts, b'PATH')
877 @command(b'perfdiscovery', formatteropts, b'PATH')
744 def perfdiscovery(ui, repo, path, **opts):
878 def perfdiscovery(ui, repo, path, **opts):
745 """benchmark discovery between local repo and the peer at given path
879 """benchmark discovery between local repo and the peer at given path
@@ -750,30 +884,38 b' def perfdiscovery(ui, repo, path, **opts'
750
884
751 def s():
885 def s():
752 repos[1] = hg.peer(ui, opts, path)
886 repos[1] = hg.peer(ui, opts, path)
887
753 def d():
888 def d():
754 setdiscovery.findcommonheads(ui, *repos)
889 setdiscovery.findcommonheads(ui, *repos)
890
755 timer(d, setup=s)
891 timer(d, setup=s)
756 fm.end()
892 fm.end()
757
893
758 @command(b'perfbookmarks', formatteropts +
894
759 [
895 @command(
760 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
896 b'perfbookmarks',
761 ])
897 formatteropts
898 + [(b'', b'clear-revlogs', False, b'refresh changelog and manifest'),],
899 )
762 def perfbookmarks(ui, repo, **opts):
900 def perfbookmarks(ui, repo, **opts):
763 """benchmark parsing bookmarks from disk to memory"""
901 """benchmark parsing bookmarks from disk to memory"""
764 opts = _byteskwargs(opts)
902 opts = _byteskwargs(opts)
765 timer, fm = gettimer(ui, opts)
903 timer, fm = gettimer(ui, opts)
766
904
767 clearrevlogs = opts[b'clear_revlogs']
905 clearrevlogs = opts[b'clear_revlogs']
906
768 def s():
907 def s():
769 if clearrevlogs:
908 if clearrevlogs:
770 clearchangelog(repo)
909 clearchangelog(repo)
771 clearfilecache(repo, b'_bookmarks')
910 clearfilecache(repo, b'_bookmarks')
911
772 def d():
912 def d():
773 repo._bookmarks
913 repo._bookmarks
914
774 timer(d, setup=s)
915 timer(d, setup=s)
775 fm.end()
916 fm.end()
776
917
918
777 @command(b'perfbundleread', formatteropts, b'BUNDLE')
919 @command(b'perfbundleread', formatteropts, b'BUNDLE')
778 def perfbundleread(ui, repo, bundlepath, **opts):
920 def perfbundleread(ui, repo, bundlepath, **opts):
779 """Benchmark reading of bundle files.
921 """Benchmark reading of bundle files.
@@ -863,25 +1005,32 b' def perfbundleread(ui, repo, bundlepath,'
863 bundle = exchange.readbundle(ui, fh, bundlepath)
1005 bundle = exchange.readbundle(ui, fh, bundlepath)
864
1006
865 if isinstance(bundle, changegroup.cg1unpacker):
1007 if isinstance(bundle, changegroup.cg1unpacker):
866 benches.extend([
1008 benches.extend(
867 (makebench(deltaiter), b'cg1 deltaiter()'),
1009 [
868 (makebench(iterchunks), b'cg1 getchunks()'),
1010 (makebench(deltaiter), b'cg1 deltaiter()'),
869 (makereadnbytes(8192), b'cg1 read(8k)'),
1011 (makebench(iterchunks), b'cg1 getchunks()'),
870 (makereadnbytes(16384), b'cg1 read(16k)'),
1012 (makereadnbytes(8192), b'cg1 read(8k)'),
871 (makereadnbytes(32768), b'cg1 read(32k)'),
1013 (makereadnbytes(16384), b'cg1 read(16k)'),
872 (makereadnbytes(131072), b'cg1 read(128k)'),
1014 (makereadnbytes(32768), b'cg1 read(32k)'),
873 ])
1015 (makereadnbytes(131072), b'cg1 read(128k)'),
1016 ]
1017 )
874 elif isinstance(bundle, bundle2.unbundle20):
1018 elif isinstance(bundle, bundle2.unbundle20):
875 benches.extend([
1019 benches.extend(
876 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
1020 [
877 (makebench(iterparts), b'bundle2 iterparts()'),
1021 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
878 (makebench(iterpartsseekable), b'bundle2 iterparts() seekable'),
1022 (makebench(iterparts), b'bundle2 iterparts()'),
879 (makebench(seek), b'bundle2 part seek()'),
1023 (
880 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
1024 makebench(iterpartsseekable),
881 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
1025 b'bundle2 iterparts() seekable',
882 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
1026 ),
883 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
1027 (makebench(seek), b'bundle2 part seek()'),
884 ])
1028 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
1029 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
1030 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
1031 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
1032 ]
1033 )
885 elif isinstance(bundle, streamclone.streamcloneapplier):
1034 elif isinstance(bundle, streamclone.streamcloneapplier):
886 raise error.Abort(b'stream clone bundles not supported')
1035 raise error.Abort(b'stream clone bundles not supported')
887 else:
1036 else:
@@ -892,9 +1041,15 b' def perfbundleread(ui, repo, bundlepath,'
892 timer(fn, title=title)
1041 timer(fn, title=title)
893 fm.end()
1042 fm.end()
894
1043
895 @command(b'perfchangegroupchangelog', formatteropts +
1044
896 [(b'', b'cgversion', b'02', b'changegroup version'),
1045 @command(
897 (b'r', b'rev', b'', b'revisions to add to changegroup')])
1046 b'perfchangegroupchangelog',
1047 formatteropts
1048 + [
1049 (b'', b'cgversion', b'02', b'changegroup version'),
1050 (b'r', b'rev', b'', b'revisions to add to changegroup'),
1051 ],
1052 )
898 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
1053 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
899 """Benchmark producing a changelog group for a changegroup.
1054 """Benchmark producing a changelog group for a changegroup.
900
1055
@@ -923,77 +1078,96 b' def perfchangegroupchangelog(ui, repo, c'
923
1078
924 fm.end()
1079 fm.end()
925
1080
1081
926 @command(b'perfdirs', formatteropts)
1082 @command(b'perfdirs', formatteropts)
927 def perfdirs(ui, repo, **opts):
1083 def perfdirs(ui, repo, **opts):
928 opts = _byteskwargs(opts)
1084 opts = _byteskwargs(opts)
929 timer, fm = gettimer(ui, opts)
1085 timer, fm = gettimer(ui, opts)
930 dirstate = repo.dirstate
1086 dirstate = repo.dirstate
931 b'a' in dirstate
1087 b'a' in dirstate
1088
932 def d():
1089 def d():
933 dirstate.hasdir(b'a')
1090 dirstate.hasdir(b'a')
934 del dirstate._map._dirs
1091 del dirstate._map._dirs
1092
935 timer(d)
1093 timer(d)
936 fm.end()
1094 fm.end()
937
1095
1096
938 @command(b'perfdirstate', formatteropts)
1097 @command(b'perfdirstate', formatteropts)
939 def perfdirstate(ui, repo, **opts):
1098 def perfdirstate(ui, repo, **opts):
940 opts = _byteskwargs(opts)
1099 opts = _byteskwargs(opts)
941 timer, fm = gettimer(ui, opts)
1100 timer, fm = gettimer(ui, opts)
942 b"a" in repo.dirstate
1101 b"a" in repo.dirstate
1102
943 def d():
1103 def d():
944 repo.dirstate.invalidate()
1104 repo.dirstate.invalidate()
945 b"a" in repo.dirstate
1105 b"a" in repo.dirstate
1106
946 timer(d)
1107 timer(d)
947 fm.end()
1108 fm.end()
948
1109
1110
949 @command(b'perfdirstatedirs', formatteropts)
1111 @command(b'perfdirstatedirs', formatteropts)
950 def perfdirstatedirs(ui, repo, **opts):
1112 def perfdirstatedirs(ui, repo, **opts):
951 opts = _byteskwargs(opts)
1113 opts = _byteskwargs(opts)
952 timer, fm = gettimer(ui, opts)
1114 timer, fm = gettimer(ui, opts)
953 b"a" in repo.dirstate
1115 b"a" in repo.dirstate
1116
954 def d():
1117 def d():
955 repo.dirstate.hasdir(b"a")
1118 repo.dirstate.hasdir(b"a")
956 del repo.dirstate._map._dirs
1119 del repo.dirstate._map._dirs
1120
957 timer(d)
1121 timer(d)
958 fm.end()
1122 fm.end()
959
1123
1124
960 @command(b'perfdirstatefoldmap', formatteropts)
1125 @command(b'perfdirstatefoldmap', formatteropts)
961 def perfdirstatefoldmap(ui, repo, **opts):
1126 def perfdirstatefoldmap(ui, repo, **opts):
962 opts = _byteskwargs(opts)
1127 opts = _byteskwargs(opts)
963 timer, fm = gettimer(ui, opts)
1128 timer, fm = gettimer(ui, opts)
964 dirstate = repo.dirstate
1129 dirstate = repo.dirstate
965 b'a' in dirstate
1130 b'a' in dirstate
1131
966 def d():
1132 def d():
967 dirstate._map.filefoldmap.get(b'a')
1133 dirstate._map.filefoldmap.get(b'a')
968 del dirstate._map.filefoldmap
1134 del dirstate._map.filefoldmap
1135
969 timer(d)
1136 timer(d)
970 fm.end()
1137 fm.end()
971
1138
1139
972 @command(b'perfdirfoldmap', formatteropts)
1140 @command(b'perfdirfoldmap', formatteropts)
973 def perfdirfoldmap(ui, repo, **opts):
1141 def perfdirfoldmap(ui, repo, **opts):
974 opts = _byteskwargs(opts)
1142 opts = _byteskwargs(opts)
975 timer, fm = gettimer(ui, opts)
1143 timer, fm = gettimer(ui, opts)
976 dirstate = repo.dirstate
1144 dirstate = repo.dirstate
977 b'a' in dirstate
1145 b'a' in dirstate
1146
978 def d():
1147 def d():
979 dirstate._map.dirfoldmap.get(b'a')
1148 dirstate._map.dirfoldmap.get(b'a')
980 del dirstate._map.dirfoldmap
1149 del dirstate._map.dirfoldmap
981 del dirstate._map._dirs
1150 del dirstate._map._dirs
1151
982 timer(d)
1152 timer(d)
983 fm.end()
1153 fm.end()
984
1154
1155
985 @command(b'perfdirstatewrite', formatteropts)
1156 @command(b'perfdirstatewrite', formatteropts)
986 def perfdirstatewrite(ui, repo, **opts):
1157 def perfdirstatewrite(ui, repo, **opts):
987 opts = _byteskwargs(opts)
1158 opts = _byteskwargs(opts)
988 timer, fm = gettimer(ui, opts)
1159 timer, fm = gettimer(ui, opts)
989 ds = repo.dirstate
1160 ds = repo.dirstate
990 b"a" in ds
1161 b"a" in ds
1162
991 def d():
1163 def d():
992 ds._dirty = True
1164 ds._dirty = True
993 ds.write(repo.currenttransaction())
1165 ds.write(repo.currenttransaction())
1166
994 timer(d)
1167 timer(d)
995 fm.end()
1168 fm.end()
996
1169
1170
997 def _getmergerevs(repo, opts):
1171 def _getmergerevs(repo, opts):
998 """parse command argument to return rev involved in merge
1172 """parse command argument to return rev involved in merge
999
1173
@@ -1016,44 +1190,64 b' def _getmergerevs(repo, opts):'
1016 ancestor = wctx.ancestor(rctx)
1190 ancestor = wctx.ancestor(rctx)
1017 return (wctx, rctx, ancestor)
1191 return (wctx, rctx, ancestor)
1018
1192
1019 @command(b'perfmergecalculate',
1193
1020 [
1194 @command(
1021 (b'r', b'rev', b'.', b'rev to merge against'),
1195 b'perfmergecalculate',
1022 (b'', b'from', b'', b'rev to merge from'),
1196 [
1023 (b'', b'base', b'', b'the revision to use as base'),
1197 (b'r', b'rev', b'.', b'rev to merge against'),
1024 ] + formatteropts)
1198 (b'', b'from', b'', b'rev to merge from'),
1199 (b'', b'base', b'', b'the revision to use as base'),
1200 ]
1201 + formatteropts,
1202 )
1025 def perfmergecalculate(ui, repo, **opts):
1203 def perfmergecalculate(ui, repo, **opts):
1026 opts = _byteskwargs(opts)
1204 opts = _byteskwargs(opts)
1027 timer, fm = gettimer(ui, opts)
1205 timer, fm = gettimer(ui, opts)
1028
1206
1029 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1207 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1208
1030 def d():
1209 def d():
1031 # acceptremote is True because we don't want prompts in the middle of
1210 # acceptremote is True because we don't want prompts in the middle of
1032 # our benchmark
1211 # our benchmark
1033 merge.calculateupdates(repo, wctx, rctx, [ancestor], branchmerge=False,
1212 merge.calculateupdates(
1034 force=False, acceptremote=True,
1213 repo,
1035 followcopies=True)
1214 wctx,
1215 rctx,
1216 [ancestor],
1217 branchmerge=False,
1218 force=False,
1219 acceptremote=True,
1220 followcopies=True,
1221 )
1222
1036 timer(d)
1223 timer(d)
1037 fm.end()
1224 fm.end()
1038
1225
1039 @command(b'perfmergecopies',
1226
1040 [
1227 @command(
1041 (b'r', b'rev', b'.', b'rev to merge against'),
1228 b'perfmergecopies',
1042 (b'', b'from', b'', b'rev to merge from'),
1229 [
1043 (b'', b'base', b'', b'the revision to use as base'),
1230 (b'r', b'rev', b'.', b'rev to merge against'),
1044 ] + formatteropts)
1231 (b'', b'from', b'', b'rev to merge from'),
1232 (b'', b'base', b'', b'the revision to use as base'),
1233 ]
1234 + formatteropts,
1235 )
1045 def perfmergecopies(ui, repo, **opts):
1236 def perfmergecopies(ui, repo, **opts):
1046 """measure runtime of `copies.mergecopies`"""
1237 """measure runtime of `copies.mergecopies`"""
1047 opts = _byteskwargs(opts)
1238 opts = _byteskwargs(opts)
1048 timer, fm = gettimer(ui, opts)
1239 timer, fm = gettimer(ui, opts)
1049 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1240 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1241
1050 def d():
1242 def d():
1051 # acceptremote is True because we don't want prompts in the middle of
1243 # acceptremote is True because we don't want prompts in the middle of
1052 # our benchmark
1244 # our benchmark
1053 copies.mergecopies(repo, wctx, rctx, ancestor)
1245 copies.mergecopies(repo, wctx, rctx, ancestor)
1246
1054 timer(d)
1247 timer(d)
1055 fm.end()
1248 fm.end()
1056
1249
1250
1057 @command(b'perfpathcopies', [], b"REV REV")
1251 @command(b'perfpathcopies', [], b"REV REV")
1058 def perfpathcopies(ui, repo, rev1, rev2, **opts):
1252 def perfpathcopies(ui, repo, rev1, rev2, **opts):
1059 """benchmark the copy tracing logic"""
1253 """benchmark the copy tracing logic"""
@@ -1061,20 +1255,26 b' def perfpathcopies(ui, repo, rev1, rev2,'
1061 timer, fm = gettimer(ui, opts)
1255 timer, fm = gettimer(ui, opts)
1062 ctx1 = scmutil.revsingle(repo, rev1, rev1)
1256 ctx1 = scmutil.revsingle(repo, rev1, rev1)
1063 ctx2 = scmutil.revsingle(repo, rev2, rev2)
1257 ctx2 = scmutil.revsingle(repo, rev2, rev2)
1258
1064 def d():
1259 def d():
1065 copies.pathcopies(ctx1, ctx2)
1260 copies.pathcopies(ctx1, ctx2)
1261
1066 timer(d)
1262 timer(d)
1067 fm.end()
1263 fm.end()
1068
1264
1069 @command(b'perfphases',
1265
1070 [(b'', b'full', False, b'include file reading time too'),
1266 @command(
1071 ], b"")
1267 b'perfphases',
1268 [(b'', b'full', False, b'include file reading time too'),],
1269 b"",
1270 )
1072 def perfphases(ui, repo, **opts):
1271 def perfphases(ui, repo, **opts):
1073 """benchmark phasesets computation"""
1272 """benchmark phasesets computation"""
1074 opts = _byteskwargs(opts)
1273 opts = _byteskwargs(opts)
1075 timer, fm = gettimer(ui, opts)
1274 timer, fm = gettimer(ui, opts)
1076 _phases = repo._phasecache
1275 _phases = repo._phasecache
1077 full = opts.get(b'full')
1276 full = opts.get(b'full')
1277
1078 def d():
1278 def d():
1079 phases = _phases
1279 phases = _phases
1080 if full:
1280 if full:
@@ -1082,30 +1282,32 b' def perfphases(ui, repo, **opts):'
1082 phases = repo._phasecache
1282 phases = repo._phasecache
1083 phases.invalidate()
1283 phases.invalidate()
1084 phases.loadphaserevs(repo)
1284 phases.loadphaserevs(repo)
1285
1085 timer(d)
1286 timer(d)
1086 fm.end()
1287 fm.end()
1087
1288
1088 @command(b'perfphasesremote',
1289
1089 [], b"[DEST]")
1290 @command(b'perfphasesremote', [], b"[DEST]")
1090 def perfphasesremote(ui, repo, dest=None, **opts):
1291 def perfphasesremote(ui, repo, dest=None, **opts):
1091 """benchmark time needed to analyse phases of the remote server"""
1292 """benchmark time needed to analyse phases of the remote server"""
1092 from mercurial.node import (
1293 from mercurial.node import bin
1093 bin,
1094 )
1095 from mercurial import (
1294 from mercurial import (
1096 exchange,
1295 exchange,
1097 hg,
1296 hg,
1098 phases,
1297 phases,
1099 )
1298 )
1299
1100 opts = _byteskwargs(opts)
1300 opts = _byteskwargs(opts)
1101 timer, fm = gettimer(ui, opts)
1301 timer, fm = gettimer(ui, opts)
1102
1302
1103 path = ui.paths.getpath(dest, default=(b'default-push', b'default'))
1303 path = ui.paths.getpath(dest, default=(b'default-push', b'default'))
1104 if not path:
1304 if not path:
1105 raise error.Abort((b'default repository not configured!'),
1305 raise error.Abort(
1106 hint=(b"see 'hg help config.paths'"))
1306 b'default repository not configured!',
1307 hint=b"see 'hg help config.paths'",
1308 )
1107 dest = path.pushloc or path.loc
1309 dest = path.pushloc or path.loc
1108 ui.status((b'analysing phase of %s\n') % util.hidepassword(dest))
1310 ui.status(b'analysing phase of %s\n' % util.hidepassword(dest))
1109 other = hg.peer(repo, opts, dest)
1311 other = hg.peer(repo, opts, dest)
1110
1312
1111 # easier to perform discovery through the operation
1313 # easier to perform discovery through the operation
@@ -1115,36 +1317,43 b' def perfphasesremote(ui, repo, dest=None'
1115 remotesubset = op.fallbackheads
1317 remotesubset = op.fallbackheads
1116
1318
1117 with other.commandexecutor() as e:
1319 with other.commandexecutor() as e:
1118 remotephases = e.callcommand(b'listkeys',
1320 remotephases = e.callcommand(
1119 {b'namespace': b'phases'}).result()
1321 b'listkeys', {b'namespace': b'phases'}
1322 ).result()
1120 del other
1323 del other
1121 publishing = remotephases.get(b'publishing', False)
1324 publishing = remotephases.get(b'publishing', False)
1122 if publishing:
1325 if publishing:
1123 ui.status((b'publishing: yes\n'))
1326 ui.status(b'publishing: yes\n')
1124 else:
1327 else:
1125 ui.status((b'publishing: no\n'))
1328 ui.status(b'publishing: no\n')
1126
1329
1127 nodemap = repo.changelog.nodemap
1330 nodemap = repo.changelog.nodemap
1128 nonpublishroots = 0
1331 nonpublishroots = 0
1129 for nhex, phase in remotephases.iteritems():
1332 for nhex, phase in remotephases.iteritems():
1130 if nhex == b'publishing': # ignore data related to publish option
1333 if nhex == b'publishing': # ignore data related to publish option
1131 continue
1334 continue
1132 node = bin(nhex)
1335 node = bin(nhex)
1133 if node in nodemap and int(phase):
1336 if node in nodemap and int(phase):
1134 nonpublishroots += 1
1337 nonpublishroots += 1
1135 ui.status((b'number of roots: %d\n') % len(remotephases))
1338 ui.status(b'number of roots: %d\n' % len(remotephases))
1136 ui.status((b'number of known non public roots: %d\n') % nonpublishroots)
1339 ui.status(b'number of known non public roots: %d\n' % nonpublishroots)
1340
1137 def d():
1341 def d():
1138 phases.remotephasessummary(repo,
1342 phases.remotephasessummary(repo, remotesubset, remotephases)
1139 remotesubset,
1343
1140 remotephases)
1141 timer(d)
1344 timer(d)
1142 fm.end()
1345 fm.end()
1143
1346
1144 @command(b'perfmanifest',[
1347
1145 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1348 @command(
1146 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1349 b'perfmanifest',
1147 ] + formatteropts, b'REV|NODE')
1350 [
1351 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1352 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1353 ]
1354 + formatteropts,
1355 b'REV|NODE',
1356 )
1148 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1357 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1149 """benchmark the time to read a manifest from disk and return a usable
1358 """benchmark the time to read a manifest from disk and return a usable
1150 dict-like object
1359 dict-like object
@@ -1169,25 +1378,32 b' def perfmanifest(ui, repo, rev, manifest'
1169 else:
1378 else:
1170 t = repo.manifestlog._revlog.lookup(rev)
1379 t = repo.manifestlog._revlog.lookup(rev)
1171 except ValueError:
1380 except ValueError:
1172 raise error.Abort(b'manifest revision must be integer or full '
1381 raise error.Abort(
1173 b'node')
1382 b'manifest revision must be integer or full ' b'node'
1383 )
1384
1174 def d():
1385 def d():
1175 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1386 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1176 repo.manifestlog[t].read()
1387 repo.manifestlog[t].read()
1388
1177 timer(d)
1389 timer(d)
1178 fm.end()
1390 fm.end()
1179
1391
1392
1180 @command(b'perfchangeset', formatteropts)
1393 @command(b'perfchangeset', formatteropts)
1181 def perfchangeset(ui, repo, rev, **opts):
1394 def perfchangeset(ui, repo, rev, **opts):
1182 opts = _byteskwargs(opts)
1395 opts = _byteskwargs(opts)
1183 timer, fm = gettimer(ui, opts)
1396 timer, fm = gettimer(ui, opts)
1184 n = scmutil.revsingle(repo, rev).node()
1397 n = scmutil.revsingle(repo, rev).node()
1398
1185 def d():
1399 def d():
1186 repo.changelog.read(n)
1400 repo.changelog.read(n)
1187 #repo.changelog._cache = None
1401 # repo.changelog._cache = None
1402
1188 timer(d)
1403 timer(d)
1189 fm.end()
1404 fm.end()
1190
1405
1406
1191 @command(b'perfignore', formatteropts)
1407 @command(b'perfignore', formatteropts)
1192 def perfignore(ui, repo, **opts):
1408 def perfignore(ui, repo, **opts):
1193 """benchmark operation related to computing ignore"""
1409 """benchmark operation related to computing ignore"""
@@ -1205,10 +1421,15 b' def perfignore(ui, repo, **opts):'
1205 timer(runone, setup=setupone, title=b"load")
1421 timer(runone, setup=setupone, title=b"load")
1206 fm.end()
1422 fm.end()
1207
1423
1208 @command(b'perfindex', [
1424
1209 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1425 @command(
1210 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1426 b'perfindex',
1211 ] + formatteropts)
1427 [
1428 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1429 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1430 ]
1431 + formatteropts,
1432 )
1212 def perfindex(ui, repo, **opts):
1433 def perfindex(ui, repo, **opts):
1213 """benchmark index creation time followed by a lookup
1434 """benchmark index creation time followed by a lookup
1214
1435
@@ -1231,9 +1452,10 b' def perfindex(ui, repo, **opts):'
1231 It is not currently possible to check for lookup of a missing node. For
1452 It is not currently possible to check for lookup of a missing node. For
1232 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1453 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1233 import mercurial.revlog
1454 import mercurial.revlog
1455
1234 opts = _byteskwargs(opts)
1456 opts = _byteskwargs(opts)
1235 timer, fm = gettimer(ui, opts)
1457 timer, fm = gettimer(ui, opts)
1236 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
1458 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1237 if opts[b'no_lookup']:
1459 if opts[b'no_lookup']:
1238 if opts['rev']:
1460 if opts['rev']:
1239 raise error.Abort('--no-lookup and --rev are mutually exclusive')
1461 raise error.Abort('--no-lookup and --rev are mutually exclusive')
@@ -1249,20 +1471,28 b' def perfindex(ui, repo, **opts):'
1249 # find the filecache func directly
1471 # find the filecache func directly
1250 # This avoid polluting the benchmark with the filecache logic
1472 # This avoid polluting the benchmark with the filecache logic
1251 makecl = unfi.__class__.changelog.func
1473 makecl = unfi.__class__.changelog.func
1474
1252 def setup():
1475 def setup():
1253 # probably not necessary, but for good measure
1476 # probably not necessary, but for good measure
1254 clearchangelog(unfi)
1477 clearchangelog(unfi)
1478
1255 def d():
1479 def d():
1256 cl = makecl(unfi)
1480 cl = makecl(unfi)
1257 for n in nodes:
1481 for n in nodes:
1258 cl.rev(n)
1482 cl.rev(n)
1483
1259 timer(d, setup=setup)
1484 timer(d, setup=setup)
1260 fm.end()
1485 fm.end()
1261
1486
1262 @command(b'perfnodemap', [
1487
1263 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1488 @command(
1264 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1489 b'perfnodemap',
1265 ] + formatteropts)
1490 [
1491 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1492 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1493 ]
1494 + formatteropts,
1495 )
1266 def perfnodemap(ui, repo, **opts):
1496 def perfnodemap(ui, repo, **opts):
1267 """benchmark the time necessary to look up revision from a cold nodemap
1497 """benchmark the time necessary to look up revision from a cold nodemap
1268
1498
@@ -1281,9 +1511,10 b' def perfnodemap(ui, repo, **opts):'
1281 hexlookup, prefix lookup and missing lookup would also be valuable.
1511 hexlookup, prefix lookup and missing lookup would also be valuable.
1282 """
1512 """
1283 import mercurial.revlog
1513 import mercurial.revlog
1514
1284 opts = _byteskwargs(opts)
1515 opts = _byteskwargs(opts)
1285 timer, fm = gettimer(ui, opts)
1516 timer, fm = gettimer(ui, opts)
1286 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
1517 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1287
1518
1288 unfi = repo.unfiltered()
1519 unfi = repo.unfiltered()
1289 clearcaches = opts['clear_caches']
1520 clearcaches = opts['clear_caches']
@@ -1298,6 +1529,7 b' def perfnodemap(ui, repo, **opts):'
1298
1529
1299 # use a list to pass reference to a nodemap from one closure to the next
1530 # use a list to pass reference to a nodemap from one closure to the next
1300 nodeget = [None]
1531 nodeget = [None]
1532
1301 def setnodeget():
1533 def setnodeget():
1302 # probably not necessary, but for good measure
1534 # probably not necessary, but for good measure
1303 clearchangelog(unfi)
1535 clearchangelog(unfi)
@@ -1310,28 +1542,35 b' def perfnodemap(ui, repo, **opts):'
1310
1542
1311 setup = None
1543 setup = None
1312 if clearcaches:
1544 if clearcaches:
1545
1313 def setup():
1546 def setup():
1314 setnodeget()
1547 setnodeget()
1548
1315 else:
1549 else:
1316 setnodeget()
1550 setnodeget()
1317 d() # prewarm the data structure
1551 d() # prewarm the data structure
1318 timer(d, setup=setup)
1552 timer(d, setup=setup)
1319 fm.end()
1553 fm.end()
1320
1554
1555
1321 @command(b'perfstartup', formatteropts)
1556 @command(b'perfstartup', formatteropts)
1322 def perfstartup(ui, repo, **opts):
1557 def perfstartup(ui, repo, **opts):
1323 opts = _byteskwargs(opts)
1558 opts = _byteskwargs(opts)
1324 timer, fm = gettimer(ui, opts)
1559 timer, fm = gettimer(ui, opts)
1560
1325 def d():
1561 def d():
1326 if os.name != r'nt':
1562 if os.name != r'nt':
1327 os.system(b"HGRCPATH= %s version -q > /dev/null" %
1563 os.system(
1328 fsencode(sys.argv[0]))
1564 b"HGRCPATH= %s version -q > /dev/null" % fsencode(sys.argv[0])
1565 )
1329 else:
1566 else:
1330 os.environ[r'HGRCPATH'] = r' '
1567 os.environ[r'HGRCPATH'] = r' '
1331 os.system(r"%s version -q > NUL" % sys.argv[0])
1568 os.system(r"%s version -q > NUL" % sys.argv[0])
1569
1332 timer(d)
1570 timer(d)
1333 fm.end()
1571 fm.end()
1334
1572
1573
1335 @command(b'perfparents', formatteropts)
1574 @command(b'perfparents', formatteropts)
1336 def perfparents(ui, repo, **opts):
1575 def perfparents(ui, repo, **opts):
1337 """benchmark the time necessary to fetch one changeset's parents.
1576 """benchmark the time necessary to fetch one changeset's parents.
@@ -1350,33 +1589,42 b' def perfparents(ui, repo, **opts):'
1350 raise error.Abort(b"repo needs %d commits for this test" % count)
1589 raise error.Abort(b"repo needs %d commits for this test" % count)
1351 repo = repo.unfiltered()
1590 repo = repo.unfiltered()
1352 nl = [repo.changelog.node(i) for i in _xrange(count)]
1591 nl = [repo.changelog.node(i) for i in _xrange(count)]
1592
1353 def d():
1593 def d():
1354 for n in nl:
1594 for n in nl:
1355 repo.changelog.parents(n)
1595 repo.changelog.parents(n)
1596
1356 timer(d)
1597 timer(d)
1357 fm.end()
1598 fm.end()
1358
1599
1600
1359 @command(b'perfctxfiles', formatteropts)
1601 @command(b'perfctxfiles', formatteropts)
1360 def perfctxfiles(ui, repo, x, **opts):
1602 def perfctxfiles(ui, repo, x, **opts):
1361 opts = _byteskwargs(opts)
1603 opts = _byteskwargs(opts)
1362 x = int(x)
1604 x = int(x)
1363 timer, fm = gettimer(ui, opts)
1605 timer, fm = gettimer(ui, opts)
1606
1364 def d():
1607 def d():
1365 len(repo[x].files())
1608 len(repo[x].files())
1609
1366 timer(d)
1610 timer(d)
1367 fm.end()
1611 fm.end()
1368
1612
1613
1369 @command(b'perfrawfiles', formatteropts)
1614 @command(b'perfrawfiles', formatteropts)
1370 def perfrawfiles(ui, repo, x, **opts):
1615 def perfrawfiles(ui, repo, x, **opts):
1371 opts = _byteskwargs(opts)
1616 opts = _byteskwargs(opts)
1372 x = int(x)
1617 x = int(x)
1373 timer, fm = gettimer(ui, opts)
1618 timer, fm = gettimer(ui, opts)
1374 cl = repo.changelog
1619 cl = repo.changelog
1620
1375 def d():
1621 def d():
1376 len(cl.read(x)[3])
1622 len(cl.read(x)[3])
1623
1377 timer(d)
1624 timer(d)
1378 fm.end()
1625 fm.end()
1379
1626
1627
1380 @command(b'perflookup', formatteropts)
1628 @command(b'perflookup', formatteropts)
1381 def perflookup(ui, repo, rev, **opts):
1629 def perflookup(ui, repo, rev, **opts):
1382 opts = _byteskwargs(opts)
1630 opts = _byteskwargs(opts)
@@ -1384,10 +1632,15 b' def perflookup(ui, repo, rev, **opts):'
1384 timer(lambda: len(repo.lookup(rev)))
1632 timer(lambda: len(repo.lookup(rev)))
1385 fm.end()
1633 fm.end()
1386
1634
1387 @command(b'perflinelogedits',
1635
1388 [(b'n', b'edits', 10000, b'number of edits'),
1636 @command(
1389 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
1637 b'perflinelogedits',
1390 ], norepo=True)
1638 [
1639 (b'n', b'edits', 10000, b'number of edits'),
1640 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
1641 ],
1642 norepo=True,
1643 )
1391 def perflinelogedits(ui, **opts):
1644 def perflinelogedits(ui, **opts):
1392 from mercurial import linelog
1645 from mercurial import linelog
1393
1646
@@ -1418,6 +1671,7 b' def perflinelogedits(ui, **opts):'
1418 timer(d)
1671 timer(d)
1419 fm.end()
1672 fm.end()
1420
1673
1674
1421 @command(b'perfrevrange', formatteropts)
1675 @command(b'perfrevrange', formatteropts)
1422 def perfrevrange(ui, repo, *specs, **opts):
1676 def perfrevrange(ui, repo, *specs, **opts):
1423 opts = _byteskwargs(opts)
1677 opts = _byteskwargs(opts)
@@ -1426,34 +1680,44 b' def perfrevrange(ui, repo, *specs, **opt'
1426 timer(lambda: len(revrange(repo, specs)))
1680 timer(lambda: len(revrange(repo, specs)))
1427 fm.end()
1681 fm.end()
1428
1682
1683
1429 @command(b'perfnodelookup', formatteropts)
1684 @command(b'perfnodelookup', formatteropts)
1430 def perfnodelookup(ui, repo, rev, **opts):
1685 def perfnodelookup(ui, repo, rev, **opts):
1431 opts = _byteskwargs(opts)
1686 opts = _byteskwargs(opts)
1432 timer, fm = gettimer(ui, opts)
1687 timer, fm = gettimer(ui, opts)
1433 import mercurial.revlog
1688 import mercurial.revlog
1434 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
1689
1690 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1435 n = scmutil.revsingle(repo, rev).node()
1691 n = scmutil.revsingle(repo, rev).node()
1436 cl = mercurial.revlog.revlog(getsvfs(repo), b"00changelog.i")
1692 cl = mercurial.revlog.revlog(getsvfs(repo), b"00changelog.i")
1693
1437 def d():
1694 def d():
1438 cl.rev(n)
1695 cl.rev(n)
1439 clearcaches(cl)
1696 clearcaches(cl)
1697
1440 timer(d)
1698 timer(d)
1441 fm.end()
1699 fm.end()
1442
1700
1443 @command(b'perflog',
1701
1444 [(b'', b'rename', False, b'ask log to follow renames')
1702 @command(
1445 ] + formatteropts)
1703 b'perflog',
1704 [(b'', b'rename', False, b'ask log to follow renames')] + formatteropts,
1705 )
1446 def perflog(ui, repo, rev=None, **opts):
1706 def perflog(ui, repo, rev=None, **opts):
1447 opts = _byteskwargs(opts)
1707 opts = _byteskwargs(opts)
1448 if rev is None:
1708 if rev is None:
1449 rev=[]
1709 rev = []
1450 timer, fm = gettimer(ui, opts)
1710 timer, fm = gettimer(ui, opts)
1451 ui.pushbuffer()
1711 ui.pushbuffer()
1452 timer(lambda: commands.log(ui, repo, rev=rev, date=b'', user=b'',
1712 timer(
1453 copies=opts.get(b'rename')))
1713 lambda: commands.log(
1714 ui, repo, rev=rev, date=b'', user=b'', copies=opts.get(b'rename')
1715 )
1716 )
1454 ui.popbuffer()
1717 ui.popbuffer()
1455 fm.end()
1718 fm.end()
1456
1719
1720
1457 @command(b'perfmoonwalk', formatteropts)
1721 @command(b'perfmoonwalk', formatteropts)
1458 def perfmoonwalk(ui, repo, **opts):
1722 def perfmoonwalk(ui, repo, **opts):
1459 """benchmark walking the changelog backwards
1723 """benchmark walking the changelog backwards
@@ -1462,21 +1726,27 b' def perfmoonwalk(ui, repo, **opts):'
1462 """
1726 """
1463 opts = _byteskwargs(opts)
1727 opts = _byteskwargs(opts)
1464 timer, fm = gettimer(ui, opts)
1728 timer, fm = gettimer(ui, opts)
1729
1465 def moonwalk():
1730 def moonwalk():
1466 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
1731 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
1467 ctx = repo[i]
1732 ctx = repo[i]
1468 ctx.branch() # read changelog data (in addition to the index)
1733 ctx.branch() # read changelog data (in addition to the index)
1734
1469 timer(moonwalk)
1735 timer(moonwalk)
1470 fm.end()
1736 fm.end()
1471
1737
1472 @command(b'perftemplating',
1738
1473 [(b'r', b'rev', [], b'revisions to run the template on'),
1739 @command(
1474 ] + formatteropts)
1740 b'perftemplating',
1741 [(b'r', b'rev', [], b'revisions to run the template on'),] + formatteropts,
1742 )
1475 def perftemplating(ui, repo, testedtemplate=None, **opts):
1743 def perftemplating(ui, repo, testedtemplate=None, **opts):
1476 """test the rendering time of a given template"""
1744 """test the rendering time of a given template"""
1477 if makelogtemplater is None:
1745 if makelogtemplater is None:
1478 raise error.Abort((b"perftemplating not available with this Mercurial"),
1746 raise error.Abort(
1479 hint=b"use 4.3 or later")
1747 b"perftemplating not available with this Mercurial",
1748 hint=b"use 4.3 or later",
1749 )
1480
1750
1481 opts = _byteskwargs(opts)
1751 opts = _byteskwargs(opts)
1482
1752
@@ -1488,11 +1758,14 b' def perftemplating(ui, repo, testedtempl'
1488 revs = [b'all()']
1758 revs = [b'all()']
1489 revs = list(scmutil.revrange(repo, revs))
1759 revs = list(scmutil.revrange(repo, revs))
1490
1760
1491 defaulttemplate = (b'{date|shortdate} [{rev}:{node|short}]'
1761 defaulttemplate = (
1492 b' {author|person}: {desc|firstline}\n')
1762 b'{date|shortdate} [{rev}:{node|short}]'
1763 b' {author|person}: {desc|firstline}\n'
1764 )
1493 if testedtemplate is None:
1765 if testedtemplate is None:
1494 testedtemplate = defaulttemplate
1766 testedtemplate = defaulttemplate
1495 displayer = makelogtemplater(nullui, repo, testedtemplate)
1767 displayer = makelogtemplater(nullui, repo, testedtemplate)
1768
1496 def format():
1769 def format():
1497 for r in revs:
1770 for r in revs:
1498 ctx = repo[r]
1771 ctx = repo[r]
@@ -1503,6 +1776,7 b' def perftemplating(ui, repo, testedtempl'
1503 timer(format)
1776 timer(format)
1504 fm.end()
1777 fm.end()
1505
1778
1779
1506 def _displaystats(ui, opts, entries, data):
1780 def _displaystats(ui, opts, entries, data):
1507 pass
1781 pass
1508 # use a second formatter because the data are quite different, not sure
1782 # use a second formatter because the data are quite different, not sure
@@ -1549,12 +1823,16 b' def _displaystats(ui, opts, entries, dat'
1549 fm.plain('%s: %s\n' % (l, stats[l]))
1823 fm.plain('%s: %s\n' % (l, stats[l]))
1550 fm.end()
1824 fm.end()
1551
1825
1552 @command(b'perfhelper-mergecopies', formatteropts +
1826
1553 [
1827 @command(
1554 (b'r', b'revs', [], b'restrict search to these revisions'),
1828 b'perfhelper-mergecopies',
1555 (b'', b'timing', False, b'provides extra data (costly)'),
1829 formatteropts
1556 (b'', b'stats', False, b'provides statistic about the measured data'),
1830 + [
1557 ])
1831 (b'r', b'revs', [], b'restrict search to these revisions'),
1832 (b'', b'timing', False, b'provides extra data (costly)'),
1833 (b'', b'stats', False, b'provides statistic about the measured data'),
1834 ],
1835 )
1558 def perfhelpermergecopies(ui, repo, revs=[], **opts):
1836 def perfhelpermergecopies(ui, repo, revs=[], **opts):
1559 """find statistics about potential parameters for `perfmergecopies`
1837 """find statistics about potential parameters for `perfmergecopies`
1560
1838
@@ -1589,10 +1867,13 b' def perfhelpermergecopies(ui, repo, revs'
1589 ("p2.time", "%(p2.time)12.3f"),
1867 ("p2.time", "%(p2.time)12.3f"),
1590 ("renames", "%(nbrenamedfiles)12d"),
1868 ("renames", "%(nbrenamedfiles)12d"),
1591 ("total.time", "%(time)12.3f"),
1869 ("total.time", "%(time)12.3f"),
1592 ]
1870 ]
1593 if not dotiming:
1871 if not dotiming:
1594 output_template = [i for i in output_template
1872 output_template = [
1595 if not ('time' in i[0] or 'renames' in i[0])]
1873 i
1874 for i in output_template
1875 if not ('time' in i[0] or 'renames' in i[0])
1876 ]
1596 header_names = [h for (h, v) in output_template]
1877 header_names = [h for (h, v) in output_template]
1597 output = ' '.join([v for (h, v) in output_template]) + '\n'
1878 output = ' '.join([v for (h, v) in output_template]) + '\n'
1598 header = ' '.join(['%12s'] * len(header_names)) + '\n'
1879 header = ' '.join(['%12s'] * len(header_names)) + '\n'
@@ -1634,27 +1915,19 b' def perfhelpermergecopies(ui, repo, revs'
1634 }
1915 }
1635 if dostats:
1916 if dostats:
1636 if p1missing:
1917 if p1missing:
1637 alldata['nbrevs'].append((
1918 alldata['nbrevs'].append(
1638 data['p1.nbrevs'],
1919 (data['p1.nbrevs'], b.hex(), p1.hex())
1639 b.hex(),
1920 )
1640 p1.hex()
1921 alldata['nbmissingfiles'].append(
1641 ))
1922 (data['p1.nbmissingfiles'], b.hex(), p1.hex())
1642 alldata['nbmissingfiles'].append((
1923 )
1643 data['p1.nbmissingfiles'],
1644 b.hex(),
1645 p1.hex()
1646 ))
1647 if p2missing:
1924 if p2missing:
1648 alldata['nbrevs'].append((
1925 alldata['nbrevs'].append(
1649 data['p2.nbrevs'],
1926 (data['p2.nbrevs'], b.hex(), p2.hex())
1650 b.hex(),
1927 )
1651 p2.hex()
1928 alldata['nbmissingfiles'].append(
1652 ))
1929 (data['p2.nbmissingfiles'], b.hex(), p2.hex())
1653 alldata['nbmissingfiles'].append((
1930 )
1654 data['p2.nbmissingfiles'],
1655 b.hex(),
1656 p2.hex()
1657 ))
1658 if dotiming:
1931 if dotiming:
1659 begin = util.timer()
1932 begin = util.timer()
1660 mergedata = copies.mergecopies(repo, p1, p2, b)
1933 mergedata = copies.mergecopies(repo, p1, p2, b)
@@ -1682,40 +1955,31 b' def perfhelpermergecopies(ui, repo, revs'
1682
1955
1683 if dostats:
1956 if dostats:
1684 if p1missing:
1957 if p1missing:
1685 alldata['parentnbrenames'].append((
1958 alldata['parentnbrenames'].append(
1686 data['p1.renamedfiles'],
1959 (data['p1.renamedfiles'], b.hex(), p1.hex())
1687 b.hex(),
1960 )
1688 p1.hex()
1961 alldata['parenttime'].append(
1689 ))
1962 (data['p1.time'], b.hex(), p1.hex())
1690 alldata['parenttime'].append((
1963 )
1691 data['p1.time'],
1692 b.hex(),
1693 p1.hex()
1694 ))
1695 if p2missing:
1964 if p2missing:
1696 alldata['parentnbrenames'].append((
1965 alldata['parentnbrenames'].append(
1697 data['p2.renamedfiles'],
1966 (data['p2.renamedfiles'], b.hex(), p2.hex())
1698 b.hex(),
1967 )
1699 p2.hex()
1968 alldata['parenttime'].append(
1700 ))
1969 (data['p2.time'], b.hex(), p2.hex())
1701 alldata['parenttime'].append((
1970 )
1702 data['p2.time'],
1703 b.hex(),
1704 p2.hex()
1705 ))
1706 if p1missing or p2missing:
1971 if p1missing or p2missing:
1707 alldata['totalnbrenames'].append((
1972 alldata['totalnbrenames'].append(
1708 data['nbrenamedfiles'],
1973 (
1709 b.hex(),
1974 data['nbrenamedfiles'],
1710 p1.hex(),
1975 b.hex(),
1711 p2.hex()
1976 p1.hex(),
1712 ))
1977 p2.hex(),
1713 alldata['totaltime'].append((
1978 )
1714 data['time'],
1979 )
1715 b.hex(),
1980 alldata['totaltime'].append(
1716 p1.hex(),
1981 (data['time'], b.hex(), p1.hex(), p2.hex())
1717 p2.hex()
1982 )
1718 ))
1719 fm.startitem()
1983 fm.startitem()
1720 fm.data(**data)
1984 fm.data(**data)
1721 # make node pretty for the human output
1985 # make node pretty for the human output
@@ -1734,20 +1998,24 b' def perfhelpermergecopies(ui, repo, revs'
1734 ('nbmissingfiles', 'number of missing files at head'),
1998 ('nbmissingfiles', 'number of missing files at head'),
1735 ]
1999 ]
1736 if dotiming:
2000 if dotiming:
1737 entries.append(('parentnbrenames',
2001 entries.append(
1738 'rename from one parent to base'))
2002 ('parentnbrenames', 'rename from one parent to base')
2003 )
1739 entries.append(('totalnbrenames', 'total number of renames'))
2004 entries.append(('totalnbrenames', 'total number of renames'))
1740 entries.append(('parenttime', 'time for one parent'))
2005 entries.append(('parenttime', 'time for one parent'))
1741 entries.append(('totaltime', 'time for both parents'))
2006 entries.append(('totaltime', 'time for both parents'))
1742 _displaystats(ui, opts, entries, alldata)
2007 _displaystats(ui, opts, entries, alldata)
1743
2008
1744
2009
1745 @command(b'perfhelper-pathcopies', formatteropts +
2010 @command(
1746 [
2011 b'perfhelper-pathcopies',
1747 (b'r', b'revs', [], b'restrict search to these revisions'),
2012 formatteropts
1748 (b'', b'timing', False, b'provides extra data (costly)'),
2013 + [
1749 (b'', b'stats', False, b'provides statistic about the measured data'),
2014 (b'r', b'revs', [], b'restrict search to these revisions'),
1750 ])
2015 (b'', b'timing', False, b'provides extra data (costly)'),
2016 (b'', b'stats', False, b'provides statistic about the measured data'),
2017 ],
2018 )
1751 def perfhelperpathcopies(ui, repo, revs=[], **opts):
2019 def perfhelperpathcopies(ui, repo, revs=[], **opts):
1752 """find statistic about potential parameters for the `perftracecopies`
2020 """find statistic about potential parameters for the `perftracecopies`
1753
2021
@@ -1769,23 +2037,32 b' def perfhelperpathcopies(ui, repo, revs='
1769
2037
1770 if dotiming:
2038 if dotiming:
1771 header = '%12s %12s %12s %12s %12s %12s\n'
2039 header = '%12s %12s %12s %12s %12s %12s\n'
1772 output = ("%(source)12s %(destination)12s "
2040 output = (
1773 "%(nbrevs)12d %(nbmissingfiles)12d "
2041 "%(source)12s %(destination)12s "
1774 "%(nbrenamedfiles)12d %(time)18.5f\n")
2042 "%(nbrevs)12d %(nbmissingfiles)12d "
1775 header_names = ("source", "destination", "nb-revs", "nb-files",
2043 "%(nbrenamedfiles)12d %(time)18.5f\n"
1776 "nb-renames", "time")
2044 )
2045 header_names = (
2046 "source",
2047 "destination",
2048 "nb-revs",
2049 "nb-files",
2050 "nb-renames",
2051 "time",
2052 )
1777 fm.plain(header % header_names)
2053 fm.plain(header % header_names)
1778 else:
2054 else:
1779 header = '%12s %12s %12s %12s\n'
2055 header = '%12s %12s %12s %12s\n'
1780 output = ("%(source)12s %(destination)12s "
2056 output = (
1781 "%(nbrevs)12d %(nbmissingfiles)12d\n")
2057 "%(source)12s %(destination)12s "
2058 "%(nbrevs)12d %(nbmissingfiles)12d\n"
2059 )
1782 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
2060 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
1783
2061
1784 if not revs:
2062 if not revs:
1785 revs = ['all()']
2063 revs = ['all()']
1786 revs = scmutil.revrange(repo, revs)
2064 revs = scmutil.revrange(repo, revs)
1787
2065
1788
1789 if dostats:
2066 if dostats:
1790 alldata = {
2067 alldata = {
1791 'nbrevs': [],
2068 'nbrevs': [],
@@ -1815,16 +2092,12 b' def perfhelperpathcopies(ui, repo, revs='
1815 b'nbmissingfiles': len(missing),
2092 b'nbmissingfiles': len(missing),
1816 }
2093 }
1817 if dostats:
2094 if dostats:
1818 alldata['nbrevs'].append((
2095 alldata['nbrevs'].append(
1819 data['nbrevs'],
2096 (data['nbrevs'], base.hex(), parent.hex(),)
1820 base.hex(),
2097 )
1821 parent.hex(),
2098 alldata['nbmissingfiles'].append(
1822 ))
2099 (data['nbmissingfiles'], base.hex(), parent.hex(),)
1823 alldata['nbmissingfiles'].append((
2100 )
1824 data['nbmissingfiles'],
1825 base.hex(),
1826 parent.hex(),
1827 ))
1828 if dotiming:
2101 if dotiming:
1829 begin = util.timer()
2102 begin = util.timer()
1830 renames = copies.pathcopies(base, parent)
2103 renames = copies.pathcopies(base, parent)
@@ -1833,16 +2106,12 b' def perfhelperpathcopies(ui, repo, revs='
1833 data['time'] = end - begin
2106 data['time'] = end - begin
1834 data['nbrenamedfiles'] = len(renames)
2107 data['nbrenamedfiles'] = len(renames)
1835 if dostats:
2108 if dostats:
1836 alldata['time'].append((
2109 alldata['time'].append(
1837 data['time'],
2110 (data['time'], base.hex(), parent.hex(),)
1838 base.hex(),
2111 )
1839 parent.hex(),
2112 alldata['nbrenames'].append(
1840 ))
2113 (data['nbrenamedfiles'], base.hex(), parent.hex(),)
1841 alldata['nbrenames'].append((
2114 )
1842 data['nbrenamedfiles'],
1843 base.hex(),
1844 parent.hex(),
1845 ))
1846 fm.startitem()
2115 fm.startitem()
1847 fm.data(**data)
2116 fm.data(**data)
1848 out = data.copy()
2117 out = data.copy()
@@ -1860,11 +2129,11 b' def perfhelperpathcopies(ui, repo, revs='
1860 ('nbmissingfiles', 'number of missing files at head'),
2129 ('nbmissingfiles', 'number of missing files at head'),
1861 ]
2130 ]
1862 if dotiming:
2131 if dotiming:
1863 entries.append(('nbrenames',
2132 entries.append(('nbrenames', 'renamed files'))
1864 'renamed files'))
1865 entries.append(('time', 'time'))
2133 entries.append(('time', 'time'))
1866 _displaystats(ui, opts, entries, alldata)
2134 _displaystats(ui, opts, entries, alldata)
1867
2135
2136
1868 @command(b'perfcca', formatteropts)
2137 @command(b'perfcca', formatteropts)
1869 def perfcca(ui, repo, **opts):
2138 def perfcca(ui, repo, **opts):
1870 opts = _byteskwargs(opts)
2139 opts = _byteskwargs(opts)
@@ -1872,16 +2141,20 b' def perfcca(ui, repo, **opts):'
1872 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
2141 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
1873 fm.end()
2142 fm.end()
1874
2143
2144
1875 @command(b'perffncacheload', formatteropts)
2145 @command(b'perffncacheload', formatteropts)
1876 def perffncacheload(ui, repo, **opts):
2146 def perffncacheload(ui, repo, **opts):
1877 opts = _byteskwargs(opts)
2147 opts = _byteskwargs(opts)
1878 timer, fm = gettimer(ui, opts)
2148 timer, fm = gettimer(ui, opts)
1879 s = repo.store
2149 s = repo.store
2150
1880 def d():
2151 def d():
1881 s.fncache._load()
2152 s.fncache._load()
2153
1882 timer(d)
2154 timer(d)
1883 fm.end()
2155 fm.end()
1884
2156
2157
1885 @command(b'perffncachewrite', formatteropts)
2158 @command(b'perffncachewrite', formatteropts)
1886 def perffncachewrite(ui, repo, **opts):
2159 def perffncachewrite(ui, repo, **opts):
1887 opts = _byteskwargs(opts)
2160 opts = _byteskwargs(opts)
@@ -1891,26 +2164,32 b' def perffncachewrite(ui, repo, **opts):'
1891 s.fncache._load()
2164 s.fncache._load()
1892 tr = repo.transaction(b'perffncachewrite')
2165 tr = repo.transaction(b'perffncachewrite')
1893 tr.addbackup(b'fncache')
2166 tr.addbackup(b'fncache')
2167
1894 def d():
2168 def d():
1895 s.fncache._dirty = True
2169 s.fncache._dirty = True
1896 s.fncache.write(tr)
2170 s.fncache.write(tr)
2171
1897 timer(d)
2172 timer(d)
1898 tr.close()
2173 tr.close()
1899 lock.release()
2174 lock.release()
1900 fm.end()
2175 fm.end()
1901
2176
2177
1902 @command(b'perffncacheencode', formatteropts)
2178 @command(b'perffncacheencode', formatteropts)
1903 def perffncacheencode(ui, repo, **opts):
2179 def perffncacheencode(ui, repo, **opts):
1904 opts = _byteskwargs(opts)
2180 opts = _byteskwargs(opts)
1905 timer, fm = gettimer(ui, opts)
2181 timer, fm = gettimer(ui, opts)
1906 s = repo.store
2182 s = repo.store
1907 s.fncache._load()
2183 s.fncache._load()
2184
1908 def d():
2185 def d():
1909 for p in s.fncache.entries:
2186 for p in s.fncache.entries:
1910 s.encode(p)
2187 s.encode(p)
2188
1911 timer(d)
2189 timer(d)
1912 fm.end()
2190 fm.end()
1913
2191
2192
1914 def _bdiffworker(q, blocks, xdiff, ready, done):
2193 def _bdiffworker(q, blocks, xdiff, ready, done):
1915 while not done.is_set():
2194 while not done.is_set():
1916 pair = q.get()
2195 pair = q.get()
@@ -1923,10 +2202,11 b' def _bdiffworker(q, blocks, xdiff, ready'
1923 mdiff.textdiff(*pair)
2202 mdiff.textdiff(*pair)
1924 q.task_done()
2203 q.task_done()
1925 pair = q.get()
2204 pair = q.get()
1926 q.task_done() # for the None one
2205 q.task_done() # for the None one
1927 with ready:
2206 with ready:
1928 ready.wait()
2207 ready.wait()
1929
2208
2209
1930 def _manifestrevision(repo, mnode):
2210 def _manifestrevision(repo, mnode):
1931 ml = repo.manifestlog
2211 ml = repo.manifestlog
1932
2212
@@ -1937,15 +2217,25 b' def _manifestrevision(repo, mnode):'
1937
2217
1938 return store.revision(mnode)
2218 return store.revision(mnode)
1939
2219
1940 @command(b'perfbdiff', revlogopts + formatteropts + [
2220
1941 (b'', b'count', 1, b'number of revisions to test (when using --startrev)'),
2221 @command(
1942 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
2222 b'perfbdiff',
1943 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
2223 revlogopts
1944 (b'', b'blocks', False, b'test computing diffs into blocks'),
2224 + formatteropts
1945 (b'', b'xdiff', False, b'use xdiff algorithm'),
2225 + [
2226 (
2227 b'',
2228 b'count',
2229 1,
2230 b'number of revisions to test (when using --startrev)',
2231 ),
2232 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
2233 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
2234 (b'', b'blocks', False, b'test computing diffs into blocks'),
2235 (b'', b'xdiff', False, b'use xdiff algorithm'),
1946 ],
2236 ],
1947
2237 b'-c|-m|FILE REV',
1948 b'-c|-m|FILE REV')
2238 )
1949 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
2239 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
1950 """benchmark a bdiff between revisions
2240 """benchmark a bdiff between revisions
1951
2241
@@ -2001,6 +2291,7 b' def perfbdiff(ui, repo, file_, rev=None,'
2001
2291
2002 withthreads = threads > 0
2292 withthreads = threads > 0
2003 if not withthreads:
2293 if not withthreads:
2294
2004 def d():
2295 def d():
2005 for pair in textpairs:
2296 for pair in textpairs:
2006 if xdiff:
2297 if xdiff:
@@ -2009,6 +2300,7 b' def perfbdiff(ui, repo, file_, rev=None,'
2009 mdiff.bdiff.blocks(*pair)
2300 mdiff.bdiff.blocks(*pair)
2010 else:
2301 else:
2011 mdiff.textdiff(*pair)
2302 mdiff.textdiff(*pair)
2303
2012 else:
2304 else:
2013 q = queue()
2305 q = queue()
2014 for i in _xrange(threads):
2306 for i in _xrange(threads):
@@ -2016,9 +2308,11 b' def perfbdiff(ui, repo, file_, rev=None,'
2016 ready = threading.Condition()
2308 ready = threading.Condition()
2017 done = threading.Event()
2309 done = threading.Event()
2018 for i in _xrange(threads):
2310 for i in _xrange(threads):
2019 threading.Thread(target=_bdiffworker,
2311 threading.Thread(
2020 args=(q, blocks, xdiff, ready, done)).start()
2312 target=_bdiffworker, args=(q, blocks, xdiff, ready, done)
2313 ).start()
2021 q.join()
2314 q.join()
2315
2022 def d():
2316 def d():
2023 for pair in textpairs:
2317 for pair in textpairs:
2024 q.put(pair)
2318 q.put(pair)
@@ -2027,6 +2321,7 b' def perfbdiff(ui, repo, file_, rev=None,'
2027 with ready:
2321 with ready:
2028 ready.notify_all()
2322 ready.notify_all()
2029 q.join()
2323 q.join()
2324
2030 timer, fm = gettimer(ui, opts)
2325 timer, fm = gettimer(ui, opts)
2031 timer(d)
2326 timer(d)
2032 fm.end()
2327 fm.end()
@@ -2038,10 +2333,22 b' def perfbdiff(ui, repo, file_, rev=None,'
2038 with ready:
2333 with ready:
2039 ready.notify_all()
2334 ready.notify_all()
2040
2335
2041 @command(b'perfunidiff', revlogopts + formatteropts + [
2336
2042 (b'', b'count', 1, b'number of revisions to test (when using --startrev)'),
2337 @command(
2043 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
2338 b'perfunidiff',
2044 ], b'-c|-m|FILE REV')
2339 revlogopts
2340 + formatteropts
2341 + [
2342 (
2343 b'',
2344 b'count',
2345 1,
2346 b'number of revisions to test (when using --startrev)',
2347 ),
2348 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
2349 ],
2350 b'-c|-m|FILE REV',
2351 )
2045 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
2352 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
2046 """benchmark a unified diff between revisions
2353 """benchmark a unified diff between revisions
2047
2354
@@ -2096,14 +2403,17 b' def perfunidiff(ui, repo, file_, rev=Non'
2096 for left, right in textpairs:
2403 for left, right in textpairs:
2097 # The date strings don't matter, so we pass empty strings.
2404 # The date strings don't matter, so we pass empty strings.
2098 headerlines, hunks = mdiff.unidiff(
2405 headerlines, hunks = mdiff.unidiff(
2099 left, b'', right, b'', b'left', b'right', binary=False)
2406 left, b'', right, b'', b'left', b'right', binary=False
2407 )
2100 # consume iterators in roughly the way patch.py does
2408 # consume iterators in roughly the way patch.py does
2101 b'\n'.join(headerlines)
2409 b'\n'.join(headerlines)
2102 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2410 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2411
2103 timer, fm = gettimer(ui, opts)
2412 timer, fm = gettimer(ui, opts)
2104 timer(d)
2413 timer(d)
2105 fm.end()
2414 fm.end()
2106
2415
2416
2107 @command(b'perfdiffwd', formatteropts)
2417 @command(b'perfdiffwd', formatteropts)
2108 def perfdiffwd(ui, repo, **opts):
2418 def perfdiffwd(ui, repo, **opts):
2109 """Profile diff of working directory changes"""
2419 """Profile diff of working directory changes"""
@@ -2113,21 +2423,23 b' def perfdiffwd(ui, repo, **opts):'
2113 'w': 'ignore_all_space',
2423 'w': 'ignore_all_space',
2114 'b': 'ignore_space_change',
2424 'b': 'ignore_space_change',
2115 'B': 'ignore_blank_lines',
2425 'B': 'ignore_blank_lines',
2116 }
2426 }
2117
2427
2118 for diffopt in ('', 'w', 'b', 'B', 'wB'):
2428 for diffopt in ('', 'w', 'b', 'B', 'wB'):
2119 opts = dict((options[c], b'1') for c in diffopt)
2429 opts = dict((options[c], b'1') for c in diffopt)
2430
2120 def d():
2431 def d():
2121 ui.pushbuffer()
2432 ui.pushbuffer()
2122 commands.diff(ui, repo, **opts)
2433 commands.diff(ui, repo, **opts)
2123 ui.popbuffer()
2434 ui.popbuffer()
2435
2124 diffopt = diffopt.encode('ascii')
2436 diffopt = diffopt.encode('ascii')
2125 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
2437 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
2126 timer(d, title=title)
2438 timer(d, title=title)
2127 fm.end()
2439 fm.end()
2128
2440
2129 @command(b'perfrevlogindex', revlogopts + formatteropts,
2441
2130 b'-c|-m|FILE')
2442 @command(b'perfrevlogindex', revlogopts + formatteropts, b'-c|-m|FILE')
2131 def perfrevlogindex(ui, repo, file_=None, **opts):
2443 def perfrevlogindex(ui, repo, file_=None, **opts):
2132 """Benchmark operations against a revlog index.
2444 """Benchmark operations against a revlog index.
2133
2445
@@ -2150,7 +2462,7 b' def perfrevlogindex(ui, repo, file_=None'
2150 revlogio = revlog.revlogio()
2462 revlogio = revlog.revlogio()
2151 inline = header & (1 << 16)
2463 inline = header & (1 << 16)
2152 else:
2464 else:
2153 raise error.Abort((b'unsupported revlog version: %d') % version)
2465 raise error.Abort(b'unsupported revlog version: %d' % version)
2154
2466
2155 rllen = len(rl)
2467 rllen = len(rl)
2156
2468
@@ -2221,22 +2533,26 b' def perfrevlogindex(ui, repo, file_=None'
2221 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
2533 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
2222 (lambda: resolvenode(node100), b'look up node at tip'),
2534 (lambda: resolvenode(node100), b'look up node at tip'),
2223 # 2x variation is to measure caching impact.
2535 # 2x variation is to measure caching impact.
2224 (lambda: resolvenodes(allnodes),
2536 (lambda: resolvenodes(allnodes), b'look up all nodes (forward)'),
2225 b'look up all nodes (forward)'),
2537 (lambda: resolvenodes(allnodes, 2), b'look up all nodes 2x (forward)'),
2226 (lambda: resolvenodes(allnodes, 2),
2538 (lambda: resolvenodes(allnodesrev), b'look up all nodes (reverse)'),
2227 b'look up all nodes 2x (forward)'),
2539 (
2228 (lambda: resolvenodes(allnodesrev),
2540 lambda: resolvenodes(allnodesrev, 2),
2229 b'look up all nodes (reverse)'),
2541 b'look up all nodes 2x (reverse)',
2230 (lambda: resolvenodes(allnodesrev, 2),
2542 ),
2231 b'look up all nodes 2x (reverse)'),
2543 (lambda: getentries(allrevs), b'retrieve all index entries (forward)'),
2232 (lambda: getentries(allrevs),
2544 (
2233 b'retrieve all index entries (forward)'),
2545 lambda: getentries(allrevs, 2),
2234 (lambda: getentries(allrevs, 2),
2546 b'retrieve all index entries 2x (forward)',
2235 b'retrieve all index entries 2x (forward)'),
2547 ),
2236 (lambda: getentries(allrevsrev),
2548 (
2237 b'retrieve all index entries (reverse)'),
2549 lambda: getentries(allrevsrev),
2238 (lambda: getentries(allrevsrev, 2),
2550 b'retrieve all index entries (reverse)',
2239 b'retrieve all index entries 2x (reverse)'),
2551 ),
2552 (
2553 lambda: getentries(allrevsrev, 2),
2554 b'retrieve all index entries 2x (reverse)',
2555 ),
2240 ]
2556 ]
2241
2557
2242 for fn, title in benches:
2558 for fn, title in benches:
@@ -2244,13 +2560,21 b' def perfrevlogindex(ui, repo, file_=None'
2244 timer(fn, title=title)
2560 timer(fn, title=title)
2245 fm.end()
2561 fm.end()
2246
2562
2247 @command(b'perfrevlogrevisions', revlogopts + formatteropts +
2563
2248 [(b'd', b'dist', 100, b'distance between the revisions'),
2564 @command(
2249 (b's', b'startrev', 0, b'revision to start reading at'),
2565 b'perfrevlogrevisions',
2250 (b'', b'reverse', False, b'read in reverse')],
2566 revlogopts
2251 b'-c|-m|FILE')
2567 + formatteropts
2252 def perfrevlogrevisions(ui, repo, file_=None, startrev=0, reverse=False,
2568 + [
2253 **opts):
2569 (b'd', b'dist', 100, b'distance between the revisions'),
2570 (b's', b'startrev', 0, b'revision to start reading at'),
2571 (b'', b'reverse', False, b'read in reverse'),
2572 ],
2573 b'-c|-m|FILE',
2574 )
2575 def perfrevlogrevisions(
2576 ui, repo, file_=None, startrev=0, reverse=False, **opts
2577 ):
2254 """Benchmark reading a series of revisions from a revlog.
2578 """Benchmark reading a series of revisions from a revlog.
2255
2579
2256 By default, we read every ``-d/--dist`` revision from 0 to tip of
2580 By default, we read every ``-d/--dist`` revision from 0 to tip of
@@ -2286,16 +2610,22 b' def perfrevlogrevisions(ui, repo, file_='
2286 timer(d)
2610 timer(d)
2287 fm.end()
2611 fm.end()
2288
2612
2289 @command(b'perfrevlogwrite', revlogopts + formatteropts +
2613
2290 [(b's', b'startrev', 1000, b'revision to start writing at'),
2614 @command(
2291 (b'', b'stoprev', -1, b'last revision to write'),
2615 b'perfrevlogwrite',
2292 (b'', b'count', 3, b'number of passes to perform'),
2616 revlogopts
2293 (b'', b'details', False, b'print timing for every revisions tested'),
2617 + formatteropts
2294 (b'', b'source', b'full', b'the kind of data feed in the revlog'),
2618 + [
2295 (b'', b'lazydeltabase', True, b'try the provided delta first'),
2619 (b's', b'startrev', 1000, b'revision to start writing at'),
2296 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
2620 (b'', b'stoprev', -1, b'last revision to write'),
2297 ],
2621 (b'', b'count', 3, b'number of passes to perform'),
2298 b'-c|-m|FILE')
2622 (b'', b'details', False, b'print timing for every revisions tested'),
2623 (b'', b'source', b'full', b'the kind of data feed in the revlog'),
2624 (b'', b'lazydeltabase', True, b'try the provided delta first'),
2625 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
2626 ],
2627 b'-c|-m|FILE',
2628 )
2299 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
2629 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
2300 """Benchmark writing a series of revisions to a revlog.
2630 """Benchmark writing a series of revisions to a revlog.
2301
2631
@@ -2329,8 +2659,13 b' def perfrevlogwrite(ui, repo, file_=None'
2329 lazydeltabase = opts['lazydeltabase']
2659 lazydeltabase = opts['lazydeltabase']
2330 source = opts['source']
2660 source = opts['source']
2331 clearcaches = opts['clear_caches']
2661 clearcaches = opts['clear_caches']
2332 validsource = (b'full', b'parent-1', b'parent-2', b'parent-smallest',
2662 validsource = (
2333 b'storage')
2663 b'full',
2664 b'parent-1',
2665 b'parent-2',
2666 b'parent-smallest',
2667 b'storage',
2668 )
2334 if source not in validsource:
2669 if source not in validsource:
2335 raise error.Abort('invalid source type: %s' % source)
2670 raise error.Abort('invalid source type: %s' % source)
2336
2671
@@ -2340,9 +2675,16 b' def perfrevlogwrite(ui, repo, file_=None'
2340 raise error.Abort('invalide run count: %d' % count)
2675 raise error.Abort('invalide run count: %d' % count)
2341 allresults = []
2676 allresults = []
2342 for c in range(count):
2677 for c in range(count):
2343 timing = _timeonewrite(ui, rl, source, startrev, stoprev, c + 1,
2678 timing = _timeonewrite(
2344 lazydeltabase=lazydeltabase,
2679 ui,
2345 clearcaches=clearcaches)
2680 rl,
2681 source,
2682 startrev,
2683 stoprev,
2684 c + 1,
2685 lazydeltabase=lazydeltabase,
2686 clearcaches=clearcaches,
2687 )
2346 allresults.append(timing)
2688 allresults.append(timing)
2347
2689
2348 ### consolidate the results in a single list
2690 ### consolidate the results in a single list
@@ -2396,20 +2738,37 b' def perfrevlogwrite(ui, repo, file_=None'
2396 # for now
2738 # for now
2397 totaltime = []
2739 totaltime = []
2398 for item in allresults:
2740 for item in allresults:
2399 totaltime.append((sum(x[1][0] for x in item),
2741 totaltime.append(
2400 sum(x[1][1] for x in item),
2742 (
2401 sum(x[1][2] for x in item),)
2743 sum(x[1][0] for x in item),
2744 sum(x[1][1] for x in item),
2745 sum(x[1][2] for x in item),
2746 )
2402 )
2747 )
2403 formatone(fm, totaltime, title="total time (%d revs)" % resultcount,
2748 formatone(
2404 displayall=displayall)
2749 fm,
2750 totaltime,
2751 title="total time (%d revs)" % resultcount,
2752 displayall=displayall,
2753 )
2405 fm.end()
2754 fm.end()
2406
2755
2756
2407 class _faketr(object):
2757 class _faketr(object):
2408 def add(s, x, y, z=None):
2758 def add(s, x, y, z=None):
2409 return None
2759 return None
2410
2760
2411 def _timeonewrite(ui, orig, source, startrev, stoprev, runidx=None,
2761
2412 lazydeltabase=True, clearcaches=True):
2762 def _timeonewrite(
2763 ui,
2764 orig,
2765 source,
2766 startrev,
2767 stoprev,
2768 runidx=None,
2769 lazydeltabase=True,
2770 clearcaches=True,
2771 ):
2413 timings = []
2772 timings = []
2414 tr = _faketr()
2773 tr = _faketr()
2415 with _temprevlog(ui, orig, startrev) as dest:
2774 with _temprevlog(ui, orig, startrev) as dest:
@@ -2419,16 +2778,21 b' def _timeonewrite(ui, orig, source, star'
2419 topic = 'adding'
2778 topic = 'adding'
2420 if runidx is not None:
2779 if runidx is not None:
2421 topic += ' (run #%d)' % runidx
2780 topic += ' (run #%d)' % runidx
2422 # Support both old and new progress API
2781 # Support both old and new progress API
2423 if util.safehasattr(ui, 'makeprogress'):
2782 if util.safehasattr(ui, 'makeprogress'):
2424 progress = ui.makeprogress(topic, unit='revs', total=total)
2783 progress = ui.makeprogress(topic, unit='revs', total=total)
2784
2425 def updateprogress(pos):
2785 def updateprogress(pos):
2426 progress.update(pos)
2786 progress.update(pos)
2787
2427 def completeprogress():
2788 def completeprogress():
2428 progress.complete()
2789 progress.complete()
2790
2429 else:
2791 else:
2792
2430 def updateprogress(pos):
2793 def updateprogress(pos):
2431 ui.progress(topic, pos, unit='revs', total=total)
2794 ui.progress(topic, pos, unit='revs', total=total)
2795
2432 def completeprogress():
2796 def completeprogress():
2433 ui.progress(topic, None, unit='revs', total=total)
2797 ui.progress(topic, None, unit='revs', total=total)
2434
2798
@@ -2445,6 +2809,7 b' def _timeonewrite(ui, orig, source, star'
2445 completeprogress()
2809 completeprogress()
2446 return timings
2810 return timings
2447
2811
2812
2448 def _getrevisionseed(orig, rev, tr, source):
2813 def _getrevisionseed(orig, rev, tr, source):
2449 from mercurial.node import nullid
2814 from mercurial.node import nullid
2450
2815
@@ -2481,8 +2846,11 b' def _getrevisionseed(orig, rev, tr, sour'
2481 baserev = orig.deltaparent(rev)
2846 baserev = orig.deltaparent(rev)
2482 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
2847 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
2483
2848
2484 return ((text, tr, linkrev, p1, p2),
2849 return (
2485 {'node': node, 'flags': flags, 'cachedelta': cachedelta})
2850 (text, tr, linkrev, p1, p2),
2851 {'node': node, 'flags': flags, 'cachedelta': cachedelta},
2852 )
2853
2486
2854
2487 @contextlib.contextmanager
2855 @contextlib.contextmanager
2488 def _temprevlog(ui, orig, truncaterev):
2856 def _temprevlog(ui, orig, truncaterev):
@@ -2523,9 +2891,9 b' def _temprevlog(ui, orig, truncaterev):'
2523 vfs = vfsmod.vfs(tmpdir)
2891 vfs = vfsmod.vfs(tmpdir)
2524 vfs.options = getattr(orig.opener, 'options', None)
2892 vfs.options = getattr(orig.opener, 'options', None)
2525
2893
2526 dest = revlog.revlog(vfs,
2894 dest = revlog.revlog(
2527 indexfile=indexname,
2895 vfs, indexfile=indexname, datafile=dataname, **revlogkwargs
2528 datafile=dataname, **revlogkwargs)
2896 )
2529 if dest._inline:
2897 if dest._inline:
2530 raise error.Abort('not supporting inline revlog (yet)')
2898 raise error.Abort('not supporting inline revlog (yet)')
2531 # make sure internals are initialized
2899 # make sure internals are initialized
@@ -2535,10 +2903,17 b' def _temprevlog(ui, orig, truncaterev):'
2535 finally:
2903 finally:
2536 shutil.rmtree(tmpdir, True)
2904 shutil.rmtree(tmpdir, True)
2537
2905
2538 @command(b'perfrevlogchunks', revlogopts + formatteropts +
2906
2539 [(b'e', b'engines', b'', b'compression engines to use'),
2907 @command(
2540 (b's', b'startrev', 0, b'revision to start at')],
2908 b'perfrevlogchunks',
2541 b'-c|-m|FILE')
2909 revlogopts
2910 + formatteropts
2911 + [
2912 (b'e', b'engines', b'', b'compression engines to use'),
2913 (b's', b'startrev', 0, b'revision to start at'),
2914 ],
2915 b'-c|-m|FILE',
2916 )
2542 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
2917 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
2543 """Benchmark operations on revlog chunks.
2918 """Benchmark operations on revlog chunks.
2544
2919
@@ -2645,17 +3020,26 b' def perfrevlogchunks(ui, repo, file_=Non'
2645
3020
2646 for engine in sorted(engines):
3021 for engine in sorted(engines):
2647 compressor = util.compengines[engine].revlogcompressor()
3022 compressor = util.compengines[engine].revlogcompressor()
2648 benches.append((functools.partial(docompress, compressor),
3023 benches.append(
2649 b'compress w/ %s' % engine))
3024 (
3025 functools.partial(docompress, compressor),
3026 b'compress w/ %s' % engine,
3027 )
3028 )
2650
3029
2651 for fn, title in benches:
3030 for fn, title in benches:
2652 timer, fm = gettimer(ui, opts)
3031 timer, fm = gettimer(ui, opts)
2653 timer(fn, title=title)
3032 timer(fn, title=title)
2654 fm.end()
3033 fm.end()
2655
3034
2656 @command(b'perfrevlogrevision', revlogopts + formatteropts +
3035
2657 [(b'', b'cache', False, b'use caches instead of clearing')],
3036 @command(
2658 b'-c|-m|FILE REV')
3037 b'perfrevlogrevision',
3038 revlogopts
3039 + formatteropts
3040 + [(b'', b'cache', False, b'use caches instead of clearing')],
3041 b'-c|-m|FILE REV',
3042 )
2659 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
3043 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
2660 """Benchmark obtaining a revlog revision.
3044 """Benchmark obtaining a revlog revision.
2661
3045
@@ -2777,22 +3161,30 b' def perfrevlogrevision(ui, repo, file_, '
2777 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
3161 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
2778 benches.append(slicing)
3162 benches.append(slicing)
2779
3163
2780 benches.extend([
3164 benches.extend(
2781 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
3165 [
2782 (lambda: dodecompress(rawchunks), b'decompress'),
3166 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
2783 (lambda: dopatch(text, bins), b'patch'),
3167 (lambda: dodecompress(rawchunks), b'decompress'),
2784 (lambda: dohash(text), b'hash'),
3168 (lambda: dopatch(text, bins), b'patch'),
2785 ])
3169 (lambda: dohash(text), b'hash'),
3170 ]
3171 )
2786
3172
2787 timer, fm = gettimer(ui, opts)
3173 timer, fm = gettimer(ui, opts)
2788 for fn, title in benches:
3174 for fn, title in benches:
2789 timer(fn, title=title)
3175 timer(fn, title=title)
2790 fm.end()
3176 fm.end()
2791
3177
2792 @command(b'perfrevset',
3178
2793 [(b'C', b'clear', False, b'clear volatile cache between each call.'),
3179 @command(
2794 (b'', b'contexts', False, b'obtain changectx for each revision')]
3180 b'perfrevset',
2795 + formatteropts, b"REVSET")
3181 [
3182 (b'C', b'clear', False, b'clear volatile cache between each call.'),
3183 (b'', b'contexts', False, b'obtain changectx for each revision'),
3184 ]
3185 + formatteropts,
3186 b"REVSET",
3187 )
2796 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
3188 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
2797 """benchmark the execution time of a revset
3189 """benchmark the execution time of a revset
2798
3190
@@ -2802,19 +3194,26 b' def perfrevset(ui, repo, expr, clear=Fal'
2802 opts = _byteskwargs(opts)
3194 opts = _byteskwargs(opts)
2803
3195
2804 timer, fm = gettimer(ui, opts)
3196 timer, fm = gettimer(ui, opts)
3197
2805 def d():
3198 def d():
2806 if clear:
3199 if clear:
2807 repo.invalidatevolatilesets()
3200 repo.invalidatevolatilesets()
2808 if contexts:
3201 if contexts:
2809 for ctx in repo.set(expr): pass
3202 for ctx in repo.set(expr):
3203 pass
2810 else:
3204 else:
2811 for r in repo.revs(expr): pass
3205 for r in repo.revs(expr):
3206 pass
3207
2812 timer(d)
3208 timer(d)
2813 fm.end()
3209 fm.end()
2814
3210
2815 @command(b'perfvolatilesets',
3211
2816 [(b'', b'clear-obsstore', False, b'drop obsstore between each call.'),
3212 @command(
2817 ] + formatteropts)
3213 b'perfvolatilesets',
3214 [(b'', b'clear-obsstore', False, b'drop obsstore between each call.'),]
3215 + formatteropts,
3216 )
2818 def perfvolatilesets(ui, repo, *names, **opts):
3217 def perfvolatilesets(ui, repo, *names, **opts):
2819 """benchmark the computation of various volatile set
3218 """benchmark the computation of various volatile set
2820
3219
@@ -2829,6 +3228,7 b' def perfvolatilesets(ui, repo, *names, *'
2829 if opts[b'clear_obsstore']:
3228 if opts[b'clear_obsstore']:
2830 clearfilecache(repo, b'obsstore')
3229 clearfilecache(repo, b'obsstore')
2831 obsolete.getrevs(repo, name)
3230 obsolete.getrevs(repo, name)
3231
2832 return d
3232 return d
2833
3233
2834 allobs = sorted(obsolete.cachefuncs)
3234 allobs = sorted(obsolete.cachefuncs)
@@ -2844,6 +3244,7 b' def perfvolatilesets(ui, repo, *names, *'
2844 if opts[b'clear_obsstore']:
3244 if opts[b'clear_obsstore']:
2845 clearfilecache(repo, b'obsstore')
3245 clearfilecache(repo, b'obsstore')
2846 repoview.filterrevs(repo, name)
3246 repoview.filterrevs(repo, name)
3247
2847 return d
3248 return d
2848
3249
2849 allfilter = sorted(repoview.filtertable)
3250 allfilter = sorted(repoview.filtertable)
@@ -2854,12 +3255,20 b' def perfvolatilesets(ui, repo, *names, *'
2854 timer(getfiltered(name), title=name)
3255 timer(getfiltered(name), title=name)
2855 fm.end()
3256 fm.end()
2856
3257
2857 @command(b'perfbranchmap',
3258
2858 [(b'f', b'full', False,
3259 @command(
2859 b'Includes build time of subset'),
3260 b'perfbranchmap',
2860 (b'', b'clear-revbranch', False,
3261 [
2861 b'purge the revbranch cache between computation'),
3262 (b'f', b'full', False, b'Includes build time of subset'),
2862 ] + formatteropts)
3263 (
3264 b'',
3265 b'clear-revbranch',
3266 False,
3267 b'purge the revbranch cache between computation',
3268 ),
3269 ]
3270 + formatteropts,
3271 )
2863 def perfbranchmap(ui, repo, *filternames, **opts):
3272 def perfbranchmap(ui, repo, *filternames, **opts):
2864 """benchmark the update of a branchmap
3273 """benchmark the update of a branchmap
2865
3274
@@ -2869,6 +3278,7 b' def perfbranchmap(ui, repo, *filternames'
2869 full = opts.get(b"full", False)
3278 full = opts.get(b"full", False)
2870 clear_revbranch = opts.get(b"clear_revbranch", False)
3279 clear_revbranch = opts.get(b"clear_revbranch", False)
2871 timer, fm = gettimer(ui, opts)
3280 timer, fm = gettimer(ui, opts)
3281
2872 def getbranchmap(filtername):
3282 def getbranchmap(filtername):
2873 """generate a benchmark function for the filtername"""
3283 """generate a benchmark function for the filtername"""
2874 if filtername is None:
3284 if filtername is None:
@@ -2880,6 +3290,7 b' def perfbranchmap(ui, repo, *filternames'
2880 else:
3290 else:
2881 # older versions
3291 # older versions
2882 filtered = view._branchcaches
3292 filtered = view._branchcaches
3293
2883 def d():
3294 def d():
2884 if clear_revbranch:
3295 if clear_revbranch:
2885 repo.revbranchcache()._clear()
3296 repo.revbranchcache()._clear()
@@ -2888,7 +3299,9 b' def perfbranchmap(ui, repo, *filternames'
2888 else:
3299 else:
2889 filtered.pop(filtername, None)
3300 filtered.pop(filtername, None)
2890 view.branchmap()
3301 view.branchmap()
3302
2891 return d
3303 return d
3304
2892 # add filter in smaller subset to bigger subset
3305 # add filter in smaller subset to bigger subset
2893 possiblefilters = set(repoview.filtertable)
3306 possiblefilters = set(repoview.filtertable)
2894 if filternames:
3307 if filternames:
@@ -2933,11 +3346,16 b' def perfbranchmap(ui, repo, *filternames'
2933 branchcachewrite.restore()
3346 branchcachewrite.restore()
2934 fm.end()
3347 fm.end()
2935
3348
2936 @command(b'perfbranchmapupdate', [
3349
2937 (b'', b'base', [], b'subset of revision to start from'),
3350 @command(
2938 (b'', b'target', [], b'subset of revision to end with'),
3351 b'perfbranchmapupdate',
2939 (b'', b'clear-caches', False, b'clear cache between each runs')
3352 [
2940 ] + formatteropts)
3353 (b'', b'base', [], b'subset of revision to start from'),
3354 (b'', b'target', [], b'subset of revision to end with'),
3355 (b'', b'clear-caches', False, b'clear cache between each runs'),
3356 ]
3357 + formatteropts,
3358 )
2941 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
3359 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
2942 """benchmark branchmap update from for <base> revs to <target> revs
3360 """benchmark branchmap update from for <base> revs to <target> revs
2943
3361
@@ -2956,11 +3374,12 b' def perfbranchmapupdate(ui, repo, base=('
2956 """
3374 """
2957 from mercurial import branchmap
3375 from mercurial import branchmap
2958 from mercurial import repoview
3376 from mercurial import repoview
3377
2959 opts = _byteskwargs(opts)
3378 opts = _byteskwargs(opts)
2960 timer, fm = gettimer(ui, opts)
3379 timer, fm = gettimer(ui, opts)
2961 clearcaches = opts[b'clear_caches']
3380 clearcaches = opts[b'clear_caches']
2962 unfi = repo.unfiltered()
3381 unfi = repo.unfiltered()
2963 x = [None] # used to pass data between closure
3382 x = [None] # used to pass data between closure
2964
3383
2965 # we use a `list` here to avoid possible side effect from smartset
3384 # we use a `list` here to avoid possible side effect from smartset
2966 baserevs = list(scmutil.revrange(repo, base))
3385 baserevs = list(scmutil.revrange(repo, base))
@@ -3037,12 +3456,16 b' def perfbranchmapupdate(ui, repo, base=('
3037 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
3456 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
3038 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
3457 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
3039
3458
3040 @command(b'perfbranchmapload', [
3459
3041 (b'f', b'filter', b'', b'Specify repoview filter'),
3460 @command(
3042 (b'', b'list', False, b'List brachmap filter caches'),
3461 b'perfbranchmapload',
3043 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
3462 [
3044
3463 (b'f', b'filter', b'', b'Specify repoview filter'),
3045 ] + formatteropts)
3464 (b'', b'list', False, b'List brachmap filter caches'),
3465 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
3466 ]
3467 + formatteropts,
3468 )
3046 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
3469 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
3047 """benchmark reading the branchmap"""
3470 """benchmark reading the branchmap"""
3048 opts = _byteskwargs(opts)
3471 opts = _byteskwargs(opts)
@@ -3052,8 +3475,9 b' def perfbranchmapload(ui, repo, filter=b'
3052 for name, kind, st in repo.cachevfs.readdir(stat=True):
3475 for name, kind, st in repo.cachevfs.readdir(stat=True):
3053 if name.startswith(b'branch2'):
3476 if name.startswith(b'branch2'):
3054 filtername = name.partition(b'-')[2] or b'unfiltered'
3477 filtername = name.partition(b'-')[2] or b'unfiltered'
3055 ui.status(b'%s - %s\n'
3478 ui.status(
3056 % (filtername, util.bytecount(st.st_size)))
3479 b'%s - %s\n' % (filtername, util.bytecount(st.st_size))
3480 )
3057 return
3481 return
3058 if not filter:
3482 if not filter:
3059 filter = None
3483 filter = None
@@ -3063,7 +3487,7 b' def perfbranchmapload(ui, repo, filter=b'
3063 else:
3487 else:
3064 repo = repoview.repoview(repo, filter)
3488 repo = repoview.repoview(repo, filter)
3065
3489
3066 repo.branchmap() # make sure we have a relevant, up to date branchmap
3490 repo.branchmap() # make sure we have a relevant, up to date branchmap
3067
3491
3068 try:
3492 try:
3069 fromfile = branchmap.branchcache.fromfile
3493 fromfile = branchmap.branchcache.fromfile
@@ -3076,18 +3500,23 b' def perfbranchmapload(ui, repo, filter=b'
3076 while fromfile(repo) is None:
3500 while fromfile(repo) is None:
3077 currentfilter = subsettable.get(currentfilter)
3501 currentfilter = subsettable.get(currentfilter)
3078 if currentfilter is None:
3502 if currentfilter is None:
3079 raise error.Abort(b'No branchmap cached for %s repo'
3503 raise error.Abort(
3080 % (filter or b'unfiltered'))
3504 b'No branchmap cached for %s repo' % (filter or b'unfiltered')
3505 )
3081 repo = repo.filtered(currentfilter)
3506 repo = repo.filtered(currentfilter)
3082 timer, fm = gettimer(ui, opts)
3507 timer, fm = gettimer(ui, opts)
3508
3083 def setup():
3509 def setup():
3084 if clearrevlogs:
3510 if clearrevlogs:
3085 clearchangelog(repo)
3511 clearchangelog(repo)
3512
3086 def bench():
3513 def bench():
3087 fromfile(repo)
3514 fromfile(repo)
3515
3088 timer(bench, setup=setup)
3516 timer(bench, setup=setup)
3089 fm.end()
3517 fm.end()
3090
3518
3519
3091 @command(b'perfloadmarkers')
3520 @command(b'perfloadmarkers')
3092 def perfloadmarkers(ui, repo):
3521 def perfloadmarkers(ui, repo):
3093 """benchmark the time to parse the on-disk markers for a repo
3522 """benchmark the time to parse the on-disk markers for a repo
@@ -3098,18 +3527,39 b' def perfloadmarkers(ui, repo):'
3098 timer(lambda: len(obsolete.obsstore(svfs)))
3527 timer(lambda: len(obsolete.obsstore(svfs)))
3099 fm.end()
3528 fm.end()
3100
3529
3101 @command(b'perflrucachedict', formatteropts +
3530
3102 [(b'', b'costlimit', 0, b'maximum total cost of items in cache'),
3531 @command(
3103 (b'', b'mincost', 0, b'smallest cost of items in cache'),
3532 b'perflrucachedict',
3104 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
3533 formatteropts
3105 (b'', b'size', 4, b'size of cache'),
3534 + [
3106 (b'', b'gets', 10000, b'number of key lookups'),
3535 (b'', b'costlimit', 0, b'maximum total cost of items in cache'),
3107 (b'', b'sets', 10000, b'number of key sets'),
3536 (b'', b'mincost', 0, b'smallest cost of items in cache'),
3108 (b'', b'mixed', 10000, b'number of mixed mode operations'),
3537 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
3109 (b'', b'mixedgetfreq', 50, b'frequency of get vs set ops in mixed mode')],
3538 (b'', b'size', 4, b'size of cache'),
3110 norepo=True)
3539 (b'', b'gets', 10000, b'number of key lookups'),
3111 def perflrucache(ui, mincost=0, maxcost=100, costlimit=0, size=4,
3540 (b'', b'sets', 10000, b'number of key sets'),
3112 gets=10000, sets=10000, mixed=10000, mixedgetfreq=50, **opts):
3541 (b'', b'mixed', 10000, b'number of mixed mode operations'),
3542 (
3543 b'',
3544 b'mixedgetfreq',
3545 50,
3546 b'frequency of get vs set ops in mixed mode',
3547 ),
3548 ],
3549 norepo=True,
3550 )
3551 def perflrucache(
3552 ui,
3553 mincost=0,
3554 maxcost=100,
3555 costlimit=0,
3556 size=4,
3557 gets=10000,
3558 sets=10000,
3559 mixed=10000,
3560 mixedgetfreq=50,
3561 **opts
3562 ):
3113 opts = _byteskwargs(opts)
3563 opts = _byteskwargs(opts)
3114
3564
3115 def doinit():
3565 def doinit():
@@ -3134,7 +3584,7 b' def perflrucache(ui, mincost=0, maxcost='
3134 d[v] = v
3584 d[v] = v
3135 for key in getseq:
3585 for key in getseq:
3136 value = d[key]
3586 value = d[key]
3137 value # silence pyflakes warning
3587 value # silence pyflakes warning
3138
3588
3139 def dogetscost():
3589 def dogetscost():
3140 d = util.lrucachedict(size, maxcost=costlimit)
3590 d = util.lrucachedict(size, maxcost=costlimit)
@@ -3143,7 +3593,7 b' def perflrucache(ui, mincost=0, maxcost='
3143 for key in getseq:
3593 for key in getseq:
3144 try:
3594 try:
3145 value = d[key]
3595 value = d[key]
3146 value # silence pyflakes warning
3596 value # silence pyflakes warning
3147 except KeyError:
3597 except KeyError:
3148 pass
3598 pass
3149
3599
@@ -3178,9 +3628,9 b' def perflrucache(ui, mincost=0, maxcost='
3178 else:
3628 else:
3179 op = 1
3629 op = 1
3180
3630
3181 mixedops.append((op,
3631 mixedops.append(
3182 random.randint(0, size * 2),
3632 (op, random.randint(0, size * 2), random.choice(costrange))
3183 random.choice(costrange)))
3633 )
3184
3634
3185 def domixed():
3635 def domixed():
3186 d = util.lrucachedict(size)
3636 d = util.lrucachedict(size)
@@ -3211,24 +3661,29 b' def perflrucache(ui, mincost=0, maxcost='
3211 ]
3661 ]
3212
3662
3213 if costlimit:
3663 if costlimit:
3214 benches.extend([
3664 benches.extend(
3215 (dogetscost, b'gets w/ cost limit'),
3665 [
3216 (doinsertscost, b'inserts w/ cost limit'),
3666 (dogetscost, b'gets w/ cost limit'),
3217 (domixedcost, b'mixed w/ cost limit'),
3667 (doinsertscost, b'inserts w/ cost limit'),
3218 ])
3668 (domixedcost, b'mixed w/ cost limit'),
3669 ]
3670 )
3219 else:
3671 else:
3220 benches.extend([
3672 benches.extend(
3221 (dogets, b'gets'),
3673 [
3222 (doinserts, b'inserts'),
3674 (dogets, b'gets'),
3223 (dosets, b'sets'),
3675 (doinserts, b'inserts'),
3224 (domixed, b'mixed')
3676 (dosets, b'sets'),
3225 ])
3677 (domixed, b'mixed'),
3678 ]
3679 )
3226
3680
3227 for fn, title in benches:
3681 for fn, title in benches:
3228 timer, fm = gettimer(ui, opts)
3682 timer, fm = gettimer(ui, opts)
3229 timer(fn, title=title)
3683 timer(fn, title=title)
3230 fm.end()
3684 fm.end()
3231
3685
3686
3232 @command(b'perfwrite', formatteropts)
3687 @command(b'perfwrite', formatteropts)
3233 def perfwrite(ui, repo, **opts):
3688 def perfwrite(ui, repo, **opts):
3234 """microbenchmark ui.write
3689 """microbenchmark ui.write
@@ -3236,15 +3691,19 b' def perfwrite(ui, repo, **opts):'
3236 opts = _byteskwargs(opts)
3691 opts = _byteskwargs(opts)
3237
3692
3238 timer, fm = gettimer(ui, opts)
3693 timer, fm = gettimer(ui, opts)
3694
3239 def write():
3695 def write():
3240 for i in range(100000):
3696 for i in range(100000):
3241 ui.write((b'Testing write performance\n'))
3697 ui.write(b'Testing write performance\n')
3698
3242 timer(write)
3699 timer(write)
3243 fm.end()
3700 fm.end()
3244
3701
3702
3245 def uisetup(ui):
3703 def uisetup(ui):
3246 if (util.safehasattr(cmdutil, b'openrevlog') and
3704 if util.safehasattr(cmdutil, b'openrevlog') and not util.safehasattr(
3247 not util.safehasattr(commands, b'debugrevlogopts')):
3705 commands, b'debugrevlogopts'
3706 ):
3248 # for "historical portability":
3707 # for "historical portability":
3249 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
3708 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
3250 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
3709 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
@@ -3252,15 +3711,24 b' def uisetup(ui):'
3252 # available since 3.5 (or 49c583ca48c4).
3711 # available since 3.5 (or 49c583ca48c4).
3253 def openrevlog(orig, repo, cmd, file_, opts):
3712 def openrevlog(orig, repo, cmd, file_, opts):
3254 if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'):
3713 if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'):
3255 raise error.Abort(b"This version doesn't support --dir option",
3714 raise error.Abort(
3256 hint=b"use 3.5 or later")
3715 b"This version doesn't support --dir option",
3716 hint=b"use 3.5 or later",
3717 )
3257 return orig(repo, cmd, file_, opts)
3718 return orig(repo, cmd, file_, opts)
3719
3258 extensions.wrapfunction(cmdutil, b'openrevlog', openrevlog)
3720 extensions.wrapfunction(cmdutil, b'openrevlog', openrevlog)
3259
3721
3260 @command(b'perfprogress', formatteropts + [
3722
3261 (b'', b'topic', b'topic', b'topic for progress messages'),
3723 @command(
3262 (b'c', b'total', 1000000, b'total value we are progressing to'),
3724 b'perfprogress',
3263 ], norepo=True)
3725 formatteropts
3726 + [
3727 (b'', b'topic', b'topic', b'topic for progress messages'),
3728 (b'c', b'total', 1000000, b'total value we are progressing to'),
3729 ],
3730 norepo=True,
3731 )
3264 def perfprogress(ui, topic=None, total=None, **opts):
3732 def perfprogress(ui, topic=None, total=None, **opts):
3265 """printing of progress bars"""
3733 """printing of progress bars"""
3266 opts = _byteskwargs(opts)
3734 opts = _byteskwargs(opts)
@@ -7,6 +7,7 b' from mercurial import ('
7 util,
7 util,
8 )
8 )
9
9
10
10 def diffstat(ui, repo, **kwargs):
11 def diffstat(ui, repo, **kwargs):
11 '''Example usage:
12 '''Example usage:
12
13
@@ -25,65 +25,103 b' import subprocess'
25 import sys
25 import sys
26
26
27 _hgenv = dict(os.environ)
27 _hgenv = dict(os.environ)
28 _hgenv.update({
28 _hgenv.update(
29 'HGPLAIN': '1',
29 {'HGPLAIN': '1',}
30 })
30 )
31
31
32 _HG_FIRST_CHANGE = '9117c6561b0bd7792fa13b50d28239d51b78e51f'
32 _HG_FIRST_CHANGE = '9117c6561b0bd7792fa13b50d28239d51b78e51f'
33
33
34
34 def _runhg(*args):
35 def _runhg(*args):
35 return subprocess.check_output(args, env=_hgenv)
36 return subprocess.check_output(args, env=_hgenv)
36
37
38
37 def _is_hg_repo(path):
39 def _is_hg_repo(path):
38 return _runhg('hg', 'log', '-R', path,
40 return (
39 '-r0', '--template={node}').strip() == _HG_FIRST_CHANGE
41 _runhg('hg', 'log', '-R', path, '-r0', '--template={node}').strip()
42 == _HG_FIRST_CHANGE
43 )
44
40
45
41 def _py3default():
46 def _py3default():
42 if sys.version_info[0] >= 3:
47 if sys.version_info[0] >= 3:
43 return sys.executable
48 return sys.executable
44 return 'python3'
49 return 'python3'
45
50
51
46 def main(argv=()):
52 def main(argv=()):
47 p = argparse.ArgumentParser()
53 p = argparse.ArgumentParser()
48 p.add_argument('--working-tests',
54 p.add_argument(
49 help='List of tests that already work in Python 3.')
55 '--working-tests', help='List of tests that already work in Python 3.'
50 p.add_argument('--commit-to-repo',
56 )
51 help='If set, commit newly fixed tests to the given repo')
57 p.add_argument(
52 p.add_argument('-j', default=os.sysconf(r'SC_NPROCESSORS_ONLN'), type=int,
58 '--commit-to-repo',
53 help='Number of parallel tests to run.')
59 help='If set, commit newly fixed tests to the given repo',
54 p.add_argument('--python3', default=_py3default(),
60 )
55 help='python3 interpreter to use for test run')
61 p.add_argument(
56 p.add_argument('--commit-user',
62 '-j',
57 default='python3-ratchet@mercurial-scm.org',
63 default=os.sysconf(r'SC_NPROCESSORS_ONLN'),
58 help='Username to specify when committing to a repo.')
64 type=int,
65 help='Number of parallel tests to run.',
66 )
67 p.add_argument(
68 '--python3',
69 default=_py3default(),
70 help='python3 interpreter to use for test run',
71 )
72 p.add_argument(
73 '--commit-user',
74 default='python3-ratchet@mercurial-scm.org',
75 help='Username to specify when committing to a repo.',
76 )
59 opts = p.parse_args(argv)
77 opts = p.parse_args(argv)
60 if opts.commit_to_repo:
78 if opts.commit_to_repo:
61 if not _is_hg_repo(opts.commit_to_repo):
79 if not _is_hg_repo(opts.commit_to_repo):
62 print('abort: specified repository is not the hg repository')
80 print('abort: specified repository is not the hg repository')
63 sys.exit(1)
81 sys.exit(1)
64 if not opts.working_tests or not os.path.isfile(opts.working_tests):
82 if not opts.working_tests or not os.path.isfile(opts.working_tests):
65 print('abort: --working-tests must exist and be a file (got %r)' %
83 print(
66 opts.working_tests)
84 'abort: --working-tests must exist and be a file (got %r)'
85 % opts.working_tests
86 )
67 sys.exit(1)
87 sys.exit(1)
68 elif opts.commit_to_repo:
88 elif opts.commit_to_repo:
69 root = _runhg('hg', 'root').strip()
89 root = _runhg('hg', 'root').strip()
70 if not opts.working_tests.startswith(root):
90 if not opts.working_tests.startswith(root):
71 print('abort: if --commit-to-repo is given, '
91 print(
72 '--working-tests must be from that repo')
92 'abort: if --commit-to-repo is given, '
93 '--working-tests must be from that repo'
94 )
73 sys.exit(1)
95 sys.exit(1)
74 try:
96 try:
75 subprocess.check_call([opts.python3, '-c',
97 subprocess.check_call(
76 'import sys ; '
98 [
77 'assert ((3, 5) <= sys.version_info < (3, 6) '
99 opts.python3,
78 'or sys.version_info >= (3, 6, 2))'])
100 '-c',
101 'import sys ; '
102 'assert ((3, 5) <= sys.version_info < (3, 6) '
103 'or sys.version_info >= (3, 6, 2))',
104 ]
105 )
79 except subprocess.CalledProcessError:
106 except subprocess.CalledProcessError:
80 print('warning: Python 3.6.0 and 3.6.1 have '
107 print(
81 'a bug which breaks Mercurial')
108 'warning: Python 3.6.0 and 3.6.1 have '
109 'a bug which breaks Mercurial'
110 )
82 print('(see https://bugs.python.org/issue29714 for details)')
111 print('(see https://bugs.python.org/issue29714 for details)')
83 sys.exit(1)
112 sys.exit(1)
84
113
85 rt = subprocess.Popen([opts.python3, 'run-tests.py', '-j', str(opts.j),
114 rt = subprocess.Popen(
86 '--blacklist', opts.working_tests, '--json'])
115 [
116 opts.python3,
117 'run-tests.py',
118 '-j',
119 str(opts.j),
120 '--blacklist',
121 opts.working_tests,
122 '--json',
123 ]
124 )
87 rt.wait()
125 rt.wait()
88 with open('report.json') as f:
126 with open('report.json') as f:
89 data = f.read()
127 data = f.read()
@@ -104,12 +142,20 b' def main(argv=()):'
104 with open(opts.working_tests, 'w') as f:
142 with open(opts.working_tests, 'w') as f:
105 for p in sorted(oldpass | newpass):
143 for p in sorted(oldpass | newpass):
106 f.write('%s\n' % p)
144 f.write('%s\n' % p)
107 _runhg('hg', 'commit', '-R', opts.commit_to_repo,
145 _runhg(
108 '--user', opts.commit_user,
146 'hg',
109 '--message', 'python3: expand list of passing tests')
147 'commit',
148 '-R',
149 opts.commit_to_repo,
150 '--user',
151 opts.commit_user,
152 '--message',
153 'python3: expand list of passing tests',
154 )
110 else:
155 else:
111 print('Newly passing tests:', '\n'.join(sorted(newpass)))
156 print('Newly passing tests:', '\n'.join(sorted(newpass)))
112 sys.exit(2)
157 sys.exit(2)
113
158
159
114 if __name__ == '__main__':
160 if __name__ == '__main__':
115 main(sys.argv[1:])
161 main(sys.argv[1:])
@@ -16,9 +16,20 b' import re'
16 import subprocess
16 import subprocess
17 import sys
17 import sys
18
18
19 DEFAULTVARIANTS = ['plain', 'min', 'max', 'first', 'last',
19 DEFAULTVARIANTS = [
20 'reverse', 'reverse+first', 'reverse+last',
20 'plain',
21 'sort', 'sort+first', 'sort+last']
21 'min',
22 'max',
23 'first',
24 'last',
25 'reverse',
26 'reverse+first',
27 'reverse+last',
28 'sort',
29 'sort+first',
30 'sort+last',
31 ]
32
22
33
23 def check_output(*args, **kwargs):
34 def check_output(*args, **kwargs):
24 kwargs.setdefault('stderr', subprocess.PIPE)
35 kwargs.setdefault('stderr', subprocess.PIPE)
@@ -29,14 +40,16 b' def check_output(*args, **kwargs):'
29 raise subprocess.CalledProcessError(proc.returncode, ' '.join(args[0]))
40 raise subprocess.CalledProcessError(proc.returncode, ' '.join(args[0]))
30 return output
41 return output
31
42
43
32 def update(rev):
44 def update(rev):
33 """update the repo to a revision"""
45 """update the repo to a revision"""
34 try:
46 try:
35 subprocess.check_call(['hg', 'update', '--quiet', '--check', str(rev)])
47 subprocess.check_call(['hg', 'update', '--quiet', '--check', str(rev)])
36 check_output(['make', 'local'],
48 check_output(
37 stderr=None) # suppress output except for error/warning
49 ['make', 'local'], stderr=None
50 ) # suppress output except for error/warning
38 except subprocess.CalledProcessError as exc:
51 except subprocess.CalledProcessError as exc:
39 print('update to revision %s failed, aborting'%rev, file=sys.stderr)
52 print('update to revision %s failed, aborting' % rev, file=sys.stderr)
40 sys.exit(exc.returncode)
53 sys.exit(exc.returncode)
41
54
42
55
@@ -48,11 +61,14 b' def hg(cmd, repo=None):'
48 fullcmd = ['./hg']
61 fullcmd = ['./hg']
49 if repo is not None:
62 if repo is not None:
50 fullcmd += ['-R', repo]
63 fullcmd += ['-R', repo]
51 fullcmd += ['--config',
64 fullcmd += [
52 'extensions.perf=' + os.path.join(contribdir, 'perf.py')]
65 '--config',
66 'extensions.perf=' + os.path.join(contribdir, 'perf.py'),
67 ]
53 fullcmd += cmd
68 fullcmd += cmd
54 return check_output(fullcmd, stderr=subprocess.STDOUT)
69 return check_output(fullcmd, stderr=subprocess.STDOUT)
55
70
71
56 def perf(revset, target=None, contexts=False):
72 def perf(revset, target=None, contexts=False):
57 """run benchmark for this very revset"""
73 """run benchmark for this very revset"""
58 try:
74 try:
@@ -64,15 +80,21 b' def perf(revset, target=None, contexts=F'
64 output = hg(args, repo=target)
80 output = hg(args, repo=target)
65 return parseoutput(output)
81 return parseoutput(output)
66 except subprocess.CalledProcessError as exc:
82 except subprocess.CalledProcessError as exc:
67 print('abort: cannot run revset benchmark: %s'%exc.cmd, file=sys.stderr)
83 print(
68 if getattr(exc, 'output', None) is None: # no output before 2.7
84 'abort: cannot run revset benchmark: %s' % exc.cmd, file=sys.stderr
85 )
86 if getattr(exc, 'output', None) is None: # no output before 2.7
69 print('(no output)', file=sys.stderr)
87 print('(no output)', file=sys.stderr)
70 else:
88 else:
71 print(exc.output, file=sys.stderr)
89 print(exc.output, file=sys.stderr)
72 return None
90 return None
73
91
74 outputre = re.compile(br'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) '
92
75 br'sys (\d+.\d+) \(best of (\d+)\)')
93 outputre = re.compile(
94 br'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) '
95 br'sys (\d+.\d+) \(best of (\d+)\)'
96 )
97
76
98
77 def parseoutput(output):
99 def parseoutput(output):
78 """parse a textual output into a dict
100 """parse a textual output into a dict
@@ -85,20 +107,30 b' def parseoutput(output):'
85 print('abort: invalid output:', file=sys.stderr)
107 print('abort: invalid output:', file=sys.stderr)
86 print(output, file=sys.stderr)
108 print(output, file=sys.stderr)
87 sys.exit(1)
109 sys.exit(1)
88 return {'comb': float(match.group(2)),
110 return {
89 'count': int(match.group(5)),
111 'comb': float(match.group(2)),
90 'sys': float(match.group(3)),
112 'count': int(match.group(5)),
91 'user': float(match.group(4)),
113 'sys': float(match.group(3)),
92 'wall': float(match.group(1)),
114 'user': float(match.group(4)),
93 }
115 'wall': float(match.group(1)),
116 }
117
94
118
95 def printrevision(rev):
119 def printrevision(rev):
96 """print data about a revision"""
120 """print data about a revision"""
97 sys.stdout.write("Revision ")
121 sys.stdout.write("Revision ")
98 sys.stdout.flush()
122 sys.stdout.flush()
99 subprocess.check_call(['hg', 'log', '--rev', str(rev), '--template',
123 subprocess.check_call(
100 '{if(tags, " ({tags})")} '
124 [
101 '{rev}:{node|short}: {desc|firstline}\n'])
125 'hg',
126 'log',
127 '--rev',
128 str(rev),
129 '--template',
130 '{if(tags, " ({tags})")} ' '{rev}:{node|short}: {desc|firstline}\n',
131 ]
132 )
133
102
134
103 def idxwidth(nbidx):
135 def idxwidth(nbidx):
104 """return the max width of number used for index
136 """return the max width of number used for index
@@ -107,7 +139,7 b' def idxwidth(nbidx):'
107 because we start with zero and we'd rather not deal with all the
139 because we start with zero and we'd rather not deal with all the
108 extra rounding business that log10 would imply.
140 extra rounding business that log10 would imply.
109 """
141 """
110 nbidx -= 1 # starts at 0
142 nbidx -= 1 # starts at 0
111 idxwidth = 0
143 idxwidth = 0
112 while nbidx:
144 while nbidx:
113 idxwidth += 1
145 idxwidth += 1
@@ -116,6 +148,7 b' def idxwidth(nbidx):'
116 idxwidth = 1
148 idxwidth = 1
117 return idxwidth
149 return idxwidth
118
150
151
119 def getfactor(main, other, field, sensitivity=0.05):
152 def getfactor(main, other, field, sensitivity=0.05):
120 """return the relative factor between values for 'field' in main and other
153 """return the relative factor between values for 'field' in main and other
121
154
@@ -125,10 +158,11 b' def getfactor(main, other, field, sensit'
125 if main is not None:
158 if main is not None:
126 factor = other[field] / main[field]
159 factor = other[field] / main[field]
127 low, high = 1 - sensitivity, 1 + sensitivity
160 low, high = 1 - sensitivity, 1 + sensitivity
128 if (low < factor < high):
161 if low < factor < high:
129 return None
162 return None
130 return factor
163 return factor
131
164
165
132 def formatfactor(factor):
166 def formatfactor(factor):
133 """format a factor into a 4 char string
167 """format a factor into a 4 char string
134
168
@@ -155,15 +189,19 b' def formatfactor(factor):'
155 factor //= 0
189 factor //= 0
156 return 'x%ix%i' % (factor, order)
190 return 'x%ix%i' % (factor, order)
157
191
192
158 def formattiming(value):
193 def formattiming(value):
159 """format a value to strictly 8 char, dropping some precision if needed"""
194 """format a value to strictly 8 char, dropping some precision if needed"""
160 if value < 10**7:
195 if value < 10 ** 7:
161 return ('%.6f' % value)[:8]
196 return ('%.6f' % value)[:8]
162 else:
197 else:
163 # value is HUGE very unlikely to happen (4+ month run)
198 # value is HUGE very unlikely to happen (4+ month run)
164 return '%i' % value
199 return '%i' % value
165
200
201
166 _marker = object()
202 _marker = object()
203
204
167 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
205 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
168 """print a line of result to stdout"""
206 """print a line of result to stdout"""
169 mask = '%%0%ii) %%s' % idxwidth(maxidx)
207 mask = '%%0%ii) %%s' % idxwidth(maxidx)
@@ -184,9 +222,10 b' def printresult(variants, idx, data, max'
184 out.append(formattiming(data[var]['comb']))
222 out.append(formattiming(data[var]['comb']))
185 out.append(formattiming(data[var]['user']))
223 out.append(formattiming(data[var]['user']))
186 out.append(formattiming(data[var]['sys']))
224 out.append(formattiming(data[var]['sys']))
187 out.append('%6d' % data[var]['count'])
225 out.append('%6d' % data[var]['count'])
188 print(mask % (idx, ' '.join(out)))
226 print(mask % (idx, ' '.join(out)))
189
227
228
190 def printheader(variants, maxidx, verbose=False, relative=False):
229 def printheader(variants, maxidx, verbose=False, relative=False):
191 header = [' ' * (idxwidth(maxidx) + 1)]
230 header = [' ' * (idxwidth(maxidx) + 1)]
192 for var in variants:
231 for var in variants:
@@ -204,12 +243,13 b' def printheader(variants, maxidx, verbos'
204 header.append('%6s' % 'count')
243 header.append('%6s' % 'count')
205 print(' '.join(header))
244 print(' '.join(header))
206
245
246
207 def getrevs(spec):
247 def getrevs(spec):
208 """get the list of rev matched by a revset"""
248 """get the list of rev matched by a revset"""
209 try:
249 try:
210 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
250 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
211 except subprocess.CalledProcessError as exc:
251 except subprocess.CalledProcessError as exc:
212 print("abort, can't get revision from %s"%spec, file=sys.stderr)
252 print("abort, can't get revision from %s" % spec, file=sys.stderr)
213 sys.exit(exc.returncode)
253 sys.exit(exc.returncode)
214 return [r for r in out.split() if r]
254 return [r for r in out.split() if r]
215
255
@@ -221,31 +261,44 b' def applyvariants(revset, variant):'
221 revset = '%s(%s)' % (var, revset)
261 revset = '%s(%s)' % (var, revset)
222 return revset
262 return revset
223
263
224 helptext="""This script will run multiple variants of provided revsets using
264
265 helptext = """This script will run multiple variants of provided revsets using
225 different revisions in your mercurial repository. After the benchmark are run
266 different revisions in your mercurial repository. After the benchmark are run
226 summary output is provided. Use it to demonstrate speed improvements or pin
267 summary output is provided. Use it to demonstrate speed improvements or pin
227 point regressions. Revsets to run are specified in a file (or from stdin), one
268 point regressions. Revsets to run are specified in a file (or from stdin), one
228 revsets per line. Line starting with '#' will be ignored, allowing insertion of
269 revsets per line. Line starting with '#' will be ignored, allowing insertion of
229 comments."""
270 comments."""
230 parser = optparse.OptionParser(usage="usage: %prog [options] <revs>",
271 parser = optparse.OptionParser(
231 description=helptext)
272 usage="usage: %prog [options] <revs>", description=helptext
232 parser.add_option("-f", "--file",
273 )
233 help="read revset from FILE (stdin if omitted)",
274 parser.add_option(
234 metavar="FILE")
275 "-f",
235 parser.add_option("-R", "--repo",
276 "--file",
236 help="run benchmark on REPO", metavar="REPO")
277 help="read revset from FILE (stdin if omitted)",
278 metavar="FILE",
279 )
280 parser.add_option("-R", "--repo", help="run benchmark on REPO", metavar="REPO")
237
281
238 parser.add_option("-v", "--verbose",
282 parser.add_option(
239 action='store_true',
283 "-v",
240 help="display all timing data (not just best total time)")
284 "--verbose",
285 action='store_true',
286 help="display all timing data (not just best total time)",
287 )
241
288
242 parser.add_option("", "--variants",
289 parser.add_option(
243 default=','.join(DEFAULTVARIANTS),
290 "",
244 help="comma separated list of variant to test "
291 "--variants",
245 "(eg: plain,min,sorted) (plain = no modification)")
292 default=','.join(DEFAULTVARIANTS),
246 parser.add_option('', '--contexts',
293 help="comma separated list of variant to test "
247 action='store_true',
294 "(eg: plain,min,sorted) (plain = no modification)",
248 help='obtain changectx from results instead of integer revs')
295 )
296 parser.add_option(
297 '',
298 '--contexts',
299 action='store_true',
300 help='obtain changectx from results instead of integer revs',
301 )
249
302
250 (options, args) = parser.parse_args()
303 (options, args) = parser.parse_args()
251
304
@@ -294,17 +347,20 b' for r in revs:'
294 data = perf(varrset, target=options.repo, contexts=options.contexts)
347 data = perf(varrset, target=options.repo, contexts=options.contexts)
295 varres[var] = data
348 varres[var] = data
296 res.append(varres)
349 res.append(varres)
297 printresult(variants, idx, varres, len(revsets),
350 printresult(
298 verbose=options.verbose)
351 variants, idx, varres, len(revsets), verbose=options.verbose
352 )
299 sys.stdout.flush()
353 sys.stdout.flush()
300 print("----------------------------")
354 print("----------------------------")
301
355
302
356
303 print("""
357 print(
358 """
304
359
305 Result by revset
360 Result by revset
306 ================
361 ================
307 """)
362 """
363 )
308
364
309 print('Revision:')
365 print('Revision:')
310 for idx, rev in enumerate(revs):
366 for idx, rev in enumerate(revs):
@@ -321,7 +377,13 b' for ridx, rset in enumerate(revsets):'
321 printheader(variants, len(results), verbose=options.verbose, relative=True)
377 printheader(variants, len(results), verbose=options.verbose, relative=True)
322 ref = None
378 ref = None
323 for idx, data in enumerate(results):
379 for idx, data in enumerate(results):
324 printresult(variants, idx, data[ridx], len(results),
380 printresult(
325 verbose=options.verbose, reference=ref)
381 variants,
382 idx,
383 data[ridx],
384 len(results),
385 verbose=options.verbose,
386 reference=ref,
387 )
326 ref = data[ridx]
388 ref = data[ridx]
327 print()
389 print()
@@ -9,16 +9,19 b' import signal'
9 import sys
9 import sys
10 import traceback
10 import traceback
11
11
12
12 def sigshow(*args):
13 def sigshow(*args):
13 sys.stderr.write("\n")
14 sys.stderr.write("\n")
14 traceback.print_stack(args[1], limit=10, file=sys.stderr)
15 traceback.print_stack(args[1], limit=10, file=sys.stderr)
15 sys.stderr.write("----\n")
16 sys.stderr.write("----\n")
16
17
18
17 def sigexit(*args):
19 def sigexit(*args):
18 sigshow(*args)
20 sigshow(*args)
19 print('alarm!')
21 print('alarm!')
20 sys.exit(1)
22 sys.exit(1)
21
23
24
22 def extsetup(ui):
25 def extsetup(ui):
23 signal.signal(signal.SIGQUIT, sigshow)
26 signal.signal(signal.SIGQUIT, sigshow)
24 signal.signal(signal.SIGALRM, sigexit)
27 signal.signal(signal.SIGALRM, sigexit)
@@ -62,9 +62,7 b' from mercurial import ('
62 registrar,
62 registrar,
63 scmutil,
63 scmutil,
64 )
64 )
65 from mercurial.utils import (
65 from mercurial.utils import dateutil
66 dateutil,
67 )
68
66
69 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
67 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
70 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
68 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
@@ -77,14 +75,17 b' command = registrar.command(cmdtable)'
77
75
78 newfile = {'new fi', 'rename', 'copy f', 'copy t'}
76 newfile = {'new fi', 'rename', 'copy f', 'copy t'}
79
77
78
80 def zerodict():
79 def zerodict():
81 return collections.defaultdict(lambda: 0)
80 return collections.defaultdict(lambda: 0)
82
81
82
83 def roundto(x, k):
83 def roundto(x, k):
84 if x > k * 2:
84 if x > k * 2:
85 return int(round(x / float(k)) * k)
85 return int(round(x / float(k)) * k)
86 return int(round(x))
86 return int(round(x))
87
87
88
88 def parsegitdiff(lines):
89 def parsegitdiff(lines):
89 filename, mar, lineadd, lineremove = None, None, zerodict(), 0
90 filename, mar, lineadd, lineremove = None, None, zerodict(), 0
90 binary = False
91 binary = False
@@ -110,10 +111,16 b' def parsegitdiff(lines):'
110 if filename:
111 if filename:
111 yield filename, mar, lineadd, lineremove, binary
112 yield filename, mar, lineadd, lineremove, binary
112
113
113 @command('analyze',
114
114 [('o', 'output', '', _('write output to given file'), _('FILE')),
115 @command(
115 ('r', 'rev', [], _('analyze specified revisions'), _('REV'))],
116 'analyze',
116 _('hg analyze'), optionalrepo=True)
117 [
118 ('o', 'output', '', _('write output to given file'), _('FILE')),
119 ('r', 'rev', [], _('analyze specified revisions'), _('REV')),
120 ],
121 _('hg analyze'),
122 optionalrepo=True,
123 )
117 def analyze(ui, repo, *revs, **opts):
124 def analyze(ui, repo, *revs, **opts):
118 '''create a simple model of a repository to use for later synthesis
125 '''create a simple model of a repository to use for later synthesis
119
126
@@ -176,8 +183,9 b' def analyze(ui, repo, *revs, **opts):'
176 revs = scmutil.revrange(repo, revs)
183 revs = scmutil.revrange(repo, revs)
177 revs.sort()
184 revs.sort()
178
185
179 progress = ui.makeprogress(_('analyzing'), unit=_('changesets'),
186 progress = ui.makeprogress(
180 total=len(revs))
187 _('analyzing'), unit=_('changesets'), total=len(revs)
188 )
181 for i, rev in enumerate(revs):
189 for i, rev in enumerate(revs):
182 progress.update(i)
190 progress.update(i)
183 ctx = repo[rev]
191 ctx = repo[rev]
@@ -198,8 +206,9 b' def analyze(ui, repo, *revs, **opts):'
198 timedelta = ctx.date()[0] - lastctx.date()[0]
206 timedelta = ctx.date()[0] - lastctx.date()[0]
199 interarrival[roundto(timedelta, 300)] += 1
207 interarrival[roundto(timedelta, 300)] += 1
200 diffopts = diffutil.diffallopts(ui, {'git': True})
208 diffopts = diffutil.diffallopts(ui, {'git': True})
201 diff = sum((d.splitlines()
209 diff = sum(
202 for d in ctx.diff(pctx, opts=diffopts)), [])
210 (d.splitlines() for d in ctx.diff(pctx, opts=diffopts)), []
211 )
203 fileadds, diradds, fileremoves, filechanges = 0, 0, 0, 0
212 fileadds, diradds, fileremoves, filechanges = 0, 0, 0, 0
204 for filename, mar, lineadd, lineremove, isbin in parsegitdiff(diff):
213 for filename, mar, lineadd, lineremove, isbin in parsegitdiff(diff):
205 if isbin:
214 if isbin:
@@ -207,8 +216,9 b' def analyze(ui, repo, *revs, **opts):'
207 added = sum(lineadd.itervalues(), 0)
216 added = sum(lineadd.itervalues(), 0)
208 if mar == 'm':
217 if mar == 'm':
209 if added and lineremove:
218 if added and lineremove:
210 lineschanged[roundto(added, 5),
219 lineschanged[
211 roundto(lineremove, 5)] += 1
220 roundto(added, 5), roundto(lineremove, 5)
221 ] += 1
212 filechanges += 1
222 filechanges += 1
213 elif mar == 'a':
223 elif mar == 'a':
214 fileadds += 1
224 fileadds += 1
@@ -238,30 +248,38 b' def analyze(ui, repo, *revs, **opts):'
238 def pronk(d):
248 def pronk(d):
239 return sorted(d.iteritems(), key=lambda x: x[1], reverse=True)
249 return sorted(d.iteritems(), key=lambda x: x[1], reverse=True)
240
250
241 json.dump({'revs': len(revs),
251 json.dump(
242 'initdirs': pronk(dirs),
252 {
243 'lineschanged': pronk(lineschanged),
253 'revs': len(revs),
244 'children': pronk(invchildren),
254 'initdirs': pronk(dirs),
245 'fileschanged': pronk(fileschanged),
255 'lineschanged': pronk(lineschanged),
246 'filesadded': pronk(filesadded),
256 'children': pronk(invchildren),
247 'linesinfilesadded': pronk(linesinfilesadded),
257 'fileschanged': pronk(fileschanged),
248 'dirsadded': pronk(dirsadded),
258 'filesadded': pronk(filesadded),
249 'filesremoved': pronk(filesremoved),
259 'linesinfilesadded': pronk(linesinfilesadded),
250 'linelengths': pronk(linelengths),
260 'dirsadded': pronk(dirsadded),
251 'parents': pronk(parents),
261 'filesremoved': pronk(filesremoved),
252 'p1distance': pronk(p1distance),
262 'linelengths': pronk(linelengths),
253 'p2distance': pronk(p2distance),
263 'parents': pronk(parents),
254 'interarrival': pronk(interarrival),
264 'p1distance': pronk(p1distance),
255 'tzoffset': pronk(tzoffset),
265 'p2distance': pronk(p2distance),
256 },
266 'interarrival': pronk(interarrival),
257 fp)
267 'tzoffset': pronk(tzoffset),
268 },
269 fp,
270 )
258 fp.close()
271 fp.close()
259
272
260 @command('synthesize',
273
261 [('c', 'count', 0, _('create given number of commits'), _('COUNT')),
274 @command(
262 ('', 'dict', '', _('path to a dictionary of words'), _('FILE')),
275 'synthesize',
263 ('', 'initfiles', 0, _('initial file count to create'), _('COUNT'))],
276 [
264 _('hg synthesize [OPTION].. DESCFILE'))
277 ('c', 'count', 0, _('create given number of commits'), _('COUNT')),
278 ('', 'dict', '', _('path to a dictionary of words'), _('FILE')),
279 ('', 'initfiles', 0, _('initial file count to create'), _('COUNT')),
280 ],
281 _('hg synthesize [OPTION].. DESCFILE'),
282 )
265 def synthesize(ui, repo, descpath, **opts):
283 def synthesize(ui, repo, descpath, **opts):
266 '''synthesize commits based on a model of an existing repository
284 '''synthesize commits based on a model of an existing repository
267
285
@@ -384,16 +402,23 b' def synthesize(ui, repo, descpath, **opt'
384
402
385 progress.complete()
403 progress.complete()
386 message = 'synthesized wide repo with %d files' % (len(files),)
404 message = 'synthesized wide repo with %d files' % (len(files),)
387 mc = context.memctx(repo, [pctx.node(), nullid], message,
405 mc = context.memctx(
388 files, filectxfn, ui.username(),
406 repo,
389 '%d %d' % dateutil.makedate())
407 [pctx.node(), nullid],
408 message,
409 files,
410 filectxfn,
411 ui.username(),
412 '%d %d' % dateutil.makedate(),
413 )
390 initnode = mc.commit()
414 initnode = mc.commit()
391 if ui.debugflag:
415 if ui.debugflag:
392 hexfn = hex
416 hexfn = hex
393 else:
417 else:
394 hexfn = short
418 hexfn = short
395 ui.status(_('added commit %s with %d files\n')
419 ui.status(
396 % (hexfn(initnode), len(files)))
420 _('added commit %s with %d files\n') % (hexfn(initnode), len(files))
421 )
397
422
398 # Synthesize incremental revisions to the repository, adding repo depth.
423 # Synthesize incremental revisions to the repository, adding repo depth.
399 count = int(opts['count'])
424 count = int(opts['count'])
@@ -437,8 +462,11 b' def synthesize(ui, repo, descpath, **opt'
437 for __ in pycompat.xrange(10):
462 for __ in pycompat.xrange(10):
438 fctx = pctx.filectx(random.choice(mfk))
463 fctx = pctx.filectx(random.choice(mfk))
439 path = fctx.path()
464 path = fctx.path()
440 if not (path in nevertouch or fctx.isbinary() or
465 if not (
441 'l' in fctx.flags()):
466 path in nevertouch
467 or fctx.isbinary()
468 or 'l' in fctx.flags()
469 ):
442 break
470 break
443 lines = fctx.data().splitlines()
471 lines = fctx.data().splitlines()
444 add, remove = pick(lineschanged)
472 add, remove = pick(lineschanged)
@@ -466,14 +494,20 b' def synthesize(ui, repo, descpath, **opt'
466 path.append(random.choice(words))
494 path.append(random.choice(words))
467 path.append(random.choice(words))
495 path.append(random.choice(words))
468 pathstr = '/'.join(filter(None, path))
496 pathstr = '/'.join(filter(None, path))
469 data = '\n'.join(
497 data = (
470 makeline()
498 '\n'.join(
471 for __ in pycompat.xrange(pick(linesinfilesadded))) + '\n'
499 makeline()
500 for __ in pycompat.xrange(pick(linesinfilesadded))
501 )
502 + '\n'
503 )
472 changes[pathstr] = data
504 changes[pathstr] = data
505
473 def filectxfn(repo, memctx, path):
506 def filectxfn(repo, memctx, path):
474 if path not in changes:
507 if path not in changes:
475 return None
508 return None
476 return context.memfilectx(repo, memctx, path, changes[path])
509 return context.memfilectx(repo, memctx, path, changes[path])
510
477 if not changes:
511 if not changes:
478 continue
512 continue
479 if revs:
513 if revs:
@@ -481,11 +515,17 b' def synthesize(ui, repo, descpath, **opt'
481 else:
515 else:
482 date = time.time() - (86400 * count)
516 date = time.time() - (86400 * count)
483 # dates in mercurial must be positive, fit in 32-bit signed integers.
517 # dates in mercurial must be positive, fit in 32-bit signed integers.
484 date = min(0x7fffffff, max(0, date))
518 date = min(0x7FFFFFFF, max(0, date))
485 user = random.choice(words) + '@' + random.choice(words)
519 user = random.choice(words) + '@' + random.choice(words)
486 mc = context.memctx(repo, pl, makeline(minimum=2),
520 mc = context.memctx(
487 sorted(changes),
521 repo,
488 filectxfn, user, '%d %d' % (date, pick(tzoffset)))
522 pl,
523 makeline(minimum=2),
524 sorted(changes),
525 filectxfn,
526 user,
527 '%d %d' % (date, pick(tzoffset)),
528 )
489 newnode = mc.commit()
529 newnode = mc.commit()
490 heads.add(repo.changelog.rev(newnode))
530 heads.add(repo.changelog.rev(newnode))
491 heads.discard(r1)
531 heads.discard(r1)
@@ -495,10 +535,12 b' def synthesize(ui, repo, descpath, **opt'
495 lock.release()
535 lock.release()
496 wlock.release()
536 wlock.release()
497
537
538
498 def renamedirs(dirs, words):
539 def renamedirs(dirs, words):
499 '''Randomly rename the directory names in the per-dir file count dict.'''
540 '''Randomly rename the directory names in the per-dir file count dict.'''
500 wordgen = itertools.cycle(words)
541 wordgen = itertools.cycle(words)
501 replacements = {'': ''}
542 replacements = {'': ''}
543
502 def rename(dirpath):
544 def rename(dirpath):
503 '''Recursively rename the directory and all path prefixes.
545 '''Recursively rename the directory and all path prefixes.
504
546
@@ -516,6 +558,7 b' def renamedirs(dirs, words):'
516 renamed = os.path.join(head, next(wordgen))
558 renamed = os.path.join(head, next(wordgen))
517 replacements[dirpath] = renamed
559 replacements[dirpath] = renamed
518 return renamed
560 return renamed
561
519 result = []
562 result = []
520 for dirpath, count in dirs.iteritems():
563 for dirpath, count in dirs.iteritems():
521 result.append([rename(dirpath.lstrip(os.sep)), count])
564 result.append([rename(dirpath.lstrip(os.sep)), count])
@@ -14,11 +14,13 b' import sys'
14 ####################
14 ####################
15 # for Python3 compatibility (almost comes from mercurial/pycompat.py)
15 # for Python3 compatibility (almost comes from mercurial/pycompat.py)
16
16
17 ispy3 = (sys.version_info[0] >= 3)
17 ispy3 = sys.version_info[0] >= 3
18
18
19
19 def identity(a):
20 def identity(a):
20 return a
21 return a
21
22
23
22 def _rapply(f, xs):
24 def _rapply(f, xs):
23 if xs is None:
25 if xs is None:
24 # assume None means non-value of optional data
26 # assume None means non-value of optional data
@@ -29,12 +31,14 b' def _rapply(f, xs):'
29 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
31 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
30 return f(xs)
32 return f(xs)
31
33
34
32 def rapply(f, xs):
35 def rapply(f, xs):
33 if f is identity:
36 if f is identity:
34 # fast path mainly for py2
37 # fast path mainly for py2
35 return xs
38 return xs
36 return _rapply(f, xs)
39 return _rapply(f, xs)
37
40
41
38 if ispy3:
42 if ispy3:
39 import builtins
43 import builtins
40
44
@@ -49,29 +53,37 b' if ispy3:'
49
53
50 def opentext(f):
54 def opentext(f):
51 return open(f, 'r')
55 return open(f, 'r')
56
57
52 else:
58 else:
53 bytestr = str
59 bytestr = str
54 sysstr = identity
60 sysstr = identity
55
61
56 opentext = open
62 opentext = open
57
63
64
58 def b2s(x):
65 def b2s(x):
59 # convert BYTES elements in "x" to SYSSTR recursively
66 # convert BYTES elements in "x" to SYSSTR recursively
60 return rapply(sysstr, x)
67 return rapply(sysstr, x)
61
68
69
62 def writeout(data):
70 def writeout(data):
63 # write "data" in BYTES into stdout
71 # write "data" in BYTES into stdout
64 sys.stdout.write(data)
72 sys.stdout.write(data)
65
73
74
66 def writeerr(data):
75 def writeerr(data):
67 # write "data" in BYTES into stderr
76 # write "data" in BYTES into stderr
68 sys.stderr.write(data)
77 sys.stderr.write(data)
69
78
79
70 ####################
80 ####################
71
81
82
72 class embeddedmatcher(object):
83 class embeddedmatcher(object):
73 """Base class to detect embedded code fragments in *.t test script
84 """Base class to detect embedded code fragments in *.t test script
74 """
85 """
86
75 __metaclass__ = abc.ABCMeta
87 __metaclass__ = abc.ABCMeta
76
88
77 def __init__(self, desc):
89 def __init__(self, desc):
@@ -126,6 +138,7 b' class embeddedmatcher(object):'
126 def codeinside(self, ctx, line):
138 def codeinside(self, ctx, line):
127 """Return actual code at line inside embedded code"""
139 """Return actual code at line inside embedded code"""
128
140
141
129 def embedded(basefile, lines, errors, matchers):
142 def embedded(basefile, lines, errors, matchers):
130 """pick embedded code fragments up from given lines
143 """pick embedded code fragments up from given lines
131
144
@@ -168,12 +181,12 b' def embedded(basefile, lines, errors, ma'
168
181
169 """
182 """
170 matcher = None
183 matcher = None
171 ctx = filename = code = startline = None # for pyflakes
184 ctx = filename = code = startline = None # for pyflakes
172
185
173 for lineno, line in enumerate(lines, 1):
186 for lineno, line in enumerate(lines, 1):
174 if not line.endswith('\n'):
187 if not line.endswith('\n'):
175 line += '\n' # to normalize EOF line
188 line += '\n' # to normalize EOF line
176 if matcher: # now, inside embedded code
189 if matcher: # now, inside embedded code
177 if matcher.endsat(ctx, line):
190 if matcher.endsat(ctx, line):
178 codeatend = matcher.codeatend(ctx, line)
191 codeatend = matcher.codeatend(ctx, line)
179 if codeatend is not None:
192 if codeatend is not None:
@@ -185,8 +198,10 b' def embedded(basefile, lines, errors, ma'
185 elif not matcher.isinside(ctx, line):
198 elif not matcher.isinside(ctx, line):
186 # this is an error of basefile
199 # this is an error of basefile
187 # (if matchers are implemented correctly)
200 # (if matchers are implemented correctly)
188 errors.append('%s:%d: unexpected line for "%s"'
201 errors.append(
189 % (basefile, lineno, matcher.desc))
202 '%s:%d: unexpected line for "%s"'
203 % (basefile, lineno, matcher.desc)
204 )
190 # stop extracting embedded code by current 'matcher',
205 # stop extracting embedded code by current 'matcher',
191 # because appearance of unexpected line might mean
206 # because appearance of unexpected line might mean
192 # that expected end-of-embedded-code line might never
207 # that expected end-of-embedded-code line might never
@@ -208,10 +223,14 b' def embedded(basefile, lines, errors, ma'
208 if matched:
223 if matched:
209 if len(matched) > 1:
224 if len(matched) > 1:
210 # this is an error of matchers, maybe
225 # this is an error of matchers, maybe
211 errors.append('%s:%d: ambiguous line for %s' %
226 errors.append(
212 (basefile, lineno,
227 '%s:%d: ambiguous line for %s'
213 ', '.join(['"%s"' % m.desc
228 % (
214 for m, c in matched])))
229 basefile,
230 lineno,
231 ', '.join(['"%s"' % m.desc for m, c in matched]),
232 )
233 )
215 # omit extracting embedded code, because choosing
234 # omit extracting embedded code, because choosing
216 # arbitrary matcher from matched ones might fail to
235 # arbitrary matcher from matched ones might fail to
217 # detect the end of embedded code as expected.
236 # detect the end of embedded code as expected.
@@ -238,8 +257,11 b' def embedded(basefile, lines, errors, ma'
238 else:
257 else:
239 # this is an error of basefile
258 # this is an error of basefile
240 # (if matchers are implemented correctly)
259 # (if matchers are implemented correctly)
241 errors.append('%s:%d: unexpected end of file for "%s"'
260 errors.append(
242 % (basefile, lineno, matcher.desc))
261 '%s:%d: unexpected end of file for "%s"'
262 % (basefile, lineno, matcher.desc)
263 )
264
243
265
244 # heredoc limit mark to ignore embedded code at check-code.py or so
266 # heredoc limit mark to ignore embedded code at check-code.py or so
245 heredocignorelimit = 'NO_CHECK_EOF'
267 heredocignorelimit = 'NO_CHECK_EOF'
@@ -252,6 +274,7 b" heredocignorelimit = 'NO_CHECK_EOF'"
252 # - << 'LIMITMARK'
274 # - << 'LIMITMARK'
253 heredoclimitpat = r'\s*<<\s*(?P<lquote>["\']?)(?P<limit>\w+)(?P=lquote)'
275 heredoclimitpat = r'\s*<<\s*(?P<lquote>["\']?)(?P<limit>\w+)(?P=lquote)'
254
276
277
255 class fileheredocmatcher(embeddedmatcher):
278 class fileheredocmatcher(embeddedmatcher):
256 """Detect "cat > FILE << LIMIT" style embedded code
279 """Detect "cat > FILE << LIMIT" style embedded code
257
280
@@ -290,6 +313,7 b' class fileheredocmatcher(embeddedmatcher'
290 >>> matcher.ignores(ctx)
313 >>> matcher.ignores(ctx)
291 True
314 True
292 """
315 """
316
293 _prefix = ' > '
317 _prefix = ' > '
294
318
295 def __init__(self, desc, namepat):
319 def __init__(self, desc, namepat):
@@ -302,8 +326,9 b' class fileheredocmatcher(embeddedmatcher'
302 # - > NAMEPAT
326 # - > NAMEPAT
303 # - > "NAMEPAT"
327 # - > "NAMEPAT"
304 # - > 'NAMEPAT'
328 # - > 'NAMEPAT'
305 namepat = (r'\s*>>?\s*(?P<nquote>["\']?)(?P<name>%s)(?P=nquote)'
329 namepat = (
306 % namepat)
330 r'\s*>>?\s*(?P<nquote>["\']?)(?P<name>%s)(?P=nquote)' % namepat
331 )
307 self._fileres = [
332 self._fileres = [
308 # "cat > NAME << LIMIT" case
333 # "cat > NAME << LIMIT" case
309 re.compile(r' \$ \s*cat' + namepat + heredoclimitpat),
334 re.compile(r' \$ \s*cat' + namepat + heredoclimitpat),
@@ -316,8 +341,10 b' class fileheredocmatcher(embeddedmatcher'
316 for filere in self._fileres:
341 for filere in self._fileres:
317 matched = filere.match(line)
342 matched = filere.match(line)
318 if matched:
343 if matched:
319 return (matched.group('name'),
344 return (
320 ' > %s\n' % matched.group('limit'))
345 matched.group('name'),
346 ' > %s\n' % matched.group('limit'),
347 )
321
348
322 def endsat(self, ctx, line):
349 def endsat(self, ctx, line):
323 return ctx[1] == line
350 return ctx[1] == line
@@ -332,17 +359,19 b' class fileheredocmatcher(embeddedmatcher'
332 return ctx[0]
359 return ctx[0]
333
360
334 def codeatstart(self, ctx, line):
361 def codeatstart(self, ctx, line):
335 return None # no embedded code at start line
362 return None # no embedded code at start line
336
363
337 def codeatend(self, ctx, line):
364 def codeatend(self, ctx, line):
338 return None # no embedded code at end line
365 return None # no embedded code at end line
339
366
340 def codeinside(self, ctx, line):
367 def codeinside(self, ctx, line):
341 return line[len(self._prefix):] # strip prefix
368 return line[len(self._prefix) :] # strip prefix
369
342
370
343 ####
371 ####
344 # for embedded python script
372 # for embedded python script
345
373
374
346 class pydoctestmatcher(embeddedmatcher):
375 class pydoctestmatcher(embeddedmatcher):
347 """Detect ">>> code" style embedded python code
376 """Detect ">>> code" style embedded python code
348
377
@@ -395,6 +424,7 b' class pydoctestmatcher(embeddedmatcher):'
395 True
424 True
396 >>> matcher.codeatend(ctx, end)
425 >>> matcher.codeatend(ctx, end)
397 """
426 """
427
398 _prefix = ' >>> '
428 _prefix = ' >>> '
399 _prefixre = re.compile(r' (>>>|\.\.\.) ')
429 _prefixre = re.compile(r' (>>>|\.\.\.) ')
400
430
@@ -419,24 +449,25 b' class pydoctestmatcher(embeddedmatcher):'
419 return not (self._prefixre.match(line) or self._outputre.match(line))
449 return not (self._prefixre.match(line) or self._outputre.match(line))
420
450
421 def isinside(self, ctx, line):
451 def isinside(self, ctx, line):
422 return True # always true, if not yet ended
452 return True # always true, if not yet ended
423
453
424 def ignores(self, ctx):
454 def ignores(self, ctx):
425 return False # should be checked always
455 return False # should be checked always
426
456
427 def filename(self, ctx):
457 def filename(self, ctx):
428 return None # no filename
458 return None # no filename
429
459
430 def codeatstart(self, ctx, line):
460 def codeatstart(self, ctx, line):
431 return line[len(self._prefix):] # strip prefix ' >>> '/' ... '
461 return line[len(self._prefix) :] # strip prefix ' >>> '/' ... '
432
462
433 def codeatend(self, ctx, line):
463 def codeatend(self, ctx, line):
434 return None # no embedded code at end line
464 return None # no embedded code at end line
435
465
436 def codeinside(self, ctx, line):
466 def codeinside(self, ctx, line):
437 if self._prefixre.match(line):
467 if self._prefixre.match(line):
438 return line[len(self._prefix):] # strip prefix ' >>> '/' ... '
468 return line[len(self._prefix) :] # strip prefix ' >>> '/' ... '
439 return '\n' # an expected output line is treated as an empty line
469 return '\n' # an expected output line is treated as an empty line
470
440
471
441 class pyheredocmatcher(embeddedmatcher):
472 class pyheredocmatcher(embeddedmatcher):
442 """Detect "python << LIMIT" style embedded python code
473 """Detect "python << LIMIT" style embedded python code
@@ -474,10 +505,12 b' class pyheredocmatcher(embeddedmatcher):'
474 >>> matcher.ignores(ctx)
505 >>> matcher.ignores(ctx)
475 True
506 True
476 """
507 """
508
477 _prefix = ' > '
509 _prefix = ' > '
478
510
479 _startre = re.compile(r' \$ (\$PYTHON|"\$PYTHON"|python).*' +
511 _startre = re.compile(
480 heredoclimitpat)
512 r' \$ (\$PYTHON|"\$PYTHON"|python).*' + heredoclimitpat
513 )
481
514
482 def __init__(self):
515 def __init__(self):
483 super(pyheredocmatcher, self).__init__("heredoc python invocation")
516 super(pyheredocmatcher, self).__init__("heredoc python invocation")
@@ -498,16 +531,17 b' class pyheredocmatcher(embeddedmatcher):'
498 return ' > %s\n' % heredocignorelimit == ctx
531 return ' > %s\n' % heredocignorelimit == ctx
499
532
500 def filename(self, ctx):
533 def filename(self, ctx):
501 return None # no filename
534 return None # no filename
502
535
503 def codeatstart(self, ctx, line):
536 def codeatstart(self, ctx, line):
504 return None # no embedded code at start line
537 return None # no embedded code at start line
505
538
506 def codeatend(self, ctx, line):
539 def codeatend(self, ctx, line):
507 return None # no embedded code at end line
540 return None # no embedded code at end line
508
541
509 def codeinside(self, ctx, line):
542 def codeinside(self, ctx, line):
510 return line[len(self._prefix):] # strip prefix
543 return line[len(self._prefix) :] # strip prefix
544
511
545
512 _pymatchers = [
546 _pymatchers = [
513 pydoctestmatcher(),
547 pydoctestmatcher(),
@@ -517,9 +551,11 b' class pyheredocmatcher(embeddedmatcher):'
517 fileheredocmatcher('heredoc .py file', r'[^<]+\.py'),
551 fileheredocmatcher('heredoc .py file', r'[^<]+\.py'),
518 ]
552 ]
519
553
554
520 def pyembedded(basefile, lines, errors):
555 def pyembedded(basefile, lines, errors):
521 return embedded(basefile, lines, errors, _pymatchers)
556 return embedded(basefile, lines, errors, _pymatchers)
522
557
558
523 ####
559 ####
524 # for embedded shell script
560 # for embedded shell script
525
561
@@ -529,22 +565,27 b' def pyembedded(basefile, lines, errors):'
529 fileheredocmatcher('heredoc .sh file', r'[^<]+\.sh'),
565 fileheredocmatcher('heredoc .sh file', r'[^<]+\.sh'),
530 ]
566 ]
531
567
568
532 def shembedded(basefile, lines, errors):
569 def shembedded(basefile, lines, errors):
533 return embedded(basefile, lines, errors, _shmatchers)
570 return embedded(basefile, lines, errors, _shmatchers)
534
571
572
535 ####
573 ####
536 # for embedded hgrc configuration
574 # for embedded hgrc configuration
537
575
538 _hgrcmatchers = [
576 _hgrcmatchers = [
539 # use '[^<]+' instead of '\S+', in order to match against
577 # use '[^<]+' instead of '\S+', in order to match against
540 # paths including whitespaces
578 # paths including whitespaces
541 fileheredocmatcher('heredoc hgrc file',
579 fileheredocmatcher(
542 r'(([^/<]+/)+hgrc|\$HGRCPATH|\${HGRCPATH})'),
580 'heredoc hgrc file', r'(([^/<]+/)+hgrc|\$HGRCPATH|\${HGRCPATH})'
581 ),
543 ]
582 ]
544
583
584
545 def hgrcembedded(basefile, lines, errors):
585 def hgrcembedded(basefile, lines, errors):
546 return embedded(basefile, lines, errors, _hgrcmatchers)
586 return embedded(basefile, lines, errors, _hgrcmatchers)
547
587
588
548 ####
589 ####
549
590
550 if __name__ == "__main__":
591 if __name__ == "__main__":
@@ -558,8 +599,7 b' if __name__ == "__main__":'
558 name = '<anonymous>'
599 name = '<anonymous>'
559 writeout("%s:%d: %s starts\n" % (basefile, starts, name))
600 writeout("%s:%d: %s starts\n" % (basefile, starts, name))
560 if opts.verbose and code:
601 if opts.verbose and code:
561 writeout(" |%s\n" %
602 writeout(" |%s\n" % "\n |".join(l for l in code.splitlines()))
562 "\n |".join(l for l in code.splitlines()))
563 writeout("%s:%d: %s ends\n" % (basefile, ends, name))
603 writeout("%s:%d: %s ends\n" % (basefile, ends, name))
564 for e in errors:
604 for e in errors:
565 writeerr("%s\n" % e)
605 writeerr("%s\n" % e)
@@ -579,9 +619,11 b' if __name__ == "__main__":'
579 return ret
619 return ret
580
620
581 commands = {}
621 commands = {}
622
582 def command(name, desc):
623 def command(name, desc):
583 def wrap(func):
624 def wrap(func):
584 commands[name] = (desc, func)
625 commands[name] = (desc, func)
626
585 return wrap
627 return wrap
586
628
587 @command("pyembedded", "detect embedded python script")
629 @command("pyembedded", "detect embedded python script")
@@ -596,21 +638,29 b' if __name__ == "__main__":'
596 def hgrcembeddedcmd(args, opts):
638 def hgrcembeddedcmd(args, opts):
597 return applyembedded(args, hgrcembedded, opts)
639 return applyembedded(args, hgrcembedded, opts)
598
640
599 availablecommands = "\n".join([" - %s: %s" % (key, value[0])
641 availablecommands = "\n".join(
600 for key, value in commands.items()])
642 [" - %s: %s" % (key, value[0]) for key, value in commands.items()]
643 )
601
644
602 parser = optparse.OptionParser("""%prog COMMAND [file ...]
645 parser = optparse.OptionParser(
646 """%prog COMMAND [file ...]
603
647
604 Pick up embedded code fragments from given file(s) or stdin, and list
648 Pick up embedded code fragments from given file(s) or stdin, and list
605 up start/end lines of them in standard compiler format
649 up start/end lines of them in standard compiler format
606 ("FILENAME:LINENO:").
650 ("FILENAME:LINENO:").
607
651
608 Available commands are:
652 Available commands are:
609 """ + availablecommands + """
653 """
610 """)
654 + availablecommands
611 parser.add_option("-v", "--verbose",
655 + """
612 help="enable additional output (e.g. actual code)",
656 """
613 action="store_true")
657 )
658 parser.add_option(
659 "-v",
660 "--verbose",
661 help="enable additional output (e.g. actual code)",
662 action="store_true",
663 )
614 (opts, args) = parser.parse_args()
664 (opts, args) = parser.parse_args()
615
665
616 if not args or args[0] not in commands:
666 if not args or args[0] not in commands:
@@ -84,19 +84,20 b' from __future__ import absolute_import'
84 hgweb_config = r'c:\your\directory\wsgi.config'
84 hgweb_config = r'c:\your\directory\wsgi.config'
85
85
86 # Global settings for IIS path translation
86 # Global settings for IIS path translation
87 path_strip = 0 # Strip this many path elements off (when using url rewrite)
87 path_strip = 0 # Strip this many path elements off (when using url rewrite)
88 path_prefix = 1 # This many path elements are prefixes (depends on the
88 path_prefix = 1 # This many path elements are prefixes (depends on the
89 # virtual path of the IIS application).
89 # virtual path of the IIS application).
90
90
91 import sys
91 import sys
92
92
93 # Adjust python path if this is not a system-wide install
93 # Adjust python path if this is not a system-wide install
94 #sys.path.insert(0, r'C:\your\custom\hg\build\lib.win32-2.7')
94 # sys.path.insert(0, r'C:\your\custom\hg\build\lib.win32-2.7')
95
95
96 # Enable tracing. Run 'python -m win32traceutil' to debug
96 # Enable tracing. Run 'python -m win32traceutil' to debug
97 if getattr(sys, 'isapidllhandle', None) is not None:
97 if getattr(sys, 'isapidllhandle', None) is not None:
98 import win32traceutil
98 import win32traceutil
99 win32traceutil.SetupForPrint # silence unused import warning
99
100 win32traceutil.SetupForPrint # silence unused import warning
100
101
101 import isapi_wsgi
102 import isapi_wsgi
102 from mercurial.hgweb.hgwebdir_mod import hgwebdir
103 from mercurial.hgweb.hgwebdir_mod import hgwebdir
@@ -104,13 +105,15 b' from mercurial.hgweb.hgwebdir_mod import'
104 # Example tweak: Replace isapi_wsgi's handler to provide better error message
105 # Example tweak: Replace isapi_wsgi's handler to provide better error message
105 # Other stuff could also be done here, like logging errors etc.
106 # Other stuff could also be done here, like logging errors etc.
106 class WsgiHandler(isapi_wsgi.IsapiWsgiHandler):
107 class WsgiHandler(isapi_wsgi.IsapiWsgiHandler):
107 error_status = '500 Internal Server Error' # less silly error message
108 error_status = '500 Internal Server Error' # less silly error message
109
108
110
109 isapi_wsgi.IsapiWsgiHandler = WsgiHandler
111 isapi_wsgi.IsapiWsgiHandler = WsgiHandler
110
112
111 # Only create the hgwebdir instance once
113 # Only create the hgwebdir instance once
112 application = hgwebdir(hgweb_config)
114 application = hgwebdir(hgweb_config)
113
115
116
114 def handler(environ, start_response):
117 def handler(environ, start_response):
115
118
116 # Translate IIS's weird URLs
119 # Translate IIS's weird URLs
@@ -125,10 +128,13 b' def handler(environ, start_response):'
125
128
126 return application(environ, start_response)
129 return application(environ, start_response)
127
130
131
128 def __ExtensionFactory__():
132 def __ExtensionFactory__():
129 return isapi_wsgi.ISAPISimpleHandler(handler)
133 return isapi_wsgi.ISAPISimpleHandler(handler)
130
134
131 if __name__=='__main__':
135
136 if __name__ == '__main__':
132 from isapi.install import ISAPIParameters, HandleCommandLine
137 from isapi.install import ISAPIParameters, HandleCommandLine
138
133 params = ISAPIParameters()
139 params = ISAPIParameters()
134 HandleCommandLine(params)
140 HandleCommandLine(params)
@@ -11,7 +11,9 b' import sys'
11 # import from the live mercurial repo
11 # import from the live mercurial repo
12 os.environ['HGMODULEPOLICY'] = 'py'
12 os.environ['HGMODULEPOLICY'] = 'py'
13 sys.path.insert(0, "..")
13 sys.path.insert(0, "..")
14 from mercurial import demandimport; demandimport.enable()
14 from mercurial import demandimport
15
16 demandimport.enable()
15 from mercurial import (
17 from mercurial import (
16 commands,
18 commands,
17 extensions,
19 extensions,
@@ -36,13 +38,16 b' initlevel_cmd = 1'
36 initlevel_ext = 1
38 initlevel_ext = 1
37 initlevel_ext_cmd = 3
39 initlevel_ext_cmd = 3
38
40
41
39 def showavailables(ui, initlevel):
42 def showavailables(ui, initlevel):
40 avail = (' available marks and order of them in this help: %s\n') % (
43 avail = ' available marks and order of them in this help: %s\n' % (
41 ', '.join(['%r' % (m * 4) for m in level2mark[initlevel + 1:]]))
44 ', '.join(['%r' % (m * 4) for m in level2mark[initlevel + 1 :]])
45 )
42 ui.warn(avail.encode('utf-8'))
46 ui.warn(avail.encode('utf-8'))
43
47
48
44 def checkseclevel(ui, doc, name, initlevel):
49 def checkseclevel(ui, doc, name, initlevel):
45 ui.note(('checking "%s"\n') % name)
50 ui.note('checking "%s"\n' % name)
46 if not isinstance(doc, bytes):
51 if not isinstance(doc, bytes):
47 doc = doc.encode('utf-8')
52 doc = doc.encode('utf-8')
48 blocks, pruned = minirst.parse(doc, 0, ['verbose'])
53 blocks, pruned = minirst.parse(doc, 0, ['verbose'])
@@ -54,66 +59,77 b' def checkseclevel(ui, doc, name, initlev'
54 mark = block[b'underline']
59 mark = block[b'underline']
55 title = block[b'lines'][0]
60 title = block[b'lines'][0]
56 if (mark not in mark2level) or (mark2level[mark] <= initlevel):
61 if (mark not in mark2level) or (mark2level[mark] <= initlevel):
57 ui.warn((('invalid section mark %r for "%s" of %s\n') %
62 ui.warn(
58 (mark * 4, title, name)).encode('utf-8'))
63 (
64 'invalid section mark %r for "%s" of %s\n'
65 % (mark * 4, title, name)
66 ).encode('utf-8')
67 )
59 showavailables(ui, initlevel)
68 showavailables(ui, initlevel)
60 errorcnt += 1
69 errorcnt += 1
61 continue
70 continue
62 nextlevel = mark2level[mark]
71 nextlevel = mark2level[mark]
63 if curlevel < nextlevel and curlevel + 1 != nextlevel:
72 if curlevel < nextlevel and curlevel + 1 != nextlevel:
64 ui.warn(('gap of section level at "%s" of %s\n') %
73 ui.warn('gap of section level at "%s" of %s\n' % (title, name))
65 (title, name))
66 showavailables(ui, initlevel)
74 showavailables(ui, initlevel)
67 errorcnt += 1
75 errorcnt += 1
68 continue
76 continue
69 ui.note(('appropriate section level for "%s %s"\n') %
77 ui.note(
70 (mark * (nextlevel * 2), title))
78 'appropriate section level for "%s %s"\n'
79 % (mark * (nextlevel * 2), title)
80 )
71 curlevel = nextlevel
81 curlevel = nextlevel
72
82
73 return errorcnt
83 return errorcnt
74
84
85
75 def checkcmdtable(ui, cmdtable, namefmt, initlevel):
86 def checkcmdtable(ui, cmdtable, namefmt, initlevel):
76 errorcnt = 0
87 errorcnt = 0
77 for k, entry in cmdtable.items():
88 for k, entry in cmdtable.items():
78 name = k.split(b"|")[0].lstrip(b"^")
89 name = k.split(b"|")[0].lstrip(b"^")
79 if not entry[0].__doc__:
90 if not entry[0].__doc__:
80 ui.note(('skip checking %s: no help document\n') %
91 ui.note('skip checking %s: no help document\n' % (namefmt % name))
81 (namefmt % name))
82 continue
92 continue
83 errorcnt += checkseclevel(ui, entry[0].__doc__,
93 errorcnt += checkseclevel(
84 namefmt % name,
94 ui, entry[0].__doc__, namefmt % name, initlevel
85 initlevel)
95 )
86 return errorcnt
96 return errorcnt
87
97
98
88 def checkhghelps(ui):
99 def checkhghelps(ui):
89 errorcnt = 0
100 errorcnt = 0
90 for h in helptable:
101 for h in helptable:
91 names, sec, doc = h[0:3]
102 names, sec, doc = h[0:3]
92 if callable(doc):
103 if callable(doc):
93 doc = doc(ui)
104 doc = doc(ui)
94 errorcnt += checkseclevel(ui, doc,
105 errorcnt += checkseclevel(
95 '%s help topic' % names[0],
106 ui, doc, '%s help topic' % names[0], initlevel_topic
96 initlevel_topic)
107 )
97
108
98 errorcnt += checkcmdtable(ui, table, '%s command', initlevel_cmd)
109 errorcnt += checkcmdtable(ui, table, '%s command', initlevel_cmd)
99
110
100 for name in sorted(list(extensions.enabled()) +
111 for name in sorted(
101 list(extensions.disabled())):
112 list(extensions.enabled()) + list(extensions.disabled())
113 ):
102 mod = extensions.load(ui, name, None)
114 mod = extensions.load(ui, name, None)
103 if not mod.__doc__:
115 if not mod.__doc__:
104 ui.note(('skip checking %s extension: no help document\n') % name)
116 ui.note('skip checking %s extension: no help document\n' % name)
105 continue
117 continue
106 errorcnt += checkseclevel(ui, mod.__doc__,
118 errorcnt += checkseclevel(
107 '%s extension' % name,
119 ui, mod.__doc__, '%s extension' % name, initlevel_ext
108 initlevel_ext)
120 )
109
121
110 cmdtable = getattr(mod, 'cmdtable', None)
122 cmdtable = getattr(mod, 'cmdtable', None)
111 if cmdtable:
123 if cmdtable:
112 errorcnt += checkcmdtable(ui, cmdtable,
124 errorcnt += checkcmdtable(
113 '%%s command of %s extension' % name,
125 ui,
114 initlevel_ext_cmd)
126 cmdtable,
127 '%%s command of %s extension' % name,
128 initlevel_ext_cmd,
129 )
115 return errorcnt
130 return errorcnt
116
131
132
117 def checkfile(ui, filename, initlevel):
133 def checkfile(ui, filename, initlevel):
118 if filename == '-':
134 if filename == '-':
119 filename = 'stdin'
135 filename = 'stdin'
@@ -122,43 +138,76 b' def checkfile(ui, filename, initlevel):'
122 with open(filename) as fp:
138 with open(filename) as fp:
123 doc = fp.read()
139 doc = fp.read()
124
140
125 ui.note(('checking input from %s with initlevel %d\n') %
141 ui.note(
126 (filename, initlevel))
142 'checking input from %s with initlevel %d\n' % (filename, initlevel)
143 )
127 return checkseclevel(ui, doc, 'input from %s' % filename, initlevel)
144 return checkseclevel(ui, doc, 'input from %s' % filename, initlevel)
128
145
146
129 def main():
147 def main():
130 optparser = optparse.OptionParser("""%prog [options]
148 optparser = optparse.OptionParser(
149 """%prog [options]
131
150
132 This checks all help documents of Mercurial (topics, commands,
151 This checks all help documents of Mercurial (topics, commands,
133 extensions and commands of them), if no file is specified by --file
152 extensions and commands of them), if no file is specified by --file
134 option.
153 option.
135 """)
154 """
136 optparser.add_option("-v", "--verbose",
155 )
137 help="enable additional output",
156 optparser.add_option(
138 action="store_true")
157 "-v", "--verbose", help="enable additional output", action="store_true"
139 optparser.add_option("-d", "--debug",
158 )
140 help="debug mode",
159 optparser.add_option(
141 action="store_true")
160 "-d", "--debug", help="debug mode", action="store_true"
142 optparser.add_option("-f", "--file",
161 )
143 help="filename to read in (or '-' for stdin)",
162 optparser.add_option(
144 action="store", default="")
163 "-f",
164 "--file",
165 help="filename to read in (or '-' for stdin)",
166 action="store",
167 default="",
168 )
145
169
146 optparser.add_option("-t", "--topic",
170 optparser.add_option(
147 help="parse file as help topic",
171 "-t",
148 action="store_const", dest="initlevel", const=0)
172 "--topic",
149 optparser.add_option("-c", "--command",
173 help="parse file as help topic",
150 help="parse file as help of core command",
174 action="store_const",
151 action="store_const", dest="initlevel", const=1)
175 dest="initlevel",
152 optparser.add_option("-e", "--extension",
176 const=0,
153 help="parse file as help of extension",
177 )
154 action="store_const", dest="initlevel", const=1)
178 optparser.add_option(
155 optparser.add_option("-C", "--extension-command",
179 "-c",
156 help="parse file as help of extension command",
180 "--command",
157 action="store_const", dest="initlevel", const=3)
181 help="parse file as help of core command",
182 action="store_const",
183 dest="initlevel",
184 const=1,
185 )
186 optparser.add_option(
187 "-e",
188 "--extension",
189 help="parse file as help of extension",
190 action="store_const",
191 dest="initlevel",
192 const=1,
193 )
194 optparser.add_option(
195 "-C",
196 "--extension-command",
197 help="parse file as help of extension command",
198 action="store_const",
199 dest="initlevel",
200 const=3,
201 )
158
202
159 optparser.add_option("-l", "--initlevel",
203 optparser.add_option(
160 help="set initial section level manually",
204 "-l",
161 action="store", type="int", default=0)
205 "--initlevel",
206 help="set initial section level manually",
207 action="store",
208 type="int",
209 default=0,
210 )
162
211
163 (options, args) = optparser.parse_args()
212 (options, args) = optparser.parse_args()
164
213
@@ -173,5 +222,6 b' option.'
173 if checkhghelps(ui):
222 if checkhghelps(ui):
174 sys.exit(1)
223 sys.exit(1)
175
224
225
176 if __name__ == "__main__":
226 if __name__ == "__main__":
177 main()
227 main()
@@ -12,6 +12,7 b' import textwrap'
12
12
13 try:
13 try:
14 import msvcrt
14 import msvcrt
15
15 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
16 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
16 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
17 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
17 except ImportError:
18 except ImportError:
@@ -22,10 +23,13 b' except ImportError:'
22 os.environ[r'HGMODULEPOLICY'] = r'allow'
23 os.environ[r'HGMODULEPOLICY'] = r'allow'
23 # import from the live mercurial repo
24 # import from the live mercurial repo
24 sys.path.insert(0, r"..")
25 sys.path.insert(0, r"..")
25 from mercurial import demandimport; demandimport.enable()
26 from mercurial import demandimport
27
28 demandimport.enable()
26 # Load util so that the locale path is set by i18n.setdatapath() before
29 # Load util so that the locale path is set by i18n.setdatapath() before
27 # calling _().
30 # calling _().
28 from mercurial import util
31 from mercurial import util
32
29 util.datapath
33 util.datapath
30 from mercurial import (
34 from mercurial import (
31 commands,
35 commands,
@@ -46,6 +50,7 b' globalopts = commands.globalopts'
46 helptable = help.helptable
50 helptable = help.helptable
47 loaddoc = help.loaddoc
51 loaddoc = help.loaddoc
48
52
53
49 def get_desc(docstr):
54 def get_desc(docstr):
50 if not docstr:
55 if not docstr:
51 return b"", b""
56 return b"", b""
@@ -56,7 +61,7 b' def get_desc(docstr):'
56
61
57 i = docstr.find(b"\n")
62 i = docstr.find(b"\n")
58 if i != -1:
63 if i != -1:
59 desc = docstr[i + 2:]
64 desc = docstr[i + 2 :]
60 else:
65 else:
61 desc = shortdesc
66 desc = shortdesc
62
67
@@ -64,6 +69,7 b' def get_desc(docstr):'
64
69
65 return (shortdesc, desc)
70 return (shortdesc, desc)
66
71
72
67 def get_opts(opts):
73 def get_opts(opts):
68 for opt in opts:
74 for opt in opts:
69 if len(opt) == 5:
75 if len(opt) == 5:
@@ -86,6 +92,7 b' def get_opts(opts):'
86 desc += default and _(b" (default: %s)") % bytes(default) or b""
92 desc += default and _(b" (default: %s)") % bytes(default) or b""
87 yield (b", ".join(allopts), desc)
93 yield (b", ".join(allopts), desc)
88
94
95
89 def get_cmd(cmd, cmdtable):
96 def get_cmd(cmd, cmdtable):
90 d = {}
97 d = {}
91 attr = cmdtable[cmd]
98 attr = cmdtable[cmd]
@@ -106,6 +113,7 b' def get_cmd(cmd, cmdtable):'
106
113
107 return d
114 return d
108
115
116
109 def showdoc(ui):
117 def showdoc(ui):
110 # print options
118 # print options
111 ui.write(minirst.section(_(b"Options")))
119 ui.write(minirst.section(_(b"Options")))
@@ -127,14 +135,22 b' def showdoc(ui):'
127 helpprinter(ui, helptable, minirst.section, exclude=[b'config'])
135 helpprinter(ui, helptable, minirst.section, exclude=[b'config'])
128
136
129 ui.write(minirst.section(_(b"Extensions")))
137 ui.write(minirst.section(_(b"Extensions")))
130 ui.write(_(b"This section contains help for extensions that are "
138 ui.write(
131 b"distributed together with Mercurial. Help for other "
139 _(
132 b"extensions is available in the help system."))
140 b"This section contains help for extensions that are "
133 ui.write((b"\n\n"
141 b"distributed together with Mercurial. Help for other "
134 b".. contents::\n"
142 b"extensions is available in the help system."
135 b" :class: htmlonly\n"
143 )
136 b" :local:\n"
144 )
137 b" :depth: 1\n\n"))
145 ui.write(
146 (
147 b"\n\n"
148 b".. contents::\n"
149 b" :class: htmlonly\n"
150 b" :local:\n"
151 b" :depth: 1\n\n"
152 )
153 )
138
154
139 for extensionname in sorted(allextensionnames()):
155 for extensionname in sorted(allextensionnames()):
140 mod = extensions.load(ui, extensionname, None)
156 mod = extensions.load(ui, extensionname, None)
@@ -143,24 +159,42 b' def showdoc(ui):'
143 cmdtable = getattr(mod, 'cmdtable', None)
159 cmdtable = getattr(mod, 'cmdtable', None)
144 if cmdtable:
160 if cmdtable:
145 ui.write(minirst.subsubsection(_(b'Commands')))
161 ui.write(minirst.subsubsection(_(b'Commands')))
146 commandprinter(ui, cmdtable, minirst.subsubsubsection,
162 commandprinter(
147 minirst.subsubsubsubsection)
163 ui,
164 cmdtable,
165 minirst.subsubsubsection,
166 minirst.subsubsubsubsection,
167 )
168
148
169
149 def showtopic(ui, topic):
170 def showtopic(ui, topic):
150 extrahelptable = [
171 extrahelptable = [
151 ([b"common"], b'', loaddoc(b'common'), help.TOPIC_CATEGORY_MISC),
172 ([b"common"], b'', loaddoc(b'common'), help.TOPIC_CATEGORY_MISC),
152 ([b"hg.1"], b'', loaddoc(b'hg.1'), help.TOPIC_CATEGORY_CONFIG),
173 ([b"hg.1"], b'', loaddoc(b'hg.1'), help.TOPIC_CATEGORY_CONFIG),
153 ([b"hg-ssh.8"], b'', loaddoc(b'hg-ssh.8'), help.TOPIC_CATEGORY_CONFIG),
174 ([b"hg-ssh.8"], b'', loaddoc(b'hg-ssh.8'), help.TOPIC_CATEGORY_CONFIG),
154 ([b"hgignore.5"], b'', loaddoc(b'hgignore.5'),
175 (
155 help.TOPIC_CATEGORY_CONFIG),
176 [b"hgignore.5"],
177 b'',
178 loaddoc(b'hgignore.5'),
179 help.TOPIC_CATEGORY_CONFIG,
180 ),
156 ([b"hgrc.5"], b'', loaddoc(b'hgrc.5'), help.TOPIC_CATEGORY_CONFIG),
181 ([b"hgrc.5"], b'', loaddoc(b'hgrc.5'), help.TOPIC_CATEGORY_CONFIG),
157 ([b"hgignore.5.gendoc"], b'', loaddoc(b'hgignore'),
182 (
158 help.TOPIC_CATEGORY_CONFIG),
183 [b"hgignore.5.gendoc"],
159 ([b"hgrc.5.gendoc"], b'', loaddoc(b'config'),
184 b'',
160 help.TOPIC_CATEGORY_CONFIG),
185 loaddoc(b'hgignore'),
186 help.TOPIC_CATEGORY_CONFIG,
187 ),
188 (
189 [b"hgrc.5.gendoc"],
190 b'',
191 loaddoc(b'config'),
192 help.TOPIC_CATEGORY_CONFIG,
193 ),
161 ]
194 ]
162 helpprinter(ui, helptable + extrahelptable, None, include=[topic])
195 helpprinter(ui, helptable + extrahelptable, None, include=[topic])
163
196
197
164 def helpprinter(ui, helptable, sectionfunc, include=[], exclude=[]):
198 def helpprinter(ui, helptable, sectionfunc, include=[], exclude=[]):
165 for h in helptable:
199 for h in helptable:
166 names, sec, doc = h[0:3]
200 names, sec, doc = h[0:3]
@@ -178,6 +212,7 b' def helpprinter(ui, helptable, sectionfu'
178 ui.write(doc)
212 ui.write(doc)
179 ui.write(b"\n")
213 ui.write(b"\n")
180
214
215
181 def commandprinter(ui, cmdtable, sectionfunc, subsectionfunc):
216 def commandprinter(ui, cmdtable, sectionfunc, subsectionfunc):
182 """Render restructuredtext describing a list of commands and their
217 """Render restructuredtext describing a list of commands and their
183 documentations, grouped by command category.
218 documentations, grouped by command category.
@@ -222,7 +257,8 b' def commandprinter(ui, cmdtable, section'
222 if helpcategory(cmd) not in cmdsbycategory:
257 if helpcategory(cmd) not in cmdsbycategory:
223 raise AssertionError(
258 raise AssertionError(
224 "The following command did not register its (category) in "
259 "The following command did not register its (category) in "
225 "help.CATEGORY_ORDER: %s (%s)" % (cmd, helpcategory(cmd)))
260 "help.CATEGORY_ORDER: %s (%s)" % (cmd, helpcategory(cmd))
261 )
226 cmdsbycategory[helpcategory(cmd)].append(cmd)
262 cmdsbycategory[helpcategory(cmd)].append(cmd)
227
263
228 # Print the help for each command. We present the commands grouped by
264 # Print the help for each command. We present the commands grouped by
@@ -270,16 +306,22 b' def commandprinter(ui, cmdtable, section'
270 if optstr.endswith(b"[+]>"):
306 if optstr.endswith(b"[+]>"):
271 multioccur = True
307 multioccur = True
272 if multioccur:
308 if multioccur:
273 ui.write(_(b"\n[+] marked option can be specified"
309 ui.write(
274 b" multiple times\n"))
310 _(
311 b"\n[+] marked option can be specified"
312 b" multiple times\n"
313 )
314 )
275 ui.write(b"\n")
315 ui.write(b"\n")
276 # aliases
316 # aliases
277 if d[b'aliases']:
317 if d[b'aliases']:
278 ui.write(_(b" aliases: %s\n\n") % b" ".join(d[b'aliases']))
318 ui.write(_(b" aliases: %s\n\n") % b" ".join(d[b'aliases']))
279
319
320
280 def allextensionnames():
321 def allextensionnames():
281 return set(extensions.enabled().keys()) | set(extensions.disabled().keys())
322 return set(extensions.enabled().keys()) | set(extensions.disabled().keys())
282
323
324
283 if __name__ == "__main__":
325 if __name__ == "__main__":
284 doc = b'hg.1.gendoc'
326 doc = b'hg.1.gendoc'
285 if len(sys.argv) > 1:
327 if len(sys.argv) > 1:
@@ -53,6 +53,7 b' from docutils import ('
53 nodes,
53 nodes,
54 writers,
54 writers,
55 )
55 )
56
56 try:
57 try:
57 import roman
58 import roman
58 except ImportError:
59 except ImportError:
@@ -65,7 +66,7 b' BLOCKQOUTE_INDENT = 3.5'
65
66
66 # Define two macros so man/roff can calculate the
67 # Define two macros so man/roff can calculate the
67 # indent/unindent margins by itself
68 # indent/unindent margins by itself
68 MACRO_DEF = (r""".
69 MACRO_DEF = r""".
69 .nr rst2man-indent-level 0
70 .nr rst2man-indent-level 0
70 .
71 .
71 .de1 rstReportMargin
72 .de1 rstReportMargin
@@ -92,11 +93,12 b' level margin: \\\\n[rst2man-indent\\\\n[rst2'
92 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
93 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
93 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
94 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
94 ..
95 ..
95 """)
96 """
97
96
98
97 class Writer(writers.Writer):
99 class Writer(writers.Writer):
98
100
99 supported = ('manpage')
101 supported = 'manpage'
100 """Formats this writer supports."""
102 """Formats this writer supports."""
101
103
102 output = None
104 output = None
@@ -118,11 +120,14 b' class Table(object):'
118 self._options = ['center']
120 self._options = ['center']
119 self._tab_char = '\t'
121 self._tab_char = '\t'
120 self._coldefs = []
122 self._coldefs = []
123
121 def new_row(self):
124 def new_row(self):
122 self._rows.append([])
125 self._rows.append([])
126
123 def append_separator(self, separator):
127 def append_separator(self, separator):
124 """Append the separator for table head."""
128 """Append the separator for table head."""
125 self._rows.append([separator])
129 self._rows.append([separator])
130
126 def append_cell(self, cell_lines):
131 def append_cell(self, cell_lines):
127 """cell_lines is an array of lines"""
132 """cell_lines is an array of lines"""
128 start = 0
133 start = 0
@@ -131,19 +136,21 b' class Table(object):'
131 self._rows[-1].append(cell_lines[start:])
136 self._rows[-1].append(cell_lines[start:])
132 if len(self._coldefs) < len(self._rows[-1]):
137 if len(self._coldefs) < len(self._rows[-1]):
133 self._coldefs.append('l')
138 self._coldefs.append('l')
139
134 def _minimize_cell(self, cell_lines):
140 def _minimize_cell(self, cell_lines):
135 """Remove leading and trailing blank and ``.sp`` lines"""
141 """Remove leading and trailing blank and ``.sp`` lines"""
136 while (cell_lines and cell_lines[0] in ('\n', '.sp\n')):
142 while cell_lines and cell_lines[0] in ('\n', '.sp\n'):
137 del cell_lines[0]
143 del cell_lines[0]
138 while (cell_lines and cell_lines[-1] in ('\n', '.sp\n')):
144 while cell_lines and cell_lines[-1] in ('\n', '.sp\n'):
139 del cell_lines[-1]
145 del cell_lines[-1]
146
140 def as_list(self):
147 def as_list(self):
141 text = ['.TS\n']
148 text = ['.TS\n']
142 text.append(' '.join(self._options) + ';\n')
149 text.append(' '.join(self._options) + ';\n')
143 text.append('|%s|.\n' % ('|'.join(self._coldefs)))
150 text.append('|%s|.\n' % ('|'.join(self._coldefs)))
144 for row in self._rows:
151 for row in self._rows:
145 # row = array of cells. cell = array of lines.
152 # row = array of cells. cell = array of lines.
146 text.append('_\n') # line above
153 text.append('_\n') # line above
147 text.append('T{\n')
154 text.append('T{\n')
148 for i in range(len(row)):
155 for i in range(len(row)):
149 cell = row[i]
156 cell = row[i]
@@ -152,13 +159,14 b' class Table(object):'
152 if not text[-1].endswith('\n'):
159 if not text[-1].endswith('\n'):
153 text[-1] += '\n'
160 text[-1] += '\n'
154 if i < len(row) - 1:
161 if i < len(row) - 1:
155 text.append('T}'+self._tab_char+'T{\n')
162 text.append('T}' + self._tab_char + 'T{\n')
156 else:
163 else:
157 text.append('T}\n')
164 text.append('T}\n')
158 text.append('_\n')
165 text.append('_\n')
159 text.append('.TE\n')
166 text.append('.TE\n')
160 return text
167 return text
161
168
169
162 class Translator(nodes.NodeVisitor):
170 class Translator(nodes.NodeVisitor):
163 """"""
171 """"""
164
172
@@ -171,8 +179,9 b' class Translator(nodes.NodeVisitor):'
171 lcode = settings.language_code
179 lcode = settings.language_code
172 arglen = len(inspect.getargspec(languages.get_language)[0])
180 arglen = len(inspect.getargspec(languages.get_language)[0])
173 if arglen == 2:
181 if arglen == 2:
174 self.language = languages.get_language(lcode,
182 self.language = languages.get_language(
175 self.document.reporter)
183 lcode, self.document.reporter
184 )
176 else:
185 else:
177 self.language = languages.get_language(lcode)
186 self.language = languages.get_language(lcode)
178 self.head = []
187 self.head = []
@@ -189,16 +198,18 b' class Translator(nodes.NodeVisitor):'
189 # writing the header .TH and .SH NAME is postboned after
198 # writing the header .TH and .SH NAME is postboned after
190 # docinfo.
199 # docinfo.
191 self._docinfo = {
200 self._docinfo = {
192 "title" : "", "title_upper": "",
201 "title": "",
193 "subtitle" : "",
202 "title_upper": "",
194 "manual_section" : "", "manual_group" : "",
203 "subtitle": "",
195 "author" : [],
204 "manual_section": "",
196 "date" : "",
205 "manual_group": "",
197 "copyright" : "",
206 "author": [],
198 "version" : "",
207 "date": "",
199 }
208 "copyright": "",
200 self._docinfo_keys = [] # a list to keep the sequence as in source.
209 "version": "",
201 self._docinfo_names = {} # to get name from text not normalized.
210 }
211 self._docinfo_keys = [] # a list to keep the sequence as in source.
212 self._docinfo_names = {} # to get name from text not normalized.
202 self._in_docinfo = None
213 self._in_docinfo = None
203 self._active_table = None
214 self._active_table = None
204 self._in_literal = False
215 self._in_literal = False
@@ -217,25 +228,21 b' class Translator(nodes.NodeVisitor):'
217 # ``B`` bold, ``I`` italic, ``R`` roman should be available.
228 # ``B`` bold, ``I`` italic, ``R`` roman should be available.
218 # Hopefully ``C`` courier too.
229 # Hopefully ``C`` courier too.
219 self.defs = {
230 self.defs = {
220 'indent' : ('.INDENT %.1f\n', '.UNINDENT\n'),
231 'indent': ('.INDENT %.1f\n', '.UNINDENT\n'),
221 'definition_list_item' : ('.TP', ''),
232 'definition_list_item': ('.TP', ''),
222 'field_name' : ('.TP\n.B ', '\n'),
233 'field_name': ('.TP\n.B ', '\n'),
223 'literal' : ('\\fB', '\\fP'),
234 'literal': ('\\fB', '\\fP'),
224 'literal_block' : ('.sp\n.nf\n.ft C\n', '\n.ft P\n.fi\n'),
235 'literal_block': ('.sp\n.nf\n.ft C\n', '\n.ft P\n.fi\n'),
225
236 'option_list_item': ('.TP\n', ''),
226 'option_list_item' : ('.TP\n', ''),
237 'reference': (r'\%', r'\:'),
227
238 'emphasis': ('\\fI', '\\fP'),
228 'reference' : (r'\%', r'\:'),
239 'strong': ('\\fB', '\\fP'),
229 'emphasis': ('\\fI', '\\fP'),
240 'term': ('\n.B ', '\n'),
230 'strong' : ('\\fB', '\\fP'),
241 'title_reference': ('\\fI', '\\fP'),
231 'term' : ('\n.B ', '\n'),
242 'topic-title': ('.SS ',),
232 'title_reference' : ('\\fI', '\\fP'),
243 'sidebar-title': ('.SS ',),
233
244 'problematic': ('\n.nf\n', '\n.fi\n'),
234 'topic-title' : ('.SS ',),
245 }
235 'sidebar-title' : ('.SS ',),
236
237 'problematic' : ('\n.nf\n', '\n.fi\n'),
238 }
239 # NOTE don't specify the newline before a dot-command, but ensure
246 # NOTE don't specify the newline before a dot-command, but ensure
240 # it is there.
247 # it is there.
241
248
@@ -244,13 +251,13 b' class Translator(nodes.NodeVisitor):'
244 line/comment."""
251 line/comment."""
245 prefix = '.\\" '
252 prefix = '.\\" '
246 out_text = ''.join(
253 out_text = ''.join(
247 [(prefix + in_line + '\n')
254 [(prefix + in_line + '\n') for in_line in text.split('\n')]
248 for in_line in text.split('\n')])
255 )
249 return out_text
256 return out_text
250
257
251 def comment(self, text):
258 def comment(self, text):
252 """Return commented version of the passed text."""
259 """Return commented version of the passed text."""
253 return self.comment_begin(text)+'.\n'
260 return self.comment_begin(text) + '.\n'
254
261
255 def ensure_eol(self):
262 def ensure_eol(self):
256 """Ensure the last line in body is terminated by new line."""
263 """Ensure the last line in body is terminated by new line."""
@@ -266,16 +273,21 b' class Translator(nodes.NodeVisitor):'
266 for i in range(len(self.body) - 1, 0, -1):
273 for i in range(len(self.body) - 1, 0, -1):
267 # remove superfluous vertical gaps.
274 # remove superfluous vertical gaps.
268 if self.body[i] == '.sp\n':
275 if self.body[i] == '.sp\n':
269 if self.body[i - 1][:4] in ('.BI ','.IP '):
276 if self.body[i - 1][:4] in ('.BI ', '.IP '):
270 self.body[i] = '.\n'
277 self.body[i] = '.\n'
271 elif (self.body[i - 1][:3] == '.B ' and
278 elif (
272 self.body[i - 2][:4] == '.TP\n'):
279 self.body[i - 1][:3] == '.B '
280 and self.body[i - 2][:4] == '.TP\n'
281 ):
273 self.body[i] = '.\n'
282 self.body[i] = '.\n'
274 elif (self.body[i - 1] == '\n' and
283 elif (
275 self.body[i - 2][0] != '.' and
284 self.body[i - 1] == '\n'
276 (self.body[i - 3][:7] == '.TP\n.B '
285 and self.body[i - 2][0] != '.'
277 or self.body[i - 3][:4] == '\n.B ')
286 and (
278 ):
287 self.body[i - 3][:7] == '.TP\n.B '
288 or self.body[i - 3][:4] == '\n.B '
289 )
290 ):
279 self.body[i] = '.\n'
291 self.body[i] = '.\n'
280 return ''.join(self.head + self.body + self.foot)
292 return ''.join(self.head + self.body + self.foot)
281
293
@@ -286,13 +298,13 b' class Translator(nodes.NodeVisitor):'
286
298
287 def visit_Text(self, node):
299 def visit_Text(self, node):
288 text = node.astext()
300 text = node.astext()
289 text = text.replace('\\','\\e')
301 text = text.replace('\\', '\\e')
290 replace_pairs = [
302 replace_pairs = [
291 (u'-', u'\\-'),
303 (u'-', u'\\-'),
292 (u"'", u'\\(aq'),
304 (u"'", u'\\(aq'),
293 (u'´', u"\\'"),
305 (u'´', u"\\'"),
294 (u'`', u'\\(ga'),
306 (u'`', u'\\(ga'),
295 ]
307 ]
296 for (in_char, out_markup) in replace_pairs:
308 for (in_char, out_markup) in replace_pairs:
297 text = text.replace(in_char, out_markup)
309 text = text.replace(in_char, out_markup)
298 # unicode
310 # unicode
@@ -310,9 +322,9 b' class Translator(nodes.NodeVisitor):'
310 def list_start(self, node):
322 def list_start(self, node):
311 class enum_char(object):
323 class enum_char(object):
312 enum_style = {
324 enum_style = {
313 'bullet' : '\\(bu',
325 'bullet': '\\(bu',
314 'emdash' : '\\(em',
326 'emdash': '\\(em',
315 }
327 }
316
328
317 def __init__(self, style):
329 def __init__(self, style):
318 self._style = style
330 self._style = style
@@ -358,6 +370,7 b' class Translator(nodes.NodeVisitor):'
358
370
359 def get_width(self):
371 def get_width(self):
360 return self._indent
372 return self._indent
373
361 def __repr__(self):
374 def __repr__(self):
362 return 'enum_style-%s' % list(self._style)
375 return 'enum_style-%s' % list(self._style)
363
376
@@ -376,10 +389,12 b' class Translator(nodes.NodeVisitor):'
376 self._list_char.pop()
389 self._list_char.pop()
377
390
378 def header(self):
391 def header(self):
379 tmpl = (".TH %(title_upper)s %(manual_section)s"
392 tmpl = (
380 " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
393 ".TH %(title_upper)s %(manual_section)s"
381 ".SH NAME\n"
394 " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
382 "%(title)s \\- %(subtitle)s\n")
395 ".SH NAME\n"
396 "%(title)s \\- %(subtitle)s\n"
397 )
383 return tmpl % self._docinfo
398 return tmpl % self._docinfo
384
399
385 def append_header(self):
400 def append_header(self):
@@ -400,8 +415,7 b' class Translator(nodes.NodeVisitor):'
400
415
401 def visit_admonition(self, node, name=None):
416 def visit_admonition(self, node, name=None):
402 if name:
417 if name:
403 self.body.append('.IP %s\n' %
418 self.body.append('.IP %s\n' % self.language.labels.get(name, name))
404 self.language.labels.get(name, name))
405
419
406 def depart_admonition(self, node):
420 def depart_admonition(self, node):
407 self.body.append('.RE\n')
421 self.body.append('.RE\n')
@@ -470,7 +484,7 b' class Translator(nodes.NodeVisitor):'
470 pass
484 pass
471
485
472 def visit_citation_reference(self, node):
486 def visit_citation_reference(self, node):
473 self.body.append('['+node.astext()+']')
487 self.body.append('[' + node.astext() + ']')
474 raise nodes.SkipNode()
488 raise nodes.SkipNode()
475
489
476 def visit_classifier(self, node):
490 def visit_classifier(self, node):
@@ -486,10 +500,9 b' class Translator(nodes.NodeVisitor):'
486 pass
500 pass
487
501
488 def write_colspecs(self):
502 def write_colspecs(self):
489 self.body.append("%s.\n" % ('L '*len(self.colspecs)))
503 self.body.append("%s.\n" % ('L ' * len(self.colspecs)))
490
504
491 def visit_comment(self, node,
505 def visit_comment(self, node, sub=re.compile('-(?=-)').sub):
492 sub=re.compile('-(?=-)').sub):
493 self.body.append(self.comment(node.astext()))
506 self.body.append(self.comment(node.astext()))
494 raise nodes.SkipNode()
507 raise nodes.SkipNode()
495
508
@@ -569,27 +582,39 b' class Translator(nodes.NodeVisitor):'
569
582
570 def visit_document(self, node):
583 def visit_document(self, node):
571 # no blank line between comment and header.
584 # no blank line between comment and header.
572 self.body.append(self.comment(self.document_start).rstrip()+'\n')
585 self.body.append(self.comment(self.document_start).rstrip() + '\n')
573 # writing header is postboned
586 # writing header is postboned
574 self.header_written = 0
587 self.header_written = 0
575
588
576 def depart_document(self, node):
589 def depart_document(self, node):
577 if self._docinfo['author']:
590 if self._docinfo['author']:
578 self.body.append('.SH AUTHOR\n%s\n'
591 self.body.append(
579 % ', '.join(self._docinfo['author']))
592 '.SH AUTHOR\n%s\n' % ', '.join(self._docinfo['author'])
580 skip = ('author', 'copyright', 'date',
593 )
581 'manual_group', 'manual_section',
594 skip = (
582 'subtitle',
595 'author',
583 'title', 'title_upper', 'version')
596 'copyright',
597 'date',
598 'manual_group',
599 'manual_section',
600 'subtitle',
601 'title',
602 'title_upper',
603 'version',
604 )
584 for name in self._docinfo_keys:
605 for name in self._docinfo_keys:
585 if name == 'address':
606 if name == 'address':
586 self.body.append("\n%s:\n%s%s.nf\n%s\n.fi\n%s%s" % (
607 self.body.append(
587 self.language.labels.get(name, name),
608 "\n%s:\n%s%s.nf\n%s\n.fi\n%s%s"
588 self.defs['indent'][0] % 0,
609 % (
589 self.defs['indent'][0] % BLOCKQOUTE_INDENT,
610 self.language.labels.get(name, name),
590 self._docinfo[name],
611 self.defs['indent'][0] % 0,
591 self.defs['indent'][1],
612 self.defs['indent'][0] % BLOCKQOUTE_INDENT,
592 self.defs['indent'][1]))
613 self._docinfo[name],
614 self.defs['indent'][1],
615 self.defs['indent'][1],
616 )
617 )
593 elif name not in skip:
618 elif name not in skip:
594 if name in self._docinfo_names:
619 if name in self._docinfo_names:
595 label = self._docinfo_names[name]
620 label = self._docinfo_names[name]
@@ -597,10 +622,10 b' class Translator(nodes.NodeVisitor):'
597 label = self.language.labels.get(name, name)
622 label = self.language.labels.get(name, name)
598 self.body.append("\n%s: %s\n" % (label, self._docinfo[name]))
623 self.body.append("\n%s: %s\n" % (label, self._docinfo[name]))
599 if self._docinfo['copyright']:
624 if self._docinfo['copyright']:
600 self.body.append('.SH COPYRIGHT\n%s\n'
625 self.body.append('.SH COPYRIGHT\n%s\n' % self._docinfo['copyright'])
601 % self._docinfo['copyright'])
626 self.body.append(
602 self.body.append(self.comment(
627 self.comment('Generated by docutils manpage writer.\n')
603 'Generated by docutils manpage writer.\n'))
628 )
604
629
605 def visit_emphasis(self, node):
630 def visit_emphasis(self, node):
606 self.body.append(self.defs['emphasis'][0])
631 self.body.append(self.defs['emphasis'][0])
@@ -611,11 +636,13 b' class Translator(nodes.NodeVisitor):'
611 def visit_entry(self, node):
636 def visit_entry(self, node):
612 # a cell in a table row
637 # a cell in a table row
613 if 'morerows' in node:
638 if 'morerows' in node:
614 self.document.reporter.warning('"table row spanning" not supported',
639 self.document.reporter.warning(
615 base_node=node)
640 '"table row spanning" not supported', base_node=node
641 )
616 if 'morecols' in node:
642 if 'morecols' in node:
617 self.document.reporter.warning(
643 self.document.reporter.warning(
618 '"table cell spanning" not supported', base_node=node)
644 '"table cell spanning" not supported', base_node=node
645 )
619 self.context.append(len(self.body))
646 self.context.append(len(self.body))
620
647
621 def depart_entry(self, node):
648 def depart_entry(self, node):
@@ -642,7 +669,7 b' class Translator(nodes.NodeVisitor):'
642
669
643 def visit_field_body(self, node):
670 def visit_field_body(self, node):
644 if self._in_docinfo:
671 if self._in_docinfo:
645 name_normalized = self._field_name.lower().replace(" ","_")
672 name_normalized = self._field_name.lower().replace(" ", "_")
646 self._docinfo_names[name_normalized] = self._field_name
673 self._docinfo_names[name_normalized] = self._field_name
647 self.visit_docinfo_item(node, name_normalized)
674 self.visit_docinfo_item(node, name_normalized)
648 raise nodes.SkipNode()
675 raise nodes.SkipNode()
@@ -675,8 +702,7 b' class Translator(nodes.NodeVisitor):'
675 self.dedent()
702 self.dedent()
676
703
677 def visit_footer(self, node):
704 def visit_footer(self, node):
678 self.document.reporter.warning('"footer" not supported',
705 self.document.reporter.warning('"footer" not supported', base_node=node)
679 base_node=node)
680
706
681 def depart_footer(self, node):
707 def depart_footer(self, node):
682 pass
708 pass
@@ -690,11 +716,12 b' class Translator(nodes.NodeVisitor):'
690 pass
716 pass
691
717
692 def footnote_backrefs(self, node):
718 def footnote_backrefs(self, node):
693 self.document.reporter.warning('"footnote_backrefs" not supported',
719 self.document.reporter.warning(
694 base_node=node)
720 '"footnote_backrefs" not supported', base_node=node
721 )
695
722
696 def visit_footnote_reference(self, node):
723 def visit_footnote_reference(self, node):
697 self.body.append('['+self.deunicode(node.astext())+']')
724 self.body.append('[' + self.deunicode(node.astext()) + ']')
698 raise nodes.SkipNode()
725 raise nodes.SkipNode()
699
726
700 def depart_footnote_reference(self, node):
727 def depart_footnote_reference(self, node):
@@ -736,8 +763,7 b' class Translator(nodes.NodeVisitor):'
736 self.body.append('\n')
763 self.body.append('\n')
737
764
738 def visit_image(self, node):
765 def visit_image(self, node):
739 self.document.reporter.warning('"image" not supported',
766 self.document.reporter.warning('"image" not supported', base_node=node)
740 base_node=node)
741 text = []
767 text = []
742 if 'alt' in node.attributes:
768 if 'alt' in node.attributes:
743 text.append(node.attributes['alt'])
769 text.append(node.attributes['alt'])
@@ -753,11 +779,11 b' class Translator(nodes.NodeVisitor):'
753
779
754 def visit_label(self, node):
780 def visit_label(self, node):
755 # footnote and citation
781 # footnote and citation
756 if (isinstance(node.parent, nodes.footnote)
782 if isinstance(node.parent, nodes.footnote) or isinstance(
757 or isinstance(node.parent, nodes.citation)):
783 node.parent, nodes.citation
784 ):
758 raise nodes.SkipNode()
785 raise nodes.SkipNode()
759 self.document.reporter.warning('"unsupported "label"',
786 self.document.reporter.warning('"unsupported "label"', base_node=node)
760 base_node=node)
761 self.body.append('[')
787 self.body.append('[')
762
788
763 def depart_label(self, node):
789 def depart_label(self, node):
@@ -794,9 +820,10 b' class Translator(nodes.NodeVisitor):'
794
820
795 def visit_list_item(self, node):
821 def visit_list_item(self, node):
796 # man 7 man argues to use ".IP" instead of ".TP"
822 # man 7 man argues to use ".IP" instead of ".TP"
797 self.body.append('.IP %s %d\n' % (
823 self.body.append(
798 next(self._list_char[-1]),
824 '.IP %s %d\n'
799 self._list_char[-1].get_width(),))
825 % (next(self._list_char[-1]), self._list_char[-1].get_width(),)
826 )
800
827
801 def depart_list_item(self, node):
828 def depart_list_item(self, node):
802 pass
829 pass
@@ -855,9 +882,9 b' class Translator(nodes.NodeVisitor):'
855 # options with parameter bold italic, .BI, -f file
882 # options with parameter bold italic, .BI, -f file
856 #
883 #
857 # we do not know if .B or .BI
884 # we do not know if .B or .BI
858 self.context.append('.B') # blind guess
885 self.context.append('.B') # blind guess
859 self.context.append(len(self.body)) # to be able to insert later
886 self.context.append(len(self.body)) # to be able to insert later
860 self.context.append(0) # option counter
887 self.context.append(0) # option counter
861
888
862 def depart_option_group(self, node):
889 def depart_option_group(self, node):
863 self.context.pop() # the counter
890 self.context.pop() # the counter
@@ -885,7 +912,7 b' class Translator(nodes.NodeVisitor):'
885 pass
912 pass
886
913
887 def visit_option_argument(self, node):
914 def visit_option_argument(self, node):
888 self.context[-3] = '.BI' # bold/italic alternate
915 self.context[-3] = '.BI' # bold/italic alternate
889 if node['delimiter'] != ' ':
916 if node['delimiter'] != ' ':
890 self.body.append('\\fB%s ' % node['delimiter'])
917 self.body.append('\\fB%s ' % node['delimiter'])
891 elif self.body[len(self.body) - 1].endswith('='):
918 elif self.body[len(self.body) - 1].endswith('='):
@@ -968,8 +995,9 b' class Translator(nodes.NodeVisitor):'
968 raise nodes.SkipNode()
995 raise nodes.SkipNode()
969
996
970 def visit_substitution_reference(self, node):
997 def visit_substitution_reference(self, node):
971 self.document.reporter.warning('"substitution_reference" not supported',
998 self.document.reporter.warning(
972 base_node=node)
999 '"substitution_reference" not supported', base_node=node
1000 )
973
1001
974 def visit_subtitle(self, node):
1002 def visit_subtitle(self, node):
975 if isinstance(node.parent, nodes.sidebar):
1003 if isinstance(node.parent, nodes.sidebar):
@@ -981,11 +1009,11 b' class Translator(nodes.NodeVisitor):'
981
1009
982 def depart_subtitle(self, node):
1010 def depart_subtitle(self, node):
983 # document subtitle calls SkipNode
1011 # document subtitle calls SkipNode
984 self.body.append(self.defs['strong'][1]+'\n.PP\n')
1012 self.body.append(self.defs['strong'][1] + '\n.PP\n')
985
1013
986 def visit_system_message(self, node):
1014 def visit_system_message(self, node):
987 # TODO add report_level
1015 # TODO add report_level
988 #if node['level'] < self.document.reporter['writer'].report_level:
1016 # if node['level'] < self.document.reporter['writer'].report_level:
989 # Level is too low to display:
1017 # Level is too low to display:
990 # raise nodes.SkipNode
1018 # raise nodes.SkipNode
991 attr = {}
1019 attr = {}
@@ -995,8 +1023,10 b' class Translator(nodes.NodeVisitor):'
995 line = ', line %s' % node['line']
1023 line = ', line %s' % node['line']
996 else:
1024 else:
997 line = ''
1025 line = ''
998 self.body.append('.IP "System Message: %s/%s (%s:%s)"\n'
1026 self.body.append(
999 % (node['type'], node['level'], node['source'], line))
1027 '.IP "System Message: %s/%s (%s:%s)"\n'
1028 % (node['type'], node['level'], node['source'], line)
1029 )
1000
1030
1001 def depart_system_message(self, node):
1031 def depart_system_message(self, node):
1002 pass
1032 pass
@@ -1111,7 +1141,9 b' class Translator(nodes.NodeVisitor):'
1111 depart_warning = depart_admonition
1141 depart_warning = depart_admonition
1112
1142
1113 def unimplemented_visit(self, node):
1143 def unimplemented_visit(self, node):
1114 raise NotImplementedError('visiting unimplemented node type: %s'
1144 raise NotImplementedError(
1115 % node.__class__.__name__)
1145 'visiting unimplemented node type: %s' % node.__class__.__name__
1146 )
1147
1116
1148
1117 # vim: set fileencoding=utf-8 et ts=4 ai :
1149 # vim: set fileencoding=utf-8 et ts=4 ai :
@@ -30,10 +30,10 b' IGNORES = {'
30 '_imp',
30 '_imp',
31 '_xmlplus',
31 '_xmlplus',
32 'fcntl',
32 'fcntl',
33 'nt', # pathlib2 tests the existence of built-in 'nt' module
33 'nt', # pathlib2 tests the existence of built-in 'nt' module
34 'win32com.gen_py',
34 'win32com.gen_py',
35 'win32com.shell', # 'appdirs' tries to import win32com.shell
35 'win32com.shell', # 'appdirs' tries to import win32com.shell
36 '_winreg', # 2.7 mimetypes needs immediate ImportError
36 '_winreg', # 2.7 mimetypes needs immediate ImportError
37 'pythoncom',
37 'pythoncom',
38 # imported by tarfile, not available under Windows
38 # imported by tarfile, not available under Windows
39 'pwd',
39 'pwd',
@@ -46,16 +46,16 b' IGNORES = {'
46 # setuptools' pkg_resources.py expects "from __main__ import x" to
46 # setuptools' pkg_resources.py expects "from __main__ import x" to
47 # raise ImportError if x not defined
47 # raise ImportError if x not defined
48 '__main__',
48 '__main__',
49 '_ssl', # conditional imports in the stdlib, issue1964
49 '_ssl', # conditional imports in the stdlib, issue1964
50 '_sre', # issue4920
50 '_sre', # issue4920
51 'rfc822',
51 'rfc822',
52 'mimetools',
52 'mimetools',
53 'sqlalchemy.events', # has import-time side effects (issue5085)
53 'sqlalchemy.events', # has import-time side effects (issue5085)
54 # setuptools 8 expects this module to explode early when not on windows
54 # setuptools 8 expects this module to explode early when not on windows
55 'distutils.msvc9compiler',
55 'distutils.msvc9compiler',
56 '__builtin__',
56 '__builtin__',
57 'builtins',
57 'builtins',
58 'urwid.command_map', # for pudb
58 'urwid.command_map', # for pudb
59 }
59 }
60
60
61 _pypy = '__pypy__' in sys.builtin_module_names
61 _pypy = '__pypy__' in sys.builtin_module_names
@@ -71,8 +71,11 b' isenabled = demandimport.isenabled'
71 disable = demandimport.disable
71 disable = demandimport.disable
72 deactivated = demandimport.deactivated
72 deactivated = demandimport.deactivated
73
73
74
74 def enable():
75 def enable():
75 # chg pre-imports modules so do not enable demandimport for it
76 # chg pre-imports modules so do not enable demandimport for it
76 if ('CHGINTERNALMARK' not in os.environ
77 if (
77 and os.environ.get('HGDEMANDIMPORT') != 'disable'):
78 'CHGINTERNALMARK' not in os.environ
79 and os.environ.get('HGDEMANDIMPORT') != 'disable'
80 ):
78 demandimport.enable()
81 demandimport.enable()
@@ -38,6 +38,7 b' contextmanager = contextlib.contextmanag'
38
38
39 nothing = object()
39 nothing = object()
40
40
41
41 def _hgextimport(importfunc, name, globals, *args, **kwargs):
42 def _hgextimport(importfunc, name, globals, *args, **kwargs):
42 try:
43 try:
43 return importfunc(name, globals, *args, **kwargs)
44 return importfunc(name, globals, *args, **kwargs)
@@ -53,6 +54,7 b' def _hgextimport(importfunc, name, globa'
53 # retry to import with "hgext_" prefix
54 # retry to import with "hgext_" prefix
54 return importfunc(hgextname, globals, *args, **kwargs)
55 return importfunc(hgextname, globals, *args, **kwargs)
55
56
57
56 class _demandmod(object):
58 class _demandmod(object):
57 """module demand-loader and proxy
59 """module demand-loader and proxy
58
60
@@ -67,8 +69,9 b' class _demandmod(object):'
67 else:
69 else:
68 head = name
70 head = name
69 after = []
71 after = []
70 object.__setattr__(self, r"_data",
72 object.__setattr__(
71 (head, globals, locals, after, level, set()))
73 self, r"_data", (head, globals, locals, after, level, set())
74 )
72 object.__setattr__(self, r"_module", None)
75 object.__setattr__(self, r"_module", None)
73
76
74 def _extend(self, name):
77 def _extend(self, name):
@@ -91,7 +94,8 b' class _demandmod(object):'
91 with tracing.log('demandimport %s', self._data[0]):
94 with tracing.log('demandimport %s', self._data[0]):
92 head, globals, locals, after, level, modrefs = self._data
95 head, globals, locals, after, level, modrefs = self._data
93 mod = _hgextimport(
96 mod = _hgextimport(
94 _origimport, head, globals, locals, None, level)
97 _origimport, head, globals, locals, None, level
98 )
95 if mod is self:
99 if mod is self:
96 # In this case, _hgextimport() above should imply
100 # In this case, _hgextimport() above should imply
97 # _demandimport(). Otherwise, _hgextimport() never
101 # _demandimport(). Otherwise, _hgextimport() never
@@ -115,8 +119,11 b' class _demandmod(object):'
115 if '.' in p:
119 if '.' in p:
116 h, t = p.split('.', 1)
120 h, t = p.split('.', 1)
117 if getattr(mod, h, nothing) is nothing:
121 if getattr(mod, h, nothing) is nothing:
118 setattr(mod, h, _demandmod(
122 setattr(
119 p, mod.__dict__, mod.__dict__, level=1))
123 mod,
124 h,
125 _demandmod(p, mod.__dict__, mod.__dict__, level=1),
126 )
120 elif t:
127 elif t:
121 subload(getattr(mod, h), t)
128 subload(getattr(mod, h), t)
122
129
@@ -164,15 +171,17 b' class _demandmod(object):'
164 self._load()
171 self._load()
165 return self._module.__doc__
172 return self._module.__doc__
166
173
174
167 _pypy = '__pypy__' in sys.builtin_module_names
175 _pypy = '__pypy__' in sys.builtin_module_names
168
176
177
169 def _demandimport(name, globals=None, locals=None, fromlist=None, level=-1):
178 def _demandimport(name, globals=None, locals=None, fromlist=None, level=-1):
170 if locals is None or name in ignores or fromlist == ('*',):
179 if locals is None or name in ignores or fromlist == ('*',):
171 # these cases we can't really delay
180 # these cases we can't really delay
172 return _hgextimport(_origimport, name, globals, locals, fromlist, level)
181 return _hgextimport(_origimport, name, globals, locals, fromlist, level)
173 elif not fromlist:
182 elif not fromlist:
174 # import a [as b]
183 # import a [as b]
175 if '.' in name: # a.b
184 if '.' in name: # a.b
176 base, rest = name.split('.', 1)
185 base, rest = name.split('.', 1)
177 # email.__init__ loading email.mime
186 # email.__init__ loading email.mime
178 if globals and globals.get('__name__', None) == base:
187 if globals and globals.get('__name__', None) == base:
@@ -244,8 +253,9 b' def _demandimport(name, globals=None, lo'
244 if level >= 0:
253 if level >= 0:
245 if name:
254 if name:
246 # "from a import b" or "from .a import b" style
255 # "from a import b" or "from .a import b" style
247 rootmod = _hgextimport(_origimport, name, globals, locals,
256 rootmod = _hgextimport(
248 level=level)
257 _origimport, name, globals, locals, level=level
258 )
249 mod = chainmodules(rootmod, name)
259 mod = chainmodules(rootmod, name)
250 elif _pypy:
260 elif _pypy:
251 # PyPy's __import__ throws an exception if invoked
261 # PyPy's __import__ throws an exception if invoked
@@ -260,8 +270,9 b' def _demandimport(name, globals=None, lo'
260 mn = mn.rsplit('.', level - 1)[0]
270 mn = mn.rsplit('.', level - 1)[0]
261 mod = sys.modules[mn]
271 mod = sys.modules[mn]
262 else:
272 else:
263 mod = _hgextimport(_origimport, name, globals, locals,
273 mod = _hgextimport(
264 level=level)
274 _origimport, name, globals, locals, level=level
275 )
265
276
266 for x in fromlist:
277 for x in fromlist:
267 processfromitem(mod, x)
278 processfromitem(mod, x)
@@ -278,23 +289,29 b' def _demandimport(name, globals=None, lo'
278
289
279 return mod
290 return mod
280
291
292
281 ignores = set()
293 ignores = set()
282
294
295
283 def init(ignoreset):
296 def init(ignoreset):
284 global ignores
297 global ignores
285 ignores = ignoreset
298 ignores = ignoreset
286
299
300
287 def isenabled():
301 def isenabled():
288 return builtins.__import__ == _demandimport
302 return builtins.__import__ == _demandimport
289
303
304
290 def enable():
305 def enable():
291 "enable global demand-loading of modules"
306 "enable global demand-loading of modules"
292 builtins.__import__ = _demandimport
307 builtins.__import__ = _demandimport
293
308
309
294 def disable():
310 def disable():
295 "disable global demand-loading of modules"
311 "disable global demand-loading of modules"
296 builtins.__import__ = _origimport
312 builtins.__import__ = _origimport
297
313
314
298 @contextmanager
315 @contextmanager
299 def deactivated():
316 def deactivated():
300 "context manager for disabling demandimport in 'with' blocks"
317 "context manager for disabling demandimport in 'with' blocks"
@@ -36,10 +36,12 b' from . import tracing'
36
36
37 _deactivated = False
37 _deactivated = False
38
38
39
39 class _lazyloaderex(importlib.util.LazyLoader):
40 class _lazyloaderex(importlib.util.LazyLoader):
40 """This is a LazyLoader except it also follows the _deactivated global and
41 """This is a LazyLoader except it also follows the _deactivated global and
41 the ignore list.
42 the ignore list.
42 """
43 """
44
43 def exec_module(self, module):
45 def exec_module(self, module):
44 """Make the module load lazily."""
46 """Make the module load lazily."""
45 with tracing.log('demandimport %s', module):
47 with tracing.log('demandimport %s', module):
@@ -48,14 +50,18 b' class _lazyloaderex(importlib.util.LazyL'
48 else:
50 else:
49 super().exec_module(module)
51 super().exec_module(module)
50
52
53
51 # This is 3.6+ because with Python 3.5 it isn't possible to lazily load
54 # This is 3.6+ because with Python 3.5 it isn't possible to lazily load
52 # extensions. See the discussion in https://bugs.python.org/issue26186 for more.
55 # extensions. See the discussion in https://bugs.python.org/issue26186 for more.
53 _extensions_loader = _lazyloaderex.factory(
56 _extensions_loader = _lazyloaderex.factory(
54 importlib.machinery.ExtensionFileLoader)
57 importlib.machinery.ExtensionFileLoader
58 )
55 _bytecode_loader = _lazyloaderex.factory(
59 _bytecode_loader = _lazyloaderex.factory(
56 importlib.machinery.SourcelessFileLoader)
60 importlib.machinery.SourcelessFileLoader
61 )
57 _source_loader = _lazyloaderex.factory(importlib.machinery.SourceFileLoader)
62 _source_loader = _lazyloaderex.factory(importlib.machinery.SourceFileLoader)
58
63
64
59 def _makefinder(path):
65 def _makefinder(path):
60 return importlib.machinery.FileFinder(
66 return importlib.machinery.FileFinder(
61 path,
67 path,
@@ -65,15 +71,19 b' def _makefinder(path):'
65 (_bytecode_loader, importlib.machinery.BYTECODE_SUFFIXES),
71 (_bytecode_loader, importlib.machinery.BYTECODE_SUFFIXES),
66 )
72 )
67
73
74
68 ignores = set()
75 ignores = set()
69
76
77
70 def init(ignoreset):
78 def init(ignoreset):
71 global ignores
79 global ignores
72 ignores = ignoreset
80 ignores = ignoreset
73
81
82
74 def isenabled():
83 def isenabled():
75 return _makefinder in sys.path_hooks and not _deactivated
84 return _makefinder in sys.path_hooks and not _deactivated
76
85
86
77 def disable():
87 def disable():
78 try:
88 try:
79 while True:
89 while True:
@@ -81,9 +91,11 b' def disable():'
81 except ValueError:
91 except ValueError:
82 pass
92 pass
83
93
94
84 def enable():
95 def enable():
85 sys.path_hooks.insert(0, _makefinder)
96 sys.path_hooks.insert(0, _makefinder)
86
97
98
87 @contextlib.contextmanager
99 @contextlib.contextmanager
88 def deactivated():
100 def deactivated():
89 # This implementation is a bit different from Python 2's. Python 3
101 # This implementation is a bit different from Python 2's. Python 3
@@ -14,6 +14,7 b' import os'
14 _checked = False
14 _checked = False
15 _session = 'none'
15 _session = 'none'
16
16
17
17 def _isactive():
18 def _isactive():
18 global _pipe, _session, _checked
19 global _pipe, _session, _checked
19 if _pipe is None:
20 if _pipe is None:
@@ -26,6 +27,7 b' def _isactive():'
26 _session = os.environ.get('HGCATAPULTSESSION', 'none')
27 _session = os.environ.get('HGCATAPULTSESSION', 'none')
27 return True
28 return True
28
29
30
29 @contextlib.contextmanager
31 @contextlib.contextmanager
30 def log(whencefmt, *whenceargs):
32 def log(whencefmt, *whenceargs):
31 if not _isactive():
33 if not _isactive():
@@ -48,6 +50,7 b' def log(whencefmt, *whenceargs):'
48 except IOError:
50 except IOError:
49 pass
51 pass
50
52
53
51 def counter(label, amount, *labelargs):
54 def counter(label, amount, *labelargs):
52 if not _isactive():
55 if not _isactive():
53 return
56 return
@@ -1,3 +1,4 b''
1 from __future__ import absolute_import
1 from __future__ import absolute_import
2 import pkgutil
2 import pkgutil
3
3 __path__ = pkgutil.extend_path(__path__, __name__)
4 __path__ = pkgutil.extend_path(__path__, __name__)
@@ -53,9 +53,7 b' from mercurial import ('
53 scmutil,
53 scmutil,
54 util,
54 util,
55 )
55 )
56 from mercurial.utils import (
56 from mercurial.utils import stringutil
57 stringutil,
58 )
59
57
60 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
58 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
61 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
59 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
@@ -81,8 +79,10 b' colortable = {'
81
79
82 defaultdict = collections.defaultdict
80 defaultdict = collections.defaultdict
83
81
82
84 class nullui(object):
83 class nullui(object):
85 """blank ui object doing nothing"""
84 """blank ui object doing nothing"""
85
86 debugflag = False
86 debugflag = False
87 verbose = False
87 verbose = False
88 quiet = True
88 quiet = True
@@ -90,16 +90,20 b' class nullui(object):'
90 def __getitem__(name):
90 def __getitem__(name):
91 def nullfunc(*args, **kwds):
91 def nullfunc(*args, **kwds):
92 return
92 return
93
93 return nullfunc
94 return nullfunc
94
95
96
95 class emptyfilecontext(object):
97 class emptyfilecontext(object):
96 """minimal filecontext representing an empty file"""
98 """minimal filecontext representing an empty file"""
99
97 def data(self):
100 def data(self):
98 return ''
101 return ''
99
102
100 def node(self):
103 def node(self):
101 return node.nullid
104 return node.nullid
102
105
106
103 def uniq(lst):
107 def uniq(lst):
104 """list -> list. remove duplicated items without changing the order"""
108 """list -> list. remove duplicated items without changing the order"""
105 seen = set()
109 seen = set()
@@ -110,6 +114,7 b' def uniq(lst):'
110 result.append(x)
114 result.append(x)
111 return result
115 return result
112
116
117
113 def getdraftstack(headctx, limit=None):
118 def getdraftstack(headctx, limit=None):
114 """(ctx, int?) -> [ctx]. get a linear stack of non-public changesets.
119 """(ctx, int?) -> [ctx]. get a linear stack of non-public changesets.
115
120
@@ -132,6 +137,7 b' def getdraftstack(headctx, limit=None):'
132 result.reverse()
137 result.reverse()
133 return result
138 return result
134
139
140
135 def getfilestack(stack, path, seenfctxs=None):
141 def getfilestack(stack, path, seenfctxs=None):
136 """([ctx], str, set) -> [fctx], {ctx: fctx}
142 """([ctx], str, set) -> [fctx], {ctx: fctx}
137
143
@@ -179,25 +185,25 b' def getfilestack(stack, path, seenfctxs='
179 fctxs = []
185 fctxs = []
180 fctxmap = {}
186 fctxmap = {}
181
187
182 pctx = stack[0].p1() # the public (immutable) ctx we stop at
188 pctx = stack[0].p1() # the public (immutable) ctx we stop at
183 for ctx in reversed(stack):
189 for ctx in reversed(stack):
184 if path not in ctx: # the file is added in the next commit
190 if path not in ctx: # the file is added in the next commit
185 pctx = ctx
191 pctx = ctx
186 break
192 break
187 fctx = ctx[path]
193 fctx = ctx[path]
188 fctxs.append(fctx)
194 fctxs.append(fctx)
189 if fctx in seenfctxs: # treat fctx as the immutable one
195 if fctx in seenfctxs: # treat fctx as the immutable one
190 pctx = None # do not add another immutable fctx
196 pctx = None # do not add another immutable fctx
191 break
197 break
192 fctxmap[ctx] = fctx # only for mutable fctxs
198 fctxmap[ctx] = fctx # only for mutable fctxs
193 copy = fctx.copysource()
199 copy = fctx.copysource()
194 if copy:
200 if copy:
195 path = copy # follow rename
201 path = copy # follow rename
196 if path in ctx: # but do not follow copy
202 if path in ctx: # but do not follow copy
197 pctx = ctx.p1()
203 pctx = ctx.p1()
198 break
204 break
199
205
200 if pctx is not None: # need an extra immutable fctx
206 if pctx is not None: # need an extra immutable fctx
201 if path in pctx:
207 if path in pctx:
202 fctxs.append(pctx[path])
208 fctxs.append(pctx[path])
203 else:
209 else:
@@ -213,10 +219,12 b' def getfilestack(stack, path, seenfctxs='
213 # remove uniq and find a different way to identify fctxs.
219 # remove uniq and find a different way to identify fctxs.
214 return uniq(fctxs), fctxmap
220 return uniq(fctxs), fctxmap
215
221
222
216 class overlaystore(patch.filestore):
223 class overlaystore(patch.filestore):
217 """read-only, hybrid store based on a dict and ctx.
224 """read-only, hybrid store based on a dict and ctx.
218 memworkingcopy: {path: content}, overrides file contents.
225 memworkingcopy: {path: content}, overrides file contents.
219 """
226 """
227
220 def __init__(self, basectx, memworkingcopy):
228 def __init__(self, basectx, memworkingcopy):
221 self.basectx = basectx
229 self.basectx = basectx
222 self.memworkingcopy = memworkingcopy
230 self.memworkingcopy = memworkingcopy
@@ -234,6 +242,7 b' class overlaystore(patch.filestore):'
234 copy = fctx.copysource()
242 copy = fctx.copysource()
235 return content, mode, copy
243 return content, mode, copy
236
244
245
237 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
246 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
238 """({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx
247 """({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx
239 memworkingcopy overrides file contents.
248 memworkingcopy overrides file contents.
@@ -249,9 +258,17 b' def overlaycontext(memworkingcopy, ctx, '
249 files = set(ctx.files()).union(memworkingcopy)
258 files = set(ctx.files()).union(memworkingcopy)
250 store = overlaystore(ctx, memworkingcopy)
259 store = overlaystore(ctx, memworkingcopy)
251 return context.memctx(
260 return context.memctx(
252 repo=ctx.repo(), parents=parents, text=desc,
261 repo=ctx.repo(),
253 files=files, filectxfn=store, user=user, date=date,
262 parents=parents,
254 branch=None, extra=extra)
263 text=desc,
264 files=files,
265 filectxfn=store,
266 user=user,
267 date=date,
268 branch=None,
269 extra=extra,
270 )
271
255
272
256 class filefixupstate(object):
273 class filefixupstate(object):
257 """state needed to apply fixups to a single file
274 """state needed to apply fixups to a single file
@@ -294,10 +311,10 b' class filefixupstate(object):'
294 assert self._checkoutlinelog() == self.contents
311 assert self._checkoutlinelog() == self.contents
295
312
296 # following fields will be filled later
313 # following fields will be filled later
297 self.chunkstats = [0, 0] # [adopted, total : int]
314 self.chunkstats = [0, 0] # [adopted, total : int]
298 self.targetlines = [] # [str]
315 self.targetlines = [] # [str]
299 self.fixups = [] # [(linelog rev, a1, a2, b1, b2)]
316 self.fixups = [] # [(linelog rev, a1, a2, b1, b2)]
300 self.finalcontents = [] # [str]
317 self.finalcontents = [] # [str]
301 self.ctxaffected = set()
318 self.ctxaffected = set()
302
319
303 def diffwith(self, targetfctx, fm=None):
320 def diffwith(self, targetfctx, fm=None):
@@ -319,7 +336,7 b' class filefixupstate(object):'
319 self.targetlines = blines
336 self.targetlines = blines
320
337
321 self.linelog.annotate(self.linelog.maxrev)
338 self.linelog.annotate(self.linelog.maxrev)
322 annotated = self.linelog.annotateresult # [(linelog rev, linenum)]
339 annotated = self.linelog.annotateresult # [(linelog rev, linenum)]
323 assert len(annotated) == len(alines)
340 assert len(annotated) == len(alines)
324 # add a dummy end line to make insertion at the end easier
341 # add a dummy end line to make insertion at the end easier
325 if annotated:
342 if annotated:
@@ -329,7 +346,7 b' class filefixupstate(object):'
329 # analyse diff blocks
346 # analyse diff blocks
330 for chunk in self._alldiffchunks(a, b, alines, blines):
347 for chunk in self._alldiffchunks(a, b, alines, blines):
331 newfixups = self._analysediffchunk(chunk, annotated)
348 newfixups = self._analysediffchunk(chunk, annotated)
332 self.chunkstats[0] += bool(newfixups) # 1 or 0
349 self.chunkstats[0] += bool(newfixups) # 1 or 0
333 self.chunkstats[1] += 1
350 self.chunkstats[1] += 1
334 self.fixups += newfixups
351 self.fixups += newfixups
335 if fm is not None:
352 if fm is not None:
@@ -346,9 +363,10 b' class filefixupstate(object):'
346 blines = self.targetlines[b1:b2]
363 blines = self.targetlines[b1:b2]
347 if self.ui.debugflag:
364 if self.ui.debugflag:
348 idx = (max(rev - 1, 0)) // 2
365 idx = (max(rev - 1, 0)) // 2
349 self.ui.write(_('%s: chunk %d:%d -> %d lines\n')
366 self.ui.write(
350 % (node.short(self.fctxs[idx].node()),
367 _('%s: chunk %d:%d -> %d lines\n')
351 a1, a2, len(blines)))
368 % (node.short(self.fctxs[idx].node()), a1, a2, len(blines))
369 )
352 self.linelog.replacelines(rev, a1, a2, b1, b2)
370 self.linelog.replacelines(rev, a1, a2, b1, b2)
353 if self.opts.get('edit_lines', False):
371 if self.opts.get('edit_lines', False):
354 self.finalcontents = self._checkoutlinelogwithedits()
372 self.finalcontents = self._checkoutlinelogwithedits()
@@ -382,12 +400,13 b' class filefixupstate(object):'
382 a1, a2, b1, b2 = chunk
400 a1, a2, b1, b2 = chunk
383 # find involved indexes from annotate result
401 # find involved indexes from annotate result
384 involved = annotated[a1:a2]
402 involved = annotated[a1:a2]
385 if not involved and annotated: # a1 == a2 and a is not empty
403 if not involved and annotated: # a1 == a2 and a is not empty
386 # pure insertion, check nearby lines. ignore lines belong
404 # pure insertion, check nearby lines. ignore lines belong
387 # to the public (first) changeset (i.e. annotated[i][0] == 1)
405 # to the public (first) changeset (i.e. annotated[i][0] == 1)
388 nearbylinenums = {a2, max(0, a1 - 1)}
406 nearbylinenums = {a2, max(0, a1 - 1)}
389 involved = [annotated[i]
407 involved = [
390 for i in nearbylinenums if annotated[i][0] != 1]
408 annotated[i] for i in nearbylinenums if annotated[i][0] != 1
409 ]
391 involvedrevs = list(set(r for r, l in involved))
410 involvedrevs = list(set(r for r, l in involved))
392 newfixups = []
411 newfixups = []
393 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
412 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
@@ -401,9 +420,9 b' class filefixupstate(object):'
401 for i in pycompat.xrange(a1, a2):
420 for i in pycompat.xrange(a1, a2):
402 rev, linenum = annotated[i]
421 rev, linenum = annotated[i]
403 if rev > 1:
422 if rev > 1:
404 if b1 == b2: # deletion, simply remove that single line
423 if b1 == b2: # deletion, simply remove that single line
405 nb1 = nb2 = 0
424 nb1 = nb2 = 0
406 else: # 1:1 line mapping, change the corresponding rev
425 else: # 1:1 line mapping, change the corresponding rev
407 nb1 = b1 + i - a1
426 nb1 = b1 + i - a1
408 nb2 = nb1 + 1
427 nb2 = nb1 + 1
409 fixuprev = rev + 1
428 fixuprev = rev + 1
@@ -448,32 +467,45 b' class filefixupstate(object):'
448 """() -> [str]. prompt all lines for edit"""
467 """() -> [str]. prompt all lines for edit"""
449 alllines = self.linelog.getalllines()
468 alllines = self.linelog.getalllines()
450 # header
469 # header
451 editortext = (_('HG: editing %s\nHG: "y" means the line to the right '
470 editortext = (
452 'exists in the changeset to the top\nHG:\n')
471 _(
453 % self.fctxs[-1].path())
472 'HG: editing %s\nHG: "y" means the line to the right '
473 'exists in the changeset to the top\nHG:\n'
474 )
475 % self.fctxs[-1].path()
476 )
454 # [(idx, fctx)]. hide the dummy emptyfilecontext
477 # [(idx, fctx)]. hide the dummy emptyfilecontext
455 visiblefctxs = [(i, f)
478 visiblefctxs = [
456 for i, f in enumerate(self.fctxs)
479 (i, f)
457 if not isinstance(f, emptyfilecontext)]
480 for i, f in enumerate(self.fctxs)
481 if not isinstance(f, emptyfilecontext)
482 ]
458 for i, (j, f) in enumerate(visiblefctxs):
483 for i, (j, f) in enumerate(visiblefctxs):
459 editortext += (_('HG: %s/%s %s %s\n') %
484 editortext += _('HG: %s/%s %s %s\n') % (
460 ('|' * i, '-' * (len(visiblefctxs) - i + 1),
485 '|' * i,
461 node.short(f.node()),
486 '-' * (len(visiblefctxs) - i + 1),
462 f.description().split('\n',1)[0]))
487 node.short(f.node()),
488 f.description().split('\n', 1)[0],
489 )
463 editortext += _('HG: %s\n') % ('|' * len(visiblefctxs))
490 editortext += _('HG: %s\n') % ('|' * len(visiblefctxs))
464 # figure out the lifetime of a line, this is relatively inefficient,
491 # figure out the lifetime of a line, this is relatively inefficient,
465 # but probably fine
492 # but probably fine
466 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}}
493 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}}
467 for i, f in visiblefctxs:
494 for i, f in visiblefctxs:
468 self.linelog.annotate((i + 1) * 2)
495 self.linelog.annotate((i + 1) * 2)
469 for l in self.linelog.annotateresult:
496 for l in self.linelog.annotateresult:
470 lineset[l].add(i)
497 lineset[l].add(i)
471 # append lines
498 # append lines
472 for l in alllines:
499 for l in alllines:
473 editortext += (' %s : %s' %
500 editortext += ' %s : %s' % (
474 (''.join([('y' if i in lineset[l] else ' ')
501 ''.join(
475 for i, _f in visiblefctxs]),
502 [
476 self._getline(l)))
503 ('y' if i in lineset[l] else ' ')
504 for i, _f in visiblefctxs
505 ]
506 ),
507 self._getline(l),
508 )
477 # run editor
509 # run editor
478 editedtext = self.ui.edit(editortext, '', action='absorb')
510 editedtext = self.ui.edit(editortext, '', action='absorb')
479 if not editedtext:
511 if not editedtext:
@@ -485,11 +517,12 b' class filefixupstate(object):'
485 for l in mdiff.splitnewlines(editedtext):
517 for l in mdiff.splitnewlines(editedtext):
486 if l.startswith('HG:'):
518 if l.startswith('HG:'):
487 continue
519 continue
488 if l[colonpos - 1:colonpos + 2] != ' : ':
520 if l[colonpos - 1 : colonpos + 2] != ' : ':
489 raise error.Abort(_('malformed line: %s') % l)
521 raise error.Abort(_('malformed line: %s') % l)
490 linecontent = l[colonpos + 2:]
522 linecontent = l[colonpos + 2 :]
491 for i, ch in enumerate(
523 for i, ch in enumerate(
492 pycompat.bytestr(l[leftpadpos:colonpos - 1])):
524 pycompat.bytestr(l[leftpadpos : colonpos - 1])
525 ):
493 if ch == 'y':
526 if ch == 'y':
494 contents[visiblefctxs[i][0]] += linecontent
527 contents[visiblefctxs[i][0]] += linecontent
495 # chunkstats is hard to calculate if anything changes, therefore
528 # chunkstats is hard to calculate if anything changes, therefore
@@ -501,9 +534,9 b' class filefixupstate(object):'
501 def _getline(self, lineinfo):
534 def _getline(self, lineinfo):
502 """((rev, linenum)) -> str. convert rev+line number to line content"""
535 """((rev, linenum)) -> str. convert rev+line number to line content"""
503 rev, linenum = lineinfo
536 rev, linenum = lineinfo
504 if rev & 1: # odd: original line taken from fctxs
537 if rev & 1: # odd: original line taken from fctxs
505 return self.contentlines[rev // 2][linenum]
538 return self.contentlines[rev // 2][linenum]
506 else: # even: fixup line from targetfctx
539 else: # even: fixup line from targetfctx
507 return self.targetlines[linenum]
540 return self.targetlines[linenum]
508
541
509 def _iscontinuous(self, a1, a2, closedinterval=False):
542 def _iscontinuous(self, a1, a2, closedinterval=False):
@@ -539,8 +572,12 b' class filefixupstate(object):'
539 lastrev = pcurrentchunk[0][0]
572 lastrev = pcurrentchunk[0][0]
540 lasta2 = pcurrentchunk[0][2]
573 lasta2 = pcurrentchunk[0][2]
541 lastb2 = pcurrentchunk[0][4]
574 lastb2 = pcurrentchunk[0][4]
542 if (a1 == lasta2 and b1 == lastb2 and rev == lastrev and
575 if (
543 self._iscontinuous(max(a1 - 1, 0), a1)):
576 a1 == lasta2
577 and b1 == lastb2
578 and rev == lastrev
579 and self._iscontinuous(max(a1 - 1, 0), a1)
580 ):
544 # merge into currentchunk
581 # merge into currentchunk
545 pcurrentchunk[0][2] = a2
582 pcurrentchunk[0][2] = a2
546 pcurrentchunk[0][4] = b2
583 pcurrentchunk[0][4] = b2
@@ -551,7 +588,6 b' class filefixupstate(object):'
551 return result
588 return result
552
589
553 def _showchanges(self, fm, alines, blines, chunk, fixups):
590 def _showchanges(self, fm, alines, blines, chunk, fixups):
554
555 def trim(line):
591 def trim(line):
556 if line.endswith('\n'):
592 if line.endswith('\n'):
557 line = line[:-1]
593 line = line[:-1]
@@ -568,9 +604,12 b' class filefixupstate(object):'
568 bidxs[i - b1] = (max(idx, 1) - 1) // 2
604 bidxs[i - b1] = (max(idx, 1) - 1) // 2
569
605
570 fm.startitem()
606 fm.startitem()
571 fm.write('hunk', ' %s\n',
607 fm.write(
572 '@@ -%d,%d +%d,%d @@'
608 'hunk',
573 % (a1, a2 - a1, b1, b2 - b1), label='diff.hunk')
609 ' %s\n',
610 '@@ -%d,%d +%d,%d @@' % (a1, a2 - a1, b1, b2 - b1),
611 label='diff.hunk',
612 )
574 fm.data(path=self.path, linetype='hunk')
613 fm.data(path=self.path, linetype='hunk')
575
614
576 def writeline(idx, diffchar, line, linetype, linelabel):
615 def writeline(idx, diffchar, line, linetype, linelabel):
@@ -582,16 +621,24 b' class filefixupstate(object):'
582 node = ctx.hex()
621 node = ctx.hex()
583 self.ctxaffected.add(ctx.changectx())
622 self.ctxaffected.add(ctx.changectx())
584 fm.write('node', '%-7.7s ', node, label='absorb.node')
623 fm.write('node', '%-7.7s ', node, label='absorb.node')
585 fm.write('diffchar ' + linetype, '%s%s\n', diffchar, line,
624 fm.write(
586 label=linelabel)
625 'diffchar ' + linetype,
626 '%s%s\n',
627 diffchar,
628 line,
629 label=linelabel,
630 )
587 fm.data(path=self.path, linetype=linetype)
631 fm.data(path=self.path, linetype=linetype)
588
632
589 for i in pycompat.xrange(a1, a2):
633 for i in pycompat.xrange(a1, a2):
590 writeline(aidxs[i - a1], '-', trim(alines[i]), 'deleted',
634 writeline(
591 'diff.deleted')
635 aidxs[i - a1], '-', trim(alines[i]), 'deleted', 'diff.deleted'
636 )
592 for i in pycompat.xrange(b1, b2):
637 for i in pycompat.xrange(b1, b2):
593 writeline(bidxs[i - b1], '+', trim(blines[i]), 'inserted',
638 writeline(
594 'diff.inserted')
639 bidxs[i - b1], '+', trim(blines[i]), 'inserted', 'diff.inserted'
640 )
641
595
642
596 class fixupstate(object):
643 class fixupstate(object):
597 """state needed to run absorb
644 """state needed to run absorb
@@ -619,13 +666,13 b' class fixupstate(object):'
619 self.repo = stack[-1].repo().unfiltered()
666 self.repo = stack[-1].repo().unfiltered()
620
667
621 # following fields will be filled later
668 # following fields will be filled later
622 self.paths = [] # [str]
669 self.paths = [] # [str]
623 self.status = None # ctx.status output
670 self.status = None # ctx.status output
624 self.fctxmap = {} # {path: {ctx: fctx}}
671 self.fctxmap = {} # {path: {ctx: fctx}}
625 self.fixupmap = {} # {path: filefixupstate}
672 self.fixupmap = {} # {path: filefixupstate}
626 self.replacemap = {} # {oldnode: newnode or None}
673 self.replacemap = {} # {oldnode: newnode or None}
627 self.finalnode = None # head after all fixups
674 self.finalnode = None # head after all fixups
628 self.ctxaffected = set() # ctx that will be absorbed into
675 self.ctxaffected = set() # ctx that will be absorbed into
629
676
630 def diffwith(self, targetctx, match=None, fm=None):
677 def diffwith(self, targetctx, match=None, fm=None):
631 """diff and prepare fixups. update self.fixupmap, self.paths"""
678 """diff and prepare fixups. update self.fixupmap, self.paths"""
@@ -648,9 +695,11 b' class fixupstate(object):'
648 targetfctx = targetctx[path]
695 targetfctx = targetctx[path]
649 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
696 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
650 # ignore symbolic links or binary, or unchanged files
697 # ignore symbolic links or binary, or unchanged files
651 if any(f.islink() or stringutil.binary(f.data())
698 if any(
652 for f in [targetfctx] + fctxs
699 f.islink() or stringutil.binary(f.data())
653 if not isinstance(f, emptyfilecontext)):
700 for f in [targetfctx] + fctxs
701 if not isinstance(f, emptyfilecontext)
702 ):
654 continue
703 continue
655 if targetfctx.data() == fctxs[-1].data() and not editopt:
704 if targetfctx.data() == fctxs[-1].data() and not editopt:
656 continue
705 continue
@@ -677,8 +726,10 b' class fixupstate(object):'
677 @property
726 @property
678 def chunkstats(self):
727 def chunkstats(self):
679 """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
728 """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
680 return dict((path, state.chunkstats)
729 return dict(
681 for path, state in self.fixupmap.iteritems())
730 (path, state.chunkstats)
731 for path, state in self.fixupmap.iteritems()
732 )
682
733
683 def commit(self):
734 def commit(self):
684 """commit changes. update self.finalnode, self.replacemap"""
735 """commit changes. update self.finalnode, self.replacemap"""
@@ -698,8 +749,10 b' class fixupstate(object):'
698 # chunkstats for each file
749 # chunkstats for each file
699 for path, stat in chunkstats.iteritems():
750 for path, stat in chunkstats.iteritems():
700 if stat[0]:
751 if stat[0]:
701 ui.write(_('%s: %d of %d chunk(s) applied\n')
752 ui.write(
702 % (path, stat[0], stat[1]))
753 _('%s: %d of %d chunk(s) applied\n')
754 % (path, stat[0], stat[1])
755 )
703 elif not ui.quiet:
756 elif not ui.quiet:
704 # a summary for all files
757 # a summary for all files
705 stats = chunkstats.values()
758 stats = chunkstats.values()
@@ -733,7 +786,9 b' class fixupstate(object):'
733 self.replacemap[ctx.node()] = lastcommitted.node()
786 self.replacemap[ctx.node()] = lastcommitted.node()
734 if memworkingcopy:
787 if memworkingcopy:
735 msg = _('%d file(s) changed, became %s') % (
788 msg = _('%d file(s) changed, became %s') % (
736 len(memworkingcopy), self._ctx2str(lastcommitted))
789 len(memworkingcopy),
790 self._ctx2str(lastcommitted),
791 )
737 else:
792 else:
738 msg = _('became %s') % self._ctx2str(lastcommitted)
793 msg = _('became %s') % self._ctx2str(lastcommitted)
739 if self.ui.verbose and msg:
794 if self.ui.verbose and msg:
@@ -754,7 +809,7 b' class fixupstate(object):'
754 """
809 """
755 result = {}
810 result = {}
756 for path in self.paths:
811 for path in self.paths:
757 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
812 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
758 if ctx not in ctx2fctx:
813 if ctx not in ctx2fctx:
759 continue
814 continue
760 fctx = ctx2fctx[ctx]
815 fctx = ctx2fctx[ctx]
@@ -766,16 +821,19 b' class fixupstate(object):'
766
821
767 def _movebookmarks(self, tr):
822 def _movebookmarks(self, tr):
768 repo = self.repo
823 repo = self.repo
769 needupdate = [(name, self.replacemap[hsh])
824 needupdate = [
770 for name, hsh in repo._bookmarks.iteritems()
825 (name, self.replacemap[hsh])
771 if hsh in self.replacemap]
826 for name, hsh in repo._bookmarks.iteritems()
827 if hsh in self.replacemap
828 ]
772 changes = []
829 changes = []
773 for name, hsh in needupdate:
830 for name, hsh in needupdate:
774 if hsh:
831 if hsh:
775 changes.append((name, hsh))
832 changes.append((name, hsh))
776 if self.ui.verbose:
833 if self.ui.verbose:
777 self.ui.write(_('moving bookmark %s to %s\n')
834 self.ui.write(
778 % (name, node.hex(hsh)))
835 _('moving bookmark %s to %s\n') % (name, node.hex(hsh))
836 )
779 else:
837 else:
780 changes.append((name, None))
838 changes.append((name, None))
781 if self.ui.verbose:
839 if self.ui.verbose:
@@ -798,8 +856,10 b' class fixupstate(object):'
798 restore = noop
856 restore = noop
799 if util.safehasattr(dirstate, '_fsmonitorstate'):
857 if util.safehasattr(dirstate, '_fsmonitorstate'):
800 bak = dirstate._fsmonitorstate.invalidate
858 bak = dirstate._fsmonitorstate.invalidate
859
801 def restore():
860 def restore():
802 dirstate._fsmonitorstate.invalidate = bak
861 dirstate._fsmonitorstate.invalidate = bak
862
803 dirstate._fsmonitorstate.invalidate = noop
863 dirstate._fsmonitorstate.invalidate = noop
804 try:
864 try:
805 with dirstate.parentchange():
865 with dirstate.parentchange():
@@ -852,11 +912,15 b' class fixupstate(object):'
852 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
912 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
853
913
854 def _cleanupoldcommits(self):
914 def _cleanupoldcommits(self):
855 replacements = {k: ([v] if v is not None else [])
915 replacements = {
856 for k, v in self.replacemap.iteritems()}
916 k: ([v] if v is not None else [])
917 for k, v in self.replacemap.iteritems()
918 }
857 if replacements:
919 if replacements:
858 scmutil.cleanupnodes(self.repo, replacements, operation='absorb',
920 scmutil.cleanupnodes(
859 fixphase=True)
921 self.repo, replacements, operation='absorb', fixphase=True
922 )
923
860
924
861 def _parsechunk(hunk):
925 def _parsechunk(hunk):
862 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
926 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
@@ -874,6 +938,7 b' def _parsechunk(hunk):'
874 blines = [l[1:] for l in patchlines[1:] if not l.startswith('-')]
938 blines = [l[1:] for l in patchlines[1:] if not l.startswith('-')]
875 return path, (a1, a2, blines)
939 return path, (a1, a2, blines)
876
940
941
877 def overlaydiffcontext(ctx, chunks):
942 def overlaydiffcontext(ctx, chunks):
878 """(ctx, [crecord.uihunk]) -> memctx
943 """(ctx, [crecord.uihunk]) -> memctx
879
944
@@ -889,8 +954,8 b' def overlaydiffcontext(ctx, chunks):'
889 # as we only care about applying changes to modified files, no mode
954 # as we only care about applying changes to modified files, no mode
890 # change, no binary diff, and no renames, it's probably okay to
955 # change, no binary diff, and no renames, it's probably okay to
891 # re-invent the logic using much simpler code here.
956 # re-invent the logic using much simpler code here.
892 memworkingcopy = {} # {path: content}
957 memworkingcopy = {} # {path: content}
893 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
958 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
894 for path, info in map(_parsechunk, chunks):
959 for path, info in map(_parsechunk, chunks):
895 if not path or not info:
960 if not path or not info:
896 continue
961 continue
@@ -905,6 +970,7 b' def overlaydiffcontext(ctx, chunks):'
905 memworkingcopy[path] = ''.join(lines)
970 memworkingcopy[path] = ''.join(lines)
906 return overlaycontext(memworkingcopy, ctx)
971 return overlaycontext(memworkingcopy, ctx)
907
972
973
908 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
974 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
909 """pick fixup chunks from targetctx, apply them to stack.
975 """pick fixup chunks from targetctx, apply them to stack.
910
976
@@ -919,12 +985,13 b' def absorb(ui, repo, stack=None, targetc'
919 raise error.Abort(_('cannot absorb into a merge'))
985 raise error.Abort(_('cannot absorb into a merge'))
920 stack = getdraftstack(headctx, limit)
986 stack = getdraftstack(headctx, limit)
921 if limit and len(stack) >= limit:
987 if limit and len(stack) >= limit:
922 ui.warn(_('absorb: only the recent %d changesets will '
988 ui.warn(
923 'be analysed\n')
989 _('absorb: only the recent %d changesets will ' 'be analysed\n')
924 % limit)
990 % limit
991 )
925 if not stack:
992 if not stack:
926 raise error.Abort(_('no mutable changeset to change'))
993 raise error.Abort(_('no mutable changeset to change'))
927 if targetctx is None: # default to working copy
994 if targetctx is None: # default to working copy
928 targetctx = repo[None]
995 targetctx = repo[None]
929 if pats is None:
996 if pats is None:
930 pats = ()
997 pats = ()
@@ -953,13 +1020,19 b' def absorb(ui, repo, stack=None, targetc'
953 fm.data(linetype='changeset')
1020 fm.data(linetype='changeset')
954 fm.write('node', '%-7.7s ', ctx.hex(), label='absorb.node')
1021 fm.write('node', '%-7.7s ', ctx.hex(), label='absorb.node')
955 descfirstline = ctx.description().splitlines()[0]
1022 descfirstline = ctx.description().splitlines()[0]
956 fm.write('descfirstline', '%s\n', descfirstline,
1023 fm.write(
957 label='absorb.description')
1024 'descfirstline',
1025 '%s\n',
1026 descfirstline,
1027 label='absorb.description',
1028 )
958 fm.end()
1029 fm.end()
959 if not opts.get('dry_run'):
1030 if not opts.get('dry_run'):
960 if (not opts.get('apply_changes') and
1031 if (
961 state.ctxaffected and
1032 not opts.get('apply_changes')
962 ui.promptchoice("apply changes (yn)? $$ &Yes $$ &No", default=1)):
1033 and state.ctxaffected
1034 and ui.promptchoice("apply changes (yn)? $$ &Yes $$ &No", default=1)
1035 ):
963 raise error.Abort(_('absorb cancelled\n'))
1036 raise error.Abort(_('absorb cancelled\n'))
964
1037
965 state.apply()
1038 state.apply()
@@ -969,20 +1042,45 b' def absorb(ui, repo, stack=None, targetc'
969 ui.write(_('nothing applied\n'))
1042 ui.write(_('nothing applied\n'))
970 return state
1043 return state
971
1044
972 @command('absorb',
1045
973 [('a', 'apply-changes', None,
1046 @command(
974 _('apply changes without prompting for confirmation')),
1047 'absorb',
975 ('p', 'print-changes', None,
1048 [
976 _('always print which changesets are modified by which changes')),
1049 (
977 ('i', 'interactive', None,
1050 'a',
978 _('interactively select which chunks to apply (EXPERIMENTAL)')),
1051 'apply-changes',
979 ('e', 'edit-lines', None,
1052 None,
980 _('edit what lines belong to which changesets before commit '
1053 _('apply changes without prompting for confirmation'),
981 '(EXPERIMENTAL)')),
1054 ),
982 ] + commands.dryrunopts + commands.templateopts + commands.walkopts,
1055 (
983 _('hg absorb [OPTION] [FILE]...'),
1056 'p',
984 helpcategory=command.CATEGORY_COMMITTING,
1057 'print-changes',
985 helpbasic=True)
1058 None,
1059 _('always print which changesets are modified by which changes'),
1060 ),
1061 (
1062 'i',
1063 'interactive',
1064 None,
1065 _('interactively select which chunks to apply (EXPERIMENTAL)'),
1066 ),
1067 (
1068 'e',
1069 'edit-lines',
1070 None,
1071 _(
1072 'edit what lines belong to which changesets before commit '
1073 '(EXPERIMENTAL)'
1074 ),
1075 ),
1076 ]
1077 + commands.dryrunopts
1078 + commands.templateopts
1079 + commands.walkopts,
1080 _('hg absorb [OPTION] [FILE]...'),
1081 helpcategory=command.CATEGORY_COMMITTING,
1082 helpbasic=True,
1083 )
986 def absorbcmd(ui, repo, *pats, **opts):
1084 def absorbcmd(ui, repo, *pats, **opts):
987 """incorporate corrections into the stack of draft changesets
1085 """incorporate corrections into the stack of draft changesets
988
1086
@@ -224,9 +224,7 b' from mercurial import ('
224 registrar,
224 registrar,
225 util,
225 util,
226 )
226 )
227 from mercurial.utils import (
227 from mercurial.utils import procutil
228 procutil,
229 )
230
228
231 urlreq = util.urlreq
229 urlreq = util.urlreq
232
230
@@ -240,33 +238,29 b' configtable = {}'
240 configitem = registrar.configitem(configtable)
238 configitem = registrar.configitem(configtable)
241
239
242 # deprecated config: acl.config
240 # deprecated config: acl.config
243 configitem('acl', 'config',
241 configitem(
244 default=None,
242 'acl', 'config', default=None,
245 )
243 )
246 configitem('acl.groups', '.*',
244 configitem(
247 default=None,
245 'acl.groups', '.*', default=None, generic=True,
248 generic=True,
249 )
246 )
250 configitem('acl.deny.branches', '.*',
247 configitem(
251 default=None,
248 'acl.deny.branches', '.*', default=None, generic=True,
252 generic=True,
253 )
249 )
254 configitem('acl.allow.branches', '.*',
250 configitem(
255 default=None,
251 'acl.allow.branches', '.*', default=None, generic=True,
256 generic=True,
257 )
252 )
258 configitem('acl.deny', '.*',
253 configitem(
259 default=None,
254 'acl.deny', '.*', default=None, generic=True,
260 generic=True,
261 )
255 )
262 configitem('acl.allow', '.*',
256 configitem(
263 default=None,
257 'acl.allow', '.*', default=None, generic=True,
264 generic=True,
265 )
258 )
266 configitem('acl', 'sources',
259 configitem(
267 default=lambda: ['serve'],
260 'acl', 'sources', default=lambda: ['serve'],
268 )
261 )
269
262
263
270 def _getusers(ui, group):
264 def _getusers(ui, group):
271
265
272 # First, try to use group definition from section [acl.groups]
266 # First, try to use group definition from section [acl.groups]
@@ -281,6 +275,7 b' def _getusers(ui, group):'
281 except KeyError:
275 except KeyError:
282 raise error.Abort(_("group '%s' is undefined") % group)
276 raise error.Abort(_("group '%s' is undefined") % group)
283
277
278
284 def _usermatch(ui, user, usersorgroups):
279 def _usermatch(ui, user, usersorgroups):
285
280
286 if usersorgroups == '*':
281 if usersorgroups == '*':
@@ -293,29 +288,35 b' def _usermatch(ui, user, usersorgroups):'
293 # if ug is a user name: !username
288 # if ug is a user name: !username
294 # if ug is a group name: !@groupname
289 # if ug is a group name: !@groupname
295 ug = ug[1:]
290 ug = ug[1:]
296 if (not ug.startswith('@') and user != ug
291 if (
297 or ug.startswith('@') and user not in _getusers(ui, ug[1:])):
292 not ug.startswith('@')
293 and user != ug
294 or ug.startswith('@')
295 and user not in _getusers(ui, ug[1:])
296 ):
298 return True
297 return True
299
298
300 # Test for user or group. Format:
299 # Test for user or group. Format:
301 # if ug is a user name: username
300 # if ug is a user name: username
302 # if ug is a group name: @groupname
301 # if ug is a group name: @groupname
303 elif (user == ug
302 elif user == ug or ug.startswith('@') and user in _getusers(ui, ug[1:]):
304 or ug.startswith('@') and user in _getusers(ui, ug[1:])):
305 return True
303 return True
306
304
307 return False
305 return False
308
306
307
309 def buildmatch(ui, repo, user, key):
308 def buildmatch(ui, repo, user, key):
310 '''return tuple of (match function, list enabled).'''
309 '''return tuple of (match function, list enabled).'''
311 if not ui.has_section(key):
310 if not ui.has_section(key):
312 ui.debug('acl: %s not enabled\n' % key)
311 ui.debug('acl: %s not enabled\n' % key)
313 return None
312 return None
314
313
315 pats = [pat for pat, users in ui.configitems(key)
314 pats = [
316 if _usermatch(ui, user, users)]
315 pat for pat, users in ui.configitems(key) if _usermatch(ui, user, users)
317 ui.debug('acl: %s enabled, %d entries for user %s\n' %
316 ]
318 (key, len(pats), user))
317 ui.debug(
318 'acl: %s enabled, %d entries for user %s\n' % (key, len(pats), user)
319 )
319
320
320 # Branch-based ACL
321 # Branch-based ACL
321 if not repo:
322 if not repo:
@@ -332,6 +333,7 b' def buildmatch(ui, repo, user, key):'
332 return match.match(repo.root, '', pats)
333 return match.match(repo.root, '', pats)
333 return util.never
334 return util.never
334
335
336
335 def ensureenabled(ui):
337 def ensureenabled(ui):
336 """make sure the extension is enabled when used as hook
338 """make sure the extension is enabled when used as hook
337
339
@@ -345,16 +347,22 b' def ensureenabled(ui):'
345 ui.setconfig('extensions', 'acl', '', source='internal')
347 ui.setconfig('extensions', 'acl', '', source='internal')
346 extensions.loadall(ui, ['acl'])
348 extensions.loadall(ui, ['acl'])
347
349
350
348 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
351 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
349
352
350 ensureenabled(ui)
353 ensureenabled(ui)
351
354
352 if hooktype not in ['pretxnchangegroup', 'pretxncommit', 'prepushkey']:
355 if hooktype not in ['pretxnchangegroup', 'pretxncommit', 'prepushkey']:
353 raise error.Abort(
356 raise error.Abort(
354 _('config error - hook type "%s" cannot stop '
357 _(
355 'incoming changesets, commits, nor bookmarks') % hooktype)
358 'config error - hook type "%s" cannot stop '
356 if (hooktype == 'pretxnchangegroup' and
359 'incoming changesets, commits, nor bookmarks'
357 source not in ui.configlist('acl', 'sources')):
360 )
361 % hooktype
362 )
363 if hooktype == 'pretxnchangegroup' and source not in ui.configlist(
364 'acl', 'sources'
365 ):
358 ui.debug('acl: changes have source "%s" - skipping\n' % source)
366 ui.debug('acl: changes have source "%s" - skipping\n' % source)
359 return
367 return
360
368
@@ -374,6 +382,7 b' def hook(ui, repo, hooktype, node=None, '
374 else:
382 else:
375 _txnhook(ui, repo, hooktype, node, source, user, **kwargs)
383 _txnhook(ui, repo, hooktype, node, source, user, **kwargs)
376
384
385
377 def _pkhook(ui, repo, hooktype, node, source, user, **kwargs):
386 def _pkhook(ui, repo, hooktype, node, source, user, **kwargs):
378 if kwargs[r'namespace'] == 'bookmarks':
387 if kwargs[r'namespace'] == 'bookmarks':
379 bookmark = kwargs[r'key']
388 bookmark = kwargs[r'key']
@@ -382,22 +391,38 b' def _pkhook(ui, repo, hooktype, node, so'
382 denybookmarks = buildmatch(ui, None, user, 'acl.deny.bookmarks')
391 denybookmarks = buildmatch(ui, None, user, 'acl.deny.bookmarks')
383
392
384 if denybookmarks and denybookmarks(bookmark):
393 if denybookmarks and denybookmarks(bookmark):
385 raise error.Abort(_('acl: user "%s" denied on bookmark "%s"'
394 raise error.Abort(
386 ' (changeset "%s")')
395 _('acl: user "%s" denied on bookmark "%s"' ' (changeset "%s")')
387 % (user, bookmark, ctx))
396 % (user, bookmark, ctx)
397 )
388 if allowbookmarks and not allowbookmarks(bookmark):
398 if allowbookmarks and not allowbookmarks(bookmark):
389 raise error.Abort(_('acl: user "%s" not allowed on bookmark "%s"'
399 raise error.Abort(
390 ' (changeset "%s")')
400 _(
391 % (user, bookmark, ctx))
401 'acl: user "%s" not allowed on bookmark "%s"'
392 ui.debug('acl: bookmark access granted: "%s" on bookmark "%s"\n'
402 ' (changeset "%s")'
393 % (ctx, bookmark))
403 )
404 % (user, bookmark, ctx)
405 )
406 ui.debug(
407 'acl: bookmark access granted: "%s" on bookmark "%s"\n'
408 % (ctx, bookmark)
409 )
410
394
411
395 def _txnhook(ui, repo, hooktype, node, source, user, **kwargs):
412 def _txnhook(ui, repo, hooktype, node, source, user, **kwargs):
396 # deprecated config: acl.config
413 # deprecated config: acl.config
397 cfg = ui.config('acl', 'config')
414 cfg = ui.config('acl', 'config')
398 if cfg:
415 if cfg:
399 ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
416 ui.readconfig(
400 'acl.deny.branches', 'acl.allow', 'acl.deny'])
417 cfg,
418 sections=[
419 'acl.groups',
420 'acl.allow.branches',
421 'acl.deny.branches',
422 'acl.allow',
423 'acl.deny',
424 ],
425 )
401
426
402 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
427 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
403 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
428 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
@@ -408,21 +433,31 b' def _txnhook(ui, repo, hooktype, node, s'
408 ctx = repo[rev]
433 ctx = repo[rev]
409 branch = ctx.branch()
434 branch = ctx.branch()
410 if denybranches and denybranches(branch):
435 if denybranches and denybranches(branch):
411 raise error.Abort(_('acl: user "%s" denied on branch "%s"'
436 raise error.Abort(
412 ' (changeset "%s")')
437 _('acl: user "%s" denied on branch "%s"' ' (changeset "%s")')
413 % (user, branch, ctx))
438 % (user, branch, ctx)
439 )
414 if allowbranches and not allowbranches(branch):
440 if allowbranches and not allowbranches(branch):
415 raise error.Abort(_('acl: user "%s" not allowed on branch "%s"'
441 raise error.Abort(
416 ' (changeset "%s")')
442 _(
417 % (user, branch, ctx))
443 'acl: user "%s" not allowed on branch "%s"'
418 ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
444 ' (changeset "%s")'
419 % (ctx, branch))
445 )
446 % (user, branch, ctx)
447 )
448 ui.debug(
449 'acl: branch access granted: "%s" on branch "%s"\n' % (ctx, branch)
450 )
420
451
421 for f in ctx.files():
452 for f in ctx.files():
422 if deny and deny(f):
453 if deny and deny(f):
423 raise error.Abort(_('acl: user "%s" denied on "%s"'
454 raise error.Abort(
424 ' (changeset "%s")') % (user, f, ctx))
455 _('acl: user "%s" denied on "%s"' ' (changeset "%s")')
456 % (user, f, ctx)
457 )
425 if allow and not allow(f):
458 if allow and not allow(f):
426 raise error.Abort(_('acl: user "%s" not allowed on "%s"'
459 raise error.Abort(
427 ' (changeset "%s")') % (user, f, ctx))
460 _('acl: user "%s" not allowed on "%s"' ' (changeset "%s")')
461 % (user, f, ctx)
462 )
428 ui.debug('acl: path access granted: "%s"\n' % ctx)
463 ui.debug('acl: path access granted: "%s"\n' % ctx)
@@ -29,20 +29,35 b" testedwith = 'ships-with-hg-core'"
29 cmdtable = {}
29 cmdtable = {}
30 command = registrar.command(cmdtable)
30 command = registrar.command(cmdtable)
31
31
32 @command('amend',
32
33 [('A', 'addremove', None,
33 @command(
34 _('mark new/missing files as added/removed before committing')),
34 'amend',
35 ('e', 'edit', None, _('invoke editor on commit messages')),
35 [
36 ('i', 'interactive', None, _('use interactive mode')),
36 (
37 (b'', b'close-branch', None,
37 'A',
38 _(b'mark a branch as closed, hiding it from the branch list')),
38 'addremove',
39 (b's', b'secret', None, _(b'use the secret phase for committing')),
39 None,
40 ('n', 'note', '', _('store a note on the amend')),
40 _('mark new/missing files as added/removed before committing'),
41 ] + cmdutil.walkopts + cmdutil.commitopts + cmdutil.commitopts2
41 ),
42 + cmdutil.commitopts3,
42 ('e', 'edit', None, _('invoke editor on commit messages')),
43 ('i', 'interactive', None, _('use interactive mode')),
44 (
45 b'',
46 b'close-branch',
47 None,
48 _(b'mark a branch as closed, hiding it from the branch list'),
49 ),
50 (b's', b'secret', None, _(b'use the secret phase for committing')),
51 ('n', 'note', '', _('store a note on the amend')),
52 ]
53 + cmdutil.walkopts
54 + cmdutil.commitopts
55 + cmdutil.commitopts2
56 + cmdutil.commitopts3,
43 _('[OPTION]... [FILE]...'),
57 _('[OPTION]... [FILE]...'),
44 helpcategory=command.CATEGORY_COMMITTING,
58 helpcategory=command.CATEGORY_COMMITTING,
45 inferrepo=True)
59 inferrepo=True,
60 )
46 def amend(ui, repo, *pats, **opts):
61 def amend(ui, repo, *pats, **opts):
47 """amend the working copy parent with all or specified outstanding changes
62 """amend the working copy parent with all or specified outstanding changes
48
63
@@ -35,22 +35,23 b' from mercurial import ('
35 pycompat,
35 pycompat,
36 registrar,
36 registrar,
37 scmutil,
37 scmutil,
38 similar
38 similar,
39 )
39 )
40
40
41 configtable = {}
41 configtable = {}
42 configitem = registrar.configitem(configtable)
42 configitem = registrar.configitem(configtable)
43
43
44 configitem('automv', 'similarity',
44 configitem(
45 default=95,
45 'automv', 'similarity', default=95,
46 )
46 )
47
47
48
48 def extsetup(ui):
49 def extsetup(ui):
49 entry = extensions.wrapcommand(
50 entry = extensions.wrapcommand(commands.table, 'commit', mvcheck)
50 commands.table, 'commit', mvcheck)
51 entry[1].append(
51 entry[1].append(
52 ('', 'no-automv', None,
52 ('', 'no-automv', None, _('disable automatic file move detection'))
53 _('disable automatic file move detection')))
53 )
54
54
55
55 def mvcheck(orig, ui, repo, *pats, **opts):
56 def mvcheck(orig, ui, repo, *pats, **opts):
56 """Hook to check for moves at commit time"""
57 """Hook to check for moves at commit time"""
@@ -65,14 +66,16 b' def mvcheck(orig, ui, repo, *pats, **opt'
65 match = scmutil.match(repo[None], pats, opts)
66 match = scmutil.match(repo[None], pats, opts)
66 added, removed = _interestingfiles(repo, match)
67 added, removed = _interestingfiles(repo, match)
67 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
68 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
68 renames = _findrenames(repo, uipathfn, added, removed,
69 renames = _findrenames(
69 threshold / 100.0)
70 repo, uipathfn, added, removed, threshold / 100.0
71 )
70
72
71 with repo.wlock():
73 with repo.wlock():
72 if renames is not None:
74 if renames is not None:
73 scmutil._markchanges(repo, (), (), renames)
75 scmutil._markchanges(repo, (), (), renames)
74 return orig(ui, repo, *pats, **pycompat.strkwargs(opts))
76 return orig(ui, repo, *pats, **pycompat.strkwargs(opts))
75
77
78
76 def _interestingfiles(repo, matcher):
79 def _interestingfiles(repo, matcher):
77 """Find what files were added or removed in this commit.
80 """Find what files were added or removed in this commit.
78
81
@@ -90,6 +93,7 b' def _interestingfiles(repo, matcher):'
90
93
91 return added, removed
94 return added, removed
92
95
96
93 def _findrenames(repo, uipathfn, added, removed, similarity):
97 def _findrenames(repo, uipathfn, added, removed, similarity):
94 """Find what files in added are really moved files.
98 """Find what files in added are really moved files.
95
99
@@ -100,11 +104,13 b' def _findrenames(repo, uipathfn, added, '
100 renames = {}
104 renames = {}
101 if similarity > 0:
105 if similarity > 0:
102 for src, dst, score in similar.findrenames(
106 for src, dst, score in similar.findrenames(
103 repo, added, removed, similarity):
107 repo, added, removed, similarity
108 ):
104 if repo.ui.verbose:
109 if repo.ui.verbose:
105 repo.ui.status(
110 repo.ui.status(
106 _('detected move of %s as %s (%d%% similar)\n') % (
111 _('detected move of %s as %s (%d%% similar)\n')
107 uipathfn(src), uipathfn(dst), score * 100))
112 % (uipathfn(src), uipathfn(dst), score * 100)
113 )
108 renames[dst] = src
114 renames[dst] = src
109 if renames:
115 if renames:
110 repo.ui.status(_('detected move of %d files\n') % len(renames))
116 repo.ui.status(_('detected move of %d files\n') % len(renames))
@@ -28,55 +28,64 b' from mercurial import ('
28 # leave the attribute unspecified.
28 # leave the attribute unspecified.
29 testedwith = 'ships-with-hg-core'
29 testedwith = 'ships-with-hg-core'
30
30
31
31 def prettyedge(before, edge, after):
32 def prettyedge(before, edge, after):
32 if edge == '~':
33 if edge == '~':
33 return '\xE2\x95\xA7' # U+2567 ╧
34 return '\xE2\x95\xA7' # U+2567 ╧
34 if edge == '/':
35 if edge == '/':
35 return '\xE2\x95\xB1' # U+2571 ╱
36 return '\xE2\x95\xB1' # U+2571 ╱
36 if edge == '-':
37 if edge == '-':
37 return '\xE2\x94\x80' # U+2500 ─
38 return '\xE2\x94\x80' # U+2500 ─
38 if edge == '|':
39 if edge == '|':
39 return '\xE2\x94\x82' # U+2502 │
40 return '\xE2\x94\x82' # U+2502 │
40 if edge == ':':
41 if edge == ':':
41 return '\xE2\x94\x86' # U+2506 ┆
42 return '\xE2\x94\x86' # U+2506 ┆
42 if edge == '\\':
43 if edge == '\\':
43 return '\xE2\x95\xB2' # U+2572 ╲
44 return '\xE2\x95\xB2' # U+2572 ╲
44 if edge == '+':
45 if edge == '+':
45 if before == ' ' and not after == ' ':
46 if before == ' ' and not after == ' ':
46 return '\xE2\x94\x9C' # U+251C ├
47 return '\xE2\x94\x9C' # U+251C ├
47 if after == ' ' and not before == ' ':
48 if after == ' ' and not before == ' ':
48 return '\xE2\x94\xA4' # U+2524 ┤
49 return '\xE2\x94\xA4' # U+2524 ┤
49 return '\xE2\x94\xBC' # U+253C ┼
50 return '\xE2\x94\xBC' # U+253C ┼
50 return edge
51 return edge
51
52
53
52 def convertedges(line):
54 def convertedges(line):
53 line = ' %s ' % line
55 line = ' %s ' % line
54 pretty = []
56 pretty = []
55 for idx in pycompat.xrange(len(line) - 2):
57 for idx in pycompat.xrange(len(line) - 2):
56 pretty.append(prettyedge(line[idx:idx + 1],
58 pretty.append(
57 line[idx + 1:idx + 2],
59 prettyedge(
58 line[idx + 2:idx + 3]))
60 line[idx : idx + 1],
61 line[idx + 1 : idx + 2],
62 line[idx + 2 : idx + 3],
63 )
64 )
59 return ''.join(pretty)
65 return ''.join(pretty)
60
66
67
61 def getprettygraphnode(orig, *args, **kwargs):
68 def getprettygraphnode(orig, *args, **kwargs):
62 node = orig(*args, **kwargs)
69 node = orig(*args, **kwargs)
63 if node == 'o':
70 if node == 'o':
64 return '\xE2\x97\x8B' # U+25CB ○
71 return '\xE2\x97\x8B' # U+25CB ○
65 if node == '@':
72 if node == '@':
66 return '\xE2\x97\x8D' # U+25CD ◍
73 return '\xE2\x97\x8D' # U+25CD ◍
67 if node == '*':
74 if node == '*':
68 return '\xE2\x88\x97' # U+2217 ∗
75 return '\xE2\x88\x97' # U+2217 ∗
69 if node == 'x':
76 if node == 'x':
70 return '\xE2\x97\x8C' # U+25CC ◌
77 return '\xE2\x97\x8C' # U+25CC ◌
71 if node == '_':
78 if node == '_':
72 return '\xE2\x95\xA4' # U+2564 ╤
79 return '\xE2\x95\xA4' # U+2564 ╤
73 return node
80 return node
74
81
82
75 def outputprettygraph(orig, ui, graph, *args, **kwargs):
83 def outputprettygraph(orig, ui, graph, *args, **kwargs):
76 (edges, text) = zip(*graph)
84 (edges, text) = zip(*graph)
77 graph = zip([convertedges(e) for e in edges], text)
85 graph = zip([convertedges(e) for e in edges], text)
78 return orig(ui, graph, *args, **kwargs)
86 return orig(ui, graph, *args, **kwargs)
79
87
88
80 def extsetup(ui):
89 def extsetup(ui):
81 if ui.plain('graph'):
90 if ui.plain('graph'):
82 return
91 return
@@ -86,8 +95,12 b' def extsetup(ui):'
86 return
95 return
87
96
88 if r'A' in encoding._wide:
97 if r'A' in encoding._wide:
89 ui.warn(_('beautifygraph: unsupported terminal settings, '
98 ui.warn(
90 'monospace narrow text required\n'))
99 _(
100 'beautifygraph: unsupported terminal settings, '
101 'monospace narrow text required\n'
102 )
103 )
91 return
104 return
92
105
93 extensions.wrapfunction(graphmod, 'outputgraph', outputprettygraph)
106 extensions.wrapfunction(graphmod, 'outputgraph', outputprettygraph)
@@ -71,30 +71,33 b' command = registrar.command(cmdtable)'
71 configtable = {}
71 configtable = {}
72 configitem = registrar.configitem(configtable)
72 configitem = registrar.configitem(configtable)
73
73
74 configitem('blackbox', 'dirty',
74 configitem(
75 default=False,
75 'blackbox', 'dirty', default=False,
76 )
76 )
77 configitem('blackbox', 'maxsize',
77 configitem(
78 default='1 MB',
78 'blackbox', 'maxsize', default='1 MB',
79 )
80 configitem(
81 'blackbox', 'logsource', default=False,
79 )
82 )
80 configitem('blackbox', 'logsource',
83 configitem(
81 default=False,
84 'blackbox', 'maxfiles', default=7,
82 )
85 )
83 configitem('blackbox', 'maxfiles',
86 configitem(
84 default=7,
87 'blackbox', 'track', default=lambda: ['*'],
85 )
88 )
86 configitem('blackbox', 'track',
89 configitem(
87 default=lambda: ['*'],
90 'blackbox',
88 )
91 'ignore',
89 configitem('blackbox', 'ignore',
90 default=lambda: ['chgserver', 'cmdserver', 'extension'],
92 default=lambda: ['chgserver', 'cmdserver', 'extension'],
91 )
93 )
92 configitem('blackbox', 'date-format',
94 configitem(
93 default='%Y/%m/%d %H:%M:%S',
95 'blackbox', 'date-format', default='%Y/%m/%d %H:%M:%S',
94 )
96 )
95
97
96 _lastlogger = loggingutil.proxylogger()
98 _lastlogger = loggingutil.proxylogger()
97
99
100
98 class blackboxlogger(object):
101 class blackboxlogger(object):
99 def __init__(self, ui, repo):
102 def __init__(self, ui, repo):
100 self._repo = repo
103 self._repo = repo
@@ -105,9 +108,9 b' class blackboxlogger(object):'
105 self._inlog = False
108 self._inlog = False
106
109
107 def tracked(self, event):
110 def tracked(self, event):
108 return ((b'*' in self._trackedevents
111 return (
109 and event not in self._ignoredevents)
112 b'*' in self._trackedevents and event not in self._ignoredevents
110 or event in self._trackedevents)
113 ) or event in self._trackedevents
111
114
112 def log(self, ui, event, msg, opts):
115 def log(self, ui, event, msg, opts):
113 # self._log() -> ctx.dirty() may create new subrepo instance, which
116 # self._log() -> ctx.dirty() may create new subrepo instance, which
@@ -129,9 +132,10 b' class blackboxlogger(object):'
129 changed = ''
132 changed = ''
130 ctx = self._repo[None]
133 ctx = self._repo[None]
131 parents = ctx.parents()
134 parents = ctx.parents()
132 rev = ('+'.join([hex(p.node()) for p in parents]))
135 rev = '+'.join([hex(p.node()) for p in parents])
133 if (ui.configbool('blackbox', 'dirty') and
136 if ui.configbool('blackbox', 'dirty') and ctx.dirty(
134 ctx.dirty(missing=True, merge=False, branch=False)):
137 missing=True, merge=False, branch=False
138 ):
135 changed = '+'
139 changed = '+'
136 if ui.configbool('blackbox', 'logsource'):
140 if ui.configbool('blackbox', 'logsource'):
137 src = ' [%s]' % event
141 src = ' [%s]' % event
@@ -141,20 +145,28 b' class blackboxlogger(object):'
141 fmt = '%s %s @%s%s (%s)%s> %s'
145 fmt = '%s %s @%s%s (%s)%s> %s'
142 args = (date, user, rev, changed, pid, src, msg)
146 args = (date, user, rev, changed, pid, src, msg)
143 with loggingutil.openlogfile(
147 with loggingutil.openlogfile(
144 ui, self._repo.vfs, name='blackbox.log',
148 ui,
145 maxfiles=self._maxfiles, maxsize=self._maxsize) as fp:
149 self._repo.vfs,
150 name='blackbox.log',
151 maxfiles=self._maxfiles,
152 maxsize=self._maxsize,
153 ) as fp:
146 fp.write(fmt % args)
154 fp.write(fmt % args)
147 except (IOError, OSError) as err:
155 except (IOError, OSError) as err:
148 # deactivate this to avoid failed logging again
156 # deactivate this to avoid failed logging again
149 self._trackedevents.clear()
157 self._trackedevents.clear()
150 ui.debug('warning: cannot write to blackbox.log: %s\n' %
158 ui.debug(
151 encoding.strtolocal(err.strerror))
159 'warning: cannot write to blackbox.log: %s\n'
160 % encoding.strtolocal(err.strerror)
161 )
152 return
162 return
153 _lastlogger.logger = self
163 _lastlogger.logger = self
154
164
165
155 def uipopulate(ui):
166 def uipopulate(ui):
156 ui.setlogger(b'blackbox', _lastlogger)
167 ui.setlogger(b'blackbox', _lastlogger)
157
168
169
158 def reposetup(ui, repo):
170 def reposetup(ui, repo):
159 # During 'hg pull' a httppeer repo is created to represent the remote repo.
171 # During 'hg pull' a httppeer repo is created to represent the remote repo.
160 # It doesn't have a .hg directory to put a blackbox in, so we don't do
172 # It doesn't have a .hg directory to put a blackbox in, so we don't do
@@ -174,12 +186,14 b' def reposetup(ui, repo):'
174
186
175 repo._wlockfreeprefix.add('blackbox.log')
187 repo._wlockfreeprefix.add('blackbox.log')
176
188
177 @command('blackbox',
189
178 [('l', 'limit', 10, _('the number of events to show')),
190 @command(
179 ],
191 'blackbox',
192 [('l', 'limit', 10, _('the number of events to show')),],
180 _('hg blackbox [OPTION]...'),
193 _('hg blackbox [OPTION]...'),
181 helpcategory=command.CATEGORY_MAINTENANCE,
194 helpcategory=command.CATEGORY_MAINTENANCE,
182 helpbasic=True)
195 helpbasic=True,
196 )
183 def blackbox(ui, repo, *revs, **opts):
197 def blackbox(ui, repo, *revs, **opts):
184 '''view the recent repository events
198 '''view the recent repository events
185 '''
199 '''
@@ -36,20 +36,26 b" configitem(MY_NAME, 'enable-branches', F"
36 cmdtable = {}
36 cmdtable = {}
37 command = registrar.command(cmdtable)
37 command = registrar.command(cmdtable)
38
38
39
39 def commit_hook(ui, repo, **kwargs):
40 def commit_hook(ui, repo, **kwargs):
40 active = repo._bookmarks.active
41 active = repo._bookmarks.active
41 if active:
42 if active:
42 if active in ui.configlist(MY_NAME, 'protect'):
43 if active in ui.configlist(MY_NAME, 'protect'):
43 raise error.Abort(
44 raise error.Abort(
44 _('cannot commit, bookmark %s is protected') % active)
45 _('cannot commit, bookmark %s is protected') % active
46 )
45 if not cwd_at_bookmark(repo, active):
47 if not cwd_at_bookmark(repo, active):
46 raise error.Abort(
48 raise error.Abort(
47 _('cannot commit, working directory out of sync with active bookmark'),
49 _(
48 hint=_("run 'hg up %s'") % active)
50 'cannot commit, working directory out of sync with active bookmark'
51 ),
52 hint=_("run 'hg up %s'") % active,
53 )
49 elif ui.configbool(MY_NAME, 'require-bookmark', True):
54 elif ui.configbool(MY_NAME, 'require-bookmark', True):
50 raise error.Abort(_('cannot commit without an active bookmark'))
55 raise error.Abort(_('cannot commit without an active bookmark'))
51 return 0
56 return 0
52
57
58
53 def bookmarks_update(orig, repo, parents, node):
59 def bookmarks_update(orig, repo, parents, node):
54 if len(parents) == 2:
60 if len(parents) == 2:
55 # called during commit
61 # called during commit
@@ -58,43 +64,59 b' def bookmarks_update(orig, repo, parents'
58 # called during update
64 # called during update
59 return False
65 return False
60
66
67
61 def bookmarks_addbookmarks(
68 def bookmarks_addbookmarks(
62 orig, repo, tr, names, rev=None, force=False, inactive=False):
69 orig, repo, tr, names, rev=None, force=False, inactive=False
70 ):
63 if not rev:
71 if not rev:
64 marks = repo._bookmarks
72 marks = repo._bookmarks
65 for name in names:
73 for name in names:
66 if name in marks:
74 if name in marks:
67 raise error.Abort(_(
75 raise error.Abort(
68 "bookmark %s already exists, to move use the --rev option"
76 _(
69 ) % name)
77 "bookmark %s already exists, to move use the --rev option"
78 )
79 % name
80 )
70 return orig(repo, tr, names, rev, force, inactive)
81 return orig(repo, tr, names, rev, force, inactive)
71
82
83
72 def commands_commit(orig, ui, repo, *args, **opts):
84 def commands_commit(orig, ui, repo, *args, **opts):
73 commit_hook(ui, repo)
85 commit_hook(ui, repo)
74 return orig(ui, repo, *args, **opts)
86 return orig(ui, repo, *args, **opts)
75
87
88
76 def commands_pull(orig, ui, repo, *args, **opts):
89 def commands_pull(orig, ui, repo, *args, **opts):
77 rc = orig(ui, repo, *args, **opts)
90 rc = orig(ui, repo, *args, **opts)
78 active = repo._bookmarks.active
91 active = repo._bookmarks.active
79 if active and not cwd_at_bookmark(repo, active):
92 if active and not cwd_at_bookmark(repo, active):
80 ui.warn(_(
93 ui.warn(
81 "working directory out of sync with active bookmark, run "
94 _(
82 "'hg up %s'"
95 "working directory out of sync with active bookmark, run "
83 ) % active)
96 "'hg up %s'"
97 )
98 % active
99 )
84 return rc
100 return rc
85
101
102
86 def commands_branch(orig, ui, repo, label=None, **opts):
103 def commands_branch(orig, ui, repo, label=None, **opts):
87 if label and not opts.get(r'clean') and not opts.get(r'rev'):
104 if label and not opts.get(r'clean') and not opts.get(r'rev'):
88 raise error.Abort(
105 raise error.Abort(
89 _("creating named branches is disabled and you should use bookmarks"),
106 _(
90 hint="see 'hg help bookflow'")
107 "creating named branches is disabled and you should use bookmarks"
108 ),
109 hint="see 'hg help bookflow'",
110 )
91 return orig(ui, repo, label, **opts)
111 return orig(ui, repo, label, **opts)
92
112
113
93 def cwd_at_bookmark(repo, mark):
114 def cwd_at_bookmark(repo, mark):
94 mark_id = repo._bookmarks[mark]
115 mark_id = repo._bookmarks[mark]
95 cur_id = repo.lookup('.')
116 cur_id = repo.lookup('.')
96 return cur_id == mark_id
117 return cur_id == mark_id
97
118
119
98 def uisetup(ui):
120 def uisetup(ui):
99 extensions.wrapfunction(bookmarks, 'update', bookmarks_update)
121 extensions.wrapfunction(bookmarks, 'update', bookmarks_update)
100 extensions.wrapfunction(bookmarks, 'addbookmarks', bookmarks_addbookmarks)
122 extensions.wrapfunction(bookmarks, 'addbookmarks', bookmarks_addbookmarks)
@@ -324,72 +324,81 b" testedwith = 'ships-with-hg-core'"
324 configtable = {}
324 configtable = {}
325 configitem = registrar.configitem(configtable)
325 configitem = registrar.configitem(configtable)
326
326
327 configitem('bugzilla', 'apikey',
327 configitem(
328 default='',
328 'bugzilla', 'apikey', default='',
329 )
329 )
330 configitem('bugzilla', 'bzdir',
330 configitem(
331 default='/var/www/html/bugzilla',
331 'bugzilla', 'bzdir', default='/var/www/html/bugzilla',
332 )
333 configitem(
334 'bugzilla', 'bzemail', default=None,
332 )
335 )
333 configitem('bugzilla', 'bzemail',
336 configitem(
334 default=None,
337 'bugzilla', 'bzurl', default='http://localhost/bugzilla/',
335 )
338 )
336 configitem('bugzilla', 'bzurl',
339 configitem(
337 default='http://localhost/bugzilla/',
340 'bugzilla', 'bzuser', default=None,
338 )
341 )
339 configitem('bugzilla', 'bzuser',
342 configitem(
340 default=None,
343 'bugzilla', 'db', default='bugs',
341 )
344 )
342 configitem('bugzilla', 'db',
345 configitem(
343 default='bugs',
346 'bugzilla',
344 )
347 'fixregexp',
345 configitem('bugzilla', 'fixregexp',
348 default=(
346 default=(br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
349 br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
347 br'(?:nos?\.?|num(?:ber)?s?)?\s*'
350 br'(?:nos?\.?|num(?:ber)?s?)?\s*'
348 br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
351 br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
349 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
352 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
353 ),
350 )
354 )
351 configitem('bugzilla', 'fixresolution',
355 configitem(
352 default='FIXED',
356 'bugzilla', 'fixresolution', default='FIXED',
353 )
357 )
354 configitem('bugzilla', 'fixstatus',
358 configitem(
355 default='RESOLVED',
359 'bugzilla', 'fixstatus', default='RESOLVED',
356 )
360 )
357 configitem('bugzilla', 'host',
361 configitem(
358 default='localhost',
362 'bugzilla', 'host', default='localhost',
359 )
363 )
360 configitem('bugzilla', 'notify',
364 configitem(
361 default=configitem.dynamicdefault,
365 'bugzilla', 'notify', default=configitem.dynamicdefault,
362 )
366 )
363 configitem('bugzilla', 'password',
367 configitem(
364 default=None,
368 'bugzilla', 'password', default=None,
365 )
369 )
366 configitem('bugzilla', 'regexp',
370 configitem(
367 default=(br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
371 'bugzilla',
368 br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
372 'regexp',
369 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
373 default=(
374 br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
375 br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
376 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
377 ),
370 )
378 )
371 configitem('bugzilla', 'strip',
379 configitem(
372 default=0,
380 'bugzilla', 'strip', default=0,
373 )
381 )
374 configitem('bugzilla', 'style',
382 configitem(
375 default=None,
383 'bugzilla', 'style', default=None,
376 )
384 )
377 configitem('bugzilla', 'template',
385 configitem(
378 default=None,
386 'bugzilla', 'template', default=None,
379 )
387 )
380 configitem('bugzilla', 'timeout',
388 configitem(
381 default=5,
389 'bugzilla', 'timeout', default=5,
382 )
390 )
383 configitem('bugzilla', 'user',
391 configitem(
384 default='bugs',
392 'bugzilla', 'user', default='bugs',
385 )
393 )
386 configitem('bugzilla', 'usermap',
394 configitem(
387 default=None,
395 'bugzilla', 'usermap', default=None,
388 )
396 )
389 configitem('bugzilla', 'version',
397 configitem(
390 default=None,
398 'bugzilla', 'version', default=None,
391 )
399 )
392
400
401
393 class bzaccess(object):
402 class bzaccess(object):
394 '''Base class for access to Bugzilla.'''
403 '''Base class for access to Bugzilla.'''
395
404
@@ -434,6 +443,7 b' class bzaccess(object):'
434 emails automatically.
443 emails automatically.
435 '''
444 '''
436
445
446
437 # Bugzilla via direct access to MySQL database.
447 # Bugzilla via direct access to MySQL database.
438 class bzmysql(bzaccess):
448 class bzmysql(bzaccess):
439 '''Support for direct MySQL access to Bugzilla.
449 '''Support for direct MySQL access to Bugzilla.
@@ -454,6 +464,7 b' class bzmysql(bzaccess):'
454 def __init__(self, ui):
464 def __init__(self, ui):
455 try:
465 try:
456 import MySQLdb as mysql
466 import MySQLdb as mysql
467
457 bzmysql._MySQLdb = mysql
468 bzmysql._MySQLdb = mysql
458 except ImportError as err:
469 except ImportError as err:
459 raise error.Abort(_('python mysql support not available: %s') % err)
470 raise error.Abort(_('python mysql support not available: %s') % err)
@@ -465,12 +476,13 b' class bzmysql(bzaccess):'
465 passwd = self.ui.config('bugzilla', 'password')
476 passwd = self.ui.config('bugzilla', 'password')
466 db = self.ui.config('bugzilla', 'db')
477 db = self.ui.config('bugzilla', 'db')
467 timeout = int(self.ui.config('bugzilla', 'timeout'))
478 timeout = int(self.ui.config('bugzilla', 'timeout'))
468 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
479 self.ui.note(
469 (host, db, user, '*' * len(passwd)))
480 _('connecting to %s:%s as %s, password %s\n')
470 self.conn = bzmysql._MySQLdb.connect(host=host,
481 % (host, db, user, '*' * len(passwd))
471 user=user, passwd=passwd,
482 )
472 db=db,
483 self.conn = bzmysql._MySQLdb.connect(
473 connect_timeout=timeout)
484 host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout
485 )
474 self.cursor = self.conn.cursor()
486 self.cursor = self.conn.cursor()
475 self.longdesc_id = self.get_longdesc_id()
487 self.longdesc_id = self.get_longdesc_id()
476 self.user_ids = {}
488 self.user_ids = {}
@@ -495,8 +507,10 b' class bzmysql(bzaccess):'
495
507
496 def filter_real_bug_ids(self, bugs):
508 def filter_real_bug_ids(self, bugs):
497 '''filter not-existing bugs from set.'''
509 '''filter not-existing bugs from set.'''
498 self.run('select bug_id from bugs where bug_id in %s' %
510 self.run(
499 bzmysql.sql_buglist(bugs.keys()))
511 'select bug_id from bugs where bug_id in %s'
512 % bzmysql.sql_buglist(bugs.keys())
513 )
500 existing = [id for (id,) in self.cursor.fetchall()]
514 existing = [id for (id,) in self.cursor.fetchall()]
501 for id in bugs.keys():
515 for id in bugs.keys():
502 if id not in existing:
516 if id not in existing:
@@ -505,12 +519,16 b' class bzmysql(bzaccess):'
505
519
506 def filter_cset_known_bug_ids(self, node, bugs):
520 def filter_cset_known_bug_ids(self, node, bugs):
507 '''filter bug ids that already refer to this changeset from set.'''
521 '''filter bug ids that already refer to this changeset from set.'''
508 self.run('''select bug_id from longdescs where
522 self.run(
509 bug_id in %s and thetext like "%%%s%%"''' %
523 '''select bug_id from longdescs where
510 (bzmysql.sql_buglist(bugs.keys()), short(node)))
524 bug_id in %s and thetext like "%%%s%%"'''
525 % (bzmysql.sql_buglist(bugs.keys()), short(node))
526 )
511 for (id,) in self.cursor.fetchall():
527 for (id,) in self.cursor.fetchall():
512 self.ui.status(_('bug %d already knows about changeset %s\n') %
528 self.ui.status(
513 (id, short(node)))
529 _('bug %d already knows about changeset %s\n')
530 % (id, short(node))
531 )
514 del bugs[id]
532 del bugs[id]
515
533
516 def notify(self, bugs, committer):
534 def notify(self, bugs, committer):
@@ -534,8 +552,9 b' class bzmysql(bzaccess):'
534 ret = fp.close()
552 ret = fp.close()
535 if ret:
553 if ret:
536 self.ui.warn(out)
554 self.ui.warn(out)
537 raise error.Abort(_('bugzilla notify command %s') %
555 raise error.Abort(
538 procutil.explainexit(ret))
556 _('bugzilla notify command %s') % procutil.explainexit(ret)
557 )
539 self.ui.status(_('done\n'))
558 self.ui.status(_('done\n'))
540
559
541 def get_user_id(self, user):
560 def get_user_id(self, user):
@@ -547,8 +566,11 b' class bzmysql(bzaccess):'
547 userid = int(user)
566 userid = int(user)
548 except ValueError:
567 except ValueError:
549 self.ui.note(_('looking up user %s\n') % user)
568 self.ui.note(_('looking up user %s\n') % user)
550 self.run('''select userid from profiles
569 self.run(
551 where login_name like %s''', user)
570 '''select userid from profiles
571 where login_name like %s''',
572 user,
573 )
552 all = self.cursor.fetchall()
574 all = self.cursor.fetchall()
553 if len(all) != 1:
575 if len(all) != 1:
554 raise KeyError(user)
576 raise KeyError(user)
@@ -567,13 +589,16 b' class bzmysql(bzaccess):'
567 try:
589 try:
568 defaultuser = self.ui.config('bugzilla', 'bzuser')
590 defaultuser = self.ui.config('bugzilla', 'bzuser')
569 if not defaultuser:
591 if not defaultuser:
570 raise error.Abort(_('cannot find bugzilla user id for %s') %
592 raise error.Abort(
571 user)
593 _('cannot find bugzilla user id for %s') % user
594 )
572 userid = self.get_user_id(defaultuser)
595 userid = self.get_user_id(defaultuser)
573 user = defaultuser
596 user = defaultuser
574 except KeyError:
597 except KeyError:
575 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
598 raise error.Abort(
576 % (user, defaultuser))
599 _('cannot find bugzilla user id for %s or %s')
600 % (user, defaultuser)
601 )
577 return (user, userid)
602 return (user, userid)
578
603
579 def updatebug(self, bugid, newstate, text, committer):
604 def updatebug(self, bugid, newstate, text, committer):
@@ -586,22 +611,29 b' class bzmysql(bzaccess):'
586
611
587 (user, userid) = self.get_bugzilla_user(committer)
612 (user, userid) = self.get_bugzilla_user(committer)
588 now = time.strftime(r'%Y-%m-%d %H:%M:%S')
613 now = time.strftime(r'%Y-%m-%d %H:%M:%S')
589 self.run('''insert into longdescs
614 self.run(
615 '''insert into longdescs
590 (bug_id, who, bug_when, thetext)
616 (bug_id, who, bug_when, thetext)
591 values (%s, %s, %s, %s)''',
617 values (%s, %s, %s, %s)''',
592 (bugid, userid, now, text))
618 (bugid, userid, now, text),
593 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
619 )
620 self.run(
621 '''insert into bugs_activity (bug_id, who, bug_when, fieldid)
594 values (%s, %s, %s, %s)''',
622 values (%s, %s, %s, %s)''',
595 (bugid, userid, now, self.longdesc_id))
623 (bugid, userid, now, self.longdesc_id),
624 )
596 self.conn.commit()
625 self.conn.commit()
597
626
627
598 class bzmysql_2_18(bzmysql):
628 class bzmysql_2_18(bzmysql):
599 '''support for bugzilla 2.18 series.'''
629 '''support for bugzilla 2.18 series.'''
600
630
601 def __init__(self, ui):
631 def __init__(self, ui):
602 bzmysql.__init__(self, ui)
632 bzmysql.__init__(self, ui)
603 self.default_notify = (
633 self.default_notify = (
604 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s")
634 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
635 )
636
605
637
606 class bzmysql_3_0(bzmysql_2_18):
638 class bzmysql_3_0(bzmysql_2_18):
607 '''support for bugzilla 3.0 series.'''
639 '''support for bugzilla 3.0 series.'''
@@ -617,8 +649,10 b' class bzmysql_3_0(bzmysql_2_18):'
617 raise error.Abort(_('unknown database schema'))
649 raise error.Abort(_('unknown database schema'))
618 return ids[0][0]
650 return ids[0][0]
619
651
652
620 # Bugzilla via XMLRPC interface.
653 # Bugzilla via XMLRPC interface.
621
654
655
622 class cookietransportrequest(object):
656 class cookietransportrequest(object):
623 """A Transport request method that retains cookies over its lifetime.
657 """A Transport request method that retains cookies over its lifetime.
624
658
@@ -636,6 +670,7 b' class cookietransportrequest(object):'
636 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
670 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
637
671
638 cookies = []
672 cookies = []
673
639 def send_cookies(self, connection):
674 def send_cookies(self, connection):
640 if self.cookies:
675 if self.cookies:
641 for cookie in self.cookies:
676 for cookie in self.cookies:
@@ -673,8 +708,12 b' class cookietransportrequest(object):'
673 self.cookies.append(cookie)
708 self.cookies.append(cookie)
674
709
675 if response.status != 200:
710 if response.status != 200:
676 raise xmlrpclib.ProtocolError(host + handler, response.status,
711 raise xmlrpclib.ProtocolError(
677 response.reason, response.msg.headers)
712 host + handler,
713 response.status,
714 response.reason,
715 response.msg.headers,
716 )
678
717
679 payload = response.read()
718 payload = response.read()
680 parser, unmarshaller = self.getparser()
719 parser, unmarshaller = self.getparser()
@@ -683,6 +722,7 b' class cookietransportrequest(object):'
683
722
684 return unmarshaller.close()
723 return unmarshaller.close()
685
724
725
686 # The explicit calls to the underlying xmlrpclib __init__() methods are
726 # The explicit calls to the underlying xmlrpclib __init__() methods are
687 # necessary. The xmlrpclib.Transport classes are old-style classes, and
727 # necessary. The xmlrpclib.Transport classes are old-style classes, and
688 # it turns out their __init__() doesn't get called when doing multiple
728 # it turns out their __init__() doesn't get called when doing multiple
@@ -692,11 +732,13 b' class cookietransport(cookietransportreq'
692 if util.safehasattr(xmlrpclib.Transport, "__init__"):
732 if util.safehasattr(xmlrpclib.Transport, "__init__"):
693 xmlrpclib.Transport.__init__(self, use_datetime)
733 xmlrpclib.Transport.__init__(self, use_datetime)
694
734
735
695 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
736 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
696 def __init__(self, use_datetime=0):
737 def __init__(self, use_datetime=0):
697 if util.safehasattr(xmlrpclib.Transport, "__init__"):
738 if util.safehasattr(xmlrpclib.Transport, "__init__"):
698 xmlrpclib.SafeTransport.__init__(self, use_datetime)
739 xmlrpclib.SafeTransport.__init__(self, use_datetime)
699
740
741
700 class bzxmlrpc(bzaccess):
742 class bzxmlrpc(bzaccess):
701 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
743 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
702
744
@@ -719,8 +761,9 b' class bzxmlrpc(bzaccess):'
719 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
761 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
720 self.bzvermajor = int(ver[0])
762 self.bzvermajor = int(ver[0])
721 self.bzverminor = int(ver[1])
763 self.bzverminor = int(ver[1])
722 login = self.bzproxy.User.login({'login': user, 'password': passwd,
764 login = self.bzproxy.User.login(
723 'restrict_login': True})
765 {'login': user, 'password': passwd, 'restrict_login': True}
766 )
724 self.bztoken = login.get('token', '')
767 self.bztoken = login.get('token', '')
725
768
726 def transport(self, uri):
769 def transport(self, uri):
@@ -731,17 +774,20 b' class bzxmlrpc(bzaccess):'
731
774
732 def get_bug_comments(self, id):
775 def get_bug_comments(self, id):
733 """Return a string with all comment text for a bug."""
776 """Return a string with all comment text for a bug."""
734 c = self.bzproxy.Bug.comments({'ids': [id],
777 c = self.bzproxy.Bug.comments(
735 'include_fields': ['text'],
778 {'ids': [id], 'include_fields': ['text'], 'token': self.bztoken}
736 'token': self.bztoken})
779 )
737 return ''.join([t['text'] for t in c['bugs']['%d' % id]['comments']])
780 return ''.join([t['text'] for t in c['bugs']['%d' % id]['comments']])
738
781
739 def filter_real_bug_ids(self, bugs):
782 def filter_real_bug_ids(self, bugs):
740 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
783 probe = self.bzproxy.Bug.get(
741 'include_fields': [],
784 {
742 'permissive': True,
785 'ids': sorted(bugs.keys()),
743 'token': self.bztoken,
786 'include_fields': [],
744 })
787 'permissive': True,
788 'token': self.bztoken,
789 }
790 )
745 for badbug in probe['faults']:
791 for badbug in probe['faults']:
746 id = badbug['id']
792 id = badbug['id']
747 self.ui.status(_('bug %d does not exist\n') % id)
793 self.ui.status(_('bug %d does not exist\n') % id)
@@ -750,8 +796,10 b' class bzxmlrpc(bzaccess):'
750 def filter_cset_known_bug_ids(self, node, bugs):
796 def filter_cset_known_bug_ids(self, node, bugs):
751 for id in sorted(bugs.keys()):
797 for id in sorted(bugs.keys()):
752 if self.get_bug_comments(id).find(short(node)) != -1:
798 if self.get_bug_comments(id).find(short(node)) != -1:
753 self.ui.status(_('bug %d already knows about changeset %s\n') %
799 self.ui.status(
754 (id, short(node)))
800 _('bug %d already knows about changeset %s\n')
801 % (id, short(node))
802 )
755 del bugs[id]
803 del bugs[id]
756
804
757 def updatebug(self, bugid, newstate, text, committer):
805 def updatebug(self, bugid, newstate, text, committer):
@@ -761,7 +809,7 b' class bzxmlrpc(bzaccess):'
761
809
762 if self.bzvermajor >= 4:
810 if self.bzvermajor >= 4:
763 args['ids'] = [bugid]
811 args['ids'] = [bugid]
764 args['comment'] = {'body' : text}
812 args['comment'] = {'body': text}
765 if 'fix' in newstate:
813 if 'fix' in newstate:
766 args['status'] = self.fixstatus
814 args['status'] = self.fixstatus
767 args['resolution'] = self.fixresolution
815 args['resolution'] = self.fixresolution
@@ -769,12 +817,17 b' class bzxmlrpc(bzaccess):'
769 self.bzproxy.Bug.update(args)
817 self.bzproxy.Bug.update(args)
770 else:
818 else:
771 if 'fix' in newstate:
819 if 'fix' in newstate:
772 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
820 self.ui.warn(
773 "to mark bugs fixed\n"))
821 _(
822 "Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
823 "to mark bugs fixed\n"
824 )
825 )
774 args['id'] = bugid
826 args['id'] = bugid
775 args['comment'] = text
827 args['comment'] = text
776 self.bzproxy.Bug.add_comment(args)
828 self.bzproxy.Bug.add_comment(args)
777
829
830
778 class bzxmlrpcemail(bzxmlrpc):
831 class bzxmlrpcemail(bzxmlrpc):
779 """Read data from Bugzilla via XMLRPC, send updates via email.
832 """Read data from Bugzilla via XMLRPC, send updates via email.
780
833
@@ -823,15 +876,18 b' class bzxmlrpcemail(bzxmlrpc):'
823 than the subject line, and leave a blank line after it.
876 than the subject line, and leave a blank line after it.
824 '''
877 '''
825 user = self.map_committer(committer)
878 user = self.map_committer(committer)
826 matches = self.bzproxy.User.get({'match': [user],
879 matches = self.bzproxy.User.get(
827 'token': self.bztoken})
880 {'match': [user], 'token': self.bztoken}
881 )
828 if not matches['users']:
882 if not matches['users']:
829 user = self.ui.config('bugzilla', 'user')
883 user = self.ui.config('bugzilla', 'user')
830 matches = self.bzproxy.User.get({'match': [user],
884 matches = self.bzproxy.User.get(
831 'token': self.bztoken})
885 {'match': [user], 'token': self.bztoken}
886 )
832 if not matches['users']:
887 if not matches['users']:
833 raise error.Abort(_("default bugzilla user %s email not found")
888 raise error.Abort(
834 % user)
889 _("default bugzilla user %s email not found") % user
890 )
835 user = matches['users'][0]['email']
891 user = matches['users'][0]['email']
836 commands.append(self.makecommandline("id", bugid))
892 commands.append(self.makecommandline("id", bugid))
837
893
@@ -856,13 +912,16 b' class bzxmlrpcemail(bzxmlrpc):'
856 cmds.append(self.makecommandline("resolution", self.fixresolution))
912 cmds.append(self.makecommandline("resolution", self.fixresolution))
857 self.send_bug_modify_email(bugid, cmds, text, committer)
913 self.send_bug_modify_email(bugid, cmds, text, committer)
858
914
915
859 class NotFound(LookupError):
916 class NotFound(LookupError):
860 pass
917 pass
861
918
919
862 class bzrestapi(bzaccess):
920 class bzrestapi(bzaccess):
863 """Read and write bugzilla data using the REST API available since
921 """Read and write bugzilla data using the REST API available since
864 Bugzilla 5.0.
922 Bugzilla 5.0.
865 """
923 """
924
866 def __init__(self, ui):
925 def __init__(self, ui):
867 bzaccess.__init__(self, ui)
926 bzaccess.__init__(self, ui)
868 bz = self.ui.config('bugzilla', 'bzurl')
927 bz = self.ui.config('bugzilla', 'bzurl')
@@ -902,14 +961,15 b' class bzrestapi(bzaccess):'
902 def _submit(self, burl, data, method='POST'):
961 def _submit(self, burl, data, method='POST'):
903 data = json.dumps(data)
962 data = json.dumps(data)
904 if method == 'PUT':
963 if method == 'PUT':
964
905 class putrequest(util.urlreq.request):
965 class putrequest(util.urlreq.request):
906 def get_method(self):
966 def get_method(self):
907 return 'PUT'
967 return 'PUT'
968
908 request_type = putrequest
969 request_type = putrequest
909 else:
970 else:
910 request_type = util.urlreq.request
971 request_type = util.urlreq.request
911 req = request_type(burl, data,
972 req = request_type(burl, data, {'Content-Type': 'application/json'})
912 {'Content-Type': 'application/json'})
913 try:
973 try:
914 resp = url.opener(self.ui).open(req)
974 resp = url.opener(self.ui).open(req)
915 return json.loads(resp.read())
975 return json.loads(resp.read())
@@ -941,8 +1001,9 b' class bzrestapi(bzaccess):'
941 result = self._fetch(burl)
1001 result = self._fetch(burl)
942 comments = result['bugs'][pycompat.bytestr(bugid)]['comments']
1002 comments = result['bugs'][pycompat.bytestr(bugid)]['comments']
943 if any(sn in c['text'] for c in comments):
1003 if any(sn in c['text'] for c in comments):
944 self.ui.status(_('bug %d already knows about changeset %s\n') %
1004 self.ui.status(
945 (bugid, sn))
1005 _('bug %d already knows about changeset %s\n') % (bugid, sn)
1006 )
946 del bugs[bugid]
1007 del bugs[bugid]
947
1008
948 def updatebug(self, bugid, newstate, text, committer):
1009 def updatebug(self, bugid, newstate, text, committer):
@@ -969,11 +1030,10 b' class bzrestapi(bzaccess):'
969 self.ui.debug('updated bug %s\n' % bugid)
1030 self.ui.debug('updated bug %s\n' % bugid)
970 else:
1031 else:
971 burl = self.apiurl(('bug', bugid, 'comment'))
1032 burl = self.apiurl(('bug', bugid, 'comment'))
972 self._submit(burl, {
1033 self._submit(
973 'comment': text,
1034 burl,
974 'is_private': False,
1035 {'comment': text, 'is_private': False, 'is_markdown': False,},
975 'is_markdown': False,
1036 )
976 })
977 self.ui.debug('added comment to bug %s\n' % bugid)
1037 self.ui.debug('added comment to bug %s\n' % bugid)
978
1038
979 def notify(self, bugs, committer):
1039 def notify(self, bugs, committer):
@@ -984,17 +1044,18 b' class bzrestapi(bzaccess):'
984 '''
1044 '''
985 pass
1045 pass
986
1046
1047
987 class bugzilla(object):
1048 class bugzilla(object):
988 # supported versions of bugzilla. different versions have
1049 # supported versions of bugzilla. different versions have
989 # different schemas.
1050 # different schemas.
990 _versions = {
1051 _versions = {
991 '2.16': bzmysql,
1052 '2.16': bzmysql,
992 '2.18': bzmysql_2_18,
1053 '2.18': bzmysql_2_18,
993 '3.0': bzmysql_3_0,
1054 '3.0': bzmysql_3_0,
994 'xmlrpc': bzxmlrpc,
1055 'xmlrpc': bzxmlrpc,
995 'xmlrpc+email': bzxmlrpcemail,
1056 'xmlrpc+email': bzxmlrpcemail,
996 'restapi': bzrestapi,
1057 'restapi': bzrestapi,
997 }
1058 }
998
1059
999 def __init__(self, ui, repo):
1060 def __init__(self, ui, repo):
1000 self.ui = ui
1061 self.ui = ui
@@ -1004,14 +1065,17 b' class bugzilla(object):'
1004 try:
1065 try:
1005 bzclass = bugzilla._versions[bzversion]
1066 bzclass = bugzilla._versions[bzversion]
1006 except KeyError:
1067 except KeyError:
1007 raise error.Abort(_('bugzilla version %s not supported') %
1068 raise error.Abort(
1008 bzversion)
1069 _('bugzilla version %s not supported') % bzversion
1070 )
1009 self.bzdriver = bzclass(self.ui)
1071 self.bzdriver = bzclass(self.ui)
1010
1072
1011 self.bug_re = re.compile(
1073 self.bug_re = re.compile(
1012 self.ui.config('bugzilla', 'regexp'), re.IGNORECASE)
1074 self.ui.config('bugzilla', 'regexp'), re.IGNORECASE
1075 )
1013 self.fix_re = re.compile(
1076 self.fix_re = re.compile(
1014 self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE)
1077 self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE
1078 )
1015 self.split_re = re.compile(br'\D+')
1079 self.split_re = re.compile(br'\D+')
1016
1080
1017 def find_bugs(self, ctx):
1081 def find_bugs(self, ctx):
@@ -1084,7 +1148,7 b' class bugzilla(object):'
1084 c = root.find('/')
1148 c = root.find('/')
1085 if c == -1:
1149 if c == -1:
1086 break
1150 break
1087 root = root[c + 1:]
1151 root = root[c + 1 :]
1088 count -= 1
1152 count -= 1
1089 return root
1153 return root
1090
1154
@@ -1093,31 +1157,39 b' class bugzilla(object):'
1093 if not tmpl:
1157 if not tmpl:
1094 mapfile = self.ui.config('bugzilla', 'style')
1158 mapfile = self.ui.config('bugzilla', 'style')
1095 if not mapfile and not tmpl:
1159 if not mapfile and not tmpl:
1096 tmpl = _('changeset {node|short} in repo {root} refers '
1160 tmpl = _(
1097 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
1161 'changeset {node|short} in repo {root} refers '
1162 'to bug {bug}.\ndetails:\n\t{desc|tabindent}'
1163 )
1098 spec = logcmdutil.templatespec(tmpl, mapfile)
1164 spec = logcmdutil.templatespec(tmpl, mapfile)
1099 t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
1165 t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
1100 self.ui.pushbuffer()
1166 self.ui.pushbuffer()
1101 t.show(ctx, changes=ctx.changeset(),
1167 t.show(
1102 bug=pycompat.bytestr(bugid),
1168 ctx,
1103 hgweb=self.ui.config('web', 'baseurl'),
1169 changes=ctx.changeset(),
1104 root=self.repo.root,
1170 bug=pycompat.bytestr(bugid),
1105 webroot=webroot(self.repo.root))
1171 hgweb=self.ui.config('web', 'baseurl'),
1172 root=self.repo.root,
1173 webroot=webroot(self.repo.root),
1174 )
1106 data = self.ui.popbuffer()
1175 data = self.ui.popbuffer()
1107 self.bzdriver.updatebug(bugid, newstate, data,
1176 self.bzdriver.updatebug(
1108 stringutil.email(ctx.user()))
1177 bugid, newstate, data, stringutil.email(ctx.user())
1178 )
1109
1179
1110 def notify(self, bugs, committer):
1180 def notify(self, bugs, committer):
1111 '''ensure Bugzilla users are notified of bug change.'''
1181 '''ensure Bugzilla users are notified of bug change.'''
1112 self.bzdriver.notify(bugs, committer)
1182 self.bzdriver.notify(bugs, committer)
1113
1183
1184
1114 def hook(ui, repo, hooktype, node=None, **kwargs):
1185 def hook(ui, repo, hooktype, node=None, **kwargs):
1115 '''add comment to bugzilla for each changeset that refers to a
1186 '''add comment to bugzilla for each changeset that refers to a
1116 bugzilla bug id. only add a comment once per bug, so same change
1187 bugzilla bug id. only add a comment once per bug, so same change
1117 seen multiple times does not fill bug with duplicate data.'''
1188 seen multiple times does not fill bug with duplicate data.'''
1118 if node is None:
1189 if node is None:
1119 raise error.Abort(_('hook type %s does not pass a changeset id') %
1190 raise error.Abort(
1120 hooktype)
1191 _('hook type %s does not pass a changeset id') % hooktype
1192 )
1121 try:
1193 try:
1122 bz = bugzilla(ui, repo)
1194 bz = bugzilla(ui, repo)
1123 ctx = repo[node]
1195 ctx = repo[node]
@@ -44,15 +44,21 b' command = registrar.command(cmdtable)'
44 # leave the attribute unspecified.
44 # leave the attribute unspecified.
45 testedwith = 'ships-with-hg-core'
45 testedwith = 'ships-with-hg-core'
46
46
47 @command('censor',
47
48 [('r', 'rev', '', _('censor file from specified revision'), _('REV')),
48 @command(
49 ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT'))],
49 'censor',
50 [
51 ('r', 'rev', '', _('censor file from specified revision'), _('REV')),
52 ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT')),
53 ],
50 _('-r REV [-t TEXT] [FILE]'),
54 _('-r REV [-t TEXT] [FILE]'),
51 helpcategory=command.CATEGORY_MAINTENANCE)
55 helpcategory=command.CATEGORY_MAINTENANCE,
56 )
52 def censor(ui, repo, path, rev='', tombstone='', **opts):
57 def censor(ui, repo, path, rev='', tombstone='', **opts):
53 with repo.wlock(), repo.lock():
58 with repo.wlock(), repo.lock():
54 return _docensor(ui, repo, path, rev, tombstone, **opts)
59 return _docensor(ui, repo, path, rev, tombstone, **opts)
55
60
61
56 def _docensor(ui, repo, path, rev='', tombstone='', **opts):
62 def _docensor(ui, repo, path, rev='', tombstone='', **opts):
57 if not path:
63 if not path:
58 raise error.Abort(_('must specify file path to censor'))
64 raise error.Abort(_('must specify file path to censor'))
@@ -88,13 +94,17 b" def _docensor(ui, repo, path, rev='', to"
88 heads.append(hc)
94 heads.append(hc)
89 if heads:
95 if heads:
90 headlist = ', '.join([short(c.node()) for c in heads])
96 headlist = ', '.join([short(c.node()) for c in heads])
91 raise error.Abort(_('cannot censor file in heads (%s)') % headlist,
97 raise error.Abort(
92 hint=_('clean/delete and commit first'))
98 _('cannot censor file in heads (%s)') % headlist,
99 hint=_('clean/delete and commit first'),
100 )
93
101
94 wp = wctx.parents()
102 wp = wctx.parents()
95 if ctx.node() in [p.node() for p in wp]:
103 if ctx.node() in [p.node() for p in wp]:
96 raise error.Abort(_('cannot censor working directory'),
104 raise error.Abort(
97 hint=_('clean/delete/update first'))
105 _('cannot censor working directory'),
106 hint=_('clean/delete/update first'),
107 )
98
108
99 with repo.transaction(b'censor') as tr:
109 with repo.transaction(b'censor') as tr:
100 flog.censorrevision(tr, fnode, tombstone=tombstone)
110 flog.censorrevision(tr, fnode, tombstone=tombstone)
@@ -35,13 +35,15 b' command = registrar.command(cmdtable)'
35 # leave the attribute unspecified.
35 # leave the attribute unspecified.
36 testedwith = 'ships-with-hg-core'
36 testedwith = 'ships-with-hg-core'
37
37
38 @command('children',
38
39 [('r', 'rev', '.',
39 @command(
40 _('show children of the specified revision'), _('REV')),
40 'children',
41 ] + templateopts,
41 [('r', 'rev', '.', _('show children of the specified revision'), _('REV')),]
42 + templateopts,
42 _('hg children [-r REV] [FILE]'),
43 _('hg children [-r REV] [FILE]'),
43 helpcategory=command.CATEGORY_CHANGE_NAVIGATION,
44 helpcategory=command.CATEGORY_CHANGE_NAVIGATION,
44 inferrepo=True)
45 inferrepo=True,
46 )
45 def children(ui, repo, file_=None, **opts):
47 def children(ui, repo, file_=None, **opts):
46 """show the children of the given or working directory revision
48 """show the children of the given or working directory revision
47
49
@@ -34,6 +34,7 b' command = registrar.command(cmdtable)'
34 # leave the attribute unspecified.
34 # leave the attribute unspecified.
35 testedwith = 'ships-with-hg-core'
35 testedwith = 'ships-with-hg-core'
36
36
37
37 def changedlines(ui, repo, ctx1, ctx2, fns):
38 def changedlines(ui, repo, ctx1, ctx2, fns):
38 added, removed = 0, 0
39 added, removed = 0, 0
39 fmatch = scmutil.matchfiles(repo, fns)
40 fmatch = scmutil.matchfiles(repo, fns)
@@ -45,38 +46,45 b' def changedlines(ui, repo, ctx1, ctx2, f'
45 removed += 1
46 removed += 1
46 return (added, removed)
47 return (added, removed)
47
48
49
48 def countrate(ui, repo, amap, *pats, **opts):
50 def countrate(ui, repo, amap, *pats, **opts):
49 """Calculate stats"""
51 """Calculate stats"""
50 opts = pycompat.byteskwargs(opts)
52 opts = pycompat.byteskwargs(opts)
51 if opts.get('dateformat'):
53 if opts.get('dateformat'):
54
52 def getkey(ctx):
55 def getkey(ctx):
53 t, tz = ctx.date()
56 t, tz = ctx.date()
54 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
57 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
55 return encoding.strtolocal(
58 return encoding.strtolocal(
56 date.strftime(encoding.strfromlocal(opts['dateformat'])))
59 date.strftime(encoding.strfromlocal(opts['dateformat']))
60 )
61
57 else:
62 else:
58 tmpl = opts.get('oldtemplate') or opts.get('template')
63 tmpl = opts.get('oldtemplate') or opts.get('template')
59 tmpl = logcmdutil.maketemplater(ui, repo, tmpl)
64 tmpl = logcmdutil.maketemplater(ui, repo, tmpl)
65
60 def getkey(ctx):
66 def getkey(ctx):
61 ui.pushbuffer()
67 ui.pushbuffer()
62 tmpl.show(ctx)
68 tmpl.show(ctx)
63 return ui.popbuffer()
69 return ui.popbuffer()
64
70
65 progress = ui.makeprogress(_('analyzing'), unit=_('revisions'),
71 progress = ui.makeprogress(
66 total=len(repo))
72 _('analyzing'), unit=_('revisions'), total=len(repo)
73 )
67 rate = {}
74 rate = {}
68 df = False
75 df = False
69 if opts.get('date'):
76 if opts.get('date'):
70 df = dateutil.matchdate(opts['date'])
77 df = dateutil.matchdate(opts['date'])
71
78
72 m = scmutil.match(repo[None], pats, opts)
79 m = scmutil.match(repo[None], pats, opts)
80
73 def prep(ctx, fns):
81 def prep(ctx, fns):
74 rev = ctx.rev()
82 rev = ctx.rev()
75 if df and not df(ctx.date()[0]): # doesn't match date format
83 if df and not df(ctx.date()[0]): # doesn't match date format
76 return
84 return
77
85
78 key = getkey(ctx).strip()
86 key = getkey(ctx).strip()
79 key = amap.get(key, key) # alias remap
87 key = amap.get(key, key) # alias remap
80 if opts.get('changesets'):
88 if opts.get('changesets'):
81 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
89 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
82 else:
90 else:
@@ -99,25 +107,54 b' def countrate(ui, repo, amap, *pats, **o'
99 return rate
107 return rate
100
108
101
109
102 @command('churn',
110 @command(
103 [('r', 'rev', [],
111 'churn',
104 _('count rate for the specified revision or revset'), _('REV')),
112 [
105 ('d', 'date', '',
113 (
106 _('count rate for revisions matching date spec'), _('DATE')),
114 'r',
107 ('t', 'oldtemplate', '',
115 'rev',
108 _('template to group changesets (DEPRECATED)'), _('TEMPLATE')),
116 [],
109 ('T', 'template', '{author|email}',
117 _('count rate for the specified revision or revset'),
110 _('template to group changesets'), _('TEMPLATE')),
118 _('REV'),
111 ('f', 'dateformat', '',
119 ),
112 _('strftime-compatible format for grouping by date'), _('FORMAT')),
120 (
113 ('c', 'changesets', False, _('count rate by number of changesets')),
121 'd',
114 ('s', 'sort', False, _('sort by key (default: sort by count)')),
122 'date',
115 ('', 'diffstat', False, _('display added/removed lines separately')),
123 '',
116 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
124 _('count rate for revisions matching date spec'),
117 ] + cmdutil.walkopts,
125 _('DATE'),
126 ),
127 (
128 't',
129 'oldtemplate',
130 '',
131 _('template to group changesets (DEPRECATED)'),
132 _('TEMPLATE'),
133 ),
134 (
135 'T',
136 'template',
137 '{author|email}',
138 _('template to group changesets'),
139 _('TEMPLATE'),
140 ),
141 (
142 'f',
143 'dateformat',
144 '',
145 _('strftime-compatible format for grouping by date'),
146 _('FORMAT'),
147 ),
148 ('c', 'changesets', False, _('count rate by number of changesets')),
149 ('s', 'sort', False, _('sort by key (default: sort by count)')),
150 ('', 'diffstat', False, _('display added/removed lines separately')),
151 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
152 ]
153 + cmdutil.walkopts,
118 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
154 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
119 helpcategory=command.CATEGORY_MAINTENANCE,
155 helpcategory=command.CATEGORY_MAINTENANCE,
120 inferrepo=True)
156 inferrepo=True,
157 )
121 def churn(ui, repo, *pats, **opts):
158 def churn(ui, repo, *pats, **opts):
122 '''histogram of changes to the repository
159 '''histogram of changes to the repository
123
160
@@ -154,6 +191,7 b' def churn(ui, repo, *pats, **opts):'
154 a .hgchurn file will be looked for in the working directory root.
191 a .hgchurn file will be looked for in the working directory root.
155 Aliases will be split from the rightmost "=".
192 Aliases will be split from the rightmost "=".
156 '''
193 '''
194
157 def pad(s, l):
195 def pad(s, l):
158 return s + " " * (l - encoding.colwidth(s))
196 return s + " " * (l - encoding.colwidth(s))
159
197
@@ -191,19 +229,25 b' def churn(ui, repo, *pats, **opts):'
191
229
192 if opts.get(r'diffstat'):
230 if opts.get(r'diffstat'):
193 width -= 15
231 width -= 15
232
194 def format(name, diffstat):
233 def format(name, diffstat):
195 added, removed = diffstat
234 added, removed = diffstat
196 return "%s %15s %s%s\n" % (pad(name, maxname),
235 return "%s %15s %s%s\n" % (
197 '+%d/-%d' % (added, removed),
236 pad(name, maxname),
198 ui.label('+' * charnum(added),
237 '+%d/-%d' % (added, removed),
199 'diffstat.inserted'),
238 ui.label('+' * charnum(added), 'diffstat.inserted'),
200 ui.label('-' * charnum(removed),
239 ui.label('-' * charnum(removed), 'diffstat.deleted'),
201 'diffstat.deleted'))
240 )
241
202 else:
242 else:
203 width -= 6
243 width -= 6
244
204 def format(name, count):
245 def format(name, count):
205 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
246 return "%s %6d %s\n" % (
206 '*' * charnum(sum(count)))
247 pad(name, maxname),
248 sum(count),
249 '*' * charnum(sum(count)),
250 )
207
251
208 def charnum(count):
252 def charnum(count):
209 return int(count * width // maxcount)
253 return int(count * width // maxcount)
@@ -203,6 +203,7 b' from mercurial import ('
203
203
204 testedwith = 'ships-with-hg-core'
204 testedwith = 'ships-with-hg-core'
205
205
206
206 def capabilities(orig, repo, proto):
207 def capabilities(orig, repo, proto):
207 caps = orig(repo, proto)
208 caps = orig(repo, proto)
208
209
@@ -214,5 +215,6 b' def capabilities(orig, repo, proto):'
214
215
215 return caps
216 return caps
216
217
218
217 def extsetup(ui):
219 def extsetup(ui):
218 extensions.wrapfunction(wireprotov1server, '_capabilities', capabilities)
220 extensions.wrapfunction(wireprotov1server, '_capabilities', capabilities)
@@ -28,13 +28,16 b" testedwith = 'ships-with-hg-core'"
28
28
29 commitopts = cmdutil.commitopts
29 commitopts = cmdutil.commitopts
30 commitopts2 = cmdutil.commitopts2
30 commitopts2 = cmdutil.commitopts2
31 commitopts3 = [('r', 'rev', [],
31 commitopts3 = [('r', 'rev', [], _('revision to check'), _('REV'))]
32 _('revision to check'), _('REV'))]
32
33
33
34 @command('close-head|close-heads', commitopts + commitopts2 + commitopts3,
34 @command(
35 'close-head|close-heads',
36 commitopts + commitopts2 + commitopts3,
35 _('[OPTION]... [REV]...'),
37 _('[OPTION]... [REV]...'),
36 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
38 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
37 inferrepo=True)
39 inferrepo=True,
40 )
38 def close_branch(ui, repo, *revs, **opts):
41 def close_branch(ui, repo, *revs, **opts):
39 """close the given head revisions
42 """close the given head revisions
40
43
@@ -44,10 +47,18 b' def close_branch(ui, repo, *revs, **opts'
44
47
45 The commit message must be specified with -l or -m.
48 The commit message must be specified with -l or -m.
46 """
49 """
50
47 def docommit(rev):
51 def docommit(rev):
48 cctx = context.memctx(repo, parents=[rev, None], text=message,
52 cctx = context.memctx(
49 files=[], filectxfn=None, user=opts.get('user'),
53 repo,
50 date=opts.get('date'), extra=extra)
54 parents=[rev, None],
55 text=message,
56 files=[],
57 filectxfn=None,
58 user=opts.get('user'),
59 date=opts.get('date'),
60 extra=extra,
61 )
51 tr = repo.transaction('commit')
62 tr = repo.transaction('commit')
52 ret = repo.commitctx(cctx, True)
63 ret = repo.commitctx(cctx, True)
53 bookmarks.update(repo, [rev, None], ret)
64 bookmarks.update(repo, [rev, None], ret)
@@ -73,7 +84,7 b' def close_branch(ui, repo, *revs, **opts'
73 message = cmdutil.logmessage(ui, opts)
84 message = cmdutil.logmessage(ui, opts)
74 if not message:
85 if not message:
75 raise error.Abort(_("no commit message specified with -l or -m"))
86 raise error.Abort(_("no commit message specified with -l or -m"))
76 extra = { 'close': '1' }
87 extra = {'close': '1'}
77
88
78 with repo.wlock(), repo.lock():
89 with repo.wlock(), repo.lock():
79 for rev in revs:
90 for rev in revs:
@@ -37,36 +37,46 b' usedinternally = {'
37 'transplant_source',
37 'transplant_source',
38 }
38 }
39
39
40
40 def extsetup(ui):
41 def extsetup(ui):
41 entry = extensions.wrapcommand(commands.table, 'commit', _commit)
42 entry = extensions.wrapcommand(commands.table, 'commit', _commit)
42 options = entry[1]
43 options = entry[1]
43 options.append(('', 'extra', [],
44 options.append(
44 _('set a changeset\'s extra values'), _("KEY=VALUE")))
45 ('', 'extra', [], _('set a changeset\'s extra values'), _("KEY=VALUE"))
46 )
47
45
48
46 def _commit(orig, ui, repo, *pats, **opts):
49 def _commit(orig, ui, repo, *pats, **opts):
47 if util.safehasattr(repo, 'unfiltered'):
50 if util.safehasattr(repo, 'unfiltered'):
48 repo = repo.unfiltered()
51 repo = repo.unfiltered()
52
49 class repoextra(repo.__class__):
53 class repoextra(repo.__class__):
50 def commit(self, *innerpats, **inneropts):
54 def commit(self, *innerpats, **inneropts):
51 extras = opts.get(r'extra')
55 extras = opts.get(r'extra')
52 for raw in extras:
56 for raw in extras:
53 if '=' not in raw:
57 if '=' not in raw:
54 msg = _("unable to parse '%s', should follow "
58 msg = _(
55 "KEY=VALUE format")
59 "unable to parse '%s', should follow "
60 "KEY=VALUE format"
61 )
56 raise error.Abort(msg % raw)
62 raise error.Abort(msg % raw)
57 k, v = raw.split('=', 1)
63 k, v = raw.split('=', 1)
58 if not k:
64 if not k:
59 msg = _("unable to parse '%s', keys can't be empty")
65 msg = _("unable to parse '%s', keys can't be empty")
60 raise error.Abort(msg % raw)
66 raise error.Abort(msg % raw)
61 if re.search(br'[^\w-]', k):
67 if re.search(br'[^\w-]', k):
62 msg = _("keys can only contain ascii letters, digits,"
68 msg = _(
63 " '_' and '-'")
69 "keys can only contain ascii letters, digits,"
70 " '_' and '-'"
71 )
64 raise error.Abort(msg)
72 raise error.Abort(msg)
65 if k in usedinternally:
73 if k in usedinternally:
66 msg = _("key '%s' is used internally, can't be set "
74 msg = _(
67 "manually")
75 "key '%s' is used internally, can't be set " "manually"
76 )
68 raise error.Abort(msg % k)
77 raise error.Abort(msg % k)
69 inneropts[r'extra'][k] = v
78 inneropts[r'extra'][k] = v
70 return super(repoextra, self).commit(*innerpats, **inneropts)
79 return super(repoextra, self).commit(*innerpats, **inneropts)
80
71 repo.__class__ = repoextra
81 repo.__class__ = repoextra
72 return orig(ui, repo, *pats, **opts)
82 return orig(ui, repo, *pats, **opts)
@@ -10,9 +10,7 b''
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import registrar
14 registrar,
15 )
16
14
17 from . import (
15 from . import (
18 convcmd,
16 convcmd,
@@ -30,28 +28,58 b" testedwith = 'ships-with-hg-core'"
30
28
31 # Commands definition was moved elsewhere to ease demandload job.
29 # Commands definition was moved elsewhere to ease demandload job.
32
30
33 @command('convert',
31
34 [('', 'authors', '',
32 @command(
35 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
33 'convert',
36 _('FILE')),
34 [
37 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
35 (
38 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
36 '',
39 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
37 'authors',
40 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
38 '',
41 ('', 'filemap', '', _('remap file names using contents of file'),
39 _(
42 _('FILE')),
40 'username mapping filename (DEPRECATED) (use --authormap instead)'
43 ('', 'full', None,
41 ),
44 _('apply filemap changes by converting all files again')),
42 _('FILE'),
45 ('', 'splicemap', '', _('splice synthesized history into place'),
43 ),
46 _('FILE')),
44 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
47 ('', 'branchmap', '', _('change branch names while converting'),
45 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
48 _('FILE')),
46 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
49 ('', 'branchsort', None, _('try to sort changesets by branches')),
47 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
50 ('', 'datesort', None, _('try to sort changesets by date')),
48 (
51 ('', 'sourcesort', None, _('preserve source changesets order')),
49 '',
52 ('', 'closesort', None, _('try to reorder closed revisions'))],
50 'filemap',
53 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
51 '',
54 norepo=True)
52 _('remap file names using contents of file'),
53 _('FILE'),
54 ),
55 (
56 '',
57 'full',
58 None,
59 _('apply filemap changes by converting all files again'),
60 ),
61 (
62 '',
63 'splicemap',
64 '',
65 _('splice synthesized history into place'),
66 _('FILE'),
67 ),
68 (
69 '',
70 'branchmap',
71 '',
72 _('change branch names while converting'),
73 _('FILE'),
74 ),
75 ('', 'branchsort', None, _('try to sort changesets by branches')),
76 ('', 'datesort', None, _('try to sort changesets by date')),
77 ('', 'sourcesort', None, _('preserve source changesets order')),
78 ('', 'closesort', None, _('try to reorder closed revisions')),
79 ],
80 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
81 norepo=True,
82 )
55 def convert(ui, src, dest=None, revmapfile=None, **opts):
83 def convert(ui, src, dest=None, revmapfile=None, **opts):
56 """convert a foreign SCM repository to a Mercurial one.
84 """convert a foreign SCM repository to a Mercurial one.
57
85
@@ -454,29 +482,37 b' def convert(ui, src, dest=None, revmapfi'
454 """
482 """
455 return convcmd.convert(ui, src, dest, revmapfile, **opts)
483 return convcmd.convert(ui, src, dest, revmapfile, **opts)
456
484
485
457 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
486 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
458 def debugsvnlog(ui, **opts):
487 def debugsvnlog(ui, **opts):
459 return subversion.debugsvnlog(ui, **opts)
488 return subversion.debugsvnlog(ui, **opts)
460
489
461 @command('debugcvsps',
490
491 @command(
492 'debugcvsps',
462 [
493 [
463 # Main options shared with cvsps-2.1
494 # Main options shared with cvsps-2.1
464 ('b', 'branches', [], _('only return changes on specified branches')),
495 ('b', 'branches', [], _('only return changes on specified branches')),
465 ('p', 'prefix', '', _('prefix to remove from file names')),
496 ('p', 'prefix', '', _('prefix to remove from file names')),
466 ('r', 'revisions', [],
497 (
467 _('only return changes after or between specified tags')),
498 'r',
468 ('u', 'update-cache', None, _("update cvs log cache")),
499 'revisions',
469 ('x', 'new-cache', None, _("create new cvs log cache")),
500 [],
470 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
501 _('only return changes after or between specified tags'),
471 ('', 'root', '', _('specify cvsroot')),
502 ),
472 # Options specific to builtin cvsps
503 ('u', 'update-cache', None, _("update cvs log cache")),
473 ('', 'parents', '', _('show parent changesets')),
504 ('x', 'new-cache', None, _("create new cvs log cache")),
474 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
505 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
475 # Options that are ignored for compatibility with cvsps-2.1
506 ('', 'root', '', _('specify cvsroot')),
476 ('A', 'cvs-direct', None, _('ignored for compatibility')),
507 # Options specific to builtin cvsps
508 ('', 'parents', '', _('show parent changesets')),
509 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
510 # Options that are ignored for compatibility with cvsps-2.1
511 ('A', 'cvs-direct', None, _('ignored for compatibility')),
477 ],
512 ],
478 _('hg debugcvsps [OPTION]... [PATH]...'),
513 _('hg debugcvsps [OPTION]... [PATH]...'),
479 norepo=True)
514 norepo=True,
515 )
480 def debugcvsps(ui, *args, **opts):
516 def debugcvsps(ui, *args, **opts):
481 '''create changeset information from CVS
517 '''create changeset information from CVS
482
518
@@ -490,34 +526,40 b' def debugcvsps(ui, *args, **opts):'
490 dates.'''
526 dates.'''
491 return cvsps.debugcvsps(ui, *args, **opts)
527 return cvsps.debugcvsps(ui, *args, **opts)
492
528
529
493 def kwconverted(context, mapping, name):
530 def kwconverted(context, mapping, name):
494 ctx = context.resource(mapping, 'ctx')
531 ctx = context.resource(mapping, 'ctx')
495 rev = ctx.extra().get('convert_revision', '')
532 rev = ctx.extra().get('convert_revision', '')
496 if rev.startswith('svn:'):
533 if rev.startswith('svn:'):
497 if name == 'svnrev':
534 if name == 'svnrev':
498 return (b"%d" % subversion.revsplit(rev)[2])
535 return b"%d" % subversion.revsplit(rev)[2]
499 elif name == 'svnpath':
536 elif name == 'svnpath':
500 return subversion.revsplit(rev)[1]
537 return subversion.revsplit(rev)[1]
501 elif name == 'svnuuid':
538 elif name == 'svnuuid':
502 return subversion.revsplit(rev)[0]
539 return subversion.revsplit(rev)[0]
503 return rev
540 return rev
504
541
542
505 templatekeyword = registrar.templatekeyword()
543 templatekeyword = registrar.templatekeyword()
506
544
545
507 @templatekeyword('svnrev', requires={'ctx'})
546 @templatekeyword('svnrev', requires={'ctx'})
508 def kwsvnrev(context, mapping):
547 def kwsvnrev(context, mapping):
509 """String. Converted subversion revision number."""
548 """String. Converted subversion revision number."""
510 return kwconverted(context, mapping, 'svnrev')
549 return kwconverted(context, mapping, 'svnrev')
511
550
551
512 @templatekeyword('svnpath', requires={'ctx'})
552 @templatekeyword('svnpath', requires={'ctx'})
513 def kwsvnpath(context, mapping):
553 def kwsvnpath(context, mapping):
514 """String. Converted subversion revision project path."""
554 """String. Converted subversion revision project path."""
515 return kwconverted(context, mapping, 'svnpath')
555 return kwconverted(context, mapping, 'svnpath')
516
556
557
517 @templatekeyword('svnuuid', requires={'ctx'})
558 @templatekeyword('svnuuid', requires={'ctx'})
518 def kwsvnuuid(context, mapping):
559 def kwsvnuuid(context, mapping):
519 """String. Converted subversion revision repository identifier."""
560 """String. Converted subversion revision repository identifier."""
520 return kwconverted(context, mapping, 'svnuuid')
561 return kwconverted(context, mapping, 'svnuuid')
521
562
563
522 # tell hggettext to extract docstrings from these functions:
564 # tell hggettext to extract docstrings from these functions:
523 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
565 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -12,18 +12,13 b' from __future__ import absolute_import'
12 import os
12 import os
13
13
14 from mercurial.i18n import _
14 from mercurial.i18n import _
15 from mercurial import (
15 from mercurial import demandimport, error
16 demandimport,
17 error
18 )
19 from . import common
16 from . import common
20
17
21 # these do not work with demandimport, blacklist
18 # these do not work with demandimport, blacklist
22 demandimport.IGNORES.update([
19 demandimport.IGNORES.update(
23 'bzrlib.transactions',
20 ['bzrlib.transactions', 'bzrlib.urlutils', 'ElementPath',]
24 'bzrlib.urlutils',
21 )
25 'ElementPath',
26 ])
27
22
28 try:
23 try:
29 # bazaar imports
24 # bazaar imports
@@ -31,6 +26,7 b' try:'
31 import bzrlib.errors
26 import bzrlib.errors
32 import bzrlib.revision
27 import bzrlib.revision
33 import bzrlib.revisionspec
28 import bzrlib.revisionspec
29
34 bzrdir = bzrlib.bzrdir
30 bzrdir = bzrlib.bzrdir
35 errors = bzrlib.errors
31 errors = bzrlib.errors
36 revision = bzrlib.revision
32 revision = bzrlib.revision
@@ -41,6 +37,7 b' except ImportError:'
41
37
42 supportedkinds = ('file', 'symlink')
38 supportedkinds = ('file', 'symlink')
43
39
40
44 class bzr_source(common.converter_source):
41 class bzr_source(common.converter_source):
45 """Reads Bazaar repositories by using the Bazaar Python libraries"""
42 """Reads Bazaar repositories by using the Bazaar Python libraries"""
46
43
@@ -48,8 +45,9 b' class bzr_source(common.converter_source'
48 super(bzr_source, self).__init__(ui, repotype, path, revs=revs)
45 super(bzr_source, self).__init__(ui, repotype, path, revs=revs)
49
46
50 if not os.path.exists(os.path.join(path, '.bzr')):
47 if not os.path.exists(os.path.join(path, '.bzr')):
51 raise common.NoRepo(_('%s does not look like a Bazaar repository')
48 raise common.NoRepo(
52 % path)
49 _('%s does not look like a Bazaar repository') % path
50 )
53
51
54 try:
52 try:
55 # access bzrlib stuff
53 # access bzrlib stuff
@@ -62,8 +60,9 b' class bzr_source(common.converter_source'
62 try:
60 try:
63 self.sourcerepo = bzrdir.BzrDir.open(path).open_repository()
61 self.sourcerepo = bzrdir.BzrDir.open(path).open_repository()
64 except errors.NoRepositoryPresent:
62 except errors.NoRepositoryPresent:
65 raise common.NoRepo(_('%s does not look like a Bazaar repository')
63 raise common.NoRepo(
66 % path)
64 _('%s does not look like a Bazaar repository') % path
65 )
67 self._parentids = {}
66 self._parentids = {}
68 self._saverev = ui.configbool('convert', 'bzr.saverev')
67 self._saverev = ui.configbool('convert', 'bzr.saverev')
69
68
@@ -78,11 +77,18 b' class bzr_source(common.converter_source'
78 except (errors.NoWorkingTree, errors.NotLocalUrl):
77 except (errors.NoWorkingTree, errors.NotLocalUrl):
79 tree = None
78 tree = None
80 branch = dir.open_branch()
79 branch = dir.open_branch()
81 if (tree is not None and tree.bzrdir.root_transport.base !=
80 if (
82 branch.bzrdir.root_transport.base):
81 tree is not None
83 self.ui.warn(_('warning: lightweight checkouts may cause '
82 and tree.bzrdir.root_transport.base
84 'conversion failures, try with a regular '
83 != branch.bzrdir.root_transport.base
85 'branch instead.\n'))
84 ):
85 self.ui.warn(
86 _(
87 'warning: lightweight checkouts may cause '
88 'conversion failures, try with a regular '
89 'branch instead.\n'
90 )
91 )
86 except Exception:
92 except Exception:
87 self.ui.note(_('bzr source type could not be determined\n'))
93 self.ui.note(_('bzr source type could not be determined\n'))
88
94
@@ -119,8 +125,9 b' class bzr_source(common.converter_source'
119 pass
125 pass
120 revid = info.rev_id
126 revid = info.rev_id
121 if revid is None:
127 if revid is None:
122 raise error.Abort(_('%s is not a valid revision')
128 raise error.Abort(
123 % self.revs[0])
129 _('%s is not a valid revision') % self.revs[0]
130 )
124 heads = [revid]
131 heads = [revid]
125 # Empty repositories return 'null:', which cannot be retrieved
132 # Empty repositories return 'null:', which cannot be retrieved
126 heads = [h for h in heads if h != 'null:']
133 heads = [h for h in heads if h != 'null:']
@@ -139,8 +146,9 b' class bzr_source(common.converter_source'
139 if kind == 'symlink':
146 if kind == 'symlink':
140 target = revtree.get_symlink_target(fileid)
147 target = revtree.get_symlink_target(fileid)
141 if target is None:
148 if target is None:
142 raise error.Abort(_('%s.%s symlink has no target')
149 raise error.Abort(
143 % (name, rev))
150 _('%s.%s symlink has no target') % (name, rev)
151 )
144 return target, mode
152 return target, mode
145 else:
153 else:
146 sio = revtree.get_file(fileid)
154 sio = revtree.get_file(fileid)
@@ -171,13 +179,15 b' class bzr_source(common.converter_source'
171 branch = self.recode(rev.properties.get('branch-nick', u'default'))
179 branch = self.recode(rev.properties.get('branch-nick', u'default'))
172 if branch == 'trunk':
180 if branch == 'trunk':
173 branch = 'default'
181 branch = 'default'
174 return common.commit(parents=parents,
182 return common.commit(
175 date='%d %d' % (rev.timestamp, -rev.timezone),
183 parents=parents,
176 author=self.recode(rev.committer),
184 date='%d %d' % (rev.timestamp, -rev.timezone),
177 desc=self.recode(rev.message),
185 author=self.recode(rev.committer),
178 branch=branch,
186 desc=self.recode(rev.message),
179 rev=version,
187 branch=branch,
180 saverev=self._saverev)
188 rev=version,
189 saverev=self._saverev,
190 )
181
191
182 def gettags(self):
192 def gettags(self):
183 bytetags = {}
193 bytetags = {}
@@ -216,11 +226,21 b' class bzr_source(common.converter_source'
216
226
217 # Process the entries by reverse lexicographic name order to
227 # Process the entries by reverse lexicographic name order to
218 # handle nested renames correctly, most specific first.
228 # handle nested renames correctly, most specific first.
219 curchanges = sorted(current.iter_changes(origin),
229 curchanges = sorted(
220 key=lambda c: c[1][0] or c[1][1],
230 current.iter_changes(origin),
221 reverse=True)
231 key=lambda c: c[1][0] or c[1][1],
222 for (fileid, paths, changed_content, versioned, parent, name,
232 reverse=True,
223 kind, executable) in curchanges:
233 )
234 for (
235 fileid,
236 paths,
237 changed_content,
238 versioned,
239 parent,
240 name,
241 kind,
242 executable,
243 ) in curchanges:
224
244
225 if paths[0] == u'' or paths[1] == u'':
245 if paths[0] == u'' or paths[1] == u'':
226 # ignore changes to tree root
246 # ignore changes to tree root
@@ -260,9 +280,11 b' class bzr_source(common.converter_source'
260 changes.append((frompath, revid))
280 changes.append((frompath, revid))
261 changes.append((topath, revid))
281 changes.append((topath, revid))
262 # add to mode cache
282 # add to mode cache
263 mode = ((entry.executable and 'x')
283 mode = (
264 or (entry.kind == 'symlink' and 's')
284 (entry.executable and 'x')
265 or '')
285 or (entry.kind == 'symlink' and 's')
286 or ''
287 )
266 self._modecache[(topath, revid)] = mode
288 self._modecache[(topath, revid)] = mode
267 # register the change as move
289 # register the change as move
268 renames[topath] = frompath
290 renames[topath] = frompath
@@ -290,8 +312,7 b' class bzr_source(common.converter_source'
290
312
291 # populate the mode cache
313 # populate the mode cache
292 kind, executable = [e[1] for e in (kind, executable)]
314 kind, executable = [e[1] for e in (kind, executable)]
293 mode = ((executable and 'x') or (kind == 'symlink' and 'l')
315 mode = (executable and 'x') or (kind == 'symlink' and 'l') or ''
294 or '')
295 self._modecache[(topath, revid)] = mode
316 self._modecache[(topath, revid)] = mode
296 changes.append((topath, revid))
317 changes.append((topath, revid))
297
318
@@ -22,20 +22,19 b' from mercurial import ('
22 pycompat,
22 pycompat,
23 util,
23 util,
24 )
24 )
25 from mercurial.utils import (
25 from mercurial.utils import procutil
26 procutil,
27 )
28
26
29 pickle = util.pickle
27 pickle = util.pickle
30 propertycache = util.propertycache
28 propertycache = util.propertycache
31
29
30
32 def _encodeornone(d):
31 def _encodeornone(d):
33 if d is None:
32 if d is None:
34 return
33 return
35 return d.encode('latin1')
34 return d.encode('latin1')
36
35
36
37 class _shlexpy3proxy(object):
37 class _shlexpy3proxy(object):
38
39 def __init__(self, l):
38 def __init__(self, l):
40 self._l = l
39 self._l = l
41
40
@@ -53,6 +52,7 b' class _shlexpy3proxy(object):'
53 def lineno(self):
52 def lineno(self):
54 return self._l.lineno
53 return self._l.lineno
55
54
55
56 def shlexer(data=None, filepath=None, wordchars=None, whitespace=None):
56 def shlexer(data=None, filepath=None, wordchars=None, whitespace=None):
57 if data is None:
57 if data is None:
58 if pycompat.ispy3:
58 if pycompat.ispy3:
@@ -62,7 +62,8 b' def shlexer(data=None, filepath=None, wo'
62 else:
62 else:
63 if filepath is not None:
63 if filepath is not None:
64 raise error.ProgrammingError(
64 raise error.ProgrammingError(
65 'shlexer only accepts data or filepath, not both')
65 'shlexer only accepts data or filepath, not both'
66 )
66 if pycompat.ispy3:
67 if pycompat.ispy3:
67 data = data.decode('latin1')
68 data = data.decode('latin1')
68 l = shlex.shlex(data, infile=filepath, posix=True)
69 l = shlex.shlex(data, infile=filepath, posix=True)
@@ -81,6 +82,7 b' def shlexer(data=None, filepath=None, wo'
81 return _shlexpy3proxy(l)
82 return _shlexpy3proxy(l)
82 return l
83 return l
83
84
85
84 def encodeargs(args):
86 def encodeargs(args):
85 def encodearg(s):
87 def encodearg(s):
86 lines = base64.encodestring(s)
88 lines = base64.encodestring(s)
@@ -90,13 +92,16 b' def encodeargs(args):'
90 s = pickle.dumps(args)
92 s = pickle.dumps(args)
91 return encodearg(s)
93 return encodearg(s)
92
94
95
93 def decodeargs(s):
96 def decodeargs(s):
94 s = base64.decodestring(s)
97 s = base64.decodestring(s)
95 return pickle.loads(s)
98 return pickle.loads(s)
96
99
100
97 class MissingTool(Exception):
101 class MissingTool(Exception):
98 pass
102 pass
99
103
104
100 def checktool(exe, name=None, abort=True):
105 def checktool(exe, name=None, abort=True):
101 name = name or exe
106 name = name or exe
102 if not procutil.findexe(exe):
107 if not procutil.findexe(exe):
@@ -106,27 +111,43 b' def checktool(exe, name=None, abort=True'
106 exc = MissingTool
111 exc = MissingTool
107 raise exc(_('cannot find required "%s" tool') % name)
112 raise exc(_('cannot find required "%s" tool') % name)
108
113
114
109 class NoRepo(Exception):
115 class NoRepo(Exception):
110 pass
116 pass
111
117
118
112 SKIPREV = 'SKIP'
119 SKIPREV = 'SKIP'
113
120
121
114 class commit(object):
122 class commit(object):
115 def __init__(self, author, date, desc, parents, branch=None, rev=None,
123 def __init__(
116 extra=None, sortkey=None, saverev=True, phase=phases.draft,
124 self,
117 optparents=None, ctx=None):
125 author,
126 date,
127 desc,
128 parents,
129 branch=None,
130 rev=None,
131 extra=None,
132 sortkey=None,
133 saverev=True,
134 phase=phases.draft,
135 optparents=None,
136 ctx=None,
137 ):
118 self.author = author or 'unknown'
138 self.author = author or 'unknown'
119 self.date = date or '0 0'
139 self.date = date or '0 0'
120 self.desc = desc
140 self.desc = desc
121 self.parents = parents # will be converted and used as parents
141 self.parents = parents # will be converted and used as parents
122 self.optparents = optparents or [] # will be used if already converted
142 self.optparents = optparents or [] # will be used if already converted
123 self.branch = branch
143 self.branch = branch
124 self.rev = rev
144 self.rev = rev
125 self.extra = extra or {}
145 self.extra = extra or {}
126 self.sortkey = sortkey
146 self.sortkey = sortkey
127 self.saverev = saverev
147 self.saverev = saverev
128 self.phase = phase
148 self.phase = phase
129 self.ctx = ctx # for hg to hg conversions
149 self.ctx = ctx # for hg to hg conversions
150
130
151
131 class converter_source(object):
152 class converter_source(object):
132 """Conversion source interface"""
153 """Conversion source interface"""
@@ -146,8 +167,10 b' class converter_source(object):'
146 such format for their revision numbering
167 such format for their revision numbering
147 """
168 """
148 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
169 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
149 raise error.Abort(_('%s entry %s is not a valid revision'
170 raise error.Abort(
150 ' identifier') % (mapname, revstr))
171 _('%s entry %s is not a valid revision' ' identifier')
172 % (mapname, revstr)
173 )
151
174
152 def before(self):
175 def before(self):
153 pass
176 pass
@@ -223,8 +246,9 b' class converter_source(object):'
223 try:
246 try:
224 return s.decode("latin-1").encode("utf-8")
247 return s.decode("latin-1").encode("utf-8")
225 except UnicodeError:
248 except UnicodeError:
226 return s.decode(pycompat.sysstr(encoding),
249 return s.decode(pycompat.sysstr(encoding), "replace").encode(
227 "replace").encode("utf-8")
250 "utf-8"
251 )
228
252
229 def getchangedfiles(self, rev, i):
253 def getchangedfiles(self, rev, i):
230 """Return the files changed by rev compared to parent[i].
254 """Return the files changed by rev compared to parent[i].
@@ -275,6 +299,7 b' class converter_source(object):'
275 """
299 """
276 return True
300 return True
277
301
302
278 class converter_sink(object):
303 class converter_sink(object):
279 """Conversion sink (target) interface"""
304 """Conversion sink (target) interface"""
280
305
@@ -301,8 +326,9 b' class converter_sink(object):'
301 mapping equivalent authors identifiers for each system."""
326 mapping equivalent authors identifiers for each system."""
302 return None
327 return None
303
328
304 def putcommit(self, files, copies, parents, commit, source, revmap, full,
329 def putcommit(
305 cleanp2):
330 self, files, copies, parents, commit, source, revmap, full, cleanp2
331 ):
306 """Create a revision with all changed files listed in 'files'
332 """Create a revision with all changed files listed in 'files'
307 and having listed parents. 'commit' is a commit object
333 and having listed parents. 'commit' is a commit object
308 containing at a minimum the author, date, and message for this
334 containing at a minimum the author, date, and message for this
@@ -369,6 +395,7 b' class converter_sink(object):'
369 special cases."""
395 special cases."""
370 raise NotImplementedError
396 raise NotImplementedError
371
397
398
372 class commandline(object):
399 class commandline(object):
373 def __init__(self, ui, command):
400 def __init__(self, ui, command):
374 self.ui = ui
401 self.ui = ui
@@ -403,11 +430,15 b' class commandline(object):'
403
430
404 def _run(self, cmd, *args, **kwargs):
431 def _run(self, cmd, *args, **kwargs):
405 def popen(cmdline):
432 def popen(cmdline):
406 p = subprocess.Popen(procutil.tonativestr(cmdline),
433 p = subprocess.Popen(
407 shell=True, bufsize=-1,
434 procutil.tonativestr(cmdline),
408 close_fds=procutil.closefds,
435 shell=True,
409 stdout=subprocess.PIPE)
436 bufsize=-1,
437 close_fds=procutil.closefds,
438 stdout=subprocess.PIPE,
439 )
410 return p
440 return p
441
411 return self._dorun(popen, cmd, *args, **kwargs)
442 return self._dorun(popen, cmd, *args, **kwargs)
412
443
413 def _run2(self, cmd, *args, **kwargs):
444 def _run2(self, cmd, *args, **kwargs):
@@ -416,7 +447,7 b' class commandline(object):'
416 def _run3(self, cmd, *args, **kwargs):
447 def _run3(self, cmd, *args, **kwargs):
417 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
448 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
418
449
419 def _dorun(self, openfunc, cmd, *args, **kwargs):
450 def _dorun(self, openfunc, cmd, *args, **kwargs):
420 cmdline = self._cmdline(cmd, *args, **kwargs)
451 cmdline = self._cmdline(cmd, *args, **kwargs)
421 self.ui.debug('running: %s\n' % (cmdline,))
452 self.ui.debug('running: %s\n' % (cmdline,))
422 self.prerun()
453 self.prerun()
@@ -495,6 +526,7 b' class commandline(object):'
495 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
526 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
496 self.run0(cmd, *(list(args) + l), **kwargs)
527 self.run0(cmd, *(list(args) + l), **kwargs)
497
528
529
498 class mapfile(dict):
530 class mapfile(dict):
499 def __init__(self, ui, path):
531 def __init__(self, ui, path):
500 super(mapfile, self).__init__()
532 super(mapfile, self).__init__()
@@ -523,7 +555,8 b' class mapfile(dict):'
523 except ValueError:
555 except ValueError:
524 raise error.Abort(
556 raise error.Abort(
525 _('syntax error in %s(%d): key/value pair expected')
557 _('syntax error in %s(%d): key/value pair expected')
526 % (self.path, i + 1))
558 % (self.path, i + 1)
559 )
527 if key not in self:
560 if key not in self:
528 self.order.append(key)
561 self.order.append(key)
529 super(mapfile, self).__setitem__(key, value)
562 super(mapfile, self).__setitem__(key, value)
@@ -535,8 +568,9 b' class mapfile(dict):'
535 self.fp = open(self.path, 'ab')
568 self.fp = open(self.path, 'ab')
536 except IOError as err:
569 except IOError as err:
537 raise error.Abort(
570 raise error.Abort(
538 _('could not open map file %r: %s') %
571 _('could not open map file %r: %s')
539 (self.path, encoding.strtolocal(err.strerror)))
572 % (self.path, encoding.strtolocal(err.strerror))
573 )
540 self.fp.write(util.tonativeeol('%s %s\n' % (key, value)))
574 self.fp.write(util.tonativeeol('%s %s\n' % (key, value)))
541 self.fp.flush()
575 self.fp.flush()
542 super(mapfile, self).__setitem__(key, value)
576 super(mapfile, self).__setitem__(key, value)
@@ -546,9 +580,11 b' class mapfile(dict):'
546 self.fp.close()
580 self.fp.close()
547 self.fp = None
581 self.fp = None
548
582
583
549 def makedatetimestamp(t):
584 def makedatetimestamp(t):
550 """Like dateutil.makedate() but for time t instead of current time"""
585 """Like dateutil.makedate() but for time t instead of current time"""
551 delta = (datetime.datetime.utcfromtimestamp(t) -
586 delta = datetime.datetime.utcfromtimestamp(
552 datetime.datetime.fromtimestamp(t))
587 t
588 ) - datetime.datetime.fromtimestamp(t)
553 tz = delta.days * 86400 + delta.seconds
589 tz = delta.days * 86400 + delta.seconds
554 return t, tz
590 return t, tz
@@ -54,12 +54,15 b' svn_source = subversion.svn_source'
54
54
55 orig_encoding = 'ascii'
55 orig_encoding = 'ascii'
56
56
57
57 def recode(s):
58 def recode(s):
58 if isinstance(s, pycompat.unicode):
59 if isinstance(s, pycompat.unicode):
59 return s.encode(pycompat.sysstr(orig_encoding), 'replace')
60 return s.encode(pycompat.sysstr(orig_encoding), 'replace')
60 else:
61 else:
61 return s.decode('utf-8').encode(
62 return s.decode('utf-8').encode(
62 pycompat.sysstr(orig_encoding), 'replace')
63 pycompat.sysstr(orig_encoding), 'replace'
64 )
65
63
66
64 def mapbranch(branch, branchmap):
67 def mapbranch(branch, branchmap):
65 '''
68 '''
@@ -90,10 +93,11 b' def mapbranch(branch, branchmap):'
90 branch = branchmap.get(branch or 'default', branch)
93 branch = branchmap.get(branch or 'default', branch)
91 # At some point we used "None" literal to denote the default branch,
94 # At some point we used "None" literal to denote the default branch,
92 # attempt to use that for backward compatibility.
95 # attempt to use that for backward compatibility.
93 if (not branch):
96 if not branch:
94 branch = branchmap.get('None', branch)
97 branch = branchmap.get('None', branch)
95 return branch
98 return branch
96
99
100
97 source_converters = [
101 source_converters = [
98 ('cvs', convert_cvs, 'branchsort'),
102 ('cvs', convert_cvs, 'branchsort'),
99 ('git', convert_git, 'branchsort'),
103 ('git', convert_git, 'branchsort'),
@@ -104,12 +108,13 b' source_converters = ['
104 ('gnuarch', gnuarch_source, 'branchsort'),
108 ('gnuarch', gnuarch_source, 'branchsort'),
105 ('bzr', bzr_source, 'branchsort'),
109 ('bzr', bzr_source, 'branchsort'),
106 ('p4', p4_source, 'branchsort'),
110 ('p4', p4_source, 'branchsort'),
107 ]
111 ]
108
112
109 sink_converters = [
113 sink_converters = [
110 ('hg', mercurial_sink),
114 ('hg', mercurial_sink),
111 ('svn', svn_sink),
115 ('svn', svn_sink),
112 ]
116 ]
117
113
118
114 def convertsource(ui, path, type, revs):
119 def convertsource(ui, path, type, revs):
115 exceptions = []
120 exceptions = []
@@ -126,6 +131,7 b' def convertsource(ui, path, type, revs):'
126 ui.write("%s\n" % pycompat.bytestr(inst.args[0]))
131 ui.write("%s\n" % pycompat.bytestr(inst.args[0]))
127 raise error.Abort(_('%s: missing or unsupported repository') % path)
132 raise error.Abort(_('%s: missing or unsupported repository') % path)
128
133
134
129 def convertsink(ui, path, type):
135 def convertsink(ui, path, type):
130 if type and type not in [s[0] for s in sink_converters]:
136 if type and type not in [s[0] for s in sink_converters]:
131 raise error.Abort(_('%s: invalid destination repository type') % type)
137 raise error.Abort(_('%s: invalid destination repository type') % type)
@@ -139,12 +145,14 b' def convertsink(ui, path, type):'
139 raise error.Abort('%s\n' % inst)
145 raise error.Abort('%s\n' % inst)
140 raise error.Abort(_('%s: unknown repository type') % path)
146 raise error.Abort(_('%s: unknown repository type') % path)
141
147
148
142 class progresssource(object):
149 class progresssource(object):
143 def __init__(self, ui, source, filecount):
150 def __init__(self, ui, source, filecount):
144 self.ui = ui
151 self.ui = ui
145 self.source = source
152 self.source = source
146 self.progress = ui.makeprogress(_('getting files'), unit=_('files'),
153 self.progress = ui.makeprogress(
147 total=filecount)
154 _('getting files'), unit=_('files'), total=filecount
155 )
148
156
149 def getfile(self, file, rev):
157 def getfile(self, file, rev):
150 self.progress.increment(item=file)
158 self.progress.increment(item=file)
@@ -159,6 +167,7 b' class progresssource(object):'
159 def close(self):
167 def close(self):
160 self.progress.complete()
168 self.progress.complete()
161
169
170
162 class converter(object):
171 class converter(object):
163 def __init__(self, ui, source, dest, revmapfile, opts):
172 def __init__(self, ui, source, dest, revmapfile, opts):
164
173
@@ -213,8 +222,13 b' class converter(object):'
213 line = list(lex)
222 line = list(lex)
214 # check number of parents
223 # check number of parents
215 if not (2 <= len(line) <= 3):
224 if not (2 <= len(line) <= 3):
216 raise error.Abort(_('syntax error in %s(%d): child parent1'
225 raise error.Abort(
217 '[,parent2] expected') % (path, i + 1))
226 _(
227 'syntax error in %s(%d): child parent1'
228 '[,parent2] expected'
229 )
230 % (path, i + 1)
231 )
218 for part in line:
232 for part in line:
219 self.source.checkrevformat(part)
233 self.source.checkrevformat(part)
220 child, p1, p2 = line[0], line[1:2], line[2:]
234 child, p1, p2 = line[0], line[1:2], line[2:]
@@ -222,13 +236,13 b' class converter(object):'
222 m[child] = p1
236 m[child] = p1
223 else:
237 else:
224 m[child] = p1 + p2
238 m[child] = p1 + p2
225 # if file does not exist or error reading, exit
239 # if file does not exist or error reading, exit
226 except IOError:
240 except IOError:
227 raise error.Abort(_('splicemap file not found or error reading %s:')
241 raise error.Abort(
228 % path)
242 _('splicemap file not found or error reading %s:') % path
243 )
229 return m
244 return m
230
245
231
232 def walktree(self, heads):
246 def walktree(self, heads):
233 '''Return a mapping that identifies the uncommitted parents of every
247 '''Return a mapping that identifies the uncommitted parents of every
234 uncommitted changeset.'''
248 uncommitted changeset.'''
@@ -236,8 +250,9 b' class converter(object):'
236 known = set()
250 known = set()
237 parents = {}
251 parents = {}
238 numcommits = self.source.numcommits()
252 numcommits = self.source.numcommits()
239 progress = self.ui.makeprogress(_('scanning'), unit=_('revisions'),
253 progress = self.ui.makeprogress(
240 total=numcommits)
254 _('scanning'), unit=_('revisions'), total=numcommits
255 )
241 while visit:
256 while visit:
242 n = visit.pop(0)
257 n = visit.pop(0)
243 if n in known:
258 if n in known:
@@ -266,8 +281,13 b' class converter(object):'
266 if c not in parents:
281 if c not in parents:
267 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
282 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
268 # Could be in source but not converted during this run
283 # Could be in source but not converted during this run
269 self.ui.warn(_('splice map revision %s is not being '
284 self.ui.warn(
270 'converted, ignoring\n') % c)
285 _(
286 'splice map revision %s is not being '
287 'converted, ignoring\n'
288 )
289 % c
290 )
271 continue
291 continue
272 pc = []
292 pc = []
273 for p in splicemap[c]:
293 for p in splicemap[c]:
@@ -325,6 +345,7 b' class converter(object):'
325 compression.
345 compression.
326 """
346 """
327 prev = [None]
347 prev = [None]
348
328 def picknext(nodes):
349 def picknext(nodes):
329 next = nodes[0]
350 next = nodes[0]
330 for n in nodes:
351 for n in nodes:
@@ -333,26 +354,34 b' class converter(object):'
333 break
354 break
334 prev[0] = next
355 prev[0] = next
335 return next
356 return next
357
336 return picknext
358 return picknext
337
359
338 def makesourcesorter():
360 def makesourcesorter():
339 """Source specific sort."""
361 """Source specific sort."""
340 keyfn = lambda n: self.commitcache[n].sortkey
362 keyfn = lambda n: self.commitcache[n].sortkey
363
341 def picknext(nodes):
364 def picknext(nodes):
342 return sorted(nodes, key=keyfn)[0]
365 return sorted(nodes, key=keyfn)[0]
366
343 return picknext
367 return picknext
344
368
345 def makeclosesorter():
369 def makeclosesorter():
346 """Close order sort."""
370 """Close order sort."""
347 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
371 keyfn = lambda n: (
348 self.commitcache[n].sortkey)
372 'close' not in self.commitcache[n].extra,
373 self.commitcache[n].sortkey,
374 )
375
349 def picknext(nodes):
376 def picknext(nodes):
350 return sorted(nodes, key=keyfn)[0]
377 return sorted(nodes, key=keyfn)[0]
378
351 return picknext
379 return picknext
352
380
353 def makedatesorter():
381 def makedatesorter():
354 """Sort revisions by date."""
382 """Sort revisions by date."""
355 dates = {}
383 dates = {}
384
356 def getdate(n):
385 def getdate(n):
357 if n not in dates:
386 if n not in dates:
358 dates[n] = dateutil.parsedate(self.commitcache[n].date)
387 dates[n] = dateutil.parsedate(self.commitcache[n].date)
@@ -390,8 +419,10 b' class converter(object):'
390 try:
419 try:
391 pendings[c].remove(n)
420 pendings[c].remove(n)
392 except ValueError:
421 except ValueError:
393 raise error.Abort(_('cycle detected between %s and %s')
422 raise error.Abort(
394 % (recode(c), recode(n)))
423 _('cycle detected between %s and %s')
424 % (recode(c), recode(n))
425 )
395 if not pendings[c]:
426 if not pendings[c]:
396 # Parents are converted, node is eligible
427 # Parents are converted, node is eligible
397 actives.insert(0, c)
428 actives.insert(0, c)
@@ -408,8 +439,9 b' class converter(object):'
408 self.ui.status(_('writing author map file %s\n') % authorfile)
439 self.ui.status(_('writing author map file %s\n') % authorfile)
409 ofile = open(authorfile, 'wb+')
440 ofile = open(authorfile, 'wb+')
410 for author in self.authors:
441 for author in self.authors:
411 ofile.write(util.tonativeeol("%s=%s\n"
442 ofile.write(
412 % (author, self.authors[author])))
443 util.tonativeeol("%s=%s\n" % (author, self.authors[author]))
444 )
413 ofile.close()
445 ofile.close()
414
446
415 def readauthormap(self, authorfile):
447 def readauthormap(self, authorfile):
@@ -464,19 +496,22 b' class converter(object):'
464 for prev in commit.parents:
496 for prev in commit.parents:
465 if prev not in self.commitcache:
497 if prev not in self.commitcache:
466 self.cachecommit(prev)
498 self.cachecommit(prev)
467 pbranches.append((self.map[prev],
499 pbranches.append(
468 self.commitcache[prev].branch))
500 (self.map[prev], self.commitcache[prev].branch)
501 )
469 self.dest.setbranch(commit.branch, pbranches)
502 self.dest.setbranch(commit.branch, pbranches)
470 try:
503 try:
471 parents = self.splicemap[rev]
504 parents = self.splicemap[rev]
472 self.ui.status(_('spliced in %s as parents of %s\n') %
505 self.ui.status(
473 (_(' and ').join(parents), rev))
506 _('spliced in %s as parents of %s\n')
507 % (_(' and ').join(parents), rev)
508 )
474 parents = [self.map.get(p, p) for p in parents]
509 parents = [self.map.get(p, p) for p in parents]
475 except KeyError:
510 except KeyError:
476 parents = [b[0] for b in pbranches]
511 parents = [b[0] for b in pbranches]
477 parents.extend(self.map[x]
512 parents.extend(
478 for x in commit.optparents
513 self.map[x] for x in commit.optparents if x in self.map
479 if x in self.map)
514 )
480 if len(pbranches) != 2:
515 if len(pbranches) != 2:
481 cleanp2 = set()
516 cleanp2 = set()
482 if len(parents) < 3:
517 if len(parents) < 3:
@@ -486,10 +521,12 b' class converter(object):'
486 # changed files N-1 times. This tweak to the number of
521 # changed files N-1 times. This tweak to the number of
487 # files makes it so the progress bar doesn't overflow
522 # files makes it so the progress bar doesn't overflow
488 # itself.
523 # itself.
489 source = progresssource(self.ui, self.source,
524 source = progresssource(
490 len(files) * (len(parents) - 1))
525 self.ui, self.source, len(files) * (len(parents) - 1)
491 newnode = self.dest.putcommit(files, copies, parents, commit,
526 )
492 source, self.map, full, cleanp2)
527 newnode = self.dest.putcommit(
528 files, copies, parents, commit, source, self.map, full, cleanp2
529 )
493 source.close()
530 source.close()
494 self.source.converted(rev, newnode)
531 self.source.converted(rev, newnode)
495 self.map[rev] = newnode
532 self.map[rev] = newnode
@@ -509,8 +546,9 b' class converter(object):'
509 c = None
546 c = None
510
547
511 self.ui.status(_("converting...\n"))
548 self.ui.status(_("converting...\n"))
512 progress = self.ui.makeprogress(_('converting'),
549 progress = self.ui.makeprogress(
513 unit=_('revisions'), total=len(t))
550 _('converting'), unit=_('revisions'), total=len(t)
551 )
514 for i, c in enumerate(t):
552 for i, c in enumerate(t):
515 num -= 1
553 num -= 1
516 desc = self.commitcache[c].desc
554 desc = self.commitcache[c].desc
@@ -538,8 +576,11 b' class converter(object):'
538 if nrev and tagsparent:
576 if nrev and tagsparent:
539 # write another hash correspondence to override the
577 # write another hash correspondence to override the
540 # previous one so we don't end up with extra tag heads
578 # previous one so we don't end up with extra tag heads
541 tagsparents = [e for e in self.map.iteritems()
579 tagsparents = [
542 if e[1] == tagsparent]
580 e
581 for e in self.map.iteritems()
582 if e[1] == tagsparent
583 ]
543 if tagsparents:
584 if tagsparents:
544 self.map[tagsparents[0][0]] = nrev
585 self.map[tagsparents[0][0]] = nrev
545
586
@@ -564,6 +605,7 b' class converter(object):'
564 self.source.after()
605 self.source.after()
565 self.map.close()
606 self.map.close()
566
607
608
567 def convert(ui, src, dest=None, revmapfile=None, **opts):
609 def convert(ui, src, dest=None, revmapfile=None, **opts):
568 opts = pycompat.byteskwargs(opts)
610 opts = pycompat.byteskwargs(opts)
569 global orig_encoding
611 global orig_encoding
@@ -582,8 +624,9 b' def convert(ui, src, dest=None, revmapfi'
582 destc = scmutil.wrapconvertsink(destc)
624 destc = scmutil.wrapconvertsink(destc)
583
625
584 try:
626 try:
585 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
627 srcc, defaultsort = convertsource(
586 opts.get('rev'))
628 ui, src, opts.get('source_type'), opts.get('rev')
629 )
587 except Exception:
630 except Exception:
588 for path in destc.created:
631 for path in destc.created:
589 shutil.rmtree(path, True)
632 shutil.rmtree(path, True)
@@ -599,8 +642,9 b' def convert(ui, src, dest=None, revmapfi'
599 sortmode = defaultsort
642 sortmode = defaultsort
600
643
601 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
644 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
602 raise error.Abort(_('--sourcesort is not supported by this data source')
645 raise error.Abort(
603 )
646 _('--sourcesort is not supported by this data source')
647 )
604 if sortmode == 'closesort' and not srcc.hasnativeclose():
648 if sortmode == 'closesort' and not srcc.hasnativeclose():
605 raise error.Abort(_('--closesort is not supported by this data source'))
649 raise error.Abort(_('--closesort is not supported by this data source'))
606
650
@@ -34,6 +34,7 b' converter_source = common.converter_sour'
34 makedatetimestamp = common.makedatetimestamp
34 makedatetimestamp = common.makedatetimestamp
35 NoRepo = common.NoRepo
35 NoRepo = common.NoRepo
36
36
37
37 class convert_cvs(converter_source):
38 class convert_cvs(converter_source):
38 def __init__(self, ui, repotype, path, revs=None):
39 def __init__(self, ui, repotype, path, revs=None):
39 super(convert_cvs, self).__init__(ui, repotype, path, revs=revs)
40 super(convert_cvs, self).__init__(ui, repotype, path, revs=revs)
@@ -63,15 +64,17 b' class convert_cvs(converter_source):'
63 maxrev = 0
64 maxrev = 0
64 if self.revs:
65 if self.revs:
65 if len(self.revs) > 1:
66 if len(self.revs) > 1:
66 raise error.Abort(_('cvs source does not support specifying '
67 raise error.Abort(
67 'multiple revs'))
68 _('cvs source does not support specifying ' 'multiple revs')
69 )
68 # TODO: handle tags
70 # TODO: handle tags
69 try:
71 try:
70 # patchset number?
72 # patchset number?
71 maxrev = int(self.revs[0])
73 maxrev = int(self.revs[0])
72 except ValueError:
74 except ValueError:
73 raise error.Abort(_('revision %s is not a patchset number')
75 raise error.Abort(
74 % self.revs[0])
76 _('revision %s is not a patchset number') % self.revs[0]
77 )
75
78
76 d = encoding.getcwd()
79 d = encoding.getcwd()
77 try:
80 try:
@@ -81,15 +84,18 b' class convert_cvs(converter_source):'
81 if not self.ui.configbool('convert', 'cvsps.cache'):
84 if not self.ui.configbool('convert', 'cvsps.cache'):
82 cache = None
85 cache = None
83 db = cvsps.createlog(self.ui, cache=cache)
86 db = cvsps.createlog(self.ui, cache=cache)
84 db = cvsps.createchangeset(self.ui, db,
87 db = cvsps.createchangeset(
88 self.ui,
89 db,
85 fuzz=int(self.ui.config('convert', 'cvsps.fuzz')),
90 fuzz=int(self.ui.config('convert', 'cvsps.fuzz')),
86 mergeto=self.ui.config('convert', 'cvsps.mergeto'),
91 mergeto=self.ui.config('convert', 'cvsps.mergeto'),
87 mergefrom=self.ui.config('convert', 'cvsps.mergefrom'))
92 mergefrom=self.ui.config('convert', 'cvsps.mergefrom'),
93 )
88
94
89 for cs in db:
95 for cs in db:
90 if maxrev and cs.id > maxrev:
96 if maxrev and cs.id > maxrev:
91 break
97 break
92 id = (b"%d" % cs.id)
98 id = b"%d" % cs.id
93 cs.author = self.recode(cs.author)
99 cs.author = self.recode(cs.author)
94 self.lastbranch[cs.branch] = id
100 self.lastbranch[cs.branch] = id
95 cs.comment = self.recode(cs.comment)
101 cs.comment = self.recode(cs.comment)
@@ -100,14 +106,19 b' class convert_cvs(converter_source):'
100
106
101 files = {}
107 files = {}
102 for f in cs.entries:
108 for f in cs.entries:
103 files[f.file] = "%s%s" % ('.'.join([(b"%d" % x)
109 files[f.file] = "%s%s" % (
104 for x in f.revision]),
110 '.'.join([(b"%d" % x) for x in f.revision]),
105 ['', '(DEAD)'][f.dead])
111 ['', '(DEAD)'][f.dead],
112 )
106
113
107 # add current commit to set
114 # add current commit to set
108 c = commit(author=cs.author, date=date,
115 c = commit(
109 parents=[(b"%d" % p.id) for p in cs.parents],
116 author=cs.author,
110 desc=cs.comment, branch=cs.branch or '')
117 date=date,
118 parents=[(b"%d" % p.id) for p in cs.parents],
119 desc=cs.comment,
120 branch=cs.branch or '',
121 )
111 self.changeset[id] = c
122 self.changeset[id] = c
112 self.files[id] = files
123 self.files[id] = files
113
124
@@ -125,8 +136,9 b' class convert_cvs(converter_source):'
125
136
126 if root.startswith(":pserver:"):
137 if root.startswith(":pserver:"):
127 root = root[9:]
138 root = root[9:]
128 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
139 m = re.match(
129 root)
140 r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)', root
141 )
130 if m:
142 if m:
131 conntype = "pserver"
143 conntype = "pserver"
132 user, passw, serv, port, root = m.groups()
144 user, passw, serv, port, root = m.groups()
@@ -166,8 +178,18 b' class convert_cvs(converter_source):'
166
178
167 sck = socket.socket()
179 sck = socket.socket()
168 sck.connect((serv, port))
180 sck.connect((serv, port))
169 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
181 sck.send(
170 "END AUTH REQUEST", ""]))
182 "\n".join(
183 [
184 "BEGIN AUTH REQUEST",
185 root,
186 user,
187 passw,
188 "END AUTH REQUEST",
189 "",
190 ]
191 )
192 )
171 if sck.recv(128) != "I LOVE YOU\n":
193 if sck.recv(128) != "I LOVE YOU\n":
172 raise error.Abort(_("CVS pserver authentication failed"))
194 raise error.Abort(_("CVS pserver authentication failed"))
173
195
@@ -205,16 +227,22 b' class convert_cvs(converter_source):'
205 self.realroot = root
227 self.realroot = root
206
228
207 self.writep.write("Root %s\n" % root)
229 self.writep.write("Root %s\n" % root)
208 self.writep.write("Valid-responses ok error Valid-requests Mode"
230 self.writep.write(
209 " M Mbinary E Checked-in Created Updated"
231 "Valid-responses ok error Valid-requests Mode"
210 " Merged Removed\n")
232 " M Mbinary E Checked-in Created Updated"
233 " Merged Removed\n"
234 )
211 self.writep.write("valid-requests\n")
235 self.writep.write("valid-requests\n")
212 self.writep.flush()
236 self.writep.flush()
213 r = self.readp.readline()
237 r = self.readp.readline()
214 if not r.startswith("Valid-requests"):
238 if not r.startswith("Valid-requests"):
215 raise error.Abort(_('unexpected response from CVS server '
239 raise error.Abort(
216 '(expected "Valid-requests", but got %r)')
240 _(
217 % r)
241 'unexpected response from CVS server '
242 '(expected "Valid-requests", but got %r)'
243 )
244 % r
245 )
218 if "UseUnchanged" in r:
246 if "UseUnchanged" in r:
219 self.writep.write("UseUnchanged\n")
247 self.writep.write("UseUnchanged\n")
220 self.writep.flush()
248 self.writep.flush()
@@ -225,7 +253,6 b' class convert_cvs(converter_source):'
225 return self.heads
253 return self.heads
226
254
227 def getfile(self, name, rev):
255 def getfile(self, name, rev):
228
229 def chunkedread(fp, count):
256 def chunkedread(fp, count):
230 # file-objects returned by socket.makefile() do not handle
257 # file-objects returned by socket.makefile() do not handle
231 # large read() requests very well.
258 # large read() requests very well.
@@ -234,8 +261,9 b' class convert_cvs(converter_source):'
234 while count > 0:
261 while count > 0:
235 data = fp.read(min(count, chunksize))
262 data = fp.read(min(count, chunksize))
236 if not data:
263 if not data:
237 raise error.Abort(_("%d bytes missing from remote file")
264 raise error.Abort(
238 % count)
265 _("%d bytes missing from remote file") % count
266 )
239 count -= len(data)
267 count -= len(data)
240 output.write(data)
268 output.write(data)
241 return output.getvalue()
269 return output.getvalue()
@@ -256,8 +284,8 b' class convert_cvs(converter_source):'
256 while True:
284 while True:
257 line = self.readp.readline()
285 line = self.readp.readline()
258 if line.startswith("Created ") or line.startswith("Updated "):
286 if line.startswith("Created ") or line.startswith("Updated "):
259 self.readp.readline() # path
287 self.readp.readline() # path
260 self.readp.readline() # entries
288 self.readp.readline() # entries
261 mode = self.readp.readline()[:-1]
289 mode = self.readp.readline()[:-1]
262 count = int(self.readp.readline()[:-1])
290 count = int(self.readp.readline()[:-1])
263 data = chunkedread(self.readp, count)
291 data = chunkedread(self.readp, count)
@@ -26,6 +26,7 b' from mercurial.utils import ('
26
26
27 pickle = util.pickle
27 pickle = util.pickle
28
28
29
29 class logentry(object):
30 class logentry(object):
30 '''Class logentry has the following attributes:
31 '''Class logentry has the following attributes:
31 .author - author name as CVS knows it
32 .author - author name as CVS knows it
@@ -46,17 +47,22 b' class logentry(object):'
46 rlog output) or None
47 rlog output) or None
47 .branchpoints - the branches that start at the current entry or empty
48 .branchpoints - the branches that start at the current entry or empty
48 '''
49 '''
50
49 def __init__(self, **entries):
51 def __init__(self, **entries):
50 self.synthetic = False
52 self.synthetic = False
51 self.__dict__.update(entries)
53 self.__dict__.update(entries)
52
54
53 def __repr__(self):
55 def __repr__(self):
54 items = (r"%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
56 items = (
55 return r"%s(%s)"%(type(self).__name__, r", ".join(items))
57 r"%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__)
58 )
59 return r"%s(%s)" % (type(self).__name__, r", ".join(items))
60
56
61
57 class logerror(Exception):
62 class logerror(Exception):
58 pass
63 pass
59
64
65
60 def getrepopath(cvspath):
66 def getrepopath(cvspath):
61 """Return the repository path from a CVS path.
67 """Return the repository path from a CVS path.
62
68
@@ -93,45 +99,52 b' def getrepopath(cvspath):'
93 if atposition != -1:
99 if atposition != -1:
94 start = atposition
100 start = atposition
95
101
96 repopath = parts[-1][parts[-1].find('/', start):]
102 repopath = parts[-1][parts[-1].find('/', start) :]
97 return repopath
103 return repopath
98
104
105
99 def createlog(ui, directory=None, root="", rlog=True, cache=None):
106 def createlog(ui, directory=None, root="", rlog=True, cache=None):
100 '''Collect the CVS rlog'''
107 '''Collect the CVS rlog'''
101
108
102 # Because we store many duplicate commit log messages, reusing strings
109 # Because we store many duplicate commit log messages, reusing strings
103 # saves a lot of memory and pickle storage space.
110 # saves a lot of memory and pickle storage space.
104 _scache = {}
111 _scache = {}
112
105 def scache(s):
113 def scache(s):
106 "return a shared version of a string"
114 "return a shared version of a string"
107 return _scache.setdefault(s, s)
115 return _scache.setdefault(s, s)
108
116
109 ui.status(_('collecting CVS rlog\n'))
117 ui.status(_('collecting CVS rlog\n'))
110
118
111 log = [] # list of logentry objects containing the CVS state
119 log = [] # list of logentry objects containing the CVS state
112
120
113 # patterns to match in CVS (r)log output, by state of use
121 # patterns to match in CVS (r)log output, by state of use
114 re_00 = re.compile(b'RCS file: (.+)$')
122 re_00 = re.compile(b'RCS file: (.+)$')
115 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$')
123 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$')
116 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$')
124 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$')
117 re_03 = re.compile(b"(Cannot access.+CVSROOT)|"
125 re_03 = re.compile(
118 b"(can't create temporary directory.+)$")
126 b"(Cannot access.+CVSROOT)|" b"(can't create temporary directory.+)$"
127 )
119 re_10 = re.compile(b'Working file: (.+)$')
128 re_10 = re.compile(b'Working file: (.+)$')
120 re_20 = re.compile(b'symbolic names:')
129 re_20 = re.compile(b'symbolic names:')
121 re_30 = re.compile(b'\t(.+): ([\\d.]+)$')
130 re_30 = re.compile(b'\t(.+): ([\\d.]+)$')
122 re_31 = re.compile(b'----------------------------$')
131 re_31 = re.compile(b'----------------------------$')
123 re_32 = re.compile(b'======================================='
132 re_32 = re.compile(
124 b'======================================$')
133 b'======================================='
134 b'======================================$'
135 )
125 re_50 = re.compile(br'revision ([\d.]+)(\s+locked by:\s+.+;)?$')
136 re_50 = re.compile(br'revision ([\d.]+)(\s+locked by:\s+.+;)?$')
126 re_60 = re.compile(br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
137 re_60 = re.compile(
127 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
138 br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
128 br'(\s+commitid:\s+([^;]+);)?'
139 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
129 br'(.*mergepoint:\s+([^;]+);)?')
140 br'(\s+commitid:\s+([^;]+);)?'
141 br'(.*mergepoint:\s+([^;]+);)?'
142 )
130 re_70 = re.compile(b'branches: (.+);$')
143 re_70 = re.compile(b'branches: (.+);$')
131
144
132 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch')
145 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch')
133
146
134 prefix = '' # leading path to strip of what we get from CVS
147 prefix = '' # leading path to strip of what we get from CVS
135
148
136 if directory is None:
149 if directory is None:
137 # Current working directory
150 # Current working directory
@@ -151,7 +164,7 b' def createlog(ui, directory=None, root="'
151
164
152 # Use the Root file in the sandbox, if it exists
165 # Use the Root file in the sandbox, if it exists
153 try:
166 try:
154 root = open(os.path.join('CVS','Root'), 'rb').read().strip()
167 root = open(os.path.join('CVS', 'Root'), 'rb').read().strip()
155 except IOError:
168 except IOError:
156 pass
169 pass
157
170
@@ -178,17 +191,20 b' def createlog(ui, directory=None, root="'
178 # are mapped to different cache file names.
191 # are mapped to different cache file names.
179 cachefile = root.split(":") + [directory, "cache"]
192 cachefile = root.split(":") + [directory, "cache"]
180 cachefile = ['-'.join(re.findall(br'\w+', s)) for s in cachefile if s]
193 cachefile = ['-'.join(re.findall(br'\w+', s)) for s in cachefile if s]
181 cachefile = os.path.join(cachedir,
194 cachefile = os.path.join(
182 '.'.join([s for s in cachefile if s]))
195 cachedir, '.'.join([s for s in cachefile if s])
196 )
183
197
184 if cache == 'update':
198 if cache == 'update':
185 try:
199 try:
186 ui.note(_('reading cvs log cache %s\n') % cachefile)
200 ui.note(_('reading cvs log cache %s\n') % cachefile)
187 oldlog = pickle.load(open(cachefile, 'rb'))
201 oldlog = pickle.load(open(cachefile, 'rb'))
188 for e in oldlog:
202 for e in oldlog:
189 if not (util.safehasattr(e, 'branchpoints') and
203 if not (
190 util.safehasattr(e, 'commitid') and
204 util.safehasattr(e, 'branchpoints')
191 util.safehasattr(e, 'mergepoint')):
205 and util.safehasattr(e, 'commitid')
206 and util.safehasattr(e, 'mergepoint')
207 ):
192 ui.status(_('ignoring old cache\n'))
208 ui.status(_('ignoring old cache\n'))
193 oldlog = []
209 oldlog = []
194 break
210 break
@@ -198,7 +214,7 b' def createlog(ui, directory=None, root="'
198 ui.note(_('error reading cache: %r\n') % e)
214 ui.note(_('error reading cache: %r\n') % e)
199
215
200 if oldlog:
216 if oldlog:
201 date = oldlog[-1].date # last commit date as a (time,tz) tuple
217 date = oldlog[-1].date # last commit date as a (time,tz) tuple
202 date = dateutil.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
218 date = dateutil.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
203
219
204 # build the CVS commandline
220 # build the CVS commandline
@@ -220,11 +236,11 b' def createlog(ui, directory=None, root="'
220 cmd.append(directory)
236 cmd.append(directory)
221
237
222 # state machine begins here
238 # state machine begins here
223 tags = {} # dictionary of revisions on current file with their tags
239 tags = {} # dictionary of revisions on current file with their tags
224 branchmap = {} # mapping between branch names and revision numbers
240 branchmap = {} # mapping between branch names and revision numbers
225 rcsmap = {}
241 rcsmap = {}
226 state = 0
242 state = 0
227 store = False # set when a new record can be appended
243 store = False # set when a new record can be appended
228
244
229 cmd = [procutil.shellquote(arg) for arg in cmd]
245 cmd = [procutil.shellquote(arg) for arg in cmd]
230 ui.note(_("running %s\n") % (' '.join(cmd)))
246 ui.note(_("running %s\n") % (' '.join(cmd)))
@@ -239,7 +255,7 b' def createlog(ui, directory=None, root="'
239 peek = util.fromnativeeol(pfp.readline())
255 peek = util.fromnativeeol(pfp.readline())
240 if line.endswith('\n'):
256 if line.endswith('\n'):
241 line = line[:-1]
257 line = line[:-1]
242 #ui.debug('state=%d line=%r\n' % (state, line))
258 # ui.debug('state=%d line=%r\n' % (state, line))
243
259
244 if state == 0:
260 if state == 0:
245 # initial state, consume input until we see 'RCS file'
261 # initial state, consume input until we see 'RCS file'
@@ -250,7 +266,7 b' def createlog(ui, directory=None, root="'
250 if rlog:
266 if rlog:
251 filename = util.normpath(rcs[:-2])
267 filename = util.normpath(rcs[:-2])
252 if filename.startswith(prefix):
268 if filename.startswith(prefix):
253 filename = filename[len(prefix):]
269 filename = filename[len(prefix) :]
254 if filename.startswith('/'):
270 if filename.startswith('/'):
255 filename = filename[1:]
271 filename = filename[1:]
256 if filename.startswith('Attic/'):
272 if filename.startswith('Attic/'):
@@ -310,8 +326,9 b' def createlog(ui, directory=None, root="'
310 if re_31.match(line):
326 if re_31.match(line):
311 state = 5
327 state = 5
312 else:
328 else:
313 assert not re_32.match(line), _('must have at least '
329 assert not re_32.match(line), _(
314 'some revisions')
330 'must have at least ' 'some revisions'
331 )
315
332
316 elif state == 5:
333 elif state == 5:
317 # expecting revision number and possibly (ignored) lock indication
334 # expecting revision number and possibly (ignored) lock indication
@@ -319,15 +336,16 b' def createlog(ui, directory=None, root="'
319 # as this state is re-entered for subsequent revisions of a file.
336 # as this state is re-entered for subsequent revisions of a file.
320 match = re_50.match(line)
337 match = re_50.match(line)
321 assert match, _('expected revision number')
338 assert match, _('expected revision number')
322 e = logentry(rcs=scache(rcs),
339 e = logentry(
323 file=scache(filename),
340 rcs=scache(rcs),
324 revision=tuple([int(x) for x in
341 file=scache(filename),
325 match.group(1).split('.')]),
342 revision=tuple([int(x) for x in match.group(1).split('.')]),
326 branches=[],
343 branches=[],
327 parent=None,
344 parent=None,
328 commitid=None,
345 commitid=None,
329 mergepoint=None,
346 mergepoint=None,
330 branchpoints=set())
347 branchpoints=set(),
348 )
331
349
332 state = 6
350 state = 6
333
351
@@ -343,9 +361,10 b' def createlog(ui, directory=None, root="'
343 if len(d.split()) != 3:
361 if len(d.split()) != 3:
344 # cvs log dates always in GMT
362 # cvs log dates always in GMT
345 d = d + ' UTC'
363 d = d + ' UTC'
346 e.date = dateutil.parsedate(d, ['%y/%m/%d %H:%M:%S',
364 e.date = dateutil.parsedate(
347 '%Y/%m/%d %H:%M:%S',
365 d,
348 '%Y-%m-%d %H:%M:%S'])
366 ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S'],
367 )
349 e.author = scache(match.group(2))
368 e.author = scache(match.group(2))
350 e.dead = match.group(3).lower() == 'dead'
369 e.dead = match.group(3).lower() == 'dead'
351
370
@@ -359,18 +378,19 b' def createlog(ui, directory=None, root="'
359 else:
378 else:
360 e.lines = None
379 e.lines = None
361
380
362 if match.group(7): # cvs 1.12 commitid
381 if match.group(7): # cvs 1.12 commitid
363 e.commitid = match.group(8)
382 e.commitid = match.group(8)
364
383
365 if match.group(9): # cvsnt mergepoint
384 if match.group(9): # cvsnt mergepoint
366 myrev = match.group(10).split('.')
385 myrev = match.group(10).split('.')
367 if len(myrev) == 2: # head
386 if len(myrev) == 2: # head
368 e.mergepoint = 'HEAD'
387 e.mergepoint = 'HEAD'
369 else:
388 else:
370 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
389 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
371 branches = [b for b in branchmap if branchmap[b] == myrev]
390 branches = [b for b in branchmap if branchmap[b] == myrev]
372 assert len(branches) == 1, ('unknown branch: %s'
391 assert len(branches) == 1, (
373 % e.mergepoint)
392 'unknown branch: %s' % e.mergepoint
393 )
374 e.mergepoint = branches[0]
394 e.mergepoint = branches[0]
375
395
376 e.comment = []
396 e.comment = []
@@ -381,8 +401,10 b' def createlog(ui, directory=None, root="'
381 # or store the commit log message otherwise
401 # or store the commit log message otherwise
382 m = re_70.match(line)
402 m = re_70.match(line)
383 if m:
403 if m:
384 e.branches = [tuple([int(y) for y in x.strip().split('.')])
404 e.branches = [
385 for x in m.group(1).split(';')]
405 tuple([int(y) for y in x.strip().split('.')])
406 for x in m.group(1).split(';')
407 ]
386 state = 8
408 state = 8
387 elif re_31.match(line) and re_50.match(peek):
409 elif re_31.match(line) and re_50.match(peek):
388 state = 5
410 state = 5
@@ -417,13 +439,16 b' def createlog(ui, directory=None, root="'
417 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
439 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
418 # these revisions now, but mark them synthetic so
440 # these revisions now, but mark them synthetic so
419 # createchangeset() can take care of them.
441 # createchangeset() can take care of them.
420 if (store and
442 if (
421 e.dead and
443 store
422 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
444 and e.dead
423 len(e.comment) == 1 and
445 and e.revision[-1] == 1
424 file_added_re.match(e.comment[0])):
446 and len(e.comment) == 1 # 1.1 or 1.1.x.1
425 ui.debug('found synthetic revision in %s: %r\n'
447 and file_added_re.match(e.comment[0])
426 % (e.rcs, e.comment[0]))
448 ):
449 ui.debug(
450 'found synthetic revision in %s: %r\n' % (e.rcs, e.comment[0])
451 )
427 e.synthetic = True
452 e.synthetic = True
428
453
429 if store:
454 if store:
@@ -442,13 +467,13 b' def createlog(ui, directory=None, root="'
442 branchpoints = set()
467 branchpoints = set()
443 for branch, revision in branchmap.iteritems():
468 for branch, revision in branchmap.iteritems():
444 revparts = tuple([int(i) for i in revision.split('.')])
469 revparts = tuple([int(i) for i in revision.split('.')])
445 if len(revparts) < 2: # bad tags
470 if len(revparts) < 2: # bad tags
446 continue
471 continue
447 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
472 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
448 # normal branch
473 # normal branch
449 if revparts[:-2] == e.revision:
474 if revparts[:-2] == e.revision:
450 branchpoints.add(branch)
475 branchpoints.add(branch)
451 elif revparts == (1, 1, 1): # vendor branch
476 elif revparts == (1, 1, 1): # vendor branch
452 if revparts in e.branches:
477 if revparts in e.branches:
453 branchpoints.add(branch)
478 branchpoints.add(branch)
454 e.branchpoints = branchpoints
479 e.branchpoints = branchpoints
@@ -458,8 +483,9 b' def createlog(ui, directory=None, root="'
458 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
483 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
459
484
460 if len(log) % 100 == 0:
485 if len(log) % 100 == 0:
461 ui.status(stringutil.ellipsis('%d %s' % (len(log), e.file), 80)
486 ui.status(
462 + '\n')
487 stringutil.ellipsis('%d %s' % (len(log), e.file), 80) + '\n'
488 )
463
489
464 log.sort(key=lambda x: (x.rcs, x.revision))
490 log.sort(key=lambda x: (x.rcs, x.revision))
465
491
@@ -487,8 +513,12 b' def createlog(ui, directory=None, root="'
487 log.sort(key=lambda x: x.date)
513 log.sort(key=lambda x: x.date)
488
514
489 if oldlog and oldlog[-1].date >= log[0].date:
515 if oldlog and oldlog[-1].date >= log[0].date:
490 raise logerror(_('log cache overlaps with new log entries,'
516 raise logerror(
491 ' re-run without cache.'))
517 _(
518 'log cache overlaps with new log entries,'
519 ' re-run without cache.'
520 )
521 )
492
522
493 log = oldlog + log
523 log = oldlog + log
494
524
@@ -502,6 +532,7 b' def createlog(ui, directory=None, root="'
502
532
503 encodings = ui.configlist('convert', 'cvsps.logencoding')
533 encodings = ui.configlist('convert', 'cvsps.logencoding')
504 if encodings:
534 if encodings:
535
505 def revstr(r):
536 def revstr(r):
506 # this is needed, because logentry.revision is a tuple of "int"
537 # this is needed, because logentry.revision is a tuple of "int"
507 # (e.g. (1, 2) for "1.2")
538 # (e.g. (1, 2) for "1.2")
@@ -511,24 +542,33 b' def createlog(ui, directory=None, root="'
511 comment = entry.comment
542 comment = entry.comment
512 for e in encodings:
543 for e in encodings:
513 try:
544 try:
514 entry.comment = comment.decode(
545 entry.comment = comment.decode(pycompat.sysstr(e)).encode(
515 pycompat.sysstr(e)).encode('utf-8')
546 'utf-8'
547 )
516 if ui.debugflag:
548 if ui.debugflag:
517 ui.debug("transcoding by %s: %s of %s\n" %
549 ui.debug(
518 (e, revstr(entry.revision), entry.file))
550 "transcoding by %s: %s of %s\n"
551 % (e, revstr(entry.revision), entry.file)
552 )
519 break
553 break
520 except UnicodeDecodeError:
554 except UnicodeDecodeError:
521 pass # try next encoding
555 pass # try next encoding
522 except LookupError as inst: # unknown encoding, maybe
556 except LookupError as inst: # unknown encoding, maybe
523 raise error.Abort(inst,
557 raise error.Abort(
524 hint=_('check convert.cvsps.logencoding'
558 inst,
525 ' configuration'))
559 hint=_(
560 'check convert.cvsps.logencoding' ' configuration'
561 ),
562 )
526 else:
563 else:
527 raise error.Abort(_("no encoding can transcode"
564 raise error.Abort(
528 " CVS log message for %s of %s")
565 _(
529 % (revstr(entry.revision), entry.file),
566 "no encoding can transcode"
530 hint=_('check convert.cvsps.logencoding'
567 " CVS log message for %s of %s"
531 ' configuration'))
568 )
569 % (revstr(entry.revision), entry.file),
570 hint=_('check convert.cvsps.logencoding' ' configuration'),
571 )
532
572
533 hook.hook(ui, None, "cvslog", True, log=log)
573 hook.hook(ui, None, "cvslog", True, log=log)
534
574
@@ -550,14 +590,16 b' class changeset(object):'
550 .mergepoint- the branch that has been merged from or None
590 .mergepoint- the branch that has been merged from or None
551 .branchpoints- the branches that start at the current entry or empty
591 .branchpoints- the branches that start at the current entry or empty
552 '''
592 '''
593
553 def __init__(self, **entries):
594 def __init__(self, **entries):
554 self.id = None
595 self.id = None
555 self.synthetic = False
596 self.synthetic = False
556 self.__dict__.update(entries)
597 self.__dict__.update(entries)
557
598
558 def __repr__(self):
599 def __repr__(self):
559 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
600 items = ("%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__))
560 return "%s(%s)"%(type(self).__name__, ", ".join(items))
601 return "%s(%s)" % (type(self).__name__, ", ".join(items))
602
561
603
562 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
604 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
563 '''Convert log into changesets.'''
605 '''Convert log into changesets.'''
@@ -574,9 +616,17 b' def createchangeset(ui, log, fuzz=60, me'
574 mindate[e.commitid] = min(e.date, mindate[e.commitid])
616 mindate[e.commitid] = min(e.date, mindate[e.commitid])
575
617
576 # Merge changesets
618 # Merge changesets
577 log.sort(key=lambda x: (mindate.get(x.commitid, (-1, 0)),
619 log.sort(
578 x.commitid or '', x.comment,
620 key=lambda x: (
579 x.author, x.branch or '', x.date, x.branchpoints))
621 mindate.get(x.commitid, (-1, 0)),
622 x.commitid or '',
623 x.comment,
624 x.author,
625 x.branch or '',
626 x.date,
627 x.branchpoints,
628 )
629 )
580
630
581 changesets = []
631 changesets = []
582 files = set()
632 files = set()
@@ -599,22 +649,35 b' def createchangeset(ui, log, fuzz=60, me'
599 # first changeset and bar the next and MYBRANCH and MYBRANCH2
649 # first changeset and bar the next and MYBRANCH and MYBRANCH2
600 # should both start off of the bar changeset. No provisions are
650 # should both start off of the bar changeset. No provisions are
601 # made to ensure that this is, in fact, what happens.
651 # made to ensure that this is, in fact, what happens.
602 if not (c and e.branchpoints == c.branchpoints and
652 if not (
603 (# cvs commitids
653 c
604 (e.commitid is not None and e.commitid == c.commitid) or
654 and e.branchpoints == c.branchpoints
605 (# no commitids, use fuzzy commit detection
655 and ( # cvs commitids
606 (e.commitid is None or c.commitid is None) and
656 (e.commitid is not None and e.commitid == c.commitid)
607 e.comment == c.comment and
657 or ( # no commitids, use fuzzy commit detection
608 e.author == c.author and
658 (e.commitid is None or c.commitid is None)
609 e.branch == c.branch and
659 and e.comment == c.comment
610 ((c.date[0] + c.date[1]) <=
660 and e.author == c.author
611 (e.date[0] + e.date[1]) <=
661 and e.branch == c.branch
612 (c.date[0] + c.date[1]) + fuzz) and
662 and (
613 e.file not in files))):
663 (c.date[0] + c.date[1])
614 c = changeset(comment=e.comment, author=e.author,
664 <= (e.date[0] + e.date[1])
615 branch=e.branch, date=e.date,
665 <= (c.date[0] + c.date[1]) + fuzz
616 entries=[], mergepoint=e.mergepoint,
666 )
617 branchpoints=e.branchpoints, commitid=e.commitid)
667 and e.file not in files
668 )
669 )
670 ):
671 c = changeset(
672 comment=e.comment,
673 author=e.author,
674 branch=e.branch,
675 date=e.date,
676 entries=[],
677 mergepoint=e.mergepoint,
678 branchpoints=e.branchpoints,
679 commitid=e.commitid,
680 )
618 changesets.append(c)
681 changesets.append(c)
619
682
620 files = set()
683 files = set()
@@ -624,7 +687,7 b' def createchangeset(ui, log, fuzz=60, me'
624
687
625 c.entries.append(e)
688 c.entries.append(e)
626 files.add(e.file)
689 files.add(e.file)
627 c.date = e.date # changeset date is date of latest commit in it
690 c.date = e.date # changeset date is date of latest commit in it
628
691
629 # Mark synthetic changesets
692 # Mark synthetic changesets
630
693
@@ -665,6 +728,7 b' def createchangeset(ui, log, fuzz=60, me'
665 # Sort changesets by date
728 # Sort changesets by date
666
729
667 odd = set()
730 odd = set()
731
668 def cscmp(l, r):
732 def cscmp(l, r):
669 d = sum(l.date) - sum(r.date)
733 d = sum(l.date) - sum(r.date)
670 if d:
734 if d:
@@ -745,8 +809,8 b' def createchangeset(ui, log, fuzz=60, me'
745 if mergefrom:
809 if mergefrom:
746 mergefrom = re.compile(mergefrom)
810 mergefrom = re.compile(mergefrom)
747
811
748 versions = {} # changeset index where we saw any particular file version
812 versions = {} # changeset index where we saw any particular file version
749 branches = {} # changeset index where we saw a branch
813 branches = {} # changeset index where we saw a branch
750 n = len(changesets)
814 n = len(changesets)
751 i = 0
815 i = 0
752 while i < n:
816 while i < n:
@@ -777,8 +841,9 b' def createchangeset(ui, log, fuzz=60, me'
777
841
778 # Ensure no changeset has a synthetic changeset as a parent.
842 # Ensure no changeset has a synthetic changeset as a parent.
779 while p.synthetic:
843 while p.synthetic:
780 assert len(p.parents) <= 1, (
844 assert len(p.parents) <= 1, _(
781 _('synthetic changeset cannot have multiple parents'))
845 'synthetic changeset cannot have multiple parents'
846 )
782 if p.parents:
847 if p.parents:
783 p = p.parents[0]
848 p = p.parents[0]
784 else:
849 else:
@@ -802,9 +867,13 b' def createchangeset(ui, log, fuzz=60, me'
802 try:
867 try:
803 candidate = changesets[branches[m]]
868 candidate = changesets[branches[m]]
804 except KeyError:
869 except KeyError:
805 ui.warn(_("warning: CVS commit message references "
870 ui.warn(
806 "non-existent branch %r:\n%s\n")
871 _(
807 % (pycompat.bytestr(m), c.comment))
872 "warning: CVS commit message references "
873 "non-existent branch %r:\n%s\n"
874 )
875 % (pycompat.bytestr(m), c.comment)
876 )
808 if m in branches and c.branch != m and not candidate.synthetic:
877 if m in branches and c.branch != m and not candidate.synthetic:
809 c.parents.append(candidate)
878 c.parents.append(candidate)
810
879
@@ -816,15 +885,19 b' def createchangeset(ui, log, fuzz=60, me'
816 if m == 'HEAD':
885 if m == 'HEAD':
817 m = None
886 m = None
818 else:
887 else:
819 m = None # if no group found then merge to HEAD
888 m = None # if no group found then merge to HEAD
820 if m in branches and c.branch != m:
889 if m in branches and c.branch != m:
821 # insert empty changeset for merge
890 # insert empty changeset for merge
822 cc = changeset(
891 cc = changeset(
823 author=c.author, branch=m, date=c.date,
892 author=c.author,
893 branch=m,
894 date=c.date,
824 comment='convert-repo: CVS merge from branch %s'
895 comment='convert-repo: CVS merge from branch %s'
825 % c.branch,
896 % c.branch,
826 entries=[], tags=[],
897 entries=[],
827 parents=[changesets[branches[m]], c])
898 tags=[],
899 parents=[changesets[branches[m]], c],
900 )
828 changesets.insert(i + 1, cc)
901 changesets.insert(i + 1, cc)
829 branches[m] = i + 1
902 branches[m] = i + 1
830
903
@@ -853,8 +926,10 b' def createchangeset(ui, log, fuzz=60, me'
853 if odd:
926 if odd:
854 for l, r in odd:
927 for l, r in odd:
855 if l.id is not None and r.id is not None:
928 if l.id is not None and r.id is not None:
856 ui.warn(_('changeset %d is both before and after %d\n')
929 ui.warn(
857 % (l.id, r.id))
930 _('changeset %d is both before and after %d\n')
931 % (l.id, r.id)
932 )
858
933
859 ui.status(_('%d changeset entries\n') % len(changesets))
934 ui.status(_('%d changeset entries\n') % len(changesets))
860
935
@@ -886,7 +961,7 b' def debugcvsps(ui, *args, **opts):'
886 else:
961 else:
887 log = createlog(ui, root=opts["root"], cache=cache)
962 log = createlog(ui, root=opts["root"], cache=cache)
888 except logerror as e:
963 except logerror as e:
889 ui.write("%r\n"%e)
964 ui.write("%r\n" % e)
890 return
965 return
891
966
892 changesets = createchangeset(ui, log, opts["fuzz"])
967 changesets = createchangeset(ui, log, opts["fuzz"])
@@ -895,14 +970,16 b' def debugcvsps(ui, *args, **opts):'
895 # Print changesets (optionally filtered)
970 # Print changesets (optionally filtered)
896
971
897 off = len(revisions)
972 off = len(revisions)
898 branches = {} # latest version number in each branch
973 branches = {} # latest version number in each branch
899 ancestors = {} # parent branch
974 ancestors = {} # parent branch
900 for cs in changesets:
975 for cs in changesets:
901
976
902 if opts["ancestors"]:
977 if opts["ancestors"]:
903 if cs.branch not in branches and cs.parents and cs.parents[0].id:
978 if cs.branch not in branches and cs.parents and cs.parents[0].id:
904 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
979 ancestors[cs.branch] = (
905 cs.parents[0].id)
980 changesets[cs.parents[0].id - 1].branch,
981 cs.parents[0].id,
982 )
906 branches[cs.branch] = cs.id
983 branches[cs.branch] = cs.id
907
984
908 # limit by branches
985 # limit by branches
@@ -914,19 +991,35 b' def debugcvsps(ui, *args, **opts):'
914 # bug-for-bug compatibility with cvsps.
991 # bug-for-bug compatibility with cvsps.
915 ui.write('---------------------\n')
992 ui.write('---------------------\n')
916 ui.write(('PatchSet %d \n' % cs.id))
993 ui.write(('PatchSet %d \n' % cs.id))
917 ui.write(('Date: %s\n' % dateutil.datestr(cs.date,
994 ui.write(
918 '%Y/%m/%d %H:%M:%S %1%2')))
995 (
996 'Date: %s\n'
997 % dateutil.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2')
998 )
999 )
919 ui.write(('Author: %s\n' % cs.author))
1000 ui.write(('Author: %s\n' % cs.author))
920 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
1001 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
921 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
1002 ui.write(
922 ','.join(cs.tags) or '(none)')))
1003 (
1004 'Tag%s: %s \n'
1005 % (
1006 ['', 's'][len(cs.tags) > 1],
1007 ','.join(cs.tags) or '(none)',
1008 )
1009 )
1010 )
923 if cs.branchpoints:
1011 if cs.branchpoints:
924 ui.write(('Branchpoints: %s \n') %
1012 ui.write(
925 ', '.join(sorted(cs.branchpoints)))
1013 'Branchpoints: %s \n' % ', '.join(sorted(cs.branchpoints))
1014 )
926 if opts["parents"] and cs.parents:
1015 if opts["parents"] and cs.parents:
927 if len(cs.parents) > 1:
1016 if len(cs.parents) > 1:
928 ui.write(('Parents: %s\n' %
1017 ui.write(
929 (','.join([(b"%d" % p.id) for p in cs.parents]))))
1018 (
1019 'Parents: %s\n'
1020 % (','.join([(b"%d" % p.id) for p in cs.parents]))
1021 )
1022 )
930 else:
1023 else:
931 ui.write(('Parent: %d\n' % cs.parents[0].id))
1024 ui.write(('Parent: %d\n' % cs.parents[0].id))
932
1025
@@ -939,28 +1032,30 b' def debugcvsps(ui, *args, **opts):'
939 if r:
1032 if r:
940 ui.write(('Ancestors: %s\n' % (','.join(r))))
1033 ui.write(('Ancestors: %s\n' % (','.join(r))))
941
1034
942 ui.write(('Log:\n'))
1035 ui.write('Log:\n')
943 ui.write('%s\n\n' % cs.comment)
1036 ui.write('%s\n\n' % cs.comment)
944 ui.write(('Members: \n'))
1037 ui.write('Members: \n')
945 for f in cs.entries:
1038 for f in cs.entries:
946 fn = f.file
1039 fn = f.file
947 if fn.startswith(opts["prefix"]):
1040 if fn.startswith(opts["prefix"]):
948 fn = fn[len(opts["prefix"]):]
1041 fn = fn[len(opts["prefix"]) :]
949 ui.write('\t%s:%s->%s%s \n' % (
1042 ui.write(
1043 '\t%s:%s->%s%s \n'
1044 % (
950 fn,
1045 fn,
951 '.'.join([b"%d" % x for x in f.parent]) or 'INITIAL',
1046 '.'.join([b"%d" % x for x in f.parent]) or 'INITIAL',
952 '.'.join([(b"%d" % x) for x in f.revision]),
1047 '.'.join([(b"%d" % x) for x in f.revision]),
953 ['', '(DEAD)'][f.dead]))
1048 ['', '(DEAD)'][f.dead],
1049 )
1050 )
954 ui.write('\n')
1051 ui.write('\n')
955
1052
956 # have we seen the start tag?
1053 # have we seen the start tag?
957 if revisions and off:
1054 if revisions and off:
958 if (revisions[0] == (b"%d" % cs.id) or
1055 if revisions[0] == (b"%d" % cs.id) or revisions[0] in cs.tags:
959 revisions[0] in cs.tags):
960 off = False
1056 off = False
961
1057
962 # see if we reached the end tag
1058 # see if we reached the end tag
963 if len(revisions) > 1 and not off:
1059 if len(revisions) > 1 and not off:
964 if (revisions[1] == (b"%d" % cs.id) or
1060 if revisions[1] == (b"%d" % cs.id) or revisions[1] in cs.tags:
965 revisions[1] in cs.tags):
966 break
1061 break
@@ -19,6 +19,7 b' from mercurial import ('
19 )
19 )
20 from mercurial.utils import dateutil
20 from mercurial.utils import dateutil
21 from . import common
21 from . import common
22
22 NoRepo = common.NoRepo
23 NoRepo = common.NoRepo
23
24
24 # The naming drift of ElementTree is fun!
25 # The naming drift of ElementTree is fun!
@@ -37,10 +38,11 b' except ImportError:'
37 except ImportError:
38 except ImportError:
38 try:
39 try:
39 import elementtree.ElementTree.ElementTree as ElementTree
40 import elementtree.ElementTree.ElementTree as ElementTree
40 import elementtree.ElementTree.XMLParser as XMLParser
41 import elementtree.ElementTree.XMLParser as XMLParser
41 except ImportError:
42 except ImportError:
42 pass
43 pass
43
44
45
44 class darcs_source(common.converter_source, common.commandline):
46 class darcs_source(common.converter_source, common.commandline):
45 def __init__(self, ui, repotype, path, revs=None):
47 def __init__(self, ui, repotype, path, revs=None):
46 common.converter_source.__init__(self, ui, repotype, path, revs=revs)
48 common.converter_source.__init__(self, ui, repotype, path, revs=revs)
@@ -54,8 +56,9 b' class darcs_source(common.converter_sour'
54 common.checktool('darcs')
56 common.checktool('darcs')
55 version = self.run0('--version').splitlines()[0].strip()
57 version = self.run0('--version').splitlines()[0].strip()
56 if version < '2.1':
58 if version < '2.1':
57 raise error.Abort(_('darcs version 2.1 or newer needed (found %r)')
59 raise error.Abort(
58 % version)
60 _('darcs version 2.1 or newer needed (found %r)') % version
61 )
59
62
60 if "ElementTree" not in globals():
63 if "ElementTree" not in globals():
61 raise error.Abort(_("Python ElementTree module is not available"))
64 raise error.Abort(_("Python ElementTree module is not available"))
@@ -71,19 +74,23 b' class darcs_source(common.converter_sour'
71 format = self.format()
74 format = self.format()
72 if format:
75 if format:
73 if format in ('darcs-1.0', 'hashed'):
76 if format in ('darcs-1.0', 'hashed'):
74 raise NoRepo(_("%s repository format is unsupported, "
77 raise NoRepo(
75 "please upgrade") % format)
78 _("%s repository format is unsupported, " "please upgrade")
79 % format
80 )
76 else:
81 else:
77 self.ui.warn(_('failed to detect repository format!'))
82 self.ui.warn(_('failed to detect repository format!'))
78
83
79 def before(self):
84 def before(self):
80 self.tmppath = pycompat.mkdtemp(
85 self.tmppath = pycompat.mkdtemp(
81 prefix='convert-' + os.path.basename(self.path) + '-')
86 prefix='convert-' + os.path.basename(self.path) + '-'
87 )
82 output, status = self.run('init', repodir=self.tmppath)
88 output, status = self.run('init', repodir=self.tmppath)
83 self.checkexit(status)
89 self.checkexit(status)
84
90
85 tree = self.xml('changes', xml_output=True, summary=True,
91 tree = self.xml(
86 repodir=self.path)
92 'changes', xml_output=True, summary=True, repodir=self.path
93 )
87 tagname = None
94 tagname = None
88 child = None
95 child = None
89 for elt in tree.findall('patch'):
96 for elt in tree.findall('patch'):
@@ -135,8 +142,9 b' class darcs_source(common.converter_sour'
135
142
136 def manifest(self):
143 def manifest(self):
137 man = []
144 man = []
138 output, status = self.run('show', 'files', no_directories=True,
145 output, status = self.run(
139 repodir=self.tmppath)
146 'show', 'files', no_directories=True, repodir=self.tmppath
147 )
140 self.checkexit(status)
148 self.checkexit(status)
141 for line in output.split('\n'):
149 for line in output.split('\n'):
142 path = line[2:]
150 path = line[2:]
@@ -155,17 +163,24 b' class darcs_source(common.converter_sour'
155 # etree can return unicode objects for name, comment, and author,
163 # etree can return unicode objects for name, comment, and author,
156 # so recode() is used to ensure str objects are emitted.
164 # so recode() is used to ensure str objects are emitted.
157 newdateformat = '%Y-%m-%d %H:%M:%S %1%2'
165 newdateformat = '%Y-%m-%d %H:%M:%S %1%2'
158 return common.commit(author=self.recode(elt.get('author')),
166 return common.commit(
159 date=dateutil.datestr(date, newdateformat),
167 author=self.recode(elt.get('author')),
160 desc=self.recode(desc).strip(),
168 date=dateutil.datestr(date, newdateformat),
161 parents=self.parents[rev])
169 desc=self.recode(desc).strip(),
170 parents=self.parents[rev],
171 )
162
172
163 def pull(self, rev):
173 def pull(self, rev):
164 output, status = self.run('pull', self.path, all=True,
174 output, status = self.run(
165 match='hash %s' % rev,
175 'pull',
166 no_test=True, no_posthook=True,
176 self.path,
167 external_merge='/bin/false',
177 all=True,
168 repodir=self.tmppath)
178 match='hash %s' % rev,
179 no_test=True,
180 no_posthook=True,
181 external_merge='/bin/false',
182 repodir=self.tmppath,
183 )
169 if status:
184 if status:
170 if output.find('We have conflicts in') == -1:
185 if output.find('We have conflicts in') == -1:
171 self.checkexit(status, output)
186 self.checkexit(status, output)
@@ -196,7 +211,7 b' class darcs_source(common.converter_sour'
196 for f in man:
211 for f in man:
197 if not f.startswith(source):
212 if not f.startswith(source):
198 continue
213 continue
199 fdest = dest + '/' + f[len(source):]
214 fdest = dest + '/' + f[len(source) :]
200 changes.append((f, rev))
215 changes.append((f, rev))
201 changes.append((fdest, rev))
216 changes.append((fdest, rev))
202 copies[fdest] = f
217 copies[fdest] = f
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now