##// 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 36 pip = venv_bin / 'pip'
37 37 python = venv_bin / 'python'
38 38
39 args = [str(pip), 'install', '-r', str(REQUIREMENTS_TXT),
40 '--disable-pip-version-check']
39 args = [
40 str(pip),
41 'install',
42 '-r',
43 str(REQUIREMENTS_TXT),
44 '--disable-pip-version-check',
45 ]
41 46
42 47 if not venv_created:
43 48 args.append('-q')
@@ -45,8 +50,7 b' def bootstrap():'
45 50 subprocess.run(args, check=True)
46 51
47 52 os.environ['HGAUTOMATION_BOOTSTRAPPED'] = '1'
48 os.environ['PATH'] = '%s%s%s' % (
49 venv_bin, os.pathsep, os.environ['PATH'])
53 os.environ['PATH'] = '%s%s%s' % (venv_bin, os.pathsep, os.environ['PATH'])
50 54
51 55 subprocess.run([str(python), __file__] + sys.argv[1:], check=True)
52 56
@@ -10,9 +10,7 b''
10 10 import pathlib
11 11 import secrets
12 12
13 from .aws import (
14 AWSConnection,
15 )
13 from .aws import AWSConnection
16 14
17 15
18 16 class HGAutomation:
@@ -19,9 +19,7 b' import time'
19 19 import boto3
20 20 import botocore.exceptions
21 21
22 from .linux import (
23 BOOTSTRAP_DEBIAN,
24 )
22 from .linux import BOOTSTRAP_DEBIAN
25 23 from .ssh import (
26 24 exec_command as ssh_exec_command,
27 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' /
38 'install-windows-dependencies.ps1')
37 INSTALL_WINDOWS_DEPENDENCIES = (
38 SOURCE_ROOT / 'contrib' / 'install-windows-dependencies.ps1'
39 )
39 40
40 41
41 42 INSTANCE_TYPES_WITH_STORAGE = {
@@ -107,7 +108,6 b' SECURITY_GROUPS = {'
107 108 'Description': 'RDP from entire Internet',
108 109 },
109 110 ],
110
111 111 },
112 112 {
113 113 'FromPort': 5985,
@@ -119,7 +119,7 b' SECURITY_GROUPS = {'
119 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 154 IAM_INSTANCE_PROFILES = {
155 'ephemeral-ec2-1': {
156 'roles': [
157 'ephemeral-ec2-role-1',
158 ],
159 }
155 'ephemeral-ec2-1': {'roles': ['ephemeral-ec2-role-1',],}
160 156 }
161 157
162 158
@@ -257,10 +253,19 b' def rsa_key_fingerprint(p: pathlib.Path)'
257 253
258 254 # TODO use rsa package.
259 255 res = subprocess.run(
260 ['openssl', 'pkcs8', '-in', str(p), '-nocrypt', '-topk8',
261 '-outform', 'DER'],
256 [
257 'openssl',
258 'pkcs8',
259 '-in',
260 str(p),
261 '-nocrypt',
262 '-topk8',
263 '-outform',
264 'DER',
265 ],
262 266 capture_output=True,
263 check=True)
267 check=True,
268 )
264 269
265 270 sha1 = hashlib.sha1(res.stdout).hexdigest()
266 271 return ':'.join(a + b for a, b in zip(sha1[::2], sha1[1::2]))
@@ -306,8 +311,9 b' def ensure_key_pairs(state_path: pathlib'
306 311 data = fh.read()
307 312
308 313 if not data.startswith('ssh-rsa '):
309 print('unexpected format for key pair file: %s; removing' %
310 pub_full)
314 print(
315 'unexpected format for key pair file: %s; removing' % pub_full
316 )
311 317 pub_full.unlink()
312 318 priv_full.unlink()
313 319 continue
@@ -327,8 +333,10 b' def ensure_key_pairs(state_path: pathlib'
327 333 del local_existing[name]
328 334
329 335 elif remote_existing[name] != local_existing[name]:
330 print('key fingerprint mismatch for %s; '
331 'removing from local and remote' % name)
336 print(
337 'key fingerprint mismatch for %s; '
338 'removing from local and remote' % name
339 )
332 340 remove_local(name)
333 341 remove_remote('%s%s' % (prefix, name))
334 342 del local_existing[name]
@@ -356,15 +364,18 b' def ensure_key_pairs(state_path: pathlib'
356 364 subprocess.run(
357 365 ['ssh-keygen', '-y', '-f', str(priv_full)],
358 366 stdout=fh,
359 check=True)
367 check=True,
368 )
360 369
361 370 pub_full.chmod(0o0600)
362 371
363 372
364 373 def delete_instance_profile(profile):
365 374 for role in profile.roles:
366 print('removing role %s from instance profile %s' % (role.name,
367 profile.name))
375 print(
376 'removing role %s from instance profile %s'
377 % (role.name, profile.name)
378 )
368 379 profile.remove_role(RoleName=role.name)
369 380
370 381 print('deleting instance profile %s' % profile.name)
@@ -404,7 +415,8 b' def ensure_iam_state(iamclient, iamresou'
404 415 print('creating IAM instance profile %s' % actual)
405 416
406 417 profile = iamresource.create_instance_profile(
407 InstanceProfileName=actual)
418 InstanceProfileName=actual
419 )
408 420 remote_profiles[name] = profile
409 421
410 422 waiter = iamclient.get_waiter('instance_profile_exists')
@@ -453,23 +465,12 b' def find_image(ec2resource, owner_id, na'
453 465
454 466 images = ec2resource.images.filter(
455 467 Filters=[
456 {
457 'Name': 'owner-id',
458 'Values': [owner_id],
459 },
460 {
461 'Name': 'state',
462 'Values': ['available'],
463 },
464 {
465 'Name': 'image-type',
466 'Values': ['machine'],
467 },
468 {
469 'Name': 'name',
470 'Values': [name],
471 },
472 ])
468 {'Name': 'owner-id', 'Values': [owner_id],},
469 {'Name': 'state', 'Values': ['available'],},
470 {'Name': 'image-type', 'Values': ['machine'],},
471 {'Name': 'name', 'Values': [name],},
472 ]
473 )
473 474
474 475 for image in images:
475 476 return image
@@ -507,13 +508,10 b' def ensure_security_groups(ec2resource, '
507 508 print('adding security group %s' % actual)
508 509
509 510 group_res = ec2resource.create_security_group(
510 Description=group['description'],
511 GroupName=actual,
511 Description=group['description'], GroupName=actual,
512 512 )
513 513
514 group_res.authorize_ingress(
515 IpPermissions=group['ingress'],
516 )
514 group_res.authorize_ingress(IpPermissions=group['ingress'],)
517 515
518 516 security_groups[name] = group_res
519 517
@@ -577,8 +575,10 b' def wait_for_ip_addresses(instances):'
577 575 instance.reload()
578 576 continue
579 577
580 print('public IP address for %s: %s' % (
581 instance.id, instance.public_ip_address))
578 print(
579 'public IP address for %s: %s'
580 % (instance.id, instance.public_ip_address)
581 )
582 582 break
583 583
584 584
@@ -603,10 +603,7 b' def wait_for_ssm(ssmclient, instances):'
603 603 while True:
604 604 res = ssmclient.describe_instance_information(
605 605 Filters=[
606 {
607 'Key': 'InstanceIds',
608 'Values': [i.id for i in instances],
609 },
606 {'Key': 'InstanceIds', 'Values': [i.id for i in instances],},
610 607 ],
611 608 )
612 609
@@ -628,9 +625,7 b' def run_ssm_command(ssmclient, instances'
628 625 InstanceIds=[i.id for i in instances],
629 626 DocumentName=document_name,
630 627 Parameters=parameters,
631 CloudWatchOutputConfig={
632 'CloudWatchOutputEnabled': True,
633 },
628 CloudWatchOutputConfig={'CloudWatchOutputEnabled': True,},
634 629 )
635 630
636 631 command_id = res['Command']['CommandId']
@@ -639,8 +634,7 b' def run_ssm_command(ssmclient, instances'
639 634 while True:
640 635 try:
641 636 res = ssmclient.get_command_invocation(
642 CommandId=command_id,
643 InstanceId=instance.id,
637 CommandId=command_id, InstanceId=instance.id,
644 638 )
645 639 except botocore.exceptions.ClientError as e:
646 640 if e.response['Error']['Code'] == 'InvocationDoesNotExist':
@@ -655,8 +649,9 b' def run_ssm_command(ssmclient, instances'
655 649 elif res['Status'] in ('Pending', 'InProgress', 'Delayed'):
656 650 time.sleep(2)
657 651 else:
658 raise Exception('command failed on %s: %s' % (
659 instance.id, res['Status']))
652 raise Exception(
653 'command failed on %s: %s' % (instance.id, res['Status'])
654 )
660 655
661 656
662 657 @contextlib.contextmanager
@@ -711,10 +706,12 b' def create_temp_windows_ec2_instances(c:'
711 706 config['IamInstanceProfile'] = {
712 707 'Name': 'hg-ephemeral-ec2-1',
713 708 }
714 config.setdefault('TagSpecifications', []).append({
709 config.setdefault('TagSpecifications', []).append(
710 {
715 711 'ResourceType': 'instance',
716 712 'Tags': [{'Key': 'Name', 'Value': 'hg-temp-windows'}],
717 })
713 }
714 )
718 715 config['UserData'] = WINDOWS_USER_DATA % password
719 716
720 717 with temporary_ec2_instances(c.ec2resource, config) as instances:
@@ -723,7 +720,9 b' def create_temp_windows_ec2_instances(c:'
723 720 print('waiting for Windows Remote Management service...')
724 721
725 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 726 print('established WinRM connection to %s' % instance.id)
728 727 instance.winrm_client = client
729 728
@@ -748,14 +747,17 b' def find_and_reconcile_image(ec2resource'
748 747 # Store a reference to a good image so it can be returned one the
749 748 # image state is reconciled.
750 749 images = ec2resource.images.filter(
751 Filters=[{'Name': 'name', 'Values': [name]}])
750 Filters=[{'Name': 'name', 'Values': [name]}]
751 )
752 752
753 753 existing_image = None
754 754
755 755 for image in images:
756 756 if image.tags is None:
757 print('image %s for %s lacks required tags; removing' % (
758 image.id, image.name))
757 print(
758 'image %s for %s lacks required tags; removing'
759 % (image.id, image.name)
760 )
759 761 remove_ami(ec2resource, image)
760 762 else:
761 763 tags = {t['Key']: t['Value'] for t in image.tags}
@@ -763,15 +765,18 b' def find_and_reconcile_image(ec2resource'
763 765 if tags.get('HGIMAGEFINGERPRINT') == fingerprint:
764 766 existing_image = image
765 767 else:
766 print('image %s for %s has wrong fingerprint; removing' % (
767 image.id, image.name))
768 print(
769 'image %s for %s has wrong fingerprint; removing'
770 % (image.id, image.name)
771 )
768 772 remove_ami(ec2resource, image)
769 773
770 774 return existing_image
771 775
772 776
773 def create_ami_from_instance(ec2client, instance, name, description,
774 fingerprint):
777 def create_ami_from_instance(
778 ec2client, instance, name, description, fingerprint
779 ):
775 780 """Create an AMI from a running instance.
776 781
777 782 Returns the ``ec2resource.Image`` representing the created AMI.
@@ -779,29 +784,19 b' def create_ami_from_instance(ec2client, '
779 784 instance.stop()
780 785
781 786 ec2client.get_waiter('instance_stopped').wait(
782 InstanceIds=[instance.id],
783 WaiterConfig={
784 'Delay': 5,
785 })
787 InstanceIds=[instance.id], WaiterConfig={'Delay': 5,}
788 )
786 789 print('%s is stopped' % instance.id)
787 790
788 image = instance.create_image(
789 Name=name,
790 Description=description,
791 )
791 image = instance.create_image(Name=name, Description=description,)
792 792
793 image.create_tags(Tags=[
794 {
795 'Key': 'HGIMAGEFINGERPRINT',
796 'Value': fingerprint,
797 },
798 ])
793 image.create_tags(
794 Tags=[{'Key': 'HGIMAGEFINGERPRINT', 'Value': fingerprint,},]
795 )
799 796
800 797 print('waiting for image %s' % image.id)
801 798
802 ec2client.get_waiter('image_available').wait(
803 ImageIds=[image.id],
804 )
799 ec2client.get_waiter('image_available').wait(ImageIds=[image.id],)
805 800
806 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 822 ssh_username = 'admin'
828 823 elif distro == 'debian10':
829 824 image = find_image(
830 ec2resource,
831 DEBIAN_ACCOUNT_ID_2,
832 'debian-10-amd64-20190909-10',
825 ec2resource, DEBIAN_ACCOUNT_ID_2, 'debian-10-amd64-20190909-10',
833 826 )
834 827 ssh_username = 'admin'
835 828 elif distro == 'ubuntu18.04':
@@ -871,10 +864,12 b' def ensure_linux_dev_ami(c: AWSConnectio'
871 864 'SecurityGroupIds': [c.security_groups['linux-dev-1'].id],
872 865 }
873 866
874 requirements2_path = (pathlib.Path(__file__).parent.parent /
875 'linux-requirements-py2.txt')
876 requirements3_path = (pathlib.Path(__file__).parent.parent /
877 'linux-requirements-py3.txt')
867 requirements2_path = (
868 pathlib.Path(__file__).parent.parent / 'linux-requirements-py2.txt'
869 )
870 requirements3_path = (
871 pathlib.Path(__file__).parent.parent / 'linux-requirements-py3.txt'
872 )
878 873 with requirements2_path.open('r', encoding='utf-8') as fh:
879 874 requirements2 = fh.read()
880 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 878 # Compute a deterministic fingerprint to determine whether image needs to
884 879 # be regenerated.
885 fingerprint = resolve_fingerprint({
880 fingerprint = resolve_fingerprint(
881 {
886 882 'instance_config': config,
887 883 'bootstrap_script': BOOTSTRAP_DEBIAN,
888 884 'requirements_py2': requirements2,
889 885 'requirements_py3': requirements3,
890 })
886 }
887 )
891 888
892 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 899 instance = instances[0]
903 900
904 901 client = wait_for_ssh(
905 instance.public_ip_address, 22,
902 instance.public_ip_address,
903 22,
906 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 908 home = '/home/%s' % ssh_username
910 909
@@ -926,8 +925,9 b' def ensure_linux_dev_ami(c: AWSConnectio'
926 925 fh.chmod(0o0700)
927 926
928 927 print('executing bootstrap')
929 chan, stdin, stdout = ssh_exec_command(client,
930 '%s/bootstrap' % home)
928 chan, stdin, stdout = ssh_exec_command(
929 client, '%s/bootstrap' % home
930 )
931 931 stdin.close()
932 932
933 933 for line in stdout:
@@ -937,17 +937,28 b' def ensure_linux_dev_ami(c: AWSConnectio'
937 937 if res:
938 938 raise Exception('non-0 exit from bootstrap: %d' % res)
939 939
940 print('bootstrap completed; stopping %s to create %s' % (
941 instance.id, name))
940 print(
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(
946 ec2client,
947 instance,
948 name,
944 949 'Mercurial Linux development environment',
945 fingerprint)
950 fingerprint,
951 )
946 952
947 953
948 954 @contextlib.contextmanager
949 def temporary_linux_dev_instances(c: AWSConnection, image, instance_type,
950 prefix='hg-', ensure_extra_volume=False):
955 def temporary_linux_dev_instances(
956 c: AWSConnection,
957 image,
958 instance_type,
959 prefix='hg-',
960 ensure_extra_volume=False,
961 ):
951 962 """Create temporary Linux development EC2 instances.
952 963
953 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 991 # This is not an exhaustive list of instance types having instance storage.
981 992 # But
982 if (ensure_extra_volume
983 and not instance_type.startswith(tuple(INSTANCE_TYPES_WITH_STORAGE))):
993 if ensure_extra_volume and not instance_type.startswith(
994 tuple(INSTANCE_TYPES_WITH_STORAGE)
995 ):
984 996 main_device = block_device_mappings[0]['DeviceName']
985 997
986 998 if main_device == 'xvda':
@@ -988,17 +1000,20 b' def temporary_linux_dev_instances(c: AWS'
988 1000 elif main_device == '/dev/sda1':
989 1001 second_device = '/dev/sdb'
990 1002 else:
991 raise ValueError('unhandled primary EBS device name: %s' %
992 main_device)
1003 raise ValueError(
1004 'unhandled primary EBS device name: %s' % main_device
1005 )
993 1006
994 block_device_mappings.append({
1007 block_device_mappings.append(
1008 {
995 1009 'DeviceName': second_device,
996 1010 'Ebs': {
997 1011 'DeleteOnTermination': True,
998 1012 'VolumeSize': 8,
999 1013 'VolumeType': 'gp2',
1014 },
1000 1015 }
1001 })
1016 )
1002 1017
1003 1018 config = {
1004 1019 'BlockDeviceMappings': block_device_mappings,
@@ -1019,9 +1034,11 b' def temporary_linux_dev_instances(c: AWS'
1019 1034
1020 1035 for instance in instances:
1021 1036 client = wait_for_ssh(
1022 instance.public_ip_address, 22,
1037 instance.public_ip_address,
1038 22,
1023 1039 username='hg',
1024 key_filename=ssh_private_key_path)
1040 key_filename=ssh_private_key_path,
1041 )
1025 1042
1026 1043 instance.ssh_client = client
1027 1044 instance.ssh_private_key_path = ssh_private_key_path
@@ -1033,8 +1050,9 b' def temporary_linux_dev_instances(c: AWS'
1033 1050 instance.ssh_client.close()
1034 1051
1035 1052
1036 def ensure_windows_dev_ami(c: AWSConnection, prefix='hg-',
1037 base_image_name=WINDOWS_BASE_IMAGE_NAME):
1053 def ensure_windows_dev_ami(
1054 c: AWSConnection, prefix='hg-', base_image_name=WINDOWS_BASE_IMAGE_NAME
1055 ):
1038 1056 """Ensure Windows Development AMI is available and up-to-date.
1039 1057
1040 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 1119 # Compute a deterministic fingerprint to determine whether image needs
1102 1120 # to be regenerated.
1103 fingerprint = resolve_fingerprint({
1121 fingerprint = resolve_fingerprint(
1122 {
1104 1123 'instance_config': config,
1105 1124 'user_data': WINDOWS_USER_DATA,
1106 1125 'initial_bootstrap': WINDOWS_BOOTSTRAP_POWERSHELL,
1107 1126 'bootstrap_commands': commands,
1108 1127 'base_image_name': base_image_name,
1109 })
1128 }
1129 )
1110 1130
1111 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 1151 ssmclient,
1132 1152 [instance],
1133 1153 'AWS-RunPowerShellScript',
1134 {
1135 'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'),
1136 },
1154 {'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'),},
1137 1155 )
1138 1156
1139 1157 # Reboot so all updates are fully applied.
@@ -1145,10 +1163,8 b' def ensure_windows_dev_ami(c: AWSConnect'
1145 1163 print('rebooting instance %s' % instance.id)
1146 1164 instance.stop()
1147 1165 ec2client.get_waiter('instance_stopped').wait(
1148 InstanceIds=[instance.id],
1149 WaiterConfig={
1150 'Delay': 5,
1151 })
1166 InstanceIds=[instance.id], WaiterConfig={'Delay': 5,}
1167 )
1152 1168
1153 1169 instance.start()
1154 1170 wait_for_ip_addresses([instance])
@@ -1159,8 +1175,11 b' def ensure_windows_dev_ami(c: AWSConnect'
1159 1175 # TODO figure out a workaround.
1160 1176
1161 1177 print('waiting for Windows Remote Management to come back...')
1162 client = wait_for_winrm(instance.public_ip_address, 'Administrator',
1163 c.automation.default_password())
1178 client = wait_for_winrm(
1179 instance.public_ip_address,
1180 'Administrator',
1181 c.automation.default_password(),
1182 )
1164 1183 print('established WinRM connection to %s' % instance.id)
1165 1184 instance.winrm_client = client
1166 1185
@@ -1168,14 +1187,23 b' def ensure_windows_dev_ami(c: AWSConnect'
1168 1187 run_powershell(instance.winrm_client, '\n'.join(commands))
1169 1188
1170 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(
1191 ec2client,
1192 instance,
1193 name,
1172 1194 'Mercurial Windows development environment',
1173 fingerprint)
1195 fingerprint,
1196 )
1174 1197
1175 1198
1176 1199 @contextlib.contextmanager
1177 def temporary_windows_dev_instances(c: AWSConnection, image, instance_type,
1178 prefix='hg-', disable_antivirus=False):
1200 def temporary_windows_dev_instances(
1201 c: AWSConnection,
1202 image,
1203 instance_type,
1204 prefix='hg-',
1205 disable_antivirus=False,
1206 ):
1179 1207 """Create a temporary Windows development EC2 instance.
1180 1208
1181 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 1233 for instance in instances:
1206 1234 run_powershell(
1207 1235 instance.winrm_client,
1208 'Set-MpPreference -DisableRealtimeMonitoring $true')
1236 'Set-MpPreference -DisableRealtimeMonitoring $true',
1237 )
1209 1238
1210 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 28 DIST_PATH = SOURCE_ROOT / 'dist'
27 29
28 30
29 def bootstrap_linux_dev(hga: HGAutomation, aws_region, distros=None,
30 parallel=False):
31 def bootstrap_linux_dev(
32 hga: HGAutomation, aws_region, distros=None, parallel=False
33 ):
31 34 c = hga.aws_connection(aws_region)
32 35
33 36 if distros:
@@ -59,8 +62,9 b' def bootstrap_windows_dev(hga: HGAutomat'
59 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,
63 base_image_name):
65 def build_inno(
66 hga: HGAutomation, aws_region, arch, revision, version, base_image_name
67 ):
64 68 c = hga.aws_connection(aws_region)
65 69 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
66 70 DIST_PATH.mkdir(exist_ok=True)
@@ -71,13 +75,14 b' def build_inno(hga: HGAutomation, aws_re'
71 75 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
72 76
73 77 for a in arch:
74 windows.build_inno_installer(instance.winrm_client, a,
75 DIST_PATH,
76 version=version)
78 windows.build_inno_installer(
79 instance.winrm_client, a, DIST_PATH, version=version
80 )
77 81
78 82
79 def build_wix(hga: HGAutomation, aws_region, arch, revision, version,
80 base_image_name):
83 def build_wix(
84 hga: HGAutomation, aws_region, arch, revision, version, base_image_name
85 ):
81 86 c = hga.aws_connection(aws_region)
82 87 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
83 88 DIST_PATH.mkdir(exist_ok=True)
@@ -88,12 +93,14 b' def build_wix(hga: HGAutomation, aws_reg'
88 93 windows.synchronize_hg(SOURCE_ROOT, revision, instance)
89 94
90 95 for a in arch:
91 windows.build_wix_installer(instance.winrm_client, a,
92 DIST_PATH, version=version)
96 windows.build_wix_installer(
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,
96 base_image_name):
101 def build_windows_wheel(
102 hga: HGAutomation, aws_region, arch, revision, base_image_name
103 ):
97 104 c = hga.aws_connection(aws_region)
98 105 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
99 106 DIST_PATH.mkdir(exist_ok=True)
@@ -107,8 +114,9 b' def build_windows_wheel(hga: HGAutomatio'
107 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,
111 version, base_image_name):
117 def build_all_windows_packages(
118 hga: HGAutomation, aws_region, revision, version, base_image_name
119 ):
112 120 c = hga.aws_connection(aws_region)
113 121 image = aws.ensure_windows_dev_ami(c, base_image_name=base_image_name)
114 122 DIST_PATH.mkdir(exist_ok=True)
@@ -124,11 +132,13 b' def build_all_windows_packages(hga: HGAu'
124 132 windows.purge_hg(winrm_client)
125 133 windows.build_wheel(winrm_client, arch, DIST_PATH)
126 134 windows.purge_hg(winrm_client)
127 windows.build_inno_installer(winrm_client, arch, DIST_PATH,
128 version=version)
135 windows.build_inno_installer(
136 winrm_client, arch, DIST_PATH, version=version
137 )
129 138 windows.purge_hg(winrm_client)
130 windows.build_wix_installer(winrm_client, arch, DIST_PATH,
131 version=version)
139 windows.build_wix_installer(
140 winrm_client, arch, DIST_PATH, version=version
141 )
132 142
133 143
134 144 def terminate_ec2_instances(hga: HGAutomation, aws_region):
@@ -141,8 +151,15 b' def purge_ec2_resources(hga: HGAutomatio'
141 151 aws.remove_resources(c)
142 152
143 153
144 def run_tests_linux(hga: HGAutomation, aws_region, instance_type,
145 python_version, test_flags, distro, filesystem):
154 def run_tests_linux(
155 hga: HGAutomation,
156 aws_region,
157 instance_type,
158 python_version,
159 test_flags,
160 distro,
161 filesystem,
162 ):
146 163 c = hga.aws_connection(aws_region)
147 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 168 ensure_extra_volume = filesystem not in ('default', 'tmpfs')
152 169
153 170 with aws.temporary_linux_dev_instances(
154 c, image, instance_type,
155 ensure_extra_volume=ensure_extra_volume) as insts:
171 c, image, instance_type, ensure_extra_volume=ensure_extra_volume
172 ) as insts:
156 173
157 174 instance = insts[0]
158 175
159 linux.prepare_exec_environment(instance.ssh_client,
160 filesystem=filesystem)
176 linux.prepare_exec_environment(
177 instance.ssh_client, filesystem=filesystem
178 )
161 179 linux.synchronize_hg(SOURCE_ROOT, instance, '.')
162 180 t_prepared = time.time()
163 linux.run_tests(instance.ssh_client, python_version,
164 test_flags)
181 linux.run_tests(instance.ssh_client, python_version, test_flags)
165 182 t_done = time.time()
166 183
167 184 t_setup = t_prepared - t_start
@@ -169,29 +186,48 b' def run_tests_linux(hga: HGAutomation, a'
169 186
170 187 print(
171 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,
176 python_version, arch, test_flags, base_image_name):
193 def run_tests_windows(
194 hga: HGAutomation,
195 aws_region,
196 instance_type,
197 python_version,
198 arch,
199 test_flags,
200 base_image_name,
201 ):
177 202 c = hga.aws_connection(aws_region)
178 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,
181 disable_antivirus=True) as insts:
205 with aws.temporary_windows_dev_instances(
206 c, image, instance_type, disable_antivirus=True
207 ) as insts:
182 208 instance = insts[0]
183 209
184 210 windows.synchronize_hg(SOURCE_ROOT, '.', instance)
185 windows.run_tests(instance.winrm_client, python_version, arch,
186 test_flags)
211 windows.run_tests(
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,
190 pypi: bool, mercurial_scm_org: bool,
191 ssh_username: str):
192 windows.publish_artifacts(DIST_PATH, version,
193 pypi=pypi, mercurial_scm_org=mercurial_scm_org,
194 ssh_username=ssh_username)
216 def publish_windows_artifacts(
217 hg: HGAutomation,
218 aws_region,
219 version: str,
220 pypi: bool,
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 233 def run_try(hga: HGAutomation, aws_region: str, rev: str):
@@ -208,25 +244,21 b' def get_parser():'
208 244 help='Path for local state files',
209 245 )
210 246 parser.add_argument(
211 '--aws-region',
212 help='AWS region to use',
213 default='us-west-2',
247 '--aws-region', help='AWS region to use', default='us-west-2',
214 248 )
215 249
216 250 subparsers = parser.add_subparsers()
217 251
218 252 sp = subparsers.add_parser(
219 'bootstrap-linux-dev',
220 help='Bootstrap Linux development environments',
253 'bootstrap-linux-dev', help='Bootstrap Linux development environments',
221 254 )
222 255 sp.add_argument(
223 '--distros',
224 help='Comma delimited list of distros to bootstrap',
256 '--distros', help='Comma delimited list of distros to bootstrap',
225 257 )
226 258 sp.add_argument(
227 259 '--parallel',
228 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 263 sp.set_defaults(func=bootstrap_linux_dev)
232 264
@@ -242,17 +274,13 b' def get_parser():'
242 274 sp.set_defaults(func=bootstrap_windows_dev)
243 275
244 276 sp = subparsers.add_parser(
245 'build-all-windows-packages',
246 help='Build all Windows packages',
277 'build-all-windows-packages', help='Build all Windows packages',
247 278 )
248 279 sp.add_argument(
249 '--revision',
250 help='Mercurial revision to build',
251 default='.',
280 '--revision', help='Mercurial revision to build', default='.',
252 281 )
253 282 sp.add_argument(
254 '--version',
255 help='Mercurial version string to use',
283 '--version', help='Mercurial version string to use',
256 284 )
257 285 sp.add_argument(
258 286 '--base-image-name',
@@ -262,8 +290,7 b' def get_parser():'
262 290 sp.set_defaults(func=build_all_windows_packages)
263 291
264 292 sp = subparsers.add_parser(
265 'build-inno',
266 help='Build Inno Setup installer(s)',
293 'build-inno', help='Build Inno Setup installer(s)',
267 294 )
268 295 sp.add_argument(
269 296 '--arch',
@@ -273,13 +300,10 b' def get_parser():'
273 300 default=['x64'],
274 301 )
275 302 sp.add_argument(
276 '--revision',
277 help='Mercurial revision to build',
278 default='.',
303 '--revision', help='Mercurial revision to build', default='.',
279 304 )
280 305 sp.add_argument(
281 '--version',
282 help='Mercurial version string to use in installer',
306 '--version', help='Mercurial version string to use in installer',
283 307 )
284 308 sp.add_argument(
285 309 '--base-image-name',
@@ -289,8 +313,7 b' def get_parser():'
289 313 sp.set_defaults(func=build_inno)
290 314
291 315 sp = subparsers.add_parser(
292 'build-windows-wheel',
293 help='Build Windows wheel(s)',
316 'build-windows-wheel', help='Build Windows wheel(s)',
294 317 )
295 318 sp.add_argument(
296 319 '--arch',
@@ -300,9 +323,7 b' def get_parser():'
300 323 default=['x64'],
301 324 )
302 325 sp.add_argument(
303 '--revision',
304 help='Mercurial revision to build',
305 default='.',
326 '--revision', help='Mercurial revision to build', default='.',
306 327 )
307 328 sp.add_argument(
308 329 '--base-image-name',
@@ -311,10 +332,7 b' def get_parser():'
311 332 )
312 333 sp.set_defaults(func=build_windows_wheel)
313 334
314 sp = subparsers.add_parser(
315 'build-wix',
316 help='Build WiX installer(s)'
317 )
335 sp = subparsers.add_parser('build-wix', help='Build WiX installer(s)')
318 336 sp.add_argument(
319 337 '--arch',
320 338 help='Architecture to build for',
@@ -323,13 +341,10 b' def get_parser():'
323 341 default=['x64'],
324 342 )
325 343 sp.add_argument(
326 '--revision',
327 help='Mercurial revision to build',
328 default='.',
344 '--revision', help='Mercurial revision to build', default='.',
329 345 )
330 346 sp.add_argument(
331 '--version',
332 help='Mercurial version string to use in installer',
347 '--version', help='Mercurial version string to use in installer',
333 348 )
334 349 sp.add_argument(
335 350 '--base-image-name',
@@ -345,15 +360,11 b' def get_parser():'
345 360 sp.set_defaults(func=terminate_ec2_instances)
346 361
347 362 sp = subparsers.add_parser(
348 'purge-ec2-resources',
349 help='Purge all EC2 resources managed by us',
363 'purge-ec2-resources', help='Purge all EC2 resources managed by us',
350 364 )
351 365 sp.set_defaults(func=purge_ec2_resources)
352 366
353 sp = subparsers.add_parser(
354 'run-tests-linux',
355 help='Run tests on Linux',
356 )
367 sp = subparsers.add_parser('run-tests-linux', help='Run tests on Linux',)
357 368 sp.add_argument(
358 369 '--distro',
359 370 help='Linux distribution to run tests on',
@@ -374,8 +385,18 b' def get_parser():'
374 385 sp.add_argument(
375 386 '--python-version',
376 387 help='Python version to use',
377 choices={'system2', 'system3', '2.7', '3.5', '3.6', '3.7', '3.8',
378 'pypy', 'pypy3.5', 'pypy3.6'},
388 choices={
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 400 default='system2',
380 401 )
381 402 sp.add_argument(
@@ -386,13 +407,10 b' def get_parser():'
386 407 sp.set_defaults(func=run_tests_linux)
387 408
388 409 sp = subparsers.add_parser(
389 'run-tests-windows',
390 help='Run tests on Windows',
410 'run-tests-windows', help='Run tests on Windows',
391 411 )
392 412 sp.add_argument(
393 '--instance-type',
394 help='EC2 instance type to use',
395 default='t3.medium',
413 '--instance-type', help='EC2 instance type to use', default='t3.medium',
396 414 )
397 415 sp.add_argument(
398 416 '--python-version',
@@ -407,8 +425,7 b' def get_parser():'
407 425 default='x64',
408 426 )
409 427 sp.add_argument(
410 '--test-flags',
411 help='Extra command line flags to pass to run-tests.py',
428 '--test-flags', help='Extra command line flags to pass to run-tests.py',
412 429 )
413 430 sp.add_argument(
414 431 '--base-image-name',
@@ -419,7 +436,7 b' def get_parser():'
419 436
420 437 sp = subparsers.add_parser(
421 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 441 sp.add_argument(
425 442 '--no-pypi',
@@ -436,22 +453,17 b' def get_parser():'
436 453 help='Skip uploading to www.mercurial-scm.org',
437 454 )
438 455 sp.add_argument(
439 '--ssh-username',
440 help='SSH username for mercurial-scm.org',
456 '--ssh-username', help='SSH username for mercurial-scm.org',
441 457 )
442 458 sp.add_argument(
443 'version',
444 help='Mercurial version string to locate local packages',
459 'version', help='Mercurial version string to locate local packages',
445 460 )
446 461 sp.set_defaults(func=publish_windows_artifacts)
447 462
448 463 sp = subparsers.add_parser(
449 'try',
450 help='Run CI automation against a custom changeset'
464 'try', help='Run CI automation against a custom changeset'
451 465 )
452 sp.add_argument('-r', '--rev',
453 default='.',
454 help='Revision to run CI on')
466 sp.add_argument('-r', '--rev', default='.', help='Revision to run CI on')
455 467 sp.set_defaults(func=run_try)
456 468
457 469 return parser
@@ -13,9 +13,7 b' import shlex'
13 13 import subprocess
14 14 import tempfile
15 15
16 from .ssh import (
17 exec_command,
18 )
16 from .ssh import exec_command
19 17
20 18
21 19 # Linux distributions that are supported.
@@ -62,7 +60,9 b' for v in ${PYENV3_VERSIONS}; do'
62 60 done
63 61
64 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 68 INSTALL_RUST = r'''
@@ -87,10 +87,13 b' wget -O ${HG_TARBALL} --progress dot:meg'
87 87 echo "${HG_SHA256} ${HG_TARBALL}" | sha256sum --check -
88 88
89 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 97 #!/bin/bash
95 98
96 99 set -ex
@@ -323,11 +326,14 b' publish = false'
323 326 EOF
324 327
325 328 sudo chown -R hg:hg /hgdev
326 '''.lstrip().format(
329 '''.lstrip()
330 .format(
327 331 install_rust=INSTALL_RUST,
328 332 install_pythons=INSTALL_PYTHONS,
329 bootstrap_virtualenv=BOOTSTRAP_VIRTUALENV
330 ).replace('\r\n', '\n')
333 bootstrap_virtualenv=BOOTSTRAP_VIRTUALENV,
334 )
335 .replace('\r\n', '\n')
336 )
331 337
332 338
333 339 # Prepares /hgdev for operations.
@@ -409,7 +415,9 b' mkdir /hgwork/tmp'
409 415 chown hg:hg /hgwork/tmp
410 416
411 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 423 HG_UPDATE_CLEAN = '''
@@ -421,7 +429,9 b' cd /hgwork/src'
421 429 ${HG} --config extensions.purge= purge --all
422 430 ${HG} update -C $1
423 431 ${HG} log -r .
424 '''.lstrip().replace('\r\n', '\n')
432 '''.lstrip().replace(
433 '\r\n', '\n'
434 )
425 435
426 436
427 437 def prepare_exec_environment(ssh_client, filesystem='default'):
@@ -456,11 +466,12 b' def prepare_exec_environment(ssh_client,'
456 466 res = chan.recv_exit_status()
457 467
458 468 if res:
459 raise Exception('non-0 exit code updating working directory; %d'
460 % res)
469 raise Exception('non-0 exit code updating working directory; %d' % 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 475 """Synchronize a local Mercurial source path to remote EC2 instance."""
465 476
466 477 with tempfile.TemporaryDirectory() as temp_dir:
@@ -482,8 +493,10 b' def synchronize_hg(source_path: pathlib.'
482 493 fh.write(' IdentityFile %s\n' % ec2_instance.ssh_private_key_path)
483 494
484 495 if not (source_path / '.hg').is_dir():
485 raise Exception('%s is not a Mercurial repository; synchronization '
486 'not yet supported' % source_path)
496 raise Exception(
497 '%s is not a Mercurial repository; synchronization '
498 'not yet supported' % source_path
499 )
487 500
488 501 env = dict(os.environ)
489 502 env['HGPLAIN'] = '1'
@@ -493,17 +506,29 b' def synchronize_hg(source_path: pathlib.'
493 506
494 507 res = subprocess.run(
495 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 515 full_revision = res.stdout.decode('ascii')
499 516
500 517 args = [
501 'python2.7', str(hg_bin),
502 '--config', 'ui.ssh=ssh -F %s' % ssh_config,
503 '--config', 'ui.remotecmd=/hgdev/venv-bootstrap/bin/hg',
518 'python2.7',
519 str(hg_bin),
520 '--config',
521 'ui.ssh=ssh -F %s' % ssh_config,
522 '--config',
523 'ui.remotecmd=/hgdev/venv-bootstrap/bin/hg',
504 524 # Also ensure .hgtags changes are present so auto version
505 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 532 'ssh://%s//hgwork/src' % public_ip,
508 533 ]
509 534
@@ -522,7 +547,8 b' def synchronize_hg(source_path: pathlib.'
522 547 fh.chmod(0o0700)
523 548
524 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 552 stdin.close()
527 553
528 554 for line in stdout:
@@ -531,8 +557,9 b' def synchronize_hg(source_path: pathlib.'
531 557 res = chan.recv_exit_status()
532 558
533 559 if res:
534 raise Exception('non-0 exit code updating working directory; %d'
535 % res)
560 raise Exception(
561 'non-0 exit code updating working directory; %d' % res
562 )
536 563
537 564
538 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 582 command = (
556 583 '/bin/sh -c "export TMPDIR=/hgwork/tmp; '
557 'cd /hgwork/src/tests && %s run-tests.py %s"' % (
558 python, test_flags))
584 'cd /hgwork/src/tests && %s run-tests.py %s"' % (python, test_flags)
585 )
559 586
560 587 chan, stdin, stdout = exec_command(ssh_client, command)
561 588
@@ -7,12 +7,8 b''
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 from twine.commands.upload import (
11 upload as twine_upload,
12 )
13 from twine.settings import (
14 Settings,
15 )
10 from twine.commands.upload import upload as twine_upload
11 from twine.settings import Settings
16 12
17 13
18 14 def upload(paths):
@@ -11,14 +11,13 b' import socket'
11 11 import time
12 12 import warnings
13 13
14 from cryptography.utils import (
15 CryptographyDeprecationWarning,
16 )
14 from cryptography.utils import CryptographyDeprecationWarning
17 15 import paramiko
18 16
19 17
20 18 def wait_for_ssh(hostname, port, timeout=60, username=None, key_filename=None):
21 19 """Wait for an SSH server to start on the specified host and port."""
20
22 21 class IgnoreHostKeyPolicy(paramiko.MissingHostKeyPolicy):
23 22 def missing_host_key(self, client, hostname, key):
24 23 return
@@ -28,17 +27,23 b' def wait_for_ssh(hostname, port, timeout'
28 27 # paramiko triggers a CryptographyDeprecationWarning in the cryptography
29 28 # package. Let's suppress
30 29 with warnings.catch_warnings():
31 warnings.filterwarnings('ignore',
32 category=CryptographyDeprecationWarning)
30 warnings.filterwarnings(
31 'ignore', category=CryptographyDeprecationWarning
32 )
33 33
34 34 while True:
35 35 client = paramiko.SSHClient()
36 36 client.set_missing_host_key_policy(IgnoreHostKeyPolicy())
37 37 try:
38 client.connect(hostname, port=port, username=username,
38 client.connect(
39 hostname,
40 port=port,
41 username=username,
39 42 key_filename=key_filename,
40 timeout=5.0, allow_agent=False,
41 look_for_keys=False)
43 timeout=5.0,
44 allow_agent=False,
45 look_for_keys=False,
46 )
42 47
43 48 return client
44 49 except socket.error:
@@ -15,12 +15,8 b' import re'
15 15 import subprocess
16 16 import tempfile
17 17
18 from .pypi import (
19 upload as pypi_upload,
20 )
21 from .winrm import (
22 run_powershell,
23 )
18 from .pypi import upload as pypi_upload
19 from .winrm import run_powershell
24 20
25 21
26 22 # PowerShell commands to activate a Visual Studio 2008 environment.
@@ -117,14 +113,21 b" MERCURIAL_SCM_BASE_URL = 'https://mercur"
117 113 X86_USER_AGENT_PATTERN = '.*Windows.*'
118 114 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
119 115
120 X86_EXE_DESCRIPTION = ('Mercurial {version} Inno Setup installer - x86 Windows '
121 '- does not require admin rights')
122 X64_EXE_DESCRIPTION = ('Mercurial {version} Inno Setup installer - x64 Windows '
123 '- does not require admin rights')
124 X86_MSI_DESCRIPTION = ('Mercurial {version} MSI installer - x86 Windows '
125 '- requires admin rights')
126 X64_MSI_DESCRIPTION = ('Mercurial {version} MSI installer - x64 Windows '
127 '- requires admin rights')
116 X86_EXE_DESCRIPTION = (
117 'Mercurial {version} Inno Setup installer - x86 Windows '
118 '- does not require admin rights'
119 )
120 X64_EXE_DESCRIPTION = (
121 'Mercurial {version} Inno Setup installer - x64 Windows '
122 '- does not require 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 132 def get_vc_prefix(arch):
130 133 if arch == 'x86':
@@ -158,10 +161,21 b' def synchronize_hg(hg_repo: pathlib.Path'
158 161 ssh_dir.chmod(0o0700)
159 162
160 163 # Generate SSH key to use for communication.
161 subprocess.run([
162 'ssh-keygen', '-t', 'rsa', '-b', '4096', '-N', '',
163 '-f', str(ssh_dir / 'id_rsa')],
164 check=True, capture_output=True)
164 subprocess.run(
165 [
166 'ssh-keygen',
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 180 # Add it to ~/.ssh/authorized_keys on remote.
167 181 # This assumes the file doesn't already exist.
@@ -182,8 +196,10 b' def synchronize_hg(hg_repo: pathlib.Path'
182 196 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
183 197
184 198 if not (hg_repo / '.hg').is_dir():
185 raise Exception('%s is not a Mercurial repository; '
186 'synchronization not yet supported' % hg_repo)
199 raise Exception(
200 '%s is not a Mercurial repository; '
201 'synchronization not yet supported' % hg_repo
202 )
187 203
188 204 env = dict(os.environ)
189 205 env['HGPLAIN'] = '1'
@@ -193,17 +209,29 b' def synchronize_hg(hg_repo: pathlib.Path'
193 209
194 210 res = subprocess.run(
195 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 218 full_revision = res.stdout.decode('ascii')
199 219
200 220 args = [
201 'python2.7', hg_bin,
202 '--config', 'ui.ssh=ssh -F %s' % ssh_config,
203 '--config', 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
221 'python2.7',
222 hg_bin,
223 '--config',
224 'ui.ssh=ssh -F %s' % ssh_config,
225 '--config',
226 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
204 227 # Also ensure .hgtags changes are present so auto version
205 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 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 241 if res.returncode not in (0, 1):
214 242 res.check_returncode()
215 243
216 run_powershell(winrm_client,
217 HG_UPDATE_CLEAN.format(revision=full_revision))
244 run_powershell(
245 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
246 )
218 247
219 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 279 winrm_client.fetch(source, str(dest))
251 280
252 281
253 def build_inno_installer(winrm_client, arch: str, dest_path: pathlib.Path,
254 version=None):
282 def build_inno_installer(
283 winrm_client, arch: str, dest_path: pathlib.Path, version=None
284 ):
255 285 """Build the Inno Setup installer on a remote machine.
256 286
257 287 Using a WinRM client, remote commands are executed to build
@@ -263,8 +293,9 b' def build_inno_installer(winrm_client, a'
263 293 if version:
264 294 extra_args.extend(['--version', version])
265 295
266 ps = get_vc_prefix(arch) + BUILD_INNO.format(arch=arch,
267 extra_args=' '.join(extra_args))
296 ps = get_vc_prefix(arch) + BUILD_INNO.format(
297 arch=arch, extra_args=' '.join(extra_args)
298 )
268 299 run_powershell(winrm_client, ps)
269 300 copy_latest_dist(winrm_client, '*.exe', dest_path)
270 301
@@ -281,8 +312,9 b' def build_wheel(winrm_client, arch: str,'
281 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,
285 version=None):
315 def build_wix_installer(
316 winrm_client, arch: str, dest_path: pathlib.Path, version=None
317 ):
286 318 """Build the WiX installer on a remote machine.
287 319
288 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 324 if version:
293 325 extra_args.extend(['--version', version])
294 326
295 ps = get_vc_prefix(arch) + BUILD_WIX.format(arch=arch,
296 extra_args=' '.join(extra_args))
327 ps = get_vc_prefix(arch) + BUILD_WIX.format(
328 arch=arch, extra_args=' '.join(extra_args)
329 )
297 330 run_powershell(winrm_client, ps)
298 331 copy_latest_dist(winrm_client, '*.msi', dest_path)
299 332
@@ -307,18 +340,16 b' def run_tests(winrm_client, python_versi'
307 340 ``run-tests.py``.
308 341 """
309 342 if not re.match(r'\d\.\d', python_version):
310 raise ValueError(r'python_version must be \d.\d; got %s' %
311 python_version)
343 raise ValueError(
344 r'python_version must be \d.\d; got %s' % python_version
345 )
312 346
313 347 if arch not in ('x86', 'x64'):
314 348 raise ValueError('arch must be x86 or x64; got %s' % arch)
315 349
316 350 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
317 351
318 ps = RUN_TESTS.format(
319 python_path=python_path,
320 test_flags=test_flags or '',
321 )
352 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
322 353
323 354 run_powershell(winrm_client, ps)
324 355
@@ -374,8 +405,8 b' def generate_latest_dat(version: str):'
374 405 version,
375 406 X64_USER_AGENT_PATTERN,
376 407 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
377 X64_MSI_DESCRIPTION.format(version=version)
378 )
408 X64_MSI_DESCRIPTION.format(version=version),
409 ),
379 410 )
380 411
381 412 lines = ['\t'.join(e) for e in entries]
@@ -396,8 +427,9 b' def publish_artifacts_pypi(dist_path: pa'
396 427 pypi_upload(wheel_paths)
397 428
398 429
399 def publish_artifacts_mercurial_scm_org(dist_path: pathlib.Path, version: str,
400 ssh_username=None):
430 def publish_artifacts_mercurial_scm_org(
431 dist_path: pathlib.Path, version: str, ssh_username=None
432 ):
401 433 """Publish Windows release artifacts to mercurial-scm.org."""
402 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 469 now = datetime.datetime.utcnow()
438 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 473 print('backing up %s to %s' % (latest_dat_path, backup_path))
441 474
442 475 with sftp.open(latest_dat_path, 'rb') as fh:
@@ -453,9 +486,13 b' def publish_artifacts_mercurial_scm_org('
453 486 fh.write(latest_dat_content.encode('ascii'))
454 487
455 488
456 def publish_artifacts(dist_path: pathlib.Path, version: str,
457 pypi=True, mercurial_scm_org=True,
458 ssh_username=None):
489 def publish_artifacts(
490 dist_path: pathlib.Path,
491 version: str,
492 pypi=True,
493 mercurial_scm_org=True,
494 ssh_username=None,
495 ):
459 496 """Publish Windows release artifacts.
460 497
461 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 505 publish_artifacts_pypi(dist_path, version)
469 506
470 507 if mercurial_scm_org:
471 publish_artifacts_mercurial_scm_org(dist_path, version,
472 ssh_username=ssh_username)
508 publish_artifacts_mercurial_scm_org(
509 dist_path, version, ssh_username=ssh_username
510 )
@@ -11,9 +11,7 b' import logging'
11 11 import pprint
12 12 import time
13 13
14 from pypsrp.client import (
15 Client,
16 )
14 from pypsrp.client import Client
17 15 from pypsrp.powershell import (
18 16 PowerShell,
19 17 PSInvocationState,
@@ -35,8 +33,13 b' def wait_for_winrm(host, username, passw'
35 33
36 34 while True:
37 35 try:
38 client = Client(host, username=username, password=password,
39 ssl=ssl, connection_timeout=5)
36 client = Client(
37 host,
38 username=username,
39 password=password,
40 ssl=ssl,
41 connection_timeout=5,
42 )
40 43 client.execute_ps("Write-Host 'Hello, World!'")
41 44 return client
42 45 except requests.exceptions.ConnectionError:
@@ -78,5 +81,7 b' def run_powershell(client, script):'
78 81 print(format_object(o))
79 82
80 83 if ps.state == PSInvocationState.FAILED:
81 raise Exception('PowerShell execution failed: %s' %
82 ' '.join(map(format_object, ps.streams.error)))
84 raise Exception(
85 'PowerShell execution failed: %s'
86 % ' '.join(map(format_object, ps.streams.error))
87 )
@@ -9,15 +9,20 b' from mercurial import ('
9 9 pycompat,
10 10 )
11 11
12
12 13 def reducetest(a, b):
13 14 tries = 0
14 15 reductions = 0
15 16 print("reducing...")
16 17 while tries < 1000:
17 a2 = "\n".join(l for l in a.splitlines()
18 if random.randint(0, 100) > 0) + "\n"
19 b2 = "\n".join(l for l in b.splitlines()
20 if random.randint(0, 100) > 0) + "\n"
18 a2 = (
19 "\n".join(l for l in a.splitlines() if random.randint(0, 100) > 0)
20 + "\n"
21 )
22 b2 = (
23 "\n".join(l for l in b.splitlines() if random.randint(0, 100) > 0)
24 + "\n"
25 )
21 26 if a2 == a and b2 == b:
22 27 continue
23 28 if a2 == b2:
@@ -32,8 +37,7 b' def reducetest(a, b):'
32 37 a = a2
33 38 b = b2
34 39
35 print("reduced:", reductions, len(a) + len(b),
36 repr(a), repr(b))
40 print("reduced:", reductions, len(a) + len(b), repr(a), repr(b))
37 41 try:
38 42 test1(a, b)
39 43 except Exception as inst:
@@ -41,6 +45,7 b' def reducetest(a, b):'
41 45
42 46 sys.exit(0)
43 47
48
44 49 def test1(a, b):
45 50 d = mdiff.textdiff(a, b)
46 51 if not d:
@@ -49,6 +54,7 b' def test1(a, b):'
49 54 if c != b:
50 55 raise ValueError("bad")
51 56
57
52 58 def testwrap(a, b):
53 59 try:
54 60 test1(a, b)
@@ -57,10 +63,12 b' def testwrap(a, b):'
57 63 print("exception:", inst)
58 64 reducetest(a, b)
59 65
66
60 67 def test(a, b):
61 68 testwrap(a, b)
62 69 testwrap(b, a)
63 70
71
64 72 def rndtest(size, noise):
65 73 a = []
66 74 src = " aaaaaaaabbbbccd"
@@ -82,6 +90,7 b' def rndtest(size, noise):'
82 90
83 91 test(a, b)
84 92
93
85 94 maxvol = 10000
86 95 startsize = 2
87 96 while True:
@@ -44,15 +44,24 b' from mercurial import ('
44 44 util,
45 45 )
46 46
47 basedir = os.path.abspath(os.path.join(os.path.dirname(__file__),
48 os.path.pardir, os.path.pardir))
47 basedir = os.path.abspath(
48 os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir)
49 )
49 50 reposdir = os.environ['REPOS_DIR']
50 reposnames = [name for name in os.listdir(reposdir)
51 if os.path.isdir(os.path.join(reposdir, name, ".hg"))]
51 reposnames = [
52 name
53 for name in os.listdir(reposdir)
54 if os.path.isdir(os.path.join(reposdir, name, ".hg"))
55 ]
52 56 if not reposnames:
53 57 raise ValueError("No repositories found in $REPO_DIR")
54 outputre = re.compile((r'! wall (\d+.\d+) comb \d+.\d+ user \d+.\d+ sys '
55 r'\d+.\d+ \(best of \d+\)'))
58 outputre = re.compile(
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 66 def runperfcommand(reponame, command, *args, **kwargs):
58 67 os.environ["HGRCPATH"] = os.environ.get("ASVHGRCPATH", "")
@@ -63,8 +72,9 b' def runperfcommand(reponame, command, *a'
63 72 else:
64 73 ui = uimod.ui()
65 74 repo = hg.repository(ui, os.path.join(reposdir, reponame))
66 perfext = extensions.load(ui, 'perfext',
67 os.path.join(basedir, 'contrib', 'perf.py'))
75 perfext = extensions.load(
76 ui, 'perfext', os.path.join(basedir, 'contrib', 'perf.py')
77 )
68 78 cmd = getattr(perfext, command)
69 79 ui.pushbuffer()
70 80 cmd(ui, repo, *args, **kwargs)
@@ -74,6 +84,7 b' def runperfcommand(reponame, command, *a'
74 84 raise ValueError("Invalid output {0}".format(output))
75 85 return float(match.group(1))
76 86
87
77 88 def perfbench(repos=reposnames, name=None, params=None):
78 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 115 def wrapped(repo, *args):
105 116 def perf(command, *a, **kw):
106 117 return runperfcommand(repo, command, *a, **kw)
118
107 119 return func(perf, *args)
108 120
109 121 wrapped.params = [p[1] for p in params]
110 122 wrapped.param_names = [p[0] for p in params]
111 123 wrapped.pretty_name = name
112 124 return wrapped
125
113 126 return decorator
@@ -9,18 +9,22 b' from __future__ import absolute_import'
9 9
10 10 from . import perfbench
11 11
12
12 13 @perfbench()
13 14 def track_tags(perf):
14 15 return perf("perftags")
15 16
17
16 18 @perfbench()
17 19 def track_status(perf):
18 20 return perf("perfstatus", unknown=False)
19 21
22
20 23 @perfbench(params=[('rev', ['1000', '10000', 'tip'])])
21 24 def track_manifest(perf, rev):
22 25 return perf("perfmanifest", rev)
23 26
27
24 28 @perfbench()
25 29 def track_heads(perf):
26 30 return perf("perfheads")
@@ -18,15 +18,16 b' import sys'
18 18
19 19 from . import basedir, perfbench
20 20
21
21 22 def createrevsetbenchmark(baseset, variants=None):
22 23 if variants is None:
23 24 # Default variants
24 variants = ["plain", "first", "last", "sort", "sort+first",
25 "sort+last"]
26 fname = "track_" + "_".join("".join([
27 c if c in string.digits + string.letters else " "
28 for c in baseset
29 ]).split())
25 variants = ["plain", "first", "last", "sort", "sort+first", "sort+last"]
26 fname = "track_" + "_".join(
27 "".join(
28 [c if c in string.digits + string.letters else " " for c in baseset]
29 ).split()
30 )
30 31
31 32 def wrap(fname, baseset):
32 33 @perfbench(name=baseset, params=[("variant", variants)])
@@ -36,18 +37,21 b' def createrevsetbenchmark(baseset, varia'
36 37 for var in variant.split("+"):
37 38 revset = "%s(%s)" % (var, revset)
38 39 return perf("perfrevset", revset)
40
39 41 f.__name__ = fname
40 42 return f
43
41 44 return wrap(fname, baseset)
42 45
46
43 47 def initializerevsetbenchmarks():
44 48 mod = sys.modules[__name__]
45 with open(os.path.join(basedir, 'contrib', 'base-revsets.txt'),
46 'rb') as fh:
49 with open(os.path.join(basedir, 'contrib', 'base-revsets.txt'), 'rb') as fh:
47 50 for line in fh:
48 51 baseset = line.strip()
49 52 if baseset and not baseset.startswith('#'):
50 53 func = createrevsetbenchmark(baseset)
51 54 setattr(mod, func.__name__, func)
52 55
56
53 57 initializerevsetbenchmarks()
@@ -18,10 +18,13 b' import tempfile'
18 18 import token
19 19 import tokenize
20 20
21
21 22 def adjusttokenpos(t, ofs):
22 23 """Adjust start/end column of the given token"""
23 return t._replace(start=(t.start[0], t.start[1] + ofs),
24 end=(t.end[0], t.end[1] + ofs))
24 return t._replace(
25 start=(t.start[0], t.start[1] + ofs), end=(t.end[0], t.end[1] + ofs)
26 )
27
25 28
26 29 def replacetokens(tokens, opts):
27 30 """Transform a stream of tokens from raw to Python 3.
@@ -82,9 +85,8 b' def replacetokens(tokens, opts):'
82 85 currtoken = tokens[k]
83 86 while currtoken.type in (token.STRING, token.NEWLINE, tokenize.NL):
84 87 k += 1
85 if (
86 currtoken.type == token.STRING
87 and currtoken.string.startswith(("'", '"'))
88 if currtoken.type == token.STRING and currtoken.string.startswith(
89 ("'", '"')
88 90 ):
89 91 sysstrtokens.add(currtoken)
90 92 try:
@@ -135,9 +137,9 b' def replacetokens(tokens, opts):'
135 137 lastparen = parens[-1]
136 138 if t.start[1] == lastparen[1]:
137 139 coloffset = lastparen[2]
138 elif (
139 t.start[1] + 1 == lastparen[1]
140 and lastparen[3] not in (token.NEWLINE, tokenize.NL)
140 elif t.start[1] + 1 == lastparen[1] and lastparen[3] not in (
141 token.NEWLINE,
142 tokenize.NL,
141 143 ):
142 144 # fix misaligned indent of s/util.Abort/error.Abort/
143 145 coloffset = lastparen[2] + (lastparen[1] - t.start[1])
@@ -202,8 +204,7 b' def replacetokens(tokens, opts):'
202 204 continue
203 205
204 206 # String literal. Prefix to make a b'' string.
205 yield adjusttokenpos(t._replace(string='b%s' % t.string),
206 coloffset)
207 yield adjusttokenpos(t._replace(string='b%s' % t.string), coloffset)
207 208 coldelta += 1
208 209 continue
209 210
@@ -213,8 +214,13 b' def replacetokens(tokens, opts):'
213 214
214 215 # *attr() builtins don't accept byte strings to 2nd argument.
215 216 if fn in (
216 'getattr', 'setattr', 'hasattr', 'safehasattr', 'wrapfunction',
217 'wrapclass', 'addattr'
217 'getattr',
218 'setattr',
219 'hasattr',
220 'safehasattr',
221 'wrapfunction',
222 'wrapclass',
223 'addattr',
218 224 ) and (opts['allow-attr-methods'] or not _isop(i - 1, '.')):
219 225 arg1idx = _findargnofcall(1)
220 226 if arg1idx is not None:
@@ -241,18 +247,23 b' def replacetokens(tokens, opts):'
241 247 _ensuresysstr(i + 4)
242 248
243 249 # Looks like "if __name__ == '__main__'".
244 if (t.type == token.NAME and t.string == '__name__'
245 and _isop(i + 1, '==')):
250 if (
251 t.type == token.NAME
252 and t.string == '__name__'
253 and _isop(i + 1, '==')
254 ):
246 255 _ensuresysstr(i + 2)
247 256
248 257 # Emit unmodified token.
249 258 yield adjusttokenpos(t, coloffset)
250 259
260
251 261 def process(fin, fout, opts):
252 262 tokens = tokenize.tokenize(fin.readline)
253 263 tokens = replacetokens(list(tokens), opts)
254 264 fout.write(tokenize.untokenize(tokens))
255 265
266
256 267 def tryunlink(fname):
257 268 try:
258 269 os.unlink(fname)
@@ -260,12 +271,14 b' def tryunlink(fname):'
260 271 if err.errno != errno.ENOENT:
261 272 raise
262 273
274
263 275 @contextlib.contextmanager
264 276 def editinplace(fname):
265 277 n = os.path.basename(fname)
266 278 d = os.path.dirname(fname)
267 fp = tempfile.NamedTemporaryFile(prefix='.%s-' % n, suffix='~', dir=d,
268 delete=False)
279 fp = tempfile.NamedTemporaryFile(
280 prefix='.%s-' % n, suffix='~', dir=d, delete=False
281 )
269 282 try:
270 283 yield fp
271 284 fp.close()
@@ -276,19 +289,37 b' def editinplace(fname):'
276 289 fp.close()
277 290 tryunlink(fp.name)
278 291
292
279 293 def main():
280 294 ap = argparse.ArgumentParser()
281 ap.add_argument('--version', action='version',
282 version='Byteify strings 1.0')
283 ap.add_argument('-i', '--inplace', action='store_true', default=False,
284 help='edit files in place')
285 ap.add_argument('--dictiter', action='store_true', default=False,
286 help='rewrite iteritems() and itervalues()'),
287 ap.add_argument('--allow-attr-methods', action='store_true',
295 ap.add_argument(
296 '--version', action='version', version='Byteify strings 1.0'
297 )
298 ap.add_argument(
299 '-i',
300 '--inplace',
301 action='store_true',
302 default=False,
303 help='edit files in place',
304 )
305 ap.add_argument(
306 '--dictiter',
307 action='store_true',
288 308 default=False,
289 help='also handle attr*() when they are methods'),
290 ap.add_argument('--treat-as-kwargs', nargs="+", default=[],
291 help="ignore kwargs-like objects"),
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 323 ap.add_argument('files', metavar='FILE', nargs='+', help='source file')
293 324 args = ap.parse_args()
294 325 opts = {
@@ -306,6 +337,7 b' def main():'
306 337 fout = sys.stdout.buffer
307 338 process(fin, fout, opts)
308 339
340
309 341 if __name__ == '__main__':
310 342 if sys.version_info.major < 3:
311 343 print('This script must be run under Python 3.')
@@ -1,12 +1,12 b''
1 1 from __future__ import absolute_import
2 2 import __builtin__
3 3 import os
4 from mercurial import (
5 util,
6 )
4 from mercurial import util
5
7 6
8 7 def lowerwrap(scope, funcname):
9 8 f = getattr(scope, funcname)
9
10 10 def wrap(fname, *args, **kwargs):
11 11 d, base = os.path.split(fname)
12 12 try:
@@ -19,11 +19,14 b' def lowerwrap(scope, funcname):'
19 19 if fn.lower() == base.lower():
20 20 return f(os.path.join(d, fn), *args, **kwargs)
21 21 return f(fname, *args, **kwargs)
22
22 23 scope.__dict__[funcname] = wrap
23 24
25
24 26 def normcase(path):
25 27 return path.lower()
26 28
29
27 30 os.path.normcase = normcase
28 31
29 32 for f in 'file open'.split():
@@ -53,15 +53,28 b' import timeit'
53 53 # Python version and OS
54 54 timer = timeit.default_timer
55 55
56
56 57 def main():
57 58 parser = argparse.ArgumentParser()
58 parser.add_argument('pipe', type=str, nargs=1,
59 help='Path of named pipe to create and listen on.')
60 parser.add_argument('output', default='trace.json', type=str, nargs='?',
61 help='Path of json file to create where the traces '
62 'will be stored.')
63 parser.add_argument('--debug', default=False, action='store_true',
64 help='Print useful debug messages')
59 parser.add_argument(
60 'pipe',
61 type=str,
62 nargs=1,
63 help='Path of named pipe to create and listen on.',
64 )
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 78 args = parser.parse_args()
66 79 fn = args.pipe[0]
67 80 os.mkfifo(fn)
@@ -86,7 +99,8 b' def main():'
86 99 payload_args = {}
87 100 pid = _threadmap[session]
88 101 ts_micros = (now - start) * 1000000
89 out.write(json.dumps(
102 out.write(
103 json.dumps(
90 104 {
91 105 "name": label,
92 106 "cat": "misc",
@@ -95,10 +109,13 b' def main():'
95 109 "pid": pid,
96 110 "tid": 1,
97 111 "args": payload_args,
98 }))
112 }
113 )
114 )
99 115 out.write(',\n')
100 116 finally:
101 117 os.unlink(fn)
102 118
119
103 120 if __name__ == '__main__':
104 121 main()
This diff has been collapsed as it changes many lines, (616 lines changed) Show them Hide them
@@ -26,11 +26,15 b' import optparse'
26 26 import os
27 27 import re
28 28 import sys
29
29 30 if sys.version_info[0] < 3:
30 31 opentext = open
31 32 else:
33
32 34 def opentext(f):
33 35 return open(f, encoding='latin1')
36
37
34 38 try:
35 39 xrange
36 40 except NameError:
@@ -42,6 +46,7 b' except ImportError:'
42 46
43 47 import testparseutil
44 48
49
45 50 def compilere(pat, multiline=False):
46 51 if multiline:
47 52 pat = '(?m)' + pat
@@ -52,10 +57,22 b' def compilere(pat, multiline=False):'
52 57 pass
53 58 return re.compile(pat)
54 59
60
55 61 # check "rules depending on implementation of repquote()" in each
56 62 # patterns (especially pypats), before changing around repquote()
57 _repquotefixedmap = {' ': ' ', '\n': '\n', '.': 'p', ':': 'q',
58 '%': '%', '\\': 'b', '*': 'A', '+': 'P', '-': 'M'}
63 _repquotefixedmap = {
64 ' ': ' ',
65 '\n': '\n',
66 '.': 'p',
67 ':': 'q',
68 '%': '%',
69 '\\': 'b',
70 '*': 'A',
71 '+': 'P',
72 '-': 'M',
73 }
74
75
59 76 def _repquoteencodechr(i):
60 77 if i > 255:
61 78 return 'u'
@@ -67,13 +84,17 b' def _repquoteencodechr(i):'
67 84 if c.isdigit():
68 85 return 'n'
69 86 return 'o'
87
88
70 89 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
71 90
91
72 92 def repquote(m):
73 93 t = m.group('text')
74 94 t = t.translate(_repquotett)
75 95 return m.group('quote') + t + m.group('quote')
76 96
97
77 98 def reppython(m):
78 99 comment = m.group('comment')
79 100 if comment:
@@ -81,20 +102,25 b' def reppython(m):'
81 102 return "#" * l + comment[l:]
82 103 return repquote(m)
83 104
105
84 106 def repcomment(m):
85 107 return m.group(1) + "#" * len(m.group(2))
86 108
109
87 110 def repccomment(m):
88 111 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
89 112 return m.group(1) + t + "*/"
90 113
114
91 115 def repcallspaces(m):
92 116 t = re.sub(r"\n\s+", "\n", m.group(2))
93 117 return m.group(1) + t
94 118
119
95 120 def repinclude(m):
96 121 return m.group(1) + "<foo>"
97 122
123
98 124 def rephere(m):
99 125 t = re.sub(r"\S", "x", m.group(2))
100 126 return m.group(1) + t
@@ -117,9 +143,14 b' testpats = ['
117 143 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
118 144 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
119 145 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
120 (r'\[[^\]]+==', '[ foo == bar ] is a bashism, use [ foo = bar ] instead'),
121 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
122 "use egrep for extended grep syntax"),
146 (
147 r'\[[^\]]+==',
148 '[ foo == bar ] is a bashism, use [ foo = bar ] instead',
149 ),
150 (
151 r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
152 "use egrep for extended grep syntax",
153 ),
123 154 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
124 155 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
125 156 (r'#!.*/bash', "don't use bash in shebang, use sh"),
@@ -136,8 +167,10 b' testpats = ['
136 167 (r'if\s*!', "don't use '!' to negate exit status"),
137 168 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
138 169 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
139 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
140 "put a backslash-escaped newline after sed 'i' command"),
170 (
171 r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
172 "put a backslash-escaped newline after sed 'i' command",
173 ),
141 174 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
142 175 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
143 176 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
@@ -145,12 +178,17 b' testpats = ['
145 178 (r'\butil\.Abort\b', "directly use error.Abort"),
146 179 (r'\|&', "don't use |&, use 2>&1"),
147 180 (r'\w = +\w', "only one space after = allowed"),
148 (r'\bsed\b.*[^\\]\\n', "don't use 'sed ... \\n', use a \\ and a newline"),
181 (
182 r'\bsed\b.*[^\\]\\n',
183 "don't use 'sed ... \\n', use a \\ and a newline",
184 ),
149 185 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
150 186 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
151 187 (r'grep.* -[ABC]', "don't use grep's context flags"),
152 (r'find.*-printf',
153 "don't use 'find -printf', it doesn't exist on BSD find(1)"),
188 (
189 r'find.*-printf',
190 "don't use 'find -printf', it doesn't exist on BSD find(1)",
191 ),
154 192 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
155 193 ],
156 194 # warnings
@@ -159,8 +197,8 b' testpats = ['
159 197 (r'^diff.*-\w*N', "don't use 'diff -N'"),
160 198 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
161 199 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
162 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
163 ]
200 (r'kill (`|\$\()', "don't use kill, use killdaemons.py"),
201 ],
164 202 ]
165 203
166 204 testfilters = [
@@ -172,43 +210,70 b' uprefix = r"^ \\$ "'
172 210 utestpats = [
173 211 [
174 212 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
175 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
176 "use regex test output patterns instead of sed"),
213 (
214 uprefix + r'.*\|\s*sed[^|>\n]*\n',
215 "use regex test output patterns instead of sed",
216 ),
177 217 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
178 218 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
179 (uprefix + r'.*\|\| echo.*(fail|error)',
180 "explicit exit code checks unnecessary"),
219 (
220 uprefix + r'.*\|\| echo.*(fail|error)',
221 "explicit exit code checks unnecessary",
222 ),
181 223 (uprefix + r'set -e', "don't use set -e"),
182 224 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
183 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
184 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
185 '# no-msys'), # in test-pull.t which is skipped on windows
186 (r'^ [^$>].*27\.0\.0\.1',
187 'use $LOCALIP not an explicit loopback address'),
188 (r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
189 'mark $LOCALIP output lines with (glob) to help tests in BSD jails'),
190 (r'^ (cat|find): .*: \$ENOENT\$',
191 'use test -f to test for file existence'),
192 (r'^ diff -[^ -]*p',
193 "don't use (external) diff with -p for portability"),
225 (
226 uprefix + r'.*:\.\S*/',
227 "x:.y in a path does not work on msys, rewrite "
228 "as x://.y, or see `hg log -k msys` for alternatives",
229 r'-\S+:\.|' '# no-msys', # -Rxxx
230 ), # in test-pull.t which is skipped on windows
231 (
232 r'^ [^$>].*27\.0\.0\.1',
233 'use $LOCALIP not an explicit loopback address',
234 ),
235 (
236 r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
237 'mark $LOCALIP output lines with (glob) to help tests in BSD jails',
238 ),
239 (
240 r'^ (cat|find): .*: \$ENOENT\$',
241 'use test -f to test for file existence',
242 ),
243 (
244 r'^ diff -[^ -]*p',
245 "don't use (external) diff with -p for portability",
246 ),
194 247 (r' readlink ', 'use readlink.py instead of readlink'),
195 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
196 "glob timezone field in diff output for portability"),
197 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
198 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability"),
199 (r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
200 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability"),
201 (r'^ @@ -[0-9]+ [+][0-9]+ @@',
202 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
203 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
248 (
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'
204 266 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
205 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
267 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)",
268 ),
206 269 ],
207 270 # warnings
208 271 [
209 (r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
210 "glob match with no glob string (?, *, /, and $LOCALIP)"),
211 ]
272 (
273 r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
274 "glob match with no glob string (?, *, /, and $LOCALIP)",
275 ),
276 ],
212 277 ]
213 278
214 279 # transform plain test rules to unified test's
@@ -236,14 +301,21 b' utestfilters = ['
236 301 commonpypats = [
237 302 [
238 303 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
239 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
240 "tuple parameter unpacking not available in Python 3+"),
241 (r'lambda\s*\(.*,.*\)',
242 "tuple parameter unpacking not available in Python 3+"),
304 (
305 r'^\s*def\s*\w+\s*\(.*,\s*\(',
306 "tuple parameter unpacking not available in Python 3+",
307 ),
308 (
309 r'lambda\s*\(.*,.*\)',
310 "tuple parameter unpacking not available in Python 3+",
311 ),
243 312 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
244 313 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
245 (r'\bdict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
246 'dict-from-generator'),
314 (
315 r'\bdict\(.*=',
316 'dict() is different in Py2 and 3 and is slower than {}',
317 'dict-from-generator',
318 ),
247 319 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
248 320 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
249 321 (r'^\s*\t', "don't use tabs"),
@@ -253,7 +325,8 b' commonpypats = ['
253 325 (r'(\w|\)),\w', "missing whitespace after ,"),
254 326 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
255 327 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
256 ((
328 (
329 (
257 330 # a line ending with a colon, potentially with trailing comments
258 331 r':([ \t]*#[^\n]*)?\n'
259 332 # one that is not a pass and not only a comment
@@ -262,93 +335,142 b' commonpypats = ['
262 335 r'((?P=indent)[^\n]+\n)*'
263 336 # a pass at the same indent level, which is bogus
264 337 r'(?P=indent)pass[ \t\n#]'
265 ), 'omit superfluous pass'),
338 ),
339 'omit superfluous pass',
340 ),
266 341 (r'[^\n]\Z', "no trailing newline"),
267 342 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
268 343 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
269 344 # "don't use underbars in identifiers"),
270 (r'^\s+(self\.)?[A-Za-z][a-z0-9]+[A-Z]\w* = ',
271 "don't use camelcase in identifiers", r'#.*camelcase-required'),
272 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
273 "linebreak after :"),
274 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
275 r'#.*old-style'),
276 (r'class\s[^( \n]+\(\):',
345 (
346 r'^\s+(self\.)?[A-Za-z][a-z0-9]+[A-Z]\w* = ',
347 "don't use camelcase in identifiers",
348 r'#.*camelcase-required',
349 ),
350 (
351 r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
352 "linebreak after :",
353 ),
354 (
355 r'class\s[^( \n]+:',
356 "old-style class, use class foo(object)",
357 r'#.*old-style',
358 ),
359 (
360 r'class\s[^( \n]+\(\):',
277 361 "class foo() creates old style object, use class foo(object)",
278 r'#.*old-style'),
279 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
280 if k not in ('print', 'exec')),
281 "Python keyword is not a function"),
362 r'#.*old-style',
363 ),
364 (
365 r'\b(%s)\('
366 % '|'.join(k for k in keyword.kwlist if k not in ('print', 'exec')),
367 "Python keyword is not a function",
368 ),
282 369 # (r'class\s[A-Z][^\(]*\((?!Exception)',
283 370 # "don't capitalize non-exception classes"),
284 371 # (r'in range\(', "use xrange"),
285 372 # (r'^\s*print\s+', "avoid using print in core and extensions"),
286 373 (r'[\x80-\xff]', "non-ASCII character literal"),
287 374 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
288 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
375 (
376 r'([\(\[][ \t]\S)|(\S[ \t][\)\]])',
377 "gratuitous whitespace in () or []",
378 ),
289 379 # (r'\s\s=', "gratuitous whitespace before ="),
290 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
291 "missing whitespace around operator"),
292 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
293 "missing whitespace around operator"),
294 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
295 "missing whitespace around operator"),
296 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
297 "wrong whitespace around ="),
298 (r'\([^()]*( =[^=]|[^<>!=]= )',
299 "no whitespace around = for named parameters"),
300 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
301 "don't use old-style two-argument raise, use Exception(message)"),
380 (
381 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
382 "missing whitespace around operator",
383 ),
384 (
385 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
386 "missing whitespace around operator",
387 ),
388 (
389 r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
390 "missing whitespace around operator",
391 ),
392 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]', "wrong whitespace around ="),
393 (
394 r'\([^()]*( =[^=]|[^<>!=]= )',
395 "no whitespace around = for named parameters",
396 ),
397 (
398 r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
399 "don't use old-style two-argument raise, use Exception(message)",
400 ),
302 401 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
303 (r' [=!]=\s+(True|False|None)',
304 "comparison with singleton, use 'is' or 'is not' instead"),
305 (r'^\s*(while|if) [01]:',
306 "use True/False for constant Boolean expression"),
402 (
403 r' [=!]=\s+(True|False|None)',
404 "comparison with singleton, use 'is' or 'is not' instead",
405 ),
406 (
407 r'^\s*(while|if) [01]:',
408 "use True/False for constant Boolean expression",
409 ),
307 410 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
308 (r'(?:(?<!def)\s+|\()hasattr\(',
411 (
412 r'(?:(?<!def)\s+|\()hasattr\(',
309 413 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
310 'instead', r'#.*hasattr-py3-only'),
311 (r'opener\([^)]*\).read\(',
312 "use opener.read() instead"),
313 (r'opener\([^)]*\).write\(',
314 "use opener.write() instead"),
414 'instead',
415 r'#.*hasattr-py3-only',
416 ),
417 (r'opener\([^)]*\).read\(', "use opener.read() instead"),
418 (r'opener\([^)]*\).write\(', "use opener.write() instead"),
315 419 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
316 420 (r'\.debug\(\_', "don't mark debug messages for translation"),
317 421 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
318 422 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
319 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
320 'legacy exception syntax; use "as" instead of ","'),
423 (
424 r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
425 'legacy exception syntax; use "as" instead of ","',
426 ),
321 427 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
322 428 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
323 (r'os\.path\.join\(.*, *(""|\'\')\)',
324 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
429 (
430 r'os\.path\.join\(.*, *(""|\'\')\)',
431 "use pathutil.normasprefix(path) instead of os.path.join(path, '')",
432 ),
325 433 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
326 434 # XXX only catch mutable arguments on the first line of the definition
327 435 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
328 436 (r'\butil\.Abort\b', "directly use error.Abort"),
329 (r'^@(\w*\.)?cachefunc', "module-level @cachefunc is risky, please avoid"),
330 (r'^import Queue', "don't use Queue, use pycompat.queue.Queue + "
331 "pycompat.queue.Empty"),
332 (r'^import cStringIO', "don't use cStringIO.StringIO, use util.stringio"),
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 ),
333 450 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
334 (r'^import SocketServer', "don't use SockerServer, use util.socketserver"),
451 (
452 r'^import SocketServer',
453 "don't use SockerServer, use util.socketserver",
454 ),
335 455 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
336 456 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
337 457 (r'^import cPickle', "don't use cPickle, use util.pickle"),
338 458 (r'^import pickle', "don't use pickle, use util.pickle"),
339 459 (r'^import httplib', "don't use httplib, use util.httplib"),
340 460 (r'^import BaseHTTPServer', "use util.httpserver instead"),
341 (r'^(from|import) mercurial\.(cext|pure|cffi)',
342 "use mercurial.policy.importmod instead"),
461 (
462 r'^(from|import) mercurial\.(cext|pure|cffi)',
463 "use mercurial.policy.importmod instead",
464 ),
343 465 (r'\.next\(\)', "don't use .next(), use next(...)"),
344 (r'([a-z]*).revision\(\1\.node\(',
345 "don't convert rev to node before passing to revision(nodeorrev)"),
466 (
467 r'([a-z]*).revision\(\1\.node\(',
468 "don't convert rev to node before passing to revision(nodeorrev)",
469 ),
346 470 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
347
348 471 ],
349 472 # warnings
350 [
351 ]
473 [],
352 474 ]
353 475
354 476 # patterns to check normal *.py files
@@ -361,21 +483,28 b' pypats = ['
361 483 # scripts for these patterns requires many changes, and has less
362 484 # profit than effort.
363 485 (r'raise Exception', "don't raise generic exceptions"),
364 (r'[\s\(](open|file)\([^)]*\)\.read\(',
365 "use util.readfile() instead"),
366 (r'[\s\(](open|file)\([^)]*\)\.write\(',
367 "use util.writefile() instead"),
368 (r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
369 "always assign an opened file to a variable, and close it afterwards"),
370 (r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
371 "always assign an opened file to a variable, and close it afterwards"),
486 (r'[\s\(](open|file)\([^)]*\)\.read\(', "use util.readfile() instead"),
487 (
488 r'[\s\(](open|file)\([^)]*\)\.write\(',
489 "use util.writefile() instead",
490 ),
491 (
492 r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
493 "always assign an opened file to a variable, and close it afterwards",
494 ),
495 (
496 r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
497 "always assign an opened file to a variable, and close it afterwards",
498 ),
372 499 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
373 500 (r'^import atexit', "don't use atexit, use ui.atexit"),
374
375 501 # rules depending on implementation of repquote()
376 (r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
377 'string join across lines with no space'),
378 (r'''(?x)ui\.(status|progress|write|note|warn)\(
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 508 [ \t\n#]*
380 509 (?# any strings/comments might precede a string, which
381 510 # contains translatable message)
@@ -389,51 +518,55 b' pypats = ['
389 518 (?# this regexp can't use [^...] style,
390 519 # because _preparepats forcibly adds "\n" into [^...],
391 520 # even though this regexp wants match it against "\n")''',
392 "missing _() in ui message (use () to hide false-positives)"),
393 ] + commonpypats[0],
521 "missing _() in ui message (use () to hide false-positives)",
522 ),
523 ]
524 + commonpypats[0],
394 525 # warnings
395 526 [
396 527 # rules depending on implementation of repquote()
397 528 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
398 ] + commonpypats[1]
529 ]
530 + commonpypats[1],
399 531 ]
400 532
401 533 # patterns to check *.py for embedded ones in test script
402 534 embeddedpypats = [
403 [
404 ] + commonpypats[0],
535 [] + commonpypats[0],
405 536 # warnings
406 [
407 ] + commonpypats[1]
537 [] + commonpypats[1],
408 538 ]
409 539
410 540 # common filters to convert *.py
411 541 commonpyfilters = [
412 (r"""(?msx)(?P<comment>\#.*?$)|
542 (
543 r"""(?msx)(?P<comment>\#.*?$)|
413 544 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
414 545 (?P<text>(([^\\]|\\.)*?))
415 (?P=quote))""", reppython),
546 (?P=quote))""",
547 reppython,
548 ),
416 549 ]
417 550
418 551 # filters to convert normal *.py files
419 pyfilters = [
420 ] + commonpyfilters
552 pyfilters = [] + commonpyfilters
421 553
422 554 # non-filter patterns
423 555 pynfpats = [
424 556 [
425 557 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
426 558 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
427 (r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
428 "use pycompat.isdarwin"),
559 (
560 r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
561 "use pycompat.isdarwin",
562 ),
429 563 ],
430 564 # warnings
431 565 [],
432 566 ]
433 567
434 568 # filters to convert *.py for embedded ones in test script
435 embeddedpyfilters = [
436 ] + commonpyfilters
569 embeddedpyfilters = [] + commonpyfilters
437 570
438 571 # extension non-filter patterns
439 572 pyextnfpats = [
@@ -447,9 +580,9 b' txtfilters = []'
447 580 txtpats = [
448 581 [
449 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 588 cpats = [
@@ -473,13 +606,12 b' cpats = ['
473 606 (r'^\s*#import\b', "use only #include in standard C code"),
474 607 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
475 608 (r'strcat\(', "don't use strcat"),
476
477 609 # rules depending on implementation of repquote()
478 610 ],
479 611 # warnings
480 612 [
481 613 # rules depending on implementation of repquote()
482 ]
614 ],
483 615 ]
484 616
485 617 cfilters = [
@@ -490,19 +622,15 b' cfilters = ['
490 622 ]
491 623
492 624 inutilpats = [
493 [
494 (r'\bui\.', "don't use ui in util"),
495 ],
625 [(r'\bui\.', "don't use ui in util"),],
496 626 # warnings
497 []
627 [],
498 628 ]
499 629
500 630 inrevlogpats = [
501 [
502 (r'\brepo\.', "don't use repo in revlog"),
503 ],
631 [(r'\brepo\.', "don't use repo in revlog"),],
504 632 # warnings
505 []
633 [],
506 634 ]
507 635
508 636 webtemplatefilters = []
@@ -510,21 +638,29 b' webtemplatefilters = []'
510 638 webtemplatepats = [
511 639 [],
512 640 [
513 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
514 'follow desc keyword with either firstline or websub'),
515 ]
641 (
642 r'{desc(\|(?!websub|firstline)[^\|]*)+}',
643 'follow desc keyword with either firstline or websub',
644 ),
645 ],
516 646 ]
517 647
518 648 allfilesfilters = []
519 649
520 650 allfilespats = [
521 651 [
522 (r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
523 'use mercurial-scm.org domain URL'),
524 (r'mercurial@selenic\.com',
525 'use mercurial-scm.org domain for mercurial ML address'),
526 (r'mercurial-devel@selenic\.com',
527 'use mercurial-scm.org domain for mercurial-devel ML address'),
652 (
653 r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
654 'use mercurial-scm.org domain URL',
655 ),
656 (
657 r'mercurial@selenic\.com',
658 'use mercurial-scm.org domain for mercurial ML address',
659 ),
660 (
661 r'mercurial-devel@selenic\.com',
662 'use mercurial-scm.org domain for mercurial-devel ML address',
663 ),
528 664 ],
529 665 # warnings
530 666 [],
@@ -532,7 +668,11 b' allfilespats = ['
532 668
533 669 py3pats = [
534 670 [
535 (r'os\.environ', "use encoding.environ instead (py3)", r'#.*re-exports'),
671 (
672 r'os\.environ',
673 "use encoding.environ instead (py3)",
674 r'#.*re-exports',
675 ),
536 676 (r'os\.name', "use pycompat.osname instead (py3)"),
537 677 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
538 678 (r'os\.sep', "use pycompat.ossep instead (py3)"),
@@ -552,20 +692,39 b' checks = ['
552 692 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
553 693 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
554 694 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
555 ('python 3', r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
556 '', pyfilters, py3pats),
695 (
696 'python 3',
697 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
698 '',
699 pyfilters,
700 py3pats,
701 ),
557 702 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
558 703 ('c', r'.*\.[ch]$', '', cfilters, cpats),
559 704 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
560 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
561 pyfilters, inrevlogpats),
562 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
563 inutilpats),
705 (
706 'layering violation repo in revlog',
707 r'mercurial/revlog\.py',
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 719 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
565 ('web template', r'mercurial/templates/.*\.tmpl', '',
566 webtemplatefilters, webtemplatepats),
567 ('all except for .po', r'.*(?<!\.po)$', '',
568 allfilesfilters, allfilespats),
720 (
721 'web template',
722 r'mercurial/templates/.*\.tmpl',
723 '',
724 webtemplatefilters,
725 webtemplatepats,
726 ),
727 ('all except for .po', r'.*(?<!\.po)$', '', allfilesfilters, allfilespats),
569 728 ]
570 729
571 730 # (desc,
@@ -573,10 +732,15 b' checks = ['
573 732 # list of patterns to convert target files
574 733 # list of patterns to detect errors/warnings)
575 734 embeddedchecks = [
576 ('embedded python',
577 testparseutil.pyembedded, embeddedpyfilters, embeddedpypats)
735 (
736 'embedded python',
737 testparseutil.pyembedded,
738 embeddedpyfilters,
739 embeddedpypats,
740 )
578 741 ]
579 742
743
580 744 def _preparepats():
581 745 def preparefailandwarn(failandwarn):
582 746 for pats in failandwarn:
@@ -605,6 +769,7 b' def _preparepats():'
605 769 filters = c[-2]
606 770 preparefilters(filters)
607 771
772
608 773 class norepeatlogger(object):
609 774 def __init__(self):
610 775 self._lastseen = None
@@ -630,8 +795,10 b' class norepeatlogger(object):'
630 795 self._lastseen = msgid
631 796 print(" " + msg)
632 797
798
633 799 _defaultlogger = norepeatlogger()
634 800
801
635 802 def getblame(f):
636 803 lines = []
637 804 for l in os.popen('hg annotate -un %s' % f):
@@ -640,8 +807,16 b' def getblame(f):'
640 807 lines.append((line[1:-1], user, rev))
641 808 return lines
642 809
643 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
644 blame=False, debug=False, lineno=True):
810
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 820 """checks style and portability of a given file
646 821
647 822 :f: filepath
@@ -673,8 +848,9 b' def checkfile(f, logfunc=_defaultlogger.'
673 848 print(name, f)
674 849 if not (re.match(match, f) or (magic and re.search(magic, pre))):
675 850 if debug:
676 print("Skipping %s for %s it doesn't match %s" % (
677 name, match, f))
851 print(
852 "Skipping %s for %s it doesn't match %s" % (name, match, f)
853 )
678 854 continue
679 855 if "no-" "check-code" in pre:
680 856 # If you're looking at this line, it's because a file has:
@@ -686,14 +862,26 b' def checkfile(f, logfunc=_defaultlogger.'
686 862 print("Skipping %s it has no-che?k-code (glob)" % f)
687 863 return "Skip" # skip checking this file
688 864
689 fc = _checkfiledata(name, f, pre, filters, pats, context,
690 logfunc, maxerr, warnings, blame, debug, lineno)
865 fc = _checkfiledata(
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 879 if fc:
692 880 result = False
693 881
694 882 if f.endswith('.t') and "no-" "check-code" not in pre:
695 883 if debug:
696 print("Checking embedded code in %s" % (f))
884 print("Checking embedded code in %s" % f)
697 885
698 886 prelines = pre.splitlines()
699 887 embeddederros = []
@@ -705,9 +893,21 b' def checkfile(f, logfunc=_defaultlogger.'
705 893
706 894 for found in embedded(f, prelines, embeddederros):
707 895 filename, starts, ends, code = found
708 fc = _checkfiledata(name, f, code, filters, pats, context,
709 logfunc, curmaxerr, warnings, blame, debug,
710 lineno, offset=starts - 1)
896 fc = _checkfiledata(
897 name,
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 911 if fc:
712 912 result = False
713 913 if curmaxerr:
@@ -717,9 +917,22 b' def checkfile(f, logfunc=_defaultlogger.'
717 917
718 918 return result
719 919
720 def _checkfiledata(name, f, filedata, filters, pats, context,
721 logfunc, maxerr, warnings, blame, debug, lineno,
722 offset=None):
920
921 def _checkfiledata(
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 936 """Execute actual error check for file data
724 937
725 938 :name: of the checking category
@@ -794,8 +1007,10 b' def _checkfiledata(name, f, filedata, fi'
794 1007
795 1008 if ignore and re.search(ignore, l, re.MULTILINE):
796 1009 if debug:
797 print("Skipping %s for %s:%s (ignore pattern)" % (
798 name, f, (n + lineoffset)))
1010 print(
1011 "Skipping %s for %s:%s (ignore pattern)"
1012 % (name, f, (n + lineoffset))
1013 )
799 1014 continue
800 1015 bd = ""
801 1016 if blame:
@@ -830,21 +1045,38 b' def _checkfiledata(name, f, filedata, fi'
830 1045
831 1046 return fc
832 1047
1048
833 1049 def main():
834 1050 parser = optparse.OptionParser("%prog [options] [files | -]")
835 parser.add_option("-w", "--warnings", action="store_true",
836 help="include warning-level checks")
837 parser.add_option("-p", "--per-file", type="int",
838 help="max warnings per file")
839 parser.add_option("-b", "--blame", action="store_true",
840 help="use annotate to generate blame info")
841 parser.add_option("", "--debug", action="store_true",
842 help="show debug information")
843 parser.add_option("", "--nolineno", action="store_false",
844 dest='lineno', help="don't show line numbers")
1051 parser.add_option(
1052 "-w",
1053 "--warnings",
1054 action="store_true",
1055 help="include warning-level checks",
1056 )
1057 parser.add_option(
1058 "-p", "--per-file", type="int", help="max warnings per file"
1059 )
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,
847 lineno=True)
1077 parser.set_defaults(
1078 per_file=15, warnings=False, blame=False, debug=False, lineno=True
1079 )
848 1080 (options, args) = parser.parse_args()
849 1081
850 1082 if len(args) == 0:
@@ -859,11 +1091,17 b' def main():'
859 1091
860 1092 ret = 0
861 1093 for f in check:
862 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
863 blame=options.blame, debug=options.debug,
864 lineno=options.lineno):
1094 if not checkfile(
1095 f,
1096 maxerr=options.per_file,
1097 warnings=options.warnings,
1098 blame=options.blame,
1099 debug=options.debug,
1100 lineno=options.lineno,
1101 ):
865 1102 ret = 1
866 1103 return ret
867 1104
1105
868 1106 if __name__ == "__main__":
869 1107 sys.exit(main())
@@ -15,7 +15,8 b' foundopts = {}'
15 15 documented = {}
16 16 allowinconsistent = set()
17 17
18 configre = re.compile(br'''
18 configre = re.compile(
19 br'''
19 20 # Function call
20 21 ui\.config(?P<ctype>|int|bool|list)\(
21 22 # First argument.
@@ -23,9 +24,12 b" configre = re.compile(br'''"
23 24 # Second argument
24 25 ['"](?P<option>\S+)['"](,\s+
25 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 33 ui\.config(?P<ctype>with)\(
30 34 # First argument is callback function. This doesn't parse robustly
31 35 # if it is e.g. a function call.
@@ -33,23 +37,32 b" configwithre = re.compile(br'''"
33 37 ['"](?P<section>\S+)['"],\s*
34 38 ['"](?P<option>\S+)['"](,\s+
35 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 48 \#\s(?P<reason>internal|experimental|deprecated|developer|inconsistent)\s
42 49 config:\s(?P<config>\S+\.\S+)$
43 ''', re.VERBOSE | re.MULTILINE)
50 ''',
51 re.VERBOSE | re.MULTILINE,
52 )
44 53
45 54 if sys.version_info[0] > 2:
55
46 56 def mkstr(b):
47 57 if isinstance(b, str):
48 58 return b
49 59 return b.decode('utf8')
60
61
50 62 else:
51 63 mkstr = lambda x: x
52 64
65
53 66 def main(args):
54 67 for f in args:
55 68 sect = b''
@@ -115,18 +128,32 b' def main(args):'
115 128 name = m.group('section') + b"." + m.group('option')
116 129 default = m.group('default')
117 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 139 default = b''
120 140 if re.match(b'[a-z.]+$', default):
121 141 default = b'<variable>'
122 if (name in foundopts and (ctype, default) != foundopts[name]
123 and name not in allowinconsistent):
142 if (
143 name in foundopts
144 and (ctype, default) != foundopts[name]
145 and name not in allowinconsistent
146 ):
124 147 print(mkstr(l.rstrip()))
125 148 fctype, fdefault = foundopts[name]
126 print("conflict on %s: %r != %r" % (
149 print(
150 "conflict on %s: %r != %r"
151 % (
127 152 mkstr(name),
128 153 (mkstr(ctype), mkstr(default)),
129 (mkstr(fctype), mkstr(fdefault))))
154 (mkstr(fctype), mkstr(fdefault)),
155 )
156 )
130 157 print("at %s:%d:" % (mkstr(f), linenum))
131 158 foundopts[name] = (ctype, default)
132 159 carryover = b''
@@ -139,9 +166,11 b' def main(args):'
139 166
140 167 for name in sorted(foundopts):
141 168 if name not in documented:
142 if not (name.startswith(b"devel.") or
143 name.startswith(b"experimental.") or
144 name.startswith(b"debug.")):
169 if not (
170 name.startswith(b"devel.")
171 or name.startswith(b"experimental.")
172 or name.startswith(b"debug.")
173 ):
145 174 ctype, default = foundopts[name]
146 175 if default:
147 176 if isinstance(default, bytes):
@@ -149,8 +178,11 b' def main(args):'
149 178 default = ' [%s]' % default
150 179 elif isinstance(default, bytes):
151 180 default = mkstr(default)
152 print("undocumented: %s (%s)%s" % (
153 mkstr(name), mkstr(ctype), default))
181 print(
182 "undocumented: %s (%s)%s"
183 % (mkstr(name), mkstr(ctype), default)
184 )
185
154 186
155 187 if __name__ == "__main__":
156 188 if len(sys.argv) > 1:
@@ -16,6 +16,7 b' import sys'
16 16 import traceback
17 17 import warnings
18 18
19
19 20 def check_compat_py2(f):
20 21 """Check Python 3 compatibility for a file with Python 2"""
21 22 with open(f, 'rb') as fh:
@@ -40,6 +41,7 b' def check_compat_py2(f):'
40 41 if haveprint and 'print_function' not in futures:
41 42 print('%s requires print_function' % f)
42 43
44
43 45 def check_compat_py3(f):
44 46 """Check Python 3 compatibility of a file with Python 3."""
45 47 with open(f, 'rb') as fh:
@@ -54,8 +56,9 b' def check_compat_py3(f):'
54 56 # Try to import the module.
55 57 # For now we only support modules in packages because figuring out module
56 58 # paths for things not in a package can be confusing.
57 if (f.startswith(('hgdemandimport/', 'hgext/', 'mercurial/'))
58 and not f.endswith('__init__.py')):
59 if f.startswith(
60 ('hgdemandimport/', 'hgext/', 'mercurial/')
61 ) and not f.endswith('__init__.py'):
59 62 assert f.endswith('.py')
60 63 name = f.replace('/', '.')[:-3]
61 64 try:
@@ -79,11 +82,16 b' def check_compat_py3(f):'
79 82
80 83 if frame.filename:
81 84 filename = os.path.basename(frame.filename)
82 print('%s: error importing: <%s> %s (error at %s:%d)' % (
83 f, type(e).__name__, e, filename, frame.lineno))
85 print(
86 '%s: error importing: <%s> %s (error at %s:%d)'
87 % (f, type(e).__name__, e, filename, frame.lineno)
88 )
84 89 else:
85 print('%s: error importing module: <%s> %s (line %d)' % (
86 f, type(e).__name__, e, frame.lineno))
90 print(
91 '%s: error importing module: <%s> %s (line %d)'
92 % (f, type(e).__name__, e, frame.lineno)
93 )
94
87 95
88 96 if __name__ == '__main__':
89 97 if sys.version_info[0] == 2:
@@ -96,7 +104,10 b" if __name__ == '__main__':"
96 104 fn(f)
97 105
98 106 for w in warns:
99 print(warnings.formatwarning(w.message, w.category,
100 w.filename, w.lineno).rstrip())
107 print(
108 warnings.formatwarning(
109 w.message, w.category, w.filename, w.lineno
110 ).rstrip()
111 )
101 112
102 113 sys.exit(0)
@@ -23,6 +23,7 b" if sys.argv[1] == '-':"
23 23 else:
24 24 log = open(sys.argv[1], 'a')
25 25
26
26 27 def read(size):
27 28 data = sys.stdin.read(size)
28 29 if not data:
@@ -31,6 +32,7 b' def read(size):'
31 32 sys.stdout.flush()
32 33 return data
33 34
35
34 36 try:
35 37 while True:
36 38 header = read(outputfmtsize)
@@ -14,6 +14,7 b' from mercurial import ('
14 14 cmdtable = {}
15 15 command = registrar.command(cmdtable)
16 16
17
17 18 def pdb(ui, repo, msg, **opts):
18 19 objects = {
19 20 'mercurial': mercurial,
@@ -24,6 +25,7 b' def pdb(ui, repo, msg, **opts):'
24 25
25 26 code.interact(msg, local=objects)
26 27
28
27 29 def ipdb(ui, repo, msg, **opts):
28 30 import IPython
29 31
@@ -33,16 +35,15 b' def ipdb(ui, repo, msg, **opts):'
33 35
34 36 IPython.embed()
35 37
38
36 39 @command(b'debugshell|dbsh', [])
37 40 def debugshell(ui, repo, **opts):
38 bannermsg = ("loaded repo : %s\n"
39 "using source: %s" % (pycompat.sysstr(repo.root),
40 mercurial.__path__[0]))
41 bannermsg = "loaded repo : %s\n" "using source: %s" % (
42 pycompat.sysstr(repo.root),
43 mercurial.__path__[0],
44 )
41 45
42 pdbmap = {
43 'pdb' : 'code',
44 'ipdb' : 'IPython'
45 }
46 pdbmap = {'pdb': 'code', 'ipdb': 'IPython'}
46 47
47 48 debugger = ui.config(b"ui", b"debugger")
48 49 if not debugger:
@@ -55,8 +56,10 b' def debugshell(ui, repo, **opts):'
55 56 with demandimport.deactivated():
56 57 __import__(pdbmap[debugger])
57 58 except ImportError:
58 ui.warn((b"%s debugger specified but %s module was not found\n")
59 % (debugger, pdbmap[debugger]))
59 ui.warn(
60 b"%s debugger specified but %s module was not found\n"
61 % (debugger, pdbmap[debugger])
62 )
60 63 debugger = b'pdb'
61 64
62 65 getattr(sys.modules[__name__], debugger)(ui, repo, bannermsg, **opts)
@@ -13,6 +13,7 b' from mercurial import ('
13 13 extensions,
14 14 )
15 15
16
16 17 def nonnormalentries(dmap):
17 18 """Compute nonnormal entries from dirstate's dmap"""
18 19 res = set()
@@ -21,6 +22,7 b' def nonnormalentries(dmap):'
21 22 res.add(f)
22 23 return res
23 24
25
24 26 def checkconsistency(ui, orig, dmap, _nonnormalset, label):
25 27 """Compute nonnormalset from dmap, check that it matches _nonnormalset"""
26 28 nonnormalcomputedmap = nonnormalentries(dmap)
@@ -30,15 +32,19 b' def checkconsistency(ui, orig, dmap, _no'
30 32 ui.develwarn(b"[nonnormalset] %s\n" % _nonnormalset, config=b'dirstate')
31 33 ui.develwarn(b"[map] %s\n" % nonnormalcomputedmap, config=b'dirstate')
32 34
35
33 36 def _checkdirstate(orig, self, arg):
34 37 """Check nonnormal set consistency before and after the call to orig"""
35 checkconsistency(self._ui, orig, self._map, self._map.nonnormalset,
36 b"before")
38 checkconsistency(
39 self._ui, orig, self._map, self._map.nonnormalset, b"before"
40 )
37 41 r = orig(self, arg)
38 checkconsistency(self._ui, orig, self._map, self._map.nonnormalset,
39 b"after")
42 checkconsistency(
43 self._ui, orig, self._map, self._map.nonnormalset, b"after"
44 )
40 45 return r
41 46
47
42 48 def extsetup(ui):
43 49 """Wrap functions modifying dirstate to check nonnormalset consistency"""
44 50 dirstatecl = dirstate.dirstate
@@ -8,8 +8,7 b' ap = argparse.ArgumentParser()'
8 8 ap.add_argument("out", metavar="some.zip", type=str, nargs=1)
9 9 args = ap.parse_args()
10 10
11 reporoot = os.path.normpath(os.path.join(os.path.dirname(__file__),
12 '..', '..'))
11 reporoot = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..'))
13 12 dirstate = os.path.join(reporoot, '.hg', 'dirstate')
14 13
15 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 33 'nhistedituserAugie Fackler <raf@durin42.com>\x00\x00\x00yA\xd7\x02'
34 34 'MtA\xd4\xe1\x01,\x00\x00\x01\x03\x03"\xa5\xcb\x86\xb6\xf4\xbaO\xa0'
35 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,7 +8,8 b' ap.add_argument("out", metavar="some.zip'
8 8 args = ap.parse_args()
9 9
10 10 with zipfile.ZipFile(args.out[0], "w", zipfile.ZIP_STORED) as zf:
11 zf.writestr("manifest_zero",
11 zf.writestr(
12 "manifest_zero",
12 13 '''PKG-INFO\09b3ed8f2b81095a13064402e930565f083346e9a
13 14 README\080b6e76643dcb44d4bc729e932fc464b3e36dbe3
14 15 hg\0b6444347c629cc058d478023905cfb83b7f5bb9d
@@ -22,9 +23,11 b' mercurial/transaction.py\\09d180df101dc14'
22 23 notes.txt\0703afcec5edb749cf5cec67831f554d6da13f2fb
23 24 setup.py\0ccf3f6daf0f13101ca73631f7a1769e328b472c9
24 25 tkmerge\03c922edb43a9c143682f7bc7b00f98b3c756ebe7
25 ''')
26 zf.writestr("badmanifest_shorthashes",
27 "narf\0aa\nnarf2\0aaa\n")
28 zf.writestr("badmanifest_nonull",
26 ''',
27 )
28 zf.writestr("badmanifest_shorthashes", "narf\0aa\nnarf2\0aaa\n")
29 zf.writestr(
30 "badmanifest_nonull",
29 31 "narf\0cccccccccccccccccccccccccccccccccccccccc\n"
30 "narf2aaaaaaaaaaaaaaaaaaaa\n")
32 "narf2aaaaaaaaaaaaaaaaaaaa\n",
33 )
@@ -13,6 +13,7 b' ap = argparse.ArgumentParser()'
13 13 ap.add_argument("out", metavar="some.zip", type=str, nargs=1)
14 14 args = ap.parse_args()
15 15
16
16 17 class deltafrag(object):
17 18 def __init__(self, start, end, data):
18 19 self.start = start
@@ -20,8 +21,11 b' class deltafrag(object):'
20 21 self.data = data
21 22
22 23 def __str__(self):
23 return struct.pack(
24 ">lll", self.start, self.end, len(self.data)) + self.data
24 return (
25 struct.pack(">lll", self.start, self.end, len(self.data))
26 + self.data
27 )
28
25 29
26 30 class delta(object):
27 31 def __init__(self, frags):
@@ -30,8 +34,8 b' class delta(object):'
30 34 def __str__(self):
31 35 return ''.join(str(f) for f in self.frags)
32 36
37
33 38 class corpus(object):
34
35 39 def __init__(self, base, deltas):
36 40 self.base = base
37 41 self.deltas = deltas
@@ -49,19 +53,19 b' class corpus(object):'
49 53 )
50 54 return "".join(parts)
51 55
56
52 57 with zipfile.ZipFile(args.out[0], "w", zipfile.ZIP_STORED) as zf:
53 58 # Manually constructed entries
54 59 zf.writestr(
55 "one_delta_applies",
56 str(corpus('a', [delta([deltafrag(0, 1, 'b')])]))
60 "one_delta_applies", str(corpus('a', [delta([deltafrag(0, 1, 'b')])]))
57 61 )
58 62 zf.writestr(
59 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 66 zf.writestr(
63 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 71 try:
@@ -70,8 +74,7 b' with zipfile.ZipFile(args.out[0], "w", z'
70 74 fl = r.file('mercurial/manifest.py')
71 75 rl = getattr(fl, '_revlog', fl)
72 76 bins = rl._chunks(rl._deltachain(10)[0])
73 zf.writestr('manifest_py_rev_10',
74 str(corpus(bins[0], bins[1:])))
77 zf.writestr('manifest_py_rev_10', str(corpus(bins[0], bins[1:])))
75 78 except: # skip this, so no re-raises
76 79 print('skipping seed file from repo data')
77 80 # Automatically discovered by running the fuzzer
@@ -81,7 +84,8 b' with zipfile.ZipFile(args.out[0], "w", z'
81 84 # https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=8876
82 85 zf.writestr(
83 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 89 zf.writestr(
86 90 "mpatch_apply_over_memcpy",
87 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 346 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
343 347 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00se\x00\x00'
344 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 8 ap.add_argument("out", metavar="some.zip", type=str, nargs=1)
9 9 args = ap.parse_args()
10 10
11 reporoot = os.path.normpath(os.path.join(os.path.dirname(__file__),
12 '..', '..'))
11 reporoot = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..'))
13 12 # typically a standalone index
14 13 changelog = os.path.join(reporoot, '.hg', 'store', '00changelog.i')
15 14 # an inline revlog with only a few revisions
16 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 19 print(changelog, os.path.exists(changelog))
20 20 print(contributing, os.path.exists(contributing))
@@ -7,21 +7,25 b' import subprocess'
7 7 import sys
8 8
9 9 # Always load hg libraries from the hg we can find on $PATH.
10 hglib = subprocess.check_output(
11 ['hg', 'debuginstall', '-T', '{hgmodules}'])
10 hglib = subprocess.check_output(['hg', 'debuginstall', '-T', '{hgmodules}'])
12 11 sys.path.insert(0, os.path.dirname(hglib))
13 12
14 13 from mercurial import util
15 14
16 15 ap = argparse.ArgumentParser()
17 ap.add_argument('--paranoid',
16 ap.add_argument(
17 '--paranoid',
18 18 action='store_true',
19 help=("Be paranoid about how version numbers compare and "
19 help=(
20 "Be paranoid about how version numbers compare and "
20 21 "produce something that's more likely to sort "
21 "reasonably."))
22 "reasonably."
23 ),
24 )
22 25 ap.add_argument('--selftest', action='store_true', help='Run self-tests.')
23 26 ap.add_argument('versionfile', help='Path to a valid mercurial __version__.py')
24 27
28
25 29 def paranoidver(ver):
26 30 """Given an hg version produce something that distutils can sort.
27 31
@@ -108,10 +112,12 b' def paranoidver(ver):'
108 112 extra = ''
109 113 return '%d.%d.%d%s' % (major, minor, micro, extra)
110 114
115
111 116 def main(argv):
112 117 opts = ap.parse_args(argv[1:])
113 118 if opts.selftest:
114 119 import doctest
120
115 121 doctest.testmod()
116 122 return
117 123 with open(opts.versionfile) as f:
@@ -125,5 +131,6 b' def main(argv):'
125 131 else:
126 132 print(ver)
127 133
134
128 135 if __name__ == '__main__':
129 136 main(sys.argv)
@@ -16,17 +16,22 b' if sys.version_info[0] >= 3:'
16 16 stdout = sys.stdout.buffer
17 17 stderr = sys.stderr.buffer
18 18 stringio = io.BytesIO
19
19 20 def bprint(*args):
20 21 # remove b'' as well for ease of test migration
21 22 pargs = [re.sub(br'''\bb(['"])''', br'\1', b'%s' % a) for a in args]
22 23 stdout.write(b' '.join(pargs) + b'\n')
24
25
23 26 else:
24 27 import cStringIO
28
25 29 stdout = sys.stdout
26 30 stderr = sys.stderr
27 31 stringio = cStringIO.StringIO
28 32 bprint = print
29 33
34
30 35 def connectpipe(path=None, extraargs=()):
31 36 cmdline = [b'hg', b'serve', b'--cmdserver', b'pipe']
32 37 if path:
@@ -38,11 +43,13 b' def connectpipe(path=None, extraargs=())'
38 43 return cmdline
39 44 return [arg.decode("utf-8") for arg in cmdline]
40 45
41 server = subprocess.Popen(tonative(cmdline), stdin=subprocess.PIPE,
42 stdout=subprocess.PIPE)
46 server = subprocess.Popen(
47 tonative(cmdline), stdin=subprocess.PIPE, stdout=subprocess.PIPE
48 )
43 49
44 50 return server
45 51
52
46 53 class unixconnection(object):
47 54 def __init__(self, sockpath):
48 55 self.sock = sock = socket.socket(socket.AF_UNIX)
@@ -55,6 +62,7 b' class unixconnection(object):'
55 62 self.stdout.close()
56 63 self.sock.close()
57 64
65
58 66 class unixserver(object):
59 67 def __init__(self, sockpath, logpath=None, repopath=None):
60 68 self.sockpath = sockpath
@@ -80,11 +88,13 b' class unixserver(object):'
80 88 os.kill(self.server.pid, signal.SIGTERM)
81 89 self.server.wait()
82 90
91
83 92 def writeblock(server, data):
84 93 server.stdin.write(struct.pack(b'>I', len(data)))
85 94 server.stdin.write(data)
86 95 server.stdin.flush()
87 96
97
88 98 def readchannel(server):
89 99 data = server.stdout.read(5)
90 100 if not data:
@@ -95,11 +105,14 b' def readchannel(server):'
95 105 else:
96 106 return channel, server.stdout.read(length)
97 107
108
98 109 def sep(text):
99 110 return text.replace(b'\\', b'/')
100 111
101 def runcommand(server, args, output=stdout, error=stderr, input=None,
102 outfilter=lambda x: x):
112
113 def runcommand(
114 server, args, output=stdout, error=stderr, input=None, outfilter=lambda x: x
115 ):
103 116 bprint(b'*** runcommand', b' '.join(args))
104 117 stdout.flush()
105 118 server.stdin.write(b'runcommand\n')
@@ -123,7 +136,7 b' def runcommand(server, args, output=stdo'
123 136 elif ch == b'm':
124 137 bprint(b"message: %r" % data)
125 138 elif ch == b'r':
126 ret, = struct.unpack('>i', data)
139 (ret,) = struct.unpack('>i', data)
127 140 if ret != 0:
128 141 bprint(b' [%d]' % ret)
129 142 return ret
@@ -132,6 +145,7 b' def runcommand(server, args, output=stdo'
132 145 if ch.isupper():
133 146 return
134 147
148
135 149 def check(func, connect=connectpipe):
136 150 stdout.flush()
137 151 server = connect()
@@ -141,7 +155,9 b' def check(func, connect=connectpipe):'
141 155 server.stdin.close()
142 156 server.wait()
143 157
158
144 159 def checkwith(connect=connectpipe, **kwargs):
145 160 def wrap(func):
146 161 return check(func, lambda: connect(**kwargs))
162
147 163 return wrap
@@ -13,6 +13,7 b' prints it to ``stderr`` on exit.'
13 13
14 14 from __future__ import absolute_import
15 15
16
16 17 def memusage(ui):
17 18 """Report memory usage of the current process."""
18 19 result = {'peak': 0, 'rss': 0}
@@ -24,8 +25,13 b' def memusage(ui):'
24 25 key = parts[0][2:-1].lower()
25 26 if key in result:
26 27 result[key] = int(parts[1])
27 ui.write_err(", ".join(["%s: %.1f MiB" % (k, v / 1024.0)
28 for k, v in result.iteritems()]) + "\n")
28 ui.write_err(
29 ", ".join(
30 ["%s: %.1f MiB" % (k, v / 1024.0) for k, v in result.iteritems()]
31 )
32 + "\n"
33 )
34
29 35
30 36 def extsetup(ui):
31 37 ui.atexit(memusage, ui)
@@ -98,7 +98,10 b' def secure_download_stream(url, size, sh'
98 98 length = 0
99 99
100 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 105 fh = gzip.GzipFile(fileobj=fh)
103 106
104 107 while True:
@@ -114,12 +117,14 b' def secure_download_stream(url, size, sh'
114 117 digest = h.hexdigest()
115 118
116 119 if length != size:
117 raise IntegrityError('size mismatch on %s: wanted %d; got %d' % (
118 url, size, length))
120 raise IntegrityError(
121 'size mismatch on %s: wanted %d; got %d' % (url, size, length)
122 )
119 123
120 124 if digest != sha256:
121 raise IntegrityError('sha256 mismatch on %s: wanted %s; got %s' % (
122 url, sha256, digest))
125 raise IntegrityError(
126 'sha256 mismatch on %s: wanted %s; got %s' % (url, sha256, digest)
127 )
123 128
124 129
125 130 def download_to_path(url: str, path: pathlib.Path, size: int, sha256: str):
@@ -162,7 +167,9 b' def download_to_path(url: str, path: pat'
162 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 173 entry = DOWNLOADS[name]
167 174
168 175 url = entry['url']
@@ -12,12 +12,8 b' import pathlib'
12 12 import shutil
13 13 import subprocess
14 14
15 from .py2exe import (
16 build_py2exe,
17 )
18 from .util import (
19 find_vc_runtime_files,
20 )
15 from .py2exe import build_py2exe
16 from .util import find_vc_runtime_files
21 17
22 18
23 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,
32 python_exe: pathlib.Path, iscc_exe: pathlib.Path,
33 version=None):
27 def build(
28 source_dir: pathlib.Path,
29 build_dir: pathlib.Path,
30 python_exe: pathlib.Path,
31 iscc_exe: pathlib.Path,
32 version=None,
33 ):
34 34 """Build the Inno installer.
35 35
36 36 Build files will be placed in ``build_dir``.
@@ -44,11 +44,18 b' def build(source_dir: pathlib.Path, buil'
44 44
45 45 vc_x64 = r'\x64' in os.environ.get('LIB', '')
46 46
47 requirements_txt = (source_dir / 'contrib' / 'packaging' /
48 'inno' / 'requirements.txt')
47 requirements_txt = (
48 source_dir / 'contrib' / 'packaging' / 'inno' / 'requirements.txt'
49 )
49 50
50 build_py2exe(source_dir, build_dir, python_exe, 'inno',
51 requirements_txt, extra_packages=EXTRA_PACKAGES)
51 build_py2exe(
52 source_dir,
53 build_dir,
54 python_exe,
55 'inno',
56 requirements_txt,
57 extra_packages=EXTRA_PACKAGES,
58 )
52 59
53 60 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
54 61 for f in find_vc_runtime_files(vc_x64):
@@ -11,9 +11,7 b' import os'
11 11 import pathlib
12 12 import subprocess
13 13
14 from .downloads import (
15 download_entry,
16 )
14 from .downloads import download_entry
17 15 from .util import (
18 16 extract_tar_to_directory,
19 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,
25 python_exe: pathlib.Path, build_name: str,
22 def build_py2exe(
23 source_dir: pathlib.Path,
24 build_dir: pathlib.Path,
25 python_exe: pathlib.Path,
26 build_name: str,
26 27 venv_requirements_txt: pathlib.Path,
27 extra_packages=None, extra_excludes=None,
28 extra_packages=None,
29 extra_excludes=None,
28 30 extra_dll_excludes=None,
29 extra_packages_script=None):
31 extra_packages_script=None,
32 ):
30 33 """Build Mercurial with py2exe.
31 34
32 35 Build files will be placed in ``build_dir``.
@@ -36,9 +39,11 b' def build_py2exe(source_dir: pathlib.Pat'
36 39 to already be configured with an active toolchain.
37 40 """
38 41 if 'VCINSTALLDIR' not in os.environ:
39 raise Exception('not running from a Visual C++ build environment; '
42 raise Exception(
43 'not running from a Visual C++ build environment; '
40 44 'execute the "Visual C++ <version> Command Prompt" '
41 'application shortcut or a vcsvarsall.bat file')
45 'application shortcut or a vcsvarsall.bat file'
46 )
42 47
43 48 # Identity x86/x64 and validate the environment matches the Python
44 49 # architecture.
@@ -48,12 +53,16 b' def build_py2exe(source_dir: pathlib.Pat'
48 53
49 54 if vc_x64:
50 55 if py_info['arch'] != '64bit':
51 raise Exception('architecture mismatch: Visual C++ environment '
52 'is configured for 64-bit but Python is 32-bit')
56 raise Exception(
57 'architecture mismatch: Visual C++ environment '
58 'is configured for 64-bit but Python is 32-bit'
59 )
53 60 else:
54 61 if py_info['arch'] != '32bit':
55 raise Exception('architecture mismatch: Visual C++ environment '
56 'is configured for 32-bit but Python is 64-bit')
62 raise Exception(
63 'architecture mismatch: Visual C++ environment '
64 'is configured for 32-bit but Python is 64-bit'
65 )
57 66
58 67 if py_info['py3']:
59 68 raise Exception('Only Python 2 is currently supported')
@@ -65,11 +74,11 b' def build_py2exe(source_dir: pathlib.Pat'
65 74 virtualenv_pkg, virtualenv_entry = download_entry('virtualenv', build_dir)
66 75 py2exe_pkg, py2exe_entry = download_entry('py2exe', build_dir)
67 76
68 venv_path = build_dir / ('venv-%s-%s' % (build_name,
69 'x64' if vc_x64 else 'x86'))
77 venv_path = build_dir / (
78 'venv-%s-%s' % (build_name, 'x64' if vc_x64 else 'x86')
79 )
70 80
71 gettext_root = build_dir / (
72 'gettext-win-%s' % gettext_entry['version'])
81 gettext_root = build_dir / ('gettext-win-%s' % gettext_entry['version'])
73 82
74 83 if not gettext_root.exists():
75 84 extract_zip_to_directory(gettext_pkg, gettext_root)
@@ -77,7 +86,8 b' def build_py2exe(source_dir: pathlib.Pat'
77 86
78 87 # This assumes Python 2. We don't need virtualenv on Python 3.
79 88 virtualenv_src_path = build_dir / (
80 'virtualenv-%s' % virtualenv_entry['version'])
89 'virtualenv-%s' % virtualenv_entry['version']
90 )
81 91 virtualenv_py = virtualenv_src_path / 'virtualenv.py'
82 92
83 93 if not virtualenv_src_path.exists():
@@ -91,14 +101,15 b' def build_py2exe(source_dir: pathlib.Pat'
91 101 if not venv_path.exists():
92 102 print('creating virtualenv with dependencies')
93 103 subprocess.run(
94 [str(python_exe), str(virtualenv_py), str(venv_path)],
95 check=True)
104 [str(python_exe), str(virtualenv_py), str(venv_path)], check=True
105 )
96 106
97 107 venv_python = venv_path / 'Scripts' / 'python.exe'
98 108 venv_pip = venv_path / 'Scripts' / 'pip.exe'
99 109
100 subprocess.run([str(venv_pip), 'install', '-r', str(venv_requirements_txt)],
101 check=True)
110 subprocess.run(
111 [str(venv_pip), 'install', '-r', str(venv_requirements_txt)], check=True
112 )
102 113
103 114 # Force distutils to use VC++ settings from environment, which was
104 115 # validated above.
@@ -107,9 +118,13 b' def build_py2exe(source_dir: pathlib.Pat'
107 118 env['MSSdk'] = '1'
108 119
109 120 if extra_packages_script:
110 more_packages = set(subprocess.check_output(
111 extra_packages_script,
112 cwd=build_dir).split(b'\0')[-1].strip().decode('utf-8').splitlines())
121 more_packages = set(
122 subprocess.check_output(extra_packages_script, cwd=build_dir)
123 .split(b'\0')[-1]
124 .strip()
125 .decode('utf-8')
126 .splitlines()
127 )
113 128 if more_packages:
114 129 if not extra_packages:
115 130 extra_packages = more_packages
@@ -119,32 +134,38 b' def build_py2exe(source_dir: pathlib.Pat'
119 134 if extra_packages:
120 135 env['HG_PY2EXE_EXTRA_PACKAGES'] = ' '.join(sorted(extra_packages))
121 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 139 if hgext3rd_extras:
124 140 env['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'] = ' '.join(hgext3rd_extras)
125 141 if extra_excludes:
126 142 env['HG_PY2EXE_EXTRA_EXCLUDES'] = ' '.join(sorted(extra_excludes))
127 143 if extra_dll_excludes:
128 144 env['HG_PY2EXE_EXTRA_DLL_EXCLUDES'] = ' '.join(
129 sorted(extra_dll_excludes))
145 sorted(extra_dll_excludes)
146 )
130 147
131 148 py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe'
132 149 if not py2exe_py_path.exists():
133 150 print('building py2exe')
134 subprocess.run([str(venv_python), 'setup.py', 'install'],
151 subprocess.run(
152 [str(venv_python), 'setup.py', 'install'],
135 153 cwd=py2exe_source_path,
136 154 env=env,
137 check=True)
155 check=True,
156 )
138 157
139 158 # Register location of msgfmt and other binaries.
140 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 165 print('building Mercurial')
144 166 subprocess.run(
145 [str(venv_python), 'setup.py',
146 'py2exe',
147 'build_doc', '--html'],
167 [str(venv_python), 'setup.py', 'py2exe', 'build_doc', '--html'],
148 168 cwd=str(source_dir),
149 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 33 prefix = 'amd64' if x64 else 'x86'
34 34
35 candidates = sorted(p for p in os.listdir(winsxs)
36 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix))
35 candidates = sorted(
36 p
37 for p in os.listdir(winsxs)
38 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix)
39 )
37 40
38 41 for p in candidates:
39 42 print('found candidate VC runtime: %s' % p)
@@ -72,7 +75,7 b' def windows_10_sdk_info():'
72 75 'version': version,
73 76 'bin_root': bin_version,
74 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 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,
93 cert_path=None, cert_password=None,
94 timestamp_url=None):
95 def sign_with_signtool(
96 file_path,
97 description,
98 subject_name=None,
99 cert_path=None,
100 cert_password=None,
101 timestamp_url=None,
102 ):
95 103 """Digitally sign a file with signtool.exe.
96 104
97 105 ``file_path`` is file to sign.
@@ -114,10 +122,13 b' def sign_with_signtool(file_path, descri'
114 122 cert_password = getpass.getpass('password for %s: ' % cert_path)
115 123
116 124 args = [
117 str(find_signtool()), 'sign',
125 str(find_signtool()),
126 'sign',
118 127 '/v',
119 '/fd', 'sha256',
120 '/d', description,
128 '/fd',
129 'sha256',
130 '/d',
131 description,
121 132 ]
122 133
123 134 if cert_path:
@@ -15,12 +15,8 b' import tempfile'
15 15 import typing
16 16 import xml.dom.minidom
17 17
18 from .downloads import (
19 download_entry,
20 )
21 from .py2exe import (
22 build_py2exe,
23 )
18 from .downloads import download_entry
19 from .py2exe import build_py2exe
24 20 from .util import (
25 21 extract_zip_to_directory,
26 22 sign_with_signtool,
@@ -84,17 +80,29 b' def normalize_version(version):'
84 80
85 81 def ensure_vc90_merge_modules(build_dir):
86 82 x86 = (
87 download_entry('vc9-crt-x86-msm', build_dir,
88 local_name='microsoft.vcxx.crt.x86_msm.msm')[0],
89 download_entry('vc9-crt-x86-msm-policy', build_dir,
90 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0]
83 download_entry(
84 'vc9-crt-x86-msm',
85 build_dir,
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 95 x64 = (
94 download_entry('vc9-crt-x64-msm', build_dir,
95 local_name='microsoft.vcxx.crt.x64_msm.msm')[0],
96 download_entry('vc9-crt-x64-msm-policy', build_dir,
97 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0]
96 download_entry(
97 'vc9-crt-x64-msm',
98 build_dir,
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 107 return {
100 108 'x86': x86,
@@ -116,17 +124,26 b' def run_candle(wix, cwd, wxs, source_dir'
116 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,
120 cert_password=None, timestamp_url=None):
127 def make_post_build_signing_fn(
128 name,
129 subject_name=None,
130 cert_path=None,
131 cert_password=None,
132 timestamp_url=None,
133 ):
121 134 """Create a callable that will use signtool to sign hg.exe."""
122 135
123 136 def post_build_sign(source_dir, build_dir, dist_dir, version):
124 137 description = '%s %s' % (name, version)
125 138
126 sign_with_signtool(dist_dir / 'hg.exe', description,
127 subject_name=subject_name, cert_path=cert_path,
139 sign_with_signtool(
140 dist_dir / 'hg.exe',
141 description,
142 subject_name=subject_name,
143 cert_path=cert_path,
128 144 cert_password=cert_password,
129 timestamp_url=timestamp_url)
145 timestamp_url=timestamp_url,
146 )
130 147
131 148 return post_build_sign
132 149
@@ -155,7 +172,8 b' def make_libraries_xml(wix_dir: pathlib.'
155 172 # We can't use ElementTree because it doesn't handle the
156 173 # <?include ?> directives.
157 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 178 component = doc.getElementsByTagName('Component')[0]
161 179
@@ -177,11 +195,16 b' def make_libraries_xml(wix_dir: pathlib.'
177 195 return doc.toprettyxml()
178 196
179 197
180 def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
181 msi_name='mercurial', version=None, post_build_fn=None,
198 def build_installer(
199 source_dir: pathlib.Path,
200 python_exe: pathlib.Path,
201 msi_name='mercurial',
202 version=None,
203 post_build_fn=None,
182 204 extra_packages_script=None,
183 205 extra_wxs:typing.Optional[typing.Dict[str,str]]=None,
184 extra_features:typing.Optional[typing.List[str]]=None):
206 extra_features: typing.Optional[typing.List[str]] = None,
207 ):
185 208 """Build a WiX MSI installer.
186 209
187 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 233 requirements_txt = wix_dir / 'requirements.txt'
211 234
212 build_py2exe(source_dir, hg_build_dir,
213 python_exe, 'wix', requirements_txt,
235 build_py2exe(
236 source_dir,
237 hg_build_dir,
238 python_exe,
239 'wix',
240 requirements_txt,
214 241 extra_packages=EXTRA_PACKAGES,
215 extra_packages_script=extra_packages_script)
242 extra_packages_script=extra_packages_script,
243 )
216 244
217 245 version = version or normalize_version(find_version(source_dir))
218 246 print('using version string: %s' % version)
@@ -265,16 +293,19 b' def build_installer(source_dir: pathlib.'
265 293
266 294 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
267 295
268 msi_path = source_dir / 'dist' / (
269 '%s-%s-%s.msi' % (msi_name, version, arch))
296 msi_path = (
297 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, version, arch))
298 )
270 299
271 300 args = [
272 301 str(wix_path / 'light.exe'),
273 302 '-nologo',
274 '-ext', 'WixUIExtension',
303 '-ext',
304 'WixUIExtension',
275 305 '-sw1076',
276 306 '-spdb',
277 '-o', str(msi_path),
307 '-o',
308 str(msi_path),
278 309 ]
279 310
280 311 for source, rel_path in SUPPORT_WXS:
@@ -286,10 +317,12 b' def build_installer(source_dir: pathlib.'
286 317 source = os.path.basename(source)
287 318 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
288 319
289 args.extend([
320 args.extend(
321 [
290 322 str(build_dir / 'library.wixobj'),
291 323 str(build_dir / 'mercurial.wixobj'),
292 ])
324 ]
325 )
293 326
294 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,
304 name: str, version=None, subject_name=None,
305 cert_path=None, cert_password=None,
306 timestamp_url=None, extra_packages_script=None,
307 extra_wxs=None, extra_features=None):
336 def build_signed_installer(
337 source_dir: pathlib.Path,
338 python_exe: pathlib.Path,
339 name: str,
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 349 """Build an installer with signed executables."""
309 350
310 351 post_build_fn = make_post_build_signing_fn(
@@ -312,16 +353,27 b' def build_signed_installer(source_dir: p'
312 353 subject_name=subject_name,
313 354 cert_path=cert_path,
314 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,
318 msi_name=name.lower(), version=version,
359 info = build_installer(
360 source_dir,
361 python_exe=python_exe,
362 msi_name=name.lower(),
363 version=version,
319 364 post_build_fn=post_build_fn,
320 365 extra_packages_script=extra_packages_script,
321 extra_wxs=extra_wxs, extra_features=extra_features)
366 extra_wxs=extra_wxs,
367 extra_features=extra_features,
368 )
322 369
323 370 description = '%s %s' % (name, version)
324 371
325 sign_with_signtool(info['msi_path'], description,
326 subject_name=subject_name, cert_path=cert_path,
327 cert_password=cert_password, timestamp_url=timestamp_url)
372 sign_with_signtool(
373 info['msi_path'],
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 19 if __name__ == '__main__':
20 20 parser = argparse.ArgumentParser()
21 21
22 parser.add_argument('--python',
23 required=True,
24 help='path to python.exe to use')
25 parser.add_argument('--iscc',
26 help='path to iscc.exe to use')
27 parser.add_argument('--version',
22 parser.add_argument(
23 '--python', required=True, help='path to python.exe to use'
24 )
25 parser.add_argument('--iscc', help='path to iscc.exe to use')
26 parser.add_argument(
27 '--version',
28 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 32 args = parser.parse_args()
32 33
@@ -36,8 +37,11 b" if __name__ == '__main__':"
36 37 if args.iscc:
37 38 iscc = pathlib.Path(args.iscc)
38 39 else:
39 iscc = (pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Inno Setup 5' /
40 'ISCC.exe')
40 iscc = (
41 pathlib.Path(os.environ['ProgramFiles(x86)'])
42 / 'Inno Setup 5'
43 / 'ISCC.exe'
44 )
41 45
42 46 here = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
43 47 source_dir = here.parent.parent.parent
@@ -47,5 +51,10 b" if __name__ == '__main__':"
47 51
48 52 from hgpackaging.inno import build
49 53
50 build(source_dir, build_dir, pathlib.Path(args.python), iscc,
51 version=args.version)
54 build(
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 17 if __name__ == '__main__':
18 18 parser = argparse.ArgumentParser()
19 19
20 parser.add_argument('--name',
21 help='Application name',
22 default='Mercurial')
23 parser.add_argument('--python',
24 help='Path to Python executable to use',
25 required=True)
26 parser.add_argument('--sign-sn',
20 parser.add_argument('--name', help='Application name', default='Mercurial')
21 parser.add_argument(
22 '--python', help='Path to Python executable to use', required=True
23 )
24 parser.add_argument(
25 '--sign-sn',
27 26 help='Subject name (or fragment thereof) of certificate '
28 'to use for signing')
29 parser.add_argument('--sign-cert',
30 help='Path to certificate to use for signing')
31 parser.add_argument('--sign-password',
32 help='Password for signing certificate')
33 parser.add_argument('--sign-timestamp-url',
34 help='URL of timestamp server to use for signing')
35 parser.add_argument('--version',
36 help='Version string to use')
37 parser.add_argument('--extra-packages-script',
38 help=('Script to execute to include extra packages in '
39 'py2exe binary.'))
40 parser.add_argument('--extra-wxs',
41 help='CSV of path_to_wxs_file=working_dir_for_wxs_file')
42 parser.add_argument('--extra-features',
43 help=('CSV of extra feature names to include '
44 'in the installer from the extra wxs files'))
27 'to use for signing',
28 )
29 parser.add_argument(
30 '--sign-cert', help='Path to certificate to use for signing'
31 )
32 parser.add_argument(
33 '--sign-password', help='Password for signing certificate'
34 )
35 parser.add_argument(
36 '--sign-timestamp-url',
37 help='URL of timestamp server to use for signing',
38 )
39 parser.add_argument('--version', help='Version string to use')
40 parser.add_argument(
41 '--extra-packages-script',
42 help=(
43 'Script to execute to include extra packages in ' 'py2exe binary.'
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 57 args = parser.parse_args()
47 58
@@ -69,7 +80,8 b" if __name__ == '__main__':"
69 80 kwargs['extra_packages_script'] = args.extra_packages_script
70 81 if args.extra_wxs:
71 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 85 if args.extra_features:
74 86 kwargs['extra_features'] = args.extra_features.split(',')
75 87
@@ -44,18 +44,12 b' def plot(data, title=None):'
44 44 comb_plt = fig.add_subplot(211)
45 45 other_plt = fig.add_subplot(212)
46 46
47 comb_plt.plot(ary[0],
48 np.cumsum(ary[1]),
49 color='red',
50 linewidth=1,
51 label='comb')
47 comb_plt.plot(
48 ary[0], np.cumsum(ary[1]), color='red', linewidth=1, label='comb'
49 )
52 50
53 51 plots = []
54 p = other_plt.plot(ary[0],
55 ary[1],
56 color='red',
57 linewidth=1,
58 label='wall')
52 p = other_plt.plot(ary[0], ary[1], color='red', linewidth=1, label='wall')
59 53 plots.append(p)
60 54
61 55 colors = {
@@ -64,20 +58,24 b' def plot(data, title=None):'
64 58 1000: ('purple', 'xkcd:dark pink'),
65 59 }
66 60 for n, color in colors.items():
67 avg_n = np.convolve(ary[1], np.full(n, 1. / n), 'valid')
68 p = other_plt.plot(ary[0][n - 1:],
61 avg_n = np.convolve(ary[1], np.full(n, 1.0 / n), 'valid')
62 p = other_plt.plot(
63 ary[0][n - 1 :],
69 64 avg_n,
70 65 color=color[0],
71 66 linewidth=1,
72 label='avg time last %d' % n)
67 label='avg time last %d' % n,
68 )
73 69 plots.append(p)
74 70
75 71 med_n = scipy.signal.medfilt(ary[1], n + 1)
76 p = other_plt.plot(ary[0],
72 p = other_plt.plot(
73 ary[0],
77 74 med_n,
78 75 color=color[1],
79 76 linewidth=1,
80 label='median time last %d' % n)
77 label='median time last %d' % n,
78 )
81 79 plots.append(p)
82 80
83 81 formatter = mticker.ScalarFormatter()
@@ -108,6 +106,7 b' def plot(data, title=None):'
108 106 else:
109 107 legline.set_alpha(0.2)
110 108 fig.canvas.draw()
109
111 110 if title is not None:
112 111 fig.canvas.set_window_title(title)
113 112 fig.canvas.mpl_connect('pick_event', onpick)
This diff has been collapsed as it changes many lines, (1114 lines changed) Show them Hide them
@@ -93,6 +93,7 b' except ImportError:'
93 93 pass
94 94 try:
95 95 from mercurial import registrar # since 3.7 (or 37d50250b696)
96
96 97 dir(registrar) # forcibly load it
97 98 except ImportError:
98 99 registrar = None
@@ -118,11 +119,14 b' try:'
118 119 except ImportError:
119 120 profiling = None
120 121
122
121 123 def identity(a):
122 124 return a
123 125
126
124 127 try:
125 128 from mercurial import pycompat
129
126 130 getargspec = pycompat.getargspec # added to module after 4.5
127 131 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
128 132 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
@@ -135,6 +139,7 b' try:'
135 139 _maxint = sys.maxint
136 140 except (NameError, ImportError, AttributeError):
137 141 import inspect
142
138 143 getargspec = inspect.getargspec
139 144 _byteskwargs = identity
140 145 _bytestr = str
@@ -155,6 +160,7 b' except (NameError, AttributeError, Impor'
155 160
156 161 try:
157 162 from mercurial import logcmdutil
163
158 164 makelogtemplater = logcmdutil.maketemplater
159 165 except (AttributeError, ImportError):
160 166 try:
@@ -166,8 +172,12 b' except (AttributeError, ImportError):'
166 172 # define util.safehasattr forcibly, because util.safehasattr has been
167 173 # available since 1.9.3 (or 94b200a11cf7)
168 174 _undefined = object()
175
176
169 177 def safehasattr(thing, attr):
170 178 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
179
180
171 181 setattr(util, 'safehasattr', safehasattr)
172 182
173 183 # for "historical portability":
@@ -185,20 +195,28 b' else:'
185 195 # available, because commands.formatteropts has been available since
186 196 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
187 197 # available since 2.2 (or ae5f92e154d3)
188 formatteropts = getattr(cmdutil, "formatteropts",
189 getattr(commands, "formatteropts", []))
198 formatteropts = getattr(
199 cmdutil, "formatteropts", getattr(commands, "formatteropts", [])
200 )
190 201
191 202 # for "historical portability":
192 203 # use locally defined option list, if debugrevlogopts isn't available,
193 204 # because commands.debugrevlogopts has been available since 3.7 (or
194 205 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
195 206 # since 1.9 (or a79fea6b3e77).
196 revlogopts = getattr(cmdutil, "debugrevlogopts",
197 getattr(commands, "debugrevlogopts", [
198 (b'c', b'changelog', False, (b'open changelog')),
199 (b'm', b'manifest', False, (b'open manifest')),
200 (b'', b'dir', False, (b'open directory manifest')),
201 ]))
207 revlogopts = getattr(
208 cmdutil,
209 "debugrevlogopts",
210 getattr(
211 commands,
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 221 cmdtable = {}
204 222
@@ -208,6 +226,7 b' cmdtable = {}'
208 226 def parsealiases(cmd):
209 227 return cmd.split(b"|")
210 228
229
211 230 if safehasattr(registrar, 'command'):
212 231 command = registrar.command(cmdtable)
213 232 elif safehasattr(cmdutil, 'command'):
@@ -217,10 +236,13 b" elif safehasattr(cmdutil, 'command'):"
217 236 # wrap original cmdutil.command, because "norepo" option has
218 237 # been available since 3.1 (or 75a96326cecb)
219 238 _command = command
239
220 240 def command(name, options=(), synopsis=None, norepo=False):
221 241 if norepo:
222 242 commands.norepo += b' %s' % b' '.join(parsealiases(name))
223 243 return _command(name, list(options), synopsis)
244
245
224 246 else:
225 247 # for "historical portability":
226 248 # define "@command" annotation locally, because cmdutil.command
@@ -234,36 +256,51 b' else:'
234 256 if norepo:
235 257 commands.norepo += b' %s' % b' '.join(parsealiases(name))
236 258 return func
259
237 260 return decorator
238 261
262
239 263 try:
240 264 import mercurial.registrar
241 265 import mercurial.configitems
266
242 267 configtable = {}
243 268 configitem = mercurial.registrar.configitem(configtable)
244 configitem(b'perf', b'presleep',
269 configitem(
270 b'perf',
271 b'presleep',
245 272 default=mercurial.configitems.dynamicdefault,
246 273 experimental=True,
247 274 )
248 configitem(b'perf', b'stub',
275 configitem(
276 b'perf',
277 b'stub',
249 278 default=mercurial.configitems.dynamicdefault,
250 279 experimental=True,
251 280 )
252 configitem(b'perf', b'parentscount',
281 configitem(
282 b'perf',
283 b'parentscount',
253 284 default=mercurial.configitems.dynamicdefault,
254 285 experimental=True,
255 286 )
256 configitem(b'perf', b'all-timing',
287 configitem(
288 b'perf',
289 b'all-timing',
257 290 default=mercurial.configitems.dynamicdefault,
258 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 299 default=mercurial.configitems.dynamicdefault,
262 300 )
263 configitem(b'perf', b'profile-benchmark',
264 default=mercurial.configitems.dynamicdefault,
265 )
266 configitem(b'perf', b'run-limits',
301 configitem(
302 b'perf',
303 b'run-limits',
267 304 default=mercurial.configitems.dynamicdefault,
268 305 experimental=True,
269 306 )
@@ -272,42 +309,50 b' except (ImportError, AttributeError):'
272 309 except TypeError:
273 310 # compatibility fix for a11fd395e83f
274 311 # hg version: 5.2
275 configitem(b'perf', b'presleep',
276 default=mercurial.configitems.dynamicdefault,
312 configitem(
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',
279 default=mercurial.configitems.dynamicdefault,
321 configitem(
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 330 default=mercurial.configitems.dynamicdefault,
283 331 )
284 configitem(b'perf', b'all-timing',
285 default=mercurial.configitems.dynamicdefault,
286 )
287 configitem(b'perf', b'pre-run',
288 default=mercurial.configitems.dynamicdefault,
332 configitem(
333 b'perf', b'run-limits', default=mercurial.configitems.dynamicdefault,
289 334 )
290 configitem(b'perf', b'profile-benchmark',
291 default=mercurial.configitems.dynamicdefault,
292 )
293 configitem(b'perf', b'run-limits',
294 default=mercurial.configitems.dynamicdefault,
295 )
335
296 336
297 337 def getlen(ui):
298 338 if ui.configbool(b"perf", b"stub", False):
299 339 return lambda x: 1
300 340 return len
301 341
342
302 343 class noop(object):
303 344 """dummy context manager"""
345
304 346 def __enter__(self):
305 347 pass
348
306 349 def __exit__(self, *args):
307 350 pass
308 351
352
309 353 NOOPCTX = noop()
310 354
355
311 356 def gettimer(ui, opts=None):
312 357 """return a timer function and formatter: (timer, formatter)
313 358
@@ -338,31 +383,42 b' def gettimer(ui, opts=None):'
338 383 # define formatter locally, because ui.formatter has been
339 384 # available since 2.2 (or ae5f92e154d3)
340 385 from mercurial import node
386
341 387 class defaultformatter(object):
342 388 """Minimized composition of baseformatter and plainformatter
343 389 """
390
344 391 def __init__(self, ui, topic, opts):
345 392 self._ui = ui
346 393 if ui.debugflag:
347 394 self.hexfunc = node.hex
348 395 else:
349 396 self.hexfunc = node.short
397
350 398 def __nonzero__(self):
351 399 return False
400
352 401 __bool__ = __nonzero__
402
353 403 def startitem(self):
354 404 pass
405
355 406 def data(self, **data):
356 407 pass
408
357 409 def write(self, fields, deftext, *fielddata, **opts):
358 410 self._ui.write(deftext % fielddata, **opts)
411
359 412 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
360 413 if cond:
361 414 self._ui.write(deftext % fielddata, **opts)
415
362 416 def plain(self, text, **opts):
363 417 self._ui.write(text, **opts)
418
364 419 def end(self):
365 420 pass
421
366 422 fm = defaultformatter(ui, b'perf', opts)
367 423
368 424 # stub function, runs code only once instead of in a loop
@@ -379,20 +435,27 b' def gettimer(ui, opts=None):'
379 435 for item in limitspec:
380 436 parts = item.split(b'-', 1)
381 437 if len(parts) < 2:
382 ui.warn((b'malformatted run limit entry, missing "-": %s\n'
383 % item))
438 ui.warn((b'malformatted run limit entry, missing "-": %s\n' % item))
384 439 continue
385 440 try:
386 441 time_limit = float(_sysstr(parts[0]))
387 442 except ValueError as e:
388 ui.warn((b'malformatted run limit entry, %s: %s\n'
389 % (_bytestr(e), item)))
443 ui.warn(
444 (
445 b'malformatted run limit entry, %s: %s\n'
446 % (_bytestr(e), item)
447 )
448 )
390 449 continue
391 450 try:
392 451 run_limit = int(_sysstr(parts[1]))
393 452 except ValueError as e:
394 ui.warn((b'malformatted run limit entry, %s: %s\n'
395 % (_bytestr(e), item)))
453 ui.warn(
454 (
455 b'malformatted run limit entry, %s: %s\n'
456 % (_bytestr(e), item)
457 )
458 )
396 459 continue
397 460 limits.append((time_limit, run_limit))
398 461 if not limits:
@@ -404,15 +467,23 b' def gettimer(ui, opts=None):'
404 467 profiler = profiling.profile(ui)
405 468
406 469 prerun = getint(ui, b"perf", b"pre-run", 0)
407 t = functools.partial(_timer, fm, displayall=displayall, limits=limits,
408 prerun=prerun, profiler=profiler)
470 t = functools.partial(
471 _timer,
472 fm,
473 displayall=displayall,
474 limits=limits,
475 prerun=prerun,
476 profiler=profiler,
477 )
409 478 return t, fm
410 479
480
411 481 def stub_timer(fm, func, setup=None, title=None):
412 482 if setup is not None:
413 483 setup()
414 484 func()
415 485
486
416 487 @contextlib.contextmanager
417 488 def timeone():
418 489 r = []
@@ -431,8 +502,17 b' DEFAULTLIMITS = ('
431 502 (10.0, 3),
432 503 )
433 504
434 def _timer(fm, func, setup=None, title=None, displayall=False,
435 limits=DEFAULTLIMITS, prerun=0, profiler=None):
505
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 516 gc.collect()
437 517 results = []
438 518 begin = util.timer()
@@ -461,8 +541,8 b' def _timer(fm, func, setup=None, title=N'
461 541 keepgoing = False
462 542 break
463 543
464 formatone(fm, results, title=title, result=r,
465 displayall=displayall)
544 formatone(fm, results, title=title, result=r, displayall=displayall)
545
466 546
467 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 554 fm.write(b'title', b'! %s\n', title)
475 555 if result:
476 556 fm.write(b'result', b'! result: %s\n', result)
557
477 558 def display(role, entry):
478 559 prefix = b''
479 560 if role != b'best':
@@ -485,6 +566,7 b' def formatone(fm, timings, title=None, r'
485 566 fm.write(prefix + b'sys', b' sys %f', entry[2])
486 567 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
487 568 fm.plain(b'\n')
569
488 570 timings.sort()
489 571 min_val = timings[0]
490 572 display(b'best', min_val)
@@ -496,8 +578,10 b' def formatone(fm, timings, title=None, r'
496 578 median = timings[len(timings) // 2]
497 579 display(b'median', median)
498 580
581
499 582 # utilities for historical portability
500 583
584
501 585 def getint(ui, section, name, default):
502 586 # for "historical portability":
503 587 # ui.configint has been available since 1.9 (or fa2b596db182)
@@ -507,8 +591,10 b' def getint(ui, section, name, default):'
507 591 try:
508 592 return int(v)
509 593 except ValueError:
510 raise error.ConfigError((b"%s.%s is not an integer ('%s')")
511 % (section, name, v))
594 raise error.ConfigError(
595 b"%s.%s is not an integer ('%s')" % (section, name, v)
596 )
597
512 598
513 599 def safeattrsetter(obj, name, ignoremissing=False):
514 600 """Ensure that 'obj' has 'name' attribute before subsequent setattr
@@ -528,20 +614,29 b' def safeattrsetter(obj, name, ignoremiss'
528 614 if not util.safehasattr(obj, name):
529 615 if ignoremissing:
530 616 return None
531 raise error.Abort((b"missing attribute %s of %s might break assumption"
532 b" of performance measurement") % (name, obj))
617 raise error.Abort(
618 (
619 b"missing attribute %s of %s might break assumption"
620 b" of performance measurement"
621 )
622 % (name, obj)
623 )
533 624
534 625 origvalue = getattr(obj, _sysstr(name))
626
535 627 class attrutil(object):
536 628 def set(self, newvalue):
537 629 setattr(obj, _sysstr(name), newvalue)
630
538 631 def restore(self):
539 632 setattr(obj, _sysstr(name), origvalue)
540 633
541 634 return attrutil()
542 635
636
543 637 # utilities to examine each internal API changes
544 638
639
545 640 def getbranchmapsubsettable():
546 641 # for "historical portability":
547 642 # subsettable is defined in:
@@ -556,8 +651,11 b' def getbranchmapsubsettable():'
556 651 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
557 652 # branchmap and repoview modules exist, but subsettable attribute
558 653 # doesn't)
559 raise error.Abort((b"perfbranchmap not available with this Mercurial"),
560 hint=b"use 2.5 or later")
654 raise error.Abort(
655 b"perfbranchmap not available with this Mercurial",
656 hint=b"use 2.5 or later",
657 )
658
561 659
562 660 def getsvfs(repo):
563 661 """Return appropriate object to access files under .hg/store
@@ -570,6 +668,7 b' def getsvfs(repo):'
570 668 else:
571 669 return getattr(repo, 'sopener')
572 670
671
573 672 def getvfs(repo):
574 673 """Return appropriate object to access files under .hg
575 674 """
@@ -581,6 +680,7 b' def getvfs(repo):'
581 680 else:
582 681 return getattr(repo, 'opener')
583 682
683
584 684 def repocleartagscachefunc(repo):
585 685 """Return the function to clear tags cache according to repo internal API
586 686 """
@@ -593,6 +693,7 b' def repocleartagscachefunc(repo):'
593 693 # 98c867ac1330), and delattr() can't work in such case
594 694 if b'_tagscache' in vars(repo):
595 695 del repo.__dict__[b'_tagscache']
696
596 697 return clearcache
597 698
598 699 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
@@ -608,10 +709,12 b' def repocleartagscachefunc(repo):'
608 709 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
609 710 # in perftags() causes failure soon
610 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 715 # utilities to clear cache
614 716
717
615 718 def clearfilecache(obj, attrname):
616 719 unfiltered = getattr(obj, 'unfiltered', None)
617 720 if unfiltered is not None:
@@ -620,23 +723,32 b' def clearfilecache(obj, attrname):'
620 723 delattr(obj, attrname)
621 724 obj._filecache.pop(attrname, None)
622 725
726
623 727 def clearchangelog(repo):
624 728 if repo is not repo.unfiltered():
625 729 object.__setattr__(repo, r'_clcachekey', None)
626 730 object.__setattr__(repo, r'_clcache', None)
627 731 clearfilecache(repo.unfiltered(), 'changelog')
628 732
733
629 734 # perf commands
630 735
736
631 737 @command(b'perfwalk', formatteropts)
632 738 def perfwalk(ui, repo, *pats, **opts):
633 739 opts = _byteskwargs(opts)
634 740 timer, fm = gettimer(ui, opts)
635 741 m = scmutil.match(repo[None], pats, {})
636 timer(lambda: len(list(repo.dirstate.walk(m, subrepos=[], unknown=True,
637 ignored=False))))
742 timer(
743 lambda: len(
744 list(
745 repo.dirstate.walk(m, subrepos=[], unknown=True, ignored=False)
746 )
747 )
748 )
638 749 fm.end()
639 750
751
640 752 @command(b'perfannotate', formatteropts)
641 753 def perfannotate(ui, repo, f, **opts):
642 754 opts = _byteskwargs(opts)
@@ -645,9 +757,12 b' def perfannotate(ui, repo, f, **opts):'
645 757 timer(lambda: len(fc.annotate(True)))
646 758 fm.end()
647 759
648 @command(b'perfstatus',
649 [(b'u', b'unknown', False,
650 b'ask status to look for unknown files')] + formatteropts)
760
761 @command(
762 b'perfstatus',
763 [(b'u', b'unknown', False, b'ask status to look for unknown files')]
764 + formatteropts,
765 )
651 766 def perfstatus(ui, repo, **opts):
652 767 opts = _byteskwargs(opts)
653 768 #m = match.always(repo.root, repo.getcwd())
@@ -657,6 +772,7 b' def perfstatus(ui, repo, **opts):'
657 772 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
658 773 fm.end()
659 774
775
660 776 @command(b'perfaddremove', formatteropts)
661 777 def perfaddremove(ui, repo, **opts):
662 778 opts = _byteskwargs(opts)
@@ -675,71 +791,89 b' def perfaddremove(ui, repo, **opts):'
675 791 repo.ui.quiet = oldquiet
676 792 fm.end()
677 793
794
678 795 def clearcaches(cl):
679 796 # behave somewhat consistently across internal API changes
680 797 if util.safehasattr(cl, b'clearcaches'):
681 798 cl.clearcaches()
682 799 elif util.safehasattr(cl, b'_nodecache'):
683 800 from mercurial.node import nullid, nullrev
801
684 802 cl._nodecache = {nullid: nullrev}
685 803 cl._nodepos = None
686 804
805
687 806 @command(b'perfheads', formatteropts)
688 807 def perfheads(ui, repo, **opts):
689 808 """benchmark the computation of a changelog heads"""
690 809 opts = _byteskwargs(opts)
691 810 timer, fm = gettimer(ui, opts)
692 811 cl = repo.changelog
812
693 813 def s():
694 814 clearcaches(cl)
815
695 816 def d():
696 817 len(cl.headrevs())
818
697 819 timer(d, setup=s)
698 820 fm.end()
699 821
700 @command(b'perftags', formatteropts+
701 [
702 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
703 ])
822
823 @command(
824 b'perftags',
825 formatteropts
826 + [(b'', b'clear-revlogs', False, b'refresh changelog and manifest'),],
827 )
704 828 def perftags(ui, repo, **opts):
705 829 opts = _byteskwargs(opts)
706 830 timer, fm = gettimer(ui, opts)
707 831 repocleartagscache = repocleartagscachefunc(repo)
708 832 clearrevlogs = opts[b'clear_revlogs']
833
709 834 def s():
710 835 if clearrevlogs:
711 836 clearchangelog(repo)
712 837 clearfilecache(repo.unfiltered(), 'manifest')
713 838 repocleartagscache()
839
714 840 def t():
715 841 return len(repo.tags())
842
716 843 timer(t, setup=s)
717 844 fm.end()
718 845
846
719 847 @command(b'perfancestors', formatteropts)
720 848 def perfancestors(ui, repo, **opts):
721 849 opts = _byteskwargs(opts)
722 850 timer, fm = gettimer(ui, opts)
723 851 heads = repo.changelog.headrevs()
852
724 853 def d():
725 854 for a in repo.changelog.ancestors(heads):
726 855 pass
856
727 857 timer(d)
728 858 fm.end()
729 859
860
730 861 @command(b'perfancestorset', formatteropts)
731 862 def perfancestorset(ui, repo, revset, **opts):
732 863 opts = _byteskwargs(opts)
733 864 timer, fm = gettimer(ui, opts)
734 865 revs = repo.revs(revset)
735 866 heads = repo.changelog.headrevs()
867
736 868 def d():
737 869 s = repo.changelog.ancestors(heads)
738 870 for rev in revs:
739 871 rev in s
872
740 873 timer(d)
741 874 fm.end()
742 875
876
743 877 @command(b'perfdiscovery', formatteropts, b'PATH')
744 878 def perfdiscovery(ui, repo, path, **opts):
745 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 885 def s():
752 886 repos[1] = hg.peer(ui, opts, path)
887
753 888 def d():
754 889 setdiscovery.findcommonheads(ui, *repos)
890
755 891 timer(d, setup=s)
756 892 fm.end()
757 893
758 @command(b'perfbookmarks', formatteropts +
759 [
760 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
761 ])
894
895 @command(
896 b'perfbookmarks',
897 formatteropts
898 + [(b'', b'clear-revlogs', False, b'refresh changelog and manifest'),],
899 )
762 900 def perfbookmarks(ui, repo, **opts):
763 901 """benchmark parsing bookmarks from disk to memory"""
764 902 opts = _byteskwargs(opts)
765 903 timer, fm = gettimer(ui, opts)
766 904
767 905 clearrevlogs = opts[b'clear_revlogs']
906
768 907 def s():
769 908 if clearrevlogs:
770 909 clearchangelog(repo)
771 910 clearfilecache(repo, b'_bookmarks')
911
772 912 def d():
773 913 repo._bookmarks
914
774 915 timer(d, setup=s)
775 916 fm.end()
776 917
918
777 919 @command(b'perfbundleread', formatteropts, b'BUNDLE')
778 920 def perfbundleread(ui, repo, bundlepath, **opts):
779 921 """Benchmark reading of bundle files.
@@ -863,25 +1005,32 b' def perfbundleread(ui, repo, bundlepath,'
863 1005 bundle = exchange.readbundle(ui, fh, bundlepath)
864 1006
865 1007 if isinstance(bundle, changegroup.cg1unpacker):
866 benches.extend([
1008 benches.extend(
1009 [
867 1010 (makebench(deltaiter), b'cg1 deltaiter()'),
868 1011 (makebench(iterchunks), b'cg1 getchunks()'),
869 1012 (makereadnbytes(8192), b'cg1 read(8k)'),
870 1013 (makereadnbytes(16384), b'cg1 read(16k)'),
871 1014 (makereadnbytes(32768), b'cg1 read(32k)'),
872 1015 (makereadnbytes(131072), b'cg1 read(128k)'),
873 ])
1016 ]
1017 )
874 1018 elif isinstance(bundle, bundle2.unbundle20):
875 benches.extend([
1019 benches.extend(
1020 [
876 1021 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
877 1022 (makebench(iterparts), b'bundle2 iterparts()'),
878 (makebench(iterpartsseekable), b'bundle2 iterparts() seekable'),
1023 (
1024 makebench(iterpartsseekable),
1025 b'bundle2 iterparts() seekable',
1026 ),
879 1027 (makebench(seek), b'bundle2 part seek()'),
880 1028 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
881 1029 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
882 1030 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
883 1031 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
884 ])
1032 ]
1033 )
885 1034 elif isinstance(bundle, streamclone.streamcloneapplier):
886 1035 raise error.Abort(b'stream clone bundles not supported')
887 1036 else:
@@ -892,9 +1041,15 b' def perfbundleread(ui, repo, bundlepath,'
892 1041 timer(fn, title=title)
893 1042 fm.end()
894 1043
895 @command(b'perfchangegroupchangelog', formatteropts +
896 [(b'', b'cgversion', b'02', b'changegroup version'),
897 (b'r', b'rev', b'', b'revisions to add to changegroup')])
1044
1045 @command(
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 1053 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
899 1054 """Benchmark producing a changelog group for a changegroup.
900 1055
@@ -923,77 +1078,96 b' def perfchangegroupchangelog(ui, repo, c'
923 1078
924 1079 fm.end()
925 1080
1081
926 1082 @command(b'perfdirs', formatteropts)
927 1083 def perfdirs(ui, repo, **opts):
928 1084 opts = _byteskwargs(opts)
929 1085 timer, fm = gettimer(ui, opts)
930 1086 dirstate = repo.dirstate
931 1087 b'a' in dirstate
1088
932 1089 def d():
933 1090 dirstate.hasdir(b'a')
934 1091 del dirstate._map._dirs
1092
935 1093 timer(d)
936 1094 fm.end()
937 1095
1096
938 1097 @command(b'perfdirstate', formatteropts)
939 1098 def perfdirstate(ui, repo, **opts):
940 1099 opts = _byteskwargs(opts)
941 1100 timer, fm = gettimer(ui, opts)
942 1101 b"a" in repo.dirstate
1102
943 1103 def d():
944 1104 repo.dirstate.invalidate()
945 1105 b"a" in repo.dirstate
1106
946 1107 timer(d)
947 1108 fm.end()
948 1109
1110
949 1111 @command(b'perfdirstatedirs', formatteropts)
950 1112 def perfdirstatedirs(ui, repo, **opts):
951 1113 opts = _byteskwargs(opts)
952 1114 timer, fm = gettimer(ui, opts)
953 1115 b"a" in repo.dirstate
1116
954 1117 def d():
955 1118 repo.dirstate.hasdir(b"a")
956 1119 del repo.dirstate._map._dirs
1120
957 1121 timer(d)
958 1122 fm.end()
959 1123
1124
960 1125 @command(b'perfdirstatefoldmap', formatteropts)
961 1126 def perfdirstatefoldmap(ui, repo, **opts):
962 1127 opts = _byteskwargs(opts)
963 1128 timer, fm = gettimer(ui, opts)
964 1129 dirstate = repo.dirstate
965 1130 b'a' in dirstate
1131
966 1132 def d():
967 1133 dirstate._map.filefoldmap.get(b'a')
968 1134 del dirstate._map.filefoldmap
1135
969 1136 timer(d)
970 1137 fm.end()
971 1138
1139
972 1140 @command(b'perfdirfoldmap', formatteropts)
973 1141 def perfdirfoldmap(ui, repo, **opts):
974 1142 opts = _byteskwargs(opts)
975 1143 timer, fm = gettimer(ui, opts)
976 1144 dirstate = repo.dirstate
977 1145 b'a' in dirstate
1146
978 1147 def d():
979 1148 dirstate._map.dirfoldmap.get(b'a')
980 1149 del dirstate._map.dirfoldmap
981 1150 del dirstate._map._dirs
1151
982 1152 timer(d)
983 1153 fm.end()
984 1154
1155
985 1156 @command(b'perfdirstatewrite', formatteropts)
986 1157 def perfdirstatewrite(ui, repo, **opts):
987 1158 opts = _byteskwargs(opts)
988 1159 timer, fm = gettimer(ui, opts)
989 1160 ds = repo.dirstate
990 1161 b"a" in ds
1162
991 1163 def d():
992 1164 ds._dirty = True
993 1165 ds.write(repo.currenttransaction())
1166
994 1167 timer(d)
995 1168 fm.end()
996 1169
1170
997 1171 def _getmergerevs(repo, opts):
998 1172 """parse command argument to return rev involved in merge
999 1173
@@ -1016,44 +1190,64 b' def _getmergerevs(repo, opts):'
1016 1190 ancestor = wctx.ancestor(rctx)
1017 1191 return (wctx, rctx, ancestor)
1018 1192
1019 @command(b'perfmergecalculate',
1193
1194 @command(
1195 b'perfmergecalculate',
1020 1196 [
1021 1197 (b'r', b'rev', b'.', b'rev to merge against'),
1022 1198 (b'', b'from', b'', b'rev to merge from'),
1023 1199 (b'', b'base', b'', b'the revision to use as base'),
1024 ] + formatteropts)
1200 ]
1201 + formatteropts,
1202 )
1025 1203 def perfmergecalculate(ui, repo, **opts):
1026 1204 opts = _byteskwargs(opts)
1027 1205 timer, fm = gettimer(ui, opts)
1028 1206
1029 1207 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1208
1030 1209 def d():
1031 1210 # acceptremote is True because we don't want prompts in the middle of
1032 1211 # our benchmark
1033 merge.calculateupdates(repo, wctx, rctx, [ancestor], branchmerge=False,
1034 force=False, acceptremote=True,
1035 followcopies=True)
1212 merge.calculateupdates(
1213 repo,
1214 wctx,
1215 rctx,
1216 [ancestor],
1217 branchmerge=False,
1218 force=False,
1219 acceptremote=True,
1220 followcopies=True,
1221 )
1222
1036 1223 timer(d)
1037 1224 fm.end()
1038 1225
1039 @command(b'perfmergecopies',
1226
1227 @command(
1228 b'perfmergecopies',
1040 1229 [
1041 1230 (b'r', b'rev', b'.', b'rev to merge against'),
1042 1231 (b'', b'from', b'', b'rev to merge from'),
1043 1232 (b'', b'base', b'', b'the revision to use as base'),
1044 ] + formatteropts)
1233 ]
1234 + formatteropts,
1235 )
1045 1236 def perfmergecopies(ui, repo, **opts):
1046 1237 """measure runtime of `copies.mergecopies`"""
1047 1238 opts = _byteskwargs(opts)
1048 1239 timer, fm = gettimer(ui, opts)
1049 1240 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1241
1050 1242 def d():
1051 1243 # acceptremote is True because we don't want prompts in the middle of
1052 1244 # our benchmark
1053 1245 copies.mergecopies(repo, wctx, rctx, ancestor)
1246
1054 1247 timer(d)
1055 1248 fm.end()
1056 1249
1250
1057 1251 @command(b'perfpathcopies', [], b"REV REV")
1058 1252 def perfpathcopies(ui, repo, rev1, rev2, **opts):
1059 1253 """benchmark the copy tracing logic"""
@@ -1061,20 +1255,26 b' def perfpathcopies(ui, repo, rev1, rev2,'
1061 1255 timer, fm = gettimer(ui, opts)
1062 1256 ctx1 = scmutil.revsingle(repo, rev1, rev1)
1063 1257 ctx2 = scmutil.revsingle(repo, rev2, rev2)
1258
1064 1259 def d():
1065 1260 copies.pathcopies(ctx1, ctx2)
1261
1066 1262 timer(d)
1067 1263 fm.end()
1068 1264
1069 @command(b'perfphases',
1070 [(b'', b'full', False, b'include file reading time too'),
1071 ], b"")
1265
1266 @command(
1267 b'perfphases',
1268 [(b'', b'full', False, b'include file reading time too'),],
1269 b"",
1270 )
1072 1271 def perfphases(ui, repo, **opts):
1073 1272 """benchmark phasesets computation"""
1074 1273 opts = _byteskwargs(opts)
1075 1274 timer, fm = gettimer(ui, opts)
1076 1275 _phases = repo._phasecache
1077 1276 full = opts.get(b'full')
1277
1078 1278 def d():
1079 1279 phases = _phases
1080 1280 if full:
@@ -1082,30 +1282,32 b' def perfphases(ui, repo, **opts):'
1082 1282 phases = repo._phasecache
1083 1283 phases.invalidate()
1084 1284 phases.loadphaserevs(repo)
1285
1085 1286 timer(d)
1086 1287 fm.end()
1087 1288
1088 @command(b'perfphasesremote',
1089 [], b"[DEST]")
1289
1290 @command(b'perfphasesremote', [], b"[DEST]")
1090 1291 def perfphasesremote(ui, repo, dest=None, **opts):
1091 1292 """benchmark time needed to analyse phases of the remote server"""
1092 from mercurial.node import (
1093 bin,
1094 )
1293 from mercurial.node import bin
1095 1294 from mercurial import (
1096 1295 exchange,
1097 1296 hg,
1098 1297 phases,
1099 1298 )
1299
1100 1300 opts = _byteskwargs(opts)
1101 1301 timer, fm = gettimer(ui, opts)
1102 1302
1103 1303 path = ui.paths.getpath(dest, default=(b'default-push', b'default'))
1104 1304 if not path:
1105 raise error.Abort((b'default repository not configured!'),
1106 hint=(b"see 'hg help config.paths'"))
1305 raise error.Abort(
1306 b'default repository not configured!',
1307 hint=b"see 'hg help config.paths'",
1308 )
1107 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 1311 other = hg.peer(repo, opts, dest)
1110 1312
1111 1313 # easier to perform discovery through the operation
@@ -1115,14 +1317,15 b' def perfphasesremote(ui, repo, dest=None'
1115 1317 remotesubset = op.fallbackheads
1116 1318
1117 1319 with other.commandexecutor() as e:
1118 remotephases = e.callcommand(b'listkeys',
1119 {b'namespace': b'phases'}).result()
1320 remotephases = e.callcommand(
1321 b'listkeys', {b'namespace': b'phases'}
1322 ).result()
1120 1323 del other
1121 1324 publishing = remotephases.get(b'publishing', False)
1122 1325 if publishing:
1123 ui.status((b'publishing: yes\n'))
1326 ui.status(b'publishing: yes\n')
1124 1327 else:
1125 ui.status((b'publishing: no\n'))
1328 ui.status(b'publishing: no\n')
1126 1329
1127 1330 nodemap = repo.changelog.nodemap
1128 1331 nonpublishroots = 0
@@ -1132,19 +1335,25 b' def perfphasesremote(ui, repo, dest=None'
1132 1335 node = bin(nhex)
1133 1336 if node in nodemap and int(phase):
1134 1337 nonpublishroots += 1
1135 ui.status((b'number of roots: %d\n') % len(remotephases))
1136 ui.status((b'number of known non public roots: %d\n') % nonpublishroots)
1338 ui.status(b'number of roots: %d\n' % len(remotephases))
1339 ui.status(b'number of known non public roots: %d\n' % nonpublishroots)
1340
1137 1341 def d():
1138 phases.remotephasessummary(repo,
1139 remotesubset,
1140 remotephases)
1342 phases.remotephasessummary(repo, remotesubset, remotephases)
1343
1141 1344 timer(d)
1142 1345 fm.end()
1143 1346
1144 @command(b'perfmanifest',[
1347
1348 @command(
1349 b'perfmanifest',
1350 [
1145 1351 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1146 1352 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1147 ] + formatteropts, b'REV|NODE')
1353 ]
1354 + formatteropts,
1355 b'REV|NODE',
1356 )
1148 1357 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1149 1358 """benchmark the time to read a manifest from disk and return a usable
1150 1359 dict-like object
@@ -1169,25 +1378,32 b' def perfmanifest(ui, repo, rev, manifest'
1169 1378 else:
1170 1379 t = repo.manifestlog._revlog.lookup(rev)
1171 1380 except ValueError:
1172 raise error.Abort(b'manifest revision must be integer or full '
1173 b'node')
1381 raise error.Abort(
1382 b'manifest revision must be integer or full ' b'node'
1383 )
1384
1174 1385 def d():
1175 1386 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1176 1387 repo.manifestlog[t].read()
1388
1177 1389 timer(d)
1178 1390 fm.end()
1179 1391
1392
1180 1393 @command(b'perfchangeset', formatteropts)
1181 1394 def perfchangeset(ui, repo, rev, **opts):
1182 1395 opts = _byteskwargs(opts)
1183 1396 timer, fm = gettimer(ui, opts)
1184 1397 n = scmutil.revsingle(repo, rev).node()
1398
1185 1399 def d():
1186 1400 repo.changelog.read(n)
1187 1401 #repo.changelog._cache = None
1402
1188 1403 timer(d)
1189 1404 fm.end()
1190 1405
1406
1191 1407 @command(b'perfignore', formatteropts)
1192 1408 def perfignore(ui, repo, **opts):
1193 1409 """benchmark operation related to computing ignore"""
@@ -1205,10 +1421,15 b' def perfignore(ui, repo, **opts):'
1205 1421 timer(runone, setup=setupone, title=b"load")
1206 1422 fm.end()
1207 1423
1208 @command(b'perfindex', [
1424
1425 @command(
1426 b'perfindex',
1427 [
1209 1428 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1210 1429 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1211 ] + formatteropts)
1430 ]
1431 + formatteropts,
1432 )
1212 1433 def perfindex(ui, repo, **opts):
1213 1434 """benchmark index creation time followed by a lookup
1214 1435
@@ -1231,6 +1452,7 b' def perfindex(ui, repo, **opts):'
1231 1452 It is not currently possible to check for lookup of a missing node. For
1232 1453 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1233 1454 import mercurial.revlog
1455
1234 1456 opts = _byteskwargs(opts)
1235 1457 timer, fm = gettimer(ui, opts)
1236 1458 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
@@ -1249,20 +1471,28 b' def perfindex(ui, repo, **opts):'
1249 1471 # find the filecache func directly
1250 1472 # This avoid polluting the benchmark with the filecache logic
1251 1473 makecl = unfi.__class__.changelog.func
1474
1252 1475 def setup():
1253 1476 # probably not necessary, but for good measure
1254 1477 clearchangelog(unfi)
1478
1255 1479 def d():
1256 1480 cl = makecl(unfi)
1257 1481 for n in nodes:
1258 1482 cl.rev(n)
1483
1259 1484 timer(d, setup=setup)
1260 1485 fm.end()
1261 1486
1262 @command(b'perfnodemap', [
1487
1488 @command(
1489 b'perfnodemap',
1490 [
1263 1491 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1264 1492 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1265 ] + formatteropts)
1493 ]
1494 + formatteropts,
1495 )
1266 1496 def perfnodemap(ui, repo, **opts):
1267 1497 """benchmark the time necessary to look up revision from a cold nodemap
1268 1498
@@ -1281,6 +1511,7 b' def perfnodemap(ui, repo, **opts):'
1281 1511 hexlookup, prefix lookup and missing lookup would also be valuable.
1282 1512 """
1283 1513 import mercurial.revlog
1514
1284 1515 opts = _byteskwargs(opts)
1285 1516 timer, fm = gettimer(ui, opts)
1286 1517 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
@@ -1298,6 +1529,7 b' def perfnodemap(ui, repo, **opts):'
1298 1529
1299 1530 # use a list to pass reference to a nodemap from one closure to the next
1300 1531 nodeget = [None]
1532
1301 1533 def setnodeget():
1302 1534 # probably not necessary, but for good measure
1303 1535 clearchangelog(unfi)
@@ -1310,28 +1542,35 b' def perfnodemap(ui, repo, **opts):'
1310 1542
1311 1543 setup = None
1312 1544 if clearcaches:
1545
1313 1546 def setup():
1314 1547 setnodeget()
1548
1315 1549 else:
1316 1550 setnodeget()
1317 1551 d() # prewarm the data structure
1318 1552 timer(d, setup=setup)
1319 1553 fm.end()
1320 1554
1555
1321 1556 @command(b'perfstartup', formatteropts)
1322 1557 def perfstartup(ui, repo, **opts):
1323 1558 opts = _byteskwargs(opts)
1324 1559 timer, fm = gettimer(ui, opts)
1560
1325 1561 def d():
1326 1562 if os.name != r'nt':
1327 os.system(b"HGRCPATH= %s version -q > /dev/null" %
1328 fsencode(sys.argv[0]))
1563 os.system(
1564 b"HGRCPATH= %s version -q > /dev/null" % fsencode(sys.argv[0])
1565 )
1329 1566 else:
1330 1567 os.environ[r'HGRCPATH'] = r' '
1331 1568 os.system(r"%s version -q > NUL" % sys.argv[0])
1569
1332 1570 timer(d)
1333 1571 fm.end()
1334 1572
1573
1335 1574 @command(b'perfparents', formatteropts)
1336 1575 def perfparents(ui, repo, **opts):
1337 1576 """benchmark the time necessary to fetch one changeset's parents.
@@ -1350,33 +1589,42 b' def perfparents(ui, repo, **opts):'
1350 1589 raise error.Abort(b"repo needs %d commits for this test" % count)
1351 1590 repo = repo.unfiltered()
1352 1591 nl = [repo.changelog.node(i) for i in _xrange(count)]
1592
1353 1593 def d():
1354 1594 for n in nl:
1355 1595 repo.changelog.parents(n)
1596
1356 1597 timer(d)
1357 1598 fm.end()
1358 1599
1600
1359 1601 @command(b'perfctxfiles', formatteropts)
1360 1602 def perfctxfiles(ui, repo, x, **opts):
1361 1603 opts = _byteskwargs(opts)
1362 1604 x = int(x)
1363 1605 timer, fm = gettimer(ui, opts)
1606
1364 1607 def d():
1365 1608 len(repo[x].files())
1609
1366 1610 timer(d)
1367 1611 fm.end()
1368 1612
1613
1369 1614 @command(b'perfrawfiles', formatteropts)
1370 1615 def perfrawfiles(ui, repo, x, **opts):
1371 1616 opts = _byteskwargs(opts)
1372 1617 x = int(x)
1373 1618 timer, fm = gettimer(ui, opts)
1374 1619 cl = repo.changelog
1620
1375 1621 def d():
1376 1622 len(cl.read(x)[3])
1623
1377 1624 timer(d)
1378 1625 fm.end()
1379 1626
1627
1380 1628 @command(b'perflookup', formatteropts)
1381 1629 def perflookup(ui, repo, rev, **opts):
1382 1630 opts = _byteskwargs(opts)
@@ -1384,10 +1632,15 b' def perflookup(ui, repo, rev, **opts):'
1384 1632 timer(lambda: len(repo.lookup(rev)))
1385 1633 fm.end()
1386 1634
1387 @command(b'perflinelogedits',
1388 [(b'n', b'edits', 10000, b'number of edits'),
1635
1636 @command(
1637 b'perflinelogedits',
1638 [
1639 (b'n', b'edits', 10000, b'number of edits'),
1389 1640 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
1390 ], norepo=True)
1641 ],
1642 norepo=True,
1643 )
1391 1644 def perflinelogedits(ui, **opts):
1392 1645 from mercurial import linelog
1393 1646
@@ -1418,6 +1671,7 b' def perflinelogedits(ui, **opts):'
1418 1671 timer(d)
1419 1672 fm.end()
1420 1673
1674
1421 1675 @command(b'perfrevrange', formatteropts)
1422 1676 def perfrevrange(ui, repo, *specs, **opts):
1423 1677 opts = _byteskwargs(opts)
@@ -1426,34 +1680,44 b' def perfrevrange(ui, repo, *specs, **opt'
1426 1680 timer(lambda: len(revrange(repo, specs)))
1427 1681 fm.end()
1428 1682
1683
1429 1684 @command(b'perfnodelookup', formatteropts)
1430 1685 def perfnodelookup(ui, repo, rev, **opts):
1431 1686 opts = _byteskwargs(opts)
1432 1687 timer, fm = gettimer(ui, opts)
1433 1688 import mercurial.revlog
1689
1434 1690 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
1435 1691 n = scmutil.revsingle(repo, rev).node()
1436 1692 cl = mercurial.revlog.revlog(getsvfs(repo), b"00changelog.i")
1693
1437 1694 def d():
1438 1695 cl.rev(n)
1439 1696 clearcaches(cl)
1697
1440 1698 timer(d)
1441 1699 fm.end()
1442 1700
1443 @command(b'perflog',
1444 [(b'', b'rename', False, b'ask log to follow renames')
1445 ] + formatteropts)
1701
1702 @command(
1703 b'perflog',
1704 [(b'', b'rename', False, b'ask log to follow renames')] + formatteropts,
1705 )
1446 1706 def perflog(ui, repo, rev=None, **opts):
1447 1707 opts = _byteskwargs(opts)
1448 1708 if rev is None:
1449 1709 rev=[]
1450 1710 timer, fm = gettimer(ui, opts)
1451 1711 ui.pushbuffer()
1452 timer(lambda: commands.log(ui, repo, rev=rev, date=b'', user=b'',
1453 copies=opts.get(b'rename')))
1712 timer(
1713 lambda: commands.log(
1714 ui, repo, rev=rev, date=b'', user=b'', copies=opts.get(b'rename')
1715 )
1716 )
1454 1717 ui.popbuffer()
1455 1718 fm.end()
1456 1719
1720
1457 1721 @command(b'perfmoonwalk', formatteropts)
1458 1722 def perfmoonwalk(ui, repo, **opts):
1459 1723 """benchmark walking the changelog backwards
@@ -1462,21 +1726,27 b' def perfmoonwalk(ui, repo, **opts):'
1462 1726 """
1463 1727 opts = _byteskwargs(opts)
1464 1728 timer, fm = gettimer(ui, opts)
1729
1465 1730 def moonwalk():
1466 1731 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
1467 1732 ctx = repo[i]
1468 1733 ctx.branch() # read changelog data (in addition to the index)
1734
1469 1735 timer(moonwalk)
1470 1736 fm.end()
1471 1737
1472 @command(b'perftemplating',
1473 [(b'r', b'rev', [], b'revisions to run the template on'),
1474 ] + formatteropts)
1738
1739 @command(
1740 b'perftemplating',
1741 [(b'r', b'rev', [], b'revisions to run the template on'),] + formatteropts,
1742 )
1475 1743 def perftemplating(ui, repo, testedtemplate=None, **opts):
1476 1744 """test the rendering time of a given template"""
1477 1745 if makelogtemplater is None:
1478 raise error.Abort((b"perftemplating not available with this Mercurial"),
1479 hint=b"use 4.3 or later")
1746 raise error.Abort(
1747 b"perftemplating not available with this Mercurial",
1748 hint=b"use 4.3 or later",
1749 )
1480 1750
1481 1751 opts = _byteskwargs(opts)
1482 1752
@@ -1488,11 +1758,14 b' def perftemplating(ui, repo, testedtempl'
1488 1758 revs = [b'all()']
1489 1759 revs = list(scmutil.revrange(repo, revs))
1490 1760
1491 defaulttemplate = (b'{date|shortdate} [{rev}:{node|short}]'
1492 b' {author|person}: {desc|firstline}\n')
1761 defaulttemplate = (
1762 b'{date|shortdate} [{rev}:{node|short}]'
1763 b' {author|person}: {desc|firstline}\n'
1764 )
1493 1765 if testedtemplate is None:
1494 1766 testedtemplate = defaulttemplate
1495 1767 displayer = makelogtemplater(nullui, repo, testedtemplate)
1768
1496 1769 def format():
1497 1770 for r in revs:
1498 1771 ctx = repo[r]
@@ -1503,6 +1776,7 b' def perftemplating(ui, repo, testedtempl'
1503 1776 timer(format)
1504 1777 fm.end()
1505 1778
1779
1506 1780 def _displaystats(ui, opts, entries, data):
1507 1781 pass
1508 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 1823 fm.plain('%s: %s\n' % (l, stats[l]))
1550 1824 fm.end()
1551 1825
1552 @command(b'perfhelper-mergecopies', formatteropts +
1553 [
1826
1827 @command(
1828 b'perfhelper-mergecopies',
1829 formatteropts
1830 + [
1554 1831 (b'r', b'revs', [], b'restrict search to these revisions'),
1555 1832 (b'', b'timing', False, b'provides extra data (costly)'),
1556 1833 (b'', b'stats', False, b'provides statistic about the measured data'),
1557 ])
1834 ],
1835 )
1558 1836 def perfhelpermergecopies(ui, repo, revs=[], **opts):
1559 1837 """find statistics about potential parameters for `perfmergecopies`
1560 1838
@@ -1591,8 +1869,11 b' def perfhelpermergecopies(ui, repo, revs'
1591 1869 ("total.time", "%(time)12.3f"),
1592 1870 ]
1593 1871 if not dotiming:
1594 output_template = [i for i in output_template
1595 if not ('time' in i[0] or 'renames' in i[0])]
1872 output_template = [
1873 i
1874 for i in output_template
1875 if not ('time' in i[0] or 'renames' in i[0])
1876 ]
1596 1877 header_names = [h for (h, v) in output_template]
1597 1878 output = ' '.join([v for (h, v) in output_template]) + '\n'
1598 1879 header = ' '.join(['%12s'] * len(header_names)) + '\n'
@@ -1634,27 +1915,19 b' def perfhelpermergecopies(ui, repo, revs'
1634 1915 }
1635 1916 if dostats:
1636 1917 if p1missing:
1637 alldata['nbrevs'].append((
1638 data['p1.nbrevs'],
1639 b.hex(),
1640 p1.hex()
1641 ))
1642 alldata['nbmissingfiles'].append((
1643 data['p1.nbmissingfiles'],
1644 b.hex(),
1645 p1.hex()
1646 ))
1918 alldata['nbrevs'].append(
1919 (data['p1.nbrevs'], b.hex(), p1.hex())
1920 )
1921 alldata['nbmissingfiles'].append(
1922 (data['p1.nbmissingfiles'], b.hex(), p1.hex())
1923 )
1647 1924 if p2missing:
1648 alldata['nbrevs'].append((
1649 data['p2.nbrevs'],
1650 b.hex(),
1651 p2.hex()
1652 ))
1653 alldata['nbmissingfiles'].append((
1654 data['p2.nbmissingfiles'],
1655 b.hex(),
1656 p2.hex()
1657 ))
1925 alldata['nbrevs'].append(
1926 (data['p2.nbrevs'], b.hex(), p2.hex())
1927 )
1928 alldata['nbmissingfiles'].append(
1929 (data['p2.nbmissingfiles'], b.hex(), p2.hex())
1930 )
1658 1931 if dotiming:
1659 1932 begin = util.timer()
1660 1933 mergedata = copies.mergecopies(repo, p1, p2, b)
@@ -1682,40 +1955,31 b' def perfhelpermergecopies(ui, repo, revs'
1682 1955
1683 1956 if dostats:
1684 1957 if p1missing:
1685 alldata['parentnbrenames'].append((
1686 data['p1.renamedfiles'],
1687 b.hex(),
1688 p1.hex()
1689 ))
1690 alldata['parenttime'].append((
1691 data['p1.time'],
1692 b.hex(),
1693 p1.hex()
1694 ))
1958 alldata['parentnbrenames'].append(
1959 (data['p1.renamedfiles'], b.hex(), p1.hex())
1960 )
1961 alldata['parenttime'].append(
1962 (data['p1.time'], b.hex(), p1.hex())
1963 )
1695 1964 if p2missing:
1696 alldata['parentnbrenames'].append((
1697 data['p2.renamedfiles'],
1698 b.hex(),
1699 p2.hex()
1700 ))
1701 alldata['parenttime'].append((
1702 data['p2.time'],
1703 b.hex(),
1704 p2.hex()
1705 ))
1965 alldata['parentnbrenames'].append(
1966 (data['p2.renamedfiles'], b.hex(), p2.hex())
1967 )
1968 alldata['parenttime'].append(
1969 (data['p2.time'], b.hex(), p2.hex())
1970 )
1706 1971 if p1missing or p2missing:
1707 alldata['totalnbrenames'].append((
1972 alldata['totalnbrenames'].append(
1973 (
1708 1974 data['nbrenamedfiles'],
1709 1975 b.hex(),
1710 1976 p1.hex(),
1711 p2.hex()
1712 ))
1713 alldata['totaltime'].append((
1714 data['time'],
1715 b.hex(),
1716 p1.hex(),
1717 p2.hex()
1718 ))
1977 p2.hex(),
1978 )
1979 )
1980 alldata['totaltime'].append(
1981 (data['time'], b.hex(), p1.hex(), p2.hex())
1982 )
1719 1983 fm.startitem()
1720 1984 fm.data(**data)
1721 1985 # make node pretty for the human output
@@ -1734,20 +1998,24 b' def perfhelpermergecopies(ui, repo, revs'
1734 1998 ('nbmissingfiles', 'number of missing files at head'),
1735 1999 ]
1736 2000 if dotiming:
1737 entries.append(('parentnbrenames',
1738 'rename from one parent to base'))
2001 entries.append(
2002 ('parentnbrenames', 'rename from one parent to base')
2003 )
1739 2004 entries.append(('totalnbrenames', 'total number of renames'))
1740 2005 entries.append(('parenttime', 'time for one parent'))
1741 2006 entries.append(('totaltime', 'time for both parents'))
1742 2007 _displaystats(ui, opts, entries, alldata)
1743 2008
1744 2009
1745 @command(b'perfhelper-pathcopies', formatteropts +
1746 [
2010 @command(
2011 b'perfhelper-pathcopies',
2012 formatteropts
2013 + [
1747 2014 (b'r', b'revs', [], b'restrict search to these revisions'),
1748 2015 (b'', b'timing', False, b'provides extra data (costly)'),
1749 2016 (b'', b'stats', False, b'provides statistic about the measured data'),
1750 ])
2017 ],
2018 )
1751 2019 def perfhelperpathcopies(ui, repo, revs=[], **opts):
1752 2020 """find statistic about potential parameters for the `perftracecopies`
1753 2021
@@ -1769,23 +2037,32 b' def perfhelperpathcopies(ui, repo, revs='
1769 2037
1770 2038 if dotiming:
1771 2039 header = '%12s %12s %12s %12s %12s %12s\n'
1772 output = ("%(source)12s %(destination)12s "
2040 output = (
2041 "%(source)12s %(destination)12s "
1773 2042 "%(nbrevs)12d %(nbmissingfiles)12d "
1774 "%(nbrenamedfiles)12d %(time)18.5f\n")
1775 header_names = ("source", "destination", "nb-revs", "nb-files",
1776 "nb-renames", "time")
2043 "%(nbrenamedfiles)12d %(time)18.5f\n"
2044 )
2045 header_names = (
2046 "source",
2047 "destination",
2048 "nb-revs",
2049 "nb-files",
2050 "nb-renames",
2051 "time",
2052 )
1777 2053 fm.plain(header % header_names)
1778 2054 else:
1779 2055 header = '%12s %12s %12s %12s\n'
1780 output = ("%(source)12s %(destination)12s "
1781 "%(nbrevs)12d %(nbmissingfiles)12d\n")
2056 output = (
2057 "%(source)12s %(destination)12s "
2058 "%(nbrevs)12d %(nbmissingfiles)12d\n"
2059 )
1782 2060 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
1783 2061
1784 2062 if not revs:
1785 2063 revs = ['all()']
1786 2064 revs = scmutil.revrange(repo, revs)
1787 2065
1788
1789 2066 if dostats:
1790 2067 alldata = {
1791 2068 'nbrevs': [],
@@ -1815,16 +2092,12 b' def perfhelperpathcopies(ui, repo, revs='
1815 2092 b'nbmissingfiles': len(missing),
1816 2093 }
1817 2094 if dostats:
1818 alldata['nbrevs'].append((
1819 data['nbrevs'],
1820 base.hex(),
1821 parent.hex(),
1822 ))
1823 alldata['nbmissingfiles'].append((
1824 data['nbmissingfiles'],
1825 base.hex(),
1826 parent.hex(),
1827 ))
2095 alldata['nbrevs'].append(
2096 (data['nbrevs'], base.hex(), parent.hex(),)
2097 )
2098 alldata['nbmissingfiles'].append(
2099 (data['nbmissingfiles'], base.hex(), parent.hex(),)
2100 )
1828 2101 if dotiming:
1829 2102 begin = util.timer()
1830 2103 renames = copies.pathcopies(base, parent)
@@ -1833,16 +2106,12 b' def perfhelperpathcopies(ui, repo, revs='
1833 2106 data['time'] = end - begin
1834 2107 data['nbrenamedfiles'] = len(renames)
1835 2108 if dostats:
1836 alldata['time'].append((
1837 data['time'],
1838 base.hex(),
1839 parent.hex(),
1840 ))
1841 alldata['nbrenames'].append((
1842 data['nbrenamedfiles'],
1843 base.hex(),
1844 parent.hex(),
1845 ))
2109 alldata['time'].append(
2110 (data['time'], base.hex(), parent.hex(),)
2111 )
2112 alldata['nbrenames'].append(
2113 (data['nbrenamedfiles'], base.hex(), parent.hex(),)
2114 )
1846 2115 fm.startitem()
1847 2116 fm.data(**data)
1848 2117 out = data.copy()
@@ -1860,11 +2129,11 b' def perfhelperpathcopies(ui, repo, revs='
1860 2129 ('nbmissingfiles', 'number of missing files at head'),
1861 2130 ]
1862 2131 if dotiming:
1863 entries.append(('nbrenames',
1864 'renamed files'))
2132 entries.append(('nbrenames', 'renamed files'))
1865 2133 entries.append(('time', 'time'))
1866 2134 _displaystats(ui, opts, entries, alldata)
1867 2135
2136
1868 2137 @command(b'perfcca', formatteropts)
1869 2138 def perfcca(ui, repo, **opts):
1870 2139 opts = _byteskwargs(opts)
@@ -1872,16 +2141,20 b' def perfcca(ui, repo, **opts):'
1872 2141 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
1873 2142 fm.end()
1874 2143
2144
1875 2145 @command(b'perffncacheload', formatteropts)
1876 2146 def perffncacheload(ui, repo, **opts):
1877 2147 opts = _byteskwargs(opts)
1878 2148 timer, fm = gettimer(ui, opts)
1879 2149 s = repo.store
2150
1880 2151 def d():
1881 2152 s.fncache._load()
2153
1882 2154 timer(d)
1883 2155 fm.end()
1884 2156
2157
1885 2158 @command(b'perffncachewrite', formatteropts)
1886 2159 def perffncachewrite(ui, repo, **opts):
1887 2160 opts = _byteskwargs(opts)
@@ -1891,26 +2164,32 b' def perffncachewrite(ui, repo, **opts):'
1891 2164 s.fncache._load()
1892 2165 tr = repo.transaction(b'perffncachewrite')
1893 2166 tr.addbackup(b'fncache')
2167
1894 2168 def d():
1895 2169 s.fncache._dirty = True
1896 2170 s.fncache.write(tr)
2171
1897 2172 timer(d)
1898 2173 tr.close()
1899 2174 lock.release()
1900 2175 fm.end()
1901 2176
2177
1902 2178 @command(b'perffncacheencode', formatteropts)
1903 2179 def perffncacheencode(ui, repo, **opts):
1904 2180 opts = _byteskwargs(opts)
1905 2181 timer, fm = gettimer(ui, opts)
1906 2182 s = repo.store
1907 2183 s.fncache._load()
2184
1908 2185 def d():
1909 2186 for p in s.fncache.entries:
1910 2187 s.encode(p)
2188
1911 2189 timer(d)
1912 2190 fm.end()
1913 2191
2192
1914 2193 def _bdiffworker(q, blocks, xdiff, ready, done):
1915 2194 while not done.is_set():
1916 2195 pair = q.get()
@@ -1927,6 +2206,7 b' def _bdiffworker(q, blocks, xdiff, ready'
1927 2206 with ready:
1928 2207 ready.wait()
1929 2208
2209
1930 2210 def _manifestrevision(repo, mnode):
1931 2211 ml = repo.manifestlog
1932 2212
@@ -1937,15 +2217,25 b' def _manifestrevision(repo, mnode):'
1937 2217
1938 2218 return store.revision(mnode)
1939 2219
1940 @command(b'perfbdiff', revlogopts + formatteropts + [
1941 (b'', b'count', 1, b'number of revisions to test (when using --startrev)'),
2220
2221 @command(
2222 b'perfbdiff',
2223 revlogopts
2224 + formatteropts
2225 + [
2226 (
2227 b'',
2228 b'count',
2229 1,
2230 b'number of revisions to test (when using --startrev)',
2231 ),
1942 2232 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
1943 2233 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
1944 2234 (b'', b'blocks', False, b'test computing diffs into blocks'),
1945 2235 (b'', b'xdiff', False, b'use xdiff algorithm'),
1946 2236 ],
1947
1948 b'-c|-m|FILE REV')
2237 b'-c|-m|FILE REV',
2238 )
1949 2239 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
1950 2240 """benchmark a bdiff between revisions
1951 2241
@@ -2001,6 +2291,7 b' def perfbdiff(ui, repo, file_, rev=None,'
2001 2291
2002 2292 withthreads = threads > 0
2003 2293 if not withthreads:
2294
2004 2295 def d():
2005 2296 for pair in textpairs:
2006 2297 if xdiff:
@@ -2009,6 +2300,7 b' def perfbdiff(ui, repo, file_, rev=None,'
2009 2300 mdiff.bdiff.blocks(*pair)
2010 2301 else:
2011 2302 mdiff.textdiff(*pair)
2303
2012 2304 else:
2013 2305 q = queue()
2014 2306 for i in _xrange(threads):
@@ -2016,9 +2308,11 b' def perfbdiff(ui, repo, file_, rev=None,'
2016 2308 ready = threading.Condition()
2017 2309 done = threading.Event()
2018 2310 for i in _xrange(threads):
2019 threading.Thread(target=_bdiffworker,
2020 args=(q, blocks, xdiff, ready, done)).start()
2311 threading.Thread(
2312 target=_bdiffworker, args=(q, blocks, xdiff, ready, done)
2313 ).start()
2021 2314 q.join()
2315
2022 2316 def d():
2023 2317 for pair in textpairs:
2024 2318 q.put(pair)
@@ -2027,6 +2321,7 b' def perfbdiff(ui, repo, file_, rev=None,'
2027 2321 with ready:
2028 2322 ready.notify_all()
2029 2323 q.join()
2324
2030 2325 timer, fm = gettimer(ui, opts)
2031 2326 timer(d)
2032 2327 fm.end()
@@ -2038,10 +2333,22 b' def perfbdiff(ui, repo, file_, rev=None,'
2038 2333 with ready:
2039 2334 ready.notify_all()
2040 2335
2041 @command(b'perfunidiff', revlogopts + formatteropts + [
2042 (b'', b'count', 1, b'number of revisions to test (when using --startrev)'),
2336
2337 @command(
2338 b'perfunidiff',
2339 revlogopts
2340 + formatteropts
2341 + [
2342 (
2343 b'',
2344 b'count',
2345 1,
2346 b'number of revisions to test (when using --startrev)',
2347 ),
2043 2348 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
2044 ], b'-c|-m|FILE REV')
2349 ],
2350 b'-c|-m|FILE REV',
2351 )
2045 2352 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
2046 2353 """benchmark a unified diff between revisions
2047 2354
@@ -2096,14 +2403,17 b' def perfunidiff(ui, repo, file_, rev=Non'
2096 2403 for left, right in textpairs:
2097 2404 # The date strings don't matter, so we pass empty strings.
2098 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 2408 # consume iterators in roughly the way patch.py does
2101 2409 b'\n'.join(headerlines)
2102 2410 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2411
2103 2412 timer, fm = gettimer(ui, opts)
2104 2413 timer(d)
2105 2414 fm.end()
2106 2415
2416
2107 2417 @command(b'perfdiffwd', formatteropts)
2108 2418 def perfdiffwd(ui, repo, **opts):
2109 2419 """Profile diff of working directory changes"""
@@ -2117,17 +2427,19 b' def perfdiffwd(ui, repo, **opts):'
2117 2427
2118 2428 for diffopt in ('', 'w', 'b', 'B', 'wB'):
2119 2429 opts = dict((options[c], b'1') for c in diffopt)
2430
2120 2431 def d():
2121 2432 ui.pushbuffer()
2122 2433 commands.diff(ui, repo, **opts)
2123 2434 ui.popbuffer()
2435
2124 2436 diffopt = diffopt.encode('ascii')
2125 2437 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
2126 2438 timer(d, title=title)
2127 2439 fm.end()
2128 2440
2129 @command(b'perfrevlogindex', revlogopts + formatteropts,
2130 b'-c|-m|FILE')
2441
2442 @command(b'perfrevlogindex', revlogopts + formatteropts, b'-c|-m|FILE')
2131 2443 def perfrevlogindex(ui, repo, file_=None, **opts):
2132 2444 """Benchmark operations against a revlog index.
2133 2445
@@ -2150,7 +2462,7 b' def perfrevlogindex(ui, repo, file_=None'
2150 2462 revlogio = revlog.revlogio()
2151 2463 inline = header & (1 << 16)
2152 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 2467 rllen = len(rl)
2156 2468
@@ -2221,22 +2533,26 b' def perfrevlogindex(ui, repo, file_=None'
2221 2533 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
2222 2534 (lambda: resolvenode(node100), b'look up node at tip'),
2223 2535 # 2x variation is to measure caching impact.
2224 (lambda: resolvenodes(allnodes),
2225 b'look up all nodes (forward)'),
2226 (lambda: resolvenodes(allnodes, 2),
2227 b'look up all nodes 2x (forward)'),
2228 (lambda: resolvenodes(allnodesrev),
2229 b'look up all nodes (reverse)'),
2230 (lambda: resolvenodes(allnodesrev, 2),
2231 b'look up all nodes 2x (reverse)'),
2232 (lambda: getentries(allrevs),
2233 b'retrieve all index entries (forward)'),
2234 (lambda: getentries(allrevs, 2),
2235 b'retrieve all index entries 2x (forward)'),
2236 (lambda: getentries(allrevsrev),
2237 b'retrieve all index entries (reverse)'),
2238 (lambda: getentries(allrevsrev, 2),
2239 b'retrieve all index entries 2x (reverse)'),
2536 (lambda: resolvenodes(allnodes), b'look up all nodes (forward)'),
2537 (lambda: resolvenodes(allnodes, 2), b'look up all nodes 2x (forward)'),
2538 (lambda: resolvenodes(allnodesrev), b'look up all nodes (reverse)'),
2539 (
2540 lambda: resolvenodes(allnodesrev, 2),
2541 b'look up all nodes 2x (reverse)',
2542 ),
2543 (lambda: getentries(allrevs), b'retrieve all index entries (forward)'),
2544 (
2545 lambda: getentries(allrevs, 2),
2546 b'retrieve all index entries 2x (forward)',
2547 ),
2548 (
2549 lambda: getentries(allrevsrev),
2550 b'retrieve all index entries (reverse)',
2551 ),
2552 (
2553 lambda: getentries(allrevsrev, 2),
2554 b'retrieve all index entries 2x (reverse)',
2555 ),
2240 2556 ]
2241 2557
2242 2558 for fn, title in benches:
@@ -2244,13 +2560,21 b' def perfrevlogindex(ui, repo, file_=None'
2244 2560 timer(fn, title=title)
2245 2561 fm.end()
2246 2562
2247 @command(b'perfrevlogrevisions', revlogopts + formatteropts +
2248 [(b'd', b'dist', 100, b'distance between the revisions'),
2563
2564 @command(
2565 b'perfrevlogrevisions',
2566 revlogopts
2567 + formatteropts
2568 + [
2569 (b'd', b'dist', 100, b'distance between the revisions'),
2249 2570 (b's', b'startrev', 0, b'revision to start reading at'),
2250 (b'', b'reverse', False, b'read in reverse')],
2251 b'-c|-m|FILE')
2252 def perfrevlogrevisions(ui, repo, file_=None, startrev=0, reverse=False,
2253 **opts):
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 2578 """Benchmark reading a series of revisions from a revlog.
2255 2579
2256 2580 By default, we read every ``-d/--dist`` revision from 0 to tip of
@@ -2286,8 +2610,13 b' def perfrevlogrevisions(ui, repo, file_='
2286 2610 timer(d)
2287 2611 fm.end()
2288 2612
2289 @command(b'perfrevlogwrite', revlogopts + formatteropts +
2290 [(b's', b'startrev', 1000, b'revision to start writing at'),
2613
2614 @command(
2615 b'perfrevlogwrite',
2616 revlogopts
2617 + formatteropts
2618 + [
2619 (b's', b'startrev', 1000, b'revision to start writing at'),
2291 2620 (b'', b'stoprev', -1, b'last revision to write'),
2292 2621 (b'', b'count', 3, b'number of passes to perform'),
2293 2622 (b'', b'details', False, b'print timing for every revisions tested'),
@@ -2295,7 +2624,8 b' def perfrevlogrevisions(ui, repo, file_='
2295 2624 (b'', b'lazydeltabase', True, b'try the provided delta first'),
2296 2625 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
2297 2626 ],
2298 b'-c|-m|FILE')
2627 b'-c|-m|FILE',
2628 )
2299 2629 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
2300 2630 """Benchmark writing a series of revisions to a revlog.
2301 2631
@@ -2329,8 +2659,13 b' def perfrevlogwrite(ui, repo, file_=None'
2329 2659 lazydeltabase = opts['lazydeltabase']
2330 2660 source = opts['source']
2331 2661 clearcaches = opts['clear_caches']
2332 validsource = (b'full', b'parent-1', b'parent-2', b'parent-smallest',
2333 b'storage')
2662 validsource = (
2663 b'full',
2664 b'parent-1',
2665 b'parent-2',
2666 b'parent-smallest',
2667 b'storage',
2668 )
2334 2669 if source not in validsource:
2335 2670 raise error.Abort('invalid source type: %s' % source)
2336 2671
@@ -2340,9 +2675,16 b' def perfrevlogwrite(ui, repo, file_=None'
2340 2675 raise error.Abort('invalide run count: %d' % count)
2341 2676 allresults = []
2342 2677 for c in range(count):
2343 timing = _timeonewrite(ui, rl, source, startrev, stoprev, c + 1,
2678 timing = _timeonewrite(
2679 ui,
2680 rl,
2681 source,
2682 startrev,
2683 stoprev,
2684 c + 1,
2344 2685 lazydeltabase=lazydeltabase,
2345 clearcaches=clearcaches)
2686 clearcaches=clearcaches,
2687 )
2346 2688 allresults.append(timing)
2347 2689
2348 2690 ### consolidate the results in a single list
@@ -2396,20 +2738,37 b' def perfrevlogwrite(ui, repo, file_=None'
2396 2738 # for now
2397 2739 totaltime = []
2398 2740 for item in allresults:
2399 totaltime.append((sum(x[1][0] for x in item),
2741 totaltime.append(
2742 (
2743 sum(x[1][0] for x in item),
2400 2744 sum(x[1][1] for x in item),
2401 sum(x[1][2] 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,
2404 displayall=displayall)
2748 formatone(
2749 fm,
2750 totaltime,
2751 title="total time (%d revs)" % resultcount,
2752 displayall=displayall,
2753 )
2405 2754 fm.end()
2406 2755
2756
2407 2757 class _faketr(object):
2408 2758 def add(s, x, y, z=None):
2409 2759 return None
2410 2760
2411 def _timeonewrite(ui, orig, source, startrev, stoprev, runidx=None,
2412 lazydeltabase=True, clearcaches=True):
2761
2762 def _timeonewrite(
2763 ui,
2764 orig,
2765 source,
2766 startrev,
2767 stoprev,
2768 runidx=None,
2769 lazydeltabase=True,
2770 clearcaches=True,
2771 ):
2413 2772 timings = []
2414 2773 tr = _faketr()
2415 2774 with _temprevlog(ui, orig, startrev) as dest:
@@ -2422,13 +2781,18 b' def _timeonewrite(ui, orig, source, star'
2422 2781 # Support both old and new progress API
2423 2782 if util.safehasattr(ui, 'makeprogress'):
2424 2783 progress = ui.makeprogress(topic, unit='revs', total=total)
2784
2425 2785 def updateprogress(pos):
2426 2786 progress.update(pos)
2787
2427 2788 def completeprogress():
2428 2789 progress.complete()
2790
2429 2791 else:
2792
2430 2793 def updateprogress(pos):
2431 2794 ui.progress(topic, pos, unit='revs', total=total)
2795
2432 2796 def completeprogress():
2433 2797 ui.progress(topic, None, unit='revs', total=total)
2434 2798
@@ -2445,6 +2809,7 b' def _timeonewrite(ui, orig, source, star'
2445 2809 completeprogress()
2446 2810 return timings
2447 2811
2812
2448 2813 def _getrevisionseed(orig, rev, tr, source):
2449 2814 from mercurial.node import nullid
2450 2815
@@ -2481,8 +2846,11 b' def _getrevisionseed(orig, rev, tr, sour'
2481 2846 baserev = orig.deltaparent(rev)
2482 2847 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
2483 2848
2484 return ((text, tr, linkrev, p1, p2),
2485 {'node': node, 'flags': flags, 'cachedelta': cachedelta})
2849 return (
2850 (text, tr, linkrev, p1, p2),
2851 {'node': node, 'flags': flags, 'cachedelta': cachedelta},
2852 )
2853
2486 2854
2487 2855 @contextlib.contextmanager
2488 2856 def _temprevlog(ui, orig, truncaterev):
@@ -2523,9 +2891,9 b' def _temprevlog(ui, orig, truncaterev):'
2523 2891 vfs = vfsmod.vfs(tmpdir)
2524 2892 vfs.options = getattr(orig.opener, 'options', None)
2525 2893
2526 dest = revlog.revlog(vfs,
2527 indexfile=indexname,
2528 datafile=dataname, **revlogkwargs)
2894 dest = revlog.revlog(
2895 vfs, indexfile=indexname, datafile=dataname, **revlogkwargs
2896 )
2529 2897 if dest._inline:
2530 2898 raise error.Abort('not supporting inline revlog (yet)')
2531 2899 # make sure internals are initialized
@@ -2535,10 +2903,17 b' def _temprevlog(ui, orig, truncaterev):'
2535 2903 finally:
2536 2904 shutil.rmtree(tmpdir, True)
2537 2905
2538 @command(b'perfrevlogchunks', revlogopts + formatteropts +
2539 [(b'e', b'engines', b'', b'compression engines to use'),
2540 (b's', b'startrev', 0, b'revision to start at')],
2541 b'-c|-m|FILE')
2906
2907 @command(
2908 b'perfrevlogchunks',
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 2917 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
2543 2918 """Benchmark operations on revlog chunks.
2544 2919
@@ -2645,17 +3020,26 b' def perfrevlogchunks(ui, repo, file_=Non'
2645 3020
2646 3021 for engine in sorted(engines):
2647 3022 compressor = util.compengines[engine].revlogcompressor()
2648 benches.append((functools.partial(docompress, compressor),
2649 b'compress w/ %s' % engine))
3023 benches.append(
3024 (
3025 functools.partial(docompress, compressor),
3026 b'compress w/ %s' % engine,
3027 )
3028 )
2650 3029
2651 3030 for fn, title in benches:
2652 3031 timer, fm = gettimer(ui, opts)
2653 3032 timer(fn, title=title)
2654 3033 fm.end()
2655 3034
2656 @command(b'perfrevlogrevision', revlogopts + formatteropts +
2657 [(b'', b'cache', False, b'use caches instead of clearing')],
2658 b'-c|-m|FILE REV')
3035
3036 @command(
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 3043 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
2660 3044 """Benchmark obtaining a revlog revision.
2661 3045
@@ -2777,22 +3161,30 b' def perfrevlogrevision(ui, repo, file_, '
2777 3161 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
2778 3162 benches.append(slicing)
2779 3163
2780 benches.extend([
3164 benches.extend(
3165 [
2781 3166 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
2782 3167 (lambda: dodecompress(rawchunks), b'decompress'),
2783 3168 (lambda: dopatch(text, bins), b'patch'),
2784 3169 (lambda: dohash(text), b'hash'),
2785 ])
3170 ]
3171 )
2786 3172
2787 3173 timer, fm = gettimer(ui, opts)
2788 3174 for fn, title in benches:
2789 3175 timer(fn, title=title)
2790 3176 fm.end()
2791 3177
2792 @command(b'perfrevset',
2793 [(b'C', b'clear', False, b'clear volatile cache between each call.'),
2794 (b'', b'contexts', False, b'obtain changectx for each revision')]
2795 + formatteropts, b"REVSET")
3178
3179 @command(
3180 b'perfrevset',
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 3188 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
2797 3189 """benchmark the execution time of a revset
2798 3190
@@ -2802,19 +3194,26 b' def perfrevset(ui, repo, expr, clear=Fal'
2802 3194 opts = _byteskwargs(opts)
2803 3195
2804 3196 timer, fm = gettimer(ui, opts)
3197
2805 3198 def d():
2806 3199 if clear:
2807 3200 repo.invalidatevolatilesets()
2808 3201 if contexts:
2809 for ctx in repo.set(expr): pass
3202 for ctx in repo.set(expr):
3203 pass
2810 3204 else:
2811 for r in repo.revs(expr): pass
3205 for r in repo.revs(expr):
3206 pass
3207
2812 3208 timer(d)
2813 3209 fm.end()
2814 3210
2815 @command(b'perfvolatilesets',
2816 [(b'', b'clear-obsstore', False, b'drop obsstore between each call.'),
2817 ] + formatteropts)
3211
3212 @command(
3213 b'perfvolatilesets',
3214 [(b'', b'clear-obsstore', False, b'drop obsstore between each call.'),]
3215 + formatteropts,
3216 )
2818 3217 def perfvolatilesets(ui, repo, *names, **opts):
2819 3218 """benchmark the computation of various volatile set
2820 3219
@@ -2829,6 +3228,7 b' def perfvolatilesets(ui, repo, *names, *'
2829 3228 if opts[b'clear_obsstore']:
2830 3229 clearfilecache(repo, b'obsstore')
2831 3230 obsolete.getrevs(repo, name)
3231
2832 3232 return d
2833 3233
2834 3234 allobs = sorted(obsolete.cachefuncs)
@@ -2844,6 +3244,7 b' def perfvolatilesets(ui, repo, *names, *'
2844 3244 if opts[b'clear_obsstore']:
2845 3245 clearfilecache(repo, b'obsstore')
2846 3246 repoview.filterrevs(repo, name)
3247
2847 3248 return d
2848 3249
2849 3250 allfilter = sorted(repoview.filtertable)
@@ -2854,12 +3255,20 b' def perfvolatilesets(ui, repo, *names, *'
2854 3255 timer(getfiltered(name), title=name)
2855 3256 fm.end()
2856 3257
2857 @command(b'perfbranchmap',
2858 [(b'f', b'full', False,
2859 b'Includes build time of subset'),
2860 (b'', b'clear-revbranch', False,
2861 b'purge the revbranch cache between computation'),
2862 ] + formatteropts)
3258
3259 @command(
3260 b'perfbranchmap',
3261 [
3262 (b'f', b'full', False, b'Includes build time of subset'),
3263 (
3264 b'',
3265 b'clear-revbranch',
3266 False,
3267 b'purge the revbranch cache between computation',
3268 ),
3269 ]
3270 + formatteropts,
3271 )
2863 3272 def perfbranchmap(ui, repo, *filternames, **opts):
2864 3273 """benchmark the update of a branchmap
2865 3274
@@ -2869,6 +3278,7 b' def perfbranchmap(ui, repo, *filternames'
2869 3278 full = opts.get(b"full", False)
2870 3279 clear_revbranch = opts.get(b"clear_revbranch", False)
2871 3280 timer, fm = gettimer(ui, opts)
3281
2872 3282 def getbranchmap(filtername):
2873 3283 """generate a benchmark function for the filtername"""
2874 3284 if filtername is None:
@@ -2880,6 +3290,7 b' def perfbranchmap(ui, repo, *filternames'
2880 3290 else:
2881 3291 # older versions
2882 3292 filtered = view._branchcaches
3293
2883 3294 def d():
2884 3295 if clear_revbranch:
2885 3296 repo.revbranchcache()._clear()
@@ -2888,7 +3299,9 b' def perfbranchmap(ui, repo, *filternames'
2888 3299 else:
2889 3300 filtered.pop(filtername, None)
2890 3301 view.branchmap()
3302
2891 3303 return d
3304
2892 3305 # add filter in smaller subset to bigger subset
2893 3306 possiblefilters = set(repoview.filtertable)
2894 3307 if filternames:
@@ -2933,11 +3346,16 b' def perfbranchmap(ui, repo, *filternames'
2933 3346 branchcachewrite.restore()
2934 3347 fm.end()
2935 3348
2936 @command(b'perfbranchmapupdate', [
3349
3350 @command(
3351 b'perfbranchmapupdate',
3352 [
2937 3353 (b'', b'base', [], b'subset of revision to start from'),
2938 3354 (b'', b'target', [], b'subset of revision to end with'),
2939 (b'', b'clear-caches', False, b'clear cache between each runs')
2940 ] + formatteropts)
3355 (b'', b'clear-caches', False, b'clear cache between each runs'),
3356 ]
3357 + formatteropts,
3358 )
2941 3359 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
2942 3360 """benchmark branchmap update from for <base> revs to <target> revs
2943 3361
@@ -2956,6 +3374,7 b' def perfbranchmapupdate(ui, repo, base=('
2956 3374 """
2957 3375 from mercurial import branchmap
2958 3376 from mercurial import repoview
3377
2959 3378 opts = _byteskwargs(opts)
2960 3379 timer, fm = gettimer(ui, opts)
2961 3380 clearcaches = opts[b'clear_caches']
@@ -3037,12 +3456,16 b' def perfbranchmapupdate(ui, repo, base=('
3037 3456 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
3038 3457 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
3039 3458
3040 @command(b'perfbranchmapload', [
3459
3460 @command(
3461 b'perfbranchmapload',
3462 [
3041 3463 (b'f', b'filter', b'', b'Specify repoview filter'),
3042 3464 (b'', b'list', False, b'List brachmap filter caches'),
3043 3465 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
3044
3045 ] + formatteropts)
3466 ]
3467 + formatteropts,
3468 )
3046 3469 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
3047 3470 """benchmark reading the branchmap"""
3048 3471 opts = _byteskwargs(opts)
@@ -3052,8 +3475,9 b' def perfbranchmapload(ui, repo, filter=b'
3052 3475 for name, kind, st in repo.cachevfs.readdir(stat=True):
3053 3476 if name.startswith(b'branch2'):
3054 3477 filtername = name.partition(b'-')[2] or b'unfiltered'
3055 ui.status(b'%s - %s\n'
3056 % (filtername, util.bytecount(st.st_size)))
3478 ui.status(
3479 b'%s - %s\n' % (filtername, util.bytecount(st.st_size))
3480 )
3057 3481 return
3058 3482 if not filter:
3059 3483 filter = None
@@ -3076,18 +3500,23 b' def perfbranchmapload(ui, repo, filter=b'
3076 3500 while fromfile(repo) is None:
3077 3501 currentfilter = subsettable.get(currentfilter)
3078 3502 if currentfilter is None:
3079 raise error.Abort(b'No branchmap cached for %s repo'
3080 % (filter or b'unfiltered'))
3503 raise error.Abort(
3504 b'No branchmap cached for %s repo' % (filter or b'unfiltered')
3505 )
3081 3506 repo = repo.filtered(currentfilter)
3082 3507 timer, fm = gettimer(ui, opts)
3508
3083 3509 def setup():
3084 3510 if clearrevlogs:
3085 3511 clearchangelog(repo)
3512
3086 3513 def bench():
3087 3514 fromfile(repo)
3515
3088 3516 timer(bench, setup=setup)
3089 3517 fm.end()
3090 3518
3519
3091 3520 @command(b'perfloadmarkers')
3092 3521 def perfloadmarkers(ui, repo):
3093 3522 """benchmark the time to parse the on-disk markers for a repo
@@ -3098,18 +3527,39 b' def perfloadmarkers(ui, repo):'
3098 3527 timer(lambda: len(obsolete.obsstore(svfs)))
3099 3528 fm.end()
3100 3529
3101 @command(b'perflrucachedict', formatteropts +
3102 [(b'', b'costlimit', 0, b'maximum total cost of items in cache'),
3530
3531 @command(
3532 b'perflrucachedict',
3533 formatteropts
3534 + [
3535 (b'', b'costlimit', 0, b'maximum total cost of items in cache'),
3103 3536 (b'', b'mincost', 0, b'smallest cost of items in cache'),
3104 3537 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
3105 3538 (b'', b'size', 4, b'size of cache'),
3106 3539 (b'', b'gets', 10000, b'number of key lookups'),
3107 3540 (b'', b'sets', 10000, b'number of key sets'),
3108 3541 (b'', b'mixed', 10000, b'number of mixed mode operations'),
3109 (b'', b'mixedgetfreq', 50, b'frequency of get vs set ops in mixed mode')],
3110 norepo=True)
3111 def perflrucache(ui, mincost=0, maxcost=100, costlimit=0, size=4,
3112 gets=10000, sets=10000, mixed=10000, mixedgetfreq=50, **opts):
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 3563 opts = _byteskwargs(opts)
3114 3564
3115 3565 def doinit():
@@ -3178,9 +3628,9 b' def perflrucache(ui, mincost=0, maxcost='
3178 3628 else:
3179 3629 op = 1
3180 3630
3181 mixedops.append((op,
3182 random.randint(0, size * 2),
3183 random.choice(costrange)))
3631 mixedops.append(
3632 (op, random.randint(0, size * 2), random.choice(costrange))
3633 )
3184 3634
3185 3635 def domixed():
3186 3636 d = util.lrucachedict(size)
@@ -3211,24 +3661,29 b' def perflrucache(ui, mincost=0, maxcost='
3211 3661 ]
3212 3662
3213 3663 if costlimit:
3214 benches.extend([
3664 benches.extend(
3665 [
3215 3666 (dogetscost, b'gets w/ cost limit'),
3216 3667 (doinsertscost, b'inserts w/ cost limit'),
3217 3668 (domixedcost, b'mixed w/ cost limit'),
3218 ])
3669 ]
3670 )
3219 3671 else:
3220 benches.extend([
3672 benches.extend(
3673 [
3221 3674 (dogets, b'gets'),
3222 3675 (doinserts, b'inserts'),
3223 3676 (dosets, b'sets'),
3224 (domixed, b'mixed')
3225 ])
3677 (domixed, b'mixed'),
3678 ]
3679 )
3226 3680
3227 3681 for fn, title in benches:
3228 3682 timer, fm = gettimer(ui, opts)
3229 3683 timer(fn, title=title)
3230 3684 fm.end()
3231 3685
3686
3232 3687 @command(b'perfwrite', formatteropts)
3233 3688 def perfwrite(ui, repo, **opts):
3234 3689 """microbenchmark ui.write
@@ -3236,15 +3691,19 b' def perfwrite(ui, repo, **opts):'
3236 3691 opts = _byteskwargs(opts)
3237 3692
3238 3693 timer, fm = gettimer(ui, opts)
3694
3239 3695 def write():
3240 3696 for i in range(100000):
3241 ui.write((b'Testing write performance\n'))
3697 ui.write(b'Testing write performance\n')
3698
3242 3699 timer(write)
3243 3700 fm.end()
3244 3701
3702
3245 3703 def uisetup(ui):
3246 if (util.safehasattr(cmdutil, b'openrevlog') and
3247 not util.safehasattr(commands, b'debugrevlogopts')):
3704 if util.safehasattr(cmdutil, b'openrevlog') and not util.safehasattr(
3705 commands, b'debugrevlogopts'
3706 ):
3248 3707 # for "historical portability":
3249 3708 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
3250 3709 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
@@ -3252,15 +3711,24 b' def uisetup(ui):'
3252 3711 # available since 3.5 (or 49c583ca48c4).
3253 3712 def openrevlog(orig, repo, cmd, file_, opts):
3254 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",
3256 hint=b"use 3.5 or later")
3714 raise error.Abort(
3715 b"This version doesn't support --dir option",
3716 hint=b"use 3.5 or later",
3717 )
3257 3718 return orig(repo, cmd, file_, opts)
3719
3258 3720 extensions.wrapfunction(cmdutil, b'openrevlog', openrevlog)
3259 3721
3260 @command(b'perfprogress', formatteropts + [
3722
3723 @command(
3724 b'perfprogress',
3725 formatteropts
3726 + [
3261 3727 (b'', b'topic', b'topic', b'topic for progress messages'),
3262 3728 (b'c', b'total', 1000000, b'total value we are progressing to'),
3263 ], norepo=True)
3729 ],
3730 norepo=True,
3731 )
3264 3732 def perfprogress(ui, topic=None, total=None, **opts):
3265 3733 """printing of progress bars"""
3266 3734 opts = _byteskwargs(opts)
@@ -7,6 +7,7 b' from mercurial import ('
7 7 util,
8 8 )
9 9
10
10 11 def diffstat(ui, repo, **kwargs):
11 12 '''Example usage:
12 13
@@ -25,65 +25,103 b' import subprocess'
25 25 import sys
26 26
27 27 _hgenv = dict(os.environ)
28 _hgenv.update({
29 'HGPLAIN': '1',
30 })
28 _hgenv.update(
29 {'HGPLAIN': '1',}
30 )
31 31
32 32 _HG_FIRST_CHANGE = '9117c6561b0bd7792fa13b50d28239d51b78e51f'
33 33
34
34 35 def _runhg(*args):
35 36 return subprocess.check_output(args, env=_hgenv)
36 37
38
37 39 def _is_hg_repo(path):
38 return _runhg('hg', 'log', '-R', path,
39 '-r0', '--template={node}').strip() == _HG_FIRST_CHANGE
40 return (
41 _runhg('hg', 'log', '-R', path, '-r0', '--template={node}').strip()
42 == _HG_FIRST_CHANGE
43 )
44
40 45
41 46 def _py3default():
42 47 if sys.version_info[0] >= 3:
43 48 return sys.executable
44 49 return 'python3'
45 50
51
46 52 def main(argv=()):
47 53 p = argparse.ArgumentParser()
48 p.add_argument('--working-tests',
49 help='List of tests that already work in Python 3.')
50 p.add_argument('--commit-to-repo',
51 help='If set, commit newly fixed tests to the given repo')
52 p.add_argument('-j', default=os.sysconf(r'SC_NPROCESSORS_ONLN'), type=int,
53 help='Number of parallel tests to run.')
54 p.add_argument('--python3', default=_py3default(),
55 help='python3 interpreter to use for test run')
56 p.add_argument('--commit-user',
54 p.add_argument(
55 '--working-tests', help='List of tests that already work in Python 3.'
56 )
57 p.add_argument(
58 '--commit-to-repo',
59 help='If set, commit newly fixed tests to the given repo',
60 )
61 p.add_argument(
62 '-j',
63 default=os.sysconf(r'SC_NPROCESSORS_ONLN'),
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',
57 74 default='python3-ratchet@mercurial-scm.org',
58 help='Username to specify when committing to a repo.')
75 help='Username to specify when committing to a repo.',
76 )
59 77 opts = p.parse_args(argv)
60 78 if opts.commit_to_repo:
61 79 if not _is_hg_repo(opts.commit_to_repo):
62 80 print('abort: specified repository is not the hg repository')
63 81 sys.exit(1)
64 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)' %
66 opts.working_tests)
83 print(
84 'abort: --working-tests must exist and be a file (got %r)'
85 % opts.working_tests
86 )
67 87 sys.exit(1)
68 88 elif opts.commit_to_repo:
69 89 root = _runhg('hg', 'root').strip()
70 90 if not opts.working_tests.startswith(root):
71 print('abort: if --commit-to-repo is given, '
72 '--working-tests must be from that repo')
91 print(
92 'abort: if --commit-to-repo is given, '
93 '--working-tests must be from that repo'
94 )
73 95 sys.exit(1)
74 96 try:
75 subprocess.check_call([opts.python3, '-c',
97 subprocess.check_call(
98 [
99 opts.python3,
100 '-c',
76 101 'import sys ; '
77 102 'assert ((3, 5) <= sys.version_info < (3, 6) '
78 'or sys.version_info >= (3, 6, 2))'])
103 'or sys.version_info >= (3, 6, 2))',
104 ]
105 )
79 106 except subprocess.CalledProcessError:
80 print('warning: Python 3.6.0 and 3.6.1 have '
81 'a bug which breaks Mercurial')
107 print(
108 'warning: Python 3.6.0 and 3.6.1 have '
109 'a bug which breaks Mercurial'
110 )
82 111 print('(see https://bugs.python.org/issue29714 for details)')
83 112 sys.exit(1)
84 113
85 rt = subprocess.Popen([opts.python3, 'run-tests.py', '-j', str(opts.j),
86 '--blacklist', opts.working_tests, '--json'])
114 rt = subprocess.Popen(
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 125 rt.wait()
88 126 with open('report.json') as f:
89 127 data = f.read()
@@ -104,12 +142,20 b' def main(argv=()):'
104 142 with open(opts.working_tests, 'w') as f:
105 143 for p in sorted(oldpass | newpass):
106 144 f.write('%s\n' % p)
107 _runhg('hg', 'commit', '-R', opts.commit_to_repo,
108 '--user', opts.commit_user,
109 '--message', 'python3: expand list of passing tests')
145 _runhg(
146 'hg',
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 155 else:
111 156 print('Newly passing tests:', '\n'.join(sorted(newpass)))
112 157 sys.exit(2)
113 158
159
114 160 if __name__ == '__main__':
115 161 main(sys.argv[1:])
@@ -16,9 +16,20 b' import re'
16 16 import subprocess
17 17 import sys
18 18
19 DEFAULTVARIANTS = ['plain', 'min', 'max', 'first', 'last',
20 'reverse', 'reverse+first', 'reverse+last',
21 'sort', 'sort+first', 'sort+last']
19 DEFAULTVARIANTS = [
20 'plain',
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 34 def check_output(*args, **kwargs):
24 35 kwargs.setdefault('stderr', subprocess.PIPE)
@@ -29,12 +40,14 b' def check_output(*args, **kwargs):'
29 40 raise subprocess.CalledProcessError(proc.returncode, ' '.join(args[0]))
30 41 return output
31 42
43
32 44 def update(rev):
33 45 """update the repo to a revision"""
34 46 try:
35 47 subprocess.check_call(['hg', 'update', '--quiet', '--check', str(rev)])
36 check_output(['make', 'local'],
37 stderr=None) # suppress output except for error/warning
48 check_output(
49 ['make', 'local'], stderr=None
50 ) # suppress output except for error/warning
38 51 except subprocess.CalledProcessError as exc:
39 52 print('update to revision %s failed, aborting'%rev, file=sys.stderr)
40 53 sys.exit(exc.returncode)
@@ -48,11 +61,14 b' def hg(cmd, repo=None):'
48 61 fullcmd = ['./hg']
49 62 if repo is not None:
50 63 fullcmd += ['-R', repo]
51 fullcmd += ['--config',
52 'extensions.perf=' + os.path.join(contribdir, 'perf.py')]
64 fullcmd += [
65 '--config',
66 'extensions.perf=' + os.path.join(contribdir, 'perf.py'),
67 ]
53 68 fullcmd += cmd
54 69 return check_output(fullcmd, stderr=subprocess.STDOUT)
55 70
71
56 72 def perf(revset, target=None, contexts=False):
57 73 """run benchmark for this very revset"""
58 74 try:
@@ -64,15 +80,21 b' def perf(revset, target=None, contexts=F'
64 80 output = hg(args, repo=target)
65 81 return parseoutput(output)
66 82 except subprocess.CalledProcessError as exc:
67 print('abort: cannot run revset benchmark: %s'%exc.cmd, file=sys.stderr)
83 print(
84 'abort: cannot run revset benchmark: %s' % exc.cmd, file=sys.stderr
85 )
68 86 if getattr(exc, 'output', None) is None: # no output before 2.7
69 87 print('(no output)', file=sys.stderr)
70 88 else:
71 89 print(exc.output, file=sys.stderr)
72 90 return None
73 91
74 outputre = re.compile(br'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) '
75 br'sys (\d+.\d+) \(best of (\d+)\)')
92
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 99 def parseoutput(output):
78 100 """parse a textual output into a dict
@@ -85,20 +107,30 b' def parseoutput(output):'
85 107 print('abort: invalid output:', file=sys.stderr)
86 108 print(output, file=sys.stderr)
87 109 sys.exit(1)
88 return {'comb': float(match.group(2)),
110 return {
111 'comb': float(match.group(2)),
89 112 'count': int(match.group(5)),
90 113 'sys': float(match.group(3)),
91 114 'user': float(match.group(4)),
92 115 'wall': float(match.group(1)),
93 116 }
94 117
118
95 119 def printrevision(rev):
96 120 """print data about a revision"""
97 121 sys.stdout.write("Revision ")
98 122 sys.stdout.flush()
99 subprocess.check_call(['hg', 'log', '--rev', str(rev), '--template',
100 '{if(tags, " ({tags})")} '
101 '{rev}:{node|short}: {desc|firstline}\n'])
123 subprocess.check_call(
124 [
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 135 def idxwidth(nbidx):
104 136 """return the max width of number used for index
@@ -116,6 +148,7 b' def idxwidth(nbidx):'
116 148 idxwidth = 1
117 149 return idxwidth
118 150
151
119 152 def getfactor(main, other, field, sensitivity=0.05):
120 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 158 if main is not None:
126 159 factor = other[field] / main[field]
127 160 low, high = 1 - sensitivity, 1 + sensitivity
128 if (low < factor < high):
161 if low < factor < high:
129 162 return None
130 163 return factor
131 164
165
132 166 def formatfactor(factor):
133 167 """format a factor into a 4 char string
134 168
@@ -155,6 +189,7 b' def formatfactor(factor):'
155 189 factor //= 0
156 190 return 'x%ix%i' % (factor, order)
157 191
192
158 193 def formattiming(value):
159 194 """format a value to strictly 8 char, dropping some precision if needed"""
160 195 if value < 10**7:
@@ -163,7 +198,10 b' def formattiming(value):'
163 198 # value is HUGE very unlikely to happen (4+ month run)
164 199 return '%i' % value
165 200
201
166 202 _marker = object()
203
204
167 205 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
168 206 """print a line of result to stdout"""
169 207 mask = '%%0%ii) %%s' % idxwidth(maxidx)
@@ -187,6 +225,7 b' def printresult(variants, idx, data, max'
187 225 out.append('%6d' % data[var]['count'])
188 226 print(mask % (idx, ' '.join(out)))
189 227
228
190 229 def printheader(variants, maxidx, verbose=False, relative=False):
191 230 header = [' ' * (idxwidth(maxidx) + 1)]
192 231 for var in variants:
@@ -204,6 +243,7 b' def printheader(variants, maxidx, verbos'
204 243 header.append('%6s' % 'count')
205 244 print(' '.join(header))
206 245
246
207 247 def getrevs(spec):
208 248 """get the list of rev matched by a revset"""
209 249 try:
@@ -221,31 +261,44 b' def applyvariants(revset, variant):'
221 261 revset = '%s(%s)' % (var, revset)
222 262 return revset
223 263
264
224 265 helptext="""This script will run multiple variants of provided revsets using
225 266 different revisions in your mercurial repository. After the benchmark are run
226 267 summary output is provided. Use it to demonstrate speed improvements or pin
227 268 point regressions. Revsets to run are specified in a file (or from stdin), one
228 269 revsets per line. Line starting with '#' will be ignored, allowing insertion of
229 270 comments."""
230 parser = optparse.OptionParser(usage="usage: %prog [options] <revs>",
231 description=helptext)
232 parser.add_option("-f", "--file",
271 parser = optparse.OptionParser(
272 usage="usage: %prog [options] <revs>", description=helptext
273 )
274 parser.add_option(
275 "-f",
276 "--file",
233 277 help="read revset from FILE (stdin if omitted)",
234 metavar="FILE")
235 parser.add_option("-R", "--repo",
236 help="run benchmark on REPO", metavar="REPO")
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(
283 "-v",
284 "--verbose",
239 285 action='store_true',
240 help="display all timing data (not just best total time)")
286 help="display all timing data (not just best total time)",
287 )
241 288
242 parser.add_option("", "--variants",
289 parser.add_option(
290 "",
291 "--variants",
243 292 default=','.join(DEFAULTVARIANTS),
244 293 help="comma separated list of variant to test "
245 "(eg: plain,min,sorted) (plain = no modification)")
246 parser.add_option('', '--contexts',
294 "(eg: plain,min,sorted) (plain = no modification)",
295 )
296 parser.add_option(
297 '',
298 '--contexts',
247 299 action='store_true',
248 help='obtain changectx from results instead of integer revs')
300 help='obtain changectx from results instead of integer revs',
301 )
249 302
250 303 (options, args) = parser.parse_args()
251 304
@@ -294,17 +347,20 b' for r in revs:'
294 347 data = perf(varrset, target=options.repo, contexts=options.contexts)
295 348 varres[var] = data
296 349 res.append(varres)
297 printresult(variants, idx, varres, len(revsets),
298 verbose=options.verbose)
350 printresult(
351 variants, idx, varres, len(revsets), verbose=options.verbose
352 )
299 353 sys.stdout.flush()
300 354 print("----------------------------")
301 355
302 356
303 print("""
357 print(
358 """
304 359
305 360 Result by revset
306 361 ================
307 """)
362 """
363 )
308 364
309 365 print('Revision:')
310 366 for idx, rev in enumerate(revs):
@@ -321,7 +377,13 b' for ridx, rset in enumerate(revsets):'
321 377 printheader(variants, len(results), verbose=options.verbose, relative=True)
322 378 ref = None
323 379 for idx, data in enumerate(results):
324 printresult(variants, idx, data[ridx], len(results),
325 verbose=options.verbose, reference=ref)
380 printresult(
381 variants,
382 idx,
383 data[ridx],
384 len(results),
385 verbose=options.verbose,
386 reference=ref,
387 )
326 388 ref = data[ridx]
327 389 print()
@@ -9,16 +9,19 b' import signal'
9 9 import sys
10 10 import traceback
11 11
12
12 13 def sigshow(*args):
13 14 sys.stderr.write("\n")
14 15 traceback.print_stack(args[1], limit=10, file=sys.stderr)
15 16 sys.stderr.write("----\n")
16 17
18
17 19 def sigexit(*args):
18 20 sigshow(*args)
19 21 print('alarm!')
20 22 sys.exit(1)
21 23
24
22 25 def extsetup(ui):
23 26 signal.signal(signal.SIGQUIT, sigshow)
24 27 signal.signal(signal.SIGALRM, sigexit)
@@ -62,9 +62,7 b' from mercurial import ('
62 62 registrar,
63 63 scmutil,
64 64 )
65 from mercurial.utils import (
66 dateutil,
67 )
65 from mercurial.utils import dateutil
68 66
69 67 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
70 68 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
@@ -77,14 +75,17 b' command = registrar.command(cmdtable)'
77 75
78 76 newfile = {'new fi', 'rename', 'copy f', 'copy t'}
79 77
78
80 79 def zerodict():
81 80 return collections.defaultdict(lambda: 0)
82 81
82
83 83 def roundto(x, k):
84 84 if x > k * 2:
85 85 return int(round(x / float(k)) * k)
86 86 return int(round(x))
87 87
88
88 89 def parsegitdiff(lines):
89 90 filename, mar, lineadd, lineremove = None, None, zerodict(), 0
90 91 binary = False
@@ -110,10 +111,16 b' def parsegitdiff(lines):'
110 111 if filename:
111 112 yield filename, mar, lineadd, lineremove, binary
112 113
113 @command('analyze',
114 [('o', 'output', '', _('write output to given file'), _('FILE')),
115 ('r', 'rev', [], _('analyze specified revisions'), _('REV'))],
116 _('hg analyze'), optionalrepo=True)
114
115 @command(
116 'analyze',
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 124 def analyze(ui, repo, *revs, **opts):
118 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 183 revs = scmutil.revrange(repo, revs)
177 184 revs.sort()
178 185
179 progress = ui.makeprogress(_('analyzing'), unit=_('changesets'),
180 total=len(revs))
186 progress = ui.makeprogress(
187 _('analyzing'), unit=_('changesets'), total=len(revs)
188 )
181 189 for i, rev in enumerate(revs):
182 190 progress.update(i)
183 191 ctx = repo[rev]
@@ -198,8 +206,9 b' def analyze(ui, repo, *revs, **opts):'
198 206 timedelta = ctx.date()[0] - lastctx.date()[0]
199 207 interarrival[roundto(timedelta, 300)] += 1
200 208 diffopts = diffutil.diffallopts(ui, {'git': True})
201 diff = sum((d.splitlines()
202 for d in ctx.diff(pctx, opts=diffopts)), [])
209 diff = sum(
210 (d.splitlines() for d in ctx.diff(pctx, opts=diffopts)), []
211 )
203 212 fileadds, diradds, fileremoves, filechanges = 0, 0, 0, 0
204 213 for filename, mar, lineadd, lineremove, isbin in parsegitdiff(diff):
205 214 if isbin:
@@ -207,8 +216,9 b' def analyze(ui, repo, *revs, **opts):'
207 216 added = sum(lineadd.itervalues(), 0)
208 217 if mar == 'm':
209 218 if added and lineremove:
210 lineschanged[roundto(added, 5),
211 roundto(lineremove, 5)] += 1
219 lineschanged[
220 roundto(added, 5), roundto(lineremove, 5)
221 ] += 1
212 222 filechanges += 1
213 223 elif mar == 'a':
214 224 fileadds += 1
@@ -238,7 +248,9 b' def analyze(ui, repo, *revs, **opts):'
238 248 def pronk(d):
239 249 return sorted(d.iteritems(), key=lambda x: x[1], reverse=True)
240 250
241 json.dump({'revs': len(revs),
251 json.dump(
252 {
253 'revs': len(revs),
242 254 'initdirs': pronk(dirs),
243 255 'lineschanged': pronk(lineschanged),
244 256 'children': pronk(invchildren),
@@ -254,14 +266,20 b' def analyze(ui, repo, *revs, **opts):'
254 266 'interarrival': pronk(interarrival),
255 267 'tzoffset': pronk(tzoffset),
256 268 },
257 fp)
269 fp,
270 )
258 271 fp.close()
259 272
260 @command('synthesize',
261 [('c', 'count', 0, _('create given number of commits'), _('COUNT')),
273
274 @command(
275 'synthesize',
276 [
277 ('c', 'count', 0, _('create given number of commits'), _('COUNT')),
262 278 ('', 'dict', '', _('path to a dictionary of words'), _('FILE')),
263 ('', 'initfiles', 0, _('initial file count to create'), _('COUNT'))],
264 _('hg synthesize [OPTION].. DESCFILE'))
279 ('', 'initfiles', 0, _('initial file count to create'), _('COUNT')),
280 ],
281 _('hg synthesize [OPTION].. DESCFILE'),
282 )
265 283 def synthesize(ui, repo, descpath, **opts):
266 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 403 progress.complete()
386 404 message = 'synthesized wide repo with %d files' % (len(files),)
387 mc = context.memctx(repo, [pctx.node(), nullid], message,
388 files, filectxfn, ui.username(),
389 '%d %d' % dateutil.makedate())
405 mc = context.memctx(
406 repo,
407 [pctx.node(), nullid],
408 message,
409 files,
410 filectxfn,
411 ui.username(),
412 '%d %d' % dateutil.makedate(),
413 )
390 414 initnode = mc.commit()
391 415 if ui.debugflag:
392 416 hexfn = hex
393 417 else:
394 418 hexfn = short
395 ui.status(_('added commit %s with %d files\n')
396 % (hexfn(initnode), len(files)))
419 ui.status(
420 _('added commit %s with %d files\n') % (hexfn(initnode), len(files))
421 )
397 422
398 423 # Synthesize incremental revisions to the repository, adding repo depth.
399 424 count = int(opts['count'])
@@ -437,8 +462,11 b' def synthesize(ui, repo, descpath, **opt'
437 462 for __ in pycompat.xrange(10):
438 463 fctx = pctx.filectx(random.choice(mfk))
439 464 path = fctx.path()
440 if not (path in nevertouch or fctx.isbinary() or
441 'l' in fctx.flags()):
465 if not (
466 path in nevertouch
467 or fctx.isbinary()
468 or 'l' in fctx.flags()
469 ):
442 470 break
443 471 lines = fctx.data().splitlines()
444 472 add, remove = pick(lineschanged)
@@ -466,14 +494,20 b' def synthesize(ui, repo, descpath, **opt'
466 494 path.append(random.choice(words))
467 495 path.append(random.choice(words))
468 496 pathstr = '/'.join(filter(None, path))
469 data = '\n'.join(
497 data = (
498 '\n'.join(
470 499 makeline()
471 for __ in pycompat.xrange(pick(linesinfilesadded))) + '\n'
500 for __ in pycompat.xrange(pick(linesinfilesadded))
501 )
502 + '\n'
503 )
472 504 changes[pathstr] = data
505
473 506 def filectxfn(repo, memctx, path):
474 507 if path not in changes:
475 508 return None
476 509 return context.memfilectx(repo, memctx, path, changes[path])
510
477 511 if not changes:
478 512 continue
479 513 if revs:
@@ -481,11 +515,17 b' def synthesize(ui, repo, descpath, **opt'
481 515 else:
482 516 date = time.time() - (86400 * count)
483 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 519 user = random.choice(words) + '@' + random.choice(words)
486 mc = context.memctx(repo, pl, makeline(minimum=2),
520 mc = context.memctx(
521 repo,
522 pl,
523 makeline(minimum=2),
487 524 sorted(changes),
488 filectxfn, user, '%d %d' % (date, pick(tzoffset)))
525 filectxfn,
526 user,
527 '%d %d' % (date, pick(tzoffset)),
528 )
489 529 newnode = mc.commit()
490 530 heads.add(repo.changelog.rev(newnode))
491 531 heads.discard(r1)
@@ -495,10 +535,12 b' def synthesize(ui, repo, descpath, **opt'
495 535 lock.release()
496 536 wlock.release()
497 537
538
498 539 def renamedirs(dirs, words):
499 540 '''Randomly rename the directory names in the per-dir file count dict.'''
500 541 wordgen = itertools.cycle(words)
501 542 replacements = {'': ''}
543
502 544 def rename(dirpath):
503 545 '''Recursively rename the directory and all path prefixes.
504 546
@@ -516,6 +558,7 b' def renamedirs(dirs, words):'
516 558 renamed = os.path.join(head, next(wordgen))
517 559 replacements[dirpath] = renamed
518 560 return renamed
561
519 562 result = []
520 563 for dirpath, count in dirs.iteritems():
521 564 result.append([rename(dirpath.lstrip(os.sep)), count])
@@ -14,11 +14,13 b' import sys'
14 14 ####################
15 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 20 def identity(a):
20 21 return a
21 22
23
22 24 def _rapply(f, xs):
23 25 if xs is None:
24 26 # assume None means non-value of optional data
@@ -29,12 +31,14 b' def _rapply(f, xs):'
29 31 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
30 32 return f(xs)
31 33
34
32 35 def rapply(f, xs):
33 36 if f is identity:
34 37 # fast path mainly for py2
35 38 return xs
36 39 return _rapply(f, xs)
37 40
41
38 42 if ispy3:
39 43 import builtins
40 44
@@ -49,29 +53,37 b' if ispy3:'
49 53
50 54 def opentext(f):
51 55 return open(f, 'r')
56
57
52 58 else:
53 59 bytestr = str
54 60 sysstr = identity
55 61
56 62 opentext = open
57 63
64
58 65 def b2s(x):
59 66 # convert BYTES elements in "x" to SYSSTR recursively
60 67 return rapply(sysstr, x)
61 68
69
62 70 def writeout(data):
63 71 # write "data" in BYTES into stdout
64 72 sys.stdout.write(data)
65 73
74
66 75 def writeerr(data):
67 76 # write "data" in BYTES into stderr
68 77 sys.stderr.write(data)
69 78
79
70 80 ####################
71 81
82
72 83 class embeddedmatcher(object):
73 84 """Base class to detect embedded code fragments in *.t test script
74 85 """
86
75 87 __metaclass__ = abc.ABCMeta
76 88
77 89 def __init__(self, desc):
@@ -126,6 +138,7 b' class embeddedmatcher(object):'
126 138 def codeinside(self, ctx, line):
127 139 """Return actual code at line inside embedded code"""
128 140
141
129 142 def embedded(basefile, lines, errors, matchers):
130 143 """pick embedded code fragments up from given lines
131 144
@@ -185,8 +198,10 b' def embedded(basefile, lines, errors, ma'
185 198 elif not matcher.isinside(ctx, line):
186 199 # this is an error of basefile
187 200 # (if matchers are implemented correctly)
188 errors.append('%s:%d: unexpected line for "%s"'
189 % (basefile, lineno, matcher.desc))
201 errors.append(
202 '%s:%d: unexpected line for "%s"'
203 % (basefile, lineno, matcher.desc)
204 )
190 205 # stop extracting embedded code by current 'matcher',
191 206 # because appearance of unexpected line might mean
192 207 # that expected end-of-embedded-code line might never
@@ -208,10 +223,14 b' def embedded(basefile, lines, errors, ma'
208 223 if matched:
209 224 if len(matched) > 1:
210 225 # this is an error of matchers, maybe
211 errors.append('%s:%d: ambiguous line for %s' %
212 (basefile, lineno,
213 ', '.join(['"%s"' % m.desc
214 for m, c in matched])))
226 errors.append(
227 '%s:%d: ambiguous line for %s'
228 % (
229 basefile,
230 lineno,
231 ', '.join(['"%s"' % m.desc for m, c in matched]),
232 )
233 )
215 234 # omit extracting embedded code, because choosing
216 235 # arbitrary matcher from matched ones might fail to
217 236 # detect the end of embedded code as expected.
@@ -238,8 +257,11 b' def embedded(basefile, lines, errors, ma'
238 257 else:
239 258 # this is an error of basefile
240 259 # (if matchers are implemented correctly)
241 errors.append('%s:%d: unexpected end of file for "%s"'
242 % (basefile, lineno, matcher.desc))
260 errors.append(
261 '%s:%d: unexpected end of file for "%s"'
262 % (basefile, lineno, matcher.desc)
263 )
264
243 265
244 266 # heredoc limit mark to ignore embedded code at check-code.py or so
245 267 heredocignorelimit = 'NO_CHECK_EOF'
@@ -252,6 +274,7 b" heredocignorelimit = 'NO_CHECK_EOF'"
252 274 # - << 'LIMITMARK'
253 275 heredoclimitpat = r'\s*<<\s*(?P<lquote>["\']?)(?P<limit>\w+)(?P=lquote)'
254 276
277
255 278 class fileheredocmatcher(embeddedmatcher):
256 279 """Detect "cat > FILE << LIMIT" style embedded code
257 280
@@ -290,6 +313,7 b' class fileheredocmatcher(embeddedmatcher'
290 313 >>> matcher.ignores(ctx)
291 314 True
292 315 """
316
293 317 _prefix = ' > '
294 318
295 319 def __init__(self, desc, namepat):
@@ -302,8 +326,9 b' class fileheredocmatcher(embeddedmatcher'
302 326 # - > NAMEPAT
303 327 # - > "NAMEPAT"
304 328 # - > 'NAMEPAT'
305 namepat = (r'\s*>>?\s*(?P<nquote>["\']?)(?P<name>%s)(?P=nquote)'
306 % namepat)
329 namepat = (
330 r'\s*>>?\s*(?P<nquote>["\']?)(?P<name>%s)(?P=nquote)' % namepat
331 )
307 332 self._fileres = [
308 333 # "cat > NAME << LIMIT" case
309 334 re.compile(r' \$ \s*cat' + namepat + heredoclimitpat),
@@ -316,8 +341,10 b' class fileheredocmatcher(embeddedmatcher'
316 341 for filere in self._fileres:
317 342 matched = filere.match(line)
318 343 if matched:
319 return (matched.group('name'),
320 ' > %s\n' % matched.group('limit'))
344 return (
345 matched.group('name'),
346 ' > %s\n' % matched.group('limit'),
347 )
321 348
322 349 def endsat(self, ctx, line):
323 350 return ctx[1] == line
@@ -340,9 +367,11 b' class fileheredocmatcher(embeddedmatcher'
340 367 def codeinside(self, ctx, line):
341 368 return line[len(self._prefix):] # strip prefix
342 369
370
343 371 ####
344 372 # for embedded python script
345 373
374
346 375 class pydoctestmatcher(embeddedmatcher):
347 376 """Detect ">>> code" style embedded python code
348 377
@@ -395,6 +424,7 b' class pydoctestmatcher(embeddedmatcher):'
395 424 True
396 425 >>> matcher.codeatend(ctx, end)
397 426 """
427
398 428 _prefix = ' >>> '
399 429 _prefixre = re.compile(r' (>>>|\.\.\.) ')
400 430
@@ -438,6 +468,7 b' class pydoctestmatcher(embeddedmatcher):'
438 468 return line[len(self._prefix):] # strip prefix ' >>> '/' ... '
439 469 return '\n' # an expected output line is treated as an empty line
440 470
471
441 472 class pyheredocmatcher(embeddedmatcher):
442 473 """Detect "python << LIMIT" style embedded python code
443 474
@@ -474,10 +505,12 b' class pyheredocmatcher(embeddedmatcher):'
474 505 >>> matcher.ignores(ctx)
475 506 True
476 507 """
508
477 509 _prefix = ' > '
478 510
479 _startre = re.compile(r' \$ (\$PYTHON|"\$PYTHON"|python).*' +
480 heredoclimitpat)
511 _startre = re.compile(
512 r' \$ (\$PYTHON|"\$PYTHON"|python).*' + heredoclimitpat
513 )
481 514
482 515 def __init__(self):
483 516 super(pyheredocmatcher, self).__init__("heredoc python invocation")
@@ -509,6 +542,7 b' class pyheredocmatcher(embeddedmatcher):'
509 542 def codeinside(self, ctx, line):
510 543 return line[len(self._prefix):] # strip prefix
511 544
545
512 546 _pymatchers = [
513 547 pydoctestmatcher(),
514 548 pyheredocmatcher(),
@@ -517,9 +551,11 b' class pyheredocmatcher(embeddedmatcher):'
517 551 fileheredocmatcher('heredoc .py file', r'[^<]+\.py'),
518 552 ]
519 553
554
520 555 def pyembedded(basefile, lines, errors):
521 556 return embedded(basefile, lines, errors, _pymatchers)
522 557
558
523 559 ####
524 560 # for embedded shell script
525 561
@@ -529,22 +565,27 b' def pyembedded(basefile, lines, errors):'
529 565 fileheredocmatcher('heredoc .sh file', r'[^<]+\.sh'),
530 566 ]
531 567
568
532 569 def shembedded(basefile, lines, errors):
533 570 return embedded(basefile, lines, errors, _shmatchers)
534 571
572
535 573 ####
536 574 # for embedded hgrc configuration
537 575
538 576 _hgrcmatchers = [
539 577 # use '[^<]+' instead of '\S+', in order to match against
540 578 # paths including whitespaces
541 fileheredocmatcher('heredoc hgrc file',
542 r'(([^/<]+/)+hgrc|\$HGRCPATH|\${HGRCPATH})'),
579 fileheredocmatcher(
580 'heredoc hgrc file', r'(([^/<]+/)+hgrc|\$HGRCPATH|\${HGRCPATH})'
581 ),
543 582 ]
544 583
584
545 585 def hgrcembedded(basefile, lines, errors):
546 586 return embedded(basefile, lines, errors, _hgrcmatchers)
547 587
588
548 589 ####
549 590
550 591 if __name__ == "__main__":
@@ -558,8 +599,7 b' if __name__ == "__main__":'
558 599 name = '<anonymous>'
559 600 writeout("%s:%d: %s starts\n" % (basefile, starts, name))
560 601 if opts.verbose and code:
561 writeout(" |%s\n" %
562 "\n |".join(l for l in code.splitlines()))
602 writeout(" |%s\n" % "\n |".join(l for l in code.splitlines()))
563 603 writeout("%s:%d: %s ends\n" % (basefile, ends, name))
564 604 for e in errors:
565 605 writeerr("%s\n" % e)
@@ -579,9 +619,11 b' if __name__ == "__main__":'
579 619 return ret
580 620
581 621 commands = {}
622
582 623 def command(name, desc):
583 624 def wrap(func):
584 625 commands[name] = (desc, func)
626
585 627 return wrap
586 628
587 629 @command("pyembedded", "detect embedded python script")
@@ -596,21 +638,29 b' if __name__ == "__main__":'
596 638 def hgrcembeddedcmd(args, opts):
597 639 return applyembedded(args, hgrcembedded, opts)
598 640
599 availablecommands = "\n".join([" - %s: %s" % (key, value[0])
600 for key, value in commands.items()])
641 availablecommands = "\n".join(
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 648 Pick up embedded code fragments from given file(s) or stdin, and list
605 649 up start/end lines of them in standard compiler format
606 650 ("FILENAME:LINENO:").
607 651
608 652 Available commands are:
609 """ + availablecommands + """
610 """)
611 parser.add_option("-v", "--verbose",
653 """
654 + availablecommands
655 + """
656 """
657 )
658 parser.add_option(
659 "-v",
660 "--verbose",
612 661 help="enable additional output (e.g. actual code)",
613 action="store_true")
662 action="store_true",
663 )
614 664 (opts, args) = parser.parse_args()
615 665
616 666 if not args or args[0] not in commands:
@@ -96,6 +96,7 b' import sys'
96 96 # Enable tracing. Run 'python -m win32traceutil' to debug
97 97 if getattr(sys, 'isapidllhandle', None) is not None:
98 98 import win32traceutil
99
99 100 win32traceutil.SetupForPrint # silence unused import warning
100 101
101 102 import isapi_wsgi
@@ -106,11 +107,13 b' from mercurial.hgweb.hgwebdir_mod import'
106 107 class WsgiHandler(isapi_wsgi.IsapiWsgiHandler):
107 108 error_status = '500 Internal Server Error' # less silly error message
108 109
110
109 111 isapi_wsgi.IsapiWsgiHandler = WsgiHandler
110 112
111 113 # Only create the hgwebdir instance once
112 114 application = hgwebdir(hgweb_config)
113 115
116
114 117 def handler(environ, start_response):
115 118
116 119 # Translate IIS's weird URLs
@@ -125,10 +128,13 b' def handler(environ, start_response):'
125 128
126 129 return application(environ, start_response)
127 130
131
128 132 def __ExtensionFactory__():
129 133 return isapi_wsgi.ISAPISimpleHandler(handler)
130 134
135
131 136 if __name__=='__main__':
132 137 from isapi.install import ISAPIParameters, HandleCommandLine
138
133 139 params = ISAPIParameters()
134 140 HandleCommandLine(params)
@@ -11,7 +11,9 b' import sys'
11 11 # import from the live mercurial repo
12 12 os.environ['HGMODULEPOLICY'] = 'py'
13 13 sys.path.insert(0, "..")
14 from mercurial import demandimport; demandimport.enable()
14 from mercurial import demandimport
15
16 demandimport.enable()
15 17 from mercurial import (
16 18 commands,
17 19 extensions,
@@ -36,13 +38,16 b' initlevel_cmd = 1'
36 38 initlevel_ext = 1
37 39 initlevel_ext_cmd = 3
38 40
41
39 42 def showavailables(ui, initlevel):
40 avail = (' available marks and order of them in this help: %s\n') % (
41 ', '.join(['%r' % (m * 4) for m in level2mark[initlevel + 1:]]))
43 avail = ' available marks and order of them in this help: %s\n' % (
44 ', '.join(['%r' % (m * 4) for m in level2mark[initlevel + 1 :]])
45 )
42 46 ui.warn(avail.encode('utf-8'))
43 47
48
44 49 def checkseclevel(ui, doc, name, initlevel):
45 ui.note(('checking "%s"\n') % name)
50 ui.note('checking "%s"\n' % name)
46 51 if not isinstance(doc, bytes):
47 52 doc = doc.encode('utf-8')
48 53 blocks, pruned = minirst.parse(doc, 0, ['verbose'])
@@ -54,66 +59,77 b' def checkseclevel(ui, doc, name, initlev'
54 59 mark = block[b'underline']
55 60 title = block[b'lines'][0]
56 61 if (mark not in mark2level) or (mark2level[mark] <= initlevel):
57 ui.warn((('invalid section mark %r for "%s" of %s\n') %
58 (mark * 4, title, name)).encode('utf-8'))
62 ui.warn(
63 (
64 'invalid section mark %r for "%s" of %s\n'
65 % (mark * 4, title, name)
66 ).encode('utf-8')
67 )
59 68 showavailables(ui, initlevel)
60 69 errorcnt += 1
61 70 continue
62 71 nextlevel = mark2level[mark]
63 72 if curlevel < nextlevel and curlevel + 1 != nextlevel:
64 ui.warn(('gap of section level at "%s" of %s\n') %
65 (title, name))
73 ui.warn('gap of section level at "%s" of %s\n' % (title, name))
66 74 showavailables(ui, initlevel)
67 75 errorcnt += 1
68 76 continue
69 ui.note(('appropriate section level for "%s %s"\n') %
70 (mark * (nextlevel * 2), title))
77 ui.note(
78 'appropriate section level for "%s %s"\n'
79 % (mark * (nextlevel * 2), title)
80 )
71 81 curlevel = nextlevel
72 82
73 83 return errorcnt
74 84
85
75 86 def checkcmdtable(ui, cmdtable, namefmt, initlevel):
76 87 errorcnt = 0
77 88 for k, entry in cmdtable.items():
78 89 name = k.split(b"|")[0].lstrip(b"^")
79 90 if not entry[0].__doc__:
80 ui.note(('skip checking %s: no help document\n') %
81 (namefmt % name))
91 ui.note('skip checking %s: no help document\n' % (namefmt % name))
82 92 continue
83 errorcnt += checkseclevel(ui, entry[0].__doc__,
84 namefmt % name,
85 initlevel)
93 errorcnt += checkseclevel(
94 ui, entry[0].__doc__, namefmt % name, initlevel
95 )
86 96 return errorcnt
87 97
98
88 99 def checkhghelps(ui):
89 100 errorcnt = 0
90 101 for h in helptable:
91 102 names, sec, doc = h[0:3]
92 103 if callable(doc):
93 104 doc = doc(ui)
94 errorcnt += checkseclevel(ui, doc,
95 '%s help topic' % names[0],
96 initlevel_topic)
105 errorcnt += checkseclevel(
106 ui, doc, '%s help topic' % names[0], initlevel_topic
107 )
97 108
98 109 errorcnt += checkcmdtable(ui, table, '%s command', initlevel_cmd)
99 110
100 for name in sorted(list(extensions.enabled()) +
101 list(extensions.disabled())):
111 for name in sorted(
112 list(extensions.enabled()) + list(extensions.disabled())
113 ):
102 114 mod = extensions.load(ui, name, None)
103 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 117 continue
106 errorcnt += checkseclevel(ui, mod.__doc__,
107 '%s extension' % name,
108 initlevel_ext)
118 errorcnt += checkseclevel(
119 ui, mod.__doc__, '%s extension' % name, initlevel_ext
120 )
109 121
110 122 cmdtable = getattr(mod, 'cmdtable', None)
111 123 if cmdtable:
112 errorcnt += checkcmdtable(ui, cmdtable,
124 errorcnt += checkcmdtable(
125 ui,
126 cmdtable,
113 127 '%%s command of %s extension' % name,
114 initlevel_ext_cmd)
128 initlevel_ext_cmd,
129 )
115 130 return errorcnt
116 131
132
117 133 def checkfile(ui, filename, initlevel):
118 134 if filename == '-':
119 135 filename = 'stdin'
@@ -122,43 +138,76 b' def checkfile(ui, filename, initlevel):'
122 138 with open(filename) as fp:
123 139 doc = fp.read()
124 140
125 ui.note(('checking input from %s with initlevel %d\n') %
126 (filename, initlevel))
141 ui.note(
142 'checking input from %s with initlevel %d\n' % (filename, initlevel)
143 )
127 144 return checkseclevel(ui, doc, 'input from %s' % filename, initlevel)
128 145
146
129 147 def main():
130 optparser = optparse.OptionParser("""%prog [options]
148 optparser = optparse.OptionParser(
149 """%prog [options]
131 150
132 151 This checks all help documents of Mercurial (topics, commands,
133 152 extensions and commands of them), if no file is specified by --file
134 153 option.
135 """)
136 optparser.add_option("-v", "--verbose",
137 help="enable additional output",
138 action="store_true")
139 optparser.add_option("-d", "--debug",
140 help="debug mode",
141 action="store_true")
142 optparser.add_option("-f", "--file",
154 """
155 )
156 optparser.add_option(
157 "-v", "--verbose", help="enable additional output", action="store_true"
158 )
159 optparser.add_option(
160 "-d", "--debug", help="debug mode", action="store_true"
161 )
162 optparser.add_option(
163 "-f",
164 "--file",
143 165 help="filename to read in (or '-' for stdin)",
144 action="store", default="")
166 action="store",
167 default="",
168 )
145 169
146 optparser.add_option("-t", "--topic",
170 optparser.add_option(
171 "-t",
172 "--topic",
147 173 help="parse file as help topic",
148 action="store_const", dest="initlevel", const=0)
149 optparser.add_option("-c", "--command",
174 action="store_const",
175 dest="initlevel",
176 const=0,
177 )
178 optparser.add_option(
179 "-c",
180 "--command",
150 181 help="parse file as help of core command",
151 action="store_const", dest="initlevel", const=1)
152 optparser.add_option("-e", "--extension",
182 action="store_const",
183 dest="initlevel",
184 const=1,
185 )
186 optparser.add_option(
187 "-e",
188 "--extension",
153 189 help="parse file as help of extension",
154 action="store_const", dest="initlevel", const=1)
155 optparser.add_option("-C", "--extension-command",
190 action="store_const",
191 dest="initlevel",
192 const=1,
193 )
194 optparser.add_option(
195 "-C",
196 "--extension-command",
156 197 help="parse file as help of extension command",
157 action="store_const", dest="initlevel", const=3)
198 action="store_const",
199 dest="initlevel",
200 const=3,
201 )
158 202
159 optparser.add_option("-l", "--initlevel",
203 optparser.add_option(
204 "-l",
205 "--initlevel",
160 206 help="set initial section level manually",
161 action="store", type="int", default=0)
207 action="store",
208 type="int",
209 default=0,
210 )
162 211
163 212 (options, args) = optparser.parse_args()
164 213
@@ -173,5 +222,6 b' option.'
173 222 if checkhghelps(ui):
174 223 sys.exit(1)
175 224
225
176 226 if __name__ == "__main__":
177 227 main()
@@ -12,6 +12,7 b' import textwrap'
12 12
13 13 try:
14 14 import msvcrt
15
15 16 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
16 17 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
17 18 except ImportError:
@@ -22,10 +23,13 b' except ImportError:'
22 23 os.environ[r'HGMODULEPOLICY'] = r'allow'
23 24 # import from the live mercurial repo
24 25 sys.path.insert(0, r"..")
25 from mercurial import demandimport; demandimport.enable()
26 from mercurial import demandimport
27
28 demandimport.enable()
26 29 # Load util so that the locale path is set by i18n.setdatapath() before
27 30 # calling _().
28 31 from mercurial import util
32
29 33 util.datapath
30 34 from mercurial import (
31 35 commands,
@@ -46,6 +50,7 b' globalopts = commands.globalopts'
46 50 helptable = help.helptable
47 51 loaddoc = help.loaddoc
48 52
53
49 54 def get_desc(docstr):
50 55 if not docstr:
51 56 return b"", b""
@@ -64,6 +69,7 b' def get_desc(docstr):'
64 69
65 70 return (shortdesc, desc)
66 71
72
67 73 def get_opts(opts):
68 74 for opt in opts:
69 75 if len(opt) == 5:
@@ -86,6 +92,7 b' def get_opts(opts):'
86 92 desc += default and _(b" (default: %s)") % bytes(default) or b""
87 93 yield (b", ".join(allopts), desc)
88 94
95
89 96 def get_cmd(cmd, cmdtable):
90 97 d = {}
91 98 attr = cmdtable[cmd]
@@ -106,6 +113,7 b' def get_cmd(cmd, cmdtable):'
106 113
107 114 return d
108 115
116
109 117 def showdoc(ui):
110 118 # print options
111 119 ui.write(minirst.section(_(b"Options")))
@@ -127,14 +135,22 b' def showdoc(ui):'
127 135 helpprinter(ui, helptable, minirst.section, exclude=[b'config'])
128 136
129 137 ui.write(minirst.section(_(b"Extensions")))
130 ui.write(_(b"This section contains help for extensions that are "
138 ui.write(
139 _(
140 b"This section contains help for extensions that are "
131 141 b"distributed together with Mercurial. Help for other "
132 b"extensions is available in the help system."))
133 ui.write((b"\n\n"
142 b"extensions is available in the help system."
143 )
144 )
145 ui.write(
146 (
147 b"\n\n"
134 148 b".. contents::\n"
135 149 b" :class: htmlonly\n"
136 150 b" :local:\n"
137 b" :depth: 1\n\n"))
151 b" :depth: 1\n\n"
152 )
153 )
138 154
139 155 for extensionname in sorted(allextensionnames()):
140 156 mod = extensions.load(ui, extensionname, None)
@@ -143,24 +159,42 b' def showdoc(ui):'
143 159 cmdtable = getattr(mod, 'cmdtable', None)
144 160 if cmdtable:
145 161 ui.write(minirst.subsubsection(_(b'Commands')))
146 commandprinter(ui, cmdtable, minirst.subsubsubsection,
147 minirst.subsubsubsubsection)
162 commandprinter(
163 ui,
164 cmdtable,
165 minirst.subsubsubsection,
166 minirst.subsubsubsubsection,
167 )
168
148 169
149 170 def showtopic(ui, topic):
150 171 extrahelptable = [
151 172 ([b"common"], b'', loaddoc(b'common'), help.TOPIC_CATEGORY_MISC),
152 173 ([b"hg.1"], b'', loaddoc(b'hg.1'), help.TOPIC_CATEGORY_CONFIG),
153 174 ([b"hg-ssh.8"], b'', loaddoc(b'hg-ssh.8'), help.TOPIC_CATEGORY_CONFIG),
154 ([b"hgignore.5"], b'', loaddoc(b'hgignore.5'),
155 help.TOPIC_CATEGORY_CONFIG),
175 (
176 [b"hgignore.5"],
177 b'',
178 loaddoc(b'hgignore.5'),
179 help.TOPIC_CATEGORY_CONFIG,
180 ),
156 181 ([b"hgrc.5"], b'', loaddoc(b'hgrc.5'), help.TOPIC_CATEGORY_CONFIG),
157 ([b"hgignore.5.gendoc"], b'', loaddoc(b'hgignore'),
158 help.TOPIC_CATEGORY_CONFIG),
159 ([b"hgrc.5.gendoc"], b'', loaddoc(b'config'),
160 help.TOPIC_CATEGORY_CONFIG),
182 (
183 [b"hgignore.5.gendoc"],
184 b'',
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 195 helpprinter(ui, helptable + extrahelptable, None, include=[topic])
163 196
197
164 198 def helpprinter(ui, helptable, sectionfunc, include=[], exclude=[]):
165 199 for h in helptable:
166 200 names, sec, doc = h[0:3]
@@ -178,6 +212,7 b' def helpprinter(ui, helptable, sectionfu'
178 212 ui.write(doc)
179 213 ui.write(b"\n")
180 214
215
181 216 def commandprinter(ui, cmdtable, sectionfunc, subsectionfunc):
182 217 """Render restructuredtext describing a list of commands and their
183 218 documentations, grouped by command category.
@@ -222,7 +257,8 b' def commandprinter(ui, cmdtable, section'
222 257 if helpcategory(cmd) not in cmdsbycategory:
223 258 raise AssertionError(
224 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 262 cmdsbycategory[helpcategory(cmd)].append(cmd)
227 263
228 264 # Print the help for each command. We present the commands grouped by
@@ -270,16 +306,22 b' def commandprinter(ui, cmdtable, section'
270 306 if optstr.endswith(b"[+]>"):
271 307 multioccur = True
272 308 if multioccur:
273 ui.write(_(b"\n[+] marked option can be specified"
274 b" multiple times\n"))
309 ui.write(
310 _(
311 b"\n[+] marked option can be specified"
312 b" multiple times\n"
313 )
314 )
275 315 ui.write(b"\n")
276 316 # aliases
277 317 if d[b'aliases']:
278 318 ui.write(_(b" aliases: %s\n\n") % b" ".join(d[b'aliases']))
279 319
320
280 321 def allextensionnames():
281 322 return set(extensions.enabled().keys()) | set(extensions.disabled().keys())
282 323
324
283 325 if __name__ == "__main__":
284 326 doc = b'hg.1.gendoc'
285 327 if len(sys.argv) > 1:
@@ -53,6 +53,7 b' from docutils import ('
53 53 nodes,
54 54 writers,
55 55 )
56
56 57 try:
57 58 import roman
58 59 except ImportError:
@@ -65,7 +66,7 b' BLOCKQOUTE_INDENT = 3.5'
65 66
66 67 # Define two macros so man/roff can calculate the
67 68 # indent/unindent margins by itself
68 MACRO_DEF = (r""".
69 MACRO_DEF = r""".
69 70 .nr rst2man-indent-level 0
70 71 .
71 72 .de1 rstReportMargin
@@ -92,11 +93,12 b' level margin: \\\\n[rst2man-indent\\\\n[rst2'
92 93 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
93 94 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
94 95 ..
95 """)
96 """
97
96 98
97 99 class Writer(writers.Writer):
98 100
99 supported = ('manpage')
101 supported = 'manpage'
100 102 """Formats this writer supports."""
101 103
102 104 output = None
@@ -118,11 +120,14 b' class Table(object):'
118 120 self._options = ['center']
119 121 self._tab_char = '\t'
120 122 self._coldefs = []
123
121 124 def new_row(self):
122 125 self._rows.append([])
126
123 127 def append_separator(self, separator):
124 128 """Append the separator for table head."""
125 129 self._rows.append([separator])
130
126 131 def append_cell(self, cell_lines):
127 132 """cell_lines is an array of lines"""
128 133 start = 0
@@ -131,12 +136,14 b' class Table(object):'
131 136 self._rows[-1].append(cell_lines[start:])
132 137 if len(self._coldefs) < len(self._rows[-1]):
133 138 self._coldefs.append('l')
139
134 140 def _minimize_cell(self, cell_lines):
135 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 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 145 del cell_lines[-1]
146
140 147 def as_list(self):
141 148 text = ['.TS\n']
142 149 text.append(' '.join(self._options) + ';\n')
@@ -159,6 +166,7 b' class Table(object):'
159 166 text.append('.TE\n')
160 167 return text
161 168
169
162 170 class Translator(nodes.NodeVisitor):
163 171 """"""
164 172
@@ -171,8 +179,9 b' class Translator(nodes.NodeVisitor):'
171 179 lcode = settings.language_code
172 180 arglen = len(inspect.getargspec(languages.get_language)[0])
173 181 if arglen == 2:
174 self.language = languages.get_language(lcode,
175 self.document.reporter)
182 self.language = languages.get_language(
183 lcode, self.document.reporter
184 )
176 185 else:
177 186 self.language = languages.get_language(lcode)
178 187 self.head = []
@@ -189,9 +198,11 b' class Translator(nodes.NodeVisitor):'
189 198 # writing the header .TH and .SH NAME is postboned after
190 199 # docinfo.
191 200 self._docinfo = {
192 "title" : "", "title_upper": "",
201 "title": "",
202 "title_upper": "",
193 203 "subtitle" : "",
194 "manual_section" : "", "manual_group" : "",
204 "manual_section": "",
205 "manual_group": "",
195 206 "author" : [],
196 207 "date" : "",
197 208 "copyright" : "",
@@ -222,18 +233,14 b' class Translator(nodes.NodeVisitor):'
222 233 'field_name' : ('.TP\n.B ', '\n'),
223 234 'literal' : ('\\fB', '\\fP'),
224 235 'literal_block' : ('.sp\n.nf\n.ft C\n', '\n.ft P\n.fi\n'),
225
226 236 'option_list_item' : ('.TP\n', ''),
227
228 237 'reference' : (r'\%', r'\:'),
229 238 'emphasis': ('\\fI', '\\fP'),
230 239 'strong' : ('\\fB', '\\fP'),
231 240 'term' : ('\n.B ', '\n'),
232 241 'title_reference' : ('\\fI', '\\fP'),
233
234 242 'topic-title' : ('.SS ',),
235 243 'sidebar-title' : ('.SS ',),
236
237 244 'problematic' : ('\n.nf\n', '\n.fi\n'),
238 245 }
239 246 # NOTE don't specify the newline before a dot-command, but ensure
@@ -244,8 +251,8 b' class Translator(nodes.NodeVisitor):'
244 251 line/comment."""
245 252 prefix = '.\\" '
246 253 out_text = ''.join(
247 [(prefix + in_line + '\n')
248 for in_line in text.split('\n')])
254 [(prefix + in_line + '\n') for in_line in text.split('\n')]
255 )
249 256 return out_text
250 257
251 258 def comment(self, text):
@@ -268,13 +275,18 b' class Translator(nodes.NodeVisitor):'
268 275 if self.body[i] == '.sp\n':
269 276 if self.body[i - 1][:4] in ('.BI ','.IP '):
270 277 self.body[i] = '.\n'
271 elif (self.body[i - 1][:3] == '.B ' and
272 self.body[i - 2][:4] == '.TP\n'):
278 elif (
279 self.body[i - 1][:3] == '.B '
280 and self.body[i - 2][:4] == '.TP\n'
281 ):
273 282 self.body[i] = '.\n'
274 elif (self.body[i - 1] == '\n' and
275 self.body[i - 2][0] != '.' and
276 (self.body[i - 3][:7] == '.TP\n.B '
277 or self.body[i - 3][:4] == '\n.B ')
283 elif (
284 self.body[i - 1] == '\n'
285 and self.body[i - 2][0] != '.'
286 and (
287 self.body[i - 3][:7] == '.TP\n.B '
288 or self.body[i - 3][:4] == '\n.B '
289 )
278 290 ):
279 291 self.body[i] = '.\n'
280 292 return ''.join(self.head + self.body + self.foot)
@@ -358,6 +370,7 b' class Translator(nodes.NodeVisitor):'
358 370
359 371 def get_width(self):
360 372 return self._indent
373
361 374 def __repr__(self):
362 375 return 'enum_style-%s' % list(self._style)
363 376
@@ -376,10 +389,12 b' class Translator(nodes.NodeVisitor):'
376 389 self._list_char.pop()
377 390
378 391 def header(self):
379 tmpl = (".TH %(title_upper)s %(manual_section)s"
392 tmpl = (
393 ".TH %(title_upper)s %(manual_section)s"
380 394 " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
381 395 ".SH NAME\n"
382 "%(title)s \\- %(subtitle)s\n")
396 "%(title)s \\- %(subtitle)s\n"
397 )
383 398 return tmpl % self._docinfo
384 399
385 400 def append_header(self):
@@ -400,8 +415,7 b' class Translator(nodes.NodeVisitor):'
400 415
401 416 def visit_admonition(self, node, name=None):
402 417 if name:
403 self.body.append('.IP %s\n' %
404 self.language.labels.get(name, name))
418 self.body.append('.IP %s\n' % self.language.labels.get(name, name))
405 419
406 420 def depart_admonition(self, node):
407 421 self.body.append('.RE\n')
@@ -488,8 +502,7 b' class Translator(nodes.NodeVisitor):'
488 502 def write_colspecs(self):
489 503 self.body.append("%s.\n" % ('L '*len(self.colspecs)))
490 504
491 def visit_comment(self, node,
492 sub=re.compile('-(?=-)').sub):
505 def visit_comment(self, node, sub=re.compile('-(?=-)').sub):
493 506 self.body.append(self.comment(node.astext()))
494 507 raise nodes.SkipNode()
495 508
@@ -575,21 +588,33 b' class Translator(nodes.NodeVisitor):'
575 588
576 589 def depart_document(self, node):
577 590 if self._docinfo['author']:
578 self.body.append('.SH AUTHOR\n%s\n'
579 % ', '.join(self._docinfo['author']))
580 skip = ('author', 'copyright', 'date',
581 'manual_group', 'manual_section',
591 self.body.append(
592 '.SH AUTHOR\n%s\n' % ', '.join(self._docinfo['author'])
593 )
594 skip = (
595 'author',
596 'copyright',
597 'date',
598 'manual_group',
599 'manual_section',
582 600 'subtitle',
583 'title', 'title_upper', 'version')
601 'title',
602 'title_upper',
603 'version',
604 )
584 605 for name in self._docinfo_keys:
585 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(
608 "\n%s:\n%s%s.nf\n%s\n.fi\n%s%s"
609 % (
587 610 self.language.labels.get(name, name),
588 611 self.defs['indent'][0] % 0,
589 612 self.defs['indent'][0] % BLOCKQOUTE_INDENT,
590 613 self._docinfo[name],
591 614 self.defs['indent'][1],
592 self.defs['indent'][1]))
615 self.defs['indent'][1],
616 )
617 )
593 618 elif name not in skip:
594 619 if name in self._docinfo_names:
595 620 label = self._docinfo_names[name]
@@ -597,10 +622,10 b' class Translator(nodes.NodeVisitor):'
597 622 label = self.language.labels.get(name, name)
598 623 self.body.append("\n%s: %s\n" % (label, self._docinfo[name]))
599 624 if self._docinfo['copyright']:
600 self.body.append('.SH COPYRIGHT\n%s\n'
601 % self._docinfo['copyright'])
602 self.body.append(self.comment(
603 'Generated by docutils manpage writer.\n'))
625 self.body.append('.SH COPYRIGHT\n%s\n' % self._docinfo['copyright'])
626 self.body.append(
627 self.comment('Generated by docutils manpage writer.\n')
628 )
604 629
605 630 def visit_emphasis(self, node):
606 631 self.body.append(self.defs['emphasis'][0])
@@ -611,11 +636,13 b' class Translator(nodes.NodeVisitor):'
611 636 def visit_entry(self, node):
612 637 # a cell in a table row
613 638 if 'morerows' in node:
614 self.document.reporter.warning('"table row spanning" not supported',
615 base_node=node)
639 self.document.reporter.warning(
640 '"table row spanning" not supported', base_node=node
641 )
616 642 if 'morecols' in node:
617 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 646 self.context.append(len(self.body))
620 647
621 648 def depart_entry(self, node):
@@ -675,8 +702,7 b' class Translator(nodes.NodeVisitor):'
675 702 self.dedent()
676 703
677 704 def visit_footer(self, node):
678 self.document.reporter.warning('"footer" not supported',
679 base_node=node)
705 self.document.reporter.warning('"footer" not supported', base_node=node)
680 706
681 707 def depart_footer(self, node):
682 708 pass
@@ -690,8 +716,9 b' class Translator(nodes.NodeVisitor):'
690 716 pass
691 717
692 718 def footnote_backrefs(self, node):
693 self.document.reporter.warning('"footnote_backrefs" not supported',
694 base_node=node)
719 self.document.reporter.warning(
720 '"footnote_backrefs" not supported', base_node=node
721 )
695 722
696 723 def visit_footnote_reference(self, node):
697 724 self.body.append('['+self.deunicode(node.astext())+']')
@@ -736,8 +763,7 b' class Translator(nodes.NodeVisitor):'
736 763 self.body.append('\n')
737 764
738 765 def visit_image(self, node):
739 self.document.reporter.warning('"image" not supported',
740 base_node=node)
766 self.document.reporter.warning('"image" not supported', base_node=node)
741 767 text = []
742 768 if 'alt' in node.attributes:
743 769 text.append(node.attributes['alt'])
@@ -753,11 +779,11 b' class Translator(nodes.NodeVisitor):'
753 779
754 780 def visit_label(self, node):
755 781 # footnote and citation
756 if (isinstance(node.parent, nodes.footnote)
757 or isinstance(node.parent, nodes.citation)):
782 if isinstance(node.parent, nodes.footnote) or isinstance(
783 node.parent, nodes.citation
784 ):
758 785 raise nodes.SkipNode()
759 self.document.reporter.warning('"unsupported "label"',
760 base_node=node)
786 self.document.reporter.warning('"unsupported "label"', base_node=node)
761 787 self.body.append('[')
762 788
763 789 def depart_label(self, node):
@@ -794,9 +820,10 b' class Translator(nodes.NodeVisitor):'
794 820
795 821 def visit_list_item(self, node):
796 822 # man 7 man argues to use ".IP" instead of ".TP"
797 self.body.append('.IP %s %d\n' % (
798 next(self._list_char[-1]),
799 self._list_char[-1].get_width(),))
823 self.body.append(
824 '.IP %s %d\n'
825 % (next(self._list_char[-1]), self._list_char[-1].get_width(),)
826 )
800 827
801 828 def depart_list_item(self, node):
802 829 pass
@@ -968,8 +995,9 b' class Translator(nodes.NodeVisitor):'
968 995 raise nodes.SkipNode()
969 996
970 997 def visit_substitution_reference(self, node):
971 self.document.reporter.warning('"substitution_reference" not supported',
972 base_node=node)
998 self.document.reporter.warning(
999 '"substitution_reference" not supported', base_node=node
1000 )
973 1001
974 1002 def visit_subtitle(self, node):
975 1003 if isinstance(node.parent, nodes.sidebar):
@@ -995,8 +1023,10 b' class Translator(nodes.NodeVisitor):'
995 1023 line = ', line %s' % node['line']
996 1024 else:
997 1025 line = ''
998 self.body.append('.IP "System Message: %s/%s (%s:%s)"\n'
999 % (node['type'], node['level'], node['source'], line))
1026 self.body.append(
1027 '.IP "System Message: %s/%s (%s:%s)"\n'
1028 % (node['type'], node['level'], node['source'], line)
1029 )
1000 1030
1001 1031 def depart_system_message(self, node):
1002 1032 pass
@@ -1111,7 +1141,9 b' class Translator(nodes.NodeVisitor):'
1111 1141 depart_warning = depart_admonition
1112 1142
1113 1143 def unimplemented_visit(self, node):
1114 raise NotImplementedError('visiting unimplemented node type: %s'
1115 % node.__class__.__name__)
1144 raise NotImplementedError(
1145 'visiting unimplemented node type: %s' % node.__class__.__name__
1146 )
1147
1116 1148
1117 1149 # vim: set fileencoding=utf-8 et ts=4 ai :
@@ -71,8 +71,11 b' isenabled = demandimport.isenabled'
71 71 disable = demandimport.disable
72 72 deactivated = demandimport.deactivated
73 73
74
74 75 def enable():
75 76 # chg pre-imports modules so do not enable demandimport for it
76 if ('CHGINTERNALMARK' not in os.environ
77 and os.environ.get('HGDEMANDIMPORT') != 'disable'):
77 if (
78 'CHGINTERNALMARK' not in os.environ
79 and os.environ.get('HGDEMANDIMPORT') != 'disable'
80 ):
78 81 demandimport.enable()
@@ -38,6 +38,7 b' contextmanager = contextlib.contextmanag'
38 38
39 39 nothing = object()
40 40
41
41 42 def _hgextimport(importfunc, name, globals, *args, **kwargs):
42 43 try:
43 44 return importfunc(name, globals, *args, **kwargs)
@@ -53,6 +54,7 b' def _hgextimport(importfunc, name, globa'
53 54 # retry to import with "hgext_" prefix
54 55 return importfunc(hgextname, globals, *args, **kwargs)
55 56
57
56 58 class _demandmod(object):
57 59 """module demand-loader and proxy
58 60
@@ -67,8 +69,9 b' class _demandmod(object):'
67 69 else:
68 70 head = name
69 71 after = []
70 object.__setattr__(self, r"_data",
71 (head, globals, locals, after, level, set()))
72 object.__setattr__(
73 self, r"_data", (head, globals, locals, after, level, set())
74 )
72 75 object.__setattr__(self, r"_module", None)
73 76
74 77 def _extend(self, name):
@@ -91,7 +94,8 b' class _demandmod(object):'
91 94 with tracing.log('demandimport %s', self._data[0]):
92 95 head, globals, locals, after, level, modrefs = self._data
93 96 mod = _hgextimport(
94 _origimport, head, globals, locals, None, level)
97 _origimport, head, globals, locals, None, level
98 )
95 99 if mod is self:
96 100 # In this case, _hgextimport() above should imply
97 101 # _demandimport(). Otherwise, _hgextimport() never
@@ -115,8 +119,11 b' class _demandmod(object):'
115 119 if '.' in p:
116 120 h, t = p.split('.', 1)
117 121 if getattr(mod, h, nothing) is nothing:
118 setattr(mod, h, _demandmod(
119 p, mod.__dict__, mod.__dict__, level=1))
122 setattr(
123 mod,
124 h,
125 _demandmod(p, mod.__dict__, mod.__dict__, level=1),
126 )
120 127 elif t:
121 128 subload(getattr(mod, h), t)
122 129
@@ -164,8 +171,10 b' class _demandmod(object):'
164 171 self._load()
165 172 return self._module.__doc__
166 173
174
167 175 _pypy = '__pypy__' in sys.builtin_module_names
168 176
177
169 178 def _demandimport(name, globals=None, locals=None, fromlist=None, level=-1):
170 179 if locals is None or name in ignores or fromlist == ('*',):
171 180 # these cases we can't really delay
@@ -244,8 +253,9 b' def _demandimport(name, globals=None, lo'
244 253 if level >= 0:
245 254 if name:
246 255 # "from a import b" or "from .a import b" style
247 rootmod = _hgextimport(_origimport, name, globals, locals,
248 level=level)
256 rootmod = _hgextimport(
257 _origimport, name, globals, locals, level=level
258 )
249 259 mod = chainmodules(rootmod, name)
250 260 elif _pypy:
251 261 # PyPy's __import__ throws an exception if invoked
@@ -260,8 +270,9 b' def _demandimport(name, globals=None, lo'
260 270 mn = mn.rsplit('.', level - 1)[0]
261 271 mod = sys.modules[mn]
262 272 else:
263 mod = _hgextimport(_origimport, name, globals, locals,
264 level=level)
273 mod = _hgextimport(
274 _origimport, name, globals, locals, level=level
275 )
265 276
266 277 for x in fromlist:
267 278 processfromitem(mod, x)
@@ -278,23 +289,29 b' def _demandimport(name, globals=None, lo'
278 289
279 290 return mod
280 291
292
281 293 ignores = set()
282 294
295
283 296 def init(ignoreset):
284 297 global ignores
285 298 ignores = ignoreset
286 299
300
287 301 def isenabled():
288 302 return builtins.__import__ == _demandimport
289 303
304
290 305 def enable():
291 306 "enable global demand-loading of modules"
292 307 builtins.__import__ = _demandimport
293 308
309
294 310 def disable():
295 311 "disable global demand-loading of modules"
296 312 builtins.__import__ = _origimport
297 313
314
298 315 @contextmanager
299 316 def deactivated():
300 317 "context manager for disabling demandimport in 'with' blocks"
@@ -36,10 +36,12 b' from . import tracing'
36 36
37 37 _deactivated = False
38 38
39
39 40 class _lazyloaderex(importlib.util.LazyLoader):
40 41 """This is a LazyLoader except it also follows the _deactivated global and
41 42 the ignore list.
42 43 """
44
43 45 def exec_module(self, module):
44 46 """Make the module load lazily."""
45 47 with tracing.log('demandimport %s', module):
@@ -48,14 +50,18 b' class _lazyloaderex(importlib.util.LazyL'
48 50 else:
49 51 super().exec_module(module)
50 52
53
51 54 # This is 3.6+ because with Python 3.5 it isn't possible to lazily load
52 55 # extensions. See the discussion in https://bugs.python.org/issue26186 for more.
53 56 _extensions_loader = _lazyloaderex.factory(
54 importlib.machinery.ExtensionFileLoader)
57 importlib.machinery.ExtensionFileLoader
58 )
55 59 _bytecode_loader = _lazyloaderex.factory(
56 importlib.machinery.SourcelessFileLoader)
60 importlib.machinery.SourcelessFileLoader
61 )
57 62 _source_loader = _lazyloaderex.factory(importlib.machinery.SourceFileLoader)
58 63
64
59 65 def _makefinder(path):
60 66 return importlib.machinery.FileFinder(
61 67 path,
@@ -65,15 +71,19 b' def _makefinder(path):'
65 71 (_bytecode_loader, importlib.machinery.BYTECODE_SUFFIXES),
66 72 )
67 73
74
68 75 ignores = set()
69 76
77
70 78 def init(ignoreset):
71 79 global ignores
72 80 ignores = ignoreset
73 81
82
74 83 def isenabled():
75 84 return _makefinder in sys.path_hooks and not _deactivated
76 85
86
77 87 def disable():
78 88 try:
79 89 while True:
@@ -81,9 +91,11 b' def disable():'
81 91 except ValueError:
82 92 pass
83 93
94
84 95 def enable():
85 96 sys.path_hooks.insert(0, _makefinder)
86 97
98
87 99 @contextlib.contextmanager
88 100 def deactivated():
89 101 # This implementation is a bit different from Python 2's. Python 3
@@ -14,6 +14,7 b' import os'
14 14 _checked = False
15 15 _session = 'none'
16 16
17
17 18 def _isactive():
18 19 global _pipe, _session, _checked
19 20 if _pipe is None:
@@ -26,6 +27,7 b' def _isactive():'
26 27 _session = os.environ.get('HGCATAPULTSESSION', 'none')
27 28 return True
28 29
30
29 31 @contextlib.contextmanager
30 32 def log(whencefmt, *whenceargs):
31 33 if not _isactive():
@@ -48,6 +50,7 b' def log(whencefmt, *whenceargs):'
48 50 except IOError:
49 51 pass
50 52
53
51 54 def counter(label, amount, *labelargs):
52 55 if not _isactive():
53 56 return
@@ -1,3 +1,4 b''
1 1 from __future__ import absolute_import
2 2 import pkgutil
3
3 4 __path__ = pkgutil.extend_path(__path__, __name__)
@@ -53,9 +53,7 b' from mercurial import ('
53 53 scmutil,
54 54 util,
55 55 )
56 from mercurial.utils import (
57 stringutil,
58 )
56 from mercurial.utils import stringutil
59 57
60 58 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
61 59 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
@@ -81,8 +79,10 b' colortable = {'
81 79
82 80 defaultdict = collections.defaultdict
83 81
82
84 83 class nullui(object):
85 84 """blank ui object doing nothing"""
85
86 86 debugflag = False
87 87 verbose = False
88 88 quiet = True
@@ -90,16 +90,20 b' class nullui(object):'
90 90 def __getitem__(name):
91 91 def nullfunc(*args, **kwds):
92 92 return
93
93 94 return nullfunc
94 95
96
95 97 class emptyfilecontext(object):
96 98 """minimal filecontext representing an empty file"""
99
97 100 def data(self):
98 101 return ''
99 102
100 103 def node(self):
101 104 return node.nullid
102 105
106
103 107 def uniq(lst):
104 108 """list -> list. remove duplicated items without changing the order"""
105 109 seen = set()
@@ -110,6 +114,7 b' def uniq(lst):'
110 114 result.append(x)
111 115 return result
112 116
117
113 118 def getdraftstack(headctx, limit=None):
114 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 137 result.reverse()
133 138 return result
134 139
140
135 141 def getfilestack(stack, path, seenfctxs=None):
136 142 """([ctx], str, set) -> [fctx], {ctx: fctx}
137 143
@@ -213,10 +219,12 b' def getfilestack(stack, path, seenfctxs='
213 219 # remove uniq and find a different way to identify fctxs.
214 220 return uniq(fctxs), fctxmap
215 221
222
216 223 class overlaystore(patch.filestore):
217 224 """read-only, hybrid store based on a dict and ctx.
218 225 memworkingcopy: {path: content}, overrides file contents.
219 226 """
227
220 228 def __init__(self, basectx, memworkingcopy):
221 229 self.basectx = basectx
222 230 self.memworkingcopy = memworkingcopy
@@ -234,6 +242,7 b' class overlaystore(patch.filestore):'
234 242 copy = fctx.copysource()
235 243 return content, mode, copy
236 244
245
237 246 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
238 247 """({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx
239 248 memworkingcopy overrides file contents.
@@ -249,9 +258,17 b' def overlaycontext(memworkingcopy, ctx, '
249 258 files = set(ctx.files()).union(memworkingcopy)
250 259 store = overlaystore(ctx, memworkingcopy)
251 260 return context.memctx(
252 repo=ctx.repo(), parents=parents, text=desc,
253 files=files, filectxfn=store, user=user, date=date,
254 branch=None, extra=extra)
261 repo=ctx.repo(),
262 parents=parents,
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 273 class filefixupstate(object):
257 274 """state needed to apply fixups to a single file
@@ -346,9 +363,10 b' class filefixupstate(object):'
346 363 blines = self.targetlines[b1:b2]
347 364 if self.ui.debugflag:
348 365 idx = (max(rev - 1, 0)) // 2
349 self.ui.write(_('%s: chunk %d:%d -> %d lines\n')
350 % (node.short(self.fctxs[idx].node()),
351 a1, a2, len(blines)))
366 self.ui.write(
367 _('%s: chunk %d:%d -> %d lines\n')
368 % (node.short(self.fctxs[idx].node()), a1, a2, len(blines))
369 )
352 370 self.linelog.replacelines(rev, a1, a2, b1, b2)
353 371 if self.opts.get('edit_lines', False):
354 372 self.finalcontents = self._checkoutlinelogwithedits()
@@ -386,8 +404,9 b' class filefixupstate(object):'
386 404 # pure insertion, check nearby lines. ignore lines belong
387 405 # to the public (first) changeset (i.e. annotated[i][0] == 1)
388 406 nearbylinenums = {a2, max(0, a1 - 1)}
389 involved = [annotated[i]
390 for i in nearbylinenums if annotated[i][0] != 1]
407 involved = [
408 annotated[i] for i in nearbylinenums if annotated[i][0] != 1
409 ]
391 410 involvedrevs = list(set(r for r, l in involved))
392 411 newfixups = []
393 412 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
@@ -448,18 +467,26 b' class filefixupstate(object):'
448 467 """() -> [str]. prompt all lines for edit"""
449 468 alllines = self.linelog.getalllines()
450 469 # header
451 editortext = (_('HG: editing %s\nHG: "y" means the line to the right '
452 'exists in the changeset to the top\nHG:\n')
453 % self.fctxs[-1].path())
470 editortext = (
471 _(
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 477 # [(idx, fctx)]. hide the dummy emptyfilecontext
455 visiblefctxs = [(i, f)
478 visiblefctxs = [
479 (i, f)
456 480 for i, f in enumerate(self.fctxs)
457 if not isinstance(f, emptyfilecontext)]
481 if not isinstance(f, emptyfilecontext)
482 ]
458 483 for i, (j, f) in enumerate(visiblefctxs):
459 editortext += (_('HG: %s/%s %s %s\n') %
460 ('|' * i, '-' * (len(visiblefctxs) - i + 1),
484 editortext += _('HG: %s/%s %s %s\n') % (
485 '|' * i,
486 '-' * (len(visiblefctxs) - i + 1),
461 487 node.short(f.node()),
462 f.description().split('\n',1)[0]))
488 f.description().split('\n', 1)[0],
489 )
463 490 editortext += _('HG: %s\n') % ('|' * len(visiblefctxs))
464 491 # figure out the lifetime of a line, this is relatively inefficient,
465 492 # but probably fine
@@ -470,10 +497,15 b' class filefixupstate(object):'
470 497 lineset[l].add(i)
471 498 # append lines
472 499 for l in alllines:
473 editortext += (' %s : %s' %
474 (''.join([('y' if i in lineset[l] else ' ')
475 for i, _f in visiblefctxs]),
476 self._getline(l)))
500 editortext += ' %s : %s' % (
501 ''.join(
502 [
503 ('y' if i in lineset[l] else ' ')
504 for i, _f in visiblefctxs
505 ]
506 ),
507 self._getline(l),
508 )
477 509 # run editor
478 510 editedtext = self.ui.edit(editortext, '', action='absorb')
479 511 if not editedtext:
@@ -489,7 +521,8 b' class filefixupstate(object):'
489 521 raise error.Abort(_('malformed line: %s') % l)
490 522 linecontent = l[colonpos + 2:]
491 523 for i, ch in enumerate(
492 pycompat.bytestr(l[leftpadpos:colonpos - 1])):
524 pycompat.bytestr(l[leftpadpos : colonpos - 1])
525 ):
493 526 if ch == 'y':
494 527 contents[visiblefctxs[i][0]] += linecontent
495 528 # chunkstats is hard to calculate if anything changes, therefore
@@ -539,8 +572,12 b' class filefixupstate(object):'
539 572 lastrev = pcurrentchunk[0][0]
540 573 lasta2 = pcurrentchunk[0][2]
541 574 lastb2 = pcurrentchunk[0][4]
542 if (a1 == lasta2 and b1 == lastb2 and rev == lastrev and
543 self._iscontinuous(max(a1 - 1, 0), a1)):
575 if (
576 a1 == lasta2
577 and b1 == lastb2
578 and rev == lastrev
579 and self._iscontinuous(max(a1 - 1, 0), a1)
580 ):
544 581 # merge into currentchunk
545 582 pcurrentchunk[0][2] = a2
546 583 pcurrentchunk[0][4] = b2
@@ -551,7 +588,6 b' class filefixupstate(object):'
551 588 return result
552 589
553 590 def _showchanges(self, fm, alines, blines, chunk, fixups):
554
555 591 def trim(line):
556 592 if line.endswith('\n'):
557 593 line = line[:-1]
@@ -568,9 +604,12 b' class filefixupstate(object):'
568 604 bidxs[i - b1] = (max(idx, 1) - 1) // 2
569 605
570 606 fm.startitem()
571 fm.write('hunk', ' %s\n',
572 '@@ -%d,%d +%d,%d @@'
573 % (a1, a2 - a1, b1, b2 - b1), label='diff.hunk')
607 fm.write(
608 'hunk',
609 ' %s\n',
610 '@@ -%d,%d +%d,%d @@' % (a1, a2 - a1, b1, b2 - b1),
611 label='diff.hunk',
612 )
574 613 fm.data(path=self.path, linetype='hunk')
575 614
576 615 def writeline(idx, diffchar, line, linetype, linelabel):
@@ -582,16 +621,24 b' class filefixupstate(object):'
582 621 node = ctx.hex()
583 622 self.ctxaffected.add(ctx.changectx())
584 623 fm.write('node', '%-7.7s ', node, label='absorb.node')
585 fm.write('diffchar ' + linetype, '%s%s\n', diffchar, line,
586 label=linelabel)
624 fm.write(
625 'diffchar ' + linetype,
626 '%s%s\n',
627 diffchar,
628 line,
629 label=linelabel,
630 )
587 631 fm.data(path=self.path, linetype=linetype)
588 632
589 633 for i in pycompat.xrange(a1, a2):
590 writeline(aidxs[i - a1], '-', trim(alines[i]), 'deleted',
591 'diff.deleted')
634 writeline(
635 aidxs[i - a1], '-', trim(alines[i]), 'deleted', 'diff.deleted'
636 )
592 637 for i in pycompat.xrange(b1, b2):
593 writeline(bidxs[i - b1], '+', trim(blines[i]), 'inserted',
594 'diff.inserted')
638 writeline(
639 bidxs[i - b1], '+', trim(blines[i]), 'inserted', 'diff.inserted'
640 )
641
595 642
596 643 class fixupstate(object):
597 644 """state needed to run absorb
@@ -648,9 +695,11 b' class fixupstate(object):'
648 695 targetfctx = targetctx[path]
649 696 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
650 697 # ignore symbolic links or binary, or unchanged files
651 if any(f.islink() or stringutil.binary(f.data())
698 if any(
699 f.islink() or stringutil.binary(f.data())
652 700 for f in [targetfctx] + fctxs
653 if not isinstance(f, emptyfilecontext)):
701 if not isinstance(f, emptyfilecontext)
702 ):
654 703 continue
655 704 if targetfctx.data() == fctxs[-1].data() and not editopt:
656 705 continue
@@ -677,8 +726,10 b' class fixupstate(object):'
677 726 @property
678 727 def chunkstats(self):
679 728 """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
680 return dict((path, state.chunkstats)
681 for path, state in self.fixupmap.iteritems())
729 return dict(
730 (path, state.chunkstats)
731 for path, state in self.fixupmap.iteritems()
732 )
682 733
683 734 def commit(self):
684 735 """commit changes. update self.finalnode, self.replacemap"""
@@ -698,8 +749,10 b' class fixupstate(object):'
698 749 # chunkstats for each file
699 750 for path, stat in chunkstats.iteritems():
700 751 if stat[0]:
701 ui.write(_('%s: %d of %d chunk(s) applied\n')
702 % (path, stat[0], stat[1]))
752 ui.write(
753 _('%s: %d of %d chunk(s) applied\n')
754 % (path, stat[0], stat[1])
755 )
703 756 elif not ui.quiet:
704 757 # a summary for all files
705 758 stats = chunkstats.values()
@@ -733,7 +786,9 b' class fixupstate(object):'
733 786 self.replacemap[ctx.node()] = lastcommitted.node()
734 787 if memworkingcopy:
735 788 msg = _('%d file(s) changed, became %s') % (
736 len(memworkingcopy), self._ctx2str(lastcommitted))
789 len(memworkingcopy),
790 self._ctx2str(lastcommitted),
791 )
737 792 else:
738 793 msg = _('became %s') % self._ctx2str(lastcommitted)
739 794 if self.ui.verbose and msg:
@@ -766,16 +821,19 b' class fixupstate(object):'
766 821
767 822 def _movebookmarks(self, tr):
768 823 repo = self.repo
769 needupdate = [(name, self.replacemap[hsh])
824 needupdate = [
825 (name, self.replacemap[hsh])
770 826 for name, hsh in repo._bookmarks.iteritems()
771 if hsh in self.replacemap]
827 if hsh in self.replacemap
828 ]
772 829 changes = []
773 830 for name, hsh in needupdate:
774 831 if hsh:
775 832 changes.append((name, hsh))
776 833 if self.ui.verbose:
777 self.ui.write(_('moving bookmark %s to %s\n')
778 % (name, node.hex(hsh)))
834 self.ui.write(
835 _('moving bookmark %s to %s\n') % (name, node.hex(hsh))
836 )
779 837 else:
780 838 changes.append((name, None))
781 839 if self.ui.verbose:
@@ -798,8 +856,10 b' class fixupstate(object):'
798 856 restore = noop
799 857 if util.safehasattr(dirstate, '_fsmonitorstate'):
800 858 bak = dirstate._fsmonitorstate.invalidate
859
801 860 def restore():
802 861 dirstate._fsmonitorstate.invalidate = bak
862
803 863 dirstate._fsmonitorstate.invalidate = noop
804 864 try:
805 865 with dirstate.parentchange():
@@ -852,11 +912,15 b' class fixupstate(object):'
852 912 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
853 913
854 914 def _cleanupoldcommits(self):
855 replacements = {k: ([v] if v is not None else [])
856 for k, v in self.replacemap.iteritems()}
915 replacements = {
916 k: ([v] if v is not None else [])
917 for k, v in self.replacemap.iteritems()
918 }
857 919 if replacements:
858 scmutil.cleanupnodes(self.repo, replacements, operation='absorb',
859 fixphase=True)
920 scmutil.cleanupnodes(
921 self.repo, replacements, operation='absorb', fixphase=True
922 )
923
860 924
861 925 def _parsechunk(hunk):
862 926 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
@@ -874,6 +938,7 b' def _parsechunk(hunk):'
874 938 blines = [l[1:] for l in patchlines[1:] if not l.startswith('-')]
875 939 return path, (a1, a2, blines)
876 940
941
877 942 def overlaydiffcontext(ctx, chunks):
878 943 """(ctx, [crecord.uihunk]) -> memctx
879 944
@@ -905,6 +970,7 b' def overlaydiffcontext(ctx, chunks):'
905 970 memworkingcopy[path] = ''.join(lines)
906 971 return overlaycontext(memworkingcopy, ctx)
907 972
973
908 974 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
909 975 """pick fixup chunks from targetctx, apply them to stack.
910 976
@@ -919,9 +985,10 b' def absorb(ui, repo, stack=None, targetc'
919 985 raise error.Abort(_('cannot absorb into a merge'))
920 986 stack = getdraftstack(headctx, limit)
921 987 if limit and len(stack) >= limit:
922 ui.warn(_('absorb: only the recent %d changesets will '
923 'be analysed\n')
924 % limit)
988 ui.warn(
989 _('absorb: only the recent %d changesets will ' 'be analysed\n')
990 % limit
991 )
925 992 if not stack:
926 993 raise error.Abort(_('no mutable changeset to change'))
927 994 if targetctx is None: # default to working copy
@@ -953,13 +1020,19 b' def absorb(ui, repo, stack=None, targetc'
953 1020 fm.data(linetype='changeset')
954 1021 fm.write('node', '%-7.7s ', ctx.hex(), label='absorb.node')
955 1022 descfirstline = ctx.description().splitlines()[0]
956 fm.write('descfirstline', '%s\n', descfirstline,
957 label='absorb.description')
1023 fm.write(
1024 'descfirstline',
1025 '%s\n',
1026 descfirstline,
1027 label='absorb.description',
1028 )
958 1029 fm.end()
959 1030 if not opts.get('dry_run'):
960 if (not opts.get('apply_changes') and
961 state.ctxaffected and
962 ui.promptchoice("apply changes (yn)? $$ &Yes $$ &No", default=1)):
1031 if (
1032 not opts.get('apply_changes')
1033 and state.ctxaffected
1034 and ui.promptchoice("apply changes (yn)? $$ &Yes $$ &No", default=1)
1035 ):
963 1036 raise error.Abort(_('absorb cancelled\n'))
964 1037
965 1038 state.apply()
@@ -969,20 +1042,45 b' def absorb(ui, repo, stack=None, targetc'
969 1042 ui.write(_('nothing applied\n'))
970 1043 return state
971 1044
972 @command('absorb',
973 [('a', 'apply-changes', None,
974 _('apply changes without prompting for confirmation')),
975 ('p', 'print-changes', None,
976 _('always print which changesets are modified by which changes')),
977 ('i', 'interactive', None,
978 _('interactively select which chunks to apply (EXPERIMENTAL)')),
979 ('e', 'edit-lines', None,
980 _('edit what lines belong to which changesets before commit '
981 '(EXPERIMENTAL)')),
982 ] + commands.dryrunopts + commands.templateopts + commands.walkopts,
1045
1046 @command(
1047 'absorb',
1048 [
1049 (
1050 'a',
1051 'apply-changes',
1052 None,
1053 _('apply changes without prompting for confirmation'),
1054 ),
1055 (
1056 'p',
1057 'print-changes',
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,
983 1080 _('hg absorb [OPTION] [FILE]...'),
984 1081 helpcategory=command.CATEGORY_COMMITTING,
985 helpbasic=True)
1082 helpbasic=True,
1083 )
986 1084 def absorbcmd(ui, repo, *pats, **opts):
987 1085 """incorporate corrections into the stack of draft changesets
988 1086
@@ -224,9 +224,7 b' from mercurial import ('
224 224 registrar,
225 225 util,
226 226 )
227 from mercurial.utils import (
228 procutil,
229 )
227 from mercurial.utils import procutil
230 228
231 229 urlreq = util.urlreq
232 230
@@ -240,33 +238,29 b' configtable = {}'
240 238 configitem = registrar.configitem(configtable)
241 239
242 240 # deprecated config: acl.config
243 configitem('acl', 'config',
244 default=None,
241 configitem(
242 'acl', 'config', default=None,
245 243 )
246 configitem('acl.groups', '.*',
247 default=None,
248 generic=True,
244 configitem(
245 'acl.groups', '.*', default=None, generic=True,
249 246 )
250 configitem('acl.deny.branches', '.*',
251 default=None,
252 generic=True,
247 configitem(
248 'acl.deny.branches', '.*', default=None, generic=True,
253 249 )
254 configitem('acl.allow.branches', '.*',
255 default=None,
256 generic=True,
250 configitem(
251 'acl.allow.branches', '.*', default=None, generic=True,
257 252 )
258 configitem('acl.deny', '.*',
259 default=None,
260 generic=True,
253 configitem(
254 'acl.deny', '.*', default=None, generic=True,
261 255 )
262 configitem('acl.allow', '.*',
263 default=None,
264 generic=True,
256 configitem(
257 'acl.allow', '.*', default=None, generic=True,
265 258 )
266 configitem('acl', 'sources',
267 default=lambda: ['serve'],
259 configitem(
260 'acl', 'sources', default=lambda: ['serve'],
268 261 )
269 262
263
270 264 def _getusers(ui, group):
271 265
272 266 # First, try to use group definition from section [acl.groups]
@@ -281,6 +275,7 b' def _getusers(ui, group):'
281 275 except KeyError:
282 276 raise error.Abort(_("group '%s' is undefined") % group)
283 277
278
284 279 def _usermatch(ui, user, usersorgroups):
285 280
286 281 if usersorgroups == '*':
@@ -293,29 +288,35 b' def _usermatch(ui, user, usersorgroups):'
293 288 # if ug is a user name: !username
294 289 # if ug is a group name: !@groupname
295 290 ug = ug[1:]
296 if (not ug.startswith('@') and user != ug
297 or ug.startswith('@') and user not in _getusers(ui, ug[1:])):
291 if (
292 not ug.startswith('@')
293 and user != ug
294 or ug.startswith('@')
295 and user not in _getusers(ui, ug[1:])
296 ):
298 297 return True
299 298
300 299 # Test for user or group. Format:
301 300 # if ug is a user name: username
302 301 # if ug is a group name: @groupname
303 elif (user == ug
304 or ug.startswith('@') and user in _getusers(ui, ug[1:])):
302 elif user == ug or ug.startswith('@') and user in _getusers(ui, ug[1:]):
305 303 return True
306 304
307 305 return False
308 306
307
309 308 def buildmatch(ui, repo, user, key):
310 309 '''return tuple of (match function, list enabled).'''
311 310 if not ui.has_section(key):
312 311 ui.debug('acl: %s not enabled\n' % key)
313 312 return None
314 313
315 pats = [pat for pat, users in ui.configitems(key)
316 if _usermatch(ui, user, users)]
317 ui.debug('acl: %s enabled, %d entries for user %s\n' %
318 (key, len(pats), user))
314 pats = [
315 pat for pat, users in ui.configitems(key) if _usermatch(ui, user, users)
316 ]
317 ui.debug(
318 'acl: %s enabled, %d entries for user %s\n' % (key, len(pats), user)
319 )
319 320
320 321 # Branch-based ACL
321 322 if not repo:
@@ -332,6 +333,7 b' def buildmatch(ui, repo, user, key):'
332 333 return match.match(repo.root, '', pats)
333 334 return util.never
334 335
336
335 337 def ensureenabled(ui):
336 338 """make sure the extension is enabled when used as hook
337 339
@@ -345,16 +347,22 b' def ensureenabled(ui):'
345 347 ui.setconfig('extensions', 'acl', '', source='internal')
346 348 extensions.loadall(ui, ['acl'])
347 349
350
348 351 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
349 352
350 353 ensureenabled(ui)
351 354
352 355 if hooktype not in ['pretxnchangegroup', 'pretxncommit', 'prepushkey']:
353 356 raise error.Abort(
354 _('config error - hook type "%s" cannot stop '
355 'incoming changesets, commits, nor bookmarks') % hooktype)
356 if (hooktype == 'pretxnchangegroup' and
357 source not in ui.configlist('acl', 'sources')):
357 _(
358 'config error - hook type "%s" cannot stop '
359 'incoming changesets, commits, nor bookmarks'
360 )
361 % hooktype
362 )
363 if hooktype == 'pretxnchangegroup' and source not in ui.configlist(
364 'acl', 'sources'
365 ):
358 366 ui.debug('acl: changes have source "%s" - skipping\n' % source)
359 367 return
360 368
@@ -374,6 +382,7 b' def hook(ui, repo, hooktype, node=None, '
374 382 else:
375 383 _txnhook(ui, repo, hooktype, node, source, user, **kwargs)
376 384
385
377 386 def _pkhook(ui, repo, hooktype, node, source, user, **kwargs):
378 387 if kwargs[r'namespace'] == 'bookmarks':
379 388 bookmark = kwargs[r'key']
@@ -382,22 +391,38 b' def _pkhook(ui, repo, hooktype, node, so'
382 391 denybookmarks = buildmatch(ui, None, user, 'acl.deny.bookmarks')
383 392
384 393 if denybookmarks and denybookmarks(bookmark):
385 raise error.Abort(_('acl: user "%s" denied on bookmark "%s"'
386 ' (changeset "%s")')
387 % (user, bookmark, ctx))
394 raise error.Abort(
395 _('acl: user "%s" denied on bookmark "%s"' ' (changeset "%s")')
396 % (user, bookmark, ctx)
397 )
388 398 if allowbookmarks and not allowbookmarks(bookmark):
389 raise error.Abort(_('acl: user "%s" not allowed on bookmark "%s"'
390 ' (changeset "%s")')
391 % (user, bookmark, ctx))
392 ui.debug('acl: bookmark access granted: "%s" on bookmark "%s"\n'
393 % (ctx, bookmark))
399 raise error.Abort(
400 _(
401 'acl: user "%s" not allowed on bookmark "%s"'
402 ' (changeset "%s")'
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 412 def _txnhook(ui, repo, hooktype, node, source, user, **kwargs):
396 413 # deprecated config: acl.config
397 414 cfg = ui.config('acl', 'config')
398 415 if cfg:
399 ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
400 'acl.deny.branches', 'acl.allow', 'acl.deny'])
416 ui.readconfig(
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 427 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
403 428 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
@@ -408,21 +433,31 b' def _txnhook(ui, repo, hooktype, node, s'
408 433 ctx = repo[rev]
409 434 branch = ctx.branch()
410 435 if denybranches and denybranches(branch):
411 raise error.Abort(_('acl: user "%s" denied on branch "%s"'
412 ' (changeset "%s")')
413 % (user, branch, ctx))
436 raise error.Abort(
437 _('acl: user "%s" denied on branch "%s"' ' (changeset "%s")')
438 % (user, branch, ctx)
439 )
414 440 if allowbranches and not allowbranches(branch):
415 raise error.Abort(_('acl: user "%s" not allowed on branch "%s"'
416 ' (changeset "%s")')
417 % (user, branch, ctx))
418 ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
419 % (ctx, branch))
441 raise error.Abort(
442 _(
443 'acl: user "%s" not allowed on branch "%s"'
444 ' (changeset "%s")'
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 452 for f in ctx.files():
422 453 if deny and deny(f):
423 raise error.Abort(_('acl: user "%s" denied on "%s"'
424 ' (changeset "%s")') % (user, f, ctx))
454 raise error.Abort(
455 _('acl: user "%s" denied on "%s"' ' (changeset "%s")')
456 % (user, f, ctx)
457 )
425 458 if allow and not allow(f):
426 raise error.Abort(_('acl: user "%s" not allowed on "%s"'
427 ' (changeset "%s")') % (user, f, ctx))
459 raise error.Abort(
460 _('acl: user "%s" not allowed on "%s"' ' (changeset "%s")')
461 % (user, f, ctx)
462 )
428 463 ui.debug('acl: path access granted: "%s"\n' % ctx)
@@ -29,20 +29,35 b" testedwith = 'ships-with-hg-core'"
29 29 cmdtable = {}
30 30 command = registrar.command(cmdtable)
31 31
32 @command('amend',
33 [('A', 'addremove', None,
34 _('mark new/missing files as added/removed before committing')),
32
33 @command(
34 'amend',
35 [
36 (
37 'A',
38 'addremove',
39 None,
40 _('mark new/missing files as added/removed before committing'),
41 ),
35 42 ('e', 'edit', None, _('invoke editor on commit messages')),
36 43 ('i', 'interactive', None, _('use interactive mode')),
37 (b'', b'close-branch', None,
38 _(b'mark a branch as closed, hiding it from the branch list')),
44 (
45 b'',
46 b'close-branch',
47 None,
48 _(b'mark a branch as closed, hiding it from the branch list'),
49 ),
39 50 (b's', b'secret', None, _(b'use the secret phase for committing')),
40 51 ('n', 'note', '', _('store a note on the amend')),
41 ] + cmdutil.walkopts + cmdutil.commitopts + cmdutil.commitopts2
52 ]
53 + cmdutil.walkopts
54 + cmdutil.commitopts
55 + cmdutil.commitopts2
42 56 + cmdutil.commitopts3,
43 57 _('[OPTION]... [FILE]...'),
44 58 helpcategory=command.CATEGORY_COMMITTING,
45 inferrepo=True)
59 inferrepo=True,
60 )
46 61 def amend(ui, repo, *pats, **opts):
47 62 """amend the working copy parent with all or specified outstanding changes
48 63
@@ -35,22 +35,23 b' from mercurial import ('
35 35 pycompat,
36 36 registrar,
37 37 scmutil,
38 similar
38 similar,
39 39 )
40 40
41 41 configtable = {}
42 42 configitem = registrar.configitem(configtable)
43 43
44 configitem('automv', 'similarity',
45 default=95,
44 configitem(
45 'automv', 'similarity', default=95,
46 46 )
47 47
48
48 49 def extsetup(ui):
49 entry = extensions.wrapcommand(
50 commands.table, 'commit', mvcheck)
50 entry = extensions.wrapcommand(commands.table, 'commit', mvcheck)
51 51 entry[1].append(
52 ('', 'no-automv', None,
53 _('disable automatic file move detection')))
52 ('', 'no-automv', None, _('disable automatic file move detection'))
53 )
54
54 55
55 56 def mvcheck(orig, ui, repo, *pats, **opts):
56 57 """Hook to check for moves at commit time"""
@@ -65,14 +66,16 b' def mvcheck(orig, ui, repo, *pats, **opt'
65 66 match = scmutil.match(repo[None], pats, opts)
66 67 added, removed = _interestingfiles(repo, match)
67 68 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
68 renames = _findrenames(repo, uipathfn, added, removed,
69 threshold / 100.0)
69 renames = _findrenames(
70 repo, uipathfn, added, removed, threshold / 100.0
71 )
70 72
71 73 with repo.wlock():
72 74 if renames is not None:
73 75 scmutil._markchanges(repo, (), (), renames)
74 76 return orig(ui, repo, *pats, **pycompat.strkwargs(opts))
75 77
78
76 79 def _interestingfiles(repo, matcher):
77 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 94 return added, removed
92 95
96
93 97 def _findrenames(repo, uipathfn, added, removed, similarity):
94 98 """Find what files in added are really moved files.
95 99
@@ -100,11 +104,13 b' def _findrenames(repo, uipathfn, added, '
100 104 renames = {}
101 105 if similarity > 0:
102 106 for src, dst, score in similar.findrenames(
103 repo, added, removed, similarity):
107 repo, added, removed, similarity
108 ):
104 109 if repo.ui.verbose:
105 110 repo.ui.status(
106 _('detected move of %s as %s (%d%% similar)\n') % (
107 uipathfn(src), uipathfn(dst), score * 100))
111 _('detected move of %s as %s (%d%% similar)\n')
112 % (uipathfn(src), uipathfn(dst), score * 100)
113 )
108 114 renames[dst] = src
109 115 if renames:
110 116 repo.ui.status(_('detected move of %d files\n') % len(renames))
@@ -28,6 +28,7 b' from mercurial import ('
28 28 # leave the attribute unspecified.
29 29 testedwith = 'ships-with-hg-core'
30 30
31
31 32 def prettyedge(before, edge, after):
32 33 if edge == '~':
33 34 return '\xE2\x95\xA7' # U+2567 ╧
@@ -49,15 +50,21 b' def prettyedge(before, edge, after):'
49 50 return '\xE2\x94\xBC' # U+253C ┼
50 51 return edge
51 52
53
52 54 def convertedges(line):
53 55 line = ' %s ' % line
54 56 pretty = []
55 57 for idx in pycompat.xrange(len(line) - 2):
56 pretty.append(prettyedge(line[idx:idx + 1],
58 pretty.append(
59 prettyedge(
60 line[idx : idx + 1],
57 61 line[idx + 1:idx + 2],
58 line[idx + 2:idx + 3]))
62 line[idx + 2 : idx + 3],
63 )
64 )
59 65 return ''.join(pretty)
60 66
67
61 68 def getprettygraphnode(orig, *args, **kwargs):
62 69 node = orig(*args, **kwargs)
63 70 if node == 'o':
@@ -72,11 +79,13 b' def getprettygraphnode(orig, *args, **kw'
72 79 return '\xE2\x95\xA4' # U+2564 ╤
73 80 return node
74 81
82
75 83 def outputprettygraph(orig, ui, graph, *args, **kwargs):
76 84 (edges, text) = zip(*graph)
77 85 graph = zip([convertedges(e) for e in edges], text)
78 86 return orig(ui, graph, *args, **kwargs)
79 87
88
80 89 def extsetup(ui):
81 90 if ui.plain('graph'):
82 91 return
@@ -86,8 +95,12 b' def extsetup(ui):'
86 95 return
87 96
88 97 if r'A' in encoding._wide:
89 ui.warn(_('beautifygraph: unsupported terminal settings, '
90 'monospace narrow text required\n'))
98 ui.warn(
99 _(
100 'beautifygraph: unsupported terminal settings, '
101 'monospace narrow text required\n'
102 )
103 )
91 104 return
92 105
93 106 extensions.wrapfunction(graphmod, 'outputgraph', outputprettygraph)
@@ -71,30 +71,33 b' command = registrar.command(cmdtable)'
71 71 configtable = {}
72 72 configitem = registrar.configitem(configtable)
73 73
74 configitem('blackbox', 'dirty',
75 default=False,
74 configitem(
75 'blackbox', 'dirty', default=False,
76 76 )
77 configitem('blackbox', 'maxsize',
78 default='1 MB',
77 configitem(
78 'blackbox', 'maxsize', default='1 MB',
79 )
80 configitem(
81 'blackbox', 'logsource', default=False,
79 82 )
80 configitem('blackbox', 'logsource',
81 default=False,
83 configitem(
84 'blackbox', 'maxfiles', default=7,
82 85 )
83 configitem('blackbox', 'maxfiles',
84 default=7,
86 configitem(
87 'blackbox', 'track', default=lambda: ['*'],
85 88 )
86 configitem('blackbox', 'track',
87 default=lambda: ['*'],
88 )
89 configitem('blackbox', 'ignore',
89 configitem(
90 'blackbox',
91 'ignore',
90 92 default=lambda: ['chgserver', 'cmdserver', 'extension'],
91 93 )
92 configitem('blackbox', 'date-format',
93 default='%Y/%m/%d %H:%M:%S',
94 configitem(
95 'blackbox', 'date-format', default='%Y/%m/%d %H:%M:%S',
94 96 )
95 97
96 98 _lastlogger = loggingutil.proxylogger()
97 99
100
98 101 class blackboxlogger(object):
99 102 def __init__(self, ui, repo):
100 103 self._repo = repo
@@ -105,9 +108,9 b' class blackboxlogger(object):'
105 108 self._inlog = False
106 109
107 110 def tracked(self, event):
108 return ((b'*' in self._trackedevents
109 and event not in self._ignoredevents)
110 or event in self._trackedevents)
111 return (
112 b'*' in self._trackedevents and event not in self._ignoredevents
113 ) or event in self._trackedevents
111 114
112 115 def log(self, ui, event, msg, opts):
113 116 # self._log() -> ctx.dirty() may create new subrepo instance, which
@@ -129,9 +132,10 b' class blackboxlogger(object):'
129 132 changed = ''
130 133 ctx = self._repo[None]
131 134 parents = ctx.parents()
132 rev = ('+'.join([hex(p.node()) for p in parents]))
133 if (ui.configbool('blackbox', 'dirty') and
134 ctx.dirty(missing=True, merge=False, branch=False)):
135 rev = '+'.join([hex(p.node()) for p in parents])
136 if ui.configbool('blackbox', 'dirty') and ctx.dirty(
137 missing=True, merge=False, branch=False
138 ):
135 139 changed = '+'
136 140 if ui.configbool('blackbox', 'logsource'):
137 141 src = ' [%s]' % event
@@ -141,20 +145,28 b' class blackboxlogger(object):'
141 145 fmt = '%s %s @%s%s (%s)%s> %s'
142 146 args = (date, user, rev, changed, pid, src, msg)
143 147 with loggingutil.openlogfile(
144 ui, self._repo.vfs, name='blackbox.log',
145 maxfiles=self._maxfiles, maxsize=self._maxsize) as fp:
148 ui,
149 self._repo.vfs,
150 name='blackbox.log',
151 maxfiles=self._maxfiles,
152 maxsize=self._maxsize,
153 ) as fp:
146 154 fp.write(fmt % args)
147 155 except (IOError, OSError) as err:
148 156 # deactivate this to avoid failed logging again
149 157 self._trackedevents.clear()
150 ui.debug('warning: cannot write to blackbox.log: %s\n' %
151 encoding.strtolocal(err.strerror))
158 ui.debug(
159 'warning: cannot write to blackbox.log: %s\n'
160 % encoding.strtolocal(err.strerror)
161 )
152 162 return
153 163 _lastlogger.logger = self
154 164
165
155 166 def uipopulate(ui):
156 167 ui.setlogger(b'blackbox', _lastlogger)
157 168
169
158 170 def reposetup(ui, repo):
159 171 # During 'hg pull' a httppeer repo is created to represent the remote repo.
160 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 187 repo._wlockfreeprefix.add('blackbox.log')
176 188
177 @command('blackbox',
178 [('l', 'limit', 10, _('the number of events to show')),
179 ],
189
190 @command(
191 'blackbox',
192 [('l', 'limit', 10, _('the number of events to show')),],
180 193 _('hg blackbox [OPTION]...'),
181 194 helpcategory=command.CATEGORY_MAINTENANCE,
182 helpbasic=True)
195 helpbasic=True,
196 )
183 197 def blackbox(ui, repo, *revs, **opts):
184 198 '''view the recent repository events
185 199 '''
@@ -36,20 +36,26 b" configitem(MY_NAME, 'enable-branches', F"
36 36 cmdtable = {}
37 37 command = registrar.command(cmdtable)
38 38
39
39 40 def commit_hook(ui, repo, **kwargs):
40 41 active = repo._bookmarks.active
41 42 if active:
42 43 if active in ui.configlist(MY_NAME, 'protect'):
43 44 raise error.Abort(
44 _('cannot commit, bookmark %s is protected') % active)
45 _('cannot commit, bookmark %s is protected') % active
46 )
45 47 if not cwd_at_bookmark(repo, active):
46 48 raise error.Abort(
47 _('cannot commit, working directory out of sync with active bookmark'),
48 hint=_("run 'hg up %s'") % active)
49 _(
50 'cannot commit, working directory out of sync with active bookmark'
51 ),
52 hint=_("run 'hg up %s'") % active,
53 )
49 54 elif ui.configbool(MY_NAME, 'require-bookmark', True):
50 55 raise error.Abort(_('cannot commit without an active bookmark'))
51 56 return 0
52 57
58
53 59 def bookmarks_update(orig, repo, parents, node):
54 60 if len(parents) == 2:
55 61 # called during commit
@@ -58,43 +64,59 b' def bookmarks_update(orig, repo, parents'
58 64 # called during update
59 65 return False
60 66
67
61 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 71 if not rev:
64 72 marks = repo._bookmarks
65 73 for name in names:
66 74 if name in marks:
67 raise error.Abort(_(
75 raise error.Abort(
76 _(
68 77 "bookmark %s already exists, to move use the --rev option"
69 ) % name)
78 )
79 % name
80 )
70 81 return orig(repo, tr, names, rev, force, inactive)
71 82
83
72 84 def commands_commit(orig, ui, repo, *args, **opts):
73 85 commit_hook(ui, repo)
74 86 return orig(ui, repo, *args, **opts)
75 87
88
76 89 def commands_pull(orig, ui, repo, *args, **opts):
77 90 rc = orig(ui, repo, *args, **opts)
78 91 active = repo._bookmarks.active
79 92 if active and not cwd_at_bookmark(repo, active):
80 ui.warn(_(
93 ui.warn(
94 _(
81 95 "working directory out of sync with active bookmark, run "
82 96 "'hg up %s'"
83 ) % active)
97 )
98 % active
99 )
84 100 return rc
85 101
102
86 103 def commands_branch(orig, ui, repo, label=None, **opts):
87 104 if label and not opts.get(r'clean') and not opts.get(r'rev'):
88 105 raise error.Abort(
89 _("creating named branches is disabled and you should use bookmarks"),
90 hint="see 'hg help bookflow'")
106 _(
107 "creating named branches is disabled and you should use bookmarks"
108 ),
109 hint="see 'hg help bookflow'",
110 )
91 111 return orig(ui, repo, label, **opts)
92 112
113
93 114 def cwd_at_bookmark(repo, mark):
94 115 mark_id = repo._bookmarks[mark]
95 116 cur_id = repo.lookup('.')
96 117 return cur_id == mark_id
97 118
119
98 120 def uisetup(ui):
99 121 extensions.wrapfunction(bookmarks, 'update', bookmarks_update)
100 122 extensions.wrapfunction(bookmarks, 'addbookmarks', bookmarks_addbookmarks)
@@ -324,71 +324,80 b" testedwith = 'ships-with-hg-core'"
324 324 configtable = {}
325 325 configitem = registrar.configitem(configtable)
326 326
327 configitem('bugzilla', 'apikey',
328 default='',
327 configitem(
328 'bugzilla', 'apikey', default='',
329 )
330 configitem(
331 'bugzilla', 'bzdir', default='/var/www/html/bugzilla',
329 332 )
330 configitem('bugzilla', 'bzdir',
331 default='/var/www/html/bugzilla',
333 configitem(
334 'bugzilla', 'bzemail', default=None,
332 335 )
333 configitem('bugzilla', 'bzemail',
334 default=None,
336 configitem(
337 'bugzilla', 'bzurl', default='http://localhost/bugzilla/',
335 338 )
336 configitem('bugzilla', 'bzurl',
337 default='http://localhost/bugzilla/',
339 configitem(
340 'bugzilla', 'bzuser', default=None,
338 341 )
339 configitem('bugzilla', 'bzuser',
340 default=None,
342 configitem(
343 'bugzilla', 'db', default='bugs',
341 344 )
342 configitem('bugzilla', 'db',
343 default='bugs',
344 )
345 configitem('bugzilla', 'fixregexp',
346 default=(br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
345 configitem(
346 'bugzilla',
347 'fixregexp',
348 default=(
349 br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
347 350 br'(?:nos?\.?|num(?:ber)?s?)?\s*'
348 351 br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
349 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
350 )
351 configitem('bugzilla', 'fixresolution',
352 default='FIXED',
352 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
353 ),
353 354 )
354 configitem('bugzilla', 'fixstatus',
355 default='RESOLVED',
355 configitem(
356 'bugzilla', 'fixresolution', default='FIXED',
356 357 )
357 configitem('bugzilla', 'host',
358 default='localhost',
358 configitem(
359 'bugzilla', 'fixstatus', default='RESOLVED',
359 360 )
360 configitem('bugzilla', 'notify',
361 default=configitem.dynamicdefault,
361 configitem(
362 'bugzilla', 'host', default='localhost',
362 363 )
363 configitem('bugzilla', 'password',
364 default=None,
364 configitem(
365 'bugzilla', 'notify', default=configitem.dynamicdefault,
365 366 )
366 configitem('bugzilla', 'regexp',
367 default=(br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
368 br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
369 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
367 configitem(
368 'bugzilla', 'password', default=None,
370 369 )
371 configitem('bugzilla', 'strip',
372 default=0,
370 configitem(
371 'bugzilla',
372 'regexp',
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 ),
373 378 )
374 configitem('bugzilla', 'style',
375 default=None,
379 configitem(
380 'bugzilla', 'strip', default=0,
376 381 )
377 configitem('bugzilla', 'template',
378 default=None,
382 configitem(
383 'bugzilla', 'style', default=None,
379 384 )
380 configitem('bugzilla', 'timeout',
381 default=5,
385 configitem(
386 'bugzilla', 'template', default=None,
382 387 )
383 configitem('bugzilla', 'user',
384 default='bugs',
388 configitem(
389 'bugzilla', 'timeout', default=5,
385 390 )
386 configitem('bugzilla', 'usermap',
387 default=None,
391 configitem(
392 'bugzilla', 'user', default='bugs',
388 393 )
389 configitem('bugzilla', 'version',
390 default=None,
394 configitem(
395 'bugzilla', 'usermap', default=None,
391 396 )
397 configitem(
398 'bugzilla', 'version', default=None,
399 )
400
392 401
393 402 class bzaccess(object):
394 403 '''Base class for access to Bugzilla.'''
@@ -434,6 +443,7 b' class bzaccess(object):'
434 443 emails automatically.
435 444 '''
436 445
446
437 447 # Bugzilla via direct access to MySQL database.
438 448 class bzmysql(bzaccess):
439 449 '''Support for direct MySQL access to Bugzilla.
@@ -454,6 +464,7 b' class bzmysql(bzaccess):'
454 464 def __init__(self, ui):
455 465 try:
456 466 import MySQLdb as mysql
467
457 468 bzmysql._MySQLdb = mysql
458 469 except ImportError as err:
459 470 raise error.Abort(_('python mysql support not available: %s') % err)
@@ -465,12 +476,13 b' class bzmysql(bzaccess):'
465 476 passwd = self.ui.config('bugzilla', 'password')
466 477 db = self.ui.config('bugzilla', 'db')
467 478 timeout = int(self.ui.config('bugzilla', 'timeout'))
468 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
469 (host, db, user, '*' * len(passwd)))
470 self.conn = bzmysql._MySQLdb.connect(host=host,
471 user=user, passwd=passwd,
472 db=db,
473 connect_timeout=timeout)
479 self.ui.note(
480 _('connecting to %s:%s as %s, password %s\n')
481 % (host, db, user, '*' * len(passwd))
482 )
483 self.conn = bzmysql._MySQLdb.connect(
484 host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout
485 )
474 486 self.cursor = self.conn.cursor()
475 487 self.longdesc_id = self.get_longdesc_id()
476 488 self.user_ids = {}
@@ -495,8 +507,10 b' class bzmysql(bzaccess):'
495 507
496 508 def filter_real_bug_ids(self, bugs):
497 509 '''filter not-existing bugs from set.'''
498 self.run('select bug_id from bugs where bug_id in %s' %
499 bzmysql.sql_buglist(bugs.keys()))
510 self.run(
511 'select bug_id from bugs where bug_id in %s'
512 % bzmysql.sql_buglist(bugs.keys())
513 )
500 514 existing = [id for (id,) in self.cursor.fetchall()]
501 515 for id in bugs.keys():
502 516 if id not in existing:
@@ -505,12 +519,16 b' class bzmysql(bzaccess):'
505 519
506 520 def filter_cset_known_bug_ids(self, node, bugs):
507 521 '''filter bug ids that already refer to this changeset from set.'''
508 self.run('''select bug_id from longdescs where
509 bug_id in %s and thetext like "%%%s%%"''' %
510 (bzmysql.sql_buglist(bugs.keys()), short(node)))
522 self.run(
523 '''select bug_id from longdescs where
524 bug_id in %s and thetext like "%%%s%%"'''
525 % (bzmysql.sql_buglist(bugs.keys()), short(node))
526 )
511 527 for (id,) in self.cursor.fetchall():
512 self.ui.status(_('bug %d already knows about changeset %s\n') %
513 (id, short(node)))
528 self.ui.status(
529 _('bug %d already knows about changeset %s\n')
530 % (id, short(node))
531 )
514 532 del bugs[id]
515 533
516 534 def notify(self, bugs, committer):
@@ -534,8 +552,9 b' class bzmysql(bzaccess):'
534 552 ret = fp.close()
535 553 if ret:
536 554 self.ui.warn(out)
537 raise error.Abort(_('bugzilla notify command %s') %
538 procutil.explainexit(ret))
555 raise error.Abort(
556 _('bugzilla notify command %s') % procutil.explainexit(ret)
557 )
539 558 self.ui.status(_('done\n'))
540 559
541 560 def get_user_id(self, user):
@@ -547,8 +566,11 b' class bzmysql(bzaccess):'
547 566 userid = int(user)
548 567 except ValueError:
549 568 self.ui.note(_('looking up user %s\n') % user)
550 self.run('''select userid from profiles
551 where login_name like %s''', user)
569 self.run(
570 '''select userid from profiles
571 where login_name like %s''',
572 user,
573 )
552 574 all = self.cursor.fetchall()
553 575 if len(all) != 1:
554 576 raise KeyError(user)
@@ -567,13 +589,16 b' class bzmysql(bzaccess):'
567 589 try:
568 590 defaultuser = self.ui.config('bugzilla', 'bzuser')
569 591 if not defaultuser:
570 raise error.Abort(_('cannot find bugzilla user id for %s') %
571 user)
592 raise error.Abort(
593 _('cannot find bugzilla user id for %s') % user
594 )
572 595 userid = self.get_user_id(defaultuser)
573 596 user = defaultuser
574 597 except KeyError:
575 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
576 % (user, defaultuser))
598 raise error.Abort(
599 _('cannot find bugzilla user id for %s or %s')
600 % (user, defaultuser)
601 )
577 602 return (user, userid)
578 603
579 604 def updatebug(self, bugid, newstate, text, committer):
@@ -586,22 +611,29 b' class bzmysql(bzaccess):'
586 611
587 612 (user, userid) = self.get_bugzilla_user(committer)
588 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 616 (bug_id, who, bug_when, thetext)
591 617 values (%s, %s, %s, %s)''',
592 (bugid, userid, now, text))
593 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
618 (bugid, userid, now, text),
619 )
620 self.run(
621 '''insert into bugs_activity (bug_id, who, bug_when, fieldid)
594 622 values (%s, %s, %s, %s)''',
595 (bugid, userid, now, self.longdesc_id))
623 (bugid, userid, now, self.longdesc_id),
624 )
596 625 self.conn.commit()
597 626
627
598 628 class bzmysql_2_18(bzmysql):
599 629 '''support for bugzilla 2.18 series.'''
600 630
601 631 def __init__(self, ui):
602 632 bzmysql.__init__(self, ui)
603 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 638 class bzmysql_3_0(bzmysql_2_18):
607 639 '''support for bugzilla 3.0 series.'''
@@ -617,8 +649,10 b' class bzmysql_3_0(bzmysql_2_18):'
617 649 raise error.Abort(_('unknown database schema'))
618 650 return ids[0][0]
619 651
652
620 653 # Bugzilla via XMLRPC interface.
621 654
655
622 656 class cookietransportrequest(object):
623 657 """A Transport request method that retains cookies over its lifetime.
624 658
@@ -636,6 +670,7 b' class cookietransportrequest(object):'
636 670 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
637 671
638 672 cookies = []
673
639 674 def send_cookies(self, connection):
640 675 if self.cookies:
641 676 for cookie in self.cookies:
@@ -673,8 +708,12 b' class cookietransportrequest(object):'
673 708 self.cookies.append(cookie)
674 709
675 710 if response.status != 200:
676 raise xmlrpclib.ProtocolError(host + handler, response.status,
677 response.reason, response.msg.headers)
711 raise xmlrpclib.ProtocolError(
712 host + handler,
713 response.status,
714 response.reason,
715 response.msg.headers,
716 )
678 717
679 718 payload = response.read()
680 719 parser, unmarshaller = self.getparser()
@@ -683,6 +722,7 b' class cookietransportrequest(object):'
683 722
684 723 return unmarshaller.close()
685 724
725
686 726 # The explicit calls to the underlying xmlrpclib __init__() methods are
687 727 # necessary. The xmlrpclib.Transport classes are old-style classes, and
688 728 # it turns out their __init__() doesn't get called when doing multiple
@@ -692,11 +732,13 b' class cookietransport(cookietransportreq'
692 732 if util.safehasattr(xmlrpclib.Transport, "__init__"):
693 733 xmlrpclib.Transport.__init__(self, use_datetime)
694 734
735
695 736 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
696 737 def __init__(self, use_datetime=0):
697 738 if util.safehasattr(xmlrpclib.Transport, "__init__"):
698 739 xmlrpclib.SafeTransport.__init__(self, use_datetime)
699 740
741
700 742 class bzxmlrpc(bzaccess):
701 743 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
702 744
@@ -719,8 +761,9 b' class bzxmlrpc(bzaccess):'
719 761 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
720 762 self.bzvermajor = int(ver[0])
721 763 self.bzverminor = int(ver[1])
722 login = self.bzproxy.User.login({'login': user, 'password': passwd,
723 'restrict_login': True})
764 login = self.bzproxy.User.login(
765 {'login': user, 'password': passwd, 'restrict_login': True}
766 )
724 767 self.bztoken = login.get('token', '')
725 768
726 769 def transport(self, uri):
@@ -731,17 +774,20 b' class bzxmlrpc(bzaccess):'
731 774
732 775 def get_bug_comments(self, id):
733 776 """Return a string with all comment text for a bug."""
734 c = self.bzproxy.Bug.comments({'ids': [id],
735 'include_fields': ['text'],
736 'token': self.bztoken})
777 c = self.bzproxy.Bug.comments(
778 {'ids': [id], 'include_fields': ['text'], 'token': self.bztoken}
779 )
737 780 return ''.join([t['text'] for t in c['bugs']['%d' % id]['comments']])
738 781
739 782 def filter_real_bug_ids(self, bugs):
740 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
783 probe = self.bzproxy.Bug.get(
784 {
785 'ids': sorted(bugs.keys()),
741 786 'include_fields': [],
742 787 'permissive': True,
743 788 'token': self.bztoken,
744 })
789 }
790 )
745 791 for badbug in probe['faults']:
746 792 id = badbug['id']
747 793 self.ui.status(_('bug %d does not exist\n') % id)
@@ -750,8 +796,10 b' class bzxmlrpc(bzaccess):'
750 796 def filter_cset_known_bug_ids(self, node, bugs):
751 797 for id in sorted(bugs.keys()):
752 798 if self.get_bug_comments(id).find(short(node)) != -1:
753 self.ui.status(_('bug %d already knows about changeset %s\n') %
754 (id, short(node)))
799 self.ui.status(
800 _('bug %d already knows about changeset %s\n')
801 % (id, short(node))
802 )
755 803 del bugs[id]
756 804
757 805 def updatebug(self, bugid, newstate, text, committer):
@@ -769,12 +817,17 b' class bzxmlrpc(bzaccess):'
769 817 self.bzproxy.Bug.update(args)
770 818 else:
771 819 if 'fix' in newstate:
772 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
773 "to mark bugs fixed\n"))
820 self.ui.warn(
821 _(
822 "Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
823 "to mark bugs fixed\n"
824 )
825 )
774 826 args['id'] = bugid
775 827 args['comment'] = text
776 828 self.bzproxy.Bug.add_comment(args)
777 829
830
778 831 class bzxmlrpcemail(bzxmlrpc):
779 832 """Read data from Bugzilla via XMLRPC, send updates via email.
780 833
@@ -823,15 +876,18 b' class bzxmlrpcemail(bzxmlrpc):'
823 876 than the subject line, and leave a blank line after it.
824 877 '''
825 878 user = self.map_committer(committer)
826 matches = self.bzproxy.User.get({'match': [user],
827 'token': self.bztoken})
879 matches = self.bzproxy.User.get(
880 {'match': [user], 'token': self.bztoken}
881 )
828 882 if not matches['users']:
829 883 user = self.ui.config('bugzilla', 'user')
830 matches = self.bzproxy.User.get({'match': [user],
831 'token': self.bztoken})
884 matches = self.bzproxy.User.get(
885 {'match': [user], 'token': self.bztoken}
886 )
832 887 if not matches['users']:
833 raise error.Abort(_("default bugzilla user %s email not found")
834 % user)
888 raise error.Abort(
889 _("default bugzilla user %s email not found") % user
890 )
835 891 user = matches['users'][0]['email']
836 892 commands.append(self.makecommandline("id", bugid))
837 893
@@ -856,13 +912,16 b' class bzxmlrpcemail(bzxmlrpc):'
856 912 cmds.append(self.makecommandline("resolution", self.fixresolution))
857 913 self.send_bug_modify_email(bugid, cmds, text, committer)
858 914
915
859 916 class NotFound(LookupError):
860 917 pass
861 918
919
862 920 class bzrestapi(bzaccess):
863 921 """Read and write bugzilla data using the REST API available since
864 922 Bugzilla 5.0.
865 923 """
924
866 925 def __init__(self, ui):
867 926 bzaccess.__init__(self, ui)
868 927 bz = self.ui.config('bugzilla', 'bzurl')
@@ -902,14 +961,15 b' class bzrestapi(bzaccess):'
902 961 def _submit(self, burl, data, method='POST'):
903 962 data = json.dumps(data)
904 963 if method == 'PUT':
964
905 965 class putrequest(util.urlreq.request):
906 966 def get_method(self):
907 967 return 'PUT'
968
908 969 request_type = putrequest
909 970 else:
910 971 request_type = util.urlreq.request
911 req = request_type(burl, data,
912 {'Content-Type': 'application/json'})
972 req = request_type(burl, data, {'Content-Type': 'application/json'})
913 973 try:
914 974 resp = url.opener(self.ui).open(req)
915 975 return json.loads(resp.read())
@@ -941,8 +1001,9 b' class bzrestapi(bzaccess):'
941 1001 result = self._fetch(burl)
942 1002 comments = result['bugs'][pycompat.bytestr(bugid)]['comments']
943 1003 if any(sn in c['text'] for c in comments):
944 self.ui.status(_('bug %d already knows about changeset %s\n') %
945 (bugid, sn))
1004 self.ui.status(
1005 _('bug %d already knows about changeset %s\n') % (bugid, sn)
1006 )
946 1007 del bugs[bugid]
947 1008
948 1009 def updatebug(self, bugid, newstate, text, committer):
@@ -969,11 +1030,10 b' class bzrestapi(bzaccess):'
969 1030 self.ui.debug('updated bug %s\n' % bugid)
970 1031 else:
971 1032 burl = self.apiurl(('bug', bugid, 'comment'))
972 self._submit(burl, {
973 'comment': text,
974 'is_private': False,
975 'is_markdown': False,
976 })
1033 self._submit(
1034 burl,
1035 {'comment': text, 'is_private': False, 'is_markdown': False,},
1036 )
977 1037 self.ui.debug('added comment to bug %s\n' % bugid)
978 1038
979 1039 def notify(self, bugs, committer):
@@ -984,6 +1044,7 b' class bzrestapi(bzaccess):'
984 1044 '''
985 1045 pass
986 1046
1047
987 1048 class bugzilla(object):
988 1049 # supported versions of bugzilla. different versions have
989 1050 # different schemas.
@@ -1004,14 +1065,17 b' class bugzilla(object):'
1004 1065 try:
1005 1066 bzclass = bugzilla._versions[bzversion]
1006 1067 except KeyError:
1007 raise error.Abort(_('bugzilla version %s not supported') %
1008 bzversion)
1068 raise error.Abort(
1069 _('bugzilla version %s not supported') % bzversion
1070 )
1009 1071 self.bzdriver = bzclass(self.ui)
1010 1072
1011 1073 self.bug_re = re.compile(
1012 self.ui.config('bugzilla', 'regexp'), re.IGNORECASE)
1074 self.ui.config('bugzilla', 'regexp'), re.IGNORECASE
1075 )
1013 1076 self.fix_re = re.compile(
1014 self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE)
1077 self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE
1078 )
1015 1079 self.split_re = re.compile(br'\D+')
1016 1080
1017 1081 def find_bugs(self, ctx):
@@ -1093,31 +1157,39 b' class bugzilla(object):'
1093 1157 if not tmpl:
1094 1158 mapfile = self.ui.config('bugzilla', 'style')
1095 1159 if not mapfile and not tmpl:
1096 tmpl = _('changeset {node|short} in repo {root} refers '
1097 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
1160 tmpl = _(
1161 'changeset {node|short} in repo {root} refers '
1162 'to bug {bug}.\ndetails:\n\t{desc|tabindent}'
1163 )
1098 1164 spec = logcmdutil.templatespec(tmpl, mapfile)
1099 1165 t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
1100 1166 self.ui.pushbuffer()
1101 t.show(ctx, changes=ctx.changeset(),
1167 t.show(
1168 ctx,
1169 changes=ctx.changeset(),
1102 1170 bug=pycompat.bytestr(bugid),
1103 1171 hgweb=self.ui.config('web', 'baseurl'),
1104 1172 root=self.repo.root,
1105 webroot=webroot(self.repo.root))
1173 webroot=webroot(self.repo.root),
1174 )
1106 1175 data = self.ui.popbuffer()
1107 self.bzdriver.updatebug(bugid, newstate, data,
1108 stringutil.email(ctx.user()))
1176 self.bzdriver.updatebug(
1177 bugid, newstate, data, stringutil.email(ctx.user())
1178 )
1109 1179
1110 1180 def notify(self, bugs, committer):
1111 1181 '''ensure Bugzilla users are notified of bug change.'''
1112 1182 self.bzdriver.notify(bugs, committer)
1113 1183
1184
1114 1185 def hook(ui, repo, hooktype, node=None, **kwargs):
1115 1186 '''add comment to bugzilla for each changeset that refers to a
1116 1187 bugzilla bug id. only add a comment once per bug, so same change
1117 1188 seen multiple times does not fill bug with duplicate data.'''
1118 1189 if node is None:
1119 raise error.Abort(_('hook type %s does not pass a changeset id') %
1120 hooktype)
1190 raise error.Abort(
1191 _('hook type %s does not pass a changeset id') % hooktype
1192 )
1121 1193 try:
1122 1194 bz = bugzilla(ui, repo)
1123 1195 ctx = repo[node]
@@ -44,15 +44,21 b' command = registrar.command(cmdtable)'
44 44 # leave the attribute unspecified.
45 45 testedwith = 'ships-with-hg-core'
46 46
47 @command('censor',
48 [('r', 'rev', '', _('censor file from specified revision'), _('REV')),
49 ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT'))],
47
48 @command(
49 'censor',
50 [
51 ('r', 'rev', '', _('censor file from specified revision'), _('REV')),
52 ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT')),
53 ],
50 54 _('-r REV [-t TEXT] [FILE]'),
51 helpcategory=command.CATEGORY_MAINTENANCE)
55 helpcategory=command.CATEGORY_MAINTENANCE,
56 )
52 57 def censor(ui, repo, path, rev='', tombstone='', **opts):
53 58 with repo.wlock(), repo.lock():
54 59 return _docensor(ui, repo, path, rev, tombstone, **opts)
55 60
61
56 62 def _docensor(ui, repo, path, rev='', tombstone='', **opts):
57 63 if not path:
58 64 raise error.Abort(_('must specify file path to censor'))
@@ -88,13 +94,17 b" def _docensor(ui, repo, path, rev='', to"
88 94 heads.append(hc)
89 95 if heads:
90 96 headlist = ', '.join([short(c.node()) for c in heads])
91 raise error.Abort(_('cannot censor file in heads (%s)') % headlist,
92 hint=_('clean/delete and commit first'))
97 raise error.Abort(
98 _('cannot censor file in heads (%s)') % headlist,
99 hint=_('clean/delete and commit first'),
100 )
93 101
94 102 wp = wctx.parents()
95 103 if ctx.node() in [p.node() for p in wp]:
96 raise error.Abort(_('cannot censor working directory'),
97 hint=_('clean/delete/update first'))
104 raise error.Abort(
105 _('cannot censor working directory'),
106 hint=_('clean/delete/update first'),
107 )
98 108
99 109 with repo.transaction(b'censor') as tr:
100 110 flog.censorrevision(tr, fnode, tombstone=tombstone)
@@ -35,13 +35,15 b' command = registrar.command(cmdtable)'
35 35 # leave the attribute unspecified.
36 36 testedwith = 'ships-with-hg-core'
37 37
38 @command('children',
39 [('r', 'rev', '.',
40 _('show children of the specified revision'), _('REV')),
41 ] + templateopts,
38
39 @command(
40 'children',
41 [('r', 'rev', '.', _('show children of the specified revision'), _('REV')),]
42 + templateopts,
42 43 _('hg children [-r REV] [FILE]'),
43 44 helpcategory=command.CATEGORY_CHANGE_NAVIGATION,
44 inferrepo=True)
45 inferrepo=True,
46 )
45 47 def children(ui, repo, file_=None, **opts):
46 48 """show the children of the given or working directory revision
47 49
@@ -34,6 +34,7 b' command = registrar.command(cmdtable)'
34 34 # leave the attribute unspecified.
35 35 testedwith = 'ships-with-hg-core'
36 36
37
37 38 def changedlines(ui, repo, ctx1, ctx2, fns):
38 39 added, removed = 0, 0
39 40 fmatch = scmutil.matchfiles(repo, fns)
@@ -45,31 +46,38 b' def changedlines(ui, repo, ctx1, ctx2, f'
45 46 removed += 1
46 47 return (added, removed)
47 48
49
48 50 def countrate(ui, repo, amap, *pats, **opts):
49 51 """Calculate stats"""
50 52 opts = pycompat.byteskwargs(opts)
51 53 if opts.get('dateformat'):
54
52 55 def getkey(ctx):
53 56 t, tz = ctx.date()
54 57 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
55 58 return encoding.strtolocal(
56 date.strftime(encoding.strfromlocal(opts['dateformat'])))
59 date.strftime(encoding.strfromlocal(opts['dateformat']))
60 )
61
57 62 else:
58 63 tmpl = opts.get('oldtemplate') or opts.get('template')
59 64 tmpl = logcmdutil.maketemplater(ui, repo, tmpl)
65
60 66 def getkey(ctx):
61 67 ui.pushbuffer()
62 68 tmpl.show(ctx)
63 69 return ui.popbuffer()
64 70
65 progress = ui.makeprogress(_('analyzing'), unit=_('revisions'),
66 total=len(repo))
71 progress = ui.makeprogress(
72 _('analyzing'), unit=_('revisions'), total=len(repo)
73 )
67 74 rate = {}
68 75 df = False
69 76 if opts.get('date'):
70 77 df = dateutil.matchdate(opts['date'])
71 78
72 79 m = scmutil.match(repo[None], pats, opts)
80
73 81 def prep(ctx, fns):
74 82 rev = ctx.rev()
75 83 if df and not df(ctx.date()[0]): # doesn't match date format
@@ -99,25 +107,54 b' def countrate(ui, repo, amap, *pats, **o'
99 107 return rate
100 108
101 109
102 @command('churn',
103 [('r', 'rev', [],
104 _('count rate for the specified revision or revset'), _('REV')),
105 ('d', 'date', '',
106 _('count rate for revisions matching date spec'), _('DATE')),
107 ('t', 'oldtemplate', '',
108 _('template to group changesets (DEPRECATED)'), _('TEMPLATE')),
109 ('T', 'template', '{author|email}',
110 _('template to group changesets'), _('TEMPLATE')),
111 ('f', 'dateformat', '',
112 _('strftime-compatible format for grouping by date'), _('FORMAT')),
110 @command(
111 'churn',
112 [
113 (
114 'r',
115 'rev',
116 [],
117 _('count rate for the specified revision or revset'),
118 _('REV'),
119 ),
120 (
121 'd',
122 'date',
123 '',
124 _('count rate for revisions matching date spec'),
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 ),
113 148 ('c', 'changesets', False, _('count rate by number of changesets')),
114 149 ('s', 'sort', False, _('sort by key (default: sort by count)')),
115 150 ('', 'diffstat', False, _('display added/removed lines separately')),
116 151 ('', 'aliases', '', _('file with email aliases'), _('FILE')),
117 ] + cmdutil.walkopts,
152 ]
153 + cmdutil.walkopts,
118 154 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"),
119 155 helpcategory=command.CATEGORY_MAINTENANCE,
120 inferrepo=True)
156 inferrepo=True,
157 )
121 158 def churn(ui, repo, *pats, **opts):
122 159 '''histogram of changes to the repository
123 160
@@ -154,6 +191,7 b' def churn(ui, repo, *pats, **opts):'
154 191 a .hgchurn file will be looked for in the working directory root.
155 192 Aliases will be split from the rightmost "=".
156 193 '''
194
157 195 def pad(s, l):
158 196 return s + " " * (l - encoding.colwidth(s))
159 197
@@ -191,19 +229,25 b' def churn(ui, repo, *pats, **opts):'
191 229
192 230 if opts.get(r'diffstat'):
193 231 width -= 15
232
194 233 def format(name, diffstat):
195 234 added, removed = diffstat
196 return "%s %15s %s%s\n" % (pad(name, maxname),
235 return "%s %15s %s%s\n" % (
236 pad(name, maxname),
197 237 '+%d/-%d' % (added, removed),
198 ui.label('+' * charnum(added),
199 'diffstat.inserted'),
200 ui.label('-' * charnum(removed),
201 'diffstat.deleted'))
238 ui.label('+' * charnum(added), 'diffstat.inserted'),
239 ui.label('-' * charnum(removed), 'diffstat.deleted'),
240 )
241
202 242 else:
203 243 width -= 6
244
204 245 def format(name, count):
205 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
206 '*' * charnum(sum(count)))
246 return "%s %6d %s\n" % (
247 pad(name, maxname),
248 sum(count),
249 '*' * charnum(sum(count)),
250 )
207 251
208 252 def charnum(count):
209 253 return int(count * width // maxcount)
@@ -203,6 +203,7 b' from mercurial import ('
203 203
204 204 testedwith = 'ships-with-hg-core'
205 205
206
206 207 def capabilities(orig, repo, proto):
207 208 caps = orig(repo, proto)
208 209
@@ -214,5 +215,6 b' def capabilities(orig, repo, proto):'
214 215
215 216 return caps
216 217
218
217 219 def extsetup(ui):
218 220 extensions.wrapfunction(wireprotov1server, '_capabilities', capabilities)
@@ -28,13 +28,16 b" testedwith = 'ships-with-hg-core'"
28 28
29 29 commitopts = cmdutil.commitopts
30 30 commitopts2 = cmdutil.commitopts2
31 commitopts3 = [('r', 'rev', [],
32 _('revision to check'), _('REV'))]
31 commitopts3 = [('r', 'rev', [], _('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 37 _('[OPTION]... [REV]...'),
36 38 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
37 inferrepo=True)
39 inferrepo=True,
40 )
38 41 def close_branch(ui, repo, *revs, **opts):
39 42 """close the given head revisions
40 43
@@ -44,10 +47,18 b' def close_branch(ui, repo, *revs, **opts'
44 47
45 48 The commit message must be specified with -l or -m.
46 49 """
50
47 51 def docommit(rev):
48 cctx = context.memctx(repo, parents=[rev, None], text=message,
49 files=[], filectxfn=None, user=opts.get('user'),
50 date=opts.get('date'), extra=extra)
52 cctx = context.memctx(
53 repo,
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 62 tr = repo.transaction('commit')
52 63 ret = repo.commitctx(cctx, True)
53 64 bookmarks.update(repo, [rev, None], ret)
@@ -37,36 +37,46 b' usedinternally = {'
37 37 'transplant_source',
38 38 }
39 39
40
40 41 def extsetup(ui):
41 42 entry = extensions.wrapcommand(commands.table, 'commit', _commit)
42 43 options = entry[1]
43 options.append(('', 'extra', [],
44 _('set a changeset\'s extra values'), _("KEY=VALUE")))
44 options.append(
45 ('', 'extra', [], _('set a changeset\'s extra values'), _("KEY=VALUE"))
46 )
47
45 48
46 49 def _commit(orig, ui, repo, *pats, **opts):
47 50 if util.safehasattr(repo, 'unfiltered'):
48 51 repo = repo.unfiltered()
52
49 53 class repoextra(repo.__class__):
50 54 def commit(self, *innerpats, **inneropts):
51 55 extras = opts.get(r'extra')
52 56 for raw in extras:
53 57 if '=' not in raw:
54 msg = _("unable to parse '%s', should follow "
55 "KEY=VALUE format")
58 msg = _(
59 "unable to parse '%s', should follow "
60 "KEY=VALUE format"
61 )
56 62 raise error.Abort(msg % raw)
57 63 k, v = raw.split('=', 1)
58 64 if not k:
59 65 msg = _("unable to parse '%s', keys can't be empty")
60 66 raise error.Abort(msg % raw)
61 67 if re.search(br'[^\w-]', k):
62 msg = _("keys can only contain ascii letters, digits,"
63 " '_' and '-'")
68 msg = _(
69 "keys can only contain ascii letters, digits,"
70 " '_' and '-'"
71 )
64 72 raise error.Abort(msg)
65 73 if k in usedinternally:
66 msg = _("key '%s' is used internally, can't be set "
67 "manually")
74 msg = _(
75 "key '%s' is used internally, can't be set " "manually"
76 )
68 77 raise error.Abort(msg % k)
69 78 inneropts[r'extra'][k] = v
70 79 return super(repoextra, self).commit(*innerpats, **inneropts)
80
71 81 repo.__class__ = repoextra
72 82 return orig(ui, repo, *pats, **opts)
@@ -10,9 +10,7 b''
10 10 from __future__ import absolute_import
11 11
12 12 from mercurial.i18n import _
13 from mercurial import (
14 registrar,
15 )
13 from mercurial import registrar
16 14
17 15 from . import (
18 16 convcmd,
@@ -30,28 +28,58 b" testedwith = 'ships-with-hg-core'"
30 28
31 29 # Commands definition was moved elsewhere to ease demandload job.
32 30
33 @command('convert',
34 [('', 'authors', '',
35 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
36 _('FILE')),
31
32 @command(
33 'convert',
34 [
35 (
36 '',
37 'authors',
38 '',
39 _(
40 'username mapping filename (DEPRECATED) (use --authormap instead)'
41 ),
42 _('FILE'),
43 ),
37 44 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
38 45 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
39 46 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
40 47 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
41 ('', 'filemap', '', _('remap file names using contents of file'),
42 _('FILE')),
43 ('', 'full', None,
44 _('apply filemap changes by converting all files again')),
45 ('', 'splicemap', '', _('splice synthesized history into place'),
46 _('FILE')),
47 ('', 'branchmap', '', _('change branch names while converting'),
48 _('FILE')),
48 (
49 '',
50 'filemap',
51 '',
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 ),
49 75 ('', 'branchsort', None, _('try to sort changesets by branches')),
50 76 ('', 'datesort', None, _('try to sort changesets by date')),
51 77 ('', 'sourcesort', None, _('preserve source changesets order')),
52 ('', 'closesort', None, _('try to reorder closed revisions'))],
78 ('', 'closesort', None, _('try to reorder closed revisions')),
79 ],
53 80 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
54 norepo=True)
81 norepo=True,
82 )
55 83 def convert(ui, src, dest=None, revmapfile=None, **opts):
56 84 """convert a foreign SCM repository to a Mercurial one.
57 85
@@ -454,17 +482,24 b' def convert(ui, src, dest=None, revmapfi'
454 482 """
455 483 return convcmd.convert(ui, src, dest, revmapfile, **opts)
456 484
485
457 486 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
458 487 def debugsvnlog(ui, **opts):
459 488 return subversion.debugsvnlog(ui, **opts)
460 489
461 @command('debugcvsps',
490
491 @command(
492 'debugcvsps',
462 493 [
463 494 # Main options shared with cvsps-2.1
464 495 ('b', 'branches', [], _('only return changes on specified branches')),
465 496 ('p', 'prefix', '', _('prefix to remove from file names')),
466 ('r', 'revisions', [],
467 _('only return changes after or between specified tags')),
497 (
498 'r',
499 'revisions',
500 [],
501 _('only return changes after or between specified tags'),
502 ),
468 503 ('u', 'update-cache', None, _("update cvs log cache")),
469 504 ('x', 'new-cache', None, _("create new cvs log cache")),
470 505 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
@@ -476,7 +511,8 b' def debugsvnlog(ui, **opts):'
476 511 ('A', 'cvs-direct', None, _('ignored for compatibility')),
477 512 ],
478 513 _('hg debugcvsps [OPTION]... [PATH]...'),
479 norepo=True)
514 norepo=True,
515 )
480 516 def debugcvsps(ui, *args, **opts):
481 517 '''create changeset information from CVS
482 518
@@ -490,34 +526,40 b' def debugcvsps(ui, *args, **opts):'
490 526 dates.'''
491 527 return cvsps.debugcvsps(ui, *args, **opts)
492 528
529
493 530 def kwconverted(context, mapping, name):
494 531 ctx = context.resource(mapping, 'ctx')
495 532 rev = ctx.extra().get('convert_revision', '')
496 533 if rev.startswith('svn:'):
497 534 if name == 'svnrev':
498 return (b"%d" % subversion.revsplit(rev)[2])
535 return b"%d" % subversion.revsplit(rev)[2]
499 536 elif name == 'svnpath':
500 537 return subversion.revsplit(rev)[1]
501 538 elif name == 'svnuuid':
502 539 return subversion.revsplit(rev)[0]
503 540 return rev
504 541
542
505 543 templatekeyword = registrar.templatekeyword()
506 544
545
507 546 @templatekeyword('svnrev', requires={'ctx'})
508 547 def kwsvnrev(context, mapping):
509 548 """String. Converted subversion revision number."""
510 549 return kwconverted(context, mapping, 'svnrev')
511 550
551
512 552 @templatekeyword('svnpath', requires={'ctx'})
513 553 def kwsvnpath(context, mapping):
514 554 """String. Converted subversion revision project path."""
515 555 return kwconverted(context, mapping, 'svnpath')
516 556
557
517 558 @templatekeyword('svnuuid', requires={'ctx'})
518 559 def kwsvnuuid(context, mapping):
519 560 """String. Converted subversion revision repository identifier."""
520 561 return kwconverted(context, mapping, 'svnuuid')
521 562
563
522 564 # tell hggettext to extract docstrings from these functions:
523 565 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -12,18 +12,13 b' from __future__ import absolute_import'
12 12 import os
13 13
14 14 from mercurial.i18n import _
15 from mercurial import (
16 demandimport,
17 error
18 )
15 from mercurial import demandimport, error
19 16 from . import common
20 17
21 18 # these do not work with demandimport, blacklist
22 demandimport.IGNORES.update([
23 'bzrlib.transactions',
24 'bzrlib.urlutils',
25 'ElementPath',
26 ])
19 demandimport.IGNORES.update(
20 ['bzrlib.transactions', 'bzrlib.urlutils', 'ElementPath',]
21 )
27 22
28 23 try:
29 24 # bazaar imports
@@ -31,6 +26,7 b' try:'
31 26 import bzrlib.errors
32 27 import bzrlib.revision
33 28 import bzrlib.revisionspec
29
34 30 bzrdir = bzrlib.bzrdir
35 31 errors = bzrlib.errors
36 32 revision = bzrlib.revision
@@ -41,6 +37,7 b' except ImportError:'
41 37
42 38 supportedkinds = ('file', 'symlink')
43 39
40
44 41 class bzr_source(common.converter_source):
45 42 """Reads Bazaar repositories by using the Bazaar Python libraries"""
46 43
@@ -48,8 +45,9 b' class bzr_source(common.converter_source'
48 45 super(bzr_source, self).__init__(ui, repotype, path, revs=revs)
49 46
50 47 if not os.path.exists(os.path.join(path, '.bzr')):
51 raise common.NoRepo(_('%s does not look like a Bazaar repository')
52 % path)
48 raise common.NoRepo(
49 _('%s does not look like a Bazaar repository') % path
50 )
53 51
54 52 try:
55 53 # access bzrlib stuff
@@ -62,8 +60,9 b' class bzr_source(common.converter_source'
62 60 try:
63 61 self.sourcerepo = bzrdir.BzrDir.open(path).open_repository()
64 62 except errors.NoRepositoryPresent:
65 raise common.NoRepo(_('%s does not look like a Bazaar repository')
66 % path)
63 raise common.NoRepo(
64 _('%s does not look like a Bazaar repository') % path
65 )
67 66 self._parentids = {}
68 67 self._saverev = ui.configbool('convert', 'bzr.saverev')
69 68
@@ -78,11 +77,18 b' class bzr_source(common.converter_source'
78 77 except (errors.NoWorkingTree, errors.NotLocalUrl):
79 78 tree = None
80 79 branch = dir.open_branch()
81 if (tree is not None and tree.bzrdir.root_transport.base !=
82 branch.bzrdir.root_transport.base):
83 self.ui.warn(_('warning: lightweight checkouts may cause '
80 if (
81 tree is not None
82 and tree.bzrdir.root_transport.base
83 != branch.bzrdir.root_transport.base
84 ):
85 self.ui.warn(
86 _(
87 'warning: lightweight checkouts may cause '
84 88 'conversion failures, try with a regular '
85 'branch instead.\n'))
89 'branch instead.\n'
90 )
91 )
86 92 except Exception:
87 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 125 pass
120 126 revid = info.rev_id
121 127 if revid is None:
122 raise error.Abort(_('%s is not a valid revision')
123 % self.revs[0])
128 raise error.Abort(
129 _('%s is not a valid revision') % self.revs[0]
130 )
124 131 heads = [revid]
125 132 # Empty repositories return 'null:', which cannot be retrieved
126 133 heads = [h for h in heads if h != 'null:']
@@ -139,8 +146,9 b' class bzr_source(common.converter_source'
139 146 if kind == 'symlink':
140 147 target = revtree.get_symlink_target(fileid)
141 148 if target is None:
142 raise error.Abort(_('%s.%s symlink has no target')
143 % (name, rev))
149 raise error.Abort(
150 _('%s.%s symlink has no target') % (name, rev)
151 )
144 152 return target, mode
145 153 else:
146 154 sio = revtree.get_file(fileid)
@@ -171,13 +179,15 b' class bzr_source(common.converter_source'
171 179 branch = self.recode(rev.properties.get('branch-nick', u'default'))
172 180 if branch == 'trunk':
173 181 branch = 'default'
174 return common.commit(parents=parents,
182 return common.commit(
183 parents=parents,
175 184 date='%d %d' % (rev.timestamp, -rev.timezone),
176 185 author=self.recode(rev.committer),
177 186 desc=self.recode(rev.message),
178 187 branch=branch,
179 188 rev=version,
180 saverev=self._saverev)
189 saverev=self._saverev,
190 )
181 191
182 192 def gettags(self):
183 193 bytetags = {}
@@ -216,11 +226,21 b' class bzr_source(common.converter_source'
216 226
217 227 # Process the entries by reverse lexicographic name order to
218 228 # handle nested renames correctly, most specific first.
219 curchanges = sorted(current.iter_changes(origin),
229 curchanges = sorted(
230 current.iter_changes(origin),
220 231 key=lambda c: c[1][0] or c[1][1],
221 reverse=True)
222 for (fileid, paths, changed_content, versioned, parent, name,
223 kind, executable) in curchanges:
232 reverse=True,
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 245 if paths[0] == u'' or paths[1] == u'':
226 246 # ignore changes to tree root
@@ -260,9 +280,11 b' class bzr_source(common.converter_source'
260 280 changes.append((frompath, revid))
261 281 changes.append((topath, revid))
262 282 # add to mode cache
263 mode = ((entry.executable and 'x')
283 mode = (
284 (entry.executable and 'x')
264 285 or (entry.kind == 'symlink' and 's')
265 or '')
286 or ''
287 )
266 288 self._modecache[(topath, revid)] = mode
267 289 # register the change as move
268 290 renames[topath] = frompath
@@ -290,8 +312,7 b' class bzr_source(common.converter_source'
290 312
291 313 # populate the mode cache
292 314 kind, executable = [e[1] for e in (kind, executable)]
293 mode = ((executable and 'x') or (kind == 'symlink' and 'l')
294 or '')
315 mode = (executable and 'x') or (kind == 'symlink' and 'l') or ''
295 316 self._modecache[(topath, revid)] = mode
296 317 changes.append((topath, revid))
297 318
@@ -22,20 +22,19 b' from mercurial import ('
22 22 pycompat,
23 23 util,
24 24 )
25 from mercurial.utils import (
26 procutil,
27 )
25 from mercurial.utils import procutil
28 26
29 27 pickle = util.pickle
30 28 propertycache = util.propertycache
31 29
30
32 31 def _encodeornone(d):
33 32 if d is None:
34 33 return
35 34 return d.encode('latin1')
36 35
36
37 37 class _shlexpy3proxy(object):
38
39 38 def __init__(self, l):
40 39 self._l = l
41 40
@@ -53,6 +52,7 b' class _shlexpy3proxy(object):'
53 52 def lineno(self):
54 53 return self._l.lineno
55 54
55
56 56 def shlexer(data=None, filepath=None, wordchars=None, whitespace=None):
57 57 if data is None:
58 58 if pycompat.ispy3:
@@ -62,7 +62,8 b' def shlexer(data=None, filepath=None, wo'
62 62 else:
63 63 if filepath is not None:
64 64 raise error.ProgrammingError(
65 'shlexer only accepts data or filepath, not both')
65 'shlexer only accepts data or filepath, not both'
66 )
66 67 if pycompat.ispy3:
67 68 data = data.decode('latin1')
68 69 l = shlex.shlex(data, infile=filepath, posix=True)
@@ -81,6 +82,7 b' def shlexer(data=None, filepath=None, wo'
81 82 return _shlexpy3proxy(l)
82 83 return l
83 84
85
84 86 def encodeargs(args):
85 87 def encodearg(s):
86 88 lines = base64.encodestring(s)
@@ -90,13 +92,16 b' def encodeargs(args):'
90 92 s = pickle.dumps(args)
91 93 return encodearg(s)
92 94
95
93 96 def decodeargs(s):
94 97 s = base64.decodestring(s)
95 98 return pickle.loads(s)
96 99
100
97 101 class MissingTool(Exception):
98 102 pass
99 103
104
100 105 def checktool(exe, name=None, abort=True):
101 106 name = name or exe
102 107 if not procutil.findexe(exe):
@@ -106,15 +111,30 b' def checktool(exe, name=None, abort=True'
106 111 exc = MissingTool
107 112 raise exc(_('cannot find required "%s" tool') % name)
108 113
114
109 115 class NoRepo(Exception):
110 116 pass
111 117
118
112 119 SKIPREV = 'SKIP'
113 120
121
114 122 class commit(object):
115 def __init__(self, author, date, desc, parents, branch=None, rev=None,
116 extra=None, sortkey=None, saverev=True, phase=phases.draft,
117 optparents=None, ctx=None):
123 def __init__(
124 self,
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 138 self.author = author or 'unknown'
119 139 self.date = date or '0 0'
120 140 self.desc = desc
@@ -128,6 +148,7 b' class commit(object):'
128 148 self.phase = phase
129 149 self.ctx = ctx # for hg to hg conversions
130 150
151
131 152 class converter_source(object):
132 153 """Conversion source interface"""
133 154
@@ -146,8 +167,10 b' class converter_source(object):'
146 167 such format for their revision numbering
147 168 """
148 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'
150 ' identifier') % (mapname, revstr))
170 raise error.Abort(
171 _('%s entry %s is not a valid revision' ' identifier')
172 % (mapname, revstr)
173 )
151 174
152 175 def before(self):
153 176 pass
@@ -223,8 +246,9 b' class converter_source(object):'
223 246 try:
224 247 return s.decode("latin-1").encode("utf-8")
225 248 except UnicodeError:
226 return s.decode(pycompat.sysstr(encoding),
227 "replace").encode("utf-8")
249 return s.decode(pycompat.sysstr(encoding), "replace").encode(
250 "utf-8"
251 )
228 252
229 253 def getchangedfiles(self, rev, i):
230 254 """Return the files changed by rev compared to parent[i].
@@ -275,6 +299,7 b' class converter_source(object):'
275 299 """
276 300 return True
277 301
302
278 303 class converter_sink(object):
279 304 """Conversion sink (target) interface"""
280 305
@@ -301,8 +326,9 b' class converter_sink(object):'
301 326 mapping equivalent authors identifiers for each system."""
302 327 return None
303 328
304 def putcommit(self, files, copies, parents, commit, source, revmap, full,
305 cleanp2):
329 def putcommit(
330 self, files, copies, parents, commit, source, revmap, full, cleanp2
331 ):
306 332 """Create a revision with all changed files listed in 'files'
307 333 and having listed parents. 'commit' is a commit object
308 334 containing at a minimum the author, date, and message for this
@@ -369,6 +395,7 b' class converter_sink(object):'
369 395 special cases."""
370 396 raise NotImplementedError
371 397
398
372 399 class commandline(object):
373 400 def __init__(self, ui, command):
374 401 self.ui = ui
@@ -403,11 +430,15 b' class commandline(object):'
403 430
404 431 def _run(self, cmd, *args, **kwargs):
405 432 def popen(cmdline):
406 p = subprocess.Popen(procutil.tonativestr(cmdline),
407 shell=True, bufsize=-1,
433 p = subprocess.Popen(
434 procutil.tonativestr(cmdline),
435 shell=True,
436 bufsize=-1,
408 437 close_fds=procutil.closefds,
409 stdout=subprocess.PIPE)
438 stdout=subprocess.PIPE,
439 )
410 440 return p
441
411 442 return self._dorun(popen, cmd, *args, **kwargs)
412 443
413 444 def _run2(self, cmd, *args, **kwargs):
@@ -495,6 +526,7 b' class commandline(object):'
495 526 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
496 527 self.run0(cmd, *(list(args) + l), **kwargs)
497 528
529
498 530 class mapfile(dict):
499 531 def __init__(self, ui, path):
500 532 super(mapfile, self).__init__()
@@ -523,7 +555,8 b' class mapfile(dict):'
523 555 except ValueError:
524 556 raise error.Abort(
525 557 _('syntax error in %s(%d): key/value pair expected')
526 % (self.path, i + 1))
558 % (self.path, i + 1)
559 )
527 560 if key not in self:
528 561 self.order.append(key)
529 562 super(mapfile, self).__setitem__(key, value)
@@ -535,8 +568,9 b' class mapfile(dict):'
535 568 self.fp = open(self.path, 'ab')
536 569 except IOError as err:
537 570 raise error.Abort(
538 _('could not open map file %r: %s') %
539 (self.path, encoding.strtolocal(err.strerror)))
571 _('could not open map file %r: %s')
572 % (self.path, encoding.strtolocal(err.strerror))
573 )
540 574 self.fp.write(util.tonativeeol('%s %s\n' % (key, value)))
541 575 self.fp.flush()
542 576 super(mapfile, self).__setitem__(key, value)
@@ -546,9 +580,11 b' class mapfile(dict):'
546 580 self.fp.close()
547 581 self.fp = None
548 582
583
549 584 def makedatetimestamp(t):
550 585 """Like dateutil.makedate() but for time t instead of current time"""
551 delta = (datetime.datetime.utcfromtimestamp(t) -
552 datetime.datetime.fromtimestamp(t))
586 delta = datetime.datetime.utcfromtimestamp(
587 t
588 ) - datetime.datetime.fromtimestamp(t)
553 589 tz = delta.days * 86400 + delta.seconds
554 590 return t, tz
@@ -54,12 +54,15 b' svn_source = subversion.svn_source'
54 54
55 55 orig_encoding = 'ascii'
56 56
57
57 58 def recode(s):
58 59 if isinstance(s, pycompat.unicode):
59 60 return s.encode(pycompat.sysstr(orig_encoding), 'replace')
60 61 else:
61 62 return s.decode('utf-8').encode(
62 pycompat.sysstr(orig_encoding), 'replace')
63 pycompat.sysstr(orig_encoding), 'replace'
64 )
65
63 66
64 67 def mapbranch(branch, branchmap):
65 68 '''
@@ -90,10 +93,11 b' def mapbranch(branch, branchmap):'
90 93 branch = branchmap.get(branch or 'default', branch)
91 94 # At some point we used "None" literal to denote the default branch,
92 95 # attempt to use that for backward compatibility.
93 if (not branch):
96 if not branch:
94 97 branch = branchmap.get('None', branch)
95 98 return branch
96 99
100
97 101 source_converters = [
98 102 ('cvs', convert_cvs, 'branchsort'),
99 103 ('git', convert_git, 'branchsort'),
@@ -111,6 +115,7 b' sink_converters = ['
111 115 ('svn', svn_sink),
112 116 ]
113 117
118
114 119 def convertsource(ui, path, type, revs):
115 120 exceptions = []
116 121 if type and type not in [s[0] for s in source_converters]:
@@ -126,6 +131,7 b' def convertsource(ui, path, type, revs):'
126 131 ui.write("%s\n" % pycompat.bytestr(inst.args[0]))
127 132 raise error.Abort(_('%s: missing or unsupported repository') % path)
128 133
134
129 135 def convertsink(ui, path, type):
130 136 if type and type not in [s[0] for s in sink_converters]:
131 137 raise error.Abort(_('%s: invalid destination repository type') % type)
@@ -139,12 +145,14 b' def convertsink(ui, path, type):'
139 145 raise error.Abort('%s\n' % inst)
140 146 raise error.Abort(_('%s: unknown repository type') % path)
141 147
148
142 149 class progresssource(object):
143 150 def __init__(self, ui, source, filecount):
144 151 self.ui = ui
145 152 self.source = source
146 self.progress = ui.makeprogress(_('getting files'), unit=_('files'),
147 total=filecount)
153 self.progress = ui.makeprogress(
154 _('getting files'), unit=_('files'), total=filecount
155 )
148 156
149 157 def getfile(self, file, rev):
150 158 self.progress.increment(item=file)
@@ -159,6 +167,7 b' class progresssource(object):'
159 167 def close(self):
160 168 self.progress.complete()
161 169
170
162 171 class converter(object):
163 172 def __init__(self, ui, source, dest, revmapfile, opts):
164 173
@@ -213,8 +222,13 b' class converter(object):'
213 222 line = list(lex)
214 223 # check number of parents
215 224 if not (2 <= len(line) <= 3):
216 raise error.Abort(_('syntax error in %s(%d): child parent1'
217 '[,parent2] expected') % (path, i + 1))
225 raise error.Abort(
226 _(
227 'syntax error in %s(%d): child parent1'
228 '[,parent2] expected'
229 )
230 % (path, i + 1)
231 )
218 232 for part in line:
219 233 self.source.checkrevformat(part)
220 234 child, p1, p2 = line[0], line[1:2], line[2:]
@@ -224,11 +238,11 b' class converter(object):'
224 238 m[child] = p1 + p2
225 239 # if file does not exist or error reading, exit
226 240 except IOError:
227 raise error.Abort(_('splicemap file not found or error reading %s:')
228 % path)
241 raise error.Abort(
242 _('splicemap file not found or error reading %s:') % path
243 )
229 244 return m
230 245
231
232 246 def walktree(self, heads):
233 247 '''Return a mapping that identifies the uncommitted parents of every
234 248 uncommitted changeset.'''
@@ -236,8 +250,9 b' class converter(object):'
236 250 known = set()
237 251 parents = {}
238 252 numcommits = self.source.numcommits()
239 progress = self.ui.makeprogress(_('scanning'), unit=_('revisions'),
240 total=numcommits)
253 progress = self.ui.makeprogress(
254 _('scanning'), unit=_('revisions'), total=numcommits
255 )
241 256 while visit:
242 257 n = visit.pop(0)
243 258 if n in known:
@@ -266,8 +281,13 b' class converter(object):'
266 281 if c not in parents:
267 282 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
268 283 # Could be in source but not converted during this run
269 self.ui.warn(_('splice map revision %s is not being '
270 'converted, ignoring\n') % c)
284 self.ui.warn(
285 _(
286 'splice map revision %s is not being '
287 'converted, ignoring\n'
288 )
289 % c
290 )
271 291 continue
272 292 pc = []
273 293 for p in splicemap[c]:
@@ -325,6 +345,7 b' class converter(object):'
325 345 compression.
326 346 """
327 347 prev = [None]
348
328 349 def picknext(nodes):
329 350 next = nodes[0]
330 351 for n in nodes:
@@ -333,26 +354,34 b' class converter(object):'
333 354 break
334 355 prev[0] = next
335 356 return next
357
336 358 return picknext
337 359
338 360 def makesourcesorter():
339 361 """Source specific sort."""
340 362 keyfn = lambda n: self.commitcache[n].sortkey
363
341 364 def picknext(nodes):
342 365 return sorted(nodes, key=keyfn)[0]
366
343 367 return picknext
344 368
345 369 def makeclosesorter():
346 370 """Close order sort."""
347 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
348 self.commitcache[n].sortkey)
371 keyfn = lambda n: (
372 'close' not in self.commitcache[n].extra,
373 self.commitcache[n].sortkey,
374 )
375
349 376 def picknext(nodes):
350 377 return sorted(nodes, key=keyfn)[0]
378
351 379 return picknext
352 380
353 381 def makedatesorter():
354 382 """Sort revisions by date."""
355 383 dates = {}
384
356 385 def getdate(n):
357 386 if n not in dates:
358 387 dates[n] = dateutil.parsedate(self.commitcache[n].date)
@@ -390,8 +419,10 b' class converter(object):'
390 419 try:
391 420 pendings[c].remove(n)
392 421 except ValueError:
393 raise error.Abort(_('cycle detected between %s and %s')
394 % (recode(c), recode(n)))
422 raise error.Abort(
423 _('cycle detected between %s and %s')
424 % (recode(c), recode(n))
425 )
395 426 if not pendings[c]:
396 427 # Parents are converted, node is eligible
397 428 actives.insert(0, c)
@@ -408,8 +439,9 b' class converter(object):'
408 439 self.ui.status(_('writing author map file %s\n') % authorfile)
409 440 ofile = open(authorfile, 'wb+')
410 441 for author in self.authors:
411 ofile.write(util.tonativeeol("%s=%s\n"
412 % (author, self.authors[author])))
442 ofile.write(
443 util.tonativeeol("%s=%s\n" % (author, self.authors[author]))
444 )
413 445 ofile.close()
414 446
415 447 def readauthormap(self, authorfile):
@@ -464,19 +496,22 b' class converter(object):'
464 496 for prev in commit.parents:
465 497 if prev not in self.commitcache:
466 498 self.cachecommit(prev)
467 pbranches.append((self.map[prev],
468 self.commitcache[prev].branch))
499 pbranches.append(
500 (self.map[prev], self.commitcache[prev].branch)
501 )
469 502 self.dest.setbranch(commit.branch, pbranches)
470 503 try:
471 504 parents = self.splicemap[rev]
472 self.ui.status(_('spliced in %s as parents of %s\n') %
473 (_(' and ').join(parents), rev))
505 self.ui.status(
506 _('spliced in %s as parents of %s\n')
507 % (_(' and ').join(parents), rev)
508 )
474 509 parents = [self.map.get(p, p) for p in parents]
475 510 except KeyError:
476 511 parents = [b[0] for b in pbranches]
477 parents.extend(self.map[x]
478 for x in commit.optparents
479 if x in self.map)
512 parents.extend(
513 self.map[x] for x in commit.optparents if x in self.map
514 )
480 515 if len(pbranches) != 2:
481 516 cleanp2 = set()
482 517 if len(parents) < 3:
@@ -486,10 +521,12 b' class converter(object):'
486 521 # changed files N-1 times. This tweak to the number of
487 522 # files makes it so the progress bar doesn't overflow
488 523 # itself.
489 source = progresssource(self.ui, self.source,
490 len(files) * (len(parents) - 1))
491 newnode = self.dest.putcommit(files, copies, parents, commit,
492 source, self.map, full, cleanp2)
524 source = progresssource(
525 self.ui, self.source, len(files) * (len(parents) - 1)
526 )
527 newnode = self.dest.putcommit(
528 files, copies, parents, commit, source, self.map, full, cleanp2
529 )
493 530 source.close()
494 531 self.source.converted(rev, newnode)
495 532 self.map[rev] = newnode
@@ -509,8 +546,9 b' class converter(object):'
509 546 c = None
510 547
511 548 self.ui.status(_("converting...\n"))
512 progress = self.ui.makeprogress(_('converting'),
513 unit=_('revisions'), total=len(t))
549 progress = self.ui.makeprogress(
550 _('converting'), unit=_('revisions'), total=len(t)
551 )
514 552 for i, c in enumerate(t):
515 553 num -= 1
516 554 desc = self.commitcache[c].desc
@@ -538,8 +576,11 b' class converter(object):'
538 576 if nrev and tagsparent:
539 577 # write another hash correspondence to override the
540 578 # previous one so we don't end up with extra tag heads
541 tagsparents = [e for e in self.map.iteritems()
542 if e[1] == tagsparent]
579 tagsparents = [
580 e
581 for e in self.map.iteritems()
582 if e[1] == tagsparent
583 ]
543 584 if tagsparents:
544 585 self.map[tagsparents[0][0]] = nrev
545 586
@@ -564,6 +605,7 b' class converter(object):'
564 605 self.source.after()
565 606 self.map.close()
566 607
608
567 609 def convert(ui, src, dest=None, revmapfile=None, **opts):
568 610 opts = pycompat.byteskwargs(opts)
569 611 global orig_encoding
@@ -582,8 +624,9 b' def convert(ui, src, dest=None, revmapfi'
582 624 destc = scmutil.wrapconvertsink(destc)
583 625
584 626 try:
585 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
586 opts.get('rev'))
627 srcc, defaultsort = convertsource(
628 ui, src, opts.get('source_type'), opts.get('rev')
629 )
587 630 except Exception:
588 631 for path in destc.created:
589 632 shutil.rmtree(path, True)
@@ -599,7 +642,8 b' def convert(ui, src, dest=None, revmapfi'
599 642 sortmode = defaultsort
600 643
601 644 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
602 raise error.Abort(_('--sourcesort is not supported by this data source')
645 raise error.Abort(
646 _('--sourcesort is not supported by this data source')
603 647 )
604 648 if sortmode == 'closesort' and not srcc.hasnativeclose():
605 649 raise error.Abort(_('--closesort is not supported by this data source'))
@@ -34,6 +34,7 b' converter_source = common.converter_sour'
34 34 makedatetimestamp = common.makedatetimestamp
35 35 NoRepo = common.NoRepo
36 36
37
37 38 class convert_cvs(converter_source):
38 39 def __init__(self, ui, repotype, path, revs=None):
39 40 super(convert_cvs, self).__init__(ui, repotype, path, revs=revs)
@@ -63,15 +64,17 b' class convert_cvs(converter_source):'
63 64 maxrev = 0
64 65 if self.revs:
65 66 if len(self.revs) > 1:
66 raise error.Abort(_('cvs source does not support specifying '
67 'multiple revs'))
67 raise error.Abort(
68 _('cvs source does not support specifying ' 'multiple revs')
69 )
68 70 # TODO: handle tags
69 71 try:
70 72 # patchset number?
71 73 maxrev = int(self.revs[0])
72 74 except ValueError:
73 raise error.Abort(_('revision %s is not a patchset number')
74 % self.revs[0])
75 raise error.Abort(
76 _('revision %s is not a patchset number') % self.revs[0]
77 )
75 78
76 79 d = encoding.getcwd()
77 80 try:
@@ -81,15 +84,18 b' class convert_cvs(converter_source):'
81 84 if not self.ui.configbool('convert', 'cvsps.cache'):
82 85 cache = None
83 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 90 fuzz=int(self.ui.config('convert', 'cvsps.fuzz')),
86 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 95 for cs in db:
90 96 if maxrev and cs.id > maxrev:
91 97 break
92 id = (b"%d" % cs.id)
98 id = b"%d" % cs.id
93 99 cs.author = self.recode(cs.author)
94 100 self.lastbranch[cs.branch] = id
95 101 cs.comment = self.recode(cs.comment)
@@ -100,14 +106,19 b' class convert_cvs(converter_source):'
100 106
101 107 files = {}
102 108 for f in cs.entries:
103 files[f.file] = "%s%s" % ('.'.join([(b"%d" % x)
104 for x in f.revision]),
105 ['', '(DEAD)'][f.dead])
109 files[f.file] = "%s%s" % (
110 '.'.join([(b"%d" % x) for x in f.revision]),
111 ['', '(DEAD)'][f.dead],
112 )
106 113
107 114 # add current commit to set
108 c = commit(author=cs.author, date=date,
115 c = commit(
116 author=cs.author,
117 date=date,
109 118 parents=[(b"%d" % p.id) for p in cs.parents],
110 desc=cs.comment, branch=cs.branch or '')
119 desc=cs.comment,
120 branch=cs.branch or '',
121 )
111 122 self.changeset[id] = c
112 123 self.files[id] = files
113 124
@@ -125,8 +136,9 b' class convert_cvs(converter_source):'
125 136
126 137 if root.startswith(":pserver:"):
127 138 root = root[9:]
128 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
129 root)
139 m = re.match(
140 r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)', root
141 )
130 142 if m:
131 143 conntype = "pserver"
132 144 user, passw, serv, port, root = m.groups()
@@ -166,8 +178,18 b' class convert_cvs(converter_source):'
166 178
167 179 sck = socket.socket()
168 180 sck.connect((serv, port))
169 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
170 "END AUTH REQUEST", ""]))
181 sck.send(
182 "\n".join(
183 [
184 "BEGIN AUTH REQUEST",
185 root,
186 user,
187 passw,
188 "END AUTH REQUEST",
189 "",
190 ]
191 )
192 )
171 193 if sck.recv(128) != "I LOVE YOU\n":
172 194 raise error.Abort(_("CVS pserver authentication failed"))
173 195
@@ -205,16 +227,22 b' class convert_cvs(converter_source):'
205 227 self.realroot = root
206 228
207 229 self.writep.write("Root %s\n" % root)
208 self.writep.write("Valid-responses ok error Valid-requests Mode"
230 self.writep.write(
231 "Valid-responses ok error Valid-requests Mode"
209 232 " M Mbinary E Checked-in Created Updated"
210 " Merged Removed\n")
233 " Merged Removed\n"
234 )
211 235 self.writep.write("valid-requests\n")
212 236 self.writep.flush()
213 237 r = self.readp.readline()
214 238 if not r.startswith("Valid-requests"):
215 raise error.Abort(_('unexpected response from CVS server '
216 '(expected "Valid-requests", but got %r)')
217 % r)
239 raise error.Abort(
240 _(
241 'unexpected response from CVS server '
242 '(expected "Valid-requests", but got %r)'
243 )
244 % r
245 )
218 246 if "UseUnchanged" in r:
219 247 self.writep.write("UseUnchanged\n")
220 248 self.writep.flush()
@@ -225,7 +253,6 b' class convert_cvs(converter_source):'
225 253 return self.heads
226 254
227 255 def getfile(self, name, rev):
228
229 256 def chunkedread(fp, count):
230 257 # file-objects returned by socket.makefile() do not handle
231 258 # large read() requests very well.
@@ -234,8 +261,9 b' class convert_cvs(converter_source):'
234 261 while count > 0:
235 262 data = fp.read(min(count, chunksize))
236 263 if not data:
237 raise error.Abort(_("%d bytes missing from remote file")
238 % count)
264 raise error.Abort(
265 _("%d bytes missing from remote file") % count
266 )
239 267 count -= len(data)
240 268 output.write(data)
241 269 return output.getvalue()
@@ -26,6 +26,7 b' from mercurial.utils import ('
26 26
27 27 pickle = util.pickle
28 28
29
29 30 class logentry(object):
30 31 '''Class logentry has the following attributes:
31 32 .author - author name as CVS knows it
@@ -46,17 +47,22 b' class logentry(object):'
46 47 rlog output) or None
47 48 .branchpoints - the branches that start at the current entry or empty
48 49 '''
50
49 51 def __init__(self, **entries):
50 52 self.synthetic = False
51 53 self.__dict__.update(entries)
52 54
53 55 def __repr__(self):
54 items = (r"%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
56 items = (
57 r"%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__)
58 )
55 59 return r"%s(%s)"%(type(self).__name__, r", ".join(items))
56 60
61
57 62 class logerror(Exception):
58 63 pass
59 64
65
60 66 def getrepopath(cvspath):
61 67 """Return the repository path from a CVS path.
62 68
@@ -96,12 +102,14 b' def getrepopath(cvspath):'
96 102 repopath = parts[-1][parts[-1].find('/', start):]
97 103 return repopath
98 104
105
99 106 def createlog(ui, directory=None, root="", rlog=True, cache=None):
100 107 '''Collect the CVS rlog'''
101 108
102 109 # Because we store many duplicate commit log messages, reusing strings
103 110 # saves a lot of memory and pickle storage space.
104 111 _scache = {}
112
105 113 def scache(s):
106 114 "return a shared version of a string"
107 115 return _scache.setdefault(s, s)
@@ -114,19 +122,24 b' def createlog(ui, directory=None, root="'
114 122 re_00 = re.compile(b'RCS file: (.+)$')
115 123 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$')
116 124 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$')
117 re_03 = re.compile(b"(Cannot access.+CVSROOT)|"
118 b"(can't create temporary directory.+)$")
125 re_03 = re.compile(
126 b"(Cannot access.+CVSROOT)|" b"(can't create temporary directory.+)$"
127 )
119 128 re_10 = re.compile(b'Working file: (.+)$')
120 129 re_20 = re.compile(b'symbolic names:')
121 130 re_30 = re.compile(b'\t(.+): ([\\d.]+)$')
122 131 re_31 = re.compile(b'----------------------------$')
123 re_32 = re.compile(b'======================================='
124 b'======================================$')
132 re_32 = re.compile(
133 b'======================================='
134 b'======================================$'
135 )
125 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(
138 br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
127 139 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
128 140 br'(\s+commitid:\s+([^;]+);)?'
129 br'(.*mergepoint:\s+([^;]+);)?')
141 br'(.*mergepoint:\s+([^;]+);)?'
142 )
130 143 re_70 = re.compile(b'branches: (.+);$')
131 144
132 145 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch')
@@ -178,17 +191,20 b' def createlog(ui, directory=None, root="'
178 191 # are mapped to different cache file names.
179 192 cachefile = root.split(":") + [directory, "cache"]
180 193 cachefile = ['-'.join(re.findall(br'\w+', s)) for s in cachefile if s]
181 cachefile = os.path.join(cachedir,
182 '.'.join([s for s in cachefile if s]))
194 cachefile = os.path.join(
195 cachedir, '.'.join([s for s in cachefile if s])
196 )
183 197
184 198 if cache == 'update':
185 199 try:
186 200 ui.note(_('reading cvs log cache %s\n') % cachefile)
187 201 oldlog = pickle.load(open(cachefile, 'rb'))
188 202 for e in oldlog:
189 if not (util.safehasattr(e, 'branchpoints') and
190 util.safehasattr(e, 'commitid') and
191 util.safehasattr(e, 'mergepoint')):
203 if not (
204 util.safehasattr(e, 'branchpoints')
205 and util.safehasattr(e, 'commitid')
206 and util.safehasattr(e, 'mergepoint')
207 ):
192 208 ui.status(_('ignoring old cache\n'))
193 209 oldlog = []
194 210 break
@@ -310,8 +326,9 b' def createlog(ui, directory=None, root="'
310 326 if re_31.match(line):
311 327 state = 5
312 328 else:
313 assert not re_32.match(line), _('must have at least '
314 'some revisions')
329 assert not re_32.match(line), _(
330 'must have at least ' 'some revisions'
331 )
315 332
316 333 elif state == 5:
317 334 # expecting revision number and possibly (ignored) lock indication
@@ -319,15 +336,16 b' def createlog(ui, directory=None, root="'
319 336 # as this state is re-entered for subsequent revisions of a file.
320 337 match = re_50.match(line)
321 338 assert match, _('expected revision number')
322 e = logentry(rcs=scache(rcs),
339 e = logentry(
340 rcs=scache(rcs),
323 341 file=scache(filename),
324 revision=tuple([int(x) for x in
325 match.group(1).split('.')]),
342 revision=tuple([int(x) for x in match.group(1).split('.')]),
326 343 branches=[],
327 344 parent=None,
328 345 commitid=None,
329 346 mergepoint=None,
330 branchpoints=set())
347 branchpoints=set(),
348 )
331 349
332 350 state = 6
333 351
@@ -343,9 +361,10 b' def createlog(ui, directory=None, root="'
343 361 if len(d.split()) != 3:
344 362 # cvs log dates always in GMT
345 363 d = d + ' UTC'
346 e.date = dateutil.parsedate(d, ['%y/%m/%d %H:%M:%S',
347 '%Y/%m/%d %H:%M:%S',
348 '%Y-%m-%d %H:%M:%S'])
364 e.date = dateutil.parsedate(
365 d,
366 ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S'],
367 )
349 368 e.author = scache(match.group(2))
350 369 e.dead = match.group(3).lower() == 'dead'
351 370
@@ -369,8 +388,9 b' def createlog(ui, directory=None, root="'
369 388 else:
370 389 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
371 390 branches = [b for b in branchmap if branchmap[b] == myrev]
372 assert len(branches) == 1, ('unknown branch: %s'
373 % e.mergepoint)
391 assert len(branches) == 1, (
392 'unknown branch: %s' % e.mergepoint
393 )
374 394 e.mergepoint = branches[0]
375 395
376 396 e.comment = []
@@ -381,8 +401,10 b' def createlog(ui, directory=None, root="'
381 401 # or store the commit log message otherwise
382 402 m = re_70.match(line)
383 403 if m:
384 e.branches = [tuple([int(y) for y in x.strip().split('.')])
385 for x in m.group(1).split(';')]
404 e.branches = [
405 tuple([int(y) for y in x.strip().split('.')])
406 for x in m.group(1).split(';')
407 ]
386 408 state = 8
387 409 elif re_31.match(line) and re_50.match(peek):
388 410 state = 5
@@ -417,13 +439,16 b' def createlog(ui, directory=None, root="'
417 439 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
418 440 # these revisions now, but mark them synthetic so
419 441 # createchangeset() can take care of them.
420 if (store and
421 e.dead and
422 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
423 len(e.comment) == 1 and
424 file_added_re.match(e.comment[0])):
425 ui.debug('found synthetic revision in %s: %r\n'
426 % (e.rcs, e.comment[0]))
442 if (
443 store
444 and e.dead
445 and e.revision[-1] == 1
446 and len(e.comment) == 1 # 1.1 or 1.1.x.1
447 and file_added_re.match(e.comment[0])
448 ):
449 ui.debug(
450 'found synthetic revision in %s: %r\n' % (e.rcs, e.comment[0])
451 )
427 452 e.synthetic = True
428 453
429 454 if store:
@@ -458,8 +483,9 b' def createlog(ui, directory=None, root="'
458 483 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
459 484
460 485 if len(log) % 100 == 0:
461 ui.status(stringutil.ellipsis('%d %s' % (len(log), e.file), 80)
462 + '\n')
486 ui.status(
487 stringutil.ellipsis('%d %s' % (len(log), e.file), 80) + '\n'
488 )
463 489
464 490 log.sort(key=lambda x: (x.rcs, x.revision))
465 491
@@ -487,8 +513,12 b' def createlog(ui, directory=None, root="'
487 513 log.sort(key=lambda x: x.date)
488 514
489 515 if oldlog and oldlog[-1].date >= log[0].date:
490 raise logerror(_('log cache overlaps with new log entries,'
491 ' re-run without cache.'))
516 raise logerror(
517 _(
518 'log cache overlaps with new log entries,'
519 ' re-run without cache.'
520 )
521 )
492 522
493 523 log = oldlog + log
494 524
@@ -502,6 +532,7 b' def createlog(ui, directory=None, root="'
502 532
503 533 encodings = ui.configlist('convert', 'cvsps.logencoding')
504 534 if encodings:
535
505 536 def revstr(r):
506 537 # this is needed, because logentry.revision is a tuple of "int"
507 538 # (e.g. (1, 2) for "1.2")
@@ -511,24 +542,33 b' def createlog(ui, directory=None, root="'
511 542 comment = entry.comment
512 543 for e in encodings:
513 544 try:
514 entry.comment = comment.decode(
515 pycompat.sysstr(e)).encode('utf-8')
545 entry.comment = comment.decode(pycompat.sysstr(e)).encode(
546 'utf-8'
547 )
516 548 if ui.debugflag:
517 ui.debug("transcoding by %s: %s of %s\n" %
518 (e, revstr(entry.revision), entry.file))
549 ui.debug(
550 "transcoding by %s: %s of %s\n"
551 % (e, revstr(entry.revision), entry.file)
552 )
519 553 break
520 554 except UnicodeDecodeError:
521 555 pass # try next encoding
522 556 except LookupError as inst: # unknown encoding, maybe
523 raise error.Abort(inst,
524 hint=_('check convert.cvsps.logencoding'
525 ' configuration'))
557 raise error.Abort(
558 inst,
559 hint=_(
560 'check convert.cvsps.logencoding' ' configuration'
561 ),
562 )
526 563 else:
527 raise error.Abort(_("no encoding can transcode"
528 " CVS log message for %s of %s")
564 raise error.Abort(
565 _(
566 "no encoding can transcode"
567 " CVS log message for %s of %s"
568 )
529 569 % (revstr(entry.revision), entry.file),
530 hint=_('check convert.cvsps.logencoding'
531 ' configuration'))
570 hint=_('check convert.cvsps.logencoding' ' configuration'),
571 )
532 572
533 573 hook.hook(ui, None, "cvslog", True, log=log)
534 574
@@ -550,6 +590,7 b' class changeset(object):'
550 590 .mergepoint- the branch that has been merged from or None
551 591 .branchpoints- the branches that start at the current entry or empty
552 592 '''
593
553 594 def __init__(self, **entries):
554 595 self.id = None
555 596 self.synthetic = False
@@ -559,6 +600,7 b' class changeset(object):'
559 600 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
560 601 return "%s(%s)"%(type(self).__name__, ", ".join(items))
561 602
603
562 604 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
563 605 '''Convert log into changesets.'''
564 606
@@ -574,9 +616,17 b' def createchangeset(ui, log, fuzz=60, me'
574 616 mindate[e.commitid] = min(e.date, mindate[e.commitid])
575 617
576 618 # Merge changesets
577 log.sort(key=lambda x: (mindate.get(x.commitid, (-1, 0)),
578 x.commitid or '', x.comment,
579 x.author, x.branch or '', x.date, x.branchpoints))
619 log.sort(
620 key=lambda x: (
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 631 changesets = []
582 632 files = set()
@@ -599,22 +649,35 b' def createchangeset(ui, log, fuzz=60, me'
599 649 # first changeset and bar the next and MYBRANCH and MYBRANCH2
600 650 # should both start off of the bar changeset. No provisions are
601 651 # made to ensure that this is, in fact, what happens.
602 if not (c and e.branchpoints == c.branchpoints and
603 (# cvs commitids
604 (e.commitid is not None and e.commitid == c.commitid) or
605 (# no commitids, use fuzzy commit detection
606 (e.commitid is None or c.commitid is None) and
607 e.comment == c.comment and
608 e.author == c.author and
609 e.branch == c.branch and
610 ((c.date[0] + c.date[1]) <=
611 (e.date[0] + e.date[1]) <=
612 (c.date[0] + c.date[1]) + fuzz) and
613 e.file not in files))):
614 c = changeset(comment=e.comment, author=e.author,
615 branch=e.branch, date=e.date,
616 entries=[], mergepoint=e.mergepoint,
617 branchpoints=e.branchpoints, commitid=e.commitid)
652 if not (
653 c
654 and e.branchpoints == c.branchpoints
655 and ( # cvs commitids
656 (e.commitid is not None and e.commitid == c.commitid)
657 or ( # no commitids, use fuzzy commit detection
658 (e.commitid is None or c.commitid is None)
659 and e.comment == c.comment
660 and e.author == c.author
661 and e.branch == c.branch
662 and (
663 (c.date[0] + c.date[1])
664 <= (e.date[0] + e.date[1])
665 <= (c.date[0] + c.date[1]) + fuzz
666 )
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 681 changesets.append(c)
619 682
620 683 files = set()
@@ -665,6 +728,7 b' def createchangeset(ui, log, fuzz=60, me'
665 728 # Sort changesets by date
666 729
667 730 odd = set()
731
668 732 def cscmp(l, r):
669 733 d = sum(l.date) - sum(r.date)
670 734 if d:
@@ -777,8 +841,9 b' def createchangeset(ui, log, fuzz=60, me'
777 841
778 842 # Ensure no changeset has a synthetic changeset as a parent.
779 843 while p.synthetic:
780 assert len(p.parents) <= 1, (
781 _('synthetic changeset cannot have multiple parents'))
844 assert len(p.parents) <= 1, _(
845 'synthetic changeset cannot have multiple parents'
846 )
782 847 if p.parents:
783 848 p = p.parents[0]
784 849 else:
@@ -802,9 +867,13 b' def createchangeset(ui, log, fuzz=60, me'
802 867 try:
803 868 candidate = changesets[branches[m]]
804 869 except KeyError:
805 ui.warn(_("warning: CVS commit message references "
806 "non-existent branch %r:\n%s\n")
807 % (pycompat.bytestr(m), c.comment))
870 ui.warn(
871 _(
872 "warning: CVS commit message references "
873 "non-existent branch %r:\n%s\n"
874 )
875 % (pycompat.bytestr(m), c.comment)
876 )
808 877 if m in branches and c.branch != m and not candidate.synthetic:
809 878 c.parents.append(candidate)
810 879
@@ -820,11 +889,15 b' def createchangeset(ui, log, fuzz=60, me'
820 889 if m in branches and c.branch != m:
821 890 # insert empty changeset for merge
822 891 cc = changeset(
823 author=c.author, branch=m, date=c.date,
892 author=c.author,
893 branch=m,
894 date=c.date,
824 895 comment='convert-repo: CVS merge from branch %s'
825 896 % c.branch,
826 entries=[], tags=[],
827 parents=[changesets[branches[m]], c])
897 entries=[],
898 tags=[],
899 parents=[changesets[branches[m]], c],
900 )
828 901 changesets.insert(i + 1, cc)
829 902 branches[m] = i + 1
830 903
@@ -853,8 +926,10 b' def createchangeset(ui, log, fuzz=60, me'
853 926 if odd:
854 927 for l, r in odd:
855 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')
857 % (l.id, r.id))
929 ui.warn(
930 _('changeset %d is both before and after %d\n')
931 % (l.id, r.id)
932 )
858 933
859 934 ui.status(_('%d changeset entries\n') % len(changesets))
860 935
@@ -901,8 +976,10 b' def debugcvsps(ui, *args, **opts):'
901 976
902 977 if opts["ancestors"]:
903 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,
905 cs.parents[0].id)
979 ancestors[cs.branch] = (
980 changesets[cs.parents[0].id - 1].branch,
981 cs.parents[0].id,
982 )
906 983 branches[cs.branch] = cs.id
907 984
908 985 # limit by branches
@@ -914,19 +991,35 b' def debugcvsps(ui, *args, **opts):'
914 991 # bug-for-bug compatibility with cvsps.
915 992 ui.write('---------------------\n')
916 993 ui.write(('PatchSet %d \n' % cs.id))
917 ui.write(('Date: %s\n' % dateutil.datestr(cs.date,
918 '%Y/%m/%d %H:%M:%S %1%2')))
994 ui.write(
995 (
996 'Date: %s\n'
997 % dateutil.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2')
998 )
999 )
919 1000 ui.write(('Author: %s\n' % cs.author))
920 1001 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
921 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
922 ','.join(cs.tags) or '(none)')))
1002 ui.write(
1003 (
1004 'Tag%s: %s \n'
1005 % (
1006 ['', 's'][len(cs.tags) > 1],
1007 ','.join(cs.tags) or '(none)',
1008 )
1009 )
1010 )
923 1011 if cs.branchpoints:
924 ui.write(('Branchpoints: %s \n') %
925 ', '.join(sorted(cs.branchpoints)))
1012 ui.write(
1013 'Branchpoints: %s \n' % ', '.join(sorted(cs.branchpoints))
1014 )
926 1015 if opts["parents"] and cs.parents:
927 1016 if len(cs.parents) > 1:
928 ui.write(('Parents: %s\n' %
929 (','.join([(b"%d" % p.id) for p in cs.parents]))))
1017 ui.write(
1018 (
1019 'Parents: %s\n'
1020 % (','.join([(b"%d" % p.id) for p in cs.parents]))
1021 )
1022 )
930 1023 else:
931 1024 ui.write(('Parent: %d\n' % cs.parents[0].id))
932 1025
@@ -939,28 +1032,30 b' def debugcvsps(ui, *args, **opts):'
939 1032 if r:
940 1033 ui.write(('Ancestors: %s\n' % (','.join(r))))
941 1034
942 ui.write(('Log:\n'))
1035 ui.write('Log:\n')
943 1036 ui.write('%s\n\n' % cs.comment)
944 ui.write(('Members: \n'))
1037 ui.write('Members: \n')
945 1038 for f in cs.entries:
946 1039 fn = f.file
947 1040 if fn.startswith(opts["prefix"]):
948 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 1045 fn,
951 1046 '.'.join([b"%d" % x for x in f.parent]) or 'INITIAL',
952 1047 '.'.join([(b"%d" % x) for x in f.revision]),
953 ['', '(DEAD)'][f.dead]))
1048 ['', '(DEAD)'][f.dead],
1049 )
1050 )
954 1051 ui.write('\n')
955 1052
956 1053 # have we seen the start tag?
957 1054 if revisions and off:
958 if (revisions[0] == (b"%d" % cs.id) or
959 revisions[0] in cs.tags):
1055 if revisions[0] == (b"%d" % cs.id) or revisions[0] in cs.tags:
960 1056 off = False
961 1057
962 1058 # see if we reached the end tag
963 1059 if len(revisions) > 1 and not off:
964 if (revisions[1] == (b"%d" % cs.id) or
965 revisions[1] in cs.tags):
1060 if revisions[1] == (b"%d" % cs.id) or revisions[1] in cs.tags:
966 1061 break
@@ -19,6 +19,7 b' from mercurial import ('
19 19 )
20 20 from mercurial.utils import dateutil
21 21 from . import common
22
22 23 NoRepo = common.NoRepo
23 24
24 25 # The naming drift of ElementTree is fun!
@@ -41,6 +42,7 b' except ImportError:'
41 42 except ImportError:
42 43 pass
43 44
45
44 46 class darcs_source(common.converter_source, common.commandline):
45 47 def __init__(self, ui, repotype, path, revs=None):
46 48 common.converter_source.__init__(self, ui, repotype, path, revs=revs)
@@ -54,8 +56,9 b' class darcs_source(common.converter_sour'
54 56 common.checktool('darcs')
55 57 version = self.run0('--version').splitlines()[0].strip()
56 58 if version < '2.1':
57 raise error.Abort(_('darcs version 2.1 or newer needed (found %r)')
58 % version)
59 raise error.Abort(
60 _('darcs version 2.1 or newer needed (found %r)') % version
61 )
59 62
60 63 if "ElementTree" not in globals():
61 64 raise error.Abort(_("Python ElementTree module is not available"))
@@ -71,19 +74,23 b' class darcs_source(common.converter_sour'
71 74 format = self.format()
72 75 if format:
73 76 if format in ('darcs-1.0', 'hashed'):
74 raise NoRepo(_("%s repository format is unsupported, "
75 "please upgrade") % format)
77 raise NoRepo(
78 _("%s repository format is unsupported, " "please upgrade")
79 % format
80 )
76 81 else:
77 82 self.ui.warn(_('failed to detect repository format!'))
78 83
79 84 def before(self):
80 85 self.tmppath = pycompat.mkdtemp(
81 prefix='convert-' + os.path.basename(self.path) + '-')
86 prefix='convert-' + os.path.basename(self.path) + '-'
87 )
82 88 output, status = self.run('init', repodir=self.tmppath)
83 89 self.checkexit(status)
84 90
85 tree = self.xml('changes', xml_output=True, summary=True,
86 repodir=self.path)
91 tree = self.xml(
92 'changes', xml_output=True, summary=True, repodir=self.path
93 )
87 94 tagname = None
88 95 child = None
89 96 for elt in tree.findall('patch'):
@@ -135,8 +142,9 b' class darcs_source(common.converter_sour'
135 142
136 143 def manifest(self):
137 144 man = []
138 output, status = self.run('show', 'files', no_directories=True,
139 repodir=self.tmppath)
145 output, status = self.run(
146 'show', 'files', no_directories=True, repodir=self.tmppath
147 )
140 148 self.checkexit(status)
141 149 for line in output.split('\n'):
142 150 path = line[2:]
@@ -155,17 +163,24 b' class darcs_source(common.converter_sour'
155 163 # etree can return unicode objects for name, comment, and author,
156 164 # so recode() is used to ensure str objects are emitted.
157 165 newdateformat = '%Y-%m-%d %H:%M:%S %1%2'
158 return common.commit(author=self.recode(elt.get('author')),
166 return common.commit(
167 author=self.recode(elt.get('author')),
159 168 date=dateutil.datestr(date, newdateformat),
160 169 desc=self.recode(desc).strip(),
161 parents=self.parents[rev])
170 parents=self.parents[rev],
171 )
162 172
163 173 def pull(self, rev):
164 output, status = self.run('pull', self.path, all=True,
174 output, status = self.run(
175 'pull',
176 self.path,
177 all=True,
165 178 match='hash %s' % rev,
166 no_test=True, no_posthook=True,
179 no_test=True,
180 no_posthook=True,
167 181 external_merge='/bin/false',
168 repodir=self.tmppath)
182 repodir=self.tmppath,
183 )
169 184 if status:
170 185 if output.find('We have conflicts in') == -1:
171 186 self.checkexit(status, output)
@@ -14,8 +14,10 b' from mercurial import ('
14 14 pycompat,
15 15 )
16 16 from . import common
17
17 18 SKIPREV = common.SKIPREV
18 19
20
19 21 def rpairs(path):
20 22 '''Yield tuples with path split at '/', starting with the full path.
21 23 No leading, trailing or double '/', please.
@@ -31,6 +33,7 b' def rpairs(path):'
31 33 i = path.rfind('/', 0, i)
32 34 yield '.', path
33 35
36
34 37 def normalize(path):
35 38 ''' We use posixpath.normpath to support cross-platform path format.
36 39 However, it doesn't handle None input. So we wrap it up. '''
@@ -38,6 +41,7 b' def normalize(path):'
38 41 return None
39 42 return posixpath.normpath(path)
40 43
44
41 45 class filemapper(object):
42 46 '''Map and filter filenames when importing.
43 47 A name can be mapped to itself, a new name, or None (omit from new
@@ -55,25 +59,31 b' class filemapper(object):'
55 59
56 60 def parse(self, path):
57 61 errs = 0
62
58 63 def check(name, mapping, listname):
59 64 if not name:
60 self.ui.warn(_('%s:%d: path to %s is missing\n') %
61 (lex.infile, lex.lineno, listname))
65 self.ui.warn(
66 _('%s:%d: path to %s is missing\n')
67 % (lex.infile, lex.lineno, listname)
68 )
62 69 return 1
63 70 if name in mapping:
64 self.ui.warn(_('%s:%d: %r already in %s list\n') %
65 (lex.infile, lex.lineno, name, listname))
71 self.ui.warn(
72 _('%s:%d: %r already in %s list\n')
73 % (lex.infile, lex.lineno, name, listname)
74 )
66 75 return 1
67 if (name.startswith('/') or
68 name.endswith('/') or
69 '//' in name):
70 self.ui.warn(_('%s:%d: superfluous / in %s %r\n') %
71 (lex.infile, lex.lineno, listname,
72 pycompat.bytestr(name)))
76 if name.startswith('/') or name.endswith('/') or '//' in name:
77 self.ui.warn(
78 _('%s:%d: superfluous / in %s %r\n')
79 % (lex.infile, lex.lineno, listname, pycompat.bytestr(name))
80 )
73 81 return 1
74 82 return 0
83
75 84 lex = common.shlexer(
76 filepath=path, wordchars='!@#$%^&*()-=+[]{}|;:,./<>?')
85 filepath=path, wordchars='!@#$%^&*()-=+[]{}|;:,./<>?'
86 )
77 87 cmd = lex.get_token()
78 88 while cmd:
79 89 if cmd == 'include':
@@ -93,8 +103,10 b' class filemapper(object):'
93 103 elif cmd == 'source':
94 104 errs += self.parse(normalize(lex.get_token()))
95 105 else:
96 self.ui.warn(_('%s:%d: unknown directive %r\n') %
97 (lex.infile, lex.lineno, pycompat.bytestr(cmd)))
106 self.ui.warn(
107 _('%s:%d: unknown directive %r\n')
108 % (lex.infile, lex.lineno, pycompat.bytestr(cmd))
109 )
98 110 errs += 1
99 111 cmd = lex.get_token()
100 112 return errs
@@ -157,6 +169,7 b' class filemapper(object):'
157 169 def active(self):
158 170 return bool(self.include or self.exclude or self.rename)
159 171
172
160 173 # This class does two additional things compared to a regular source:
161 174 #
162 175 # - Filter and rename files. This is mostly wrapped by the filemapper
@@ -171,6 +184,7 b' class filemapper(object):'
171 184 # touch files we're interested in, but also merges that merge two
172 185 # or more interesting revisions.
173 186
187
174 188 class filemap_source(common.converter_source):
175 189 def __init__(self, ui, baseconverter, filemap):
176 190 super(filemap_source, self).__init__(ui, baseconverter.repotype)
@@ -189,8 +203,9 b' class filemap_source(common.converter_so'
189 203 self.children = {}
190 204 self.seenchildren = {}
191 205 # experimental config: convert.ignoreancestorcheck
192 self.ignoreancestorcheck = self.ui.configbool('convert',
193 'ignoreancestorcheck')
206 self.ignoreancestorcheck = self.ui.configbool(
207 'convert', 'ignoreancestorcheck'
208 )
194 209
195 210 def before(self):
196 211 self.base.before()
@@ -348,8 +363,7 b' class filemap_source(common.converter_so'
348 363 if p in self.wantedancestors:
349 364 wrev.update(self.wantedancestors[p])
350 365 else:
351 self.ui.warn(_('warning: %s parent %s is missing\n') %
352 (rev, p))
366 self.ui.warn(_('warning: %s parent %s is missing\n') % (rev, p))
353 367 wrev.add(rev)
354 368 self.wantedancestors[rev] = wrev
355 369
@@ -382,10 +396,13 b' class filemap_source(common.converter_so'
382 396 if mp1 == SKIPREV or mp1 in knownparents:
383 397 continue
384 398
385 isancestor = (not self.ignoreancestorcheck and
386 any(p2 for p2 in parents
387 if p1 != p2 and mp1 != self.parentmap[p2]
388 and mp1 in self.wantedancestors[p2]))
399 isancestor = not self.ignoreancestorcheck and any(
400 p2
401 for p2 in parents
402 if p1 != p2
403 and mp1 != self.parentmap[p2]
404 and mp1 in self.wantedancestors[p2]
405 )
389 406 if not isancestor and not hasbranchparent and len(parents) > 1:
390 407 # This could be expensive, avoid unnecessary calls.
391 408 if self._cachedcommit(p1).branch == branch:
@@ -16,9 +16,8 b' from mercurial import ('
16 16 pycompat,
17 17 )
18 18
19 from . import (
20 common,
21 )
19 from . import common
20
22 21
23 22 class submodule(object):
24 23 def __init__(self, path, node, url):
@@ -32,6 +31,7 b' class submodule(object):'
32 31 def hgsubstate(self):
33 32 return "%s %s" % (self.node, self.path)
34 33
34
35 35 # Keys in extra fields that should not be copied if the user requests.
36 36 bannedextrakeys = {
37 37 # Git commit object built-ins.
@@ -44,6 +44,7 b' bannedextrakeys = {'
44 44 'close',
45 45 }
46 46
47
47 48 class convert_git(common.converter_source, common.commandline):
48 49 # Windows does not support GIT_DIR= construct while other systems
49 50 # cannot remove environment variable. Just assume none have
@@ -78,8 +79,9 b' class convert_git(common.converter_sourc'
78 79 if os.path.isdir(path + "/.git"):
79 80 path += "/.git"
80 81 if not os.path.exists(path + "/objects"):
81 raise common.NoRepo(_("%s does not look like a Git repository") %
82 path)
82 raise common.NoRepo(
83 _("%s does not look like a Git repository") % path
84 )
83 85
84 86 # The default value (50) is based on the default for 'git diff'.
85 87 similarity = ui.configint('convert', 'git.similarity')
@@ -106,8 +108,10 b' class convert_git(common.converter_sourc'
106 108 self.copyextrakeys = self.ui.configlist('convert', 'git.extrakeys')
107 109 banned = set(self.copyextrakeys) & bannedextrakeys
108 110 if banned:
109 raise error.Abort(_('copying of extra key is forbidden: %s') %
110 _(', ').join(sorted(banned)))
111 raise error.Abort(
112 _('copying of extra key is forbidden: %s')
113 % _(', ').join(sorted(banned))
114 )
111 115
112 116 committeractions = self.ui.configlist('convert', 'git.committeractions')
113 117
@@ -126,19 +130,31 b' class convert_git(common.converter_sourc'
126 130 messagealways = v or 'committer:'
127 131
128 132 if messagedifferent and messagealways:
129 raise error.Abort(_('committeractions cannot define both '
130 'messagedifferent and messagealways'))
133 raise error.Abort(
134 _(
135 'committeractions cannot define both '
136 'messagedifferent and messagealways'
137 )
138 )
131 139
132 140 dropcommitter = 'dropcommitter' in committeractions
133 141 replaceauthor = 'replaceauthor' in committeractions
134 142
135 143 if dropcommitter and replaceauthor:
136 raise error.Abort(_('committeractions cannot define both '
137 'dropcommitter and replaceauthor'))
144 raise error.Abort(
145 _(
146 'committeractions cannot define both '
147 'dropcommitter and replaceauthor'
148 )
149 )
138 150
139 151 if dropcommitter and messagealways:
140 raise error.Abort(_('committeractions cannot define both '
141 'dropcommitter and messagealways'))
152 raise error.Abort(
153 _(
154 'committeractions cannot define both '
155 'dropcommitter and messagealways'
156 )
157 )
142 158
143 159 if not messagedifferent and not messagealways:
144 160 messagedifferent = 'committer:'
@@ -176,13 +192,16 b' class convert_git(common.converter_sourc'
176 192 self.catfilepipe[0].flush()
177 193 info = self.catfilepipe[1].readline().split()
178 194 if info[1] != ftype:
179 raise error.Abort(_('cannot read %r object at %s') % (
180 pycompat.bytestr(ftype), rev))
195 raise error.Abort(
196 _('cannot read %r object at %s')
197 % (pycompat.bytestr(ftype), rev)
198 )
181 199 size = int(info[2])
182 200 data = self.catfilepipe[1].read(size)
183 201 if len(data) < size:
184 raise error.Abort(_('cannot read %r object at %s: unexpected size')
185 % (ftype, rev))
202 raise error.Abort(
203 _('cannot read %r object at %s: unexpected size') % (ftype, rev)
204 )
186 205 # read the trailing newline
187 206 self.catfilepipe[1].read(1)
188 207 return data
@@ -216,8 +235,10 b' class convert_git(common.converter_sourc'
216 235 self.submodules = []
217 236 c = config.config()
218 237 # Each item in .gitmodules starts with whitespace that cant be parsed
219 c.parse('.gitmodules', '\n'.join(line.strip() for line in
220 content.split('\n')))
238 c.parse(
239 '.gitmodules',
240 '\n'.join(line.strip() for line in content.split('\n')),
241 )
221 242 for sec in c.sections():
222 243 s = c[sec]
223 244 if 'url' in s and 'path' in s:
@@ -228,15 +249,18 b' class convert_git(common.converter_sourc'
228 249 if ret:
229 250 # This can happen if a file is in the repo that has permissions
230 251 # 160000, but there is no .gitmodules file.
231 self.ui.warn(_("warning: cannot read submodules config file in "
232 "%s\n") % version)
252 self.ui.warn(
253 _("warning: cannot read submodules config file in " "%s\n")
254 % version
255 )
233 256 return
234 257
235 258 try:
236 259 self.parsegitmodules(modules)
237 260 except error.ParseError:
238 self.ui.warn(_("warning: unable to parse .gitmodules in %s\n")
239 % version)
261 self.ui.warn(
262 _("warning: unable to parse .gitmodules in %s\n") % version
263 )
240 264 return
241 265
242 266 for m in self.submodules:
@@ -249,7 +273,9 b' class convert_git(common.converter_sourc'
249 273 if full:
250 274 raise error.Abort(_("convert from git does not support --full"))
251 275 self.modecache = {}
252 cmd = ['diff-tree','-z', '--root', '-m', '-r'] + self.simopt + [version]
276 cmd = (
277 ['diff-tree', '-z', '--root', '-m', '-r'] + self.simopt + [version]
278 )
253 279 output, status = self.gitrun(*cmd)
254 280 if status:
255 281 raise error.Abort(_('cannot read changes in %s') % version)
@@ -264,12 +290,13 b' class convert_git(common.converter_sourc'
264 290 i = 0
265 291
266 292 skipsubmodules = self.ui.configbool('convert', 'git.skipsubmodules')
293
267 294 def add(entry, f, isdest):
268 295 seen.add(f)
269 296 h = entry[3]
270 p = (entry[1] == "100755")
271 s = (entry[1] == "120000")
272 renamesource = (not isdest and entry[4][0] == 'R')
297 p = entry[1] == "100755"
298 s = entry[1] == "120000"
299 renamesource = not isdest and entry[4][0] == 'R'
273 300
274 301 if f == '.gitmodules':
275 302 if skipsubmodules:
@@ -377,18 +404,23 b' class convert_git(common.converter_sourc'
377 404 date = tm + " " + (b"%d" % tz)
378 405 saverev = self.ui.configbool('convert', 'git.saverev')
379 406
380 c = common.commit(parents=parents, date=date, author=author,
407 c = common.commit(
408 parents=parents,
409 date=date,
410 author=author,
381 411 desc=message,
382 412 rev=version,
383 413 extra=extra,
384 saverev=saverev)
414 saverev=saverev,
415 )
385 416 return c
386 417
387 418 def numcommits(self):
388 419 output, ret = self.gitrunlines('rev-list', '--all')
389 420 if ret:
390 raise error.Abort(_('cannot retrieve number of commits in %s')
391 % self.path)
421 raise error.Abort(
422 _('cannot retrieve number of commits in %s') % self.path
423 )
392 424 return len(output)
393 425
394 426 def gettags(self):
@@ -425,8 +457,9 b' class convert_git(common.converter_sourc'
425 457 def getchangedfiles(self, version, i):
426 458 changes = []
427 459 if i is None:
428 output, status = self.gitrunlines('diff-tree', '--root', '-m',
429 '-r', version)
460 output, status = self.gitrunlines(
461 'diff-tree', '--root', '-m', '-r', version
462 )
430 463 if status:
431 464 raise error.Abort(_('cannot read changes in %s') % version)
432 465 for l in output:
@@ -435,9 +468,15 b' class convert_git(common.converter_sourc'
435 468 m, f = l[:-1].split("\t")
436 469 changes.append(f)
437 470 else:
438 output, status = self.gitrunlines('diff-tree', '--name-only',
439 '--root', '-r', version,
440 '%s^%d' % (version, i + 1), '--')
471 output, status = self.gitrunlines(
472 'diff-tree',
473 '--name-only',
474 '--root',
475 '-r',
476 version,
477 '%s^%d' % (version, i + 1),
478 '--',
479 )
441 480 if status:
442 481 raise error.Abort(_('cannot read changes in %s') % version)
443 482 changes = [f.rstrip('\n') for f in output]
@@ -452,7 +491,7 b' class convert_git(common.converter_sourc'
452 491 reftypes = [
453 492 # (git prefix, hg prefix)
454 493 ('refs/remotes/origin/', remoteprefix + '/'),
455 ('refs/heads/', '')
494 ('refs/heads/', ''),
456 495 ]
457 496
458 497 exclude = {
@@ -26,8 +26,8 b' from mercurial.utils import ('
26 26 )
27 27 from . import common
28 28
29
29 30 class gnuarch_source(common.converter_source, common.commandline):
30
31 31 class gnuarch_rev(object):
32 32 def __init__(self, rev):
33 33 self.rev = rev
@@ -45,8 +45,9 b' class gnuarch_source(common.converter_so'
45 45 super(gnuarch_source, self).__init__(ui, repotype, path, revs=revs)
46 46
47 47 if not os.path.exists(os.path.join(path, '{arch}')):
48 raise common.NoRepo(_("%s does not look like a GNU Arch repository")
49 % path)
48 raise common.NoRepo(
49 _("%s does not look like a GNU Arch repository") % path
50 )
50 51
51 52 # Could use checktool, but we want to check for baz or tla.
52 53 self.execmd = None
@@ -74,8 +75,9 b' class gnuarch_source(common.converter_so'
74 75
75 76 def before(self):
76 77 # Get registered archives
77 self.archives = [i.rstrip('\n')
78 for i in self.runlines0('archives', '-n')]
78 self.archives = [
79 i.rstrip('\n') for i in self.runlines0('archives', '-n')
80 ]
79 81
80 82 if self.execmd == 'tla':
81 83 output = self.run0('tree-version', self.path)
@@ -85,8 +87,9 b' class gnuarch_source(common.converter_so'
85 87
86 88 # Get name of temporary directory
87 89 version = self.treeversion.split('/')
88 self.tmppath = os.path.join(pycompat.fsencode(tempfile.gettempdir()),
89 'hg-%s' % version[1])
90 self.tmppath = os.path.join(
91 pycompat.fsencode(tempfile.gettempdir()), 'hg-%s' % version[1]
92 )
90 93
91 94 # Generate parents dictionary
92 95 self.parents[None] = []
@@ -97,14 +100,20 b' class gnuarch_source(common.converter_so'
97 100
98 101 archive = treeversion.split('/')[0]
99 102 if archive not in self.archives:
100 self.ui.status(_('tree analysis stopped because it points to '
101 'an unregistered archive %s...\n') % archive)
103 self.ui.status(
104 _(
105 'tree analysis stopped because it points to '
106 'an unregistered archive %s...\n'
107 )
108 % archive
109 )
102 110 break
103 111
104 112 # Get the complete list of revisions for that tree version
105 113 output, status = self.runlines('revisions', '-r', '-f', treeversion)
106 self.checkexit(status, 'failed retrieving revisions for %s'
107 % treeversion)
114 self.checkexit(
115 status, 'failed retrieving revisions for %s' % treeversion
116 )
108 117
109 118 # No new iteration unless a revision has a continuation-of header
110 119 treeversion = None
@@ -132,7 +141,8 b' class gnuarch_source(common.converter_so'
132 141 # by the continuation-of header.
133 142 if self.changes[rev].continuationof:
134 143 treeversion = '--'.join(
135 self.changes[rev].continuationof.split('--')[:-1])
144 self.changes[rev].continuationof.split('--')[:-1]
145 )
136 146 break
137 147
138 148 # If we reached a base-0 revision w/o any continuation-of
@@ -189,9 +199,13 b' class gnuarch_source(common.converter_so'
189 199
190 200 def getcommit(self, rev):
191 201 changes = self.changes[rev]
192 return common.commit(author=changes.author, date=changes.date,
193 desc=changes.summary, parents=self.parents[rev],
194 rev=rev)
202 return common.commit(
203 author=changes.author,
204 date=changes.date,
205 desc=changes.summary,
206 parents=self.parents[rev],
207 rev=rev,
208 )
195 209
196 210 def gettags(self):
197 211 return self.tags
@@ -207,8 +221,7 b' class gnuarch_source(common.converter_so'
207 221
208 222 def _update(self, rev):
209 223 self.ui.debug('applying revision %s...\n' % rev)
210 changeset, status = self.runlines('replay', '-d', self.tmppath,
211 rev)
224 changeset, status = self.runlines('replay', '-d', self.tmppath, rev)
212 225 if status:
213 226 # Something went wrong while merging (baz or tla
214 227 # issue?), get latest revision and try from there
@@ -216,8 +229,9 b' class gnuarch_source(common.converter_so'
216 229 self._obtainrevision(rev)
217 230 else:
218 231 old_rev = self.parents[rev][0]
219 self.ui.debug('computing changeset between %s and %s...\n'
220 % (old_rev, rev))
232 self.ui.debug(
233 'computing changeset between %s and %s...\n' % (old_rev, rev)
234 )
221 235 self._parsechangeset(changeset, rev)
222 236
223 237 def _getfile(self, name, rev):
@@ -286,21 +300,23 b' class gnuarch_source(common.converter_so'
286 300
287 301 # Commit date
288 302 self.changes[rev].date = dateutil.datestr(
289 dateutil.strdate(catlog['Standard-date'],
290 '%Y-%m-%d %H:%M:%S'))
303 dateutil.strdate(catlog['Standard-date'], '%Y-%m-%d %H:%M:%S')
304 )
291 305
292 306 # Commit author
293 307 self.changes[rev].author = self.recode(catlog['Creator'])
294 308
295 309 # Commit description
296 self.changes[rev].summary = '\n\n'.join((catlog['Summary'],
297 catlog.get_payload()))
310 self.changes[rev].summary = '\n\n'.join(
311 (catlog['Summary'], catlog.get_payload())
312 )
298 313 self.changes[rev].summary = self.recode(self.changes[rev].summary)
299 314
300 315 # Commit revision origin when dealing with a branch or tag
301 316 if 'Continuation-of' in catlog:
302 317 self.changes[rev].continuationof = self.recode(
303 catlog['Continuation-of'])
318 catlog['Continuation-of']
319 )
304 320 except Exception:
305 321 raise error.Abort(_('could not parse cat-log of %s') % rev)
306 322
@@ -37,14 +37,17 b' from mercurial import ('
37 37 util,
38 38 )
39 39 from mercurial.utils import dateutil
40
40 41 stringio = util.stringio
41 42
42 43 from . import common
44
43 45 mapfile = common.mapfile
44 46 NoRepo = common.NoRepo
45 47
46 48 sha1re = re.compile(br'\b[0-9a-f]{12,40}\b')
47 49
50
48 51 class mercurial_sink(common.converter_sink):
49 52 def __init__(self, ui, repotype, path):
50 53 common.converter_sink.__init__(self, ui, repotype, path)
@@ -56,8 +59,9 b' class mercurial_sink(common.converter_si'
56 59 try:
57 60 self.repo = hg.repository(self.ui, path)
58 61 if not self.repo.local():
59 raise NoRepo(_('%s is not a local Mercurial repository')
60 % path)
62 raise NoRepo(
63 _('%s is not a local Mercurial repository') % path
64 )
61 65 except error.RepoError as err:
62 66 ui.traceback()
63 67 raise NoRepo(err.args[0])
@@ -66,13 +70,15 b' class mercurial_sink(common.converter_si'
66 70 ui.status(_('initializing destination %s repository\n') % path)
67 71 self.repo = hg.repository(self.ui, path, create=True)
68 72 if not self.repo.local():
69 raise NoRepo(_('%s is not a local Mercurial repository')
70 % path)
73 raise NoRepo(
74 _('%s is not a local Mercurial repository') % path
75 )
71 76 self.created.append(path)
72 77 except error.RepoError:
73 78 ui.traceback()
74 raise NoRepo(_("could not create hg repository %s as sink")
75 % path)
79 raise NoRepo(
80 _("could not create hg repository %s as sink") % path
81 )
76 82 self.lock = None
77 83 self.wlock = None
78 84 self.filemapmode = False
@@ -100,7 +106,7 b' class mercurial_sink(common.converter_si'
100 106 if not self.clonebranches:
101 107 return
102 108
103 setbranch = (branch != self.lastbranch)
109 setbranch = branch != self.lastbranch
104 110 self.lastbranch = branch
105 111 if not branch:
106 112 branch = 'default'
@@ -130,8 +136,9 b' class mercurial_sink(common.converter_si'
130 136 pbranchpath = os.path.join(self.path, pbranch)
131 137 prepo = hg.peer(self.ui, {}, pbranchpath)
132 138 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
133 exchange.pull(self.repo, prepo,
134 [prepo.lookup(h) for h in heads])
139 exchange.pull(
140 self.repo, prepo, [prepo.lookup(h) for h in heads]
141 )
135 142 self.before()
136 143
137 144 def _rewritetags(self, source, revmap, data):
@@ -166,8 +173,9 b' class mercurial_sink(common.converter_si'
166 173 if revid != nodemod.nullhex:
167 174 revmap = self.subrevmaps.get(subpath)
168 175 if revmap is None:
169 revmap = mapfile(self.ui,
170 self.repo.wjoin(subpath, '.hg/shamap'))
176 revmap = mapfile(
177 self.ui, self.repo.wjoin(subpath, '.hg/shamap')
178 )
171 179 self.subrevmaps[subpath] = revmap
172 180
173 181 # It is reasonable that one or more of the subrepos don't
@@ -184,8 +192,10 b' class mercurial_sink(common.converter_si'
184 192 newid = revmap.get(revid)
185 193 if not newid:
186 194 if len(revmap) > 0:
187 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
188 (revid, subpath))
195 self.ui.warn(
196 _("%s is missing from %s/.hg/shamap\n")
197 % (revid, subpath)
198 )
189 199 else:
190 200 revid = newid
191 201
@@ -204,8 +214,15 b' class mercurial_sink(common.converter_si'
204 214 anc = [p1ctx.ancestor(p2ctx)]
205 215 # Calculate what files are coming from p2
206 216 actions, diverge, rename = mergemod.calculateupdates(
207 self.repo, p1ctx, p2ctx, anc, branchmerge=True,
208 force=True, acceptremote=False, followcopies=False)
217 self.repo,
218 p1ctx,
219 p2ctx,
220 anc,
221 branchmerge=True,
222 force=True,
223 acceptremote=False,
224 followcopies=False,
225 )
209 226
210 227 for file, (action, info, msg) in actions.iteritems():
211 228 if source.targetfilebelongstosource(file):
@@ -216,10 +233,14 b' class mercurial_sink(common.converter_si'
216 233 # If the file requires actual merging, abort. We don't have enough
217 234 # context to resolve merges correctly.
218 235 if action in ['m', 'dm', 'cd', 'dc']:
219 raise error.Abort(_("unable to convert merge commit "
236 raise error.Abort(
237 _(
238 "unable to convert merge commit "
220 239 "since target parents do not merge cleanly (file "
221 "%s, parents %s and %s)") % (file, p1ctx,
222 p2ctx))
240 "%s, parents %s and %s)"
241 )
242 % (file, p1ctx, p2ctx)
243 )
223 244 elif action == 'k':
224 245 # 'keep' means nothing changed from p1
225 246 continue
@@ -227,8 +248,9 b' class mercurial_sink(common.converter_si'
227 248 # Any other change means we want to take the p2 version
228 249 yield file
229 250
230 def putcommit(self, files, copies, parents, commit, source, revmap, full,
231 cleanp2):
251 def putcommit(
252 self, files, copies, parents, commit, source, revmap, full, cleanp2
253 ):
232 254 files = dict(files)
233 255
234 256 def getfilectx(repo, memctx, f):
@@ -251,8 +273,15 b' class mercurial_sink(common.converter_si'
251 273 data = self._rewritetags(source, revmap, data)
252 274 if f == '.hgsubstate':
253 275 data = self._rewritesubstate(source, data)
254 return context.memfilectx(self.repo, memctx, f, data, 'l' in mode,
255 'x' in mode, copies.get(f))
276 return context.memfilectx(
277 self.repo,
278 memctx,
279 f,
280 data,
281 'l' in mode,
282 'x' in mode,
283 copies.get(f),
284 )
256 285
257 286 pl = []
258 287 for p in parents:
@@ -285,8 +314,12 b' class mercurial_sink(common.converter_si'
285 314 if sourcename:
286 315 extra['convert_source'] = sourcename
287 316
288 for label in ('source', 'transplant_source', 'rebase_source',
289 'intermediate-source'):
317 for label in (
318 'source',
319 'transplant_source',
320 'rebase_source',
321 'intermediate-source',
322 ):
290 323 node = extra.get(label)
291 324
292 325 if node is None:
@@ -326,13 +359,25 b' class mercurial_sink(common.converter_si'
326 359 p2files.add(file)
327 360 fileset.add(file)
328 361
329 ctx = context.memctx(self.repo, (p1, p2), text, fileset,
330 getfilectx, commit.author, commit.date, extra)
362 ctx = context.memctx(
363 self.repo,
364 (p1, p2),
365 text,
366 fileset,
367 getfilectx,
368 commit.author,
369 commit.date,
370 extra,
371 )
331 372
332 373 # We won't know if the conversion changes the node until after the
333 374 # commit, so copy the source's phase for now.
334 self.repo.ui.setconfig('phases', 'new-commit',
335 phases.phasenames[commit.phase], 'convert')
375 self.repo.ui.setconfig(
376 'phases',
377 'new-commit',
378 phases.phasenames[commit.phase],
379 'convert',
380 )
336 381
337 382 with self.repo.transaction("convert") as tr:
338 383 if self.repo.ui.config('convert', 'hg.preserve-hash'):
@@ -347,8 +392,9 b' class mercurial_sink(common.converter_si'
347 392 if commit.rev != node:
348 393 ctx = self.repo[node]
349 394 if ctx.phase() < phases.draft:
350 phases.registernew(self.repo, tr, phases.draft,
351 [ctx.node()])
395 phases.registernew(
396 self.repo, tr, phases.draft, [ctx.node()]
397 )
352 398
353 399 text = "(octopus merge fixup)\n"
354 400 p2 = node
@@ -372,7 +418,8 b' class mercurial_sink(common.converter_si'
372 418 for h in heads:
373 419 if '.hgtags' in self.repo[h]:
374 420 oldlines.update(
375 set(self.repo[h]['.hgtags'].data().splitlines(True)))
421 set(self.repo[h]['.hgtags'].data().splitlines(True))
422 )
376 423 oldlines = sorted(list(oldlines))
377 424
378 425 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
@@ -398,15 +445,23 b' class mercurial_sink(common.converter_si'
398 445 return None, None
399 446
400 447 data = "".join(newlines)
448
401 449 def getfilectx(repo, memctx, f):
402 450 return context.memfilectx(repo, memctx, f, data, False, False, None)
403 451
404 452 self.ui.status(_("updating tags\n"))
405 453 date = "%d 0" % int(time.mktime(time.gmtime()))
406 454 extra = {'branch': self.tagsbranch}
407 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
408 [".hgtags"], getfilectx, "convert-repo", date,
409 extra)
455 ctx = context.memctx(
456 self.repo,
457 (tagparent, None),
458 "update tags",
459 [".hgtags"],
460 getfilectx,
461 "convert-repo",
462 date,
463 extra,
464 )
410 465 node = self.repo.commitctx(ctx)
411 466 return nodemod.hex(node), nodemod.hex(tagparent)
412 467
@@ -423,8 +478,10 b' class mercurial_sink(common.converter_si'
423 478 tr = self.repo.transaction('bookmark')
424 479 self.ui.status(_("updating bookmarks\n"))
425 480 destmarks = self.repo._bookmarks
426 changes = [(bookmark, nodemod.bin(updatedbookmark[bookmark]))
427 for bookmark in updatedbookmark]
481 changes = [
482 (bookmark, nodemod.bin(updatedbookmark[bookmark]))
483 for bookmark in updatedbookmark
484 ]
428 485 destmarks.applychanges(self.repo, tr, changes)
429 486 tr.close()
430 487 finally:
@@ -436,11 +493,17 b' class mercurial_sink(common.converter_si'
436 493
437 494 def hascommitforsplicemap(self, rev):
438 495 if rev not in self.repo and self.clonebranches:
439 raise error.Abort(_('revision %s not found in destination '
496 raise error.Abort(
497 _(
498 'revision %s not found in destination '
440 499 'repository (lookups with clonebranches=true '
441 'are not implemented)') % rev)
500 'are not implemented)'
501 )
502 % rev
503 )
442 504 return rev in self.repo
443 505
506
444 507 class mercurial_source(common.converter_source):
445 508 def __init__(self, ui, repotype, path, revs=None):
446 509 common.converter_source.__init__(self, ui, repotype, path, revs)
@@ -468,8 +531,9 b' class mercurial_source(common.converter_'
468 531 try:
469 532 startnode = self.repo.lookup(startnode)
470 533 except error.RepoError:
471 raise error.Abort(_('%s is not a valid start revision')
472 % startnode)
534 raise error.Abort(
535 _('%s is not a valid start revision') % startnode
536 )
473 537 startrev = self.repo.changelog.rev(startnode)
474 538 children = {startnode: 1}
475 539 for r in self.repo.changelog.descendants([startrev]):
@@ -483,8 +547,9 b' class mercurial_source(common.converter_'
483 547 self._heads = self.repo.heads()
484 548 else:
485 549 if revs or startnode is not None:
486 raise error.Abort(_('hg.revs cannot be combined with '
487 'hg.startrev or --rev'))
550 raise error.Abort(
551 _('hg.revs cannot be combined with ' 'hg.startrev or --rev')
552 )
488 553 nodes = set()
489 554 parents = set()
490 555 for r in scmutil.revrange(self.repo, [hgrevs]):
@@ -580,9 +645,9 b' class mercurial_source(common.converter_'
580 645 optparents = [p.hex() for p in ctx.parents() if p and p not in _parents]
581 646 crev = rev
582 647
583 return common.commit(author=ctx.user(),
584 date=dateutil.datestr(ctx.date(),
585 '%Y-%m-%d %H:%M:%S %1%2'),
648 return common.commit(
649 author=ctx.user(),
650 date=dateutil.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
586 651 desc=ctx.description(),
587 652 rev=crev,
588 653 parents=parents,
@@ -592,17 +657,26 b' class mercurial_source(common.converter_'
592 657 sortkey=ctx.rev(),
593 658 saverev=self.saverev,
594 659 phase=ctx.phase(),
595 ctx=ctx)
660 ctx=ctx,
661 )
596 662
597 663 def numcommits(self):
598 664 return len(self.repo)
599 665
600 666 def gettags(self):
601 667 # This will get written to .hgtags, filter non global tags out.
602 tags = [t for t in self.repo.tagslist()
603 if self.repo.tagtype(t[0]) == 'global']
604 return dict([(name, nodemod.hex(node)) for name, node in tags
605 if self.keep(node)])
668 tags = [
669 t
670 for t in self.repo.tagslist()
671 if self.repo.tagtype(t[0]) == 'global'
672 ]
673 return dict(
674 [
675 (name, nodemod.hex(node))
676 for name, node in tags
677 if self.keep(node)
678 ]
679 )
606 680
607 681 def getchangedfiles(self, rev, i):
608 682 ctx = self._changectx(rev)
@@ -19,12 +19,17 b' from mercurial.utils import dateutil'
19 19
20 20 from . import common
21 21
22
22 23 class monotone_source(common.converter_source, common.commandline):
23 24 def __init__(self, ui, repotype, path=None, revs=None):
24 25 common.converter_source.__init__(self, ui, repotype, path, revs)
25 26 if revs and len(revs) > 1:
26 raise error.Abort(_('monotone source does not support specifying '
27 'multiple revs'))
27 raise error.Abort(
28 _(
29 'monotone source does not support specifying '
30 'multiple revs'
31 )
32 )
28 33 common.commandline.__init__(self, ui, 'mtn')
29 34
30 35 self.ui = ui
@@ -32,8 +37,9 b' class monotone_source(common.converter_s'
32 37 self.automatestdio = False
33 38 self.revs = revs
34 39
35 norepo = common.NoRepo(_("%s does not look like a monotone repository")
36 % path)
40 norepo = common.NoRepo(
41 _("%s does not look like a monotone repository") % path
42 )
37 43 if not os.path.exists(os.path.join(path, '_MTN')):
38 44 # Could be a monotone repository (SQLite db file)
39 45 try:
@@ -53,22 +59,24 b' class monotone_source(common.converter_s'
53 59 lines = br'(?:.|\n)+'
54 60
55 61 self.dir_re = re.compile(space + "dir" + name)
56 self.file_re = re.compile(space + "file" + name +
57 "content" + revision)
58 self.add_file_re = re.compile(space + "add_file" + name +
59 "content" + revision)
60 self.patch_re = re.compile(space + "patch" + name +
61 "from" + revision + "to" + revision)
62 self.file_re = re.compile(space + "file" + name + "content" + revision)
63 self.add_file_re = re.compile(
64 space + "add_file" + name + "content" + revision
65 )
66 self.patch_re = re.compile(
67 space + "patch" + name + "from" + revision + "to" + revision
68 )
62 69 self.rename_re = re.compile(space + "rename" + name + "to" + name)
63 70 self.delete_re = re.compile(space + "delete" + name)
64 self.tag_re = re.compile(space + "tag" + name + "revision" +
65 revision)
66 self.cert_re = re.compile(lines + space + "name" + name +
67 "value" + value)
71 self.tag_re = re.compile(space + "tag" + name + "revision" + revision)
72 self.cert_re = re.compile(
73 lines + space + "name" + name + "value" + value
74 )
68 75
69 76 attr = space + "file" + lines + space + "attr" + space
70 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
71 space + '"true"')
77 self.attr_execute_re = re.compile(
78 attr + '"mtn:execute"' + space + '"true"'
79 )
72 80
73 81 # cached data
74 82 self.manifest_rev = None
@@ -140,13 +148,19 b' class monotone_source(common.converter_s'
140 148 try:
141 149 length = pycompat.long(lengthstr[:-1])
142 150 except TypeError:
143 raise error.Abort(_('bad mtn packet - bad packet size %s')
144 % lengthstr)
151 raise error.Abort(
152 _('bad mtn packet - bad packet size %s') % lengthstr
153 )
145 154
146 155 read = self.mtnreadfp.read(length)
147 156 if len(read) != length:
148 raise error.Abort(_("bad mtn packet - unable to read full packet "
149 "read %s of %s") % (len(read), length))
157 raise error.Abort(
158 _(
159 "bad mtn packet - unable to read full packet "
160 "read %s of %s"
161 )
162 % (len(read), length)
163 )
150 164
151 165 return (commandnbr, stream, length, read)
152 166
@@ -154,14 +168,16 b' class monotone_source(common.converter_s'
154 168 retval = []
155 169 while True:
156 170 commandnbr, stream, length, output = self.mtnstdioreadpacket()
157 self.ui.debug('mtn: read packet %s:%s:%d\n' %
158 (commandnbr, stream, length))
171 self.ui.debug(
172 'mtn: read packet %s:%s:%d\n' % (commandnbr, stream, length)
173 )
159 174
160 175 if stream == 'l':
161 176 # End of command
162 177 if output != '0':
163 raise error.Abort(_("mtn command '%s' returned %s") %
164 (command, output))
178 raise error.Abort(
179 _("mtn command '%s' returned %s") % (command, output)
180 )
165 181 break
166 182 elif stream in 'ew':
167 183 # Error, warning output
@@ -207,8 +223,12 b' class monotone_source(common.converter_s'
207 223 return name in self.dirs
208 224
209 225 def mtngetcerts(self, rev):
210 certs = {"author":"<missing>", "date":"<missing>",
211 "changelog":"<missing>", "branch":"<missing>"}
226 certs = {
227 "author": "<missing>",
228 "date": "<missing>",
229 "changelog": "<missing>",
230 "branch": "<missing>",
231 }
212 232 certlist = self.mtnrun("certs", rev)
213 233 # mtn < 0.45:
214 234 # key "test@selenic.com"
@@ -237,8 +257,9 b' class monotone_source(common.converter_s'
237 257
238 258 def getchanges(self, rev, full):
239 259 if full:
240 raise error.Abort(_("convert from monotone does not support "
241 "--full"))
260 raise error.Abort(
261 _("convert from monotone does not support " "--full")
262 )
242 263 revision = self.mtnrun("get_revision", rev).split("\n\n")
243 264 files = {}
244 265 ignoremove = {}
@@ -286,7 +307,9 b' class monotone_source(common.converter_s'
286 307 for tofile, fromfile in renamed.items():
287 308 self.ui.debug(
288 309 "copying file in renamed directory from '%s' to '%s'"
289 % (fromfile, tofile), '\n')
310 % (fromfile, tofile),
311 '\n',
312 )
290 313 files[tofile] = rev
291 314 copies[tofile] = fromfile
292 315 for fromfile in renamed.values():
@@ -318,7 +341,8 b' class monotone_source(common.converter_s'
318 341 rev=rev,
319 342 parents=self.mtnrun("parents", rev).splitlines(),
320 343 branch=certs["branch"],
321 extra=extra)
344 extra=extra,
345 )
322 346
323 347 def gettags(self):
324 348 tags = {}
@@ -339,30 +363,40 b' class monotone_source(common.converter_s'
339 363 versionstr = self.mtnrunsingle("interface_version")
340 364 version = float(versionstr)
341 365 except Exception:
342 raise error.Abort(_("unable to determine mtn automate interface "
343 "version"))
366 raise error.Abort(
367 _("unable to determine mtn automate interface " "version")
368 )
344 369
345 370 if version >= 12.0:
346 371 self.automatestdio = True
347 self.ui.debug("mtn automate version %f - using automate stdio\n" %
348 version)
372 self.ui.debug(
373 "mtn automate version %f - using automate stdio\n" % version
374 )
349 375
350 376 # launch the long-running automate stdio process
351 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
352 '-d', self.path)
377 self.mtnwritefp, self.mtnreadfp = self._run2(
378 'automate', 'stdio', '-d', self.path
379 )
353 380 # read the headers
354 381 read = self.mtnreadfp.readline()
355 382 if read != 'format-version: 2\n':
356 raise error.Abort(_('mtn automate stdio header unexpected: %s')
357 % read)
383 raise error.Abort(
384 _('mtn automate stdio header unexpected: %s') % read
385 )
358 386 while read != '\n':
359 387 read = self.mtnreadfp.readline()
360 388 if not read:
361 raise error.Abort(_("failed to reach end of mtn automate "
362 "stdio headers"))
389 raise error.Abort(
390 _(
391 "failed to reach end of mtn automate "
392 "stdio headers"
393 )
394 )
363 395 else:
364 self.ui.debug("mtn automate version %s - not using automate stdio "
365 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
396 self.ui.debug(
397 "mtn automate version %s - not using automate stdio "
398 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version
399 )
366 400
367 401 def after(self):
368 402 if self.automatestdio:
@@ -22,6 +22,7 b' from mercurial.utils import ('
22 22
23 23 from . import common
24 24
25
25 26 def loaditer(f):
26 27 "Yield the dictionary objects generated by p4"
27 28 try:
@@ -33,6 +34,7 b' def loaditer(f):'
33 34 except EOFError:
34 35 pass
35 36
37
36 38 def decodefilename(filename):
37 39 """Perforce escapes special characters @, #, *, or %
38 40 with %40, %23, %2A, or %25 respectively
@@ -47,6 +49,7 b' def decodefilename(filename):'
47 49 filename = filename.replace(k, v)
48 50 return filename
49 51
52
50 53 class p4_source(common.converter_source):
51 54 def __init__(self, ui, repotype, path, revs=None):
52 55 # avoid import cycle
@@ -55,25 +58,30 b' class p4_source(common.converter_source)'
55 58 super(p4_source, self).__init__(ui, repotype, path, revs=revs)
56 59
57 60 if "/" in path and not path.startswith('//'):
58 raise common.NoRepo(_('%s does not look like a P4 repository') %
59 path)
61 raise common.NoRepo(
62 _('%s does not look like a P4 repository') % path
63 )
60 64
61 65 common.checktool('p4', abort=False)
62 66
63 67 self.revmap = {}
64 self.encoding = self.ui.config('convert', 'p4.encoding',
65 convcmd.orig_encoding)
68 self.encoding = self.ui.config(
69 'convert', 'p4.encoding', convcmd.orig_encoding
70 )
66 71 self.re_type = re.compile(
67 72 br"([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
68 br"(\+\w+)?$")
73 br"(\+\w+)?$"
74 )
69 75 self.re_keywords = re.compile(
70 76 br"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
71 br":[^$\n]*\$")
77 br":[^$\n]*\$"
78 )
72 79 self.re_keywords_old = re.compile(br"\$(Id|Header):[^$\n]*\$")
73 80
74 81 if revs and len(revs) > 1:
75 raise error.Abort(_("p4 source does not support specifying "
76 "multiple revisions"))
82 raise error.Abort(
83 _("p4 source does not support specifying " "multiple revisions")
84 )
77 85
78 86 def setrevmap(self, revmap):
79 87 """Sets the parsed revmap dictionary.
@@ -189,7 +197,7 b' class p4_source(common.converter_source)'
189 197 if filename:
190 198 files.append((filename, d["rev%d" % i]))
191 199 depotname[filename] = oldname
192 if (d.get("action%d" % i) == "move/add"):
200 if d.get("action%d" % i) == "move/add":
193 201 copiedfiles.append(filename)
194 202 localname[oldname] = filename
195 203 i += 1
@@ -198,8 +206,7 b' class p4_source(common.converter_source)'
198 206 for filename in copiedfiles:
199 207 oldname = depotname[filename]
200 208
201 flcmd = ('p4 -G filelog %s'
202 % procutil.shellquote(oldname))
209 flcmd = 'p4 -G filelog %s' % procutil.shellquote(oldname)
203 210 flstdout = procutil.popen(flcmd, mode='rb')
204 211
205 212 copiedfilename = None
@@ -208,8 +215,10 b' class p4_source(common.converter_source)'
208 215
209 216 i = 0
210 217 while ("change%d" % i) in d:
211 if (d["change%d" % i] == change and
212 d["action%d" % i] == "move/add"):
218 if (
219 d["change%d" % i] == change
220 and d["action%d" % i] == "move/add"
221 ):
213 222 j = 0
214 223 while ("file%d,%d" % (i, j)) in d:
215 224 if d["how%d,%d" % (i, j)] == "moved from":
@@ -225,8 +234,10 b' class p4_source(common.converter_source)'
225 234 if copiedfilename:
226 235 copies[filename] = copiedfilename
227 236 else:
228 ui.warn(_("cannot find source for copied file: %s@%s\n")
229 % (filename, change))
237 ui.warn(
238 _("cannot find source for copied file: %s@%s\n")
239 % (filename, change)
240 )
230 241
231 242 changeset[change] = c
232 243 files_map[change] = files
@@ -272,8 +283,9 b' class p4_source(common.converter_source)'
272 283 return self.heads
273 284
274 285 def getfile(self, name, rev):
275 cmd = ('p4 -G print %s'
276 % procutil.shellquote("%s#%s" % (self.depotname[name], rev)))
286 cmd = 'p4 -G print %s' % procutil.shellquote(
287 "%s#%s" % (self.depotname[name], rev)
288 )
277 289
278 290 lasterror = None
279 291 while True:
@@ -304,8 +316,9 b' class p4_source(common.converter_source)'
304 316 p4type = self.re_type.match(d["type"])
305 317 if p4type:
306 318 mode = ""
307 flags = ((p4type.group(1) or "")
308 + (p4type.group(3) or ""))
319 flags = (p4type.group(1) or "") + (
320 p4type.group(3) or ""
321 )
309 322 if "x" in flags:
310 323 mode = "x"
311 324 if p4type.group(2) == "symlink":
@@ -350,10 +363,15 b' class p4_source(common.converter_source)'
350 363 if parents is None:
351 364 parents = []
352 365
353 return common.commit(author=self.recode(obj["user"]),
366 return common.commit(
367 author=self.recode(obj["user"]),
354 368 date=dateutil.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
355 parents=parents, desc=desc, branch=None, rev=obj['change'],
356 extra={"p4": obj['change'], "convert_revision": obj['change']})
369 parents=parents,
370 desc=desc,
371 branch=None,
372 rev=obj['change'],
373 extra={"p4": obj['change'], "convert_revision": obj['change']},
374 )
357 375
358 376 def _fetch_revision(self, rev):
359 377 """Return an output of `p4 describe` including author, commit date as
@@ -369,7 +387,8 b' class p4_source(common.converter_source)'
369 387 d = self._fetch_revision(rev)
370 388 return self._construct_commit(d, parents=None)
371 389 raise error.Abort(
372 _("cannot find %s in the revmap or parsed changesets") % rev)
390 _("cannot find %s in the revmap or parsed changesets") % rev
391 )
373 392
374 393 def gettags(self):
375 394 return {}
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 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