This required a couple of small tweaks to un-confuse black, but now it
works. Big formatting changes come from:
- Dramatically improved collection-splitting logic upstream
- Black having a strong (correct IMO) opinion that """ is better than '''
indygreg | |
martinvonz | |
mharbison72 |
hg-reviewers |
This required a couple of small tweaks to un-confuse black, but now it
works. Big formatting changes come from:
Automatic diff as part of commit; lint not applicable. |
Automatic diff as part of commit; unit tests not applicable. |
Amended this into setup.py, which was changed in D9427 with the older black:
@@ -816,7 +816,8 @@ class buildhgexe(build_ext): if sys.version_info[0] >= 3: fsdecode = os.fsdecode dest = os.path.join( - os.path.dirname(self.hgtarget), fsdecode(dllbasename), + os.path.dirname(self.hgtarget), + fsdecode(dllbasename), ) if not os.path.exists(dest):
"Action": "sts:AssumeRole" | "Action": "sts:AssumeRole" | ||||
} | } | ||||
] | ] | ||||
} | } | ||||
'''.strip() | '''.strip() | ||||
IAM_INSTANCE_PROFILES = { | IAM_INSTANCE_PROFILES = { | ||||
'ephemeral-ec2-1': {'roles': ['ephemeral-ec2-role-1',],} | 'ephemeral-ec2-1': { | ||||
'roles': [ | |||||
'ephemeral-ec2-role-1', | |||||
], | |||||
} | |||||
} | } | ||||
# User Data for Windows EC2 instance. Mainly used to set the password | # User Data for Windows EC2 instance. Mainly used to set the password | ||||
# and configure WinRM. | # and configure WinRM. | ||||
# Inspired by the User Data script used by Packer | # Inspired by the User Data script used by Packer | ||||
# (from https://www.packer.io/intro/getting-started/build-image.html). | # (from https://www.packer.io/intro/getting-started/build-image.html). | ||||
WINDOWS_USER_DATA = r''' | WINDOWS_USER_DATA = r''' | ||||
profile.add_role(RoleName=role) | profile.add_role(RoleName=role) | ||||
def find_image(ec2resource, owner_id, name, reverse_sort_field=None): | def find_image(ec2resource, owner_id, name, reverse_sort_field=None): | ||||
"""Find an AMI by its owner ID and name.""" | """Find an AMI by its owner ID and name.""" | ||||
images = ec2resource.images.filter( | images = ec2resource.images.filter( | ||||
Filters=[ | Filters=[ | ||||
{'Name': 'owner-id', 'Values': [owner_id],}, | { | ||||
{'Name': 'state', 'Values': ['available'],}, | 'Name': 'owner-id', | ||||
{'Name': 'image-type', 'Values': ['machine'],}, | 'Values': [owner_id], | ||||
{'Name': 'name', 'Values': [name],}, | }, | ||||
{ | |||||
'Name': 'state', | |||||
'Values': ['available'], | |||||
}, | |||||
{ | |||||
'Name': 'image-type', | |||||
'Values': ['machine'], | |||||
}, | |||||
{ | |||||
'Name': 'name', | |||||
'Values': [name], | |||||
}, | |||||
] | ] | ||||
) | ) | ||||
if reverse_sort_field: | if reverse_sort_field: | ||||
images = sorted( | images = sorted( | ||||
images, | images, | ||||
key=lambda image: getattr(image, reverse_sort_field), | key=lambda image: getattr(image, reverse_sort_field), | ||||
reverse=True, | reverse=True, | ||||
if name in existing: | if name in existing: | ||||
security_groups[name] = existing[name] | security_groups[name] = existing[name] | ||||
continue | continue | ||||
actual = '%s%s' % (prefix, name) | actual = '%s%s' % (prefix, name) | ||||
print('adding security group %s' % actual) | print('adding security group %s' % actual) | ||||
group_res = ec2resource.create_security_group( | group_res = ec2resource.create_security_group( | ||||
Description=group['description'], GroupName=actual, | Description=group['description'], | ||||
GroupName=actual, | |||||
) | ) | ||||
group_res.authorize_ingress(IpPermissions=group['ingress'],) | group_res.authorize_ingress( | ||||
IpPermissions=group['ingress'], | |||||
) | |||||
security_groups[name] = group_res | security_groups[name] = group_res | ||||
return security_groups | return security_groups | ||||
def terminate_ec2_instances(ec2resource, prefix='hg-'): | def terminate_ec2_instances(ec2resource, prefix='hg-'): | ||||
"""Terminate all EC2 instances managed by us.""" | """Terminate all EC2 instances managed by us.""" | ||||
snapshot.delete() | snapshot.delete() | ||||
def wait_for_ssm(ssmclient, instances): | def wait_for_ssm(ssmclient, instances): | ||||
"""Wait for SSM to come online for an iterable of instance IDs.""" | """Wait for SSM to come online for an iterable of instance IDs.""" | ||||
while True: | while True: | ||||
res = ssmclient.describe_instance_information( | res = ssmclient.describe_instance_information( | ||||
Filters=[ | Filters=[ | ||||
{'Key': 'InstanceIds', 'Values': [i.id for i in instances],}, | { | ||||
'Key': 'InstanceIds', | |||||
'Values': [i.id for i in instances], | |||||
}, | |||||
], | ], | ||||
) | ) | ||||
available = len(res['InstanceInformationList']) | available = len(res['InstanceInformationList']) | ||||
wanted = len(instances) | wanted = len(instances) | ||||
print('%d/%d instances available in SSM' % (available, wanted)) | print('%d/%d instances available in SSM' % (available, wanted)) | ||||
if available == wanted: | if available == wanted: | ||||
return | return | ||||
time.sleep(2) | time.sleep(2) | ||||
def run_ssm_command(ssmclient, instances, document_name, parameters): | def run_ssm_command(ssmclient, instances, document_name, parameters): | ||||
"""Run a PowerShell script on an EC2 instance.""" | """Run a PowerShell script on an EC2 instance.""" | ||||
res = ssmclient.send_command( | res = ssmclient.send_command( | ||||
InstanceIds=[i.id for i in instances], | InstanceIds=[i.id for i in instances], | ||||
DocumentName=document_name, | DocumentName=document_name, | ||||
Parameters=parameters, | Parameters=parameters, | ||||
CloudWatchOutputConfig={'CloudWatchOutputEnabled': True,}, | CloudWatchOutputConfig={ | ||||
'CloudWatchOutputEnabled': True, | |||||
}, | |||||
) | ) | ||||
command_id = res['Command']['CommandId'] | command_id = res['Command']['CommandId'] | ||||
for instance in instances: | for instance in instances: | ||||
while True: | while True: | ||||
try: | try: | ||||
res = ssmclient.get_command_invocation( | res = ssmclient.get_command_invocation( | ||||
CommandId=command_id, InstanceId=instance.id, | CommandId=command_id, | ||||
InstanceId=instance.id, | |||||
) | ) | ||||
except botocore.exceptions.ClientError as e: | except botocore.exceptions.ClientError as e: | ||||
if e.response['Error']['Code'] == 'InvocationDoesNotExist': | if e.response['Error']['Code'] == 'InvocationDoesNotExist': | ||||
print('could not find SSM command invocation; waiting') | print('could not find SSM command invocation; waiting') | ||||
time.sleep(1) | time.sleep(1) | ||||
continue | continue | ||||
else: | else: | ||||
raise | raise | ||||
): | ): | ||||
"""Create an AMI from a running instance. | """Create an AMI from a running instance. | ||||
Returns the ``ec2resource.Image`` representing the created AMI. | Returns the ``ec2resource.Image`` representing the created AMI. | ||||
""" | """ | ||||
instance.stop() | instance.stop() | ||||
ec2client.get_waiter('instance_stopped').wait( | ec2client.get_waiter('instance_stopped').wait( | ||||
InstanceIds=[instance.id], WaiterConfig={'Delay': 5,} | InstanceIds=[instance.id], | ||||
WaiterConfig={ | |||||
'Delay': 5, | |||||
}, | |||||
) | ) | ||||
print('%s is stopped' % instance.id) | print('%s is stopped' % instance.id) | ||||
image = instance.create_image(Name=name, Description=description,) | image = instance.create_image( | ||||
Name=name, | |||||
Description=description, | |||||
) | |||||
image.create_tags( | image.create_tags( | ||||
Tags=[{'Key': 'HGIMAGEFINGERPRINT', 'Value': fingerprint,},] | Tags=[ | ||||
{ | |||||
'Key': 'HGIMAGEFINGERPRINT', | |||||
'Value': fingerprint, | |||||
}, | |||||
] | |||||
) | ) | ||||
print('waiting for image %s' % image.id) | print('waiting for image %s' % image.id) | ||||
ec2client.get_waiter('image_available').wait(ImageIds=[image.id],) | ec2client.get_waiter('image_available').wait( | ||||
ImageIds=[image.id], | |||||
) | |||||
print('image %s available as %s' % (image.id, image.name)) | print('image %s available as %s' % (image.id, image.name)) | ||||
return image | return image | ||||
def ensure_linux_dev_ami(c: AWSConnection, distro='debian10', prefix='hg-'): | def ensure_linux_dev_ami(c: AWSConnection, distro='debian10', prefix='hg-'): | ||||
"""Ensures a Linux development AMI is available and up-to-date. | """Ensures a Linux development AMI is available and up-to-date. | ||||
image = find_image( | image = find_image( | ||||
ec2resource, | ec2resource, | ||||
DEBIAN_ACCOUNT_ID, | DEBIAN_ACCOUNT_ID, | ||||
'debian-stretch-hvm-x86_64-gp2-2019-09-08-17994', | 'debian-stretch-hvm-x86_64-gp2-2019-09-08-17994', | ||||
) | ) | ||||
ssh_username = 'admin' | ssh_username = 'admin' | ||||
elif distro == 'debian10': | elif distro == 'debian10': | ||||
image = find_image( | image = find_image( | ||||
ec2resource, DEBIAN_ACCOUNT_ID_2, 'debian-10-amd64-20190909-10', | ec2resource, | ||||
DEBIAN_ACCOUNT_ID_2, | |||||
'debian-10-amd64-20190909-10', | |||||
) | ) | ||||
ssh_username = 'admin' | ssh_username = 'admin' | ||||
elif distro == 'ubuntu18.04': | elif distro == 'ubuntu18.04': | ||||
image = find_image( | image = find_image( | ||||
ec2resource, | ec2resource, | ||||
UBUNTU_ACCOUNT_ID, | UBUNTU_ACCOUNT_ID, | ||||
'ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-20190918', | 'ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-20190918', | ||||
) | ) | ||||
try: | try: | ||||
yield instances | yield instances | ||||
finally: | finally: | ||||
for instance in instances: | for instance in instances: | ||||
instance.ssh_client.close() | instance.ssh_client.close() | ||||
def ensure_windows_dev_ami( | def ensure_windows_dev_ami( | ||||
c: AWSConnection, prefix='hg-', base_image_name=WINDOWS_BASE_IMAGE_NAME, | c: AWSConnection, | ||||
prefix='hg-', | |||||
base_image_name=WINDOWS_BASE_IMAGE_NAME, | |||||
): | ): | ||||
"""Ensure Windows Development AMI is available and up-to-date. | """Ensure Windows Development AMI is available and up-to-date. | ||||
If necessary, a modern AMI will be built by starting a temporary EC2 | If necessary, a modern AMI will be built by starting a temporary EC2 | ||||
instance and bootstrapping it. | instance and bootstrapping it. | ||||
Obsolete AMIs will be deleted so there is only a single AMI having the | Obsolete AMIs will be deleted so there is only a single AMI having the | ||||
desired name. | desired name. | ||||
# trust issues that make it difficult to invoke Windows Update | # trust issues that make it difficult to invoke Windows Update | ||||
# remotely. So we use SSM, which has a mechanism for running Windows | # remotely. So we use SSM, which has a mechanism for running Windows | ||||
# Update. | # Update. | ||||
print('installing Windows features...') | print('installing Windows features...') | ||||
run_ssm_command( | run_ssm_command( | ||||
ssmclient, | ssmclient, | ||||
[instance], | [instance], | ||||
'AWS-RunPowerShellScript', | 'AWS-RunPowerShellScript', | ||||
{'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'),}, | { | ||||
'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'), | |||||
}, | |||||
) | ) | ||||
# Reboot so all updates are fully applied. | # Reboot so all updates are fully applied. | ||||
# | # | ||||
# We don't use instance.reboot() here because it is asynchronous and | # We don't use instance.reboot() here because it is asynchronous and | ||||
# we don't know when exactly the instance has rebooted. It could take | # we don't know when exactly the instance has rebooted. It could take | ||||
# a while to stop and we may start trying to interact with the instance | # a while to stop and we may start trying to interact with the instance | ||||
# before it has rebooted. | # before it has rebooted. | ||||
print('rebooting instance %s' % instance.id) | print('rebooting instance %s' % instance.id) | ||||
instance.stop() | instance.stop() | ||||
ec2client.get_waiter('instance_stopped').wait( | ec2client.get_waiter('instance_stopped').wait( | ||||
InstanceIds=[instance.id], WaiterConfig={'Delay': 5,} | InstanceIds=[instance.id], | ||||
WaiterConfig={ | |||||
'Delay': 5, | |||||
}, | |||||
) | ) | ||||
instance.start() | instance.start() | ||||
wait_for_ip_addresses([instance]) | wait_for_ip_addresses([instance]) | ||||
# There is a race condition here between the User Data PS script running | # There is a race condition here between the User Data PS script running | ||||
# and us connecting to WinRM. This can manifest as | # and us connecting to WinRM. This can manifest as | ||||
# "AuthorizationManager check failed" failures during run_powershell(). | # "AuthorizationManager check failed" failures during run_powershell(). |
parser = argparse.ArgumentParser() | parser = argparse.ArgumentParser() | ||||
parser.add_argument( | parser.add_argument( | ||||
'--state-path', | '--state-path', | ||||
default='~/.hgautomation', | default='~/.hgautomation', | ||||
help='Path for local state files', | help='Path for local state files', | ||||
) | ) | ||||
parser.add_argument( | parser.add_argument( | ||||
'--aws-region', help='AWS region to use', default='us-west-2', | '--aws-region', | ||||
help='AWS region to use', | |||||
default='us-west-2', | |||||
) | ) | ||||
subparsers = parser.add_subparsers() | subparsers = parser.add_subparsers() | ||||
sp = subparsers.add_parser( | sp = subparsers.add_parser( | ||||
'bootstrap-linux-dev', help='Bootstrap Linux development environments', | 'bootstrap-linux-dev', | ||||
help='Bootstrap Linux development environments', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--distros', help='Comma delimited list of distros to bootstrap', | '--distros', | ||||
help='Comma delimited list of distros to bootstrap', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--parallel', | '--parallel', | ||||
action='store_true', | action='store_true', | ||||
help='Generate AMIs in parallel (not CTRL-c safe)', | help='Generate AMIs in parallel (not CTRL-c safe)', | ||||
) | ) | ||||
sp.set_defaults(func=bootstrap_linux_dev) | sp.set_defaults(func=bootstrap_linux_dev) | ||||
sp = subparsers.add_parser( | sp = subparsers.add_parser( | ||||
'bootstrap-windows-dev', | 'bootstrap-windows-dev', | ||||
help='Bootstrap the Windows development environment', | help='Bootstrap the Windows development environment', | ||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--base-image-name', | '--base-image-name', | ||||
help='AMI name of base image', | help='AMI name of base image', | ||||
default=aws.WINDOWS_BASE_IMAGE_NAME, | default=aws.WINDOWS_BASE_IMAGE_NAME, | ||||
) | ) | ||||
sp.set_defaults(func=bootstrap_windows_dev) | sp.set_defaults(func=bootstrap_windows_dev) | ||||
sp = subparsers.add_parser( | sp = subparsers.add_parser( | ||||
'build-all-windows-packages', help='Build all Windows packages', | 'build-all-windows-packages', | ||||
help='Build all Windows packages', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--revision', help='Mercurial revision to build', default='.', | '--revision', | ||||
help='Mercurial revision to build', | |||||
default='.', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--version', help='Mercurial version string to use', | '--version', | ||||
help='Mercurial version string to use', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--base-image-name', | '--base-image-name', | ||||
help='AMI name of base image', | help='AMI name of base image', | ||||
default=aws.WINDOWS_BASE_IMAGE_NAME, | default=aws.WINDOWS_BASE_IMAGE_NAME, | ||||
) | ) | ||||
sp.set_defaults(func=build_all_windows_packages) | sp.set_defaults(func=build_all_windows_packages) | ||||
sp = subparsers.add_parser( | sp = subparsers.add_parser( | ||||
'build-inno', help='Build Inno Setup installer(s)', | 'build-inno', | ||||
help='Build Inno Setup installer(s)', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--python-version', | '--python-version', | ||||
help='Which version of Python to target', | help='Which version of Python to target', | ||||
choices={2, 3}, | choices={2, 3}, | ||||
type=int, | type=int, | ||||
nargs='*', | nargs='*', | ||||
default=[3], | default=[3], | ||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--arch', | '--arch', | ||||
help='Architecture to build for', | help='Architecture to build for', | ||||
choices={'x86', 'x64'}, | choices={'x86', 'x64'}, | ||||
nargs='*', | nargs='*', | ||||
default=['x64'], | default=['x64'], | ||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--revision', help='Mercurial revision to build', default='.', | '--revision', | ||||
help='Mercurial revision to build', | |||||
default='.', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--version', help='Mercurial version string to use in installer', | '--version', | ||||
help='Mercurial version string to use in installer', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--base-image-name', | '--base-image-name', | ||||
help='AMI name of base image', | help='AMI name of base image', | ||||
default=aws.WINDOWS_BASE_IMAGE_NAME, | default=aws.WINDOWS_BASE_IMAGE_NAME, | ||||
) | ) | ||||
sp.set_defaults(func=build_inno) | sp.set_defaults(func=build_inno) | ||||
sp = subparsers.add_parser( | sp = subparsers.add_parser( | ||||
'build-windows-wheel', help='Build Windows wheel(s)', | 'build-windows-wheel', | ||||
help='Build Windows wheel(s)', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--python-version', | '--python-version', | ||||
help='Python version to build for', | help='Python version to build for', | ||||
choices={'2.7', '3.7', '3.8', '3.9'}, | choices={'2.7', '3.7', '3.8', '3.9'}, | ||||
nargs='*', | nargs='*', | ||||
default=['3.8'], | default=['3.8'], | ||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--arch', | '--arch', | ||||
help='Architecture to build for', | help='Architecture to build for', | ||||
choices={'x86', 'x64'}, | choices={'x86', 'x64'}, | ||||
nargs='*', | nargs='*', | ||||
default=['x64'], | default=['x64'], | ||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--revision', help='Mercurial revision to build', default='.', | '--revision', | ||||
help='Mercurial revision to build', | |||||
default='.', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--base-image-name', | '--base-image-name', | ||||
help='AMI name of base image', | help='AMI name of base image', | ||||
default=aws.WINDOWS_BASE_IMAGE_NAME, | default=aws.WINDOWS_BASE_IMAGE_NAME, | ||||
) | ) | ||||
sp.set_defaults(func=build_windows_wheel) | sp.set_defaults(func=build_windows_wheel) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--arch', | '--arch', | ||||
help='Architecture to build for', | help='Architecture to build for', | ||||
choices={'x86', 'x64'}, | choices={'x86', 'x64'}, | ||||
nargs='*', | nargs='*', | ||||
default=['x64'], | default=['x64'], | ||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--revision', help='Mercurial revision to build', default='.', | '--revision', | ||||
help='Mercurial revision to build', | |||||
default='.', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--version', help='Mercurial version string to use in installer', | '--version', | ||||
help='Mercurial version string to use in installer', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--base-image-name', | '--base-image-name', | ||||
help='AMI name of base image', | help='AMI name of base image', | ||||
default=aws.WINDOWS_BASE_IMAGE_NAME, | default=aws.WINDOWS_BASE_IMAGE_NAME, | ||||
) | ) | ||||
sp.set_defaults(func=build_wix) | sp.set_defaults(func=build_wix) | ||||
sp = subparsers.add_parser( | sp = subparsers.add_parser( | ||||
'terminate-ec2-instances', | 'terminate-ec2-instances', | ||||
help='Terminate all active EC2 instances managed by us', | help='Terminate all active EC2 instances managed by us', | ||||
) | ) | ||||
sp.set_defaults(func=terminate_ec2_instances) | sp.set_defaults(func=terminate_ec2_instances) | ||||
sp = subparsers.add_parser( | sp = subparsers.add_parser( | ||||
'purge-ec2-resources', help='Purge all EC2 resources managed by us', | 'purge-ec2-resources', | ||||
help='Purge all EC2 resources managed by us', | |||||
) | ) | ||||
sp.set_defaults(func=purge_ec2_resources) | sp.set_defaults(func=purge_ec2_resources) | ||||
sp = subparsers.add_parser('run-tests-linux', help='Run tests on Linux',) | sp = subparsers.add_parser( | ||||
'run-tests-linux', | |||||
help='Run tests on Linux', | |||||
) | |||||
sp.add_argument( | sp.add_argument( | ||||
'--distro', | '--distro', | ||||
help='Linux distribution to run tests on', | help='Linux distribution to run tests on', | ||||
choices=linux.DISTROS, | choices=linux.DISTROS, | ||||
default='debian10', | default='debian10', | ||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--filesystem', | '--filesystem', | ||||
sp.add_argument( | sp.add_argument( | ||||
'test_flags', | 'test_flags', | ||||
help='Extra command line flags to pass to run-tests.py', | help='Extra command line flags to pass to run-tests.py', | ||||
nargs='*', | nargs='*', | ||||
) | ) | ||||
sp.set_defaults(func=run_tests_linux) | sp.set_defaults(func=run_tests_linux) | ||||
sp = subparsers.add_parser( | sp = subparsers.add_parser( | ||||
'run-tests-windows', help='Run tests on Windows', | 'run-tests-windows', | ||||
help='Run tests on Windows', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--instance-type', help='EC2 instance type to use', default='t3.medium', | '--instance-type', | ||||
help='EC2 instance type to use', | |||||
default='t3.medium', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--python-version', | '--python-version', | ||||
help='Python version to use', | help='Python version to use', | ||||
choices={'2.7', '3.5', '3.6', '3.7', '3.8', '3.9'}, | choices={'2.7', '3.5', '3.6', '3.7', '3.8', '3.9'}, | ||||
default='2.7', | default='2.7', | ||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--arch', | '--arch', | ||||
help='Architecture to test', | help='Architecture to test', | ||||
choices={'x86', 'x64'}, | choices={'x86', 'x64'}, | ||||
default='x64', | default='x64', | ||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--test-flags', help='Extra command line flags to pass to run-tests.py', | '--test-flags', | ||||
help='Extra command line flags to pass to run-tests.py', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--base-image-name', | '--base-image-name', | ||||
help='AMI name of base image', | help='AMI name of base image', | ||||
default=aws.WINDOWS_BASE_IMAGE_NAME, | default=aws.WINDOWS_BASE_IMAGE_NAME, | ||||
) | ) | ||||
sp.set_defaults(func=run_tests_windows) | sp.set_defaults(func=run_tests_windows) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--no-mercurial-scm-org', | '--no-mercurial-scm-org', | ||||
dest='mercurial_scm_org', | dest='mercurial_scm_org', | ||||
action='store_false', | action='store_false', | ||||
default=True, | default=True, | ||||
help='Skip uploading to www.mercurial-scm.org', | help='Skip uploading to www.mercurial-scm.org', | ||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'--ssh-username', help='SSH username for mercurial-scm.org', | '--ssh-username', | ||||
help='SSH username for mercurial-scm.org', | |||||
) | ) | ||||
sp.add_argument( | sp.add_argument( | ||||
'version', help='Mercurial version string to locate local packages', | 'version', | ||||
help='Mercurial version string to locate local packages', | |||||
) | ) | ||||
sp.set_defaults(func=publish_windows_artifacts) | sp.set_defaults(func=publish_windows_artifacts) | ||||
sp = subparsers.add_parser( | sp = subparsers.add_parser( | ||||
'try', help='Run CI automation against a custom changeset' | 'try', help='Run CI automation against a custom changeset' | ||||
) | ) | ||||
sp.add_argument('-r', '--rev', default='.', help='Revision to run CI on') | sp.add_argument('-r', '--rev', default='.', help='Revision to run CI on') | ||||
sp.set_defaults(func=run_try) | sp.set_defaults(func=run_try) |
if arch == "x86": | if arch == "x86": | ||||
target_triple = "i686-pc-windows-msvc" | target_triple = "i686-pc-windows-msvc" | ||||
elif arch == "x64": | elif arch == "x64": | ||||
target_triple = "x86_64-pc-windows-msvc" | target_triple = "x86_64-pc-windows-msvc" | ||||
else: | else: | ||||
raise Exception("unhandled arch: %s" % arch) | raise Exception("unhandled arch: %s" % arch) | ||||
ps = BUILD_INNO_PYTHON3.format( | ps = BUILD_INNO_PYTHON3.format( | ||||
pyoxidizer_target=target_triple, version=version, | pyoxidizer_target=target_triple, | ||||
version=version, | |||||
) | ) | ||||
else: | else: | ||||
extra_args = [] | extra_args = [] | ||||
if version: | if version: | ||||
extra_args.extend(['--version', version]) | extra_args.extend(['--version', version]) | ||||
ps = get_vc_prefix(arch) + BUILD_INNO_PYTHON2.format( | ps = get_vc_prefix(arch) + BUILD_INNO_PYTHON2.format( | ||||
arch=arch, extra_args=' '.join(extra_args) | arch=arch, extra_args=' '.join(extra_args) | ||||
if arch == "x86": | if arch == "x86": | ||||
target_triple = "i686-pc-windows-msvc" | target_triple = "i686-pc-windows-msvc" | ||||
elif arch == "x64": | elif arch == "x64": | ||||
target_triple = "x86_64-pc-windows-msvc" | target_triple = "x86_64-pc-windows-msvc" | ||||
else: | else: | ||||
raise Exception("unhandled arch: %s" % arch) | raise Exception("unhandled arch: %s" % arch) | ||||
ps = BUILD_WIX_PYTHON3.format( | ps = BUILD_WIX_PYTHON3.format( | ||||
pyoxidizer_target=target_triple, version=version, | pyoxidizer_target=target_triple, | ||||
version=version, | |||||
) | ) | ||||
else: | else: | ||||
extra_args = [] | extra_args = [] | ||||
if version: | if version: | ||||
extra_args.extend(['--version', version]) | extra_args.extend(['--version', version]) | ||||
ps = get_vc_prefix(arch) + BUILD_WIX_PYTHON2.format( | ps = get_vc_prefix(arch) + BUILD_WIX_PYTHON2.format( | ||||
arch=arch, extra_args=' '.join(extra_args) | arch=arch, extra_args=' '.join(extra_args) | ||||
r'python_version must be \d.\d; got %s' % python_version | r'python_version must be \d.\d; got %s' % python_version | ||||
) | ) | ||||
if arch not in ('x86', 'x64'): | if arch not in ('x86', 'x64'): | ||||
raise ValueError('arch must be x86 or x64; got %s' % arch) | raise ValueError('arch must be x86 or x64; got %s' % arch) | ||||
python_path = 'python%s-%s' % (python_version.replace('.', ''), arch) | python_path = 'python%s-%s' % (python_version.replace('.', ''), arch) | ||||
ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',) | ps = RUN_TESTS.format( | ||||
python_path=python_path, | |||||
test_flags=test_flags or '', | |||||
) | |||||
run_powershell(winrm_client, ps) | run_powershell(winrm_client, ps) | ||||
def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str): | def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str): | ||||
return ( | return ( | ||||
dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version), | dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version), | ||||
dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version), | dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version), |
coldelta += 1 | coldelta += 1 | ||||
continue | continue | ||||
# This looks like a function call. | # This looks like a function call. | ||||
if t.type == token.NAME and _isop(i + 1, '('): | if t.type == token.NAME and _isop(i + 1, '('): | ||||
fn = t.string | fn = t.string | ||||
# *attr() builtins don't accept byte strings to 2nd argument. | # *attr() builtins don't accept byte strings to 2nd argument. | ||||
if fn in ( | if ( | ||||
fn | |||||
in ( | |||||
'getattr', | 'getattr', | ||||
'setattr', | 'setattr', | ||||
'hasattr', | 'hasattr', | ||||
'safehasattr', | 'safehasattr', | ||||
'wrapfunction', | 'wrapfunction', | ||||
'wrapclass', | 'wrapclass', | ||||
'addattr', | 'addattr', | ||||
) and (opts['allow-attr-methods'] or not _isop(i - 1, '.')): | ) | ||||
and (opts['allow-attr-methods'] or not _isop(i - 1, '.')) | |||||
): | |||||
arg1idx = _findargnofcall(1) | arg1idx = _findargnofcall(1) | ||||
if arg1idx is not None: | if arg1idx is not None: | ||||
_ensuresysstr(arg1idx) | _ensuresysstr(arg1idx) | ||||
# .encode() and .decode() on str/bytes/unicode don't accept | # .encode() and .decode() on str/bytes/unicode don't accept | ||||
# byte strings on Python 3. | # byte strings on Python 3. | ||||
elif fn in ('encode', 'decode') and _isop(i - 1, '.'): | elif fn in ('encode', 'decode') and _isop(i - 1, '.'): | ||||
for argn in range(2): | for argn in range(2): |
cfilters = [ | cfilters = [ | ||||
(r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment), | (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment), | ||||
(r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote), | (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote), | ||||
(r'''(#\s*include\s+<)([^>]+)>''', repinclude), | (r'''(#\s*include\s+<)([^>]+)>''', repinclude), | ||||
(r'(\()([^)]+\))', repcallspaces), | (r'(\()([^)]+\))', repcallspaces), | ||||
] | ] | ||||
inutilpats = [ | inutilpats = [ | ||||
[(r'\bui\.', "don't use ui in util"),], | [ | ||||
(r'\bui\.', "don't use ui in util"), | |||||
], | |||||
# warnings | # warnings | ||||
[], | [], | ||||
] | ] | ||||
inrevlogpats = [ | inrevlogpats = [ | ||||
[(r'\brepo\.', "don't use repo in revlog"),], | [ | ||||
(r'\brepo\.', "don't use repo in revlog"), | |||||
], | |||||
# warnings | # warnings | ||||
[], | [], | ||||
] | ] | ||||
webtemplatefilters = [] | webtemplatefilters = [] | ||||
webtemplatepats = [ | webtemplatepats = [ | ||||
[], | [], |
build_dir = SOURCE_DIR / "build" | build_dir = SOURCE_DIR / "build" | ||||
if pyoxidizer_target: | if pyoxidizer_target: | ||||
inno.build_with_pyoxidizer( | inno.build_with_pyoxidizer( | ||||
SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version | SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version | ||||
) | ) | ||||
else: | else: | ||||
inno.build_with_py2exe( | inno.build_with_py2exe( | ||||
SOURCE_DIR, build_dir, pathlib.Path(python), iscc, version=version, | SOURCE_DIR, | ||||
build_dir, | |||||
pathlib.Path(python), | |||||
iscc, | |||||
version=version, | |||||
) | ) | ||||
def build_wix( | def build_wix( | ||||
name=None, | name=None, | ||||
pyoxidizer_target=None, | pyoxidizer_target=None, | ||||
python=None, | python=None, | ||||
version=None, | version=None, |
comment_end_string='##}', | comment_end_string='##}', | ||||
) | ) | ||||
try: | try: | ||||
template = jinja_env.get_template('mercurial.iss') | template = jinja_env.get_template('mercurial.iss') | ||||
except jinja2.TemplateSyntaxError as e: | except jinja2.TemplateSyntaxError as e: | ||||
raise Exception( | raise Exception( | ||||
'template syntax error at %s:%d: %s' | 'template syntax error at %s:%d: %s' | ||||
% (e.name, e.lineno, e.message,) | % ( | ||||
e.name, | |||||
e.lineno, | |||||
e.message, | |||||
) | |||||
) | ) | ||||
content = template.render(package_files=package_files) | content = template.render(package_files=package_files) | ||||
with (inno_build_dir / 'mercurial.iss').open('w', encoding='utf-8') as fh: | with (inno_build_dir / 'mercurial.iss').open('w', encoding='utf-8') as fh: | ||||
fh.write(content) | fh.write(content) | ||||
# Copy additional files used by Inno. | # Copy additional files used by Inno. |
] | ] | ||||
for source, rel_path in sorted((extra_wxs or {}).items()): | for source, rel_path in sorted((extra_wxs or {}).items()): | ||||
assert source.endswith('.wxs') | assert source.endswith('.wxs') | ||||
source = os.path.basename(source) | source = os.path.basename(source) | ||||
args.append(str(build_dir / ('%s.wixobj' % source[:-4]))) | args.append(str(build_dir / ('%s.wixobj' % source[:-4]))) | ||||
args.extend( | args.extend( | ||||
[str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),] | [ | ||||
str(build_dir / 'stage.wixobj'), | |||||
str(build_dir / 'mercurial.wixobj'), | |||||
] | |||||
) | ) | ||||
subprocess.run(args, cwd=str(source_dir), check=True) | subprocess.run(args, cwd=str(source_dir), check=True) | ||||
print('%s created' % msi_path) | print('%s created' % msi_path) | ||||
if signing_info: | if signing_info: | ||||
sign_with_signtool( | sign_with_signtool( |
) | ) | ||||
configitem( | configitem( | ||||
b'perf', | b'perf', | ||||
b'all-timing', | b'all-timing', | ||||
default=mercurial.configitems.dynamicdefault, | default=mercurial.configitems.dynamicdefault, | ||||
experimental=True, | experimental=True, | ||||
) | ) | ||||
configitem( | configitem( | ||||
b'perf', b'pre-run', default=mercurial.configitems.dynamicdefault, | b'perf', | ||||
b'pre-run', | |||||
default=mercurial.configitems.dynamicdefault, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'perf', | b'perf', | ||||
b'profile-benchmark', | b'profile-benchmark', | ||||
default=mercurial.configitems.dynamicdefault, | default=mercurial.configitems.dynamicdefault, | ||||
) | ) | ||||
configitem( | configitem( | ||||
b'perf', | b'perf', | ||||
b'run-limits', | b'run-limits', | ||||
default=mercurial.configitems.dynamicdefault, | default=mercurial.configitems.dynamicdefault, | ||||
experimental=True, | experimental=True, | ||||
) | ) | ||||
except (ImportError, AttributeError): | except (ImportError, AttributeError): | ||||
pass | pass | ||||
except TypeError: | except TypeError: | ||||
# compatibility fix for a11fd395e83f | # compatibility fix for a11fd395e83f | ||||
# hg version: 5.2 | # hg version: 5.2 | ||||
configitem( | configitem( | ||||
b'perf', b'presleep', default=mercurial.configitems.dynamicdefault, | b'perf', | ||||
b'presleep', | |||||
default=mercurial.configitems.dynamicdefault, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'perf', b'stub', default=mercurial.configitems.dynamicdefault, | b'perf', | ||||
b'stub', | |||||
default=mercurial.configitems.dynamicdefault, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'perf', b'parentscount', default=mercurial.configitems.dynamicdefault, | b'perf', | ||||
b'parentscount', | |||||
default=mercurial.configitems.dynamicdefault, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'perf', b'all-timing', default=mercurial.configitems.dynamicdefault, | b'perf', | ||||
b'all-timing', | |||||
default=mercurial.configitems.dynamicdefault, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'perf', b'pre-run', default=mercurial.configitems.dynamicdefault, | b'perf', | ||||
b'pre-run', | |||||
default=mercurial.configitems.dynamicdefault, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'perf', | b'perf', | ||||
b'profile-benchmark', | b'profile-benchmark', | ||||
default=mercurial.configitems.dynamicdefault, | default=mercurial.configitems.dynamicdefault, | ||||
) | ) | ||||
configitem( | configitem( | ||||
b'perf', b'run-limits', default=mercurial.configitems.dynamicdefault, | b'perf', | ||||
b'run-limits', | |||||
default=mercurial.configitems.dynamicdefault, | |||||
) | ) | ||||
def getlen(ui): | def getlen(ui): | ||||
if ui.configbool(b"perf", b"stub", False): | if ui.configbool(b"perf", b"stub", False): | ||||
return lambda x: 1 | return lambda x: 1 | ||||
return len | return len | ||||
fm = uiformatter(b'perf', opts) | fm = uiformatter(b'perf', opts) | ||||
else: | else: | ||||
# for "historical portability": | # for "historical portability": | ||||
# define formatter locally, because ui.formatter has been | # define formatter locally, because ui.formatter has been | ||||
# available since 2.2 (or ae5f92e154d3) | # available since 2.2 (or ae5f92e154d3) | ||||
from mercurial import node | from mercurial import node | ||||
class defaultformatter(object): | class defaultformatter(object): | ||||
"""Minimized composition of baseformatter and plainformatter | """Minimized composition of baseformatter and plainformatter""" | ||||
""" | |||||
def __init__(self, ui, topic, opts): | def __init__(self, ui, topic, opts): | ||||
self._ui = ui | self._ui = ui | ||||
if ui.debugflag: | if ui.debugflag: | ||||
self.hexfunc = node.hex | self.hexfunc = node.hex | ||||
else: | else: | ||||
self.hexfunc = node.short | self.hexfunc = node.short | ||||
# doesn't) | # doesn't) | ||||
raise error.Abort( | raise error.Abort( | ||||
b"perfbranchmap not available with this Mercurial", | b"perfbranchmap not available with this Mercurial", | ||||
hint=b"use 2.5 or later", | hint=b"use 2.5 or later", | ||||
) | ) | ||||
def getsvfs(repo): | def getsvfs(repo): | ||||
"""Return appropriate object to access files under .hg/store | """Return appropriate object to access files under .hg/store""" | ||||
""" | |||||
# for "historical portability": | # for "historical portability": | ||||
# repo.svfs has been available since 2.3 (or 7034365089bf) | # repo.svfs has been available since 2.3 (or 7034365089bf) | ||||
svfs = getattr(repo, 'svfs', None) | svfs = getattr(repo, 'svfs', None) | ||||
if svfs: | if svfs: | ||||
return svfs | return svfs | ||||
else: | else: | ||||
return getattr(repo, 'sopener') | return getattr(repo, 'sopener') | ||||
def getvfs(repo): | def getvfs(repo): | ||||
"""Return appropriate object to access files under .hg | """Return appropriate object to access files under .hg""" | ||||
""" | |||||
# for "historical portability": | # for "historical portability": | ||||
# repo.vfs has been available since 2.3 (or 7034365089bf) | # repo.vfs has been available since 2.3 (or 7034365089bf) | ||||
vfs = getattr(repo, 'vfs', None) | vfs = getattr(repo, 'vfs', None) | ||||
if vfs: | if vfs: | ||||
return vfs | return vfs | ||||
else: | else: | ||||
return getattr(repo, 'opener') | return getattr(repo, 'opener') | ||||
def repocleartagscachefunc(repo): | def repocleartagscachefunc(repo): | ||||
"""Return the function to clear tags cache according to repo internal API | """Return the function to clear tags cache according to repo internal API""" | ||||
""" | |||||
if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525) | if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525) | ||||
# in this case, setattr(repo, '_tagscache', None) or so isn't | # in this case, setattr(repo, '_tagscache', None) or so isn't | ||||
# correct way to clear tags cache, because existing code paths | # correct way to clear tags cache, because existing code paths | ||||
# expect _tagscache to be a structured object. | # expect _tagscache to be a structured object. | ||||
def clearcache(): | def clearcache(): | ||||
# _tagscache has been filteredpropertycache since 2.5 (or | # _tagscache has been filteredpropertycache since 2.5 (or | ||||
# 98c867ac1330), and delattr() can't work in such case | # 98c867ac1330), and delattr() can't work in such case | ||||
if '_tagscache' in vars(repo): | if '_tagscache' in vars(repo): | ||||
timer(d, setup=s) | timer(d, setup=s) | ||||
fm.end() | fm.end() | ||||
@command( | @command( | ||||
b'perftags', | b'perftags', | ||||
formatteropts | formatteropts | ||||
+ [(b'', b'clear-revlogs', False, b'refresh changelog and manifest'),], | + [ | ||||
(b'', b'clear-revlogs', False, b'refresh changelog and manifest'), | |||||
], | |||||
) | ) | ||||
def perftags(ui, repo, **opts): | def perftags(ui, repo, **opts): | ||||
opts = _byteskwargs(opts) | opts = _byteskwargs(opts) | ||||
timer, fm = gettimer(ui, opts) | timer, fm = gettimer(ui, opts) | ||||
repocleartagscache = repocleartagscachefunc(repo) | repocleartagscache = repocleartagscachefunc(repo) | ||||
clearrevlogs = opts[b'clear_revlogs'] | clearrevlogs = opts[b'clear_revlogs'] | ||||
def s(): | def s(): | ||||
rev in s | rev in s | ||||
timer(d) | timer(d) | ||||
fm.end() | fm.end() | ||||
@command(b'perfdiscovery', formatteropts, b'PATH') | @command(b'perfdiscovery', formatteropts, b'PATH') | ||||
def perfdiscovery(ui, repo, path, **opts): | def perfdiscovery(ui, repo, path, **opts): | ||||
"""benchmark discovery between local repo and the peer at given path | """benchmark discovery between local repo and the peer at given path""" | ||||
""" | |||||
repos = [repo, None] | repos = [repo, None] | ||||
timer, fm = gettimer(ui, opts) | timer, fm = gettimer(ui, opts) | ||||
path = ui.expandpath(path) | path = ui.expandpath(path) | ||||
def s(): | def s(): | ||||
repos[1] = hg.peer(ui, opts, path) | repos[1] = hg.peer(ui, opts, path) | ||||
def d(): | def d(): | ||||
setdiscovery.findcommonheads(ui, *repos) | setdiscovery.findcommonheads(ui, *repos) | ||||
timer(d, setup=s) | timer(d, setup=s) | ||||
fm.end() | fm.end() | ||||
@command( | @command( | ||||
b'perfbookmarks', | b'perfbookmarks', | ||||
formatteropts | formatteropts | ||||
+ [(b'', b'clear-revlogs', False, b'refresh changelog and manifest'),], | + [ | ||||
(b'', b'clear-revlogs', False, b'refresh changelog and manifest'), | |||||
], | |||||
) | ) | ||||
def perfbookmarks(ui, repo, **opts): | def perfbookmarks(ui, repo, **opts): | ||||
"""benchmark parsing bookmarks from disk to memory""" | """benchmark parsing bookmarks from disk to memory""" | ||||
opts = _byteskwargs(opts) | opts = _byteskwargs(opts) | ||||
timer, fm = gettimer(ui, opts) | timer, fm = gettimer(ui, opts) | ||||
clearrevlogs = opts[b'clear_revlogs'] | clearrevlogs = opts[b'clear_revlogs'] | ||||
b"a" in repo.dirstate | b"a" in repo.dirstate | ||||
timer(d, setup=setup) | timer(d, setup=setup) | ||||
fm.end() | fm.end() | ||||
@command(b'perfdirstatedirs', formatteropts) | @command(b'perfdirstatedirs', formatteropts) | ||||
def perfdirstatedirs(ui, repo, **opts): | def perfdirstatedirs(ui, repo, **opts): | ||||
"""benchmap a 'dirstate.hasdir' call from an empty `dirs` cache | """benchmap a 'dirstate.hasdir' call from an empty `dirs` cache""" | ||||
""" | |||||
opts = _byteskwargs(opts) | opts = _byteskwargs(opts) | ||||
timer, fm = gettimer(ui, opts) | timer, fm = gettimer(ui, opts) | ||||
repo.dirstate.hasdir(b"a") | repo.dirstate.hasdir(b"a") | ||||
def setup(): | def setup(): | ||||
del repo.dirstate._map._dirs | del repo.dirstate._map._dirs | ||||
def d(): | def d(): | ||||
dirstate._map.dirfoldmap.get(b'a') | dirstate._map.dirfoldmap.get(b'a') | ||||
timer(d, setup=setup) | timer(d, setup=setup) | ||||
fm.end() | fm.end() | ||||
@command(b'perfdirstatewrite', formatteropts) | @command(b'perfdirstatewrite', formatteropts) | ||||
def perfdirstatewrite(ui, repo, **opts): | def perfdirstatewrite(ui, repo, **opts): | ||||
"""benchmap the time it take to write a dirstate on disk | """benchmap the time it take to write a dirstate on disk""" | ||||
""" | |||||
opts = _byteskwargs(opts) | opts = _byteskwargs(opts) | ||||
timer, fm = gettimer(ui, opts) | timer, fm = gettimer(ui, opts) | ||||
ds = repo.dirstate | ds = repo.dirstate | ||||
b"a" in ds | b"a" in ds | ||||
def setup(): | def setup(): | ||||
ds._dirty = True | ds._dirty = True | ||||
copies.pathcopies(ctx1, ctx2) | copies.pathcopies(ctx1, ctx2) | ||||
timer(d) | timer(d) | ||||
fm.end() | fm.end() | ||||
@command( | @command( | ||||
b'perfphases', | b'perfphases', | ||||
[(b'', b'full', False, b'include file reading time too'),], | [ | ||||
(b'', b'full', False, b'include file reading time too'), | |||||
], | |||||
b"", | b"", | ||||
) | ) | ||||
def perfphases(ui, repo, **opts): | def perfphases(ui, repo, **opts): | ||||
"""benchmark phasesets computation""" | """benchmark phasesets computation""" | ||||
opts = _byteskwargs(opts) | opts = _byteskwargs(opts) | ||||
timer, fm = gettimer(ui, opts) | timer, fm = gettimer(ui, opts) | ||||
_phases = repo._phasecache | _phases = repo._phasecache | ||||
full = opts.get(b'full') | full = opts.get(b'full') | ||||
ctx.branch() # read changelog data (in addition to the index) | ctx.branch() # read changelog data (in addition to the index) | ||||
timer(moonwalk) | timer(moonwalk) | ||||
fm.end() | fm.end() | ||||
@command( | @command( | ||||
b'perftemplating', | b'perftemplating', | ||||
[(b'r', b'rev', [], b'revisions to run the template on'),] + formatteropts, | [ | ||||
(b'r', b'rev', [], b'revisions to run the template on'), | |||||
] | |||||
+ formatteropts, | |||||
) | ) | ||||
def perftemplating(ui, repo, testedtemplate=None, **opts): | def perftemplating(ui, repo, testedtemplate=None, **opts): | ||||
"""test the rendering time of a given template""" | """test the rendering time of a given template""" | ||||
if makelogtemplater is None: | if makelogtemplater is None: | ||||
raise error.Abort( | raise error.Abort( | ||||
b"perftemplating not available with this Mercurial", | b"perftemplating not available with this Mercurial", | ||||
hint=b"use 4.3 or later", | hint=b"use 4.3 or later", | ||||
) | ) | ||||
data = { | data = { | ||||
b'source': base.hex(), | b'source': base.hex(), | ||||
b'destination': parent.hex(), | b'destination': parent.hex(), | ||||
b'nbrevs': len(repo.revs('only(%d, %d)', p, b)), | b'nbrevs': len(repo.revs('only(%d, %d)', p, b)), | ||||
b'nbmissingfiles': len(missing), | b'nbmissingfiles': len(missing), | ||||
} | } | ||||
if dostats: | if dostats: | ||||
alldata['nbrevs'].append( | alldata['nbrevs'].append( | ||||
(data['nbrevs'], base.hex(), parent.hex(),) | ( | ||||
data['nbrevs'], | |||||
base.hex(), | |||||
parent.hex(), | |||||
) | |||||
) | ) | ||||
alldata['nbmissingfiles'].append( | alldata['nbmissingfiles'].append( | ||||
(data['nbmissingfiles'], base.hex(), parent.hex(),) | ( | ||||
data['nbmissingfiles'], | |||||
base.hex(), | |||||
parent.hex(), | |||||
) | |||||
) | ) | ||||
if dotiming: | if dotiming: | ||||
begin = util.timer() | begin = util.timer() | ||||
renames = copies.pathcopies(base, parent) | renames = copies.pathcopies(base, parent) | ||||
end = util.timer() | end = util.timer() | ||||
# not very stable timing since we did only one run | # not very stable timing since we did only one run | ||||
data['time'] = end - begin | data['time'] = end - begin | ||||
data['nbrenamedfiles'] = len(renames) | data['nbrenamedfiles'] = len(renames) | ||||
if dostats: | if dostats: | ||||
alldata['time'].append( | alldata['time'].append( | ||||
(data['time'], base.hex(), parent.hex(),) | ( | ||||
data['time'], | |||||
base.hex(), | |||||
parent.hex(), | |||||
) | |||||
) | ) | ||||
alldata['nbrenames'].append( | alldata['nbrenames'].append( | ||||
(data['nbrenamedfiles'], base.hex(), parent.hex(),) | ( | ||||
data['nbrenamedfiles'], | |||||
base.hex(), | |||||
parent.hex(), | |||||
) | |||||
) | ) | ||||
fm.startitem() | fm.startitem() | ||||
fm.data(**data) | fm.data(**data) | ||||
out = data.copy() | out = data.copy() | ||||
out['source'] = fm.hexfunc(base.node()) | out['source'] = fm.hexfunc(base.node()) | ||||
out['destination'] = fm.hexfunc(parent.node()) | out['destination'] = fm.hexfunc(parent.node()) | ||||
fm.plain(output % out) | fm.plain(output % out) | ||||
pass | pass | ||||
timer(d) | timer(d) | ||||
fm.end() | fm.end() | ||||
@command( | @command( | ||||
b'perfvolatilesets', | b'perfvolatilesets', | ||||
[(b'', b'clear-obsstore', False, b'drop obsstore between each call.'),] | [ | ||||
(b'', b'clear-obsstore', False, b'drop obsstore between each call.'), | |||||
] | |||||
+ formatteropts, | + formatteropts, | ||||
) | ) | ||||
def perfvolatilesets(ui, repo, *names, **opts): | def perfvolatilesets(ui, repo, *names, **opts): | ||||
"""benchmark the computation of various volatile set | """benchmark the computation of various volatile set | ||||
Volatile set computes element related to filtering and obsolescence.""" | Volatile set computes element related to filtering and obsolescence.""" | ||||
opts = _byteskwargs(opts) | opts = _byteskwargs(opts) | ||||
timer, fm = gettimer(ui, opts) | timer, fm = gettimer(ui, opts) | ||||
(b'', b'nlines', 100, b'number of lines'), | (b'', b'nlines', 100, b'number of lines'), | ||||
(b'', b'nitems', 100, b'number of items (per line)'), | (b'', b'nitems', 100, b'number of items (per line)'), | ||||
(b'', b'item', b'x', b'item that is written'), | (b'', b'item', b'x', b'item that is written'), | ||||
(b'', b'batch-line', None, b'pass whole line to write method at once'), | (b'', b'batch-line', None, b'pass whole line to write method at once'), | ||||
(b'', b'flush-line', None, b'flush after each line'), | (b'', b'flush-line', None, b'flush after each line'), | ||||
], | ], | ||||
) | ) | ||||
def perfwrite(ui, repo, **opts): | def perfwrite(ui, repo, **opts): | ||||
"""microbenchmark ui.write (and others) | """microbenchmark ui.write (and others)""" | ||||
""" | |||||
opts = _byteskwargs(opts) | opts = _byteskwargs(opts) | ||||
write = getattr(ui, _sysstr(opts[b'write_method'])) | write = getattr(ui, _sysstr(opts[b'write_method'])) | ||||
nlines = int(opts[b'nlines']) | nlines = int(opts[b'nlines']) | ||||
nitems = int(opts[b'nitems']) | nitems = int(opts[b'nitems']) | ||||
item = opts[b'item'] | item = opts[b'item'] | ||||
batch_line = opts.get(b'batch_line') | batch_line = opts.get(b'batch_line') | ||||
flush_line = opts.get(b'flush_line') | flush_line = opts.get(b'flush_line') |
''' | ''' | ||||
Examples of useful python hooks for Mercurial. | Examples of useful python hooks for Mercurial. | ||||
''' | ''' | ||||
from __future__ import absolute_import | from __future__ import absolute_import | ||||
from mercurial import ( | from mercurial import ( | ||||
patch, | patch, | ||||
util, | util, | ||||
) | ) | ||||
def diffstat(ui, repo, **kwargs): | def diffstat(ui, repo, **kwargs): | ||||
'''Example usage: | """Example usage: | ||||
[hooks] | [hooks] | ||||
commit.diffstat = python:/path/to/this/file.py:diffstat | commit.diffstat = python:/path/to/this/file.py:diffstat | ||||
changegroup.diffstat = python:/path/to/this/file.py:diffstat | changegroup.diffstat = python:/path/to/this/file.py:diffstat | ||||
''' | """ | ||||
if kwargs.get('parent2'): | if kwargs.get('parent2'): | ||||
return | return | ||||
node = kwargs['node'] | node = kwargs['node'] | ||||
first = repo[node].p1().node() | first = repo[node].p1().node() | ||||
if 'url' in kwargs: | if 'url' in kwargs: | ||||
last = repo.changelog.tip() | last = repo.changelog.tip() | ||||
else: | else: | ||||
last = node | last = node | ||||
diff = patch.diff(repo, first, last) | diff = patch.diff(repo, first, last) | ||||
ui.write(patch.diffstat(util.iterlines(diff))) | ui.write(patch.diffstat(util.iterlines(diff))) |
"dictBuilder/divsufsort.c", | "dictBuilder/divsufsort.c", | ||||
"dictBuilder/zdict.c", | "dictBuilder/zdict.c", | ||||
) | ) | ||||
] | ] | ||||
# Headers whose preprocessed output will be fed into cdef(). | # Headers whose preprocessed output will be fed into cdef(). | ||||
HEADERS = [ | HEADERS = [ | ||||
os.path.join(HERE, "zstd", *p) | os.path.join(HERE, "zstd", *p) | ||||
for p in (("zstd.h",), ("dictBuilder", "zdict.h"),) | for p in ( | ||||
("zstd.h",), | |||||
("dictBuilder", "zdict.h"), | |||||
) | |||||
] | ] | ||||
INCLUDE_DIRS = [ | INCLUDE_DIRS = [ | ||||
os.path.join(HERE, d) | os.path.join(HERE, d) | ||||
for d in ( | for d in ( | ||||
"zstd", | "zstd", | ||||
"zstd/common", | "zstd/common", | ||||
"zstd/compress", | "zstd/compress", | ||||
if hasattr(compiler, "initialize"): | if hasattr(compiler, "initialize"): | ||||
compiler.initialize() | compiler.initialize() | ||||
# Distutils doesn't set compiler.preprocessor, so invoke the preprocessor | # Distutils doesn't set compiler.preprocessor, so invoke the preprocessor | ||||
# manually. | # manually. | ||||
if compiler.compiler_type == "unix": | if compiler.compiler_type == "unix": | ||||
args = list(compiler.executables["compiler"]) | args = list(compiler.executables["compiler"]) | ||||
args.extend( | args.extend( | ||||
["-E", "-DZSTD_STATIC_LINKING_ONLY", "-DZDICT_STATIC_LINKING_ONLY",] | [ | ||||
"-E", | |||||
"-DZSTD_STATIC_LINKING_ONLY", | |||||
"-DZDICT_STATIC_LINKING_ONLY", | |||||
] | |||||
) | ) | ||||
elif compiler.compiler_type == "msvc": | elif compiler.compiler_type == "msvc": | ||||
args = [compiler.cc] | args = [compiler.cc] | ||||
args.extend( | args.extend( | ||||
["/EP", "/DZSTD_STATIC_LINKING_ONLY", "/DZDICT_STATIC_LINKING_ONLY",] | [ | ||||
"/EP", | |||||
"/DZSTD_STATIC_LINKING_ONLY", | |||||
"/DZDICT_STATIC_LINKING_ONLY", | |||||
] | |||||
) | ) | ||||
else: | else: | ||||
raise Exception("unsupported compiler type: %s" % compiler.compiler_type) | raise Exception("unsupported compiler type: %s" % compiler.compiler_type) | ||||
def preprocess(path): | def preprocess(path): | ||||
with open(path, "rb") as fh: | with open(path, "rb") as fh: | ||||
lines = [] | lines = [] |
import argparse | import argparse | ||||
import json | import json | ||||
import os | import os | ||||
import subprocess | import subprocess | ||||
import sys | import sys | ||||
_hgenv = dict(os.environ) | _hgenv = dict(os.environ) | ||||
_hgenv.update( | _hgenv.update( | ||||
{'HGPLAIN': '1',} | { | ||||
'HGPLAIN': '1', | |||||
} | |||||
) | ) | ||||
_HG_FIRST_CHANGE = '9117c6561b0bd7792fa13b50d28239d51b78e51f' | _HG_FIRST_CHANGE = '9117c6561b0bd7792fa13b50d28239d51b78e51f' | ||||
def _runhg(*args): | def _runhg(*args): | ||||
return subprocess.check_output(args, env=_hgenv) | return subprocess.check_output(args, env=_hgenv) | ||||
[ | [ | ||||
('o', 'output', '', _('write output to given file'), _('FILE')), | ('o', 'output', '', _('write output to given file'), _('FILE')), | ||||
('r', 'rev', [], _('analyze specified revisions'), _('REV')), | ('r', 'rev', [], _('analyze specified revisions'), _('REV')), | ||||
], | ], | ||||
_('hg analyze'), | _('hg analyze'), | ||||
optionalrepo=True, | optionalrepo=True, | ||||
) | ) | ||||
def analyze(ui, repo, *revs, **opts): | def analyze(ui, repo, *revs, **opts): | ||||
'''create a simple model of a repository to use for later synthesis | """create a simple model of a repository to use for later synthesis | ||||
This command examines every changeset in the given range (or all | This command examines every changeset in the given range (or all | ||||
of history if none are specified) and creates a simple statistical | of history if none are specified) and creates a simple statistical | ||||
model of the history of the repository. It also measures the directory | model of the history of the repository. It also measures the directory | ||||
structure of the repository as checked out. | structure of the repository as checked out. | ||||
The model is written out to a JSON file, and can be used by | The model is written out to a JSON file, and can be used by | ||||
:hg:`synthesize` to create or augment a repository with synthetic | :hg:`synthesize` to create or augment a repository with synthetic | ||||
commits that have a structure that is statistically similar to the | commits that have a structure that is statistically similar to the | ||||
analyzed repository. | analyzed repository. | ||||
''' | """ | ||||
root = repo.root | root = repo.root | ||||
if not root.endswith(os.path.sep): | if not root.endswith(os.path.sep): | ||||
root += os.path.sep | root += os.path.sep | ||||
revs = list(revs) | revs = list(revs) | ||||
revs.extend(opts['rev']) | revs.extend(opts['rev']) | ||||
if not revs: | if not revs: | ||||
revs = [':'] | revs = [':'] | ||||
[ | [ | ||||
('c', 'count', 0, _('create given number of commits'), _('COUNT')), | ('c', 'count', 0, _('create given number of commits'), _('COUNT')), | ||||
('', 'dict', '', _('path to a dictionary of words'), _('FILE')), | ('', 'dict', '', _('path to a dictionary of words'), _('FILE')), | ||||
('', 'initfiles', 0, _('initial file count to create'), _('COUNT')), | ('', 'initfiles', 0, _('initial file count to create'), _('COUNT')), | ||||
], | ], | ||||
_('hg synthesize [OPTION].. DESCFILE'), | _('hg synthesize [OPTION].. DESCFILE'), | ||||
) | ) | ||||
def synthesize(ui, repo, descpath, **opts): | def synthesize(ui, repo, descpath, **opts): | ||||
'''synthesize commits based on a model of an existing repository | """synthesize commits based on a model of an existing repository | ||||
The model must have been generated by :hg:`analyze`. Commits will | The model must have been generated by :hg:`analyze`. Commits will | ||||
be generated randomly according to the probabilities described in | be generated randomly according to the probabilities described in | ||||
the model. If --initfiles is set, the repository will be seeded with | the model. If --initfiles is set, the repository will be seeded with | ||||
the given number files following the modeled repository's directory | the given number files following the modeled repository's directory | ||||
structure. | structure. | ||||
When synthesizing new content, commit descriptions, and user | When synthesizing new content, commit descriptions, and user | ||||
names, words will be chosen randomly from a dictionary that is | names, words will be chosen randomly from a dictionary that is | ||||
presumed to contain one word per line. Use --dict to specify the | presumed to contain one word per line. Use --dict to specify the | ||||
path to an alternate dictionary to use. | path to an alternate dictionary to use. | ||||
''' | """ | ||||
try: | try: | ||||
fp = hg.openpath(ui, descpath) | fp = hg.openpath(ui, descpath) | ||||
except Exception as err: | except Exception as err: | ||||
raise error.Abort('%s: %s' % (descpath, err[0].strerror)) | raise error.Abort('%s: %s' % (descpath, err[0].strerror)) | ||||
desc = json.load(fp) | desc = json.load(fp) | ||||
fp.close() | fp.close() | ||||
def cdf(l): | def cdf(l): | ||||
def renamedirs(dirs, words): | def renamedirs(dirs, words): | ||||
'''Randomly rename the directory names in the per-dir file count dict.''' | '''Randomly rename the directory names in the per-dir file count dict.''' | ||||
wordgen = itertools.cycle(words) | wordgen = itertools.cycle(words) | ||||
replacements = {'': ''} | replacements = {'': ''} | ||||
def rename(dirpath): | def rename(dirpath): | ||||
'''Recursively rename the directory and all path prefixes. | """Recursively rename the directory and all path prefixes. | ||||
The mapping from path to renamed path is stored for all path prefixes | The mapping from path to renamed path is stored for all path prefixes | ||||
as in dynamic programming, ensuring linear runtime and consistent | as in dynamic programming, ensuring linear runtime and consistent | ||||
renaming regardless of iteration order through the model. | renaming regardless of iteration order through the model. | ||||
''' | """ | ||||
if dirpath in replacements: | if dirpath in replacements: | ||||
return replacements[dirpath] | return replacements[dirpath] | ||||
head, _ = os.path.split(dirpath) | head, _ = os.path.split(dirpath) | ||||
if head: | if head: | ||||
head = rename(head) | head = rename(head) | ||||
else: | else: | ||||
head = '' | head = '' | ||||
renamed = os.path.join(head, next(wordgen)) | renamed = os.path.join(head, next(wordgen)) | ||||
replacements[dirpath] = renamed | replacements[dirpath] = renamed | ||||
return renamed | return renamed | ||||
result = [] | result = [] | ||||
for dirpath, count in dirs.iteritems(): | for dirpath, count in dirs.iteritems(): | ||||
result.append([rename(dirpath.lstrip(os.sep)), count]) | result.append([rename(dirpath.lstrip(os.sep)), count]) | ||||
return result | return result |
# write "data" in BYTES into stderr | # write "data" in BYTES into stderr | ||||
sys.stderr.write(data) | sys.stderr.write(data) | ||||
#################### | #################### | ||||
class embeddedmatcher(object): # pytype: disable=ignored-metaclass | class embeddedmatcher(object): # pytype: disable=ignored-metaclass | ||||
"""Base class to detect embedded code fragments in *.t test script | """Base class to detect embedded code fragments in *.t test script""" | ||||
""" | |||||
__metaclass__ = abc.ABCMeta | __metaclass__ = abc.ABCMeta | ||||
def __init__(self, desc): | def __init__(self, desc): | ||||
self.desc = desc | self.desc = desc | ||||
@abc.abstractmethod | @abc.abstractmethod | ||||
def startsat(self, line): | def startsat(self, line): | ||||
"""Examine whether embedded code starts at line | """Examine whether embedded code starts at line | ||||
This can return arbitrary object, and it is used as 'ctx' for | This can return arbitrary object, and it is used as 'ctx' for | ||||
subsequent method invocations. | subsequent method invocations. | ||||
""" | """ | ||||
@abc.abstractmethod | @abc.abstractmethod | ||||
def endsat(self, ctx, line): | def endsat(self, ctx, line): | ||||
"""Examine whether embedded code ends at line""" | """Examine whether embedded code ends at line""" | ||||
@abc.abstractmethod | @abc.abstractmethod | ||||
def isinside(self, ctx, line): | def isinside(self, ctx, line): | ||||
"""Examine whether line is inside embedded code, if not yet endsat | """Examine whether line is inside embedded code, if not yet endsat""" | ||||
""" | |||||
@abc.abstractmethod | @abc.abstractmethod | ||||
def ignores(self, ctx): | def ignores(self, ctx): | ||||
"""Examine whether detected embedded code should be ignored""" | """Examine whether detected embedded code should be ignored""" | ||||
@abc.abstractmethod | @abc.abstractmethod | ||||
def filename(self, ctx): | def filename(self, ctx): | ||||
"""Return filename of embedded code | """Return filename of embedded code |
def depart_line(self, node): | def depart_line(self, node): | ||||
self.body.append('\n') | self.body.append('\n') | ||||
def visit_list_item(self, node): | def visit_list_item(self, node): | ||||
# man 7 man argues to use ".IP" instead of ".TP" | # man 7 man argues to use ".IP" instead of ".TP" | ||||
self.body.append( | self.body.append( | ||||
'.IP %s %d\n' | '.IP %s %d\n' | ||||
% (next(self._list_char[-1]), self._list_char[-1].get_width(),) | % ( | ||||
next(self._list_char[-1]), | |||||
self._list_char[-1].get_width(), | |||||
) | |||||
) | ) | ||||
def depart_list_item(self, node): | def depart_list_item(self, node): | ||||
pass | pass | ||||
def visit_literal(self, node): | def visit_literal(self, node): | ||||
self.body.append(self.defs['literal'][0]) | self.body.append(self.defs['literal'][0]) | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
# deprecated config: acl.config | # deprecated config: acl.config | ||||
configitem( | configitem( | ||||
b'acl', b'config', default=None, | b'acl', | ||||
b'config', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'acl.groups', b'.*', default=None, generic=True, | b'acl.groups', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'acl.deny.branches', b'.*', default=None, generic=True, | b'acl.deny.branches', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'acl.allow.branches', b'.*', default=None, generic=True, | b'acl.allow.branches', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'acl.deny', b'.*', default=None, generic=True, | b'acl.deny', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'acl.allow', b'.*', default=None, generic=True, | b'acl.allow', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'acl', b'sources', default=lambda: [b'serve'], | b'acl', | ||||
b'sources', | |||||
default=lambda: [b'serve'], | |||||
) | ) | ||||
def _getusers(ui, group): | def _getusers(ui, group): | ||||
# First, try to use group definition from section [acl.groups] | # First, try to use group definition from section [acl.groups] | ||||
hgrcusers = ui.configlist(b'acl.groups', group) | hgrcusers = ui.configlist(b'acl.groups', group) | ||||
if hgrcusers: | if hgrcusers: |
scmutil, | scmutil, | ||||
similar, | similar, | ||||
) | ) | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'automv', b'similarity', default=95, | b'automv', | ||||
b'similarity', | |||||
default=95, | |||||
) | ) | ||||
def extsetup(ui): | def extsetup(ui): | ||||
entry = extensions.wrapcommand(commands.table, b'commit', mvcheck) | entry = extensions.wrapcommand(commands.table, b'commit', mvcheck) | ||||
entry[1].append( | entry[1].append( | ||||
(b'', b'no-automv', None, _(b'disable automatic file move detection')) | (b'', b'no-automv', None, _(b'disable automatic file move detection')) | ||||
) | ) |
cmdtable = {} | cmdtable = {} | ||||
command = registrar.command(cmdtable) | command = registrar.command(cmdtable) | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'blackbox', b'dirty', default=False, | b'blackbox', | ||||
b'dirty', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'blackbox', b'maxsize', default=b'1 MB', | b'blackbox', | ||||
b'maxsize', | |||||
default=b'1 MB', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'blackbox', b'logsource', default=False, | b'blackbox', | ||||
b'logsource', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'blackbox', b'maxfiles', default=7, | b'blackbox', | ||||
b'maxfiles', | |||||
default=7, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'blackbox', b'track', default=lambda: [b'*'], | b'blackbox', | ||||
b'track', | |||||
default=lambda: [b'*'], | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'blackbox', | b'blackbox', | ||||
b'ignore', | b'ignore', | ||||
default=lambda: [b'chgserver', b'cmdserver', b'extension'], | default=lambda: [b'chgserver', b'cmdserver', b'extension'], | ||||
) | ) | ||||
configitem( | configitem( | ||||
b'blackbox', b'date-format', default=b'%Y/%m/%d %H:%M:%S', | b'blackbox', | ||||
b'date-format', | |||||
default=b'%Y/%m/%d %H:%M:%S', | |||||
) | ) | ||||
_lastlogger = loggingutil.proxylogger() | _lastlogger = loggingutil.proxylogger() | ||||
class blackboxlogger(object): | class blackboxlogger(object): | ||||
def __init__(self, ui, repo): | def __init__(self, ui, repo): | ||||
self._repo = repo | self._repo = repo | ||||
if _lastlogger.logger is None: | if _lastlogger.logger is None: | ||||
_lastlogger.logger = logger | _lastlogger.logger = logger | ||||
repo._wlockfreeprefix.add(b'blackbox.log') | repo._wlockfreeprefix.add(b'blackbox.log') | ||||
@command( | @command( | ||||
b'blackbox', | b'blackbox', | ||||
[(b'l', b'limit', 10, _(b'the number of events to show')),], | [ | ||||
(b'l', b'limit', 10, _(b'the number of events to show')), | |||||
], | |||||
_(b'hg blackbox [OPTION]...'), | _(b'hg blackbox [OPTION]...'), | ||||
helpcategory=command.CATEGORY_MAINTENANCE, | helpcategory=command.CATEGORY_MAINTENANCE, | ||||
helpbasic=True, | helpbasic=True, | ||||
) | ) | ||||
def blackbox(ui, repo, *revs, **opts): | def blackbox(ui, repo, *revs, **opts): | ||||
'''view the recent repository events | """view the recent repository events""" | ||||
''' | |||||
if not repo.vfs.exists(b'blackbox.log'): | if not repo.vfs.exists(b'blackbox.log'): | ||||
return | return | ||||
limit = opts.get('limit') | limit = opts.get('limit') | ||||
fp = repo.vfs(b'blackbox.log', b'r') | fp = repo.vfs(b'blackbox.log', b'r') | ||||
lines = fp.read().split(b'\n') | lines = fp.read().split(b'\n') | ||||
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'apikey', default=b'', | b'bugzilla', | ||||
b'apikey', | |||||
default=b'', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'bzdir', default=b'/var/www/html/bugzilla', | b'bugzilla', | ||||
b'bzdir', | |||||
default=b'/var/www/html/bugzilla', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'bzemail', default=None, | b'bugzilla', | ||||
b'bzemail', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'bzurl', default=b'http://localhost/bugzilla/', | b'bugzilla', | ||||
b'bzurl', | |||||
default=b'http://localhost/bugzilla/', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'bzuser', default=None, | b'bugzilla', | ||||
b'bzuser', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'db', default=b'bugs', | b'bugzilla', | ||||
b'db', | |||||
default=b'bugs', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', | b'bugzilla', | ||||
b'fixregexp', | b'fixregexp', | ||||
default=( | default=( | ||||
br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*' | br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*' | ||||
br'(?:nos?\.?|num(?:ber)?s?)?\s*' | br'(?:nos?\.?|num(?:ber)?s?)?\s*' | ||||
br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)' | br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)' | ||||
br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?' | br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?' | ||||
), | ), | ||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'fixresolution', default=b'FIXED', | b'bugzilla', | ||||
b'fixresolution', | |||||
default=b'FIXED', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'fixstatus', default=b'RESOLVED', | b'bugzilla', | ||||
b'fixstatus', | |||||
default=b'RESOLVED', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'host', default=b'localhost', | b'bugzilla', | ||||
b'host', | |||||
default=b'localhost', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'notify', default=configitem.dynamicdefault, | b'bugzilla', | ||||
b'notify', | |||||
default=configitem.dynamicdefault, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'password', default=None, | b'bugzilla', | ||||
b'password', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', | b'bugzilla', | ||||
b'regexp', | b'regexp', | ||||
default=( | default=( | ||||
br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*' | br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*' | ||||
br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)' | br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)' | ||||
br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?' | br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?' | ||||
), | ), | ||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'strip', default=0, | b'bugzilla', | ||||
b'strip', | |||||
default=0, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'style', default=None, | b'bugzilla', | ||||
b'style', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'template', default=None, | b'bugzilla', | ||||
b'template', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'timeout', default=5, | b'bugzilla', | ||||
b'timeout', | |||||
default=5, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'user', default=b'bugs', | b'bugzilla', | ||||
b'user', | |||||
default=b'bugs', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'usermap', default=None, | b'bugzilla', | ||||
b'usermap', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'bugzilla', b'version', default=None, | b'bugzilla', | ||||
b'version', | |||||
default=None, | |||||
) | ) | ||||
class bzaccess(object): | class bzaccess(object): | ||||
'''Base class for access to Bugzilla.''' | '''Base class for access to Bugzilla.''' | ||||
def __init__(self, ui): | def __init__(self, ui): | ||||
self.ui = ui | self.ui = ui | ||||
def filter_real_bug_ids(self, bugs): | def filter_real_bug_ids(self, bugs): | ||||
'''remove bug IDs that do not exist in Bugzilla from bugs.''' | '''remove bug IDs that do not exist in Bugzilla from bugs.''' | ||||
def filter_cset_known_bug_ids(self, node, bugs): | def filter_cset_known_bug_ids(self, node, bugs): | ||||
'''remove bug IDs where node occurs in comment text from bugs.''' | '''remove bug IDs where node occurs in comment text from bugs.''' | ||||
def updatebug(self, bugid, newstate, text, committer): | def updatebug(self, bugid, newstate, text, committer): | ||||
'''update the specified bug. Add comment text and set new states. | """update the specified bug. Add comment text and set new states. | ||||
If possible add the comment as being from the committer of | If possible add the comment as being from the committer of | ||||
the changeset. Otherwise use the default Bugzilla user. | the changeset. Otherwise use the default Bugzilla user. | ||||
''' | """ | ||||
def notify(self, bugs, committer): | def notify(self, bugs, committer): | ||||
'''Force sending of Bugzilla notification emails. | """Force sending of Bugzilla notification emails. | ||||
Only required if the access method does not trigger notification | Only required if the access method does not trigger notification | ||||
emails automatically. | emails automatically. | ||||
''' | """ | ||||
# Bugzilla via direct access to MySQL database. | # Bugzilla via direct access to MySQL database. | ||||
class bzmysql(bzaccess): | class bzmysql(bzaccess): | ||||
'''Support for direct MySQL access to Bugzilla. | """Support for direct MySQL access to Bugzilla. | ||||
The earliest Bugzilla version this is tested with is version 2.16. | The earliest Bugzilla version this is tested with is version 2.16. | ||||
If your Bugzilla is version 3.4 or above, you are strongly | If your Bugzilla is version 3.4 or above, you are strongly | ||||
recommended to use the XMLRPC access method instead. | recommended to use the XMLRPC access method instead. | ||||
''' | """ | ||||
@staticmethod | @staticmethod | ||||
def sql_buglist(ids): | def sql_buglist(ids): | ||||
'''return SQL-friendly list of bug ids''' | '''return SQL-friendly list of bug ids''' | ||||
return b'(' + b','.join(map(str, ids)) + b')' | return b'(' + b','.join(map(str, ids)) + b')' | ||||
_MySQLdb = None | _MySQLdb = None | ||||
all = self.cursor.fetchall() | all = self.cursor.fetchall() | ||||
if len(all) != 1: | if len(all) != 1: | ||||
raise KeyError(user) | raise KeyError(user) | ||||
userid = int(all[0][0]) | userid = int(all[0][0]) | ||||
self.user_ids[user] = userid | self.user_ids[user] = userid | ||||
return userid | return userid | ||||
def get_bugzilla_user(self, committer): | def get_bugzilla_user(self, committer): | ||||
'''See if committer is a registered bugzilla user. Return | """See if committer is a registered bugzilla user. Return | ||||
bugzilla username and userid if so. If not, return default | bugzilla username and userid if so. If not, return default | ||||
bugzilla username and userid.''' | bugzilla username and userid.""" | ||||
user = self.map_committer(committer) | user = self.map_committer(committer) | ||||
try: | try: | ||||
userid = self.get_user_id(user) | userid = self.get_user_id(user) | ||||
except KeyError: | except KeyError: | ||||
try: | try: | ||||
defaultuser = self.ui.config(b'bugzilla', b'bzuser') | defaultuser = self.ui.config(b'bugzilla', b'bzuser') | ||||
if not defaultuser: | if not defaultuser: | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'cannot find bugzilla user id for %s') % user | _(b'cannot find bugzilla user id for %s') % user | ||||
) | ) | ||||
userid = self.get_user_id(defaultuser) | userid = self.get_user_id(defaultuser) | ||||
user = defaultuser | user = defaultuser | ||||
except KeyError: | except KeyError: | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'cannot find bugzilla user id for %s or %s') | _(b'cannot find bugzilla user id for %s or %s') | ||||
% (user, defaultuser) | % (user, defaultuser) | ||||
) | ) | ||||
return (user, userid) | return (user, userid) | ||||
def updatebug(self, bugid, newstate, text, committer): | def updatebug(self, bugid, newstate, text, committer): | ||||
'''update bug state with comment text. | """update bug state with comment text. | ||||
Try adding comment as committer of changeset, otherwise as | Try adding comment as committer of changeset, otherwise as | ||||
default bugzilla user.''' | default bugzilla user.""" | ||||
if len(newstate) > 0: | if len(newstate) > 0: | ||||
self.ui.warn(_(b"Bugzilla/MySQL cannot update bug state\n")) | self.ui.warn(_(b"Bugzilla/MySQL cannot update bug state\n")) | ||||
(user, userid) = self.get_bugzilla_user(committer) | (user, userid) = self.get_bugzilla_user(committer) | ||||
now = time.strftime('%Y-%m-%d %H:%M:%S') | now = time.strftime('%Y-%m-%d %H:%M:%S') | ||||
self.run( | self.run( | ||||
'''insert into longdescs | '''insert into longdescs | ||||
(bug_id, who, bug_when, thetext) | (bug_id, who, bug_when, thetext) | ||||
if self.bzvermajor >= 4: | if self.bzvermajor >= 4: | ||||
return b"@%s %s" % (fieldname, pycompat.bytestr(value)) | return b"@%s %s" % (fieldname, pycompat.bytestr(value)) | ||||
else: | else: | ||||
if fieldname == b"id": | if fieldname == b"id": | ||||
fieldname = b"bug_id" | fieldname = b"bug_id" | ||||
return b"@%s = %s" % (fieldname, pycompat.bytestr(value)) | return b"@%s = %s" % (fieldname, pycompat.bytestr(value)) | ||||
def send_bug_modify_email(self, bugid, commands, comment, committer): | def send_bug_modify_email(self, bugid, commands, comment, committer): | ||||
'''send modification message to Bugzilla bug via email. | """send modification message to Bugzilla bug via email. | ||||
The message format is documented in the Bugzilla email_in.pl | The message format is documented in the Bugzilla email_in.pl | ||||
specification. commands is a list of command lines, comment is the | specification. commands is a list of command lines, comment is the | ||||
comment text. | comment text. | ||||
To stop users from crafting commit comments with | To stop users from crafting commit comments with | ||||
Bugzilla commands, specify the bug ID via the message body, rather | Bugzilla commands, specify the bug ID via the message body, rather | ||||
than the subject line, and leave a blank line after it. | than the subject line, and leave a blank line after it. | ||||
''' | """ | ||||
user = self.map_committer(committer) | user = self.map_committer(committer) | ||||
matches = self.bzproxy.User.get( | matches = self.bzproxy.User.get( | ||||
{b'match': [user], b'token': self.bztoken} | {b'match': [user], b'token': self.bztoken} | ||||
) | ) | ||||
if not matches[b'users']: | if not matches[b'users']: | ||||
user = self.ui.config(b'bugzilla', b'user') | user = self.ui.config(b'bugzilla', b'user') | ||||
matches = self.bzproxy.User.get( | matches = self.bzproxy.User.get( | ||||
{b'match': [user], b'token': self.bztoken} | {b'match': [user], b'token': self.bztoken} | ||||
if any(sn in c[b'text'] for c in comments): | if any(sn in c[b'text'] for c in comments): | ||||
self.ui.status( | self.ui.status( | ||||
_(b'bug %d already knows about changeset %s\n') | _(b'bug %d already knows about changeset %s\n') | ||||
% (bugid, sn) | % (bugid, sn) | ||||
) | ) | ||||
del bugs[bugid] | del bugs[bugid] | ||||
def updatebug(self, bugid, newstate, text, committer): | def updatebug(self, bugid, newstate, text, committer): | ||||
'''update the specified bug. Add comment text and set new states. | """update the specified bug. Add comment text and set new states. | ||||
If possible add the comment as being from the committer of | If possible add the comment as being from the committer of | ||||
the changeset. Otherwise use the default Bugzilla user. | the changeset. Otherwise use the default Bugzilla user. | ||||
''' | """ | ||||
bugmod = {} | bugmod = {} | ||||
if b'hours' in newstate: | if b'hours' in newstate: | ||||
bugmod[b'work_time'] = newstate[b'hours'] | bugmod[b'work_time'] = newstate[b'hours'] | ||||
if b'fix' in newstate: | if b'fix' in newstate: | ||||
bugmod[b'status'] = self.fixstatus | bugmod[b'status'] = self.fixstatus | ||||
bugmod[b'resolution'] = self.fixresolution | bugmod[b'resolution'] = self.fixresolution | ||||
if bugmod: | if bugmod: | ||||
# if we have to change the bugs state do it here | # if we have to change the bugs state do it here | ||||
b'comment': text, | b'comment': text, | ||||
b'is_private': False, | b'is_private': False, | ||||
b'is_markdown': False, | b'is_markdown': False, | ||||
}, | }, | ||||
) | ) | ||||
self.ui.debug(b'added comment to bug %s\n' % bugid) | self.ui.debug(b'added comment to bug %s\n' % bugid) | ||||
def notify(self, bugs, committer): | def notify(self, bugs, committer): | ||||
'''Force sending of Bugzilla notification emails. | """Force sending of Bugzilla notification emails. | ||||
Only required if the access method does not trigger notification | Only required if the access method does not trigger notification | ||||
emails automatically. | emails automatically. | ||||
''' | """ | ||||
pass | pass | ||||
class bugzilla(object): | class bugzilla(object): | ||||
# supported versions of bugzilla. different versions have | # supported versions of bugzilla. different versions have | ||||
# different schemas. | # different schemas. | ||||
_versions = { | _versions = { | ||||
b'2.16': bzmysql, | b'2.16': bzmysql, | ||||
self.ui.config(b'bugzilla', b'regexp'), re.IGNORECASE | self.ui.config(b'bugzilla', b'regexp'), re.IGNORECASE | ||||
) | ) | ||||
self.fix_re = re.compile( | self.fix_re = re.compile( | ||||
self.ui.config(b'bugzilla', b'fixregexp'), re.IGNORECASE | self.ui.config(b'bugzilla', b'fixregexp'), re.IGNORECASE | ||||
) | ) | ||||
self.split_re = re.compile(br'\D+') | self.split_re = re.compile(br'\D+') | ||||
def find_bugs(self, ctx): | def find_bugs(self, ctx): | ||||
'''return bugs dictionary created from commit comment. | """return bugs dictionary created from commit comment. | ||||
Extract bug info from changeset comments. Filter out any that are | Extract bug info from changeset comments. Filter out any that are | ||||
not known to Bugzilla, and any that already have a reference to | not known to Bugzilla, and any that already have a reference to | ||||
the given changeset in their comments. | the given changeset in their comments. | ||||
''' | """ | ||||
start = 0 | start = 0 | ||||
bugs = {} | bugs = {} | ||||
bugmatch = self.bug_re.search(ctx.description(), start) | bugmatch = self.bug_re.search(ctx.description(), start) | ||||
fixmatch = self.fix_re.search(ctx.description(), start) | fixmatch = self.fix_re.search(ctx.description(), start) | ||||
while True: | while True: | ||||
bugattribs = {} | bugattribs = {} | ||||
if not bugmatch and not fixmatch: | if not bugmatch and not fixmatch: | ||||
break | break | ||||
if bugs: | if bugs: | ||||
self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs) | self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs) | ||||
return bugs | return bugs | ||||
def update(self, bugid, newstate, ctx): | def update(self, bugid, newstate, ctx): | ||||
'''update bugzilla bug with reference to changeset.''' | '''update bugzilla bug with reference to changeset.''' | ||||
def webroot(root): | def webroot(root): | ||||
'''strip leading prefix of repo root and turn into | """strip leading prefix of repo root and turn into | ||||
url-safe path.''' | url-safe path.""" | ||||
count = int(self.ui.config(b'bugzilla', b'strip')) | count = int(self.ui.config(b'bugzilla', b'strip')) | ||||
root = util.pconvert(root) | root = util.pconvert(root) | ||||
while count > 0: | while count > 0: | ||||
c = root.find(b'/') | c = root.find(b'/') | ||||
if c == -1: | if c == -1: | ||||
break | break | ||||
root = root[c + 1 :] | root = root[c + 1 :] | ||||
count -= 1 | count -= 1 | ||||
) | ) | ||||
def notify(self, bugs, committer): | def notify(self, bugs, committer): | ||||
'''ensure Bugzilla users are notified of bug change.''' | '''ensure Bugzilla users are notified of bug change.''' | ||||
self.bzdriver.notify(bugs, committer) | self.bzdriver.notify(bugs, committer) | ||||
def hook(ui, repo, hooktype, node=None, **kwargs): | def hook(ui, repo, hooktype, node=None, **kwargs): | ||||
'''add comment to bugzilla for each changeset that refers to a | """add comment to bugzilla for each changeset that refers to a | ||||
bugzilla bug id. only add a comment once per bug, so same change | bugzilla bug id. only add a comment once per bug, so same change | ||||
seen multiple times does not fill bug with duplicate data.''' | seen multiple times does not fill bug with duplicate data.""" | ||||
if node is None: | if node is None: | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'hook type %s does not pass a changeset id') % hooktype | _(b'hook type %s does not pass a changeset id') % hooktype | ||||
) | ) | ||||
try: | try: | ||||
bz = bugzilla(ui, repo) | bz = bugzilla(ui, repo) | ||||
ctx = repo[node] | ctx = repo[node] | ||||
bugs = bz.find_bugs(ctx) | bugs = bz.find_bugs(ctx) | ||||
if bugs: | if bugs: | ||||
for bug in bugs: | for bug in bugs: | ||||
bz.update(bug, bugs[bug], ctx) | bz.update(bug, bugs[bug], ctx) | ||||
bz.notify(bugs, stringutil.email(ctx.user())) | bz.notify(bugs, stringutil.email(ctx.user())) | ||||
except Exception as e: | except Exception as e: | ||||
raise error.Abort(_(b'Bugzilla error: %s') % e) | raise error.Abort(_(b'Bugzilla error: %s') % e) |
(b'', b'aliases', b'', _(b'file with email aliases'), _(b'FILE')), | (b'', b'aliases', b'', _(b'file with email aliases'), _(b'FILE')), | ||||
] | ] | ||||
+ cmdutil.walkopts, | + cmdutil.walkopts, | ||||
_(b"hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"), | _(b"hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"), | ||||
helpcategory=command.CATEGORY_MAINTENANCE, | helpcategory=command.CATEGORY_MAINTENANCE, | ||||
inferrepo=True, | inferrepo=True, | ||||
) | ) | ||||
def churn(ui, repo, *pats, **opts): | def churn(ui, repo, *pats, **opts): | ||||
'''histogram of changes to the repository | """histogram of changes to the repository | ||||
This command will display a histogram representing the number | This command will display a histogram representing the number | ||||
of changed lines or revisions, grouped according to the given | of changed lines or revisions, grouped according to the given | ||||
template. The default template will group changes by author. | template. The default template will group changes by author. | ||||
The --dateformat option may be used to group the results by | The --dateformat option may be used to group the results by | ||||
date instead. | date instead. | ||||
Statistics are based on the number of changed lines, or | Statistics are based on the number of changed lines, or | ||||
It is possible to map alternate email addresses to a main address | It is possible to map alternate email addresses to a main address | ||||
by providing a file using the following format:: | by providing a file using the following format:: | ||||
<alias email> = <actual email> | <alias email> = <actual email> | ||||
Such a file may be specified with the --aliases option, otherwise | Such a file may be specified with the --aliases option, otherwise | ||||
a .hgchurn file will be looked for in the working directory root. | a .hgchurn file will be looked for in the working directory root. | ||||
Aliases will be split from the rightmost "=". | Aliases will be split from the rightmost "=". | ||||
''' | """ | ||||
def pad(s, l): | def pad(s, l): | ||||
return s + b" " * (l - encoding.colwidth(s)) | return s + b" " * (l - encoding.colwidth(s)) | ||||
amap = {} | amap = {} | ||||
aliases = opts.get('aliases') | aliases = opts.get('aliases') | ||||
if not aliases and os.path.exists(repo.wjoin(b'.hgchurn')): | if not aliases and os.path.exists(repo.wjoin(b'.hgchurn')): | ||||
aliases = repo.wjoin(b'.hgchurn') | aliases = repo.wjoin(b'.hgchurn') |
), | ), | ||||
# Options that are ignored for compatibility with cvsps-2.1 | # Options that are ignored for compatibility with cvsps-2.1 | ||||
(b'A', b'cvs-direct', None, _(b'ignored for compatibility')), | (b'A', b'cvs-direct', None, _(b'ignored for compatibility')), | ||||
], | ], | ||||
_(b'hg debugcvsps [OPTION]... [PATH]...'), | _(b'hg debugcvsps [OPTION]... [PATH]...'), | ||||
norepo=True, | norepo=True, | ||||
) | ) | ||||
def debugcvsps(ui, *args, **opts): | def debugcvsps(ui, *args, **opts): | ||||
'''create changeset information from CVS | """create changeset information from CVS | ||||
This command is intended as a debugging tool for the CVS to | This command is intended as a debugging tool for the CVS to | ||||
Mercurial converter, and can be used as a direct replacement for | Mercurial converter, and can be used as a direct replacement for | ||||
cvsps. | cvsps. | ||||
Hg debugcvsps reads the CVS rlog for current directory (or any | Hg debugcvsps reads the CVS rlog for current directory (or any | ||||
named directory) in the CVS repository, and converts the log to a | named directory) in the CVS repository, and converts the log to a | ||||
series of changesets based on matching commit log entries and | series of changesets based on matching commit log entries and | ||||
dates.''' | dates.""" | ||||
return cvsps.debugcvsps(ui, *args, **opts) | return cvsps.debugcvsps(ui, *args, **opts) | ||||
def kwconverted(context, mapping, name): | def kwconverted(context, mapping, name): | ||||
ctx = context.resource(mapping, b'ctx') | ctx = context.resource(mapping, b'ctx') | ||||
rev = ctx.extra().get(b'convert_revision', b'') | rev = ctx.extra().get(b'convert_revision', b'') | ||||
if rev.startswith(b'svn:'): | if rev.startswith(b'svn:'): | ||||
if name == b'svnrev': | if name == b'svnrev': |
demandimport, | demandimport, | ||||
error, | error, | ||||
pycompat, | pycompat, | ||||
) | ) | ||||
from . import common | from . import common | ||||
# these do not work with demandimport, blacklist | # these do not work with demandimport, blacklist | ||||
demandimport.IGNORES.update( | demandimport.IGNORES.update( | ||||
[b'bzrlib.transactions', b'bzrlib.urlutils', b'ElementPath',] | [ | ||||
b'bzrlib.transactions', | |||||
b'bzrlib.urlutils', | |||||
b'ElementPath', | |||||
] | |||||
) | ) | ||||
try: | try: | ||||
# bazaar imports | # bazaar imports | ||||
import bzrlib.bzrdir | import bzrlib.bzrdir | ||||
import bzrlib.errors | import bzrlib.errors | ||||
import bzrlib.revision | import bzrlib.revision | ||||
import bzrlib.revisionspec | import bzrlib.revisionspec |
self.ui = ui | self.ui = ui | ||||
self.path = path | self.path = path | ||||
self.revs = revs | self.revs = revs | ||||
self.repotype = repotype | self.repotype = repotype | ||||
self.encoding = b'utf-8' | self.encoding = b'utf-8' | ||||
def checkhexformat(self, revstr, mapname=b'splicemap'): | def checkhexformat(self, revstr, mapname=b'splicemap'): | ||||
""" fails if revstr is not a 40 byte hex. mercurial and git both uses | """fails if revstr is not a 40 byte hex. mercurial and git both uses | ||||
such format for their revision numbering | such format for their revision numbering | ||||
""" | """ | ||||
if not re.match(br'[0-9a-fA-F]{40,40}$', revstr): | if not re.match(br'[0-9a-fA-F]{40,40}$', revstr): | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'%s entry %s is not a valid revision identifier') | _(b'%s entry %s is not a valid revision identifier') | ||||
% (mapname, revstr) | % (mapname, revstr) | ||||
) | ) | ||||
def before(self): | def before(self): | ||||
def hasnativeorder(self): | def hasnativeorder(self): | ||||
"""Return true if this source has a meaningful, native revision | """Return true if this source has a meaningful, native revision | ||||
order. For instance, Mercurial revisions are store sequentially | order. For instance, Mercurial revisions are store sequentially | ||||
while there is no such global ordering with Darcs. | while there is no such global ordering with Darcs. | ||||
""" | """ | ||||
return False | return False | ||||
def hasnativeclose(self): | def hasnativeclose(self): | ||||
"""Return true if this source has ability to close branch. | """Return true if this source has ability to close branch.""" | ||||
""" | |||||
return False | return False | ||||
def lookuprev(self, rev): | def lookuprev(self, rev): | ||||
"""If rev is a meaningful revision reference in source, return | """If rev is a meaningful revision reference in source, return | ||||
the referenced identifier in the same format used by getcommit(). | the referenced identifier in the same format used by getcommit(). | ||||
return None otherwise. | return None otherwise. | ||||
""" | """ | ||||
return None | return None | ||||
def getbookmarks(self): | def getbookmarks(self): | ||||
"""Return the bookmarks as a dictionary of name: revision | """Return the bookmarks as a dictionary of name: revision | ||||
Bookmark names are to be UTF-8 strings. | Bookmark names are to be UTF-8 strings. | ||||
""" | """ | ||||
return {} | return {} | ||||
def checkrevformat(self, revstr, mapname=b'splicemap'): | def checkrevformat(self, revstr, mapname=b'splicemap'): | ||||
"""revstr is a string that describes a revision in the given | """revstr is a string that describes a revision in the given | ||||
source control system. Return true if revstr has correct | source control system. Return true if revstr has correct | ||||
format. | format. | ||||
""" | """ | ||||
return True | return True | ||||
class converter_sink(object): | class converter_sink(object): | ||||
"""Conversion sink (target) interface""" | """Conversion sink (target) interface""" | ||||
def __init__(self, ui, repotype, path): | def __init__(self, ui, repotype, path): |
return s.encode(pycompat.sysstr(orig_encoding), 'replace') | return s.encode(pycompat.sysstr(orig_encoding), 'replace') | ||||
else: | else: | ||||
return s.decode('utf-8').encode( | return s.decode('utf-8').encode( | ||||
pycompat.sysstr(orig_encoding), 'replace' | pycompat.sysstr(orig_encoding), 'replace' | ||||
) | ) | ||||
def mapbranch(branch, branchmap): | def mapbranch(branch, branchmap): | ||||
''' | """ | ||||
>>> bmap = {b'default': b'branch1'} | >>> bmap = {b'default': b'branch1'} | ||||
>>> for i in [b'', None]: | >>> for i in [b'', None]: | ||||
... mapbranch(i, bmap) | ... mapbranch(i, bmap) | ||||
'branch1' | 'branch1' | ||||
'branch1' | 'branch1' | ||||
>>> bmap = {b'None': b'branch2'} | >>> bmap = {b'None': b'branch2'} | ||||
>>> for i in [b'', None]: | >>> for i in [b'', None]: | ||||
... mapbranch(i, bmap) | ... mapbranch(i, bmap) | ||||
'branch2' | 'branch2' | ||||
'branch2' | 'branch2' | ||||
>>> bmap = {b'None': b'branch3', b'default': b'branch4'} | >>> bmap = {b'None': b'branch3', b'default': b'branch4'} | ||||
>>> for i in [b'None', b'', None, b'default', b'branch5']: | >>> for i in [b'None', b'', None, b'default', b'branch5']: | ||||
... mapbranch(i, bmap) | ... mapbranch(i, bmap) | ||||
'branch3' | 'branch3' | ||||
'branch4' | 'branch4' | ||||
'branch4' | 'branch4' | ||||
'branch4' | 'branch4' | ||||
'branch5' | 'branch5' | ||||
''' | """ | ||||
# If branch is None or empty, this commit is coming from the source | # If branch is None or empty, this commit is coming from the source | ||||
# repository's default branch and destined for the default branch in the | # repository's default branch and destined for the default branch in the | ||||
# destination repository. For such commits, using a literal "default" | # destination repository. For such commits, using a literal "default" | ||||
# in branchmap below allows the user to map "default" to an alternate | # in branchmap below allows the user to map "default" to an alternate | ||||
# default branch in the destination repository. | # default branch in the destination repository. | ||||
branch = branchmap.get(branch or b'default', branch) | branch = branchmap.get(branch or b'default', branch) | ||||
# At some point we used "None" literal to denote the default branch, | # At some point we used "None" literal to denote the default branch, | ||||
# attempt to use that for backward compatibility. | # attempt to use that for backward compatibility. | ||||
if opts.get(b'authormap'): | if opts.get(b'authormap'): | ||||
self.readauthormap(opts.get(b'authormap')) | self.readauthormap(opts.get(b'authormap')) | ||||
self.authorfile = self.dest.authorfile() | self.authorfile = self.dest.authorfile() | ||||
self.splicemap = self.parsesplicemap(opts.get(b'splicemap')) | self.splicemap = self.parsesplicemap(opts.get(b'splicemap')) | ||||
self.branchmap = mapfile(ui, opts.get(b'branchmap')) | self.branchmap = mapfile(ui, opts.get(b'branchmap')) | ||||
def parsesplicemap(self, path): | def parsesplicemap(self, path): | ||||
""" check and validate the splicemap format and | """check and validate the splicemap format and | ||||
return a child/parents dictionary. | return a child/parents dictionary. | ||||
Format checking has two parts. | Format checking has two parts. | ||||
1. generic format which is same across all source types | 1. generic format which is same across all source types | ||||
2. specific format checking which may be different for | 2. specific format checking which may be different for | ||||
different source type. This logic is implemented in | different source type. This logic is implemented in | ||||
checkrevformat function in source files like | checkrevformat function in source files like | ||||
hg.py, subversion.py etc. | hg.py, subversion.py etc. | ||||
""" | """ | ||||
if not path: | if not path: | ||||
return {} | return {} | ||||
m = {} | m = {} | ||||
try: | try: | ||||
fp = open(path, b'rb') | fp = open(path, b'rb') | ||||
for i, line in enumerate(util.iterfile(fp)): | for i, line in enumerate(util.iterfile(fp)): | ||||
# if file does not exist or error reading, exit | # if file does not exist or error reading, exit | ||||
except IOError: | except IOError: | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'splicemap file not found or error reading %s:') % path | _(b'splicemap file not found or error reading %s:') % path | ||||
) | ) | ||||
return m | return m | ||||
def walktree(self, heads): | def walktree(self, heads): | ||||
'''Return a mapping that identifies the uncommitted parents of every | """Return a mapping that identifies the uncommitted parents of every | ||||
uncommitted changeset.''' | uncommitted changeset.""" | ||||
visit = list(heads) | visit = list(heads) | ||||
known = set() | known = set() | ||||
parents = {} | parents = {} | ||||
numcommits = self.source.numcommits() | numcommits = self.source.numcommits() | ||||
progress = self.ui.makeprogress( | progress = self.ui.makeprogress( | ||||
_(b'scanning'), unit=_(b'revisions'), total=numcommits | _(b'scanning'), unit=_(b'revisions'), total=numcommits | ||||
) | ) | ||||
while visit: | while visit: | ||||
continue | continue | ||||
# Parent is not in dest and not being converted, not good | # Parent is not in dest and not being converted, not good | ||||
if p not in parents: | if p not in parents: | ||||
raise error.Abort(_(b'unknown splice map parent: %s') % p) | raise error.Abort(_(b'unknown splice map parent: %s') % p) | ||||
pc.append(p) | pc.append(p) | ||||
parents[c] = pc | parents[c] = pc | ||||
def toposort(self, parents, sortmode): | def toposort(self, parents, sortmode): | ||||
'''Return an ordering such that every uncommitted changeset is | """Return an ordering such that every uncommitted changeset is | ||||
preceded by all its uncommitted ancestors.''' | preceded by all its uncommitted ancestors.""" | ||||
def mapchildren(parents): | def mapchildren(parents): | ||||
"""Return a (children, roots) tuple where 'children' maps parent | """Return a (children, roots) tuple where 'children' maps parent | ||||
revision identifiers to children ones, and 'roots' is the list of | revision identifiers to children ones, and 'roots' is the list of | ||||
revisions without parents. 'parents' must be a mapping of revision | revisions without parents. 'parents' must be a mapping of revision | ||||
identifier to its parents ones. | identifier to its parents ones. | ||||
""" | """ | ||||
visit = collections.deque(sorted(parents)) | visit = collections.deque(sorted(parents)) |
procutil, | procutil, | ||||
stringutil, | stringutil, | ||||
) | ) | ||||
pickle = util.pickle | pickle = util.pickle | ||||
class logentry(object): | class logentry(object): | ||||
'''Class logentry has the following attributes: | """Class logentry has the following attributes: | ||||
.author - author name as CVS knows it | .author - author name as CVS knows it | ||||
.branch - name of branch this revision is on | .branch - name of branch this revision is on | ||||
.branches - revision tuple of branches starting at this revision | .branches - revision tuple of branches starting at this revision | ||||
.comment - commit message | .comment - commit message | ||||
.commitid - CVS commitid or None | .commitid - CVS commitid or None | ||||
.date - the commit date as a (time, tz) tuple | .date - the commit date as a (time, tz) tuple | ||||
.dead - true if file revision is dead | .dead - true if file revision is dead | ||||
.file - Name of file | .file - Name of file | ||||
.lines - a tuple (+lines, -lines) or None | .lines - a tuple (+lines, -lines) or None | ||||
.parent - Previous revision of this entry | .parent - Previous revision of this entry | ||||
.rcs - name of file as returned from CVS | .rcs - name of file as returned from CVS | ||||
.revision - revision number as tuple | .revision - revision number as tuple | ||||
.tags - list of tags on the file | .tags - list of tags on the file | ||||
.synthetic - is this a synthetic "file ... added on ..." revision? | .synthetic - is this a synthetic "file ... added on ..." revision? | ||||
.mergepoint - the branch that has been merged from (if present in | .mergepoint - the branch that has been merged from (if present in | ||||
rlog output) or None | rlog output) or None | ||||
.branchpoints - the branches that start at the current entry or empty | .branchpoints - the branches that start at the current entry or empty | ||||
''' | """ | ||||
def __init__(self, **entries): | def __init__(self, **entries): | ||||
self.synthetic = False | self.synthetic = False | ||||
self.__dict__.update(entries) | self.__dict__.update(entries) | ||||
def __repr__(self): | def __repr__(self): | ||||
items = ("%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__)) | items = ("%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__)) | ||||
return "%s(%s)" % (type(self).__name__, ", ".join(items)) | return "%s(%s)" % (type(self).__name__, ", ".join(items)) | ||||
) | ) | ||||
hook.hook(ui, None, b"cvslog", True, log=log) | hook.hook(ui, None, b"cvslog", True, log=log) | ||||
return log | return log | ||||
class changeset(object): | class changeset(object): | ||||
'''Class changeset has the following attributes: | """Class changeset has the following attributes: | ||||
.id - integer identifying this changeset (list index) | .id - integer identifying this changeset (list index) | ||||
.author - author name as CVS knows it | .author - author name as CVS knows it | ||||
.branch - name of branch this changeset is on, or None | .branch - name of branch this changeset is on, or None | ||||
.comment - commit message | .comment - commit message | ||||
.commitid - CVS commitid or None | .commitid - CVS commitid or None | ||||
.date - the commit date as a (time,tz) tuple | .date - the commit date as a (time,tz) tuple | ||||
.entries - list of logentry objects in this changeset | .entries - list of logentry objects in this changeset | ||||
.parents - list of one or two parent changesets | .parents - list of one or two parent changesets | ||||
.tags - list of tags on this changeset | .tags - list of tags on this changeset | ||||
.synthetic - from synthetic revision "file ... added on branch ..." | .synthetic - from synthetic revision "file ... added on branch ..." | ||||
.mergepoint- the branch that has been merged from or None | .mergepoint- the branch that has been merged from or None | ||||
.branchpoints- the branches that start at the current entry or empty | .branchpoints- the branches that start at the current entry or empty | ||||
''' | """ | ||||
def __init__(self, **entries): | def __init__(self, **entries): | ||||
self.id = None | self.id = None | ||||
self.synthetic = False | self.synthetic = False | ||||
self.__dict__.update(entries) | self.__dict__.update(entries) | ||||
def __repr__(self): | def __repr__(self): | ||||
items = ( | items = ( | ||||
ui.status(_(b'%d changeset entries\n') % len(changesets)) | ui.status(_(b'%d changeset entries\n') % len(changesets)) | ||||
hook.hook(ui, None, b"cvschangesets", True, changesets=changesets) | hook.hook(ui, None, b"cvschangesets", True, changesets=changesets) | ||||
return changesets | return changesets | ||||
def debugcvsps(ui, *args, **opts): | def debugcvsps(ui, *args, **opts): | ||||
'''Read CVS rlog for current directory or named path in | """Read CVS rlog for current directory or named path in | ||||
repository, and convert the log to changesets based on matching | repository, and convert the log to changesets based on matching | ||||
commit log entries and dates. | commit log entries and dates. | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
if opts[b"new_cache"]: | if opts[b"new_cache"]: | ||||
cache = b"write" | cache = b"write" | ||||
elif opts[b"update_cache"]: | elif opts[b"update_cache"]: | ||||
cache = b"update" | cache = b"update" | ||||
else: | else: | ||||
cache = None | cache = None | ||||
pycompat, | pycompat, | ||||
) | ) | ||||
from . import common | from . import common | ||||
SKIPREV = common.SKIPREV | SKIPREV = common.SKIPREV | ||||
def rpairs(path): | def rpairs(path): | ||||
'''Yield tuples with path split at '/', starting with the full path. | """Yield tuples with path split at '/', starting with the full path. | ||||
No leading, trailing or double '/', please. | No leading, trailing or double '/', please. | ||||
>>> for x in rpairs(b'foo/bar/baz'): print(x) | >>> for x in rpairs(b'foo/bar/baz'): print(x) | ||||
('foo/bar/baz', '') | ('foo/bar/baz', '') | ||||
('foo/bar', 'baz') | ('foo/bar', 'baz') | ||||
('foo', 'bar/baz') | ('foo', 'bar/baz') | ||||
('.', 'foo/bar/baz') | ('.', 'foo/bar/baz') | ||||
''' | """ | ||||
i = len(path) | i = len(path) | ||||
while i != -1: | while i != -1: | ||||
yield path[:i], path[i + 1 :] | yield path[:i], path[i + 1 :] | ||||
i = path.rfind(b'/', 0, i) | i = path.rfind(b'/', 0, i) | ||||
yield b'.', path | yield b'.', path | ||||
def normalize(path): | def normalize(path): | ||||
''' We use posixpath.normpath to support cross-platform path format. | """We use posixpath.normpath to support cross-platform path format. | ||||
However, it doesn't handle None input. So we wrap it up. ''' | However, it doesn't handle None input. So we wrap it up.""" | ||||
if path is None: | if path is None: | ||||
return None | return None | ||||
return posixpath.normpath(path) | return posixpath.normpath(path) | ||||
class filemapper(object): | class filemapper(object): | ||||
'''Map and filter filenames when importing. | """Map and filter filenames when importing. | ||||
A name can be mapped to itself, a new name, or None (omit from new | A name can be mapped to itself, a new name, or None (omit from new | ||||
repository).''' | repository).""" | ||||
def __init__(self, ui, path=None): | def __init__(self, ui, path=None): | ||||
self.ui = ui | self.ui = ui | ||||
self.include = {} | self.include = {} | ||||
self.exclude = {} | self.exclude = {} | ||||
self.rename = {} | self.rename = {} | ||||
self.targetprefixes = None | self.targetprefixes = None | ||||
if path: | if path: |
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'eol', b'fix-trailing-newline', default=False, | b'eol', | ||||
b'fix-trailing-newline', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'eol', b'native', default=pycompat.oslinesep, | b'eol', | ||||
b'native', | |||||
default=pycompat.oslinesep, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'eol', b'only-consistent', default=True, | b'eol', | ||||
b'only-consistent', | |||||
default=True, | |||||
) | ) | ||||
# Matches a lone LF, i.e., one that is not part of CRLF. | # Matches a lone LF, i.e., one that is not part of CRLF. | ||||
singlelf = re.compile(b'(^|[^\r])\n') | singlelf = re.compile(b'(^|[^\r])\n') | ||||
def inconsistenteol(data): | def inconsistenteol(data): | ||||
return b'\r\n' in data and singlelf.search(data) | return b'\r\n' in data and singlelf.search(data) |
cmdtable = {} | cmdtable = {} | ||||
command = registrar.command(cmdtable) | command = registrar.command(cmdtable) | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'extdiff', br'opts\..*', default=b'', generic=True, | b'extdiff', | ||||
br'opts\..*', | |||||
default=b'', | |||||
generic=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'extdiff', br'gui\..*', generic=True, | b'extdiff', | ||||
br'gui\..*', | |||||
generic=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'diff-tools', br'.*\.diffargs$', default=None, generic=True, | b'diff-tools', | ||||
br'.*\.diffargs$', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'diff-tools', br'.*\.gui$', generic=True, | b'diff-tools', | ||||
br'.*\.gui$', | |||||
generic=True, | |||||
) | ) | ||||
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||||
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||||
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
def snapshot(ui, repo, files, node, tmproot, listsubrepos): | def snapshot(ui, repo, files, node, tmproot, listsubrepos): | ||||
'''snapshot files as of some revision | """snapshot files as of some revision | ||||
if not using snapshot, -I/-X does not work and recursive diff | if not using snapshot, -I/-X does not work and recursive diff | ||||
in tools like kdiff3 and meld displays too many files.''' | in tools like kdiff3 and meld displays too many files.""" | ||||
dirname = os.path.basename(repo.root) | dirname = os.path.basename(repo.root) | ||||
if dirname == b"": | if dirname == b"": | ||||
dirname = b"root" | dirname = b"root" | ||||
if node is not None: | if node is not None: | ||||
dirname = b'%s.%s' % (dirname, short(node)) | dirname = b'%s.%s' % (dirname, short(node)) | ||||
base = os.path.join(tmproot, dirname) | base = os.path.join(tmproot, dirname) | ||||
os.mkdir(base) | os.mkdir(base) | ||||
fnsandstat = [] | fnsandstat = [] | ||||
br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1' | br'\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)\1' | ||||
) | ) | ||||
if not do3way and not re.search(regex, cmdline): | if not do3way and not re.search(regex, cmdline): | ||||
cmdline += b' $parent1 $child' | cmdline += b' $parent1 $child' | ||||
return re.sub(regex, quote, cmdline) | return re.sub(regex, quote, cmdline) | ||||
def _systembackground(cmd, environ=None, cwd=None): | def _systembackground(cmd, environ=None, cwd=None): | ||||
''' like 'procutil.system', but returns the Popen object directly | """like 'procutil.system', but returns the Popen object directly | ||||
so we don't have to wait on it. | so we don't have to wait on it. | ||||
''' | """ | ||||
env = procutil.shellenviron(environ) | env = procutil.shellenviron(environ) | ||||
proc = subprocess.Popen( | proc = subprocess.Popen( | ||||
procutil.tonativestr(cmd), | procutil.tonativestr(cmd), | ||||
shell=True, | shell=True, | ||||
close_fds=procutil.closefds, | close_fds=procutil.closefds, | ||||
env=procutil.tonativeenv(env), | env=procutil.tonativeenv(env), | ||||
cwd=pycompat.rapply(procutil.tonativestr, cwd), | cwd=pycompat.rapply(procutil.tonativestr, cwd), | ||||
) | ) | ||||
b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn) | b'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn) | ||||
) | ) | ||||
util.copyfile(copy_fn, working_fn) | util.copyfile(copy_fn, working_fn) | ||||
return 1 | return 1 | ||||
def dodiff(ui, repo, cmdline, pats, opts, guitool=False): | def dodiff(ui, repo, cmdline, pats, opts, guitool=False): | ||||
'''Do the actual diff: | """Do the actual diff: | ||||
- copy to a temp structure if diffing 2 internal revisions | - copy to a temp structure if diffing 2 internal revisions | ||||
- copy to a temp structure if diffing working revision with | - copy to a temp structure if diffing working revision with | ||||
another one and more than 1 file is changed | another one and more than 1 file is changed | ||||
- just invoke the diff for a single file in the working dir | - just invoke the diff for a single file in the working dir | ||||
''' | """ | ||||
cmdutil.check_at_most_one_arg(opts, b'rev', b'change') | cmdutil.check_at_most_one_arg(opts, b'rev', b'change') | ||||
revs = opts.get(b'rev') | revs = opts.get(b'rev') | ||||
change = opts.get(b'change') | change = opts.get(b'change') | ||||
do3way = b'$parent2' in cmdline | do3way = b'$parent2' in cmdline | ||||
if change: | if change: | ||||
ctx2 = scmutil.revsingle(repo, change, None) | ctx2 = scmutil.revsingle(repo, change, None) | ||||
] | ] | ||||
+ cmdutil.walkopts | + cmdutil.walkopts | ||||
+ cmdutil.subrepoopts | + cmdutil.subrepoopts | ||||
) | ) | ||||
@command( | @command( | ||||
b'extdiff', | b'extdiff', | ||||
[(b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')),] | [ | ||||
(b'p', b'program', b'', _(b'comparison program to run'), _(b'CMD')), | |||||
] | |||||
+ extdiffopts, | + extdiffopts, | ||||
_(b'hg extdiff [OPT]... [FILE]...'), | _(b'hg extdiff [OPT]... [FILE]...'), | ||||
helpcategory=command.CATEGORY_FILE_CONTENTS, | helpcategory=command.CATEGORY_FILE_CONTENTS, | ||||
inferrepo=True, | inferrepo=True, | ||||
) | ) | ||||
def extdiff(ui, repo, *pats, **opts): | def extdiff(ui, repo, *pats, **opts): | ||||
'''use external program to diff repository (or selected files) | """use external program to diff repository (or selected files) | ||||
Show differences between revisions for the specified files, using | Show differences between revisions for the specified files, using | ||||
an external program. The default program used is diff, with | an external program. The default program used is diff, with | ||||
default options "-Npru". | default options "-Npru". | ||||
To select a different program, use the -p/--program option. The | To select a different program, use the -p/--program option. The | ||||
program will be passed the names of two directories to compare, | program will be passed the names of two directories to compare, | ||||
unless the --per-file option is specified (see below). To pass | unless the --per-file option is specified (see below). To pass | ||||
external program only once the previous external program (for the | external program only once the previous external program (for the | ||||
previous file diff) has exited. If the external program has a | previous file diff) has exited. If the external program has a | ||||
graphical interface, it can open all the file diffs at once instead | graphical interface, it can open all the file diffs at once instead | ||||
of one by one. See :hg:`help -e extdiff` for information about how | of one by one. See :hg:`help -e extdiff` for information about how | ||||
to tell Mercurial that a given program has a graphical interface. | to tell Mercurial that a given program has a graphical interface. | ||||
The --confirm option will prompt the user before each invocation of | The --confirm option will prompt the user before each invocation of | ||||
the external program. It is ignored if --per-file isn't specified. | the external program. It is ignored if --per-file isn't specified. | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
program = opts.get(b'program') | program = opts.get(b'program') | ||||
option = opts.get(b'option') | option = opts.get(b'option') | ||||
if not program: | if not program: | ||||
program = b'diff' | program = b'diff' | ||||
option = option or [b'-Npru'] | option = option or [b'-Npru'] | ||||
cmdline = b' '.join(map(procutil.shellquote, [program] + option)) | cmdline = b' '.join(map(procutil.shellquote, [program] + option)) | ||||
return dodiff(ui, repo, cmdline, pats, opts) | return dodiff(ui, repo, cmdline, pats, opts) |
ERRMAX = 128 | ERRMAX = 128 | ||||
_executable = _mountpoint = _service = None | _executable = _mountpoint = _service = None | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'factotum', b'executable', default=b'/bin/auth/factotum', | b'factotum', | ||||
b'executable', | |||||
default=b'/bin/auth/factotum', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'factotum', b'mountpoint', default=b'/mnt/factotum', | b'factotum', | ||||
b'mountpoint', | |||||
default=b'/mnt/factotum', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'factotum', b'service', default=b'hg', | b'factotum', | ||||
b'service', | |||||
default=b'hg', | |||||
) | ) | ||||
def auth_getkey(self, params): | def auth_getkey(self, params): | ||||
if not self.ui.interactive(): | if not self.ui.interactive(): | ||||
raise error.Abort(_(b'factotum not interactive')) | raise error.Abort(_(b'factotum not interactive')) | ||||
if b'user=' not in params: | if b'user=' not in params: | ||||
params = b'%s user?' % params | params = b'%s user?' % params |
] | ] | ||||
+ cmdutil.commitopts | + cmdutil.commitopts | ||||
+ cmdutil.commitopts2 | + cmdutil.commitopts2 | ||||
+ cmdutil.remoteopts, | + cmdutil.remoteopts, | ||||
_(b'hg fetch [SOURCE]'), | _(b'hg fetch [SOURCE]'), | ||||
helpcategory=command.CATEGORY_REMOTE_REPO_MANAGEMENT, | helpcategory=command.CATEGORY_REMOTE_REPO_MANAGEMENT, | ||||
) | ) | ||||
def fetch(ui, repo, source=b'default', **opts): | def fetch(ui, repo, source=b'default', **opts): | ||||
'''pull changes from a remote repository, merge new changes if needed. | """pull changes from a remote repository, merge new changes if needed. | ||||
This finds all changes from the repository at the specified path | This finds all changes from the repository at the specified path | ||||
or URL and adds them to the local repository. | or URL and adds them to the local repository. | ||||
If the pulled changes add a new branch head, the head is | If the pulled changes add a new branch head, the head is | ||||
automatically merged, and the result of the merge is committed. | automatically merged, and the result of the merge is committed. | ||||
Otherwise, the working directory is updated to include the new | Otherwise, the working directory is updated to include the new | ||||
changes. | changes. | ||||
When a merge is needed, the working directory is first updated to | When a merge is needed, the working directory is first updated to | ||||
the newly pulled changes. Local changes are then merged into the | the newly pulled changes. Local changes are then merged into the | ||||
pulled changes. To switch the merge order, use --switch-parent. | pulled changes. To switch the merge order, use --switch-parent. | ||||
See :hg:`help dates` for a list of formats valid for -d/--date. | See :hg:`help dates` for a list of formats valid for -d/--date. | ||||
Returns 0 on success. | Returns 0 on success. | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
date = opts.get(b'date') | date = opts.get(b'date') | ||||
if date: | if date: | ||||
opts[b'date'] = dateutil.parsedate(date) | opts[b'date'] = dateutil.parsedate(date) | ||||
parent = repo.dirstate.p1() | parent = repo.dirstate.p1() | ||||
branch = repo.dirstate.branch() | branch = repo.dirstate.branch() |
""" | """ | ||||
replacements = { | replacements = { | ||||
prec: [succ] for prec, succ in pycompat.iteritems(replacements) | prec: [succ] for prec, succ in pycompat.iteritems(replacements) | ||||
} | } | ||||
scmutil.cleanupnodes(repo, replacements, b'fix', fixphase=True) | scmutil.cleanupnodes(repo, replacements, b'fix', fixphase=True) | ||||
def getworkqueue(ui, repo, pats, opts, revstofix, basectxs): | def getworkqueue(ui, repo, pats, opts, revstofix, basectxs): | ||||
""""Constructs the list of files to be fixed at specific revisions | """ "Constructs the list of files to be fixed at specific revisions | ||||
It is up to the caller how to consume the work items, and the only | It is up to the caller how to consume the work items, and the only | ||||
dependence between them is that replacement revisions must be committed in | dependence between them is that replacement revisions must be committed in | ||||
topological order. Each work item represents a file in the working copy or | topological order. Each work item represents a file in the working copy or | ||||
in some revision that should be fixed and written back to the working copy | in some revision that should be fixed and written back to the working copy | ||||
or into a replacement revision. | or into a replacement revision. | ||||
Work items for the same revision are grouped together, so that a worker | Work items for the same revision are grouped together, so that a worker |
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'fsmonitor', b'mode', default=b'on', | b'fsmonitor', | ||||
b'mode', | |||||
default=b'on', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'fsmonitor', b'walk_on_invalidate', default=False, | b'fsmonitor', | ||||
b'walk_on_invalidate', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'fsmonitor', b'timeout', default=b'2', | b'fsmonitor', | ||||
b'timeout', | |||||
default=b'2', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'fsmonitor', b'blacklistusers', default=list, | b'fsmonitor', | ||||
b'blacklistusers', | |||||
default=list, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'fsmonitor', b'watchman_exe', default=b'watchman', | b'fsmonitor', | ||||
b'watchman_exe', | |||||
default=b'watchman', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'fsmonitor', b'verbose', default=True, experimental=True, | b'fsmonitor', | ||||
b'verbose', | |||||
default=True, | |||||
experimental=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'experimental', b'fsmonitor.transaction_notify', default=False, | b'experimental', | ||||
b'fsmonitor.transaction_notify', | |||||
default=False, | |||||
) | ) | ||||
# This extension is incompatible with the following blacklisted extensions | # This extension is incompatible with the following blacklisted extensions | ||||
# and will disable itself when encountering one of these: | # and will disable itself when encountering one of these: | ||||
_blacklist = [b'largefiles', b'eol'] | _blacklist = [b'largefiles', b'eol'] | ||||
def debuginstall(ui, fm): | def debuginstall(ui, fm): | ||||
encoded = decoded.encode(_fsencoding, 'strict') | encoded = decoded.encode(_fsencoding, 'strict') | ||||
except UnicodeEncodeError as e: | except UnicodeEncodeError as e: | ||||
raise error.Abort(stringutil.forcebytestr(e)) | raise error.Abort(stringutil.forcebytestr(e)) | ||||
return encoded | return encoded | ||||
def overridewalk(orig, self, match, subrepos, unknown, ignored, full=True): | def overridewalk(orig, self, match, subrepos, unknown, ignored, full=True): | ||||
'''Replacement for dirstate.walk, hooking into Watchman. | """Replacement for dirstate.walk, hooking into Watchman. | ||||
Whenever full is False, ignored is False, and the Watchman client is | Whenever full is False, ignored is False, and the Watchman client is | ||||
available, use Watchman combined with saved state to possibly return only a | available, use Watchman combined with saved state to possibly return only a | ||||
subset of files.''' | subset of files.""" | ||||
def bail(reason): | def bail(reason): | ||||
self._ui.debug(b'fsmonitor: fallback to core status, %s\n' % reason) | self._ui.debug(b'fsmonitor: fallback to core status, %s\n' % reason) | ||||
return orig(match, subrepos, unknown, ignored, full=True) | return orig(match, subrepos, unknown, ignored, full=True) | ||||
if full: | if full: | ||||
return bail(b'full rewalk requested') | return bail(b'full rewalk requested') | ||||
if ignored: | if ignored: | ||||
if pycompat.isdarwin: | if pycompat.isdarwin: | ||||
# An assist for avoiding the dangling-symlink fsevents bug | # An assist for avoiding the dangling-symlink fsevents bug | ||||
extensions.wrapfunction(os, b'symlink', wrapsymlink) | extensions.wrapfunction(os, b'symlink', wrapsymlink) | ||||
extensions.wrapfunction(merge, b'_update', wrapupdate) | extensions.wrapfunction(merge, b'_update', wrapupdate) | ||||
def wrapsymlink(orig, source, link_name): | def wrapsymlink(orig, source, link_name): | ||||
''' if we create a dangling symlink, also touch the parent dir | """if we create a dangling symlink, also touch the parent dir | ||||
to encourage fsevents notifications to work more correctly ''' | to encourage fsevents notifications to work more correctly""" | ||||
try: | try: | ||||
return orig(source, link_name) | return orig(source, link_name) | ||||
finally: | finally: | ||||
try: | try: | ||||
os.utime(os.path.dirname(link_name), None) | os.utime(os.path.dirname(link_name), None) | ||||
except OSError: | except OSError: | ||||
pass | pass | ||||
class state_update(object): | class state_update(object): | ||||
''' This context manager is responsible for dispatching the state-enter | """This context manager is responsible for dispatching the state-enter | ||||
and state-leave signals to the watchman service. The enter and leave | and state-leave signals to the watchman service. The enter and leave | ||||
methods can be invoked manually (for scenarios where context manager | methods can be invoked manually (for scenarios where context manager | ||||
semantics are not possible). If parameters oldnode and newnode are None, | semantics are not possible). If parameters oldnode and newnode are None, | ||||
they will be populated based on current working copy in enter and | they will be populated based on current working copy in enter and | ||||
leave, respectively. Similarly, if the distance is none, it will be | leave, respectively. Similarly, if the distance is none, it will be | ||||
calculated based on the oldnode and newnode in the leave method.''' | calculated based on the oldnode and newnode in the leave method.""" | ||||
def __init__( | def __init__( | ||||
self, | self, | ||||
repo, | repo, | ||||
name, | name, | ||||
oldnode=None, | oldnode=None, | ||||
newnode=None, | newnode=None, | ||||
distance=None, | distance=None, |
"unable to connect to %s: %s" % (sockpath, exc) | "unable to connect to %s: %s" % (sockpath, exc) | ||||
) | ) | ||||
self.sockpath = sockpath | self.sockpath = sockpath | ||||
self.exc = exc | self.exc = exc | ||||
class SocketTimeout(WatchmanError): | class SocketTimeout(WatchmanError): | ||||
"""A specialized exception raised for socket timeouts during communication to/from watchman. | """A specialized exception raised for socket timeouts during communication to/from watchman. | ||||
This makes it easier to implement non-blocking loops as callers can easily distinguish | This makes it easier to implement non-blocking loops as callers can easily distinguish | ||||
between a routine timeout and an actual error condition. | between a routine timeout and an actual error condition. | ||||
Note that catching WatchmanError will also catch this as it is a super-class, so backwards | Note that catching WatchmanError will also catch this as it is a super-class, so backwards | ||||
compatibility in exception handling is preserved. | compatibility in exception handling is preserved. | ||||
""" | """ | ||||
class CommandError(WatchmanError): | class CommandError(WatchmanError): | ||||
"""error returned by watchman | """error returned by watchman | ||||
self.msg is the message returned by watchman. | self.msg is the message returned by watchman. | ||||
""" | """ | ||||
def write(self, buf): | def write(self, buf): | ||||
""" write some data """ | """ write some data """ | ||||
raise NotImplementedError() | raise NotImplementedError() | ||||
def setTimeout(self, value): | def setTimeout(self, value): | ||||
pass | pass | ||||
def readLine(self): | def readLine(self): | ||||
""" read a line | """read a line | ||||
Maintains its own buffer, callers of the transport should not mix | Maintains its own buffer, callers of the transport should not mix | ||||
calls to readBytes and readLine. | calls to readBytes and readLine. | ||||
""" | """ | ||||
if self.buf is None: | if self.buf is None: | ||||
self.buf = [] | self.buf = [] | ||||
# Buffer may already have a line if we've received unilateral | # Buffer may already have a line if we've received unilateral | ||||
# response(s) from the server | # response(s) from the server | ||||
def write(self, data): | def write(self, data): | ||||
try: | try: | ||||
self.sock.sendall(data) | self.sock.sendall(data) | ||||
except socket.timeout: | except socket.timeout: | ||||
raise SocketTimeout("timed out sending query command") | raise SocketTimeout("timed out sending query command") | ||||
def _get_overlapped_result_ex_impl(pipe, olap, nbytes, millis, alertable): | def _get_overlapped_result_ex_impl(pipe, olap, nbytes, millis, alertable): | ||||
""" Windows 7 and earlier does not support GetOverlappedResultEx. The | """Windows 7 and earlier does not support GetOverlappedResultEx. The | ||||
alternative is to use GetOverlappedResult and wait for read or write | alternative is to use GetOverlappedResult and wait for read or write | ||||
operation to complete. This is done be using CreateEvent and | operation to complete. This is done be using CreateEvent and | ||||
WaitForSingleObjectEx. CreateEvent, WaitForSingleObjectEx | WaitForSingleObjectEx. CreateEvent, WaitForSingleObjectEx | ||||
and GetOverlappedResult are all part of Windows API since WindowsXP. | and GetOverlappedResult are all part of Windows API since WindowsXP. | ||||
This is the exact same implementation that can be found in the watchman | This is the exact same implementation that can be found in the watchman | ||||
source code (see get_overlapped_result_ex_impl in stream_win.c). This | source code (see get_overlapped_result_ex_impl in stream_win.c). This | ||||
way, maintenance should be simplified. | way, maintenance should be simplified. | ||||
""" | """ | ||||
CloseHandle(self._waitable) | CloseHandle(self._waitable) | ||||
self._waitable = None | self._waitable = None | ||||
def setTimeout(self, value): | def setTimeout(self, value): | ||||
# convert to milliseconds | # convert to milliseconds | ||||
self.timeout = int(value * 1000) | self.timeout = int(value * 1000) | ||||
def readBytes(self, size): | def readBytes(self, size): | ||||
""" A read can block for an unbounded amount of time, even if the | """A read can block for an unbounded amount of time, even if the | ||||
kernel reports that the pipe handle is signalled, so we need to | kernel reports that the pipe handle is signalled, so we need to | ||||
always perform our reads asynchronously | always perform our reads asynchronously | ||||
""" | """ | ||||
# try to satisfy the read from any buffered data | # try to satisfy the read from any buffered data | ||||
if self._iobuf: | if self._iobuf: | ||||
if size >= len(self._iobuf): | if size >= len(self._iobuf): | ||||
res = self._iobuf | res = self._iobuf | ||||
self.buf = None | self.buf = None | ||||
return res | return res | ||||
return binpath | return binpath | ||||
# The test harness sets WATCHMAN_BINARY to the binary under test, | # The test harness sets WATCHMAN_BINARY to the binary under test, | ||||
# so we use that by default, otherwise, allow resolving watchman | # so we use that by default, otherwise, allow resolving watchman | ||||
# from the users PATH. | # from the users PATH. | ||||
return os.environ.get("WATCHMAN_BINARY", "watchman") | return os.environ.get("WATCHMAN_BINARY", "watchman") | ||||
class CLIProcessTransport(Transport): | class CLIProcessTransport(Transport): | ||||
""" open a pipe to the cli to talk to the service | """open a pipe to the cli to talk to the service | ||||
This intended to be used only in the test harness! | This intended to be used only in the test harness! | ||||
The CLI is an oddball because we only support JSON input | The CLI is an oddball because we only support JSON input | ||||
and cannot send multiple commands through the same instance, | and cannot send multiple commands through the same instance, | ||||
so we spawn a new process for each command. | so we spawn a new process for each command. | ||||
We disable server spawning for this implementation, again, because | We disable server spawning for this implementation, again, because | ||||
it is intended to be used only in our test harness. You really | it is intended to be used only in our test harness. You really | ||||
raise WatchmanError("watchman response decode error: %s" % e) | raise WatchmanError("watchman response decode error: %s" % e) | ||||
def send(self, *args): | def send(self, *args): | ||||
cmd = bser.dumps(*args) # Defaults to BSER v1 | cmd = bser.dumps(*args) # Defaults to BSER v1 | ||||
self.transport.write(cmd) | self.transport.write(cmd) | ||||
class ImmutableBserCodec(BserCodec): | class ImmutableBserCodec(BserCodec): | ||||
""" use the BSER encoding, decoding values using the newer | """use the BSER encoding, decoding values using the newer | ||||
immutable object support """ | immutable object support""" | ||||
def _loads(self, response): | def _loads(self, response): | ||||
return bser.loads( | return bser.loads( | ||||
response, | response, | ||||
False, | False, | ||||
value_encoding=self._value_encoding, | value_encoding=self._value_encoding, | ||||
value_errors=self._value_errors, | value_errors=self._value_errors, | ||||
) | ) | ||||
capabilities=self.bser_capabilities | capabilities=self.bser_capabilities | ||||
) | ) | ||||
else: | else: | ||||
cmd = bser.dumps(*args) | cmd = bser.dumps(*args) | ||||
self.transport.write(cmd) | self.transport.write(cmd) | ||||
class ImmutableBser2Codec(Bser2WithFallbackCodec, ImmutableBserCodec): | class ImmutableBser2Codec(Bser2WithFallbackCodec, ImmutableBserCodec): | ||||
""" use the BSER encoding, decoding values using the newer | """use the BSER encoding, decoding values using the newer | ||||
immutable object support """ | immutable object support""" | ||||
pass | pass | ||||
class JsonCodec(Codec): | class JsonCodec(Codec): | ||||
""" Use json codec. This is here primarily for testing purposes """ | """ Use json codec. This is here primarily for testing purposes """ | ||||
json = None | json = None | ||||
def close(self): | def close(self): | ||||
if self.tport: | if self.tport: | ||||
self.tport.close() | self.tport.close() | ||||
self.tport = None | self.tport = None | ||||
self.recvConn = None | self.recvConn = None | ||||
self.sendConn = None | self.sendConn = None | ||||
def receive(self): | def receive(self): | ||||
""" receive the next PDU from the watchman service | """receive the next PDU from the watchman service | ||||
If the client has activated subscriptions or logs then | If the client has activated subscriptions or logs then | ||||
this PDU may be a unilateral PDU sent by the service to | this PDU may be a unilateral PDU sent by the service to | ||||
inform the client of a log event or subscription change. | inform the client of a log event or subscription change. | ||||
It may also simply be the response portion of a request | It may also simply be the response portion of a request | ||||
initiated by query. | initiated by query. | ||||
return True | return True | ||||
# Fall back to checking for known unilateral responses | # Fall back to checking for known unilateral responses | ||||
for k in self.unilateral: | for k in self.unilateral: | ||||
if k in res: | if k in res: | ||||
return True | return True | ||||
return False | return False | ||||
def getLog(self, remove=True): | def getLog(self, remove=True): | ||||
""" Retrieve buffered log data | """Retrieve buffered log data | ||||
If remove is true the data will be removed from the buffer. | If remove is true the data will be removed from the buffer. | ||||
Otherwise it will be left in the buffer | Otherwise it will be left in the buffer | ||||
""" | """ | ||||
res = self.logs | res = self.logs | ||||
if remove: | if remove: | ||||
self.logs = [] | self.logs = [] | ||||
return res | return res | ||||
def getSubscription(self, name, remove=True, root=None): | def getSubscription(self, name, remove=True, root=None): | ||||
""" Retrieve the data associated with a named subscription | """Retrieve the data associated with a named subscription | ||||
If remove is True (the default), the subscription data is removed | If remove is True (the default), the subscription data is removed | ||||
from the buffer. Otherwise the data is returned but left in | from the buffer. Otherwise the data is returned but left in | ||||
the buffer. | the buffer. | ||||
Returns None if there is no data associated with `name` | Returns None if there is no data associated with `name` | ||||
If root is not None, then only return the subscription | If root is not None, then only return the subscription | ||||
if name not in self.subs: | if name not in self.subs: | ||||
return None | return None | ||||
sub = self.subs[name] | sub = self.subs[name] | ||||
if remove: | if remove: | ||||
del self.subs[name] | del self.subs[name] | ||||
return sub | return sub | ||||
def query(self, *args): | def query(self, *args): | ||||
""" Send a query to the watchman service and return the response | """Send a query to the watchman service and return the response | ||||
This call will block until the response is returned. | This call will block until the response is returned. | ||||
If any unilateral responses are sent by the service in between | If any unilateral responses are sent by the service in between | ||||
the request-response they will be buffered up in the client object | the request-response they will be buffered up in the client object | ||||
and NOT returned via this method. | and NOT returned via this method. | ||||
""" | """ | ||||
log("calling client.query") | log("calling client.query") |
def check(version, name): | def check(version, name): | ||||
if name in cap_versions: | if name in cap_versions: | ||||
return version >= parse_version(cap_versions[name]) | return version >= parse_version(cap_versions[name]) | ||||
return False | return False | ||||
def synthesize(vers, opts): | def synthesize(vers, opts): | ||||
""" Synthesize a capability enabled version response | """Synthesize a capability enabled version response | ||||
This is a very limited emulation for relatively recent feature sets | This is a very limited emulation for relatively recent feature sets | ||||
""" | """ | ||||
parsed_version = parse_version(vers["version"]) | parsed_version = parse_version(vers["version"]) | ||||
vers["capabilities"] = {} | vers["capabilities"] = {} | ||||
for name in opts["optional"]: | for name in opts["optional"]: | ||||
vers["capabilities"][name] = check(parsed_version, name) | vers["capabilities"][name] = check(parsed_version, name) | ||||
for name in opts["required"]: | for name in opts["required"]: | ||||
have = check(parsed_version, name) | have = check(parsed_version, name) | ||||
vers["capabilities"][name] = have | vers["capabilities"][name] = have | ||||
if not have: | if not have: | ||||
vers["error"] = ( | vers["error"] = ( | ||||
"client required capability `" | "client required capability `" | ||||
+ name | + name | ||||
+ "` is not supported by this server" | + "` is not supported by this server" | ||||
) | ) | ||||
return vers | return vers |
gitutil, | gitutil, | ||||
index, | index, | ||||
) | ) | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
# git.log-index-cache-miss: internal knob for testing | # git.log-index-cache-miss: internal knob for testing | ||||
configitem( | configitem( | ||||
b"git", b"log-index-cache-miss", default=False, | b"git", | ||||
b"log-index-cache-miss", | |||||
default=False, | |||||
) | ) | ||||
# TODO: extract an interface for this in core | # TODO: extract an interface for this in core | ||||
class gitstore(object): # store.basicstore): | class gitstore(object): # store.basicstore): | ||||
def __init__(self, path, vfstype): | def __init__(self, path, vfstype): | ||||
self.vfs = vfstype(path) | self.vfs = vfstype(path) | ||||
self.path = self.vfs.base | self.path = self.vfs.base | ||||
self.createmode = store._calcmode(self.vfs) | self.createmode = store._calcmode(self.vfs) | ||||
def expandname(self, bname): | def expandname(self, bname): | ||||
if bname == b'.': | if bname == b'.': | ||||
if self.active: | if self.active: | ||||
return self.active | return self.active | ||||
raise error.RepoLookupError(_(b"no active bookmark")) | raise error.RepoLookupError(_(b"no active bookmark")) | ||||
return bname | return bname | ||||
def applychanges(self, repo, tr, changes): | def applychanges(self, repo, tr, changes): | ||||
"""Apply a list of changes to bookmarks | """Apply a list of changes to bookmarks""" | ||||
""" | |||||
# TODO: this should respect transactions, but that's going to | # TODO: this should respect transactions, but that's going to | ||||
# require enlarging the gitbmstore to know how to do in-memory | # require enlarging the gitbmstore to know how to do in-memory | ||||
# temporary writes and read those back prior to transaction | # temporary writes and read those back prior to transaction | ||||
# finalization. | # finalization. | ||||
for name, node in changes: | for name, node in changes: | ||||
if node is None: | if node is None: | ||||
self.gitrepo.references.delete( | self.gitrepo.references.delete( | ||||
_BMS_PREFIX + pycompat.fsdecode(name) | _BMS_PREFIX + pycompat.fsdecode(name) |
@util.propertycache | @util.propertycache | ||||
def _dirs(self): | def _dirs(self): | ||||
return pathutil.dirs(self) | return pathutil.dirs(self) | ||||
def hasdir(self, dir): | def hasdir(self, dir): | ||||
return dir in self._dirs | return dir in self._dirs | ||||
def diff(self, other, match=lambda x: True, clean=False): | def diff(self, other, match=lambda x: True, clean=False): | ||||
'''Finds changes between the current manifest and m2. | """Finds changes between the current manifest and m2. | ||||
The result is returned as a dict with filename as key and | The result is returned as a dict with filename as key and | ||||
values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the | values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the | ||||
nodeid in the current/other manifest and fl1/fl2 is the flag | nodeid in the current/other manifest and fl1/fl2 is the flag | ||||
in the current/other manifest. Where the file does not exist, | in the current/other manifest. Where the file does not exist, | ||||
the nodeid will be None and the flags will be the empty | the nodeid will be None and the flags will be the empty | ||||
string. | string. | ||||
''' | """ | ||||
result = {} | result = {} | ||||
def _iterativediff(t1, t2, subdir): | def _iterativediff(t1, t2, subdir): | ||||
"""compares two trees and appends new tree nodes to examine to | """compares two trees and appends new tree nodes to examine to | ||||
the stack""" | the stack""" | ||||
if t1 is None: | if t1 is None: | ||||
t1 = {} | t1 = {} | ||||
if t2 is None: | if t2 is None: |
@command( | @command( | ||||
b'githelp|git', | b'githelp|git', | ||||
[], | [], | ||||
_(b'hg githelp'), | _(b'hg githelp'), | ||||
helpcategory=command.CATEGORY_HELP, | helpcategory=command.CATEGORY_HELP, | ||||
helpbasic=True, | helpbasic=True, | ||||
) | ) | ||||
def githelp(ui, repo, *args, **kwargs): | def githelp(ui, repo, *args, **kwargs): | ||||
'''suggests the Mercurial equivalent of the given git command | """suggests the Mercurial equivalent of the given git command | ||||
Usage: hg githelp -- <git command> | Usage: hg githelp -- <git command> | ||||
''' | """ | ||||
if len(args) == 0 or (len(args) == 1 and args[0] == b'git'): | if len(args) == 0 or (len(args) == 1 and args[0] == b'git'): | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'missing git command - usage: hg githelp -- <git command>') | _(b'missing git command - usage: hg githelp -- <git command>') | ||||
) | ) | ||||
if args[0] == b'git': | if args[0] == b'git': | ||||
args = args[1:] | args = args[1:] |
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'gpg', b'cmd', default=b'gpg', | b'gpg', | ||||
b'cmd', | |||||
default=b'gpg', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'gpg', b'key', default=None, | b'gpg', | ||||
b'key', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'gpg', b'.*', default=None, generic=True, | b'gpg', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
# Custom help category | # Custom help category | ||||
_HELP_CATEGORY = b'gpg' | _HELP_CATEGORY = b'gpg' | ||||
help.CATEGORY_ORDER.insert( | help.CATEGORY_ORDER.insert( | ||||
help.CATEGORY_ORDER.index(registrar.command.CATEGORY_HELP), _HELP_CATEGORY | help.CATEGORY_ORDER.index(registrar.command.CATEGORY_HELP), _HELP_CATEGORY | ||||
) | ) | ||||
help.CATEGORY_NAMES[_HELP_CATEGORY] = b'Signing changes (GPG)' | help.CATEGORY_NAMES[_HELP_CATEGORY] = b'Signing changes (GPG)' | ||||
fp.write(sig) | fp.write(sig) | ||||
fp.close() | fp.close() | ||||
fd, datafile = pycompat.mkstemp(prefix=b"hg-gpg-", suffix=b".txt") | fd, datafile = pycompat.mkstemp(prefix=b"hg-gpg-", suffix=b".txt") | ||||
fp = os.fdopen(fd, 'wb') | fp = os.fdopen(fd, 'wb') | ||||
fp.write(data) | fp.write(data) | ||||
fp.close() | fp.close() | ||||
gpgcmd = ( | gpgcmd = ( | ||||
b"%s --logger-fd 1 --status-fd 1 --verify \"%s\" \"%s\"" | b"%s --logger-fd 1 --status-fd 1 --verify \"%s\" \"%s\"" | ||||
% (self.path, sigfile, datafile,) | % ( | ||||
self.path, | |||||
sigfile, | |||||
datafile, | |||||
) | |||||
) | ) | ||||
ret = procutil.filter(b"", gpgcmd) | ret = procutil.filter(b"", gpgcmd) | ||||
finally: | finally: | ||||
for f in (sigfile, datafile): | for f in (sigfile, datafile): | ||||
try: | try: | ||||
if f: | if f: | ||||
os.unlink(f) | os.unlink(f) | ||||
except OSError: | except OSError: |
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'hgk', b'path', default=b'hgk', | b'hgk', | ||||
b'path', | |||||
default=b'hgk', | |||||
) | ) | ||||
@command( | @command( | ||||
b'debug-diff-tree', | b'debug-diff-tree', | ||||
[ | [ | ||||
(b'p', b'patch', None, _(b'generate patch')), | (b'p', b'patch', None, _(b'generate patch')), | ||||
(b'r', b'recursive', None, _(b'recursive')), | (b'r', b'recursive', None, _(b'recursive')), |
pickle = util.pickle | pickle = util.pickle | ||||
cmdtable = {} | cmdtable = {} | ||||
command = registrar.command(cmdtable) | command = registrar.command(cmdtable) | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'experimental', b'histedit.autoverb', default=False, | b'experimental', | ||||
b'histedit.autoverb', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'histedit', b'defaultrev', default=None, | b'histedit', | ||||
b'defaultrev', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'histedit', b'dropmissing', default=False, | b'histedit', | ||||
b'dropmissing', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'histedit', b'linelen', default=80, | b'histedit', | ||||
b'linelen', | |||||
default=80, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'histedit', b'singletransaction', default=False, | b'histedit', | ||||
b'singletransaction', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'ui', b'interface.histedit', default=None, | b'ui', | ||||
b'interface.histedit', | |||||
default=None, | |||||
) | ) | ||||
configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}') | configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}') | ||||
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||||
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||||
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
actiontable = {} | actiontable = {} | ||||
primaryactions = set() | primaryactions = set() | ||||
secondaryactions = set() | secondaryactions = set() | ||||
tertiaryactions = set() | tertiaryactions = set() | ||||
internalactions = set() | internalactions = set() | ||||
def geteditcomment(ui, first, last): | def geteditcomment(ui, first, last): | ||||
""" construct the editor comment | """construct the editor comment | ||||
The comment includes:: | The comment includes:: | ||||
- an intro | - an intro | ||||
- sorted primary commands | - sorted primary commands | ||||
- sorted short commands | - sorted short commands | ||||
- sorted long commands | - sorted long commands | ||||
- additional hints | - additional hints | ||||
Commands are only included once. | Commands are only included once. | ||||
class histeditaction(object): | class histeditaction(object): | ||||
def __init__(self, state, node): | def __init__(self, state, node): | ||||
self.state = state | self.state = state | ||||
self.repo = state.repo | self.repo = state.repo | ||||
self.node = node | self.node = node | ||||
@classmethod | @classmethod | ||||
def fromrule(cls, state, rule): | def fromrule(cls, state, rule): | ||||
"""Parses the given rule, returning an instance of the histeditaction. | """Parses the given rule, returning an instance of the histeditaction.""" | ||||
""" | |||||
ruleid = rule.strip().split(b' ', 1)[0] | ruleid = rule.strip().split(b' ', 1)[0] | ||||
# ruleid can be anything from rev numbers, hashes, "bookmarks" etc | # ruleid can be anything from rev numbers, hashes, "bookmarks" etc | ||||
# Check for validation of rule ids and get the rulehash | # Check for validation of rule ids and get the rulehash | ||||
try: | try: | ||||
rev = node.bin(ruleid) | rev = node.bin(ruleid) | ||||
except TypeError: | except TypeError: | ||||
try: | try: | ||||
_ctx = scmutil.revsingle(state.repo, ruleid) | _ctx = scmutil.revsingle(state.repo, ruleid) | ||||
# trim to 75 columns by default so it's not stupidly wide in my editor | # trim to 75 columns by default so it's not stupidly wide in my editor | ||||
# (the 5 more are left for verb) | # (the 5 more are left for verb) | ||||
maxlen = self.repo.ui.configint(b'histedit', b'linelen') | maxlen = self.repo.ui.configint(b'histedit', b'linelen') | ||||
maxlen = max(maxlen, 22) # avoid truncating hash | maxlen = max(maxlen, 22) # avoid truncating hash | ||||
return stringutil.ellipsis(line, maxlen) | return stringutil.ellipsis(line, maxlen) | ||||
def tostate(self): | def tostate(self): | ||||
"""Print an action in format used by histedit state files | """Print an action in format used by histedit state files | ||||
(the first line is a verb, the remainder is the second) | (the first line is a verb, the remainder is the second) | ||||
""" | """ | ||||
return b"%s\n%s" % (self.verb, node.hex(self.node)) | return b"%s\n%s" % (self.verb, node.hex(self.node)) | ||||
def run(self): | def run(self): | ||||
"""Runs the action. The default behavior is simply apply the action's | """Runs the action. The default behavior is simply apply the action's | ||||
rulectx onto the current parentctx.""" | rulectx onto the current parentctx.""" | ||||
self.applychange() | self.applychange() | ||||
self.continuedirty() | self.continuedirty() | ||||
if other in self.conflicts: | if other in self.conflicts: | ||||
self.conflicts.remove(other) | self.conflicts.remove(other) | ||||
return self.conflicts | return self.conflicts | ||||
# ============ EVENTS =============== | # ============ EVENTS =============== | ||||
def movecursor(state, oldpos, newpos): | def movecursor(state, oldpos, newpos): | ||||
'''Change the rule/changeset that the cursor is pointing to, regardless of | """Change the rule/changeset that the cursor is pointing to, regardless of | ||||
current mode (you can switch between patches from the view patch window).''' | current mode (you can switch between patches from the view patch window).""" | ||||
state[b'pos'] = newpos | state[b'pos'] = newpos | ||||
mode, _ = state[b'mode'] | mode, _ = state[b'mode'] | ||||
if mode == MODE_RULES: | if mode == MODE_RULES: | ||||
# Scroll through the list by updating the view for MODE_RULES, so that | # Scroll through the list by updating the view for MODE_RULES, so that | ||||
# even if we are not currently viewing the rules, switching back will | # even if we are not currently viewing the rules, switching back will | ||||
# result in the cursor's rule being visible. | # result in the cursor's rule being visible. | ||||
modestate = state[b'modes'][MODE_RULES] | modestate = state[b'modes'][MODE_RULES] | ||||
if next: | if next: | ||||
index += 1 | index += 1 | ||||
else: | else: | ||||
index -= 1 | index -= 1 | ||||
changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)]) | changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)]) | ||||
def changeview(state, delta, unit): | def changeview(state, delta, unit): | ||||
'''Change the region of whatever is being viewed (a patch or the list of | """Change the region of whatever is being viewed (a patch or the list of | ||||
changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.''' | changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.""" | ||||
mode, _ = state[b'mode'] | mode, _ = state[b'mode'] | ||||
if mode != MODE_PATCH: | if mode != MODE_PATCH: | ||||
return | return | ||||
mode_state = state[b'modes'][mode] | mode_state = state[b'modes'][mode] | ||||
num_lines = len(mode_state[b'patchcontents']) | num_lines = len(mode_state[b'patchcontents']) | ||||
page_height = state[b'page_height'] | page_height = state[b'page_height'] | ||||
unit = page_height if unit == b'page' else 1 | unit = page_height if unit == b'page' else 1 | ||||
num_pages = 1 + (num_lines - 1) // page_height | num_pages = 1 + (num_lines - 1) // page_height | ||||
state = { | state = { | ||||
b'pos': 0, | b'pos': 0, | ||||
b'rules': rules, | b'rules': rules, | ||||
b'selected': None, | b'selected': None, | ||||
b'mode': (MODE_INIT, MODE_INIT), | b'mode': (MODE_INIT, MODE_INIT), | ||||
b'page_height': None, | b'page_height': None, | ||||
b'modes': { | b'modes': { | ||||
MODE_RULES: {b'line_offset': 0,}, | MODE_RULES: { | ||||
MODE_PATCH: {b'line_offset': 0,}, | b'line_offset': 0, | ||||
}, | |||||
MODE_PATCH: { | |||||
b'line_offset': 0, | |||||
}, | |||||
}, | }, | ||||
b'repo': repo, | b'repo': repo, | ||||
} | } | ||||
# eventloop | # eventloop | ||||
ch = None | ch = None | ||||
stdscr.clear() | stdscr.clear() | ||||
stdscr.refresh() | stdscr.refresh() |
) | ) | ||||
from mercurial.utils import dateutil | from mercurial.utils import dateutil | ||||
from .. import notify | from .. import notify | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'notify_obsoleted', b'domain', default=None, | b'notify_obsoleted', | ||||
b'domain', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify_obsoleted', b'messageidseed', default=None, | b'notify_obsoleted', | ||||
b'messageidseed', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify_obsoleted', | b'notify_obsoleted', | ||||
b'template', | b'template', | ||||
default=b'''Subject: changeset abandoned | default=b'''Subject: changeset abandoned | ||||
This changeset has been abandoned. | This changeset has been abandoned. | ||||
''', | ''', |
) | ) | ||||
from mercurial.utils import dateutil | from mercurial.utils import dateutil | ||||
from .. import notify | from .. import notify | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'notify_published', b'domain', default=None, | b'notify_published', | ||||
b'domain', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify_published', b'messageidseed', default=None, | b'notify_published', | ||||
b'messageidseed', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify_published', | b'notify_published', | ||||
b'template', | b'template', | ||||
default=b'''Subject: changeset published | default=b'''Subject: changeset published | ||||
This changeset has been published. | This changeset has been published. | ||||
''', | ''', |
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'infinitepush', b'server', default=False, | b'infinitepush', | ||||
b'server', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'infinitepush', b'storetype', default=b'', | b'infinitepush', | ||||
b'storetype', | |||||
default=b'', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'infinitepush', b'indextype', default=b'', | b'infinitepush', | ||||
b'indextype', | |||||
default=b'', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'infinitepush', b'indexpath', default=b'', | b'infinitepush', | ||||
b'indexpath', | |||||
default=b'', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'infinitepush', b'storeallparts', default=False, | b'infinitepush', | ||||
b'storeallparts', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'infinitepush', b'reponame', default=b'', | b'infinitepush', | ||||
b'reponame', | |||||
default=b'', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'scratchbranch', b'storepath', default=b'', | b'scratchbranch', | ||||
b'storepath', | |||||
default=b'', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'infinitepush', b'branchpattern', default=b'', | b'infinitepush', | ||||
b'branchpattern', | |||||
default=b'', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'infinitepush', b'pushtobundlestore', default=False, | b'infinitepush', | ||||
b'pushtobundlestore', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'experimental', b'server-bundlestore-bookmark', default=b'', | b'experimental', | ||||
b'server-bundlestore-bookmark', | |||||
default=b'', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'experimental', b'infinitepush-scratchpush', default=False, | b'experimental', | ||||
b'infinitepush-scratchpush', | |||||
default=False, | |||||
) | ) | ||||
experimental = b'experimental' | experimental = b'experimental' | ||||
configbookmark = b'server-bundlestore-bookmark' | configbookmark = b'server-bundlestore-bookmark' | ||||
configscratchpush = b'infinitepush-scratchpush' | configscratchpush = b'infinitepush-scratchpush' | ||||
scratchbranchparttype = bundleparts.scratchbranchparttype | scratchbranchparttype = bundleparts.scratchbranchparttype | ||||
revsetpredicate = registrar.revsetpredicate() | revsetpredicate = registrar.revsetpredicate() | ||||
loglevel = ui.config(b'infinitepush', b'loglevel', b'DEBUG') | loglevel = ui.config(b'infinitepush', b'loglevel', b'DEBUG') | ||||
numeric_loglevel = getattr(logging, loglevel.upper(), None) | numeric_loglevel = getattr(logging, loglevel.upper(), None) | ||||
if not isinstance(numeric_loglevel, int): | if not isinstance(numeric_loglevel, int): | ||||
raise error.Abort(_(b'invalid log level %s') % loglevel) | raise error.Abort(_(b'invalid log level %s') % loglevel) | ||||
return numeric_loglevel | return numeric_loglevel | ||||
def _tryhoist(ui, remotebookmark): | def _tryhoist(ui, remotebookmark): | ||||
'''returns a bookmarks with hoisted part removed | """returns a bookmarks with hoisted part removed | ||||
Remotenames extension has a 'hoist' config that allows to use remote | Remotenames extension has a 'hoist' config that allows to use remote | ||||
bookmarks without specifying remote path. For example, 'hg update master' | bookmarks without specifying remote path. For example, 'hg update master' | ||||
works as well as 'hg update remote/master'. We want to allow the same in | works as well as 'hg update remote/master'. We want to allow the same in | ||||
infinitepush. | infinitepush. | ||||
''' | """ | ||||
if common.isremotebooksenabled(ui): | if common.isremotebooksenabled(ui): | ||||
hoist = ui.config(b'remotenames', b'hoistedpeer') + b'/' | hoist = ui.config(b'remotenames', b'hoistedpeer') + b'/' | ||||
if remotebookmark.startswith(hoist): | if remotebookmark.startswith(hoist): | ||||
return remotebookmark[len(hoist) :] | return remotebookmark[len(hoist) :] | ||||
return remotebookmark | return remotebookmark | ||||
yield pushkey.decodekeys(d) | yield pushkey.decodekeys(d) | ||||
def _readbundlerevs(bundlerepo): | def _readbundlerevs(bundlerepo): | ||||
return list(bundlerepo.revs(b'bundle()')) | return list(bundlerepo.revs(b'bundle()')) | ||||
def _includefilelogstobundle(bundlecaps, bundlerepo, bundlerevs, ui): | def _includefilelogstobundle(bundlecaps, bundlerepo, bundlerevs, ui): | ||||
'''Tells remotefilelog to include all changed files to the changegroup | """Tells remotefilelog to include all changed files to the changegroup | ||||
By default remotefilelog doesn't include file content to the changegroup. | By default remotefilelog doesn't include file content to the changegroup. | ||||
But we need to include it if we are fetching from bundlestore. | But we need to include it if we are fetching from bundlestore. | ||||
''' | """ | ||||
changedfiles = set() | changedfiles = set() | ||||
cl = bundlerepo.changelog | cl = bundlerepo.changelog | ||||
for r in bundlerevs: | for r in bundlerevs: | ||||
# [3] means changed files | # [3] means changed files | ||||
changedfiles.update(cl.read(r)[3]) | changedfiles.update(cl.read(r)[3]) | ||||
if not changedfiles: | if not changedfiles: | ||||
return bundlecaps | return bundlecaps | ||||
if not appended: | if not appended: | ||||
# Not found excludepattern cap. Just append it | # Not found excludepattern cap. Just append it | ||||
newcaps.append(b'excludepattern=' + changedfiles) | newcaps.append(b'excludepattern=' + changedfiles) | ||||
return newcaps | return newcaps | ||||
def _rebundle(bundlerepo, bundleroots, unknownhead): | def _rebundle(bundlerepo, bundleroots, unknownhead): | ||||
''' | """ | ||||
Bundle may include more revision then user requested. For example, | Bundle may include more revision then user requested. For example, | ||||
if user asks for revision but bundle also consists its descendants. | if user asks for revision but bundle also consists its descendants. | ||||
This function will filter out all revision that user is not requested. | This function will filter out all revision that user is not requested. | ||||
''' | """ | ||||
parts = [] | parts = [] | ||||
version = b'02' | version = b'02' | ||||
outgoing = discovery.outgoing( | outgoing = discovery.outgoing( | ||||
bundlerepo, commonheads=bundleroots, ancestorsof=[unknownhead] | bundlerepo, commonheads=bundleroots, ancestorsof=[unknownhead] | ||||
) | ) | ||||
cgstream = changegroup.makestream(bundlerepo, outgoing, version, b'pull') | cgstream = changegroup.makestream(bundlerepo, outgoing, version, b'pull') | ||||
cgstream = util.chunkbuffer(cgstream).read() | cgstream = util.chunkbuffer(cgstream).read() | ||||
def _needsrebundling(head, bundlerepo): | def _needsrebundling(head, bundlerepo): | ||||
bundleheads = list(bundlerepo.revs(b'heads(bundle())')) | bundleheads = list(bundlerepo.revs(b'heads(bundle())')) | ||||
return not ( | return not ( | ||||
len(bundleheads) == 1 and bundlerepo[bundleheads[0]].node() == head | len(bundleheads) == 1 and bundlerepo[bundleheads[0]].node() == head | ||||
) | ) | ||||
def _generateoutputparts(head, bundlerepo, bundleroots, bundlefile): | def _generateoutputparts(head, bundlerepo, bundleroots, bundlefile): | ||||
'''generates bundle that will be send to the user | """generates bundle that will be send to the user | ||||
returns tuple with raw bundle string and bundle type | returns tuple with raw bundle string and bundle type | ||||
''' | """ | ||||
parts = [] | parts = [] | ||||
if not _needsrebundling(head, bundlerepo): | if not _needsrebundling(head, bundlerepo): | ||||
with util.posixfile(bundlefile, b"rb") as f: | with util.posixfile(bundlefile, b"rb") as f: | ||||
unbundler = exchange.readbundle(bundlerepo.ui, f, bundlefile) | unbundler = exchange.readbundle(bundlerepo.ui, f, bundlefile) | ||||
if isinstance(unbundler, changegroup.cg1unpacker): | if isinstance(unbundler, changegroup.cg1unpacker): | ||||
part = bundle2.bundlepart( | part = bundle2.bundlepart( | ||||
b'changegroup', data=unbundler._stream.read() | b'changegroup', data=unbundler._stream.read() | ||||
) | ) | ||||
if part.type in (b'pushkey', b'changegroup'): | if part.type in (b'pushkey', b'changegroup'): | ||||
if op.reply is not None: | if op.reply is not None: | ||||
rpart = op.reply.newpart(b'reply:%s' % part.type) | rpart = op.reply.newpart(b'reply:%s' % part.type) | ||||
rpart.addparam( | rpart.addparam( | ||||
b'in-reply-to', b'%d' % part.id, mandatory=False | b'in-reply-to', b'%d' % part.id, mandatory=False | ||||
) | ) | ||||
rpart.addparam(b'return', b'1', mandatory=False) | rpart.addparam(b'return', b'1', mandatory=False) | ||||
op.records.add(part.type, {b'return': 1,}) | op.records.add( | ||||
part.type, | |||||
{ | |||||
b'return': 1, | |||||
}, | |||||
) | |||||
if bundlepart: | if bundlepart: | ||||
bundler.addpart(bundlepart) | bundler.addpart(bundlepart) | ||||
# storing the bundle in the bundlestore | # storing the bundle in the bundlestore | ||||
buf = util.chunkbuffer(bundler.getchunks()) | buf = util.chunkbuffer(bundler.getchunks()) | ||||
fd, bundlefile = pycompat.mkstemp() | fd, bundlefile = pycompat.mkstemp() | ||||
try: | try: | ||||
try: | try: | ||||
rpart.addparam( | rpart.addparam( | ||||
b'in-reply-to', str(part.id), mandatory=False | b'in-reply-to', str(part.id), mandatory=False | ||||
) | ) | ||||
rpart.addparam(b'return', b'1', mandatory=False) | rpart.addparam(b'return', b'1', mandatory=False) | ||||
else: | else: | ||||
bundle2._processpart(op, part) | bundle2._processpart(op, part) | ||||
if handleallparts: | if handleallparts: | ||||
op.records.add(part.type, {b'return': 1,}) | op.records.add( | ||||
part.type, | |||||
{ | |||||
b'return': 1, | |||||
}, | |||||
) | |||||
if bundlepart: | if bundlepart: | ||||
bundler.addpart(bundlepart) | bundler.addpart(bundlepart) | ||||
# If commits were sent, store them | # If commits were sent, store them | ||||
if cgparams: | if cgparams: | ||||
buf = util.chunkbuffer(bundler.getchunks()) | buf = util.chunkbuffer(bundler.getchunks()) | ||||
fd, bundlefile = pycompat.mkstemp() | fd, bundlefile = pycompat.mkstemp() | ||||
try: | try: | ||||
b'old': oldnode, | b'old': oldnode, | ||||
} | } | ||||
op.reply.newpart( | op.reply.newpart( | ||||
b'pushkey', mandatoryparams=pycompat.iteritems(params) | b'pushkey', mandatoryparams=pycompat.iteritems(params) | ||||
) | ) | ||||
def bundle2pushkey(orig, op, part): | def bundle2pushkey(orig, op, part): | ||||
'''Wrapper of bundle2.handlepushkey() | """Wrapper of bundle2.handlepushkey() | ||||
The only goal is to skip calling the original function if flag is set. | The only goal is to skip calling the original function if flag is set. | ||||
It's set if infinitepush push is happening. | It's set if infinitepush push is happening. | ||||
''' | """ | ||||
if op.records[scratchbranchparttype + b'_skippushkey']: | if op.records[scratchbranchparttype + b'_skippushkey']: | ||||
if op.reply is not None: | if op.reply is not None: | ||||
rpart = op.reply.newpart(b'reply:pushkey') | rpart = op.reply.newpart(b'reply:pushkey') | ||||
rpart.addparam(b'in-reply-to', str(part.id), mandatory=False) | rpart.addparam(b'in-reply-to', str(part.id), mandatory=False) | ||||
rpart.addparam(b'return', b'1', mandatory=False) | rpart.addparam(b'return', b'1', mandatory=False) | ||||
return 1 | return 1 | ||||
return orig(op, part) | return orig(op, part) | ||||
def bundle2handlephases(orig, op, part): | def bundle2handlephases(orig, op, part): | ||||
'''Wrapper of bundle2.handlephases() | """Wrapper of bundle2.handlephases() | ||||
The only goal is to skip calling the original function if flag is set. | The only goal is to skip calling the original function if flag is set. | ||||
It's set if infinitepush push is happening. | It's set if infinitepush push is happening. | ||||
''' | """ | ||||
if op.records[scratchbranchparttype + b'_skipphaseheads']: | if op.records[scratchbranchparttype + b'_skipphaseheads']: | ||||
return | return | ||||
return orig(op, part) | return orig(op, part) | ||||
def _asyncsavemetadata(root, nodes): | def _asyncsavemetadata(root, nodes): | ||||
'''starts a separate process that fills metadata for the nodes | """starts a separate process that fills metadata for the nodes | ||||
This function creates a separate process and doesn't wait for it's | This function creates a separate process and doesn't wait for it's | ||||
completion. This was done to avoid slowing down pushes | completion. This was done to avoid slowing down pushes | ||||
''' | """ | ||||
maxnodes = 50 | maxnodes = 50 | ||||
if len(nodes) > maxnodes: | if len(nodes) > maxnodes: | ||||
return | return | ||||
nodesargs = [] | nodesargs = [] | ||||
for node in nodes: | for node in nodes: | ||||
nodesargs.append(b'--node') | nodesargs.append(b'--node') | ||||
nodesargs.append(node) | nodesargs.append(node) |
heads = repo.revs(b'heads(%r)', revset) | heads = repo.revs(b'heads(%r)', revset) | ||||
if len(heads) > 1: | if len(heads) > 1: | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'cannot push more than one head to a scratch branch') | _(b'cannot push more than one head to a scratch branch') | ||||
) | ) | ||||
def _handlelfs(repo, missing): | def _handlelfs(repo, missing): | ||||
'''Special case if lfs is enabled | """Special case if lfs is enabled | ||||
If lfs is enabled then we need to call prepush hook | If lfs is enabled then we need to call prepush hook | ||||
to make sure large files are uploaded to lfs | to make sure large files are uploaded to lfs | ||||
''' | """ | ||||
try: | try: | ||||
lfsmod = extensions.find(b'lfs') | lfsmod = extensions.find(b'lfs') | ||||
lfsmod.wrapper.uploadblobsfromrevs(repo, missing) | lfsmod.wrapper.uploadblobsfromrevs(repo, missing) | ||||
except KeyError: | except KeyError: | ||||
# Ignore if lfs extension is not enabled | # Ignore if lfs extension is not enabled | ||||
return | return | ||||
raise NotImplementedError() | raise NotImplementedError() | ||||
def addmanybookmarks(self, bookmarks): | def addmanybookmarks(self, bookmarks): | ||||
"""Takes a dict with mapping from bookmark to hash and records mapping | """Takes a dict with mapping from bookmark to hash and records mapping | ||||
in the metadata store.""" | in the metadata store.""" | ||||
raise NotImplementedError() | raise NotImplementedError() | ||||
def deletebookmarks(self, patterns): | def deletebookmarks(self, patterns): | ||||
"""Accepts list of bookmarks and deletes them. | """Accepts list of bookmarks and deletes them.""" | ||||
""" | |||||
raise NotImplementedError() | raise NotImplementedError() | ||||
def getbundle(self, node): | def getbundle(self, node): | ||||
"""Returns the bundleid for the bundle that contains the given node.""" | """Returns the bundleid for the bundle that contains the given node.""" | ||||
raise NotImplementedError() | raise NotImplementedError() | ||||
def getnode(self, bookmark): | def getnode(self, bookmark): | ||||
"""Returns the node for the given bookmark. None if it doesn't exist.""" | """Returns the node for the given bookmark. None if it doesn't exist.""" |
pattern = pattern.replace(b'_', b'\\_') | pattern = pattern.replace(b'_', b'\\_') | ||||
pattern = pattern.replace(b'%', b'\\%') | pattern = pattern.replace(b'%', b'\\%') | ||||
if pattern.endswith(b'*'): | if pattern.endswith(b'*'): | ||||
pattern = pattern[:-1] + b'%' | pattern = pattern[:-1] + b'%' | ||||
return pattern | return pattern | ||||
class sqlindexapi(indexapi.indexapi): | class sqlindexapi(indexapi.indexapi): | ||||
''' | """ | ||||
Sql backend for infinitepush index. See schema.sql | Sql backend for infinitepush index. See schema.sql | ||||
''' | """ | ||||
def __init__( | def __init__( | ||||
self, | self, | ||||
reponame, | reponame, | ||||
host, | host, | ||||
port, | port, | ||||
database, | database, | ||||
user, | user, |
} | } | ||||
templatefilter = registrar.templatefilter() | templatefilter = registrar.templatefilter() | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'keywordset', b'svn', default=False, | b'keywordset', | ||||
b'svn', | |||||
default=False, | |||||
) | ) | ||||
# date like in cvs' $Date | # date like in cvs' $Date | ||||
@templatefilter(b'utcdate', intype=templateutil.date) | @templatefilter(b'utcdate', intype=templateutil.date) | ||||
def utcdate(date): | def utcdate(date): | ||||
'''Date. Returns a UTC-date in this format: "2009/08/18 11:00:13". | """Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".""" | ||||
''' | |||||
dateformat = b'%Y/%m/%d %H:%M:%S' | dateformat = b'%Y/%m/%d %H:%M:%S' | ||||
return dateutil.datestr((date[0], 0), dateformat) | return dateutil.datestr((date[0], 0), dateformat) | ||||
# date like in svn's $Date | # date like in svn's $Date | ||||
@templatefilter(b'svnisodate', intype=templateutil.date) | @templatefilter(b'svnisodate', intype=templateutil.date) | ||||
def svnisodate(date): | def svnisodate(date): | ||||
'''Date. Returns a date in this format: "2009-08-18 13:00:13 | """Date. Returns a date in this format: "2009-08-18 13:00:13 | ||||
+0200 (Tue, 18 Aug 2009)". | +0200 (Tue, 18 Aug 2009)". | ||||
''' | """ | ||||
return dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)') | return dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)') | ||||
# date like in svn's $Id | # date like in svn's $Id | ||||
@templatefilter(b'svnutcdate', intype=templateutil.date) | @templatefilter(b'svnutcdate', intype=templateutil.date) | ||||
def svnutcdate(date): | def svnutcdate(date): | ||||
'''Date. Returns a UTC-date in this format: "2009-08-18 | """Date. Returns a UTC-date in this format: "2009-08-18 | ||||
11:00:13Z". | 11:00:13Z". | ||||
''' | """ | ||||
dateformat = b'%Y-%m-%d %H:%M:%SZ' | dateformat = b'%Y-%m-%d %H:%M:%SZ' | ||||
return dateutil.datestr((date[0], 0), dateformat) | return dateutil.datestr((date[0], 0), dateformat) | ||||
# make keyword tools accessible | # make keyword tools accessible | ||||
kwtools = {b'hgcmd': b''} | kwtools = {b'hgcmd': b''} | ||||
b'LastChangedDate': b'{date|svnisodate}', | b'LastChangedDate': b'{date|svnisodate}', | ||||
}, | }, | ||||
) | ) | ||||
templates.update(kwsets[ui.configbool(b'keywordset', b'svn')]) | templates.update(kwsets[ui.configbool(b'keywordset', b'svn')]) | ||||
return templates | return templates | ||||
def _shrinktext(text, subfunc): | def _shrinktext(text, subfunc): | ||||
'''Helper for keyword expansion removal in text. | """Helper for keyword expansion removal in text. | ||||
Depending on subfunc also returns number of substitutions.''' | Depending on subfunc also returns number of substitutions.""" | ||||
return subfunc(br'$\1$', text) | return subfunc(br'$\1$', text) | ||||
def _preselect(wstatus, changed): | def _preselect(wstatus, changed): | ||||
'''Retrieves modified and added files from a working directory state | """Retrieves modified and added files from a working directory state | ||||
and returns the subset of each contained in given changed files | and returns the subset of each contained in given changed files | ||||
retrieved from a change context.''' | retrieved from a change context.""" | ||||
modified = [f for f in wstatus.modified if f in changed] | modified = [f for f in wstatus.modified if f in changed] | ||||
added = [f for f in wstatus.added if f in changed] | added = [f for f in wstatus.added if f in changed] | ||||
return modified, added | return modified, added | ||||
class kwtemplater(object): | class kwtemplater(object): | ||||
''' | """ | ||||
Sets up keyword templates, corresponding keyword regex, and | Sets up keyword templates, corresponding keyword regex, and | ||||
provides keyword substitution functions. | provides keyword substitution functions. | ||||
''' | """ | ||||
def __init__(self, ui, repo, inc, exc): | def __init__(self, ui, repo, inc, exc): | ||||
self.ui = ui | self.ui = ui | ||||
self._repo = weakref.ref(repo) | self._repo = weakref.ref(repo) | ||||
self.match = match.match(repo.root, b'', [], inc, exc) | self.match = match.match(repo.root, b'', [], inc, exc) | ||||
self.restrict = kwtools[b'hgcmd'] in restricted.split() | self.restrict = kwtools[b'hgcmd'] in restricted.split() | ||||
self.postcommit = False | self.postcommit = False | ||||
and self.match(path) | and self.match(path) | ||||
and not stringutil.binary(data) | and not stringutil.binary(data) | ||||
): | ): | ||||
ctx = self.linkctx(path, node) | ctx = self.linkctx(path, node) | ||||
return self.substitute(data, path, ctx, self.rekw.sub) | return self.substitute(data, path, ctx, self.rekw.sub) | ||||
return data | return data | ||||
def iskwfile(self, cand, ctx): | def iskwfile(self, cand, ctx): | ||||
'''Returns subset of candidates which are configured for keyword | """Returns subset of candidates which are configured for keyword | ||||
expansion but are not symbolic links.''' | expansion but are not symbolic links.""" | ||||
return [f for f in cand if self.match(f) and b'l' not in ctx.flags(f)] | return [f for f in cand if self.match(f) and b'l' not in ctx.flags(f)] | ||||
def overwrite(self, ctx, candidates, lookup, expand, rekw=False): | def overwrite(self, ctx, candidates, lookup, expand, rekw=False): | ||||
'''Overwrites selected files expanding/shrinking keywords.''' | '''Overwrites selected files expanding/shrinking keywords.''' | ||||
if self.restrict or lookup or self.postcommit: # exclude kw_copy | if self.restrict or lookup or self.postcommit: # exclude kw_copy | ||||
candidates = self.iskwfile(candidates, ctx) | candidates = self.iskwfile(candidates, ctx) | ||||
if not candidates: | if not candidates: | ||||
return | return | ||||
'''Returns lines with keyword substitutions removed.''' | '''Returns lines with keyword substitutions removed.''' | ||||
if self.match(fname): | if self.match(fname): | ||||
text = b''.join(lines) | text = b''.join(lines) | ||||
if not stringutil.binary(text): | if not stringutil.binary(text): | ||||
return _shrinktext(text, self.rekwexp.sub).splitlines(True) | return _shrinktext(text, self.rekwexp.sub).splitlines(True) | ||||
return lines | return lines | ||||
def wread(self, fname, data): | def wread(self, fname, data): | ||||
'''If in restricted mode returns data read from wdir with | """If in restricted mode returns data read from wdir with | ||||
keyword substitutions removed.''' | keyword substitutions removed.""" | ||||
if self.restrict: | if self.restrict: | ||||
return self.shrink(fname, data) | return self.shrink(fname, data) | ||||
return data | return data | ||||
class kwfilelog(filelog.filelog): | class kwfilelog(filelog.filelog): | ||||
''' | """ | ||||
Subclass of filelog to hook into its read, add, cmp methods. | Subclass of filelog to hook into its read, add, cmp methods. | ||||
Keywords are "stored" unexpanded, and processed on reading. | Keywords are "stored" unexpanded, and processed on reading. | ||||
''' | """ | ||||
def __init__(self, opener, kwt, path): | def __init__(self, opener, kwt, path): | ||||
super(kwfilelog, self).__init__(opener, path) | super(kwfilelog, self).__init__(opener, path) | ||||
self.kwt = kwt | self.kwt = kwt | ||||
self.path = path | self.path = path | ||||
def read(self, node): | def read(self, node): | ||||
'''Expands keywords when reading filelog.''' | '''Expands keywords when reading filelog.''' | ||||
def cmp(self, node, text): | def cmp(self, node, text): | ||||
'''Removes keyword substitutions for comparison.''' | '''Removes keyword substitutions for comparison.''' | ||||
text = self.kwt.shrink(self.path, text) | text = self.kwt.shrink(self.path, text) | ||||
return super(kwfilelog, self).cmp(node, text) | return super(kwfilelog, self).cmp(node, text) | ||||
def _status(ui, repo, wctx, kwt, *pats, **opts): | def _status(ui, repo, wctx, kwt, *pats, **opts): | ||||
'''Bails out if [keyword] configuration is not active. | """Bails out if [keyword] configuration is not active. | ||||
Returns status of working directory.''' | Returns status of working directory.""" | ||||
if kwt: | if kwt: | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
return repo.status( | return repo.status( | ||||
match=scmutil.match(wctx, pats, opts), | match=scmutil.match(wctx, pats, opts), | ||||
clean=True, | clean=True, | ||||
unknown=opts.get(b'unknown') or opts.get(b'all'), | unknown=opts.get(b'unknown') or opts.get(b'all'), | ||||
) | ) | ||||
if ui.configitems(b'keyword'): | if ui.configitems(b'keyword'): | ||||
[ | [ | ||||
(b'd', b'default', None, _(b'show default keyword template maps')), | (b'd', b'default', None, _(b'show default keyword template maps')), | ||||
(b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')), | (b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')), | ||||
], | ], | ||||
_(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'), | _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'), | ||||
optionalrepo=True, | optionalrepo=True, | ||||
) | ) | ||||
def demo(ui, repo, *args, **opts): | def demo(ui, repo, *args, **opts): | ||||
'''print [keywordmaps] configuration and an expansion example | """print [keywordmaps] configuration and an expansion example | ||||
Show current, custom, or default keyword template maps and their | Show current, custom, or default keyword template maps and their | ||||
expansions. | expansions. | ||||
Extend the current configuration by specifying maps as arguments | Extend the current configuration by specifying maps as arguments | ||||
and using -f/--rcfile to source an external hgrc file. | and using -f/--rcfile to source an external hgrc file. | ||||
Use -d/--default to disable current configuration. | Use -d/--default to disable current configuration. | ||||
See :hg:`help templates` for information on templates and filters. | See :hg:`help templates` for information on templates and filters. | ||||
''' | """ | ||||
def demoitems(section, items): | def demoitems(section, items): | ||||
ui.write(b'[%s]\n' % section) | ui.write(b'[%s]\n' % section) | ||||
for k, v in sorted(items): | for k, v in sorted(items): | ||||
if isinstance(v, bool): | if isinstance(v, bool): | ||||
v = stringutil.pprint(v) | v = stringutil.pprint(v) | ||||
ui.write(b'%s = %s\n' % (k, v)) | ui.write(b'%s = %s\n' % (k, v)) | ||||
@command( | @command( | ||||
b'kwexpand', | b'kwexpand', | ||||
cmdutil.walkopts, | cmdutil.walkopts, | ||||
_(b'hg kwexpand [OPTION]... [FILE]...'), | _(b'hg kwexpand [OPTION]... [FILE]...'), | ||||
inferrepo=True, | inferrepo=True, | ||||
) | ) | ||||
def expand(ui, repo, *pats, **opts): | def expand(ui, repo, *pats, **opts): | ||||
'''expand keywords in the working directory | """expand keywords in the working directory | ||||
Run after (re)enabling keyword expansion. | Run after (re)enabling keyword expansion. | ||||
kwexpand refuses to run if given files contain local changes. | kwexpand refuses to run if given files contain local changes. | ||||
''' | """ | ||||
# 3rd argument sets expansion to True | # 3rd argument sets expansion to True | ||||
_kwfwrite(ui, repo, True, *pats, **opts) | _kwfwrite(ui, repo, True, *pats, **opts) | ||||
@command( | @command( | ||||
b'kwfiles', | b'kwfiles', | ||||
[ | [ | ||||
(b'A', b'all', None, _(b'show keyword status flags of all files')), | (b'A', b'all', None, _(b'show keyword status flags of all files')), | ||||
(b'i', b'ignore', None, _(b'show files excluded from expansion')), | (b'i', b'ignore', None, _(b'show files excluded from expansion')), | ||||
(b'u', b'unknown', None, _(b'only show unknown (not tracked) files')), | (b'u', b'unknown', None, _(b'only show unknown (not tracked) files')), | ||||
] | ] | ||||
+ cmdutil.walkopts, | + cmdutil.walkopts, | ||||
_(b'hg kwfiles [OPTION]... [FILE]...'), | _(b'hg kwfiles [OPTION]... [FILE]...'), | ||||
inferrepo=True, | inferrepo=True, | ||||
) | ) | ||||
def files(ui, repo, *pats, **opts): | def files(ui, repo, *pats, **opts): | ||||
'''show files configured for keyword expansion | """show files configured for keyword expansion | ||||
List which files in the working directory are matched by the | List which files in the working directory are matched by the | ||||
[keyword] configuration patterns. | [keyword] configuration patterns. | ||||
Useful to prevent inadvertent keyword expansion and to speed up | Useful to prevent inadvertent keyword expansion and to speed up | ||||
execution by including only files that are actual candidates for | execution by including only files that are actual candidates for | ||||
expansion. | expansion. | ||||
See :hg:`help keyword` on how to construct patterns both for | See :hg:`help keyword` on how to construct patterns both for | ||||
inclusion and exclusion of files. | inclusion and exclusion of files. | ||||
With -A/--all and -v/--verbose the codes used to show the status | With -A/--all and -v/--verbose the codes used to show the status | ||||
of files are:: | of files are:: | ||||
K = keyword expansion candidate | K = keyword expansion candidate | ||||
k = keyword expansion candidate (not tracked) | k = keyword expansion candidate (not tracked) | ||||
I = ignored | I = ignored | ||||
i = ignored (not tracked) | i = ignored (not tracked) | ||||
''' | """ | ||||
kwt = getattr(repo, '_keywordkwt', None) | kwt = getattr(repo, '_keywordkwt', None) | ||||
wctx = repo[None] | wctx = repo[None] | ||||
status = _status(ui, repo, wctx, kwt, *pats, **opts) | status = _status(ui, repo, wctx, kwt, *pats, **opts) | ||||
if pats: | if pats: | ||||
cwd = repo.getcwd() | cwd = repo.getcwd() | ||||
else: | else: | ||||
cwd = b'' | cwd = b'' | ||||
files = [] | files = [] | ||||
@command( | @command( | ||||
b'kwshrink', | b'kwshrink', | ||||
cmdutil.walkopts, | cmdutil.walkopts, | ||||
_(b'hg kwshrink [OPTION]... [FILE]...'), | _(b'hg kwshrink [OPTION]... [FILE]...'), | ||||
inferrepo=True, | inferrepo=True, | ||||
) | ) | ||||
def shrink(ui, repo, *pats, **opts): | def shrink(ui, repo, *pats, **opts): | ||||
'''revert expanded keywords in the working directory | """revert expanded keywords in the working directory | ||||
Must be run before changing/disabling active keywords. | Must be run before changing/disabling active keywords. | ||||
kwshrink refuses to run if given files contain local changes. | kwshrink refuses to run if given files contain local changes. | ||||
''' | """ | ||||
# 3rd argument sets expansion to False | # 3rd argument sets expansion to False | ||||
_kwfwrite(ui, repo, False, *pats, **opts) | _kwfwrite(ui, repo, False, *pats, **opts) | ||||
# monkeypatches | # monkeypatches | ||||
def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None): | def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None): | ||||
'''Monkeypatch/wrap patch.patchfile.__init__ to avoid | """Monkeypatch/wrap patch.patchfile.__init__ to avoid | ||||
rejects or conflicts due to expanded keywords in working dir.''' | rejects or conflicts due to expanded keywords in working dir.""" | ||||
orig(self, ui, gp, backend, store, eolmode) | orig(self, ui, gp, backend, store, eolmode) | ||||
kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None) | kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None) | ||||
if kwt: | if kwt: | ||||
# shrink keywords read from working dir | # shrink keywords read from working dir | ||||
self.lines = kwt.shrinklines(self.fname, self.lines) | self.lines = kwt.shrinklines(self.fname, self.lines) | ||||
def kwdiff(orig, repo, *args, **kwargs): | def kwdiff(orig, repo, *args, **kwargs): | ||||
ctx = repo[newid] | ctx = repo[newid] | ||||
kwt.restrict = True | kwt.restrict = True | ||||
kwt.overwrite(ctx, ctx.files(), False, True) | kwt.overwrite(ctx, ctx.files(), False, True) | ||||
kwt.restrict = False | kwt.restrict = False | ||||
return newid | return newid | ||||
def kw_copy(orig, ui, repo, pats, opts, rename=False): | def kw_copy(orig, ui, repo, pats, opts, rename=False): | ||||
'''Wraps cmdutil.copy so that copy/rename destinations do not | """Wraps cmdutil.copy so that copy/rename destinations do not | ||||
contain expanded keywords. | contain expanded keywords. | ||||
Note that the source of a regular file destination may also be a | Note that the source of a regular file destination may also be a | ||||
symlink: | symlink: | ||||
hg cp sym x -> x is symlink | hg cp sym x -> x is symlink | ||||
cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords) | cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords) | ||||
For the latter we have to follow the symlink to find out whether its | For the latter we have to follow the symlink to find out whether its | ||||
target is configured for expansion and we therefore must unexpand the | target is configured for expansion and we therefore must unexpand the | ||||
keywords in the destination.''' | keywords in the destination.""" | ||||
kwt = getattr(repo, '_keywordkwt', None) | kwt = getattr(repo, '_keywordkwt', None) | ||||
if kwt is None: | if kwt is None: | ||||
return orig(ui, repo, pats, opts, rename) | return orig(ui, repo, pats, opts, rename) | ||||
with repo.wlock(): | with repo.wlock(): | ||||
orig(ui, repo, pats, opts, rename) | orig(ui, repo, pats, opts, rename) | ||||
if opts.get(b'dry_run'): | if opts.get(b'dry_run'): | ||||
return | return | ||||
wctx = repo[None] | wctx = repo[None] | ||||
cwd = repo.getcwd() | cwd = repo.getcwd() | ||||
def haskwsource(dest): | def haskwsource(dest): | ||||
'''Returns true if dest is a regular file and configured for | """Returns true if dest is a regular file and configured for | ||||
expansion or a symlink which points to a file configured for | expansion or a symlink which points to a file configured for | ||||
expansion. ''' | expansion.""" | ||||
source = repo.dirstate.copied(dest) | source = repo.dirstate.copied(dest) | ||||
if b'l' in wctx.flags(source): | if b'l' in wctx.flags(source): | ||||
source = pathutil.canonpath( | source = pathutil.canonpath( | ||||
repo.root, cwd, os.path.realpath(source) | repo.root, cwd, os.path.realpath(source) | ||||
) | ) | ||||
return kwt.match(source) | return kwt.match(source) | ||||
candidates = [ | candidates = [ | ||||
) | ) | ||||
or self.size() == fctx.size() | or self.size() == fctx.size() | ||||
): | ): | ||||
return self._filelog.cmp(self._filenode, fctx.data()) | return self._filelog.cmp(self._filenode, fctx.data()) | ||||
return True | return True | ||||
def uisetup(ui): | def uisetup(ui): | ||||
''' Monkeypatches dispatch._parse to retrieve user command. | """Monkeypatches dispatch._parse to retrieve user command. | ||||
Overrides file method to return kwfilelog instead of filelog | Overrides file method to return kwfilelog instead of filelog | ||||
if file matches user configuration. | if file matches user configuration. | ||||
Wraps commit to overwrite configured files with updated | Wraps commit to overwrite configured files with updated | ||||
keyword substitutions. | keyword substitutions. | ||||
Monkeypatches patch and webcommands.''' | Monkeypatches patch and webcommands.""" | ||||
def kwdispatch_parse(orig, ui, args): | def kwdispatch_parse(orig, ui, args): | ||||
'''Monkeypatch dispatch._parse to obtain running hg command.''' | '''Monkeypatch dispatch._parse to obtain running hg command.''' | ||||
cmd, func, args, options, cmdoptions = orig(ui, args) | cmd, func, args, options, cmdoptions = orig(ui, args) | ||||
kwtools[b'hgcmd'] = cmd | kwtools[b'hgcmd'] = cmd | ||||
return cmd, func, args, options, cmdoptions | return cmd, func, args, options, cmdoptions | ||||
extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse) | extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse) |
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
eh = exthelper.exthelper() | eh = exthelper.exthelper() | ||||
eh.merge(lfcommands.eh) | eh.merge(lfcommands.eh) | ||||
eh.merge(overrides.eh) | eh.merge(overrides.eh) | ||||
eh.merge(proto.eh) | eh.merge(proto.eh) | ||||
eh.configitem( | eh.configitem( | ||||
b'largefiles', b'minsize', default=eh.configitem.dynamicdefault, | b'largefiles', | ||||
b'minsize', | |||||
default=eh.configitem.dynamicdefault, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'largefiles', b'patterns', default=list, | b'largefiles', | ||||
b'patterns', | |||||
default=list, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'largefiles', b'usercache', default=None, | b'largefiles', | ||||
b'usercache', | |||||
default=None, | |||||
) | ) | ||||
cmdtable = eh.cmdtable | cmdtable = eh.cmdtable | ||||
configtable = eh.configtable | configtable = eh.configtable | ||||
extsetup = eh.finalextsetup | extsetup = eh.finalextsetup | ||||
reposetup = reposetup.reposetup | reposetup = reposetup.reposetup | ||||
uisetup = eh.finaluisetup | uisetup = eh.finaluisetup | ||||
from mercurial.i18n import _ | from mercurial.i18n import _ | ||||
from mercurial import node, util | from mercurial import node, util | ||||
from . import lfutil | from . import lfutil | ||||
class StoreError(Exception): | class StoreError(Exception): | ||||
'''Raised when there is a problem getting files from or putting | """Raised when there is a problem getting files from or putting | ||||
files to a central store.''' | files to a central store.""" | ||||
def __init__(self, filename, hash, url, detail): | def __init__(self, filename, hash, url, detail): | ||||
self.filename = filename | self.filename = filename | ||||
self.hash = hash | self.hash = hash | ||||
self.url = url | self.url = url | ||||
self.detail = detail | self.detail = detail | ||||
def longmessage(self): | def longmessage(self): | ||||
self.repo = repo | self.repo = repo | ||||
self.url = url | self.url = url | ||||
def put(self, source, hash): | def put(self, source, hash): | ||||
'''Put source file into the store so it can be retrieved by hash.''' | '''Put source file into the store so it can be retrieved by hash.''' | ||||
raise NotImplementedError(b'abstract method') | raise NotImplementedError(b'abstract method') | ||||
def exists(self, hashes): | def exists(self, hashes): | ||||
'''Check to see if the store contains the given hashes. Given an | """Check to see if the store contains the given hashes. Given an | ||||
iterable of hashes it returns a mapping from hash to bool.''' | iterable of hashes it returns a mapping from hash to bool.""" | ||||
raise NotImplementedError(b'abstract method') | raise NotImplementedError(b'abstract method') | ||||
def get(self, files): | def get(self, files): | ||||
'''Get the specified largefiles from the store and write to local | """Get the specified largefiles from the store and write to local | ||||
files under repo.root. files is a list of (filename, hash) | files under repo.root. files is a list of (filename, hash) | ||||
tuples. Return (success, missing), lists of files successfully | tuples. Return (success, missing), lists of files successfully | ||||
downloaded and those not found in the store. success is a list | downloaded and those not found in the store. success is a list | ||||
of (filename, hash) tuples; missing is a list of filenames that | of (filename, hash) tuples; missing is a list of filenames that | ||||
we could not get. (The detailed error message will already have | we could not get. (The detailed error message will already have | ||||
been presented to the user, so missing is just supplied as a | been presented to the user, so missing is just supplied as a | ||||
summary.)''' | summary.)""" | ||||
success = [] | success = [] | ||||
missing = [] | missing = [] | ||||
ui = self.ui | ui = self.ui | ||||
at = 0 | at = 0 | ||||
available = self.exists({hash for (_filename, hash) in files}) | available = self.exists({hash for (_filename, hash) in files}) | ||||
with ui.makeprogress( | with ui.makeprogress( | ||||
_(b'getting largefiles'), unit=_(b'files'), total=len(files) | _(b'getting largefiles'), unit=_(b'files'), total=len(files) | ||||
util.unlink(tmpname) | util.unlink(tmpname) | ||||
return False | return False | ||||
util.rename(tmpname, storefilename) | util.rename(tmpname, storefilename) | ||||
lfutil.linktousercache(self.repo, hash) | lfutil.linktousercache(self.repo, hash) | ||||
return True | return True | ||||
def verify(self, revs, contents=False): | def verify(self, revs, contents=False): | ||||
'''Verify the existence (and, optionally, contents) of every big | """Verify the existence (and, optionally, contents) of every big | ||||
file revision referenced by every changeset in revs. | file revision referenced by every changeset in revs. | ||||
Return 0 if all is well, non-zero on any errors.''' | Return 0 if all is well, non-zero on any errors.""" | ||||
self.ui.status( | self.ui.status( | ||||
_(b'searching %d changesets for largefiles\n') % len(revs) | _(b'searching %d changesets for largefiles\n') % len(revs) | ||||
) | ) | ||||
verified = set() # set of (filename, filenode) tuples | verified = set() # set of (filename, filenode) tuples | ||||
filestocheck = [] # list of (cset, filename, expectedhash) | filestocheck = [] # list of (cset, filename, expectedhash) | ||||
for rev in revs: | for rev in revs: | ||||
cctx = self.repo[rev] | cctx = self.repo[rev] | ||||
else: | else: | ||||
self.ui.status( | self.ui.status( | ||||
_(b'verified existence of %d revisions of %d largefiles\n') | _(b'verified existence of %d revisions of %d largefiles\n') | ||||
% (numrevs, numlfiles) | % (numrevs, numlfiles) | ||||
) | ) | ||||
return int(failed) | return int(failed) | ||||
def _getfile(self, tmpfile, filename, hash): | def _getfile(self, tmpfile, filename, hash): | ||||
'''Fetch one revision of one file from the store and write it | """Fetch one revision of one file from the store and write it | ||||
to tmpfile. Compute the hash of the file on-the-fly as it | to tmpfile. Compute the hash of the file on-the-fly as it | ||||
downloads and return the hash. Close tmpfile. Raise | downloads and return the hash. Close tmpfile. Raise | ||||
StoreError if unable to download the file (e.g. it does not | StoreError if unable to download the file (e.g. it does not | ||||
exist in the store).''' | exist in the store).""" | ||||
raise NotImplementedError(b'abstract method') | raise NotImplementedError(b'abstract method') | ||||
def _verifyfiles(self, contents, filestocheck): | def _verifyfiles(self, contents, filestocheck): | ||||
'''Perform the actual verification of files in the store. | """Perform the actual verification of files in the store. | ||||
'contents' controls verification of content hash. | 'contents' controls verification of content hash. | ||||
'filestocheck' is list of files to check. | 'filestocheck' is list of files to check. | ||||
Returns _true_ if any problems are found! | Returns _true_ if any problems are found! | ||||
''' | """ | ||||
raise NotImplementedError(b'abstract method') | raise NotImplementedError(b'abstract method') |
_(b'convert from a largefiles repo to a normal repo'), | _(b'convert from a largefiles repo to a normal repo'), | ||||
), | ), | ||||
], | ], | ||||
_(b'hg lfconvert SOURCE DEST [FILE ...]'), | _(b'hg lfconvert SOURCE DEST [FILE ...]'), | ||||
norepo=True, | norepo=True, | ||||
inferrepo=True, | inferrepo=True, | ||||
) | ) | ||||
def lfconvert(ui, src, dest, *pats, **opts): | def lfconvert(ui, src, dest, *pats, **opts): | ||||
'''convert a normal repository to a largefiles repository | """convert a normal repository to a largefiles repository | ||||
Convert repository SOURCE to a new repository DEST, identical to | Convert repository SOURCE to a new repository DEST, identical to | ||||
SOURCE except that certain files will be converted as largefiles: | SOURCE except that certain files will be converted as largefiles: | ||||
specifically, any file that matches any PATTERN *or* whose size is | specifically, any file that matches any PATTERN *or* whose size is | ||||
above the minimum size threshold is converted as a largefile. The | above the minimum size threshold is converted as a largefile. The | ||||
size used to determine whether or not to track a file as a | size used to determine whether or not to track a file as a | ||||
largefile is the size of the first version of the file. The | largefile is the size of the first version of the file. The | ||||
minimum size can be specified either with --size or in | minimum size can be specified either with --size or in | ||||
configuration as ``largefiles.size``. | configuration as ``largefiles.size``. | ||||
After running this command you will need to make sure that | After running this command you will need to make sure that | ||||
largefiles is enabled anywhere you intend to push the new | largefiles is enabled anywhere you intend to push the new | ||||
repository. | repository. | ||||
Use --to-normal to convert largefiles back to normal files; after | Use --to-normal to convert largefiles back to normal files; after | ||||
this, the DEST repository can be used without largefiles at all.''' | this, the DEST repository can be used without largefiles at all.""" | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
if opts[b'to_normal']: | if opts[b'to_normal']: | ||||
tolfile = False | tolfile = False | ||||
else: | else: | ||||
tolfile = True | tolfile = True | ||||
size = lfutil.getminsize(ui, True, opts.get(b'size'), default=None) | size = lfutil.getminsize(ui, True, opts.get(b'size'), default=None) | ||||
newdata.append(b'%s %s\n' % (node.hex(revmap[newid]), name)) | newdata.append(b'%s %s\n' % (node.hex(revmap[newid]), name)) | ||||
except KeyError: | except KeyError: | ||||
ui.warn(_(b'no mapping for id %s\n') % id) | ui.warn(_(b'no mapping for id %s\n') % id) | ||||
continue | continue | ||||
return b''.join(newdata) | return b''.join(newdata) | ||||
def _islfile(file, ctx, matcher, size): | def _islfile(file, ctx, matcher, size): | ||||
'''Return true if file should be considered a largefile, i.e. | """Return true if file should be considered a largefile, i.e. | ||||
matcher matches it or it is larger than size.''' | matcher matches it or it is larger than size.""" | ||||
# never store special .hg* files as largefiles | # never store special .hg* files as largefiles | ||||
if file == b'.hgtags' or file == b'.hgignore' or file == b'.hgsigs': | if file == b'.hgtags' or file == b'.hgignore' or file == b'.hgsigs': | ||||
return False | return False | ||||
if matcher and matcher(file): | if matcher and matcher(file): | ||||
return True | return True | ||||
try: | try: | ||||
return ctx.filectx(file).size() >= size * 1024 * 1024 | return ctx.filectx(file).size() >= size * 1024 * 1024 | ||||
except error.LookupError: | except error.LookupError: | ||||
% hash | % hash | ||||
) | ) | ||||
# XXX check for errors here | # XXX check for errors here | ||||
store.put(source, hash) | store.put(source, hash) | ||||
at += 1 | at += 1 | ||||
def verifylfiles(ui, repo, all=False, contents=False): | def verifylfiles(ui, repo, all=False, contents=False): | ||||
'''Verify that every largefile revision in the current changeset | """Verify that every largefile revision in the current changeset | ||||
exists in the central store. With --contents, also verify that | exists in the central store. With --contents, also verify that | ||||
the contents of each local largefile file revision are correct (SHA-1 hash | the contents of each local largefile file revision are correct (SHA-1 hash | ||||
matches the revision ID). With --all, check every changeset in | matches the revision ID). With --all, check every changeset in | ||||
this repository.''' | this repository.""" | ||||
if all: | if all: | ||||
revs = repo.revs(b'all()') | revs = repo.revs(b'all()') | ||||
else: | else: | ||||
revs = [b'.'] | revs = [b'.'] | ||||
store = storefactory.openstore(repo) | store = storefactory.openstore(repo) | ||||
return store.verify(revs, contents=contents) | return store.verify(revs, contents=contents) | ||||
def cachelfiles(ui, repo, node, filelist=None): | def cachelfiles(ui, repo, node, filelist=None): | ||||
'''cachelfiles ensures that all largefiles needed by the specified revision | """cachelfiles ensures that all largefiles needed by the specified revision | ||||
are present in the repository's largefile cache. | are present in the repository's largefile cache. | ||||
returns a tuple (cached, missing). cached is the list of files downloaded | returns a tuple (cached, missing). cached is the list of files downloaded | ||||
by this operation; missing is the list of files that were needed but could | by this operation; missing is the list of files that were needed but could | ||||
not be found.''' | not be found.""" | ||||
lfiles = lfutil.listlfiles(repo, node) | lfiles = lfutil.listlfiles(repo, node) | ||||
if filelist: | if filelist: | ||||
lfiles = set(lfiles) & set(filelist) | lfiles = set(lfiles) & set(filelist) | ||||
toget = [] | toget = [] | ||||
ctx = repo[node] | ctx = repo[node] | ||||
for lfile in lfiles: | for lfile in lfiles: | ||||
try: | try: | ||||
if totalmissing > 0: | if totalmissing > 0: | ||||
ui.status(_(b"%d largefiles failed to download\n") % totalmissing) | ui.status(_(b"%d largefiles failed to download\n") % totalmissing) | ||||
return totalsuccess, totalmissing | return totalsuccess, totalmissing | ||||
def updatelfiles( | def updatelfiles( | ||||
ui, repo, filelist=None, printmessage=None, normallookup=False | ui, repo, filelist=None, printmessage=None, normallookup=False | ||||
): | ): | ||||
'''Update largefiles according to standins in the working directory | """Update largefiles according to standins in the working directory | ||||
If ``printmessage`` is other than ``None``, it means "print (or | If ``printmessage`` is other than ``None``, it means "print (or | ||||
ignore, for false) message forcibly". | ignore, for false) message forcibly". | ||||
''' | """ | ||||
statuswriter = lfutil.getstatuswriter(ui, repo, printmessage) | statuswriter = lfutil.getstatuswriter(ui, repo, printmessage) | ||||
with repo.wlock(): | with repo.wlock(): | ||||
lfdirstate = lfutil.openlfdirstate(ui, repo) | lfdirstate = lfutil.openlfdirstate(ui, repo) | ||||
lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate) | lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate) | ||||
if filelist is not None: | if filelist is not None: | ||||
filelist = set(filelist) | filelist = set(filelist) | ||||
lfiles = [f for f in lfiles if f in filelist] | lfiles = [f for f in lfiles if f in filelist] |
# if hardlinks fail, fallback on atomic copy | # if hardlinks fail, fallback on atomic copy | ||||
with open(src, b'rb') as srcf, util.atomictempfile(dest) as dstf: | with open(src, b'rb') as srcf, util.atomictempfile(dest) as dstf: | ||||
for chunk in util.filechunkiter(srcf): | for chunk in util.filechunkiter(srcf): | ||||
dstf.write(chunk) | dstf.write(chunk) | ||||
os.chmod(dest, os.stat(src).st_mode) | os.chmod(dest, os.stat(src).st_mode) | ||||
def usercachepath(ui, hash): | def usercachepath(ui, hash): | ||||
'''Return the correct location in the "global" largefiles cache for a file | """Return the correct location in the "global" largefiles cache for a file | ||||
with the given hash. | with the given hash. | ||||
This cache is used for sharing of largefiles across repositories - both | This cache is used for sharing of largefiles across repositories - both | ||||
to preserve download bandwidth and storage space.''' | to preserve download bandwidth and storage space.""" | ||||
return os.path.join(_usercachedir(ui), hash) | return os.path.join(_usercachedir(ui), hash) | ||||
def _usercachedir(ui, name=longname): | def _usercachedir(ui, name=longname): | ||||
'''Return the location of the "global" largefiles cache.''' | '''Return the location of the "global" largefiles cache.''' | ||||
path = ui.configpath(name, b'usercache') | path = ui.configpath(name, b'usercache') | ||||
if path: | if path: | ||||
return path | return path | ||||
def inusercache(ui, hash): | def inusercache(ui, hash): | ||||
path = usercachepath(ui, hash) | path = usercachepath(ui, hash) | ||||
return os.path.exists(path) | return os.path.exists(path) | ||||
def findfile(repo, hash): | def findfile(repo, hash): | ||||
'''Return store path of the largefile with the specified hash. | """Return store path of the largefile with the specified hash. | ||||
As a side effect, the file might be linked from user cache. | As a side effect, the file might be linked from user cache. | ||||
Return None if the file can't be found locally.''' | Return None if the file can't be found locally.""" | ||||
path, exists = findstorepath(repo, hash) | path, exists = findstorepath(repo, hash) | ||||
if exists: | if exists: | ||||
repo.ui.note(_(b'found %s in store\n') % hash) | repo.ui.note(_(b'found %s in store\n') % hash) | ||||
return path | return path | ||||
elif inusercache(repo.ui, hash): | elif inusercache(repo.ui, hash): | ||||
repo.ui.note(_(b'found %s in system cache\n') % hash) | repo.ui.note(_(b'found %s in system cache\n') % hash) | ||||
path = storepath(repo, hash) | path = storepath(repo, hash) | ||||
link(usercachepath(repo.ui, hash), path) | link(usercachepath(repo.ui, hash), path) | ||||
def write(self, tr=False): | def write(self, tr=False): | ||||
# (1) disable PENDING mode always | # (1) disable PENDING mode always | ||||
# (lfdirstate isn't yet managed as a part of the transaction) | # (lfdirstate isn't yet managed as a part of the transaction) | ||||
# (2) avoid develwarn 'use dirstate.write with ....' | # (2) avoid develwarn 'use dirstate.write with ....' | ||||
super(largefilesdirstate, self).write(None) | super(largefilesdirstate, self).write(None) | ||||
def openlfdirstate(ui, repo, create=True): | def openlfdirstate(ui, repo, create=True): | ||||
''' | """ | ||||
Return a dirstate object that tracks largefiles: i.e. its root is | Return a dirstate object that tracks largefiles: i.e. its root is | ||||
the repo root, but it is saved in .hg/largefiles/dirstate. | the repo root, but it is saved in .hg/largefiles/dirstate. | ||||
''' | """ | ||||
vfs = repo.vfs | vfs = repo.vfs | ||||
lfstoredir = longname | lfstoredir = longname | ||||
opener = vfsmod.vfs(vfs.join(lfstoredir)) | opener = vfsmod.vfs(vfs.join(lfstoredir)) | ||||
lfdirstate = largefilesdirstate( | lfdirstate = largefilesdirstate( | ||||
opener, | opener, | ||||
ui, | ui, | ||||
repo.root, | repo.root, | ||||
repo.dirstate._validate, | repo.dirstate._validate, | ||||
modified.append(lfile) | modified.append(lfile) | ||||
else: | else: | ||||
clean.append(lfile) | clean.append(lfile) | ||||
lfdirstate.normal(lfile) | lfdirstate.normal(lfile) | ||||
return s | return s | ||||
def listlfiles(repo, rev=None, matcher=None): | def listlfiles(repo, rev=None, matcher=None): | ||||
'''return a list of largefiles in the working copy or the | """return a list of largefiles in the working copy or the | ||||
specified changeset''' | specified changeset""" | ||||
if matcher is None: | if matcher is None: | ||||
matcher = getstandinmatcher(repo) | matcher = getstandinmatcher(repo) | ||||
# ignore unknown files in working directory | # ignore unknown files in working directory | ||||
return [ | return [ | ||||
splitstandin(f) | splitstandin(f) | ||||
for f in repo[rev].walk(matcher) | for f in repo[rev].walk(matcher) | ||||
if rev is not None or repo.dirstate[f] != b'?' | if rev is not None or repo.dirstate[f] != b'?' | ||||
] | ] | ||||
def instore(repo, hash, forcelocal=False): | def instore(repo, hash, forcelocal=False): | ||||
'''Return true if a largefile with the given hash exists in the store''' | '''Return true if a largefile with the given hash exists in the store''' | ||||
return os.path.exists(storepath(repo, hash, forcelocal)) | return os.path.exists(storepath(repo, hash, forcelocal)) | ||||
def storepath(repo, hash, forcelocal=False): | def storepath(repo, hash, forcelocal=False): | ||||
'''Return the correct location in the repository largefiles store for a | """Return the correct location in the repository largefiles store for a | ||||
file with the given hash.''' | file with the given hash.""" | ||||
if not forcelocal and repo.shared(): | if not forcelocal and repo.shared(): | ||||
return repo.vfs.reljoin(repo.sharedpath, longname, hash) | return repo.vfs.reljoin(repo.sharedpath, longname, hash) | ||||
return repo.vfs.join(longname, hash) | return repo.vfs.join(longname, hash) | ||||
def findstorepath(repo, hash): | def findstorepath(repo, hash): | ||||
'''Search through the local store path(s) to find the file for the given | """Search through the local store path(s) to find the file for the given | ||||
hash. If the file is not found, its path in the primary store is returned. | hash. If the file is not found, its path in the primary store is returned. | ||||
The return value is a tuple of (path, exists(path)). | The return value is a tuple of (path, exists(path)). | ||||
''' | """ | ||||
# For shared repos, the primary store is in the share source. But for | # For shared repos, the primary store is in the share source. But for | ||||
# backward compatibility, force a lookup in the local store if it wasn't | # backward compatibility, force a lookup in the local store if it wasn't | ||||
# found in the share source. | # found in the share source. | ||||
path = storepath(repo, hash, False) | path = storepath(repo, hash, False) | ||||
if instore(repo, hash): | if instore(repo, hash): | ||||
return (path, True) | return (path, True) | ||||
elif repo.shared() and instore(repo, hash, True): | elif repo.shared() and instore(repo, hash, True): | ||||
return storepath(repo, hash, True), True | return storepath(repo, hash, True), True | ||||
return (path, False) | return (path, False) | ||||
def copyfromcache(repo, hash, filename): | def copyfromcache(repo, hash, filename): | ||||
'''Copy the specified largefile from the repo or system cache to | """Copy the specified largefile from the repo or system cache to | ||||
filename in the repository. Return true on success or false if the | filename in the repository. Return true on success or false if the | ||||
file was not found in either cache (which should not happened: | file was not found in either cache (which should not happened: | ||||
this is meant to be called only after ensuring that the needed | this is meant to be called only after ensuring that the needed | ||||
largefile exists in the cache).''' | largefile exists in the cache).""" | ||||
wvfs = repo.wvfs | wvfs = repo.wvfs | ||||
path = findfile(repo, hash) | path = findfile(repo, hash) | ||||
if path is None: | if path is None: | ||||
return False | return False | ||||
wvfs.makedirs(wvfs.dirname(wvfs.join(filename))) | wvfs.makedirs(wvfs.dirname(wvfs.join(filename))) | ||||
# The write may fail before the file is fully written, but we | # The write may fail before the file is fully written, but we | ||||
# don't use atomic writes in the working copy. | # don't use atomic writes in the working copy. | ||||
with open(path, b'rb') as srcfd, wvfs(filename, b'wb') as destfd: | with open(path, b'rb') as srcfd, wvfs(filename, b'wb') as destfd: | ||||
storepath(repo, hash), createmode=repo.store.createmode | storepath(repo, hash), createmode=repo.store.createmode | ||||
) as dstf: | ) as dstf: | ||||
for chunk in util.filechunkiter(srcf): | for chunk in util.filechunkiter(srcf): | ||||
dstf.write(chunk) | dstf.write(chunk) | ||||
linktousercache(repo, hash) | linktousercache(repo, hash) | ||||
def linktousercache(repo, hash): | def linktousercache(repo, hash): | ||||
'''Link / copy the largefile with the specified hash from the store | """Link / copy the largefile with the specified hash from the store | ||||
to the cache.''' | to the cache.""" | ||||
path = usercachepath(repo.ui, hash) | path = usercachepath(repo.ui, hash) | ||||
link(storepath(repo, hash), path) | link(storepath(repo, hash), path) | ||||
def getstandinmatcher(repo, rmatcher=None): | def getstandinmatcher(repo, rmatcher=None): | ||||
'''Return a match object that applies rmatcher to the standin directory''' | '''Return a match object that applies rmatcher to the standin directory''' | ||||
wvfs = repo.wvfs | wvfs = repo.wvfs | ||||
standindir = shortname | standindir = shortname | ||||
# no warnings about missing files or directories | # no warnings about missing files or directories | ||||
badfn = lambda f, msg: None | badfn = lambda f, msg: None | ||||
if rmatcher and not rmatcher.always(): | if rmatcher and not rmatcher.always(): | ||||
pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()] | pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()] | ||||
if not pats: | if not pats: | ||||
pats = [wvfs.join(standindir)] | pats = [wvfs.join(standindir)] | ||||
match = scmutil.match(repo[None], pats, badfn=badfn) | match = scmutil.match(repo[None], pats, badfn=badfn) | ||||
else: | else: | ||||
# no patterns: relative to repo root | # no patterns: relative to repo root | ||||
match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn) | match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn) | ||||
return match | return match | ||||
def composestandinmatcher(repo, rmatcher): | def composestandinmatcher(repo, rmatcher): | ||||
'''Return a matcher that accepts standins corresponding to the | """Return a matcher that accepts standins corresponding to the | ||||
files accepted by rmatcher. Pass the list of files in the matcher | files accepted by rmatcher. Pass the list of files in the matcher | ||||
as the paths specified by the user.''' | as the paths specified by the user.""" | ||||
smatcher = getstandinmatcher(repo, rmatcher) | smatcher = getstandinmatcher(repo, rmatcher) | ||||
isstandin = smatcher.matchfn | isstandin = smatcher.matchfn | ||||
def composedmatchfn(f): | def composedmatchfn(f): | ||||
return isstandin(f) and rmatcher.matchfn(splitstandin(f)) | return isstandin(f) and rmatcher.matchfn(splitstandin(f)) | ||||
smatcher.matchfn = composedmatchfn | smatcher.matchfn = composedmatchfn | ||||
return smatcher | return smatcher | ||||
def standin(filename): | def standin(filename): | ||||
'''Return the repo-relative path to the standin for the specified big | """Return the repo-relative path to the standin for the specified big | ||||
file.''' | file.""" | ||||
# Notes: | # Notes: | ||||
# 1) Some callers want an absolute path, but for instance addlargefiles | # 1) Some callers want an absolute path, but for instance addlargefiles | ||||
# needs it repo-relative so it can be passed to repo[None].add(). So | # needs it repo-relative so it can be passed to repo[None].add(). So | ||||
# leave it up to the caller to use repo.wjoin() to get an absolute path. | # leave it up to the caller to use repo.wjoin() to get an absolute path. | ||||
# 2) Join with '/' because that's what dirstate always uses, even on | # 2) Join with '/' because that's what dirstate always uses, even on | ||||
# Windows. Change existing separator to '/' first in case we are | # Windows. Change existing separator to '/' first in case we are | ||||
# passed filenames from an external source (like the command line). | # passed filenames from an external source (like the command line). | ||||
return shortnameslash + util.pconvert(filename) | return shortnameslash + util.pconvert(filename) | ||||
def isstandin(filename): | def isstandin(filename): | ||||
'''Return true if filename is a big file standin. filename must be | """Return true if filename is a big file standin. filename must be | ||||
in Mercurial's internal form (slash-separated).''' | in Mercurial's internal form (slash-separated).""" | ||||
return filename.startswith(shortnameslash) | return filename.startswith(shortnameslash) | ||||
def splitstandin(filename): | def splitstandin(filename): | ||||
# Split on / because that's what dirstate always uses, even on Windows. | # Split on / because that's what dirstate always uses, even on Windows. | ||||
# Change local separator to / first just in case we are passed filenames | # Change local separator to / first just in case we are passed filenames | ||||
# from an external source (like the command line). | # from an external source (like the command line). | ||||
bits = util.pconvert(filename).split(b'/', 1) | bits = util.pconvert(filename).split(b'/', 1) | ||||
hash = hashfile(file) | hash = hashfile(file) | ||||
executable = getexecutable(file) | executable = getexecutable(file) | ||||
writestandin(repo, standin, hash, executable) | writestandin(repo, standin, hash, executable) | ||||
else: | else: | ||||
raise error.Abort(_(b'%s: file not found!') % lfile) | raise error.Abort(_(b'%s: file not found!') % lfile) | ||||
def readasstandin(fctx): | def readasstandin(fctx): | ||||
'''read hex hash from given filectx of standin file | """read hex hash from given filectx of standin file | ||||
This encapsulates how "standin" data is stored into storage layer.''' | This encapsulates how "standin" data is stored into storage layer.""" | ||||
return fctx.data().strip() | return fctx.data().strip() | ||||
def writestandin(repo, standin, hash, executable): | def writestandin(repo, standin, hash, executable): | ||||
'''write hash to <repo.root>/<standin>''' | '''write hash to <repo.root>/<standin>''' | ||||
repo.wwrite(standin, hash + b'\n', executable and b'x' or b'') | repo.wwrite(standin, hash + b'\n', executable and b'x' or b'') | ||||
def copyandhash(instream, outfile): | def copyandhash(instream, outfile): | ||||
'''Read bytes from instream (iterable) and write them to outfile, | """Read bytes from instream (iterable) and write them to outfile, | ||||
computing the SHA-1 hash of the data along the way. Return the hash.''' | computing the SHA-1 hash of the data along the way. Return the hash.""" | ||||
hasher = hashutil.sha1(b'') | hasher = hashutil.sha1(b'') | ||||
for data in instream: | for data in instream: | ||||
hasher.update(data) | hasher.update(data) | ||||
outfile.write(data) | outfile.write(data) | ||||
return hex(hasher.digest()) | return hex(hasher.digest()) | ||||
def hashfile(file): | def hashfile(file): | ||||
if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None): | if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None): | ||||
files.add(f) | files.add(f) | ||||
for fn in files: | for fn in files: | ||||
if isstandin(fn) and fn in ctx: | if isstandin(fn) and fn in ctx: | ||||
addfunc(fn, readasstandin(ctx[fn])) | addfunc(fn, readasstandin(ctx[fn])) | ||||
def updatestandinsbymatch(repo, match): | def updatestandinsbymatch(repo, match): | ||||
'''Update standins in the working directory according to specified match | """Update standins in the working directory according to specified match | ||||
This returns (possibly modified) ``match`` object to be used for | This returns (possibly modified) ``match`` object to be used for | ||||
subsequent commit process. | subsequent commit process. | ||||
''' | """ | ||||
ui = repo.ui | ui = repo.ui | ||||
# Case 1: user calls commit with no specific files or | # Case 1: user calls commit with no specific files or | ||||
# include/exclude patterns: refresh and commit all files that | # include/exclude patterns: refresh and commit all files that | ||||
# are "dirty". | # are "dirty". | ||||
if match is None or match.always(): | if match is None or match.always(): | ||||
# Spend a bit of time here to get a list of files we know | # Spend a bit of time here to get a list of files we know | ||||
return f in standins | return f in standins | ||||
match.matchfn = matchfn | match.matchfn = matchfn | ||||
return match | return match | ||||
class automatedcommithook(object): | class automatedcommithook(object): | ||||
'''Stateful hook to update standins at the 1st commit of resuming | """Stateful hook to update standins at the 1st commit of resuming | ||||
For efficiency, updating standins in the working directory should | For efficiency, updating standins in the working directory should | ||||
be avoided while automated committing (like rebase, transplant and | be avoided while automated committing (like rebase, transplant and | ||||
so on), because they should be updated before committing. | so on), because they should be updated before committing. | ||||
But the 1st commit of resuming automated committing (e.g. ``rebase | But the 1st commit of resuming automated committing (e.g. ``rebase | ||||
--continue``) should update them, because largefiles may be | --continue``) should update them, because largefiles may be | ||||
modified manually. | modified manually. | ||||
''' | """ | ||||
def __init__(self, resuming): | def __init__(self, resuming): | ||||
self.resuming = resuming | self.resuming = resuming | ||||
def __call__(self, repo, match): | def __call__(self, repo, match): | ||||
if self.resuming: | if self.resuming: | ||||
self.resuming = False # avoids updating at subsequent commits | self.resuming = False # avoids updating at subsequent commits | ||||
return updatestandinsbymatch(repo, match) | return updatestandinsbymatch(repo, match) | ||||
else: | else: | ||||
return match | return match | ||||
def getstatuswriter(ui, repo, forcibly=None): | def getstatuswriter(ui, repo, forcibly=None): | ||||
'''Return the function to write largefiles specific status out | """Return the function to write largefiles specific status out | ||||
If ``forcibly`` is ``None``, this returns the last element of | If ``forcibly`` is ``None``, this returns the last element of | ||||
``repo._lfstatuswriters`` as "default" writer function. | ``repo._lfstatuswriters`` as "default" writer function. | ||||
Otherwise, this returns the function to always write out (or | Otherwise, this returns the function to always write out (or | ||||
ignore if ``not forcibly``) status. | ignore if ``not forcibly``) status. | ||||
''' | """ | ||||
if forcibly is None and util.safehasattr(repo, b'_largefilesenabled'): | if forcibly is None and util.safehasattr(repo, b'_largefilesenabled'): | ||||
return repo._lfstatuswriters[-1] | return repo._lfstatuswriters[-1] | ||||
else: | else: | ||||
if forcibly: | if forcibly: | ||||
return ui.status # forcibly WRITE OUT | return ui.status # forcibly WRITE OUT | ||||
else: | else: | ||||
return lambda *msg, **opts: None # forcibly IGNORE | return lambda *msg, **opts: None # forcibly IGNORE |
from . import ( | from . import ( | ||||
basestore, | basestore, | ||||
lfutil, | lfutil, | ||||
) | ) | ||||
class localstore(basestore.basestore): | class localstore(basestore.basestore): | ||||
'''localstore first attempts to grab files out of the store in the remote | """localstore first attempts to grab files out of the store in the remote | ||||
Mercurial repository. Failing that, it attempts to grab the files from | Mercurial repository. Failing that, it attempts to grab the files from | ||||
the user cache.''' | the user cache.""" | ||||
def __init__(self, ui, repo, remote): | def __init__(self, ui, repo, remote): | ||||
self.remote = remote.local() | self.remote = remote.local() | ||||
super(localstore, self).__init__(ui, repo, self.remote.url()) | super(localstore, self).__init__(ui, repo, self.remote.url()) | ||||
def put(self, source, hash): | def put(self, source, hash): | ||||
if lfutil.instore(self.remote, hash): | if lfutil.instore(self.remote, hash): | ||||
return | return |
lfstatus = lfutil.lfstatus | lfstatus = lfutil.lfstatus | ||||
MERGE_ACTION_LARGEFILE_MARK_REMOVED = b'lfmr' | MERGE_ACTION_LARGEFILE_MARK_REMOVED = b'lfmr' | ||||
# -- Utility functions: commonly/repeatedly needed functionality --------------- | # -- Utility functions: commonly/repeatedly needed functionality --------------- | ||||
def composelargefilematcher(match, manifest): | def composelargefilematcher(match, manifest): | ||||
'''create a matcher that matches only the largefiles in the original | """create a matcher that matches only the largefiles in the original | ||||
matcher''' | matcher""" | ||||
m = copy.copy(match) | m = copy.copy(match) | ||||
lfile = lambda f: lfutil.standin(f) in manifest | lfile = lambda f: lfutil.standin(f) in manifest | ||||
m._files = [lf for lf in m._files if lfile(lf)] | m._files = [lf for lf in m._files if lfile(lf)] | ||||
m._fileset = set(m._files) | m._fileset = set(m._files) | ||||
m.always = lambda: False | m.always = lambda: False | ||||
origmatchfn = m.matchfn | origmatchfn = m.matchfn | ||||
m.matchfn = lambda f: lfile(f) and origmatchfn(f) | m.matchfn = lambda f: lfile(f) and origmatchfn(f) | ||||
return m | return m | ||||
) | ) | ||||
if repo.ui.promptchoice(usermsg, 0) == 0: # pick remote largefile | if repo.ui.promptchoice(usermsg, 0) == 0: # pick remote largefile | ||||
mresult.addfile(lfile, b'r', None, b'replaced by standin') | mresult.addfile(lfile, b'r', None, b'replaced by standin') | ||||
mresult.addfile(standin, b'g', sargs, b'replaces standin') | mresult.addfile(standin, b'g', sargs, b'replaces standin') | ||||
else: # keep local normal file | else: # keep local normal file | ||||
mresult.addfile(lfile, b'k', None, b'replaces standin') | mresult.addfile(lfile, b'k', None, b'replaces standin') | ||||
if branchmerge: | if branchmerge: | ||||
mresult.addfile( | mresult.addfile( | ||||
standin, b'k', None, b'replaced by non-standin', | standin, | ||||
b'k', | |||||
None, | |||||
b'replaced by non-standin', | |||||
) | ) | ||||
else: | else: | ||||
mresult.addfile( | mresult.addfile( | ||||
standin, b'r', None, b'replaced by non-standin', | standin, | ||||
b'r', | |||||
None, | |||||
b'replaced by non-standin', | |||||
) | ) | ||||
elif lm in (b'g', b'dc') and sm != b'r': | elif lm in (b'g', b'dc') and sm != b'r': | ||||
if lm == b'dc': | if lm == b'dc': | ||||
f1, f2, fa, move, anc = largs | f1, f2, fa, move, anc = largs | ||||
largs = (p2[f2].flags(), False) | largs = (p2[f2].flags(), False) | ||||
# Case 2: largefile in the working copy, normal file in | # Case 2: largefile in the working copy, normal file in | ||||
# the second parent | # the second parent | ||||
usermsg = ( | usermsg = ( | ||||
_( | _( | ||||
b'remote turned local largefile %s into a normal file\n' | b'remote turned local largefile %s into a normal file\n' | ||||
b'keep (l)argefile or use (n)ormal file?' | b'keep (l)argefile or use (n)ormal file?' | ||||
b'$$ &Largefile $$ &Normal file' | b'$$ &Largefile $$ &Normal file' | ||||
) | ) | ||||
% lfile | % lfile | ||||
) | ) | ||||
if repo.ui.promptchoice(usermsg, 0) == 0: # keep local largefile | if repo.ui.promptchoice(usermsg, 0) == 0: # keep local largefile | ||||
if branchmerge: | if branchmerge: | ||||
# largefile can be restored from standin safely | # largefile can be restored from standin safely | ||||
mresult.addfile( | mresult.addfile( | ||||
lfile, b'k', None, b'replaced by standin', | lfile, | ||||
b'k', | |||||
None, | |||||
b'replaced by standin', | |||||
) | ) | ||||
mresult.addfile(standin, b'k', None, b'replaces standin') | mresult.addfile(standin, b'k', None, b'replaces standin') | ||||
else: | else: | ||||
# "lfile" should be marked as "removed" without | # "lfile" should be marked as "removed" without | ||||
# removal of itself | # removal of itself | ||||
mresult.addfile( | mresult.addfile( | ||||
lfile, | lfile, | ||||
MERGE_ACTION_LARGEFILE_MARK_REMOVED, | MERGE_ACTION_LARGEFILE_MARK_REMOVED, | ||||
None, | None, | ||||
b'forget non-standin largefile', | b'forget non-standin largefile', | ||||
) | ) | ||||
# linear-merge should treat this largefile as 're-added' | # linear-merge should treat this largefile as 're-added' | ||||
mresult.addfile(standin, b'a', None, b'keep standin') | mresult.addfile(standin, b'a', None, b'keep standin') | ||||
else: # pick remote normal file | else: # pick remote normal file | ||||
mresult.addfile(lfile, b'g', largs, b'replaces standin') | mresult.addfile(lfile, b'g', largs, b'replaces standin') | ||||
mresult.addfile( | mresult.addfile( | ||||
standin, b'r', None, b'replaced by non-standin', | standin, | ||||
b'r', | |||||
None, | |||||
b'replaced by non-standin', | |||||
) | ) | ||||
return mresult | return mresult | ||||
@eh.wrapfunction(mergestatemod, b'recordupdates') | @eh.wrapfunction(mergestatemod, b'recordupdates') | ||||
def mergerecordupdates(orig, repo, actions, branchmerge, getfiledata): | def mergerecordupdates(orig, repo, actions, branchmerge, getfiledata): | ||||
if MERGE_ACTION_LARGEFILE_MARK_REMOVED in actions: | if MERGE_ACTION_LARGEFILE_MARK_REMOVED in actions: |
eh = exthelper.exthelper() | eh = exthelper.exthelper() | ||||
# these will all be replaced by largefiles.uisetup | # these will all be replaced by largefiles.uisetup | ||||
ssholdcallstream = None | ssholdcallstream = None | ||||
httpoldcallstream = None | httpoldcallstream = None | ||||
def putlfile(repo, proto, sha): | def putlfile(repo, proto, sha): | ||||
'''Server command for putting a largefile into a repository's local store | """Server command for putting a largefile into a repository's local store | ||||
and into the user cache.''' | and into the user cache.""" | ||||
with proto.mayberedirectstdio() as output: | with proto.mayberedirectstdio() as output: | ||||
path = lfutil.storepath(repo, sha) | path = lfutil.storepath(repo, sha) | ||||
util.makedirs(os.path.dirname(path)) | util.makedirs(os.path.dirname(path)) | ||||
tmpfp = util.atomictempfile(path, createmode=repo.store.createmode) | tmpfp = util.atomictempfile(path, createmode=repo.store.createmode) | ||||
try: | try: | ||||
for p in proto.getpayload(): | for p in proto.getpayload(): | ||||
tmpfp.write(p) | tmpfp.write(p) | ||||
) | ) | ||||
finally: | finally: | ||||
tmpfp.discard() | tmpfp.discard() | ||||
return wireprototypes.pushres(0, output.getvalue() if output else b'') | return wireprototypes.pushres(0, output.getvalue() if output else b'') | ||||
def getlfile(repo, proto, sha): | def getlfile(repo, proto, sha): | ||||
'''Server command for retrieving a largefile from the repository-local | """Server command for retrieving a largefile from the repository-local | ||||
cache or user cache.''' | cache or user cache.""" | ||||
filename = lfutil.findfile(repo, sha) | filename = lfutil.findfile(repo, sha) | ||||
if not filename: | if not filename: | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'requested largefile %s not present in cache') % sha | _(b'requested largefile %s not present in cache') % sha | ||||
) | ) | ||||
f = open(filename, b'rb') | f = open(filename, b'rb') | ||||
length = os.fstat(f.fileno())[6] | length = os.fstat(f.fileno())[6] | ||||
# Since we can't set an HTTP content-length header here, and | # Since we can't set an HTTP content-length header here, and | ||||
# Mercurial core provides no way to give the length of a streamres | # Mercurial core provides no way to give the length of a streamres | ||||
# (and reading the entire file into RAM would be ill-advised), we | # (and reading the entire file into RAM would be ill-advised), we | ||||
# just send the length on the first line of the response, like the | # just send the length on the first line of the response, like the | ||||
# ssh proto does for string responses. | # ssh proto does for string responses. | ||||
def generator(): | def generator(): | ||||
yield b'%d\n' % length | yield b'%d\n' % length | ||||
for chunk in util.filechunkiter(f): | for chunk in util.filechunkiter(f): | ||||
yield chunk | yield chunk | ||||
return wireprototypes.streamreslegacy(gen=generator()) | return wireprototypes.streamreslegacy(gen=generator()) | ||||
def statlfile(repo, proto, sha): | def statlfile(repo, proto, sha): | ||||
'''Server command for checking if a largefile is present - returns '2\n' if | """Server command for checking if a largefile is present - returns '2\n' if | ||||
the largefile is missing, '0\n' if it seems to be in good condition. | the largefile is missing, '0\n' if it seems to be in good condition. | ||||
The value 1 is reserved for mismatched checksum, but that is too expensive | The value 1 is reserved for mismatched checksum, but that is too expensive | ||||
to be verified on every stat and must be caught be running 'hg verify' | to be verified on every stat and must be caught be running 'hg verify' | ||||
server side.''' | server side.""" | ||||
filename = lfutil.findfile(repo, sha) | filename = lfutil.findfile(repo, sha) | ||||
if not filename: | if not filename: | ||||
return wireprototypes.bytesresponse(b'2\n') | return wireprototypes.bytesresponse(b'2\n') | ||||
return wireprototypes.bytesresponse(b'0\n') | return wireprototypes.bytesresponse(b'0\n') | ||||
def wirereposetup(ui, repo): | def wirereposetup(ui, repo): | ||||
class lfileswirerepository(repo.__class__): | class lfileswirerepository(repo.__class__): | ||||
def _capabilities(orig, repo, proto): | def _capabilities(orig, repo, proto): | ||||
'''announce largefile server capability''' | '''announce largefile server capability''' | ||||
caps = orig(repo, proto) | caps = orig(repo, proto) | ||||
caps.append(b'largefiles=serve') | caps.append(b'largefiles=serve') | ||||
return caps | return caps | ||||
def heads(orig, repo, proto): | def heads(orig, repo, proto): | ||||
'''Wrap server command - largefile capable clients will know to call | """Wrap server command - largefile capable clients will know to call | ||||
lheads instead''' | lheads instead""" | ||||
if lfutil.islfilesrepo(repo): | if lfutil.islfilesrepo(repo): | ||||
return wireprototypes.ooberror(LARGEFILES_REQUIRED_MSG) | return wireprototypes.ooberror(LARGEFILES_REQUIRED_MSG) | ||||
return orig(repo, proto) | return orig(repo, proto) | ||||
def sshrepocallstream(self, cmd, **args): | def sshrepocallstream(self, cmd, **args): | ||||
if cmd == b'heads' and self.capable(b'largefiles'): | if cmd == b'heads' and self.capable(b'largefiles'): |
'''Put file with the given hash in the remote store.''' | '''Put file with the given hash in the remote store.''' | ||||
raise NotImplementedError(b'abstract method') | raise NotImplementedError(b'abstract method') | ||||
def _get(self, hash): | def _get(self, hash): | ||||
'''Get a iterator for content with the given hash.''' | '''Get a iterator for content with the given hash.''' | ||||
raise NotImplementedError(b'abstract method') | raise NotImplementedError(b'abstract method') | ||||
def _stat(self, hashes): | def _stat(self, hashes): | ||||
'''Get information about availability of files specified by | """Get information about availability of files specified by | ||||
hashes in the remote store. Return dictionary mapping hashes | hashes in the remote store. Return dictionary mapping hashes | ||||
to return code where 0 means that file is available, other | to return code where 0 means that file is available, other | ||||
values if not.''' | values if not.""" | ||||
raise NotImplementedError(b'abstract method') | raise NotImplementedError(b'abstract method') |
editor=editor, | editor=editor, | ||||
extra=extra, | extra=extra, | ||||
) | ) | ||||
return result | return result | ||||
# TODO: _subdirlfs should be moved into "lfutil.py", because | # TODO: _subdirlfs should be moved into "lfutil.py", because | ||||
# it is referred only from "lfutil.updatestandinsbymatch" | # it is referred only from "lfutil.updatestandinsbymatch" | ||||
def _subdirlfs(self, files, lfiles): | def _subdirlfs(self, files, lfiles): | ||||
''' | """ | ||||
Adjust matched file list | Adjust matched file list | ||||
If we pass a directory to commit whose only committable files | If we pass a directory to commit whose only committable files | ||||
are largefiles, the core commit code aborts before finding | are largefiles, the core commit code aborts before finding | ||||
the largefiles. | the largefiles. | ||||
So we do the following: | So we do the following: | ||||
For directories that only have largefiles as matches, | For directories that only have largefiles as matches, | ||||
we explicitly add the largefiles to the match list and remove | we explicitly add the largefiles to the match list and remove | ||||
the directory. | the directory. | ||||
In other cases, we leave the match list unmodified. | In other cases, we leave the match list unmodified. | ||||
''' | """ | ||||
actualfiles = [] | actualfiles = [] | ||||
dirs = [] | dirs = [] | ||||
regulars = [] | regulars = [] | ||||
for f in files: | for f in files: | ||||
if lfutil.isstandin(f + b'/'): | if lfutil.isstandin(f + b'/'): | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'file "%s" is a largefile standin') % f, | _(b'file "%s" is a largefile standin') % f, |
def _put(self, hash, fd): | def _put(self, hash, fd): | ||||
return self.remote.putlfile(hash, fd) | return self.remote.putlfile(hash, fd) | ||||
def _get(self, hash): | def _get(self, hash): | ||||
return self.remote.getlfile(hash) | return self.remote.getlfile(hash) | ||||
def _stat(self, hashes): | def _stat(self, hashes): | ||||
'''For each hash, return 0 if it is available, other values if not. | """For each hash, return 0 if it is available, other values if not. | ||||
It is usually 2 if the largefile is missing, but might be 1 the server | It is usually 2 if the largefile is missing, but might be 1 the server | ||||
has a corrupted copy.''' | has a corrupted copy.""" | ||||
with self.remote.commandexecutor() as e: | with self.remote.commandexecutor() as e: | ||||
fs = [] | fs = [] | ||||
for hash in hashes: | for hash in hashes: | ||||
fs.append((hash, e.callcommand(b'statlfile', {b'sha': hash,}))) | fs.append( | ||||
( | |||||
hash, | |||||
e.callcommand( | |||||
b'statlfile', | |||||
{ | |||||
b'sha': hash, | |||||
}, | |||||
), | |||||
) | |||||
) | |||||
return {hash: f.result() for hash, f in fs} | return {hash: f.result() for hash, f in fs} |
configtable = eh.configtable | configtable = eh.configtable | ||||
extsetup = eh.finalextsetup | extsetup = eh.finalextsetup | ||||
uisetup = eh.finaluisetup | uisetup = eh.finaluisetup | ||||
filesetpredicate = eh.filesetpredicate | filesetpredicate = eh.filesetpredicate | ||||
reposetup = eh.finalreposetup | reposetup = eh.finalreposetup | ||||
templatekeyword = eh.templatekeyword | templatekeyword = eh.templatekeyword | ||||
eh.configitem( | eh.configitem( | ||||
b'experimental', b'lfs.serve', default=True, | b'experimental', | ||||
b'lfs.serve', | |||||
default=True, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'experimental', b'lfs.user-agent', default=None, | b'experimental', | ||||
b'lfs.user-agent', | |||||
default=None, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'experimental', b'lfs.disableusercache', default=False, | b'experimental', | ||||
b'lfs.disableusercache', | |||||
default=False, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'experimental', b'lfs.worker-enable', default=True, | b'experimental', | ||||
b'lfs.worker-enable', | |||||
default=True, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'lfs', b'url', default=None, | b'lfs', | ||||
b'url', | |||||
default=None, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'lfs', b'usercache', default=None, | b'lfs', | ||||
b'usercache', | |||||
default=None, | |||||
) | ) | ||||
# Deprecated | # Deprecated | ||||
eh.configitem( | eh.configitem( | ||||
b'lfs', b'threshold', default=None, | b'lfs', | ||||
b'threshold', | |||||
default=None, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'lfs', b'track', default=b'none()', | b'lfs', | ||||
b'track', | |||||
default=b'none()', | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'lfs', b'retry', default=5, | b'lfs', | ||||
b'retry', | |||||
default=5, | |||||
) | ) | ||||
lfsprocessor = ( | lfsprocessor = ( | ||||
wrapper.readfromstore, | wrapper.readfromstore, | ||||
wrapper.writetostore, | wrapper.writetostore, | ||||
wrapper.bypasscheckhash, | wrapper.bypasscheckhash, | ||||
) | ) | ||||
def walk(self, path=None, onerror=None): | def walk(self, path=None, onerror=None): | ||||
return (b'', [], []) | return (b'', [], []) | ||||
def write(self, oid, data): | def write(self, oid, data): | ||||
pass | pass | ||||
class lfsuploadfile(httpconnectionmod.httpsendfile): | class lfsuploadfile(httpconnectionmod.httpsendfile): | ||||
"""a file-like object that supports keepalive. | """a file-like object that supports keepalive.""" | ||||
""" | |||||
def __init__(self, ui, filename): | def __init__(self, ui, filename): | ||||
super(lfsuploadfile, self).__init__(ui, filename, b'rb') | super(lfsuploadfile, self).__init__(ui, filename, b'rb') | ||||
self.read = self._data.read | self.read = self._data.read | ||||
def _makeprogress(self): | def _makeprogress(self): | ||||
return None # progress is handled by the worker client | return None # progress is handled by the worker client | ||||
def has(self, oid): | def has(self, oid): | ||||
"""Returns True if the local blobstore contains the requested blob, | """Returns True if the local blobstore contains the requested blob, | ||||
False otherwise.""" | False otherwise.""" | ||||
return self.cachevfs.exists(oid) or self.vfs.exists(oid) | return self.cachevfs.exists(oid) or self.vfs.exists(oid) | ||||
def _urlerrorreason(urlerror): | def _urlerrorreason(urlerror): | ||||
'''Create a friendly message for the given URLError to be used in an | """Create a friendly message for the given URLError to be used in an | ||||
LfsRemoteError message. | LfsRemoteError message. | ||||
''' | """ | ||||
inst = urlerror | inst = urlerror | ||||
if isinstance(urlerror.reason, Exception): | if isinstance(urlerror.reason, Exception): | ||||
inst = urlerror.reason | inst = urlerror.reason | ||||
if util.safehasattr(inst, b'reason'): | if util.safehasattr(inst, b'reason'): | ||||
try: # usually it is in the form (errno, strerror) | try: # usually it is in the form (errno, strerror) | ||||
reason = inst.reason.args[1] | reason = inst.reason.args[1] | ||||
See https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md | See https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md | ||||
""" | """ | ||||
objects = [ | objects = [ | ||||
{'oid': pycompat.strurl(p.oid()), 'size': p.size()} | {'oid': pycompat.strurl(p.oid()), 'size': p.size()} | ||||
for p in pointers | for p in pointers | ||||
] | ] | ||||
requestdata = pycompat.bytesurl( | requestdata = pycompat.bytesurl( | ||||
json.dumps( | json.dumps( | ||||
{'objects': objects, 'operation': pycompat.strurl(action),} | { | ||||
'objects': objects, | |||||
'operation': pycompat.strurl(action), | |||||
} | |||||
) | ) | ||||
) | ) | ||||
url = b'%s/objects/batch' % self.baseurl | url = b'%s/objects/batch' % self.baseurl | ||||
batchreq = util.urlreq.request(pycompat.strurl(url), data=requestdata) | batchreq = util.urlreq.request(pycompat.strurl(url), data=requestdata) | ||||
batchreq.add_header('Accept', 'application/vnd.git-lfs+json') | batchreq.add_header('Accept', 'application/vnd.git-lfs+json') | ||||
batchreq.add_header('Content-Type', 'application/vnd.git-lfs+json') | batchreq.add_header('Content-Type', 'application/vnd.git-lfs+json') | ||||
try: | try: | ||||
with contextlib.closing(self.urlopener.open(batchreq)) as rsp: | with contextlib.closing(self.urlopener.open(batchreq)) as rsp: |
if not util.safehasattr(repo.svfs, b'lfsremoteblobstore'): | if not util.safehasattr(repo.svfs, b'lfsremoteblobstore'): | ||||
return False | return False | ||||
# if remotestore is a null store, downloads will lead to nothing | # if remotestore is a null store, downloads will lead to nothing | ||||
return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote) | return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote) | ||||
def uploadblobsfromrevs(repo, revs): | def uploadblobsfromrevs(repo, revs): | ||||
'''upload lfs blobs introduced by revs | """upload lfs blobs introduced by revs | ||||
Note: also used by other extensions e. g. infinitepush. avoid renaming. | Note: also used by other extensions e. g. infinitepush. avoid renaming. | ||||
''' | """ | ||||
if _canskipupload(repo): | if _canskipupload(repo): | ||||
return | return | ||||
pointers = extractpointers(repo, revs) | pointers = extractpointers(repo, revs) | ||||
uploadblobs(repo, pointers) | uploadblobs(repo, pointers) | ||||
def prepush(pushop): | def prepush(pushop): | ||||
"""Prepush hook. | """Prepush hook. |
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'mq', b'git', default=b'auto', | b'mq', | ||||
b'git', | |||||
default=b'auto', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'mq', b'keepchanges', default=False, | b'mq', | ||||
b'keepchanges', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'mq', b'plain', default=False, | b'mq', | ||||
b'plain', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'mq', b'secret', default=False, | b'mq', | ||||
b'secret', | |||||
default=False, | |||||
) | ) | ||||
# force load strip extension formerly included in mq and import some utility | # force load strip extension formerly included in mq and import some utility | ||||
try: | try: | ||||
extensions.find(b'strip') | extensions.find(b'strip') | ||||
except KeyError: | except KeyError: | ||||
# note: load is lazy so we could avoid the try-except, | # note: load is lazy so we could avoid the try-except, | ||||
# but I (marmoute) prefer this explicit code. | # but I (marmoute) prefer this explicit code. | ||||
class dummyui(object): | class dummyui(object): | ||||
def debug(self, msg): | def debug(self, msg): | ||||
pass | pass | ||||
def log(self, event, msgfmt, *msgargs, **opts): | def log(self, event, msgfmt, *msgargs, **opts): | ||||
pass | pass | ||||
extensions.load(dummyui(), b'strip', b'') | extensions.load(dummyui(), b'strip', b'') | ||||
strip = strip.strip | strip = strip.strip | ||||
def checksubstate(repo, baserev=None): | def checksubstate(repo, baserev=None): | ||||
'''return list of subrepos at a different revision than substate. | """return list of subrepos at a different revision than substate. | ||||
Abort if any subrepos have uncommitted changes.''' | Abort if any subrepos have uncommitted changes.""" | ||||
inclsubs = [] | inclsubs = [] | ||||
wctx = repo[None] | wctx = repo[None] | ||||
if baserev: | if baserev: | ||||
bctx = repo[baserev] | bctx = repo[baserev] | ||||
else: | else: | ||||
bctx = wctx.p1() | bctx = wctx.p1() | ||||
for s in sorted(wctx.substate): | for s in sorted(wctx.substate): | ||||
wctx.sub(s).bailifchanged(True) | wctx.sub(s).bailifchanged(True) | ||||
s = b'\n'.join(self.comments).rstrip() | s = b'\n'.join(self.comments).rstrip() | ||||
if not s: | if not s: | ||||
return b'' | return b'' | ||||
return s + b'\n\n' | return s + b'\n\n' | ||||
__str__ = encoding.strmethod(__bytes__) | __str__ = encoding.strmethod(__bytes__) | ||||
def _delmsg(self): | def _delmsg(self): | ||||
'''Remove existing message, keeping the rest of the comments fields. | """Remove existing message, keeping the rest of the comments fields. | ||||
If comments contains 'subject: ', message will prepend | If comments contains 'subject: ', message will prepend | ||||
the field and a blank line.''' | the field and a blank line.""" | ||||
if self.message: | if self.message: | ||||
subj = b'subject: ' + self.message[0].lower() | subj = b'subject: ' + self.message[0].lower() | ||||
for i in pycompat.xrange(len(self.comments)): | for i in pycompat.xrange(len(self.comments)): | ||||
if subj == self.comments[i].lower(): | if subj == self.comments[i].lower(): | ||||
del self.comments[i] | del self.comments[i] | ||||
self.message = self.message[2:] | self.message = self.message[2:] | ||||
break | break | ||||
ci = 0 | ci = 0 | ||||
self.applied.append(statusentry(head, patch)) | self.applied.append(statusentry(head, patch)) | ||||
self.applieddirty = True | self.applieddirty = True | ||||
if err: | if err: | ||||
return (err, head) | return (err, head) | ||||
self.savedirty() | self.savedirty() | ||||
return (0, head) | return (0, head) | ||||
def patch(self, repo, patchfile): | def patch(self, repo, patchfile): | ||||
'''Apply patchfile to the working directory. | """Apply patchfile to the working directory. | ||||
patchfile: name of patch file''' | patchfile: name of patch file""" | ||||
files = set() | files = set() | ||||
try: | try: | ||||
fuzz = patchmod.patch( | fuzz = patchmod.patch( | ||||
self.ui, repo, patchfile, strip=1, files=files, eolmode=None | self.ui, repo, patchfile, strip=1, files=files, eolmode=None | ||||
) | ) | ||||
return (True, list(files), fuzz) | return (True, list(files), fuzz) | ||||
except Exception as inst: | except Exception as inst: | ||||
self.ui.note(stringutil.forcebytestr(inst) + b'\n') | self.ui.note(stringutil.forcebytestr(inst) + b'\n') | ||||
return name | return name | ||||
def checkkeepchanges(self, keepchanges, force): | def checkkeepchanges(self, keepchanges, force): | ||||
if force and keepchanges: | if force and keepchanges: | ||||
raise error.Abort(_(b'cannot use both --force and --keep-changes')) | raise error.Abort(_(b'cannot use both --force and --keep-changes')) | ||||
def new(self, repo, patchfn, *pats, **opts): | def new(self, repo, patchfn, *pats, **opts): | ||||
"""options: | """options: | ||||
msg: a string or a no-argument function returning a string | msg: a string or a no-argument function returning a string | ||||
""" | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
msg = opts.get(b'msg') | msg = opts.get(b'msg') | ||||
edit = opts.get(b'edit') | edit = opts.get(b'edit') | ||||
editform = opts.get(b'editform', b'mq.qnew') | editform = opts.get(b'editform', b'mq.qnew') | ||||
user = opts.get(b'user') | user = opts.get(b'user') | ||||
date = opts.get(b'date') | date = opts.get(b'date') | ||||
if date: | if date: | ||||
tobackup=tobackup, | tobackup=tobackup, | ||||
keepchanges=keepchanges, | keepchanges=keepchanges, | ||||
) | ) | ||||
except AbortNoCleanup: | except AbortNoCleanup: | ||||
raise | raise | ||||
except: # re-raises | except: # re-raises | ||||
self.ui.warn(_(b'cleaning up working directory...\n')) | self.ui.warn(_(b'cleaning up working directory...\n')) | ||||
cmdutil.revert( | cmdutil.revert( | ||||
self.ui, repo, repo[b'.'], no_backup=True, | self.ui, | ||||
repo, | |||||
repo[b'.'], | |||||
no_backup=True, | |||||
) | ) | ||||
# only remove unknown files that we know we touched or | # only remove unknown files that we know we touched or | ||||
# created while patching | # created while patching | ||||
for f in all_files: | for f in all_files: | ||||
if f not in repo.dirstate: | if f not in repo.dirstate: | ||||
repo.wvfs.unlinkpath(f, ignoremissing=True) | repo.wvfs.unlinkpath(f, ignoremissing=True) | ||||
self.ui.warn(_(b'done\n')) | self.ui.warn(_(b'done\n')) | ||||
raise | raise | ||||
), | ), | ||||
] | ] | ||||
+ cmdutil.remoteopts, | + cmdutil.remoteopts, | ||||
_(b'hg qclone [OPTION]... SOURCE [DEST]'), | _(b'hg qclone [OPTION]... SOURCE [DEST]'), | ||||
helpcategory=command.CATEGORY_REPO_CREATION, | helpcategory=command.CATEGORY_REPO_CREATION, | ||||
norepo=True, | norepo=True, | ||||
) | ) | ||||
def clone(ui, source, dest=None, **opts): | def clone(ui, source, dest=None, **opts): | ||||
'''clone main and patch repository at same time | """clone main and patch repository at same time | ||||
If source is local, destination will have no patches applied. If | If source is local, destination will have no patches applied. If | ||||
source is remote, this command can not check if patches are | source is remote, this command can not check if patches are | ||||
applied in source, so cannot guarantee that patches are not | applied in source, so cannot guarantee that patches are not | ||||
applied in destination. If you clone remote repository, be sure | applied in destination. If you clone remote repository, be sure | ||||
before that it has no patches applied. | before that it has no patches applied. | ||||
Source patch repository is looked for in <src>/.hg/patches by | Source patch repository is looked for in <src>/.hg/patches by | ||||
default. Use -p <url> to change. | default. Use -p <url> to change. | ||||
The patch directory must be a nested Mercurial repository, as | The patch directory must be a nested Mercurial repository, as | ||||
would be created by :hg:`init --mq`. | would be created by :hg:`init --mq`. | ||||
Return 0 on success. | Return 0 on success. | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
def patchdir(repo): | def patchdir(repo): | ||||
"""compute a patch repo url from a repo object""" | """compute a patch repo url from a repo object""" | ||||
url = repo.url() | url = repo.url() | ||||
if url.endswith(b'/'): | if url.endswith(b'/'): | ||||
url = url[:-1] | url = url[:-1] | ||||
return url + b'/.hg/patches' | return url + b'/.hg/patches' | ||||
r = q.qrepo() | r = q.qrepo() | ||||
if not r: | if not r: | ||||
raise error.Abort(b'no queue repository') | raise error.Abort(b'no queue repository') | ||||
commands.commit(r.ui, r, *pats, **opts) | commands.commit(r.ui, r, *pats, **opts) | ||||
@command( | @command( | ||||
b"qseries", | b"qseries", | ||||
[(b'm', b'missing', None, _(b'print patches not in series')),] + seriesopts, | [ | ||||
(b'm', b'missing', None, _(b'print patches not in series')), | |||||
] | |||||
+ seriesopts, | |||||
_(b'hg qseries [-ms]'), | _(b'hg qseries [-ms]'), | ||||
helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | ||||
) | ) | ||||
def series(ui, repo, **opts): | def series(ui, repo, **opts): | ||||
"""print the entire series file | """print the entire series file | ||||
Returns 0 on success.""" | Returns 0 on success.""" | ||||
repo.mq.qseries( | repo.mq.qseries( | ||||
), | ), | ||||
(b'f', b'force', None, _(b'overwrite any local changes')), | (b'f', b'force', None, _(b'overwrite any local changes')), | ||||
(b'', b'no-backup', None, _(b'do not save backup copies of files')), | (b'', b'no-backup', None, _(b'do not save backup copies of files')), | ||||
], | ], | ||||
_(b'hg qgoto [OPTION]... PATCH'), | _(b'hg qgoto [OPTION]... PATCH'), | ||||
helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | ||||
) | ) | ||||
def goto(ui, repo, patch, **opts): | def goto(ui, repo, patch, **opts): | ||||
'''push or pop patches until named patch is at top of stack | """push or pop patches until named patch is at top of stack | ||||
Returns 0 on success.''' | Returns 0 on success.""" | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
opts = fixkeepchangesopts(ui, opts) | opts = fixkeepchangesopts(ui, opts) | ||||
q = repo.mq | q = repo.mq | ||||
patch = q.lookup(patch) | patch = q.lookup(patch) | ||||
nobackup = opts.get(b'no_backup') | nobackup = opts.get(b'no_backup') | ||||
keepchanges = opts.get(b'keep_changes') | keepchanges = opts.get(b'keep_changes') | ||||
if q.isapplied(patch): | if q.isapplied(patch): | ||||
ret = q.pop( | ret = q.pop( | ||||
[ | [ | ||||
(b'l', b'list', None, _(b'list all patches and guards')), | (b'l', b'list', None, _(b'list all patches and guards')), | ||||
(b'n', b'none', None, _(b'drop all guards')), | (b'n', b'none', None, _(b'drop all guards')), | ||||
], | ], | ||||
_(b'hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'), | _(b'hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'), | ||||
helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | ||||
) | ) | ||||
def guard(ui, repo, *args, **opts): | def guard(ui, repo, *args, **opts): | ||||
'''set or print guards for a patch | """set or print guards for a patch | ||||
Guards control whether a patch can be pushed. A patch with no | Guards control whether a patch can be pushed. A patch with no | ||||
guards is always pushed. A patch with a positive guard ("+foo") is | guards is always pushed. A patch with a positive guard ("+foo") is | ||||
pushed only if the :hg:`qselect` command has activated it. A patch with | pushed only if the :hg:`qselect` command has activated it. A patch with | ||||
a negative guard ("-foo") is never pushed if the :hg:`qselect` command | a negative guard ("-foo") is never pushed if the :hg:`qselect` command | ||||
has activated it. | has activated it. | ||||
With no arguments, print the currently active guards. | With no arguments, print the currently active guards. | ||||
With arguments, set guards for the named patch. | With arguments, set guards for the named patch. | ||||
.. note:: | .. note:: | ||||
Specifying negative guards now requires '--'. | Specifying negative guards now requires '--'. | ||||
To set guards on another patch:: | To set guards on another patch:: | ||||
hg qguard other.patch -- +2.6.17 -stable | hg qguard other.patch -- +2.6.17 -stable | ||||
Returns 0 on success. | Returns 0 on success. | ||||
''' | """ | ||||
def status(idx): | def status(idx): | ||||
guards = q.seriesguards[idx] or [b'unguarded'] | guards = q.seriesguards[idx] or [b'unguarded'] | ||||
if q.series[idx] in applied: | if q.series[idx] in applied: | ||||
state = b'applied' | state = b'applied' | ||||
elif q.pushable(idx)[0]: | elif q.pushable(idx)[0]: | ||||
state = b'unapplied' | state = b'unapplied' | ||||
else: | else: | ||||
(b's', b'series', None, _(b'list all guards in series file')), | (b's', b'series', None, _(b'list all guards in series file')), | ||||
(b'', b'pop', None, _(b'pop to before first guarded applied patch')), | (b'', b'pop', None, _(b'pop to before first guarded applied patch')), | ||||
(b'', b'reapply', None, _(b'pop, then reapply patches')), | (b'', b'reapply', None, _(b'pop, then reapply patches')), | ||||
], | ], | ||||
_(b'hg qselect [OPTION]... [GUARD]...'), | _(b'hg qselect [OPTION]... [GUARD]...'), | ||||
helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | ||||
) | ) | ||||
def select(ui, repo, *args, **opts): | def select(ui, repo, *args, **opts): | ||||
'''set or print guarded patches to push | """set or print guarded patches to push | ||||
Use the :hg:`qguard` command to set or print guards on patch, then use | Use the :hg:`qguard` command to set or print guards on patch, then use | ||||
qselect to tell mq which guards to use. A patch will be pushed if | qselect to tell mq which guards to use. A patch will be pushed if | ||||
it has no guards or any positive guards match the currently | it has no guards or any positive guards match the currently | ||||
selected guard, but will not be pushed if any negative guards | selected guard, but will not be pushed if any negative guards | ||||
match the current guard. For example:: | match the current guard. For example:: | ||||
qguard foo.patch -- -stable (negative guard) | qguard foo.patch -- -stable (negative guard) | ||||
guarded patches by default. Use --pop to pop back to the last | guarded patches by default. Use --pop to pop back to the last | ||||
applied patch that is not guarded. Use --reapply (which implies | applied patch that is not guarded. Use --reapply (which implies | ||||
--pop) to push back to the current patch afterwards, but skip | --pop) to push back to the current patch afterwards, but skip | ||||
guarded patches. | guarded patches. | ||||
Use -s/--series to print a list of all guards in the series file | Use -s/--series to print a list of all guards in the series file | ||||
(no other arguments needed). Use -v for more information. | (no other arguments needed). Use -v for more information. | ||||
Returns 0 on success.''' | Returns 0 on success.""" | ||||
q = repo.mq | q = repo.mq | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
guards = q.active() | guards = q.active() | ||||
pushable = lambda i: q.pushable(q.applied[i].name)[0] | pushable = lambda i: q.pushable(q.applied[i].name)[0] | ||||
if args or opts.get(b'none'): | if args or opts.get(b'none'): | ||||
old_unapplied = q.unapplied(repo) | old_unapplied = q.unapplied(repo) | ||||
old_guarded = [ | old_guarded = [ | ||||
(b'', b'rename', False, _(b'rename active queue')), | (b'', b'rename', False, _(b'rename active queue')), | ||||
(b'', b'delete', False, _(b'delete reference to queue')), | (b'', b'delete', False, _(b'delete reference to queue')), | ||||
(b'', b'purge', False, _(b'delete queue, and remove patch dir')), | (b'', b'purge', False, _(b'delete queue, and remove patch dir')), | ||||
], | ], | ||||
_(b'[OPTION] [QUEUE]'), | _(b'[OPTION] [QUEUE]'), | ||||
helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | ||||
) | ) | ||||
def qqueue(ui, repo, name=None, **opts): | def qqueue(ui, repo, name=None, **opts): | ||||
'''manage multiple patch queues | """manage multiple patch queues | ||||
Supports switching between different patch queues, as well as creating | Supports switching between different patch queues, as well as creating | ||||
new patch queues and deleting existing ones. | new patch queues and deleting existing ones. | ||||
Omitting a queue name or specifying -l/--list will show you the registered | Omitting a queue name or specifying -l/--list will show you the registered | ||||
queues - by default the "normal" patches queue is registered. The currently | queues - by default the "normal" patches queue is registered. The currently | ||||
active queue will be marked with "(active)". Specifying --active will print | active queue will be marked with "(active)". Specifying --active will print | ||||
only the name of the active queue. | only the name of the active queue. | ||||
To create a new queue, use -c/--create. The queue is automatically made | To create a new queue, use -c/--create. The queue is automatically made | ||||
active, except in the case where there are applied patches from the | active, except in the case where there are applied patches from the | ||||
currently active queue in the repository. Then the queue will only be | currently active queue in the repository. Then the queue will only be | ||||
created and switching will fail. | created and switching will fail. | ||||
To delete an existing queue, use --delete. You cannot delete the currently | To delete an existing queue, use --delete. You cannot delete the currently | ||||
active queue. | active queue. | ||||
Returns 0 on success. | Returns 0 on success. | ||||
''' | """ | ||||
q = repo.mq | q = repo.mq | ||||
_defaultqueue = b'patches' | _defaultqueue = b'patches' | ||||
_allqueues = b'patches.queues' | _allqueues = b'patches.queues' | ||||
_activequeue = b'patches.queue' | _activequeue = b'patches.queue' | ||||
def _getcurrent(): | def _getcurrent(): | ||||
cur = os.path.basename(q.path) | cur = os.path.basename(q.path) | ||||
if cur.startswith(b'patches-'): | if cur.startswith(b'patches-'): | ||||
ui.note(_(b"mq: (empty queue)\n")) | ui.note(_(b"mq: (empty queue)\n")) | ||||
revsetpredicate = registrar.revsetpredicate() | revsetpredicate = registrar.revsetpredicate() | ||||
@revsetpredicate(b'mq()') | @revsetpredicate(b'mq()') | ||||
def revsetmq(repo, subset, x): | def revsetmq(repo, subset, x): | ||||
"""Changesets managed by MQ. | """Changesets managed by MQ.""" | ||||
""" | |||||
revsetlang.getargs(x, 0, 0, _(b"mq takes no arguments")) | revsetlang.getargs(x, 0, 0, _(b"mq takes no arguments")) | ||||
applied = {repo[r.node].rev() for r in repo.mq.applied} | applied = {repo[r.node].rev() for r in repo.mq.applied} | ||||
return smartset.baseset([r for r in subset if r in applied]) | return smartset.baseset([r for r in subset if r in applied]) | ||||
# tell hggettext to extract docstrings from these functions: | # tell hggettext to extract docstrings from these functions: | ||||
i18nfunctions = [revsetmq] | i18nfunctions = [revsetmq] | ||||
version, | version, | ||||
common, | common, | ||||
heads, | heads, | ||||
kwargs.get('depth', None), | kwargs.get('depth', None), | ||||
) | ) | ||||
def generateellipsesbundle2( | def generateellipsesbundle2( | ||||
bundler, repo, include, exclude, version, common, heads, depth, | bundler, | ||||
repo, | |||||
include, | |||||
exclude, | |||||
version, | |||||
common, | |||||
heads, | |||||
depth, | |||||
): | ): | ||||
match = narrowspec.match(repo.root, include=include, exclude=exclude) | match = narrowspec.match(repo.root, include=include, exclude=exclude) | ||||
if depth is not None: | if depth is not None: | ||||
depth = int(depth) | depth = int(depth) | ||||
if depth < 1: | if depth < 1: | ||||
raise error.Abort(_(b'depth must be positive, got %d') % depth) | raise error.Abort(_(b'depth must be positive, got %d') % depth) | ||||
heads = set(heads or repo.heads()) | heads = set(heads or repo.heads()) | ||||
part = bundler.newpart(b'changegroup', data=cgdata) | part = bundler.newpart(b'changegroup', data=cgdata) | ||||
part.addparam(b'version', version) | part.addparam(b'version', version) | ||||
if scmutil.istreemanifest(repo): | if scmutil.istreemanifest(repo): | ||||
part.addparam(b'treemanifest', b'1') | part.addparam(b'treemanifest', b'1') | ||||
def generate_ellipses_bundle2_for_widening( | def generate_ellipses_bundle2_for_widening( | ||||
bundler, repo, oldmatch, newmatch, version, common, known, | bundler, | ||||
repo, | |||||
oldmatch, | |||||
newmatch, | |||||
version, | |||||
common, | |||||
known, | |||||
): | ): | ||||
common = set(common or [nullid]) | common = set(common or [nullid]) | ||||
# Steps: | # Steps: | ||||
# 1. Send kill for "$known & ::common" | # 1. Send kill for "$known & ::common" | ||||
# | # | ||||
# 2. Send changegroup for ::common | # 2. Send changegroup for ::common | ||||
# | # | ||||
# 3. Proceed. | # 3. Proceed. |
newmatch, | newmatch, | ||||
common, | common, | ||||
known, | known, | ||||
cgversion, | cgversion, | ||||
ellipses, | ellipses, | ||||
) | ) | ||||
else: | else: | ||||
narrowbundle2.generate_ellipses_bundle2_for_widening( | narrowbundle2.generate_ellipses_bundle2_for_widening( | ||||
bundler, repo, oldmatch, newmatch, cgversion, common, known, | bundler, | ||||
repo, | |||||
oldmatch, | |||||
newmatch, | |||||
cgversion, | |||||
common, | |||||
known, | |||||
) | ) | ||||
except error.Abort as exc: | except error.Abort as exc: | ||||
bundler = bundle2.bundle20(repo.ui) | bundler = bundle2.bundle20(repo.ui) | ||||
manargs = [(b'message', exc.message)] | manargs = [(b'message', exc.message)] | ||||
advargs = [] | advargs = [] | ||||
if exc.hint is not None: | if exc.hint is not None: | ||||
advargs.append((b'hint', exc.hint)) | advargs.append((b'hint', exc.hint)) | ||||
bundler.addpart(bundle2.bundlepart(b'error:abort', manargs, advargs)) | bundler.addpart(bundle2.bundlepart(b'error:abort', manargs, advargs)) |
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'notify', b'changegroup', default=None, | b'notify', | ||||
b'changegroup', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'config', default=None, | b'notify', | ||||
b'config', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'diffstat', default=True, | b'notify', | ||||
b'diffstat', | |||||
default=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'domain', default=None, | b'notify', | ||||
b'domain', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'messageidseed', default=None, | b'notify', | ||||
b'messageidseed', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'fromauthor', default=None, | b'notify', | ||||
b'fromauthor', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'incoming', default=None, | b'notify', | ||||
b'incoming', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'maxdiff', default=300, | b'notify', | ||||
b'maxdiff', | |||||
default=300, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'maxdiffstat', default=-1, | b'notify', | ||||
b'maxdiffstat', | |||||
default=-1, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'maxsubject', default=67, | b'notify', | ||||
b'maxsubject', | |||||
default=67, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'mbox', default=None, | b'notify', | ||||
b'mbox', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'merge', default=True, | b'notify', | ||||
b'merge', | |||||
default=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'outgoing', default=None, | b'notify', | ||||
b'outgoing', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'reply-to-predecessor', default=False, | b'notify', | ||||
b'reply-to-predecessor', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'sources', default=b'serve', | b'notify', | ||||
b'sources', | |||||
default=b'serve', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'showfunc', default=None, | b'notify', | ||||
b'showfunc', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'strip', default=0, | b'notify', | ||||
b'strip', | |||||
default=0, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'style', default=None, | b'notify', | ||||
b'style', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'template', default=None, | b'notify', | ||||
b'template', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'notify', b'test', default=True, | b'notify', | ||||
b'test', | |||||
default=True, | |||||
) | ) | ||||
# template for single changeset can include email headers. | # template for single changeset can include email headers. | ||||
single_template = b''' | single_template = b''' | ||||
Subject: changeset in {webroot}: {desc|firstline|strip} | Subject: changeset in {webroot}: {desc|firstline|strip} | ||||
From: {author} | From: {author} | ||||
changeset {node|short} in {root} | changeset {node|short} in {root} | ||||
difflines = difflines[:maxdiff] | difflines = difflines[:maxdiff] | ||||
elif difflines: | elif difflines: | ||||
self.ui.write(_(b'\ndiffs (%d lines):\n\n') % len(difflines)) | self.ui.write(_(b'\ndiffs (%d lines):\n\n') % len(difflines)) | ||||
self.ui.write(b"\n".join(difflines)) | self.ui.write(b"\n".join(difflines)) | ||||
def hook(ui, repo, hooktype, node=None, source=None, **kwargs): | def hook(ui, repo, hooktype, node=None, source=None, **kwargs): | ||||
'''send email notifications to interested subscribers. | """send email notifications to interested subscribers. | ||||
if used as changegroup hook, send one email for all changesets in | if used as changegroup hook, send one email for all changesets in | ||||
changegroup. else send one email per changeset.''' | changegroup. else send one email per changeset.""" | ||||
n = notifier(ui, repo, hooktype) | n = notifier(ui, repo, hooktype) | ||||
ctx = repo.unfiltered()[node] | ctx = repo.unfiltered()[node] | ||||
if not n.subs: | if not n.subs: | ||||
ui.debug(b'notify: no subscribers to repository %s\n' % n.root) | ui.debug(b'notify: no subscribers to repository %s\n' % n.root) | ||||
return | return | ||||
if n.skipsource(source): | if n.skipsource(source): |
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'pager', b'attend', default=lambda: attended, | b'pager', | ||||
b'attend', | |||||
default=lambda: attended, | |||||
) | ) | ||||
def uisetup(ui): | def uisetup(ui): | ||||
def pagecmd(orig, ui, options, cmd, cmdfunc): | def pagecmd(orig, ui, options, cmd, cmdfunc): | ||||
auto = options[b'pager'] == b'auto' | auto = options[b'pager'] == b'auto' | ||||
if auto and not ui.pageractive: | if auto and not ui.pageractive: | ||||
usepager = False | usepager = False |
cmdtable = {} | cmdtable = {} | ||||
command = registrar.command(cmdtable) | command = registrar.command(cmdtable) | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'patchbomb', b'bundletype', default=None, | b'patchbomb', | ||||
b'bundletype', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'patchbomb', b'bcc', default=None, | b'patchbomb', | ||||
b'bcc', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'patchbomb', b'cc', default=None, | b'patchbomb', | ||||
b'cc', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'patchbomb', b'confirm', default=False, | b'patchbomb', | ||||
b'confirm', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'patchbomb', b'flagtemplate', default=None, | b'patchbomb', | ||||
b'flagtemplate', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'patchbomb', b'from', default=None, | b'patchbomb', | ||||
b'from', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'patchbomb', b'intro', default=b'auto', | b'patchbomb', | ||||
b'intro', | |||||
default=b'auto', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'patchbomb', b'publicurl', default=None, | b'patchbomb', | ||||
b'publicurl', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'patchbomb', b'reply-to', default=None, | b'patchbomb', | ||||
b'reply-to', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'patchbomb', b'to', default=None, | b'patchbomb', | ||||
b'to', | |||||
default=None, | |||||
) | ) | ||||
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||||
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||||
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
def _addpullheader(seq, ctx): | def _addpullheader(seq, ctx): | ||||
"""Add a header pointing to a public URL where the changeset is available | """Add a header pointing to a public URL where the changeset is available""" | ||||
""" | |||||
repo = ctx.repo() | repo = ctx.repo() | ||||
# experimental config: patchbomb.publicurl | # experimental config: patchbomb.publicurl | ||||
# waiting for some logic that check that the changeset are available on the | # waiting for some logic that check that the changeset are available on the | ||||
# destination before patchbombing anything. | # destination before patchbombing anything. | ||||
publicurl = repo.ui.config(b'patchbomb', b'publicurl') | publicurl = repo.ui.config(b'patchbomb', b'publicurl') | ||||
if publicurl: | if publicurl: | ||||
return b'Available At %s\n# hg pull %s -r %s' % ( | return b'Available At %s\n# hg pull %s -r %s' % ( | ||||
publicurl, | publicurl, | ||||
), | ), | ||||
] | ] | ||||
+ emailopts | + emailopts | ||||
+ cmdutil.remoteopts, | + cmdutil.remoteopts, | ||||
_(b'hg email [OPTION]... [DEST]...'), | _(b'hg email [OPTION]... [DEST]...'), | ||||
helpcategory=command.CATEGORY_IMPORT_EXPORT, | helpcategory=command.CATEGORY_IMPORT_EXPORT, | ||||
) | ) | ||||
def email(ui, repo, *revs, **opts): | def email(ui, repo, *revs, **opts): | ||||
'''send changesets by email | """send changesets by email | ||||
By default, diffs are sent in the format generated by | By default, diffs are sent in the format generated by | ||||
:hg:`export`, one per message. The series starts with a "[PATCH 0 | :hg:`export`, one per message. The series starts with a "[PATCH 0 | ||||
of N]" introduction, which describes the series as a whole. | of N]" introduction, which describes the series as a whole. | ||||
Each patch email has a Subject line of "[PATCH M of N] ...", using | Each patch email has a Subject line of "[PATCH M of N] ...", using | ||||
the first line of the changeset description as the subject text. | the first line of the changeset description as the subject text. | ||||
The message contains two or three parts. First, the changeset | The message contains two or three parts. First, the changeset | ||||
hg email -o -m mbox && # generate an mbox file... | hg email -o -m mbox && # generate an mbox file... | ||||
mutt -R -f mbox # ... and view it with mutt | mutt -R -f mbox # ... and view it with mutt | ||||
hg email -o -m mbox && # generate an mbox file ... | hg email -o -m mbox && # generate an mbox file ... | ||||
formail -s sendmail \\ # ... and use formail to send from the mbox | formail -s sendmail \\ # ... and use formail to send from the mbox | ||||
-bm -t < mbox # ... using sendmail | -bm -t < mbox # ... using sendmail | ||||
Before using this command, you will need to enable email in your | Before using this command, you will need to enable email in your | ||||
hgrc. See the [email] section in hgrc(5) for details. | hgrc. See the [email] section in hgrc(5) for details. | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
_charsets = mail._charsets(ui) | _charsets = mail._charsets(ui) | ||||
bundle = opts.get(b'bundle') | bundle = opts.get(b'bundle') | ||||
date = opts.get(b'date') | date = opts.get(b'date') | ||||
mbox = opts.get(b'mbox') | mbox = opts.get(b'mbox') | ||||
outgoing = opts.get(b'outgoing') | outgoing = opts.get(b'outgoing') |
cmdtable = eh.cmdtable | cmdtable = eh.cmdtable | ||||
command = eh.command | command = eh.command | ||||
configtable = eh.configtable | configtable = eh.configtable | ||||
templatekeyword = eh.templatekeyword | templatekeyword = eh.templatekeyword | ||||
uisetup = eh.finaluisetup | uisetup = eh.finaluisetup | ||||
# developer config: phabricator.batchsize | # developer config: phabricator.batchsize | ||||
eh.configitem( | eh.configitem( | ||||
b'phabricator', b'batchsize', default=12, | b'phabricator', | ||||
b'batchsize', | |||||
default=12, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'phabricator', b'callsign', default=None, | b'phabricator', | ||||
b'callsign', | |||||
default=None, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'phabricator', b'curlcmd', default=None, | b'phabricator', | ||||
b'curlcmd', | |||||
default=None, | |||||
) | ) | ||||
# developer config: phabricator.debug | # developer config: phabricator.debug | ||||
eh.configitem( | eh.configitem( | ||||
b'phabricator', b'debug', default=False, | b'phabricator', | ||||
b'debug', | |||||
default=False, | |||||
) | ) | ||||
# developer config: phabricator.repophid | # developer config: phabricator.repophid | ||||
eh.configitem( | eh.configitem( | ||||
b'phabricator', b'repophid', default=None, | b'phabricator', | ||||
b'repophid', | |||||
default=None, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'phabricator', b'url', default=None, | b'phabricator', | ||||
b'url', | |||||
default=None, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'phabsend', b'confirm', default=False, | b'phabsend', | ||||
b'confirm', | |||||
default=False, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'phabimport', b'secret', default=False, | b'phabimport', | ||||
b'secret', | |||||
default=False, | |||||
) | ) | ||||
eh.configitem( | eh.configitem( | ||||
b'phabimport', b'obsolete', default=False, | b'phabimport', | ||||
b'obsolete', | |||||
default=False, | |||||
) | ) | ||||
colortable = { | colortable = { | ||||
b'phabricator.action.created': b'green', | b'phabricator.action.created': b'green', | ||||
b'phabricator.action.skipped': b'magenta', | b'phabricator.action.skipped': b'magenta', | ||||
b'phabricator.action.updated': b'magenta', | b'phabricator.action.updated': b'magenta', | ||||
b'phabricator.drev': b'bold', | b'phabricator.drev': b'bold', | ||||
b'phabricator.status.abandoned': b'magenta dim', | b'phabricator.status.abandoned': b'magenta dim', | ||||
b' (ADVANCED)' | b' (ADVANCED)' | ||||
), | ), | ||||
), | ), | ||||
] | ] | ||||
@eh.wrapfunction(localrepo, "loadhgrc") | @eh.wrapfunction(localrepo, "loadhgrc") | ||||
def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements, *args, **opts): | def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements, *args, **opts): | ||||
"""Load ``.arcconfig`` content into a ui instance on repository open. | """Load ``.arcconfig`` content into a ui instance on repository open.""" | ||||
""" | |||||
result = False | result = False | ||||
arcconfig = {} | arcconfig = {} | ||||
try: | try: | ||||
# json.loads only accepts bytes from 3.6+ | # json.loads only accepts bytes from 3.6+ | ||||
rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig")) | rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig")) | ||||
# json.loads only returns unicode strings | # json.loads only returns unicode strings | ||||
arcconfig = pycompat.rapply( | arcconfig = pycompat.rapply( | ||||
class DiffFileType(object): | class DiffFileType(object): | ||||
TEXT = 1 | TEXT = 1 | ||||
IMAGE = 2 | IMAGE = 2 | ||||
BINARY = 3 | BINARY = 3 | ||||
@attr.s | @attr.s | ||||
class phabhunk(dict): | class phabhunk(dict): | ||||
"""Represents a Differential hunk, which is owned by a Differential change | """Represents a Differential hunk, which is owned by a Differential change""" | ||||
""" | |||||
oldOffset = attr.ib(default=0) # camelcase-required | oldOffset = attr.ib(default=0) # camelcase-required | ||||
oldLength = attr.ib(default=0) # camelcase-required | oldLength = attr.ib(default=0) # camelcase-required | ||||
newOffset = attr.ib(default=0) # camelcase-required | newOffset = attr.ib(default=0) # camelcase-required | ||||
newLength = attr.ib(default=0) # camelcase-required | newLength = attr.ib(default=0) # camelcase-required | ||||
corpus = attr.ib(default='') | corpus = attr.ib(default='') | ||||
# These get added to the phabchange's equivalents | # These get added to the phabchange's equivalents | ||||
addLines = attr.ib(default=0) # camelcase-required | addLines = attr.ib(default=0) # camelcase-required | ||||
% scmutil.formatchangeid(old) | % scmutil.formatchangeid(old) | ||||
) | ) | ||||
continue | continue | ||||
parents = [ | parents = [ | ||||
mapping.get(old.p1().node(), (old.p1(),))[0], | mapping.get(old.p1().node(), (old.p1(),))[0], | ||||
mapping.get(old.p2().node(), (old.p2(),))[0], | mapping.get(old.p2().node(), (old.p2(),))[0], | ||||
] | ] | ||||
newdesc = rewriteutil.update_hash_refs( | newdesc = rewriteutil.update_hash_refs( | ||||
repo, newdesc, mapping, | repo, | ||||
newdesc, | |||||
mapping, | |||||
) | ) | ||||
new = context.metadataonlyctx( | new = context.metadataonlyctx( | ||||
repo, | repo, | ||||
old, | old, | ||||
parents=parents, | parents=parents, | ||||
text=newdesc, | text=newdesc, | ||||
user=old.user(), | user=old.user(), | ||||
date=old.date(), | date=old.date(), | ||||
def template_review(context, mapping): | def template_review(context, mapping): | ||||
""":phabreview: Object describing the review for this changeset. | """:phabreview: Object describing the review for this changeset. | ||||
Has attributes `url` and `id`. | Has attributes `url` and `id`. | ||||
""" | """ | ||||
ctx = context.resource(mapping, b'ctx') | ctx = context.resource(mapping, b'ctx') | ||||
m = _differentialrevisiondescre.search(ctx.description()) | m = _differentialrevisiondescre.search(ctx.description()) | ||||
if m: | if m: | ||||
return templateutil.hybriddict( | return templateutil.hybriddict( | ||||
{b'url': m.group('url'), b'id': b"D%s" % m.group('id'),} | { | ||||
b'url': m.group('url'), | |||||
b'id': b"D%s" % m.group('id'), | |||||
} | |||||
) | ) | ||||
else: | else: | ||||
tags = ctx.repo().nodetags(ctx.node()) | tags = ctx.repo().nodetags(ctx.node()) | ||||
for t in tags: | for t in tags: | ||||
if _differentialrevisiontagre.match(t): | if _differentialrevisiontagre.match(t): | ||||
url = ctx.repo().ui.config(b'phabricator', b'url') | url = ctx.repo().ui.config(b'phabricator', b'url') | ||||
if not url.endswith(b'/'): | if not url.endswith(b'/'): | ||||
url += b'/' | url += b'/' | ||||
url += t | url += t | ||||
return templateutil.hybriddict({b'url': url, b'id': t,}) | return templateutil.hybriddict( | ||||
{ | |||||
b'url': url, | |||||
b'id': t, | |||||
} | |||||
) | |||||
return None | return None | ||||
@eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'}) | @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'}) | ||||
def template_status(context, mapping): | def template_status(context, mapping): | ||||
""":phabstatus: String. Status of Phabricator differential. | """:phabstatus: String. Status of Phabricator differential.""" | ||||
""" | |||||
ctx = context.resource(mapping, b'ctx') | ctx = context.resource(mapping, b'ctx') | ||||
repo = context.resource(mapping, b'repo') | repo = context.resource(mapping, b'repo') | ||||
ui = context.resource(mapping, b'ui') | ui = context.resource(mapping, b'ui') | ||||
rev = ctx.rev() | rev = ctx.rev() | ||||
try: | try: | ||||
drevid = getdrevmap(repo, [rev])[rev] | drevid = getdrevmap(repo, [rev])[rev] | ||||
except KeyError: | except KeyError: | ||||
return None | return None | ||||
drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]}) | drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]}) | ||||
for drev in drevs: | for drev in drevs: | ||||
if int(drev[b'id']) == drevid: | if int(drev[b'id']) == drevid: | ||||
return templateutil.hybriddict( | return templateutil.hybriddict( | ||||
{b'url': drev[b'uri'], b'status': drev[b'statusName'],} | { | ||||
b'url': drev[b'uri'], | |||||
b'status': drev[b'statusName'], | |||||
} | |||||
) | ) | ||||
return None | return None | ||||
@show.showview(b'phabstatus', csettopic=b'work') | @show.showview(b'phabstatus', csettopic=b'work') | ||||
def phabstatusshowview(ui, repo, displayer): | def phabstatusshowview(ui, repo, displayer): | ||||
"""Phabricator differiential status""" | """Phabricator differiential status""" | ||||
revs = repo.revs('sort(_underway(), topo)') | revs = repo.revs('sort(_underway(), topo)') |
), | ), | ||||
), | ), | ||||
] | ] | ||||
+ cmdutil.walkopts, | + cmdutil.walkopts, | ||||
_(b'hg purge [OPTION]... [DIR]...'), | _(b'hg purge [OPTION]... [DIR]...'), | ||||
helpcategory=command.CATEGORY_WORKING_DIRECTORY, | helpcategory=command.CATEGORY_WORKING_DIRECTORY, | ||||
) | ) | ||||
def purge(ui, repo, *dirs, **opts): | def purge(ui, repo, *dirs, **opts): | ||||
'''removes files not tracked by Mercurial | """removes files not tracked by Mercurial | ||||
Delete files not known to Mercurial. This is useful to test local | Delete files not known to Mercurial. This is useful to test local | ||||
and uncommitted changes in an otherwise-clean source tree. | and uncommitted changes in an otherwise-clean source tree. | ||||
This means that purge will delete the following by default: | This means that purge will delete the following by default: | ||||
- Unknown files: files marked with "?" by :hg:`status` | - Unknown files: files marked with "?" by :hg:`status` | ||||
- Empty directories: in fact Mercurial ignores directories unless | - Empty directories: in fact Mercurial ignores directories unless | ||||
If directories are given on the command line, only files in these | If directories are given on the command line, only files in these | ||||
directories are considered. | directories are considered. | ||||
Be careful with purge, as you could irreversibly delete some files | Be careful with purge, as you could irreversibly delete some files | ||||
you forgot to add to the repository. If you only want to print the | you forgot to add to the repository. If you only want to print the | ||||
list of files that this program would delete, use the --print | list of files that this program would delete, use the --print | ||||
option. | option. | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
cmdutil.check_at_most_one_arg(opts, b'all', b'ignored') | cmdutil.check_at_most_one_arg(opts, b'all', b'ignored') | ||||
act = not opts.get(b'print') | act = not opts.get(b'print') | ||||
eol = b'\n' | eol = b'\n' | ||||
if opts.get(b'print0'): | if opts.get(b'print0'): | ||||
eol = b'\0' | eol = b'\0' | ||||
act = False # --print0 implies --print | act = False # --print0 implies --print |
self.obsoletewithoutsuccessorindestination, | self.obsoletewithoutsuccessorindestination, | ||||
) | ) | ||||
for rev in sortedrevs: | for rev in sortedrevs: | ||||
self._rebasenode(tr, rev, allowdivergence, progress) | self._rebasenode(tr, rev, allowdivergence, progress) | ||||
p.complete() | p.complete() | ||||
ui.note(_(b'rebase merging completed\n')) | ui.note(_(b'rebase merging completed\n')) | ||||
def _concludenode(self, rev, editor, commitmsg=None): | def _concludenode(self, rev, editor, commitmsg=None): | ||||
'''Commit the wd changes with parents p1 and p2. | """Commit the wd changes with parents p1 and p2. | ||||
Reuse commit info from rev but also store useful information in extra. | Reuse commit info from rev but also store useful information in extra. | ||||
Return node of committed revision.''' | Return node of committed revision.""" | ||||
repo = self.repo | repo = self.repo | ||||
ctx = repo[rev] | ctx = repo[rev] | ||||
if commitmsg is None: | if commitmsg is None: | ||||
commitmsg = ctx.description() | commitmsg = ctx.description() | ||||
# Skip replacement if collapsing, as that degenerates to p1 for all | # Skip replacement if collapsing, as that degenerates to p1 for all | ||||
# nodes. | # nodes. | ||||
if not self.collapsef: | if not self.collapsef: | ||||
_(b'starting dry-run rebase; repository will not be changed\n') | _(b'starting dry-run rebase; repository will not be changed\n') | ||||
) | ) | ||||
with repo.wlock(), repo.lock(): | with repo.wlock(), repo.lock(): | ||||
needsabort = True | needsabort = True | ||||
try: | try: | ||||
overrides = {(b'rebase', b'singletransaction'): True} | overrides = {(b'rebase', b'singletransaction'): True} | ||||
with ui.configoverride(overrides, b'rebase'): | with ui.configoverride(overrides, b'rebase'): | ||||
_origrebase( | _origrebase( | ||||
ui, repo, action, opts, rbsrt, | ui, | ||||
repo, | |||||
action, | |||||
opts, | |||||
rbsrt, | |||||
) | ) | ||||
except error.ConflictResolutionRequired: | except error.ConflictResolutionRequired: | ||||
ui.status(_(b'hit a merge conflict\n')) | ui.status(_(b'hit a merge conflict\n')) | ||||
return 1 | return 1 | ||||
except error.Abort: | except error.Abort: | ||||
needsabort = False | needsabort = False | ||||
raise | raise | ||||
else: | else: | ||||
b'unable to collapse on top of %d, there is more ' | b'unable to collapse on top of %d, there is more ' | ||||
b'than one external parent: %s' | b'than one external parent: %s' | ||||
) | ) | ||||
% (max(destancestors), b', '.join(b"%d" % p for p in sorted(parents))) | % (max(destancestors), b', '.join(b"%d" % p for p in sorted(parents))) | ||||
) | ) | ||||
def commitmemorynode(repo, wctx, editor, extra, user, date, commitmsg): | def commitmemorynode(repo, wctx, editor, extra, user, date, commitmsg): | ||||
'''Commit the memory changes with parents p1 and p2. | """Commit the memory changes with parents p1 and p2. | ||||
Return node of committed revision.''' | Return node of committed revision.""" | ||||
# By convention, ``extra['branch']`` (set by extrafn) clobbers | # By convention, ``extra['branch']`` (set by extrafn) clobbers | ||||
# ``branch`` (used when passing ``--keepbranches``). | # ``branch`` (used when passing ``--keepbranches``). | ||||
branch = None | branch = None | ||||
if b'branch' in extra: | if b'branch' in extra: | ||||
branch = extra[b'branch'] | branch = extra[b'branch'] | ||||
# FIXME: We call _compact() because it's required to correctly detect | # FIXME: We call _compact() because it's required to correctly detect | ||||
# changed files. This was added to fix a regression shortly before the 5.5 | # changed files. This was added to fix a regression shortly before the 5.5 | ||||
if memctx.isempty() and not repo.ui.configbool(b'ui', b'allowemptycommit'): | if memctx.isempty() and not repo.ui.configbool(b'ui', b'allowemptycommit'): | ||||
return None | return None | ||||
commitres = repo.commitctx(memctx) | commitres = repo.commitctx(memctx) | ||||
wctx.clean() # Might be reused | wctx.clean() # Might be reused | ||||
return commitres | return commitres | ||||
def commitnode(repo, editor, extra, user, date, commitmsg): | def commitnode(repo, editor, extra, user, date, commitmsg): | ||||
'''Commit the wd changes with parents p1 and p2. | """Commit the wd changes with parents p1 and p2. | ||||
Return node of committed revision.''' | Return node of committed revision.""" | ||||
dsguard = util.nullcontextmanager() | dsguard = util.nullcontextmanager() | ||||
if not repo.ui.configbool(b'rebase', b'singletransaction'): | if not repo.ui.configbool(b'rebase', b'singletransaction'): | ||||
dsguard = dirstateguard.dirstateguard(repo, b'rebase') | dsguard = dirstateguard.dirstateguard(repo, b'rebase') | ||||
with dsguard: | with dsguard: | ||||
# Commit might fail if unresolved files exist | # Commit might fail if unresolved files exist | ||||
newnode = repo.commit( | newnode = repo.commit( | ||||
text=commitmsg, user=user, date=date, extra=extra, editor=editor | text=commitmsg, user=user, date=date, extra=extra, editor=editor | ||||
) | ) | ||||
result.append(r) | result.append(r) | ||||
if not result: | if not result: | ||||
raise error.Abort(_(b'source and destination form a cycle')) | raise error.Abort(_(b'source and destination form a cycle')) | ||||
srcset -= set(result) | srcset -= set(result) | ||||
yield result | yield result | ||||
def buildstate(repo, destmap, collapse): | def buildstate(repo, destmap, collapse): | ||||
'''Define which revisions are going to be rebased and where | """Define which revisions are going to be rebased and where | ||||
repo: repo | repo: repo | ||||
destmap: {srcrev: destrev} | destmap: {srcrev: destrev} | ||||
''' | """ | ||||
rebaseset = destmap.keys() | rebaseset = destmap.keys() | ||||
originalwd = repo[b'.'].rev() | originalwd = repo[b'.'].rev() | ||||
# This check isn't strictly necessary, since mq detects commits over an | # This check isn't strictly necessary, since mq detects commits over an | ||||
# applied patch. But it prevents messing up the working directory when | # applied patch. But it prevents messing up the working directory when | ||||
# a partially completed rebase is blocked by mq. | # a partially completed rebase is blocked by mq. | ||||
if b'qtip' in repo.tags(): | if b'qtip' in repo.tags(): | ||||
mqapplied = {repo[s.node].rev() for s in repo.mq.applied} | mqapplied = {repo[s.node].rev() for s in repo.mq.applied} |
b"record", | b"record", | ||||
# same options as commit + white space diff options | # same options as commit + white space diff options | ||||
[c for c in commands.table[b'commit|ci'][1][:] if c[1] != b"interactive"] | [c for c in commands.table[b'commit|ci'][1][:] if c[1] != b"interactive"] | ||||
+ cmdutil.diffwsopts, | + cmdutil.diffwsopts, | ||||
_(b'hg record [OPTION]... [FILE]...'), | _(b'hg record [OPTION]... [FILE]...'), | ||||
helpcategory=command.CATEGORY_COMMITTING, | helpcategory=command.CATEGORY_COMMITTING, | ||||
) | ) | ||||
def record(ui, repo, *pats, **opts): | def record(ui, repo, *pats, **opts): | ||||
'''interactively select changes to commit | """interactively select changes to commit | ||||
If a list of files is omitted, all changes reported by :hg:`status` | If a list of files is omitted, all changes reported by :hg:`status` | ||||
will be candidates for recording. | will be candidates for recording. | ||||
See :hg:`help dates` for a list of formats valid for -d/--date. | See :hg:`help dates` for a list of formats valid for -d/--date. | ||||
If using the text interface (see :hg:`help config`), | If using the text interface (see :hg:`help config`), | ||||
you will be prompted for whether to record changes to each | you will be prompted for whether to record changes to each | ||||
f - record remaining changes to this file | f - record remaining changes to this file | ||||
d - done, skip remaining changes and files | d - done, skip remaining changes and files | ||||
a - record all changes to all remaining files | a - record all changes to all remaining files | ||||
q - quit, recording no changes | q - quit, recording no changes | ||||
? - display help | ? - display help | ||||
This command is not available when committing a merge.''' | This command is not available when committing a merge.""" | ||||
if not ui.interactive(): | if not ui.interactive(): | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'running non-interactively, use %s instead') % b'commit' | _(b'running non-interactively, use %s instead') % b'commit' | ||||
) | ) | ||||
opts["interactive"] = True | opts["interactive"] = True | ||||
overrides = {(b'experimental', b'crecord'): False} | overrides = {(b'experimental', b'crecord'): False} | ||||
@command( | @command( | ||||
b'qrecord', | b'qrecord', | ||||
[], | [], | ||||
_(b'hg qrecord [OPTION]... PATCH [FILE]...'), | _(b'hg qrecord [OPTION]... PATCH [FILE]...'), | ||||
helpcategory=command.CATEGORY_COMMITTING, | helpcategory=command.CATEGORY_COMMITTING, | ||||
inferrepo=True, | inferrepo=True, | ||||
) | ) | ||||
def qrecord(ui, repo, patch, *pats, **opts): | def qrecord(ui, repo, patch, *pats, **opts): | ||||
'''interactively record a new patch | """interactively record a new patch | ||||
See :hg:`help qnew` & :hg:`help record` for more information and | See :hg:`help qnew` & :hg:`help record` for more information and | ||||
usage. | usage. | ||||
''' | """ | ||||
return _qrecord(b'qnew', ui, repo, patch, *pats, **opts) | return _qrecord(b'qnew', ui, repo, patch, *pats, **opts) | ||||
def _qrecord(cmdsuggest, ui, repo, patch, *pats, **opts): | def _qrecord(cmdsuggest, ui, repo, patch, *pats, **opts): | ||||
try: | try: | ||||
mq = extensions.find(b'mq') | mq = extensions.find(b'mq') | ||||
except KeyError: | except KeyError: | ||||
raise error.Abort(_(b"'mq' extension not loaded")) | raise error.Abort(_(b"'mq' extension not loaded")) |
def getrenamedfn(orig, repo, endrev=None): | def getrenamedfn(orig, repo, endrev=None): | ||||
if not isenabled(repo) or copies.usechangesetcentricalgo(repo): | if not isenabled(repo) or copies.usechangesetcentricalgo(repo): | ||||
return orig(repo, endrev) | return orig(repo, endrev) | ||||
rcache = {} | rcache = {} | ||||
def getrenamed(fn, rev): | def getrenamed(fn, rev): | ||||
'''looks up all renames for a file (up to endrev) the first | """looks up all renames for a file (up to endrev) the first | ||||
time the file is given. It indexes on the changerev and only | time the file is given. It indexes on the changerev and only | ||||
parses the manifest if linkrev != changerev. | parses the manifest if linkrev != changerev. | ||||
Returns rename info for fn at changerev rev.''' | Returns rename info for fn at changerev rev.""" | ||||
if rev in rcache.setdefault(fn, {}): | if rev in rcache.setdefault(fn, {}): | ||||
return rcache[fn][rev] | return rcache[fn][rev] | ||||
try: | try: | ||||
fctx = repo[rev].filectx(fn) | fctx = repo[rev].filectx(fn) | ||||
for ancestor in fctx.ancestors(): | for ancestor in fctx.ancestors(): | ||||
if ancestor.path() == fn: | if ancestor.path() == fn: | ||||
renamed = ancestor.renamed() | renamed = ancestor.renamed() | ||||
for actx in fctx.ancestors(): | for actx in fctx.ancestors(): | ||||
s.add(actx.linkrev()) | s.add(actx.linkrev()) | ||||
return smartset.baseset([r for r in subset if r in s]) | return smartset.baseset([r for r in subset if r in s]) | ||||
@command(b'gc', [], _(b'hg gc [REPO...]'), norepo=True) | @command(b'gc', [], _(b'hg gc [REPO...]'), norepo=True) | ||||
def gc(ui, *args, **opts): | def gc(ui, *args, **opts): | ||||
'''garbage collect the client and server filelog caches | """garbage collect the client and server filelog caches""" | ||||
''' | |||||
cachepaths = set() | cachepaths = set() | ||||
# get the system client cache | # get the system client cache | ||||
systemcache = shallowutil.getcachepath(ui, allowempty=True) | systemcache = shallowutil.getcachepath(ui, allowempty=True) | ||||
if systemcache: | if systemcache: | ||||
cachepaths.add(systemcache) | cachepaths.add(systemcache) | ||||
# get repo client and server cache | # get repo client and server cache | ||||
for path in ctx.walk(match): | for path in ctx.walk(match): | ||||
if (not sparsematch or sparsematch(path)) and path in mf: | if (not sparsematch or sparsematch(path)) and path in mf: | ||||
allfiles.append((path, hex(mf[path]))) | allfiles.append((path, hex(mf[path]))) | ||||
repo.fileservice.prefetch(allfiles) | repo.fileservice.prefetch(allfiles) | ||||
@command( | @command( | ||||
b'debugremotefilelog', | b'debugremotefilelog', | ||||
[(b'd', b'decompress', None, _(b'decompress the filelog first')),], | [ | ||||
(b'd', b'decompress', None, _(b'decompress the filelog first')), | |||||
], | |||||
_(b'hg debugremotefilelog <path>'), | _(b'hg debugremotefilelog <path>'), | ||||
norepo=True, | norepo=True, | ||||
) | ) | ||||
def debugremotefilelog(ui, path, **opts): | def debugremotefilelog(ui, path, **opts): | ||||
return debugcommands.debugremotefilelog(ui, path, **opts) | return debugcommands.debugremotefilelog(ui, path, **opts) | ||||
@command( | @command( | ||||
b'verifyremotefilelog', | b'verifyremotefilelog', | ||||
[(b'd', b'decompress', None, _(b'decompress the filelogs first')),], | [ | ||||
(b'd', b'decompress', None, _(b'decompress the filelogs first')), | |||||
], | |||||
_(b'hg verifyremotefilelogs <directory>'), | _(b'hg verifyremotefilelogs <directory>'), | ||||
norepo=True, | norepo=True, | ||||
) | ) | ||||
def verifyremotefilelog(ui, path, **opts): | def verifyremotefilelog(ui, path, **opts): | ||||
return debugcommands.verifyremotefilelog(ui, path, **opts) | return debugcommands.verifyremotefilelog(ui, path, **opts) | ||||
@command( | @command( |
# Clean up the repo cache directory. | # Clean up the repo cache directory. | ||||
self._cleanupdirectory(self._getrepocachepath()) | self._cleanupdirectory(self._getrepocachepath()) | ||||
# BELOW THIS ARE NON-STANDARD APIS | # BELOW THIS ARE NON-STANDARD APIS | ||||
def _cleanupdirectory(self, rootdir): | def _cleanupdirectory(self, rootdir): | ||||
"""Removes the empty directories and unnecessary files within the root | """Removes the empty directories and unnecessary files within the root | ||||
directory recursively. Note that this method does not remove the root | directory recursively. Note that this method does not remove the root | ||||
directory itself. """ | directory itself.""" | ||||
oldfiles = set() | oldfiles = set() | ||||
otherfiles = set() | otherfiles = set() | ||||
# osutil.listdir returns stat information which saves some rmdir/listdir | # osutil.listdir returns stat information which saves some rmdir/listdir | ||||
# syscalls. | # syscalls. | ||||
for name, mode in util.osutil.listdir(rootdir): | for name, mode in util.osutil.listdir(rootdir): | ||||
if stat.S_ISDIR(mode): | if stat.S_ISDIR(mode): | ||||
dirpath = os.path.join(rootdir, name) | dirpath = os.path.join(rootdir, name) |
from . import ( | from . import ( | ||||
basestore, | basestore, | ||||
constants, | constants, | ||||
shallowutil, | shallowutil, | ||||
) | ) | ||||
class ChainIndicies(object): | class ChainIndicies(object): | ||||
"""A static class for easy reference to the delta chain indicies. | """A static class for easy reference to the delta chain indicies.""" | ||||
""" | |||||
# The filename of this revision delta | # The filename of this revision delta | ||||
NAME = 0 | NAME = 0 | ||||
# The mercurial file node for this revision delta | # The mercurial file node for this revision delta | ||||
NODE = 1 | NODE = 1 | ||||
# The filename of the delta base's revision. This is useful when delta | # The filename of the delta base's revision. This is useful when delta | ||||
# between different files (like in the case of a move or copy, we can delta | # between different files (like in the case of a move or copy, we can delta | ||||
# against the original file content). | # against the original file content). | ||||
while chain: | while chain: | ||||
delta = chain.pop()[ChainIndicies.DATA] | delta = chain.pop()[ChainIndicies.DATA] | ||||
text = mdiff.patches(text, [delta]) | text = mdiff.patches(text, [delta]) | ||||
return text | return text | ||||
@basestore.baseunionstore.retriable | @basestore.baseunionstore.retriable | ||||
def getdelta(self, name, node): | def getdelta(self, name, node): | ||||
"""Return the single delta entry for the given name/node pair. | """Return the single delta entry for the given name/node pair.""" | ||||
""" | |||||
for store in self.stores: | for store in self.stores: | ||||
try: | try: | ||||
return store.getdelta(name, node) | return store.getdelta(name, node) | ||||
except KeyError: | except KeyError: | ||||
pass | pass | ||||
raise KeyError((name, hex(node))) | raise KeyError((name, hex(node))) | ||||
writerthread.join() | writerthread.join() | ||||
# End the command | # End the command | ||||
pipeo.write(b'\n') | pipeo.write(b'\n') | ||||
pipeo.flush() | pipeo.flush() | ||||
class fileserverclient(object): | class fileserverclient(object): | ||||
"""A client for requesting files from the remote file server. | """A client for requesting files from the remote file server.""" | ||||
""" | |||||
def __init__(self, repo): | def __init__(self, repo): | ||||
ui = repo.ui | ui = repo.ui | ||||
self.repo = repo | self.repo = repo | ||||
self.ui = ui | self.ui = ui | ||||
self.cacheprocess = ui.config(b"remotefilelog", b"cacheprocess") | self.cacheprocess = ui.config(b"remotefilelog", b"cacheprocess") | ||||
if self.cacheprocess: | if self.cacheprocess: | ||||
self.cacheprocess = util.expandpath(self.cacheprocess) | self.cacheprocess = util.expandpath(self.cacheprocess) | ||||
) | ) | ||||
if self.remotecache.connected: | if self.remotecache.connected: | ||||
self.remotecache.close() | self.remotecache.close() | ||||
def prefetch( | def prefetch( | ||||
self, fileids, force=False, fetchdata=True, fetchhistory=False | self, fileids, force=False, fetchdata=True, fetchhistory=False | ||||
): | ): | ||||
"""downloads the given file versions to the cache | """downloads the given file versions to the cache""" | ||||
""" | |||||
repo = self.repo | repo = self.repo | ||||
idstocheck = [] | idstocheck = [] | ||||
for file, id in fileids: | for file, id in fileids: | ||||
# hack | # hack | ||||
# - we don't use .hgtags | # - we don't use .hgtags | ||||
# - workingctx produces ids with length 42, | # - workingctx produces ids with length 42, | ||||
# which we skip since they aren't in any cache | # which we skip since they aren't in any cache | ||||
if ( | if ( |
linknode = self._adjustlinknode( | linknode = self._adjustlinknode( | ||||
self._path, self._filelog, self._filenode, self._descendantrev | self._path, self._filelog, self._filenode, self._descendantrev | ||||
) | ) | ||||
return self._repo.unfiltered().changelog.rev(linknode) | return self._repo.unfiltered().changelog.rev(linknode) | ||||
else: | else: | ||||
return self.linkrev() | return self.linkrev() | ||||
def filectx(self, fileid, changeid=None): | def filectx(self, fileid, changeid=None): | ||||
'''opens an arbitrary revision of the file without | """opens an arbitrary revision of the file without | ||||
opening a new filelog''' | opening a new filelog""" | ||||
return remotefilectx( | return remotefilectx( | ||||
self._repo, | self._repo, | ||||
self._path, | self._path, | ||||
fileid=fileid, | fileid=fileid, | ||||
filelog=self._filelog, | filelog=self._filelog, | ||||
changeid=changeid, | changeid=changeid, | ||||
) | ) | ||||
constants, | constants, | ||||
shallowutil, | shallowutil, | ||||
) | ) | ||||
_sshv1server = wireprotoserver.sshv1protocolhandler | _sshv1server = wireprotoserver.sshv1protocolhandler | ||||
def setupserver(ui, repo): | def setupserver(ui, repo): | ||||
"""Sets up a normal Mercurial repo so it can serve files to shallow repos. | """Sets up a normal Mercurial repo so it can serve files to shallow repos.""" | ||||
""" | |||||
onetimesetup(ui) | onetimesetup(ui) | ||||
# don't send files to shallow clients during pulls | # don't send files to shallow clients during pulls | ||||
def generatefiles( | def generatefiles( | ||||
orig, self, changedfiles, linknodes, commonrevs, source, *args, **kwargs | orig, self, changedfiles, linknodes, commonrevs, source, *args, **kwargs | ||||
): | ): | ||||
caps = self._bundlecaps or [] | caps = self._bundlecaps or [] | ||||
if constants.BUNDLE2_CAPABLITY in caps: | if constants.BUNDLE2_CAPABLITY in caps: | ||||
changegroup.cgpacker, b'generatefiles', generatefiles | changegroup.cgpacker, b'generatefiles', generatefiles | ||||
) | ) | ||||
onetime = False | onetime = False | ||||
def onetimesetup(ui): | def onetimesetup(ui): | ||||
"""Configures the wireprotocol for both clients and servers. | """Configures the wireprotocol for both clients and servers.""" | ||||
""" | |||||
global onetime | global onetime | ||||
if onetime: | if onetime: | ||||
return | return | ||||
onetime = True | onetime = True | ||||
# support file content requests | # support file content requests | ||||
wireprotov1server.wireprotocommand( | wireprotov1server.wireprotocommand( | ||||
b'x_rfl_getflogheads', b'path', permission=b'pull' | b'x_rfl_getflogheads', b'path', permission=b'pull' | ||||
os.umask(oldumask) | os.umask(oldumask) | ||||
else: | else: | ||||
with open(filecachepath, b"rb") as f: | with open(filecachepath, b"rb") as f: | ||||
text = f.read() | text = f.read() | ||||
return text | return text | ||||
def getflogheads(repo, proto, path): | def getflogheads(repo, proto, path): | ||||
"""A server api for requesting a filelog's heads | """A server api for requesting a filelog's heads""" | ||||
""" | |||||
flog = repo.file(path) | flog = repo.file(path) | ||||
heads = flog.heads() | heads = flog.heads() | ||||
return b'\n'.join((hex(head) for head in heads if head != nullid)) | return b'\n'.join((hex(head) for head in heads if head != nullid)) | ||||
def getfile(repo, proto, file, node): | def getfile(repo, proto, file, node): | ||||
"""A server api for requesting a particular version of a file. Can be used | """A server api for requesting a particular version of a file. Can be used | ||||
in batches to request many files at once. The return protocol is: | in batches to request many files at once. The return protocol is: | ||||
cachepath = os.path.join(repo.path, b"remotefilelogcache") | cachepath = os.path.join(repo.path, b"remotefilelogcache") | ||||
node = bin(node.strip()) | node = bin(node.strip()) | ||||
if node == nullid: | if node == nullid: | ||||
return b'0\0' | return b'0\0' | ||||
return b'0\0' + _loadfileblob(repo, cachepath, file, node) | return b'0\0' + _loadfileblob(repo, cachepath, file, node) | ||||
def getfiles(repo, proto): | def getfiles(repo, proto): | ||||
"""A server api for requesting particular versions of particular files. | """A server api for requesting particular versions of particular files.""" | ||||
""" | |||||
if shallowutil.isenabled(repo): | if shallowutil.isenabled(repo): | ||||
raise error.Abort(_(b'cannot fetch remote files from shallow repo')) | raise error.Abort(_(b'cannot fetch remote files from shallow repo')) | ||||
if not isinstance(proto, _sshv1server): | if not isinstance(proto, _sshv1server): | ||||
raise error.Abort(_(b'cannot fetch remote files over non-ssh protocol')) | raise error.Abort(_(b'cannot fetch remote files over non-ssh protocol')) | ||||
def streamer(): | def streamer(): | ||||
fin = proto._fin | fin = proto._fin | ||||
kwargs = {} | kwargs = {} | ||||
if repo.ui.configbool(b'devel', b'remotefilelog.bg-wait'): | if repo.ui.configbool(b'devel', b'remotefilelog.bg-wait'): | ||||
kwargs['record_wait'] = repo.ui.atexit | kwargs['record_wait'] = repo.ui.atexit | ||||
procutil.runbgcommand(cmd, encoding.environ, ensurestart=False, **kwargs) | procutil.runbgcommand(cmd, encoding.environ, ensurestart=False, **kwargs) | ||||
def fullrepack(repo, options=None): | def fullrepack(repo, options=None): | ||||
"""If ``packsonly`` is True, stores creating only loose objects are skipped. | """If ``packsonly`` is True, stores creating only loose objects are skipped.""" | ||||
""" | |||||
if util.safehasattr(repo, 'shareddatastores'): | if util.safehasattr(repo, 'shareddatastores'): | ||||
datasource = contentstore.unioncontentstore(*repo.shareddatastores) | datasource = contentstore.unioncontentstore(*repo.shareddatastores) | ||||
historysource = metadatastore.unionmetadatastore( | historysource = metadatastore.unionmetadatastore( | ||||
*repo.sharedhistorystores, allowincomplete=True | *repo.sharedhistorystores, allowincomplete=True | ||||
) | ) | ||||
packpath = shallowutil.getcachepackpath( | packpath = shallowutil.getcachepackpath( | ||||
repo, constants.FILEPACK_CATEGORY | repo, constants.FILEPACK_CATEGORY | ||||
return value | return value | ||||
def addcreated(self, value): | def addcreated(self, value): | ||||
self.created.add(value) | self.created.add(value) | ||||
class repackentry(object): | class repackentry(object): | ||||
"""Simple class representing a single revision entry in the repackledger. | """Simple class representing a single revision entry in the repackledger.""" | ||||
""" | |||||
__slots__ = ( | __slots__ = ( | ||||
'filename', | 'filename', | ||||
'node', | 'node', | ||||
'datasource', | 'datasource', | ||||
'historysource', | 'historysource', | ||||
'datarepacked', | 'datarepacked', | ||||
'historyrepacked', | 'historyrepacked', |
raise error.Abort( | raise error.Abort( | ||||
b"no remotefilelog server " | b"no remotefilelog server " | ||||
b"configured - is your .hg/hgrc trusted?" | b"configured - is your .hg/hgrc trusted?" | ||||
) | ) | ||||
return path | return path | ||||
def maybesparsematch(self, *revs, **kwargs): | def maybesparsematch(self, *revs, **kwargs): | ||||
''' | """ | ||||
A wrapper that allows the remotefilelog to invoke sparsematch() if | A wrapper that allows the remotefilelog to invoke sparsematch() if | ||||
this is a sparse repository, or returns None if this is not a | this is a sparse repository, or returns None if this is not a | ||||
sparse repository. | sparse repository. | ||||
''' | """ | ||||
if revs: | if revs: | ||||
ret = sparse.matcher(repo, revs=revs) | ret = sparse.matcher(repo, revs=revs) | ||||
else: | else: | ||||
ret = sparse.matcher(repo) | ret = sparse.matcher(repo) | ||||
if ret.always(): | if ret.always(): | ||||
return None | return None | ||||
return ret | return ret | ||||
self.fileservice.prefetch(files) | self.fileservice.prefetch(files) | ||||
return super(shallowrepository, self).commitctx( | return super(shallowrepository, self).commitctx( | ||||
ctx, error=error, origctx=origctx | ctx, error=error, origctx=origctx | ||||
) | ) | ||||
def backgroundprefetch( | def backgroundprefetch( | ||||
self, revs, base=None, repack=False, pats=None, opts=None | self, revs, base=None, repack=False, pats=None, opts=None | ||||
): | ): | ||||
"""Runs prefetch in background with optional repack | """Runs prefetch in background with optional repack""" | ||||
""" | |||||
cmd = [procutil.hgexecutable(), b'-R', repo.origroot, b'prefetch'] | cmd = [procutil.hgexecutable(), b'-R', repo.origroot, b'prefetch'] | ||||
if repack: | if repack: | ||||
cmd.append(b'--repack') | cmd.append(b'--repack') | ||||
if revs: | if revs: | ||||
cmd += [b'-r', revs] | cmd += [b'-r', revs] | ||||
# We know this command will find a binary, so don't block | # We know this command will find a binary, so don't block | ||||
# on it starting. | # on it starting. | ||||
kwargs = {} | kwargs = {} |
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
templatekeyword = registrar.templatekeyword() | templatekeyword = registrar.templatekeyword() | ||||
revsetpredicate = registrar.revsetpredicate() | revsetpredicate = registrar.revsetpredicate() | ||||
configitem( | configitem( | ||||
b'remotenames', b'bookmarks', default=True, | b'remotenames', | ||||
b'bookmarks', | |||||
default=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'remotenames', b'branches', default=True, | b'remotenames', | ||||
b'branches', | |||||
default=True, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'remotenames', b'hoistedpeer', default=b'default', | b'remotenames', | ||||
b'hoistedpeer', | |||||
default=b'default', | |||||
) | ) | ||||
class lazyremotenamedict(mutablemapping): | class lazyremotenamedict(mutablemapping): | ||||
""" | """ | ||||
Read-only dict-like Class to lazily resolve remotename entries | Read-only dict-like Class to lazily resolve remotename entries | ||||
We are doing that because remotenames startup was slow. | We are doing that because remotenames startup was slow. |
) | ) | ||||
hg.schemes[scheme] = ShortRepository(url, scheme, t) | hg.schemes[scheme] = ShortRepository(url, scheme, t) | ||||
extensions.wrapfunction(util, b'hasdriveletter', hasdriveletter) | extensions.wrapfunction(util, b'hasdriveletter', hasdriveletter) | ||||
@command(b'debugexpandscheme', norepo=True) | @command(b'debugexpandscheme', norepo=True) | ||||
def expandscheme(ui, url, **opts): | def expandscheme(ui, url, **opts): | ||||
"""given a repo path, provide the scheme-expanded path | """given a repo path, provide the scheme-expanded path""" | ||||
""" | |||||
repo = hg._peerlookup(url) | repo = hg._peerlookup(url) | ||||
if isinstance(repo, ShortRepository): | if isinstance(repo, ShortRepository): | ||||
url = repo.resolve(url) | url = repo.resolve(url) | ||||
ui.write(url + b'\n') | ui.write(url + b'\n') |
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
@command( | @command( | ||||
b'share', | b'share', | ||||
[ | [ | ||||
(b'U', b'noupdate', None, _(b'do not create a working directory')), | (b'U', b'noupdate', None, _(b'do not create a working directory')), | ||||
(b'B', b'bookmarks', None, _(b'also share bookmarks')), | (b'B', b'bookmarks', None, _(b'also share bookmarks')), | ||||
(b'', b'relative', None, _(b'point to source using a relative path'),), | ( | ||||
b'', | |||||
b'relative', | |||||
None, | |||||
_(b'point to source using a relative path'), | |||||
), | |||||
], | ], | ||||
_(b'[-U] [-B] SOURCE [DEST]'), | _(b'[-U] [-B] SOURCE [DEST]'), | ||||
helpcategory=command.CATEGORY_REPO_CREATION, | helpcategory=command.CATEGORY_REPO_CREATION, | ||||
norepo=True, | norepo=True, | ||||
) | ) | ||||
def share( | def share( | ||||
ui, source, dest=None, noupdate=False, bookmarks=False, relative=False | ui, source, dest=None, noupdate=False, bookmarks=False, relative=False | ||||
): | ): |
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'transplant', b'filter', default=None, | b'transplant', | ||||
b'filter', | |||||
default=None, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'transplant', b'log', default=None, | b'transplant', | ||||
b'log', | |||||
default=None, | |||||
) | ) | ||||
class transplantentry(object): | class transplantentry(object): | ||||
def __init__(self, lnode, rnode): | def __init__(self, lnode, rnode): | ||||
self.lnode = lnode | self.lnode = lnode | ||||
self.rnode = rnode | self.rnode = rnode | ||||
editform = cmdutil.mergeeditform(repo[None], b'transplant') | editform = cmdutil.mergeeditform(repo[None], b'transplant') | ||||
return cmdutil.getcommiteditor( | return cmdutil.getcommiteditor( | ||||
editform=editform, **pycompat.strkwargs(opts) | editform=editform, **pycompat.strkwargs(opts) | ||||
) | ) | ||||
self.getcommiteditor = getcommiteditor | self.getcommiteditor = getcommiteditor | ||||
def applied(self, repo, node, parent): | def applied(self, repo, node, parent): | ||||
'''returns True if a node is already an ancestor of parent | """returns True if a node is already an ancestor of parent | ||||
or is parent or has already been transplanted''' | or is parent or has already been transplanted""" | ||||
if hasnode(repo, parent): | if hasnode(repo, parent): | ||||
parentrev = repo.changelog.rev(parent) | parentrev = repo.changelog.rev(parent) | ||||
if hasnode(repo, node): | if hasnode(repo, node): | ||||
rev = repo.changelog.rev(node) | rev = repo.changelog.rev(node) | ||||
reachable = repo.changelog.ancestors( | reachable = repo.changelog.ancestors( | ||||
[parentrev], rev, inclusive=True | [parentrev], rev, inclusive=True | ||||
) | ) | ||||
if rev in reachable: | if rev in reachable: | ||||
], | ], | ||||
_( | _( | ||||
b'hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] ' | b'hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] ' | ||||
b'[-m REV] [REV]...' | b'[-m REV] [REV]...' | ||||
), | ), | ||||
helpcategory=command.CATEGORY_CHANGE_MANAGEMENT, | helpcategory=command.CATEGORY_CHANGE_MANAGEMENT, | ||||
) | ) | ||||
def transplant(ui, repo, *revs, **opts): | def transplant(ui, repo, *revs, **opts): | ||||
'''transplant changesets from another branch | """transplant changesets from another branch | ||||
Selected changesets will be applied on top of the current working | Selected changesets will be applied on top of the current working | ||||
directory with the log of the original changeset. The changesets | directory with the log of the original changeset. The changesets | ||||
are copied and will thus appear twice in the history with different | are copied and will thus appear twice in the history with different | ||||
identities. | identities. | ||||
Consider using the graft command if everything is inside the same | Consider using the graft command if everything is inside the same | ||||
repository - it will use merges and will usually give a better result. | repository - it will use merges and will usually give a better result. | ||||
proper parent changeset by calling :hg:`transplant --parent`. | proper parent changeset by calling :hg:`transplant --parent`. | ||||
If no merges or revisions are provided, :hg:`transplant` will | If no merges or revisions are provided, :hg:`transplant` will | ||||
start an interactive changeset browser. | start an interactive changeset browser. | ||||
If a changeset application fails, you can fix the merge by hand | If a changeset application fails, you can fix the merge by hand | ||||
and then resume where you left off by calling :hg:`transplant | and then resume where you left off by calling :hg:`transplant | ||||
--continue/-c`. | --continue/-c`. | ||||
''' | """ | ||||
with repo.wlock(): | with repo.wlock(): | ||||
return _dotransplant(ui, repo, *revs, **opts) | return _dotransplant(ui, repo, *revs, **opts) | ||||
def _dotransplant(ui, repo, *revs, **opts): | def _dotransplant(ui, repo, *revs, **opts): | ||||
def incwalk(repo, csets, match=util.always): | def incwalk(repo, csets, match=util.always): | ||||
for node in csets: | for node in csets: | ||||
if match(node): | if match(node): | ||||
yield node | yield node | ||||
def transplantwalk(repo, dest, heads, match=util.always): | def transplantwalk(repo, dest, heads, match=util.always): | ||||
'''Yield all nodes that are ancestors of a head but not ancestors | """Yield all nodes that are ancestors of a head but not ancestors | ||||
of dest. | of dest. | ||||
If no heads are specified, the heads of repo will be used.''' | If no heads are specified, the heads of repo will be used.""" | ||||
if not heads: | if not heads: | ||||
heads = repo.heads() | heads = repo.heads() | ||||
ancestors = [] | ancestors = [] | ||||
ctx = repo[dest] | ctx = repo[dest] | ||||
for head in heads: | for head in heads: | ||||
ancestors.append(ctx.ancestor(repo[head]).node()) | ancestors.append(ctx.ancestor(repo[head]).node()) | ||||
for node in repo.changelog.nodesbetween(ancestors, heads)[0]: | for node in repo.changelog.nodesbetween(ancestors, heads)[0]: | ||||
if match(node): | if match(node): | ||||
return tp.resume(repo, repo, {}) | return tp.resume(repo, repo, {}) | ||||
revsetpredicate = registrar.revsetpredicate() | revsetpredicate = registrar.revsetpredicate() | ||||
@revsetpredicate(b'transplanted([set])') | @revsetpredicate(b'transplanted([set])') | ||||
def revsettransplanted(repo, subset, x): | def revsettransplanted(repo, subset, x): | ||||
"""Transplanted changesets in set, or all transplanted changesets. | """Transplanted changesets in set, or all transplanted changesets.""" | ||||
""" | |||||
if x: | if x: | ||||
s = revset.getset(repo, subset, x) | s = revset.getset(repo, subset, x) | ||||
else: | else: | ||||
s = subset | s = subset | ||||
return smartset.baseset( | return smartset.baseset( | ||||
[r for r in s if repo[r].extra().get(b'transplant_source')] | [r for r in s if repo[r].extra().get(b'transplant_source')] | ||||
) | ) | ||||
cmdtable = {} | cmdtable = {} | ||||
command = registrar.command(cmdtable) | command = registrar.command(cmdtable) | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'experimental', b'uncommitondirtywdir', default=False, | b'experimental', | ||||
b'uncommitondirtywdir', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'experimental', b'uncommit.keep', default=False, | b'experimental', | ||||
b'uncommit.keep', | |||||
default=False, | |||||
) | ) | ||||
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||||
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||||
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
# Encoding.encoding may be updated by --encoding option. | # Encoding.encoding may be updated by --encoding option. | ||||
# Use a lambda do delay the resolution. | # Use a lambda do delay the resolution. | ||||
configitem( | configitem( | ||||
b'win32mbcs', b'encoding', default=lambda: encoding.encoding, | b'win32mbcs', | ||||
b'encoding', | |||||
default=lambda: encoding.encoding, | |||||
) | ) | ||||
_encoding = None # see extsetup | _encoding = None # see extsetup | ||||
def decode(arg): | def decode(arg): | ||||
if isinstance(arg, bytes): | if isinstance(arg, bytes): | ||||
uarg = arg.decode(_encoding) | uarg = arg.decode(_encoding) |
# be specifying the version(s) of Mercurial they are tested with, or | # be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | # leave the attribute unspecified. | ||||
testedwith = b'ships-with-hg-core' | testedwith = b'ships-with-hg-core' | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'win32text', b'warn', default=True, | b'win32text', | ||||
b'warn', | |||||
default=True, | |||||
) | ) | ||||
# regexp for single LF without CR preceding. | # regexp for single LF without CR preceding. | ||||
re_single_lf = re.compile(b'(^|[^\r])\n', re.MULTILINE) | re_single_lf = re.compile(b'(^|[^\r])\n', re.MULTILINE) | ||||
newlinestr = {b'\r\n': b'CRLF', b'\r': b'CR'} | newlinestr = {b'\r\n': b'CRLF', b'\r': b'CR'} | ||||
filterstr = {b'\r\n': b'clever', b'\r': b'mac'} | filterstr = {b'\r\n': b'clever', b'\r': b'mac'} | ||||
checkers.append((func, level)) | checkers.append((func, level)) | ||||
func.match = match | func.match = match | ||||
return func | return func | ||||
return decorator | return decorator | ||||
def match(checker, pe): | def match(checker, pe): | ||||
"""Examine whether POEntry "pe" is target of specified checker or not | """Examine whether POEntry "pe" is target of specified checker or not""" | ||||
""" | |||||
if not checker.match(pe.msgid): | if not checker.match(pe.msgid): | ||||
return | return | ||||
# examine suppression by translator comment | # examine suppression by translator comment | ||||
nochecker = 'no-%s-check' % checker.__name__ | nochecker = 'no-%s-check' % checker.__name__ | ||||
for tc in pe.tcomment.split(): | for tc in pe.tcomment.split(): | ||||
if nochecker == tc: | if nochecker == tc: | ||||
return | return | ||||
return True | return True |
gca = commonancestorsheads(pfunc, *orignodes) | gca = commonancestorsheads(pfunc, *orignodes) | ||||
if len(gca) <= 1: | if len(gca) <= 1: | ||||
return gca | return gca | ||||
return deepest(gca) | return deepest(gca) | ||||
class incrementalmissingancestors(object): | class incrementalmissingancestors(object): | ||||
'''persistent state used to calculate missing ancestors incrementally | """persistent state used to calculate missing ancestors incrementally | ||||
Although similar in spirit to lazyancestors below, this is a separate class | Although similar in spirit to lazyancestors below, this is a separate class | ||||
because trying to support contains and missingancestors operations with the | because trying to support contains and missingancestors operations with the | ||||
same internal data structures adds needless complexity.''' | same internal data structures adds needless complexity.""" | ||||
def __init__(self, pfunc, bases): | def __init__(self, pfunc, bases): | ||||
self.bases = set(bases) | self.bases = set(bases) | ||||
if not self.bases: | if not self.bases: | ||||
self.bases.add(nullrev) | self.bases.add(nullrev) | ||||
self.pfunc = pfunc | self.pfunc = pfunc | ||||
def hasbases(self): | def hasbases(self): | ||||
continue | continue | ||||
revs.discard(curr) | revs.discard(curr) | ||||
bases.update(pfunc(curr)) | bases.update(pfunc(curr)) | ||||
if len(revs) == keepcount: | if len(revs) == keepcount: | ||||
# no more potential revs to discard | # no more potential revs to discard | ||||
break | break | ||||
def missingancestors(self, revs): | def missingancestors(self, revs): | ||||
'''return all the ancestors of revs that are not ancestors of self.bases | """return all the ancestors of revs that are not ancestors of self.bases | ||||
This may include elements from revs. | This may include elements from revs. | ||||
Equivalent to the revset (::revs - ::self.bases). Revs are returned in | Equivalent to the revset (::revs - ::self.bases). Revs are returned in | ||||
revision number order, which is a topological order.''' | revision number order, which is a topological order.""" | ||||
revsvisit = set(revs) | revsvisit = set(revs) | ||||
basesvisit = self.bases | basesvisit = self.bases | ||||
pfunc = self.pfunc | pfunc = self.pfunc | ||||
bothvisit = revsvisit.intersection(basesvisit) | bothvisit = revsvisit.intersection(basesvisit) | ||||
revsvisit.difference_update(bothvisit) | revsvisit.difference_update(bothvisit) | ||||
if not revsvisit: | if not revsvisit: | ||||
return [] | return [] | ||||
stringio = util.stringio | stringio = util.stringio | ||||
# from unzip source code: | # from unzip source code: | ||||
_UNX_IFREG = 0x8000 | _UNX_IFREG = 0x8000 | ||||
_UNX_IFLNK = 0xA000 | _UNX_IFLNK = 0xA000 | ||||
def tidyprefix(dest, kind, prefix): | def tidyprefix(dest, kind, prefix): | ||||
'''choose prefix to use for names in archive. make sure prefix is | """choose prefix to use for names in archive. make sure prefix is | ||||
safe for consumers.''' | safe for consumers.""" | ||||
if prefix: | if prefix: | ||||
prefix = util.normpath(prefix) | prefix = util.normpath(prefix) | ||||
else: | else: | ||||
if not isinstance(dest, bytes): | if not isinstance(dest, bytes): | ||||
raise ValueError(b'dest must be string if no prefix') | raise ValueError(b'dest must be string if no prefix') | ||||
prefix = os.path.basename(dest) | prefix = os.path.basename(dest) | ||||
lower = prefix.lower() | lower = prefix.lower() | ||||
dirty = b'+' | dirty = b'+' | ||||
fm.data(dirty=dirty) | fm.data(dirty=dirty) | ||||
fm.end() | fm.end() | ||||
return out.getvalue() | return out.getvalue() | ||||
class tarit(object): | class tarit(object): | ||||
'''write archive to tar file or stream. can write uncompressed, | """write archive to tar file or stream. can write uncompressed, | ||||
or compress with gzip or bzip2.''' | or compress with gzip or bzip2.""" | ||||
if pycompat.ispy3: | if pycompat.ispy3: | ||||
GzipFileWithTime = gzip.GzipFile # camelcase-required | GzipFileWithTime = gzip.GzipFile # camelcase-required | ||||
else: | else: | ||||
class GzipFileWithTime(gzip.GzipFile): | class GzipFileWithTime(gzip.GzipFile): | ||||
def __init__(self, *args, **kw): | def __init__(self, *args, **kw): | ||||
timestamp = None | timestamp = None | ||||
gzfileobj = self.GzipFileWithTime( | gzfileobj = self.GzipFileWithTime( | ||||
name, | name, | ||||
pycompat.sysstr(mode + b'b'), | pycompat.sysstr(mode + b'b'), | ||||
zlib.Z_BEST_COMPRESSION, | zlib.Z_BEST_COMPRESSION, | ||||
fileobj, | fileobj, | ||||
mtime=mtime, | mtime=mtime, | ||||
) | ) | ||||
self.fileobj = gzfileobj | self.fileobj = gzfileobj | ||||
return tarfile.TarFile.taropen( # pytype: disable=attribute-error | return ( | ||||
tarfile.TarFile.taropen( # pytype: disable=attribute-error | |||||
name, pycompat.sysstr(mode), gzfileobj | name, pycompat.sysstr(mode), gzfileobj | ||||
) | ) | ||||
) | |||||
else: | else: | ||||
try: | try: | ||||
return tarfile.open( | return tarfile.open( | ||||
name, pycompat.sysstr(mode + kind), fileobj | name, pycompat.sysstr(mode + kind), fileobj | ||||
) | ) | ||||
except tarfile.CompressionError as e: | except tarfile.CompressionError as e: | ||||
raise error.Abort(pycompat.bytestr(e)) | raise error.Abort(pycompat.bytestr(e)) | ||||
def done(self): | def done(self): | ||||
self.z.close() | self.z.close() | ||||
if self.fileobj: | if self.fileobj: | ||||
self.fileobj.close() | self.fileobj.close() | ||||
class zipit(object): | class zipit(object): | ||||
'''write archive to zip file or stream. can write uncompressed, | """write archive to zip file or stream. can write uncompressed, | ||||
or compressed with deflate.''' | or compressed with deflate.""" | ||||
def __init__(self, dest, mtime, compress=True): | def __init__(self, dest, mtime, compress=True): | ||||
if isinstance(dest, bytes): | if isinstance(dest, bytes): | ||||
dest = pycompat.fsdecode(dest) | dest = pycompat.fsdecode(dest) | ||||
self.z = zipfile.ZipFile( | self.z = zipfile.ZipFile( | ||||
dest, 'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED | dest, 'w', compress and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED | ||||
) | ) | ||||
node, | node, | ||||
kind, | kind, | ||||
decode=True, | decode=True, | ||||
match=None, | match=None, | ||||
prefix=b'', | prefix=b'', | ||||
mtime=None, | mtime=None, | ||||
subrepos=False, | subrepos=False, | ||||
): | ): | ||||
'''create archive of repo as it was at node. | """create archive of repo as it was at node. | ||||
dest can be name of directory, name of archive file, or file | dest can be name of directory, name of archive file, or file | ||||
object to write archive to. | object to write archive to. | ||||
kind is type of archive to create. | kind is type of archive to create. | ||||
decode tells whether to put files through decode filters from | decode tells whether to put files through decode filters from | ||||
hgrc. | hgrc. | ||||
match is a matcher to filter names of files to write to archive. | match is a matcher to filter names of files to write to archive. | ||||
prefix is name of path to put before every archive member. | prefix is name of path to put before every archive member. | ||||
mtime is the modified time, in seconds, or None to use the changeset time. | mtime is the modified time, in seconds, or None to use the changeset time. | ||||
subrepos tells whether to include subrepos. | subrepos tells whether to include subrepos. | ||||
''' | """ | ||||
if kind == b'txz' and not pycompat.ispy3: | if kind == b'txz' and not pycompat.ispy3: | ||||
raise error.Abort(_(b'xz compression is only available in Python 3')) | raise error.Abort(_(b'xz compression is only available in Python 3')) | ||||
if kind == b'files': | if kind == b'files': | ||||
if prefix: | if prefix: | ||||
raise error.Abort(_(b'cannot give prefix when archiving to files')) | raise error.Abort(_(b'cannot give prefix when archiving to files')) | ||||
else: | else: |
else: | else: | ||||
nrefs.remove(mark) | nrefs.remove(mark) | ||||
def names(self, node): | def names(self, node): | ||||
"""Return a sorted list of bookmarks pointing to the specified node""" | """Return a sorted list of bookmarks pointing to the specified node""" | ||||
return self._nodemap.get(node, []) | return self._nodemap.get(node, []) | ||||
def applychanges(self, repo, tr, changes): | def applychanges(self, repo, tr, changes): | ||||
"""Apply a list of changes to bookmarks | """Apply a list of changes to bookmarks""" | ||||
""" | |||||
bmchanges = tr.changes.get(b'bookmarks') | bmchanges = tr.changes.get(b'bookmarks') | ||||
for name, node in changes: | for name, node in changes: | ||||
old = self._refmap.get(name) | old = self._refmap.get(name) | ||||
if node is None: | if node is None: | ||||
self._del(name) | self._del(name) | ||||
else: | else: | ||||
self._set(name, node) | self._set(name, node) | ||||
if bmchanges is not None: | if bmchanges is not None: | ||||
heads = [] | heads = [] | ||||
for mark, n in pycompat.iteritems(repo._bookmarks): | for mark, n in pycompat.iteritems(repo._bookmarks): | ||||
if mark.split(b'@', 1)[0] == name: | if mark.split(b'@', 1)[0] == name: | ||||
heads.append(n) | heads.append(n) | ||||
return heads | return heads | ||||
def calculateupdate(ui, repo): | def calculateupdate(ui, repo): | ||||
'''Return a tuple (activemark, movemarkfrom) indicating the active bookmark | """Return a tuple (activemark, movemarkfrom) indicating the active bookmark | ||||
and where to move the active bookmark from, if needed.''' | and where to move the active bookmark from, if needed.""" | ||||
checkout, movemarkfrom = None, None | checkout, movemarkfrom = None, None | ||||
activemark = repo._activebookmark | activemark = repo._activebookmark | ||||
if isactivewdirparent(repo): | if isactivewdirparent(repo): | ||||
movemarkfrom = repo[b'.'].node() | movemarkfrom = repo[b'.'].node() | ||||
elif activemark: | elif activemark: | ||||
ui.status(_(b"updating to active bookmark %s\n") % activemark) | ui.status(_(b"updating to active bookmark %s\n") % activemark) | ||||
checkout = activemark | checkout = activemark | ||||
return (checkout, movemarkfrom) | return (checkout, movemarkfrom) | ||||
if new not in repo: | if new not in repo: | ||||
return False | return False | ||||
changes = [(key, repo[new].node())] | changes = [(key, repo[new].node())] | ||||
marks.applychanges(repo, tr, changes) | marks.applychanges(repo, tr, changes) | ||||
return True | return True | ||||
def comparebookmarks(repo, srcmarks, dstmarks, targets=None): | def comparebookmarks(repo, srcmarks, dstmarks, targets=None): | ||||
'''Compare bookmarks between srcmarks and dstmarks | """Compare bookmarks between srcmarks and dstmarks | ||||
This returns tuple "(addsrc, adddst, advsrc, advdst, diverge, | This returns tuple "(addsrc, adddst, advsrc, advdst, diverge, | ||||
differ, invalid)", each are list of bookmarks below: | differ, invalid)", each are list of bookmarks below: | ||||
:addsrc: added on src side (removed on dst side, perhaps) | :addsrc: added on src side (removed on dst side, perhaps) | ||||
:adddst: added on dst side (removed on src side, perhaps) | :adddst: added on dst side (removed on src side, perhaps) | ||||
:advsrc: advanced on src side | :advsrc: advanced on src side | ||||
:advdst: advanced on dst side | :advdst: advanced on dst side | ||||
:diverge: diverge | :diverge: diverge | ||||
:differ: changed, but changeset referred on src is unknown on dst | :differ: changed, but changeset referred on src is unknown on dst | ||||
:invalid: unknown on both side | :invalid: unknown on both side | ||||
:same: same on both side | :same: same on both side | ||||
Each elements of lists in result tuple is tuple "(bookmark name, | Each elements of lists in result tuple is tuple "(bookmark name, | ||||
changeset ID on source side, changeset ID on destination | changeset ID on source side, changeset ID on destination | ||||
side)". Each changeset ID is a binary node or None. | side)". Each changeset ID is a binary node or None. | ||||
Changeset IDs of tuples in "addsrc", "adddst", "differ" or | Changeset IDs of tuples in "addsrc", "adddst", "differ" or | ||||
"invalid" list may be unknown for repo. | "invalid" list may be unknown for repo. | ||||
If "targets" is specified, only bookmarks listed in it are | If "targets" is specified, only bookmarks listed in it are | ||||
examined. | examined. | ||||
''' | """ | ||||
if targets: | if targets: | ||||
bset = set(targets) | bset = set(targets) | ||||
else: | else: | ||||
srcmarkset = set(srcmarks) | srcmarkset = set(srcmarks) | ||||
dstmarkset = set(dstmarks) | dstmarkset = set(dstmarks) | ||||
bset = srcmarkset | dstmarkset | bset = srcmarkset | dstmarkset | ||||
else: | else: | ||||
# it is too expensive to examine in detail, in this case | # it is too expensive to examine in detail, in this case | ||||
differ((b, scid, dcid)) | differ((b, scid, dcid)) | ||||
return results | return results | ||||
def _diverge(ui, b, path, localmarks, remotenode): | def _diverge(ui, b, path, localmarks, remotenode): | ||||
'''Return appropriate diverged bookmark for specified ``path`` | """Return appropriate diverged bookmark for specified ``path`` | ||||
This returns None, if it is failed to assign any divergent | This returns None, if it is failed to assign any divergent | ||||
bookmark name. | bookmark name. | ||||
This reuses already existing one with "@number" suffix, if it | This reuses already existing one with "@number" suffix, if it | ||||
refers ``remotenode``. | refers ``remotenode``. | ||||
''' | """ | ||||
if b == b'@': | if b == b'@': | ||||
b = b'' | b = b'' | ||||
# try to use an @pathalias suffix | # try to use an @pathalias suffix | ||||
# if an @pathalias already exists, we overwrite (update) it | # if an @pathalias already exists, we overwrite (update) it | ||||
if path.startswith(b"file:"): | if path.startswith(b"file:"): | ||||
path = util.url(path).path | path = util.url(path).path | ||||
for p, u in ui.configitems(b"paths"): | for p, u in ui.configitems(b"paths"): | ||||
if u.startswith(b"file:"): | if u.startswith(b"file:"): | ||||
key = lambda t: (t[0], t[1] or b'') | key = lambda t: (t[0], t[1] or b'') | ||||
for b, node, writer, msg in sorted(changed, key=key): | for b, node, writer, msg in sorted(changed, key=key): | ||||
changes.append((b, node)) | changes.append((b, node)) | ||||
writer(msg) | writer(msg) | ||||
localmarks.applychanges(repo, tr, changes) | localmarks.applychanges(repo, tr, changes) | ||||
def incoming(ui, repo, peer): | def incoming(ui, repo, peer): | ||||
'''Show bookmarks incoming from other to repo | """Show bookmarks incoming from other to repo""" | ||||
''' | |||||
ui.status(_(b"searching for changed bookmarks\n")) | ui.status(_(b"searching for changed bookmarks\n")) | ||||
with peer.commandexecutor() as e: | with peer.commandexecutor() as e: | ||||
remotemarks = unhexlifybookmarks( | remotemarks = unhexlifybookmarks( | ||||
e.callcommand(b'listkeys', {b'namespace': b'bookmarks',}).result() | e.callcommand( | ||||
b'listkeys', | |||||
{ | |||||
b'namespace': b'bookmarks', | |||||
}, | |||||
).result() | |||||
) | ) | ||||
r = comparebookmarks(repo, remotemarks, repo._bookmarks) | r = comparebookmarks(repo, remotemarks, repo._bookmarks) | ||||
addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r | addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r | ||||
incomings = [] | incomings = [] | ||||
if ui.debugflag: | if ui.debugflag: | ||||
getid = lambda id: id | getid = lambda id: id | ||||
for s in sorted(incomings): | for s in sorted(incomings): | ||||
ui.write(s) | ui.write(s) | ||||
return 0 | return 0 | ||||
def outgoing(ui, repo, other): | def outgoing(ui, repo, other): | ||||
'''Show bookmarks outgoing from repo to other | """Show bookmarks outgoing from repo to other""" | ||||
''' | |||||
ui.status(_(b"searching for changed bookmarks\n")) | ui.status(_(b"searching for changed bookmarks\n")) | ||||
remotemarks = unhexlifybookmarks(other.listkeys(b'bookmarks')) | remotemarks = unhexlifybookmarks(other.listkeys(b'bookmarks')) | ||||
r = comparebookmarks(repo, repo._bookmarks, remotemarks) | r = comparebookmarks(repo, repo._bookmarks, remotemarks) | ||||
addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r | addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r | ||||
outgoings = [] | outgoings = [] | ||||
if ui.debugflag: | if ui.debugflag: | ||||
for s in sorted(outgoings): | for s in sorted(outgoings): | ||||
ui.write(s) | ui.write(s) | ||||
return 0 | return 0 | ||||
def summary(repo, peer): | def summary(repo, peer): | ||||
'''Compare bookmarks between repo and other for "hg summary" output | """Compare bookmarks between repo and other for "hg summary" output | ||||
This returns "(# of incoming, # of outgoing)" tuple. | This returns "(# of incoming, # of outgoing)" tuple. | ||||
''' | """ | ||||
with peer.commandexecutor() as e: | with peer.commandexecutor() as e: | ||||
remotemarks = unhexlifybookmarks( | remotemarks = unhexlifybookmarks( | ||||
e.callcommand(b'listkeys', {b'namespace': b'bookmarks',}).result() | e.callcommand( | ||||
b'listkeys', | |||||
{ | |||||
b'namespace': b'bookmarks', | |||||
}, | |||||
).result() | |||||
) | ) | ||||
r = comparebookmarks(repo, remotemarks, repo._bookmarks) | r = comparebookmarks(repo, remotemarks, repo._bookmarks) | ||||
addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r | addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r | ||||
return (len(addsrc), len(adddst)) | return (len(addsrc), len(adddst)) | ||||
def validdest(repo, old, new): | def validdest(repo, old, new): |
List, | List, | ||||
Optional, | Optional, | ||||
Set, | Set, | ||||
Tuple, | Tuple, | ||||
Union, | Union, | ||||
) | ) | ||||
assert any( | assert any( | ||||
(Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union,) | ( | ||||
Any, | |||||
Callable, | |||||
Dict, | |||||
Iterable, | |||||
List, | |||||
Optional, | |||||
Set, | |||||
Tuple, | |||||
Union, | |||||
) | |||||
) | ) | ||||
subsettable = repoviewutil.subsettable | subsettable = repoviewutil.subsettable | ||||
calcsize = struct.calcsize | calcsize = struct.calcsize | ||||
pack_into = struct.pack_into | pack_into = struct.pack_into | ||||
unpack_from = struct.unpack_from | unpack_from = struct.unpack_from | ||||
cache.write(rview) | cache.write(rview) | ||||
return | return | ||||
def clear(self): | def clear(self): | ||||
self._per_filter.clear() | self._per_filter.clear() | ||||
def _unknownnode(node): | def _unknownnode(node): | ||||
""" raises ValueError when branchcache found a node which does not exists | """raises ValueError when branchcache found a node which does not exists""" | ||||
""" | |||||
raise ValueError('node %s does not exist' % pycompat.sysstr(hex(node))) | raise ValueError('node %s does not exist' % pycompat.sysstr(hex(node))) | ||||
def _branchcachedesc(repo): | def _branchcachedesc(repo): | ||||
if repo.filtername is not None: | if repo.filtername is not None: | ||||
return b'branch cache (%s)' % repo.filtername | return b'branch cache (%s)' % repo.filtername | ||||
else: | else: | ||||
return b'branch cache' | return b'branch cache' | ||||
entries=(), | entries=(), | ||||
tipnode=nullid, | tipnode=nullid, | ||||
tiprev=nullrev, | tiprev=nullrev, | ||||
filteredhash=None, | filteredhash=None, | ||||
closednodes=None, | closednodes=None, | ||||
hasnode=None, | hasnode=None, | ||||
): | ): | ||||
# type: (Union[Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]], bytes, int, Optional[bytes], Optional[Set[bytes]], Optional[Callable[[bytes], bool]]) -> None | # type: (Union[Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]], bytes, int, Optional[bytes], Optional[Set[bytes]], Optional[Callable[[bytes], bool]]) -> None | ||||
""" hasnode is a function which can be used to verify whether changelog | """hasnode is a function which can be used to verify whether changelog | ||||
has a given node or not. If it's not provided, we assume that every node | has a given node or not. If it's not provided, we assume that every node | ||||
we have exists in changelog """ | we have exists in changelog""" | ||||
self.tipnode = tipnode | self.tipnode = tipnode | ||||
self.tiprev = tiprev | self.tiprev = tiprev | ||||
self.filteredhash = filteredhash | self.filteredhash = filteredhash | ||||
# closednodes is a set of nodes that close their branch. If the branch | # closednodes is a set of nodes that close their branch. If the branch | ||||
# cache has been updated, it may contain nodes that are no longer | # cache has been updated, it may contain nodes that are no longer | ||||
# heads. | # heads. | ||||
if closednodes is None: | if closednodes is None: | ||||
self._closednodes = set() | self._closednodes = set() | ||||
finally: | finally: | ||||
if f: | if f: | ||||
f.close() | f.close() | ||||
return bcache | return bcache | ||||
def load(self, repo, lineiter): | def load(self, repo, lineiter): | ||||
""" fully loads the branchcache by reading from the file using the line | """fully loads the branchcache by reading from the file using the line | ||||
iterator passed""" | iterator passed""" | ||||
for line in lineiter: | for line in lineiter: | ||||
line = line.rstrip(b'\n') | line = line.rstrip(b'\n') | ||||
if not line: | if not line: | ||||
continue | continue | ||||
node, state, label = line.split(b" ", 2) | node, state, label = line.split(b" ", 2) | ||||
if state not in b'oc': | if state not in b'oc': | ||||
raise ValueError('invalid branch state') | raise ValueError('invalid branch state') | ||||
try: | try: | ||||
return (self.tipnode == repo.changelog.node(self.tiprev)) and ( | return (self.tipnode == repo.changelog.node(self.tiprev)) and ( | ||||
self.filteredhash == scmutil.filteredhash(repo, self.tiprev) | self.filteredhash == scmutil.filteredhash(repo, self.tiprev) | ||||
) | ) | ||||
except IndexError: | except IndexError: | ||||
return False | return False | ||||
def _branchtip(self, heads): | def _branchtip(self, heads): | ||||
'''Return tuple with last open head in heads and false, | """Return tuple with last open head in heads and false, | ||||
otherwise return last closed head and true.''' | otherwise return last closed head and true.""" | ||||
tip = heads[-1] | tip = heads[-1] | ||||
closed = True | closed = True | ||||
for h in reversed(heads): | for h in reversed(heads): | ||||
if h not in self._closednodes: | if h not in self._closednodes: | ||||
tip = h | tip = h | ||||
closed = False | closed = False | ||||
break | break | ||||
return tip, closed | return tip, closed | ||||
def branchtip(self, branch): | def branchtip(self, branch): | ||||
'''Return the tipmost open head on branch head, otherwise return the | """Return the tipmost open head on branch head, otherwise return the | ||||
tipmost closed head on branch. | tipmost closed head on branch. | ||||
Raise KeyError for unknown branch.''' | Raise KeyError for unknown branch.""" | ||||
return self._branchtip(self[branch])[0] | return self._branchtip(self[branch])[0] | ||||
def iteropen(self, nodes): | def iteropen(self, nodes): | ||||
return (n for n in nodes if n not in self._closednodes) | return (n for n in nodes if n not in self._closednodes) | ||||
def branchheads(self, branch, closed=False): | def branchheads(self, branch, closed=False): | ||||
self._verifybranch(branch) | self._verifybranch(branch) | ||||
heads = self._entries[branch] | heads = self._entries[branch] |
def processparts(repo, op, unbundler): | def processparts(repo, op, unbundler): | ||||
with partiterator(repo, op, unbundler) as parts: | with partiterator(repo, op, unbundler) as parts: | ||||
for part in parts: | for part in parts: | ||||
_processpart(op, part) | _processpart(op, part) | ||||
def _processchangegroup(op, cg, tr, source, url, **kwargs): | def _processchangegroup(op, cg, tr, source, url, **kwargs): | ||||
ret = cg.apply(op.repo, tr, source, url, **kwargs) | ret = cg.apply(op.repo, tr, source, url, **kwargs) | ||||
op.records.add(b'changegroup', {b'return': ret,}) | op.records.add( | ||||
b'changegroup', | |||||
{ | |||||
b'return': ret, | |||||
}, | |||||
) | |||||
return ret | return ret | ||||
def _gethandler(op, part): | def _gethandler(op, part): | ||||
status = b'unknown' # used by debug output | status = b'unknown' # used by debug output | ||||
try: | try: | ||||
handler = parthandlermapping.get(part.type) | handler = parthandlermapping.get(part.type) | ||||
if handler is None: | if handler is None: | ||||
raw = remote.capable(b'bundle2') | raw = remote.capable(b'bundle2') | ||||
if not raw and raw != b'': | if not raw and raw != b'': | ||||
return {} | return {} | ||||
capsblob = urlreq.unquote(remote.capable(b'bundle2')) | capsblob = urlreq.unquote(remote.capable(b'bundle2')) | ||||
return decodecaps(capsblob) | return decodecaps(capsblob) | ||||
def obsmarkersversion(caps): | def obsmarkersversion(caps): | ||||
"""extract the list of supported obsmarkers versions from a bundle2caps dict | """extract the list of supported obsmarkers versions from a bundle2caps dict""" | ||||
""" | |||||
obscaps = caps.get(b'obsmarkers', ()) | obscaps = caps.get(b'obsmarkers', ()) | ||||
return [int(c[1:]) for c in obscaps if c.startswith(b'V')] | return [int(c[1:]) for c in obscaps if c.startswith(b'V')] | ||||
def writenewbundle( | def writenewbundle( | ||||
ui, | ui, | ||||
repo, | repo, | ||||
source, | source, |
msg = _(b'Unsupported changegroup version: %s') | msg = _(b'Unsupported changegroup version: %s') | ||||
raise error.Abort(msg % version) | raise error.Abort(msg % version) | ||||
if bundle.compressed(): | if bundle.compressed(): | ||||
cgstream = self._writetempbundle(part.read, b'.cg%sun' % version) | cgstream = self._writetempbundle(part.read, b'.cg%sun' % version) | ||||
self._cgunpacker = changegroup.getunbundler(version, cgstream, b'UN') | self._cgunpacker = changegroup.getunbundler(version, cgstream, b'UN') | ||||
def _writetempbundle(self, readfn, suffix, header=b''): | def _writetempbundle(self, readfn, suffix, header=b''): | ||||
"""Write a temporary file to disk | """Write a temporary file to disk""" | ||||
""" | |||||
fdtemp, temp = self.vfs.mkstemp(prefix=b"hg-bundle-", suffix=suffix) | fdtemp, temp = self.vfs.mkstemp(prefix=b"hg-bundle-", suffix=suffix) | ||||
self.tempfile = temp | self.tempfile = temp | ||||
with os.fdopen(fdtemp, 'wb') as fptemp: | with os.fdopen(fdtemp, 'wb') as fptemp: | ||||
fptemp.write(header) | fptemp.write(header) | ||||
while True: | while True: | ||||
chunk = readfn(2 ** 18) | chunk = readfn(2 ** 18) | ||||
if not chunk: | if not chunk: | ||||
def release(self): | def release(self): | ||||
raise NotImplementedError | raise NotImplementedError | ||||
def getremotechanges( | def getremotechanges( | ||||
ui, repo, peer, onlyheads=None, bundlename=None, force=False | ui, repo, peer, onlyheads=None, bundlename=None, force=False | ||||
): | ): | ||||
'''obtains a bundle of changes incoming from peer | """obtains a bundle of changes incoming from peer | ||||
"onlyheads" restricts the returned changes to those reachable from the | "onlyheads" restricts the returned changes to those reachable from the | ||||
specified heads. | specified heads. | ||||
"bundlename", if given, stores the bundle to this file path permanently; | "bundlename", if given, stores the bundle to this file path permanently; | ||||
otherwise it's stored to a temp file and gets deleted again when you call | otherwise it's stored to a temp file and gets deleted again when you call | ||||
the returned "cleanupfn". | the returned "cleanupfn". | ||||
"force" indicates whether to proceed on unrelated repos. | "force" indicates whether to proceed on unrelated repos. | ||||
Returns a tuple (local, csets, cleanupfn): | Returns a tuple (local, csets, cleanupfn): | ||||
"local" is a local repo from which to obtain the actual incoming | "local" is a local repo from which to obtain the actual incoming | ||||
changesets; it is a bundlerepo for the obtained bundle when the | changesets; it is a bundlerepo for the obtained bundle when the | ||||
original "peer" is remote. | original "peer" is remote. | ||||
"csets" lists the incoming changeset node ids. | "csets" lists the incoming changeset node ids. | ||||
"cleanupfn" must be called without arguments when you're done processing | "cleanupfn" must be called without arguments when you're done processing | ||||
the changes; it closes both the original "peer" and the one returned | the changes; it closes both the original "peer" and the one returned | ||||
here. | here. | ||||
''' | """ | ||||
tmp = discovery.findcommonincoming(repo, peer, heads=onlyheads, force=force) | tmp = discovery.findcommonincoming(repo, peer, heads=onlyheads, force=force) | ||||
common, incoming, rheads = tmp | common, incoming, rheads = tmp | ||||
if not incoming: | if not incoming: | ||||
try: | try: | ||||
if bundlename: | if bundlename: | ||||
os.unlink(bundlename) | os.unlink(bundlename) | ||||
except OSError: | except OSError: | ||||
pass | pass | ||||
}, | }, | ||||
).result() | ).result() | ||||
elif onlyheads is None and not peer.capable(b'changegroupsubset'): | elif onlyheads is None and not peer.capable(b'changegroupsubset'): | ||||
# compat with older servers when pulling all remote heads | # compat with older servers when pulling all remote heads | ||||
with peer.commandexecutor() as e: | with peer.commandexecutor() as e: | ||||
cg = e.callcommand( | cg = e.callcommand( | ||||
b'changegroup', | b'changegroup', | ||||
{b'nodes': incoming, b'source': b'incoming',}, | { | ||||
b'nodes': incoming, | |||||
b'source': b'incoming', | |||||
}, | |||||
).result() | ).result() | ||||
rheads = None | rheads = None | ||||
else: | else: | ||||
with peer.commandexecutor() as e: | with peer.commandexecutor() as e: | ||||
cg = e.callcommand( | cg = e.callcommand( | ||||
b'changegroupsubset', | b'changegroupsubset', | ||||
{ | { | ||||
csets = localrepo.changelog.findmissing(common, rheads) | csets = localrepo.changelog.findmissing(common, rheads) | ||||
if bundlerepo: | if bundlerepo: | ||||
reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev :]] | reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev :]] | ||||
with peer.commandexecutor() as e: | with peer.commandexecutor() as e: | ||||
remotephases = e.callcommand( | remotephases = e.callcommand( | ||||
b'listkeys', {b'namespace': b'phases',} | b'listkeys', | ||||
{ | |||||
b'namespace': b'phases', | |||||
}, | |||||
).result() | ).result() | ||||
pullop = exchange.pulloperation(bundlerepo, peer, heads=reponodes) | pullop = exchange.pulloperation(bundlerepo, peer, heads=reponodes) | ||||
pullop.trmanager = bundletransactionmanager() | pullop.trmanager = bundletransactionmanager() | ||||
exchange._pullapplyphases(pullop, remotephases) | exchange._pullapplyphases(pullop, remotephases) | ||||
def cleanup(): | def cleanup(): | ||||
if bundlerepo: | if bundlerepo: | ||||
bundlerepo.close() | bundlerepo.close() | ||||
if bundle: | if bundle: | ||||
os.unlink(bundle) | os.unlink(bundle) | ||||
peer.close() | peer.close() | ||||
return (localrepo, csets, cleanup) | return (localrepo, csets, cleanup) |
def stripdesc(desc): | def stripdesc(desc): | ||||
"""strip trailing whitespace and leading and trailing empty lines""" | """strip trailing whitespace and leading and trailing empty lines""" | ||||
return b'\n'.join([l.rstrip() for l in desc.splitlines()]).strip(b'\n') | return b'\n'.join([l.rstrip() for l in desc.splitlines()]).strip(b'\n') | ||||
class appender(object): | class appender(object): | ||||
'''the changelog index must be updated last on disk, so we use this class | """the changelog index must be updated last on disk, so we use this class | ||||
to delay writes to it''' | to delay writes to it""" | ||||
def __init__(self, vfs, name, mode, buf): | def __init__(self, vfs, name, mode, buf): | ||||
self.data = buf | self.data = buf | ||||
fp = vfs(name, mode) | fp = vfs(name, mode) | ||||
self.fp = fp | self.fp = fp | ||||
self.offset = fp.tell() | self.offset = fp.tell() | ||||
self.size = vfs.fstat(fp).st_size | self.size = vfs.fstat(fp).st_size | ||||
self._end = self.size | self._end = self.size |
except crecordmod.fallbackerror as e: | except crecordmod.fallbackerror as e: | ||||
ui.warn(b'%s\n' % e) | ui.warn(b'%s\n' % e) | ||||
ui.warn(_(b'falling back to text mode\n')) | ui.warn(_(b'falling back to text mode\n')) | ||||
return patch.filterpatch(ui, originalhunks, match, operation) | return patch.filterpatch(ui, originalhunks, match, operation) | ||||
def recordfilter(ui, originalhunks, match, operation=None): | def recordfilter(ui, originalhunks, match, operation=None): | ||||
""" Prompts the user to filter the originalhunks and return a list of | """Prompts the user to filter the originalhunks and return a list of | ||||
selected hunks. | selected hunks. | ||||
*operation* is used for to build ui messages to indicate the user what | *operation* is used for to build ui messages to indicate the user what | ||||
kind of filtering they are doing: reverting, committing, shelving, etc. | kind of filtering they are doing: reverting, committing, shelving, etc. | ||||
(see patch.filterpatch). | (see patch.filterpatch). | ||||
""" | """ | ||||
usecurses = crecordmod.checkcurses(ui) | usecurses = crecordmod.checkcurses(ui) | ||||
testfile = ui.config(b'experimental', b'crecordtest') | testfile = ui.config(b'experimental', b'crecordtest') | ||||
oldwrite = setupwrapcolorwrite(ui) | oldwrite = setupwrapcolorwrite(ui) | ||||
oldp, p = p, os.path.dirname(p) | oldp, p = p, os.path.dirname(p) | ||||
if p == oldp: | if p == oldp: | ||||
return None | return None | ||||
return p | return p | ||||
def bailifchanged(repo, merge=True, hint=None): | def bailifchanged(repo, merge=True, hint=None): | ||||
""" enforce the precondition that working directory must be clean. | """enforce the precondition that working directory must be clean. | ||||
'merge' can be set to false if a pending uncommitted merge should be | 'merge' can be set to false if a pending uncommitted merge should be | ||||
ignored (such as when 'update --check' runs). | ignored (such as when 'update --check' runs). | ||||
'hint' is the usual hint given to Abort exception. | 'hint' is the usual hint given to Abort exception. | ||||
""" | """ | ||||
if merge and repo.dirstate.p2() != nullid: | if merge and repo.dirstate.p2() != nullid: | ||||
repo, | repo, | ||||
revs, | revs, | ||||
basefm, | basefm, | ||||
fntemplate=b'hg-%h.patch', | fntemplate=b'hg-%h.patch', | ||||
switch_parent=False, | switch_parent=False, | ||||
opts=None, | opts=None, | ||||
match=None, | match=None, | ||||
): | ): | ||||
'''export changesets as hg patches | """export changesets as hg patches | ||||
Args: | Args: | ||||
repo: The repository from which we're exporting revisions. | repo: The repository from which we're exporting revisions. | ||||
revs: A list of revisions to export as revision numbers. | revs: A list of revisions to export as revision numbers. | ||||
basefm: A formatter to which patches should be written. | basefm: A formatter to which patches should be written. | ||||
fntemplate: An optional string to use for generating patch file names. | fntemplate: An optional string to use for generating patch file names. | ||||
switch_parent: If True, show diffs against second parent when not nullid. | switch_parent: If True, show diffs against second parent when not nullid. | ||||
Default is false, which always shows diff against p1. | Default is false, which always shows diff against p1. | ||||
opts: diff options to use for generating the patch. | opts: diff options to use for generating the patch. | ||||
match: If specified, only export changes to files matching this matcher. | match: If specified, only export changes to files matching this matcher. | ||||
Returns: | Returns: | ||||
Nothing. | Nothing. | ||||
Side Effect: | Side Effect: | ||||
"HG Changeset Patch" data is emitted to one of the following | "HG Changeset Patch" data is emitted to one of the following | ||||
destinations: | destinations: | ||||
fntemplate specified: Each rev is written to a unique file named using | fntemplate specified: Each rev is written to a unique file named using | ||||
the given template. | the given template. | ||||
Otherwise: All revs will be written to basefm. | Otherwise: All revs will be written to basefm. | ||||
''' | """ | ||||
_prefetchchangedfiles(repo, revs, match) | _prefetchchangedfiles(repo, revs, match) | ||||
if not fntemplate: | if not fntemplate: | ||||
_exportfile( | _exportfile( | ||||
repo, revs, basefm, b'<unnamed>', switch_parent, opts, match | repo, revs, basefm, b'<unnamed>', switch_parent, opts, match | ||||
) | ) | ||||
else: | else: | ||||
_exportfntemplate( | _exportfntemplate( | ||||
if not opts.get(b'dry_run'): | if not opts.get(b'dry_run'): | ||||
needdata = (b'revert', b'add', b'undelete') | needdata = (b'revert', b'add', b'undelete') | ||||
oplist = [actions[name][0] for name in needdata] | oplist = [actions[name][0] for name in needdata] | ||||
prefetch = scmutil.prefetchfiles | prefetch = scmutil.prefetchfiles | ||||
matchfiles = scmutil.matchfiles( | matchfiles = scmutil.matchfiles( | ||||
repo, [f for sublist in oplist for f in sublist] | repo, [f for sublist in oplist for f in sublist] | ||||
) | ) | ||||
prefetch( | prefetch( | ||||
repo, [(ctx.rev(), matchfiles)], | repo, | ||||
[(ctx.rev(), matchfiles)], | |||||
) | ) | ||||
match = scmutil.match(repo[None], pats) | match = scmutil.match(repo[None], pats) | ||||
_performrevert( | _performrevert( | ||||
repo, | repo, | ||||
ctx, | ctx, | ||||
names, | names, | ||||
uipathfn, | uipathfn, | ||||
actions, | actions, | ||||
# | # | ||||
# otherwise, 'changes' is a tuple of tuples below: | # otherwise, 'changes' is a tuple of tuples below: | ||||
# - (sourceurl, sourcebranch, sourcepeer, incoming) | # - (sourceurl, sourcebranch, sourcepeer, incoming) | ||||
# - (desturl, destbranch, destpeer, outgoing) | # - (desturl, destbranch, destpeer, outgoing) | ||||
summaryremotehooks = util.hooks() | summaryremotehooks = util.hooks() | ||||
def checkunfinished(repo, commit=False, skipmerge=False): | def checkunfinished(repo, commit=False, skipmerge=False): | ||||
'''Look for an unfinished multistep operation, like graft, and abort | """Look for an unfinished multistep operation, like graft, and abort | ||||
if found. It's probably good to check this right before | if found. It's probably good to check this right before | ||||
bailifchanged(). | bailifchanged(). | ||||
''' | """ | ||||
# Check for non-clearable states first, so things like rebase will take | # Check for non-clearable states first, so things like rebase will take | ||||
# precedence over update. | # precedence over update. | ||||
for state in statemod._unfinishedstates: | for state in statemod._unfinishedstates: | ||||
if ( | if ( | ||||
state._clearable | state._clearable | ||||
or (commit and state._allowcommit) | or (commit and state._allowcommit) | ||||
or state._reportonly | or state._reportonly | ||||
): | ): | ||||
or s._reportonly | or s._reportonly | ||||
): | ): | ||||
continue | continue | ||||
if s.isunfinished(repo): | if s.isunfinished(repo): | ||||
raise error.StateError(s.msg(), hint=s.hint()) | raise error.StateError(s.msg(), hint=s.hint()) | ||||
def clearunfinished(repo): | def clearunfinished(repo): | ||||
'''Check for unfinished operations (as above), and clear the ones | """Check for unfinished operations (as above), and clear the ones | ||||
that are clearable. | that are clearable. | ||||
''' | """ | ||||
for state in statemod._unfinishedstates: | for state in statemod._unfinishedstates: | ||||
if state._reportonly: | if state._reportonly: | ||||
continue | continue | ||||
if not state._clearable and state.isunfinished(repo): | if not state._clearable and state.isunfinished(repo): | ||||
raise error.StateError(state.msg(), hint=state.hint()) | raise error.StateError(state.msg(), hint=state.hint()) | ||||
for s in statemod._unfinishedstates: | for s in statemod._unfinishedstates: | ||||
if s._opname == b'merge' or state._reportonly: | if s._opname == b'merge' or state._reportonly: | ||||
continue | continue | ||||
if s._clearable and s.isunfinished(repo): | if s._clearable and s.isunfinished(repo): | ||||
util.unlink(repo.vfs.join(s._fname)) | util.unlink(repo.vfs.join(s._fname)) | ||||
def getunfinishedstate(repo): | def getunfinishedstate(repo): | ||||
''' Checks for unfinished operations and returns statecheck object | """Checks for unfinished operations and returns statecheck object | ||||
for it''' | for it""" | ||||
for state in statemod._unfinishedstates: | for state in statemod._unfinishedstates: | ||||
if state.isunfinished(repo): | if state.isunfinished(repo): | ||||
return state | return state | ||||
return None | return None | ||||
def howtocontinue(repo): | def howtocontinue(repo): | ||||
'''Check for an unfinished operation and return the command to finish | """Check for an unfinished operation and return the command to finish | ||||
it. | it. | ||||
statemod._unfinishedstates list is checked for an unfinished operation | statemod._unfinishedstates list is checked for an unfinished operation | ||||
and the corresponding message to finish it is generated if a method to | and the corresponding message to finish it is generated if a method to | ||||
continue is supported by the operation. | continue is supported by the operation. | ||||
Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is | Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is | ||||
a boolean. | a boolean. | ||||
''' | """ | ||||
contmsg = _(b"continue: %s") | contmsg = _(b"continue: %s") | ||||
for state in statemod._unfinishedstates: | for state in statemod._unfinishedstates: | ||||
if not state._continueflag: | if not state._continueflag: | ||||
continue | continue | ||||
if state.isunfinished(repo): | if state.isunfinished(repo): | ||||
return contmsg % state.continuemsg(), True | return contmsg % state.continuemsg(), True | ||||
if repo[None].dirty(missing=True, merge=False, branch=False): | if repo[None].dirty(missing=True, merge=False, branch=False): | ||||
return contmsg % _(b"hg commit"), False | return contmsg % _(b"hg commit"), False | ||||
return None, None | return None, None | ||||
def checkafterresolved(repo): | def checkafterresolved(repo): | ||||
'''Inform the user about the next action after completing hg resolve | """Inform the user about the next action after completing hg resolve | ||||
If there's a an unfinished operation that supports continue flag, | If there's a an unfinished operation that supports continue flag, | ||||
howtocontinue will yield repo.ui.warn as the reporter. | howtocontinue will yield repo.ui.warn as the reporter. | ||||
Otherwise, it will yield repo.ui.note. | Otherwise, it will yield repo.ui.note. | ||||
''' | """ | ||||
msg, warning = howtocontinue(repo) | msg, warning = howtocontinue(repo) | ||||
if msg is not None: | if msg is not None: | ||||
if warning: | if warning: | ||||
repo.ui.warn(b"%s\n" % msg) | repo.ui.warn(b"%s\n" % msg) | ||||
else: | else: | ||||
repo.ui.note(b"%s\n" % msg) | repo.ui.note(b"%s\n" % msg) | ||||
def wrongtooltocontinue(repo, task): | def wrongtooltocontinue(repo, task): | ||||
'''Raise an abort suggesting how to properly continue if there is an | """Raise an abort suggesting how to properly continue if there is an | ||||
active task. | active task. | ||||
Uses howtocontinue() to find the active task. | Uses howtocontinue() to find the active task. | ||||
If there's no task (repo.ui.note for 'hg commit'), it does not offer | If there's no task (repo.ui.note for 'hg commit'), it does not offer | ||||
a hint. | a hint. | ||||
''' | """ | ||||
after = howtocontinue(repo) | after = howtocontinue(repo) | ||||
hint = None | hint = None | ||||
if after[1]: | if after[1]: | ||||
hint = after[0] | hint = after[0] | ||||
raise error.StateError(_(b'no %s in progress') % task, hint=hint) | raise error.StateError(_(b'no %s in progress') % task, hint=hint) | ||||
def abortgraft(ui, repo, graftstate): | def abortgraft(ui, repo, graftstate): |
(b't', b'type', b'', _(b'type of distribution to create'), _(b'TYPE')), | (b't', b'type', b'', _(b'type of distribution to create'), _(b'TYPE')), | ||||
] | ] | ||||
+ subrepoopts | + subrepoopts | ||||
+ walkopts, | + walkopts, | ||||
_(b'[OPTION]... DEST'), | _(b'[OPTION]... DEST'), | ||||
helpcategory=command.CATEGORY_IMPORT_EXPORT, | helpcategory=command.CATEGORY_IMPORT_EXPORT, | ||||
) | ) | ||||
def archive(ui, repo, dest, **opts): | def archive(ui, repo, dest, **opts): | ||||
'''create an unversioned archive of a repository revision | """create an unversioned archive of a repository revision | ||||
By default, the revision used is the parent of the working | By default, the revision used is the parent of the working | ||||
directory; use -r/--rev to specify a different revision. | directory; use -r/--rev to specify a different revision. | ||||
The archive type is automatically detected based on file | The archive type is automatically detected based on file | ||||
extension (to override, use -t/--type). | extension (to override, use -t/--type). | ||||
.. container:: verbose | .. container:: verbose | ||||
using a format string; see :hg:`help export` for details. | using a format string; see :hg:`help export` for details. | ||||
Each member added to an archive file has a directory prefix | Each member added to an archive file has a directory prefix | ||||
prepended. Use -p/--prefix to specify a format string for the | prepended. Use -p/--prefix to specify a format string for the | ||||
prefix. The default is the basename of the archive, with suffixes | prefix. The default is the basename of the archive, with suffixes | ||||
removed. | removed. | ||||
Returns 0 on success. | Returns 0 on success. | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
rev = opts.get(b'rev') | rev = opts.get(b'rev') | ||||
if rev: | if rev: | ||||
repo = scmutil.unhidehashlikerevs(repo, [rev], b'nowarn') | repo = scmutil.unhidehashlikerevs(repo, [rev], b'nowarn') | ||||
ctx = scmutil.revsingle(repo, rev) | ctx = scmutil.revsingle(repo, rev) | ||||
if not ctx: | if not ctx: | ||||
raise error.InputError( | raise error.InputError( | ||||
+ mergetoolopts | + mergetoolopts | ||||
+ walkopts | + walkopts | ||||
+ commitopts | + commitopts | ||||
+ commitopts2, | + commitopts2, | ||||
_(b'[OPTION]... [-r] REV'), | _(b'[OPTION]... [-r] REV'), | ||||
helpcategory=command.CATEGORY_CHANGE_MANAGEMENT, | helpcategory=command.CATEGORY_CHANGE_MANAGEMENT, | ||||
) | ) | ||||
def backout(ui, repo, node=None, rev=None, **opts): | def backout(ui, repo, node=None, rev=None, **opts): | ||||
'''reverse effect of earlier changeset | """reverse effect of earlier changeset | ||||
Prepare a new changeset with the effect of REV undone in the | Prepare a new changeset with the effect of REV undone in the | ||||
current working directory. If no conflicts were encountered, | current working directory. If no conflicts were encountered, | ||||
it will be committed immediately. | it will be committed immediately. | ||||
If REV is the parent of the working directory, then this new changeset | If REV is the parent of the working directory, then this new changeset | ||||
is committed automatically (unless --no-commit is specified). | is committed automatically (unless --no-commit is specified). | ||||
See :hg:`help dates` for a list of formats valid for -d/--date. | See :hg:`help dates` for a list of formats valid for -d/--date. | ||||
See :hg:`help revert` for a way to restore files to the state | See :hg:`help revert` for a way to restore files to the state | ||||
of another revision. | of another revision. | ||||
Returns 0 on success, 1 if nothing to backout or there are unresolved | Returns 0 on success, 1 if nothing to backout or there are unresolved | ||||
files. | files. | ||||
''' | """ | ||||
with repo.wlock(), repo.lock(): | with repo.wlock(), repo.lock(): | ||||
return _dobackout(ui, repo, node, rev, **opts) | return _dobackout(ui, repo, node, rev, **opts) | ||||
def _dobackout(ui, repo, node=None, rev=None, **opts): | def _dobackout(ui, repo, node=None, rev=None, **opts): | ||||
cmdutil.check_incompatible_arguments(opts, 'no_commit', ['commit', 'merge']) | cmdutil.check_incompatible_arguments(opts, 'no_commit', ['commit', 'merge']) | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
(b'i', b'inactive', False, _(b'mark a bookmark inactive')), | (b'i', b'inactive', False, _(b'mark a bookmark inactive')), | ||||
(b'l', b'list', False, _(b'list existing bookmarks')), | (b'l', b'list', False, _(b'list existing bookmarks')), | ||||
] | ] | ||||
+ formatteropts, | + formatteropts, | ||||
_(b'hg bookmarks [OPTIONS]... [NAME]...'), | _(b'hg bookmarks [OPTIONS]... [NAME]...'), | ||||
helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | ||||
) | ) | ||||
def bookmark(ui, repo, *names, **opts): | def bookmark(ui, repo, *names, **opts): | ||||
'''create a new bookmark or list existing bookmarks | """create a new bookmark or list existing bookmarks | ||||
Bookmarks are labels on changesets to help track lines of development. | Bookmarks are labels on changesets to help track lines of development. | ||||
Bookmarks are unversioned and can be moved, renamed and deleted. | Bookmarks are unversioned and can be moved, renamed and deleted. | ||||
Deleting or moving a bookmark has no effect on the associated changesets. | Deleting or moving a bookmark has no effect on the associated changesets. | ||||
Creating or updating to a bookmark causes it to be marked as 'active'. | Creating or updating to a bookmark causes it to be marked as 'active'. | ||||
The active bookmark is indicated with a '*'. | The active bookmark is indicated with a '*'. | ||||
When a commit is made, the active bookmark will advance to the new commit. | When a commit is made, the active bookmark will advance to the new commit. | ||||
- move the '@' bookmark from another branch:: | - move the '@' bookmark from another branch:: | ||||
hg book -f @ | hg book -f @ | ||||
- print only the active bookmark name:: | - print only the active bookmark name:: | ||||
hg book -ql . | hg book -ql . | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
force = opts.get(b'force') | force = opts.get(b'force') | ||||
rev = opts.get(b'rev') | rev = opts.get(b'rev') | ||||
inactive = opts.get(b'inactive') # meaning add/rename to inactive bookmark | inactive = opts.get(b'inactive') # meaning add/rename to inactive bookmark | ||||
action = cmdutil.check_at_most_one_arg(opts, b'delete', b'rename', b'list') | action = cmdutil.check_at_most_one_arg(opts, b'delete', b'rename', b'list') | ||||
if action: | if action: | ||||
cmdutil.check_incompatible_arguments(opts, action, [b'rev']) | cmdutil.check_incompatible_arguments(opts, action, [b'rev']) | ||||
with ui.formatter(b'files', opts) as fm: | with ui.formatter(b'files', opts) as fm: | ||||
return cmdutil.files( | return cmdutil.files( | ||||
ui, ctx, m, uipathfn, fm, fmt, opts.get(b'subrepos') | ui, ctx, m, uipathfn, fm, fmt, opts.get(b'subrepos') | ||||
) | ) | ||||
@command( | @command( | ||||
b'forget', | b'forget', | ||||
[(b'i', b'interactive', None, _(b'use interactive mode')),] | [ | ||||
(b'i', b'interactive', None, _(b'use interactive mode')), | |||||
] | |||||
+ walkopts | + walkopts | ||||
+ dryrunopts, | + dryrunopts, | ||||
_(b'[OPTION]... FILE...'), | _(b'[OPTION]... FILE...'), | ||||
helpcategory=command.CATEGORY_WORKING_DIRECTORY, | helpcategory=command.CATEGORY_WORKING_DIRECTORY, | ||||
helpbasic=True, | helpbasic=True, | ||||
inferrepo=True, | inferrepo=True, | ||||
) | ) | ||||
def forget(ui, repo, *pats, **opts): | def forget(ui, repo, *pats, **opts): | ||||
] | ] | ||||
+ commitopts2 | + commitopts2 | ||||
+ mergetoolopts | + mergetoolopts | ||||
+ dryrunopts, | + dryrunopts, | ||||
_(b'[OPTION]... [-r REV]... REV...'), | _(b'[OPTION]... [-r REV]... REV...'), | ||||
helpcategory=command.CATEGORY_CHANGE_MANAGEMENT, | helpcategory=command.CATEGORY_CHANGE_MANAGEMENT, | ||||
) | ) | ||||
def graft(ui, repo, *revs, **opts): | def graft(ui, repo, *revs, **opts): | ||||
'''copy changes from other branches onto the current branch | """copy changes from other branches onto the current branch | ||||
This command uses Mercurial's merge logic to copy individual | This command uses Mercurial's merge logic to copy individual | ||||
changes from other branches without merging branches in the | changes from other branches without merging branches in the | ||||
history graph. This is sometimes known as 'backporting' or | history graph. This is sometimes known as 'backporting' or | ||||
'cherry-picking'. By default, graft will copy user, date, and | 'cherry-picking'. By default, graft will copy user, date, and | ||||
description from the source changesets. | description from the source changesets. | ||||
Changesets that are ancestors of the current revision, that have | Changesets that are ancestors of the current revision, that have | ||||
- land a feature branch as one changeset:: | - land a feature branch as one changeset:: | ||||
hg up -cr default | hg up -cr default | ||||
hg graft -r featureX --base "ancestor('featureX', 'default')" | hg graft -r featureX --base "ancestor('featureX', 'default')" | ||||
See :hg:`help revisions` for more about specifying revisions. | See :hg:`help revisions` for more about specifying revisions. | ||||
Returns 0 on successful completion, 1 if there are unresolved files. | Returns 0 on successful completion, 1 if there are unresolved files. | ||||
''' | """ | ||||
with repo.wlock(): | with repo.wlock(): | ||||
return _dograft(ui, repo, *revs, **opts) | return _dograft(ui, repo, *revs, **opts) | ||||
def _dograft(ui, repo, *revs, **opts): | def _dograft(ui, repo, *revs, **opts): | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
if revs and opts.get(b'rev'): | if revs and opts.get(b'rev'): | ||||
ui.warn( | ui.warn( | ||||
_(b'update to new branch head if new descendants were pulled'), | _(b'update to new branch head if new descendants were pulled'), | ||||
), | ), | ||||
( | ( | ||||
b'f', | b'f', | ||||
b'force', | b'force', | ||||
None, | None, | ||||
_(b'run even when remote repository is unrelated'), | _(b'run even when remote repository is unrelated'), | ||||
), | ), | ||||
(b'', b'confirm', None, _(b'confirm pull before applying changes'),), | ( | ||||
b'', | |||||
b'confirm', | |||||
None, | |||||
_(b'confirm pull before applying changes'), | |||||
), | |||||
( | ( | ||||
b'r', | b'r', | ||||
b'rev', | b'rev', | ||||
[], | [], | ||||
_(b'a remote changeset intended to be added'), | _(b'a remote changeset intended to be added'), | ||||
_(b'REV'), | _(b'REV'), | ||||
), | ), | ||||
(b'B', b'bookmark', [], _(b"bookmark to pull"), _(b'BOOKMARK')), | (b'B', b'bookmark', [], _(b"bookmark to pull"), _(b'BOOKMARK')), | ||||
Returns 0 if push was successful, 1 if nothing to push. | Returns 0 if push was successful, 1 if nothing to push. | ||||
""" | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
if opts.get(b'all_bookmarks'): | if opts.get(b'all_bookmarks'): | ||||
cmdutil.check_incompatible_arguments( | cmdutil.check_incompatible_arguments( | ||||
opts, b'all_bookmarks', [b'bookmark', b'rev'], | opts, | ||||
b'all_bookmarks', | |||||
[b'bookmark', b'rev'], | |||||
) | ) | ||||
opts[b'bookmark'] = list(repo._bookmarks) | opts[b'bookmark'] = list(repo._bookmarks) | ||||
if opts.get(b'bookmark'): | if opts.get(b'bookmark'): | ||||
ui.setconfig(b'bookmarks', b'pushing', opts[b'bookmark'], b'push') | ui.setconfig(b'bookmarks', b'pushing', opts[b'bookmark'], b'push') | ||||
for b in opts[b'bookmark']: | for b in opts[b'bookmark']: | ||||
# translate -B options to -r so changesets get pushed | # translate -B options to -r so changesets get pushed | ||||
b = repo._bookmarks.expandname(b) | b = repo._bookmarks.expandname(b) | ||||
elif not result and pushop.bkresult: | elif not result and pushop.bkresult: | ||||
result = 2 | result = 2 | ||||
return result | return result | ||||
@command( | @command( | ||||
b'recover', | b'recover', | ||||
[(b'', b'verify', False, b"run `hg verify` after successful recover"),], | [ | ||||
(b'', b'verify', False, b"run `hg verify` after successful recover"), | |||||
], | |||||
helpcategory=command.CATEGORY_MAINTENANCE, | helpcategory=command.CATEGORY_MAINTENANCE, | ||||
) | ) | ||||
def recover(ui, repo, **opts): | def recover(ui, repo, **opts): | ||||
"""roll back an interrupted transaction | """roll back an interrupted transaction | ||||
Recover from an interrupted commit or pull. | Recover from an interrupted commit or pull. | ||||
This command tries to fix the repository status after an | This command tries to fix the repository status after an | ||||
), | ), | ||||
), | ), | ||||
] | ] | ||||
+ cmdutil.walkopts, | + cmdutil.walkopts, | ||||
_(b'hg shelve [OPTION]... [FILE]...'), | _(b'hg shelve [OPTION]... [FILE]...'), | ||||
helpcategory=command.CATEGORY_WORKING_DIRECTORY, | helpcategory=command.CATEGORY_WORKING_DIRECTORY, | ||||
) | ) | ||||
def shelve(ui, repo, *pats, **opts): | def shelve(ui, repo, *pats, **opts): | ||||
'''save and set aside changes from the working directory | """save and set aside changes from the working directory | ||||
Shelving takes files that "hg status" reports as not clean, saves | Shelving takes files that "hg status" reports as not clean, saves | ||||
the modifications to a bundle (a shelved change), and reverts the | the modifications to a bundle (a shelved change), and reverts the | ||||
files so that their state in the working directory becomes clean. | files so that their state in the working directory becomes clean. | ||||
To restore these changes to the working directory, using "hg | To restore these changes to the working directory, using "hg | ||||
unshelve"; this will work even if you switch to a different | unshelve"; this will work even if you switch to a different | ||||
commit. | commit. | ||||
branch. To specify a different name, use ``--name``. | branch. To specify a different name, use ``--name``. | ||||
To see a list of existing shelved changes, use the ``--list`` | To see a list of existing shelved changes, use the ``--list`` | ||||
option. For each shelved change, this will print its name, age, | option. For each shelved change, this will print its name, age, | ||||
and description; use ``--patch`` or ``--stat`` for more details. | and description; use ``--patch`` or ``--stat`` for more details. | ||||
To delete specific shelved changes, use ``--delete``. To delete | To delete specific shelved changes, use ``--delete``. To delete | ||||
all shelved changes, use ``--cleanup``. | all shelved changes, use ``--cleanup``. | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
allowables = [ | allowables = [ | ||||
(b'addremove', {b'create'}), # 'create' is pseudo action | (b'addremove', {b'create'}), # 'create' is pseudo action | ||||
(b'unknown', {b'create'}), | (b'unknown', {b'create'}), | ||||
(b'cleanup', {b'cleanup'}), | (b'cleanup', {b'cleanup'}), | ||||
# ('date', {'create'}), # ignored for passing '--date "0 0"' in tests | # ('date', {'create'}), # ignored for passing '--date "0 0"' in tests | ||||
(b'delete', {b'delete'}), | (b'delete', {b'delete'}), | ||||
(b'edit', {b'create'}), | (b'edit', {b'create'}), | ||||
fn.condwrite(ui.verbose and v, b"ver", b"%s", v) | fn.condwrite(ui.verbose and v, b"ver", b"%s", v) | ||||
if ui.verbose: | if ui.verbose: | ||||
fn.plain(b"\n") | fn.plain(b"\n") | ||||
fn.end() | fn.end() | ||||
fm.end() | fm.end() | ||||
def loadcmdtable(ui, name, cmdtable): | def loadcmdtable(ui, name, cmdtable): | ||||
"""Load command functions from specified cmdtable | """Load command functions from specified cmdtable""" | ||||
""" | |||||
overrides = [cmd for cmd in cmdtable if cmd in table] | overrides = [cmd for cmd in cmdtable if cmd in table] | ||||
if overrides: | if overrides: | ||||
ui.warn( | ui.warn( | ||||
_(b"extension '%s' overrides commands: %s\n") | _(b"extension '%s' overrides commands: %s\n") | ||||
% (name, b" ".join(overrides)) | % (name, b" ".join(overrides)) | ||||
) | ) | ||||
table.update(cmdtable) | table.update(cmdtable) |
finally: | finally: | ||||
signal.signal(signal.SIGINT, signal.SIG_IGN) | signal.signal(signal.SIGINT, signal.SIG_IGN) | ||||
# On KeyboardInterrupt, print error message and exit *after* SIGINT | # On KeyboardInterrupt, print error message and exit *after* SIGINT | ||||
# handler removed. | # handler removed. | ||||
req.ui.error(_(b'interrupted!\n')) | req.ui.error(_(b'interrupted!\n')) | ||||
return -1 | return -1 | ||||
def runcommand(self): | def runcommand(self): | ||||
""" reads a list of \0 terminated arguments, executes | """reads a list of \0 terminated arguments, executes | ||||
and writes the return code to the result channel """ | and writes the return code to the result channel""" | ||||
from . import dispatch # avoid cycle | from . import dispatch # avoid cycle | ||||
args = self._readlist() | args = self._readlist() | ||||
# copy the uis so changes (e.g. --config or --verbose) don't | # copy the uis so changes (e.g. --config or --verbose) don't | ||||
# persist between requests | # persist between requests | ||||
copiedui = self.ui.copy() | copiedui = self.ui.copy() | ||||
uis = [copiedui] | uis = [copiedui] |
p1.node(), | p1.node(), | ||||
p2.node(), | p2.node(), | ||||
user, | user, | ||||
ctx.date(), | ctx.date(), | ||||
extra, | extra, | ||||
) | ) | ||||
xp1, xp2 = p1.hex(), p2 and p2.hex() or b'' | xp1, xp2 = p1.hex(), p2 and p2.hex() or b'' | ||||
repo.hook( | repo.hook( | ||||
b'pretxncommit', throw=True, node=hex(n), parent1=xp1, parent2=xp2, | b'pretxncommit', | ||||
throw=True, | |||||
node=hex(n), | |||||
parent1=xp1, | |||||
parent2=xp2, | |||||
) | ) | ||||
# set the new commit is proper phase | # set the new commit is proper phase | ||||
targetphase = subrepoutil.newcommitphase(repo.ui, ctx) | targetphase = subrepoutil.newcommitphase(repo.ui, ctx) | ||||
# prevent unmarking changesets as public on recommit | # prevent unmarking changesets as public on recommit | ||||
waspublic = oldtip == repo.changelog.tiprev() and not repo[n].phase() | waspublic = oldtip == repo.changelog.tiprev() and not repo[n].phase() | ||||
if targetphase and not waspublic: | if targetphase and not waspublic: | ||||
if writechangesetcopy: | if writechangesetcopy: | ||||
files.update_copies_from_p1(ctx.p1copies()) | files.update_copies_from_p1(ctx.p1copies()) | ||||
files.update_copies_from_p2(ctx.p2copies()) | files.update_copies_from_p2(ctx.p2copies()) | ||||
return mn, files | return mn, files | ||||
def _get_salvaged(repo, ms, ctx): | def _get_salvaged(repo, ms, ctx): | ||||
""" returns a list of salvaged files | """returns a list of salvaged files | ||||
returns empty list if config option which process salvaged files are | returns empty list if config option which process salvaged files are | ||||
not enabled """ | not enabled""" | ||||
salvaged = [] | salvaged = [] | ||||
copy_sd = repo.filecopiesmode == b'changeset-sidedata' | copy_sd = repo.filecopiesmode == b'changeset-sidedata' | ||||
if copy_sd and len(ctx.parents()) > 1: | if copy_sd and len(ctx.parents()) > 1: | ||||
if ms.active(): | if ms.active(): | ||||
for fname in sorted(ms.allextras().keys()): | for fname in sorted(ms.allextras().keys()): | ||||
might_removed = ms.extras(fname).get(b'merge-removal-candidate') | might_removed = ms.extras(fname).get(b'merge-removal-candidate') | ||||
if might_removed == b'yes': | if might_removed == b'yes': | ||||
if fname in ctx: | if fname in ctx: | ||||
files.mark_removed(f) | files.mark_removed(f) | ||||
mn = _commit_manifest(tr, linkrev, ctx, mctx, m, files.touched, added, drop) | mn = _commit_manifest(tr, linkrev, ctx, mctx, m, files.touched, added, drop) | ||||
return mn | return mn | ||||
def _filecommit( | def _filecommit( | ||||
repo, fctx, manifest1, manifest2, linkrev, tr, includecopymeta, ms, | repo, | ||||
fctx, | |||||
manifest1, | |||||
manifest2, | |||||
linkrev, | |||||
tr, | |||||
includecopymeta, | |||||
ms, | |||||
): | ): | ||||
""" | """ | ||||
commit an individual file as part of a larger transaction | commit an individual file as part of a larger transaction | ||||
input: | input: | ||||
fctx: a file context with the content we are trying to commit | fctx: a file context with the content we are trying to commit | ||||
manifest1: manifest of changeset first parent | manifest1: manifest of changeset first parent |
message = l.rstrip() | message = l.rstrip() | ||||
if l.startswith(b' '): | if l.startswith(b' '): | ||||
message = b"unexpected leading whitespace: %s" % message | message = b"unexpected leading whitespace: %s" % message | ||||
raise error.ConfigError(message, (b"%s:%d" % (src, line))) | raise error.ConfigError(message, (b"%s:%d" % (src, line))) | ||||
def read(self, path, fp=None, sections=None, remap=None): | def read(self, path, fp=None, sections=None, remap=None): | ||||
if not fp: | if not fp: | ||||
fp = util.posixfile(path, b'rb') | fp = util.posixfile(path, b'rb') | ||||
assert getattr(fp, 'mode', 'rb') == 'rb', ( | assert ( | ||||
b'config files must be opened in binary mode, got fp=%r mode=%r' | getattr(fp, 'mode', 'rb') == 'rb' | ||||
% (fp, fp.mode,) | ), b'config files must be opened in binary mode, got fp=%r mode=%r' % ( | ||||
fp, | |||||
fp.mode, | |||||
) | ) | ||||
dir = os.path.dirname(path) | dir = os.path.dirname(path) | ||||
def include(rel, remap, sections): | def include(rel, remap, sections): | ||||
abs = os.path.normpath(os.path.join(dir, rel)) | abs = os.path.normpath(os.path.join(dir, rel)) | ||||
self.read(abs, remap=remap, sections=sections) | self.read(abs, remap=remap, sections=sections) | ||||
return f | return f | ||||
coreconfigitem = getitemregister(coreitems) | coreconfigitem = getitemregister(coreitems) | ||||
def _registerdiffopts(section, configprefix=b''): | def _registerdiffopts(section, configprefix=b''): | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'nodates', default=False, | section, | ||||
configprefix + b'nodates', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'showfunc', default=False, | section, | ||||
configprefix + b'showfunc', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'unified', default=None, | section, | ||||
configprefix + b'unified', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'git', default=False, | section, | ||||
configprefix + b'git', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'ignorews', default=False, | section, | ||||
configprefix + b'ignorews', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'ignorewsamount', default=False, | section, | ||||
configprefix + b'ignorewsamount', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'ignoreblanklines', default=False, | section, | ||||
configprefix + b'ignoreblanklines', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'ignorewseol', default=False, | section, | ||||
configprefix + b'ignorewseol', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'nobinary', default=False, | section, | ||||
configprefix + b'nobinary', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'noprefix', default=False, | section, | ||||
configprefix + b'noprefix', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
section, configprefix + b'word-diff', default=False, | section, | ||||
configprefix + b'word-diff', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'alias', b'.*', default=dynamicdefault, generic=True, | b'alias', | ||||
b'.*', | |||||
default=dynamicdefault, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'auth', b'cookiefile', default=None, | b'auth', | ||||
b'cookiefile', | |||||
default=None, | |||||
) | ) | ||||
_registerdiffopts(section=b'annotate') | _registerdiffopts(section=b'annotate') | ||||
# bookmarks.pushing: internal hack for discovery | # bookmarks.pushing: internal hack for discovery | ||||
coreconfigitem( | coreconfigitem( | ||||
b'bookmarks', b'pushing', default=list, | b'bookmarks', | ||||
b'pushing', | |||||
default=list, | |||||
) | ) | ||||
# bundle.mainreporoot: internal hack for bundlerepo | # bundle.mainreporoot: internal hack for bundlerepo | ||||
coreconfigitem( | coreconfigitem( | ||||
b'bundle', b'mainreporoot', default=b'', | b'bundle', | ||||
b'mainreporoot', | |||||
default=b'', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'censor', b'policy', default=b'abort', experimental=True, | b'censor', | ||||
b'policy', | |||||
default=b'abort', | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'chgserver', b'idletimeout', default=3600, | b'chgserver', | ||||
b'idletimeout', | |||||
default=3600, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'chgserver', b'skiphash', default=False, | b'chgserver', | ||||
b'skiphash', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'cmdserver', b'log', default=None, | b'cmdserver', | ||||
b'log', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'cmdserver', b'max-log-files', default=7, | b'cmdserver', | ||||
b'max-log-files', | |||||
default=7, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'cmdserver', b'max-log-size', default=b'1 MB', | b'cmdserver', | ||||
b'max-log-size', | |||||
default=b'1 MB', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'cmdserver', b'max-repo-cache', default=0, experimental=True, | b'cmdserver', | ||||
b'max-repo-cache', | |||||
default=0, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'cmdserver', b'message-encodings', default=list, | b'cmdserver', | ||||
b'message-encodings', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'cmdserver', | b'cmdserver', | ||||
b'track-log', | b'track-log', | ||||
default=lambda: [b'chgserver', b'cmdserver', b'repocache'], | default=lambda: [b'chgserver', b'cmdserver', b'repocache'], | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'cmdserver', b'shutdown-on-interrupt', default=True, | b'cmdserver', | ||||
b'shutdown-on-interrupt', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'color', b'.*', default=None, generic=True, | b'color', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'color', b'mode', default=b'auto', | b'color', | ||||
b'mode', | |||||
default=b'auto', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'color', b'pagermode', default=dynamicdefault, | b'color', | ||||
b'pagermode', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'command-templates', | b'command-templates', | ||||
b'graphnode', | b'graphnode', | ||||
default=None, | default=None, | ||||
alias=[(b'ui', b'graphnodetemplate')], | alias=[(b'ui', b'graphnodetemplate')], | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'command-templates', b'log', default=None, alias=[(b'ui', b'logtemplate')], | b'command-templates', | ||||
b'log', | |||||
default=None, | |||||
alias=[(b'ui', b'logtemplate')], | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'command-templates', | b'command-templates', | ||||
b'mergemarker', | b'mergemarker', | ||||
default=( | default=( | ||||
b'{node|short} ' | b'{node|short} ' | ||||
b'{ifeq(tags, "tip", "", ' | b'{ifeq(tags, "tip", "", ' | ||||
b'ifeq(tags, "", "", "{tags} "))}' | b'ifeq(tags, "", "", "{tags} "))}' | ||||
b'{if(bookmarks, "{bookmarks} ")}' | b'{if(bookmarks, "{bookmarks} ")}' | ||||
b'{ifeq(branch, "default", "", "{branch} ")}' | b'{ifeq(branch, "default", "", "{branch} ")}' | ||||
b'- {author|user}: {desc|firstline}' | b'- {author|user}: {desc|firstline}' | ||||
), | ), | ||||
alias=[(b'ui', b'mergemarkertemplate')], | alias=[(b'ui', b'mergemarkertemplate')], | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'command-templates', | b'command-templates', | ||||
b'pre-merge-tool-output', | b'pre-merge-tool-output', | ||||
default=None, | default=None, | ||||
alias=[(b'ui', b'pre-merge-tool-output-template')], | alias=[(b'ui', b'pre-merge-tool-output-template')], | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'command-templates', b'oneline-summary', default=None, | b'command-templates', | ||||
b'oneline-summary', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'command-templates', | b'command-templates', | ||||
b'oneline-summary.*', | b'oneline-summary.*', | ||||
default=dynamicdefault, | default=dynamicdefault, | ||||
generic=True, | generic=True, | ||||
) | ) | ||||
_registerdiffopts(section=b'commands', configprefix=b'commit.interactive.') | _registerdiffopts(section=b'commands', configprefix=b'commit.interactive.') | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'commit.post-status', default=False, | b'commands', | ||||
b'commit.post-status', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'grep.all-files', default=False, experimental=True, | b'commands', | ||||
b'grep.all-files', | |||||
default=False, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'merge.require-rev', default=False, | b'commands', | ||||
b'merge.require-rev', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'push.require-revs', default=False, | b'commands', | ||||
b'push.require-revs', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'resolve.confirm', default=False, | b'commands', | ||||
b'resolve.confirm', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'resolve.explicit-re-merge', default=False, | b'commands', | ||||
b'resolve.explicit-re-merge', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'resolve.mark-check', default=b'none', | b'commands', | ||||
b'resolve.mark-check', | |||||
default=b'none', | |||||
) | ) | ||||
_registerdiffopts(section=b'commands', configprefix=b'revert.interactive.') | _registerdiffopts(section=b'commands', configprefix=b'revert.interactive.') | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'show.aliasprefix', default=list, | b'commands', | ||||
b'show.aliasprefix', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'status.relative', default=False, | b'commands', | ||||
b'status.relative', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'status.skipstates', default=[], experimental=True, | b'commands', | ||||
b'status.skipstates', | |||||
default=[], | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'status.terse', default=b'', | b'commands', | ||||
b'status.terse', | |||||
default=b'', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'status.verbose', default=False, | b'commands', | ||||
b'status.verbose', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'update.check', default=None, | b'commands', | ||||
b'update.check', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'update.requiredest', default=False, | b'commands', | ||||
b'update.requiredest', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'committemplate', b'.*', default=None, generic=True, | b'committemplate', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'bzr.saverev', default=True, | b'convert', | ||||
b'bzr.saverev', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'cvsps.cache', default=True, | b'convert', | ||||
b'cvsps.cache', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'cvsps.fuzz', default=60, | b'convert', | ||||
b'cvsps.fuzz', | |||||
default=60, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'cvsps.logencoding', default=None, | b'convert', | ||||
b'cvsps.logencoding', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'cvsps.mergefrom', default=None, | b'convert', | ||||
b'cvsps.mergefrom', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'cvsps.mergeto', default=None, | b'convert', | ||||
b'cvsps.mergeto', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'git.committeractions', default=lambda: [b'messagedifferent'], | b'convert', | ||||
b'git.committeractions', | |||||
default=lambda: [b'messagedifferent'], | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'git.extrakeys', default=list, | b'convert', | ||||
b'git.extrakeys', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'git.findcopiesharder', default=False, | b'convert', | ||||
b'git.findcopiesharder', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'git.remoteprefix', default=b'remote', | b'convert', | ||||
b'git.remoteprefix', | |||||
default=b'remote', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'git.renamelimit', default=400, | b'convert', | ||||
b'git.renamelimit', | |||||
default=400, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'git.saverev', default=True, | b'convert', | ||||
b'git.saverev', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'git.similarity', default=50, | b'convert', | ||||
b'git.similarity', | |||||
default=50, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'git.skipsubmodules', default=False, | b'convert', | ||||
b'git.skipsubmodules', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'hg.clonebranches', default=False, | b'convert', | ||||
b'hg.clonebranches', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'hg.ignoreerrors', default=False, | b'convert', | ||||
b'hg.ignoreerrors', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'hg.preserve-hash', default=False, | b'convert', | ||||
b'hg.preserve-hash', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'hg.revs', default=None, | b'convert', | ||||
b'hg.revs', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'hg.saverev', default=False, | b'convert', | ||||
b'hg.saverev', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'hg.sourcename', default=None, | b'convert', | ||||
b'hg.sourcename', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'hg.startrev', default=None, | b'convert', | ||||
b'hg.startrev', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'hg.tagsbranch', default=b'default', | b'convert', | ||||
b'hg.tagsbranch', | |||||
default=b'default', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'hg.usebranchnames', default=True, | b'convert', | ||||
b'hg.usebranchnames', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'ignoreancestorcheck', default=False, experimental=True, | b'convert', | ||||
b'ignoreancestorcheck', | |||||
default=False, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'localtimezone', default=False, | b'convert', | ||||
b'localtimezone', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'p4.encoding', default=dynamicdefault, | b'convert', | ||||
b'p4.encoding', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'p4.startrev', default=0, | b'convert', | ||||
b'p4.startrev', | |||||
default=0, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'skiptags', default=False, | b'convert', | ||||
b'skiptags', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'svn.debugsvnlog', default=True, | b'convert', | ||||
b'svn.debugsvnlog', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'svn.trunk', default=None, | b'convert', | ||||
b'svn.trunk', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'svn.tags', default=None, | b'convert', | ||||
b'svn.tags', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'svn.branches', default=None, | b'convert', | ||||
b'svn.branches', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'convert', b'svn.startrev', default=0, | b'convert', | ||||
b'svn.startrev', | |||||
default=0, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'debug', b'dirstate.delaywrite', default=0, | b'debug', | ||||
b'dirstate.delaywrite', | |||||
default=0, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'defaults', b'.*', default=None, generic=True, | b'defaults', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'all-warnings', default=False, | b'devel', | ||||
b'all-warnings', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'bundle2.debug', default=False, | b'devel', | ||||
b'bundle2.debug', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'bundle.delta', default=b'', | b'devel', | ||||
b'bundle.delta', | |||||
default=b'', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'cache-vfs', default=None, | b'devel', | ||||
b'cache-vfs', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'check-locks', default=False, | b'devel', | ||||
b'check-locks', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'check-relroot', default=False, | b'devel', | ||||
b'check-relroot', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'default-date', default=None, | b'devel', | ||||
b'default-date', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'deprec-warn', default=False, | b'devel', | ||||
b'deprec-warn', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'disableloaddefaultcerts', default=False, | b'devel', | ||||
b'disableloaddefaultcerts', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'warn-empty-changegroup', default=False, | b'devel', | ||||
b'warn-empty-changegroup', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'legacy.exchange', default=list, | b'devel', | ||||
b'legacy.exchange', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'persistent-nodemap', default=False, | b'devel', | ||||
b'persistent-nodemap', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'servercafile', default=b'', | b'devel', | ||||
b'servercafile', | |||||
default=b'', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'serverexactprotocol', default=b'', | b'devel', | ||||
b'serverexactprotocol', | |||||
default=b'', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'serverrequirecert', default=False, | b'devel', | ||||
b'serverrequirecert', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'strip-obsmarkers', default=True, | b'devel', | ||||
b'strip-obsmarkers', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'warn-config', default=None, | b'devel', | ||||
b'warn-config', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'warn-config-default', default=None, | b'devel', | ||||
b'warn-config-default', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'user.obsmarker', default=None, | b'devel', | ||||
b'user.obsmarker', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'warn-config-unknown', default=None, | b'devel', | ||||
b'warn-config-unknown', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'debug.copies', default=False, | b'devel', | ||||
b'debug.copies', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'debug.extensions', default=False, | b'devel', | ||||
b'debug.extensions', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'debug.repo-filters', default=False, | b'devel', | ||||
b'debug.repo-filters', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'debug.peer-request', default=False, | b'devel', | ||||
b'debug.peer-request', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'devel', b'discovery.randomize', default=True, | b'devel', | ||||
b'discovery.randomize', | |||||
default=True, | |||||
) | ) | ||||
_registerdiffopts(section=b'diff') | _registerdiffopts(section=b'diff') | ||||
coreconfigitem( | coreconfigitem( | ||||
b'email', b'bcc', default=None, | b'email', | ||||
b'bcc', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'email', b'cc', default=None, | b'email', | ||||
b'cc', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'email', b'charsets', default=list, | b'email', | ||||
b'charsets', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'email', b'from', default=None, | b'email', | ||||
b'from', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'email', b'method', default=b'smtp', | b'email', | ||||
b'method', | |||||
default=b'smtp', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'email', b'reply-to', default=None, | b'email', | ||||
b'reply-to', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'email', b'to', default=None, | b'email', | ||||
b'to', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'archivemetatemplate', default=dynamicdefault, | b'experimental', | ||||
b'archivemetatemplate', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'auto-publish', default=b'publish', | b'experimental', | ||||
b'auto-publish', | |||||
default=b'publish', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'bundle-phases', default=False, | b'experimental', | ||||
b'bundle-phases', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'bundle2-advertise', default=True, | b'experimental', | ||||
b'bundle2-advertise', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'bundle2-output-capture', default=False, | b'experimental', | ||||
b'bundle2-output-capture', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'bundle2.pushback', default=False, | b'experimental', | ||||
b'bundle2.pushback', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'bundle2lazylocking', default=False, | b'experimental', | ||||
b'bundle2lazylocking', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'bundlecomplevel', default=None, | b'experimental', | ||||
b'bundlecomplevel', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'bundlecomplevel.bzip2', default=None, | b'experimental', | ||||
b'bundlecomplevel.bzip2', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'bundlecomplevel.gzip', default=None, | b'experimental', | ||||
b'bundlecomplevel.gzip', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'bundlecomplevel.none', default=None, | b'experimental', | ||||
b'bundlecomplevel.none', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'bundlecomplevel.zstd', default=None, | b'experimental', | ||||
b'bundlecomplevel.zstd', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'changegroup3', default=False, | b'experimental', | ||||
b'changegroup3', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'cleanup-as-archived', default=False, | b'experimental', | ||||
b'cleanup-as-archived', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'clientcompressionengines', default=list, | b'experimental', | ||||
b'clientcompressionengines', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'copytrace', default=b'on', | b'experimental', | ||||
b'copytrace', | |||||
default=b'on', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'copytrace.movecandidateslimit', default=100, | b'experimental', | ||||
b'copytrace.movecandidateslimit', | |||||
default=100, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'copytrace.sourcecommitlimit', default=100, | b'experimental', | ||||
b'copytrace.sourcecommitlimit', | |||||
default=100, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'copies.read-from', default=b"filelog-only", | b'experimental', | ||||
b'copies.read-from', | |||||
default=b"filelog-only", | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'copies.write-to', default=b'filelog-only', | b'experimental', | ||||
b'copies.write-to', | |||||
default=b'filelog-only', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'crecordtest', default=None, | b'experimental', | ||||
b'crecordtest', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'directaccess', default=False, | b'experimental', | ||||
b'directaccess', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'directaccess.revnums', default=False, | b'experimental', | ||||
b'directaccess.revnums', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'editortmpinhg', default=False, | b'experimental', | ||||
b'editortmpinhg', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'evolution', default=list, | b'experimental', | ||||
b'evolution', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', | b'experimental', | ||||
b'evolution.allowdivergence', | b'evolution.allowdivergence', | ||||
default=False, | default=False, | ||||
alias=[(b'experimental', b'allowdivergence')], | alias=[(b'experimental', b'allowdivergence')], | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'evolution.allowunstable', default=None, | b'experimental', | ||||
b'evolution.allowunstable', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'evolution.createmarkers', default=None, | b'experimental', | ||||
b'evolution.createmarkers', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', | b'experimental', | ||||
b'evolution.effect-flags', | b'evolution.effect-flags', | ||||
default=True, | default=True, | ||||
alias=[(b'experimental', b'effect-flags')], | alias=[(b'experimental', b'effect-flags')], | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'evolution.exchange', default=None, | b'experimental', | ||||
b'evolution.exchange', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'evolution.bundle-obsmarker', default=False, | b'experimental', | ||||
b'evolution.bundle-obsmarker', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'log.topo', default=False, | b'experimental', | ||||
b'log.topo', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'evolution.report-instabilities', default=True, | b'experimental', | ||||
b'evolution.report-instabilities', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'evolution.track-operation', default=True, | b'experimental', | ||||
b'evolution.track-operation', | |||||
default=True, | |||||
) | ) | ||||
# repo-level config to exclude a revset visibility | # repo-level config to exclude a revset visibility | ||||
# | # | ||||
# The target use case is to use `share` to expose different subset of the same | # The target use case is to use `share` to expose different subset of the same | ||||
# repository, especially server side. See also `server.view`. | # repository, especially server side. See also `server.view`. | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'extra-filter-revs', default=None, | b'experimental', | ||||
b'extra-filter-revs', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'maxdeltachainspan', default=-1, | b'experimental', | ||||
b'maxdeltachainspan', | |||||
default=-1, | |||||
) | ) | ||||
# tracks files which were undeleted (merge might delete them but we explicitly | # tracks files which were undeleted (merge might delete them but we explicitly | ||||
# kept/undeleted them) and creates new filenodes for them | # kept/undeleted them) and creates new filenodes for them | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'merge-track-salvaged', default=False, | b'experimental', | ||||
b'merge-track-salvaged', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'mergetempdirprefix', default=None, | b'experimental', | ||||
b'mergetempdirprefix', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'mmapindexthreshold', default=None, | b'experimental', | ||||
b'mmapindexthreshold', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'narrow', default=False, | b'experimental', | ||||
b'narrow', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'nonnormalparanoidcheck', default=False, | b'experimental', | ||||
b'nonnormalparanoidcheck', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'exportableenviron', default=list, | b'experimental', | ||||
b'exportableenviron', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'extendedheader.index', default=None, | b'experimental', | ||||
b'extendedheader.index', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'extendedheader.similarity', default=False, | b'experimental', | ||||
b'extendedheader.similarity', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'graphshorten', default=False, | b'experimental', | ||||
b'graphshorten', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'graphstyle.parent', default=dynamicdefault, | b'experimental', | ||||
b'graphstyle.parent', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'graphstyle.missing', default=dynamicdefault, | b'experimental', | ||||
b'graphstyle.missing', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'graphstyle.grandparent', default=dynamicdefault, | b'experimental', | ||||
b'graphstyle.grandparent', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'hook-track-tags', default=False, | b'experimental', | ||||
b'hook-track-tags', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'httppeer.advertise-v2', default=False, | b'experimental', | ||||
b'httppeer.advertise-v2', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'httppeer.v2-encoder-order', default=None, | b'experimental', | ||||
b'httppeer.v2-encoder-order', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'httppostargs', default=False, | b'experimental', | ||||
b'httppostargs', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem(b'experimental', b'nointerrupt', default=False) | coreconfigitem(b'experimental', b'nointerrupt', default=False) | ||||
coreconfigitem(b'experimental', b'nointerrupt-interactiveonly', default=True) | coreconfigitem(b'experimental', b'nointerrupt-interactiveonly', default=True) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'obsmarkers-exchange-debug', default=False, | b'experimental', | ||||
b'obsmarkers-exchange-debug', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'remotenames', default=False, | b'experimental', | ||||
b'remotenames', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'removeemptydirs', default=True, | b'experimental', | ||||
b'removeemptydirs', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'revert.interactive.select-to-keep', default=False, | b'experimental', | ||||
b'revert.interactive.select-to-keep', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'revisions.prefixhexnode', default=False, | b'experimental', | ||||
b'revisions.prefixhexnode', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'revlogv2', default=None, | b'experimental', | ||||
b'revlogv2', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'revisions.disambiguatewithin', default=None, | b'experimental', | ||||
b'revisions.disambiguatewithin', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'rust.index', default=False, | b'experimental', | ||||
b'rust.index', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'server.filesdata.recommended-batch-size', default=50000, | b'experimental', | ||||
b'server.filesdata.recommended-batch-size', | |||||
default=50000, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', | b'experimental', | ||||
b'server.manifestdata.recommended-batch-size', | b'server.manifestdata.recommended-batch-size', | ||||
default=100000, | default=100000, | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'server.stream-narrow-clones', default=False, | b'experimental', | ||||
b'server.stream-narrow-clones', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'single-head-per-branch', default=False, | b'experimental', | ||||
b'single-head-per-branch', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', | b'experimental', | ||||
b'single-head-per-branch:account-closed-heads', | b'single-head-per-branch:account-closed-heads', | ||||
default=False, | default=False, | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'sshserver.support-v2', default=False, | b'experimental', | ||||
b'sshserver.support-v2', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'sparse-read', default=False, | b'experimental', | ||||
b'sparse-read', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'sparse-read.density-threshold', default=0.50, | b'experimental', | ||||
b'sparse-read.density-threshold', | |||||
default=0.50, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'sparse-read.min-gap-size', default=b'65K', | b'experimental', | ||||
b'sparse-read.min-gap-size', | |||||
default=b'65K', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'treemanifest', default=False, | b'experimental', | ||||
b'treemanifest', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'update.atomic-file', default=False, | b'experimental', | ||||
b'update.atomic-file', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'sshpeer.advertise-v2', default=False, | b'experimental', | ||||
b'sshpeer.advertise-v2', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'web.apiserver', default=False, | b'experimental', | ||||
b'web.apiserver', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'web.api.http-v2', default=False, | b'experimental', | ||||
b'web.api.http-v2', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'web.api.debugreflect', default=False, | b'experimental', | ||||
b'web.api.debugreflect', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'worker.wdir-get-thread-safe', default=False, | b'experimental', | ||||
b'worker.wdir-get-thread-safe', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'worker.repository-upgrade', default=False, | b'experimental', | ||||
b'worker.repository-upgrade', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'xdiff', default=False, | b'experimental', | ||||
b'xdiff', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'extensions', b'.*', default=None, generic=True, | b'extensions', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'extdata', b'.*', default=None, generic=True, | b'extdata', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'bookmarks-in-store', default=False, | b'format', | ||||
b'bookmarks-in-store', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'chunkcachesize', default=None, experimental=True, | b'format', | ||||
b'chunkcachesize', | |||||
default=None, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'dotencode', default=True, | b'format', | ||||
b'dotencode', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'generaldelta', default=False, experimental=True, | b'format', | ||||
b'generaldelta', | |||||
default=False, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'manifestcachesize', default=None, experimental=True, | b'format', | ||||
b'manifestcachesize', | |||||
default=None, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'maxchainlen', default=dynamicdefault, experimental=True, | b'format', | ||||
b'maxchainlen', | |||||
default=dynamicdefault, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'obsstore-version', default=None, | b'format', | ||||
b'obsstore-version', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'sparse-revlog', default=True, | b'format', | ||||
b'sparse-revlog', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', | b'format', | ||||
b'revlog-compression', | b'revlog-compression', | ||||
default=lambda: [b'zlib'], | default=lambda: [b'zlib'], | ||||
alias=[(b'experimental', b'format.compression')], | alias=[(b'experimental', b'format.compression')], | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'usefncache', default=True, | b'format', | ||||
b'usefncache', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'usegeneraldelta', default=True, | b'format', | ||||
b'usegeneraldelta', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'usestore', default=True, | b'format', | ||||
b'usestore', | |||||
default=True, | |||||
) | ) | ||||
# Right now, the only efficient implement of the nodemap logic is in Rust, so | # Right now, the only efficient implement of the nodemap logic is in Rust, so | ||||
# the persistent nodemap feature needs to stay experimental as long as the Rust | # the persistent nodemap feature needs to stay experimental as long as the Rust | ||||
# extensions are an experimental feature. | # extensions are an experimental feature. | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'use-persistent-nodemap', default=False, experimental=True | b'format', b'use-persistent-nodemap', default=False, experimental=True | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', | b'format', | ||||
b'exp-use-copies-side-data-changeset', | b'exp-use-copies-side-data-changeset', | ||||
default=False, | default=False, | ||||
experimental=True, | experimental=True, | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'exp-use-side-data', default=False, experimental=True, | b'format', | ||||
b'exp-use-side-data', | |||||
default=False, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'exp-share-safe', default=False, experimental=True, | b'format', | ||||
b'exp-share-safe', | |||||
default=False, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'format', b'internal-phase', default=False, experimental=True, | b'format', | ||||
b'internal-phase', | |||||
default=False, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'fsmonitor', b'warn_when_unused', default=True, | b'fsmonitor', | ||||
b'warn_when_unused', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'fsmonitor', b'warn_update_file_count', default=50000, | b'fsmonitor', | ||||
b'warn_update_file_count', | |||||
default=50000, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'fsmonitor', b'warn_update_file_count_rust', default=400000, | b'fsmonitor', | ||||
b'warn_update_file_count_rust', | |||||
default=400000, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'help', br'hidden-command\..*', default=False, generic=True, | b'help', | ||||
br'hidden-command\..*', | |||||
default=False, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'help', br'hidden-topic\..*', default=False, generic=True, | b'help', | ||||
br'hidden-topic\..*', | |||||
default=False, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'hooks', b'.*', default=dynamicdefault, generic=True, | b'hooks', | ||||
b'.*', | |||||
default=dynamicdefault, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'hgweb-paths', b'.*', default=list, generic=True, | b'hgweb-paths', | ||||
b'.*', | |||||
default=list, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'hostfingerprints', b'.*', default=list, generic=True, | b'hostfingerprints', | ||||
b'.*', | |||||
default=list, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'hostsecurity', b'ciphers', default=None, | b'hostsecurity', | ||||
b'ciphers', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'hostsecurity', b'minimumprotocol', default=dynamicdefault, | b'hostsecurity', | ||||
b'minimumprotocol', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'hostsecurity', | b'hostsecurity', | ||||
b'.*:minimumprotocol$', | b'.*:minimumprotocol$', | ||||
default=dynamicdefault, | default=dynamicdefault, | ||||
generic=True, | generic=True, | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'hostsecurity', b'.*:ciphers$', default=dynamicdefault, generic=True, | b'hostsecurity', | ||||
b'.*:ciphers$', | |||||
default=dynamicdefault, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'hostsecurity', b'.*:fingerprints$', default=list, generic=True, | b'hostsecurity', | ||||
b'.*:fingerprints$', | |||||
default=list, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'hostsecurity', b'.*:verifycertsfile$', default=None, generic=True, | b'hostsecurity', | ||||
b'.*:verifycertsfile$', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'http_proxy', b'always', default=False, | b'http_proxy', | ||||
b'always', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'http_proxy', b'host', default=None, | b'http_proxy', | ||||
b'host', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'http_proxy', b'no', default=list, | b'http_proxy', | ||||
b'no', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'http_proxy', b'passwd', default=None, | b'http_proxy', | ||||
b'passwd', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'http_proxy', b'user', default=None, | b'http_proxy', | ||||
b'user', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'http', b'timeout', default=None, | b'http', | ||||
b'timeout', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'logtoprocess', b'commandexception', default=None, | b'logtoprocess', | ||||
b'commandexception', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'logtoprocess', b'commandfinish', default=None, | b'logtoprocess', | ||||
b'commandfinish', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'logtoprocess', b'command', default=None, | b'logtoprocess', | ||||
b'command', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'logtoprocess', b'develwarn', default=None, | b'logtoprocess', | ||||
b'develwarn', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'logtoprocess', b'uiblocked', default=None, | b'logtoprocess', | ||||
b'uiblocked', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge', b'checkunknown', default=b'abort', | b'merge', | ||||
b'checkunknown', | |||||
default=b'abort', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge', b'checkignored', default=b'abort', | b'merge', | ||||
b'checkignored', | |||||
default=b'abort', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'merge.checkpathconflicts', default=False, | b'experimental', | ||||
b'merge.checkpathconflicts', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge', b'followcopies', default=True, | b'merge', | ||||
b'followcopies', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge', b'on-failure', default=b'continue', | b'merge', | ||||
b'on-failure', | |||||
default=b'continue', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge', b'preferancestor', default=lambda: [b'*'], experimental=True, | b'merge', | ||||
b'preferancestor', | |||||
default=lambda: [b'*'], | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge', b'strict-capability-check', default=False, | b'merge', | ||||
b'strict-capability-check', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', b'.*', default=None, generic=True, | b'merge-tools', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', | b'merge-tools', | ||||
br'.*\.args$', | br'.*\.args$', | ||||
default=b"$local $base $other", | default=b"$local $base $other", | ||||
generic=True, | generic=True, | ||||
priority=-1, | priority=-1, | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', br'.*\.binary$', default=False, generic=True, priority=-1, | b'merge-tools', | ||||
br'.*\.binary$', | |||||
default=False, | |||||
generic=True, | |||||
priority=-1, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', br'.*\.check$', default=list, generic=True, priority=-1, | b'merge-tools', | ||||
br'.*\.check$', | |||||
default=list, | |||||
generic=True, | |||||
priority=-1, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', | b'merge-tools', | ||||
br'.*\.checkchanged$', | br'.*\.checkchanged$', | ||||
default=False, | default=False, | ||||
generic=True, | generic=True, | ||||
priority=-1, | priority=-1, | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', | b'merge-tools', | ||||
br'.*\.executable$', | br'.*\.executable$', | ||||
default=dynamicdefault, | default=dynamicdefault, | ||||
generic=True, | generic=True, | ||||
priority=-1, | priority=-1, | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', br'.*\.fixeol$', default=False, generic=True, priority=-1, | b'merge-tools', | ||||
br'.*\.fixeol$', | |||||
default=False, | |||||
generic=True, | |||||
priority=-1, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', br'.*\.gui$', default=False, generic=True, priority=-1, | b'merge-tools', | ||||
br'.*\.gui$', | |||||
default=False, | |||||
generic=True, | |||||
priority=-1, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', | b'merge-tools', | ||||
br'.*\.mergemarkers$', | br'.*\.mergemarkers$', | ||||
default=b'basic', | default=b'basic', | ||||
generic=True, | generic=True, | ||||
priority=-1, | priority=-1, | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', | b'merge-tools', | ||||
br'.*\.mergemarkertemplate$', | br'.*\.mergemarkertemplate$', | ||||
default=dynamicdefault, # take from command-templates.mergemarker | default=dynamicdefault, # take from command-templates.mergemarker | ||||
generic=True, | generic=True, | ||||
priority=-1, | priority=-1, | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', br'.*\.priority$', default=0, generic=True, priority=-1, | b'merge-tools', | ||||
br'.*\.priority$', | |||||
default=0, | |||||
generic=True, | |||||
priority=-1, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', | b'merge-tools', | ||||
br'.*\.premerge$', | br'.*\.premerge$', | ||||
default=dynamicdefault, | default=dynamicdefault, | ||||
generic=True, | generic=True, | ||||
priority=-1, | priority=-1, | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'merge-tools', br'.*\.symlink$', default=False, generic=True, priority=-1, | b'merge-tools', | ||||
br'.*\.symlink$', | |||||
default=False, | |||||
generic=True, | |||||
priority=-1, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'pager', b'attend-.*', default=dynamicdefault, generic=True, | b'pager', | ||||
b'attend-.*', | |||||
default=dynamicdefault, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'pager', b'ignore', default=list, | b'pager', | ||||
b'ignore', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'pager', b'pager', default=dynamicdefault, | b'pager', | ||||
b'pager', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'patch', b'eol', default=b'strict', | b'patch', | ||||
b'eol', | |||||
default=b'strict', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'patch', b'fuzz', default=2, | b'patch', | ||||
b'fuzz', | |||||
default=2, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'paths', b'default', default=None, | b'paths', | ||||
b'default', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'paths', b'default-push', default=None, | b'paths', | ||||
b'default-push', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'paths', b'.*', default=None, generic=True, | b'paths', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'phases', b'checksubrepos', default=b'follow', | b'phases', | ||||
b'checksubrepos', | |||||
default=b'follow', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'phases', b'new-commit', default=b'draft', | b'phases', | ||||
b'new-commit', | |||||
default=b'draft', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'phases', b'publish', default=True, | b'phases', | ||||
b'publish', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'enabled', default=False, | b'profiling', | ||||
b'enabled', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'format', default=b'text', | b'profiling', | ||||
b'format', | |||||
default=b'text', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'freq', default=1000, | b'profiling', | ||||
b'freq', | |||||
default=1000, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'limit', default=30, | b'profiling', | ||||
b'limit', | |||||
default=30, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'nested', default=0, | b'profiling', | ||||
b'nested', | |||||
default=0, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'output', default=None, | b'profiling', | ||||
b'output', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'showmax', default=0.999, | b'profiling', | ||||
b'showmax', | |||||
default=0.999, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'showmin', default=dynamicdefault, | b'profiling', | ||||
b'showmin', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'showtime', default=True, | b'profiling', | ||||
b'showtime', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'sort', default=b'inlinetime', | b'profiling', | ||||
b'sort', | |||||
default=b'inlinetime', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'statformat', default=b'hotpath', | b'profiling', | ||||
b'statformat', | |||||
default=b'hotpath', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'time-track', default=dynamicdefault, | b'profiling', | ||||
b'time-track', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'profiling', b'type', default=b'stat', | b'profiling', | ||||
b'type', | |||||
default=b'stat', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'progress', b'assume-tty', default=False, | b'progress', | ||||
b'assume-tty', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'progress', b'changedelay', default=1, | b'progress', | ||||
b'changedelay', | |||||
default=1, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'progress', b'clear-complete', default=True, | b'progress', | ||||
b'clear-complete', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'progress', b'debug', default=False, | b'progress', | ||||
b'debug', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'progress', b'delay', default=3, | b'progress', | ||||
b'delay', | |||||
default=3, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'progress', b'disable', default=False, | b'progress', | ||||
b'disable', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'progress', b'estimateinterval', default=60.0, | b'progress', | ||||
b'estimateinterval', | |||||
default=60.0, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'progress', | b'progress', | ||||
b'format', | b'format', | ||||
default=lambda: [b'topic', b'bar', b'number', b'estimate'], | default=lambda: [b'topic', b'bar', b'number', b'estimate'], | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'progress', b'refresh', default=0.1, | b'progress', | ||||
b'refresh', | |||||
default=0.1, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'progress', b'width', default=dynamicdefault, | b'progress', | ||||
b'width', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'pull', b'confirm', default=False, | b'pull', | ||||
b'confirm', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'push', b'pushvars.server', default=False, | b'push', | ||||
b'pushvars.server', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'rewrite', | b'rewrite', | ||||
b'backup-bundle', | b'backup-bundle', | ||||
default=True, | default=True, | ||||
alias=[(b'ui', b'history-editing-backup')], | alias=[(b'ui', b'history-editing-backup')], | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'rewrite', b'update-timestamp', default=False, | b'rewrite', | ||||
b'update-timestamp', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'rewrite', b'empty-successor', default=b'skip', experimental=True, | b'rewrite', | ||||
b'empty-successor', | |||||
default=b'skip', | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'storage', b'new-repo-backend', default=b'revlogv1', experimental=True, | b'storage', | ||||
b'new-repo-backend', | |||||
default=b'revlogv1', | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'storage', | b'storage', | ||||
b'revlog.optimize-delta-parent-choice', | b'revlog.optimize-delta-parent-choice', | ||||
default=True, | default=True, | ||||
alias=[(b'format', b'aggressivemergedeltas')], | alias=[(b'format', b'aggressivemergedeltas')], | ||||
) | ) | ||||
# experimental as long as rust is experimental (or a C version is implemented) | # experimental as long as rust is experimental (or a C version is implemented) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'storage', b'revlog.nodemap.mmap', default=True, experimental=True | b'storage', b'revlog.nodemap.mmap', default=True, experimental=True | ||||
) | ) | ||||
# experimental as long as format.use-persistent-nodemap is. | # experimental as long as format.use-persistent-nodemap is. | ||||
coreconfigitem( | coreconfigitem( | ||||
b'storage', b'revlog.nodemap.mode', default=b'compat', experimental=True | b'storage', b'revlog.nodemap.mode', default=b'compat', experimental=True | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'storage', b'revlog.reuse-external-delta', default=True, | b'storage', | ||||
b'revlog.reuse-external-delta', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'storage', b'revlog.reuse-external-delta-parent', default=None, | b'storage', | ||||
b'revlog.reuse-external-delta-parent', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'storage', b'revlog.zlib.level', default=None, | b'storage', | ||||
b'revlog.zlib.level', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'storage', b'revlog.zstd.level', default=None, | b'storage', | ||||
b'revlog.zstd.level', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'bookmarks-pushkey-compat', default=True, | b'server', | ||||
b'bookmarks-pushkey-compat', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'bundle1', default=True, | b'server', | ||||
b'bundle1', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'bundle1gd', default=None, | b'server', | ||||
b'bundle1gd', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'bundle1.pull', default=None, | b'server', | ||||
b'bundle1.pull', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'bundle1gd.pull', default=None, | b'server', | ||||
b'bundle1gd.pull', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'bundle1.push', default=None, | b'server', | ||||
b'bundle1.push', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'bundle1gd.push', default=None, | b'server', | ||||
b'bundle1gd.push', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', | b'server', | ||||
b'bundle2.stream', | b'bundle2.stream', | ||||
default=True, | default=True, | ||||
alias=[(b'experimental', b'bundle2.stream')], | alias=[(b'experimental', b'bundle2.stream')], | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'compressionengines', default=list, | b'server', | ||||
b'compressionengines', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'concurrent-push-mode', default=b'check-related', | b'server', | ||||
b'concurrent-push-mode', | |||||
default=b'check-related', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'disablefullbundle', default=False, | b'server', | ||||
b'disablefullbundle', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'maxhttpheaderlen', default=1024, | b'server', | ||||
b'maxhttpheaderlen', | |||||
default=1024, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'pullbundle', default=False, | b'server', | ||||
b'pullbundle', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'preferuncompressed', default=False, | b'server', | ||||
b'preferuncompressed', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'streamunbundle', default=False, | b'server', | ||||
b'streamunbundle', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'uncompressed', default=True, | b'server', | ||||
b'uncompressed', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'uncompressedallowsecret', default=False, | b'server', | ||||
b'uncompressedallowsecret', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'view', default=b'served', | b'server', | ||||
b'view', | |||||
default=b'served', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'validate', default=False, | b'server', | ||||
b'validate', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'zliblevel', default=-1, | b'server', | ||||
b'zliblevel', | |||||
default=-1, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'server', b'zstdlevel', default=3, | b'server', | ||||
b'zstdlevel', | |||||
default=3, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'share', b'pool', default=None, | b'share', | ||||
b'pool', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'share', b'poolnaming', default=b'identity', | b'share', | ||||
b'poolnaming', | |||||
default=b'identity', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'shelve', b'maxbackups', default=10, | b'shelve', | ||||
b'maxbackups', | |||||
default=10, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'smtp', b'host', default=None, | b'smtp', | ||||
b'host', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'smtp', b'local_hostname', default=None, | b'smtp', | ||||
b'local_hostname', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'smtp', b'password', default=None, | b'smtp', | ||||
b'password', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'smtp', b'port', default=dynamicdefault, | b'smtp', | ||||
b'port', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'smtp', b'tls', default=b'none', | b'smtp', | ||||
b'tls', | |||||
default=b'none', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'smtp', b'username', default=None, | b'smtp', | ||||
b'username', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'sparse', b'missingwarning', default=True, experimental=True, | b'sparse', | ||||
b'missingwarning', | |||||
default=True, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'subrepos', | b'subrepos', | ||||
b'allowed', | b'allowed', | ||||
default=dynamicdefault, # to make backporting simpler | default=dynamicdefault, # to make backporting simpler | ||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'subrepos', b'hg:allowed', default=dynamicdefault, | b'subrepos', | ||||
b'hg:allowed', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'subrepos', b'git:allowed', default=dynamicdefault, | b'subrepos', | ||||
b'git:allowed', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'subrepos', b'svn:allowed', default=dynamicdefault, | b'subrepos', | ||||
b'svn:allowed', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'templates', b'.*', default=None, generic=True, | b'templates', | ||||
b'.*', | |||||
default=None, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'templateconfig', b'.*', default=dynamicdefault, generic=True, | b'templateconfig', | ||||
b'.*', | |||||
default=dynamicdefault, | |||||
generic=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'trusted', b'groups', default=list, | b'trusted', | ||||
b'groups', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'trusted', b'users', default=list, | b'trusted', | ||||
b'users', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'_usedassubrepo', default=False, | b'ui', | ||||
b'_usedassubrepo', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'allowemptycommit', default=False, | b'ui', | ||||
b'allowemptycommit', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'archivemeta', default=True, | b'ui', | ||||
b'archivemeta', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'askusername', default=False, | b'ui', | ||||
b'askusername', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'available-memory', default=None, | b'ui', | ||||
b'available-memory', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'clonebundlefallback', default=False, | b'ui', | ||||
b'clonebundlefallback', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'clonebundleprefers', default=list, | b'ui', | ||||
b'clonebundleprefers', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'clonebundles', default=True, | b'ui', | ||||
b'clonebundles', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'color', default=b'auto', | b'ui', | ||||
b'color', | |||||
default=b'auto', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'commitsubrepos', default=False, | b'ui', | ||||
b'commitsubrepos', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'debug', default=False, | b'ui', | ||||
b'debug', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'debugger', default=None, | b'ui', | ||||
b'debugger', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'editor', default=dynamicdefault, | b'ui', | ||||
b'editor', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'detailed-exit-code', default=False, experimental=True, | b'ui', | ||||
b'detailed-exit-code', | |||||
default=False, | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'fallbackencoding', default=None, | b'ui', | ||||
b'fallbackencoding', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'forcecwd', default=None, | b'ui', | ||||
b'forcecwd', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'forcemerge', default=None, | b'ui', | ||||
b'forcemerge', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'formatdebug', default=False, | b'ui', | ||||
b'formatdebug', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'formatjson', default=False, | b'ui', | ||||
b'formatjson', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'formatted', default=None, | b'ui', | ||||
b'formatted', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'interactive', default=None, | b'ui', | ||||
b'interactive', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'interface', default=None, | b'ui', | ||||
b'interface', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'interface.chunkselector', default=None, | b'ui', | ||||
b'interface.chunkselector', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'large-file-limit', default=10000000, | b'ui', | ||||
b'large-file-limit', | |||||
default=10000000, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'logblockedtimes', default=False, | b'ui', | ||||
b'logblockedtimes', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'merge', default=None, | b'ui', | ||||
b'merge', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'mergemarkers', default=b'basic', | b'ui', | ||||
b'mergemarkers', | |||||
default=b'basic', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'message-output', default=b'stdio', | b'ui', | ||||
b'message-output', | |||||
default=b'stdio', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'nontty', default=False, | b'ui', | ||||
b'nontty', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'origbackuppath', default=None, | b'ui', | ||||
b'origbackuppath', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'paginate', default=True, | b'ui', | ||||
b'paginate', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'patch', default=None, | b'ui', | ||||
b'patch', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'portablefilenames', default=b'warn', | b'ui', | ||||
b'portablefilenames', | |||||
default=b'warn', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'promptecho', default=False, | b'ui', | ||||
b'promptecho', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'quiet', default=False, | b'ui', | ||||
b'quiet', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'quietbookmarkmove', default=False, | b'ui', | ||||
b'quietbookmarkmove', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'relative-paths', default=b'legacy', | b'ui', | ||||
b'relative-paths', | |||||
default=b'legacy', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'remotecmd', default=b'hg', | b'ui', | ||||
b'remotecmd', | |||||
default=b'hg', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'report_untrusted', default=True, | b'ui', | ||||
b'report_untrusted', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'rollback', default=True, | b'ui', | ||||
b'rollback', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'signal-safe-lock', default=True, | b'ui', | ||||
b'signal-safe-lock', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'slash', default=False, | b'ui', | ||||
b'slash', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'ssh', default=b'ssh', | b'ui', | ||||
b'ssh', | |||||
default=b'ssh', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'ssherrorhint', default=None, | b'ui', | ||||
b'ssherrorhint', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'statuscopies', default=False, | b'ui', | ||||
b'statuscopies', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'strict', default=False, | b'ui', | ||||
b'strict', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'style', default=b'', | b'ui', | ||||
b'style', | |||||
default=b'', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'supportcontact', default=None, | b'ui', | ||||
b'supportcontact', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'textwidth', default=78, | b'ui', | ||||
b'textwidth', | |||||
default=78, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'timeout', default=b'600', | b'ui', | ||||
b'timeout', | |||||
default=b'600', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'timeout.warn', default=0, | b'ui', | ||||
b'timeout.warn', | |||||
default=0, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'timestamp-output', default=False, | b'ui', | ||||
b'timestamp-output', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'traceback', default=False, | b'ui', | ||||
b'traceback', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'tweakdefaults', default=False, | b'ui', | ||||
b'tweakdefaults', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem(b'ui', b'username', alias=[(b'ui', b'user')]) | coreconfigitem(b'ui', b'username', alias=[(b'ui', b'user')]) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'ui', b'verbose', default=False, | b'ui', | ||||
b'verbose', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'verify', b'skipflags', default=None, | b'verify', | ||||
b'skipflags', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'allowbz2', default=False, | b'web', | ||||
b'allowbz2', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'allowgz', default=False, | b'web', | ||||
b'allowgz', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'allow-pull', alias=[(b'web', b'allowpull')], default=True, | b'web', | ||||
b'allow-pull', | |||||
alias=[(b'web', b'allowpull')], | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'allow-push', alias=[(b'web', b'allow_push')], default=list, | b'web', | ||||
b'allow-push', | |||||
alias=[(b'web', b'allow_push')], | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'allowzip', default=False, | b'web', | ||||
b'allowzip', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'archivesubrepos', default=False, | b'web', | ||||
b'archivesubrepos', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'cache', default=True, | b'web', | ||||
b'cache', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'comparisoncontext', default=5, | b'web', | ||||
b'comparisoncontext', | |||||
default=5, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'contact', default=None, | b'web', | ||||
b'contact', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'deny_push', default=list, | b'web', | ||||
b'deny_push', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'guessmime', default=False, | b'web', | ||||
b'guessmime', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'hidden', default=False, | b'web', | ||||
b'hidden', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'labels', default=list, | b'web', | ||||
b'labels', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'logoimg', default=b'hglogo.png', | b'web', | ||||
b'logoimg', | |||||
default=b'hglogo.png', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'logourl', default=b'https://mercurial-scm.org/', | b'web', | ||||
b'logourl', | |||||
default=b'https://mercurial-scm.org/', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'accesslog', default=b'-', | b'web', | ||||
b'accesslog', | |||||
default=b'-', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'address', default=b'', | b'web', | ||||
b'address', | |||||
default=b'', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'allow-archive', alias=[(b'web', b'allow_archive')], default=list, | b'web', | ||||
b'allow-archive', | |||||
alias=[(b'web', b'allow_archive')], | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'allow_read', default=list, | b'web', | ||||
b'allow_read', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'baseurl', default=None, | b'web', | ||||
b'baseurl', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'cacerts', default=None, | b'web', | ||||
b'cacerts', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'certificate', default=None, | b'web', | ||||
b'certificate', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'collapse', default=False, | b'web', | ||||
b'collapse', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'csp', default=None, | b'web', | ||||
b'csp', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'deny_read', default=list, | b'web', | ||||
b'deny_read', | |||||
default=list, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'descend', default=True, | b'web', | ||||
b'descend', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'description', default=b"", | b'web', | ||||
b'description', | |||||
default=b"", | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'encoding', default=lambda: encoding.encoding, | b'web', | ||||
b'encoding', | |||||
default=lambda: encoding.encoding, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'errorlog', default=b'-', | b'web', | ||||
b'errorlog', | |||||
default=b'-', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'ipv6', default=False, | b'web', | ||||
b'ipv6', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'maxchanges', default=10, | b'web', | ||||
b'maxchanges', | |||||
default=10, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'maxfiles', default=10, | b'web', | ||||
b'maxfiles', | |||||
default=10, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'maxshortchanges', default=60, | b'web', | ||||
b'maxshortchanges', | |||||
default=60, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'motd', default=b'', | b'web', | ||||
b'motd', | |||||
default=b'', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'name', default=dynamicdefault, | b'web', | ||||
b'name', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'port', default=8000, | b'web', | ||||
b'port', | |||||
default=8000, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'prefix', default=b'', | b'web', | ||||
b'prefix', | |||||
default=b'', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'push_ssl', default=True, | b'web', | ||||
b'push_ssl', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'refreshinterval', default=20, | b'web', | ||||
b'refreshinterval', | |||||
default=20, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'server-header', default=None, | b'web', | ||||
b'server-header', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'static', default=None, | b'web', | ||||
b'static', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'staticurl', default=None, | b'web', | ||||
b'staticurl', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'stripes', default=1, | b'web', | ||||
b'stripes', | |||||
default=1, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'style', default=b'paper', | b'web', | ||||
b'style', | |||||
default=b'paper', | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'templates', default=None, | b'web', | ||||
b'templates', | |||||
default=None, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'web', b'view', default=b'served', experimental=True, | b'web', | ||||
b'view', | |||||
default=b'served', | |||||
experimental=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'worker', b'backgroundclose', default=dynamicdefault, | b'worker', | ||||
b'backgroundclose', | |||||
default=dynamicdefault, | |||||
) | ) | ||||
# Windows defaults to a limit of 512 open files. A buffer of 128 | # Windows defaults to a limit of 512 open files. A buffer of 128 | ||||
# should give us enough headway. | # should give us enough headway. | ||||
coreconfigitem( | coreconfigitem( | ||||
b'worker', b'backgroundclosemaxqueue', default=384, | b'worker', | ||||
b'backgroundclosemaxqueue', | |||||
default=384, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'worker', b'backgroundcloseminfilecount', default=2048, | b'worker', | ||||
b'backgroundcloseminfilecount', | |||||
default=2048, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'worker', b'backgroundclosethreadcount', default=4, | b'worker', | ||||
b'backgroundclosethreadcount', | |||||
default=4, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'worker', b'enabled', default=True, | b'worker', | ||||
b'enabled', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'worker', b'numcpus', default=None, | b'worker', | ||||
b'numcpus', | |||||
default=None, | |||||
) | ) | ||||
# Rebase related configuration moved to core because other extension are doing | # Rebase related configuration moved to core because other extension are doing | ||||
# strange things. For example, shelve import the extensions to reuse some bit | # strange things. For example, shelve import the extensions to reuse some bit | ||||
# without formally loading it. | # without formally loading it. | ||||
coreconfigitem( | coreconfigitem( | ||||
b'commands', b'rebase.requiredest', default=False, | b'commands', | ||||
b'rebase.requiredest', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'experimental', b'rebaseskipobsolete', default=True, | b'experimental', | ||||
b'rebaseskipobsolete', | |||||
default=True, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'rebase', b'singletransaction', default=False, | b'rebase', | ||||
b'singletransaction', | |||||
default=False, | |||||
) | ) | ||||
coreconfigitem( | coreconfigitem( | ||||
b'rebase', b'experimental.inmemory', default=False, | b'rebase', | ||||
b'experimental.inmemory', | |||||
default=False, | |||||
) | ) |
def sub(self, path, allowcreate=True): | def sub(self, path, allowcreate=True): | ||||
'''return a subrepo for the stored revision of path, never wdir()''' | '''return a subrepo for the stored revision of path, never wdir()''' | ||||
return subrepo.subrepo(self, path, allowcreate=allowcreate) | return subrepo.subrepo(self, path, allowcreate=allowcreate) | ||||
def nullsub(self, path, pctx): | def nullsub(self, path, pctx): | ||||
return subrepo.nullsubrepo(self, path, pctx) | return subrepo.nullsubrepo(self, path, pctx) | ||||
def workingsub(self, path): | def workingsub(self, path): | ||||
'''return a subrepo for the stored revision, or wdir if this is a wdir | """return a subrepo for the stored revision, or wdir if this is a wdir | ||||
context. | context. | ||||
''' | """ | ||||
return subrepo.subrepo(self, path, allowwdir=True) | return subrepo.subrepo(self, path, allowwdir=True) | ||||
def match( | def match( | ||||
self, | self, | ||||
pats=None, | pats=None, | ||||
include=None, | include=None, | ||||
exclude=None, | exclude=None, | ||||
default=b'glob', | default=b'glob', | ||||
return a | return a | ||||
# In theory, we should never get out of that loop without a result. | # In theory, we should never get out of that loop without a result. | ||||
# But if manifest uses a buggy file revision (not children of the | # But if manifest uses a buggy file revision (not children of the | ||||
# one it replaces) we could. Such a buggy situation will likely | # one it replaces) we could. Such a buggy situation will likely | ||||
# result is crash somewhere else at to some point. | # result is crash somewhere else at to some point. | ||||
return lkr | return lkr | ||||
def isintroducedafter(self, changelogrev): | def isintroducedafter(self, changelogrev): | ||||
"""True if a filectx has been introduced after a given floor revision | """True if a filectx has been introduced after a given floor revision""" | ||||
""" | |||||
if self.linkrev() >= changelogrev: | if self.linkrev() >= changelogrev: | ||||
return True | return True | ||||
introrev = self._introrev(stoprev=changelogrev) | introrev = self._introrev(stoprev=changelogrev) | ||||
if introrev is None: | if introrev is None: | ||||
return False | return False | ||||
return introrev >= changelogrev | return introrev >= changelogrev | ||||
def introrev(self): | def introrev(self): | ||||
This is often equivalent to how the data would be expressed on disk. | This is often equivalent to how the data would be expressed on disk. | ||||
""" | """ | ||||
return self._repo.wwritedata(self.path(), self.data()) | return self._repo.wwritedata(self.path(), self.data()) | ||||
class filectx(basefilectx): | class filectx(basefilectx): | ||||
"""A filecontext object makes access to data related to a particular | """A filecontext object makes access to data related to a particular | ||||
filerevision convenient.""" | filerevision convenient.""" | ||||
def __init__( | def __init__( | ||||
self, | self, | ||||
repo, | repo, | ||||
path, | path, | ||||
changeid=None, | changeid=None, | ||||
fileid=None, | fileid=None, | ||||
filelog=None, | filelog=None, | ||||
changectx=None, | changectx=None, | ||||
): | ): | ||||
"""changeid must be a revision number, if specified. | """changeid must be a revision number, if specified. | ||||
fileid can be a file revision or node.""" | fileid can be a file revision or node.""" | ||||
self._repo = repo | self._repo = repo | ||||
self._path = path | self._path = path | ||||
assert ( | assert ( | ||||
changeid is not None or fileid is not None or changectx is not None | changeid is not None or fileid is not None or changectx is not None | ||||
), ( | ), b"bad args: changeid=%r, fileid=%r, changectx=%r" % ( | ||||
b"bad args: changeid=%r, fileid=%r, changectx=%r" | changeid, | ||||
% (changeid, fileid, changectx,) | fileid, | ||||
changectx, | |||||
) | ) | ||||
if filelog is not None: | if filelog is not None: | ||||
self._filelog = filelog | self._filelog = filelog | ||||
if changeid is not None: | if changeid is not None: | ||||
self._changeid = changeid | self._changeid = changeid | ||||
if changectx is not None: | if changectx is not None: | ||||
# behavior" is seen as better as "crash" | # behavior" is seen as better as "crash" | ||||
# | # | ||||
# Linkrevs have several serious troubles with filtering that are | # Linkrevs have several serious troubles with filtering that are | ||||
# complicated to solve. Proper handling of the issue here should be | # complicated to solve. Proper handling of the issue here should be | ||||
# considered when solving linkrev issue are on the table. | # considered when solving linkrev issue are on the table. | ||||
return self._repo.unfiltered()[self._changeid] | return self._repo.unfiltered()[self._changeid] | ||||
def filectx(self, fileid, changeid=None): | def filectx(self, fileid, changeid=None): | ||||
'''opens an arbitrary revision of the file without | """opens an arbitrary revision of the file without | ||||
opening a new filelog''' | opening a new filelog""" | ||||
return filectx( | return filectx( | ||||
self._repo, | self._repo, | ||||
self._path, | self._path, | ||||
fileid=fileid, | fileid=fileid, | ||||
filelog=self._filelog, | filelog=self._filelog, | ||||
changeid=changeid, | changeid=changeid, | ||||
) | ) | ||||
] | ] | ||||
def children(self): | def children(self): | ||||
return [] | return [] | ||||
class workingfilectx(committablefilectx): | class workingfilectx(committablefilectx): | ||||
"""A workingfilectx object makes access to data related to a particular | """A workingfilectx object makes access to data related to a particular | ||||
file in the working directory convenient.""" | file in the working directory convenient.""" | ||||
def __init__(self, repo, path, filelog=None, workingctx=None): | def __init__(self, repo, path, filelog=None, workingctx=None): | ||||
super(workingfilectx, self).__init__(repo, path, filelog, workingctx) | super(workingfilectx, self).__init__(repo, path, filelog, workingctx) | ||||
@propertycache | @propertycache | ||||
def _changectx(self): | def _changectx(self): | ||||
return workingctx(self._repo) | return workingctx(self._repo) | ||||
[], | [], | ||||
[], | [], | ||||
[], | [], | ||||
clean, | clean, | ||||
) | ) | ||||
@propertycache | @propertycache | ||||
def _changedset(self): | def _changedset(self): | ||||
"""Return the set of files changed in this context | """Return the set of files changed in this context""" | ||||
""" | |||||
changed = set(self._status.modified) | changed = set(self._status.modified) | ||||
changed.update(self._status.added) | changed.update(self._status.added) | ||||
changed.update(self._status.removed) | changed.update(self._status.removed) | ||||
return changed | return changed | ||||
def makecachingfilectxfn(func): | def makecachingfilectxfn(func): | ||||
"""Create a filectxfn that caches based on the path. | """Create a filectxfn that caches based on the path. | ||||
for f in self._status.removed: | for f in self._status.removed: | ||||
if f in man: | if f in man: | ||||
del man[f] | del man[f] | ||||
return man | return man | ||||
@propertycache | @propertycache | ||||
def _status(self): | def _status(self): | ||||
"""Calculate exact status from ``files`` specified at construction | """Calculate exact status from ``files`` specified at construction""" | ||||
""" | |||||
man1 = self.p1().manifest() | man1 = self.p1().manifest() | ||||
p2 = self._parents[1] | p2 = self._parents[1] | ||||
# "1 < len(self._parents)" can't be used for checking | # "1 < len(self._parents)" can't be used for checking | ||||
# existence of the 2nd parent, because "memctx._parents" is | # existence of the 2nd parent, because "memctx._parents" is | ||||
# explicitly initialized by the list, of which length is 2. | # explicitly initialized by the list, of which length is 2. | ||||
if p2.node() != nullid: | if p2.node() != nullid: | ||||
man2 = p2.manifest() | man2 = p2.manifest() | ||||
managing = lambda f: f in man1 or f in man2 | managing = lambda f: f in man1 or f in man2 |
if _isfullcopytraceable(repo, c1, base): | if _isfullcopytraceable(repo, c1, base): | ||||
return _fullcopytracing(repo, c1, c2, base) | return _fullcopytracing(repo, c1, c2, base) | ||||
return _heuristicscopytracing(repo, c1, c2, base) | return _heuristicscopytracing(repo, c1, c2, base) | ||||
else: | else: | ||||
return _fullcopytracing(repo, c1, c2, base) | return _fullcopytracing(repo, c1, c2, base) | ||||
def _isfullcopytraceable(repo, c1, base): | def _isfullcopytraceable(repo, c1, base): | ||||
""" Checks that if base, source and destination are all no-public branches, | """Checks that if base, source and destination are all no-public branches, | ||||
if yes let's use the full copytrace algorithm for increased capabilities | if yes let's use the full copytrace algorithm for increased capabilities | ||||
since it will be fast enough. | since it will be fast enough. | ||||
`experimental.copytrace.sourcecommitlimit` can be used to set a limit for | `experimental.copytrace.sourcecommitlimit` can be used to set a limit for | ||||
number of changesets from c1 to base such that if number of changesets are | number of changesets from c1 to base such that if number of changesets are | ||||
more than the limit, full copytracing algorithm won't be used. | more than the limit, full copytracing algorithm won't be used. | ||||
""" | """ | ||||
if c1.rev() is None: | if c1.rev() is None: | ||||
self, copy=None, renamedelete=None, dirmove=None, movewithdir=None | self, copy=None, renamedelete=None, dirmove=None, movewithdir=None | ||||
): | ): | ||||
self.copy = {} if copy is None else copy | self.copy = {} if copy is None else copy | ||||
self.renamedelete = {} if renamedelete is None else renamedelete | self.renamedelete = {} if renamedelete is None else renamedelete | ||||
self.dirmove = {} if dirmove is None else dirmove | self.dirmove = {} if dirmove is None else dirmove | ||||
self.movewithdir = {} if movewithdir is None else movewithdir | self.movewithdir = {} if movewithdir is None else movewithdir | ||||
def __repr__(self): | def __repr__(self): | ||||
return ( | return '<branch_copies\n copy=%r\n renamedelete=%r\n dirmove=%r\n movewithdir=%r\n>' % ( | ||||
'<branch_copies\n copy=%r\n renamedelete=%r\n dirmove=%r\n movewithdir=%r\n>' | self.copy, | ||||
% (self.copy, self.renamedelete, self.dirmove, self.movewithdir,) | self.renamedelete, | ||||
self.dirmove, | |||||
self.movewithdir, | |||||
) | ) | ||||
def _fullcopytracing(repo, c1, c2, base): | def _fullcopytracing(repo, c1, c2, base): | ||||
""" The full copytracing algorithm which finds all the new files that were | """The full copytracing algorithm which finds all the new files that were | ||||
added from merge base up to the top commit and for each file it checks if | added from merge base up to the top commit and for each file it checks if | ||||
this file was copied from another file. | this file was copied from another file. | ||||
This is pretty slow when a lot of changesets are involved but will track all | This is pretty slow when a lot of changesets are involved but will track all | ||||
the copies. | the copies. | ||||
""" | """ | ||||
m1 = c1.manifest() | m1 = c1.manifest() | ||||
m2 = c2.manifest() | m2 = c2.manifest() | ||||
% (f, df) | % (f, df) | ||||
) | ) | ||||
break | break | ||||
return dirmove, movewithdir | return dirmove, movewithdir | ||||
def _heuristicscopytracing(repo, c1, c2, base): | def _heuristicscopytracing(repo, c1, c2, base): | ||||
""" Fast copytracing using filename heuristics | """Fast copytracing using filename heuristics | ||||
Assumes that moves or renames are of following two types: | Assumes that moves or renames are of following two types: | ||||
1) Inside a directory only (same directory name but different filenames) | 1) Inside a directory only (same directory name but different filenames) | ||||
2) Move from one directory to another | 2) Move from one directory to another | ||||
(same filenames but different directory names) | (same filenames but different directory names) | ||||
Works only when there are no merge commits in the "source branch". | Works only when there are no merge commits in the "source branch". |
""" | """ | ||||
for header in self.headerlist: | for header in self.headerlist: | ||||
for hunk in header.allchildren(): | for hunk in header.allchildren(): | ||||
for line in hunk.allchildren(): | for line in hunk.allchildren(): | ||||
self.toggleapply(line) | self.toggleapply(line) | ||||
def toggleallbetween(self): | def toggleallbetween(self): | ||||
"""toggle applied on or off for all items in range [lastapplied, | """toggle applied on or off for all items in range [lastapplied, | ||||
current]. """ | current].""" | ||||
if ( | if ( | ||||
not self.lastapplieditem | not self.lastapplieditem | ||||
or self.currentselecteditem == self.lastapplieditem | or self.currentselecteditem == self.lastapplieditem | ||||
): | ): | ||||
# Treat this like a normal 'x'/' ' | # Treat this like a normal 'x'/' ' | ||||
self.toggleapply() | self.toggleapply() | ||||
return | return | ||||
def _decoratelines(text, fctx): | def _decoratelines(text, fctx): | ||||
n = _countlines(text) | n = _countlines(text) | ||||
linenos = pycompat.rangelist(1, n + 1) | linenos = pycompat.rangelist(1, n + 1) | ||||
return _annotatedfile([fctx] * n, linenos, [False] * n, text) | return _annotatedfile([fctx] * n, linenos, [False] * n, text) | ||||
def _annotatepair(parents, childfctx, child, skipchild, diffopts): | def _annotatepair(parents, childfctx, child, skipchild, diffopts): | ||||
r''' | r""" | ||||
Given parent and child fctxes and annotate data for parents, for all lines | Given parent and child fctxes and annotate data for parents, for all lines | ||||
in either parent that match the child, annotate the child with the parent's | in either parent that match the child, annotate the child with the parent's | ||||
data. | data. | ||||
Additionally, if `skipchild` is True, replace all other lines with parent | Additionally, if `skipchild` is True, replace all other lines with parent | ||||
annotate data as well such that child is never blamed for any lines. | annotate data as well such that child is never blamed for any lines. | ||||
See test-annotate.py for unit tests. | See test-annotate.py for unit tests. | ||||
''' | """ | ||||
pblocks = [ | pblocks = [ | ||||
(parent, mdiff.allblocks(parent.text, child.text, opts=diffopts)) | (parent, mdiff.allblocks(parent.text, child.text, opts=diffopts)) | ||||
for parent in parents | for parent in parents | ||||
] | ] | ||||
if skipchild: | if skipchild: | ||||
# Need to iterate over the blocks twice -- make it a list | # Need to iterate over the blocks twice -- make it a list | ||||
pblocks = [(p, list(blocks)) for (p, blocks) in pblocks] | pblocks = [(p, list(blocks)) for (p, blocks) in pblocks] |
addspaces=True, | addspaces=True, | ||||
wraplabels=False, | wraplabels=False, | ||||
wrapannotations=False, | wrapannotations=False, | ||||
wrapcommands=False, | wrapcommands=False, | ||||
wrapnonlinear=False, | wrapnonlinear=False, | ||||
usedots=False, | usedots=False, | ||||
maxlinewidth=70, | maxlinewidth=70, | ||||
): | ): | ||||
'''generates lines of a textual representation for a dag event stream | """generates lines of a textual representation for a dag event stream | ||||
events should generate what parsedag() does, so: | events should generate what parsedag() does, so: | ||||
('n', (id, [parentids])) for node creation | ('n', (id, [parentids])) for node creation | ||||
('l', (id, labelname)) for labels on nodes | ('l', (id, labelname)) for labels on nodes | ||||
('a', text) for annotations | ('a', text) for annotations | ||||
('c', text) for commands | ('c', text) for commands | ||||
('C', text) for line commands ('!!') | ('C', text) for line commands ('!!') | ||||
>>> dagtext([]) | >>> dagtext([]) | ||||
'' | '' | ||||
Combining parsedag and dagtext: | Combining parsedag and dagtext: | ||||
>>> dagtext(parsedag(b'+1 :f +1 :p2 *f */p2')) | >>> dagtext(parsedag(b'+1 :f +1 :p2 *f */p2')) | ||||
'+1 :f +1 :p2 *f */p2' | '+1 :f +1 :p2 *f */p2' | ||||
''' | """ | ||||
return b"\n".join( | return b"\n".join( | ||||
dagtextlines( | dagtextlines( | ||||
dag, | dag, | ||||
addspaces, | addspaces, | ||||
wraplabels, | wraplabels, | ||||
wrapannotations, | wrapannotations, | ||||
wrapcommands, | wrapcommands, | ||||
wrapnonlinear, | wrapnonlinear, | ||||
usedots, | usedots, | ||||
maxlinewidth, | maxlinewidth, | ||||
) | ) | ||||
) | ) |
b"common heads: %s\n" % b" ".join(sorted(short(n) for n in common)) | b"common heads: %s\n" % b" ".join(sorted(short(n) for n in common)) | ||||
) | ) | ||||
_chunksize = 4 << 10 | _chunksize = 4 << 10 | ||||
@command( | @command( | ||||
b'debugdownload', [(b'o', b'output', b'', _(b'path')),], optionalrepo=True | b'debugdownload', | ||||
[ | |||||
(b'o', b'output', b'', _(b'path')), | |||||
], | |||||
optionalrepo=True, | |||||
) | ) | ||||
def debugdownload(ui, repo, url, output=None, **opts): | def debugdownload(ui, repo, url, output=None, **opts): | ||||
"""download a resource using Mercurial logic and config | """download a resource using Mercurial logic and config""" | ||||
""" | |||||
fh = urlmod.open(ui, url, output) | fh = urlmod.open(ui, url, output) | ||||
dest = ui | dest = ui | ||||
if output: | if output: | ||||
dest = open(output, b"wb", _chunksize) | dest = open(output, b"wb", _chunksize) | ||||
try: | try: | ||||
data = fh.read(_chunksize) | data = fh.read(_chunksize) | ||||
while data: | while data: | ||||
if not util.safehasattr(index, b'stats'): | if not util.safehasattr(index, b'stats'): | ||||
raise error.Abort(_(b'debugindexstats only works with native code')) | raise error.Abort(_(b'debugindexstats only works with native code')) | ||||
for k, v in sorted(index.stats().items()): | for k, v in sorted(index.stats().items()): | ||||
ui.write(b'%s: %d\n' % (k, v)) | ui.write(b'%s: %d\n' % (k, v)) | ||||
@command(b'debuginstall', [] + cmdutil.formatteropts, b'', norepo=True) | @command(b'debuginstall', [] + cmdutil.formatteropts, b'', norepo=True) | ||||
def debuginstall(ui, **opts): | def debuginstall(ui, **opts): | ||||
'''test Mercurial installation | """test Mercurial installation | ||||
Returns 0 on success. | Returns 0 on success. | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
problems = 0 | problems = 0 | ||||
fm = ui.formatter(b'debuginstall', opts) | fm = ui.formatter(b'debuginstall', opts) | ||||
fm.startitem() | fm.startitem() | ||||
# encoding might be unknown or wrong. don't translate these messages. | # encoding might be unknown or wrong. don't translate these messages. | ||||
b'', | b'', | ||||
b'metadata', | b'metadata', | ||||
False, | False, | ||||
_(b'display the on disk meta data for the nodemap'), | _(b'display the on disk meta data for the nodemap'), | ||||
), | ), | ||||
], | ], | ||||
) | ) | ||||
def debugnodemap(ui, repo, **opts): | def debugnodemap(ui, repo, **opts): | ||||
"""write and inspect on disk nodemap | """write and inspect on disk nodemap""" | ||||
""" | |||||
if opts['dump_new']: | if opts['dump_new']: | ||||
unfi = repo.unfiltered() | unfi = repo.unfiltered() | ||||
cl = unfi.changelog | cl = unfi.changelog | ||||
if util.safehasattr(cl.index, "nodemap_data_all"): | if util.safehasattr(cl.index, "nodemap_data_all"): | ||||
data = cl.index.nodemap_data_all() | data = cl.index.nodemap_data_all() | ||||
else: | else: | ||||
data = nodemap.persistent_data(cl.index) | data = nodemap.persistent_data(cl.index) | ||||
ui.write(data) | ui.write(data) | ||||
(b'f', b'full', None, _(b'complete an entire path')), | (b'f', b'full', None, _(b'complete an entire path')), | ||||
(b'n', b'normal', None, _(b'show only normal files')), | (b'n', b'normal', None, _(b'show only normal files')), | ||||
(b'a', b'added', None, _(b'show only added files')), | (b'a', b'added', None, _(b'show only added files')), | ||||
(b'r', b'removed', None, _(b'show only removed files')), | (b'r', b'removed', None, _(b'show only removed files')), | ||||
], | ], | ||||
_(b'FILESPEC...'), | _(b'FILESPEC...'), | ||||
) | ) | ||||
def debugpathcomplete(ui, repo, *specs, **opts): | def debugpathcomplete(ui, repo, *specs, **opts): | ||||
'''complete part or all of a tracked path | """complete part or all of a tracked path | ||||
This command supports shells that offer path name completion. It | This command supports shells that offer path name completion. It | ||||
currently completes only files already known to the dirstate. | currently completes only files already known to the dirstate. | ||||
Completion extends only to the next path segment unless | Completion extends only to the next path segment unless | ||||
--full is specified, in which case entire paths are used.''' | --full is specified, in which case entire paths are used.""" | ||||
def complete(path, acceptable): | def complete(path, acceptable): | ||||
dirstate = repo.dirstate | dirstate = repo.dirstate | ||||
spec = os.path.normpath(os.path.join(encoding.getcwd(), path)) | spec = os.path.normpath(os.path.join(encoding.getcwd(), path)) | ||||
rootdir = repo.root + pycompat.ossep | rootdir = repo.root + pycompat.ossep | ||||
if spec != repo.root and not spec.startswith(rootdir): | if spec != repo.root and not spec.startswith(rootdir): | ||||
return [], [] | return [], [] | ||||
if os.path.isdir(spec): | if os.path.isdir(spec): | ||||
finally: | finally: | ||||
if not ui.debugflag: | if not ui.debugflag: | ||||
ui.popbuffer() | ui.popbuffer() | ||||
ui.write(b'%s = %s\n' % (path, tool)) | ui.write(b'%s = %s\n' % (path, tool)) | ||||
@command(b'debugpushkey', [], _(b'REPO NAMESPACE [KEY OLD NEW]'), norepo=True) | @command(b'debugpushkey', [], _(b'REPO NAMESPACE [KEY OLD NEW]'), norepo=True) | ||||
def debugpushkey(ui, repopath, namespace, *keyinfo, **opts): | def debugpushkey(ui, repopath, namespace, *keyinfo, **opts): | ||||
'''access the pushkey key/value protocol | """access the pushkey key/value protocol | ||||
With two args, list the keys in the given namespace. | With two args, list the keys in the given namespace. | ||||
With five args, set a key to new if it currently is set to old. | With five args, set a key to new if it currently is set to old. | ||||
Reports success or failure. | Reports success or failure. | ||||
''' | """ | ||||
target = hg.peer(ui, {}, repopath) | target = hg.peer(ui, {}, repopath) | ||||
if keyinfo: | if keyinfo: | ||||
key, old, new = keyinfo | key, old, new = keyinfo | ||||
with target.commandexecutor() as e: | with target.commandexecutor() as e: | ||||
r = e.callcommand( | r = e.callcommand( | ||||
b'pushkey', | b'pushkey', | ||||
{ | { | ||||
for key, value in sidedata: | for key, value in sidedata: | ||||
ui.writenoi18n(b' entry-%04o size %d\n' % (key, len(value))) | ui.writenoi18n(b' entry-%04o size %d\n' % (key, len(value))) | ||||
if ui.verbose: | if ui.verbose: | ||||
ui.writenoi18n(b' %s\n' % stringutil.pprint(value)) | ui.writenoi18n(b' %s\n' % stringutil.pprint(value)) | ||||
@command(b'debugssl', [], b'[SOURCE]', optionalrepo=True) | @command(b'debugssl', [], b'[SOURCE]', optionalrepo=True) | ||||
def debugssl(ui, repo, source=None, **opts): | def debugssl(ui, repo, source=None, **opts): | ||||
'''test a secure connection to a server | """test a secure connection to a server | ||||
This builds the certificate chain for the server on Windows, installing the | This builds the certificate chain for the server on Windows, installing the | ||||
missing intermediates and trusted root via Windows Update if necessary. It | missing intermediates and trusted root via Windows Update if necessary. It | ||||
does nothing on other platforms. | does nothing on other platforms. | ||||
If SOURCE is omitted, the 'default' path will be used. If a URL is given, | If SOURCE is omitted, the 'default' path will be used. If a URL is given, | ||||
that server is used. See :hg:`help urls` for more information. | that server is used. See :hg:`help urls` for more information. | ||||
If the update succeeds, retry the original operation. Otherwise, the cause | If the update succeeds, retry the original operation. Otherwise, the cause | ||||
of the SSL error is likely another issue. | of the SSL error is likely another issue. | ||||
''' | """ | ||||
if not pycompat.iswindows: | if not pycompat.iswindows: | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'certificate chain building is only possible on Windows') | _(b'certificate chain building is only possible on Windows') | ||||
) | ) | ||||
if not source: | if not source: | ||||
if not repo: | if not repo: | ||||
raise error.Abort( | raise error.Abort( | ||||
ui.writenoi18n(b"* functions: %s\n" % b', '.join(sorted(funcs))) | ui.writenoi18n(b"* functions: %s\n" % b', '.join(sorted(funcs))) | ||||
for r in revs: | for r in revs: | ||||
displayer.show(repo[r], **pycompat.strkwargs(props)) | displayer.show(repo[r], **pycompat.strkwargs(props)) | ||||
displayer.close() | displayer.close() | ||||
@command( | @command( | ||||
b'debuguigetpass', | b'debuguigetpass', | ||||
[(b'p', b'prompt', b'', _(b'prompt text'), _(b'TEXT')),], | [ | ||||
(b'p', b'prompt', b'', _(b'prompt text'), _(b'TEXT')), | |||||
], | |||||
_(b'[-p TEXT]'), | _(b'[-p TEXT]'), | ||||
norepo=True, | norepo=True, | ||||
) | ) | ||||
def debuguigetpass(ui, prompt=b''): | def debuguigetpass(ui, prompt=b''): | ||||
"""show prompt to type password""" | """show prompt to type password""" | ||||
r = ui.getpass(prompt) | r = ui.getpass(prompt) | ||||
if r is not None: | if r is not None: | ||||
r = encoding.strtolocal(r) | r = encoding.strtolocal(r) | ||||
else: | else: | ||||
r = b"<default response>" | r = b"<default response>" | ||||
ui.writenoi18n(b'response: %s\n' % r) | ui.writenoi18n(b'response: %s\n' % r) | ||||
@command( | @command( | ||||
b'debuguiprompt', | b'debuguiprompt', | ||||
[(b'p', b'prompt', b'', _(b'prompt text'), _(b'TEXT')),], | [ | ||||
(b'p', b'prompt', b'', _(b'prompt text'), _(b'TEXT')), | |||||
], | |||||
_(b'[-p TEXT]'), | _(b'[-p TEXT]'), | ||||
norepo=True, | norepo=True, | ||||
) | ) | ||||
def debuguiprompt(ui, prompt=b''): | def debuguiprompt(ui, prompt=b''): | ||||
"""show plain prompt""" | """show plain prompt""" | ||||
r = ui.prompt(prompt) | r = ui.prompt(prompt) | ||||
ui.writenoi18n(b'response: %s\n' % r) | ui.writenoi18n(b'response: %s\n' % r) | ||||
} | } | ||||
# Turn pipes/sockets into observers so we can log I/O. | # Turn pipes/sockets into observers so we can log I/O. | ||||
if ui.verbose: | if ui.verbose: | ||||
openerargs.update( | openerargs.update( | ||||
{ | { | ||||
'loggingfh': ui, | 'loggingfh': ui, | ||||
'loggingname': b's', | 'loggingname': b's', | ||||
'loggingopts': {'logdata': True, 'logdataapis': False,}, | 'loggingopts': { | ||||
'logdata': True, | |||||
'logdataapis': False, | |||||
}, | |||||
} | } | ||||
) | ) | ||||
if ui.debugflag: | if ui.debugflag: | ||||
openerargs['loggingopts']['logdataapis'] = True | openerargs['loggingopts']['logdataapis'] = True | ||||
# Don't send default headers when in raw mode. This allows us to | # Don't send default headers when in raw mode. This allows us to | ||||
# bypass most of the behavior of our URL handling code so we can | # bypass most of the behavior of our URL handling code so we can |
opts=None, | opts=None, | ||||
untrusted=False, | untrusted=False, | ||||
section=b'diff', | section=b'diff', | ||||
git=False, | git=False, | ||||
whitespace=False, | whitespace=False, | ||||
formatchanging=False, | formatchanging=False, | ||||
configprefix=b'', | configprefix=b'', | ||||
): | ): | ||||
'''return diffopts with only opted-in features parsed | """return diffopts with only opted-in features parsed | ||||
Features: | Features: | ||||
- git: git-style diffs | - git: git-style diffs | ||||
- whitespace: whitespace options like ignoreblanklines and ignorews | - whitespace: whitespace options like ignoreblanklines and ignorews | ||||
- formatchanging: options that will likely break or cause correctness issues | - formatchanging: options that will likely break or cause correctness issues | ||||
with most diff parsers | with most diff parsers | ||||
''' | """ | ||||
def get(key, name=None, getter=ui.configbool, forceplain=None): | def get(key, name=None, getter=ui.configbool, forceplain=None): | ||||
if opts: | if opts: | ||||
v = opts.get(key) | v = opts.get(key) | ||||
# diffopts flags are either None-default (which is passed | # diffopts flags are either None-default (which is passed | ||||
# through unchanged, so we can identify unset values), or | # through unchanged, so we can identify unset values), or | ||||
# some other falsey default (eg --unified, which defaults | # some other falsey default (eg --unified, which defaults | ||||
# to an empty string). We only want to override the config | # to an empty string). We only want to override the config |
finally: | finally: | ||||
os.close(tmpfd) | os.close(tmpfd) | ||||
vfs.unlink(tmpname) | vfs.unlink(tmpname) | ||||
@interfaceutil.implementer(intdirstate.idirstate) | @interfaceutil.implementer(intdirstate.idirstate) | ||||
class dirstate(object): | class dirstate(object): | ||||
def __init__(self, opener, ui, root, validate, sparsematchfn): | def __init__(self, opener, ui, root, validate, sparsematchfn): | ||||
'''Create a new dirstate object. | """Create a new dirstate object. | ||||
opener is an open()-like callable that can be used to open the | opener is an open()-like callable that can be used to open the | ||||
dirstate file; root is the root of the directory tracked by | dirstate file; root is the root of the directory tracked by | ||||
the dirstate. | the dirstate. | ||||
''' | """ | ||||
self._opener = opener | self._opener = opener | ||||
self._validate = validate | self._validate = validate | ||||
self._root = root | self._root = root | ||||
self._sparsematchfn = sparsematchfn | self._sparsematchfn = sparsematchfn | ||||
# ntpath.join(root, '') of Python 2.7.9 does not add sep if root is | # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is | ||||
# UNC path pointing to root share (issue4557) | # UNC path pointing to root share (issue4557) | ||||
self._rootdir = pathutil.normasprefix(root) | self._rootdir = pathutil.normasprefix(root) | ||||
self._dirty = False | self._dirty = False | ||||
"""make sure the parents are loaded | """make sure the parents are loaded | ||||
Used to avoid a race condition. | Used to avoid a race condition. | ||||
""" | """ | ||||
self._pl | self._pl | ||||
@contextlib.contextmanager | @contextlib.contextmanager | ||||
def parentchange(self): | def parentchange(self): | ||||
'''Context manager for handling dirstate parents. | """Context manager for handling dirstate parents. | ||||
If an exception occurs in the scope of the context manager, | If an exception occurs in the scope of the context manager, | ||||
the incoherent dirstate won't be written when wlock is | the incoherent dirstate won't be written when wlock is | ||||
released. | released. | ||||
''' | """ | ||||
self._parentwriters += 1 | self._parentwriters += 1 | ||||
yield | yield | ||||
# Typically we want the "undo" step of a context manager in a | # Typically we want the "undo" step of a context manager in a | ||||
# finally block so it happens even when an exception | # finally block so it happens even when an exception | ||||
# occurs. In this case, however, we only want to decrement | # occurs. In this case, however, we only want to decrement | ||||
# parentwriters if the code in the with statement exits | # parentwriters if the code in the with statement exits | ||||
# normally, so we don't have a try/finally here on purpose. | # normally, so we don't have a try/finally here on purpose. | ||||
self._parentwriters -= 1 | self._parentwriters -= 1 | ||||
def pendingparentchange(self): | def pendingparentchange(self): | ||||
'''Returns true if the dirstate is in the middle of a set of changes | """Returns true if the dirstate is in the middle of a set of changes | ||||
that modify the dirstate parent. | that modify the dirstate parent. | ||||
''' | """ | ||||
return self._parentwriters > 0 | return self._parentwriters > 0 | ||||
@propertycache | @propertycache | ||||
def _map(self): | def _map(self): | ||||
"""Return the dirstate contents (see documentation for dirstatemap).""" | """Return the dirstate contents (see documentation for dirstatemap).""" | ||||
self._map = self._mapcls(self._ui, self._opener, self._root) | self._map = self._mapcls(self._ui, self._opener, self._root) | ||||
return self._map | return self._map | ||||
def _cwd(self): | def _cwd(self): | ||||
# internal config: ui.forcecwd | # internal config: ui.forcecwd | ||||
forcecwd = self._ui.config(b'ui', b'forcecwd') | forcecwd = self._ui.config(b'ui', b'forcecwd') | ||||
if forcecwd: | if forcecwd: | ||||
return forcecwd | return forcecwd | ||||
return encoding.getcwd() | return encoding.getcwd() | ||||
def getcwd(self): | def getcwd(self): | ||||
'''Return the path from which a canonical path is calculated. | """Return the path from which a canonical path is calculated. | ||||
This path should be used to resolve file patterns or to convert | This path should be used to resolve file patterns or to convert | ||||
canonical paths back to file paths for display. It shouldn't be | canonical paths back to file paths for display. It shouldn't be | ||||
used to get real file paths. Use vfs functions instead. | used to get real file paths. Use vfs functions instead. | ||||
''' | """ | ||||
cwd = self._cwd | cwd = self._cwd | ||||
if cwd == self._root: | if cwd == self._root: | ||||
return b'' | return b'' | ||||
# self._root ends with a path separator if self._root is '/' or 'C:\' | # self._root ends with a path separator if self._root is '/' or 'C:\' | ||||
rootsep = self._root | rootsep = self._root | ||||
if not util.endswithsep(rootsep): | if not util.endswithsep(rootsep): | ||||
rootsep += pycompat.ossep | rootsep += pycompat.ossep | ||||
if cwd.startswith(rootsep): | if cwd.startswith(rootsep): | ||||
return cwd[len(rootsep) :] | return cwd[len(rootsep) :] | ||||
else: | else: | ||||
# we're outside the repo. return an absolute path. | # we're outside the repo. return an absolute path. | ||||
return cwd | return cwd | ||||
def pathto(self, f, cwd=None): | def pathto(self, f, cwd=None): | ||||
if cwd is None: | if cwd is None: | ||||
cwd = self.getcwd() | cwd = self.getcwd() | ||||
path = util.pathto(self._root, cwd, f) | path = util.pathto(self._root, cwd, f) | ||||
if self._slash: | if self._slash: | ||||
return util.pconvert(path) | return util.pconvert(path) | ||||
return path | return path | ||||
def __getitem__(self, key): | def __getitem__(self, key): | ||||
'''Return the current state of key (a filename) in the dirstate. | """Return the current state of key (a filename) in the dirstate. | ||||
States are: | States are: | ||||
n normal | n normal | ||||
m needs merging | m needs merging | ||||
r marked for removal | r marked for removal | ||||
a marked for addition | a marked for addition | ||||
? not tracked | ? not tracked | ||||
''' | """ | ||||
return self._map.get(key, (b"?",))[0] | return self._map.get(key, (b"?",))[0] | ||||
def __contains__(self, key): | def __contains__(self, key): | ||||
return key in self._map | return key in self._map | ||||
def __iter__(self): | def __iter__(self): | ||||
return iter(sorted(self._map)) | return iter(sorted(self._map)) | ||||
ce = self._filecache[b'_branch'] | ce = self._filecache[b'_branch'] | ||||
if ce: | if ce: | ||||
ce.refresh() | ce.refresh() | ||||
except: # re-raises | except: # re-raises | ||||
f.discard() | f.discard() | ||||
raise | raise | ||||
def invalidate(self): | def invalidate(self): | ||||
'''Causes the next access to reread the dirstate. | """Causes the next access to reread the dirstate. | ||||
This is different from localrepo.invalidatedirstate() because it always | This is different from localrepo.invalidatedirstate() because it always | ||||
rereads the dirstate. Use localrepo.invalidatedirstate() if you want to | rereads the dirstate. Use localrepo.invalidatedirstate() if you want to | ||||
check whether the dirstate has changed before rereading it.''' | check whether the dirstate has changed before rereading it.""" | ||||
for a in ("_map", "_branch", "_ignore"): | for a in ("_map", "_branch", "_ignore"): | ||||
if a in self.__dict__: | if a in self.__dict__: | ||||
delattr(self, a) | delattr(self, a) | ||||
self._lastnormaltime = 0 | self._lastnormaltime = 0 | ||||
self._dirty = False | self._dirty = False | ||||
self._updatedfiles.clear() | self._updatedfiles.clear() | ||||
self._parentwriters = 0 | self._parentwriters = 0 | ||||
_(b'file %r in dirstate clashes with %r') | _(b'file %r in dirstate clashes with %r') | ||||
% (pycompat.bytestr(d), pycompat.bytestr(f)) | % (pycompat.bytestr(d), pycompat.bytestr(f)) | ||||
) | ) | ||||
self._dirty = True | self._dirty = True | ||||
self._updatedfiles.add(f) | self._updatedfiles.add(f) | ||||
self._map.addfile(f, oldstate, state, mode, size, mtime) | self._map.addfile(f, oldstate, state, mode, size, mtime) | ||||
def normal(self, f, parentfiledata=None): | def normal(self, f, parentfiledata=None): | ||||
'''Mark a file normal and clean. | """Mark a file normal and clean. | ||||
parentfiledata: (mode, size, mtime) of the clean file | parentfiledata: (mode, size, mtime) of the clean file | ||||
parentfiledata should be computed from memory (for mode, | parentfiledata should be computed from memory (for mode, | ||||
size), as or close as possible from the point where we | size), as or close as possible from the point where we | ||||
determined the file was clean, to limit the risk of the | determined the file was clean, to limit the risk of the | ||||
file having been changed by an external process between the | file having been changed by an external process between the | ||||
moment where the file was determined to be clean and now.''' | moment where the file was determined to be clean and now.""" | ||||
if parentfiledata: | if parentfiledata: | ||||
(mode, size, mtime) = parentfiledata | (mode, size, mtime) = parentfiledata | ||||
else: | else: | ||||
s = os.lstat(self._join(f)) | s = os.lstat(self._join(f)) | ||||
mode = s.st_mode | mode = s.st_mode | ||||
size = s.st_size | size = s.st_size | ||||
mtime = s[stat.ST_MTIME] | mtime = s[stat.ST_MTIME] | ||||
self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask) | self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask) | ||||
# store discovered result in dirfoldmap so that future | # store discovered result in dirfoldmap so that future | ||||
# normalizefile calls don't start matching directories | # normalizefile calls don't start matching directories | ||||
folded = self._discoverpath( | folded = self._discoverpath( | ||||
path, normed, ignoremissing, exists, self._map.dirfoldmap | path, normed, ignoremissing, exists, self._map.dirfoldmap | ||||
) | ) | ||||
return folded | return folded | ||||
def normalize(self, path, isknown=False, ignoremissing=False): | def normalize(self, path, isknown=False, ignoremissing=False): | ||||
''' | """ | ||||
normalize the case of a pathname when on a casefolding filesystem | normalize the case of a pathname when on a casefolding filesystem | ||||
isknown specifies whether the filename came from walking the | isknown specifies whether the filename came from walking the | ||||
disk, to avoid extra filesystem access. | disk, to avoid extra filesystem access. | ||||
If ignoremissing is True, missing path are returned | If ignoremissing is True, missing path are returned | ||||
unchanged. Otherwise, we try harder to normalize possibly | unchanged. Otherwise, we try harder to normalize possibly | ||||
existing path components. | existing path components. | ||||
The normalized case is determined based on the following precedence: | The normalized case is determined based on the following precedence: | ||||
- version of name already stored in the dirstate | - version of name already stored in the dirstate | ||||
- version of name stored on disk | - version of name stored on disk | ||||
- version provided via command arguments | - version provided via command arguments | ||||
''' | """ | ||||
if self._checkcase: | if self._checkcase: | ||||
return self._normalize(path, isknown, ignoremissing) | return self._normalize(path, isknown, ignoremissing) | ||||
return path | return path | ||||
def clear(self): | def clear(self): | ||||
self._map.clear() | self._map.clear() | ||||
self._lastnormaltime = 0 | self._lastnormaltime = 0 | ||||
for f in to_lookup: | for f in to_lookup: | ||||
self.normallookup(f) | self.normallookup(f) | ||||
for f in to_drop: | for f in to_drop: | ||||
self.drop(f) | self.drop(f) | ||||
self._dirty = True | self._dirty = True | ||||
def identity(self): | def identity(self): | ||||
'''Return identity of dirstate itself to detect changing in storage | """Return identity of dirstate itself to detect changing in storage | ||||
If identity of previous dirstate is equal to this, writing | If identity of previous dirstate is equal to this, writing | ||||
changes based on the former dirstate out can keep consistency. | changes based on the former dirstate out can keep consistency. | ||||
''' | """ | ||||
return self._map.identity | return self._map.identity | ||||
def write(self, tr): | def write(self, tr): | ||||
if not self._dirty: | if not self._dirty: | ||||
return | return | ||||
filename = self._filename | filename = self._filename | ||||
if tr: | if tr: | ||||
self._root, b'', [], [pattern], warn=self._ui.warn | self._root, b'', [], [pattern], warn=self._ui.warn | ||||
) | ) | ||||
if m(f): | if m(f): | ||||
return (i, lineno, line) | return (i, lineno, line) | ||||
visited.add(i) | visited.add(i) | ||||
return (None, -1, b"") | return (None, -1, b"") | ||||
def _walkexplicit(self, match, subrepos): | def _walkexplicit(self, match, subrepos): | ||||
'''Get stat data about the files explicitly specified by match. | """Get stat data about the files explicitly specified by match. | ||||
Return a triple (results, dirsfound, dirsnotfound). | Return a triple (results, dirsfound, dirsnotfound). | ||||
- results is a mapping from filename to stat result. It also contains | - results is a mapping from filename to stat result. It also contains | ||||
listings mapping subrepos and .hg to None. | listings mapping subrepos and .hg to None. | ||||
- dirsfound is a list of files found to be directories. | - dirsfound is a list of files found to be directories. | ||||
- dirsnotfound is a list of files that the dirstate thinks are | - dirsnotfound is a list of files that the dirstate thinks are | ||||
directories and that were not found.''' | directories and that were not found.""" | ||||
def badtype(mode): | def badtype(mode): | ||||
kind = _(b'unknown') | kind = _(b'unknown') | ||||
if stat.S_ISCHR(mode): | if stat.S_ISCHR(mode): | ||||
kind = _(b'character device') | kind = _(b'character device') | ||||
elif stat.S_ISBLK(mode): | elif stat.S_ISBLK(mode): | ||||
kind = _(b'block device') | kind = _(b'block device') | ||||
elif stat.S_ISFIFO(mode): | elif stat.S_ISFIFO(mode): | ||||
path, norm, True, None, self._map.dirfoldmap | path, norm, True, None, self._map.dirfoldmap | ||||
) | ) | ||||
if path != folded: | if path != folded: | ||||
results[path] = None | results[path] = None | ||||
return results, dirsfound, dirsnotfound | return results, dirsfound, dirsnotfound | ||||
def walk(self, match, subrepos, unknown, ignored, full=True): | def walk(self, match, subrepos, unknown, ignored, full=True): | ||||
''' | """ | ||||
Walk recursively through the directory tree, finding all files | Walk recursively through the directory tree, finding all files | ||||
matched by match. | matched by match. | ||||
If full is False, maybe skip some known-clean files. | If full is False, maybe skip some known-clean files. | ||||
Return a dict mapping filename to stat-like object (either | Return a dict mapping filename to stat-like object (either | ||||
mercurial.osutil.stat instance or return value of os.stat()). | mercurial.osutil.stat instance or return value of os.stat()). | ||||
''' | """ | ||||
# full is a flag that extensions that hook into walk can use -- this | # full is a flag that extensions that hook into walk can use -- this | ||||
# implementation doesn't use it at all. This satisfies the contract | # implementation doesn't use it at all. This satisfies the contract | ||||
# because we only guarantee a "maybe". | # because we only guarantee a "maybe". | ||||
if ignored: | if ignored: | ||||
ignore = util.never | ignore = util.never | ||||
dirignore = util.never | dirignore = util.never | ||||
elif unknown: | elif unknown: | ||||
deleted=deleted, | deleted=deleted, | ||||
unknown=unknown, | unknown=unknown, | ||||
ignored=ignored, | ignored=ignored, | ||||
clean=clean, | clean=clean, | ||||
) | ) | ||||
return (lookup, status) | return (lookup, status) | ||||
def status(self, match, subrepos, ignored, clean, unknown): | def status(self, match, subrepos, ignored, clean, unknown): | ||||
'''Determine the status of the working copy relative to the | """Determine the status of the working copy relative to the | ||||
dirstate and return a pair of (unsure, status), where status is of type | dirstate and return a pair of (unsure, status), where status is of type | ||||
scmutil.status and: | scmutil.status and: | ||||
unsure: | unsure: | ||||
files that might have been modified since the dirstate was | files that might have been modified since the dirstate was | ||||
written, but need to be read to be sure (size is the same | written, but need to be read to be sure (size is the same | ||||
but mtime differs) | but mtime differs) | ||||
status.modified: | status.modified: | ||||
files that have definitely been modified since the dirstate | files that have definitely been modified since the dirstate | ||||
was written (different size or mode) | was written (different size or mode) | ||||
status.clean: | status.clean: | ||||
files that have definitely not been modified since the | files that have definitely not been modified since the | ||||
dirstate was written | dirstate was written | ||||
''' | """ | ||||
listignored, listclean, listunknown = ignored, clean, unknown | listignored, listclean, listunknown = ignored, clean, unknown | ||||
lookup, modified, added, unknown, ignored = [], [], [], [], [] | lookup, modified, added, unknown, ignored = [], [], [], [], [] | ||||
removed, deleted, clean = [], [], [] | removed, deleted, clean = [], [], [] | ||||
dmap = self._map | dmap = self._map | ||||
dmap.preload() | dmap.preload() | ||||
use_rust = True | use_rust = True | ||||
elif state == b'r': | elif state == b'r': | ||||
radd(fn) | radd(fn) | ||||
status = scmutil.status( | status = scmutil.status( | ||||
modified, added, removed, deleted, unknown, ignored, clean | modified, added, removed, deleted, unknown, ignored, clean | ||||
) | ) | ||||
return (lookup, status) | return (lookup, status) | ||||
def matches(self, match): | def matches(self, match): | ||||
''' | """ | ||||
return files in the dirstate (in whatever state) filtered by match | return files in the dirstate (in whatever state) filtered by match | ||||
''' | """ | ||||
dmap = self._map | dmap = self._map | ||||
if rustmod is not None: | if rustmod is not None: | ||||
dmap = self._map._rustmap | dmap = self._map._rustmap | ||||
if match.always(): | if match.always(): | ||||
return dmap.keys() | return dmap.keys() | ||||
files = match.files() | files = match.files() | ||||
if match.isexact(): | if match.isexact(): |
from . import ( | from . import ( | ||||
error, | error, | ||||
narrowspec, | narrowspec, | ||||
util, | util, | ||||
) | ) | ||||
class dirstateguard(util.transactional): | class dirstateguard(util.transactional): | ||||
'''Restore dirstate at unexpected failure. | """Restore dirstate at unexpected failure. | ||||
At the construction, this class does: | At the construction, this class does: | ||||
- write current ``repo.dirstate`` out, and | - write current ``repo.dirstate`` out, and | ||||
- save ``.hg/dirstate`` into the backup file | - save ``.hg/dirstate`` into the backup file | ||||
This restores ``.hg/dirstate`` from backup file, if ``release()`` | This restores ``.hg/dirstate`` from backup file, if ``release()`` | ||||
is invoked before ``close()``. | is invoked before ``close()``. | ||||
This just removes the backup file at ``close()`` before ``release()``. | This just removes the backup file at ``close()`` before ``release()``. | ||||
''' | """ | ||||
def __init__(self, repo, name): | def __init__(self, repo, name): | ||||
self._repo = repo | self._repo = repo | ||||
self._active = False | self._active = False | ||||
self._closed = False | self._closed = False | ||||
self._backupname = b'dirstate.backup.%s.%d' % (name, id(self)) | self._backupname = b'dirstate.backup.%s.%d' % (name, id(self)) | ||||
self._narrowspecbackupname = b'narrowspec.backup.%s.%d' % ( | self._narrowspecbackupname = b'narrowspec.backup.%s.%d' % ( | ||||
name, | name, |
if heads and not anyinc: | if heads and not anyinc: | ||||
# server could be lying on the advertised heads | # server could be lying on the advertised heads | ||||
has_node = repo.changelog.hasnode | has_node = repo.changelog.hasnode | ||||
anyinc = any(not has_node(n) for n in heads) | anyinc = any(not has_node(n) for n in heads) | ||||
return (list(common), anyinc, heads or list(srvheads)) | return (list(common), anyinc, heads or list(srvheads)) | ||||
class outgoing(object): | class outgoing(object): | ||||
'''Represents the result of a findcommonoutgoing() call. | """Represents the result of a findcommonoutgoing() call. | ||||
Members: | Members: | ||||
ancestorsof is a list of the nodes whose ancestors are included in the | ancestorsof is a list of the nodes whose ancestors are included in the | ||||
outgoing operation. | outgoing operation. | ||||
missing is a list of those ancestors of ancestorsof that are present in | missing is a list of those ancestors of ancestorsof that are present in | ||||
local but not in remote. | local but not in remote. | ||||
common is a set containing revs common between the local and the remote | common is a set containing revs common between the local and the remote | ||||
repository (at least all of those that are ancestors of ancestorsof). | repository (at least all of those that are ancestors of ancestorsof). | ||||
commonheads is the list of heads of common. | commonheads is the list of heads of common. | ||||
excluded is the list of missing changeset that shouldn't be sent | excluded is the list of missing changeset that shouldn't be sent | ||||
remotely. | remotely. | ||||
Some members are computed on demand from the heads, unless provided upfront | Some members are computed on demand from the heads, unless provided upfront | ||||
by discovery.''' | by discovery.""" | ||||
def __init__( | def __init__( | ||||
self, repo, commonheads=None, ancestorsof=None, missingroots=None | self, repo, commonheads=None, ancestorsof=None, missingroots=None | ||||
): | ): | ||||
# at least one of them must not be set | # at least one of them must not be set | ||||
assert None in (commonheads, missingroots) | assert None in (commonheads, missingroots) | ||||
cl = repo.changelog | cl = repo.changelog | ||||
if ancestorsof is None: | if ancestorsof is None: | ||||
stacklevel=2, | stacklevel=2, | ||||
) | ) | ||||
return self.ancestorsof | return self.ancestorsof | ||||
def findcommonoutgoing( | def findcommonoutgoing( | ||||
repo, other, onlyheads=None, force=False, commoninc=None, portable=False | repo, other, onlyheads=None, force=False, commoninc=None, portable=False | ||||
): | ): | ||||
'''Return an outgoing instance to identify the nodes present in repo but | """Return an outgoing instance to identify the nodes present in repo but | ||||
not in other. | not in other. | ||||
If onlyheads is given, only nodes ancestral to nodes in onlyheads | If onlyheads is given, only nodes ancestral to nodes in onlyheads | ||||
(inclusive) are included. If you already know the local repo's heads, | (inclusive) are included. If you already know the local repo's heads, | ||||
passing them in onlyheads is faster than letting them be recomputed here. | passing them in onlyheads is faster than letting them be recomputed here. | ||||
If commoninc is given, it must be the result of a prior call to | If commoninc is given, it must be the result of a prior call to | ||||
findcommonincoming(repo, other, force) to avoid recomputing it here. | findcommonincoming(repo, other, force) to avoid recomputing it here. | ||||
If portable is given, compute more conservative common and ancestorsof, | If portable is given, compute more conservative common and ancestorsof, | ||||
to make bundles created from the instance more portable.''' | to make bundles created from the instance more portable.""" | ||||
# declare an empty outgoing object to be filled later | # declare an empty outgoing object to be filled later | ||||
og = outgoing(repo, None, None) | og = outgoing(repo, None, None) | ||||
# get common set if not provided | # get common set if not provided | ||||
if commoninc is None: | if commoninc is None: | ||||
commoninc = findcommonincoming( | commoninc = findcommonincoming( | ||||
repo, other, force=force, ancestorsof=onlyheads | repo, other, force=force, ancestorsof=onlyheads | ||||
) | ) | ||||
def _nowarnheads(pushop): | def _nowarnheads(pushop): | ||||
# Compute newly pushed bookmarks. We don't warn about bookmarked heads. | # Compute newly pushed bookmarks. We don't warn about bookmarked heads. | ||||
repo = pushop.repo.unfiltered() | repo = pushop.repo.unfiltered() | ||||
remote = pushop.remote | remote = pushop.remote | ||||
localbookmarks = repo._bookmarks | localbookmarks = repo._bookmarks | ||||
with remote.commandexecutor() as e: | with remote.commandexecutor() as e: | ||||
remotebookmarks = e.callcommand( | remotebookmarks = e.callcommand( | ||||
b'listkeys', {b'namespace': b'bookmarks',} | b'listkeys', | ||||
{ | |||||
b'namespace': b'bookmarks', | |||||
}, | |||||
).result() | ).result() | ||||
bookmarkedheads = set() | bookmarkedheads = set() | ||||
# internal config: bookmarks.pushing | # internal config: bookmarks.pushing | ||||
newbookmarks = [ | newbookmarks = [ | ||||
localbookmarks.expandname(b) | localbookmarks.expandname(b) | ||||
for b in pushop.ui.configlist(b'bookmarks', b'pushing') | for b in pushop.ui.configlist(b'bookmarks', b'pushing') | ||||
elif len(newhs) > len(oldhs): | elif len(newhs) > len(oldhs): | ||||
# remove bookmarked or existing remote heads from the new heads list | # remove bookmarked or existing remote heads from the new heads list | ||||
dhs = sorted(newhs - nowarnheads - oldhs) | dhs = sorted(newhs - nowarnheads - oldhs) | ||||
if dhs: | if dhs: | ||||
if errormsg is None: | if errormsg is None: | ||||
if branch not in (b'default', None): | if branch not in (b'default', None): | ||||
errormsg = _( | errormsg = _( | ||||
b"push creates new remote head %s on branch '%s'" | b"push creates new remote head %s on branch '%s'" | ||||
) % (short(dhs[0]), branch,) | ) % ( | ||||
short(dhs[0]), | |||||
branch, | |||||
) | |||||
elif repo[dhs[0]].bookmarks(): | elif repo[dhs[0]].bookmarks(): | ||||
errormsg = _( | errormsg = _( | ||||
b"push creates new remote head %s " | b"push creates new remote head %s " | ||||
b"with bookmark '%s'" | b"with bookmark '%s'" | ||||
) % (short(dhs[0]), repo[dhs[0]].bookmarks()[0]) | ) % (short(dhs[0]), repo[dhs[0]].bookmarks()[0]) | ||||
else: | else: | ||||
errormsg = _(b"push creates new remote head %s") % short( | errormsg = _(b"push creates new remote head %s") % short( | ||||
dhs[0] | dhs[0] |
cmd = re.sub(br'\$(\d+|\$)', replacer, cmd) | cmd = re.sub(br'\$(\d+|\$)', replacer, cmd) | ||||
givenargs = [x for i, x in enumerate(givenargs) if i not in nums] | givenargs = [x for i, x in enumerate(givenargs) if i not in nums] | ||||
args = pycompat.shlexsplit(cmd) | args = pycompat.shlexsplit(cmd) | ||||
return args + givenargs | return args + givenargs | ||||
def aliasinterpolate(name, args, cmd): | def aliasinterpolate(name, args, cmd): | ||||
'''interpolate args into cmd for shell aliases | """interpolate args into cmd for shell aliases | ||||
This also handles $0, $@ and "$@". | This also handles $0, $@ and "$@". | ||||
''' | """ | ||||
# util.interpolate can't deal with "$@" (with quotes) because it's only | # util.interpolate can't deal with "$@" (with quotes) because it's only | ||||
# built to match prefix + patterns. | # built to match prefix + patterns. | ||||
replacemap = {b'$%d' % (i + 1): arg for i, arg in enumerate(args)} | replacemap = {b'$%d' % (i + 1): arg for i, arg in enumerate(args)} | ||||
replacemap[b'$0'] = name | replacemap[b'$0'] = name | ||||
replacemap[b'$$'] = b'$' | replacemap[b'$$'] = b'$' | ||||
replacemap[b'$@'] = b' '.join(args) | replacemap[b'$@'] = b' '.join(args) | ||||
# Typical Unix shells interpolate "$@" (with quotes) as all the positional | # Typical Unix shells interpolate "$@" (with quotes) as all the positional | ||||
# parameters, separated out into words. Emulate the same behavior here by | # parameters, separated out into words. Emulate the same behavior here by | ||||
cmdhelp = None | cmdhelp = None | ||||
self.alias = True | self.alias = True | ||||
self._populatehelp(ui, name, cmd, self.fn, cmdhelp) | self._populatehelp(ui, name, cmd, self.fn, cmdhelp) | ||||
except error.UnknownCommand: | except error.UnknownCommand: | ||||
self.badalias = _( | self.badalias = _( | ||||
b"alias '%s' resolves to unknown command '%s'" | b"alias '%s' resolves to unknown command '%s'" | ||||
) % (self.name, cmd,) | ) % ( | ||||
self.name, | |||||
cmd, | |||||
) | |||||
self.unknowncmd = True | self.unknowncmd = True | ||||
except error.AmbiguousCommand: | except error.AmbiguousCommand: | ||||
self.badalias = _( | self.badalias = _( | ||||
b"alias '%s' resolves to ambiguous command '%s'" | b"alias '%s' resolves to ambiguous command '%s'" | ||||
) % (self.name, cmd,) | ) % ( | ||||
self.name, | |||||
cmd, | |||||
) | |||||
def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None): | def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None): | ||||
# confine strings to be passed to i18n.gettext() | # confine strings to be passed to i18n.gettext() | ||||
cfg = {} | cfg = {} | ||||
for k in (b'doc', b'help', b'category'): | for k in (b'doc', b'help', b'category'): | ||||
v = ui.config(b'alias', b'%s:%s' % (name, k), None) | v = ui.config(b'alias', b'%s:%s' % (name, k), None) | ||||
if v is None: | if v is None: | ||||
continue | continue |
encoding = _encodingrewrites.get(encoding, encoding) | encoding = _encodingrewrites.get(encoding, encoding) | ||||
except locale.Error: | except locale.Error: | ||||
encoding = b'ascii' | encoding = b'ascii' | ||||
encodingmode = environ.get(b"HGENCODINGMODE", b"strict") | encodingmode = environ.get(b"HGENCODINGMODE", b"strict") | ||||
fallbackencoding = b'ISO-8859-1' | fallbackencoding = b'ISO-8859-1' | ||||
class localstr(bytes): | class localstr(bytes): | ||||
'''This class allows strings that are unmodified to be | """This class allows strings that are unmodified to be | ||||
round-tripped to the local encoding and back''' | round-tripped to the local encoding and back""" | ||||
def __new__(cls, u, l): | def __new__(cls, u, l): | ||||
s = bytes.__new__(cls, l) | s = bytes.__new__(cls, l) | ||||
s._utf8 = u | s._utf8 = u | ||||
return s | return s | ||||
if pycompat.TYPE_CHECKING: | if pycompat.TYPE_CHECKING: | ||||
# pseudo implementation to help pytype see localstr() constructor | # pseudo implementation to help pytype see localstr() constructor | ||||
eaw = getattr(unicodedata, 'east_asian_width', None) | eaw = getattr(unicodedata, 'east_asian_width', None) | ||||
if eaw is not None: | if eaw is not None: | ||||
return sum([eaw(c) in _wide and 2 or 1 for c in d]) | return sum([eaw(c) in _wide and 2 or 1 for c in d]) | ||||
return len(d) | return len(d) | ||||
def getcols(s, start, c): | def getcols(s, start, c): | ||||
# type: (bytes, int, int) -> bytes | # type: (bytes, int, int) -> bytes | ||||
'''Use colwidth to find a c-column substring of s starting at byte | """Use colwidth to find a c-column substring of s starting at byte | ||||
index start''' | index start""" | ||||
for x in pycompat.xrange(start + c, len(s)): | for x in pycompat.xrange(start + c, len(s)): | ||||
t = s[start:x] | t = s[start:x] | ||||
if colwidth(t) == c: | if colwidth(t) == c: | ||||
return t | return t | ||||
raise ValueError('substring not found') | raise ValueError('substring not found') | ||||
def trim(s, width, ellipsis=b'', leftside=False): | def trim(s, width, ellipsis=b'', leftside=False): | ||||
return uu.encode(_sysstr(encoding)) | return uu.encode(_sysstr(encoding)) | ||||
except UnicodeError: | except UnicodeError: | ||||
return s.upper() # we don't know how to fold this except in ASCII | return s.upper() # we don't know how to fold this except in ASCII | ||||
except LookupError as k: | except LookupError as k: | ||||
raise error.Abort(k, hint=b"please check your locale settings") | raise error.Abort(k, hint=b"please check your locale settings") | ||||
class normcasespecs(object): | class normcasespecs(object): | ||||
'''what a platform's normcase does to ASCII strings | """what a platform's normcase does to ASCII strings | ||||
This is specified per platform, and should be consistent with what normcase | This is specified per platform, and should be consistent with what normcase | ||||
on that platform actually does. | on that platform actually does. | ||||
lower: normcase lowercases ASCII strings | lower: normcase lowercases ASCII strings | ||||
upper: normcase uppercases ASCII strings | upper: normcase uppercases ASCII strings | ||||
other: the fallback function should always be called | other: the fallback function should always be called | ||||
This should be kept in sync with normcase_spec in util.h.''' | This should be kept in sync with normcase_spec in util.h.""" | ||||
lower = -1 | lower = -1 | ||||
upper = 1 | upper = 1 | ||||
other = 0 | other = 0 | ||||
def jsonescape(s, paranoid=False): | def jsonescape(s, paranoid=False): | ||||
# type: (Any, Any) -> Any | # type: (Any, Any) -> Any | ||||
'''returns a string suitable for JSON | """returns a string suitable for JSON | ||||
JSON is problematic for us because it doesn't support non-Unicode | JSON is problematic for us because it doesn't support non-Unicode | ||||
bytes. To deal with this, we take the following approach: | bytes. To deal with this, we take the following approach: | ||||
- localstr/safelocalstr objects are converted back to UTF-8 | - localstr/safelocalstr objects are converted back to UTF-8 | ||||
- valid UTF-8/ASCII strings are passed as-is | - valid UTF-8/ASCII strings are passed as-is | ||||
- other strings are converted to UTF-8b surrogate encoding | - other strings are converted to UTF-8b surrogate encoding | ||||
- apply JSON-specified string escaping | - apply JSON-specified string escaping | ||||
>>> jsonescape(b'a weird byte: \\xdd', paranoid=True) | >>> jsonescape(b'a weird byte: \\xdd', paranoid=True) | ||||
'a weird byte: \\\\udcdd' | 'a weird byte: \\\\udcdd' | ||||
>>> jsonescape(b'utf-8: caf\\xc3\\xa9', paranoid=True) | >>> jsonescape(b'utf-8: caf\\xc3\\xa9', paranoid=True) | ||||
'utf-8: caf\\\\u00e9' | 'utf-8: caf\\\\u00e9' | ||||
>>> jsonescape(b'non-BMP: \\xf0\\x9d\\x84\\x9e', paranoid=True) | >>> jsonescape(b'non-BMP: \\xf0\\x9d\\x84\\x9e', paranoid=True) | ||||
'non-BMP: \\\\ud834\\\\udd1e' | 'non-BMP: \\\\ud834\\\\udd1e' | ||||
>>> jsonescape(b'<foo@example.org>', paranoid=True) | >>> jsonescape(b'<foo@example.org>', paranoid=True) | ||||
'\\\\u003cfoo@example.org\\\\u003e' | '\\\\u003cfoo@example.org\\\\u003e' | ||||
''' | """ | ||||
u8chars = toutf8b(s) | u8chars = toutf8b(s) | ||||
try: | try: | ||||
return _jsonescapeu8fast(u8chars, paranoid) | return _jsonescapeu8fast(u8chars, paranoid) | ||||
except ValueError: | except ValueError: | ||||
pass | pass | ||||
return charencodepure.jsonescapeu8fallback(u8chars, paranoid) | return charencodepure.jsonescapeu8fallback(u8chars, paranoid) | ||||
# We need to decode/encode U+DCxx codes transparently since invalid UTF-8 | # We need to decode/encode U+DCxx codes transparently since invalid UTF-8 | ||||
# bytes are mapped to that range. | # bytes are mapped to that range. | ||||
if pycompat.ispy3: | if pycompat.ispy3: | ||||
_utf8strict = r'surrogatepass' | _utf8strict = r'surrogatepass' | ||||
else: | else: | ||||
_utf8strict = r'strict' | _utf8strict = r'strict' | ||||
_utf8len = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 4] | _utf8len = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 4] | ||||
def getutf8char(s, pos): | def getutf8char(s, pos): | ||||
# type: (bytes, int) -> bytes | # type: (bytes, int) -> bytes | ||||
'''get the next full utf-8 character in the given string, starting at pos | """get the next full utf-8 character in the given string, starting at pos | ||||
Raises a UnicodeError if the given location does not start a valid | Raises a UnicodeError if the given location does not start a valid | ||||
utf-8 character. | utf-8 character. | ||||
''' | """ | ||||
# find how many bytes to attempt decoding from first nibble | # find how many bytes to attempt decoding from first nibble | ||||
l = _utf8len[ord(s[pos : pos + 1]) >> 4] | l = _utf8len[ord(s[pos : pos + 1]) >> 4] | ||||
if not l: # ascii | if not l: # ascii | ||||
return s[pos : pos + 1] | return s[pos : pos + 1] | ||||
c = s[pos : pos + l] | c = s[pos : pos + l] | ||||
# validate with attempted decode | # validate with attempted decode | ||||
c.decode("utf-8", _utf8strict) | c.decode("utf-8", _utf8strict) | ||||
return c | return c | ||||
def toutf8b(s): | def toutf8b(s): | ||||
# type: (bytes) -> bytes | # type: (bytes) -> bytes | ||||
'''convert a local, possibly-binary string into UTF-8b | """convert a local, possibly-binary string into UTF-8b | ||||
This is intended as a generic method to preserve data when working | This is intended as a generic method to preserve data when working | ||||
with schemes like JSON and XML that have no provision for | with schemes like JSON and XML that have no provision for | ||||
arbitrary byte strings. As Mercurial often doesn't know | arbitrary byte strings. As Mercurial often doesn't know | ||||
what encoding data is in, we use so-called UTF-8b. | what encoding data is in, we use so-called UTF-8b. | ||||
If a string is already valid UTF-8 (or ASCII), it passes unmodified. | If a string is already valid UTF-8 (or ASCII), it passes unmodified. | ||||
Otherwise, unsupported bytes are mapped to UTF-16 surrogate range, | Otherwise, unsupported bytes are mapped to UTF-16 surrogate range, | ||||
- non-lossy local strings (aka safelocalstr) get sent as UTF-8 as well | - non-lossy local strings (aka safelocalstr) get sent as UTF-8 as well | ||||
- because we must preserve UTF-8 bytestring in places such as | - because we must preserve UTF-8 bytestring in places such as | ||||
filenames, metadata can't be roundtripped without help | filenames, metadata can't be roundtripped without help | ||||
(Note: "UTF-8b" often refers to decoding a mix of valid UTF-8 and | (Note: "UTF-8b" often refers to decoding a mix of valid UTF-8 and | ||||
arbitrary bytes into an internal Unicode format that can be | arbitrary bytes into an internal Unicode format that can be | ||||
re-encoded back into the original. Here we are exposing the | re-encoded back into the original. Here we are exposing the | ||||
internal surrogate encoding as a UTF-8 string.) | internal surrogate encoding as a UTF-8 string.) | ||||
''' | """ | ||||
if isinstance(s, localstr): | if isinstance(s, localstr): | ||||
# assume that the original UTF-8 sequence would never contain | # assume that the original UTF-8 sequence would never contain | ||||
# invalid characters in U+DCxx range | # invalid characters in U+DCxx range | ||||
return s._utf8 | return s._utf8 | ||||
elif isinstance(s, safelocalstr): | elif isinstance(s, safelocalstr): | ||||
# already verified that s is non-lossy in legacy encoding, which | # already verified that s is non-lossy in legacy encoding, which | ||||
# shouldn't contain characters in U+DCxx range | # shouldn't contain characters in U+DCxx range | ||||
c = unichr(0xDC00 + ord(s[pos])).encode('utf-8', _utf8strict) | c = unichr(0xDC00 + ord(s[pos])).encode('utf-8', _utf8strict) | ||||
pos += 1 | pos += 1 | ||||
r += c | r += c | ||||
return r | return r | ||||
def fromutf8b(s): | def fromutf8b(s): | ||||
# type: (bytes) -> bytes | # type: (bytes) -> bytes | ||||
'''Given a UTF-8b string, return a local, possibly-binary string. | """Given a UTF-8b string, return a local, possibly-binary string. | ||||
return the original binary string. This | return the original binary string. This | ||||
is a round-trip process for strings like filenames, but metadata | is a round-trip process for strings like filenames, but metadata | ||||
that's was passed through tolocal will remain in UTF-8. | that's was passed through tolocal will remain in UTF-8. | ||||
>>> roundtrip = lambda x: fromutf8b(toutf8b(x)) == x | >>> roundtrip = lambda x: fromutf8b(toutf8b(x)) == x | ||||
>>> m = b"\\xc3\\xa9\\x99abcd" | >>> m = b"\\xc3\\xa9\\x99abcd" | ||||
>>> toutf8b(m) | >>> toutf8b(m) | ||||
'\\xc3\\xa9\\xed\\xb2\\x99abcd' | '\\xc3\\xa9\\xed\\xb2\\x99abcd' | ||||
>>> roundtrip(m) | >>> roundtrip(m) | ||||
True | True | ||||
>>> roundtrip(b"\\xc2\\xc2\\x80") | >>> roundtrip(b"\\xc2\\xc2\\x80") | ||||
True | True | ||||
>>> roundtrip(b"\\xef\\xbf\\xbd") | >>> roundtrip(b"\\xef\\xbf\\xbd") | ||||
True | True | ||||
>>> roundtrip(b"\\xef\\xef\\xbf\\xbd") | >>> roundtrip(b"\\xef\\xef\\xbf\\xbd") | ||||
True | True | ||||
>>> roundtrip(b"\\xf1\\x80\\x80\\x80\\x80") | >>> roundtrip(b"\\xf1\\x80\\x80\\x80\\x80") | ||||
True | True | ||||
''' | """ | ||||
if isasciistr(s): | if isasciistr(s): | ||||
return s | return s | ||||
# fast path - look for uDxxx prefixes in s | # fast path - look for uDxxx prefixes in s | ||||
if b"\xed" not in s: | if b"\xed" not in s: | ||||
return s | return s | ||||
# We could do this with the unicode type but some Python builds | # We could do this with the unicode type but some Python builds |
hint=_( | hint=_( | ||||
b'see https://mercurial-scm.org/wiki/MergeStateRecords for ' | b'see https://mercurial-scm.org/wiki/MergeStateRecords for ' | ||||
b'more information' | b'more information' | ||||
), | ), | ||||
) | ) | ||||
class UnknownVersion(Abort): | class UnknownVersion(Abort): | ||||
"""generic exception for aborting from an encounter with an unknown version | """generic exception for aborting from an encounter with an unknown version""" | ||||
""" | |||||
def __init__(self, msg, hint=None, version=None): | def __init__(self, msg, hint=None, version=None): | ||||
self.version = version | self.version = version | ||||
super(UnknownVersion, self).__init__(msg, hint=hint) | super(UnknownVersion, self).__init__(msg, hint=hint) | ||||
class LockError(IOError): | class LockError(IOError): | ||||
def __init__(self, errno, strerror, filename, desc): | def __init__(self, errno, strerror, filename, desc): |
remote, | remote, | ||||
force=False, | force=False, | ||||
revs=None, | revs=None, | ||||
newbranch=False, | newbranch=False, | ||||
bookmarks=(), | bookmarks=(), | ||||
publish=False, | publish=False, | ||||
opargs=None, | opargs=None, | ||||
): | ): | ||||
'''Push outgoing changesets (limited by revs) from a local | """Push outgoing changesets (limited by revs) from a local | ||||
repository to remote. Return an integer: | repository to remote. Return an integer: | ||||
- None means nothing to push | - None means nothing to push | ||||
- 0 means HTTP error | - 0 means HTTP error | ||||
- 1 means we pushed and remote head count is unchanged *or* | - 1 means we pushed and remote head count is unchanged *or* | ||||
we have outgoing changesets but refused to push | we have outgoing changesets but refused to push | ||||
- other values as described by addchangegroup() | - other values as described by addchangegroup() | ||||
''' | """ | ||||
if opargs is None: | if opargs is None: | ||||
opargs = {} | opargs = {} | ||||
pushop = pushoperation( | pushop = pushoperation( | ||||
repo, | repo, | ||||
remote, | remote, | ||||
force, | force, | ||||
revs, | revs, | ||||
newbranch, | newbranch, | ||||
if all(unficl.hasnode(n) for n in pullop.rheads): | if all(unficl.hasnode(n) for n in pullop.rheads): | ||||
break | break | ||||
new_heads = headsofdiff(unficl.heads(), old_heads) | new_heads = headsofdiff(unficl.heads(), old_heads) | ||||
pullop.common = headsofunion(new_heads, pullop.common) | pullop.common = headsofunion(new_heads, pullop.common) | ||||
pullop.rheads = set(pullop.rheads) - pullop.common | pullop.rheads = set(pullop.rheads) - pullop.common | ||||
def add_confirm_callback(repo, pullop): | def add_confirm_callback(repo, pullop): | ||||
""" adds a finalize callback to transaction which can be used to show stats | """adds a finalize callback to transaction which can be used to show stats | ||||
to user and confirm the pull before committing transaction """ | to user and confirm the pull before committing transaction""" | ||||
tr = pullop.trmanager.transaction() | tr = pullop.trmanager.transaction() | ||||
scmutil.registersummarycallback( | scmutil.registersummarycallback( | ||||
repo, tr, txnname=b'pull', as_validator=True | repo, tr, txnname=b'pull', as_validator=True | ||||
) | ) | ||||
reporef = weakref.ref(repo.unfiltered()) | reporef = weakref.ref(repo.unfiltered()) | ||||
def prompt(tr): | def prompt(tr): | ||||
if pullop.remote.capable(b'getbundle'): | if pullop.remote.capable(b'getbundle'): | ||||
# TODO: get bundlecaps from remote | # TODO: get bundlecaps from remote | ||||
cg = pullop.remote.getbundle( | cg = pullop.remote.getbundle( | ||||
b'pull', common=pullop.common, heads=pullop.heads or pullop.rheads | b'pull', common=pullop.common, heads=pullop.heads or pullop.rheads | ||||
) | ) | ||||
elif pullop.heads is None: | elif pullop.heads is None: | ||||
with pullop.remote.commandexecutor() as e: | with pullop.remote.commandexecutor() as e: | ||||
cg = e.callcommand( | cg = e.callcommand( | ||||
b'changegroup', {b'nodes': pullop.fetch, b'source': b'pull',} | b'changegroup', | ||||
{ | |||||
b'nodes': pullop.fetch, | |||||
b'source': b'pull', | |||||
}, | |||||
).result() | ).result() | ||||
elif not pullop.remote.capable(b'changegroupsubset'): | elif not pullop.remote.capable(b'changegroupsubset'): | ||||
raise error.Abort( | raise error.Abort( | ||||
_( | _( | ||||
b"partial pull cannot be done because " | b"partial pull cannot be done because " | ||||
b"other repository doesn't support " | b"other repository doesn't support " | ||||
b"changegroupsubset." | b"changegroupsubset." |
) | ) | ||||
# And adjust the phase of all changesets accordingly. | # And adjust the phase of all changesets accordingly. | ||||
for phasenumber, phase in phases.phasenames.items(): | for phasenumber, phase in phases.phasenames.items(): | ||||
if phase == b'secret' or not csetres[b'nodesbyphase'][phase]: | if phase == b'secret' or not csetres[b'nodesbyphase'][phase]: | ||||
continue | continue | ||||
phases.advanceboundary( | phases.advanceboundary( | ||||
repo, tr, phasenumber, csetres[b'nodesbyphase'][phase], | repo, | ||||
tr, | |||||
phasenumber, | |||||
csetres[b'nodesbyphase'][phase], | |||||
) | ) | ||||
# Write bookmark updates. | # Write bookmark updates. | ||||
bookmarks.updatefromremote( | bookmarks.updatefromremote( | ||||
repo.ui, | repo.ui, | ||||
repo, | repo, | ||||
csetres[b'bookmarks'], | csetres[b'bookmarks'], | ||||
remote.url(), | remote.url(), | ||||
return False | return False | ||||
return True | return True | ||||
def _fetchrawstorefiles(repo, remote): | def _fetchrawstorefiles(repo, remote): | ||||
with remote.commandexecutor() as e: | with remote.commandexecutor() as e: | ||||
objs = e.callcommand( | objs = e.callcommand( | ||||
b'rawstorefiledata', {b'files': [b'changelog', b'manifestlog'],} | b'rawstorefiledata', | ||||
{ | |||||
b'files': [b'changelog', b'manifestlog'], | |||||
}, | |||||
).result() | ).result() | ||||
# First object is a summary of files data that follows. | # First object is a summary of files data that follows. | ||||
overall = next(objs) | overall = next(objs) | ||||
progress = repo.ui.makeprogress( | progress = repo.ui.makeprogress( | ||||
_(b'clone'), total=overall[b'totalsize'], unit=_(b'bytes') | _(b'clone'), total=overall[b'totalsize'], unit=_(b'bytes') | ||||
) | ) | ||||
for i in pycompat.xrange(0, len(csets), batchsize): | for i in pycompat.xrange(0, len(csets), batchsize): | ||||
batch = [x for x in csets[i : i + batchsize]] | batch = [x for x in csets[i : i + batchsize]] | ||||
if not batch: | if not batch: | ||||
continue | continue | ||||
with remote.commandexecutor() as e: | with remote.commandexecutor() as e: | ||||
args = { | args = { | ||||
b'revisions': [ | b'revisions': [ | ||||
{b'type': b'changesetexplicit', b'nodes': batch,} | { | ||||
b'type': b'changesetexplicit', | |||||
b'nodes': batch, | |||||
} | |||||
], | ], | ||||
b'fields': fields, | b'fields': fields, | ||||
b'haveparents': haveparents, | b'haveparents': haveparents, | ||||
} | } | ||||
if pathfilter: | if pathfilter: | ||||
args[b'pathfilter'] = pathfilter | args[b'pathfilter'] = pathfilter | ||||
for objname, loadermod, loadername in extraloaders: | for objname, loadermod, loadername in extraloaders: | ||||
extraobj = getattr(module, objname, None) | extraobj = getattr(module, objname, None) | ||||
if extraobj is not None: | if extraobj is not None: | ||||
getattr(loadermod, loadername)(ui, name, extraobj) | getattr(loadermod, loadername)(ui, name, extraobj) | ||||
def afterloaded(extension, callback): | def afterloaded(extension, callback): | ||||
'''Run the specified function after a named extension is loaded. | """Run the specified function after a named extension is loaded. | ||||
If the named extension is already loaded, the callback will be called | If the named extension is already loaded, the callback will be called | ||||
immediately. | immediately. | ||||
If the named extension never loads, the callback will be called after | If the named extension never loads, the callback will be called after | ||||
all extensions have been loaded. | all extensions have been loaded. | ||||
The callback receives the named argument ``loaded``, which is a boolean | The callback receives the named argument ``loaded``, which is a boolean | ||||
indicating whether the dependent extension actually loaded. | indicating whether the dependent extension actually loaded. | ||||
''' | """ | ||||
if extension in _extensions: | if extension in _extensions: | ||||
# Report loaded as False if the extension is disabled | # Report loaded as False if the extension is disabled | ||||
loaded = _extensions[extension] is not None | loaded = _extensions[extension] is not None | ||||
callback(loaded=loaded) | callback(loaded=loaded) | ||||
else: | else: | ||||
_aftercallbacks.setdefault(extension, []).append(callback) | _aftercallbacks.setdefault(extension, []).append(callback) | ||||
ui.traceback(force=True) | ui.traceback(force=True) | ||||
ui.warn( | ui.warn( | ||||
_(b'*** failed to populate ui by extension %s: %s\n') | _(b'*** failed to populate ui by extension %s: %s\n') | ||||
% (name, stringutil.forcebytestr(inst)) | % (name, stringutil.forcebytestr(inst)) | ||||
) | ) | ||||
def bind(func, *args): | def bind(func, *args): | ||||
'''Partial function application | """Partial function application | ||||
Returns a new function that is the partial application of args and kwargs | Returns a new function that is the partial application of args and kwargs | ||||
to func. For example, | to func. For example, | ||||
f(1, 2, bar=3) === bind(f, 1)(2, bar=3)''' | f(1, 2, bar=3) === bind(f, 1)(2, bar=3)""" | ||||
assert callable(func) | assert callable(func) | ||||
def closure(*a, **kw): | def closure(*a, **kw): | ||||
return func(*(args + a), **kw) | return func(*(args + a), **kw) | ||||
return closure | return closure | ||||
def __enter__(self): | def __enter__(self): | ||||
wrapfunction(self._container, self._funcname, self._wrapper) | wrapfunction(self._container, self._funcname, self._wrapper) | ||||
def __exit__(self, exctype, excvalue, traceback): | def __exit__(self, exctype, excvalue, traceback): | ||||
unwrapfunction(self._container, self._funcname, self._wrapper) | unwrapfunction(self._container, self._funcname, self._wrapper) | ||||
def wrapfunction(container, funcname, wrapper): | def wrapfunction(container, funcname, wrapper): | ||||
'''Wrap the function named funcname in container | """Wrap the function named funcname in container | ||||
Replace the funcname member in the given container with the specified | Replace the funcname member in the given container with the specified | ||||
wrapper. The container is typically a module, class, or instance. | wrapper. The container is typically a module, class, or instance. | ||||
The wrapper will be called like | The wrapper will be called like | ||||
wrapper(orig, *args, **kwargs) | wrapper(orig, *args, **kwargs) | ||||
[...extension stuff...] | [...extension stuff...] | ||||
repo.__class__ = myrepo | repo.__class__ = myrepo | ||||
In general, combining wrapfunction() with subclassing does not | In general, combining wrapfunction() with subclassing does not | ||||
work. Since you cannot control what other extensions are loaded by | work. Since you cannot control what other extensions are loaded by | ||||
your end users, you should play nicely with others by using the | your end users, you should play nicely with others by using the | ||||
subclass trick. | subclass trick. | ||||
''' | """ | ||||
assert callable(wrapper) | assert callable(wrapper) | ||||
origfn = getattr(container, funcname) | origfn = getattr(container, funcname) | ||||
assert callable(origfn) | assert callable(origfn) | ||||
if inspect.ismodule(container): | if inspect.ismodule(container): | ||||
# origfn is not an instance or class method. "partial" can be used. | # origfn is not an instance or class method. "partial" can be used. | ||||
# "partial" won't insert a frame in traceback. | # "partial" won't insert a frame in traceback. | ||||
wrap = functools.partial(wrapper, origfn) | wrap = functools.partial(wrapper, origfn) | ||||
else: | else: | ||||
# "partial" cannot be safely used. Emulate its effect by using "bind". | # "partial" cannot be safely used. Emulate its effect by using "bind". | ||||
# The downside is one more frame in traceback. | # The downside is one more frame in traceback. | ||||
wrap = bind(wrapper, origfn) | wrap = bind(wrapper, origfn) | ||||
_updatewrapper(wrap, origfn, wrapper) | _updatewrapper(wrap, origfn, wrapper) | ||||
setattr(container, funcname, wrap) | setattr(container, funcname, wrap) | ||||
return origfn | return origfn | ||||
def unwrapfunction(container, funcname, wrapper=None): | def unwrapfunction(container, funcname, wrapper=None): | ||||
'''undo wrapfunction | """undo wrapfunction | ||||
If wrappers is None, undo the last wrap. Otherwise removes the wrapper | If wrappers is None, undo the last wrap. Otherwise removes the wrapper | ||||
from the chain of wrappers. | from the chain of wrappers. | ||||
Return the removed wrapper. | Return the removed wrapper. | ||||
Raise IndexError if wrapper is None and nothing to unwrap; ValueError if | Raise IndexError if wrapper is None and nothing to unwrap; ValueError if | ||||
wrapper is not None but is not found in the wrapper chain. | wrapper is not None but is not found in the wrapper chain. | ||||
''' | """ | ||||
chain = getwrapperchain(container, funcname) | chain = getwrapperchain(container, funcname) | ||||
origfn = chain.pop() | origfn = chain.pop() | ||||
if wrapper is None: | if wrapper is None: | ||||
wrapper = chain[0] | wrapper = chain[0] | ||||
chain.remove(wrapper) | chain.remove(wrapper) | ||||
setattr(container, funcname, origfn) | setattr(container, funcname, origfn) | ||||
for w in reversed(chain): | for w in reversed(chain): | ||||
wrapfunction(container, funcname, w) | wrapfunction(container, funcname, w) | ||||
return wrapper | return wrapper | ||||
def getwrapperchain(container, funcname): | def getwrapperchain(container, funcname): | ||||
'''get a chain of wrappers of a function | """get a chain of wrappers of a function | ||||
Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc] | Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc] | ||||
The wrapper functions are the ones passed to wrapfunction, whose first | The wrapper functions are the ones passed to wrapfunction, whose first | ||||
argument is origfunc. | argument is origfunc. | ||||
''' | """ | ||||
result = [] | result = [] | ||||
fn = getattr(container, funcname) | fn = getattr(container, funcname) | ||||
while fn: | while fn: | ||||
assert callable(fn) | assert callable(fn) | ||||
result.append(getattr(fn, '_unboundwrapper', fn)) | result.append(getattr(fn, '_unboundwrapper', fn)) | ||||
fn = getattr(fn, '_origfunc', None) | fn = getattr(fn, '_origfunc', None) | ||||
return result | return result | ||||
# If no path was provided for a disabled extension (e.g. "color=!"), | # If no path was provided for a disabled extension (e.g. "color=!"), | ||||
# don't replace the path we already found by the scan above. | # don't replace the path we already found by the scan above. | ||||
if path: | if path: | ||||
exts[name] = path | exts[name] = path | ||||
return exts | return exts | ||||
def _moduledoc(file): | def _moduledoc(file): | ||||
'''return the top-level python documentation for the given file | """return the top-level python documentation for the given file | ||||
Loosely inspired by pydoc.source_synopsis(), but rewritten to | Loosely inspired by pydoc.source_synopsis(), but rewritten to | ||||
handle triple quotes and to return the whole text instead of just | handle triple quotes and to return the whole text instead of just | ||||
the synopsis''' | the synopsis""" | ||||
result = [] | result = [] | ||||
line = file.readline() | line = file.readline() | ||||
while line[:1] == b'#' or not line.strip(): | while line[:1] == b'#' or not line.strip(): | ||||
line = file.readline() | line = file.readline() | ||||
if not line: | if not line: | ||||
break | break | ||||
break | break | ||||
else: | else: | ||||
cmd = aliases[0] | cmd = aliases[0] | ||||
doc = _disabledhelp(path) | doc = _disabledhelp(path) | ||||
return (cmd, name, doc) | return (cmd, name, doc) | ||||
def disabledcmd(ui, cmd, strict=False): | def disabledcmd(ui, cmd, strict=False): | ||||
'''find cmd from disabled extensions without importing. | """find cmd from disabled extensions without importing. | ||||
returns (cmdname, extname, doc)''' | returns (cmdname, extname, doc)""" | ||||
paths = _disabledpaths() | paths = _disabledpaths() | ||||
if not paths: | if not paths: | ||||
raise error.UnknownCommand(cmd) | raise error.UnknownCommand(cmd) | ||||
ext = None | ext = None | ||||
# first, search for an extension with the same name as the command | # first, search for an extension with the same name as the command | ||||
path = paths.pop(cmd, None) | path = paths.pop(cmd, None) |
def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None): | def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None): | ||||
return _filemerge( | return _filemerge( | ||||
False, repo, wctx, mynode, orig, fcd, fco, fca, labels=labels | False, repo, wctx, mynode, orig, fcd, fco, fca, labels=labels | ||||
) | ) | ||||
def loadinternalmerge(ui, extname, registrarobj): | def loadinternalmerge(ui, extname, registrarobj): | ||||
"""Load internal merge tool from specified registrarobj | """Load internal merge tool from specified registrarobj""" | ||||
""" | |||||
for name, func in pycompat.iteritems(registrarobj._table): | for name, func in pycompat.iteritems(registrarobj._table): | ||||
fullname = b':' + name | fullname = b':' + name | ||||
internals[fullname] = func | internals[fullname] = func | ||||
internals[b'internal:' + name] = func | internals[b'internal:' + name] = func | ||||
internalsdoc[fullname] = func | internalsdoc[fullname] = func | ||||
capabilities = sorted([k for k, v in func.capabilities.items() if v]) | capabilities = sorted([k for k, v in func.capabilities.items() if v]) | ||||
if capabilities: | if capabilities: |
# x - argument in tree form | # x - argument in tree form | ||||
symbols = filesetlang.symbols | symbols = filesetlang.symbols | ||||
predicate = registrar.filesetpredicate(symbols) | predicate = registrar.filesetpredicate(symbols) | ||||
@predicate(b'modified()', callstatus=True, weight=_WEIGHT_STATUS) | @predicate(b'modified()', callstatus=True, weight=_WEIGHT_STATUS) | ||||
def modified(mctx, x): | def modified(mctx, x): | ||||
"""File that is modified according to :hg:`status`. | """File that is modified according to :hg:`status`.""" | ||||
""" | |||||
# i18n: "modified" is a keyword | # i18n: "modified" is a keyword | ||||
getargs(x, 0, 0, _(b"modified takes no arguments")) | getargs(x, 0, 0, _(b"modified takes no arguments")) | ||||
s = set(mctx.status().modified) | s = set(mctx.status().modified) | ||||
return mctx.predicate(s.__contains__, predrepr=b'modified') | return mctx.predicate(s.__contains__, predrepr=b'modified') | ||||
@predicate(b'added()', callstatus=True, weight=_WEIGHT_STATUS) | @predicate(b'added()', callstatus=True, weight=_WEIGHT_STATUS) | ||||
def added(mctx, x): | def added(mctx, x): | ||||
"""File that is added according to :hg:`status`. | """File that is added according to :hg:`status`.""" | ||||
""" | |||||
# i18n: "added" is a keyword | # i18n: "added" is a keyword | ||||
getargs(x, 0, 0, _(b"added takes no arguments")) | getargs(x, 0, 0, _(b"added takes no arguments")) | ||||
s = set(mctx.status().added) | s = set(mctx.status().added) | ||||
return mctx.predicate(s.__contains__, predrepr=b'added') | return mctx.predicate(s.__contains__, predrepr=b'added') | ||||
@predicate(b'removed()', callstatus=True, weight=_WEIGHT_STATUS) | @predicate(b'removed()', callstatus=True, weight=_WEIGHT_STATUS) | ||||
def removed(mctx, x): | def removed(mctx, x): | ||||
"""File that is removed according to :hg:`status`. | """File that is removed according to :hg:`status`.""" | ||||
""" | |||||
# i18n: "removed" is a keyword | # i18n: "removed" is a keyword | ||||
getargs(x, 0, 0, _(b"removed takes no arguments")) | getargs(x, 0, 0, _(b"removed takes no arguments")) | ||||
s = set(mctx.status().removed) | s = set(mctx.status().removed) | ||||
return mctx.predicate(s.__contains__, predrepr=b'removed') | return mctx.predicate(s.__contains__, predrepr=b'removed') | ||||
@predicate(b'deleted()', callstatus=True, weight=_WEIGHT_STATUS) | @predicate(b'deleted()', callstatus=True, weight=_WEIGHT_STATUS) | ||||
def deleted(mctx, x): | def deleted(mctx, x): | ||||
"""Alias for ``missing()``. | """Alias for ``missing()``.""" | ||||
""" | |||||
# i18n: "deleted" is a keyword | # i18n: "deleted" is a keyword | ||||
getargs(x, 0, 0, _(b"deleted takes no arguments")) | getargs(x, 0, 0, _(b"deleted takes no arguments")) | ||||
s = set(mctx.status().deleted) | s = set(mctx.status().deleted) | ||||
return mctx.predicate(s.__contains__, predrepr=b'deleted') | return mctx.predicate(s.__contains__, predrepr=b'deleted') | ||||
@predicate(b'missing()', callstatus=True, weight=_WEIGHT_STATUS) | @predicate(b'missing()', callstatus=True, weight=_WEIGHT_STATUS) | ||||
def missing(mctx, x): | def missing(mctx, x): | ||||
"""File that is missing according to :hg:`status`. | """File that is missing according to :hg:`status`.""" | ||||
""" | |||||
# i18n: "missing" is a keyword | # i18n: "missing" is a keyword | ||||
getargs(x, 0, 0, _(b"missing takes no arguments")) | getargs(x, 0, 0, _(b"missing takes no arguments")) | ||||
s = set(mctx.status().deleted) | s = set(mctx.status().deleted) | ||||
return mctx.predicate(s.__contains__, predrepr=b'deleted') | return mctx.predicate(s.__contains__, predrepr=b'deleted') | ||||
@predicate(b'unknown()', callstatus=True, weight=_WEIGHT_STATUS_THOROUGH) | @predicate(b'unknown()', callstatus=True, weight=_WEIGHT_STATUS_THOROUGH) | ||||
def unknown(mctx, x): | def unknown(mctx, x): | ||||
# i18n: "ignored" is a keyword | # i18n: "ignored" is a keyword | ||||
getargs(x, 0, 0, _(b"ignored takes no arguments")) | getargs(x, 0, 0, _(b"ignored takes no arguments")) | ||||
s = set(mctx.status().ignored) | s = set(mctx.status().ignored) | ||||
return mctx.predicate(s.__contains__, predrepr=b'ignored') | return mctx.predicate(s.__contains__, predrepr=b'ignored') | ||||
@predicate(b'clean()', callstatus=True, weight=_WEIGHT_STATUS) | @predicate(b'clean()', callstatus=True, weight=_WEIGHT_STATUS) | ||||
def clean(mctx, x): | def clean(mctx, x): | ||||
"""File that is clean according to :hg:`status`. | """File that is clean according to :hg:`status`.""" | ||||
""" | |||||
# i18n: "clean" is a keyword | # i18n: "clean" is a keyword | ||||
getargs(x, 0, 0, _(b"clean takes no arguments")) | getargs(x, 0, 0, _(b"clean takes no arguments")) | ||||
s = set(mctx.status().clean) | s = set(mctx.status().clean) | ||||
return mctx.predicate(s.__contains__, predrepr=b'clean') | return mctx.predicate(s.__contains__, predrepr=b'clean') | ||||
@predicate(b'tracked()') | @predicate(b'tracked()') | ||||
def tracked(mctx, x): | def tracked(mctx, x): | ||||
"""File that is under Mercurial control.""" | """File that is under Mercurial control.""" | ||||
# i18n: "tracked" is a keyword | # i18n: "tracked" is a keyword | ||||
getargs(x, 0, 0, _(b"tracked takes no arguments")) | getargs(x, 0, 0, _(b"tracked takes no arguments")) | ||||
return mctx.predicate(mctx.ctx.__contains__, predrepr=b'tracked') | return mctx.predicate(mctx.ctx.__contains__, predrepr=b'tracked') | ||||
@predicate(b'binary()', weight=_WEIGHT_READ_CONTENTS) | @predicate(b'binary()', weight=_WEIGHT_READ_CONTENTS) | ||||
def binary(mctx, x): | def binary(mctx, x): | ||||
"""File that appears to be binary (contains NUL bytes). | """File that appears to be binary (contains NUL bytes).""" | ||||
""" | |||||
# i18n: "binary" is a keyword | # i18n: "binary" is a keyword | ||||
getargs(x, 0, 0, _(b"binary takes no arguments")) | getargs(x, 0, 0, _(b"binary takes no arguments")) | ||||
return mctx.fpredicate( | return mctx.fpredicate( | ||||
lambda fctx: fctx.isbinary(), predrepr=b'binary', cache=True | lambda fctx: fctx.isbinary(), predrepr=b'binary', cache=True | ||||
) | ) | ||||
@predicate(b'exec()') | @predicate(b'exec()') | ||||
def exec_(mctx, x): | def exec_(mctx, x): | ||||
"""File that is marked as executable. | """File that is marked as executable.""" | ||||
""" | |||||
# i18n: "exec" is a keyword | # i18n: "exec" is a keyword | ||||
getargs(x, 0, 0, _(b"exec takes no arguments")) | getargs(x, 0, 0, _(b"exec takes no arguments")) | ||||
ctx = mctx.ctx | ctx = mctx.ctx | ||||
return mctx.predicate(lambda f: ctx.flags(f) == b'x', predrepr=b'exec') | return mctx.predicate(lambda f: ctx.flags(f) == b'x', predrepr=b'exec') | ||||
@predicate(b'symlink()') | @predicate(b'symlink()') | ||||
def symlink(mctx, x): | def symlink(mctx, x): | ||||
"""File that is marked as a symlink. | """File that is marked as a symlink.""" | ||||
""" | |||||
# i18n: "symlink" is a keyword | # i18n: "symlink" is a keyword | ||||
getargs(x, 0, 0, _(b"symlink takes no arguments")) | getargs(x, 0, 0, _(b"symlink takes no arguments")) | ||||
ctx = mctx.ctx | ctx = mctx.ctx | ||||
return mctx.predicate(lambda f: ctx.flags(f) == b'l', predrepr=b'symlink') | return mctx.predicate(lambda f: ctx.flags(f) == b'l', predrepr=b'symlink') | ||||
@predicate(b'resolved()', weight=_WEIGHT_STATUS) | @predicate(b'resolved()', weight=_WEIGHT_STATUS) | ||||
def resolved(mctx, x): | def resolved(mctx, x): | ||||
"""File that is marked resolved according to :hg:`resolve -l`. | """File that is marked resolved according to :hg:`resolve -l`.""" | ||||
""" | |||||
# i18n: "resolved" is a keyword | # i18n: "resolved" is a keyword | ||||
getargs(x, 0, 0, _(b"resolved takes no arguments")) | getargs(x, 0, 0, _(b"resolved takes no arguments")) | ||||
if mctx.ctx.rev() is not None: | if mctx.ctx.rev() is not None: | ||||
return mctx.never() | return mctx.never() | ||||
ms = mergestatemod.mergestate.read(mctx.ctx.repo()) | ms = mergestatemod.mergestate.read(mctx.ctx.repo()) | ||||
return mctx.predicate( | return mctx.predicate( | ||||
lambda f: f in ms and ms[f] == b'r', predrepr=b'resolved' | lambda f: f in ms and ms[f] == b'r', predrepr=b'resolved' | ||||
) | ) | ||||
@predicate(b'unresolved()', weight=_WEIGHT_STATUS) | @predicate(b'unresolved()', weight=_WEIGHT_STATUS) | ||||
def unresolved(mctx, x): | def unresolved(mctx, x): | ||||
"""File that is marked unresolved according to :hg:`resolve -l`. | """File that is marked unresolved according to :hg:`resolve -l`.""" | ||||
""" | |||||
# i18n: "unresolved" is a keyword | # i18n: "unresolved" is a keyword | ||||
getargs(x, 0, 0, _(b"unresolved takes no arguments")) | getargs(x, 0, 0, _(b"unresolved takes no arguments")) | ||||
if mctx.ctx.rev() is not None: | if mctx.ctx.rev() is not None: | ||||
return mctx.never() | return mctx.never() | ||||
ms = mergestatemod.mergestate.read(mctx.ctx.repo()) | ms = mergestatemod.mergestate.read(mctx.ctx.repo()) | ||||
return mctx.predicate( | return mctx.predicate( | ||||
lambda f: f in ms and ms[f] == b'u', predrepr=b'unresolved' | lambda f: f in ms and ms[f] == b'u', predrepr=b'unresolved' | ||||
) | ) | ||||
@predicate(b'hgignore()', weight=_WEIGHT_STATUS) | @predicate(b'hgignore()', weight=_WEIGHT_STATUS) | ||||
def hgignore(mctx, x): | def hgignore(mctx, x): | ||||
"""File that matches the active .hgignore pattern. | """File that matches the active .hgignore pattern.""" | ||||
""" | |||||
# i18n: "hgignore" is a keyword | # i18n: "hgignore" is a keyword | ||||
getargs(x, 0, 0, _(b"hgignore takes no arguments")) | getargs(x, 0, 0, _(b"hgignore takes no arguments")) | ||||
return mctx.ctx.repo().dirstate._ignore | return mctx.ctx.repo().dirstate._ignore | ||||
@predicate(b'portable()', weight=_WEIGHT_CHECK_FILENAME) | @predicate(b'portable()', weight=_WEIGHT_CHECK_FILENAME) | ||||
def portable(mctx, x): | def portable(mctx, x): | ||||
"""File that has a portable name. (This doesn't include filenames with case | """File that has a portable name. (This doesn't include filenames with case | ||||
collisions.) | collisions.) | ||||
""" | """ | ||||
# i18n: "portable" is a keyword | # i18n: "portable" is a keyword | ||||
getargs(x, 0, 0, _(b"portable takes no arguments")) | getargs(x, 0, 0, _(b"portable takes no arguments")) | ||||
return mctx.predicate( | return mctx.predicate( | ||||
lambda f: util.checkwinfilename(f) is None, predrepr=b'portable' | lambda f: util.checkwinfilename(f) is None, predrepr=b'portable' | ||||
) | ) | ||||
@predicate(b'grep(regex)', weight=_WEIGHT_READ_CONTENTS) | @predicate(b'grep(regex)', weight=_WEIGHT_READ_CONTENTS) | ||||
def grep(mctx, x): | def grep(mctx, x): | ||||
"""File contains the given regular expression. | """File contains the given regular expression.""" | ||||
""" | |||||
try: | try: | ||||
# i18n: "grep" is a keyword | # i18n: "grep" is a keyword | ||||
r = re.compile(getstring(x, _(b"grep requires a pattern"))) | r = re.compile(getstring(x, _(b"grep requires a pattern"))) | ||||
except re.error as e: | except re.error as e: | ||||
raise error.ParseError( | raise error.ParseError( | ||||
_(b'invalid match pattern: %s') % stringutil.forcebytestr(e) | _(b'invalid match pattern: %s') % stringutil.forcebytestr(e) | ||||
) | ) | ||||
return mctx.fpredicate( | return mctx.fpredicate( | ||||
return True | return True | ||||
return False | return False | ||||
return mctx.fpredicate(eolp, predrepr=(b'eol(%r)', enc), cache=True) | return mctx.fpredicate(eolp, predrepr=(b'eol(%r)', enc), cache=True) | ||||
@predicate(b'copied()') | @predicate(b'copied()') | ||||
def copied(mctx, x): | def copied(mctx, x): | ||||
"""File that is recorded as being copied. | """File that is recorded as being copied.""" | ||||
""" | |||||
# i18n: "copied" is a keyword | # i18n: "copied" is a keyword | ||||
getargs(x, 0, 0, _(b"copied takes no arguments")) | getargs(x, 0, 0, _(b"copied takes no arguments")) | ||||
def copiedp(fctx): | def copiedp(fctx): | ||||
p = fctx.parents() | p = fctx.parents() | ||||
return p and p[0].path() != fctx.path() | return p and p[0].path() != fctx.path() | ||||
return mctx.fpredicate(copiedp, predrepr=b'copied', cache=True) | return mctx.fpredicate(copiedp, predrepr=b'copied', cache=True) | ||||
raise error.ParseError(reverr) | raise error.ParseError(reverr) | ||||
basectx, ctx = scmutil.revpair(repo, [baserevspec, revspec]) | basectx, ctx = scmutil.revpair(repo, [baserevspec, revspec]) | ||||
mc = mctx.switch(basectx, ctx) | mc = mctx.switch(basectx, ctx) | ||||
return getmatch(mc, x) | return getmatch(mc, x) | ||||
@predicate(b'subrepo([pattern])') | @predicate(b'subrepo([pattern])') | ||||
def subrepo(mctx, x): | def subrepo(mctx, x): | ||||
"""Subrepositories whose paths match the given pattern. | """Subrepositories whose paths match the given pattern.""" | ||||
""" | |||||
# i18n: "subrepo" is a keyword | # i18n: "subrepo" is a keyword | ||||
getargs(x, 0, 1, _(b"subrepo takes at most one argument")) | getargs(x, 0, 1, _(b"subrepo takes at most one argument")) | ||||
ctx = mctx.ctx | ctx = mctx.ctx | ||||
sstate = ctx.substate | sstate = ctx.substate | ||||
if x: | if x: | ||||
pat = getpattern( | pat = getpattern( | ||||
x, | x, | ||||
matchmod.allpatternkinds, | matchmod.allpatternkinds, | ||||
tree = filesetlang.parse(expr) | tree = filesetlang.parse(expr) | ||||
tree = filesetlang.analyze(tree) | tree = filesetlang.analyze(tree) | ||||
tree = filesetlang.optimize(tree) | tree = filesetlang.optimize(tree) | ||||
mctx = matchctx(ctx.p1(), ctx, cwd, badfn=badfn) | mctx = matchctx(ctx.p1(), ctx, cwd, badfn=badfn) | ||||
return getmatch(mctx, tree) | return getmatch(mctx, tree) | ||||
def loadpredicate(ui, extname, registrarobj): | def loadpredicate(ui, extname, registrarobj): | ||||
"""Load fileset predicates from specified registrarobj | """Load fileset predicates from specified registrarobj""" | ||||
""" | |||||
for name, func in pycompat.iteritems(registrarobj._table): | for name, func in pycompat.iteritems(registrarobj._table): | ||||
symbols[name] = func | symbols[name] = func | ||||
# tell hggettext to extract docstrings from these functions: | # tell hggettext to extract docstrings from these functions: | ||||
i18nfunctions = symbols.values() | i18nfunctions = symbols.values() |
commands, | commands, | ||||
name, | name, | ||||
unknowncmd=False, | unknowncmd=False, | ||||
full=True, | full=True, | ||||
subtopic=None, | subtopic=None, | ||||
fullname=None, | fullname=None, | ||||
**opts | **opts | ||||
): | ): | ||||
''' | """ | ||||
Generate the help for 'name' as unformatted restructured text. If | Generate the help for 'name' as unformatted restructured text. If | ||||
'name' is None, describe the commands available. | 'name' is None, describe the commands available. | ||||
''' | """ | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
def helpcmd(name, subtopic=None): | def helpcmd(name, subtopic=None): | ||||
try: | try: | ||||
aliases, entry = cmdutil.findcmd( | aliases, entry = cmdutil.findcmd( | ||||
name, commands.table, strict=unknowncmd | name, commands.table, strict=unknowncmd | ||||
) | ) |
'''return a repository peer for the specified path''' | '''return a repository peer for the specified path''' | ||||
rui = remoteui(uiorrepo, opts) | rui = remoteui(uiorrepo, opts) | ||||
return _peerorrepo( | return _peerorrepo( | ||||
rui, path, create, intents=intents, createopts=createopts | rui, path, create, intents=intents, createopts=createopts | ||||
).peer() | ).peer() | ||||
def defaultdest(source): | def defaultdest(source): | ||||
'''return default destination of clone if none is given | """return default destination of clone if none is given | ||||
>>> defaultdest(b'foo') | >>> defaultdest(b'foo') | ||||
'foo' | 'foo' | ||||
>>> defaultdest(b'/foo/bar') | >>> defaultdest(b'/foo/bar') | ||||
'bar' | 'bar' | ||||
>>> defaultdest(b'/') | >>> defaultdest(b'/') | ||||
'' | '' | ||||
>>> defaultdest(b'') | >>> defaultdest(b'') | ||||
'' | '' | ||||
>>> defaultdest(b'http://example.org/') | >>> defaultdest(b'http://example.org/') | ||||
'' | '' | ||||
>>> defaultdest(b'http://example.org/foo/') | >>> defaultdest(b'http://example.org/foo/') | ||||
'foo' | 'foo' | ||||
''' | """ | ||||
path = util.url(source).path | path = util.url(source).path | ||||
if not path: | if not path: | ||||
return b'' | return b'' | ||||
return os.path.basename(os.path.normpath(path)) | return os.path.basename(os.path.normpath(path)) | ||||
def sharedreposource(repo): | def sharedreposource(repo): | ||||
"""Returns repository object for source repository of a shared repo. | """Returns repository object for source repository of a shared repo. | ||||
postshare(srcrepo, r, defaultpath=defaultpath) | postshare(srcrepo, r, defaultpath=defaultpath) | ||||
r = repository(ui, dest) | r = repository(ui, dest) | ||||
_postshareupdate(r, update, checkout=checkout) | _postshareupdate(r, update, checkout=checkout) | ||||
return r | return r | ||||
def _prependsourcehgrc(repo): | def _prependsourcehgrc(repo): | ||||
""" copies the source repo config and prepend it in current repo .hg/hgrc | """copies the source repo config and prepend it in current repo .hg/hgrc | ||||
on unshare. This is only done if the share was perfomed using share safe | on unshare. This is only done if the share was perfomed using share safe | ||||
method where we share config of source in shares""" | method where we share config of source in shares""" | ||||
srcvfs = vfsmod.vfs(repo.sharedpath) | srcvfs = vfsmod.vfs(repo.sharedpath) | ||||
dstvfs = vfsmod.vfs(repo.path) | dstvfs = vfsmod.vfs(repo.path) | ||||
if not srcvfs.exists(b'hgrc'): | if not srcvfs.exists(b'hgrc'): | ||||
return | return | ||||
uprev = repo.lookup(test) | uprev = repo.lookup(test) | ||||
break | break | ||||
except error.RepoLookupError: | except error.RepoLookupError: | ||||
continue | continue | ||||
_update(repo, uprev) | _update(repo, uprev) | ||||
def copystore(ui, srcrepo, destpath): | def copystore(ui, srcrepo, destpath): | ||||
'''copy files from store of srcrepo in destpath | """copy files from store of srcrepo in destpath | ||||
returns destlock | returns destlock | ||||
''' | """ | ||||
destlock = None | destlock = None | ||||
try: | try: | ||||
hardlink = None | hardlink = None | ||||
topic = _(b'linking') if hardlink else _(b'copying') | topic = _(b'linking') if hardlink else _(b'copying') | ||||
with ui.makeprogress(topic, unit=_(b'files')) as progress: | with ui.makeprogress(topic, unit=_(b'files')) as progress: | ||||
num = 0 | num = 0 | ||||
srcpublishing = srcrepo.publishing() | srcpublishing = srcrepo.publishing() | ||||
srcvfs = vfsmod.vfs(srcrepo.sharedpath) | srcvfs = vfsmod.vfs(srcrepo.sharedpath) | ||||
) | ) | ||||
) | ) | ||||
# TODO this is batchable. | # TODO this is batchable. | ||||
remoterevs = [] | remoterevs = [] | ||||
for r in rev: | for r in rev: | ||||
with srcpeer.commandexecutor() as e: | with srcpeer.commandexecutor() as e: | ||||
remoterevs.append( | remoterevs.append( | ||||
e.callcommand(b'lookup', {b'key': r,}).result() | e.callcommand( | ||||
b'lookup', | |||||
{ | |||||
b'key': r, | |||||
}, | |||||
).result() | |||||
) | ) | ||||
revs = remoterevs | revs = remoterevs | ||||
# Obtain a lock before checking for or cloning the pooled repo otherwise | # Obtain a lock before checking for or cloning the pooled repo otherwise | ||||
# 2 clients may race creating or populating it. | # 2 clients may race creating or populating it. | ||||
pooldir = os.path.dirname(sharepath) | pooldir = os.path.dirname(sharepath) | ||||
# lock class requires the directory to exist. | # lock class requires the directory to exist. | ||||
try: | try: | ||||
if sharenamemode == b'identity': | if sharenamemode == b'identity': | ||||
# Resolve the name from the initial changeset in the remote | # Resolve the name from the initial changeset in the remote | ||||
# repository. This returns nullid when the remote is empty. It | # repository. This returns nullid when the remote is empty. It | ||||
# raises RepoLookupError if revision 0 is filtered or otherwise | # raises RepoLookupError if revision 0 is filtered or otherwise | ||||
# not available. If we fail to resolve, sharing is not enabled. | # not available. If we fail to resolve, sharing is not enabled. | ||||
try: | try: | ||||
with srcpeer.commandexecutor() as e: | with srcpeer.commandexecutor() as e: | ||||
rootnode = e.callcommand( | rootnode = e.callcommand( | ||||
b'lookup', {b'key': b'0',} | b'lookup', | ||||
{ | |||||
b'key': b'0', | |||||
}, | |||||
).result() | ).result() | ||||
if rootnode != node.nullid: | if rootnode != node.nullid: | ||||
sharepath = os.path.join(sharepool, node.hex(rootnode)) | sharepath = os.path.join(sharepool, node.hex(rootnode)) | ||||
else: | else: | ||||
ui.status( | ui.status( | ||||
_( | _( | ||||
b'(not using pooled storage: ' | b'(not using pooled storage: ' | ||||
) | ) | ||||
) | ) | ||||
# TODO this is batchable. | # TODO this is batchable. | ||||
remoterevs = [] | remoterevs = [] | ||||
for rev in revs: | for rev in revs: | ||||
with srcpeer.commandexecutor() as e: | with srcpeer.commandexecutor() as e: | ||||
remoterevs.append( | remoterevs.append( | ||||
e.callcommand(b'lookup', {b'key': rev,}).result() | e.callcommand( | ||||
b'lookup', | |||||
{ | |||||
b'key': rev, | |||||
}, | |||||
).result() | |||||
) | ) | ||||
revs = remoterevs | revs = remoterevs | ||||
checkout = revs[0] | checkout = revs[0] | ||||
else: | else: | ||||
revs = None | revs = None | ||||
local = destpeer.local() | local = destpeer.local() | ||||
if local: | if local: | ||||
if ui.configbool(b'experimental', b'remotenames'): | if ui.configbool(b'experimental', b'remotenames'): | ||||
logexchange.pullremotenames(destrepo, srcpeer) | logexchange.pullremotenames(destrepo, srcpeer) | ||||
if update: | if update: | ||||
if update is not True: | if update is not True: | ||||
with srcpeer.commandexecutor() as e: | with srcpeer.commandexecutor() as e: | ||||
checkout = e.callcommand( | checkout = e.callcommand( | ||||
b'lookup', {b'key': update,} | b'lookup', | ||||
{ | |||||
b'key': update, | |||||
}, | |||||
).result() | ).result() | ||||
uprev = None | uprev = None | ||||
status = None | status = None | ||||
if checkout is not None: | if checkout is not None: | ||||
# Some extensions (at least hg-git and hg-subversion) have | # Some extensions (at least hg-git and hg-subversion) have | ||||
# a peer.lookup() implementation that returns a name instead | # a peer.lookup() implementation that returns a name instead | ||||
# of a nodeid. We work around it here until we've figured | # of a nodeid. We work around it here until we've figured | ||||
if warndest: | if warndest: | ||||
destutil.statusotherdests(ui, repo) | destutil.statusotherdests(ui, repo) | ||||
return ret | return ret | ||||
def merge( | def merge( | ||||
ctx, force=False, remind=True, labels=None, | ctx, | ||||
force=False, | |||||
remind=True, | |||||
labels=None, | |||||
): | ): | ||||
"""Branch merge with node, resolving changes. Return true if any | """Branch merge with node, resolving changes. Return true if any | ||||
unresolved conflicts.""" | unresolved conflicts.""" | ||||
repo = ctx.repo() | repo = ctx.repo() | ||||
stats = mergemod.merge(ctx, force=force, labels=labels) | stats = mergemod.merge(ctx, force=force, labels=labels) | ||||
_showstats(repo, stats) | _showstats(repo, stats) | ||||
if stats.unresolvedcount: | if stats.unresolvedcount: | ||||
repo.ui.status( | repo.ui.status( |
from . import ( | from . import ( | ||||
hgweb_mod, | hgweb_mod, | ||||
hgwebdir_mod, | hgwebdir_mod, | ||||
server, | server, | ||||
) | ) | ||||
def hgweb(config, name=None, baseui=None): | def hgweb(config, name=None, baseui=None): | ||||
'''create an hgweb wsgi object | """create an hgweb wsgi object | ||||
config can be one of: | config can be one of: | ||||
- repo object (single repo view) | - repo object (single repo view) | ||||
- path to repo (single repo view) | - path to repo (single repo view) | ||||
- path to config file (multi-repo view) | - path to config file (multi-repo view) | ||||
- dict of virtual:real pairs (multi-repo view) | - dict of virtual:real pairs (multi-repo view) | ||||
- list of virtual:real tuples (multi-repo view) | - list of virtual:real tuples (multi-repo view) | ||||
''' | """ | ||||
if isinstance(config, pycompat.unicode): | if isinstance(config, pycompat.unicode): | ||||
raise error.ProgrammingError( | raise error.ProgrammingError( | ||||
b'Mercurial only supports encoded strings: %r' % config | b'Mercurial only supports encoded strings: %r' % config | ||||
) | ) | ||||
if ( | if ( | ||||
(isinstance(config, bytes) and not os.path.isdir(config)) | (isinstance(config, bytes) and not os.path.isdir(config)) | ||||
or isinstance(config, dict) | or isinstance(config, dict) |
If userlist has a single '*' member, all users are considered members. | If userlist has a single '*' member, all users are considered members. | ||||
Can be overridden by extensions to provide more complex authorization | Can be overridden by extensions to provide more complex authorization | ||||
schemes. | schemes. | ||||
""" | """ | ||||
return userlist == [b'*'] or username in userlist | return userlist == [b'*'] or username in userlist | ||||
def checkauthz(hgweb, req, op): | def checkauthz(hgweb, req, op): | ||||
'''Check permission for operation based on request data (including | """Check permission for operation based on request data (including | ||||
authentication info). Return if op allowed, else raise an ErrorResponse | authentication info). Return if op allowed, else raise an ErrorResponse | ||||
exception.''' | exception.""" | ||||
user = req.remoteuser | user = req.remoteuser | ||||
deny_read = hgweb.configlist(b'web', b'deny_read') | deny_read = hgweb.configlist(b'web', b'deny_read') | ||||
if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)): | if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)): | ||||
raise ErrorResponse(HTTP_UNAUTHORIZED, b'read not authorized') | raise ErrorResponse(HTTP_UNAUTHORIZED, b'read not authorized') | ||||
allow_read = hgweb.configlist(b'web', b'allow_read') | allow_read = hgweb.configlist(b'web', b'allow_read') |
mapfile, fp = templater.try_open_template(location, path) | mapfile, fp = templater.try_open_template(location, path) | ||||
if mapfile: | if mapfile: | ||||
return style, mapfile, fp | return style, mapfile, fp | ||||
raise RuntimeError(b"No hgweb templates found in %r" % path) | raise RuntimeError(b"No hgweb templates found in %r" % path) | ||||
def makebreadcrumb(url, prefix=b''): | def makebreadcrumb(url, prefix=b''): | ||||
'''Return a 'URL breadcrumb' list | """Return a 'URL breadcrumb' list | ||||
A 'URL breadcrumb' is a list of URL-name pairs, | A 'URL breadcrumb' is a list of URL-name pairs, | ||||
corresponding to each of the path items on a URL. | corresponding to each of the path items on a URL. | ||||
This can be used to create path navigation entries. | This can be used to create path navigation entries. | ||||
''' | """ | ||||
if url.endswith(b'/'): | if url.endswith(b'/'): | ||||
url = url[:-1] | url = url[:-1] | ||||
if prefix: | if prefix: | ||||
url = b'/' + prefix + url | url = b'/' + prefix + url | ||||
relpath = url | relpath = url | ||||
if relpath.startswith(b'/'): | if relpath.startswith(b'/'): | ||||
relpath = relpath[1:] | relpath = relpath[1:] | ||||
b'is a generator?' | b'is a generator?' | ||||
) | ) | ||||
assert self._bodywritefn | assert self._bodywritefn | ||||
return offsettrackingwriter(self._bodywritefn) | return offsettrackingwriter(self._bodywritefn) | ||||
def wsgiapplication(app_maker): | def wsgiapplication(app_maker): | ||||
'''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir() | """For compatibility with old CGI scripts. A plain hgweb() or hgwebdir() | ||||
can and should now be used as a WSGI application.''' | can and should now be used as a WSGI application.""" | ||||
application = app_maker() | application = app_maker() | ||||
def run_wsgi(env, respond): | def run_wsgi(env, respond): | ||||
return application(env, respond) | return application(env, respond) | ||||
return run_wsgi | return run_wsgi |
b'tags': nodetagsdict(repo, node), | b'tags': nodetagsdict(repo, node), | ||||
b'bookmarks': nodebookmarksdict(repo, node), | b'bookmarks': nodebookmarksdict(repo, node), | ||||
b'parent': lambda context, mapping: parents(ctx), | b'parent': lambda context, mapping: parents(ctx), | ||||
b'child': lambda context, mapping: children(ctx), | b'child': lambda context, mapping: children(ctx), | ||||
} | } | ||||
def changelistentry(web, ctx): | def changelistentry(web, ctx): | ||||
'''Obtain a dictionary to be used for entries in a changelist. | """Obtain a dictionary to be used for entries in a changelist. | ||||
This function is called when producing items for the "entries" list passed | This function is called when producing items for the "entries" list passed | ||||
to the "shortlog" and "changelog" templates. | to the "shortlog" and "changelog" templates. | ||||
''' | """ | ||||
repo = web.repo | repo = web.repo | ||||
rev = ctx.rev() | rev = ctx.rev() | ||||
n = scmutil.binnode(ctx) | n = scmutil.binnode(ctx) | ||||
showtags = showtag(repo, b'changelogtag', n) | showtags = showtag(repo, b'changelogtag', n) | ||||
files = listfilediffs(ctx.files(), n, web.maxfiles) | files = listfilediffs(ctx.files(), n, web.maxfiles) | ||||
entry = commonentry(repo, ctx) | entry = commonentry(repo, ctx) | ||||
entry.update( | entry.update( |
from .utils import ( | from .utils import ( | ||||
procutil, | procutil, | ||||
resourceutil, | resourceutil, | ||||
stringutil, | stringutil, | ||||
) | ) | ||||
def pythonhook(ui, repo, htype, hname, funcname, args, throw): | def pythonhook(ui, repo, htype, hname, funcname, args, throw): | ||||
'''call python hook. hook is callable object, looked up as | """call python hook. hook is callable object, looked up as | ||||
name in python module. if callable returns "true", hook | name in python module. if callable returns "true", hook | ||||
fails, else passes. if hook raises exception, treated as | fails, else passes. if hook raises exception, treated as | ||||
hook failure. exception propagates if throw is "true". | hook failure. exception propagates if throw is "true". | ||||
reason for "true" meaning "hook failed" is so that | reason for "true" meaning "hook failed" is so that | ||||
unmodified commands (e.g. mercurial.commands.update) can | unmodified commands (e.g. mercurial.commands.update) can | ||||
be run as hooks without wrappers to convert return values.''' | be run as hooks without wrappers to convert return values.""" | ||||
if callable(funcname): | if callable(funcname): | ||||
obj = funcname | obj = funcname | ||||
funcname = pycompat.sysbytes(obj.__module__ + "." + obj.__name__) | funcname = pycompat.sysbytes(obj.__module__ + "." + obj.__name__) | ||||
else: | else: | ||||
d = funcname.rfind(b'.') | d = funcname.rfind(b'.') | ||||
if d == -1: | if d == -1: | ||||
raise error.HookLoadError( | raise error.HookLoadError( |
permissions.remove(b'pull') | permissions.remove(b'pull') | ||||
if len(permissions) > 1: | if len(permissions) > 1: | ||||
raise error.RepoError( | raise error.RepoError( | ||||
_(b'cannot make request requiring multiple permissions: %s') | _(b'cannot make request requiring multiple permissions: %s') | ||||
% _(b', ').join(sorted(permissions)) | % _(b', ').join(sorted(permissions)) | ||||
) | ) | ||||
permission = {b'push': b'rw', b'pull': b'ro',}[permissions.pop()] | permission = { | ||||
b'push': b'rw', | |||||
b'pull': b'ro', | |||||
}[permissions.pop()] | |||||
handler, resp = sendv2request( | handler, resp = sendv2request( | ||||
self._ui, | self._ui, | ||||
self._opener, | self._opener, | ||||
self._requestbuilder, | self._requestbuilder, | ||||
self._apiurl, | self._apiurl, | ||||
permission, | permission, | ||||
calls, | calls, | ||||
# init | # init | ||||
# Callable receiving (ui, repourl, servicepath, opener, requestbuilder, | # Callable receiving (ui, repourl, servicepath, opener, requestbuilder, | ||||
# apidescriptor) to create a peer. | # apidescriptor) to create a peer. | ||||
# | # | ||||
# priority | # priority | ||||
# Integer priority for the service. If we could choose from multiple | # Integer priority for the service. If we could choose from multiple | ||||
# services, we choose the one with the highest priority. | # services, we choose the one with the highest priority. | ||||
API_PEERS = { | API_PEERS = { | ||||
wireprototypes.HTTP_WIREPROTO_V2: {b'init': httpv2peer, b'priority': 50,}, | wireprototypes.HTTP_WIREPROTO_V2: { | ||||
b'init': httpv2peer, | |||||
b'priority': 50, | |||||
}, | |||||
} | } | ||||
def performhandshake(ui, url, opener, requestbuilder): | def performhandshake(ui, url, opener, requestbuilder): | ||||
# The handshake is a request to the capabilities command. | # The handshake is a request to the capabilities command. | ||||
caps = None | caps = None | ||||
from __future__ import absolute_import, print_function | from __future__ import absolute_import, print_function | ||||
import contextlib | import contextlib | ||||
from .. import node as nodemod | from .. import node as nodemod | ||||
from . import util as interfaceutil | from . import util as interfaceutil | ||||
class idirstate(interfaceutil.Interface): | class idirstate(interfaceutil.Interface): | ||||
def __init__(opener, ui, root, validate, sparsematchfn): | def __init__(opener, ui, root, validate, sparsematchfn): | ||||
'''Create a new dirstate object. | """Create a new dirstate object. | ||||
opener is an open()-like callable that can be used to open the | opener is an open()-like callable that can be used to open the | ||||
dirstate file; root is the root of the directory tracked by | dirstate file; root is the root of the directory tracked by | ||||
the dirstate. | the dirstate. | ||||
''' | """ | ||||
# TODO: all these private methods and attributes should be made | # TODO: all these private methods and attributes should be made | ||||
# public or removed from the interface. | # public or removed from the interface. | ||||
_ignore = interfaceutil.Attribute("""Matcher for ignored files.""") | _ignore = interfaceutil.Attribute("""Matcher for ignored files.""") | ||||
def _ignorefiles(): | def _ignorefiles(): | ||||
"""Return a list of files containing patterns to ignore.""" | """Return a list of files containing patterns to ignore.""" | ||||
def _ignorefileandline(f): | def _ignorefileandline(f): | ||||
"""Given a file `f`, return the ignore file and line that ignores it.""" | """Given a file `f`, return the ignore file and line that ignores it.""" | ||||
_checklink = interfaceutil.Attribute("""Callable for checking symlinks.""") | _checklink = interfaceutil.Attribute("""Callable for checking symlinks.""") | ||||
_checkexec = interfaceutil.Attribute("""Callable for checking exec bits.""") | _checkexec = interfaceutil.Attribute("""Callable for checking exec bits.""") | ||||
@contextlib.contextmanager | @contextlib.contextmanager | ||||
def parentchange(): | def parentchange(): | ||||
'''Context manager for handling dirstate parents. | """Context manager for handling dirstate parents. | ||||
If an exception occurs in the scope of the context manager, | If an exception occurs in the scope of the context manager, | ||||
the incoherent dirstate won't be written when wlock is | the incoherent dirstate won't be written when wlock is | ||||
released. | released. | ||||
''' | """ | ||||
def pendingparentchange(): | def pendingparentchange(): | ||||
'''Returns true if the dirstate is in the middle of a set of changes | """Returns true if the dirstate is in the middle of a set of changes | ||||
that modify the dirstate parent. | that modify the dirstate parent. | ||||
''' | """ | ||||
def hasdir(d): | def hasdir(d): | ||||
pass | pass | ||||
def flagfunc(buildfallback): | def flagfunc(buildfallback): | ||||
pass | pass | ||||
def getcwd(): | def getcwd(): | ||||
'''Return the path from which a canonical path is calculated. | """Return the path from which a canonical path is calculated. | ||||
This path should be used to resolve file patterns or to convert | This path should be used to resolve file patterns or to convert | ||||
canonical paths back to file paths for display. It shouldn't be | canonical paths back to file paths for display. It shouldn't be | ||||
used to get real file paths. Use vfs functions instead. | used to get real file paths. Use vfs functions instead. | ||||
''' | """ | ||||
def pathto(f, cwd=None): | def pathto(f, cwd=None): | ||||
pass | pass | ||||
def __getitem__(key): | def __getitem__(key): | ||||
'''Return the current state of key (a filename) in the dirstate. | """Return the current state of key (a filename) in the dirstate. | ||||
States are: | States are: | ||||
n normal | n normal | ||||
m needs merging | m needs merging | ||||
r marked for removal | r marked for removal | ||||
a marked for addition | a marked for addition | ||||
? not tracked | ? not tracked | ||||
''' | """ | ||||
def __contains__(key): | def __contains__(key): | ||||
"""Check if bytestring `key` is known to the dirstate.""" | """Check if bytestring `key` is known to the dirstate.""" | ||||
def __iter__(): | def __iter__(): | ||||
"""Iterate the dirstate's contained filenames as bytestrings.""" | """Iterate the dirstate's contained filenames as bytestrings.""" | ||||
def items(): | def items(): | ||||
See localrepo.setparents() | See localrepo.setparents() | ||||
""" | """ | ||||
def setbranch(branch): | def setbranch(branch): | ||||
pass | pass | ||||
def invalidate(): | def invalidate(): | ||||
'''Causes the next access to reread the dirstate. | """Causes the next access to reread the dirstate. | ||||
This is different from localrepo.invalidatedirstate() because it always | This is different from localrepo.invalidatedirstate() because it always | ||||
rereads the dirstate. Use localrepo.invalidatedirstate() if you want to | rereads the dirstate. Use localrepo.invalidatedirstate() if you want to | ||||
check whether the dirstate has changed before rereading it.''' | check whether the dirstate has changed before rereading it.""" | ||||
def copy(source, dest): | def copy(source, dest): | ||||
"""Mark dest as a copy of source. Unmark dest if source is None.""" | """Mark dest as a copy of source. Unmark dest if source is None.""" | ||||
def copied(file): | def copied(file): | ||||
pass | pass | ||||
def copies(): | def copies(): | ||||
pass | pass | ||||
def normal(f, parentfiledata=None): | def normal(f, parentfiledata=None): | ||||
'''Mark a file normal and clean. | """Mark a file normal and clean. | ||||
parentfiledata: (mode, size, mtime) of the clean file | parentfiledata: (mode, size, mtime) of the clean file | ||||
parentfiledata should be computed from memory (for mode, | parentfiledata should be computed from memory (for mode, | ||||
size), as or close as possible from the point where we | size), as or close as possible from the point where we | ||||
determined the file was clean, to limit the risk of the | determined the file was clean, to limit the risk of the | ||||
file having been changed by an external process between the | file having been changed by an external process between the | ||||
moment where the file was determined to be clean and now.''' | moment where the file was determined to be clean and now.""" | ||||
pass | pass | ||||
def normallookup(f): | def normallookup(f): | ||||
'''Mark a file normal, but possibly dirty.''' | '''Mark a file normal, but possibly dirty.''' | ||||
def otherparent(f): | def otherparent(f): | ||||
'''Mark as coming from the other parent, always dirty.''' | '''Mark as coming from the other parent, always dirty.''' | ||||
def add(f): | def add(f): | ||||
'''Mark a file added.''' | '''Mark a file added.''' | ||||
def remove(f): | def remove(f): | ||||
'''Mark a file removed.''' | '''Mark a file removed.''' | ||||
def merge(f): | def merge(f): | ||||
'''Mark a file merged.''' | '''Mark a file merged.''' | ||||
def drop(f): | def drop(f): | ||||
'''Drop a file from the dirstate''' | '''Drop a file from the dirstate''' | ||||
def normalize(path, isknown=False, ignoremissing=False): | def normalize(path, isknown=False, ignoremissing=False): | ||||
''' | """ | ||||
normalize the case of a pathname when on a casefolding filesystem | normalize the case of a pathname when on a casefolding filesystem | ||||
isknown specifies whether the filename came from walking the | isknown specifies whether the filename came from walking the | ||||
disk, to avoid extra filesystem access. | disk, to avoid extra filesystem access. | ||||
If ignoremissing is True, missing path are returned | If ignoremissing is True, missing path are returned | ||||
unchanged. Otherwise, we try harder to normalize possibly | unchanged. Otherwise, we try harder to normalize possibly | ||||
existing path components. | existing path components. | ||||
The normalized case is determined based on the following precedence: | The normalized case is determined based on the following precedence: | ||||
- version of name already stored in the dirstate | - version of name already stored in the dirstate | ||||
- version of name stored on disk | - version of name stored on disk | ||||
- version provided via command arguments | - version provided via command arguments | ||||
''' | """ | ||||
def clear(): | def clear(): | ||||
pass | pass | ||||
def rebuild(parent, allfiles, changedfiles=None): | def rebuild(parent, allfiles, changedfiles=None): | ||||
pass | pass | ||||
def identity(): | def identity(): | ||||
'''Return identity of dirstate it to detect changing in storage | """Return identity of dirstate it to detect changing in storage | ||||
If identity of previous dirstate is equal to this, writing | If identity of previous dirstate is equal to this, writing | ||||
changes based on the former dirstate out can keep consistency. | changes based on the former dirstate out can keep consistency. | ||||
''' | """ | ||||
def write(tr): | def write(tr): | ||||
pass | pass | ||||
def addparentchangecallback(category, callback): | def addparentchangecallback(category, callback): | ||||
"""add a callback to be called when the wd parents are changed | """add a callback to be called when the wd parents are changed | ||||
Callback will be called with the following arguments: | Callback will be called with the following arguments: | ||||
dirstate, (oldp1, oldp2), (newp1, newp2) | dirstate, (oldp1, oldp2), (newp1, newp2) | ||||
Category is a unique identifier to allow overwriting an old callback | Category is a unique identifier to allow overwriting an old callback | ||||
with a newer callback. | with a newer callback. | ||||
""" | """ | ||||
def walk(match, subrepos, unknown, ignored, full=True): | def walk(match, subrepos, unknown, ignored, full=True): | ||||
''' | """ | ||||
Walk recursively through the directory tree, finding all files | Walk recursively through the directory tree, finding all files | ||||
matched by match. | matched by match. | ||||
If full is False, maybe skip some known-clean files. | If full is False, maybe skip some known-clean files. | ||||
Return a dict mapping filename to stat-like object (either | Return a dict mapping filename to stat-like object (either | ||||
mercurial.osutil.stat instance or return value of os.stat()). | mercurial.osutil.stat instance or return value of os.stat()). | ||||
''' | """ | ||||
def status(match, subrepos, ignored, clean, unknown): | def status(match, subrepos, ignored, clean, unknown): | ||||
'''Determine the status of the working copy relative to the | """Determine the status of the working copy relative to the | ||||
dirstate and return a pair of (unsure, status), where status is of type | dirstate and return a pair of (unsure, status), where status is of type | ||||
scmutil.status and: | scmutil.status and: | ||||
unsure: | unsure: | ||||
files that might have been modified since the dirstate was | files that might have been modified since the dirstate was | ||||
written, but need to be read to be sure (size is the same | written, but need to be read to be sure (size is the same | ||||
but mtime differs) | but mtime differs) | ||||
status.modified: | status.modified: | ||||
files that have definitely been modified since the dirstate | files that have definitely been modified since the dirstate | ||||
was written (different size or mode) | was written (different size or mode) | ||||
status.clean: | status.clean: | ||||
files that have definitely not been modified since the | files that have definitely not been modified since the | ||||
dirstate was written | dirstate was written | ||||
''' | """ | ||||
def matches(match): | def matches(match): | ||||
''' | """ | ||||
return files in the dirstate (in whatever state) filtered by match | return files in the dirstate (in whatever state) filtered by match | ||||
''' | """ | ||||
def savebackup(tr, backupname): | def savebackup(tr, backupname): | ||||
'''Save current dirstate into backup file''' | '''Save current dirstate into backup file''' | ||||
def restorebackup(tr, backupname): | def restorebackup(tr, backupname): | ||||
'''Restore dirstate by backup file''' | '''Restore dirstate by backup file''' | ||||
def clearbackup(tr, backupname): | def clearbackup(tr, backupname): | ||||
'''Clear backup file''' | '''Clear backup file''' |
def size(rev): | def size(rev): | ||||
"""Obtain the fulltext size of file data. | """Obtain the fulltext size of file data. | ||||
Any metadata is excluded from size measurements. | Any metadata is excluded from size measurements. | ||||
""" | """ | ||||
def revision(node, raw=False): | def revision(node, raw=False): | ||||
""""Obtain fulltext data for a node. | """ "Obtain fulltext data for a node. | ||||
By default, any storage transformations are applied before the data | By default, any storage transformations are applied before the data | ||||
is returned. If ``raw`` is True, non-raw storage transformations | is returned. If ``raw`` is True, non-raw storage transformations | ||||
are not applied. | are not applied. | ||||
The fulltext data may contain a header containing metadata. Most | The fulltext data may contain a header containing metadata. Most | ||||
consumers should use ``read()`` to obtain the actual file data. | consumers should use ``read()`` to obtain the actual file data. | ||||
""" | """ | ||||
def rawdata(node): | def rawdata(node): | ||||
"""Obtain raw data for a node. | """Obtain raw data for a node.""" | ||||
""" | |||||
def read(node): | def read(node): | ||||
"""Resolve file fulltext data. | """Resolve file fulltext data. | ||||
This is similar to ``revision()`` except any metadata in the data | This is similar to ``revision()`` except any metadata in the data | ||||
headers is stripped. | headers is stripped. | ||||
""" | """ | ||||
DEBUG = None | DEBUG = None | ||||
class ConnectionManager(object): | class ConnectionManager(object): | ||||
""" | """ | ||||
The connection manager must be able to: | The connection manager must be able to: | ||||
* keep track of all existing | * keep track of all existing | ||||
""" | """ | ||||
def __init__(self): | def __init__(self): | ||||
self._lock = threading.Lock() | self._lock = threading.Lock() | ||||
self._hostmap = collections.defaultdict(list) # host -> [connection] | self._hostmap = collections.defaultdict(list) # host -> [connection] | ||||
self._connmap = {} # map connections to host | self._connmap = {} # map connections to host | ||||
self._readymap = {} # map connection to ready state | self._readymap = {} # map connection to ready state | ||||
def add(self, host, connection, ready): | def add(self, host, connection, ready): | ||||
self._broken_pipe_resp = self.getresponse() | self._broken_pipe_resp = self.getresponse() | ||||
reraise = False | reraise = False | ||||
self.close() | self.close() | ||||
if reraise: | if reraise: | ||||
raise | raise | ||||
def wrapgetresponse(cls): | def wrapgetresponse(cls): | ||||
"""Wraps getresponse in cls with a broken-pipe sane version. | """Wraps getresponse in cls with a broken-pipe sane version.""" | ||||
""" | |||||
def safegetresponse(self): | def safegetresponse(self): | ||||
# In safesend() we might set the _broken_pipe_resp | # In safesend() we might set the _broken_pipe_resp | ||||
# attribute, in which case the socket has already | # attribute, in which case the socket has already | ||||
# been closed and we just need to give them the response | # been closed and we just need to give them the response | ||||
# back. Otherwise, we use the normal response path. | # back. Otherwise, we use the normal response path. | ||||
r = getattr(self, '_broken_pipe_resp', None) | r = getattr(self, '_broken_pipe_resp', None) | ||||
if r is not None: | if r is not None: |
# set of (path, vfs-location) tuples. vfs-location is: | # set of (path, vfs-location) tuples. vfs-location is: | ||||
# - 'plain for vfs relative paths | # - 'plain for vfs relative paths | ||||
# - '' for svfs relative paths | # - '' for svfs relative paths | ||||
_cachedfiles = set() | _cachedfiles = set() | ||||
class _basefilecache(scmutil.filecache): | class _basefilecache(scmutil.filecache): | ||||
"""All filecache usage on repo are done for logic that should be unfiltered | """All filecache usage on repo are done for logic that should be unfiltered""" | ||||
""" | |||||
def __get__(self, repo, type=None): | def __get__(self, repo, type=None): | ||||
if repo is None: | if repo is None: | ||||
return self | return self | ||||
# proxy to unfiltered __dict__ since filtered repo has no entry | # proxy to unfiltered __dict__ since filtered repo has no entry | ||||
unfi = repo.unfiltered() | unfi = repo.unfiltered() | ||||
try: | try: | ||||
return unfi.__dict__[self.sname] | return unfi.__dict__[self.sname] | ||||
def commandexecutor(self): | def commandexecutor(self): | ||||
return localcommandexecutor(self) | return localcommandexecutor(self) | ||||
# End of peer interface. | # End of peer interface. | ||||
@interfaceutil.implementer(repository.ipeerlegacycommands) | @interfaceutil.implementer(repository.ipeerlegacycommands) | ||||
class locallegacypeer(localpeer): | class locallegacypeer(localpeer): | ||||
'''peer extension which implements legacy methods too; used for tests with | """peer extension which implements legacy methods too; used for tests with | ||||
restricted capabilities''' | restricted capabilities""" | ||||
def __init__(self, repo): | def __init__(self, repo): | ||||
super(locallegacypeer, self).__init__(repo, caps=legacycaps) | super(locallegacypeer, self).__init__(repo, caps=legacycaps) | ||||
# Begin of baselegacywirecommands interface. | # Begin of baselegacywirecommands interface. | ||||
def between(self, pairs): | def between(self, pairs): | ||||
return self._repo.between(pairs) | return self._repo.between(pairs) | ||||
# | # | ||||
# The function receives a set of requirement strings that the repository | # The function receives a set of requirement strings that the repository | ||||
# is capable of opening. Functions will typically add elements to the | # is capable of opening. Functions will typically add elements to the | ||||
# set to reflect that the extension knows how to handle that requirements. | # set to reflect that the extension knows how to handle that requirements. | ||||
featuresetupfuncs = set() | featuresetupfuncs = set() | ||||
def _getsharedvfs(hgvfs, requirements): | def _getsharedvfs(hgvfs, requirements): | ||||
""" returns the vfs object pointing to root of shared source | """returns the vfs object pointing to root of shared source | ||||
repo for a shared repository | repo for a shared repository | ||||
hgvfs is vfs pointing at .hg/ of current repo (shared one) | hgvfs is vfs pointing at .hg/ of current repo (shared one) | ||||
requirements is a set of requirements of current repo (shared one) | requirements is a set of requirements of current repo (shared one) | ||||
""" | """ | ||||
# The ``shared`` or ``relshared`` requirements indicate the | # The ``shared`` or ``relshared`` requirements indicate the | ||||
# store lives in the path contained in the ``.hg/sharedpath`` file. | # store lives in the path contained in the ``.hg/sharedpath`` file. | ||||
# This is an absolute path for ``shared`` and relative to | # This is an absolute path for ``shared`` and relative to | ||||
# ``.hg/`` for ``relshared``. | # ``.hg/`` for ``relshared``. | ||||
sharedpath = hgvfs.read(b'sharedpath').rstrip(b'\n') | sharedpath = hgvfs.read(b'sharedpath').rstrip(b'\n') | ||||
if requirementsmod.RELATIVE_SHARED_REQUIREMENT in requirements: | if requirementsmod.RELATIVE_SHARED_REQUIREMENT in requirements: | ||||
sharedpath = hgvfs.join(sharedpath) | sharedpath = hgvfs.join(sharedpath) | ||||
sharedvfs = vfsmod.vfs(sharedpath, realpath=True) | sharedvfs = vfsmod.vfs(sharedpath, realpath=True) | ||||
if not sharedvfs.exists(): | if not sharedvfs.exists(): | ||||
raise error.RepoError( | raise error.RepoError( | ||||
_(b'.hg/sharedpath points to nonexistent directory %s') | _(b'.hg/sharedpath points to nonexistent directory %s') | ||||
% sharedvfs.base | % sharedvfs.base | ||||
) | ) | ||||
return sharedvfs | return sharedvfs | ||||
def _readrequires(vfs, allowmissing): | def _readrequires(vfs, allowmissing): | ||||
""" reads the require file present at root of this vfs | """reads the require file present at root of this vfs | ||||
and return a set of requirements | and return a set of requirements | ||||
If allowmissing is True, we suppress ENOENT if raised""" | If allowmissing is True, we suppress ENOENT if raised""" | ||||
# requires file contains a newline-delimited list of | # requires file contains a newline-delimited list of | ||||
# features/capabilities the opener (us) must have in order to use | # features/capabilities the opener (us) must have in order to use | ||||
# the repository. This file was introduced in Mercurial 0.9.2, | # the repository. This file was introduced in Mercurial 0.9.2, | ||||
# which means very old repositories may not have one. We assume | # which means very old repositories may not have one. We assume | ||||
# a missing file translates to no requirements. | # a missing file translates to no requirements. | ||||
# no need to pay the cost of repoview.changelog | # no need to pay the cost of repoview.changelog | ||||
unfi = self.unfiltered() | unfi = self.unfiltered() | ||||
return len(unfi.changelog) | return len(unfi.changelog) | ||||
def __iter__(self): | def __iter__(self): | ||||
return iter(self.changelog) | return iter(self.changelog) | ||||
def revs(self, expr, *args): | def revs(self, expr, *args): | ||||
'''Find revisions matching a revset. | """Find revisions matching a revset. | ||||
The revset is specified as a string ``expr`` that may contain | The revset is specified as a string ``expr`` that may contain | ||||
%-formatting to escape certain types. See ``revsetlang.formatspec``. | %-formatting to escape certain types. See ``revsetlang.formatspec``. | ||||
Revset aliases from the configuration are not expanded. To expand | Revset aliases from the configuration are not expanded. To expand | ||||
user aliases, consider calling ``scmutil.revrange()`` or | user aliases, consider calling ``scmutil.revrange()`` or | ||||
``repo.anyrevs([expr], user=True)``. | ``repo.anyrevs([expr], user=True)``. | ||||
Returns a smartset.abstractsmartset, which is a list-like interface | Returns a smartset.abstractsmartset, which is a list-like interface | ||||
that contains integer revisions. | that contains integer revisions. | ||||
''' | """ | ||||
tree = revsetlang.spectree(expr, *args) | tree = revsetlang.spectree(expr, *args) | ||||
return revset.makematcher(tree)(self) | return revset.makematcher(tree)(self) | ||||
def set(self, expr, *args): | def set(self, expr, *args): | ||||
'''Find revisions matching a revset and emit changectx instances. | """Find revisions matching a revset and emit changectx instances. | ||||
This is a convenience wrapper around ``revs()`` that iterates the | This is a convenience wrapper around ``revs()`` that iterates the | ||||
result and is a generator of changectx instances. | result and is a generator of changectx instances. | ||||
Revset aliases from the configuration are not expanded. To expand | Revset aliases from the configuration are not expanded. To expand | ||||
user aliases, consider calling ``scmutil.revrange()``. | user aliases, consider calling ``scmutil.revrange()``. | ||||
''' | """ | ||||
for r in self.revs(expr, *args): | for r in self.revs(expr, *args): | ||||
yield self[r] | yield self[r] | ||||
def anyrevs(self, specs, user=False, localalias=None): | def anyrevs(self, specs, user=False, localalias=None): | ||||
'''Find revisions matching one of the given revsets. | """Find revisions matching one of the given revsets. | ||||
Revset aliases from the configuration are not expanded by default. To | Revset aliases from the configuration are not expanded by default. To | ||||
expand user aliases, specify ``user=True``. To provide some local | expand user aliases, specify ``user=True``. To provide some local | ||||
definitions overriding user aliases, set ``localalias`` to | definitions overriding user aliases, set ``localalias`` to | ||||
``{name: definitionstring}``. | ``{name: definitionstring}``. | ||||
''' | """ | ||||
if specs == [b'null']: | if specs == [b'null']: | ||||
return revset.baseset([nullrev]) | return revset.baseset([nullrev]) | ||||
if specs == [b'.']: | if specs == [b'.']: | ||||
quick_data = self._quick_access_changeid.get(b'.') | quick_data = self._quick_access_changeid.get(b'.') | ||||
if quick_data is not None: | if quick_data is not None: | ||||
return revset.baseset([quick_data[0]]) | return revset.baseset([quick_data[0]]) | ||||
if user: | if user: | ||||
m = revset.matchany( | m = revset.matchany( | ||||
This a convenience method to aid invoking hooks. Extensions likely | This a convenience method to aid invoking hooks. Extensions likely | ||||
won't call this unless they have registered a custom hook or are | won't call this unless they have registered a custom hook or are | ||||
replacing code that is expected to call a hook. | replacing code that is expected to call a hook. | ||||
""" | """ | ||||
return hook.hook(self.ui, self, name, throw, **args) | return hook.hook(self.ui, self, name, throw, **args) | ||||
@filteredpropertycache | @filteredpropertycache | ||||
def _tagscache(self): | def _tagscache(self): | ||||
'''Returns a tagscache object that contains various tags related | """Returns a tagscache object that contains various tags related | ||||
caches.''' | caches.""" | ||||
# This simplifies its cache management by having one decorated | # This simplifies its cache management by having one decorated | ||||
# function (this one) and the rest simply fetch things from it. | # function (this one) and the rest simply fetch things from it. | ||||
class tagscache(object): | class tagscache(object): | ||||
def __init__(self): | def __init__(self): | ||||
# These two define the set of tags for this repository. tags | # These two define the set of tags for this repository. tags | ||||
# maps tag name to node; tagtypes maps tag name to 'global' or | # maps tag name to node; tagtypes maps tag name to 'global' or | ||||
# 'local'. (Global tags are defined by .hgtags across all | # 'local'. (Global tags are defined by .hgtags across all | ||||
# ignore tags to unknown nodes | # ignore tags to unknown nodes | ||||
rev(v) | rev(v) | ||||
t[k] = v | t[k] = v | ||||
except (error.LookupError, ValueError): | except (error.LookupError, ValueError): | ||||
pass | pass | ||||
return t | return t | ||||
def _findtags(self): | def _findtags(self): | ||||
'''Do the hard work of finding tags. Return a pair of dicts | """Do the hard work of finding tags. Return a pair of dicts | ||||
(tags, tagtypes) where tags maps tag name to node, and tagtypes | (tags, tagtypes) where tags maps tag name to node, and tagtypes | ||||
maps tag name to a string like \'global\' or \'local\'. | maps tag name to a string like \'global\' or \'local\'. | ||||
Subclasses or extensions are free to add their own tags, but | Subclasses or extensions are free to add their own tags, but | ||||
should be aware that the returned dicts will be retained for the | should be aware that the returned dicts will be retained for the | ||||
duration of the localrepo object.''' | duration of the localrepo object.""" | ||||
# XXX what tagtype should subclasses/extensions use? Currently | # XXX what tagtype should subclasses/extensions use? Currently | ||||
# mq and bookmarks add tags, but do not set the tagtype at all. | # mq and bookmarks add tags, but do not set the tagtype at all. | ||||
# Should each extension invent its own tag type? Should there | # Should each extension invent its own tag type? Should there | ||||
# be one tagtype for all such "virtual" tags? Or is the status | # be one tagtype for all such "virtual" tags? Or is the status | ||||
# quo fine? | # quo fine? | ||||
# map tag name to (node, hist) | # map tag name to (node, hist) | ||||
tags[b'tip'] = self.changelog.tip() | tags[b'tip'] = self.changelog.tip() | ||||
tagtypes = { | tagtypes = { | ||||
encoding.tolocal(name): value | encoding.tolocal(name): value | ||||
for (name, value) in pycompat.iteritems(tagtypes) | for (name, value) in pycompat.iteritems(tagtypes) | ||||
} | } | ||||
return (tags, tagtypes) | return (tags, tagtypes) | ||||
def tagtype(self, tagname): | def tagtype(self, tagname): | ||||
''' | """ | ||||
return the type of the given tag. result can be: | return the type of the given tag. result can be: | ||||
'local' : a local tag | 'local' : a local tag | ||||
'global' : a global tag | 'global' : a global tag | ||||
None : tag does not exist | None : tag does not exist | ||||
''' | """ | ||||
return self._tagscache.tagtypes.get(tagname) | return self._tagscache.tagtypes.get(tagname) | ||||
def tagslist(self): | def tagslist(self): | ||||
'''return a list of tags ordered by revision''' | '''return a list of tags ordered by revision''' | ||||
if not self._tagscache.tagslist: | if not self._tagscache.tagslist: | ||||
l = [] | l = [] | ||||
for t, n in pycompat.iteritems(self.tags()): | for t, n in pycompat.iteritems(self.tags()): | ||||
self._tagscache.nodetagscache = nodetagscache | self._tagscache.nodetagscache = nodetagscache | ||||
return self._tagscache.nodetagscache.get(node, []) | return self._tagscache.nodetagscache.get(node, []) | ||||
def nodebookmarks(self, node): | def nodebookmarks(self, node): | ||||
"""return the list of bookmarks pointing to the specified node""" | """return the list of bookmarks pointing to the specified node""" | ||||
return self._bookmarks.names(node) | return self._bookmarks.names(node) | ||||
def branchmap(self): | def branchmap(self): | ||||
'''returns a dictionary {branch: [branchheads]} with branchheads | """returns a dictionary {branch: [branchheads]} with branchheads | ||||
ordered by increasing revision number''' | ordered by increasing revision number""" | ||||
return self._branchcaches[self] | return self._branchcaches[self] | ||||
@unfilteredmethod | @unfilteredmethod | ||||
def revbranchcache(self): | def revbranchcache(self): | ||||
if not self._revbranchcache: | if not self._revbranchcache: | ||||
self._revbranchcache = branchmap.revbranchcache(self.unfiltered()) | self._revbranchcache = branchmap.revbranchcache(self.unfiltered()) | ||||
return self._revbranchcache | return self._revbranchcache | ||||
def branchtip(self, branch, ignoremissing=False): | def branchtip(self, branch, ignoremissing=False): | ||||
'''return the tip node for a given branch | """return the tip node for a given branch | ||||
If ignoremissing is True, then this method will not raise an error. | If ignoremissing is True, then this method will not raise an error. | ||||
This is helpful for callers that only expect None for a missing branch | This is helpful for callers that only expect None for a missing branch | ||||
(e.g. namespace). | (e.g. namespace). | ||||
''' | """ | ||||
try: | try: | ||||
return self.branchmap().branchtip(branch) | return self.branchmap().branchtip(branch) | ||||
except KeyError: | except KeyError: | ||||
if not ignoremissing: | if not ignoremissing: | ||||
raise error.RepoLookupError(_(b"unknown branch '%s'") % branch) | raise error.RepoLookupError(_(b"unknown branch '%s'") % branch) | ||||
else: | else: | ||||
pass | pass | ||||
return self.vfs.reljoin(self.root, f, *insidef) | return self.vfs.reljoin(self.root, f, *insidef) | ||||
def setparents(self, p1, p2=nullid): | def setparents(self, p1, p2=nullid): | ||||
self[None].setparents(p1, p2) | self[None].setparents(p1, p2) | ||||
self._quick_access_changeid_invalidate() | self._quick_access_changeid_invalidate() | ||||
def filectx(self, path, changeid=None, fileid=None, changectx=None): | def filectx(self, path, changeid=None, fileid=None, changectx=None): | ||||
"""changeid must be a changeset revision, if specified. | """changeid must be a changeset revision, if specified. | ||||
fileid can be a file revision or node.""" | fileid can be a file revision or node.""" | ||||
return context.filectx( | return context.filectx( | ||||
self, path, changeid, fileid, changectx=changectx | self, path, changeid, fileid, changectx=changectx | ||||
) | ) | ||||
def getcwd(self): | def getcwd(self): | ||||
return self.dirstate.getcwd() | return self.dirstate.getcwd() | ||||
def pathto(self, f, cwd=None): | def pathto(self, f, cwd=None): | ||||
tr.hookargs[b'txnname'] = desc | tr.hookargs[b'txnname'] = desc | ||||
tr.hookargs[b'changes'] = tr.changes | tr.hookargs[b'changes'] = tr.changes | ||||
# note: writing the fncache only during finalize mean that the file is | # note: writing the fncache only during finalize mean that the file is | ||||
# outdated when running hooks. As fncache is used for streaming clone, | # outdated when running hooks. As fncache is used for streaming clone, | ||||
# this is not expected to break anything that happen during the hooks. | # this is not expected to break anything that happen during the hooks. | ||||
tr.addfinalize(b'flush-fncache', self.store.write) | tr.addfinalize(b'flush-fncache', self.store.write) | ||||
def txnclosehook(tr2): | def txnclosehook(tr2): | ||||
"""To be run if transaction is successful, will schedule a hook run | """To be run if transaction is successful, will schedule a hook run""" | ||||
""" | |||||
# Don't reference tr2 in hook() so we don't hold a reference. | # Don't reference tr2 in hook() so we don't hold a reference. | ||||
# This reduces memory consumption when there are multiple | # This reduces memory consumption when there are multiple | ||||
# transactions per lock. This can likely go away if issue5045 | # transactions per lock. This can likely go away if issue5045 | ||||
# fixes the function accumulation. | # fixes the function accumulation. | ||||
hookargs = tr2.hookargs | hookargs = tr2.hookargs | ||||
def hookfunc(unused_success): | def hookfunc(unused_success): | ||||
repo = reporef() | repo = reporef() | ||||
tr.addfinalize(b'txnclose-hook', txnclosehook) | tr.addfinalize(b'txnclose-hook', txnclosehook) | ||||
# Include a leading "-" to make it happen before the transaction summary | # Include a leading "-" to make it happen before the transaction summary | ||||
# reports registered via scmutil.registersummarycallback() whose names | # reports registered via scmutil.registersummarycallback() whose names | ||||
# are 00-txnreport etc. That way, the caches will be warm when the | # are 00-txnreport etc. That way, the caches will be warm when the | ||||
# callbacks run. | # callbacks run. | ||||
tr.addpostclose(b'-warm-cache', self._buildcacheupdater(tr)) | tr.addpostclose(b'-warm-cache', self._buildcacheupdater(tr)) | ||||
def txnaborthook(tr2): | def txnaborthook(tr2): | ||||
"""To be run if transaction is aborted | """To be run if transaction is aborted""" | ||||
""" | |||||
reporef().hook( | reporef().hook( | ||||
b'txnabort', throw=False, **pycompat.strkwargs(tr2.hookargs) | b'txnabort', throw=False, **pycompat.strkwargs(tr2.hookargs) | ||||
) | ) | ||||
tr.addabort(b'txnabort-hook', txnaborthook) | tr.addabort(b'txnabort-hook', txnaborthook) | ||||
# avoid eager cache invalidation. in-memory data should be identical | # avoid eager cache invalidation. in-memory data should be identical | ||||
# to stored data if transaction has no error. | # to stored data if transaction has no error. | ||||
tr.addpostclose(b'refresh-filecachestats', self._refreshfilecachestats) | tr.addpostclose(b'refresh-filecachestats', self._refreshfilecachestats) | ||||
self._sparsesignaturecache.clear() | self._sparsesignaturecache.clear() | ||||
def invalidatevolatilesets(self): | def invalidatevolatilesets(self): | ||||
self.filteredrevcache.clear() | self.filteredrevcache.clear() | ||||
obsolete.clearobscaches(self) | obsolete.clearobscaches(self) | ||||
self._quick_access_changeid_invalidate() | self._quick_access_changeid_invalidate() | ||||
def invalidatedirstate(self): | def invalidatedirstate(self): | ||||
'''Invalidates the dirstate, causing the next call to dirstate | """Invalidates the dirstate, causing the next call to dirstate | ||||
to check if it was modified since the last time it was read, | to check if it was modified since the last time it was read, | ||||
rereading it if it has. | rereading it if it has. | ||||
This is different to dirstate.invalidate() that it doesn't always | This is different to dirstate.invalidate() that it doesn't always | ||||
rereads the dirstate. Use dirstate.invalidate() if you want to | rereads the dirstate. Use dirstate.invalidate() if you want to | ||||
explicitly read the dirstate again (i.e. restoring it to a previous | explicitly read the dirstate again (i.e. restoring it to a previous | ||||
known good state).''' | known good state).""" | ||||
if hasunfilteredcache(self, 'dirstate'): | if hasunfilteredcache(self, 'dirstate'): | ||||
for k in self.dirstate._filecache: | for k in self.dirstate._filecache: | ||||
try: | try: | ||||
delattr(self.dirstate, k) | delattr(self.dirstate, k) | ||||
except AttributeError: | except AttributeError: | ||||
pass | pass | ||||
delattr(self.unfiltered(), 'dirstate') | delattr(self.unfiltered(), 'dirstate') | ||||
def invalidate(self, clearfilecache=False): | def invalidate(self, clearfilecache=False): | ||||
'''Invalidates both store and non-store parts other than dirstate | """Invalidates both store and non-store parts other than dirstate | ||||
If a transaction is running, invalidation of store is omitted, | If a transaction is running, invalidation of store is omitted, | ||||
because discarding in-memory changes might cause inconsistency | because discarding in-memory changes might cause inconsistency | ||||
(e.g. incomplete fncache causes unintentional failure, but | (e.g. incomplete fncache causes unintentional failure, but | ||||
redundant one doesn't). | redundant one doesn't). | ||||
''' | """ | ||||
unfiltered = self.unfiltered() # all file caches are stored unfiltered | unfiltered = self.unfiltered() # all file caches are stored unfiltered | ||||
for k in list(self._filecache.keys()): | for k in list(self._filecache.keys()): | ||||
# dirstate is invalidated separately in invalidatedirstate() | # dirstate is invalidated separately in invalidatedirstate() | ||||
if k == b'dirstate': | if k == b'dirstate': | ||||
continue | continue | ||||
if ( | if ( | ||||
k == b'changelog' | k == b'changelog' | ||||
and self.currenttransaction() | and self.currenttransaction() | ||||
self.invalidatecaches() | self.invalidatecaches() | ||||
if not self.currenttransaction(): | if not self.currenttransaction(): | ||||
# TODO: Changing contents of store outside transaction | # TODO: Changing contents of store outside transaction | ||||
# causes inconsistency. We should make in-memory store | # causes inconsistency. We should make in-memory store | ||||
# changes detectable, and abort if changed. | # changes detectable, and abort if changed. | ||||
self.store.invalidatecaches() | self.store.invalidatecaches() | ||||
def invalidateall(self): | def invalidateall(self): | ||||
'''Fully invalidates both store and non-store parts, causing the | """Fully invalidates both store and non-store parts, causing the | ||||
subsequent operation to reread any outside changes.''' | subsequent operation to reread any outside changes.""" | ||||
# extension should hook this to invalidate its caches | # extension should hook this to invalidate its caches | ||||
self.invalidate() | self.invalidate() | ||||
self.invalidatedirstate() | self.invalidatedirstate() | ||||
@unfilteredmethod | @unfilteredmethod | ||||
def _refreshfilecachestats(self, tr): | def _refreshfilecachestats(self, tr): | ||||
"""Reload stats of cached files so that they are flagged as valid""" | """Reload stats of cached files so that they are flagged as valid""" | ||||
for k, ce in self._filecache.items(): | for k, ce in self._filecache.items(): | ||||
k = pycompat.sysstr(k) | k = pycompat.sysstr(k) | ||||
if k == 'dirstate' or k not in self.__dict__: | if k == 'dirstate' or k not in self.__dict__: | ||||
continue | continue | ||||
ce.refresh() | ce.refresh() | ||||
def _lock( | def _lock( | ||||
self, vfs, lockname, wait, releasefn, acquirefn, desc, | self, | ||||
vfs, | |||||
lockname, | |||||
wait, | |||||
releasefn, | |||||
acquirefn, | |||||
desc, | |||||
): | ): | ||||
timeout = 0 | timeout = 0 | ||||
warntimeout = 0 | warntimeout = 0 | ||||
if wait: | if wait: | ||||
timeout = self.ui.configint(b"ui", b"timeout") | timeout = self.ui.configint(b"ui", b"timeout") | ||||
warntimeout = self.ui.configint(b"ui", b"timeout.warn") | warntimeout = self.ui.configint(b"ui", b"timeout.warn") | ||||
# internal config: ui.signal-safe-lock | # internal config: ui.signal-safe-lock | ||||
signalsafe = self.ui.configbool(b'ui', b'signal-safe-lock') | signalsafe = self.ui.configbool(b'ui', b'signal-safe-lock') | ||||
l = ref and ref() | l = ref and ref() | ||||
if l and l.held: | if l and l.held: | ||||
l.postrelease.append(callback) | l.postrelease.append(callback) | ||||
break | break | ||||
else: # no lock have been found. | else: # no lock have been found. | ||||
callback(True) | callback(True) | ||||
def lock(self, wait=True): | def lock(self, wait=True): | ||||
'''Lock the repository store (.hg/store) and return a weak reference | """Lock the repository store (.hg/store) and return a weak reference | ||||
to the lock. Use this before modifying the store (e.g. committing or | to the lock. Use this before modifying the store (e.g. committing or | ||||
stripping). If you are opening a transaction, get a lock as well.) | stripping). If you are opening a transaction, get a lock as well.) | ||||
If both 'lock' and 'wlock' must be acquired, ensure you always acquires | If both 'lock' and 'wlock' must be acquired, ensure you always acquires | ||||
'wlock' first to avoid a dead-lock hazard.''' | 'wlock' first to avoid a dead-lock hazard.""" | ||||
l = self._currentlock(self._lockref) | l = self._currentlock(self._lockref) | ||||
if l is not None: | if l is not None: | ||||
l.lock() | l.lock() | ||||
return l | return l | ||||
l = self._lock( | l = self._lock( | ||||
vfs=self.svfs, | vfs=self.svfs, | ||||
lockname=b"lock", | lockname=b"lock", | ||||
wait=wait, | wait=wait, | ||||
releasefn=None, | releasefn=None, | ||||
acquirefn=self.invalidate, | acquirefn=self.invalidate, | ||||
desc=_(b'repository %s') % self.origroot, | desc=_(b'repository %s') % self.origroot, | ||||
) | ) | ||||
self._lockref = weakref.ref(l) | self._lockref = weakref.ref(l) | ||||
return l | return l | ||||
def wlock(self, wait=True): | def wlock(self, wait=True): | ||||
'''Lock the non-store parts of the repository (everything under | """Lock the non-store parts of the repository (everything under | ||||
.hg except .hg/store) and return a weak reference to the lock. | .hg except .hg/store) and return a weak reference to the lock. | ||||
Use this before modifying files in .hg. | Use this before modifying files in .hg. | ||||
If both 'lock' and 'wlock' must be acquired, ensure you always acquires | If both 'lock' and 'wlock' must be acquired, ensure you always acquires | ||||
'wlock' first to avoid a dead-lock hazard.''' | 'wlock' first to avoid a dead-lock hazard.""" | ||||
l = self._wlockref and self._wlockref() | l = self._wlockref and self._wlockref() | ||||
if l is not None and l.held: | if l is not None and l.held: | ||||
l.lock() | l.lock() | ||||
return l | return l | ||||
# We do not need to check for non-waiting lock acquisition. Such | # We do not need to check for non-waiting lock acquisition. Such | ||||
# acquisition would not cause dead-lock as they would just fail. | # acquisition would not cause dead-lock as they would just fail. | ||||
if wait and ( | if wait and ( | ||||
return ret | return ret | ||||
@unfilteredmethod | @unfilteredmethod | ||||
def commitctx(self, ctx, error=False, origctx=None): | def commitctx(self, ctx, error=False, origctx=None): | ||||
return commit.commitctx(self, ctx, error=error, origctx=origctx) | return commit.commitctx(self, ctx, error=error, origctx=origctx) | ||||
@unfilteredmethod | @unfilteredmethod | ||||
def destroying(self): | def destroying(self): | ||||
'''Inform the repository that nodes are about to be destroyed. | """Inform the repository that nodes are about to be destroyed. | ||||
Intended for use by strip and rollback, so there's a common | Intended for use by strip and rollback, so there's a common | ||||
place for anything that has to be done before destroying history. | place for anything that has to be done before destroying history. | ||||
This is mostly useful for saving state that is in memory and waiting | This is mostly useful for saving state that is in memory and waiting | ||||
to be flushed when the current lock is released. Because a call to | to be flushed when the current lock is released. Because a call to | ||||
destroyed is imminent, the repo will be invalidated causing those | destroyed is imminent, the repo will be invalidated causing those | ||||
changes to stay in memory (waiting for the next unlock), or vanish | changes to stay in memory (waiting for the next unlock), or vanish | ||||
completely. | completely. | ||||
''' | """ | ||||
# When using the same lock to commit and strip, the phasecache is left | # When using the same lock to commit and strip, the phasecache is left | ||||
# dirty after committing. Then when we strip, the repo is invalidated, | # dirty after committing. Then when we strip, the repo is invalidated, | ||||
# causing those changes to disappear. | # causing those changes to disappear. | ||||
if '_phasecache' in vars(self): | if '_phasecache' in vars(self): | ||||
self._phasecache.write() | self._phasecache.write() | ||||
@unfilteredmethod | @unfilteredmethod | ||||
def destroyed(self): | def destroyed(self): | ||||
'''Inform the repository that nodes have been destroyed. | """Inform the repository that nodes have been destroyed. | ||||
Intended for use by strip and rollback, so there's a common | Intended for use by strip and rollback, so there's a common | ||||
place for anything that has to be done after destroying history. | place for anything that has to be done after destroying history. | ||||
''' | """ | ||||
# When one tries to: | # When one tries to: | ||||
# 1) destroy nodes thus calling this method (e.g. strip) | # 1) destroy nodes thus calling this method (e.g. strip) | ||||
# 2) use phasecache somewhere (e.g. commit) | # 2) use phasecache somewhere (e.g. commit) | ||||
# | # | ||||
# then 2) will fail because the phasecache contains nodes that were | # then 2) will fail because the phasecache contains nodes that were | ||||
# removed. We can either remove phasecache from the filecache, | # removed. We can either remove phasecache from the filecache, | ||||
# causing it to reload next time it is accessed, or simply filter | # causing it to reload next time it is accessed, or simply filter | ||||
# the removed nodes now and write the updated cache. | # the removed nodes now and write the updated cache. | ||||
headrevs = reversed(cl.headrevs()) | headrevs = reversed(cl.headrevs()) | ||||
return [cl.node(rev) for rev in headrevs] | return [cl.node(rev) for rev in headrevs] | ||||
heads = self.changelog.heads(start) | heads = self.changelog.heads(start) | ||||
# sort the output in rev descending order | # sort the output in rev descending order | ||||
return sorted(heads, key=self.changelog.rev, reverse=True) | return sorted(heads, key=self.changelog.rev, reverse=True) | ||||
def branchheads(self, branch=None, start=None, closed=False): | def branchheads(self, branch=None, start=None, closed=False): | ||||
'''return a (possibly filtered) list of heads for the given branch | """return a (possibly filtered) list of heads for the given branch | ||||
Heads are returned in topological order, from newest to oldest. | Heads are returned in topological order, from newest to oldest. | ||||
If branch is None, use the dirstate branch. | If branch is None, use the dirstate branch. | ||||
If start is not None, return only heads reachable from start. | If start is not None, return only heads reachable from start. | ||||
If closed is True, return heads that are marked as closed as well. | If closed is True, return heads that are marked as closed as well. | ||||
''' | """ | ||||
if branch is None: | if branch is None: | ||||
branch = self[None].branch() | branch = self[None].branch() | ||||
branches = self.branchmap() | branches = self.branchmap() | ||||
if not branches.hasbranch(branch): | if not branches.hasbranch(branch): | ||||
return [] | return [] | ||||
# the cache returns heads ordered lowest to highest | # the cache returns heads ordered lowest to highest | ||||
bheads = list(reversed(branches.branchheads(branch, closed=closed))) | bheads = list(reversed(branches.branchheads(branch, closed=closed))) | ||||
if start is not None: | if start is not None: | ||||
# requirement | # requirement | ||||
if ui.configbool(b'format', b'exp-share-safe'): | if ui.configbool(b'format', b'exp-share-safe'): | ||||
requirements.add(requirementsmod.SHARESAFE_REQUIREMENT) | requirements.add(requirementsmod.SHARESAFE_REQUIREMENT) | ||||
return requirements | return requirements | ||||
def checkrequirementscompat(ui, requirements): | def checkrequirementscompat(ui, requirements): | ||||
""" Checks compatibility of repository requirements enabled and disabled. | """Checks compatibility of repository requirements enabled and disabled. | ||||
Returns a set of requirements which needs to be dropped because dependend | Returns a set of requirements which needs to be dropped because dependend | ||||
requirements are not enabled. Also warns users about it """ | requirements are not enabled. Also warns users about it""" | ||||
dropped = set() | dropped = set() | ||||
if b'store' not in requirements: | if b'store' not in requirements: | ||||
if bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT in requirements: | if bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT in requirements: | ||||
ui.warn( | ui.warn( | ||||
_( | _( | ||||
b'ignoring enabled \'format.bookmarks-in-store\' config ' | b'ignoring enabled \'format.bookmarks-in-store\' config ' |
else: | else: | ||||
ui.debug(b"got lock after %d seconds\n" % l.delay) | ui.debug(b"got lock after %d seconds\n" % l.delay) | ||||
if l.acquirefn: | if l.acquirefn: | ||||
l.acquirefn() | l.acquirefn() | ||||
return l | return l | ||||
class lock(object): | class lock(object): | ||||
'''An advisory lock held by one process to control access to a set | """An advisory lock held by one process to control access to a set | ||||
of files. Non-cooperating processes or incorrectly written scripts | of files. Non-cooperating processes or incorrectly written scripts | ||||
can ignore Mercurial's locking scheme and stomp all over the | can ignore Mercurial's locking scheme and stomp all over the | ||||
repository, so don't do that. | repository, so don't do that. | ||||
Typically used via localrepository.lock() to lock the repository | Typically used via localrepository.lock() to lock the repository | ||||
store (.hg/store/) or localrepository.wlock() to lock everything | store (.hg/store/) or localrepository.wlock() to lock everything | ||||
else under .hg/.''' | else under .hg/.""" | ||||
# lock is symlink on platforms that support it, file on others. | # lock is symlink on platforms that support it, file on others. | ||||
# symlink is used because create of directory entry and contents | # symlink is used because create of directory entry and contents | ||||
# are atomic even over nfs. | # are atomic even over nfs. | ||||
# old-style lock: symlink to pid | # old-style lock: symlink to pid | ||||
# new-style lock: symlink to hostname:pid | # new-style lock: symlink to hostname:pid |
if obsfate: | if obsfate: | ||||
for obsfateline in obsfate: | for obsfateline in obsfate: | ||||
self.ui.write( | self.ui.write( | ||||
self._columns[b'obsolete'] % obsfateline, | self._columns[b'obsolete'] % obsfateline, | ||||
label=b'log.obsfate', | label=b'log.obsfate', | ||||
) | ) | ||||
def _exthook(self, ctx): | def _exthook(self, ctx): | ||||
'''empty method used by extension as a hook point | """empty method used by extension as a hook point""" | ||||
''' | |||||
def _showpatch(self, ctx, graphwidth=0): | def _showpatch(self, ctx, graphwidth=0): | ||||
if self._includestat: | if self._includestat: | ||||
self._differ.showdiff( | self._differ.showdiff( | ||||
self.ui, ctx, self._diffopts, graphwidth, stat=True | self.ui, ctx, self._diffopts, graphwidth, stat=True | ||||
) | ) | ||||
if self._includestat and self._includediff: | if self._includestat and self._includediff: | ||||
self.ui.write(b"\n") | self.ui.write(b"\n") | ||||
fm.data(diffstat=self.ui.popbuffer()) | fm.data(diffstat=self.ui.popbuffer()) | ||||
if self._includediff or b'diff' in datahint: | if self._includediff or b'diff' in datahint: | ||||
self.ui.pushbuffer() | self.ui.pushbuffer() | ||||
self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False) | self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False) | ||||
fm.data(diff=self.ui.popbuffer()) | fm.data(diff=self.ui.popbuffer()) | ||||
class changesettemplater(changesetprinter): | class changesettemplater(changesetprinter): | ||||
'''format changeset information. | """format changeset information. | ||||
Note: there are a variety of convenience functions to build a | Note: there are a variety of convenience functions to build a | ||||
changesettemplater for common cases. See functions such as: | changesettemplater for common cases. See functions such as: | ||||
maketemplater, changesetdisplayer, buildcommittemplate, or other | maketemplater, changesetdisplayer, buildcommittemplate, or other | ||||
functions that use changesest_templater. | functions that use changesest_templater. | ||||
''' | """ | ||||
# Arguments before "buffered" used to be positional. Consider not | # Arguments before "buffered" used to be positional. Consider not | ||||
# adding/removing arguments before "buffered" to not break callers. | # adding/removing arguments before "buffered" to not break callers. | ||||
def __init__( | def __init__( | ||||
self, ui, repo, tmplspec, differ=None, diffopts=None, buffered=False | self, ui, repo, tmplspec, differ=None, diffopts=None, buffered=False | ||||
): | ): | ||||
changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered) | changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered) | ||||
# tres is shared with _graphnodeformatter() | # tres is shared with _graphnodeformatter() |
pull or clone operation. | pull or clone operation. | ||||
localrepo is our local repository | localrepo is our local repository | ||||
remoterepo is the peer instance | remoterepo is the peer instance | ||||
""" | """ | ||||
remotepath = activepath(localrepo, remoterepo) | remotepath = activepath(localrepo, remoterepo) | ||||
with remoterepo.commandexecutor() as e: | with remoterepo.commandexecutor() as e: | ||||
bookmarks = e.callcommand( | bookmarks = e.callcommand( | ||||
b'listkeys', {b'namespace': b'bookmarks',} | b'listkeys', | ||||
{ | |||||
b'namespace': b'bookmarks', | |||||
}, | |||||
).result() | ).result() | ||||
# on a push, we don't want to keep obsolete heads since | # on a push, we don't want to keep obsolete heads since | ||||
# they won't show up as heads on the next pull, so we | # they won't show up as heads on the next pull, so we | ||||
# remove them here otherwise we would require the user | # remove them here otherwise we would require the user | ||||
# to issue a pull to refresh the storage | # to issue a pull to refresh the storage | ||||
bmap = {} | bmap = {} | ||||
repo = localrepo.unfiltered() | repo = localrepo.unfiltered() |
if pycompat.TYPE_CHECKING: | if pycompat.TYPE_CHECKING: | ||||
from typing import Any, List, Tuple, Union | from typing import Any, List, Tuple, Union | ||||
# keep pyflakes happy | # keep pyflakes happy | ||||
assert all((Any, List, Tuple, Union)) | assert all((Any, List, Tuple, Union)) | ||||
class STARTTLS(smtplib.SMTP): | class STARTTLS(smtplib.SMTP): | ||||
'''Derived class to verify the peer certificate for STARTTLS. | """Derived class to verify the peer certificate for STARTTLS. | ||||
This class allows to pass any keyword arguments to SSL socket creation. | This class allows to pass any keyword arguments to SSL socket creation. | ||||
''' | """ | ||||
def __init__(self, ui, host=None, **kwargs): | def __init__(self, ui, host=None, **kwargs): | ||||
smtplib.SMTP.__init__(self, **kwargs) | smtplib.SMTP.__init__(self, **kwargs) | ||||
self._ui = ui | self._ui = ui | ||||
self._host = host | self._host = host | ||||
def starttls(self, keyfile=None, certfile=None): | def starttls(self, keyfile=None, certfile=None): | ||||
if not self.has_extn("starttls"): | if not self.has_extn("starttls"): | ||||
self.helo_resp = None | self.helo_resp = None | ||||
self.ehlo_resp = None | self.ehlo_resp = None | ||||
self.esmtp_features = {} | self.esmtp_features = {} | ||||
self.does_esmtp = 0 | self.does_esmtp = 0 | ||||
return (resp, reply) | return (resp, reply) | ||||
class SMTPS(smtplib.SMTP): | class SMTPS(smtplib.SMTP): | ||||
'''Derived class to verify the peer certificate for SMTPS. | """Derived class to verify the peer certificate for SMTPS. | ||||
This class allows to pass any keyword arguments to SSL socket creation. | This class allows to pass any keyword arguments to SSL socket creation. | ||||
''' | """ | ||||
def __init__(self, ui, keyfile=None, certfile=None, host=None, **kwargs): | def __init__(self, ui, keyfile=None, certfile=None, host=None, **kwargs): | ||||
self.keyfile = keyfile | self.keyfile = keyfile | ||||
self.certfile = certfile | self.certfile = certfile | ||||
smtplib.SMTP.__init__(self, **kwargs) | smtplib.SMTP.__init__(self, **kwargs) | ||||
self._host = host | self._host = host | ||||
self.default_port = smtplib.SMTP_SSL_PORT | self.default_port = smtplib.SMTP_SSL_PORT | ||||
self._ui = ui | self._ui = ui | ||||
% (encoding.strtolocal(sender), encoding.strtolocal(date)) | % (encoding.strtolocal(sender), encoding.strtolocal(date)) | ||||
) | ) | ||||
fp.write(msg) | fp.write(msg) | ||||
fp.write(b'\n\n') | fp.write(b'\n\n') | ||||
fp.close() | fp.close() | ||||
def connect(ui, mbox=None): | def connect(ui, mbox=None): | ||||
'''make a mail connection. return a function to send mail. | """make a mail connection. return a function to send mail. | ||||
call as sendmail(sender, list-of-recipients, msg).''' | call as sendmail(sender, list-of-recipients, msg).""" | ||||
if mbox: | if mbox: | ||||
open(mbox, b'wb').close() | open(mbox, b'wb').close() | ||||
return lambda s, r, m: _mbox(mbox, s, r, m) | return lambda s, r, m: _mbox(mbox, s, r, m) | ||||
if ui.config(b'email', b'method') == b'smtp': | if ui.config(b'email', b'method') == b'smtp': | ||||
return _smtp(ui) | return _smtp(ui) | ||||
return lambda s, r, m: _sendmail(ui, s, r, m) | return lambda s, r, m: _sendmail(ui, s, r, m) | ||||
# "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1" | # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1" | ||||
if cs.startswith("iso") and not cs.startswith("iso-"): | if cs.startswith("iso") and not cs.startswith("iso-"): | ||||
return "iso-" + cs[3:] | return "iso-" + cs[3:] | ||||
return cs | return cs | ||||
def mimetextpatch(s, subtype='plain', display=False): | def mimetextpatch(s, subtype='plain', display=False): | ||||
# type: (bytes, str, bool) -> email.message.Message | # type: (bytes, str, bool) -> email.message.Message | ||||
'''Return MIME message suitable for a patch. | """Return MIME message suitable for a patch. | ||||
Charset will be detected by first trying to decode as us-ascii, then utf-8, | Charset will be detected by first trying to decode as us-ascii, then utf-8, | ||||
and finally the global encodings. If all those fail, fall back to | and finally the global encodings. If all those fail, fall back to | ||||
ISO-8859-1, an encoding with that allows all byte sequences. | ISO-8859-1, an encoding with that allows all byte sequences. | ||||
Transfer encodings will be used if necessary.''' | Transfer encodings will be used if necessary.""" | ||||
cs = [ | cs = [ | ||||
'us-ascii', | 'us-ascii', | ||||
'utf-8', | 'utf-8', | ||||
pycompat.sysstr(encoding.encoding), | pycompat.sysstr(encoding.encoding), | ||||
pycompat.sysstr(encoding.fallbackencoding), | pycompat.sysstr(encoding.fallbackencoding), | ||||
] | ] | ||||
if display: | if display: | ||||
cs = ['us-ascii'] | cs = ['us-ascii'] | ||||
for charset in cs: | for charset in cs: | ||||
try: | try: | ||||
s.decode(charset) | s.decode(charset) | ||||
return mimetextqp(s, subtype, codec2iana(charset)) | return mimetextqp(s, subtype, codec2iana(charset)) | ||||
except UnicodeDecodeError: | except UnicodeDecodeError: | ||||
pass | pass | ||||
return mimetextqp(s, subtype, "iso-8859-1") | return mimetextqp(s, subtype, "iso-8859-1") | ||||
def mimetextqp(body, subtype, charset): | def mimetextqp(body, subtype, charset): | ||||
# type: (bytes, str, str) -> email.message.Message | # type: (bytes, str, str) -> email.message.Message | ||||
'''Return MIME message. | """Return MIME message. | ||||
Quoted-printable transfer encoding will be used if necessary. | Quoted-printable transfer encoding will be used if necessary. | ||||
''' | """ | ||||
cs = email.charset.Charset(charset) | cs = email.charset.Charset(charset) | ||||
msg = email.message.Message() | msg = email.message.Message() | ||||
msg.set_type('text/' + subtype) | msg.set_type('text/' + subtype) | ||||
for line in body.splitlines(): | for line in body.splitlines(): | ||||
if len(line) > 950: | if len(line) > 950: | ||||
cs.body_encoding = email.charset.QP | cs.body_encoding = email.charset.QP | ||||
break | break | ||||
for cs in fallbacks: # find unique charsets while keeping order | for cs in fallbacks: # find unique charsets while keeping order | ||||
if cs not in charsets: | if cs not in charsets: | ||||
charsets.append(cs) | charsets.append(cs) | ||||
return [cs for cs in charsets if not cs.endswith('ascii')] | return [cs for cs in charsets if not cs.endswith('ascii')] | ||||
def _encode(ui, s, charsets): | def _encode(ui, s, charsets): | ||||
# type: (Any, bytes, List[str]) -> Tuple[bytes, str] | # type: (Any, bytes, List[str]) -> Tuple[bytes, str] | ||||
'''Returns (converted) string, charset tuple. | """Returns (converted) string, charset tuple. | ||||
Finds out best charset by cycling through sendcharsets in descending | Finds out best charset by cycling through sendcharsets in descending | ||||
order. Tries both encoding and fallbackencoding for input. Only as | order. Tries both encoding and fallbackencoding for input. Only as | ||||
last resort send as is in fake ascii. | last resort send as is in fake ascii. | ||||
Caveat: Do not use for mail parts containing patches!''' | Caveat: Do not use for mail parts containing patches!""" | ||||
sendcharsets = charsets or _charsets(ui) | sendcharsets = charsets or _charsets(ui) | ||||
if not isinstance(s, bytes): | if not isinstance(s, bytes): | ||||
# We have unicode data, which we need to try and encode to | # We have unicode data, which we need to try and encode to | ||||
# some reasonable-ish encoding. Try the encodings the user | # some reasonable-ish encoding. Try the encodings the user | ||||
# wants, and fall back to garbage-in-ascii. | # wants, and fall back to garbage-in-ascii. | ||||
for ocs in sendcharsets: | for ocs in sendcharsets: | ||||
try: | try: | ||||
return s.encode(ocs), ocs | return s.encode(ocs), ocs | ||||
if display or not address: | if display or not address: | ||||
return encoding.strfromlocal(address or b'') | return encoding.strfromlocal(address or b'') | ||||
name, addr = email.utils.parseaddr(encoding.strfromlocal(address)) | name, addr = email.utils.parseaddr(encoding.strfromlocal(address)) | ||||
return _addressencode(ui, name, addr, charsets) | return _addressencode(ui, name, addr, charsets) | ||||
def addrlistencode(ui, addrs, charsets=None, display=False): | def addrlistencode(ui, addrs, charsets=None, display=False): | ||||
# type: (Any, List[bytes], List[str], bool) -> List[str] | # type: (Any, List[bytes], List[str], bool) -> List[str] | ||||
'''Turns a list of addresses into a list of RFC-2047 compliant headers. | """Turns a list of addresses into a list of RFC-2047 compliant headers. | ||||
A single element of input list may contain multiple addresses, but output | A single element of input list may contain multiple addresses, but output | ||||
always has one address per item''' | always has one address per item""" | ||||
straddrs = [] | straddrs = [] | ||||
for a in addrs: | for a in addrs: | ||||
assert isinstance(a, bytes), '%r unexpectedly not a bytestr' % a | assert isinstance(a, bytes), '%r unexpectedly not a bytestr' % a | ||||
straddrs.append(encoding.strfromlocal(a)) | straddrs.append(encoding.strfromlocal(a)) | ||||
if display: | if display: | ||||
return [a.strip() for a in straddrs if a.strip()] | return [a.strip() for a in straddrs if a.strip()] | ||||
result = [] | result = [] | ||||
for name, addr in email.utils.getaddresses(straddrs): | for name, addr in email.utils.getaddresses(straddrs): | ||||
if name or addr: | if name or addr: | ||||
r = _addressencode(ui, name, addr, charsets) | r = _addressencode(ui, name, addr, charsets) | ||||
result.append(r) | result.append(r) | ||||
return result | return result | ||||
def mimeencode(ui, s, charsets=None, display=False): | def mimeencode(ui, s, charsets=None, display=False): | ||||
# type: (Any, bytes, List[str], bool) -> email.message.Message | # type: (Any, bytes, List[str], bool) -> email.message.Message | ||||
'''creates mime text object, encodes it if needed, and sets | """creates mime text object, encodes it if needed, and sets | ||||
charset and transfer-encoding accordingly.''' | charset and transfer-encoding accordingly.""" | ||||
cs = 'us-ascii' | cs = 'us-ascii' | ||||
if not display: | if not display: | ||||
s, cs = _encode(ui, s, charsets) | s, cs = _encode(ui, s, charsets) | ||||
return mimetextqp(s, 'plain', cs) | return mimetextqp(s, 'plain', cs) | ||||
if pycompat.ispy3: | if pycompat.ispy3: | ||||
def dirs(self): | def dirs(self): | ||||
return self._dirs | return self._dirs | ||||
def hasdir(self, dir): | def hasdir(self, dir): | ||||
return dir in self._dirs | return dir in self._dirs | ||||
def _filesfastpath(self, match): | def _filesfastpath(self, match): | ||||
'''Checks whether we can correctly and quickly iterate over matcher | """Checks whether we can correctly and quickly iterate over matcher | ||||
files instead of over manifest files.''' | files instead of over manifest files.""" | ||||
files = match.files() | files = match.files() | ||||
return len(files) < 100 and ( | return len(files) < 100 and ( | ||||
match.isexact() | match.isexact() | ||||
or (match.prefix() and all(fn in self for fn in files)) | or (match.prefix() and all(fn in self for fn in files)) | ||||
) | ) | ||||
def walk(self, match): | def walk(self, match): | ||||
'''Generates matching file names. | """Generates matching file names. | ||||
Equivalent to manifest.matches(match).iterkeys(), but without creating | Equivalent to manifest.matches(match).iterkeys(), but without creating | ||||
an entirely new manifest. | an entirely new manifest. | ||||
It also reports nonexistent files by marking them bad with match.bad(). | It also reports nonexistent files by marking them bad with match.bad(). | ||||
''' | """ | ||||
if match.always(): | if match.always(): | ||||
for f in iter(self): | for f in iter(self): | ||||
yield f | yield f | ||||
return | return | ||||
fset = set(match.files()) | fset = set(match.files()) | ||||
# avoid the entire walk if we're only looking for specific files | # avoid the entire walk if we're only looking for specific files | ||||
m._lm[fn] = lm[fn] | m._lm[fn] = lm[fn] | ||||
return m | return m | ||||
m = manifestdict() | m = manifestdict() | ||||
m._lm = self._lm.filtercopy(match) | m._lm = self._lm.filtercopy(match) | ||||
return m | return m | ||||
def diff(self, m2, match=None, clean=False): | def diff(self, m2, match=None, clean=False): | ||||
'''Finds changes between the current manifest and m2. | """Finds changes between the current manifest and m2. | ||||
Args: | Args: | ||||
m2: the manifest to which this manifest should be compared. | m2: the manifest to which this manifest should be compared. | ||||
clean: if true, include files unchanged between these manifests | clean: if true, include files unchanged between these manifests | ||||
with a None value in the returned dictionary. | with a None value in the returned dictionary. | ||||
The result is returned as a dict with filename as key and | The result is returned as a dict with filename as key and | ||||
values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the | values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the | ||||
nodeid in the current/other manifest and fl1/fl2 is the flag | nodeid in the current/other manifest and fl1/fl2 is the flag | ||||
in the current/other manifest. Where the file does not exist, | in the current/other manifest. Where the file does not exist, | ||||
the nodeid will be None and the flags will be the empty | the nodeid will be None and the flags will be the empty | ||||
string. | string. | ||||
''' | """ | ||||
if match: | if match: | ||||
m1 = self._matches(match) | m1 = self._matches(match) | ||||
m2 = m2._matches(match) | m2 = m2._matches(match) | ||||
return m1.diff(m2, clean=clean) | return m1.diff(m2, clean=clean) | ||||
return self._lm.diff(m2._lm, clean) | return self._lm.diff(m2._lm, clean) | ||||
def setflag(self, key, flag): | def setflag(self, key, flag): | ||||
if flag not in _manifestflags: | if flag not in _manifestflags: | ||||
deltatext = mdiff.textdiff( | deltatext = mdiff.textdiff( | ||||
util.buffer(base), util.buffer(arraytext) | util.buffer(base), util.buffer(arraytext) | ||||
) | ) | ||||
return arraytext, deltatext | return arraytext, deltatext | ||||
def _msearch(m, s, lo=0, hi=None): | def _msearch(m, s, lo=0, hi=None): | ||||
'''return a tuple (start, end) that says where to find s within m. | """return a tuple (start, end) that says where to find s within m. | ||||
If the string is found m[start:end] are the line containing | If the string is found m[start:end] are the line containing | ||||
that string. If start == end the string was not found and | that string. If start == end the string was not found and | ||||
they indicate the proper sorted insertion point. | they indicate the proper sorted insertion point. | ||||
m should be a buffer, a memoryview or a byte string. | m should be a buffer, a memoryview or a byte string. | ||||
s is a byte string''' | s is a byte string""" | ||||
def advance(i, c): | def advance(i, c): | ||||
while i < lenm and m[i : i + 1] != c: | while i < lenm and m[i : i + 1] != c: | ||||
i += 1 | i += 1 | ||||
return i | return i | ||||
if not s: | if not s: | ||||
return (lo, lo) | return (lo, lo) | ||||
hex(self._node), | hex(self._node), | ||||
bool(self._loadfunc is _noop), | bool(self._loadfunc is _noop), | ||||
self._dirty, | self._dirty, | ||||
id(self), | id(self), | ||||
) | ) | ||||
) | ) | ||||
def dir(self): | def dir(self): | ||||
'''The directory that this tree manifest represents, including a | """The directory that this tree manifest represents, including a | ||||
trailing '/'. Empty string for the repo root directory.''' | trailing '/'. Empty string for the repo root directory.""" | ||||
return self._dir | return self._dir | ||||
def node(self): | def node(self): | ||||
'''This node of this instance. nullid for unsaved instances. Should | """This node of this instance. nullid for unsaved instances. Should | ||||
be updated when the instance is read or written from a revlog. | be updated when the instance is read or written from a revlog. | ||||
''' | """ | ||||
assert not self._dirty | assert not self._dirty | ||||
return self._node | return self._node | ||||
def setnode(self, node): | def setnode(self, node): | ||||
self._node = node | self._node = node | ||||
self._dirty = False | self._dirty = False | ||||
def iterentries(self): | def iterentries(self): | ||||
self._loadlazy(topdir) | self._loadlazy(topdir) | ||||
if topdir in self._dirs: | if topdir in self._dirs: | ||||
return self._dirs[topdir].hasdir(subdir) | return self._dirs[topdir].hasdir(subdir) | ||||
return False | return False | ||||
dirslash = dir + b'/' | dirslash = dir + b'/' | ||||
return dirslash in self._dirs or dirslash in self._lazydirs | return dirslash in self._dirs or dirslash in self._lazydirs | ||||
def walk(self, match): | def walk(self, match): | ||||
'''Generates matching file names. | """Generates matching file names. | ||||
It also reports nonexistent files by marking them bad with match.bad(). | It also reports nonexistent files by marking them bad with match.bad(). | ||||
''' | """ | ||||
if match.always(): | if match.always(): | ||||
for f in iter(self): | for f in iter(self): | ||||
yield f | yield f | ||||
return | return | ||||
fset = set(match.files()) | fset = set(match.files()) | ||||
for fn in self._walk(match): | for fn in self._walk(match): | ||||
if match(fullp): | if match(fullp): | ||||
yield fullp | yield fullp | ||||
else: | else: | ||||
if not visit or p[:-1] in visit: | if not visit or p[:-1] in visit: | ||||
for f in self._dirs[p]._walk(match): | for f in self._dirs[p]._walk(match): | ||||
yield f | yield f | ||||
def _matches(self, match): | def _matches(self, match): | ||||
'''recursively generate a new manifest filtered by the match argument. | """recursively generate a new manifest filtered by the match argument.""" | ||||
''' | |||||
if match.always(): | if match.always(): | ||||
return self.copy() | return self.copy() | ||||
return self._matches_inner(match) | return self._matches_inner(match) | ||||
def _matches_inner(self, match): | def _matches_inner(self, match): | ||||
if match.always(): | if match.always(): | ||||
return self.copy() | return self.copy() | ||||
if not ret._isempty(): | if not ret._isempty(): | ||||
ret._dirty = True | ret._dirty = True | ||||
return ret | return ret | ||||
def fastdelta(self, base, changes): | def fastdelta(self, base, changes): | ||||
raise FastdeltaUnavailable() | raise FastdeltaUnavailable() | ||||
def diff(self, m2, match=None, clean=False): | def diff(self, m2, match=None, clean=False): | ||||
'''Finds changes between the current manifest and m2. | """Finds changes between the current manifest and m2. | ||||
Args: | Args: | ||||
m2: the manifest to which this manifest should be compared. | m2: the manifest to which this manifest should be compared. | ||||
clean: if true, include files unchanged between these manifests | clean: if true, include files unchanged between these manifests | ||||
with a None value in the returned dictionary. | with a None value in the returned dictionary. | ||||
The result is returned as a dict with filename as key and | The result is returned as a dict with filename as key and | ||||
values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the | values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the | ||||
nodeid in the current/other manifest and fl1/fl2 is the flag | nodeid in the current/other manifest and fl1/fl2 is the flag | ||||
in the current/other manifest. Where the file does not exist, | in the current/other manifest. Where the file does not exist, | ||||
the nodeid will be None and the flags will be the empty | the nodeid will be None and the flags will be the empty | ||||
string. | string. | ||||
''' | """ | ||||
if match and not match.always(): | if match and not match.always(): | ||||
m1 = self._matches(match) | m1 = self._matches(match) | ||||
m2 = m2._matches(match) | m2 = m2._matches(match) | ||||
return m1.diff(m2, clean=clean) | return m1.diff(m2, clean=clean) | ||||
result = {} | result = {} | ||||
emptytree = treemanifest() | emptytree = treemanifest() | ||||
def _iterativediff(t1, t2, stack): | def _iterativediff(t1, t2, stack): | ||||
class FastdeltaUnavailable(Exception): | class FastdeltaUnavailable(Exception): | ||||
"""Exception raised when fastdelta isn't usable on a manifest.""" | """Exception raised when fastdelta isn't usable on a manifest.""" | ||||
@interfaceutil.implementer(repository.imanifeststorage) | @interfaceutil.implementer(repository.imanifeststorage) | ||||
class manifestrevlog(object): | class manifestrevlog(object): | ||||
'''A revlog that stores manifest texts. This is responsible for caching the | """A revlog that stores manifest texts. This is responsible for caching the | ||||
full-text manifest contents. | full-text manifest contents. | ||||
''' | """ | ||||
def __init__( | def __init__( | ||||
self, | self, | ||||
opener, | opener, | ||||
tree=b'', | tree=b'', | ||||
dirlogcache=None, | dirlogcache=None, | ||||
indexfile=None, | indexfile=None, | ||||
treemanifest=False, | treemanifest=False, | ||||
else: | else: | ||||
text = store.revision(self._node) | text = store.revision(self._node) | ||||
arraytext = bytearray(text) | arraytext = bytearray(text) | ||||
store.fulltextcache[self._node] = arraytext | store.fulltextcache[self._node] = arraytext | ||||
self._data = manifestdict(text) | self._data = manifestdict(text) | ||||
return self._data | return self._data | ||||
def readfast(self, shallow=False): | def readfast(self, shallow=False): | ||||
'''Calls either readdelta or read, based on which would be less work. | """Calls either readdelta or read, based on which would be less work. | ||||
readdelta is called if the delta is against the p1, and therefore can be | readdelta is called if the delta is against the p1, and therefore can be | ||||
read quickly. | read quickly. | ||||
If `shallow` is True, nothing changes since this is a flat manifest. | If `shallow` is True, nothing changes since this is a flat manifest. | ||||
''' | """ | ||||
store = self._storage() | store = self._storage() | ||||
r = store.rev(self._node) | r = store.rev(self._node) | ||||
deltaparent = store.deltaparent(r) | deltaparent = store.deltaparent(r) | ||||
if deltaparent != nullrev and deltaparent in store.parentrevs(r): | if deltaparent != nullrev and deltaparent in store.parentrevs(r): | ||||
return self.readdelta() | return self.readdelta() | ||||
return self.read() | return self.read() | ||||
def readdelta(self, shallow=False): | def readdelta(self, shallow=False): | ||||
'''Returns a manifest containing just the entries that are present | """Returns a manifest containing just the entries that are present | ||||
in this manifest, but not in its p1 manifest. This is efficient to read | in this manifest, but not in its p1 manifest. This is efficient to read | ||||
if the revlog delta is already p1. | if the revlog delta is already p1. | ||||
Changing the value of `shallow` has no effect on flat manifests. | Changing the value of `shallow` has no effect on flat manifests. | ||||
''' | """ | ||||
store = self._storage() | store = self._storage() | ||||
r = store.rev(self._node) | r = store.rev(self._node) | ||||
d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r)) | d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r)) | ||||
return manifestdict(d) | return manifestdict(d) | ||||
def find(self, key): | def find(self, key): | ||||
return self.read().find(key) | return self.read().find(key) | ||||
memmf._treemanifest = self.read().copy() | memmf._treemanifest = self.read().copy() | ||||
return memmf | return memmf | ||||
@propertycache | @propertycache | ||||
def parents(self): | def parents(self): | ||||
return self._storage().parents(self._node) | return self._storage().parents(self._node) | ||||
def readdelta(self, shallow=False): | def readdelta(self, shallow=False): | ||||
'''Returns a manifest containing just the entries that are present | """Returns a manifest containing just the entries that are present | ||||
in this manifest, but not in its p1 manifest. This is efficient to read | in this manifest, but not in its p1 manifest. This is efficient to read | ||||
if the revlog delta is already p1. | if the revlog delta is already p1. | ||||
If `shallow` is True, this will read the delta for this directory, | If `shallow` is True, this will read the delta for this directory, | ||||
without recursively reading subdirectory manifests. Instead, any | without recursively reading subdirectory manifests. Instead, any | ||||
subdirectory entry will be reported as it appears in the manifest, i.e. | subdirectory entry will be reported as it appears in the manifest, i.e. | ||||
the subdirectory will be reported among files and distinguished only by | the subdirectory will be reported among files and distinguished only by | ||||
its 't' flag. | its 't' flag. | ||||
''' | """ | ||||
store = self._storage() | store = self._storage() | ||||
if shallow: | if shallow: | ||||
r = store.rev(self._node) | r = store.rev(self._node) | ||||
d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r)) | d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r)) | ||||
return manifestdict(d) | return manifestdict(d) | ||||
else: | else: | ||||
# Need to perform a slow delta | # Need to perform a slow delta | ||||
r0 = store.deltaparent(store.rev(self._node)) | r0 = store.deltaparent(store.rev(self._node)) | ||||
m0 = self._manifestlog.get(self._dir, store.node(r0)).read() | m0 = self._manifestlog.get(self._dir, store.node(r0)).read() | ||||
m1 = self.read() | m1 = self.read() | ||||
md = treemanifest(dir=self._dir) | md = treemanifest(dir=self._dir) | ||||
for f, ((n0, fl0), (n1, fl1)) in pycompat.iteritems(m0.diff(m1)): | for f, ((n0, fl0), (n1, fl1)) in pycompat.iteritems(m0.diff(m1)): | ||||
if n1: | if n1: | ||||
md[f] = n1 | md[f] = n1 | ||||
if fl1: | if fl1: | ||||
md.setflag(f, fl1) | md.setflag(f, fl1) | ||||
return md | return md | ||||
def readfast(self, shallow=False): | def readfast(self, shallow=False): | ||||
'''Calls either readdelta or read, based on which would be less work. | """Calls either readdelta or read, based on which would be less work. | ||||
readdelta is called if the delta is against the p1, and therefore can be | readdelta is called if the delta is against the p1, and therefore can be | ||||
read quickly. | read quickly. | ||||
If `shallow` is True, it only returns the entries from this manifest, | If `shallow` is True, it only returns the entries from this manifest, | ||||
and not any submanifests. | and not any submanifests. | ||||
''' | """ | ||||
store = self._storage() | store = self._storage() | ||||
r = store.rev(self._node) | r = store.rev(self._node) | ||||
deltaparent = store.deltaparent(r) | deltaparent = store.deltaparent(r) | ||||
if deltaparent != nullrev and deltaparent in store.parentrevs(r): | if deltaparent != nullrev and deltaparent in store.parentrevs(r): | ||||
return self.readdelta(shallow=shallow) | return self.readdelta(shallow=shallow) | ||||
if shallow: | if shallow: | ||||
return manifestdict(store.revision(self._node)) | return manifestdict(store.revision(self._node)) |
b'rootfilesin', | b'rootfilesin', | ||||
) | ) | ||||
cwdrelativepatternkinds = (b'relpath', b'glob') | cwdrelativepatternkinds = (b'relpath', b'glob') | ||||
propertycache = util.propertycache | propertycache = util.propertycache | ||||
def _rematcher(regex): | def _rematcher(regex): | ||||
'''compile the regexp with the best available regexp engine and return a | """compile the regexp with the best available regexp engine and return a | ||||
matcher function''' | matcher function""" | ||||
m = util.re.compile(regex) | m = util.re.compile(regex) | ||||
try: | try: | ||||
# slightly faster, provided by facebook's re2 bindings | # slightly faster, provided by facebook's re2 bindings | ||||
return m.test_match | return m.test_match | ||||
except AttributeError: | except AttributeError: | ||||
return m.match | return m.match | ||||
matchers.append(pm) | matchers.append(pm) | ||||
continue | continue | ||||
other.append((kind, pat, source)) | other.append((kind, pat, source)) | ||||
return matchers, other | return matchers, other | ||||
def _expandsubinclude(kindpats, root): | def _expandsubinclude(kindpats, root): | ||||
'''Returns the list of subinclude matcher args and the kindpats without the | """Returns the list of subinclude matcher args and the kindpats without the | ||||
subincludes in it.''' | subincludes in it.""" | ||||
relmatchers = [] | relmatchers = [] | ||||
other = [] | other = [] | ||||
for kind, pat, source in kindpats: | for kind, pat, source in kindpats: | ||||
if kind == b'subinclude': | if kind == b'subinclude': | ||||
sourceroot = pathutil.dirname(util.normpath(source)) | sourceroot = pathutil.dirname(util.normpath(source)) | ||||
pat = util.pconvert(pat) | pat = util.pconvert(pat) | ||||
path = pathutil.join(sourceroot, pat) | path = pathutil.join(sourceroot, pat) | ||||
newroot = pathutil.dirname(path) | newroot = pathutil.dirname(path) | ||||
matcherargs = (newroot, b'', [], [b'include:%s' % path]) | matcherargs = (newroot, b'', [], [b'include:%s' % path]) | ||||
prefix = pathutil.canonpath(root, root, newroot) | prefix = pathutil.canonpath(root, root, newroot) | ||||
if prefix: | if prefix: | ||||
prefix += b'/' | prefix += b'/' | ||||
relmatchers.append((prefix, matcherargs)) | relmatchers.append((prefix, matcherargs)) | ||||
else: | else: | ||||
other.append((kind, pat, source)) | other.append((kind, pat, source)) | ||||
return relmatchers, other | return relmatchers, other | ||||
def _kindpatsalwaysmatch(kindpats): | def _kindpatsalwaysmatch(kindpats): | ||||
""""Checks whether the kindspats match everything, as e.g. | """ "Checks whether the kindspats match everything, as e.g. | ||||
'relpath:.' does. | 'relpath:.' does. | ||||
""" | """ | ||||
for kind, pat, source in kindpats: | for kind, pat, source in kindpats: | ||||
if pat != b'' or kind not in [b'relpath', b'glob']: | if pat != b'' or kind not in [b'relpath', b'glob']: | ||||
return False | return False | ||||
return True | return True | ||||
def _buildkindpatsmatcher( | def _buildkindpatsmatcher( | ||||
matchercls, root, cwd, kindpats, ctx=None, listsubrepos=False, badfn=None, | matchercls, | ||||
root, | |||||
cwd, | |||||
kindpats, | |||||
ctx=None, | |||||
listsubrepos=False, | |||||
badfn=None, | |||||
): | ): | ||||
matchers = [] | matchers = [] | ||||
fms, kindpats = _expandsets( | fms, kindpats = _expandsets( | ||||
cwd, kindpats, ctx=ctx, listsubrepos=listsubrepos, badfn=badfn, | cwd, | ||||
kindpats, | |||||
ctx=ctx, | |||||
listsubrepos=listsubrepos, | |||||
badfn=badfn, | |||||
) | ) | ||||
if kindpats: | if kindpats: | ||||
m = matchercls(root, kindpats, badfn=badfn) | m = matchercls(root, kindpats, badfn=badfn) | ||||
matchers.append(m) | matchers.append(m) | ||||
if fms: | if fms: | ||||
matchers.extend(fms) | matchers.extend(fms) | ||||
if not matchers: | if not matchers: | ||||
return nevermatcher(badfn=badfn) | return nevermatcher(badfn=badfn) | ||||
one. | one. | ||||
""" | """ | ||||
m = copy.copy(match) | m = copy.copy(match) | ||||
m.bad = badfn | m.bad = badfn | ||||
return m | return m | ||||
def _donormalize(patterns, default, root, cwd, auditor=None, warn=None): | def _donormalize(patterns, default, root, cwd, auditor=None, warn=None): | ||||
'''Convert 'kind:pat' from the patterns list to tuples with kind and | """Convert 'kind:pat' from the patterns list to tuples with kind and | ||||
normalized and rooted patterns and with listfiles expanded.''' | normalized and rooted patterns and with listfiles expanded.""" | ||||
kindpats = [] | kindpats = [] | ||||
for kind, pat in [_patsplit(p, default) for p in patterns]: | for kind, pat in [_patsplit(p, default) for p in patterns]: | ||||
if kind in cwdrelativepatternkinds: | if kind in cwdrelativepatternkinds: | ||||
pat = pathutil.canonpath(root, cwd, pat, auditor=auditor) | pat = pathutil.canonpath(root, cwd, pat, auditor=auditor) | ||||
elif kind in (b'relglob', b'path', b'rootfilesin', b'rootglob'): | elif kind in (b'relglob', b'path', b'rootfilesin', b'rootglob'): | ||||
pat = util.normpath(pat) | pat = util.normpath(pat) | ||||
elif kind in (b'listfile', b'listfile0'): | elif kind in (b'listfile', b'listfile0'): | ||||
try: | try: | ||||
self.bad = badfn | self.bad = badfn | ||||
def __call__(self, fn): | def __call__(self, fn): | ||||
return self.matchfn(fn) | return self.matchfn(fn) | ||||
# Callbacks related to how the matcher is used by dirstate.walk. | # Callbacks related to how the matcher is used by dirstate.walk. | ||||
# Subscribers to these events must monkeypatch the matcher object. | # Subscribers to these events must monkeypatch the matcher object. | ||||
def bad(self, f, msg): | def bad(self, f, msg): | ||||
'''Callback from dirstate.walk for each explicit file that can't be | """Callback from dirstate.walk for each explicit file that can't be | ||||
found/accessed, with an error message.''' | found/accessed, with an error message.""" | ||||
# If an traversedir is set, it will be called when a directory discovered | # If an traversedir is set, it will be called when a directory discovered | ||||
# by recursive traversal is visited. | # by recursive traversal is visited. | ||||
traversedir = None | traversedir = None | ||||
@propertycache | @propertycache | ||||
def _files(self): | def _files(self): | ||||
return [] | return [] | ||||
def files(self): | def files(self): | ||||
'''Explicitly listed files or patterns or roots: | """Explicitly listed files or patterns or roots: | ||||
if no patterns or .always(): empty list, | if no patterns or .always(): empty list, | ||||
if exact: list exact files, | if exact: list exact files, | ||||
if not .anypats(): list all files and dirs, | if not .anypats(): list all files and dirs, | ||||
else: optimal roots''' | else: optimal roots""" | ||||
return self._files | return self._files | ||||
@propertycache | @propertycache | ||||
def _fileset(self): | def _fileset(self): | ||||
return set(self._files) | return set(self._files) | ||||
def exact(self, f): | def exact(self, f): | ||||
'''Returns True if f is in .files().''' | '''Returns True if f is in .files().''' | ||||
return f in self._fileset | return f in self._fileset | ||||
def matchfn(self, f): | def matchfn(self, f): | ||||
return False | return False | ||||
def visitdir(self, dir): | def visitdir(self, dir): | ||||
'''Decides whether a directory should be visited based on whether it | """Decides whether a directory should be visited based on whether it | ||||
has potential matches in it or one of its subdirectories. This is | has potential matches in it or one of its subdirectories. This is | ||||
based on the match's primary, included, and excluded patterns. | based on the match's primary, included, and excluded patterns. | ||||
Returns the string 'all' if the given directory and all subdirectories | Returns the string 'all' if the given directory and all subdirectories | ||||
should be visited. Otherwise returns True or False indicating whether | should be visited. Otherwise returns True or False indicating whether | ||||
the given directory should be visited. | the given directory should be visited. | ||||
''' | """ | ||||
return True | return True | ||||
def visitchildrenset(self, dir): | def visitchildrenset(self, dir): | ||||
'''Decides whether a directory should be visited based on whether it | """Decides whether a directory should be visited based on whether it | ||||
has potential matches in it or one of its subdirectories, and | has potential matches in it or one of its subdirectories, and | ||||
potentially lists which subdirectories of that directory should be | potentially lists which subdirectories of that directory should be | ||||
visited. This is based on the match's primary, included, and excluded | visited. This is based on the match's primary, included, and excluded | ||||
patterns. | patterns. | ||||
This function is very similar to 'visitdir', and the following mapping | This function is very similar to 'visitdir', and the following mapping | ||||
can be applied: | can be applied: | ||||
Most matchers do not know if they're representing files or | Most matchers do not know if they're representing files or | ||||
directories. They see ['path:dir/f'] and don't know whether 'f' is a | directories. They see ['path:dir/f'] and don't know whether 'f' is a | ||||
file or a directory, so visitchildrenset('dir') for most matchers will | file or a directory, so visitchildrenset('dir') for most matchers will | ||||
return {'f'}, but if the matcher knows it's a file (like exactmatcher | return {'f'}, but if the matcher knows it's a file (like exactmatcher | ||||
does), it may return 'this'. Do not rely on the return being a set | does), it may return 'this'. Do not rely on the return being a set | ||||
indicating that there are no files in this dir to investigate (or | indicating that there are no files in this dir to investigate (or | ||||
equivalently that if there are files to investigate in 'dir' that it | equivalently that if there are files to investigate in 'dir' that it | ||||
will always return 'this'). | will always return 'this'). | ||||
''' | """ | ||||
return b'this' | return b'this' | ||||
def always(self): | def always(self): | ||||
'''Matcher will match everything and .files() will be empty -- | """Matcher will match everything and .files() will be empty -- | ||||
optimization might be possible.''' | optimization might be possible.""" | ||||
return False | return False | ||||
def isexact(self): | def isexact(self): | ||||
'''Matcher will match exactly the list of files in .files() -- | """Matcher will match exactly the list of files in .files() -- | ||||
optimization might be possible.''' | optimization might be possible.""" | ||||
return False | return False | ||||
def prefix(self): | def prefix(self): | ||||
'''Matcher will match the paths in .files() recursively -- | """Matcher will match the paths in .files() recursively -- | ||||
optimization might be possible.''' | optimization might be possible.""" | ||||
return False | return False | ||||
def anypats(self): | def anypats(self): | ||||
'''None of .always(), .isexact(), and .prefix() is true -- | """None of .always(), .isexact(), and .prefix() is true -- | ||||
optimizations will be difficult.''' | optimizations will be difficult.""" | ||||
return not self.always() and not self.isexact() and not self.prefix() | return not self.always() and not self.isexact() and not self.prefix() | ||||
class alwaysmatcher(basematcher): | class alwaysmatcher(basematcher): | ||||
'''Matches everything.''' | '''Matches everything.''' | ||||
def __init__(self, badfn=None): | def __init__(self, badfn=None): | ||||
super(alwaysmatcher, self).__init__(badfn) | super(alwaysmatcher, self).__init__(badfn) | ||||
return set() | return set() | ||||
@encoding.strmethod | @encoding.strmethod | ||||
def __repr__(self): | def __repr__(self): | ||||
return b'<includematcher includes=%r>' % pycompat.bytestr(self._pats) | return b'<includematcher includes=%r>' % pycompat.bytestr(self._pats) | ||||
class exactmatcher(basematcher): | class exactmatcher(basematcher): | ||||
r'''Matches the input files exactly. They are interpreted as paths, not | r"""Matches the input files exactly. They are interpreted as paths, not | ||||
patterns (so no kind-prefixes). | patterns (so no kind-prefixes). | ||||
>>> m = exactmatcher([b'a.txt', br're:.*\.c$']) | >>> m = exactmatcher([b'a.txt', br're:.*\.c$']) | ||||
>>> m(b'a.txt') | >>> m(b'a.txt') | ||||
True | True | ||||
>>> m(b'b.txt') | >>> m(b'b.txt') | ||||
False | False | ||||
Input files that would be matched are exactly those returned by .files() | Input files that would be matched are exactly those returned by .files() | ||||
>>> m.files() | >>> m.files() | ||||
['a.txt', 're:.*\\.c$'] | ['a.txt', 're:.*\\.c$'] | ||||
So pattern 're:.*\.c$' is not considered as a regex, but as a file name | So pattern 're:.*\.c$' is not considered as a regex, but as a file name | ||||
>>> m(b'main.c') | >>> m(b'main.c') | ||||
False | False | ||||
>>> m(br're:.*\.c$') | >>> m(br're:.*\.c$') | ||||
True | True | ||||
''' | """ | ||||
def __init__(self, files, badfn=None): | def __init__(self, files, badfn=None): | ||||
super(exactmatcher, self).__init__(badfn) | super(exactmatcher, self).__init__(badfn) | ||||
if isinstance(files, list): | if isinstance(files, list): | ||||
self._files = files | self._files = files | ||||
else: | else: | ||||
self._files = list(files) | self._files = list(files) | ||||
return True | return True | ||||
@encoding.strmethod | @encoding.strmethod | ||||
def __repr__(self): | def __repr__(self): | ||||
return b'<exactmatcher files=%r>' % self._files | return b'<exactmatcher files=%r>' % self._files | ||||
class differencematcher(basematcher): | class differencematcher(basematcher): | ||||
'''Composes two matchers by matching if the first matches and the second | """Composes two matchers by matching if the first matches and the second | ||||
does not. | does not. | ||||
The second matcher's non-matching-attributes (bad, traversedir) are ignored. | The second matcher's non-matching-attributes (bad, traversedir) are ignored. | ||||
''' | """ | ||||
def __init__(self, m1, m2): | def __init__(self, m1, m2): | ||||
super(differencematcher, self).__init__() | super(differencematcher, self).__init__() | ||||
self._m1 = m1 | self._m1 = m1 | ||||
self._m2 = m2 | self._m2 = m2 | ||||
self.bad = m1.bad | self.bad = m1.bad | ||||
self.traversedir = m1.traversedir | self.traversedir = m1.traversedir | ||||
return self._m1.isexact() | return self._m1.isexact() | ||||
@encoding.strmethod | @encoding.strmethod | ||||
def __repr__(self): | def __repr__(self): | ||||
return b'<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2) | return b'<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2) | ||||
def intersectmatchers(m1, m2): | def intersectmatchers(m1, m2): | ||||
'''Composes two matchers by matching if both of them match. | """Composes two matchers by matching if both of them match. | ||||
The second matcher's non-matching-attributes (bad, traversedir) are ignored. | The second matcher's non-matching-attributes (bad, traversedir) are ignored. | ||||
''' | """ | ||||
if m1 is None or m2 is None: | if m1 is None or m2 is None: | ||||
return m1 or m2 | return m1 or m2 | ||||
if m1.always(): | if m1.always(): | ||||
m = copy.copy(m2) | m = copy.copy(m2) | ||||
# TODO: Consider encapsulating these things in a class so there's only | # TODO: Consider encapsulating these things in a class so there's only | ||||
# one thing to copy from m1. | # one thing to copy from m1. | ||||
m.bad = m1.bad | m.bad = m1.bad | ||||
m.traversedir = m1.traversedir | m.traversedir = m1.traversedir | ||||
return r | return r | ||||
@encoding.strmethod | @encoding.strmethod | ||||
def __repr__(self): | def __repr__(self): | ||||
return b'<unionmatcher matchers=%r>' % self._matchers | return b'<unionmatcher matchers=%r>' % self._matchers | ||||
def patkind(pattern, default=None): | def patkind(pattern, default=None): | ||||
r'''If pattern is 'kind:pat' with a known kind, return kind. | r"""If pattern is 'kind:pat' with a known kind, return kind. | ||||
>>> patkind(br're:.*\.c$') | >>> patkind(br're:.*\.c$') | ||||
're' | 're' | ||||
>>> patkind(b'glob:*.c') | >>> patkind(b'glob:*.c') | ||||
'glob' | 'glob' | ||||
>>> patkind(b'relpath:test.py') | >>> patkind(b'relpath:test.py') | ||||
'relpath' | 'relpath' | ||||
>>> patkind(b'main.py') | >>> patkind(b'main.py') | ||||
>>> patkind(b'main.py', default=b're') | >>> patkind(b'main.py', default=b're') | ||||
're' | 're' | ||||
''' | """ | ||||
return _patsplit(pattern, default)[0] | return _patsplit(pattern, default)[0] | ||||
def _patsplit(pattern, default): | def _patsplit(pattern, default): | ||||
"""Split a string into the optional pattern kind prefix and the actual | """Split a string into the optional pattern kind prefix and the actual | ||||
pattern.""" | pattern.""" | ||||
if b':' in pattern: | if b':' in pattern: | ||||
kind, pat = pattern.split(b':', 1) | kind, pat = pattern.split(b':', 1) | ||||
if kind in allpatternkinds: | if kind in allpatternkinds: | ||||
return kind, pat | return kind, pat | ||||
return default, pattern | return default, pattern | ||||
def _globre(pat): | def _globre(pat): | ||||
r'''Convert an extended glob string to a regexp string. | r"""Convert an extended glob string to a regexp string. | ||||
>>> from . import pycompat | >>> from . import pycompat | ||||
>>> def bprint(s): | >>> def bprint(s): | ||||
... print(pycompat.sysstr(s)) | ... print(pycompat.sysstr(s)) | ||||
>>> bprint(_globre(br'?')) | >>> bprint(_globre(br'?')) | ||||
. | . | ||||
>>> bprint(_globre(br'*')) | >>> bprint(_globre(br'*')) | ||||
[^/]* | [^/]* | ||||
>>> bprint(_globre(br'**')) | >>> bprint(_globre(br'**')) | ||||
.* | .* | ||||
>>> bprint(_globre(br'**/a')) | >>> bprint(_globre(br'**/a')) | ||||
(?:.*/)?a | (?:.*/)?a | ||||
>>> bprint(_globre(br'a/**/b')) | >>> bprint(_globre(br'a/**/b')) | ||||
a/(?:.*/)?b | a/(?:.*/)?b | ||||
>>> bprint(_globre(br'[a*?!^][^b][!c]')) | >>> bprint(_globre(br'[a*?!^][^b][!c]')) | ||||
[a*?!^][\^b][^c] | [a*?!^][\^b][^c] | ||||
>>> bprint(_globre(br'{a,b}')) | >>> bprint(_globre(br'{a,b}')) | ||||
(?:a|b) | (?:a|b) | ||||
>>> bprint(_globre(br'.\*\?')) | >>> bprint(_globre(br'.\*\?')) | ||||
\.\*\? | \.\*\? | ||||
''' | """ | ||||
i, n = 0, len(pat) | i, n = 0, len(pat) | ||||
res = b'' | res = b'' | ||||
group = 0 | group = 0 | ||||
escape = util.stringutil.regexbytesescapemap.get | escape = util.stringutil.regexbytesescapemap.get | ||||
def peek(): | def peek(): | ||||
return i < n and pat[i : i + 1] | return i < n and pat[i : i + 1] | ||||
else: | else: | ||||
res += escape(c, c) | res += escape(c, c) | ||||
else: | else: | ||||
res += escape(c, c) | res += escape(c, c) | ||||
return res | return res | ||||
def _regex(kind, pat, globsuffix): | def _regex(kind, pat, globsuffix): | ||||
'''Convert a (normalized) pattern of any kind into a | """Convert a (normalized) pattern of any kind into a | ||||
regular expression. | regular expression. | ||||
globsuffix is appended to the regexp of globs.''' | globsuffix is appended to the regexp of globs.""" | ||||
if not pat and kind in (b'glob', b'relpath'): | if not pat and kind in (b'glob', b'relpath'): | ||||
return b'' | return b'' | ||||
if kind == b're': | if kind == b're': | ||||
return pat | return pat | ||||
if kind in (b'path', b'relpath'): | if kind in (b'path', b'relpath'): | ||||
if pat == b'.': | if pat == b'.': | ||||
return b'' | return b'' | ||||
return util.stringutil.reescape(pat) + b'(?:/|$)' | return util.stringutil.reescape(pat) + b'(?:/|$)' | ||||
return pat | return pat | ||||
return b'.*' + pat | return b'.*' + pat | ||||
if kind in (b'glob', b'rootglob'): | if kind in (b'glob', b'rootglob'): | ||||
return _globre(pat) + globsuffix | return _globre(pat) + globsuffix | ||||
raise error.ProgrammingError(b'not a regex pattern: %s:%s' % (kind, pat)) | raise error.ProgrammingError(b'not a regex pattern: %s:%s' % (kind, pat)) | ||||
def _buildmatch(kindpats, globsuffix, root): | def _buildmatch(kindpats, globsuffix, root): | ||||
'''Return regexp string and a matcher function for kindpats. | """Return regexp string and a matcher function for kindpats. | ||||
globsuffix is appended to the regexp of globs.''' | globsuffix is appended to the regexp of globs.""" | ||||
matchfuncs = [] | matchfuncs = [] | ||||
subincludes, kindpats = _expandsubinclude(kindpats, root) | subincludes, kindpats = _expandsubinclude(kindpats, root) | ||||
if subincludes: | if subincludes: | ||||
submatchers = {} | submatchers = {} | ||||
def matchsubinclude(f): | def matchsubinclude(f): | ||||
for prefix, matcherargs in subincludes: | for prefix, matcherargs in subincludes: | ||||
_(b"%s: invalid pattern (%s): %s") % (s, k, p) | _(b"%s: invalid pattern (%s): %s") % (s, k, p) | ||||
) | ) | ||||
else: | else: | ||||
raise error.Abort(_(b"invalid pattern (%s): %s") % (k, p)) | raise error.Abort(_(b"invalid pattern (%s): %s") % (k, p)) | ||||
raise error.Abort(_(b"invalid pattern")) | raise error.Abort(_(b"invalid pattern")) | ||||
def _patternrootsanddirs(kindpats): | def _patternrootsanddirs(kindpats): | ||||
'''Returns roots and directories corresponding to each pattern. | """Returns roots and directories corresponding to each pattern. | ||||
This calculates the roots and directories exactly matching the patterns and | This calculates the roots and directories exactly matching the patterns and | ||||
returns a tuple of (roots, dirs) for each. It does not return other | returns a tuple of (roots, dirs) for each. It does not return other | ||||
directories which may also need to be considered, like the parent | directories which may also need to be considered, like the parent | ||||
directories. | directories. | ||||
''' | """ | ||||
r = [] | r = [] | ||||
d = [] | d = [] | ||||
for kind, pat, source in kindpats: | for kind, pat, source in kindpats: | ||||
if kind in (b'glob', b'rootglob'): # find the non-glob prefix | if kind in (b'glob', b'rootglob'): # find the non-glob prefix | ||||
root = [] | root = [] | ||||
for p in pat.split(b'/'): | for p in pat.split(b'/'): | ||||
if b'[' in p or b'{' in p or b'*' in p or b'?' in p: | if b'[' in p or b'{' in p or b'*' in p or b'?' in p: | ||||
break | break | ||||
def _roots(kindpats): | def _roots(kindpats): | ||||
'''Returns root directories to match recursively from the given patterns.''' | '''Returns root directories to match recursively from the given patterns.''' | ||||
roots, dirs = _patternrootsanddirs(kindpats) | roots, dirs = _patternrootsanddirs(kindpats) | ||||
return roots | return roots | ||||
def _rootsdirsandparents(kindpats): | def _rootsdirsandparents(kindpats): | ||||
'''Returns roots and exact directories from patterns. | """Returns roots and exact directories from patterns. | ||||
`roots` are directories to match recursively, `dirs` should | `roots` are directories to match recursively, `dirs` should | ||||
be matched non-recursively, and `parents` are the implicitly required | be matched non-recursively, and `parents` are the implicitly required | ||||
directories to walk to items in either roots or dirs. | directories to walk to items in either roots or dirs. | ||||
Returns a tuple of (roots, dirs, parents). | Returns a tuple of (roots, dirs, parents). | ||||
>>> r = _rootsdirsandparents( | >>> r = _rootsdirsandparents( | ||||
... (b'path', b'', b'')]) | ... (b'path', b'', b'')]) | ||||
>>> print(r[0:2], sorted(r[2])) # the set has an unstable output | >>> print(r[0:2], sorted(r[2])) # the set has an unstable output | ||||
(['r', 'p/p', ''], []) ['', 'p'] | (['r', 'p/p', ''], []) ['', 'p'] | ||||
>>> r = _rootsdirsandparents( | >>> r = _rootsdirsandparents( | ||||
... [(b'relglob', b'rg*', b''), (b're', b're/', b''), | ... [(b'relglob', b'rg*', b''), (b're', b're/', b''), | ||||
... (b'relre', b'rr', b'')]) | ... (b'relre', b'rr', b'')]) | ||||
>>> print(r[0:2], sorted(r[2])) # the set has an unstable output | >>> print(r[0:2], sorted(r[2])) # the set has an unstable output | ||||
(['', '', ''], []) [''] | (['', '', ''], []) [''] | ||||
''' | """ | ||||
r, d = _patternrootsanddirs(kindpats) | r, d = _patternrootsanddirs(kindpats) | ||||
p = set() | p = set() | ||||
# Add the parents as non-recursive/exact directories, since they must be | # Add the parents as non-recursive/exact directories, since they must be | ||||
# scanned to get to either the roots or the other exact directories. | # scanned to get to either the roots or the other exact directories. | ||||
p.update(pathutil.dirs(d)) | p.update(pathutil.dirs(d)) | ||||
p.update(pathutil.dirs(r)) | p.update(pathutil.dirs(r)) | ||||
# FIXME: all uses of this function convert these to sets, do so before | # FIXME: all uses of this function convert these to sets, do so before | ||||
# returning. | # returning. | ||||
# FIXME: all uses of this function do not need anything in 'roots' and | # FIXME: all uses of this function do not need anything in 'roots' and | ||||
# 'dirs' to also be in 'parents', consider removing them before returning. | # 'dirs' to also be in 'parents', consider removing them before returning. | ||||
return r, d, p | return r, d, p | ||||
def _explicitfiles(kindpats): | def _explicitfiles(kindpats): | ||||
'''Returns the potential explicit filenames from the patterns. | """Returns the potential explicit filenames from the patterns. | ||||
>>> _explicitfiles([(b'path', b'foo/bar', b'')]) | >>> _explicitfiles([(b'path', b'foo/bar', b'')]) | ||||
['foo/bar'] | ['foo/bar'] | ||||
>>> _explicitfiles([(b'rootfilesin', b'foo/bar', b'')]) | >>> _explicitfiles([(b'rootfilesin', b'foo/bar', b'')]) | ||||
[] | [] | ||||
''' | """ | ||||
# Keep only the pattern kinds where one can specify filenames (vs only | # Keep only the pattern kinds where one can specify filenames (vs only | ||||
# directory names). | # directory names). | ||||
filable = [kp for kp in kindpats if kp[0] not in (b'rootfilesin',)] | filable = [kp for kp in kindpats if kp[0] not in (b'rootfilesin',)] | ||||
return _roots(filable) | return _roots(filable) | ||||
def _prefix(kindpats): | def _prefix(kindpats): | ||||
'''Whether all the patterns match a prefix (i.e. recursively)''' | '''Whether all the patterns match a prefix (i.e. recursively)''' | ||||
for kind, pat, source in kindpats: | for kind, pat, source in kindpats: | ||||
if kind not in (b'path', b'relpath'): | if kind not in (b'path', b'relpath'): | ||||
return False | return False | ||||
return True | return True | ||||
_commentre = None | _commentre = None | ||||
def readpatternfile(filepath, warn, sourceinfo=False): | def readpatternfile(filepath, warn, sourceinfo=False): | ||||
'''parse a pattern file, returning a list of | """parse a pattern file, returning a list of | ||||
patterns. These patterns should be given to compile() | patterns. These patterns should be given to compile() | ||||
to be validated and converted into a match function. | to be validated and converted into a match function. | ||||
trailing white space is dropped. | trailing white space is dropped. | ||||
the escape character is backslash. | the escape character is backslash. | ||||
comments start with #. | comments start with #. | ||||
empty lines are skipped. | empty lines are skipped. | ||||
lines can be of the following formats: | lines can be of the following formats: | ||||
syntax: regexp # defaults following lines to non-rooted regexps | syntax: regexp # defaults following lines to non-rooted regexps | ||||
syntax: glob # defaults following lines to non-rooted globs | syntax: glob # defaults following lines to non-rooted globs | ||||
re:pattern # non-rooted regular expression | re:pattern # non-rooted regular expression | ||||
glob:pattern # non-rooted glob | glob:pattern # non-rooted glob | ||||
rootglob:pat # rooted glob (same root as ^ in regexps) | rootglob:pat # rooted glob (same root as ^ in regexps) | ||||
pattern # pattern of the current default type | pattern # pattern of the current default type | ||||
if sourceinfo is set, returns a list of tuples: | if sourceinfo is set, returns a list of tuples: | ||||
(pattern, lineno, originalline). | (pattern, lineno, originalline). | ||||
This is useful to debug ignore patterns. | This is useful to debug ignore patterns. | ||||
''' | """ | ||||
syntaxes = { | syntaxes = { | ||||
b're': b'relre:', | b're': b'relre:', | ||||
b'regexp': b'relre:', | b'regexp': b'relre:', | ||||
b'glob': b'relglob:', | b'glob': b'relglob:', | ||||
b'rootglob': b'rootglob:', | b'rootglob': b'rootglob:', | ||||
b'include': b'include', | b'include': b'include', | ||||
b'subinclude': b'subinclude', | b'subinclude': b'subinclude', |
patches = mpatch.patches | patches = mpatch.patches | ||||
patchedsize = mpatch.patchedsize | patchedsize = mpatch.patchedsize | ||||
textdiff = bdiff.bdiff | textdiff = bdiff.bdiff | ||||
splitnewlines = bdiff.splitnewlines | splitnewlines = bdiff.splitnewlines | ||||
# TODO: this looks like it could be an attrs, which might help pytype | # TODO: this looks like it could be an attrs, which might help pytype | ||||
class diffopts(object): | class diffopts(object): | ||||
'''context is the number of context lines | """context is the number of context lines | ||||
text treats all files as text | text treats all files as text | ||||
showfunc enables diff -p output | showfunc enables diff -p output | ||||
git enables the git extended patch format | git enables the git extended patch format | ||||
nodates removes dates from diff headers | nodates removes dates from diff headers | ||||
nobinary ignores binary files | nobinary ignores binary files | ||||
noprefix disables the 'a/' and 'b/' prefixes (ignored in plain mode) | noprefix disables the 'a/' and 'b/' prefixes (ignored in plain mode) | ||||
ignorews ignores all whitespace changes in the diff | ignorews ignores all whitespace changes in the diff | ||||
ignorewsamount ignores changes in the amount of whitespace | ignorewsamount ignores changes in the amount of whitespace | ||||
ignoreblanklines ignores changes whose lines are all blank | ignoreblanklines ignores changes whose lines are all blank | ||||
upgrade generates git diffs to avoid data loss | upgrade generates git diffs to avoid data loss | ||||
''' | """ | ||||
_HAS_DYNAMIC_ATTRIBUTES = True | _HAS_DYNAMIC_ATTRIBUTES = True | ||||
defaults = { | defaults = { | ||||
b'context': 3, | b'context': 3, | ||||
b'text': False, | b'text': False, | ||||
b'showfunc': False, | b'showfunc': False, | ||||
b'git': False, | b'git': False, |
b'remote differs from untracked local', | b'remote differs from untracked local', | ||||
) | ) | ||||
elif config == b'abort': | elif config == b'abort': | ||||
abortconflicts.add(f) | abortconflicts.add(f) | ||||
else: | else: | ||||
if config == b'warn': | if config == b'warn': | ||||
warnconflicts.add(f) | warnconflicts.add(f) | ||||
mresult.addfile( | mresult.addfile( | ||||
f, mergestatemod.ACTION_GET, (fl2, True), b'remote created', | f, | ||||
mergestatemod.ACTION_GET, | |||||
(fl2, True), | |||||
b'remote created', | |||||
) | ) | ||||
for f in sorted(abortconflicts): | for f in sorted(abortconflicts): | ||||
warn = repo.ui.warn | warn = repo.ui.warn | ||||
if f in pathconflicts: | if f in pathconflicts: | ||||
if repo.wvfs.isfileorlink(f): | if repo.wvfs.isfileorlink(f): | ||||
warn(_(b"%s: untracked file conflicts with directory\n") % f) | warn(_(b"%s: untracked file conflicts with directory\n") % f) | ||||
else: | else: | ||||
for f in wctx.deleted(): | for f in wctx.deleted(): | ||||
if f not in mctx: | if f not in mctx: | ||||
mresult.addfile(f, m, None, b"forget deleted") | mresult.addfile(f, m, None, b"forget deleted") | ||||
if not branchmerge: | if not branchmerge: | ||||
for f in wctx.removed(): | for f in wctx.removed(): | ||||
if f not in mctx: | if f not in mctx: | ||||
mresult.addfile( | mresult.addfile( | ||||
f, mergestatemod.ACTION_FORGET, None, b"forget removed", | f, | ||||
mergestatemod.ACTION_FORGET, | |||||
None, | |||||
b"forget removed", | |||||
) | ) | ||||
def _checkcollision(repo, wmf, mresult): | def _checkcollision(repo, wmf, mresult): | ||||
""" | """ | ||||
Check for case-folding collisions. | Check for case-folding collisions. | ||||
""" | """ | ||||
# If the repo is narrowed, filter out files outside the narrowspec. | # If the repo is narrowed, filter out files outside the narrowspec. | ||||
) | ) | ||||
else: | else: | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b'conflict in file \'%s\' is outside narrow clone') % f | _(b'conflict in file \'%s\' is outside narrow clone') % f | ||||
) | ) | ||||
class mergeresult(object): | class mergeresult(object): | ||||
'''An object representing result of merging manifests. | """An object representing result of merging manifests. | ||||
It has information about what actions need to be performed on dirstate | It has information about what actions need to be performed on dirstate | ||||
mapping of divergent renames and other such cases.''' | mapping of divergent renames and other such cases.""" | ||||
def __init__(self): | def __init__(self): | ||||
""" | """ | ||||
filemapping: dict of filename as keys and action related info as values | filemapping: dict of filename as keys and action related info as values | ||||
diverge: mapping of source name -> list of dest name for | diverge: mapping of source name -> list of dest name for | ||||
divergent renames | divergent renames | ||||
renamedelete: mapping of source name -> list of destinations for files | renamedelete: mapping of source name -> list of destinations for files | ||||
deleted on one side and renamed on other. | deleted on one side and renamed on other. | ||||
commitinfo: dict containing data which should be used on commit | commitinfo: dict containing data which should be used on commit | ||||
contains a filename -> info mapping | contains a filename -> info mapping | ||||
actionmapping: dict of action names as keys and values are dict of | actionmapping: dict of action names as keys and values are dict of | ||||
filename as key and related data as values | filename as key and related data as values | ||||
""" | """ | ||||
self._filemapping = {} | self._filemapping = {} | ||||
self._diverge = {} | self._diverge = {} | ||||
self._renamedelete = {} | self._renamedelete = {} | ||||
self._commitinfo = collections.defaultdict(dict) | self._commitinfo = collections.defaultdict(dict) | ||||
self._actionmapping = collections.defaultdict(dict) | self._actionmapping = collections.defaultdict(dict) | ||||
def updatevalues(self, diverge, renamedelete): | def updatevalues(self, diverge, renamedelete): | ||||
self._diverge = diverge | self._diverge = diverge | ||||
self._renamedelete = renamedelete | self._renamedelete = renamedelete | ||||
def addfile(self, filename, action, data, message): | def addfile(self, filename, action, data, message): | ||||
""" adds a new file to the mergeresult object | """adds a new file to the mergeresult object | ||||
filename: file which we are adding | filename: file which we are adding | ||||
action: one of mergestatemod.ACTION_* | action: one of mergestatemod.ACTION_* | ||||
data: a tuple of information like fctx and ctx related to this merge | data: a tuple of information like fctx and ctx related to this merge | ||||
message: a message about the merge | message: a message about the merge | ||||
""" | """ | ||||
# if the file already existed, we need to delete it's old | # if the file already existed, we need to delete it's old | ||||
# entry form _actionmapping too | # entry form _actionmapping too | ||||
if filename in self._filemapping: | if filename in self._filemapping: | ||||
a, d, m = self._filemapping[filename] | a, d, m = self._filemapping[filename] | ||||
del self._actionmapping[a][filename] | del self._actionmapping[a][filename] | ||||
self._filemapping[filename] = (action, data, message) | self._filemapping[filename] = (action, data, message) | ||||
self._actionmapping[action][filename] = (data, message) | self._actionmapping[action][filename] = (data, message) | ||||
def getfile(self, filename, default_return=None): | def getfile(self, filename, default_return=None): | ||||
""" returns (action, args, msg) about this file | """returns (action, args, msg) about this file | ||||
returns default_return if the file is not present """ | returns default_return if the file is not present""" | ||||
if filename in self._filemapping: | if filename in self._filemapping: | ||||
return self._filemapping[filename] | return self._filemapping[filename] | ||||
return default_return | return default_return | ||||
def files(self, actions=None): | def files(self, actions=None): | ||||
""" returns files on which provided action needs to perfromed | """returns files on which provided action needs to perfromed | ||||
If actions is None, all files are returned | If actions is None, all files are returned | ||||
""" | """ | ||||
# TODO: think whether we should return renamedelete and | # TODO: think whether we should return renamedelete and | ||||
# diverge filenames also | # diverge filenames also | ||||
if actions is None: | if actions is None: | ||||
for f in self._filemapping: | for f in self._filemapping: | ||||
yield f | yield f | ||||
else: | else: | ||||
for a in actions: | for a in actions: | ||||
for f in self._actionmapping[a]: | for f in self._actionmapping[a]: | ||||
yield f | yield f | ||||
def removefile(self, filename): | def removefile(self, filename): | ||||
""" removes a file from the mergeresult object as the file might | """removes a file from the mergeresult object as the file might | ||||
not merging anymore """ | not merging anymore""" | ||||
action, data, message = self._filemapping[filename] | action, data, message = self._filemapping[filename] | ||||
del self._filemapping[filename] | del self._filemapping[filename] | ||||
del self._actionmapping[action][filename] | del self._actionmapping[action][filename] | ||||
def getactions(self, actions, sort=False): | def getactions(self, actions, sort=False): | ||||
""" get list of files which are marked with these actions | """get list of files which are marked with these actions | ||||
if sort is true, files for each action is sorted and then added | if sort is true, files for each action is sorted and then added | ||||
Returns a list of tuple of form (filename, data, message) | Returns a list of tuple of form (filename, data, message) | ||||
""" | """ | ||||
for a in actions: | for a in actions: | ||||
if sort: | if sort: | ||||
for f in sorted(self._actionmapping[a]): | for f in sorted(self._actionmapping[a]): | ||||
args, msg = self._actionmapping[a][f] | args, msg = self._actionmapping[a][f] | ||||
yield f, args, msg | yield f, args, msg | ||||
else: | else: | ||||
for f, (args, msg) in pycompat.iteritems( | for f, (args, msg) in pycompat.iteritems( | ||||
self._actionmapping[a] | self._actionmapping[a] | ||||
): | ): | ||||
yield f, args, msg | yield f, args, msg | ||||
def len(self, actions=None): | def len(self, actions=None): | ||||
""" returns number of files which needs actions | """returns number of files which needs actions | ||||
if actions is passed, total of number of files in that action | if actions is passed, total of number of files in that action | ||||
only is returned """ | only is returned""" | ||||
if actions is None: | if actions is None: | ||||
return len(self._filemapping) | return len(self._filemapping) | ||||
return sum(len(self._actionmapping[a]) for a in actions) | return sum(len(self._actionmapping[a]) for a in actions) | ||||
def filemap(self, sort=False): | def filemap(self, sort=False): | ||||
if sorted: | if sorted: | ||||
for key, val in sorted(pycompat.iteritems(self._filemapping)): | for key, val in sorted(pycompat.iteritems(self._filemapping)): | ||||
yield key, val | yield key, val | ||||
else: | else: | ||||
for key, val in pycompat.iteritems(self._filemapping): | for key, val in pycompat.iteritems(self._filemapping): | ||||
yield key, val | yield key, val | ||||
def addcommitinfo(self, filename, key, value): | def addcommitinfo(self, filename, key, value): | ||||
""" adds key-value information about filename which will be required | """adds key-value information about filename which will be required | ||||
while committing this merge """ | while committing this merge""" | ||||
self._commitinfo[filename][key] = value | self._commitinfo[filename][key] = value | ||||
@property | @property | ||||
def diverge(self): | def diverge(self): | ||||
return self._diverge | return self._diverge | ||||
@property | @property | ||||
def renamedelete(self): | def renamedelete(self): | ||||
return self._renamedelete | return self._renamedelete | ||||
@property | @property | ||||
def commitinfo(self): | def commitinfo(self): | ||||
return self._commitinfo | return self._commitinfo | ||||
@property | @property | ||||
def actionsdict(self): | def actionsdict(self): | ||||
""" returns a dictionary of actions to be perfomed with action as key | """returns a dictionary of actions to be perfomed with action as key | ||||
and a list of files and related arguments as values """ | and a list of files and related arguments as values""" | ||||
res = collections.defaultdict(list) | res = collections.defaultdict(list) | ||||
for a, d in pycompat.iteritems(self._actionmapping): | for a, d in pycompat.iteritems(self._actionmapping): | ||||
for f, (args, msg) in pycompat.iteritems(d): | for f, (args, msg) in pycompat.iteritems(d): | ||||
res[a].append((f, args, msg)) | res[a].append((f, args, msg)) | ||||
return res | return res | ||||
def setactions(self, actions): | def setactions(self, actions): | ||||
self._filemapping = actions | self._filemapping = actions | ||||
self._actionmapping = collections.defaultdict(dict) | self._actionmapping = collections.defaultdict(dict) | ||||
for f, (act, data, msg) in pycompat.iteritems(self._filemapping): | for f, (act, data, msg) in pycompat.iteritems(self._filemapping): | ||||
self._actionmapping[act][f] = data, msg | self._actionmapping[act][f] = data, msg | ||||
def hasconflicts(self): | def hasconflicts(self): | ||||
""" tells whether this merge resulted in some actions which can | """tells whether this merge resulted in some actions which can | ||||
result in conflicts or not """ | result in conflicts or not""" | ||||
for a in self._actionmapping.keys(): | for a in self._actionmapping.keys(): | ||||
if ( | if ( | ||||
a | a | ||||
not in ( | not in ( | ||||
mergestatemod.ACTION_GET, | mergestatemod.ACTION_GET, | ||||
mergestatemod.ACTION_EXEC, | mergestatemod.ACTION_EXEC, | ||||
mergestatemod.ACTION_REMOVE, | mergestatemod.ACTION_REMOVE, | ||||
mergestatemod.ACTION_PATH_CONFLICT_RESOLVE, | mergestatemod.ACTION_PATH_CONFLICT_RESOLVE, | ||||
b'other replaced from %s' % fa, | b'other replaced from %s' % fa, | ||||
) | ) | ||||
else: | else: | ||||
a = ma[f] | a = ma[f] | ||||
fla = ma.flags(f) | fla = ma.flags(f) | ||||
nol = b'l' not in fl1 + fl2 + fla | nol = b'l' not in fl1 + fl2 + fla | ||||
if n2 == a and fl2 == fla: | if n2 == a and fl2 == fla: | ||||
mresult.addfile( | mresult.addfile( | ||||
f, mergestatemod.ACTION_KEEP, (), b'remote unchanged', | f, | ||||
mergestatemod.ACTION_KEEP, | |||||
(), | |||||
b'remote unchanged', | |||||
) | ) | ||||
elif n1 == a and fl1 == fla: # local unchanged - use remote | elif n1 == a and fl1 == fla: # local unchanged - use remote | ||||
if n1 == n2: # optimization: keep local content | if n1 == n2: # optimization: keep local content | ||||
mresult.addfile( | mresult.addfile( | ||||
f, | f, | ||||
mergestatemod.ACTION_EXEC, | mergestatemod.ACTION_EXEC, | ||||
(fl2,), | (fl2,), | ||||
b'update permissions', | b'update permissions', | ||||
if branchmerge: | if branchmerge: | ||||
mresult.addcommitinfo( | mresult.addcommitinfo( | ||||
f, b'merge-removal-candidate', b'yes' | f, b'merge-removal-candidate', b'yes' | ||||
) | ) | ||||
elif n1 == addednodeid: | elif n1 == addednodeid: | ||||
# This file was locally added. We should forget it instead of | # This file was locally added. We should forget it instead of | ||||
# deleting it. | # deleting it. | ||||
mresult.addfile( | mresult.addfile( | ||||
f, mergestatemod.ACTION_FORGET, None, b'remote deleted', | f, | ||||
mergestatemod.ACTION_FORGET, | |||||
None, | |||||
b'remote deleted', | |||||
) | ) | ||||
else: | else: | ||||
mresult.addfile( | mresult.addfile( | ||||
f, mergestatemod.ACTION_REMOVE, None, b'other deleted', | f, | ||||
mergestatemod.ACTION_REMOVE, | |||||
None, | |||||
b'other deleted', | |||||
) | ) | ||||
if branchmerge: | if branchmerge: | ||||
# the file must be absent after merging, | # the file must be absent after merging, | ||||
# howeber the user might make | # howeber the user might make | ||||
# the file reappear using revert and if they does, | # the file reappear using revert and if they does, | ||||
# we force create a new node | # we force create a new node | ||||
mresult.addcommitinfo( | mresult.addcommitinfo( | ||||
f, b'merge-removal-candidate', b'yes' | f, b'merge-removal-candidate', b'yes' | ||||
renamedelete.update(branch_copies2.renamedelete) | renamedelete.update(branch_copies2.renamedelete) | ||||
mresult.updatevalues(diverge, renamedelete) | mresult.updatevalues(diverge, renamedelete) | ||||
return mresult | return mresult | ||||
def _resolvetrivial(repo, wctx, mctx, ancestor, mresult): | def _resolvetrivial(repo, wctx, mctx, ancestor, mresult): | ||||
"""Resolves false conflicts where the nodeid changed but the content | """Resolves false conflicts where the nodeid changed but the content | ||||
remained the same.""" | remained the same.""" | ||||
# We force a copy of actions.items() because we're going to mutate | # We force a copy of actions.items() because we're going to mutate | ||||
# actions as we resolve trivial conflicts. | # actions as we resolve trivial conflicts. | ||||
for f in list(mresult.files((mergestatemod.ACTION_CHANGED_DELETED,))): | for f in list(mresult.files((mergestatemod.ACTION_CHANGED_DELETED,))): | ||||
if f in ancestor and not wctx[f].cmp(ancestor[f]): | if f in ancestor and not wctx[f].cmp(ancestor[f]): | ||||
# local did change but ended up with same content | # local did change but ended up with same content | ||||
mresult.addfile( | mresult.addfile( | ||||
f, mergestatemod.ACTION_REMOVE, None, b'prompt same' | f, mergestatemod.ACTION_REMOVE, None, b'prompt same' | ||||
) | ) | ||||
mergestatemod.ACTION_LOCAL_DIR_RENAME_GET, | mergestatemod.ACTION_LOCAL_DIR_RENAME_GET, | ||||
mergestatemod.ACTION_MERGE, | mergestatemod.ACTION_MERGE, | ||||
] | ] | ||||
) | ) | ||||
prefetch = scmutil.prefetchfiles | prefetch = scmutil.prefetchfiles | ||||
matchfiles = scmutil.matchfiles | matchfiles = scmutil.matchfiles | ||||
prefetch( | prefetch( | ||||
repo, [(ctx.rev(), matchfiles(repo, files),)], | repo, | ||||
[ | |||||
( | |||||
ctx.rev(), | |||||
matchfiles(repo, files), | |||||
) | |||||
], | |||||
) | ) | ||||
@attr.s(frozen=True) | @attr.s(frozen=True) | ||||
class updateresult(object): | class updateresult(object): | ||||
updatedcount = attr.ib() | updatedcount = attr.ib() | ||||
mergedcount = attr.ib() | mergedcount = attr.ib() | ||||
removedcount = attr.ib() | removedcount = attr.ib() | ||||
unresolvedcount = attr.ib() | unresolvedcount = attr.ib() | ||||
def isempty(self): | def isempty(self): | ||||
return not ( | return not ( | ||||
self.updatedcount | self.updatedcount | ||||
or self.mergedcount | or self.mergedcount | ||||
or self.removedcount | or self.removedcount | ||||
or self.unresolvedcount | or self.unresolvedcount | ||||
) | ) | ||||
def applyupdates( | def applyupdates( | ||||
repo, mresult, wctx, mctx, overwrite, wantfiledata, labels=None, | repo, | ||||
mresult, | |||||
wctx, | |||||
mctx, | |||||
overwrite, | |||||
wantfiledata, | |||||
labels=None, | |||||
): | ): | ||||
"""apply the merge action list to the working directory | """apply the merge action list to the working directory | ||||
mresult is a mergeresult object representing result of the merge | mresult is a mergeresult object representing result of the merge | ||||
wctx is the working copy context | wctx is the working copy context | ||||
mctx is the context to be merged into the working copy | mctx is the context to be merged into the working copy | ||||
Return a tuple of (counts, filedata), where counts is a tuple | Return a tuple of (counts, filedata), where counts is a tuple | ||||
b'fsmonitor', b'warn_update_file_count' | b'fsmonitor', b'warn_update_file_count' | ||||
) | ) | ||||
# avoid cycle dirstate -> sparse -> merge -> dirstate | # avoid cycle dirstate -> sparse -> merge -> dirstate | ||||
from . import dirstate | from . import dirstate | ||||
if dirstate.rustmod is not None: | if dirstate.rustmod is not None: | ||||
# When using rust status, fsmonitor becomes necessary at higher sizes | # When using rust status, fsmonitor becomes necessary at higher sizes | ||||
fsmonitorthreshold = repo.ui.configint( | fsmonitorthreshold = repo.ui.configint( | ||||
b'fsmonitor', b'warn_update_file_count_rust', | b'fsmonitor', | ||||
b'warn_update_file_count_rust', | |||||
) | ) | ||||
try: | try: | ||||
# avoid cycle: extensions -> cmdutil -> merge | # avoid cycle: extensions -> cmdutil -> merge | ||||
from . import extensions | from . import extensions | ||||
extensions.find(b'fsmonitor') | extensions.find(b'fsmonitor') | ||||
fsmonitorenabled = repo.ui.config(b'fsmonitor', b'mode') != b'off' | fsmonitorenabled = repo.ui.config(b'fsmonitor', b'mode') != b'off' | ||||
b"local%(l)s changed %(f)s which other%(o)s deleted\n" | b"local%(l)s changed %(f)s which other%(o)s deleted\n" | ||||
b"use (c)hanged version or (d)elete?" | b"use (c)hanged version or (d)elete?" | ||||
b"$$ &Changed $$ &Delete" | b"$$ &Changed $$ &Delete" | ||||
) | ) | ||||
% prompts, | % prompts, | ||||
0, | 0, | ||||
): | ): | ||||
mresult.addfile( | mresult.addfile( | ||||
f, mergestatemod.ACTION_REMOVE, None, b'prompt delete', | f, | ||||
mergestatemod.ACTION_REMOVE, | |||||
None, | |||||
b'prompt delete', | |||||
) | ) | ||||
elif f in p1: | elif f in p1: | ||||
mresult.addfile( | mresult.addfile( | ||||
f, | f, | ||||
mergestatemod.ACTION_ADD_MODIFIED, | mergestatemod.ACTION_ADD_MODIFIED, | ||||
None, | None, | ||||
b'prompt keep', | b'prompt keep', | ||||
) | ) | ||||
else: | else: | ||||
mresult.addfile( | mresult.addfile( | ||||
f, mergestatemod.ACTION_ADD, None, b'prompt keep', | f, | ||||
mergestatemod.ACTION_ADD, | |||||
None, | |||||
b'prompt keep', | |||||
) | ) | ||||
elif m == mergestatemod.ACTION_DELETED_CHANGED: | elif m == mergestatemod.ACTION_DELETED_CHANGED: | ||||
f1, f2, fa, move, anc = args | f1, f2, fa, move, anc = args | ||||
flags = p2[f2].flags() | flags = p2[f2].flags() | ||||
if ( | if ( | ||||
repo.ui.promptchoice( | repo.ui.promptchoice( | ||||
_( | _( | ||||
b"other%(o)s changed %(f)s which local%(l)s deleted\n" | b"other%(o)s changed %(f)s which local%(l)s deleted\n" | ||||
repo.vfs.write(b'updatestate', p2.hex()) | repo.vfs.write(b'updatestate', p2.hex()) | ||||
_advertisefsmonitor( | _advertisefsmonitor( | ||||
repo, mresult.len((mergestatemod.ACTION_GET,)), p1.node() | repo, mresult.len((mergestatemod.ACTION_GET,)), p1.node() | ||||
) | ) | ||||
wantfiledata = updatedirstate and not branchmerge | wantfiledata = updatedirstate and not branchmerge | ||||
stats, getfiledata = applyupdates( | stats, getfiledata = applyupdates( | ||||
repo, mresult, wc, p2, overwrite, wantfiledata, labels=labels, | repo, | ||||
mresult, | |||||
wc, | |||||
p2, | |||||
overwrite, | |||||
wantfiledata, | |||||
labels=labels, | |||||
) | ) | ||||
if updatedirstate: | if updatedirstate: | ||||
with repo.dirstate.parentchange(): | with repo.dirstate.parentchange(): | ||||
repo.setparents(fp1, fp2) | repo.setparents(fp1, fp2) | ||||
mergestatemod.recordupdates( | mergestatemod.recordupdates( | ||||
repo, mresult.actionsdict, branchmerge, getfiledata | repo, mresult.actionsdict, branchmerge, getfiledata | ||||
) | ) |
NO_OP_ACTIONS = ( | NO_OP_ACTIONS = ( | ||||
ACTION_KEEP, | ACTION_KEEP, | ||||
ACTION_KEEP_ABSENT, | ACTION_KEEP_ABSENT, | ||||
ACTION_KEEP_NEW, | ACTION_KEEP_NEW, | ||||
) | ) | ||||
class _mergestate_base(object): | class _mergestate_base(object): | ||||
'''track 3-way merge state of individual files | """track 3-way merge state of individual files | ||||
The merge state is stored on disk when needed. Two files are used: one with | The merge state is stored on disk when needed. Two files are used: one with | ||||
an old format (version 1), and one with a new format (version 2). Version 2 | an old format (version 1), and one with a new format (version 2). Version 2 | ||||
stores a superset of the data in version 1, including new kinds of records | stores a superset of the data in version 1, including new kinds of records | ||||
in the future. For more about the new format, see the documentation for | in the future. For more about the new format, see the documentation for | ||||
`_readrecordsv2`. | `_readrecordsv2`. | ||||
Each record can contain arbitrary content, and has an associated type. This | Each record can contain arbitrary content, and has an associated type. This | ||||
u: unresolved conflict | u: unresolved conflict | ||||
r: resolved conflict | r: resolved conflict | ||||
pu: unresolved path conflict (file conflicts with directory) | pu: unresolved path conflict (file conflicts with directory) | ||||
pr: resolved path conflict | pr: resolved path conflict | ||||
o: file was merged in favor of other parent of merge (DEPRECATED) | o: file was merged in favor of other parent of merge (DEPRECATED) | ||||
The resolve command transitions between 'u' and 'r' for conflicts and | The resolve command transitions between 'u' and 'r' for conflicts and | ||||
'pu' and 'pr' for path conflicts. | 'pu' and 'pr' for path conflicts. | ||||
''' | """ | ||||
def __init__(self, repo): | def __init__(self, repo): | ||||
"""Initialize the merge state. | """Initialize the merge state. | ||||
Do not use this directly! Instead call read() or clean().""" | Do not use this directly! Instead call read() or clean().""" | ||||
self._repo = repo | self._repo = repo | ||||
self._state = {} | self._state = {} | ||||
self._stateextras = collections.defaultdict(dict) | self._stateextras = collections.defaultdict(dict) | ||||
path: the path that conflicts | path: the path that conflicts | ||||
frename: the filename the conflicting file was renamed to | frename: the filename the conflicting file was renamed to | ||||
forigin: origin of the file ('l' or 'r' for local/remote) | forigin: origin of the file ('l' or 'r' for local/remote) | ||||
""" | """ | ||||
self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin] | self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin] | ||||
self._dirty = True | self._dirty = True | ||||
def addcommitinfo(self, path, data): | def addcommitinfo(self, path, data): | ||||
""" stores information which is required at commit | """stores information which is required at commit | ||||
into _stateextras """ | into _stateextras""" | ||||
self._stateextras[path].update(data) | self._stateextras[path].update(data) | ||||
self._dirty = True | self._dirty = True | ||||
def __contains__(self, dfile): | def __contains__(self, dfile): | ||||
return dfile in self._state | return dfile in self._state | ||||
def __getitem__(self, dfile): | def __getitem__(self, dfile): | ||||
return self._state[dfile][0] | return self._state[dfile][0] |
elif p1.rev() == p2.rev(): | elif p1.rev() == p2.rev(): | ||||
# In the wild, one can encounter such "non-merge" | # In the wild, one can encounter such "non-merge" | ||||
return _process_linear(p1, ctx) | return _process_linear(p1, ctx) | ||||
else: | else: | ||||
return _process_merge(p1, p2, ctx) | return _process_merge(p1, p2, ctx) | ||||
def _process_root(ctx): | def _process_root(ctx): | ||||
"""compute the appropriate changed files for a changeset with no parents | """compute the appropriate changed files for a changeset with no parents""" | ||||
""" | |||||
# Simple, there was nothing before it, so everything is added. | # Simple, there was nothing before it, so everything is added. | ||||
md = ChangingFiles() | md = ChangingFiles() | ||||
manifest = ctx.manifest() | manifest = ctx.manifest() | ||||
for filename in manifest: | for filename in manifest: | ||||
md.mark_added(filename) | md.mark_added(filename) | ||||
return md | return md | ||||
def _process_linear(parent_ctx, children_ctx, parent=1): | def _process_linear(parent_ctx, children_ctx, parent=1): | ||||
"""compute the appropriate changed files for a changeset with a single parent | """compute the appropriate changed files for a changeset with a single parent""" | ||||
""" | |||||
md = ChangingFiles() | md = ChangingFiles() | ||||
parent_manifest = parent_ctx.manifest() | parent_manifest = parent_ctx.manifest() | ||||
children_manifest = children_ctx.manifest() | children_manifest = children_ctx.manifest() | ||||
copies_candidate = [] | copies_candidate = [] | ||||
for filename, d in parent_manifest.diff(children_manifest).items(): | for filename, d in parent_manifest.diff(children_manifest).items(): | ||||
if d[1][0] is None: | if d[1][0] is None: | ||||
assert False, "unreachable" | assert False, "unreachable" | ||||
def _missing_from_all_ancestors(mas, filename): | def _missing_from_all_ancestors(mas, filename): | ||||
return all(_find(ma, filename) is None for ma in mas) | return all(_find(ma, filename) is None for ma in mas) | ||||
def computechangesetfilesadded(ctx): | def computechangesetfilesadded(ctx): | ||||
"""return the list of files added in a changeset | """return the list of files added in a changeset""" | ||||
""" | |||||
added = [] | added = [] | ||||
for f in ctx.files(): | for f in ctx.files(): | ||||
if not any(f in p for p in ctx.parents()): | if not any(f in p for p in ctx.parents()): | ||||
added.append(f) | added.append(f) | ||||
return added | return added | ||||
def get_removal_filter(ctx, x=None): | def get_removal_filter(ctx, x=None): | ||||
return all(f in ma and ma.find(f) == m2.find(f) for ma in mas()) | return all(f in ma and ma.find(f) == m2.find(f) for ma in mas()) | ||||
else: | else: | ||||
return True | return True | ||||
return deletionfromparent | return deletionfromparent | ||||
def computechangesetfilesremoved(ctx): | def computechangesetfilesremoved(ctx): | ||||
"""return the list of files removed in a changeset | """return the list of files removed in a changeset""" | ||||
""" | |||||
removed = [] | removed = [] | ||||
for f in ctx.files(): | for f in ctx.files(): | ||||
if f not in ctx: | if f not in ctx: | ||||
removed.append(f) | removed.append(f) | ||||
if removed: | if removed: | ||||
rf = get_removal_filter(ctx) | rf = get_removal_filter(ctx) | ||||
removed = [r for r in removed if not rf(r)] | removed = [r for r in removed if not rf(r)] | ||||
return removed | return removed | ||||
def computechangesetfilesmerged(ctx): | def computechangesetfilesmerged(ctx): | ||||
"""return the list of files merged in a changeset | """return the list of files merged in a changeset""" | ||||
""" | |||||
merged = [] | merged = [] | ||||
if len(ctx.parents()) < 2: | if len(ctx.parents()) < 2: | ||||
return merged | return merged | ||||
for f in ctx.files(): | for f in ctx.files(): | ||||
if f in ctx: | if f in ctx: | ||||
fctx = ctx[f] | fctx = ctx[f] | ||||
parents = fctx._filelog.parents(fctx._filenode) | parents = fctx._filelog.parents(fctx._filenode) | ||||
if parents[1] != node.nullid: | if parents[1] != node.nullid: |
return b"%s\n%s\n\n" % (s, b"." * encoding.colwidth(s)) | return b"%s\n%s\n\n" % (s, b"." * encoding.colwidth(s)) | ||||
def subsubsubsubsection(s): | def subsubsubsubsection(s): | ||||
return b"%s\n%s\n\n" % (s, b"'" * encoding.colwidth(s)) | return b"%s\n%s\n\n" % (s, b"'" * encoding.colwidth(s)) | ||||
def replace(text, substs): | def replace(text, substs): | ||||
''' | """ | ||||
Apply a list of (find, replace) pairs to a text. | Apply a list of (find, replace) pairs to a text. | ||||
>>> replace(b"foo bar", [(b'f', b'F'), (b'b', b'B')]) | >>> replace(b"foo bar", [(b'f', b'F'), (b'b', b'B')]) | ||||
'Foo Bar' | 'Foo Bar' | ||||
>>> encoding.encoding = b'latin1' | >>> encoding.encoding = b'latin1' | ||||
>>> replace(b'\\x81\\\\', [(b'\\\\', b'/')]) | >>> replace(b'\\x81\\\\', [(b'\\\\', b'/')]) | ||||
'\\x81/' | '\\x81/' | ||||
>>> encoding.encoding = b'shiftjis' | >>> encoding.encoding = b'shiftjis' | ||||
>>> replace(b'\\x81\\\\', [(b'\\\\', b'/')]) | >>> replace(b'\\x81\\\\', [(b'\\\\', b'/')]) | ||||
'\\x81\\\\' | '\\x81\\\\' | ||||
''' | """ | ||||
# some character encodings (cp932 for Japanese, at least) use | # some character encodings (cp932 for Japanese, at least) use | ||||
# ASCII characters other than control/alphabet/digit as a part of | # ASCII characters other than control/alphabet/digit as a part of | ||||
# multi-bytes characters, so direct replacing with such characters | # multi-bytes characters, so direct replacing with such characters | ||||
# on strings in local encoding causes invalid byte sequences. | # on strings in local encoding causes invalid byte sequences. | ||||
utext = text.decode(pycompat.sysstr(encoding.encoding)) | utext = text.decode(pycompat.sysstr(encoding.encoding)) | ||||
for f, t in substs: | for f, t in substs: | ||||
utext = utext.replace(f.decode("ascii"), t.decode("ascii")) | utext = utext.replace(f.decode("ascii"), t.decode("ascii")) | ||||
i += 1 | i += 1 | ||||
return blocks, pruned | return blocks, pruned | ||||
_sectionre = re.compile(br"""^([-=`:.'"~^_*+#])\1+$""") | _sectionre = re.compile(br"""^([-=`:.'"~^_*+#])\1+$""") | ||||
def findtables(blocks): | def findtables(blocks): | ||||
'''Find simple tables | """Find simple tables | ||||
Only simple one-line table elements are supported | Only simple one-line table elements are supported | ||||
''' | """ | ||||
for block in blocks: | for block in blocks: | ||||
# Searching for a block that looks like this: | # Searching for a block that looks like this: | ||||
# | # | ||||
# === ==== === | # === ==== === | ||||
# A B C | # A B C | ||||
# === ==== === <- optional | # === ==== === <- optional | ||||
# 1 2 3 | # 1 2 3 | ||||
This groups bullets, options, and definitions together with no vertical | This groups bullets, options, and definitions together with no vertical | ||||
space between them, and adds an empty block between all other blocks. | space between them, and adds an empty block between all other blocks. | ||||
""" | """ | ||||
i = 1 | i = 1 | ||||
while i < len(blocks): | while i < len(blocks): | ||||
if blocks[i][b'type'] == blocks[i - 1][b'type'] and blocks[i][ | if blocks[i][b'type'] == blocks[i - 1][b'type'] and blocks[i][ | ||||
b'type' | b'type' | ||||
] in (b'bullet', b'option', b'field',): | ] in ( | ||||
b'bullet', | |||||
b'option', | |||||
b'field', | |||||
): | |||||
i += 1 | i += 1 | ||||
elif not blocks[i - 1][b'lines']: | elif not blocks[i - 1][b'lines']: | ||||
# no lines in previous block, do not separate | # no lines in previous block, do not separate | ||||
i += 1 | i += 1 | ||||
else: | else: | ||||
blocks.insert( | blocks.insert( | ||||
i, {b'lines': [b''], b'indent': 0, b'type': b'margin'} | i, {b'lines': [b''], b'indent': 0, b'type': b'margin'} | ||||
) | ) |
def clearwcbackup(repo, backupname): | def clearwcbackup(repo, backupname): | ||||
if requirements.NARROW_REQUIREMENT not in repo.requirements: | if requirements.NARROW_REQUIREMENT not in repo.requirements: | ||||
return | return | ||||
repo.vfs.tryunlink(backupname) | repo.vfs.tryunlink(backupname) | ||||
def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes): | def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes): | ||||
r""" Restricts the patterns according to repo settings, | r"""Restricts the patterns according to repo settings, | ||||
results in a logical AND operation | results in a logical AND operation | ||||
:param req_includes: requested includes | :param req_includes: requested includes | ||||
:param req_excludes: requested excludes | :param req_excludes: requested excludes | ||||
:param repo_includes: repo includes | :param repo_includes: repo includes | ||||
:param repo_excludes: repo excludes | :param repo_excludes: repo excludes | ||||
:return: include patterns, exclude patterns, and invalid include patterns. | :return: include patterns, exclude patterns, and invalid include patterns. | ||||
""" | """ |
# we have a public predecessor | # we have a public predecessor | ||||
bumped.add(rev) | bumped.add(rev) | ||||
break # Next draft! | break # Next draft! | ||||
return bumped | return bumped | ||||
@cachefor(b'contentdivergent') | @cachefor(b'contentdivergent') | ||||
def _computecontentdivergentset(repo): | def _computecontentdivergentset(repo): | ||||
"""the set of rev that compete to be the final successors of some revision. | """the set of rev that compete to be the final successors of some revision.""" | ||||
""" | |||||
divergent = set() | divergent = set() | ||||
obsstore = repo.obsstore | obsstore = repo.obsstore | ||||
newermap = {} | newermap = {} | ||||
tonode = repo.changelog.node | tonode = repo.changelog.node | ||||
for rev in repo.revs(b'(not public()) - obsolete()'): | for rev in repo.revs(b'(not public()) - obsolete()'): | ||||
node = tonode(rev) | node = tonode(rev) | ||||
mark = obsstore.predecessors.get(node, ()) | mark = obsstore.predecessors.get(node, ()) | ||||
toprocess = set(mark) | toprocess = set(mark) |
re.compile(b'^branch$'), | re.compile(b'^branch$'), | ||||
re.compile(b'^.*-source$'), | re.compile(b'^.*-source$'), | ||||
re.compile(b'^.*_source$'), | re.compile(b'^.*_source$'), | ||||
re.compile(b'^source$'), | re.compile(b'^source$'), | ||||
] | ] | ||||
def metanotblacklisted(metaitem): | def metanotblacklisted(metaitem): | ||||
""" Check that the key of a meta item (extrakey, extravalue) does not | """Check that the key of a meta item (extrakey, extravalue) does not | ||||
match at least one of the blacklist pattern | match at least one of the blacklist pattern | ||||
""" | """ | ||||
metakey = metaitem[0] | metakey = metaitem[0] | ||||
return not any(pattern.match(metakey) for pattern in METABLACKLIST) | return not any(pattern.match(metakey) for pattern in METABLACKLIST) | ||||
def _prepare_hunk(hunk): | def _prepare_hunk(hunk): | ||||
right = _getdifflines(rightdiff) | right = _getdifflines(rightdiff) | ||||
if left != right: | if left != right: | ||||
return False | return False | ||||
return True | return True | ||||
def geteffectflag(source, successors): | def geteffectflag(source, successors): | ||||
""" From an obs-marker relation, compute what changed between the | """From an obs-marker relation, compute what changed between the | ||||
predecessor and the successor. | predecessor and the successor. | ||||
""" | """ | ||||
effects = 0 | effects = 0 | ||||
for changectx in successors: | for changectx in successors: | ||||
# Check if description has changed | # Check if description has changed | ||||
if changectx.description() != source.description(): | if changectx.description() != source.description(): | ||||
effects |= DESCCHANGED | effects |= DESCCHANGED | ||||
values = [] | values = [] | ||||
for sset in fullsuccessorsets: | for sset in fullsuccessorsets: | ||||
values.append({b'successors': sset, b'markers': sset.markers}) | values.append({b'successors': sset, b'markers': sset.markers}) | ||||
return values | return values | ||||
def _getobsfate(successorssets): | def _getobsfate(successorssets): | ||||
""" Compute a changeset obsolescence fate based on its successorssets. | """Compute a changeset obsolescence fate based on its successorssets. | ||||
Successors can be the tipmost ones or the immediate ones. This function | Successors can be the tipmost ones or the immediate ones. This function | ||||
return values are not meant to be shown directly to users, it is meant to | return values are not meant to be shown directly to users, it is meant to | ||||
be used by internal functions only. | be used by internal functions only. | ||||
Returns one fate from the following values: | Returns one fate from the following values: | ||||
- pruned | - pruned | ||||
- diverged | - diverged | ||||
- superseded | - superseded | ||||
- superseded_split | - superseded_split | ||||
if len(successors) == 1: | if len(successors) == 1: | ||||
return b'superseded' | return b'superseded' | ||||
else: | else: | ||||
return b'superseded_split' | return b'superseded_split' | ||||
def obsfateverb(successorset, markers): | def obsfateverb(successorset, markers): | ||||
""" Return the verb summarizing the successorset and potentially using | """Return the verb summarizing the successorset and potentially using | ||||
information from the markers | information from the markers | ||||
""" | """ | ||||
if not successorset: | if not successorset: | ||||
verb = b'pruned' | verb = b'pruned' | ||||
elif len(successorset) == 1: | elif len(successorset) == 1: | ||||
verb = b'rewritten' | verb = b'rewritten' | ||||
else: | else: | ||||
verb = b'split' | verb = b'split' | ||||
return verb | return verb | ||||
def markersdates(markers): | def markersdates(markers): | ||||
"""returns the list of dates for a list of markers | """returns the list of dates for a list of markers""" | ||||
""" | |||||
return [m[4] for m in markers] | return [m[4] for m in markers] | ||||
def markersusers(markers): | def markersusers(markers): | ||||
""" Returns a sorted list of markers users without duplicates | """Returns a sorted list of markers users without duplicates""" | ||||
""" | |||||
markersmeta = [dict(m[3]) for m in markers] | markersmeta = [dict(m[3]) for m in markers] | ||||
users = { | users = { | ||||
encoding.tolocal(meta[b'user']) | encoding.tolocal(meta[b'user']) | ||||
for meta in markersmeta | for meta in markersmeta | ||||
if meta.get(b'user') | if meta.get(b'user') | ||||
} | } | ||||
return sorted(users) | return sorted(users) | ||||
def markersoperations(markers): | def markersoperations(markers): | ||||
""" Returns a sorted list of markers operations without duplicates | """Returns a sorted list of markers operations without duplicates""" | ||||
""" | |||||
markersmeta = [dict(m[3]) for m in markers] | markersmeta = [dict(m[3]) for m in markers] | ||||
operations = { | operations = { | ||||
meta.get(b'operation') for meta in markersmeta if meta.get(b'operation') | meta.get(b'operation') for meta in markersmeta if meta.get(b'operation') | ||||
} | } | ||||
return sorted(operations) | return sorted(operations) | ||||
def obsfateprinter(ui, repo, successors, markers, formatctx): | def obsfateprinter(ui, repo, successors, markers, formatctx): | ||||
""" Build a obsfate string for a single successorset using all obsfate | """Build a obsfate string for a single successorset using all obsfate | ||||
related function defined in obsutil | related function defined in obsutil | ||||
""" | """ | ||||
quiet = ui.quiet | quiet = ui.quiet | ||||
verbose = ui.verbose | verbose = ui.verbose | ||||
normal = not verbose and not quiet | normal = not verbose and not quiet | ||||
line = [] | line = [] | ||||
b"superseded_split": _(b"hidden revision '%s' was split as: %s"), | b"superseded_split": _(b"hidden revision '%s' was split as: %s"), | ||||
b"superseded_split_several": _( | b"superseded_split_several": _( | ||||
b"hidden revision '%s' was split as: %s and %d more" | b"hidden revision '%s' was split as: %s and %d more" | ||||
), | ), | ||||
} | } | ||||
def _getfilteredreason(repo, changeid, ctx): | def _getfilteredreason(repo, changeid, ctx): | ||||
"""return a human-friendly string on why a obsolete changeset is hidden | """return a human-friendly string on why a obsolete changeset is hidden""" | ||||
""" | |||||
successors = successorssets(repo, ctx.node()) | successors = successorssets(repo, ctx.node()) | ||||
fate = _getobsfate(successors) | fate = _getobsfate(successors) | ||||
# Be more precise in case the revision is superseded | # Be more precise in case the revision is superseded | ||||
if fate == b'pruned': | if fate == b'pruned': | ||||
return filteredmsgtable[b'pruned'] % changeid | return filteredmsgtable[b'pruned'] % changeid | ||||
elif fate == b'diverged': | elif fate == b'diverged': | ||||
return filteredmsgtable[b'diverged'] % changeid | return filteredmsgtable[b'diverged'] % changeid |
if placeholder is not None and not isinstance(placeholder, tuple): | if placeholder is not None and not isinstance(placeholder, tuple): | ||||
raise error.ProgrammingError(b'placeholder must be a node tuple') | raise error.ProgrammingError(b'placeholder must be a node tuple') | ||||
matches = [tree] | matches = [tree] | ||||
if _matchtree(pattern, tree, placeholder, incompletenodes, matches): | if _matchtree(pattern, tree, placeholder, incompletenodes, matches): | ||||
return matches | return matches | ||||
def parseerrordetail(inst): | def parseerrordetail(inst): | ||||
"""Compose error message from specified ParseError object | """Compose error message from specified ParseError object""" | ||||
""" | |||||
if inst.location is not None: | if inst.location is not None: | ||||
return _(b'at %d: %s') % (inst.location, inst.message) | return _(b'at %d: %s') % (inst.location, inst.message) | ||||
else: | else: | ||||
return inst.message | return inst.message | ||||
class alias(object): | class alias(object): | ||||
"""Parsed result of alias""" | """Parsed result of alias""" |
(b'Date', b'date'), | (b'Date', b'date'), | ||||
(b'Branch', b'branch'), | (b'Branch', b'branch'), | ||||
(b'Node ID', b'nodeid'), | (b'Node ID', b'nodeid'), | ||||
] | ] | ||||
@contextlib.contextmanager | @contextlib.contextmanager | ||||
def extract(ui, fileobj): | def extract(ui, fileobj): | ||||
'''extract patch from data read from fileobj. | """extract patch from data read from fileobj. | ||||
patch can be a normal patch or contained in an email message. | patch can be a normal patch or contained in an email message. | ||||
return a dictionary. Standard keys are: | return a dictionary. Standard keys are: | ||||
- filename, | - filename, | ||||
- message, | - message, | ||||
- user, | - user, | ||||
- date, | - date, | ||||
- branch, | - branch, | ||||
- node, | - node, | ||||
- p1, | - p1, | ||||
- p2. | - p2. | ||||
Any item can be missing from the dictionary. If filename is missing, | Any item can be missing from the dictionary. If filename is missing, | ||||
fileobj did not contain a patch. Caller must unlink filename when done.''' | fileobj did not contain a patch. Caller must unlink filename when done.""" | ||||
fd, tmpname = pycompat.mkstemp(prefix=b'hg-patch-') | fd, tmpname = pycompat.mkstemp(prefix=b'hg-patch-') | ||||
tmpfp = os.fdopen(fd, 'wb') | tmpfp = os.fdopen(fd, 'wb') | ||||
try: | try: | ||||
yield _extract(ui, fileobj, tmpname, tmpfp) | yield _extract(ui, fileobj, tmpname, tmpfp) | ||||
finally: | finally: | ||||
tmpfp.close() | tmpfp.close() | ||||
os.unlink(tmpname) | os.unlink(tmpname) | ||||
def close(self): | def close(self): | ||||
if self.dirty: | if self.dirty: | ||||
self.writelines(self.fname, self.lines, self.mode) | self.writelines(self.fname, self.lines, self.mode) | ||||
self.write_rej() | self.write_rej() | ||||
return len(self.rej) | return len(self.rej) | ||||
class header(object): | class header(object): | ||||
"""patch header | """patch header""" | ||||
""" | |||||
diffgit_re = re.compile(b'diff --git a/(.*) b/(.*)$') | diffgit_re = re.compile(b'diff --git a/(.*) b/(.*)$') | ||||
diff_re = re.compile(b'diff -r .* (.*)$') | diff_re = re.compile(b'diff -r .* (.*)$') | ||||
allhunks_re = re.compile(b'(?:index|deleted file) ') | allhunks_re = re.compile(b'(?:index|deleted file) ') | ||||
pretty_re = re.compile(b'(?:new file|deleted file) ') | pretty_re = re.compile(b'(?:new file|deleted file) ') | ||||
special_re = re.compile(b'(?:index|deleted|copy|rename|new mode) ') | special_re = re.compile(b'(?:index|deleted|copy|rename|new mode) ') | ||||
newfile_re = re.compile(b'(?:new file|copy to|rename to)') | newfile_re = re.compile(b'(?:new file|copy to|rename to)') | ||||
b'unhandled transition: %s -> %s' % (state, newstate) | b'unhandled transition: %s -> %s' % (state, newstate) | ||||
) | ) | ||||
state = newstate | state = newstate | ||||
del fp | del fp | ||||
return p.finished() | return p.finished() | ||||
def pathtransform(path, strip, prefix): | def pathtransform(path, strip, prefix): | ||||
'''turn a path from a patch into a path suitable for the repository | """turn a path from a patch into a path suitable for the repository | ||||
prefix, if not empty, is expected to be normalized with a / at the end. | prefix, if not empty, is expected to be normalized with a / at the end. | ||||
Returns (stripped components, path in repository). | Returns (stripped components, path in repository). | ||||
>>> pathtransform(b'a/b/c', 0, b'') | >>> pathtransform(b'a/b/c', 0, b'') | ||||
('', 'a/b/c') | ('', 'a/b/c') | ||||
>>> pathtransform(b' a/b/c ', 0, b'') | >>> pathtransform(b' a/b/c ', 0, b'') | ||||
('', ' a/b/c') | ('', ' a/b/c') | ||||
>>> pathtransform(b' a/b/c ', 2, b'') | >>> pathtransform(b' a/b/c ', 2, b'') | ||||
('a/b/', 'c') | ('a/b/', 'c') | ||||
>>> pathtransform(b'a/b/c', 0, b'd/e/') | >>> pathtransform(b'a/b/c', 0, b'd/e/') | ||||
('', 'd/e/a/b/c') | ('', 'd/e/a/b/c') | ||||
>>> pathtransform(b' a//b/c ', 2, b'd/e/') | >>> pathtransform(b' a//b/c ', 2, b'd/e/') | ||||
('a//b/', 'd/e/c') | ('a//b/', 'd/e/c') | ||||
>>> pathtransform(b'a/b/c', 3, b'') | >>> pathtransform(b'a/b/c', 3, b'') | ||||
Traceback (most recent call last): | Traceback (most recent call last): | ||||
PatchError: unable to strip away 1 of 3 dirs from a/b/c | PatchError: unable to strip away 1 of 3 dirs from a/b/c | ||||
''' | """ | ||||
pathlen = len(path) | pathlen = len(path) | ||||
i = 0 | i = 0 | ||||
if strip == 0: | if strip == 0: | ||||
return b'', prefix + path.rstrip() | return b'', prefix + path.rstrip() | ||||
count = strip | count = strip | ||||
while count > 0: | while count > 0: | ||||
i = path.find(b'/', i) | i = path.find(b'/', i) | ||||
if i == -1: | if i == -1: | ||||
changes=None, | changes=None, | ||||
opts=None, | opts=None, | ||||
losedatafn=None, | losedatafn=None, | ||||
pathfn=None, | pathfn=None, | ||||
copy=None, | copy=None, | ||||
copysourcematch=None, | copysourcematch=None, | ||||
hunksfilterfn=None, | hunksfilterfn=None, | ||||
): | ): | ||||
'''yields diff of changes to files between two nodes, or node and | """yields diff of changes to files between two nodes, or node and | ||||
working directory. | working directory. | ||||
if node1 is None, use first dirstate parent instead. | if node1 is None, use first dirstate parent instead. | ||||
if node2 is None, compare node1 with working directory. | if node2 is None, compare node1 with working directory. | ||||
losedatafn(**kwarg) is a callable run when opts.upgrade=True and | losedatafn(**kwarg) is a callable run when opts.upgrade=True and | ||||
every time some change cannot be represented with the current | every time some change cannot be represented with the current | ||||
patch format. Return False to upgrade to git patch format, True to | patch format. Return False to upgrade to git patch format, True to | ||||
copy, if not empty, should contain mappings {dst@y: src@x} of copy | copy, if not empty, should contain mappings {dst@y: src@x} of copy | ||||
information. | information. | ||||
if copysourcematch is not None, then copy sources will be filtered by this | if copysourcematch is not None, then copy sources will be filtered by this | ||||
matcher | matcher | ||||
hunksfilterfn, if not None, should be a function taking a filectx and | hunksfilterfn, if not None, should be a function taking a filectx and | ||||
hunks generator that may yield filtered hunks. | hunks generator that may yield filtered hunks. | ||||
''' | """ | ||||
if not node1 and not node2: | if not node1 and not node2: | ||||
node1 = repo.dirstate.p1() | node1 = repo.dirstate.p1() | ||||
ctx1 = repo[node1] | ctx1 = repo[node1] | ||||
ctx2 = repo[node2] | ctx2 = repo[node2] | ||||
for fctx1, fctx2, hdr, hunks in diffhunks( | for fctx1, fctx2, hdr, hunks in diffhunks( | ||||
repo, | repo, | ||||
def diffui(*args, **kw): | def diffui(*args, **kw): | ||||
'''like diff(), but yields 2-tuples of (output, label) for ui.write()''' | '''like diff(), but yields 2-tuples of (output, label) for ui.write()''' | ||||
return difflabel(diff, *args, **kw) | return difflabel(diff, *args, **kw) | ||||
def _filepairs(modified, added, removed, copy, opts): | def _filepairs(modified, added, removed, copy, opts): | ||||
'''generates tuples (f1, f2, copyop), where f1 is the name of the file | """generates tuples (f1, f2, copyop), where f1 is the name of the file | ||||
before and f2 is the the name after. For added files, f1 will be None, | before and f2 is the the name after. For added files, f1 will be None, | ||||
and for removed files, f2 will be None. copyop may be set to None, 'copy' | and for removed files, f2 will be None. copyop may be set to None, 'copy' | ||||
or 'rename' (the latter two only if opts.git is set).''' | or 'rename' (the latter two only if opts.git is set).""" | ||||
gone = set() | gone = set() | ||||
copyto = {v: k for k, v in copy.items()} | copyto = {v: k for k, v in copy.items()} | ||||
addedset, removedset = set(added), set(removed) | addedset, removedset = set(added), set(removed) | ||||
for f in sorted(modified + added + removed): | for f in sorted(modified + added + removed): | ||||
copyop = None | copyop = None | ||||
added, | added, | ||||
removed, | removed, | ||||
copy, | copy, | ||||
getfilectx, | getfilectx, | ||||
opts, | opts, | ||||
losedatafn, | losedatafn, | ||||
pathfn, | pathfn, | ||||
): | ): | ||||
'''given input data, generate a diff and yield it in blocks | """given input data, generate a diff and yield it in blocks | ||||
If generating a diff would lose data like flags or binary data and | If generating a diff would lose data like flags or binary data and | ||||
losedatafn is not None, it will be called. | losedatafn is not None, it will be called. | ||||
pathfn is applied to every path in the diff output. | pathfn is applied to every path in the diff output. | ||||
''' | """ | ||||
if opts.noprefix: | if opts.noprefix: | ||||
aprefix = bprefix = b'' | aprefix = bprefix = b'' | ||||
else: | else: | ||||
aprefix = b'a/' | aprefix = b'a/' | ||||
bprefix = b'b/' | bprefix = b'b/' | ||||
def diffline(f, revs): | def diffline(f, revs): | ||||
content2 = fctx2.data() | content2 = fctx2.data() | ||||
data1 = (ctx1, fctx1, path1, flag1, content1, date1) | data1 = (ctx1, fctx1, path1, flag1, content1, date1) | ||||
data2 = (ctx2, fctx2, path2, flag2, content2, date2) | data2 = (ctx2, fctx2, path2, flag2, content2, date2) | ||||
yield diffcontent(data1, data2, header, binary, opts) | yield diffcontent(data1, data2, header, binary, opts) | ||||
def diffcontent(data1, data2, header, binary, opts): | def diffcontent(data1, data2, header, binary, opts): | ||||
""" diffs two versions of a file. | """diffs two versions of a file. | ||||
data1 and data2 are tuples containg: | data1 and data2 are tuples containg: | ||||
* ctx: changeset for the file | * ctx: changeset for the file | ||||
* fctx: file context for that file | * fctx: file context for that file | ||||
* path1: name of the file | * path1: name of the file | ||||
* flag: flags of the file | * flag: flags of the file | ||||
* content: full content of the file (can be null in case of binary) | * content: full content of the file (can be null in case of binary) | ||||
_(b' %d files changed, %d insertions(+), %d deletions(-)\n') | _(b' %d files changed, %d insertions(+), %d deletions(-)\n') | ||||
% (len(stats), totaladds, totalremoves) | % (len(stats), totaladds, totalremoves) | ||||
) | ) | ||||
return b''.join(output) | return b''.join(output) | ||||
def diffstatui(*args, **kw): | def diffstatui(*args, **kw): | ||||
'''like diffstat(), but yields 2-tuples of (output, label) for | """like diffstat(), but yields 2-tuples of (output, label) for | ||||
ui.write() | ui.write() | ||||
''' | """ | ||||
for line in diffstat(*args, **kw).splitlines(): | for line in diffstat(*args, **kw).splitlines(): | ||||
if line and line[-1] in b'+-': | if line and line[-1] in b'+-': | ||||
name, graph = line.rsplit(b' ', 1) | name, graph = line.rsplit(b' ', 1) | ||||
yield (name + b' ', b'') | yield (name + b' ', b'') | ||||
m = re.search(br'\++', graph) | m = re.search(br'\++', graph) | ||||
if m: | if m: | ||||
yield (m.group(0), b'diffstat.inserted') | yield (m.group(0), b'diffstat.inserted') | ||||
m = re.search(br'-+', graph) | m = re.search(br'-+', graph) | ||||
if m: | if m: | ||||
yield (m.group(0), b'diffstat.deleted') | yield (m.group(0), b'diffstat.deleted') | ||||
else: | else: | ||||
yield (line, b'') | yield (line, b'') | ||||
yield (b'\n', b'') | yield (b'\n', b'') |
parsers = policy.importmod('parsers') | parsers = policy.importmod('parsers') | ||||
def _lowerclean(s): | def _lowerclean(s): | ||||
return encoding.hfsignoreclean(s.lower()) | return encoding.hfsignoreclean(s.lower()) | ||||
class pathauditor(object): | class pathauditor(object): | ||||
'''ensure that a filesystem path contains no banned components. | """ensure that a filesystem path contains no banned components. | ||||
the following properties of a path are checked: | the following properties of a path are checked: | ||||
- ends with a directory separator | - ends with a directory separator | ||||
- under top-level .hg | - under top-level .hg | ||||
- starts at the root of a windows drive | - starts at the root of a windows drive | ||||
- contains ".." | - contains ".." | ||||
More check are also done about the file system states: | More check are also done about the file system states: | ||||
- traverses a symlink (e.g. a/symlink_here/b) | - traverses a symlink (e.g. a/symlink_here/b) | ||||
- inside a nested repository (a callback can be used to approve | - inside a nested repository (a callback can be used to approve | ||||
some nested repositories, e.g., subrepositories) | some nested repositories, e.g., subrepositories) | ||||
The file system checks are only done when 'realfs' is set to True (the | The file system checks are only done when 'realfs' is set to True (the | ||||
default). They should be disable then we are auditing path for operation on | default). They should be disable then we are auditing path for operation on | ||||
stored history. | stored history. | ||||
If 'cached' is set to True, audited paths and sub-directories are cached. | If 'cached' is set to True, audited paths and sub-directories are cached. | ||||
Be careful to not keep the cache of unmanaged directories for long because | Be careful to not keep the cache of unmanaged directories for long because | ||||
audited paths may be replaced with symlinks. | audited paths may be replaced with symlinks. | ||||
''' | """ | ||||
def __init__(self, root, callback=None, realfs=True, cached=False): | def __init__(self, root, callback=None, realfs=True, cached=False): | ||||
self.audited = set() | self.audited = set() | ||||
self.auditeddir = set() | self.auditeddir = set() | ||||
self.root = root | self.root = root | ||||
self._realfs = realfs | self._realfs = realfs | ||||
self._cached = cached | self._cached = cached | ||||
self.callback = callback | self.callback = callback | ||||
if os.path.lexists(root) and not util.fscasesensitive(root): | if os.path.lexists(root) and not util.fscasesensitive(root): | ||||
self.normcase = util.normcase | self.normcase = util.normcase | ||||
else: | else: | ||||
self.normcase = lambda x: x | self.normcase = lambda x: x | ||||
def __call__(self, path, mode=None): | def __call__(self, path, mode=None): | ||||
'''Check the relative path. | """Check the relative path. | ||||
path may contain a pattern (e.g. foodir/**.txt)''' | path may contain a pattern (e.g. foodir/**.txt)""" | ||||
path = util.localpath(path) | path = util.localpath(path) | ||||
normpath = self.normcase(path) | normpath = self.normcase(path) | ||||
if normpath in self.audited: | if normpath in self.audited: | ||||
return | return | ||||
# AIX ignores "/" at end of path, others raise EISDIR. | # AIX ignores "/" at end of path, others raise EISDIR. | ||||
if util.endswithsep(path): | if util.endswithsep(path): | ||||
raise error.Abort(_(b"path ends in directory separator: %s") % path) | raise error.Abort(_(b"path ends in directory separator: %s") % path) | ||||
yield | yield | ||||
finally: | finally: | ||||
self.audited.clear() | self.audited.clear() | ||||
self.auditeddir.clear() | self.auditeddir.clear() | ||||
self._cached = False | self._cached = False | ||||
def canonpath(root, cwd, myname, auditor=None): | def canonpath(root, cwd, myname, auditor=None): | ||||
'''return the canonical path of myname, given cwd and root | """return the canonical path of myname, given cwd and root | ||||
>>> def check(root, cwd, myname): | >>> def check(root, cwd, myname): | ||||
... a = pathauditor(root, realfs=False) | ... a = pathauditor(root, realfs=False) | ||||
... try: | ... try: | ||||
... return canonpath(root, cwd, myname, a) | ... return canonpath(root, cwd, myname, a) | ||||
... except error.Abort: | ... except error.Abort: | ||||
... return 'aborted' | ... return 'aborted' | ||||
>>> def unixonly(root, cwd, myname, expected='aborted'): | >>> def unixonly(root, cwd, myname, expected='aborted'): | ||||
>>> unixonly(b'/repo', b'/', b'filename') | >>> unixonly(b'/repo', b'/', b'filename') | ||||
'aborted' | 'aborted' | ||||
>>> unixonly(b'/repo', b'/', b'repo/filename', b'filename') | >>> unixonly(b'/repo', b'/', b'repo/filename', b'filename') | ||||
'filename' | 'filename' | ||||
>>> unixonly(b'/repo', b'/repo', b'filename', b'filename') | >>> unixonly(b'/repo', b'/repo', b'filename', b'filename') | ||||
'filename' | 'filename' | ||||
>>> unixonly(b'/repo', b'/repo/subdir', b'filename', b'subdir/filename') | >>> unixonly(b'/repo', b'/repo/subdir', b'filename', b'subdir/filename') | ||||
'subdir/filename' | 'subdir/filename' | ||||
''' | """ | ||||
if util.endswithsep(root): | if util.endswithsep(root): | ||||
rootsep = root | rootsep = root | ||||
else: | else: | ||||
rootsep = root + pycompat.ossep | rootsep = root + pycompat.ossep | ||||
name = myname | name = myname | ||||
if not os.path.isabs(name): | if not os.path.isabs(name): | ||||
name = os.path.join(root, cwd, name) | name = os.path.join(root, cwd, name) | ||||
name = os.path.normpath(name) | name = os.path.normpath(name) | ||||
pass | pass | ||||
raise error.Abort( | raise error.Abort( | ||||
_(b"%s not under root '%s'") % (myname, root), hint=hint | _(b"%s not under root '%s'") % (myname, root), hint=hint | ||||
) | ) | ||||
def normasprefix(path): | def normasprefix(path): | ||||
'''normalize the specified path as path prefix | """normalize the specified path as path prefix | ||||
Returned value can be used safely for "p.startswith(prefix)", | Returned value can be used safely for "p.startswith(prefix)", | ||||
"p[len(prefix):]", and so on. | "p[len(prefix):]", and so on. | ||||
For efficiency, this expects "path" argument to be already | For efficiency, this expects "path" argument to be already | ||||
normalized by "os.path.normpath", "os.path.realpath", and so on. | normalized by "os.path.normpath", "os.path.realpath", and so on. | ||||
See also issue3033 for detail about need of this function. | See also issue3033 for detail about need of this function. | ||||
>>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/') | >>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/') | ||||
'/foo/bar/' | '/foo/bar/' | ||||
>>> normasprefix(b'/').replace(pycompat.ossep, b'/') | >>> normasprefix(b'/').replace(pycompat.ossep, b'/') | ||||
'/' | '/' | ||||
''' | """ | ||||
d, p = os.path.splitdrive(path) | d, p = os.path.splitdrive(path) | ||||
if len(p) != len(pycompat.ossep): | if len(p) != len(pycompat.ossep): | ||||
return path + pycompat.ossep | return path + pycompat.ossep | ||||
else: | else: | ||||
return path | return path | ||||
def finddirs(path): | def finddirs(path): | ||||
pos = path.rfind(b'/') | pos = path.rfind(b'/') | ||||
while pos != -1: | while pos != -1: | ||||
yield path[:pos] | yield path[:pos] | ||||
pos = path.rfind(b'/', 0, pos) | pos = path.rfind(b'/', 0, pos) | ||||
yield b'' | yield b'' | ||||
class dirs(object): | class dirs(object): | ||||
'''a multiset of directory names from a set of file paths''' | '''a multiset of directory names from a set of file paths''' | ||||
def __init__(self, map, skip=None): | def __init__(self, map, skip=None): | ||||
''' | """ | ||||
a dict map indicates a dirstate while a list indicates a manifest | a dict map indicates a dirstate while a list indicates a manifest | ||||
''' | """ | ||||
self._dirs = {} | self._dirs = {} | ||||
addpath = self.addpath | addpath = self.addpath | ||||
if isinstance(map, dict) and skip is not None: | if isinstance(map, dict) and skip is not None: | ||||
for f, s in pycompat.iteritems(map): | for f, s in pycompat.iteritems(map): | ||||
if s[0] != skip: | if s[0] != skip: | ||||
addpath(f) | addpath(f) | ||||
elif skip is not None: | elif skip is not None: | ||||
raise error.ProgrammingError( | raise error.ProgrammingError( |
else: | else: | ||||
# The underlying file object seeks as required in Python 3: | # The underlying file object seeks as required in Python 3: | ||||
# https://github.com/python/cpython/blob/v3.7.3/Modules/_io/fileio.c#L474 | # https://github.com/python/cpython/blob/v3.7.3/Modules/_io/fileio.c#L474 | ||||
posixfile = open | posixfile = open | ||||
def split(p): | def split(p): | ||||
'''Same as posixpath.split, but faster | """Same as posixpath.split, but faster | ||||
>>> import posixpath | >>> import posixpath | ||||
>>> for f in [b'/absolute/path/to/file', | >>> for f in [b'/absolute/path/to/file', | ||||
... b'relative/path/to/file', | ... b'relative/path/to/file', | ||||
... b'file_alone', | ... b'file_alone', | ||||
... b'path/to/directory/', | ... b'path/to/directory/', | ||||
... b'/multiple/path//separators', | ... b'/multiple/path//separators', | ||||
... b'/file_at_root', | ... b'/file_at_root', | ||||
... b'///multiple_leading_separators_at_root', | ... b'///multiple_leading_separators_at_root', | ||||
... b'']: | ... b'']: | ||||
... assert split(f) == posixpath.split(f), f | ... assert split(f) == posixpath.split(f), f | ||||
''' | """ | ||||
ht = p.rsplit(b'/', 1) | ht = p.rsplit(b'/', 1) | ||||
if len(ht) == 1: | if len(ht) == 1: | ||||
return b'', p | return b'', p | ||||
nh = ht[0].rstrip(b'/') | nh = ht[0].rstrip(b'/') | ||||
if nh: | if nh: | ||||
return nh, ht[1] | return nh, ht[1] | ||||
return ht[0] + b'/', ht[1] | return ht[0] + b'/', ht[1] | ||||
# and obey umask. | # and obey umask. | ||||
os.chmod(f, s | (s & 0o444) >> 2 & ~umask) | os.chmod(f, s | (s & 0o444) >> 2 & ~umask) | ||||
elif not x and sx: | elif not x and sx: | ||||
# Turn off all +x bits | # Turn off all +x bits | ||||
os.chmod(f, s & 0o666) | os.chmod(f, s & 0o666) | ||||
def copymode(src, dst, mode=None, enforcewritable=False): | def copymode(src, dst, mode=None, enforcewritable=False): | ||||
'''Copy the file mode from the file at path src to dst. | """Copy the file mode from the file at path src to dst. | ||||
If src doesn't exist, we're using mode instead. If mode is None, we're | If src doesn't exist, we're using mode instead. If mode is None, we're | ||||
using umask.''' | using umask.""" | ||||
try: | try: | ||||
st_mode = os.lstat(src).st_mode & 0o777 | st_mode = os.lstat(src).st_mode & 0o777 | ||||
except OSError as inst: | except OSError as inst: | ||||
if inst.errno != errno.ENOENT: | if inst.errno != errno.ENOENT: | ||||
raise | raise | ||||
st_mode = mode | st_mode = mode | ||||
if st_mode is None: | if st_mode is None: | ||||
st_mode = ~umask | st_mode = ~umask | ||||
except OSError as inst: | except OSError as inst: | ||||
# sshfs might report failure while successfully creating the link | # sshfs might report failure while successfully creating the link | ||||
if inst.errno == errno.EIO and os.path.exists(name): | if inst.errno == errno.EIO and os.path.exists(name): | ||||
unlink(name) | unlink(name) | ||||
return False | return False | ||||
def checkosfilename(path): | def checkosfilename(path): | ||||
'''Check that the base-relative path is a valid filename on this platform. | """Check that the base-relative path is a valid filename on this platform. | ||||
Returns None if the path is ok, or a UI string describing the problem.''' | Returns None if the path is ok, or a UI string describing the problem.""" | ||||
return None # on posix platforms, every path is ok | return None # on posix platforms, every path is ok | ||||
def getfsmountpoint(dirpath): | def getfsmountpoint(dirpath): | ||||
'''Get the filesystem mount point from a directory (best-effort) | """Get the filesystem mount point from a directory (best-effort) | ||||
Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc. | Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc. | ||||
''' | """ | ||||
return getattr(osutil, 'getfsmountpoint', lambda x: None)(dirpath) | return getattr(osutil, 'getfsmountpoint', lambda x: None)(dirpath) | ||||
def getfstype(dirpath): | def getfstype(dirpath): | ||||
'''Get the filesystem type name from a directory (best-effort) | """Get the filesystem type name from a directory (best-effort) | ||||
Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc. | Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc. | ||||
''' | """ | ||||
return getattr(osutil, 'getfstype', lambda x: None)(dirpath) | return getattr(osutil, 'getfstype', lambda x: None)(dirpath) | ||||
def setbinary(fd): | def setbinary(fd): | ||||
pass | pass | ||||
def pconvert(path): | def pconvert(path): | ||||
# what normcase does to ASCII strings | # what normcase does to ASCII strings | ||||
normcasespec = encoding.normcasespecs.lower | normcasespec = encoding.normcasespecs.lower | ||||
# fallback normcase function for non-ASCII strings | # fallback normcase function for non-ASCII strings | ||||
normcasefallback = normcase | normcasefallback = normcase | ||||
if pycompat.isdarwin: | if pycompat.isdarwin: | ||||
def normcase(path): | def normcase(path): | ||||
''' | """ | ||||
Normalize a filename for OS X-compatible comparison: | Normalize a filename for OS X-compatible comparison: | ||||
- escape-encode invalid characters | - escape-encode invalid characters | ||||
- decompose to NFD | - decompose to NFD | ||||
- lowercase | - lowercase | ||||
- omit ignored characters [200c-200f, 202a-202e, 206a-206f,feff] | - omit ignored characters [200c-200f, 202a-202e, 206a-206f,feff] | ||||
>>> normcase(b'UPPER') | >>> normcase(b'UPPER') | ||||
'upper' | 'upper' | ||||
>>> normcase(b'Caf\\xc3\\xa9') | >>> normcase(b'Caf\\xc3\\xa9') | ||||
'cafe\\xcc\\x81' | 'cafe\\xcc\\x81' | ||||
>>> normcase(b'\\xc3\\x89') | >>> normcase(b'\\xc3\\x89') | ||||
'e\\xcc\\x81' | 'e\\xcc\\x81' | ||||
>>> normcase(b'\\xb8\\xca\\xc3\\xca\\xbe\\xc8.JPG') # issue3918 | >>> normcase(b'\\xb8\\xca\\xc3\\xca\\xbe\\xc8.JPG') # issue3918 | ||||
'%b8%ca%c3\\xca\\xbe%c8.jpg' | '%b8%ca%c3\\xca\\xbe%c8.jpg' | ||||
''' | """ | ||||
try: | try: | ||||
return encoding.asciilower(path) # exception for non-ASCII | return encoding.asciilower(path) # exception for non-ASCII | ||||
except UnicodeDecodeError: | except UnicodeDecodeError: | ||||
return normcasefallback(path) | return normcasefallback(path) | ||||
normcasespec = encoding.normcasespecs.lower | normcasespec = encoding.normcasespecs.lower | ||||
if pycompat.sysplatform == b'cygwin': | if pycompat.sysplatform == b'cygwin': | ||||
# workaround for cygwin, in which mount point part of path is | # workaround for cygwin, in which mount point part of path is | ||||
# treated as case sensitive, even though underlying NTFS is case | # treated as case sensitive, even though underlying NTFS is case | ||||
# insensitive. | # insensitive. | ||||
# default mount points | # default mount points | ||||
cygwinmountpoints = sorted( | cygwinmountpoints = sorted( | ||||
[b"/usr/bin", b"/usr/lib", b"/cygdrive",], reverse=True | [ | ||||
b"/usr/bin", | |||||
b"/usr/lib", | |||||
b"/cygdrive", | |||||
], | |||||
reverse=True, | |||||
) | ) | ||||
# use upper-ing as normcase as same as NTFS workaround | # use upper-ing as normcase as same as NTFS workaround | ||||
def normcase(path): | def normcase(path): | ||||
pathlen = len(path) | pathlen = len(path) | ||||
if (pathlen == 0) or (path[0] != pycompat.ossep): | if (pathlen == 0) or (path[0] != pycompat.ossep): | ||||
# treat as relative | # treat as relative | ||||
return encoding.upper(path) | return encoding.upper(path) | ||||
def isowner(st): | def isowner(st): | ||||
"""Return True if the stat object st is from the current user.""" | """Return True if the stat object st is from the current user.""" | ||||
return st.st_uid == os.getuid() | return st.st_uid == os.getuid() | ||||
def findexe(command): | def findexe(command): | ||||
'''Find executable for command searching like which does. | """Find executable for command searching like which does. | ||||
If command is a basename then PATH is searched for command. | If command is a basename then PATH is searched for command. | ||||
PATH isn't searched if command is an absolute or relative path. | PATH isn't searched if command is an absolute or relative path. | ||||
If command isn't found None is returned.''' | If command isn't found None is returned.""" | ||||
if pycompat.sysplatform == b'OpenVMS': | if pycompat.sysplatform == b'OpenVMS': | ||||
return command | return command | ||||
def findexisting(executable): | def findexisting(executable): | ||||
b'Will return executable if existing file' | b'Will return executable if existing file' | ||||
if os.path.isfile(executable) and os.access(executable, os.X_OK): | if os.path.isfile(executable) and os.access(executable, os.X_OK): | ||||
return executable | return executable | ||||
return None | return None | ||||
def setsignalhandler(): | def setsignalhandler(): | ||||
pass | pass | ||||
_wantedkinds = {stat.S_IFREG, stat.S_IFLNK} | _wantedkinds = {stat.S_IFREG, stat.S_IFLNK} | ||||
def statfiles(files): | def statfiles(files): | ||||
'''Stat each file in files. Yield each stat, or None if a file does not | """Stat each file in files. Yield each stat, or None if a file does not | ||||
exist or has a type we don't care about.''' | exist or has a type we don't care about.""" | ||||
lstat = os.lstat | lstat = os.lstat | ||||
getkind = stat.S_IFMT | getkind = stat.S_IFMT | ||||
for nf in files: | for nf in files: | ||||
try: | try: | ||||
st = lstat(nf) | st = lstat(nf) | ||||
if getkind(st.st_mode) not in _wantedkinds: | if getkind(st.st_mode) not in _wantedkinds: | ||||
st = None | st = None | ||||
except OSError as err: | except OSError as err: |
# it's been long enough we should print anyway | # it's been long enough we should print anyway | ||||
or now - self.lastprint >= self.changedelay | or now - self.lastprint >= self.changedelay | ||||
): | ): | ||||
return True | return True | ||||
else: | else: | ||||
return False | return False | ||||
def _calibrateestimate(self, topic, now, pos): | def _calibrateestimate(self, topic, now, pos): | ||||
'''Adjust starttimes and startvals for topic so ETA works better | """Adjust starttimes and startvals for topic so ETA works better | ||||
If progress is non-linear (ex. get much slower in the last minute), | If progress is non-linear (ex. get much slower in the last minute), | ||||
it's more friendly to only use a recent time span for ETA and speed | it's more friendly to only use a recent time span for ETA and speed | ||||
calculation. | calculation. | ||||
[======================================> ] | [======================================> ] | ||||
^^^^^^^ | ^^^^^^^ | ||||
estimateinterval, only use this for estimation | estimateinterval, only use this for estimation | ||||
''' | """ | ||||
interval = self.estimateinterval | interval = self.estimateinterval | ||||
if interval <= 0: | if interval <= 0: | ||||
return | return | ||||
elapsed = now - self.starttimes[topic] | elapsed = now - self.starttimes[topic] | ||||
if elapsed > interval: | if elapsed > interval: | ||||
delta = pos - self.startvals[topic] | delta = pos - self.startvals[topic] | ||||
newdelta = delta * interval / elapsed | newdelta = delta * interval / elapsed | ||||
# If a stall happens temporarily, ETA could change dramatically | # If a stall happens temporarily, ETA could change dramatically |
try: | try: | ||||
s.decode('ascii') | s.decode('ascii') | ||||
return True | return True | ||||
except UnicodeDecodeError: | except UnicodeDecodeError: | ||||
return False | return False | ||||
def asciilower(s): | def asciilower(s): | ||||
'''convert a string to lowercase if ASCII | """convert a string to lowercase if ASCII | ||||
Raises UnicodeDecodeError if non-ASCII characters are found.''' | Raises UnicodeDecodeError if non-ASCII characters are found.""" | ||||
s.decode('ascii') | s.decode('ascii') | ||||
return s.lower() | return s.lower() | ||||
def asciiupper(s): | def asciiupper(s): | ||||
'''convert a string to uppercase if ASCII | """convert a string to uppercase if ASCII | ||||
Raises UnicodeDecodeError if non-ASCII characters are found.''' | Raises UnicodeDecodeError if non-ASCII characters are found.""" | ||||
s.decode('ascii') | s.decode('ascii') | ||||
return s.upper() | return s.upper() | ||||
_jsonmap = [] | _jsonmap = [] | ||||
_jsonmap.extend(b"\\u%04x" % x for x in range(32)) | _jsonmap.extend(b"\\u%04x" % x for x in range(32)) | ||||
_jsonmap.extend(pycompat.bytechr(x) for x in range(32, 127)) | _jsonmap.extend(pycompat.bytechr(x) for x in range(32, 127)) | ||||
_jsonmap.append(b'\\u007f') | _jsonmap.append(b'\\u007f') |
import struct | import struct | ||||
from .. import pycompat | from .. import pycompat | ||||
stringio = pycompat.bytesio | stringio = pycompat.bytesio | ||||
class mpatchError(Exception): | class mpatchError(Exception): | ||||
"""error raised when a delta cannot be decoded | """error raised when a delta cannot be decoded""" | ||||
""" | |||||
# This attempts to apply a series of patches in time proportional to | # This attempts to apply a series of patches in time proportional to | ||||
# the total size of the patches, rather than patches * len(text). This | # the total size of the patches, rather than patches * len(text). This | ||||
# means rather than shuffling strings around, we shuffle around | # means rather than shuffling strings around, we shuffle around | ||||
# pointers to fragments with fragment lists. | # pointers to fragments with fragment lists. | ||||
# | # | ||||
# When the fragment lists get too long, we collapse them. To do this | # When the fragment lists get too long, we collapse them. To do this |
if statmod.S_ISFIFO(mode): | if statmod.S_ISFIFO(mode): | ||||
return statmod.S_IFIFO | return statmod.S_IFIFO | ||||
if statmod.S_ISSOCK(mode): | if statmod.S_ISSOCK(mode): | ||||
return statmod.S_IFSOCK | return statmod.S_IFSOCK | ||||
return mode | return mode | ||||
def listdir(path, stat=False, skip=None): | def listdir(path, stat=False, skip=None): | ||||
'''listdir(path, stat=False) -> list_of_tuples | """listdir(path, stat=False) -> list_of_tuples | ||||
Return a sorted list containing information about the entries | Return a sorted list containing information about the entries | ||||
in the directory. | in the directory. | ||||
If stat is True, each element is a 3-tuple: | If stat is True, each element is a 3-tuple: | ||||
(name, type, stat object) | (name, type, stat object) | ||||
Otherwise, each element is a 2-tuple: | Otherwise, each element is a 2-tuple: | ||||
(name, type) | (name, type) | ||||
''' | """ | ||||
result = [] | result = [] | ||||
prefix = path | prefix = path | ||||
if not prefix.endswith(pycompat.ossep): | if not prefix.endswith(pycompat.ossep): | ||||
prefix += pycompat.ossep | prefix += pycompat.ossep | ||||
names = os.listdir(path) | names = os.listdir(path) | ||||
names.sort() | names.sort() | ||||
for fn in names: | for fn in names: | ||||
st = os.lstat(prefix + fn) | st = os.lstat(prefix + fn) | ||||
def _raiseioerror(name): | def _raiseioerror(name): | ||||
err = ctypes.WinError() | err = ctypes.WinError() | ||||
raise IOError( | raise IOError( | ||||
err.errno, '%s: %s' % (encoding.strfromlocal(name), err.strerror) | err.errno, '%s: %s' % (encoding.strfromlocal(name), err.strerror) | ||||
) | ) | ||||
class posixfile(object): | class posixfile(object): | ||||
'''a file object aiming for POSIX-like semantics | """a file object aiming for POSIX-like semantics | ||||
CPython's open() returns a file that was opened *without* setting the | CPython's open() returns a file that was opened *without* setting the | ||||
_FILE_SHARE_DELETE flag, which causes rename and unlink to abort. | _FILE_SHARE_DELETE flag, which causes rename and unlink to abort. | ||||
This even happens if any hardlinked copy of the file is in open state. | This even happens if any hardlinked copy of the file is in open state. | ||||
We set _FILE_SHARE_DELETE here, so files opened with posixfile can be | We set _FILE_SHARE_DELETE here, so files opened with posixfile can be | ||||
renamed and deleted while they are held open. | renamed and deleted while they are held open. | ||||
Note that if a file opened with posixfile is unlinked, the file | Note that if a file opened with posixfile is unlinked, the file | ||||
remains but cannot be opened again or be recreated under the same name, | remains but cannot be opened again or be recreated under the same name, | ||||
until all reading processes have closed the file.''' | until all reading processes have closed the file.""" | ||||
def __init__(self, name, mode=b'r', bufsize=-1): | def __init__(self, name, mode=b'r', bufsize=-1): | ||||
if b'b' in mode: | if b'b' in mode: | ||||
flags = _O_BINARY | flags = _O_BINARY | ||||
else: | else: | ||||
flags = _O_TEXT | flags = _O_TEXT | ||||
m0 = mode[0:1] | m0 = mode[0:1] | ||||
def __iter__(self): | def __iter__(self): | ||||
return self._file | return self._file | ||||
def __getattr__(self, name): | def __getattr__(self, name): | ||||
return getattr(self._file, name) | return getattr(self._file, name) | ||||
def __setattr__(self, name, value): | def __setattr__(self, name, value): | ||||
'''mimics the read-only attributes of Python file objects | """mimics the read-only attributes of Python file objects | ||||
by raising 'TypeError: readonly attribute' if someone tries: | by raising 'TypeError: readonly attribute' if someone tries: | ||||
f = posixfile('foo.txt') | f = posixfile('foo.txt') | ||||
f.name = 'bla' | f.name = 'bla' | ||||
''' | """ | ||||
return self._file.__setattr__(name, value) | return self._file.__setattr__(name, value) | ||||
def __enter__(self): | def __enter__(self): | ||||
self._file.__enter__() | self._file.__enter__() | ||||
return self | return self | ||||
def __exit__(self, exc_type, exc_value, exc_tb): | def __exit__(self, exc_type, exc_value, exc_tb): | ||||
return self._file.__exit__(exc_type, exc_value, exc_tb) | return self._file.__exit__(exc_type, exc_value, exc_tb) |
def parse_index2(data, inline): | def parse_index2(data, inline): | ||||
if not inline: | if not inline: | ||||
return IndexObject(data), None | return IndexObject(data), None | ||||
return InlinedIndexObject(data, inline), (0, data) | return InlinedIndexObject(data, inline), (0, data) | ||||
def parse_index_devel_nodemap(data, inline): | def parse_index_devel_nodemap(data, inline): | ||||
"""like parse_index2, but alway return a PersistentNodeMapIndexObject | """like parse_index2, but alway return a PersistentNodeMapIndexObject""" | ||||
""" | |||||
return PersistentNodeMapIndexObject(data), None | return PersistentNodeMapIndexObject(data), None | ||||
def parse_dirstate(dmap, copymap, st): | def parse_dirstate(dmap, copymap, st): | ||||
parents = [st[:20], st[20:40]] | parents = [st[:20], st[20:40]] | ||||
# dereference fields so they will be local in loop | # dereference fields so they will be local in loop | ||||
format = b">cllll" | format = b">cllll" | ||||
e_size = struct.calcsize(format) | e_size = struct.calcsize(format) |
join = os.path.join | join = os.path.join | ||||
return sorted( | return sorted( | ||||
join(p, f) for f, k in util.listdir(p) if f.endswith(b'.rc') | join(p, f) for f, k in util.listdir(p) if f.endswith(b'.rc') | ||||
) | ) | ||||
return [p] | return [p] | ||||
def envrcitems(env=None): | def envrcitems(env=None): | ||||
'''Return [(section, name, value, source)] config items. | """Return [(section, name, value, source)] config items. | ||||
The config items are extracted from environment variables specified by env, | The config items are extracted from environment variables specified by env, | ||||
used to override systemrc, but not userrc. | used to override systemrc, but not userrc. | ||||
If env is not provided, encoding.environ will be used. | If env is not provided, encoding.environ will be used. | ||||
''' | """ | ||||
if env is None: | if env is None: | ||||
env = encoding.environ | env = encoding.environ | ||||
checklist = [ | checklist = [ | ||||
(b'EDITOR', b'ui', b'editor'), | (b'EDITOR', b'ui', b'editor'), | ||||
(b'VISUAL', b'ui', b'editor'), | (b'VISUAL', b'ui', b'editor'), | ||||
(b'PAGER', b'pager', b'pager'), | (b'PAGER', b'pager', b'pager'), | ||||
] | ] | ||||
result = [] | result = [] | ||||
(b'mercurial.defaultrc', r) | (b'mercurial.defaultrc', r) | ||||
for r in sorted(rsrcs) | for r in sorted(rsrcs) | ||||
if resourceutil.is_resource(b'mercurial.defaultrc', r) | if resourceutil.is_resource(b'mercurial.defaultrc', r) | ||||
and r.endswith(b'.rc') | and r.endswith(b'.rc') | ||||
] | ] | ||||
def rccomponents(): | def rccomponents(): | ||||
'''return an ordered [(type, obj)] about where to load configs. | """return an ordered [(type, obj)] about where to load configs. | ||||
respect $HGRCPATH. if $HGRCPATH is empty, only .hg/hgrc of current repo is | respect $HGRCPATH. if $HGRCPATH is empty, only .hg/hgrc of current repo is | ||||
used. if $HGRCPATH is not set, the platform default will be used. | used. if $HGRCPATH is not set, the platform default will be used. | ||||
if a directory is provided, *.rc files under it will be used. | if a directory is provided, *.rc files under it will be used. | ||||
type could be either 'path', 'items' or 'resource'. If type is 'path', | type could be either 'path', 'items' or 'resource'. If type is 'path', | ||||
obj is a string, and is the config file path. if type is 'items', obj is a | obj is a string, and is the config file path. if type is 'items', obj is a | ||||
list of (section, name, value, source) that should fill the config directly. | list of (section, name, value, source) that should fill the config directly. | ||||
If type is 'resource', obj is a tuple of (package name, resource name). | If type is 'resource', obj is a tuple of (package name, resource name). | ||||
''' | """ | ||||
envrc = (b'items', envrcitems()) | envrc = (b'items', envrcitems()) | ||||
if b'HGRCPATH' in encoding.environ: | if b'HGRCPATH' in encoding.environ: | ||||
# assume HGRCPATH is all about user configs so environments can be | # assume HGRCPATH is all about user configs so environments can be | ||||
# overridden. | # overridden. | ||||
_rccomponents = [envrc] | _rccomponents = [envrc] | ||||
for p in encoding.environ[b'HGRCPATH'].split(pycompat.ospathsep): | for p in encoding.environ[b'HGRCPATH'].split(pycompat.ospathsep): | ||||
if not p: | if not p: | ||||
continue | continue | ||||
_rccomponents.extend((b'path', p) for p in _expandrcpath(p)) | _rccomponents.extend((b'path', p) for p in _expandrcpath(p)) | ||||
else: | else: | ||||
_rccomponents = [(b'resource', r) for r in default_rc_resources()] | _rccomponents = [(b'resource', r) for r in default_rc_resources()] | ||||
normpaths = lambda paths: [ | normpaths = lambda paths: [ | ||||
(b'path', os.path.normpath(p)) for p in paths | (b'path', os.path.normpath(p)) for p in paths | ||||
] | ] | ||||
_rccomponents.extend(normpaths(systemrcpath())) | _rccomponents.extend(normpaths(systemrcpath())) | ||||
_rccomponents.append(envrc) | _rccomponents.append(envrc) | ||||
_rccomponents.extend(normpaths(userrcpath())) | _rccomponents.extend(normpaths(userrcpath())) | ||||
return _rccomponents | return _rccomponents | ||||
def defaultpagerenv(): | def defaultpagerenv(): | ||||
'''return a dict of default environment variables and their values, | """return a dict of default environment variables and their values, | ||||
intended to be set before starting a pager. | intended to be set before starting a pager. | ||||
''' | """ | ||||
return {b'LESS': b'FRX', b'LV': b'-c'} | return {b'LESS': b'FRX', b'LV': b'-c'} | ||||
def use_repo_hgrc(): | def use_repo_hgrc(): | ||||
"""True if repositories `.hg/hgrc` config should be read""" | """True if repositories `.hg/hgrc` config should be read""" | ||||
return b'HGRCSKIPREPO' not in encoding.environ | return b'HGRCSKIPREPO' not in encoding.environ |
if dups: | if dups: | ||||
msg = b'duplicate registration for names: "%s"' % b'", "'.join(dups) | msg = b'duplicate registration for names: "%s"' % b'", "'.join(dups) | ||||
raise error.ProgrammingError(msg) | raise error.ProgrammingError(msg) | ||||
self._table.update(registrarbase._table) | self._table.update(registrarbase._table) | ||||
def _parsefuncdecl(self, decl): | def _parsefuncdecl(self, decl): | ||||
"""Parse function declaration and return the name of function in it | """Parse function declaration and return the name of function in it""" | ||||
""" | |||||
i = decl.find(b'(') | i = decl.find(b'(') | ||||
if i >= 0: | if i >= 0: | ||||
return decl[:i] | return decl[:i] | ||||
else: | else: | ||||
return decl | return decl | ||||
def _getname(self, decl): | def _getname(self, decl): | ||||
"""Return the name of the registered function from decl | """Return the name of the registered function from decl | ||||
Derived class should override this, if it allows more | Derived class should override this, if it allows more | ||||
descriptive 'decl' string than just a name. | descriptive 'decl' string than just a name. | ||||
""" | """ | ||||
return decl | return decl | ||||
_docformat = None | _docformat = None | ||||
def _formatdoc(self, decl, doc): | def _formatdoc(self, decl, doc): | ||||
"""Return formatted document of the registered function for help | """Return formatted document of the registered function for help | ||||
'doc' is '__doc__.strip()' of the registered function. | 'doc' is '__doc__.strip()' of the registered function. | ||||
""" | """ | ||||
return self._docformat % (decl, doc) | return self._docformat % (decl, doc) | ||||
def _extrasetup(self, name, func): | def _extrasetup(self, name, func): | ||||
"""Execute extra setup for registered function, if needed | """Execute extra setup for registered function, if needed""" | ||||
""" | |||||
class command(_funcregistrarbase): | class command(_funcregistrarbase): | ||||
"""Decorator to register a command function to table | """Decorator to register a command function to table | ||||
This class receives a command table as its argument. The table should | This class receives a command table as its argument. The table should | ||||
be a dict. | be a dict. | ||||
_docformat = b"``%s``\n %s" | _docformat = b"``%s``\n %s" | ||||
def _extrasetup(self, name, func, callstatus=False, weight=1): | def _extrasetup(self, name, func, callstatus=False, weight=1): | ||||
func._callstatus = callstatus | func._callstatus = callstatus | ||||
func._weight = weight | func._weight = weight | ||||
class _templateregistrarbase(_funcregistrarbase): | class _templateregistrarbase(_funcregistrarbase): | ||||
"""Base of decorator to register functions as template specific one | """Base of decorator to register functions as template specific one""" | ||||
""" | |||||
_docformat = b":%s: %s" | _docformat = b":%s: %s" | ||||
class templatekeyword(_templateregistrarbase): | class templatekeyword(_templateregistrarbase): | ||||
"""Decorator to register template keyword | """Decorator to register template keyword | ||||
Usage:: | Usage:: |
assertions and lead to crashes.""" | assertions and lead to crashes.""" | ||||
obsoletes = obsolete.getrevs(repo, b'obsolete') | obsoletes = obsolete.getrevs(repo, b'obsolete') | ||||
internals = repo._phasecache.getrevset(repo, phases.localhiddenphases) | internals = repo._phasecache.getrevset(repo, phases.localhiddenphases) | ||||
internals = frozenset(internals) | internals = frozenset(internals) | ||||
return obsoletes | internals | return obsoletes | internals | ||||
def pinnedrevs(repo): | def pinnedrevs(repo): | ||||
"""revisions blocking hidden changesets from being filtered | """revisions blocking hidden changesets from being filtered""" | ||||
""" | |||||
cl = repo.changelog | cl = repo.changelog | ||||
pinned = set() | pinned = set() | ||||
pinned.update([par.rev() for par in repo[None].parents()]) | pinned.update([par.rev() for par in repo[None].parents()]) | ||||
pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()]) | pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()]) | ||||
tags = {} | tags = {} | ||||
tagsmod.readlocaltags(repo.ui, repo, tags, {}) | tagsmod.readlocaltags(repo.ui, repo, tags, {}) |
if maybewdir: | if maybewdir: | ||||
raise error.WdirUnsupported | raise error.WdirUnsupported | ||||
return None | return None | ||||
except TypeError: | except TypeError: | ||||
pass | pass | ||||
def lookup(self, id): | def lookup(self, id): | ||||
"""locate a node based on: | """locate a node based on: | ||||
- revision number or str(revision number) | - revision number or str(revision number) | ||||
- nodeid or subset of hex nodeid | - nodeid or subset of hex nodeid | ||||
""" | """ | ||||
n = self._match(id) | n = self._match(id) | ||||
if n is not None: | if n is not None: | ||||
return n | return n | ||||
n = self._partialmatch(id) | n = self._partialmatch(id) | ||||
if n: | if n: | ||||
return n | return n | ||||
if base == rev: | if base == rev: | ||||
return nullrev | return nullrev | ||||
elif self._generaldelta: | elif self._generaldelta: | ||||
return base | return base | ||||
else: | else: | ||||
return rev - 1 | return rev - 1 | ||||
def issnapshot(self, rev): | def issnapshot(self, rev): | ||||
"""tells whether rev is a snapshot | """tells whether rev is a snapshot""" | ||||
""" | |||||
if not self._sparserevlog: | if not self._sparserevlog: | ||||
return self.deltaparent(rev) == nullrev | return self.deltaparent(rev) == nullrev | ||||
elif util.safehasattr(self.index, b'issnapshot'): | elif util.safehasattr(self.index, b'issnapshot'): | ||||
# directly assign the method to cache the testing and access | # directly assign the method to cache the testing and access | ||||
self.issnapshot = self.index.issnapshot | self.issnapshot = self.index.issnapshot | ||||
return self.issnapshot(rev) | return self.issnapshot(rev) | ||||
if rev == nullrev: | if rev == nullrev: | ||||
return True | return True | ||||
# the temp file replace the real index when we exit the context | # the temp file replace the real index when we exit the context | ||||
# manager | # manager | ||||
tr.replace(self.indexfile, trindex * self._io.size) | tr.replace(self.indexfile, trindex * self._io.size) | ||||
nodemaputil.setup_persistent_nodemap(tr, self) | nodemaputil.setup_persistent_nodemap(tr, self) | ||||
self._chunkclear() | self._chunkclear() | ||||
def _nodeduplicatecallback(self, transaction, node): | def _nodeduplicatecallback(self, transaction, node): | ||||
"""called when trying to add a node already stored. | """called when trying to add a node already stored.""" | ||||
""" | |||||
def addrevision( | def addrevision( | ||||
self, | self, | ||||
text, | text, | ||||
transaction, | transaction, | ||||
link, | link, | ||||
p1, | p1, | ||||
p2, | p2, |
return # no need to register again | return # no need to register again | ||||
tr.addpending( | tr.addpending( | ||||
callback_id, lambda tr: _persist_nodemap(tr, revlog, pending=True) | callback_id, lambda tr: _persist_nodemap(tr, revlog, pending=True) | ||||
) | ) | ||||
tr.addfinalize(callback_id, lambda tr: _persist_nodemap(tr, revlog)) | tr.addfinalize(callback_id, lambda tr: _persist_nodemap(tr, revlog)) | ||||
class _NoTransaction(object): | class _NoTransaction(object): | ||||
"""transaction like object to update the nodemap outside a transaction | """transaction like object to update the nodemap outside a transaction""" | ||||
""" | |||||
def __init__(self): | def __init__(self): | ||||
self._postclose = {} | self._postclose = {} | ||||
def addpostclose(self, callback_id, callback_func): | def addpostclose(self, callback_id, callback_func): | ||||
self._postclose[callback_id] = callback_func | self._postclose[callback_id] = callback_func | ||||
def registertmp(self, *args, **kwargs): | def registertmp(self, *args, **kwargs): | ||||
notr = _NoTransaction() | notr = _NoTransaction() | ||||
_persist_nodemap(notr, revlog) | _persist_nodemap(notr, revlog) | ||||
for k in sorted(notr._postclose): | for k in sorted(notr._postclose): | ||||
notr._postclose[k](None) | notr._postclose[k](None) | ||||
def _persist_nodemap(tr, revlog, pending=False): | def _persist_nodemap(tr, revlog, pending=False): | ||||
"""Write nodemap data on disk for a given revlog | """Write nodemap data on disk for a given revlog""" | ||||
""" | |||||
if getattr(revlog, 'filteredrevs', ()): | if getattr(revlog, 'filteredrevs', ()): | ||||
raise error.ProgrammingError( | raise error.ProgrammingError( | ||||
"cannot persist nodemap of a filtered changelog" | "cannot persist nodemap of a filtered changelog" | ||||
) | ) | ||||
if revlog.nodemap_file is None: | if revlog.nodemap_file is None: | ||||
msg = "calling persist nodemap on a revlog without the feature enableb" | msg = "calling persist nodemap on a revlog without the feature enableb" | ||||
raise error.ProgrammingError(msg) | raise error.ProgrammingError(msg) | ||||
# | # | ||||
# The implementation focus on simplicity, not on performance. A Rust | # The implementation focus on simplicity, not on performance. A Rust | ||||
# implementation should provide a efficient version of the same binary | # implementation should provide a efficient version of the same binary | ||||
# persistence. This reference python implementation is never meant to be | # persistence. This reference python implementation is never meant to be | ||||
# extensively use in production. | # extensively use in production. | ||||
def persistent_data(index): | def persistent_data(index): | ||||
"""return the persistent binary form for a nodemap for a given index | """return the persistent binary form for a nodemap for a given index""" | ||||
""" | |||||
trie = _build_trie(index) | trie = _build_trie(index) | ||||
return _persist_trie(trie) | return _persist_trie(trie) | ||||
def update_persistent_data(index, root, max_idx, last_rev): | def update_persistent_data(index, root, max_idx, last_rev): | ||||
"""return the incremental update for persistent nodemap from a given index | """return the incremental update for persistent nodemap from a given index""" | ||||
""" | |||||
changed_block, trie = _update_trie(index, root, last_rev) | changed_block, trie = _update_trie(index, root, last_rev) | ||||
return ( | return ( | ||||
changed_block * S_BLOCK.size, | changed_block * S_BLOCK.size, | ||||
_persist_trie(trie, existing_idx=max_idx), | _persist_trie(trie, existing_idx=max_idx), | ||||
) | ) | ||||
S_BLOCK = struct.Struct(">" + ("l" * 16)) | S_BLOCK = struct.Struct(">" + ("l" * 16)) |
except error.WdirUnsupported: | except error.WdirUnsupported: | ||||
r = repo[r].p1().rev() | r = repo[r].p1().rev() | ||||
ps.add(r) | ps.add(r) | ||||
return subset & ps | return subset & ps | ||||
@predicate(b'author(string)', safe=True, weight=10) | @predicate(b'author(string)', safe=True, weight=10) | ||||
def author(repo, subset, x): | def author(repo, subset, x): | ||||
"""Alias for ``user(string)``. | """Alias for ``user(string)``.""" | ||||
""" | |||||
# i18n: "author" is a keyword | # i18n: "author" is a keyword | ||||
n = getstring(x, _(b"author requires a string")) | n = getstring(x, _(b"author requires a string")) | ||||
kind, pattern, matcher = _substringmatcher(n, casesensitive=False) | kind, pattern, matcher = _substringmatcher(n, casesensitive=False) | ||||
return subset.filter( | return subset.filter( | ||||
lambda x: matcher(repo[x].user()), condrepr=(b'<user %r>', n) | lambda x: matcher(repo[x].user()), condrepr=(b'<user %r>', n) | ||||
) | ) | ||||
cs.add(r) | cs.add(r) | ||||
if p2 != nullrev and p2 in parentset: | if p2 != nullrev and p2 in parentset: | ||||
cs.add(r) | cs.add(r) | ||||
return baseset(cs) | return baseset(cs) | ||||
@predicate(b'children(set)', safe=True) | @predicate(b'children(set)', safe=True) | ||||
def children(repo, subset, x): | def children(repo, subset, x): | ||||
"""Child changesets of changesets in set. | """Child changesets of changesets in set.""" | ||||
""" | |||||
s = getset(repo, fullreposet(repo), x) | s = getset(repo, fullreposet(repo), x) | ||||
cs = _children(repo, subset, s) | cs = _children(repo, subset, s) | ||||
return subset & cs | return subset & cs | ||||
@predicate(b'closed()', safe=True, weight=10) | @predicate(b'closed()', safe=True, weight=10) | ||||
def closed(repo, subset, x): | def closed(repo, subset, x): | ||||
"""Changeset is closed. | """Changeset is closed.""" | ||||
""" | |||||
# i18n: "closed" is a keyword | # i18n: "closed" is a keyword | ||||
getargs(x, 0, 0, _(b"closed takes no arguments")) | getargs(x, 0, 0, _(b"closed takes no arguments")) | ||||
return subset.filter( | return subset.filter( | ||||
lambda r: repo[r].closesbranch(), condrepr=b'<branch closed>' | lambda r: repo[r].closesbranch(), condrepr=b'<branch closed>' | ||||
) | ) | ||||
# for internal use | # for internal use | ||||
@predicate(b'_commonancestorheads(set)', safe=True) | @predicate(b'_commonancestorheads(set)', safe=True) | ||||
def _commonancestorheads(repo, subset, x): | def _commonancestorheads(repo, subset, x): | ||||
# This is an internal method is for quickly calculating "heads(::x and | # This is an internal method is for quickly calculating "heads(::x and | ||||
# ::y)" | # ::y)" | ||||
# These greatest common ancestors are the same ones that the consensus bid | # These greatest common ancestors are the same ones that the consensus bid | ||||
# merge will find. | # merge will find. | ||||
startrevs = getset(repo, fullreposet(repo), x, order=anyorder) | startrevs = getset(repo, fullreposet(repo), x, order=anyorder) | ||||
ancs = repo.changelog._commonancestorsheads(*list(startrevs)) | ancs = repo.changelog._commonancestorsheads(*list(startrevs)) | ||||
return subset & baseset(ancs) | return subset & baseset(ancs) | ||||
@predicate(b'commonancestors(set)', safe=True) | @predicate(b'commonancestors(set)', safe=True) | ||||
def commonancestors(repo, subset, x): | def commonancestors(repo, subset, x): | ||||
"""Changesets that are ancestors of every changeset in set. | """Changesets that are ancestors of every changeset in set.""" | ||||
""" | |||||
startrevs = getset(repo, fullreposet(repo), x, order=anyorder) | startrevs = getset(repo, fullreposet(repo), x, order=anyorder) | ||||
if not startrevs: | if not startrevs: | ||||
return baseset() | return baseset() | ||||
for r in startrevs: | for r in startrevs: | ||||
subset &= dagop.revancestors(repo, baseset([r])) | subset &= dagop.revancestors(repo, baseset([r])) | ||||
return subset | return subset | ||||
return subset.filter( | return subset.filter( | ||||
lambda r: _matchvalue(r), condrepr=(b'<converted %r>', rev) | lambda r: _matchvalue(r), condrepr=(b'<converted %r>', rev) | ||||
) | ) | ||||
@predicate(b'date(interval)', safe=True, weight=10) | @predicate(b'date(interval)', safe=True, weight=10) | ||||
def date(repo, subset, x): | def date(repo, subset, x): | ||||
"""Changesets within the interval, see :hg:`help dates`. | """Changesets within the interval, see :hg:`help dates`.""" | ||||
""" | |||||
# i18n: "date" is a keyword | # i18n: "date" is a keyword | ||||
ds = getstring(x, _(b"date requires a string")) | ds = getstring(x, _(b"date requires a string")) | ||||
dm = dateutil.matchdate(ds) | dm = dateutil.matchdate(ds) | ||||
return subset.filter( | return subset.filter( | ||||
lambda x: dm(repo[x].date()[0]), condrepr=(b'<date %r>', ds) | lambda x: dm(repo[x].date()[0]), condrepr=(b'<date %r>', ds) | ||||
) | ) | ||||
_(b'extdata takes at least 1 string argument'), | _(b'extdata takes at least 1 string argument'), | ||||
) | ) | ||||
data = scmutil.extdatasource(repo, source) | data = scmutil.extdatasource(repo, source) | ||||
return subset & baseset(data) | return subset & baseset(data) | ||||
@predicate(b'extinct()', safe=True) | @predicate(b'extinct()', safe=True) | ||||
def extinct(repo, subset, x): | def extinct(repo, subset, x): | ||||
"""Obsolete changesets with obsolete descendants only. (EXPERIMENTAL) | """Obsolete changesets with obsolete descendants only. (EXPERIMENTAL)""" | ||||
""" | |||||
# i18n: "extinct" is a keyword | # i18n: "extinct" is a keyword | ||||
getargs(x, 0, 0, _(b"extinct takes no arguments")) | getargs(x, 0, 0, _(b"extinct takes no arguments")) | ||||
extincts = obsmod.getrevs(repo, b'extinct') | extincts = obsmod.getrevs(repo, b'extinct') | ||||
return subset & extincts | return subset & extincts | ||||
@predicate(b'extra(label, [value])', safe=True) | @predicate(b'extra(label, [value])', safe=True) | ||||
def extra(repo, subset, x): | def extra(repo, subset, x): | ||||
# deletion in changelog | # deletion in changelog | ||||
continue | continue | ||||
return subset & s | return subset & s | ||||
@predicate(b'first(set, [n])', safe=True, takeorder=True, weight=0) | @predicate(b'first(set, [n])', safe=True, takeorder=True, weight=0) | ||||
def first(repo, subset, x, order): | def first(repo, subset, x, order): | ||||
"""An alias for limit(). | """An alias for limit().""" | ||||
""" | |||||
return limit(repo, subset, x, order) | return limit(repo, subset, x, order) | ||||
def _follow(repo, subset, x, name, followfirst=False): | def _follow(repo, subset, x, name, followfirst=False): | ||||
args = getargsdict(x, name, b'file startrev') | args = getargsdict(x, name, b'file startrev') | ||||
revs = None | revs = None | ||||
if b'startrev' in args: | if b'startrev' in args: | ||||
revs = getset(repo, fullreposet(repo), args[b'startrev']) | revs = getset(repo, fullreposet(repo), args[b'startrev']) | ||||
), | ), | ||||
iterasc=False, | iterasc=False, | ||||
) | ) | ||||
return subset & rs | return subset & rs | ||||
@predicate(b'all()', safe=True) | @predicate(b'all()', safe=True) | ||||
def getall(repo, subset, x): | def getall(repo, subset, x): | ||||
"""All changesets, the same as ``0:tip``. | """All changesets, the same as ``0:tip``.""" | ||||
""" | |||||
# i18n: "all" is a keyword | # i18n: "all" is a keyword | ||||
getargs(x, 0, 0, _(b"all takes no arguments")) | getargs(x, 0, 0, _(b"all takes no arguments")) | ||||
return subset & spanset(repo) # drop "null" if any | return subset & spanset(repo) # drop "null" if any | ||||
@predicate(b'grep(regex)', weight=10) | @predicate(b'grep(regex)', weight=10) | ||||
def grep(repo, subset, x): | def grep(repo, subset, x): | ||||
"""Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')`` | """Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')`` | ||||
""" | """ | ||||
# i18n: "file" is a keyword | # i18n: "file" is a keyword | ||||
pat = getstring(x, _(b"file requires a pattern")) | pat = getstring(x, _(b"file requires a pattern")) | ||||
return _matchfiles(repo, subset, (b'string', b'p:' + pat)) | return _matchfiles(repo, subset, (b'string', b'p:' + pat)) | ||||
@predicate(b'head()', safe=True) | @predicate(b'head()', safe=True) | ||||
def head(repo, subset, x): | def head(repo, subset, x): | ||||
"""Changeset is a named branch head. | """Changeset is a named branch head.""" | ||||
""" | |||||
# i18n: "head" is a keyword | # i18n: "head" is a keyword | ||||
getargs(x, 0, 0, _(b"head takes no arguments")) | getargs(x, 0, 0, _(b"head takes no arguments")) | ||||
hs = set() | hs = set() | ||||
cl = repo.changelog | cl = repo.changelog | ||||
for ls in repo.branchmap().iterheads(): | for ls in repo.branchmap().iterheads(): | ||||
hs.update(cl.rev(h) for h in ls) | hs.update(cl.rev(h) for h in ls) | ||||
return subset & baseset(hs) | return subset & baseset(hs) | ||||
@predicate(b'heads(set)', safe=True, takeorder=True) | @predicate(b'heads(set)', safe=True, takeorder=True) | ||||
def heads(repo, subset, x, order): | def heads(repo, subset, x, order): | ||||
"""Members of set with no children in set. | """Members of set with no children in set.""" | ||||
""" | |||||
# argument set should never define order | # argument set should never define order | ||||
if order == defineorder: | if order == defineorder: | ||||
order = followorder | order = followorder | ||||
inputset = getset(repo, fullreposet(repo), x, order=order) | inputset = getset(repo, fullreposet(repo), x, order=order) | ||||
wdirparents = None | wdirparents = None | ||||
if node.wdirrev in inputset: | if node.wdirrev in inputset: | ||||
# a bit slower, but not common so good enough for now | # a bit slower, but not common so good enough for now | ||||
wdirparents = [p.rev() for p in repo[None].parents()] | wdirparents = [p.rev() for p in repo[None].parents()] | ||||
inputset = set(inputset) | inputset = set(inputset) | ||||
inputset.discard(node.wdirrev) | inputset.discard(node.wdirrev) | ||||
heads = repo.changelog.headrevs(inputset) | heads = repo.changelog.headrevs(inputset) | ||||
if wdirparents is not None: | if wdirparents is not None: | ||||
heads.difference_update(wdirparents) | heads.difference_update(wdirparents) | ||||
heads.add(node.wdirrev) | heads.add(node.wdirrev) | ||||
heads = baseset(heads) | heads = baseset(heads) | ||||
return subset & heads | return subset & heads | ||||
@predicate(b'hidden()', safe=True) | @predicate(b'hidden()', safe=True) | ||||
def hidden(repo, subset, x): | def hidden(repo, subset, x): | ||||
"""Hidden changesets. | """Hidden changesets.""" | ||||
""" | |||||
# i18n: "hidden" is a keyword | # i18n: "hidden" is a keyword | ||||
getargs(x, 0, 0, _(b"hidden takes no arguments")) | getargs(x, 0, 0, _(b"hidden takes no arguments")) | ||||
hiddenrevs = repoview.filterrevs(repo, b'visible') | hiddenrevs = repoview.filterrevs(repo, b'visible') | ||||
return subset & hiddenrevs | return subset & hiddenrevs | ||||
@predicate(b'keyword(string)', safe=True, weight=10) | @predicate(b'keyword(string)', safe=True, weight=10) | ||||
def keyword(repo, subset, x): | def keyword(repo, subset, x): | ||||
for t in c.files() + [c.user(), c.description()] | for t in c.files() + [c.user(), c.description()] | ||||
) | ) | ||||
return subset.filter(matches, condrepr=(b'<keyword %r>', kw)) | return subset.filter(matches, condrepr=(b'<keyword %r>', kw)) | ||||
@predicate(b'limit(set[, n[, offset]])', safe=True, takeorder=True, weight=0) | @predicate(b'limit(set[, n[, offset]])', safe=True, takeorder=True, weight=0) | ||||
def limit(repo, subset, x, order): | def limit(repo, subset, x, order): | ||||
"""First n members of set, defaulting to 1, starting from offset. | """First n members of set, defaulting to 1, starting from offset.""" | ||||
""" | |||||
args = getargsdict(x, b'limit', b'set n offset') | args = getargsdict(x, b'limit', b'set n offset') | ||||
if b'set' not in args: | if b'set' not in args: | ||||
# i18n: "limit" is a keyword | # i18n: "limit" is a keyword | ||||
raise error.ParseError(_(b"limit requires one to three arguments")) | raise error.ParseError(_(b"limit requires one to three arguments")) | ||||
# i18n: "limit" is a keyword | # i18n: "limit" is a keyword | ||||
lim = getinteger(args.get(b'n'), _(b"limit expects a number"), default=1) | lim = getinteger(args.get(b'n'), _(b"limit expects a number"), default=1) | ||||
if lim < 0: | if lim < 0: | ||||
raise error.ParseError(_(b"negative number to select")) | raise error.ParseError(_(b"negative number to select")) | ||||
# i18n: "limit" is a keyword | # i18n: "limit" is a keyword | ||||
ofs = getinteger( | ofs = getinteger( | ||||
args.get(b'offset'), _(b"limit expects a number"), default=0 | args.get(b'offset'), _(b"limit expects a number"), default=0 | ||||
) | ) | ||||
if ofs < 0: | if ofs < 0: | ||||
raise error.ParseError(_(b"negative offset")) | raise error.ParseError(_(b"negative offset")) | ||||
os = getset(repo, fullreposet(repo), args[b'set']) | os = getset(repo, fullreposet(repo), args[b'set']) | ||||
ls = os.slice(ofs, ofs + lim) | ls = os.slice(ofs, ofs + lim) | ||||
if order == followorder and lim > 1: | if order == followorder and lim > 1: | ||||
return subset & ls | return subset & ls | ||||
return ls & subset | return ls & subset | ||||
@predicate(b'last(set, [n])', safe=True, takeorder=True) | @predicate(b'last(set, [n])', safe=True, takeorder=True) | ||||
def last(repo, subset, x, order): | def last(repo, subset, x, order): | ||||
"""Last n members of set, defaulting to 1. | """Last n members of set, defaulting to 1.""" | ||||
""" | |||||
# i18n: "last" is a keyword | # i18n: "last" is a keyword | ||||
l = getargs(x, 1, 2, _(b"last requires one or two arguments")) | l = getargs(x, 1, 2, _(b"last requires one or two arguments")) | ||||
lim = 1 | lim = 1 | ||||
if len(l) == 2: | if len(l) == 2: | ||||
# i18n: "last" is a keyword | # i18n: "last" is a keyword | ||||
lim = getinteger(l[1], _(b"last expects a number")) | lim = getinteger(l[1], _(b"last expects a number")) | ||||
if lim < 0: | if lim < 0: | ||||
raise error.ParseError(_(b"negative number to select")) | raise error.ParseError(_(b"negative number to select")) | ||||
os = getset(repo, fullreposet(repo), l[0]) | os = getset(repo, fullreposet(repo), l[0]) | ||||
os.reverse() | os.reverse() | ||||
ls = os.slice(0, lim) | ls = os.slice(0, lim) | ||||
if order == followorder and lim > 1: | if order == followorder and lim > 1: | ||||
return subset & ls | return subset & ls | ||||
ls.reverse() | ls.reverse() | ||||
return ls & subset | return ls & subset | ||||
@predicate(b'max(set)', safe=True) | @predicate(b'max(set)', safe=True) | ||||
def maxrev(repo, subset, x): | def maxrev(repo, subset, x): | ||||
"""Changeset with highest revision number in set. | """Changeset with highest revision number in set.""" | ||||
""" | |||||
os = getset(repo, fullreposet(repo), x) | os = getset(repo, fullreposet(repo), x) | ||||
try: | try: | ||||
m = os.max() | m = os.max() | ||||
if m in subset: | if m in subset: | ||||
return baseset([m], datarepr=(b'<max %r, %r>', subset, os)) | return baseset([m], datarepr=(b'<max %r, %r>', subset, os)) | ||||
except ValueError: | except ValueError: | ||||
# os.max() throws a ValueError when the collection is empty. | # os.max() throws a ValueError when the collection is empty. | ||||
# Same as python's max(). | # Same as python's max(). | ||||
pass | pass | ||||
return baseset(datarepr=(b'<max %r, %r>', subset, os)) | return baseset(datarepr=(b'<max %r, %r>', subset, os)) | ||||
@predicate(b'merge()', safe=True) | @predicate(b'merge()', safe=True) | ||||
def merge(repo, subset, x): | def merge(repo, subset, x): | ||||
"""Changeset is a merge changeset. | """Changeset is a merge changeset.""" | ||||
""" | |||||
# i18n: "merge" is a keyword | # i18n: "merge" is a keyword | ||||
getargs(x, 0, 0, _(b"merge takes no arguments")) | getargs(x, 0, 0, _(b"merge takes no arguments")) | ||||
cl = repo.changelog | cl = repo.changelog | ||||
nullrev = node.nullrev | nullrev = node.nullrev | ||||
def ismerge(r): | def ismerge(r): | ||||
try: | try: | ||||
return cl.parentrevs(r)[1] != nullrev | return cl.parentrevs(r)[1] != nullrev | ||||
except error.WdirUnsupported: | except error.WdirUnsupported: | ||||
return bool(repo[r].p2()) | return bool(repo[r].p2()) | ||||
return subset.filter(ismerge, condrepr=b'<merge>') | return subset.filter(ismerge, condrepr=b'<merge>') | ||||
@predicate(b'branchpoint()', safe=True) | @predicate(b'branchpoint()', safe=True) | ||||
def branchpoint(repo, subset, x): | def branchpoint(repo, subset, x): | ||||
"""Changesets with more than one child. | """Changesets with more than one child.""" | ||||
""" | |||||
# i18n: "branchpoint" is a keyword | # i18n: "branchpoint" is a keyword | ||||
getargs(x, 0, 0, _(b"branchpoint takes no arguments")) | getargs(x, 0, 0, _(b"branchpoint takes no arguments")) | ||||
cl = repo.changelog | cl = repo.changelog | ||||
if not subset: | if not subset: | ||||
return baseset() | return baseset() | ||||
# XXX this should be 'parentset.min()' assuming 'parentset' is a smartset | # XXX this should be 'parentset.min()' assuming 'parentset' is a smartset | ||||
# (and if it is not, it should.) | # (and if it is not, it should.) | ||||
baserev = min(subset) | baserev = min(subset) | ||||
parentscount = [0] * (len(repo) - baserev) | parentscount = [0] * (len(repo) - baserev) | ||||
for r in cl.revs(start=baserev + 1): | for r in cl.revs(start=baserev + 1): | ||||
for p in cl.parentrevs(r): | for p in cl.parentrevs(r): | ||||
if p >= baserev: | if p >= baserev: | ||||
parentscount[p - baserev] += 1 | parentscount[p - baserev] += 1 | ||||
return subset.filter( | return subset.filter( | ||||
lambda r: parentscount[r - baserev] > 1, condrepr=b'<branchpoint>' | lambda r: parentscount[r - baserev] > 1, condrepr=b'<branchpoint>' | ||||
) | ) | ||||
@predicate(b'min(set)', safe=True) | @predicate(b'min(set)', safe=True) | ||||
def minrev(repo, subset, x): | def minrev(repo, subset, x): | ||||
"""Changeset with lowest revision number in set. | """Changeset with lowest revision number in set.""" | ||||
""" | |||||
os = getset(repo, fullreposet(repo), x) | os = getset(repo, fullreposet(repo), x) | ||||
try: | try: | ||||
m = os.min() | m = os.min() | ||||
if m in subset: | if m in subset: | ||||
return baseset([m], datarepr=(b'<min %r, %r>', subset, os)) | return baseset([m], datarepr=(b'<min %r, %r>', subset, os)) | ||||
except ValueError: | except ValueError: | ||||
# os.min() throws a ValueError when the collection is empty. | # os.min() throws a ValueError when the collection is empty. | ||||
# Same as python's min(). | # Same as python's min(). | ||||
names.update(repo[n].rev() for n in ns.nodes(repo, name)) | names.update(repo[n].rev() for n in ns.nodes(repo, name)) | ||||
names -= {node.nullrev} | names -= {node.nullrev} | ||||
return subset & names | return subset & names | ||||
@predicate(b'id(string)', safe=True) | @predicate(b'id(string)', safe=True) | ||||
def node_(repo, subset, x): | def node_(repo, subset, x): | ||||
"""Revision non-ambiguously specified by the given hex string prefix. | """Revision non-ambiguously specified by the given hex string prefix.""" | ||||
""" | |||||
# i18n: "id" is a keyword | # i18n: "id" is a keyword | ||||
l = getargs(x, 1, 1, _(b"id requires one argument")) | l = getargs(x, 1, 1, _(b"id requires one argument")) | ||||
# i18n: "id" is a keyword | # i18n: "id" is a keyword | ||||
n = getstring(l[0], _(b"id requires a string")) | n = getstring(l[0], _(b"id requires a string")) | ||||
if len(n) == 40: | if len(n) == 40: | ||||
try: | try: | ||||
rn = repo.changelog.rev(node.bin(n)) | rn = repo.changelog.rev(node.bin(n)) | ||||
except error.WdirUnsupported: | except error.WdirUnsupported: | ||||
if rn is None: | if rn is None: | ||||
return baseset() | return baseset() | ||||
result = baseset([rn]) | result = baseset([rn]) | ||||
return result & subset | return result & subset | ||||
@predicate(b'none()', safe=True) | @predicate(b'none()', safe=True) | ||||
def none(repo, subset, x): | def none(repo, subset, x): | ||||
"""No changesets. | """No changesets.""" | ||||
""" | |||||
# i18n: "none" is a keyword | # i18n: "none" is a keyword | ||||
getargs(x, 0, 0, _(b"none takes no arguments")) | getargs(x, 0, 0, _(b"none takes no arguments")) | ||||
return baseset() | return baseset() | ||||
@predicate(b'obsolete()', safe=True) | @predicate(b'obsolete()', safe=True) | ||||
def obsolete(repo, subset, x): | def obsolete(repo, subset, x): | ||||
"""Mutable changeset with a newer version. (EXPERIMENTAL)""" | """Mutable changeset with a newer version. (EXPERIMENTAL)""" | ||||
repo.ui.popbuffer() | repo.ui.popbuffer() | ||||
cl = repo.changelog | cl = repo.changelog | ||||
o = {cl.rev(r) for r in outgoing.missing} | o = {cl.rev(r) for r in outgoing.missing} | ||||
return subset & o | return subset & o | ||||
@predicate(b'p1([set])', safe=True) | @predicate(b'p1([set])', safe=True) | ||||
def p1(repo, subset, x): | def p1(repo, subset, x): | ||||
"""First parent of changesets in set, or the working directory. | """First parent of changesets in set, or the working directory.""" | ||||
""" | |||||
if x is None: | if x is None: | ||||
p = repo[x].p1().rev() | p = repo[x].p1().rev() | ||||
if p >= 0: | if p >= 0: | ||||
return subset & baseset([p]) | return subset & baseset([p]) | ||||
return baseset() | return baseset() | ||||
ps = set() | ps = set() | ||||
cl = repo.changelog | cl = repo.changelog | ||||
for r in getset(repo, fullreposet(repo), x): | for r in getset(repo, fullreposet(repo), x): | ||||
try: | try: | ||||
ps.add(cl.parentrevs(r)[0]) | ps.add(cl.parentrevs(r)[0]) | ||||
except error.WdirUnsupported: | except error.WdirUnsupported: | ||||
ps.add(repo[r].p1().rev()) | ps.add(repo[r].p1().rev()) | ||||
ps -= {node.nullrev} | ps -= {node.nullrev} | ||||
# XXX we should turn this into a baseset instead of a set, smartset may do | # XXX we should turn this into a baseset instead of a set, smartset may do | ||||
# some optimizations from the fact this is a baseset. | # some optimizations from the fact this is a baseset. | ||||
return subset & ps | return subset & ps | ||||
@predicate(b'p2([set])', safe=True) | @predicate(b'p2([set])', safe=True) | ||||
def p2(repo, subset, x): | def p2(repo, subset, x): | ||||
"""Second parent of changesets in set, or the working directory. | """Second parent of changesets in set, or the working directory.""" | ||||
""" | |||||
if x is None: | if x is None: | ||||
ps = repo[x].parents() | ps = repo[x].parents() | ||||
try: | try: | ||||
p = ps[1].rev() | p = ps[1].rev() | ||||
if p >= 0: | if p >= 0: | ||||
return subset & baseset([p]) | return subset & baseset([p]) | ||||
return baseset() | return baseset() | ||||
except IndexError: | except IndexError: | ||||
return True | return True | ||||
return False | return False | ||||
return subset.filter(matches, condrepr=(b'<matching%r %r>', fields, revs)) | return subset.filter(matches, condrepr=(b'<matching%r %r>', fields, revs)) | ||||
@predicate(b'reverse(set)', safe=True, takeorder=True, weight=0) | @predicate(b'reverse(set)', safe=True, takeorder=True, weight=0) | ||||
def reverse(repo, subset, x, order): | def reverse(repo, subset, x, order): | ||||
"""Reverse order of set. | """Reverse order of set.""" | ||||
""" | |||||
l = getset(repo, subset, x, order) | l = getset(repo, subset, x, order) | ||||
if order == defineorder: | if order == defineorder: | ||||
l.reverse() | l.reverse() | ||||
return l | return l | ||||
@predicate(b'roots(set)', safe=True) | @predicate(b'roots(set)', safe=True) | ||||
def roots(repo, subset, x): | def roots(repo, subset, x): | ||||
"""Changesets in set with no parent changeset in set. | """Changesets in set with no parent changeset in set.""" | ||||
""" | |||||
s = getset(repo, fullreposet(repo), x) | s = getset(repo, fullreposet(repo), x) | ||||
parents = repo.changelog.parentrevs | parents = repo.changelog.parentrevs | ||||
def filter(r): | def filter(r): | ||||
for p in parents(r): | for p in parents(r): | ||||
if 0 <= p and p in s: | if 0 <= p and p in s: | ||||
return False | return False | ||||
return True | return True | ||||
@predicate(b'tagged', safe=True) | @predicate(b'tagged', safe=True) | ||||
def tagged(repo, subset, x): | def tagged(repo, subset, x): | ||||
return tag(repo, subset, x) | return tag(repo, subset, x) | ||||
@predicate(b'orphan()', safe=True) | @predicate(b'orphan()', safe=True) | ||||
def orphan(repo, subset, x): | def orphan(repo, subset, x): | ||||
"""Non-obsolete changesets with obsolete ancestors. (EXPERIMENTAL) | """Non-obsolete changesets with obsolete ancestors. (EXPERIMENTAL)""" | ||||
""" | |||||
# i18n: "orphan" is a keyword | # i18n: "orphan" is a keyword | ||||
getargs(x, 0, 0, _(b"orphan takes no arguments")) | getargs(x, 0, 0, _(b"orphan takes no arguments")) | ||||
orphan = obsmod.getrevs(repo, b'orphan') | orphan = obsmod.getrevs(repo, b'orphan') | ||||
return subset & orphan | return subset & orphan | ||||
@predicate(b'unstable()', safe=True) | @predicate(b'unstable()', safe=True) | ||||
def unstable(repo, subset, x): | def unstable(repo, subset, x): | ||||
"""Changesets with instabilities. (EXPERIMENTAL) | """Changesets with instabilities. (EXPERIMENTAL)""" | ||||
""" | |||||
# i18n: "unstable" is a keyword | # i18n: "unstable" is a keyword | ||||
getargs(x, 0, 0, b'unstable takes no arguments') | getargs(x, 0, 0, b'unstable takes no arguments') | ||||
_unstable = set() | _unstable = set() | ||||
_unstable.update(obsmod.getrevs(repo, b'orphan')) | _unstable.update(obsmod.getrevs(repo, b'orphan')) | ||||
_unstable.update(obsmod.getrevs(repo, b'phasedivergent')) | _unstable.update(obsmod.getrevs(repo, b'phasedivergent')) | ||||
_unstable.update(obsmod.getrevs(repo, b'contentdivergent')) | _unstable.update(obsmod.getrevs(repo, b'contentdivergent')) | ||||
return subset & baseset(_unstable) | return subset & baseset(_unstable) | ||||
if subset is None: | if subset is None: | ||||
subset = fullreposet(repo) | subset = fullreposet(repo) | ||||
return getset(repo, subset, tree, order) | return getset(repo, subset, tree, order) | ||||
return mfunc | return mfunc | ||||
def loadpredicate(ui, extname, registrarobj): | def loadpredicate(ui, extname, registrarobj): | ||||
"""Load revset predicates from specified registrarobj | """Load revset predicates from specified registrarobj""" | ||||
""" | |||||
for name, func in pycompat.iteritems(registrarobj._table): | for name, func in pycompat.iteritems(registrarobj._table): | ||||
symbols[name] = func | symbols[name] = func | ||||
if func._safe: | if func._safe: | ||||
safesymbols.add(name) | safesymbols.add(name) | ||||
# load built-in predicates explicitly to setup safesymbols | # load built-in predicates explicitly to setup safesymbols | ||||
loadpredicate(None, None, predicate) | loadpredicate(None, None, predicate) | ||||
# tell hggettext to extract docstrings from these functions: | # tell hggettext to extract docstrings from these functions: | ||||
i18nfunctions = symbols.values() | i18nfunctions = symbols.values() |
) | ) | ||||
) | set(map(pycompat.bytechr, pycompat.xrange(128, 256))) | ) | set(map(pycompat.bytechr, pycompat.xrange(128, 256))) | ||||
# default set of valid characters for non-initial letters of symbols | # default set of valid characters for non-initial letters of symbols | ||||
_symletters = _syminitletters | set(pycompat.iterbytestr(b'-/')) | _symletters = _syminitletters | set(pycompat.iterbytestr(b'-/')) | ||||
def tokenize(program, lookup=None, syminitletters=None, symletters=None): | def tokenize(program, lookup=None, syminitletters=None, symletters=None): | ||||
''' | """ | ||||
Parse a revset statement into a stream of tokens | Parse a revset statement into a stream of tokens | ||||
``syminitletters`` is the set of valid characters for the initial | ``syminitletters`` is the set of valid characters for the initial | ||||
letter of symbols. | letter of symbols. | ||||
By default, character ``c`` is recognized as valid for initial | By default, character ``c`` is recognized as valid for initial | ||||
letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``. | letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``. | ||||
``symletters`` is the set of valid characters for non-initial | ``symletters`` is the set of valid characters for non-initial | ||||
letters of symbols. | letters of symbols. | ||||
By default, character ``c`` is recognized as valid for non-initial | By default, character ``c`` is recognized as valid for non-initial | ||||
letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``. | letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``. | ||||
Check that @ is a valid unquoted token character (issue3686): | Check that @ is a valid unquoted token character (issue3686): | ||||
>>> list(tokenize(b"@::")) | >>> list(tokenize(b"@::")) | ||||
[('symbol', '@', 0), ('::', None, 1), ('end', None, 3)] | [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)] | ||||
''' | """ | ||||
if not isinstance(program, bytes): | if not isinstance(program, bytes): | ||||
raise error.ProgrammingError( | raise error.ProgrammingError( | ||||
b'revset statement must be bytes, got %r' % program | b'revset statement must be bytes, got %r' % program | ||||
) | ) | ||||
program = pycompat.bytestr(program) | program = pycompat.bytestr(program) | ||||
if syminitletters is None: | if syminitletters is None: | ||||
syminitletters = _syminitletters | syminitletters = _syminitletters | ||||
if symletters is None: | if symletters is None: | ||||
for name, alias in sorted(pycompat.iteritems(aliases)): | for name, alias in sorted(pycompat.iteritems(aliases)): | ||||
if alias.error and not alias.warned: | if alias.error and not alias.warned: | ||||
warn(_(b'warning: %s\n') % (alias.error)) | warn(_(b'warning: %s\n') % (alias.error)) | ||||
alias.warned = True | alias.warned = True | ||||
return tree | return tree | ||||
def foldconcat(tree): | def foldconcat(tree): | ||||
"""Fold elements to be concatenated by `##` | """Fold elements to be concatenated by `##`""" | ||||
""" | |||||
if not isinstance(tree, tuple) or tree[0] in ( | if not isinstance(tree, tuple) or tree[0] in ( | ||||
b'string', | b'string', | ||||
b'symbol', | b'symbol', | ||||
b'smartset', | b'smartset', | ||||
): | ): | ||||
return tree | return tree | ||||
if tree[0] == b'_concat': | if tree[0] == b'_concat': | ||||
pending = [tree] | pending = [tree] | ||||
_formatlistfuncs = { | _formatlistfuncs = { | ||||
b'l': _formatlistexp, | b'l': _formatlistexp, | ||||
b'p': _formatparamexp, | b'p': _formatparamexp, | ||||
} | } | ||||
def formatspec(expr, *args): | def formatspec(expr, *args): | ||||
''' | """ | ||||
This is a convenience function for using revsets internally, and | This is a convenience function for using revsets internally, and | ||||
escapes arguments appropriately. Aliases are intentionally ignored | escapes arguments appropriately. Aliases are intentionally ignored | ||||
so that intended expression behavior isn't accidentally subverted. | so that intended expression behavior isn't accidentally subverted. | ||||
Supported arguments: | Supported arguments: | ||||
%r = revset expression, parenthesized | %r = revset expression, parenthesized | ||||
%d = rev(int(arg)), no quoting | %d = rev(int(arg)), no quoting | ||||
>>> formatspec(b'branch(%b)', b) | >>> formatspec(b'branch(%b)', b) | ||||
"branch('default')" | "branch('default')" | ||||
>>> formatspec(b'root(%ls)', [b'a', b'b', b'c', b'd']) | >>> formatspec(b'root(%ls)', [b'a', b'b', b'c', b'd']) | ||||
"root(_list('a\\\\x00b\\\\x00c\\\\x00d'))" | "root(_list('a\\\\x00b\\\\x00c\\\\x00d'))" | ||||
>>> formatspec(b'sort(%r, %ps)', b':', [b'desc', b'user']) | >>> formatspec(b'sort(%r, %ps)', b':', [b'desc', b'user']) | ||||
"sort((:), 'desc', 'user')" | "sort((:), 'desc', 'user')" | ||||
>>> formatspec(b'%ls', [b'a', b"'"]) | >>> formatspec(b'%ls', [b'a', b"'"]) | ||||
"_list('a\\\\x00\\\\'')" | "_list('a\\\\x00\\\\'')" | ||||
''' | """ | ||||
parsed = _parseargs(expr, args) | parsed = _parseargs(expr, args) | ||||
ret = [] | ret = [] | ||||
for t, arg in parsed: | for t, arg in parsed: | ||||
if t is None: | if t is None: | ||||
ret.append(arg) | ret.append(arg) | ||||
elif t == b'baseset': | elif t == b'baseset': | ||||
if isinstance(arg, set): | if isinstance(arg, set): | ||||
arg = sorted(arg) | arg = sorted(arg) |
parsers = policy.importmod('parsers') | parsers = policy.importmod('parsers') | ||||
rustrevlog = policy.importrust('revlog') | rustrevlog = policy.importrust('revlog') | ||||
termsize = scmplatform.termsize | termsize = scmplatform.termsize | ||||
@attr.s(slots=True, repr=False) | @attr.s(slots=True, repr=False) | ||||
class status(object): | class status(object): | ||||
'''Struct with a list of files per status. | """Struct with a list of files per status. | ||||
The 'deleted', 'unknown' and 'ignored' properties are only | The 'deleted', 'unknown' and 'ignored' properties are only | ||||
relevant to the working copy. | relevant to the working copy. | ||||
''' | """ | ||||
modified = attr.ib(default=attr.Factory(list)) | modified = attr.ib(default=attr.Factory(list)) | ||||
added = attr.ib(default=attr.Factory(list)) | added = attr.ib(default=attr.Factory(list)) | ||||
removed = attr.ib(default=attr.Factory(list)) | removed = attr.ib(default=attr.Factory(list)) | ||||
deleted = attr.ib(default=attr.Factory(list)) | deleted = attr.ib(default=attr.Factory(list)) | ||||
unknown = attr.ib(default=attr.Factory(list)) | unknown = attr.ib(default=attr.Factory(list)) | ||||
ignored = attr.ib(default=attr.Factory(list)) | ignored = attr.ib(default=attr.Factory(list)) | ||||
clean = attr.ib(default=attr.Factory(list)) | clean = attr.ib(default=attr.Factory(list)) | ||||
# status and diff will have an accurate result when it does | # status and diff will have an accurate result when it does | ||||
# 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared | # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared | ||||
# against itself. | # against itself. | ||||
for subpath in missing: | for subpath in missing: | ||||
yield subpath, ctx2.nullsub(subpath, ctx1) | yield subpath, ctx2.nullsub(subpath, ctx1) | ||||
def nochangesfound(ui, repo, excluded=None): | def nochangesfound(ui, repo, excluded=None): | ||||
'''Report no changes for push/pull, excluded is None or a list of | """Report no changes for push/pull, excluded is None or a list of | ||||
nodes excluded from the push/pull. | nodes excluded from the push/pull. | ||||
''' | """ | ||||
secretlist = [] | secretlist = [] | ||||
if excluded: | if excluded: | ||||
for n in excluded: | for n in excluded: | ||||
ctx = repo[n] | ctx = repo[n] | ||||
if ctx.phase() >= phases.secret and not ctx.extinct(): | if ctx.phase() >= phases.secret and not ctx.extinct(): | ||||
secretlist.append(n) | secretlist.append(n) | ||||
if secretlist: | if secretlist: | ||||
if msg: | if msg: | ||||
msg = b"%s: %s" % (msg, procutil.shellquote(f)) | msg = b"%s: %s" % (msg, procutil.shellquote(f)) | ||||
if abort: | if abort: | ||||
raise error.InputError(msg) | raise error.InputError(msg) | ||||
ui.warn(_(b"warning: %s\n") % msg) | ui.warn(_(b"warning: %s\n") % msg) | ||||
def checkportabilityalert(ui): | def checkportabilityalert(ui): | ||||
'''check if the user's config requests nothing, a warning, or abort for | """check if the user's config requests nothing, a warning, or abort for | ||||
non-portable filenames''' | non-portable filenames""" | ||||
val = ui.config(b'ui', b'portablefilenames') | val = ui.config(b'ui', b'portablefilenames') | ||||
lval = val.lower() | lval = val.lower() | ||||
bval = stringutil.parsebool(val) | bval = stringutil.parsebool(val) | ||||
abort = pycompat.iswindows or lval == b'abort' | abort = pycompat.iswindows or lval == b'abort' | ||||
warn = bval or lval == b'warn' | warn = bval or lval == b'warn' | ||||
if bval is None and not (warn or abort or lval == b'ignore'): | if bval is None and not (warn or abort or lval == b'ignore'): | ||||
raise error.ConfigError( | raise error.ConfigError( | ||||
_(b"ui.portablefilenames value is invalid ('%s')") % val | _(b"ui.portablefilenames value is invalid ('%s')") % val | ||||
for rev in revs: | for rev in revs: | ||||
s.update(b'%d;' % rev) | s.update(b'%d;' % rev) | ||||
key = s.digest() | key = s.digest() | ||||
cl._filteredrevs_hashcache[maxrev] = key | cl._filteredrevs_hashcache[maxrev] = key | ||||
return key | return key | ||||
def walkrepos(path, followsym=False, seen_dirs=None, recurse=False): | def walkrepos(path, followsym=False, seen_dirs=None, recurse=False): | ||||
'''yield every hg repository under path, always recursively. | """yield every hg repository under path, always recursively. | ||||
The recurse flag will only control recursion into repo working dirs''' | The recurse flag will only control recursion into repo working dirs""" | ||||
def errhandler(err): | def errhandler(err): | ||||
if err.filename == path: | if err.filename == path: | ||||
raise err | raise err | ||||
samestat = getattr(os.path, 'samestat', None) | samestat = getattr(os.path, 'samestat', None) | ||||
if followsym and samestat is not None: | if followsym and samestat is not None: | ||||
def increasingwindows(windowsize=8, sizelimit=512): | def increasingwindows(windowsize=8, sizelimit=512): | ||||
while True: | while True: | ||||
yield windowsize | yield windowsize | ||||
if windowsize < sizelimit: | if windowsize < sizelimit: | ||||
windowsize *= 2 | windowsize *= 2 | ||||
def walkchangerevs(repo, revs, makefilematcher, prepare): | def walkchangerevs(repo, revs, makefilematcher, prepare): | ||||
'''Iterate over files and the revs in a "windowed" way. | """Iterate over files and the revs in a "windowed" way. | ||||
Callers most commonly need to iterate backwards over the history | Callers most commonly need to iterate backwards over the history | ||||
in which they are interested. Doing so has awful (quadratic-looking) | in which they are interested. Doing so has awful (quadratic-looking) | ||||
performance, so we use iterators in a "windowed" way. | performance, so we use iterators in a "windowed" way. | ||||
We walk a window of revisions in the desired order. Within the | We walk a window of revisions in the desired order. Within the | ||||
window, we first walk forwards to gather data, then in the desired | window, we first walk forwards to gather data, then in the desired | ||||
order (usually backwards) to display it. | order (usually backwards) to display it. | ||||
This function returns an iterator yielding contexts. Before | This function returns an iterator yielding contexts. Before | ||||
yielding each context, the iterator will first call the prepare | yielding each context, the iterator will first call the prepare | ||||
function on each context in the window in forward order.''' | function on each context in the window in forward order.""" | ||||
if not revs: | if not revs: | ||||
return [] | return [] | ||||
change = repo.__getitem__ | change = repo.__getitem__ | ||||
def iterate(): | def iterate(): | ||||
it = iter(revs) | it = iter(revs) | ||||
stopiteration = False | stopiteration = False | ||||
def subdiruipathfn(subpath, uipathfn): | def subdiruipathfn(subpath, uipathfn): | ||||
'''Create a new uipathfn that treats the file as relative to subpath.''' | '''Create a new uipathfn that treats the file as relative to subpath.''' | ||||
return lambda f: uipathfn(posixpath.join(subpath, f)) | return lambda f: uipathfn(posixpath.join(subpath, f)) | ||||
def anypats(pats, opts): | def anypats(pats, opts): | ||||
'''Checks if any patterns, including --include and --exclude were given. | """Checks if any patterns, including --include and --exclude were given. | ||||
Some commands (e.g. addremove) use this condition for deciding whether to | Some commands (e.g. addremove) use this condition for deciding whether to | ||||
print absolute or relative paths. | print absolute or relative paths. | ||||
''' | """ | ||||
return bool(pats or opts.get(b'include') or opts.get(b'exclude')) | return bool(pats or opts.get(b'include') or opts.get(b'exclude')) | ||||
def expandpats(pats): | def expandpats(pats): | ||||
'''Expand bare globs when running on windows. | """Expand bare globs when running on windows. | ||||
On posix we assume it already has already been done by sh.''' | On posix we assume it already has already been done by sh.""" | ||||
if not util.expandglobs: | if not util.expandglobs: | ||||
return list(pats) | return list(pats) | ||||
ret = [] | ret = [] | ||||
for kindpat in pats: | for kindpat in pats: | ||||
kind, pat = matchmod._patsplit(kindpat, None) | kind, pat = matchmod._patsplit(kindpat, None) | ||||
if kind is None: | if kind is None: | ||||
try: | try: | ||||
globbed = glob.glob(pat) | globbed = glob.glob(pat) | ||||
except re.error: | except re.error: | ||||
globbed = [pat] | globbed = [pat] | ||||
if globbed: | if globbed: | ||||
ret.extend(globbed) | ret.extend(globbed) | ||||
continue | continue | ||||
ret.append(kindpat) | ret.append(kindpat) | ||||
return ret | return ret | ||||
def matchandpats( | def matchandpats( | ||||
ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None | ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None | ||||
): | ): | ||||
'''Return a matcher and the patterns that were used. | """Return a matcher and the patterns that were used. | ||||
The matcher will warn about bad matches, unless an alternate badfn callback | The matcher will warn about bad matches, unless an alternate badfn callback | ||||
is provided.''' | is provided.""" | ||||
if opts is None: | if opts is None: | ||||
opts = {} | opts = {} | ||||
if not globbed and default == b'relpath': | if not globbed and default == b'relpath': | ||||
pats = expandpats(pats or []) | pats = expandpats(pats or []) | ||||
uipathfn = getuipathfn(ctx.repo(), legacyrelativevalue=True) | uipathfn = getuipathfn(ctx.repo(), legacyrelativevalue=True) | ||||
def bad(f, msg): | def bad(f, msg): | ||||
return None if no special directory is configured""" | return None if no special directory is configured""" | ||||
origbackuppath = ui.config(b'ui', b'origbackuppath') | origbackuppath = ui.config(b'ui', b'origbackuppath') | ||||
if not origbackuppath: | if not origbackuppath: | ||||
return None | return None | ||||
return vfs.vfs(repo.wvfs.join(origbackuppath)) | return vfs.vfs(repo.wvfs.join(origbackuppath)) | ||||
def backuppath(ui, repo, filepath): | def backuppath(ui, repo, filepath): | ||||
'''customize where working copy backup files (.orig files) are created | """customize where working copy backup files (.orig files) are created | ||||
Fetch user defined path from config file: [ui] origbackuppath = <path> | Fetch user defined path from config file: [ui] origbackuppath = <path> | ||||
Fall back to default (filepath with .orig suffix) if not specified | Fall back to default (filepath with .orig suffix) if not specified | ||||
filepath is repo-relative | filepath is repo-relative | ||||
Returns an absolute path | Returns an absolute path | ||||
''' | """ | ||||
origvfs = getorigvfs(ui, repo) | origvfs = getorigvfs(ui, repo) | ||||
if origvfs is None: | if origvfs is None: | ||||
return repo.wjoin(filepath + b".orig") | return repo.wjoin(filepath + b".orig") | ||||
origbackupdir = origvfs.dirname(filepath) | origbackupdir = origvfs.dirname(filepath) | ||||
if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir): | if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir): | ||||
ui.note(_(b'creating directory: %s\n') % origvfs.join(origbackupdir)) | ui.note(_(b'creating directory: %s\n') % origvfs.join(origbackupdir)) | ||||
for f in rejected: | for f in rejected: | ||||
if f in m.files(): | if f in m.files(): | ||||
return 1 | return 1 | ||||
return ret | return ret | ||||
def marktouched(repo, files, similarity=0.0): | def marktouched(repo, files, similarity=0.0): | ||||
'''Assert that files have somehow been operated upon. files are relative to | """Assert that files have somehow been operated upon. files are relative to | ||||
the repo root.''' | the repo root.""" | ||||
m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x)) | m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x)) | ||||
rejected = [] | rejected = [] | ||||
added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m) | added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m) | ||||
if repo.ui.verbose: | if repo.ui.verbose: | ||||
unknownset = set(unknown + forgotten) | unknownset = set(unknown + forgotten) | ||||
toprint = unknownset.copy() | toprint = unknownset.copy() | ||||
for f in rejected: | for f in rejected: | ||||
if f in m.files(): | if f in m.files(): | ||||
return 1 | return 1 | ||||
return 0 | return 0 | ||||
def _interestingfiles(repo, matcher): | def _interestingfiles(repo, matcher): | ||||
'''Walk dirstate with matcher, looking for files that addremove would care | """Walk dirstate with matcher, looking for files that addremove would care | ||||
about. | about. | ||||
This is different from dirstate.status because it doesn't care about | This is different from dirstate.status because it doesn't care about | ||||
whether files are modified or clean.''' | whether files are modified or clean.""" | ||||
added, unknown, deleted, removed, forgotten = [], [], [], [], [] | added, unknown, deleted, removed, forgotten = [], [], [], [], [] | ||||
audit_path = pathutil.pathauditor(repo.root, cached=True) | audit_path = pathutil.pathauditor(repo.root, cached=True) | ||||
ctx = repo[None] | ctx = repo[None] | ||||
dirstate = repo.dirstate | dirstate = repo.dirstate | ||||
matcher = repo.narrowmatch(matcher, includeexact=True) | matcher = repo.narrowmatch(matcher, includeexact=True) | ||||
walkresults = dirstate.walk( | walkresults = dirstate.walk( | ||||
matcher, | matcher, | ||||
) | ) | ||||
% (uipathfn(old), uipathfn(new), score * 100) | % (uipathfn(old), uipathfn(new), score * 100) | ||||
) | ) | ||||
renames[new] = old | renames[new] = old | ||||
return renames | return renames | ||||
def _markchanges(repo, unknown, deleted, renames): | def _markchanges(repo, unknown, deleted, renames): | ||||
'''Marks the files in unknown as added, the files in deleted as removed, | """Marks the files in unknown as added, the files in deleted as removed, | ||||
and the files in renames as copied.''' | and the files in renames as copied.""" | ||||
wctx = repo[None] | wctx = repo[None] | ||||
with repo.wlock(): | with repo.wlock(): | ||||
wctx.forget(deleted) | wctx.forget(deleted) | ||||
wctx.add(unknown) | wctx.add(unknown) | ||||
for new, old in pycompat.iteritems(renames): | for new, old in pycompat.iteritems(renames): | ||||
wctx.copy(old, new) | wctx.copy(old, new) | ||||
return getrenamed | return getrenamed | ||||
rcache = {} | rcache = {} | ||||
if endrev is None: | if endrev is None: | ||||
endrev = len(repo) | endrev = len(repo) | ||||
def getrenamed(fn, rev): | def getrenamed(fn, rev): | ||||
'''looks up all renames for a file (up to endrev) the first | """looks up all renames for a file (up to endrev) the first | ||||
time the file is given. It indexes on the changerev and only | time the file is given. It indexes on the changerev and only | ||||
parses the manifest if linkrev != changerev. | parses the manifest if linkrev != changerev. | ||||
Returns rename info for fn at changerev rev.''' | Returns rename info for fn at changerev rev.""" | ||||
if fn not in rcache: | if fn not in rcache: | ||||
rcache[fn] = {} | rcache[fn] = {} | ||||
fl = repo.file(fn) | fl = repo.file(fn) | ||||
for i in fl: | for i in fl: | ||||
lr = fl.linkrev(i) | lr = fl.linkrev(i) | ||||
renamed = fl.renamed(fl.node(i)) | renamed = fl.renamed(fl.node(i)) | ||||
rcache[fn][lr] = renamed and renamed[0] | rcache[fn][lr] = renamed and renamed[0] | ||||
if lr >= endrev: | if lr >= endrev: | ||||
for dst, src in pycompat.iteritems(copies): | for dst, src in pycompat.iteritems(copies): | ||||
if src not in newctx or dst in newctx or ds[dst] != b'a': | if src not in newctx or dst in newctx or ds[dst] != b'a': | ||||
src = None | src = None | ||||
ds.copy(src, dst) | ds.copy(src, dst) | ||||
repo._quick_access_changeid_invalidate() | repo._quick_access_changeid_invalidate() | ||||
def filterrequirements(requirements): | def filterrequirements(requirements): | ||||
""" filters the requirements into two sets: | """filters the requirements into two sets: | ||||
wcreq: requirements which should be written in .hg/requires | wcreq: requirements which should be written in .hg/requires | ||||
storereq: which should be written in .hg/store/requires | storereq: which should be written in .hg/store/requires | ||||
Returns (wcreq, storereq) | Returns (wcreq, storereq) | ||||
""" | """ | ||||
if requirementsmod.SHARESAFE_REQUIREMENT in requirements: | if requirementsmod.SHARESAFE_REQUIREMENT in requirements: | ||||
wc, store = set(), set() | wc, store = set(), set() | ||||
b'%s:%s %d/%d%s (%4.2f%%)\n' | b'%s:%s %d/%d%s (%4.2f%%)\n' | ||||
% (self.topic, item, self.pos, self.total, unit, pct) | % (self.topic, item, self.pos, self.total, unit, pct) | ||||
) | ) | ||||
else: | else: | ||||
self.ui.debug(b'%s:%s %d%s\n' % (self.topic, item, self.pos, unit)) | self.ui.debug(b'%s:%s %d%s\n' % (self.topic, item, self.pos, unit)) | ||||
def gdinitconfig(ui): | def gdinitconfig(ui): | ||||
"""helper function to know if a repo should be created as general delta | """helper function to know if a repo should be created as general delta""" | ||||
""" | |||||
# experimental config: format.generaldelta | # experimental config: format.generaldelta | ||||
return ui.configbool(b'format', b'generaldelta') or ui.configbool( | return ui.configbool(b'format', b'generaldelta') or ui.configbool( | ||||
b'format', b'usegeneraldelta' | b'format', b'usegeneraldelta' | ||||
) | ) | ||||
def gddeltaconfig(ui): | def gddeltaconfig(ui): | ||||
"""helper function to know if incoming delta should be optimised | """helper function to know if incoming delta should be optimised""" | ||||
""" | |||||
# experimental config: format.generaldelta | # experimental config: format.generaldelta | ||||
return ui.configbool(b'format', b'generaldelta') | return ui.configbool(b'format', b'generaldelta') | ||||
class simplekeyvaluefile(object): | class simplekeyvaluefile(object): | ||||
"""A simple file with key=value lines | """A simple file with key=value lines | ||||
Keys must be alphanumerics and start with a letter, values must not | Keys must be alphanumerics and start with a letter, values must not |
local, | local, | ||||
remote, | remote, | ||||
initialsamplesize=100, | initialsamplesize=100, | ||||
fullsamplesize=200, | fullsamplesize=200, | ||||
abortwhenunrelated=True, | abortwhenunrelated=True, | ||||
ancestorsof=None, | ancestorsof=None, | ||||
samplegrowth=1.05, | samplegrowth=1.05, | ||||
): | ): | ||||
'''Return a tuple (common, anyincoming, remoteheads) used to identify | """Return a tuple (common, anyincoming, remoteheads) used to identify | ||||
missing nodes from or in remote. | missing nodes from or in remote. | ||||
''' | """ | ||||
start = util.timer() | start = util.timer() | ||||
roundtrips = 0 | roundtrips = 0 | ||||
cl = local.changelog | cl = local.changelog | ||||
clnode = cl.node | clnode = cl.node | ||||
clrev = cl.rev | clrev = cl.rev | ||||
if ancestorsof is not None: | if ancestorsof is not None: | ||||
# indices between sample and externalized version must match | # indices between sample and externalized version must match | ||||
sample = list(sample) | sample = list(sample) | ||||
else: | else: | ||||
sample = ownheads | sample = ownheads | ||||
with remote.commandexecutor() as e: | with remote.commandexecutor() as e: | ||||
fheads = e.callcommand(b'heads', {}) | fheads = e.callcommand(b'heads', {}) | ||||
fknown = e.callcommand( | fknown = e.callcommand( | ||||
b'known', {b'nodes': [clnode(r) for r in sample],} | b'known', | ||||
{ | |||||
b'nodes': [clnode(r) for r in sample], | |||||
}, | |||||
) | ) | ||||
srvheadhashes, yesno = fheads.result(), fknown.result() | srvheadhashes, yesno = fheads.result(), fknown.result() | ||||
if cl.tip() == nullid: | if cl.tip() == nullid: | ||||
if srvheadhashes != [nullid]: | if srvheadhashes != [nullid]: | ||||
return [nullid], True, srvheadhashes | return [nullid], True, srvheadhashes | ||||
return [nullid], False, [] | return [nullid], False, [] | ||||
% (roundtrips, stats['undecided'], len(sample)) | % (roundtrips, stats['undecided'], len(sample)) | ||||
) | ) | ||||
# indices between sample and externalized version must match | # indices between sample and externalized version must match | ||||
sample = list(sample) | sample = list(sample) | ||||
with remote.commandexecutor() as e: | with remote.commandexecutor() as e: | ||||
yesno = e.callcommand( | yesno = e.callcommand( | ||||
b'known', {b'nodes': [clnode(r) for r in sample],} | b'known', | ||||
{ | |||||
b'nodes': [clnode(r) for r in sample], | |||||
}, | |||||
).result() | ).result() | ||||
full = True | full = True | ||||
disco.addinfo(zip(sample, yesno)) | disco.addinfo(zip(sample, yesno)) | ||||
result = disco.commonheads() | result = disco.commonheads() | ||||
elapsed = util.timer() - start | elapsed = util.timer() - start |
def _restoreactivebookmark(repo, mark): | def _restoreactivebookmark(repo, mark): | ||||
if mark: | if mark: | ||||
bookmarks.activate(repo, mark) | bookmarks.activate(repo, mark) | ||||
def _aborttransaction(repo, tr): | def _aborttransaction(repo, tr): | ||||
'''Abort current transaction for shelve/unshelve, but keep dirstate | """Abort current transaction for shelve/unshelve, but keep dirstate""" | ||||
''' | |||||
dirstatebackupname = b'dirstate.shelve' | dirstatebackupname = b'dirstate.shelve' | ||||
repo.dirstate.savebackup(tr, dirstatebackupname) | repo.dirstate.savebackup(tr, dirstatebackupname) | ||||
tr.abort() | tr.abort() | ||||
repo.dirstate.restorebackup(None, dirstatebackupname) | repo.dirstate.restorebackup(None, dirstatebackupname) | ||||
def getshelvename(repo, parent, opts): | def getshelvename(repo, parent, opts): | ||||
"""Decide on the name this shelve is going to have""" | """Decide on the name this shelve is going to have""" |
from .i18n import _ | from .i18n import _ | ||||
from . import ( | from . import ( | ||||
mdiff, | mdiff, | ||||
pycompat, | pycompat, | ||||
) | ) | ||||
def _findexactmatches(repo, added, removed): | def _findexactmatches(repo, added, removed): | ||||
'''find renamed files that have no changes | """find renamed files that have no changes | ||||
Takes a list of new filectxs and a list of removed filectxs, and yields | Takes a list of new filectxs and a list of removed filectxs, and yields | ||||
(before, after) tuples of exact matches. | (before, after) tuples of exact matches. | ||||
''' | """ | ||||
# Build table of removed files: {hash(fctx.data()): [fctx, ...]}. | # Build table of removed files: {hash(fctx.data()): [fctx, ...]}. | ||||
# We use hash() to discard fctx.data() from memory. | # We use hash() to discard fctx.data() from memory. | ||||
hashes = {} | hashes = {} | ||||
progress = repo.ui.makeprogress( | progress = repo.ui.makeprogress( | ||||
_(b'searching for exact renames'), | _(b'searching for exact renames'), | ||||
total=(len(added) + len(removed)), | total=(len(added) + len(removed)), | ||||
unit=_(b'files'), | unit=_(b'files'), | ||||
) | ) | ||||
return equal * 2.0 / lengths | return equal * 2.0 / lengths | ||||
def score(fctx1, fctx2): | def score(fctx1, fctx2): | ||||
return _score(fctx1, _ctxdata(fctx2)) | return _score(fctx1, _ctxdata(fctx2)) | ||||
def _findsimilarmatches(repo, added, removed, threshold): | def _findsimilarmatches(repo, added, removed, threshold): | ||||
'''find potentially renamed files based on similar file content | """find potentially renamed files based on similar file content | ||||
Takes a list of new filectxs and a list of removed filectxs, and yields | Takes a list of new filectxs and a list of removed filectxs, and yields | ||||
(before, after, score) tuples of partial matches. | (before, after, score) tuples of partial matches. | ||||
''' | """ | ||||
copies = {} | copies = {} | ||||
progress = repo.ui.makeprogress( | progress = repo.ui.makeprogress( | ||||
_(b'searching for similar files'), unit=_(b'files'), total=len(removed) | _(b'searching for similar files'), unit=_(b'files'), total=len(removed) | ||||
) | ) | ||||
for r in removed: | for r in removed: | ||||
progress.increment() | progress.increment() | ||||
data = None | data = None | ||||
for a in added: | for a in added: |
sb = min(ra[1], rb[1]) | sb = min(ra[1], rb[1]) | ||||
if sa < sb: | if sa < sb: | ||||
return sa, sb | return sa, sb | ||||
else: | else: | ||||
return None | return None | ||||
def compare_range(a, astart, aend, b, bstart, bend): | def compare_range(a, astart, aend, b, bstart, bend): | ||||
"""Compare a[astart:aend] == b[bstart:bend], without slicing. | """Compare a[astart:aend] == b[bstart:bend], without slicing.""" | ||||
""" | |||||
if (aend - astart) != (bend - bstart): | if (aend - astart) != (bend - bstart): | ||||
return False | return False | ||||
for ia, ib in zip( | for ia, ib in zip( | ||||
pycompat.xrange(astart, aend), pycompat.xrange(bstart, bend) | pycompat.xrange(astart, aend), pycompat.xrange(bstart, bend) | ||||
): | ): | ||||
if a[ia] != b[ib]: | if a[ia] != b[ib]: | ||||
return False | return False | ||||
else: | else: | ||||
name_base=None, | name_base=None, | ||||
start_marker=b'<<<<<<<', | start_marker=b'<<<<<<<', | ||||
mid_marker=b'=======', | mid_marker=b'=======', | ||||
end_marker=b'>>>>>>>', | end_marker=b'>>>>>>>', | ||||
base_marker=None, | base_marker=None, | ||||
localorother=None, | localorother=None, | ||||
minimize=False, | minimize=False, | ||||
): | ): | ||||
"""Return merge in cvs-like form. | """Return merge in cvs-like form.""" | ||||
""" | |||||
self.conflicts = False | self.conflicts = False | ||||
newline = b'\n' | newline = b'\n' | ||||
if len(self.a) > 0: | if len(self.a) > 0: | ||||
if self.a[0].endswith(b'\r\n'): | if self.a[0].endswith(b'\r\n'): | ||||
newline = b'\r\n' | newline = b'\r\n' | ||||
elif self.a[0].endswith(b'\r'): | elif self.a[0].endswith(b'\r'): | ||||
newline = b'\r' | newline = b'\r' | ||||
if name_a and start_marker: | if name_a and start_marker: |
# closed prematurely. | # closed prematurely. | ||||
_forwardoutput(self._ui, self._side) | _forwardoutput(self._ui, self._side) | ||||
return r | return r | ||||
def readline(self): | def readline(self): | ||||
return self._call(b'readline') | return self._call(b'readline') | ||||
def _call(self, methname, data=None): | def _call(self, methname, data=None): | ||||
"""call <methname> on "main", forward output of "side" while blocking | """call <methname> on "main", forward output of "side" while blocking""" | ||||
""" | |||||
# data can be '' or 0 | # data can be '' or 0 | ||||
if (data is not None and not data) or self._main.closed: | if (data is not None and not data) or self._main.closed: | ||||
_forwardoutput(self._ui, self._side) | _forwardoutput(self._ui, self._side) | ||||
return b'' | return b'' | ||||
while True: | while True: | ||||
mainready, sideready = self._wait() | mainready, sideready = self._wait() | ||||
if sideready: | if sideready: | ||||
_forwardoutput(self._ui, self._side) | _forwardoutput(self._ui, self._side) |
s[b'verifymode'] = ssl.CERT_NONE | s[b'verifymode'] = ssl.CERT_NONE | ||||
assert s[b'verifymode'] is not None | assert s[b'verifymode'] is not None | ||||
return s | return s | ||||
def commonssloptions(minimumprotocol): | def commonssloptions(minimumprotocol): | ||||
"""Return SSLContext options common to servers and clients. | """Return SSLContext options common to servers and clients.""" | ||||
""" | |||||
if minimumprotocol not in configprotocols: | if minimumprotocol not in configprotocols: | ||||
raise ValueError(b'protocol value not supported: %s' % minimumprotocol) | raise ValueError(b'protocol value not supported: %s' % minimumprotocol) | ||||
# SSLv2 and SSLv3 are broken. We ban them outright. | # SSLv2 and SSLv3 are broken. We ban them outright. | ||||
options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ||||
if minimumprotocol == b'tls1.0': | if minimumprotocol == b'tls1.0': | ||||
# Defaults above are to use TLS 1.0+ | # Defaults above are to use TLS 1.0+ | ||||
for frag in remainder: | for frag in remainder: | ||||
pats.append(stringutil.reescape(frag)) | pats.append(stringutil.reescape(frag)) | ||||
pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE) | pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE) | ||||
return pat.match(hostname) is not None | return pat.match(hostname) is not None | ||||
def _verifycert(cert, hostname): | def _verifycert(cert, hostname): | ||||
'''Verify that cert (in socket.getpeercert() format) matches hostname. | """Verify that cert (in socket.getpeercert() format) matches hostname. | ||||
CRLs is not handled. | CRLs is not handled. | ||||
Returns error message if any problems are found and None on success. | Returns error message if any problems are found and None on success. | ||||
''' | """ | ||||
if not cert: | if not cert: | ||||
return _(b'no certificate received') | return _(b'no certificate received') | ||||
dnsnames = [] | dnsnames = [] | ||||
san = cert.get('subjectAltName', []) | san = cert.get('subjectAltName', []) | ||||
for key, value in san: | for key, value in san: | ||||
if key == 'DNS': | if key == 'DNS': | ||||
try: | try: |
The class object can write all the data to a file in .hg/ directory and | The class object can write all the data to a file in .hg/ directory and | ||||
can populate the object data reading that file. | can populate the object data reading that file. | ||||
Uses cbor to serialize and deserialize data while writing and reading from | Uses cbor to serialize and deserialize data while writing and reading from | ||||
disk. | disk. | ||||
""" | """ | ||||
def __init__(self, repo, fname): | def __init__(self, repo, fname): | ||||
""" repo is the repo object | """repo is the repo object | ||||
fname is the file name in which data should be stored in .hg directory | fname is the file name in which data should be stored in .hg directory | ||||
""" | """ | ||||
self._repo = repo | self._repo = repo | ||||
self.fname = fname | self.fname = fname | ||||
def read(self): | def read(self): | ||||
# type: () -> Dict[bytes, Any] | # type: () -> Dict[bytes, Any] | ||||
"""read the existing state file and return a dict of data stored""" | """read the existing state file and return a dict of data stored""" | ||||
def exists(self): | def exists(self): | ||||
"""check whether the state file exists or not""" | """check whether the state file exists or not""" | ||||
return self._repo.vfs.exists(self.fname) | return self._repo.vfs.exists(self.fname) | ||||
class _statecheck(object): | class _statecheck(object): | ||||
"""a utility class that deals with multistep operations like graft, | """a utility class that deals with multistep operations like graft, | ||||
histedit, bisect, update etc and check whether such commands | histedit, bisect, update etc and check whether such commands | ||||
are in an unfinished conditition or not and return appropriate message | are in an unfinished conditition or not and return appropriate message | ||||
and hint. | and hint. | ||||
It also has the ability to register and determine the states of any new | It also has the ability to register and determine the states of any new | ||||
multistep operation or multistep command extension. | multistep operation or multistep command extension. | ||||
""" | """ | ||||
def __init__( | def __init__( | ||||
self, | self, | ||||
opname, | opname, | ||||
fname, | fname, | ||||
clearable, | clearable, | ||||
allowcommit, | allowcommit, | ||||
return _(b"use 'hg %s --continue' or 'hg %s --abort'") % ( | return _(b"use 'hg %s --continue' or 'hg %s --abort'") % ( | ||||
self._opname, | self._opname, | ||||
self._opname, | self._opname, | ||||
) | ) | ||||
else: | else: | ||||
return _( | return _( | ||||
b"use 'hg %s --continue', 'hg %s --abort', " | b"use 'hg %s --continue', 'hg %s --abort', " | ||||
b"or 'hg %s --stop'" | b"or 'hg %s --stop'" | ||||
) % (self._opname, self._opname, self._opname,) | ) % ( | ||||
self._opname, | |||||
self._opname, | |||||
self._opname, | |||||
) | |||||
return self._cmdhint | return self._cmdhint | ||||
def msg(self): | def msg(self): | ||||
"""returns the status message corresponding to the command""" | """returns the status message corresponding to the command""" | ||||
if not self._cmdmsg: | if not self._cmdmsg: | ||||
return _(b'%s in progress') % (self._opname) | return _(b'%s in progress') % (self._opname) | ||||
return self._cmdmsg | return self._cmdmsg |
sites.append( | sites.append( | ||||
CodeSite.get(siteparts[0], int(siteparts[1]), siteparts[2]) | CodeSite.get(siteparts[0], int(siteparts[1]), siteparts[2]) | ||||
) | ) | ||||
state.samples.append(Sample(sites, time)) | state.samples.append(Sample(sites, time)) | ||||
def reset(frequency=None): | def reset(frequency=None): | ||||
'''Clear out the state of the profiler. Do not call while the | """Clear out the state of the profiler. Do not call while the | ||||
profiler is running. | profiler is running. | ||||
The optional frequency argument specifies the number of samples to | The optional frequency argument specifies the number of samples to | ||||
collect per second.''' | collect per second.""" | ||||
assert state.profile_level == 0, b"Can't reset() while statprof is running" | assert state.profile_level == 0, b"Can't reset() while statprof is running" | ||||
CodeSite.cache.clear() | CodeSite.cache.clear() | ||||
state.reset(frequency) | state.reset(frequency) | ||||
@contextmanager | @contextmanager | ||||
def profile(): | def profile(): | ||||
start() | start() | ||||
if format not in (DisplayFormats.Json, DisplayFormats.Chrome): | if format not in (DisplayFormats.Json, DisplayFormats.Chrome): | ||||
fp.write(b'---\n') | fp.write(b'---\n') | ||||
fp.write(b'Sample count: %d\n' % len(data.samples)) | fp.write(b'Sample count: %d\n' % len(data.samples)) | ||||
fp.write(b'Total time: %f seconds (%f wall)\n' % data.accumulated_time) | fp.write(b'Total time: %f seconds (%f wall)\n' % data.accumulated_time) | ||||
def display_by_line(data, fp): | def display_by_line(data, fp): | ||||
'''Print the profiler data with each sample line represented | """Print the profiler data with each sample line represented | ||||
as one row in a table. Sorted by self-time per line.''' | as one row in a table. Sorted by self-time per line.""" | ||||
stats = SiteStats.buildstats(data.samples) | stats = SiteStats.buildstats(data.samples) | ||||
stats.sort(reverse=True, key=lambda x: x.selfseconds()) | stats.sort(reverse=True, key=lambda x: x.selfseconds()) | ||||
fp.write( | fp.write( | ||||
b'%5.5s %10.10s %7.7s %-8.8s\n' | b'%5.5s %10.10s %7.7s %-8.8s\n' | ||||
% (b'% ', b'cumulative', b'self', b'') | % (b'% ', b'cumulative', b'self', b'') | ||||
) | ) | ||||
fp.write( | fp.write( | ||||
stat.totalseconds(), | stat.totalseconds(), | ||||
stat.selfseconds(), | stat.selfseconds(), | ||||
sitelabel, | sitelabel, | ||||
) | ) | ||||
) | ) | ||||
def display_by_method(data, fp): | def display_by_method(data, fp): | ||||
'''Print the profiler data with each sample function represented | """Print the profiler data with each sample function represented | ||||
as one row in a table. Important lines within that function are | as one row in a table. Important lines within that function are | ||||
output as nested rows. Sorted by self-time per line.''' | output as nested rows. Sorted by self-time per line.""" | ||||
fp.write( | fp.write( | ||||
b'%5.5s %10.10s %7.7s %-8.8s\n' | b'%5.5s %10.10s %7.7s %-8.8s\n' | ||||
% (b'% ', b'cumulative', b'self', b'') | % (b'% ', b'cumulative', b'self', b'') | ||||
) | ) | ||||
fp.write( | fp.write( | ||||
b'%5.5s %9.9s %8.8s %-8.8s\n' | b'%5.5s %9.9s %8.8s %-8.8s\n' | ||||
% (b"time", b"seconds", b"seconds", b"name") | % (b"time", b"seconds", b"seconds", b"name") | ||||
) | ) | ||||
os.system(b"perl ~/flamegraph.pl %s > %s" % (path, outputfile)) | os.system(b"perl ~/flamegraph.pl %s > %s" % (path, outputfile)) | ||||
fp.write(b'Written to %s\n' % outputfile) | fp.write(b'Written to %s\n' % outputfile) | ||||
_pathcache = {} | _pathcache = {} | ||||
def simplifypath(path): | def simplifypath(path): | ||||
'''Attempt to make the path to a Python module easier to read by | """Attempt to make the path to a Python module easier to read by | ||||
removing whatever part of the Python search path it was found | removing whatever part of the Python search path it was found | ||||
on.''' | on.""" | ||||
if path in _pathcache: | if path in _pathcache: | ||||
return _pathcache[path] | return _pathcache[path] | ||||
hgpath = encoding.__file__.rsplit(os.sep, 2)[0] | hgpath = encoding.__file__.rsplit(os.sep, 2)[0] | ||||
for p in [hgpath] + sys.path: | for p in [hgpath] + sys.path: | ||||
prefix = p + os.sep | prefix = p + os.sep | ||||
if path.startswith(prefix): | if path.startswith(prefix): | ||||
path = path[len(prefix) :] | path = path[len(prefix) :] |
return matcher.visitdir(path[len(b'meta/') : -len(b'/00manifest.i')]) | return matcher.visitdir(path[len(b'meta/') : -len(b'/00manifest.i')]) | ||||
raise error.ProgrammingError(b"cannot decode path %s" % path) | raise error.ProgrammingError(b"cannot decode path %s" % path) | ||||
# This avoids a collision between a file named foo and a dir named | # This avoids a collision between a file named foo and a dir named | ||||
# foo.i or foo.d | # foo.i or foo.d | ||||
def _encodedir(path): | def _encodedir(path): | ||||
''' | """ | ||||
>>> _encodedir(b'data/foo.i') | >>> _encodedir(b'data/foo.i') | ||||
'data/foo.i' | 'data/foo.i' | ||||
>>> _encodedir(b'data/foo.i/bla.i') | >>> _encodedir(b'data/foo.i/bla.i') | ||||
'data/foo.i.hg/bla.i' | 'data/foo.i.hg/bla.i' | ||||
>>> _encodedir(b'data/foo.i.hg/bla.i') | >>> _encodedir(b'data/foo.i.hg/bla.i') | ||||
'data/foo.i.hg.hg/bla.i' | 'data/foo.i.hg.hg/bla.i' | ||||
>>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n') | >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n') | ||||
'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n' | 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n' | ||||
''' | """ | ||||
return ( | return ( | ||||
path.replace(b".hg/", b".hg.hg/") | path.replace(b".hg/", b".hg.hg/") | ||||
.replace(b".i/", b".i.hg/") | .replace(b".i/", b".i.hg/") | ||||
.replace(b".d/", b".d.hg/") | .replace(b".d/", b".d.hg/") | ||||
) | ) | ||||
encodedir = getattr(parsers, 'encodedir', _encodedir) | encodedir = getattr(parsers, 'encodedir', _encodedir) | ||||
def decodedir(path): | def decodedir(path): | ||||
''' | """ | ||||
>>> decodedir(b'data/foo.i') | >>> decodedir(b'data/foo.i') | ||||
'data/foo.i' | 'data/foo.i' | ||||
>>> decodedir(b'data/foo.i.hg/bla.i') | >>> decodedir(b'data/foo.i.hg/bla.i') | ||||
'data/foo.i/bla.i' | 'data/foo.i/bla.i' | ||||
>>> decodedir(b'data/foo.i.hg.hg/bla.i') | >>> decodedir(b'data/foo.i.hg.hg/bla.i') | ||||
'data/foo.i.hg/bla.i' | 'data/foo.i.hg/bla.i' | ||||
''' | """ | ||||
if b".hg/" not in path: | if b".hg/" not in path: | ||||
return path | return path | ||||
return ( | return ( | ||||
path.replace(b".d.hg/", b".d/") | path.replace(b".d.hg/", b".d/") | ||||
.replace(b".i.hg/", b".i/") | .replace(b".i.hg/", b".i/") | ||||
.replace(b".hg.hg/", b".hg/") | .replace(b".hg.hg/", b".hg/") | ||||
) | ) | ||||
def _reserved(): | def _reserved(): | ||||
''' characters that are problematic for filesystems | """characters that are problematic for filesystems | ||||
* ascii escapes (0..31) | * ascii escapes (0..31) | ||||
* ascii hi (126..255) | * ascii hi (126..255) | ||||
* windows specials | * windows specials | ||||
these characters will be escaped by encodefunctions | these characters will be escaped by encodefunctions | ||||
''' | """ | ||||
winreserved = [ord(x) for x in u'\\:*?"<>|'] | winreserved = [ord(x) for x in u'\\:*?"<>|'] | ||||
for x in range(32): | for x in range(32): | ||||
yield x | yield x | ||||
for x in range(126, 256): | for x in range(126, 256): | ||||
yield x | yield x | ||||
for x in winreserved: | for x in winreserved: | ||||
yield x | yield x | ||||
def _buildencodefun(): | def _buildencodefun(): | ||||
''' | """ | ||||
>>> enc, dec = _buildencodefun() | >>> enc, dec = _buildencodefun() | ||||
>>> enc(b'nothing/special.txt') | >>> enc(b'nothing/special.txt') | ||||
'nothing/special.txt' | 'nothing/special.txt' | ||||
>>> dec(b'nothing/special.txt') | >>> dec(b'nothing/special.txt') | ||||
'nothing/special.txt' | 'nothing/special.txt' | ||||
>>> enc(b'HELLO') | >>> enc(b'HELLO') | ||||
'_h_e_l_l_o' | '_h_e_l_l_o' | ||||
>>> dec(b'_h_e_l_l_o') | >>> dec(b'_h_e_l_l_o') | ||||
'HELLO' | 'HELLO' | ||||
>>> enc(b'hello:world?') | >>> enc(b'hello:world?') | ||||
'hello~3aworld~3f' | 'hello~3aworld~3f' | ||||
>>> dec(b'hello~3aworld~3f') | >>> dec(b'hello~3aworld~3f') | ||||
'hello:world?' | 'hello:world?' | ||||
>>> enc(b'the\\x07quick\\xADshot') | >>> enc(b'the\\x07quick\\xADshot') | ||||
'the~07quick~adshot' | 'the~07quick~adshot' | ||||
>>> dec(b'the~07quick~adshot') | >>> dec(b'the~07quick~adshot') | ||||
'the\\x07quick\\xadshot' | 'the\\x07quick\\xadshot' | ||||
''' | """ | ||||
e = b'_' | e = b'_' | ||||
xchr = pycompat.bytechr | xchr = pycompat.bytechr | ||||
asciistr = list(map(xchr, range(127))) | asciistr = list(map(xchr, range(127))) | ||||
capitals = list(range(ord(b"A"), ord(b"Z") + 1)) | capitals = list(range(ord(b"A"), ord(b"Z") + 1)) | ||||
cmap = {x: x for x in asciistr} | cmap = {x: x for x in asciistr} | ||||
for x in _reserved(): | for x in _reserved(): | ||||
cmap[xchr(x)] = b"~%02x" % x | cmap[xchr(x)] = b"~%02x" % x | ||||
lambda s: b''.join(list(decode(s))), | lambda s: b''.join(list(decode(s))), | ||||
) | ) | ||||
_encodefname, _decodefname = _buildencodefun() | _encodefname, _decodefname = _buildencodefun() | ||||
def encodefilename(s): | def encodefilename(s): | ||||
''' | """ | ||||
>>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO') | >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO') | ||||
'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o' | 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o' | ||||
''' | """ | ||||
return _encodefname(encodedir(s)) | return _encodefname(encodedir(s)) | ||||
def decodefilename(s): | def decodefilename(s): | ||||
''' | """ | ||||
>>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o') | >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o') | ||||
'foo.i/bar.d/bla.hg/hi:world?/HELLO' | 'foo.i/bar.d/bla.hg/hi:world?/HELLO' | ||||
''' | """ | ||||
return decodedir(_decodefname(s)) | return decodedir(_decodefname(s)) | ||||
def _buildlowerencodefun(): | def _buildlowerencodefun(): | ||||
''' | """ | ||||
>>> f = _buildlowerencodefun() | >>> f = _buildlowerencodefun() | ||||
>>> f(b'nothing/special.txt') | >>> f(b'nothing/special.txt') | ||||
'nothing/special.txt' | 'nothing/special.txt' | ||||
>>> f(b'HELLO') | >>> f(b'HELLO') | ||||
'hello' | 'hello' | ||||
>>> f(b'hello:world?') | >>> f(b'hello:world?') | ||||
'hello~3aworld~3f' | 'hello~3aworld~3f' | ||||
>>> f(b'the\\x07quick\\xADshot') | >>> f(b'the\\x07quick\\xADshot') | ||||
'the~07quick~adshot' | 'the~07quick~adshot' | ||||
''' | """ | ||||
xchr = pycompat.bytechr | xchr = pycompat.bytechr | ||||
cmap = {xchr(x): xchr(x) for x in pycompat.xrange(127)} | cmap = {xchr(x): xchr(x) for x in pycompat.xrange(127)} | ||||
for x in _reserved(): | for x in _reserved(): | ||||
cmap[xchr(x)] = b"~%02x" % x | cmap[xchr(x)] = b"~%02x" % x | ||||
for x in range(ord(b"A"), ord(b"Z") + 1): | for x in range(ord(b"A"), ord(b"Z") + 1): | ||||
cmap[xchr(x)] = xchr(x).lower() | cmap[xchr(x)] = xchr(x).lower() | ||||
def lowerencode(s): | def lowerencode(s): | ||||
return b"".join([cmap[c] for c in pycompat.iterbytestr(s)]) | return b"".join([cmap[c] for c in pycompat.iterbytestr(s)]) | ||||
return lowerencode | return lowerencode | ||||
lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun() | lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun() | ||||
# Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9 | # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9 | ||||
_winres3 = (b'aux', b'con', b'prn', b'nul') # length 3 | _winres3 = (b'aux', b'con', b'prn', b'nul') # length 3 | ||||
_winres4 = (b'com', b'lpt') # length 4 (with trailing 1..9) | _winres4 = (b'com', b'lpt') # length 4 (with trailing 1..9) | ||||
def _auxencode(path, dotencode): | def _auxencode(path, dotencode): | ||||
''' | """ | ||||
Encodes filenames containing names reserved by Windows or which end in | Encodes filenames containing names reserved by Windows or which end in | ||||
period or space. Does not touch other single reserved characters c. | period or space. Does not touch other single reserved characters c. | ||||
Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here. | Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here. | ||||
Additionally encodes space or period at the beginning, if dotencode is | Additionally encodes space or period at the beginning, if dotencode is | ||||
True. Parameter path is assumed to be all lowercase. | True. Parameter path is assumed to be all lowercase. | ||||
A segment only needs encoding if a reserved name appears as a | A segment only needs encoding if a reserved name appears as a | ||||
basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux" | basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux" | ||||
doesn't need encoding. | doesn't need encoding. | ||||
>>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.' | >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.' | ||||
>>> _auxencode(s.split(b'/'), True) | >>> _auxencode(s.split(b'/'), True) | ||||
['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e'] | ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e'] | ||||
>>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.' | >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.' | ||||
>>> _auxencode(s.split(b'/'), False) | >>> _auxencode(s.split(b'/'), False) | ||||
['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e'] | ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e'] | ||||
>>> _auxencode([b'foo. '], True) | >>> _auxencode([b'foo. '], True) | ||||
['foo.~20'] | ['foo.~20'] | ||||
>>> _auxencode([b' .foo'], True) | >>> _auxencode([b' .foo'], True) | ||||
['~20.foo'] | ['~20.foo'] | ||||
''' | """ | ||||
for i, n in enumerate(path): | for i, n in enumerate(path): | ||||
if not n: | if not n: | ||||
continue | continue | ||||
if dotencode and n[0] in b'. ': | if dotencode and n[0] in b'. ': | ||||
n = b"~%02x" % ord(n[0:1]) + n[1:] | n = b"~%02x" % ord(n[0:1]) + n[1:] | ||||
path[i] = n | path[i] = n | ||||
else: | else: | ||||
l = n.find(b'.') | l = n.find(b'.') | ||||
spaceleft = _maxstorepathlen - len(res) | spaceleft = _maxstorepathlen - len(res) | ||||
if spaceleft > 0: | if spaceleft > 0: | ||||
filler = basename[:spaceleft] | filler = basename[:spaceleft] | ||||
res = b'dh/' + dirs + filler + digest + ext | res = b'dh/' + dirs + filler + digest + ext | ||||
return res | return res | ||||
def _hybridencode(path, dotencode): | def _hybridencode(path, dotencode): | ||||
'''encodes path with a length limit | """encodes path with a length limit | ||||
Encodes all paths that begin with 'data/', according to the following. | Encodes all paths that begin with 'data/', according to the following. | ||||
Default encoding (reversible): | Default encoding (reversible): | ||||
Encodes all uppercase letters 'X' as '_x'. All reserved or illegal | Encodes all uppercase letters 'X' as '_x'. All reserved or illegal | ||||
characters are encoded as '~xx', where xx is the two digit hex code | characters are encoded as '~xx', where xx is the two digit hex code | ||||
of the character (see encodefilename). | of the character (see encodefilename). | ||||
(the basename is everything after the last path separator). The filler | (the basename is everything after the last path separator). The filler | ||||
is as long as possible, filling in characters from the basename until | is as long as possible, filling in characters from the basename until | ||||
the encoded path has _maxstorepathlen characters (or all chars of the | the encoded path has _maxstorepathlen characters (or all chars of the | ||||
basename have been taken). | basename have been taken). | ||||
The extension (e.g. '.i' or '.d') is preserved. | The extension (e.g. '.i' or '.d') is preserved. | ||||
The string 'data/' at the beginning is replaced with 'dh/', if the hashed | The string 'data/' at the beginning is replaced with 'dh/', if the hashed | ||||
encoding was used. | encoding was used. | ||||
''' | """ | ||||
path = encodedir(path) | path = encodedir(path) | ||||
ef = _encodefname(path).split(b'/') | ef = _encodefname(path).split(b'/') | ||||
res = b'/'.join(_auxencode(ef, dotencode)) | res = b'/'.join(_auxencode(ef, dotencode)) | ||||
if len(res) > _maxstorepathlen: | if len(res) > _maxstorepathlen: | ||||
res = _hashencode(path, dotencode) | res = _hashencode(path, dotencode) | ||||
return res | return res | ||||
def datafiles(self, matcher=None): | def datafiles(self, matcher=None): | ||||
return self._walk(b'data', True) + self._walk(b'meta', True) | return self._walk(b'data', True) + self._walk(b'meta', True) | ||||
def topfiles(self): | def topfiles(self): | ||||
# yield manifest before changelog | # yield manifest before changelog | ||||
return reversed(self._walk(b'', False)) | return reversed(self._walk(b'', False)) | ||||
def walk(self, matcher=None): | def walk(self, matcher=None): | ||||
'''yields (unencoded, encoded, size) | """yields (unencoded, encoded, size) | ||||
if a matcher is passed, storage files of only those tracked paths | if a matcher is passed, storage files of only those tracked paths | ||||
are passed with matches the matcher | are passed with matches the matcher | ||||
''' | """ | ||||
# yield data files first | # yield data files first | ||||
for x in self.datafiles(matcher): | for x in self.datafiles(matcher): | ||||
yield x | yield x | ||||
for x in self.topfiles(): | for x in self.topfiles(): | ||||
yield x | yield x | ||||
def copylist(self): | def copylist(self): | ||||
return _data | return _data | ||||
def __init__(self, vfs): | def __init__(self, vfs): | ||||
self.vfs = vfs | self.vfs = vfs | ||||
self.entries = None | self.entries = None | ||||
self._dirty = False | self._dirty = False | ||||
# set of new additions to fncache | # set of new additions to fncache | ||||
self.addls = set() | self.addls = set() | ||||
def ensureloaded(self, warn=None): | def ensureloaded(self, warn=None): | ||||
'''read the fncache file if not already read. | """read the fncache file if not already read. | ||||
If the file on disk is corrupted, raise. If warn is provided, | If the file on disk is corrupted, raise. If warn is provided, | ||||
warn and keep going instead.''' | warn and keep going instead.""" | ||||
if self.entries is None: | if self.entries is None: | ||||
self._load(warn) | self._load(warn) | ||||
def _load(self, warn=None): | def _load(self, warn=None): | ||||
'''fill the entries from the fncache file''' | '''fill the entries from the fncache file''' | ||||
self._dirty = False | self._dirty = False | ||||
try: | try: | ||||
fp = self.vfs(b'fncache', mode=b'rb') | fp = self.vfs(b'fncache', mode=b'rb') |
b'force', | b'force', | ||||
None, | None, | ||||
_( | _( | ||||
b'force removal of changesets, discard ' | b'force removal of changesets, discard ' | ||||
b'uncommitted changes (no backup)' | b'uncommitted changes (no backup)' | ||||
), | ), | ||||
), | ), | ||||
(b'', b'no-backup', None, _(b'do not save backup bundle')), | (b'', b'no-backup', None, _(b'do not save backup bundle')), | ||||
(b'', b'nobackup', None, _(b'do not save backup bundle (DEPRECATED)'),), | ( | ||||
b'', | |||||
b'nobackup', | |||||
None, | |||||
_(b'do not save backup bundle (DEPRECATED)'), | |||||
), | |||||
(b'n', b'', None, _(b'ignored (DEPRECATED)')), | (b'n', b'', None, _(b'ignored (DEPRECATED)')), | ||||
( | ( | ||||
b'k', | b'k', | ||||
b'keep', | b'keep', | ||||
None, | None, | ||||
_(b"do not modify working directory during strip"), | _(b"do not modify working directory during strip"), | ||||
), | ), | ||||
( | ( |
hg = None | hg = None | ||||
reporelpath = subrepoutil.reporelpath | reporelpath = subrepoutil.reporelpath | ||||
subrelpath = subrepoutil.subrelpath | subrelpath = subrepoutil.subrelpath | ||||
_abssource = subrepoutil._abssource | _abssource = subrepoutil._abssource | ||||
propertycache = util.propertycache | propertycache = util.propertycache | ||||
def _expandedabspath(path): | def _expandedabspath(path): | ||||
''' | """ | ||||
get a path or url and if it is a path expand it and return an absolute path | get a path or url and if it is a path expand it and return an absolute path | ||||
''' | """ | ||||
expandedpath = util.urllocalpath(util.expandpath(path)) | expandedpath = util.urllocalpath(util.expandpath(path)) | ||||
u = util.url(expandedpath) | u = util.url(expandedpath) | ||||
if not u.scheme: | if not u.scheme: | ||||
path = util.normpath(os.path.abspath(u.path)) | path = util.normpath(os.path.abspath(u.path)) | ||||
return path | return path | ||||
def _getstorehashcachename(remotepath): | def _getstorehashcachename(remotepath): | ||||
This returns None, otherwise. | This returns None, otherwise. | ||||
""" | """ | ||||
if self.dirty(ignoreupdate=ignoreupdate, missing=missing): | if self.dirty(ignoreupdate=ignoreupdate, missing=missing): | ||||
return _(b'uncommitted changes in subrepository "%s"') % subrelpath( | return _(b'uncommitted changes in subrepository "%s"') % subrelpath( | ||||
self | self | ||||
) | ) | ||||
def bailifchanged(self, ignoreupdate=False, hint=None): | def bailifchanged(self, ignoreupdate=False, hint=None): | ||||
"""raise Abort if subrepository is ``dirty()`` | """raise Abort if subrepository is ``dirty()``""" | ||||
""" | |||||
dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate, missing=True) | dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate, missing=True) | ||||
if dirtyreason: | if dirtyreason: | ||||
raise error.Abort(dirtyreason, hint=hint) | raise error.Abort(dirtyreason, hint=hint) | ||||
def basestate(self): | def basestate(self): | ||||
"""current working directory base state, disregarding .hgsubstate | """current working directory base state, disregarding .hgsubstate | ||||
state and working directory modifications""" | state and working directory modifications""" | ||||
raise NotImplementedError | raise NotImplementedError | ||||
def checknested(self, path): | def checknested(self, path): | ||||
"""check if path is a subrepository within this repository""" | """check if path is a subrepository within this repository""" | ||||
return False | return False | ||||
def commit(self, text, user, date): | def commit(self, text, user, date): | ||||
"""commit the current changes to the subrepo with the given | """commit the current changes to the subrepo with the given | ||||
log message. Use given user and date if possible. Return the | log message. Use given user and date if possible. Return the | ||||
new state of the subrepo. | new state of the subrepo. | ||||
""" | """ | ||||
raise NotImplementedError | raise NotImplementedError | ||||
def phase(self, state): | def phase(self, state): | ||||
"""returns phase of specified state in the subrepository. | """returns phase of specified state in the subrepository.""" | ||||
""" | |||||
return phases.public | return phases.public | ||||
def remove(self): | def remove(self): | ||||
"""remove the subrepo | """remove the subrepo | ||||
(should verify the dirstate is not dirty first) | (should verify the dirstate is not dirty first) | ||||
""" | """ | ||||
raise NotImplementedError | raise NotImplementedError | ||||
archiver.addfile( | archiver.addfile( | ||||
prefix + name, mode, symlink, self.filedata(name, decode) | prefix + name, mode, symlink, self.filedata(name, decode) | ||||
) | ) | ||||
progress.increment() | progress.increment() | ||||
progress.complete() | progress.complete() | ||||
return total | return total | ||||
def walk(self, match): | def walk(self, match): | ||||
''' | """ | ||||
walk recursively through the directory tree, finding all files | walk recursively through the directory tree, finding all files | ||||
matched by the match function | matched by the match function | ||||
''' | """ | ||||
def forget(self, match, prefix, uipathfn, dryrun, interactive): | def forget(self, match, prefix, uipathfn, dryrun, interactive): | ||||
return ([], []) | return ([], []) | ||||
def removefiles( | def removefiles( | ||||
self, | self, | ||||
matcher, | matcher, | ||||
prefix, | prefix, | ||||
% (substate[0], substate[2]) | % (substate[0], substate[2]) | ||||
) | ) | ||||
return [] | return [] | ||||
def shortid(self, revid): | def shortid(self, revid): | ||||
return revid | return revid | ||||
def unshare(self): | def unshare(self): | ||||
''' | """ | ||||
convert this repository from shared to normal storage. | convert this repository from shared to normal storage. | ||||
''' | """ | ||||
def verify(self, onpush=False): | def verify(self, onpush=False): | ||||
"""verify the revision of this repository that is held in `_state` is | """verify the revision of this repository that is held in `_state` is | ||||
present and not hidden. Return 0 on success or warning, 1 on any | present and not hidden. Return 0 on success or warning, 1 on any | ||||
error. In the case of ``onpush``, warnings or errors will raise an | error. In the case of ``onpush``, warnings or errors will raise an | ||||
exception if the result of pushing would be a broken remote repository. | exception if the result of pushing would be a broken remote repository. | ||||
""" | """ | ||||
return 0 | return 0 | ||||
@propertycache | @propertycache | ||||
def wvfs(self): | def wvfs(self): | ||||
"""return vfs to access the working directory of this subrepository | """return vfs to access the working directory of this subrepository""" | ||||
""" | |||||
return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path)) | return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path)) | ||||
@propertycache | @propertycache | ||||
def _relpath(self): | def _relpath(self): | ||||
"""return path to this subrepository as seen from outermost repository | """return path to this subrepository as seen from outermost repository""" | ||||
""" | |||||
return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path) | return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path) | ||||
class hgsubrepo(abstractsubrepo): | class hgsubrepo(abstractsubrepo): | ||||
def __init__(self, ctx, path, state, allowcreate): | def __init__(self, ctx, path, state, allowcreate): | ||||
super(hgsubrepo, self).__init__(ctx, path) | super(hgsubrepo, self).__init__(ctx, path) | ||||
self._state = state | self._state = state | ||||
r = ctx.repo() | r = ctx.repo() | ||||
break | break | ||||
if clean: | if clean: | ||||
# if not empty: | # if not empty: | ||||
# the cached and current pull states have a different size | # the cached and current pull states have a different size | ||||
clean = next(itercache, None) is None | clean = next(itercache, None) is None | ||||
return clean | return clean | ||||
def _calcstorehash(self, remotepath): | def _calcstorehash(self, remotepath): | ||||
'''calculate a unique "store hash" | """calculate a unique "store hash" | ||||
This method is used to to detect when there are changes that may | This method is used to to detect when there are changes that may | ||||
require a push to a given remote path.''' | require a push to a given remote path.""" | ||||
# sort the files that will be hashed in increasing (likely) file size | # sort the files that will be hashed in increasing (likely) file size | ||||
filelist = (b'bookmarks', b'store/phaseroots', b'store/00changelog.i') | filelist = (b'bookmarks', b'store/phaseroots', b'store/00changelog.i') | ||||
yield b'# %s\n' % _expandedabspath(remotepath) | yield b'# %s\n' % _expandedabspath(remotepath) | ||||
vfs = self._repo.vfs | vfs = self._repo.vfs | ||||
for relname in filelist: | for relname in filelist: | ||||
filehash = node.hex(hashutil.sha1(vfs.tryread(relname)).digest()) | filehash = node.hex(hashutil.sha1(vfs.tryread(relname)).digest()) | ||||
yield b'%s = %s\n' % (relname, filehash) | yield b'%s = %s\n' % (relname, filehash) | ||||
@propertycache | @propertycache | ||||
def _cachestorehashvfs(self): | def _cachestorehashvfs(self): | ||||
return vfsmod.vfs(self._repo.vfs.join(b'cache/storehash')) | return vfsmod.vfs(self._repo.vfs.join(b'cache/storehash')) | ||||
def _readstorehashcache(self, remotepath): | def _readstorehashcache(self, remotepath): | ||||
'''read the store hash cache for a given remote repository''' | '''read the store hash cache for a given remote repository''' | ||||
cachefile = _getstorehashcachename(remotepath) | cachefile = _getstorehashcachename(remotepath) | ||||
return self._cachestorehashvfs.tryreadlines(cachefile, b'r') | return self._cachestorehashvfs.tryreadlines(cachefile, b'r') | ||||
def _cachestorehash(self, remotepath): | def _cachestorehash(self, remotepath): | ||||
'''cache the current store hash | """cache the current store hash | ||||
Each remote repo requires its own store hash cache, because a subrepo | Each remote repo requires its own store hash cache, because a subrepo | ||||
store may be "clean" versus a given remote repo, but not versus another | store may be "clean" versus a given remote repo, but not versus another | ||||
''' | """ | ||||
cachefile = _getstorehashcachename(remotepath) | cachefile = _getstorehashcachename(remotepath) | ||||
with self._repo.lock(): | with self._repo.lock(): | ||||
storehash = list(self._calcstorehash(remotepath)) | storehash = list(self._calcstorehash(remotepath)) | ||||
vfs = self._cachestorehashvfs | vfs = self._cachestorehashvfs | ||||
vfs.writelines(cachefile, storehash, mode=b'wb', notindexed=True) | vfs.writelines(cachefile, storehash, mode=b'wb', notindexed=True) | ||||
def _getctx(self): | def _getctx(self): | ||||
'''fetch the context for this subrepo revision, possibly a workingctx | """fetch the context for this subrepo revision, possibly a workingctx""" | ||||
''' | |||||
if self._ctx.rev() is None: | if self._ctx.rev() is None: | ||||
return self._repo[None] # workingctx if parent is workingctx | return self._repo[None] # workingctx if parent is workingctx | ||||
else: | else: | ||||
rev = self._state[1] | rev = self._state[1] | ||||
return self._repo[rev] | return self._repo[rev] | ||||
@annotatesubrepoerror | @annotatesubrepoerror | ||||
def _initrepo(self, parentrepo, source, create): | def _initrepo(self, parentrepo, source, create): | ||||
if onpush: | if onpush: | ||||
raise error.Abort(msg) | raise error.Abort(msg) | ||||
else: | else: | ||||
self._repo.ui.warn(b'%s\n' % msg) | self._repo.ui.warn(b'%s\n' % msg) | ||||
return 0 | return 0 | ||||
@propertycache | @propertycache | ||||
def wvfs(self): | def wvfs(self): | ||||
"""return own wvfs for efficiency and consistency | """return own wvfs for efficiency and consistency""" | ||||
""" | |||||
return self._repo.wvfs | return self._repo.wvfs | ||||
@propertycache | @propertycache | ||||
def _relpath(self): | def _relpath(self): | ||||
"""return path to this subrepository as seen from outermost repository | """return path to this subrepository as seen from outermost repository""" | ||||
""" | |||||
# Keep consistent dir separators by avoiding vfs.join(self._path) | # Keep consistent dir separators by avoiding vfs.join(self._path) | ||||
return reporelpath(self._repo) | return reporelpath(self._repo) | ||||
class svnsubrepo(abstractsubrepo): | class svnsubrepo(abstractsubrepo): | ||||
def __init__(self, ctx, path, state, allowcreate): | def __init__(self, ctx, path, state, allowcreate): | ||||
super(svnsubrepo, self).__init__(ctx, path) | super(svnsubrepo, self).__init__(ctx, path) | ||||
self._state = state | self._state = state | ||||
continue | continue | ||||
item = s[0].getAttribute('item') | item = s[0].getAttribute('item') | ||||
props = s[0].getAttribute('props') | props = s[0].getAttribute('props') | ||||
path = e.getAttribute('path').encode('utf8') | path = e.getAttribute('path').encode('utf8') | ||||
if item == 'external': | if item == 'external': | ||||
externals.append(path) | externals.append(path) | ||||
elif item == 'missing': | elif item == 'missing': | ||||
missing.append(path) | missing.append(path) | ||||
if item not in ( | if ( | ||||
item | |||||
not in ( | |||||
'', | '', | ||||
'normal', | 'normal', | ||||
'unversioned', | 'unversioned', | ||||
'external', | 'external', | ||||
) or props not in ('', 'none', 'normal'): | ) | ||||
or props not in ('', 'none', 'normal') | |||||
): | |||||
changes.append(path) | changes.append(path) | ||||
for path in changes: | for path in changes: | ||||
for ext in externals: | for ext in externals: | ||||
if path == ext or path.startswith(ext + pycompat.ossep): | if path == ext or path.startswith(ext + pycompat.ossep): | ||||
return True, True, bool(missing) | return True, True, bool(missing) | ||||
return bool(changes), False, bool(missing) | return bool(changes), False, bool(missing) | ||||
@annotatesubrepoerror | @annotatesubrepoerror | ||||
m = re.search(br'^git version (\d+)\.(\d+)', out) | m = re.search(br'^git version (\d+)\.(\d+)', out) | ||||
if m: | if m: | ||||
return (int(m.group(1)), int(m.group(2)), 0) | return (int(m.group(1)), int(m.group(2)), 0) | ||||
return -1 | return -1 | ||||
@staticmethod | @staticmethod | ||||
def _checkversion(out): | def _checkversion(out): | ||||
'''ensure git version is new enough | """ensure git version is new enough | ||||
>>> _checkversion = gitsubrepo._checkversion | >>> _checkversion = gitsubrepo._checkversion | ||||
>>> _checkversion(b'git version 1.6.0') | >>> _checkversion(b'git version 1.6.0') | ||||
'ok' | 'ok' | ||||
>>> _checkversion(b'git version 1.8.5') | >>> _checkversion(b'git version 1.8.5') | ||||
'ok' | 'ok' | ||||
>>> _checkversion(b'git version 1.4.0') | >>> _checkversion(b'git version 1.4.0') | ||||
'abort' | 'abort' | ||||
>>> _checkversion(b'git version 1.5.0') | >>> _checkversion(b'git version 1.5.0') | ||||
'warning' | 'warning' | ||||
>>> _checkversion(b'git version 1.9-rc0') | >>> _checkversion(b'git version 1.9-rc0') | ||||
'ok' | 'ok' | ||||
>>> _checkversion(b'git version 1.9.0.265.g81cdec2') | >>> _checkversion(b'git version 1.9.0.265.g81cdec2') | ||||
'ok' | 'ok' | ||||
>>> _checkversion(b'git version 1.9.0.GIT') | >>> _checkversion(b'git version 1.9.0.GIT') | ||||
'ok' | 'ok' | ||||
>>> _checkversion(b'git version 12345') | >>> _checkversion(b'git version 12345') | ||||
'unknown' | 'unknown' | ||||
>>> _checkversion(b'no') | >>> _checkversion(b'no') | ||||
'unknown' | 'unknown' | ||||
''' | """ | ||||
version = gitsubrepo._gitversion(out) | version = gitsubrepo._gitversion(out) | ||||
# git 1.4.0 can't work at all, but 1.5.X can in at least some cases, | # git 1.4.0 can't work at all, but 1.5.X can in at least some cases, | ||||
# despite the docstring comment. For now, error on 1.4.0, warn on | # despite the docstring comment. For now, error on 1.4.0, warn on | ||||
# 1.5.0 but attempt to continue. | # 1.5.0 but attempt to continue. | ||||
if version == -1: | if version == -1: | ||||
return b'unknown' | return b'unknown' | ||||
if version < (1, 5, 0): | if version < (1, 5, 0): | ||||
return b'abort' | return b'abort' | ||||
def _gitupdatestat(self): | def _gitupdatestat(self): | ||||
"""This must be run before git diff-index. | """This must be run before git diff-index. | ||||
diff-index only looks at changes to file stat; | diff-index only looks at changes to file stat; | ||||
this command looks at file contents and updates the stat.""" | this command looks at file contents and updates the stat.""" | ||||
self._gitcommand([b'update-index', b'-q', b'--refresh']) | self._gitcommand([b'update-index', b'-q', b'--refresh']) | ||||
def _gitbranchmap(self): | def _gitbranchmap(self): | ||||
'''returns 2 things: | """returns 2 things: | ||||
a map from git branch to revision | a map from git branch to revision | ||||
a map from revision to branches''' | a map from revision to branches""" | ||||
branch2rev = {} | branch2rev = {} | ||||
rev2branch = {} | rev2branch = {} | ||||
out = self._gitcommand( | out = self._gitcommand( | ||||
[b'for-each-ref', b'--format', b'%(objectname) %(refname)'] | [b'for-each-ref', b'--format', b'%(objectname) %(refname)'] | ||||
) | ) | ||||
for line in out.split(b'\n'): | for line in out.split(b'\n'): | ||||
revision, ref = line.split(b' ') | revision, ref = line.split(b' ') |
tags as tagsmod, | tags as tagsmod, | ||||
util, | util, | ||||
) | ) | ||||
hexnullid = hex(nullid) | hexnullid = hex(nullid) | ||||
def readtagsformerge(ui, repo, lines, fn=b'', keeplinenums=False): | def readtagsformerge(ui, repo, lines, fn=b'', keeplinenums=False): | ||||
'''read the .hgtags file into a structure that is suitable for merging | """read the .hgtags file into a structure that is suitable for merging | ||||
Depending on the keeplinenums flag, clear the line numbers associated | Depending on the keeplinenums flag, clear the line numbers associated | ||||
with each tag. This is done because only the line numbers of the first | with each tag. This is done because only the line numbers of the first | ||||
parent are useful for merging. | parent are useful for merging. | ||||
''' | """ | ||||
filetags = tagsmod._readtaghist( | filetags = tagsmod._readtaghist( | ||||
ui, repo, lines, fn=fn, recode=None, calcnodelines=True | ui, repo, lines, fn=fn, recode=None, calcnodelines=True | ||||
)[1] | )[1] | ||||
for tagname, taginfo in filetags.items(): | for tagname, taginfo in filetags.items(): | ||||
if not keeplinenums: | if not keeplinenums: | ||||
for el in taginfo: | for el in taginfo: | ||||
el[1] = None | el[1] = None | ||||
return filetags | return filetags | ||||
def grouptagnodesbyline(tagnodes): | def grouptagnodesbyline(tagnodes): | ||||
''' | """ | ||||
Group nearby nodes (i.e. those that must be written next to each other) | Group nearby nodes (i.e. those that must be written next to each other) | ||||
The input is a list of [node, position] pairs, corresponding to a given tag | The input is a list of [node, position] pairs, corresponding to a given tag | ||||
The position is the line number where the node was found on the first parent | The position is the line number where the node was found on the first parent | ||||
.hgtags file, or None for those nodes that came from the base or the second | .hgtags file, or None for those nodes that came from the base or the second | ||||
parent .hgtags files. | parent .hgtags files. | ||||
This function groups those [node, position] pairs, returning a list of | This function groups those [node, position] pairs, returning a list of | ||||
groups of nodes that must be written next to each other because their | groups of nodes that must be written next to each other because their | ||||
positions are consecutive or have no position preference (because their | positions are consecutive or have no position preference (because their | ||||
position is None). | position is None). | ||||
The result is a list of [position, [consecutive node list]] | The result is a list of [position, [consecutive node list]] | ||||
''' | """ | ||||
firstlinenum = None | firstlinenum = None | ||||
for hexnode, linenum in tagnodes: | for hexnode, linenum in tagnodes: | ||||
firstlinenum = linenum | firstlinenum = linenum | ||||
if firstlinenum is not None: | if firstlinenum is not None: | ||||
break | break | ||||
if firstlinenum is None: | if firstlinenum is None: | ||||
return [[None, [el[0] for el in tagnodes]]] | return [[None, [el[0] for el in tagnodes]]] | ||||
tagnodes[0][1] = firstlinenum | tagnodes[0][1] = firstlinenum | ||||
groupednodes = [[firstlinenum, []]] | groupednodes = [[firstlinenum, []]] | ||||
prevlinenum = firstlinenum | prevlinenum = firstlinenum | ||||
for hexnode, linenum in tagnodes: | for hexnode, linenum in tagnodes: | ||||
if linenum is not None and linenum - prevlinenum > 1: | if linenum is not None and linenum - prevlinenum > 1: | ||||
groupednodes.append([linenum, []]) | groupednodes.append([linenum, []]) | ||||
groupednodes[-1][1].append(hexnode) | groupednodes[-1][1].append(hexnode) | ||||
if linenum is not None: | if linenum is not None: | ||||
prevlinenum = linenum | prevlinenum = linenum | ||||
return groupednodes | return groupednodes | ||||
def writemergedtags(fcd, mergedtags): | def writemergedtags(fcd, mergedtags): | ||||
''' | """ | ||||
write the merged tags while trying to minimize the diff to the first parent | write the merged tags while trying to minimize the diff to the first parent | ||||
This function uses the ordering info stored on the merged tags dict to | This function uses the ordering info stored on the merged tags dict to | ||||
generate an .hgtags file which is correct (in the sense that its contents | generate an .hgtags file which is correct (in the sense that its contents | ||||
correspond to the result of the tag merge) while also being as close as | correspond to the result of the tag merge) while also being as close as | ||||
possible to the first parent's .hgtags file. | possible to the first parent's .hgtags file. | ||||
''' | """ | ||||
# group the node-tag pairs that must be written next to each other | # group the node-tag pairs that must be written next to each other | ||||
for tname, taglist in list(mergedtags.items()): | for tname, taglist in list(mergedtags.items()): | ||||
mergedtags[tname] = grouptagnodesbyline(taglist) | mergedtags[tname] = grouptagnodesbyline(taglist) | ||||
# convert the grouped merged tags dict into a format that resembles the | # convert the grouped merged tags dict into a format that resembles the | ||||
# final .hgtags file (i.e. a list of blocks of 'node tag' pairs) | # final .hgtags file (i.e. a list of blocks of 'node tag' pairs) | ||||
def taglist2string(tlist, tname): | def taglist2string(tlist, tname): | ||||
return b'\n'.join([b'%s %s' % (hexnode, tname) for hexnode in tlist]) | return b'\n'.join([b'%s %s' % (hexnode, tname) for hexnode in tlist]) | ||||
# finally we can join the sorted groups to get the final contents of the | # finally we can join the sorted groups to get the final contents of the | ||||
# merged .hgtags file, and then write it to disk | # merged .hgtags file, and then write it to disk | ||||
mergedtagstring = b'\n'.join([tags for rank, tags in finaltags if tags]) | mergedtagstring = b'\n'.join([tags for rank, tags in finaltags if tags]) | ||||
fcd.write(mergedtagstring + b'\n', fcd.flags()) | fcd.write(mergedtagstring + b'\n', fcd.flags()) | ||||
def singletagmerge(p1nodes, p2nodes): | def singletagmerge(p1nodes, p2nodes): | ||||
''' | """ | ||||
merge the nodes corresponding to a single tag | merge the nodes corresponding to a single tag | ||||
Note that the inputs are lists of node-linenum pairs (i.e. not just lists | Note that the inputs are lists of node-linenum pairs (i.e. not just lists | ||||
of nodes) | of nodes) | ||||
''' | """ | ||||
if not p2nodes: | if not p2nodes: | ||||
return p1nodes | return p1nodes | ||||
if not p1nodes: | if not p1nodes: | ||||
return p2nodes | return p2nodes | ||||
# there is no conflict unless both tags point to different revisions | # there is no conflict unless both tags point to different revisions | ||||
# and have a non identical tag history | # and have a non identical tag history | ||||
p1currentnode = p1nodes[-1][0] | p1currentnode = p1nodes[-1][0] | ||||
# - non common lowest ranking nodes | # - non common lowest ranking nodes | ||||
# - non common highest ranking nodes | # - non common highest ranking nodes | ||||
# note that the common nodes plus the non common lowest ranking nodes is the | # note that the common nodes plus the non common lowest ranking nodes is the | ||||
# whole list of lr nodes | # whole list of lr nodes | ||||
return lrnodes + hrnodes[commonidx:] | return lrnodes + hrnodes[commonidx:] | ||||
def merge(repo, fcd, fco, fca): | def merge(repo, fcd, fco, fca): | ||||
''' | """ | ||||
Merge the tags of two revisions, taking into account the base tags | Merge the tags of two revisions, taking into account the base tags | ||||
Try to minimize the diff between the merged tags and the first parent tags | Try to minimize the diff between the merged tags and the first parent tags | ||||
''' | """ | ||||
ui = repo.ui | ui = repo.ui | ||||
# read the p1, p2 and base tags | # read the p1, p2 and base tags | ||||
# only keep the line numbers for the p1 tags | # only keep the line numbers for the p1 tags | ||||
p1tags = readtagsformerge( | p1tags = readtagsformerge( | ||||
ui, repo, fcd.data().splitlines(), fn=b"p1 tags", keeplinenums=True | ui, repo, fcd.data().splitlines(), fn=b"p1 tags", keeplinenums=True | ||||
) | ) | ||||
p2tags = readtagsformerge( | p2tags = readtagsformerge( | ||||
ui, repo, fco.data().splitlines(), fn=b"p2 tags", keeplinenums=False | ui, repo, fco.data().splitlines(), fn=b"p2 tags", keeplinenums=False |
elif new is None: | elif new is None: | ||||
fp.write(remove % (old, tag)) | fp.write(remove % (old, tag)) | ||||
else: | else: | ||||
fp.write(updateold % (old, tag)) | fp.write(updateold % (old, tag)) | ||||
fp.write(updatenew % (new, tag)) | fp.write(updatenew % (new, tag)) | ||||
def findglobaltags(ui, repo): | def findglobaltags(ui, repo): | ||||
'''Find global tags in a repo: return a tagsmap | """Find global tags in a repo: return a tagsmap | ||||
tagsmap: tag name to (node, hist) 2-tuples. | tagsmap: tag name to (node, hist) 2-tuples. | ||||
The tags cache is read and updated as a side-effect of calling. | The tags cache is read and updated as a side-effect of calling. | ||||
''' | """ | ||||
(heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo) | (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo) | ||||
if cachetags is not None: | if cachetags is not None: | ||||
assert not shouldwrite | assert not shouldwrite | ||||
# XXX is this really 100% correct? are there oddball special | # XXX is this really 100% correct? are there oddball special | ||||
# cases where a global tag should outrank a local tag but won't, | # cases where a global tag should outrank a local tag but won't, | ||||
# because cachetags does not contain rank info? | # because cachetags does not contain rank info? | ||||
alltags = {} | alltags = {} | ||||
_updatetags(cachetags, alltags) | _updatetags(cachetags, alltags) | ||||
cl.rev(filetags[t][0]) | cl.rev(filetags[t][0]) | ||||
except (LookupError, ValueError): | except (LookupError, ValueError): | ||||
del filetags[t] | del filetags[t] | ||||
_updatetags(filetags, alltags, b'local', tagtypes) | _updatetags(filetags, alltags, b'local', tagtypes) | ||||
def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False): | def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False): | ||||
'''Read tag definitions from a file (or any source of lines). | """Read tag definitions from a file (or any source of lines). | ||||
This function returns two sortdicts with similar information: | This function returns two sortdicts with similar information: | ||||
- the first dict, bintaghist, contains the tag information as expected by | - the first dict, bintaghist, contains the tag information as expected by | ||||
the _readtags function, i.e. a mapping from tag name to (node, hist): | the _readtags function, i.e. a mapping from tag name to (node, hist): | ||||
- node is the node id from the last line read for that name, | - node is the node id from the last line read for that name, | ||||
- hist is the list of node ids previously associated with it (in file | - hist is the list of node ids previously associated with it (in file | ||||
order). All node ids are binary, not hex. | order). All node ids are binary, not hex. | ||||
- the second dict, hextaglines, is a mapping from tag name to a list of | - the second dict, hextaglines, is a mapping from tag name to a list of | ||||
[hexnode, line number] pairs, ordered from the oldest to the newest node. | [hexnode, line number] pairs, ordered from the oldest to the newest node. | ||||
When calcnodelines is False the hextaglines dict is not calculated (an | When calcnodelines is False the hextaglines dict is not calculated (an | ||||
empty dict is returned). This is done to improve this function's | empty dict is returned). This is done to improve this function's | ||||
performance in cases where the line numbers are not needed. | performance in cases where the line numbers are not needed. | ||||
''' | """ | ||||
bintaghist = util.sortdict() | bintaghist = util.sortdict() | ||||
hextaglines = util.sortdict() | hextaglines = util.sortdict() | ||||
count = 0 | count = 0 | ||||
def dbg(msg): | def dbg(msg): | ||||
ui.debug(b"%s, line %d: %s\n" % (fn, count, msg)) | ui.debug(b"%s, line %d: %s\n" % (fn, count, msg)) | ||||
# map tag name to (node, hist) | # map tag name to (node, hist) | ||||
if name not in bintaghist: | if name not in bintaghist: | ||||
bintaghist[name] = [] | bintaghist[name] = [] | ||||
bintaghist[name].append(nodebin) | bintaghist[name].append(nodebin) | ||||
return bintaghist, hextaglines | return bintaghist, hextaglines | ||||
def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False): | def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False): | ||||
'''Read tag definitions from a file (or any source of lines). | """Read tag definitions from a file (or any source of lines). | ||||
Returns a mapping from tag name to (node, hist). | Returns a mapping from tag name to (node, hist). | ||||
"node" is the node id from the last line read for that name. "hist" | "node" is the node id from the last line read for that name. "hist" | ||||
is the list of node ids previously associated with it (in file order). | is the list of node ids previously associated with it (in file order). | ||||
All node ids are binary, not hex. | All node ids are binary, not hex. | ||||
''' | """ | ||||
filetags, nodelines = _readtaghist( | filetags, nodelines = _readtaghist( | ||||
ui, repo, lines, fn, recode=recode, calcnodelines=calcnodelines | ui, repo, lines, fn, recode=recode, calcnodelines=calcnodelines | ||||
) | ) | ||||
# util.sortdict().__setitem__ is much slower at replacing then inserting | # util.sortdict().__setitem__ is much slower at replacing then inserting | ||||
# new entries. The difference can matter if there are thousands of tags. | # new entries. The difference can matter if there are thousands of tags. | ||||
# Create a new sortdict to avoid the performance penalty. | # Create a new sortdict to avoid the performance penalty. | ||||
newtags = util.sortdict() | newtags = util.sortdict() | ||||
for tag, taghist in filetags.items(): | for tag, taghist in filetags.items(): | ||||
"""name of a tagcache file for a given repo or repoview""" | """name of a tagcache file for a given repo or repoview""" | ||||
filename = b'tags2' | filename = b'tags2' | ||||
if repo.filtername: | if repo.filtername: | ||||
filename = b'%s-%s' % (filename, repo.filtername) | filename = b'%s-%s' % (filename, repo.filtername) | ||||
return filename | return filename | ||||
def _readtagcache(ui, repo): | def _readtagcache(ui, repo): | ||||
'''Read the tag cache. | """Read the tag cache. | ||||
Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite). | Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite). | ||||
If the cache is completely up-to-date, "cachetags" is a dict of the | If the cache is completely up-to-date, "cachetags" is a dict of the | ||||
form returned by _readtags() and "heads", "fnodes", and "validinfo" are | form returned by _readtags() and "heads", "fnodes", and "validinfo" are | ||||
None and "shouldwrite" is False. | None and "shouldwrite" is False. | ||||
If the cache is not up to date, "cachetags" is None. "heads" is a list | If the cache is not up to date, "cachetags" is None. "heads" is a list | ||||
of all heads currently in the repository, ordered from tip to oldest. | of all heads currently in the repository, ordered from tip to oldest. | ||||
"validinfo" is a tuple describing cache validation info. This is used | "validinfo" is a tuple describing cache validation info. This is used | ||||
when writing the tags cache. "fnodes" is a mapping from head to .hgtags | when writing the tags cache. "fnodes" is a mapping from head to .hgtags | ||||
filenode. "shouldwrite" is True. | filenode. "shouldwrite" is True. | ||||
If the cache is not up to date, the caller is responsible for reading tag | If the cache is not up to date, the caller is responsible for reading tag | ||||
info from each returned head. (See findglobaltags().) | info from each returned head. (See findglobaltags().) | ||||
''' | """ | ||||
try: | try: | ||||
cachefile = repo.cachevfs(_filename(repo), b'r') | cachefile = repo.cachevfs(_filename(repo), b'r') | ||||
# force reading the file for static-http | # force reading the file for static-http | ||||
cachelines = iter(cachefile) | cachelines = iter(cachefile) | ||||
except IOError: | except IOError: | ||||
cachefile = None | cachefile = None | ||||
cacherev = None | cacherev = None | ||||
try: | try: | ||||
cachefile.close() | cachefile.close() | ||||
except (OSError, IOError): | except (OSError, IOError): | ||||
pass | pass | ||||
def tag(repo, names, node, message, local, user, date, editor=False): | def tag(repo, names, node, message, local, user, date, editor=False): | ||||
'''tag a revision with one or more symbolic names. | """tag a revision with one or more symbolic names. | ||||
names is a list of strings or, when adding a single tag, names may be a | names is a list of strings or, when adding a single tag, names may be a | ||||
string. | string. | ||||
if local is True, the tags are stored in a per-repository file. | if local is True, the tags are stored in a per-repository file. | ||||
otherwise, they are stored in the .hgtags file, and a new | otherwise, they are stored in the .hgtags file, and a new | ||||
changeset is committed with the change. | changeset is committed with the change. | ||||
keyword arguments: | keyword arguments: | ||||
local: whether to store tags in non-version-controlled file | local: whether to store tags in non-version-controlled file | ||||
(default False) | (default False) | ||||
message: commit message to use if committing | message: commit message to use if committing | ||||
user: name of user to use if committing | user: name of user to use if committing | ||||
date: date tuple to use if committing''' | date: date tuple to use if committing""" | ||||
if not local: | if not local: | ||||
m = matchmod.exact([b'.hgtags']) | m = matchmod.exact([b'.hgtags']) | ||||
st = repo.status(match=m, unknown=True, ignored=True) | st = repo.status(match=m, unknown=True, ignored=True) | ||||
if any( | if any( | ||||
( | ( | ||||
st.modified, | st.modified, | ||||
st.added, | st.added, |
""" | """ | ||||
if websubtable: | if websubtable: | ||||
for regexp, format in websubtable: | for regexp, format in websubtable: | ||||
text = regexp.sub(format, text) | text = regexp.sub(format, text) | ||||
return text | return text | ||||
def loadfilter(ui, extname, registrarobj): | def loadfilter(ui, extname, registrarobj): | ||||
"""Load template filter from specified registrarobj | """Load template filter from specified registrarobj""" | ||||
""" | |||||
for name, func in pycompat.iteritems(registrarobj._table): | for name, func in pycompat.iteritems(registrarobj._table): | ||||
filters[name] = func | filters[name] = func | ||||
# tell hggettext to extract docstrings from these functions: | # tell hggettext to extract docstrings from these functions: | ||||
i18nfunctions = filters.values() | i18nfunctions = filters.values() |
tokens = text.split(splitter) | tokens = text.split(splitter) | ||||
if num >= len(tokens) or num < -len(tokens): | if num >= len(tokens) or num < -len(tokens): | ||||
return b'' | return b'' | ||||
else: | else: | ||||
return tokens[num] | return tokens[num] | ||||
def loadfunction(ui, extname, registrarobj): | def loadfunction(ui, extname, registrarobj): | ||||
"""Load template function from specified registrarobj | """Load template function from specified registrarobj""" | ||||
""" | |||||
for name, func in pycompat.iteritems(registrarobj._table): | for name, func in pycompat.iteritems(registrarobj._table): | ||||
funcs[name] = func | funcs[name] = func | ||||
# tell hggettext to extract docstrings from these functions: | # tell hggettext to extract docstrings from these functions: | ||||
i18nfunctions = funcs.values() | i18nfunctions = funcs.values() |
tmpl = ( | tmpl = ( | ||||
b'{instability}:{if(divergentnodes, " ")}{divergentnodes} ' | b'{instability}:{if(divergentnodes, " ")}{divergentnodes} ' | ||||
b'{reason} {node|short}' | b'{reason} {node|short}' | ||||
) | ) | ||||
return templateutil.mappinglist(entries, tmpl=tmpl, sep=b'\n') | return templateutil.mappinglist(entries, tmpl=tmpl, sep=b'\n') | ||||
def loadkeyword(ui, extname, registrarobj): | def loadkeyword(ui, extname, registrarobj): | ||||
"""Load template keyword from specified registrarobj | """Load template keyword from specified registrarobj""" | ||||
""" | |||||
for name, func in pycompat.iteritems(registrarobj._table): | for name, func in pycompat.iteritems(registrarobj._table): | ||||
keywords[name] = func | keywords[name] = func | ||||
# tell hggettext to extract docstrings from these functions: | # tell hggettext to extract docstrings from these functions: | ||||
i18nfunctions = keywords.values() | i18nfunctions = keywords.values() |
def lookup(self, mapping, key): | def lookup(self, mapping, key): | ||||
return None | return None | ||||
def populatemap(self, context, origmapping, newmapping): | def populatemap(self, context, origmapping, newmapping): | ||||
return {} | return {} | ||||
class engine(object): | class engine(object): | ||||
'''template expansion engine. | """template expansion engine. | ||||
template expansion works like this. a map file contains key=value | template expansion works like this. a map file contains key=value | ||||
pairs. if value is quoted, it is treated as string. otherwise, it | pairs. if value is quoted, it is treated as string. otherwise, it | ||||
is treated as name of template file. | is treated as name of template file. | ||||
templater is asked to expand a key in map. it looks up key, and | templater is asked to expand a key in map. it looks up key, and | ||||
looks for strings like this: {foo}. it expands {foo} by looking up | looks for strings like this: {foo}. it expands {foo} by looking up | ||||
foo in map, and substituting it. expansion is recursive: it stops | foo in map, and substituting it. expansion is recursive: it stops | ||||
when there is no more {foo} to replace. | when there is no more {foo} to replace. | ||||
expansion also allows formatting and filtering. | expansion also allows formatting and filtering. | ||||
format uses key to expand each item in list. syntax is | format uses key to expand each item in list. syntax is | ||||
{key%format}. | {key%format}. | ||||
filter uses function to transform value. syntax is | filter uses function to transform value. syntax is | ||||
{key|filter1|filter2|...}.''' | {key|filter1|filter2|...}.""" | ||||
def __init__(self, loader, filters=None, defaults=None, resources=None): | def __init__(self, loader, filters=None, defaults=None, resources=None): | ||||
self._loader = loader | self._loader = loader | ||||
if filters is None: | if filters is None: | ||||
filters = {} | filters = {} | ||||
self._filters = filters | self._filters = filters | ||||
self._funcs = templatefuncs.funcs # make this a parameter if needed | self._funcs = templatefuncs.funcs # make this a parameter if needed | ||||
if defaults is None: | if defaults is None: | ||||
"""Load, parse, and cache the specified template if available""" | """Load, parse, and cache the specified template if available""" | ||||
try: | try: | ||||
self._load(t) | self._load(t) | ||||
return True | return True | ||||
except templateutil.TemplateNotFound: | except templateutil.TemplateNotFound: | ||||
return False | return False | ||||
def process(self, t, mapping): | def process(self, t, mapping): | ||||
'''Perform expansion. t is name of map element to expand. | """Perform expansion. t is name of map element to expand. | ||||
mapping contains added elements for use during expansion. Is a | mapping contains added elements for use during expansion. Is a | ||||
generator.''' | generator.""" | ||||
func, data = self._load(t) | func, data = self._load(t) | ||||
return self._expand(func, data, mapping) | return self._expand(func, data, mapping) | ||||
def expand(self, tmpl, mapping): | def expand(self, tmpl, mapping): | ||||
"""Perform expansion over a literal template | """Perform expansion over a literal template | ||||
No user aliases will be expanded since this is supposed to be called | No user aliases will be expanded since this is supposed to be called | ||||
with an internal template string. | with an internal template string. | ||||
dir = templatedir() | dir = templatedir() | ||||
if dir: | if dir: | ||||
abs = os.path.normpath(os.path.join(dir, rel)) | abs = os.path.normpath(os.path.join(dir, rel)) | ||||
if os.path.isfile(abs): | if os.path.isfile(abs): | ||||
subresource = util.posixfile(abs, b'rb') | subresource = util.posixfile(abs, b'rb') | ||||
if subresource: | if subresource: | ||||
data = subresource.read() | data = subresource.read() | ||||
conf.parse( | conf.parse( | ||||
abs, data, sections=sections, remap=remap, include=include, | abs, | ||||
data, | |||||
sections=sections, | |||||
remap=remap, | |||||
include=include, | |||||
) | ) | ||||
data = fp.read() | data = fp.read() | ||||
conf.parse(mapfile, data, remap={b'': b'templates'}, include=include) | conf.parse(mapfile, data, remap={b'': b'templates'}, include=include) | ||||
cache = {} | cache = {} | ||||
tmap = {} | tmap = {} | ||||
aliases = [] | aliases = [] | ||||
def templatedir(): | def templatedir(): | ||||
'''return the directory used for template files, or None.''' | '''return the directory used for template files, or None.''' | ||||
path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates')) | path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates')) | ||||
return path if os.path.isdir(path) else None | return path if os.path.isdir(path) else None | ||||
def open_template(name, templatepath=None): | def open_template(name, templatepath=None): | ||||
'''returns a file-like object for the given template, and its full path | """returns a file-like object for the given template, and its full path | ||||
If the name is a relative path and we're in a frozen binary, the template | If the name is a relative path and we're in a frozen binary, the template | ||||
will be read from the mercurial.templates package instead. The returned path | will be read from the mercurial.templates package instead. The returned path | ||||
will then be the relative path. | will then be the relative path. | ||||
''' | """ | ||||
# Does the name point directly to a map file? | # Does the name point directly to a map file? | ||||
if os.path.isfile(name) or os.path.isabs(name): | if os.path.isfile(name) or os.path.isabs(name): | ||||
return name, open(name, mode='rb') | return name, open(name, mode='rb') | ||||
# Does the name point to a template in the provided templatepath, or | # Does the name point to a template in the provided templatepath, or | ||||
# in mercurial/templates/ if no path was provided? | # in mercurial/templates/ if no path was provided? | ||||
if templatepath is None: | if templatepath is None: | ||||
templatepath = templatedir() | templatepath = templatedir() |
# Assuming a delta is stored, we shouldn't need to validate node1 in | # Assuming a delta is stored, we shouldn't need to validate node1 in | ||||
# order to retrieve node2. | # order to retrieve node2. | ||||
self.assertEqual(f.read(node2), fulltext2) | self.assertEqual(f.read(node2), fulltext2) | ||||
def testcensored(self): | def testcensored(self): | ||||
f = self._makefilefn() | f = self._makefilefn() | ||||
stored1 = storageutil.packmeta({b'censored': b'tombstone',}, b'') | stored1 = storageutil.packmeta( | ||||
{ | |||||
b'censored': b'tombstone', | |||||
}, | |||||
b'', | |||||
) | |||||
with self._maketransactionfn() as tr: | with self._maketransactionfn() as tr: | ||||
node0 = f.add(b'foo', None, tr, 0, nullid, nullid) | node0 = f.add(b'foo', None, tr, 0, nullid, nullid) | ||||
# The node value doesn't matter since we can't verify it. | # The node value doesn't matter since we can't verify it. | ||||
node1 = b'\xbb' * 20 | node1 = b'\xbb' * 20 | ||||
self._addrawrevisionfn( | self._addrawrevisionfn( | ||||
f.read(1) | f.read(1) | ||||
def testcensoredrawrevision(self): | def testcensoredrawrevision(self): | ||||
# Like above, except we do the rawdata() request first to | # Like above, except we do the rawdata() request first to | ||||
# isolate revision caching behavior. | # isolate revision caching behavior. | ||||
f = self._makefilefn() | f = self._makefilefn() | ||||
stored1 = storageutil.packmeta({b'censored': b'tombstone',}, b'') | stored1 = storageutil.packmeta( | ||||
{ | |||||
b'censored': b'tombstone', | |||||
}, | |||||
b'', | |||||
) | |||||
with self._maketransactionfn() as tr: | with self._maketransactionfn() as tr: | ||||
node0 = f.add(b'foo', None, tr, 0, nullid, nullid) | node0 = f.add(b'foo', None, tr, 0, nullid, nullid) | ||||
# The node value doesn't matter since we can't verify it. | # The node value doesn't matter since we can't verify it. | ||||
node1 = b'\xbb' * 20 | node1 = b'\xbb' * 20 | ||||
self._addrawrevisionfn( | self._addrawrevisionfn( | ||||
self.assertEqual(f.node(0), nodes[0]) | self.assertEqual(f.node(0), nodes[0]) | ||||
self.assertEqual(f.node(1), nodes[1]) | self.assertEqual(f.node(1), nodes[1]) | ||||
self.assertEqual(f.node(2), nodes[2]) | self.assertEqual(f.node(2), nodes[2]) | ||||
def testdeltaagainstcensored(self): | def testdeltaagainstcensored(self): | ||||
# Attempt to apply a delta made against a censored revision. | # Attempt to apply a delta made against a censored revision. | ||||
f = self._makefilefn() | f = self._makefilefn() | ||||
stored1 = storageutil.packmeta({b'censored': b'tombstone',}, b'') | stored1 = storageutil.packmeta( | ||||
{ | |||||
b'censored': b'tombstone', | |||||
}, | |||||
b'', | |||||
) | |||||
with self._maketransactionfn() as tr: | with self._maketransactionfn() as tr: | ||||
node0 = f.add(b'foo\n' * 30, None, tr, 0, nullid, nullid) | node0 = f.add(b'foo\n' * 30, None, tr, 0, nullid, nullid) | ||||
# The node value doesn't matter since we can't verify it. | # The node value doesn't matter since we can't verify it. | ||||
node1 = b'\xbb' * 20 | node1 = b'\xbb' * 20 | ||||
self._addrawrevisionfn( | self._addrawrevisionfn( |
entries = [] | entries = [] | ||||
for l in self._file: | for l in self._file: | ||||
file, troffset = l.split(b'\0') | file, troffset = l.split(b'\0') | ||||
entries.append((file, int(troffset))) | entries.append((file, int(troffset))) | ||||
return entries | return entries | ||||
@active | @active | ||||
def replace(self, file, offset): | def replace(self, file, offset): | ||||
''' | """ | ||||
replace can only replace already committed entries | replace can only replace already committed entries | ||||
that are not pending in the queue | that are not pending in the queue | ||||
''' | """ | ||||
if file in self._newfiles: | if file in self._newfiles: | ||||
if not offset: | if not offset: | ||||
return | return | ||||
self._newfiles.remove(file) | self._newfiles.remove(file) | ||||
self._offsetmap[file] = offset | self._offsetmap[file] = offset | ||||
elif file in self._offsetmap: | elif file in self._offsetmap: | ||||
if not offset: | if not offset: | ||||
del self._offsetmap[file] | del self._offsetmap[file] | ||||
Category is a unique identifier to allow overwriting an old callback | Category is a unique identifier to allow overwriting an old callback | ||||
with a newer callback. | with a newer callback. | ||||
""" | """ | ||||
self._pendingcallback[category] = callback | self._pendingcallback[category] = callback | ||||
@active | @active | ||||
def writepending(self): | def writepending(self): | ||||
'''write pending file to temporary version | """write pending file to temporary version | ||||
This is used to allow hooks to view a transaction before commit''' | This is used to allow hooks to view a transaction before commit""" | ||||
categories = sorted(self._pendingcallback) | categories = sorted(self._pendingcallback) | ||||
for cat in categories: | for cat in categories: | ||||
# remove callback since the data will have been flushed | # remove callback since the data will have been flushed | ||||
any = self._pendingcallback.pop(cat)(self) | any = self._pendingcallback.pop(cat)(self) | ||||
self._anypending = self._anypending or any | self._anypending = self._anypending or any | ||||
self._anypending |= self._generatefiles(suffix=b'.pending') | self._anypending |= self._generatefiles(suffix=b'.pending') | ||||
return self._anypending | return self._anypending | ||||
@active | @active | ||||
def hasfinalize(self, category): | def hasfinalize(self, category): | ||||
"""check is a callback already exist for a category | """check is a callback already exist for a category""" | ||||
""" | |||||
return category in self._finalizecallback | return category in self._finalizecallback | ||||
@active | @active | ||||
def addfinalize(self, category, callback): | def addfinalize(self, category, callback): | ||||
"""add a callback to be called when the transaction is closed | """add a callback to be called when the transaction is closed | ||||
The transaction will be given as callback's first argument. | The transaction will be given as callback's first argument. | ||||
Category is a unique identifier to allow overwriting an old callback | Category is a unique identifier to allow overwriting an old callback | ||||
with a newer callback. | with a newer callback. | ||||
""" | """ | ||||
self._abortcallback[category] = callback | self._abortcallback[category] = callback | ||||
@active | @active | ||||
def addvalidator(self, category, callback): | def addvalidator(self, category, callback): | ||||
""" adds a callback to be called when validating the transaction. | """adds a callback to be called when validating the transaction. | ||||
The transaction will be given as the first argument to the callback. | The transaction will be given as the first argument to the callback. | ||||
callback should raise exception if to abort transaction """ | callback should raise exception if to abort transaction""" | ||||
self._validatecallback[category] = callback | self._validatecallback[category] = callback | ||||
@active | @active | ||||
def close(self): | def close(self): | ||||
'''commit the transaction''' | '''commit the transaction''' | ||||
if self._count == 1: | if self._count == 1: | ||||
for category in sorted(self._validatecallback): | for category in sorted(self._validatecallback): | ||||
self._validatecallback[category](self) | self._validatecallback[category](self) | ||||
categories = sorted(self._postclosecallback) | categories = sorted(self._postclosecallback) | ||||
for cat in categories: | for cat in categories: | ||||
self._postclosecallback[cat](self) | self._postclosecallback[cat](self) | ||||
# Prevent double usage and help clear cycles. | # Prevent double usage and help clear cycles. | ||||
self._postclosecallback = None | self._postclosecallback = None | ||||
@active | @active | ||||
def abort(self): | def abort(self): | ||||
'''abort the transaction (generally called on error, or when the | """abort the transaction (generally called on error, or when the | ||||
transaction is not explicitly committed before going out of | transaction is not explicitly committed before going out of | ||||
scope)''' | scope)""" | ||||
self._abort() | self._abort() | ||||
def _writeundo(self): | def _writeundo(self): | ||||
"""write transaction data for possible future undo call""" | """write transaction data for possible future undo call""" | ||||
if self._undoname is None: | if self._undoname is None: | ||||
return | return | ||||
undobackupfile = self._opener.open( | undobackupfile = self._opener.open( | ||||
b"%s.backupfiles" % self._undoname, b'w' | b"%s.backupfiles" % self._undoname, b'w' |
reqcnt += 1 | reqcnt += 1 | ||||
progress.increment() | progress.increment() | ||||
repo.ui.debug( | repo.ui.debug( | ||||
b"request %d: %s\n" % (reqcnt, b" ".join(map(short, r))) | b"request %d: %s\n" % (reqcnt, b" ".join(map(short, r))) | ||||
) | ) | ||||
for p in pycompat.xrange(0, len(r), 10): | for p in pycompat.xrange(0, len(r), 10): | ||||
with remote.commandexecutor() as e: | with remote.commandexecutor() as e: | ||||
branches = e.callcommand( | branches = e.callcommand( | ||||
b'branches', {b'nodes': r[p : p + 10],} | b'branches', | ||||
{ | |||||
b'nodes': r[p : p + 10], | |||||
}, | |||||
).result() | ).result() | ||||
for b in branches: | for b in branches: | ||||
repo.ui.debug( | repo.ui.debug( | ||||
b"received %s:%s\n" % (short(b[0]), short(b[1])) | b"received %s:%s\n" % (short(b[0]), short(b[1])) | ||||
) | ) | ||||
unknown.append(b) | unknown.append(b) | ||||
# txnutil.py - transaction related utilities | # txnutil.py - transaction related utilities | ||||
# | # | ||||
# Copyright FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others | # Copyright FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others | ||||
# | # | ||||
# This software may be used and distributed according to the terms of the | # This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | # GNU General Public License version 2 or any later version. | ||||
from __future__ import absolute_import | from __future__ import absolute_import | ||||
import errno | import errno | ||||
from . import encoding | from . import encoding | ||||
def mayhavepending(root): | def mayhavepending(root): | ||||
'''return whether 'root' may have pending changes, which are | """return whether 'root' may have pending changes, which are | ||||
visible to this process. | visible to this process. | ||||
''' | """ | ||||
return root == encoding.environ.get(b'HG_PENDING') | return root == encoding.environ.get(b'HG_PENDING') | ||||
def trypending(root, vfs, filename, **kwargs): | def trypending(root, vfs, filename, **kwargs): | ||||
'''Open file to be read according to HG_PENDING environment variable | """Open file to be read according to HG_PENDING environment variable | ||||
This opens '.pending' of specified 'filename' only when HG_PENDING | This opens '.pending' of specified 'filename' only when HG_PENDING | ||||
is equal to 'root'. | is equal to 'root'. | ||||
This returns '(fp, is_pending_opened)' tuple. | This returns '(fp, is_pending_opened)' tuple. | ||||
''' | """ | ||||
if mayhavepending(root): | if mayhavepending(root): | ||||
try: | try: | ||||
return (vfs(b'%s.pending' % filename, **kwargs), True) | return (vfs(b'%s.pending' % filename, **kwargs), True) | ||||
except IOError as inst: | except IOError as inst: | ||||
if inst.errno != errno.ENOENT: | if inst.errno != errno.ENOENT: | ||||
raise | raise | ||||
return (vfs(filename, **kwargs), False) | return (vfs(filename, **kwargs), False) |
def walkconfig(self, untrusted=False): | def walkconfig(self, untrusted=False): | ||||
cfg = self._data(untrusted) | cfg = self._data(untrusted) | ||||
for section in cfg.sections(): | for section in cfg.sections(): | ||||
for name, value in self.configitems(section, untrusted): | for name, value in self.configitems(section, untrusted): | ||||
yield section, name, value | yield section, name, value | ||||
def plain(self, feature=None): | def plain(self, feature=None): | ||||
'''is plain mode active? | """is plain mode active? | ||||
Plain mode means that all configuration variables which affect | Plain mode means that all configuration variables which affect | ||||
the behavior and output of Mercurial should be | the behavior and output of Mercurial should be | ||||
ignored. Additionally, the output should be stable, | ignored. Additionally, the output should be stable, | ||||
reproducible and suitable for use in scripts or applications. | reproducible and suitable for use in scripts or applications. | ||||
The only way to trigger plain mode is by setting either the | The only way to trigger plain mode is by setting either the | ||||
`HGPLAIN' or `HGPLAINEXCEPT' environment variables. | `HGPLAIN' or `HGPLAINEXCEPT' environment variables. | ||||
The return value can either be | The return value can either be | ||||
- False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT | - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT | ||||
- False if feature is disabled by default and not included in HGPLAIN | - False if feature is disabled by default and not included in HGPLAIN | ||||
- True otherwise | - True otherwise | ||||
''' | """ | ||||
if ( | if ( | ||||
b'HGPLAIN' not in encoding.environ | b'HGPLAIN' not in encoding.environ | ||||
and b'HGPLAINEXCEPT' not in encoding.environ | and b'HGPLAINEXCEPT' not in encoding.environ | ||||
): | ): | ||||
return False | return False | ||||
exceptions = ( | exceptions = ( | ||||
encoding.environ.get(b'HGPLAINEXCEPT', b'').strip().split(b',') | encoding.environ.get(b'HGPLAINEXCEPT', b'').strip().split(b',') | ||||
) | ) | ||||
return self._colormode is None | return self._colormode is None | ||||
def canbatchlabeledwrites(self): | def canbatchlabeledwrites(self): | ||||
'''check if write calls with labels are batchable''' | '''check if write calls with labels are batchable''' | ||||
# Windows color printing is special, see ``write``. | # Windows color printing is special, see ``write``. | ||||
return self._colormode != b'win32' | return self._colormode != b'win32' | ||||
def write(self, *args, **opts): | def write(self, *args, **opts): | ||||
'''write args to output | """write args to output | ||||
By default, this method simply writes to the buffer or stdout. | By default, this method simply writes to the buffer or stdout. | ||||
Color mode can be set on the UI class to have the output decorated | Color mode can be set on the UI class to have the output decorated | ||||
with color modifier before being written to stdout. | with color modifier before being written to stdout. | ||||
The color used is controlled by an optional keyword argument, "label". | The color used is controlled by an optional keyword argument, "label". | ||||
This should be a string containing label names separated by space. | This should be a string containing label names separated by space. | ||||
Label names take the form of "topic.type". For example, ui.debug() | Label names take the form of "topic.type". For example, ui.debug() | ||||
issues a label of "ui.debug". | issues a label of "ui.debug". | ||||
Progress reports via stderr are normally cleared before writing as | Progress reports via stderr are normally cleared before writing as | ||||
stdout and stderr go to the same terminal. This can be skipped with | stdout and stderr go to the same terminal. This can be skipped with | ||||
the optional keyword argument "keepprogressbar". The progress bar | the optional keyword argument "keepprogressbar". The progress bar | ||||
will continue to occupy a partial line on stderr in that case. | will continue to occupy a partial line on stderr in that case. | ||||
This functionality is intended when Mercurial acts as data source | This functionality is intended when Mercurial acts as data source | ||||
in a pipe. | in a pipe. | ||||
When labeling output for a specific command, a label of | When labeling output for a specific command, a label of | ||||
"cmdname.type" is recommended. For example, status issues | "cmdname.type" is recommended. For example, status issues | ||||
a label of "status.modified" for modified files. | a label of "status.modified" for modified files. | ||||
''' | """ | ||||
dest = self._fout | dest = self._fout | ||||
# inlined _write() for speed | # inlined _write() for speed | ||||
if self._buffers: | if self._buffers: | ||||
label = opts.get('label', b'') | label = opts.get('label', b'') | ||||
if label and self._bufferapplylabels: | if label and self._bufferapplylabels: | ||||
self._buffers[-1].extend(self.label(a, label) for a in args) | self._buffers[-1].extend(self.label(a, label) for a in args) | ||||
else: | else: | ||||
return True | return True | ||||
@property | @property | ||||
def _exithandlers(self): | def _exithandlers(self): | ||||
return _reqexithandlers | return _reqexithandlers | ||||
def atexit(self, func, *args, **kwargs): | def atexit(self, func, *args, **kwargs): | ||||
'''register a function to run after dispatching a request | """register a function to run after dispatching a request | ||||
Handlers do not stay registered across request boundaries.''' | Handlers do not stay registered across request boundaries.""" | ||||
self._exithandlers.append((func, args, kwargs)) | self._exithandlers.append((func, args, kwargs)) | ||||
return func | return func | ||||
def interface(self, feature): | def interface(self, feature): | ||||
"""what interface to use for interactive console features? | """what interface to use for interactive console features? | ||||
The interface is controlled by the value of `ui.interface` but also by | The interface is controlled by the value of `ui.interface` but also by | ||||
the value of feature-specific configuration. For example: | the value of feature-specific configuration. For example: | ||||
ui.interface.histedit = text | ui.interface.histedit = text | ||||
Then histedit will use the text interface and chunkselector will use | Then histedit will use the text interface and chunkselector will use | ||||
the default curses interface (crecord at the moment). | the default curses interface (crecord at the moment). | ||||
""" | """ | ||||
alldefaults = frozenset([b"text", b"curses"]) | alldefaults = frozenset([b"text", b"curses"]) | ||||
featureinterfaces = { | featureinterfaces = { | ||||
b"chunkselector": [b"text", b"curses",], | b"chunkselector": [ | ||||
b"histedit": [b"text", b"curses",], | b"text", | ||||
b"curses", | |||||
], | |||||
b"histedit": [ | |||||
b"text", | |||||
b"curses", | |||||
], | |||||
} | } | ||||
# Feature-specific interface | # Feature-specific interface | ||||
if feature not in featureinterfaces.keys(): | if feature not in featureinterfaces.keys(): | ||||
# Programming error, not user error | # Programming error, not user error | ||||
raise ValueError(b"Unknown feature requested %s" % feature) | raise ValueError(b"Unknown feature requested %s" % feature) | ||||
availableinterfaces = frozenset(featureinterfaces[feature]) | availableinterfaces = frozenset(featureinterfaces[feature]) | ||||
self.warn( | self.warn( | ||||
_(b"invalid value for ui.interface.%s: %s (using %s)\n") | _(b"invalid value for ui.interface.%s: %s (using %s)\n") | ||||
% (feature, f, choseninterface) | % (feature, f, choseninterface) | ||||
) | ) | ||||
return choseninterface | return choseninterface | ||||
def interactive(self): | def interactive(self): | ||||
'''is interactive input allowed? | """is interactive input allowed? | ||||
An interactive session is a session where input can be reasonably read | An interactive session is a session where input can be reasonably read | ||||
from `sys.stdin'. If this function returns false, any attempt to read | from `sys.stdin'. If this function returns false, any attempt to read | ||||
from stdin should fail with an error, unless a sensible default has been | from stdin should fail with an error, unless a sensible default has been | ||||
specified. | specified. | ||||
Interactiveness is triggered by the value of the `ui.interactive' | Interactiveness is triggered by the value of the `ui.interactive' | ||||
configuration variable or - if it is unset - when `sys.stdin' points | configuration variable or - if it is unset - when `sys.stdin' points | ||||
to a terminal device. | to a terminal device. | ||||
This function refers to input only; for output, see `ui.formatted()'. | This function refers to input only; for output, see `ui.formatted()'. | ||||
''' | """ | ||||
i = self.configbool(b"ui", b"interactive") | i = self.configbool(b"ui", b"interactive") | ||||
if i is None: | if i is None: | ||||
# some environments replace stdin without implementing isatty | # some environments replace stdin without implementing isatty | ||||
# usually those are non-interactive | # usually those are non-interactive | ||||
return self._isatty(self._fin) | return self._isatty(self._fin) | ||||
return i | return i | ||||
def termwidth(self): | def termwidth(self): | ||||
'''how wide is the terminal in columns? | """how wide is the terminal in columns?""" | ||||
''' | |||||
if b'COLUMNS' in encoding.environ: | if b'COLUMNS' in encoding.environ: | ||||
try: | try: | ||||
return int(encoding.environ[b'COLUMNS']) | return int(encoding.environ[b'COLUMNS']) | ||||
except ValueError: | except ValueError: | ||||
pass | pass | ||||
return scmutil.termsize(self)[0] | return scmutil.termsize(self)[0] | ||||
def formatted(self): | def formatted(self): | ||||
'''should formatted output be used? | """should formatted output be used? | ||||
It is often desirable to format the output to suite the output medium. | It is often desirable to format the output to suite the output medium. | ||||
Examples of this are truncating long lines or colorizing messages. | Examples of this are truncating long lines or colorizing messages. | ||||
However, this is not often not desirable when piping output into other | However, this is not often not desirable when piping output into other | ||||
utilities, e.g. `grep'. | utilities, e.g. `grep'. | ||||
Formatted output is triggered by the value of the `ui.formatted' | Formatted output is triggered by the value of the `ui.formatted' | ||||
configuration variable or - if it is unset - when `sys.stdout' points | configuration variable or - if it is unset - when `sys.stdout' points | ||||
to a terminal device. Please note that `ui.formatted' should be | to a terminal device. Please note that `ui.formatted' should be | ||||
considered an implementation detail; it is not intended for use outside | considered an implementation detail; it is not intended for use outside | ||||
Mercurial or its extensions. | Mercurial or its extensions. | ||||
This function refers to output only; for input, see `ui.interactive()'. | This function refers to output only; for input, see `ui.interactive()'. | ||||
This function always returns false when in plain mode, see `ui.plain()'. | This function always returns false when in plain mode, see `ui.plain()'. | ||||
''' | """ | ||||
if self.plain(): | if self.plain(): | ||||
return False | return False | ||||
i = self.configbool(b"ui", b"formatted") | i = self.configbool(b"ui", b"formatted") | ||||
if i is None: | if i is None: | ||||
# some environments replace stdout without implementing isatty | # some environments replace stdout without implementing isatty | ||||
# usually those are non-interactive | # usually those are non-interactive | ||||
return self._isatty(self._fout) | return self._isatty(self._fout) | ||||
raise EOFError | raise EOFError | ||||
return l.rstrip(b'\n') | return l.rstrip(b'\n') | ||||
else: | else: | ||||
return encoding.strtolocal(getpass.getpass('')) | return encoding.strtolocal(getpass.getpass('')) | ||||
except EOFError: | except EOFError: | ||||
raise error.ResponseExpected() | raise error.ResponseExpected() | ||||
def status(self, *msg, **opts): | def status(self, *msg, **opts): | ||||
'''write status message to output (if ui.quiet is False) | """write status message to output (if ui.quiet is False) | ||||
This adds an output label of "ui.status". | This adds an output label of "ui.status". | ||||
''' | """ | ||||
if not self.quiet: | if not self.quiet: | ||||
self._writemsg(self._fmsgout, type=b'status', *msg, **opts) | self._writemsg(self._fmsgout, type=b'status', *msg, **opts) | ||||
def warn(self, *msg, **opts): | def warn(self, *msg, **opts): | ||||
'''write warning message to output (stderr) | """write warning message to output (stderr) | ||||
This adds an output label of "ui.warning". | This adds an output label of "ui.warning". | ||||
''' | """ | ||||
self._writemsg(self._fmsgerr, type=b'warning', *msg, **opts) | self._writemsg(self._fmsgerr, type=b'warning', *msg, **opts) | ||||
def error(self, *msg, **opts): | def error(self, *msg, **opts): | ||||
'''write error message to output (stderr) | """write error message to output (stderr) | ||||
This adds an output label of "ui.error". | This adds an output label of "ui.error". | ||||
''' | """ | ||||
self._writemsg(self._fmsgerr, type=b'error', *msg, **opts) | self._writemsg(self._fmsgerr, type=b'error', *msg, **opts) | ||||
def note(self, *msg, **opts): | def note(self, *msg, **opts): | ||||
'''write note to output (if ui.verbose is True) | """write note to output (if ui.verbose is True) | ||||
This adds an output label of "ui.note". | This adds an output label of "ui.note". | ||||
''' | """ | ||||
if self.verbose: | if self.verbose: | ||||
self._writemsg(self._fmsgout, type=b'note', *msg, **opts) | self._writemsg(self._fmsgout, type=b'note', *msg, **opts) | ||||
def debug(self, *msg, **opts): | def debug(self, *msg, **opts): | ||||
'''write debug message to output (if ui.debugflag is True) | """write debug message to output (if ui.debugflag is True) | ||||
This adds an output label of "ui.debug". | This adds an output label of "ui.debug". | ||||
''' | """ | ||||
if self.debugflag: | if self.debugflag: | ||||
self._writemsg(self._fmsgout, type=b'debug', *msg, **opts) | self._writemsg(self._fmsgout, type=b'debug', *msg, **opts) | ||||
self.log(b'debug', b'%s', b''.join(msg)) | self.log(b'debug', b'%s', b''.join(msg)) | ||||
# Aliases to defeat check-code. | # Aliases to defeat check-code. | ||||
statusnoi18n = status | statusnoi18n = status | ||||
notenoi18n = note | notenoi18n = note | ||||
warnnoi18n = warn | warnnoi18n = warn | ||||
self, | self, | ||||
cmd, | cmd, | ||||
environ=None, | environ=None, | ||||
cwd=None, | cwd=None, | ||||
onerr=None, | onerr=None, | ||||
errprefix=None, | errprefix=None, | ||||
blockedtag=None, | blockedtag=None, | ||||
): | ): | ||||
'''execute shell command with appropriate output stream. command | """execute shell command with appropriate output stream. command | ||||
output will be redirected if fout is not stdout. | output will be redirected if fout is not stdout. | ||||
if command fails and onerr is None, return status, else raise onerr | if command fails and onerr is None, return status, else raise onerr | ||||
object as exception. | object as exception. | ||||
''' | """ | ||||
if blockedtag is None: | if blockedtag is None: | ||||
# Long cmds tend to be because of an absolute path on cmd. Keep | # Long cmds tend to be because of an absolute path on cmd. Keep | ||||
# the tail end instead | # the tail end instead | ||||
cmdsuffix = cmd.translate(None, _keepalnum)[-85:] | cmdsuffix = cmd.translate(None, _keepalnum)[-85:] | ||||
blockedtag = b'unknown_system_' + cmdsuffix | blockedtag = b'unknown_system_' + cmdsuffix | ||||
out = self._fout | out = self._fout | ||||
if any(s[1] for s in self._bufferstates): | if any(s[1] for s in self._bufferstates): | ||||
out = self | out = self | ||||
return rc | return rc | ||||
def _runsystem(self, cmd, environ, cwd, out): | def _runsystem(self, cmd, environ, cwd, out): | ||||
"""actually execute the given shell command (can be overridden by | """actually execute the given shell command (can be overridden by | ||||
extensions like chg)""" | extensions like chg)""" | ||||
return procutil.system(cmd, environ=environ, cwd=cwd, out=out) | return procutil.system(cmd, environ=environ, cwd=cwd, out=out) | ||||
def traceback(self, exc=None, force=False): | def traceback(self, exc=None, force=False): | ||||
'''print exception traceback if traceback printing enabled or forced. | """print exception traceback if traceback printing enabled or forced. | ||||
only to call in exception handler. returns true if traceback | only to call in exception handler. returns true if traceback | ||||
printed.''' | printed.""" | ||||
if self.tracebackflag or force: | if self.tracebackflag or force: | ||||
if exc is None: | if exc is None: | ||||
exc = sys.exc_info() | exc = sys.exc_info() | ||||
cause = getattr(exc[1], 'cause', None) | cause = getattr(exc[1], 'cause', None) | ||||
if cause is not None: | if cause is not None: | ||||
causetb = traceback.format_tb(cause[2]) | causetb = traceback.format_tb(cause[2]) | ||||
exctb = traceback.format_tb(exc[2]) | exctb = traceback.format_tb(exc[2]) | ||||
"""Install logger which can be identified later by the given name | """Install logger which can be identified later by the given name | ||||
More than one loggers can be registered. Use extension or module | More than one loggers can be registered. Use extension or module | ||||
name to uniquely identify the logger instance. | name to uniquely identify the logger instance. | ||||
""" | """ | ||||
self._loggers[name] = logger | self._loggers[name] = logger | ||||
def log(self, event, msgfmt, *msgargs, **opts): | def log(self, event, msgfmt, *msgargs, **opts): | ||||
'''hook for logging facility extensions | """hook for logging facility extensions | ||||
event should be a readily-identifiable subsystem, which will | event should be a readily-identifiable subsystem, which will | ||||
allow filtering. | allow filtering. | ||||
msgfmt should be a newline-terminated format string to log, and | msgfmt should be a newline-terminated format string to log, and | ||||
*msgargs are %-formatted into it. | *msgargs are %-formatted into it. | ||||
**opts currently has no defined meanings. | **opts currently has no defined meanings. | ||||
''' | """ | ||||
if not self._loggers: | if not self._loggers: | ||||
return | return | ||||
activeloggers = [ | activeloggers = [ | ||||
l for l in pycompat.itervalues(self._loggers) if l.tracked(event) | l for l in pycompat.itervalues(self._loggers) if l.tracked(event) | ||||
] | ] | ||||
if not activeloggers: | if not activeloggers: | ||||
return | return | ||||
msg = msgfmt % msgargs | msg = msgfmt % msgargs | ||||
opts = pycompat.byteskwargs(opts) | opts = pycompat.byteskwargs(opts) | ||||
# guard against recursion from e.g. ui.debug() | # guard against recursion from e.g. ui.debug() | ||||
registeredloggers = self._loggers | registeredloggers = self._loggers | ||||
self._loggers = {} | self._loggers = {} | ||||
try: | try: | ||||
for logger in activeloggers: | for logger in activeloggers: | ||||
logger.log(self, event, msg, opts) | logger.log(self, event, msg, opts) | ||||
finally: | finally: | ||||
self._loggers = registeredloggers | self._loggers = registeredloggers | ||||
def label(self, msg, label): | def label(self, msg, label): | ||||
'''style msg based on supplied label | """style msg based on supplied label | ||||
If some color mode is enabled, this will add the necessary control | If some color mode is enabled, this will add the necessary control | ||||
characters to apply such color. In addition, 'debug' color mode adds | characters to apply such color. In addition, 'debug' color mode adds | ||||
markup showing which label affects a piece of text. | markup showing which label affects a piece of text. | ||||
ui.write(s, 'label') is equivalent to | ui.write(s, 'label') is equivalent to | ||||
ui.write(ui.label(s, 'label')). | ui.write(ui.label(s, 'label')). | ||||
''' | """ | ||||
if self._colormode is not None: | if self._colormode is not None: | ||||
return color.colorlabel(self, msg, label) | return color.colorlabel(self, msg, label) | ||||
return msg | return msg | ||||
def develwarn(self, msg, stacklevel=1, config=None): | def develwarn(self, msg, stacklevel=1, config=None): | ||||
"""issue a developer warning message | """issue a developer warning message | ||||
Use 'stacklevel' to report the offender some layers further up in the | Use 'stacklevel' to report the offender some layers further up in the |
httplib = util.httplib | httplib = util.httplib | ||||
stringio = util.stringio | stringio = util.stringio | ||||
urlerr = util.urlerr | urlerr = util.urlerr | ||||
urlreq = util.urlreq | urlreq = util.urlreq | ||||
def escape(s, quote=None): | def escape(s, quote=None): | ||||
'''Replace special characters "&", "<" and ">" to HTML-safe sequences. | """Replace special characters "&", "<" and ">" to HTML-safe sequences. | ||||
If the optional flag quote is true, the quotation mark character (") | If the optional flag quote is true, the quotation mark character (") | ||||
is also translated. | is also translated. | ||||
This is the same as cgi.escape in Python, but always operates on | This is the same as cgi.escape in Python, but always operates on | ||||
bytes, whereas cgi.escape in Python 3 only works on unicodes. | bytes, whereas cgi.escape in Python 3 only works on unicodes. | ||||
''' | """ | ||||
s = s.replace(b"&", b"&") | s = s.replace(b"&", b"&") | ||||
s = s.replace(b"<", b"<") | s = s.replace(b"<", b"<") | ||||
s = s.replace(b">", b">") | s = s.replace(b">", b">") | ||||
if quote: | if quote: | ||||
s = s.replace(b'"', b""") | s = s.replace(b'"', b""") | ||||
return s | return s | ||||
ui, | ui, | ||||
authinfo=None, | authinfo=None, | ||||
useragent=None, | useragent=None, | ||||
loggingfh=None, | loggingfh=None, | ||||
loggingname=b's', | loggingname=b's', | ||||
loggingopts=None, | loggingopts=None, | ||||
sendaccept=True, | sendaccept=True, | ||||
): | ): | ||||
''' | """ | ||||
construct an opener suitable for urllib2 | construct an opener suitable for urllib2 | ||||
authinfo will be added to the password manager | authinfo will be added to the password manager | ||||
The opener can be configured to log socket events if the various | The opener can be configured to log socket events if the various | ||||
``logging*`` arguments are specified. | ``logging*`` arguments are specified. | ||||
``loggingfh`` denotes a file object to log events to. | ``loggingfh`` denotes a file object to log events to. | ||||
``loggingname`` denotes the name of the to print when logging. | ``loggingname`` denotes the name of the to print when logging. | ||||
``loggingopts`` is a dict of keyword arguments to pass to the constructed | ``loggingopts`` is a dict of keyword arguments to pass to the constructed | ||||
``util.socketobserver`` instance. | ``util.socketobserver`` instance. | ||||
``sendaccept`` allows controlling whether the ``Accept`` request header | ``sendaccept`` allows controlling whether the ``Accept`` request header | ||||
is sent. The header is sent by default. | is sent. The header is sent by default. | ||||
''' | """ | ||||
timeout = ui.configwith(float, b'http', b'timeout') | timeout = ui.configwith(float, b'http', b'timeout') | ||||
handlers = [] | handlers = [] | ||||
if loggingfh: | if loggingfh: | ||||
handlers.append( | handlers.append( | ||||
logginghttphandler( | logginghttphandler( | ||||
loggingfh, loggingname, loggingopts or {}, timeout=timeout | loggingfh, loggingname, loggingopts or {}, timeout=timeout | ||||
) | ) |
b"ProxyHandler", | b"ProxyHandler", | ||||
b"Request", | b"Request", | ||||
b"url2pathname", | b"url2pathname", | ||||
b"urlopen", | b"urlopen", | ||||
), | ), | ||||
) | ) | ||||
import urllib.response | import urllib.response | ||||
urlreq._registeraliases(urllib.response, (b"addclosehook", b"addinfourl",)) | urlreq._registeraliases( | ||||
urllib.response, | |||||
( | |||||
b"addclosehook", | |||||
b"addinfourl", | |||||
), | |||||
) | |||||
import urllib.error | import urllib.error | ||||
urlerr._registeraliases(urllib.error, (b"HTTPError", b"URLError",)) | urlerr._registeraliases( | ||||
urllib.error, | |||||
( | |||||
b"HTTPError", | |||||
b"URLError", | |||||
), | |||||
) | |||||
import http.server | import http.server | ||||
httpserver._registeraliases( | httpserver._registeraliases( | ||||
http.server, | http.server, | ||||
( | ( | ||||
b"HTTPServer", | b"HTTPServer", | ||||
b"BaseHTTPRequestHandler", | b"BaseHTTPRequestHandler", | ||||
b"SimpleHTTPRequestHandler", | b"SimpleHTTPRequestHandler", | ||||
b"HTTPPasswordMgrWithDefaultRealm", | b"HTTPPasswordMgrWithDefaultRealm", | ||||
b"HTTPSHandler", | b"HTTPSHandler", | ||||
b"install_opener", | b"install_opener", | ||||
b"ProxyHandler", | b"ProxyHandler", | ||||
b"Request", | b"Request", | ||||
b"urlopen", | b"urlopen", | ||||
), | ), | ||||
) | ) | ||||
urlreq._registeraliases(urlparse, (b"urlparse", b"urlunparse",)) | urlreq._registeraliases( | ||||
urlparse, | |||||
( | |||||
b"urlparse", | |||||
b"urlunparse", | |||||
), | |||||
) | |||||
urlreq._registeralias(urlparse, b"parse_qs", b"parseqs") | urlreq._registeralias(urlparse, b"parse_qs", b"parseqs") | ||||
urlreq._registeralias(urlparse, b"parse_qsl", b"parseqsl") | urlreq._registeralias(urlparse, b"parse_qsl", b"parseqsl") | ||||
urlerr._registeraliases(urllib2, (b"HTTPError", b"URLError",)) | urlerr._registeraliases( | ||||
urllib2, | |||||
( | |||||
b"HTTPError", | |||||
b"URLError", | |||||
), | |||||
) | |||||
httpserver._registeraliases( | httpserver._registeraliases( | ||||
BaseHTTPServer, (b"HTTPServer", b"BaseHTTPRequestHandler",) | BaseHTTPServer, | ||||
( | |||||
b"HTTPServer", | |||||
b"BaseHTTPRequestHandler", | |||||
), | |||||
) | ) | ||||
httpserver._registeraliases( | httpserver._registeraliases( | ||||
SimpleHTTPServer, (b"SimpleHTTPRequestHandler",) | SimpleHTTPServer, (b"SimpleHTTPRequestHandler",) | ||||
) | ) | ||||
httpserver._registeraliases(CGIHTTPServer, (b"CGIHTTPRequestHandler",)) | httpserver._registeraliases(CGIHTTPServer, (b"CGIHTTPRequestHandler",)) | ||||
def gethost(req): | def gethost(req): | ||||
return req.get_host() | return req.get_host() |
def copy(self): | def copy(self): | ||||
"""always do a cheap copy""" | """always do a cheap copy""" | ||||
self._copied = getattr(self, '_copied', 0) + 1 | self._copied = getattr(self, '_copied', 0) + 1 | ||||
return self | return self | ||||
class sortdict(collections.OrderedDict): | class sortdict(collections.OrderedDict): | ||||
'''a simple sorted dictionary | """a simple sorted dictionary | ||||
>>> d1 = sortdict([(b'a', 0), (b'b', 1)]) | >>> d1 = sortdict([(b'a', 0), (b'b', 1)]) | ||||
>>> d2 = d1.copy() | >>> d2 = d1.copy() | ||||
>>> d2 | >>> d2 | ||||
sortdict([('a', 0), ('b', 1)]) | sortdict([('a', 0), ('b', 1)]) | ||||
>>> d2.update([(b'a', 2)]) | >>> d2.update([(b'a', 2)]) | ||||
>>> list(d2.keys()) # should still be in last-set order | >>> list(d2.keys()) # should still be in last-set order | ||||
['b', 'a'] | ['b', 'a'] | ||||
>>> d1.insert(1, b'a.5', 0.5) | >>> d1.insert(1, b'a.5', 0.5) | ||||
>>> d1 | >>> d1 | ||||
sortdict([('a', 0), ('a.5', 0.5), ('b', 1)]) | sortdict([('a', 0), ('a.5', 0.5), ('b', 1)]) | ||||
''' | """ | ||||
def __setitem__(self, key, value): | def __setitem__(self, key, value): | ||||
if key in self: | if key in self: | ||||
del self[key] | del self[key] | ||||
super(sortdict, self).__setitem__(key, value) | super(sortdict, self).__setitem__(key, value) | ||||
if pycompat.ispypy: | if pycompat.ispypy: | ||||
# __setitem__() isn't called as of PyPy 5.8.0 | # __setitem__() isn't called as of PyPy 5.8.0 | ||||
def clearcachedproperty(obj, prop): | def clearcachedproperty(obj, prop): | ||||
'''clear a cached property value, if one has been set''' | '''clear a cached property value, if one has been set''' | ||||
prop = pycompat.sysstr(prop) | prop = pycompat.sysstr(prop) | ||||
if prop in obj.__dict__: | if prop in obj.__dict__: | ||||
del obj.__dict__[prop] | del obj.__dict__[prop] | ||||
def increasingchunks(source, min=1024, max=65536): | def increasingchunks(source, min=1024, max=65536): | ||||
'''return no less than min bytes per chunk while data remains, | """return no less than min bytes per chunk while data remains, | ||||
doubling min after each chunk until it reaches max''' | doubling min after each chunk until it reaches max""" | ||||
def log2(x): | def log2(x): | ||||
if not x: | if not x: | ||||
return 0 | return 0 | ||||
i = 0 | i = 0 | ||||
while x: | while x: | ||||
x >>= 1 | x >>= 1 | ||||
i += 1 | i += 1 | ||||
if pycompat.ispypy: | if pycompat.ispypy: | ||||
# PyPy runs slower with gc disabled | # PyPy runs slower with gc disabled | ||||
nogc = lambda x: x | nogc = lambda x: x | ||||
def pathto(root, n1, n2): | def pathto(root, n1, n2): | ||||
'''return the relative path from one place to another. | """return the relative path from one place to another. | ||||
root should use os.sep to separate directories | root should use os.sep to separate directories | ||||
n1 should use os.sep to separate directories | n1 should use os.sep to separate directories | ||||
n2 should use "/" to separate directories | n2 should use "/" to separate directories | ||||
returns an os.sep-separated path. | returns an os.sep-separated path. | ||||
If n1 is a relative path, it's assumed it's | If n1 is a relative path, it's assumed it's | ||||
relative to root. | relative to root. | ||||
n2 should always be relative to root. | n2 should always be relative to root. | ||||
''' | """ | ||||
if not n1: | if not n1: | ||||
return localpath(n2) | return localpath(n2) | ||||
if os.path.isabs(n1): | if os.path.isabs(n1): | ||||
if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]: | if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]: | ||||
return os.path.join(root, localpath(n2)) | return os.path.join(root, localpath(n2)) | ||||
n2 = b'/'.join((pconvert(root), n2)) | n2 = b'/'.join((pconvert(root), n2)) | ||||
a, b = splitpath(n1), n2.split(b'/') | a, b = splitpath(n1), n2.split(b'/') | ||||
a.reverse() | a.reverse() | ||||
b'tmpfs', | b'tmpfs', | ||||
b'ufs', | b'ufs', | ||||
b'xfs', | b'xfs', | ||||
b'zfs', | b'zfs', | ||||
} | } | ||||
def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False): | def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False): | ||||
'''copy a file, preserving mode and optionally other stat info like | """copy a file, preserving mode and optionally other stat info like | ||||
atime/mtime | atime/mtime | ||||
checkambig argument is used with filestat, and is useful only if | checkambig argument is used with filestat, and is useful only if | ||||
destination file is guarded by any lock (e.g. repo.lock or | destination file is guarded by any lock (e.g. repo.lock or | ||||
repo.wlock). | repo.wlock). | ||||
copystat and checkambig should be exclusive. | copystat and checkambig should be exclusive. | ||||
''' | """ | ||||
assert not (copystat and checkambig) | assert not (copystat and checkambig) | ||||
oldstat = None | oldstat = None | ||||
if os.path.lexists(dest): | if os.path.lexists(dest): | ||||
if checkambig: | if checkambig: | ||||
oldstat = checkambig and filestat.frompath(dest) | oldstat = checkambig and filestat.frompath(dest) | ||||
unlink(dest) | unlink(dest) | ||||
if hardlink: | if hardlink: | ||||
# Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks | # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks | ||||
b'lpt7', | b'lpt7', | ||||
b'lpt8', | b'lpt8', | ||||
b'lpt9', | b'lpt9', | ||||
} | } | ||||
_winreservedchars = b':*?"<>|' | _winreservedchars = b':*?"<>|' | ||||
def checkwinfilename(path): | def checkwinfilename(path): | ||||
r'''Check that the base-relative path is a valid filename on Windows. | r"""Check that the base-relative path is a valid filename on Windows. | ||||
Returns None if the path is ok, or a UI string describing the problem. | Returns None if the path is ok, or a UI string describing the problem. | ||||
>>> checkwinfilename(b"just/a/normal/path") | >>> checkwinfilename(b"just/a/normal/path") | ||||
>>> checkwinfilename(b"foo/bar/con.xml") | >>> checkwinfilename(b"foo/bar/con.xml") | ||||
"filename contains 'con', which is reserved on Windows" | "filename contains 'con', which is reserved on Windows" | ||||
>>> checkwinfilename(b"foo/con.xml/bar") | >>> checkwinfilename(b"foo/con.xml/bar") | ||||
"filename contains 'con', which is reserved on Windows" | "filename contains 'con', which is reserved on Windows" | ||||
>>> checkwinfilename(b"foo/bar/xml.con") | >>> checkwinfilename(b"foo/bar/xml.con") | ||||
>>> checkwinfilename(b"foo/bar/AUX/bla.txt") | >>> checkwinfilename(b"foo/bar/AUX/bla.txt") | ||||
"filename contains 'AUX', which is reserved on Windows" | "filename contains 'AUX', which is reserved on Windows" | ||||
>>> checkwinfilename(b"foo/bar/bla:.txt") | >>> checkwinfilename(b"foo/bar/bla:.txt") | ||||
"filename contains ':', which is reserved on Windows" | "filename contains ':', which is reserved on Windows" | ||||
>>> checkwinfilename(b"foo/bar/b\07la.txt") | >>> checkwinfilename(b"foo/bar/b\07la.txt") | ||||
"filename contains '\\x07', which is invalid on Windows" | "filename contains '\\x07', which is invalid on Windows" | ||||
>>> checkwinfilename(b"foo/bar/bla ") | >>> checkwinfilename(b"foo/bar/bla ") | ||||
"filename ends with ' ', which is not allowed on Windows" | "filename ends with ' ', which is not allowed on Windows" | ||||
>>> checkwinfilename(b"../bar") | >>> checkwinfilename(b"../bar") | ||||
>>> checkwinfilename(b"foo\\") | >>> checkwinfilename(b"foo\\") | ||||
"filename ends with '\\', which is invalid on Windows" | "filename ends with '\\', which is invalid on Windows" | ||||
>>> checkwinfilename(b"foo\\/bar") | >>> checkwinfilename(b"foo\\/bar") | ||||
"directory name ends with '\\', which is invalid on Windows" | "directory name ends with '\\', which is invalid on Windows" | ||||
''' | """ | ||||
if path.endswith(b'\\'): | if path.endswith(b'\\'): | ||||
return _(b"filename ends with '\\', which is invalid on Windows") | return _(b"filename ends with '\\', which is invalid on Windows") | ||||
if b'\\/' in path: | if b'\\/' in path: | ||||
return _(b"directory name ends with '\\', which is invalid on Windows") | return _(b"directory name ends with '\\', which is invalid on Windows") | ||||
for n in path.replace(b'\\', b'/').split(b'/'): | for n in path.replace(b'\\', b'/').split(b'/'): | ||||
if not n: | if not n: | ||||
continue | continue | ||||
for c in _filenamebytestr(n): | for c in _filenamebytestr(n): | ||||
global _re2 | global _re2 | ||||
try: | try: | ||||
# check if match works, see issue3964 | # check if match works, see issue3964 | ||||
_re2 = bool(re2.match(r'\[([^\[]+)\]', b'[ui]')) | _re2 = bool(re2.match(r'\[([^\[]+)\]', b'[ui]')) | ||||
except ImportError: | except ImportError: | ||||
_re2 = False | _re2 = False | ||||
def compile(self, pat, flags=0): | def compile(self, pat, flags=0): | ||||
'''Compile a regular expression, using re2 if possible | """Compile a regular expression, using re2 if possible | ||||
For best performance, use only re2-compatible regexp features. The | For best performance, use only re2-compatible regexp features. The | ||||
only flags from the re module that are re2-compatible are | only flags from the re module that are re2-compatible are | ||||
IGNORECASE and MULTILINE.''' | IGNORECASE and MULTILINE.""" | ||||
if _re2 is None: | if _re2 is None: | ||||
self._checkre2() | self._checkre2() | ||||
if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0: | if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0: | ||||
if flags & remod.IGNORECASE: | if flags & remod.IGNORECASE: | ||||
pat = b'(?i)' + pat | pat = b'(?i)' + pat | ||||
if flags & remod.MULTILINE: | if flags & remod.MULTILINE: | ||||
pat = b'(?m)' + pat | pat = b'(?m)' + pat | ||||
try: | try: | ||||
return re2.compile(pat) | return re2.compile(pat) | ||||
except re2.error: | except re2.error: | ||||
pass | pass | ||||
return remod.compile(pat, flags) | return remod.compile(pat, flags) | ||||
@propertycache | @propertycache | ||||
def escape(self): | def escape(self): | ||||
'''Return the version of escape corresponding to self.compile. | """Return the version of escape corresponding to self.compile. | ||||
This is imperfect because whether re2 or re is used for a particular | This is imperfect because whether re2 or re is used for a particular | ||||
function depends on the flags, etc, but it's the best we can do. | function depends on the flags, etc, but it's the best we can do. | ||||
''' | """ | ||||
global _re2 | global _re2 | ||||
if _re2 is None: | if _re2 is None: | ||||
self._checkre2() | self._checkre2() | ||||
if _re2: | if _re2: | ||||
return re2.escape | return re2.escape | ||||
else: | else: | ||||
return remod.escape | return remod.escape | ||||
re = _re() | re = _re() | ||||
_fspathcache = {} | _fspathcache = {} | ||||
def fspath(name, root): | def fspath(name, root): | ||||
'''Get name in the case stored in the filesystem | """Get name in the case stored in the filesystem | ||||
The name should be relative to root, and be normcase-ed for efficiency. | The name should be relative to root, and be normcase-ed for efficiency. | ||||
Note that this function is unnecessary, and should not be | Note that this function is unnecessary, and should not be | ||||
called, for case-sensitive filesystems (simply because it's expensive). | called, for case-sensitive filesystems (simply because it's expensive). | ||||
The root should be normcase-ed, too. | The root should be normcase-ed, too. | ||||
''' | """ | ||||
def _makefspathcacheentry(dir): | def _makefspathcacheentry(dir): | ||||
return {normcase(n): n for n in os.listdir(dir)} | return {normcase(n): n for n in os.listdir(dir)} | ||||
seps = pycompat.ossep | seps = pycompat.ossep | ||||
if pycompat.osaltsep: | if pycompat.osaltsep: | ||||
seps = seps + pycompat.osaltsep | seps = seps + pycompat.osaltsep | ||||
# Protect backslashes. This gets silly very quickly. | # Protect backslashes. This gets silly very quickly. | ||||
return ( | return ( | ||||
path.endswith(pycompat.ossep) | path.endswith(pycompat.ossep) | ||||
or pycompat.osaltsep | or pycompat.osaltsep | ||||
and path.endswith(pycompat.osaltsep) | and path.endswith(pycompat.osaltsep) | ||||
) | ) | ||||
def splitpath(path): | def splitpath(path): | ||||
'''Split path by os.sep. | """Split path by os.sep. | ||||
Note that this function does not use os.altsep because this is | Note that this function does not use os.altsep because this is | ||||
an alternative of simple "xxx.split(os.sep)". | an alternative of simple "xxx.split(os.sep)". | ||||
It is recommended to use os.path.normpath() before using this | It is recommended to use os.path.normpath() before using this | ||||
function if need.''' | function if need.""" | ||||
return path.split(pycompat.ossep) | return path.split(pycompat.ossep) | ||||
def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False): | def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False): | ||||
"""Create a temporary file with the same contents from name | """Create a temporary file with the same contents from name | ||||
The permission bits are copied from the original file. | The permission bits are copied from the original file. | ||||
raise | raise | ||||
return True | return True | ||||
def __ne__(self, other): | def __ne__(self, other): | ||||
return not self == other | return not self == other | ||||
class atomictempfile(object): | class atomictempfile(object): | ||||
'''writable file object that atomically updates a file | """writable file object that atomically updates a file | ||||
All writes will go to a temporary copy of the original file. Call | All writes will go to a temporary copy of the original file. Call | ||||
close() when you are done writing, and atomictempfile will rename | close() when you are done writing, and atomictempfile will rename | ||||
the temporary copy to the original name, making the changes | the temporary copy to the original name, making the changes | ||||
visible. If the object is destroyed without being closed, all your | visible. If the object is destroyed without being closed, all your | ||||
writes are discarded. | writes are discarded. | ||||
checkambig argument of constructor is used with filestat, and is | checkambig argument of constructor is used with filestat, and is | ||||
useful only if target file is guarded by any lock (e.g. repo.lock | useful only if target file is guarded by any lock (e.g. repo.lock | ||||
or repo.wlock). | or repo.wlock). | ||||
''' | """ | ||||
def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False): | def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False): | ||||
self.__name = name # permanent name | self.__name = name # permanent name | ||||
self._tempname = mktempcopy( | self._tempname = mktempcopy( | ||||
name, | name, | ||||
emptyok=(b'w' in mode), | emptyok=(b'w' in mode), | ||||
createmode=createmode, | createmode=createmode, | ||||
enforcewritable=(b'w' in mode), | enforcewritable=(b'w' in mode), | ||||
timing_stats.elapsed = timer() - timing_stats.start | timing_stats.elapsed = timer() - timing_stats.start | ||||
timedcm._nested -= 1 | timedcm._nested -= 1 | ||||
timedcm._nested = 0 | timedcm._nested = 0 | ||||
def timed(func): | def timed(func): | ||||
'''Report the execution time of a function call to stderr. | """Report the execution time of a function call to stderr. | ||||
During development, use as a decorator when you need to measure | During development, use as a decorator when you need to measure | ||||
the cost of a function, e.g. as follows: | the cost of a function, e.g. as follows: | ||||
@util.timed | @util.timed | ||||
def foo(a, b, c): | def foo(a, b, c): | ||||
pass | pass | ||||
''' | """ | ||||
def wrapper(*args, **kwargs): | def wrapper(*args, **kwargs): | ||||
with timedcm(pycompat.bytestr(func.__name__)) as time_stats: | with timedcm(pycompat.bytestr(func.__name__)) as time_stats: | ||||
result = func(*args, **kwargs) | result = func(*args, **kwargs) | ||||
stderr = procutil.stderr | stderr = procutil.stderr | ||||
stderr.write( | stderr.write( | ||||
b'%s%s: %s\n' | b'%s%s: %s\n' | ||||
% ( | % ( | ||||
(b'kb', 2 ** 10), | (b'kb', 2 ** 10), | ||||
(b'mb', 2 ** 20), | (b'mb', 2 ** 20), | ||||
(b'gb', 2 ** 30), | (b'gb', 2 ** 30), | ||||
(b'b', 1), | (b'b', 1), | ||||
) | ) | ||||
def sizetoint(s): | def sizetoint(s): | ||||
'''Convert a space specifier to a byte count. | """Convert a space specifier to a byte count. | ||||
>>> sizetoint(b'30') | >>> sizetoint(b'30') | ||||
30 | 30 | ||||
>>> sizetoint(b'2.2kb') | >>> sizetoint(b'2.2kb') | ||||
2252 | 2252 | ||||
>>> sizetoint(b'6M') | >>> sizetoint(b'6M') | ||||
6291456 | 6291456 | ||||
''' | """ | ||||
t = s.strip().lower() | t = s.strip().lower() | ||||
try: | try: | ||||
for k, u in _sizeunits: | for k, u in _sizeunits: | ||||
if t.endswith(k): | if t.endswith(k): | ||||
return int(float(t[: -len(k)]) * u) | return int(float(t[: -len(k)]) * u) | ||||
return int(t) | return int(t) | ||||
except ValueError: | except ValueError: | ||||
raise error.ParseError(_(b"couldn't parse size: %s") % s) | raise error.ParseError(_(b"couldn't parse size: %s") % s) | ||||
class hooks(object): | class hooks(object): | ||||
'''A collection of hook functions that can be used to extend a | """A collection of hook functions that can be used to extend a | ||||
function's behavior. Hooks are called in lexicographic order, | function's behavior. Hooks are called in lexicographic order, | ||||
based on the names of their sources.''' | based on the names of their sources.""" | ||||
def __init__(self): | def __init__(self): | ||||
self._hooks = [] | self._hooks = [] | ||||
def add(self, source, hook): | def add(self, source, hook): | ||||
self._hooks.append((source, hook)) | self._hooks.append((source, hook)) | ||||
def __call__(self, *args): | def __call__(self, *args): | ||||
self._hooks.sort(key=lambda x: x[0]) | self._hooks.sort(key=lambda x: x[0]) | ||||
results = [] | results = [] | ||||
for source, hook in self._hooks: | for source, hook in self._hooks: | ||||
results.append(hook(*args)) | results.append(hook(*args)) | ||||
return results | return results | ||||
def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0): | def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0): | ||||
'''Yields lines for a nicely formatted stacktrace. | """Yields lines for a nicely formatted stacktrace. | ||||
Skips the 'skip' last entries, then return the last 'depth' entries. | Skips the 'skip' last entries, then return the last 'depth' entries. | ||||
Each file+linenumber is formatted according to fileline. | Each file+linenumber is formatted according to fileline. | ||||
Each line is formatted according to line. | Each line is formatted according to line. | ||||
If line is None, it yields: | If line is None, it yields: | ||||
length of longest filepath+line number, | length of longest filepath+line number, | ||||
filepath+linenumber, | filepath+linenumber, | ||||
function | function | ||||
Not be used in production code but very convenient while developing. | Not be used in production code but very convenient while developing. | ||||
''' | """ | ||||
entries = [ | entries = [ | ||||
(fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func)) | (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func)) | ||||
for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1] | for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1] | ||||
][-depth:] | ][-depth:] | ||||
if entries: | if entries: | ||||
fnmax = max(len(entry[0]) for entry in entries) | fnmax = max(len(entry[0]) for entry in entries) | ||||
for fnln, func in entries: | for fnln, func in entries: | ||||
if line is None: | if line is None: | ||||
yield (fnmax, fnln, func) | yield (fnmax, fnln, func) | ||||
else: | else: | ||||
yield line % (fnmax, fnln, func) | yield line % (fnmax, fnln, func) | ||||
def debugstacktrace( | def debugstacktrace( | ||||
msg=b'stacktrace', | msg=b'stacktrace', | ||||
skip=0, | skip=0, | ||||
f=procutil.stderr, | f=procutil.stderr, | ||||
otherf=procutil.stdout, | otherf=procutil.stdout, | ||||
depth=0, | depth=0, | ||||
prefix=b'', | prefix=b'', | ||||
): | ): | ||||
'''Writes a message to f (stderr) with a nicely formatted stacktrace. | """Writes a message to f (stderr) with a nicely formatted stacktrace. | ||||
Skips the 'skip' entries closest to the call, then show 'depth' entries. | Skips the 'skip' entries closest to the call, then show 'depth' entries. | ||||
By default it will flush stdout first. | By default it will flush stdout first. | ||||
It can be used everywhere and intentionally does not require an ui object. | It can be used everywhere and intentionally does not require an ui object. | ||||
Not be used in production code but very convenient while developing. | Not be used in production code but very convenient while developing. | ||||
''' | """ | ||||
if otherf: | if otherf: | ||||
otherf.flush() | otherf.flush() | ||||
f.write(b'%s%s at:\n' % (prefix, msg.rstrip())) | f.write(b'%s%s at:\n' % (prefix, msg.rstrip())) | ||||
for line in getstackframes(skip + 1, depth=depth): | for line in getstackframes(skip + 1, depth=depth): | ||||
f.write(prefix + line) | f.write(prefix + line) | ||||
f.flush() | f.flush() | ||||
# complex value to deal with. | # complex value to deal with. | ||||
if self._state == self._STATE_NONE: | if self._state == self._STATE_NONE: | ||||
# A normal value. | # A normal value. | ||||
if special == SPECIAL_NONE: | if special == SPECIAL_NONE: | ||||
self._decodedvalues.append(value) | self._decodedvalues.append(value) | ||||
elif special == SPECIAL_START_ARRAY: | elif special == SPECIAL_START_ARRAY: | ||||
self._collectionstack.append( | self._collectionstack.append( | ||||
{b'remaining': value, b'v': [],} | { | ||||
b'remaining': value, | |||||
b'v': [], | |||||
} | |||||
) | ) | ||||
self._state = self._STATE_WANT_ARRAY_VALUE | self._state = self._STATE_WANT_ARRAY_VALUE | ||||
elif special == SPECIAL_START_MAP: | elif special == SPECIAL_START_MAP: | ||||
self._collectionstack.append( | self._collectionstack.append( | ||||
{b'remaining': value, b'v': {},} | { | ||||
b'remaining': value, | |||||
b'v': {}, | |||||
} | |||||
) | ) | ||||
self._state = self._STATE_WANT_MAP_KEY | self._state = self._STATE_WANT_MAP_KEY | ||||
elif special == SPECIAL_START_SET: | elif special == SPECIAL_START_SET: | ||||
self._collectionstack.append( | self._collectionstack.append( | ||||
{b'remaining': value, b'v': set(),} | { | ||||
b'remaining': value, | |||||
b'v': set(), | |||||
} | |||||
) | ) | ||||
self._state = self._STATE_WANT_SET_VALUE | self._state = self._STATE_WANT_SET_VALUE | ||||
elif special == SPECIAL_START_INDEFINITE_BYTESTRING: | elif special == SPECIAL_START_INDEFINITE_BYTESTRING: | ||||
self._state = self._STATE_WANT_BYTESTRING_CHUNK_FIRST | self._state = self._STATE_WANT_BYTESTRING_CHUNK_FIRST | ||||
else: | else: | ||||
raise CBORDecodeError( | raise CBORDecodeError( | ||||
elif special == SPECIAL_START_ARRAY: | elif special == SPECIAL_START_ARRAY: | ||||
lastc = self._collectionstack[-1] | lastc = self._collectionstack[-1] | ||||
newvalue = [] | newvalue = [] | ||||
lastc[b'v'].append(newvalue) | lastc[b'v'].append(newvalue) | ||||
lastc[b'remaining'] -= 1 | lastc[b'remaining'] -= 1 | ||||
self._collectionstack.append( | self._collectionstack.append( | ||||
{b'remaining': value, b'v': newvalue,} | { | ||||
b'remaining': value, | |||||
b'v': newvalue, | |||||
} | |||||
) | ) | ||||
# self._state doesn't need changed. | # self._state doesn't need changed. | ||||
# A map nested within an array. | # A map nested within an array. | ||||
elif special == SPECIAL_START_MAP: | elif special == SPECIAL_START_MAP: | ||||
lastc = self._collectionstack[-1] | lastc = self._collectionstack[-1] | ||||
newvalue = {} | newvalue = {} | ||||
elif special == SPECIAL_START_SET: | elif special == SPECIAL_START_SET: | ||||
lastc = self._collectionstack[-1] | lastc = self._collectionstack[-1] | ||||
newvalue = set() | newvalue = set() | ||||
lastc[b'v'].append(newvalue) | lastc[b'v'].append(newvalue) | ||||
lastc[b'remaining'] -= 1 | lastc[b'remaining'] -= 1 | ||||
self._collectionstack.append( | self._collectionstack.append( | ||||
{b'remaining': value, b'v': newvalue,} | { | ||||
b'remaining': value, | |||||
b'v': newvalue, | |||||
} | |||||
) | ) | ||||
self._state = self._STATE_WANT_SET_VALUE | self._state = self._STATE_WANT_SET_VALUE | ||||
elif special == SPECIAL_START_INDEFINITE_BYTESTRING: | elif special == SPECIAL_START_INDEFINITE_BYTESTRING: | ||||
raise CBORDecodeError( | raise CBORDecodeError( | ||||
b'indefinite length bytestrings ' | b'indefinite length bytestrings ' | ||||
b'not allowed as array values' | b'not allowed as array values' | ||||
elif special == SPECIAL_START_ARRAY: | elif special == SPECIAL_START_ARRAY: | ||||
lastc = self._collectionstack[-1] | lastc = self._collectionstack[-1] | ||||
newvalue = [] | newvalue = [] | ||||
lastc[b'v'][self._currentmapkey] = newvalue | lastc[b'v'][self._currentmapkey] = newvalue | ||||
lastc[b'remaining'] -= 1 | lastc[b'remaining'] -= 1 | ||||
self._collectionstack.append( | self._collectionstack.append( | ||||
{b'remaining': value, b'v': newvalue,} | { | ||||
b'remaining': value, | |||||
b'v': newvalue, | |||||
} | |||||
) | ) | ||||
self._state = self._STATE_WANT_ARRAY_VALUE | self._state = self._STATE_WANT_ARRAY_VALUE | ||||
# A new map is used as the map value. | # A new map is used as the map value. | ||||
elif special == SPECIAL_START_MAP: | elif special == SPECIAL_START_MAP: | ||||
lastc = self._collectionstack[-1] | lastc = self._collectionstack[-1] | ||||
newvalue = {} | newvalue = {} | ||||
lastc[b'v'][self._currentmapkey] = newvalue | lastc[b'v'][self._currentmapkey] = newvalue | ||||
lastc[b'remaining'] -= 1 | lastc[b'remaining'] -= 1 | ||||
self._collectionstack.append( | self._collectionstack.append( | ||||
{b'remaining': value, b'v': newvalue,} | { | ||||
b'remaining': value, | |||||
b'v': newvalue, | |||||
} | |||||
) | ) | ||||
self._state = self._STATE_WANT_MAP_KEY | self._state = self._STATE_WANT_MAP_KEY | ||||
# A new set is used as the map value. | # A new set is used as the map value. | ||||
elif special == SPECIAL_START_SET: | elif special == SPECIAL_START_SET: | ||||
lastc = self._collectionstack[-1] | lastc = self._collectionstack[-1] | ||||
newvalue = set() | newvalue = set() | ||||
lastc[b'v'][self._currentmapkey] = newvalue | lastc[b'v'][self._currentmapkey] = newvalue | ||||
lastc[b'remaining'] -= 1 | lastc[b'remaining'] -= 1 | ||||
self._collectionstack.append( | self._collectionstack.append( | ||||
{b'remaining': value, b'v': newvalue,} | { | ||||
b'remaining': value, | |||||
b'v': newvalue, | |||||
} | |||||
) | ) | ||||
self._state = self._STATE_WANT_SET_VALUE | self._state = self._STATE_WANT_SET_VALUE | ||||
elif special == SPECIAL_START_INDEFINITE_BYTESTRING: | elif special == SPECIAL_START_INDEFINITE_BYTESTRING: | ||||
raise CBORDecodeError( | raise CBORDecodeError( | ||||
b'indefinite length bytestrings not ' | b'indefinite length bytestrings not ' | ||||
b'allowed as map values' | b'allowed as map values' |
_ = i18n._ | _ = i18n._ | ||||
# compression code | # compression code | ||||
SERVERROLE = b'server' | SERVERROLE = b'server' | ||||
CLIENTROLE = b'client' | CLIENTROLE = b'client' | ||||
compewireprotosupport = collections.namedtuple( | compewireprotosupport = collections.namedtuple( | ||||
'compenginewireprotosupport', ('name', 'serverpriority', 'clientpriority'), | 'compenginewireprotosupport', | ||||
('name', 'serverpriority', 'clientpriority'), | |||||
) | ) | ||||
class propertycache(object): | class propertycache(object): | ||||
def __init__(self, func): | def __init__(self, func): | ||||
self.func = func | self.func = func | ||||
self.name = func.__name__ | self.name = func.__name__ | ||||
b'%b %d %Y', | b'%b %d %Y', | ||||
b'%b %d', | b'%b %d', | ||||
b'%H:%M:%S', | b'%H:%M:%S', | ||||
b'%I:%M:%S%p', | b'%I:%M:%S%p', | ||||
b'%H:%M', | b'%H:%M', | ||||
b'%I:%M%p', | b'%I:%M%p', | ||||
) | ) | ||||
extendeddateformats = defaultdateformats + (b"%Y", b"%Y-%m", b"%b", b"%b %Y",) | extendeddateformats = defaultdateformats + ( | ||||
b"%Y", | |||||
b"%Y-%m", | |||||
b"%b", | |||||
b"%b %Y", | |||||
) | |||||
def makedate(timestamp=None): | def makedate(timestamp=None): | ||||
'''Return a unix timestamp (or the current time) as a (unixtime, | """Return a unix timestamp (or the current time) as a (unixtime, | ||||
offset) tuple based off the local timezone.''' | offset) tuple based off the local timezone.""" | ||||
if timestamp is None: | if timestamp is None: | ||||
timestamp = time.time() | timestamp = time.time() | ||||
if timestamp < 0: | if timestamp < 0: | ||||
hint = _(b"check your clock") | hint = _(b"check your clock") | ||||
raise error.Abort(_(b"negative timestamp: %d") % timestamp, hint=hint) | raise error.Abort(_(b"negative timestamp: %d") % timestamp, hint=hint) | ||||
delta = datetime.datetime.utcfromtimestamp( | delta = datetime.datetime.utcfromtimestamp( | ||||
timestamp | timestamp | ||||
) - datetime.datetime.fromtimestamp(timestamp) | ) - datetime.datetime.fromtimestamp(timestamp) | ||||
def shortdate(date=None): | def shortdate(date=None): | ||||
"""turn (timestamp, tzoff) tuple into iso 8631 date.""" | """turn (timestamp, tzoff) tuple into iso 8631 date.""" | ||||
return datestr(date, format=b'%Y-%m-%d') | return datestr(date, format=b'%Y-%m-%d') | ||||
def parsetimezone(s): | def parsetimezone(s): | ||||
"""find a trailing timezone, if any, in string, and return a | """find a trailing timezone, if any, in string, and return a | ||||
(offset, remainder) pair""" | (offset, remainder) pair""" | ||||
s = pycompat.bytestr(s) | s = pycompat.bytestr(s) | ||||
if s.endswith(b"GMT") or s.endswith(b"UTC"): | if s.endswith(b"GMT") or s.endswith(b"UTC"): | ||||
return 0, s[:-3].rstrip() | return 0, s[:-3].rstrip() | ||||
# Unix-style timezones [+-]hhmm | # Unix-style timezones [+-]hhmm | ||||
if len(s) >= 5 and s[-5] in b"+-" and s[-4:].isdigit(): | if len(s) >= 5 and s[-5] in b"+-" and s[-4:].isdigit(): | ||||
sign = (s[-5] == b"+") and 1 or -1 | sign = (s[-5] == b"+") and 1 or -1 |
stdin=subprocess.PIPE, | stdin=subprocess.PIPE, | ||||
stdout=subprocess.PIPE, | stdout=subprocess.PIPE, | ||||
) | ) | ||||
pout, perr = p.communicate(s) | pout, perr = p.communicate(s) | ||||
return pout | return pout | ||||
def tempfilter(s, cmd): | def tempfilter(s, cmd): | ||||
'''filter string S through a pair of temporary files with CMD. | """filter string S through a pair of temporary files with CMD. | ||||
CMD is used as a template to create the real command to be run, | CMD is used as a template to create the real command to be run, | ||||
with the strings INFILE and OUTFILE replaced by the real names of | with the strings INFILE and OUTFILE replaced by the real names of | ||||
the temporary files generated.''' | the temporary files generated.""" | ||||
inname, outname = None, None | inname, outname = None, None | ||||
try: | try: | ||||
infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-') | infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-') | ||||
fp = os.fdopen(infd, 'wb') | fp = os.fdopen(infd, 'wb') | ||||
fp.write(s) | fp.write(s) | ||||
fp.close() | fp.close() | ||||
outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-') | outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-') | ||||
os.close(outfd) | os.close(outfd) | ||||
def shelltonative(cmd, env): | def shelltonative(cmd, env): | ||||
return cmd | return cmd | ||||
tonativestr = pycompat.identity | tonativestr = pycompat.identity | ||||
def tonativeenv(env): | def tonativeenv(env): | ||||
'''convert the environment from bytes to strings suitable for Popen(), etc. | """convert the environment from bytes to strings suitable for Popen(), etc.""" | ||||
''' | |||||
return pycompat.rapply(tonativestr, env) | return pycompat.rapply(tonativestr, env) | ||||
def system(cmd, environ=None, cwd=None, out=None): | def system(cmd, environ=None, cwd=None, out=None): | ||||
'''enhanced shell command execution. | """enhanced shell command execution. | ||||
run with environment maybe modified, maybe in different dir. | run with environment maybe modified, maybe in different dir. | ||||
if out is specified, it is assumed to be a file-like object that has a | if out is specified, it is assumed to be a file-like object that has a | ||||
write() method. stdout and stderr will be redirected to out.''' | write() method. stdout and stderr will be redirected to out.""" | ||||
try: | try: | ||||
stdout.flush() | stdout.flush() | ||||
except Exception: | except Exception: | ||||
pass | pass | ||||
env = shellenviron(environ) | env = shellenviron(environ) | ||||
if out is None or isstdout(out): | if out is None or isstdout(out): | ||||
rc = subprocess.call( | rc = subprocess.call( | ||||
tonativestr(cmd), | tonativestr(cmd), | ||||
env, | env, | ||||
shell=False, | shell=False, | ||||
stdout=None, | stdout=None, | ||||
stderr=None, | stderr=None, | ||||
ensurestart=True, | ensurestart=True, | ||||
record_wait=None, | record_wait=None, | ||||
stdin_bytes=None, | stdin_bytes=None, | ||||
): | ): | ||||
'''Spawn a command without waiting for it to finish. | """Spawn a command without waiting for it to finish. | ||||
When `record_wait` is not None, the spawned process will not be fully | When `record_wait` is not None, the spawned process will not be fully | ||||
detached and the `record_wait` argument will be called with a the | detached and the `record_wait` argument will be called with a the | ||||
`Subprocess.wait` function for the spawned process. This is mostly | `Subprocess.wait` function for the spawned process. This is mostly | ||||
useful for developers that need to make sure the spawned process | useful for developers that need to make sure the spawned process | ||||
finished before a certain point. (eg: writing test)''' | finished before a certain point. (eg: writing test)""" | ||||
if pycompat.isdarwin: | if pycompat.isdarwin: | ||||
# avoid crash in CoreFoundation in case another thread | # avoid crash in CoreFoundation in case another thread | ||||
# calls gui() while we're calling fork(). | # calls gui() while we're calling fork(). | ||||
gui() | gui() | ||||
# double-fork to completely detach from the parent process | # double-fork to completely detach from the parent process | ||||
# based on http://code.activestate.com/recipes/278731 | # based on http://code.activestate.com/recipes/278731 | ||||
if record_wait is None: | if record_wait is None: |
if f != -1: | if f != -1: | ||||
return author[:f].strip(b' "').replace(b'\\"', b'"') | return author[:f].strip(b' "').replace(b'\\"', b'"') | ||||
f = author.find(b'@') | f = author.find(b'@') | ||||
return author[:f].replace(b'.', b' ') | return author[:f].replace(b'.', b' ') | ||||
@attr.s(hash=True) | @attr.s(hash=True) | ||||
class mailmapping(object): | class mailmapping(object): | ||||
'''Represents a username/email key or value in | """Represents a username/email key or value in | ||||
a mailmap file''' | a mailmap file""" | ||||
email = attr.ib() | email = attr.ib() | ||||
name = attr.ib(default=None) | name = attr.ib(default=None) | ||||
def _ismailmaplineinvalid(names, emails): | def _ismailmaplineinvalid(names, emails): | ||||
'''Returns True if the parsed names and emails | """Returns True if the parsed names and emails | ||||
in a mailmap entry are invalid. | in a mailmap entry are invalid. | ||||
>>> # No names or emails fails | >>> # No names or emails fails | ||||
>>> names, emails = [], [] | >>> names, emails = [], [] | ||||
>>> _ismailmaplineinvalid(names, emails) | >>> _ismailmaplineinvalid(names, emails) | ||||
True | True | ||||
>>> # Only one email fails | >>> # Only one email fails | ||||
>>> emails = [b'email@email.com'] | >>> emails = [b'email@email.com'] | ||||
>>> _ismailmaplineinvalid(names, emails) | >>> _ismailmaplineinvalid(names, emails) | ||||
True | True | ||||
>>> # One email and one name passes | >>> # One email and one name passes | ||||
>>> names = [b'Test Name'] | >>> names = [b'Test Name'] | ||||
>>> _ismailmaplineinvalid(names, emails) | >>> _ismailmaplineinvalid(names, emails) | ||||
False | False | ||||
>>> # No names but two emails passes | >>> # No names but two emails passes | ||||
>>> names = [] | >>> names = [] | ||||
>>> emails = [b'proper@email.com', b'commit@email.com'] | >>> emails = [b'proper@email.com', b'commit@email.com'] | ||||
>>> _ismailmaplineinvalid(names, emails) | >>> _ismailmaplineinvalid(names, emails) | ||||
False | False | ||||
''' | """ | ||||
return not emails or not names and len(emails) < 2 | return not emails or not names and len(emails) < 2 | ||||
def parsemailmap(mailmapcontent): | def parsemailmap(mailmapcontent): | ||||
"""Parses data in the .mailmap format | """Parses data in the .mailmap format | ||||
>>> mmdata = b"\\n".join([ | >>> mmdata = b"\\n".join([ | ||||
... b'# Comment', | ... b'# Comment', | ||||
# Check to see if we have parsed the line into a valid form | # Check to see if we have parsed the line into a valid form | ||||
# We require at least one email, and either at least one | # We require at least one email, and either at least one | ||||
# name or a second email | # name or a second email | ||||
if _ismailmaplineinvalid(names, emails): | if _ismailmaplineinvalid(names, emails): | ||||
continue | continue | ||||
mailmapkey = mailmapping( | mailmapkey = mailmapping( | ||||
email=emails[-1], name=names[-1] if len(names) == 2 else None, | email=emails[-1], | ||||
name=names[-1] if len(names) == 2 else None, | |||||
) | ) | ||||
mailmap[mailmapkey] = mailmapping( | mailmap[mailmapkey] = mailmapping( | ||||
email=emails[0], name=names[0] if names else None, | email=emails[0], | ||||
name=names[0] if names else None, | |||||
) | ) | ||||
return mailmap | return mailmap | ||||
def mapname(mailmap, author): | def mapname(mailmap, author): | ||||
"""Returns the author field according to the mailmap cache, or | """Returns the author field according to the mailmap cache, or | ||||
the original author field. | the original author field. | ||||
proper.email if proper.email else commit.email, | proper.email if proper.email else commit.email, | ||||
) | ) | ||||
_correctauthorformat = remod.compile(br'^[^<]+\s<[^<>]+@[^<>]+>$') | _correctauthorformat = remod.compile(br'^[^<]+\s<[^<>]+@[^<>]+>$') | ||||
def isauthorwellformed(author): | def isauthorwellformed(author): | ||||
'''Return True if the author field is well formed | """Return True if the author field is well formed | ||||
(ie "Contributor Name <contrib@email.dom>") | (ie "Contributor Name <contrib@email.dom>") | ||||
>>> isauthorwellformed(b'Good Author <good@author.com>') | >>> isauthorwellformed(b'Good Author <good@author.com>') | ||||
True | True | ||||
>>> isauthorwellformed(b'Author <good@author.com>') | >>> isauthorwellformed(b'Author <good@author.com>') | ||||
True | True | ||||
>>> isauthorwellformed(b'Bad Author') | >>> isauthorwellformed(b'Bad Author') | ||||
False | False | ||||
>>> isauthorwellformed(b'Bad Author <author@author.com') | >>> isauthorwellformed(b'Bad Author <author@author.com') | ||||
False | False | ||||
>>> isauthorwellformed(b'Bad Author author@author.com') | >>> isauthorwellformed(b'Bad Author author@author.com') | ||||
False | False | ||||
>>> isauthorwellformed(b'<author@author.com>') | >>> isauthorwellformed(b'<author@author.com>') | ||||
False | False | ||||
>>> isauthorwellformed(b'Bad Author <author>') | >>> isauthorwellformed(b'Bad Author <author>') | ||||
False | False | ||||
''' | """ | ||||
return _correctauthorformat.match(author) is not None | return _correctauthorformat.match(author) is not None | ||||
def ellipsis(text, maxlength=400): | def ellipsis(text, maxlength=400): | ||||
"""Trim string to at most maxlength (default: 400) columns in display.""" | """Trim string to at most maxlength (default: 400) columns in display.""" | ||||
return encoding.trim(text, maxlength, ellipsis=b'...') | return encoding.trim(text, maxlength, ellipsis=b'...') | ||||
return self.readlines(path, mode=mode) | return self.readlines(path, mode=mode) | ||||
except IOError as inst: | except IOError as inst: | ||||
if inst.errno != errno.ENOENT: | if inst.errno != errno.ENOENT: | ||||
raise | raise | ||||
return [] | return [] | ||||
@util.propertycache | @util.propertycache | ||||
def open(self): | def open(self): | ||||
'''Open ``path`` file, which is relative to vfs root. | """Open ``path`` file, which is relative to vfs root. | ||||
Newly created directories are marked as "not to be indexed by | Newly created directories are marked as "not to be indexed by | ||||
the content indexing service", if ``notindexed`` is specified | the content indexing service", if ``notindexed`` is specified | ||||
for "write" mode access. | for "write" mode access. | ||||
''' | """ | ||||
return self.__call__ | return self.__call__ | ||||
def read(self, path): | def read(self, path): | ||||
with self(path, b'rb') as fp: | with self(path, b'rb') as fp: | ||||
return fp.read() | return fp.read() | ||||
def readlines(self, path, mode=b'rb'): | def readlines(self, path, mode=b'rb'): | ||||
with self(path, mode=mode) as fp: | with self(path, mode=mode) as fp: | ||||
def isfile(self, path=None): | def isfile(self, path=None): | ||||
return os.path.isfile(self.join(path)) | return os.path.isfile(self.join(path)) | ||||
def islink(self, path=None): | def islink(self, path=None): | ||||
return os.path.islink(self.join(path)) | return os.path.islink(self.join(path)) | ||||
def isfileorlink(self, path=None): | def isfileorlink(self, path=None): | ||||
'''return whether path is a regular file or a symlink | """return whether path is a regular file or a symlink | ||||
Unlike isfile, this doesn't follow symlinks.''' | Unlike isfile, this doesn't follow symlinks.""" | ||||
try: | try: | ||||
st = self.lstat(path) | st = self.lstat(path) | ||||
except OSError: | except OSError: | ||||
return False | return False | ||||
mode = st.st_mode | mode = st.st_mode | ||||
return stat.S_ISREG(mode) or stat.S_ISLNK(mode) | return stat.S_ISREG(mode) or stat.S_ISLNK(mode) | ||||
def reljoin(self, *paths): | def reljoin(self, *paths): | ||||
_avoidambig(dstpath, oldstat) | _avoidambig(dstpath, oldstat) | ||||
return ret | return ret | ||||
return util.rename(srcpath, dstpath) | return util.rename(srcpath, dstpath) | ||||
def readlink(self, path): | def readlink(self, path): | ||||
return util.readlink(self.join(path)) | return util.readlink(self.join(path)) | ||||
def removedirs(self, path=None): | def removedirs(self, path=None): | ||||
"""Remove a leaf directory and all empty intermediate ones | """Remove a leaf directory and all empty intermediate ones""" | ||||
""" | |||||
return util.removedirs(self.join(path)) | return util.removedirs(self.join(path)) | ||||
def rmdir(self, path=None): | def rmdir(self, path=None): | ||||
"""Remove an empty directory.""" | """Remove an empty directory.""" | ||||
return os.rmdir(self.join(path)) | return os.rmdir(self.join(path)) | ||||
def rmtree(self, path=None, ignore_errors=False, forcibly=False): | def rmtree(self, path=None, ignore_errors=False, forcibly=False): | ||||
"""Remove a directory tree recursively | """Remove a directory tree recursively | ||||
yield bfc | yield bfc | ||||
finally: | finally: | ||||
vfs._backgroundfilecloser = ( | vfs._backgroundfilecloser = ( | ||||
None # pytype: disable=attribute-error | None # pytype: disable=attribute-error | ||||
) | ) | ||||
class vfs(abstractvfs): | class vfs(abstractvfs): | ||||
'''Operate files relative to a base directory | """Operate files relative to a base directory | ||||
This class is used to hide the details of COW semantics and | This class is used to hide the details of COW semantics and | ||||
remote file access from higher level code. | remote file access from higher level code. | ||||
'cacheaudited' should be enabled only if (a) vfs object is short-lived, or | 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or | ||||
(b) the base directory is managed by hg and considered sort-of append-only. | (b) the base directory is managed by hg and considered sort-of append-only. | ||||
See pathutil.pathauditor() for details. | See pathutil.pathauditor() for details. | ||||
''' | """ | ||||
def __init__( | def __init__( | ||||
self, | self, | ||||
base, | base, | ||||
audit=True, | audit=True, | ||||
cacheaudited=False, | cacheaudited=False, | ||||
expandpath=False, | expandpath=False, | ||||
realpath=False, | realpath=False, | ||||
mode=b"r", | mode=b"r", | ||||
atomictemp=False, | atomictemp=False, | ||||
notindexed=False, | notindexed=False, | ||||
backgroundclose=False, | backgroundclose=False, | ||||
checkambig=False, | checkambig=False, | ||||
auditpath=True, | auditpath=True, | ||||
makeparentdirs=True, | makeparentdirs=True, | ||||
): | ): | ||||
'''Open ``path`` file, which is relative to vfs root. | """Open ``path`` file, which is relative to vfs root. | ||||
By default, parent directories are created as needed. Newly created | By default, parent directories are created as needed. Newly created | ||||
directories are marked as "not to be indexed by the content indexing | directories are marked as "not to be indexed by the content indexing | ||||
service", if ``notindexed`` is specified for "write" mode access. | service", if ``notindexed`` is specified for "write" mode access. | ||||
Set ``makeparentdirs=False`` to not create directories implicitly. | Set ``makeparentdirs=False`` to not create directories implicitly. | ||||
If ``backgroundclose`` is passed, the file may be closed asynchronously. | If ``backgroundclose`` is passed, the file may be closed asynchronously. | ||||
It can only be used if the ``self.backgroundclosing()`` context manager | It can only be used if the ``self.backgroundclosing()`` context manager | ||||
only for writing), and is useful only if target file is | only for writing), and is useful only if target file is | ||||
guarded by any lock (e.g. repo.lock or repo.wlock). | guarded by any lock (e.g. repo.lock or repo.wlock). | ||||
To avoid file stat ambiguity forcibly, checkambig=True involves | To avoid file stat ambiguity forcibly, checkambig=True involves | ||||
copying ``path`` file opened in "append" mode (e.g. for | copying ``path`` file opened in "append" mode (e.g. for | ||||
truncation), if it is owned by another. Therefore, use | truncation), if it is owned by another. Therefore, use | ||||
combination of append mode and checkambig=True only in limited | combination of append mode and checkambig=True only in limited | ||||
cases (see also issue5418 and issue5584 for detail). | cases (see also issue5418 and issue5584 for detail). | ||||
''' | """ | ||||
if auditpath: | if auditpath: | ||||
self._auditpath(path, mode) | self._auditpath(path, mode) | ||||
f = self.join(path) | f = self.join(path) | ||||
if b"b" not in mode: | if b"b" not in mode: | ||||
mode += b"b" # for that other OS | mode += b"b" # for that other OS | ||||
nlink = -1 | nlink = -1 |
if not _kernel32.GetFileInformationByHandle(fh, ctypes.byref(fi)): | if not _kernel32.GetFileInformationByHandle(fh, ctypes.byref(fi)): | ||||
_raiseoserror(name) | _raiseoserror(name) | ||||
return fi | return fi | ||||
finally: | finally: | ||||
_kernel32.CloseHandle(fh) | _kernel32.CloseHandle(fh) | ||||
def checkcertificatechain(cert, build=True): | def checkcertificatechain(cert, build=True): | ||||
'''Tests the given certificate to see if there is a complete chain to a | """Tests the given certificate to see if there is a complete chain to a | ||||
trusted root certificate. As a side effect, missing certificates are | trusted root certificate. As a side effect, missing certificates are | ||||
downloaded and installed unless ``build=False``. True is returned if a | downloaded and installed unless ``build=False``. True is returned if a | ||||
chain to a trusted root exists (even if built on the fly), otherwise | chain to a trusted root exists (even if built on the fly), otherwise | ||||
False. NB: A chain to a trusted root does NOT imply that the certificate | False. NB: A chain to a trusted root does NOT imply that the certificate | ||||
is valid. | is valid. | ||||
''' | """ | ||||
chainctxptr = ctypes.POINTER(CERT_CHAIN_CONTEXT) | chainctxptr = ctypes.POINTER(CERT_CHAIN_CONTEXT) | ||||
pchainctx = chainctxptr() | pchainctx = chainctxptr() | ||||
chainpara = CERT_CHAIN_PARA( | chainpara = CERT_CHAIN_PARA( | ||||
cbSize=ctypes.sizeof(CERT_CHAIN_PARA), RequestedUsage=CERT_USAGE_MATCH() | cbSize=ctypes.sizeof(CERT_CHAIN_PARA), RequestedUsage=CERT_USAGE_MATCH() | ||||
) | ) | ||||
def lasterrorwaspipeerror(err): | def lasterrorwaspipeerror(err): | ||||
if err.errno != errno.EINVAL: | if err.errno != errno.EINVAL: | ||||
return False | return False | ||||
err = _kernel32.GetLastError() | err = _kernel32.GetLastError() | ||||
return err == _ERROR_BROKEN_PIPE or err == _ERROR_NO_DATA | return err == _ERROR_BROKEN_PIPE or err == _ERROR_NO_DATA | ||||
def testpid(pid): | def testpid(pid): | ||||
'''return True if pid is still running or unable to | """return True if pid is still running or unable to | ||||
determine, False otherwise''' | determine, False otherwise""" | ||||
h = _kernel32.OpenProcess(_PROCESS_QUERY_INFORMATION, False, pid) | h = _kernel32.OpenProcess(_PROCESS_QUERY_INFORMATION, False, pid) | ||||
if h: | if h: | ||||
try: | try: | ||||
status = _DWORD() | status = _DWORD() | ||||
if _kernel32.GetExitCodeProcess(h, ctypes.byref(status)): | if _kernel32.GetExitCodeProcess(h, ctypes.byref(status)): | ||||
return status.value == _STILL_ACTIVE | return status.value == _STILL_ACTIVE | ||||
finally: | finally: | ||||
_kernel32.CloseHandle(h) | _kernel32.CloseHandle(h) | ||||
raise ctypes.WinError() | raise ctypes.WinError() | ||||
return buf.value | return buf.value | ||||
_signalhandler = [] | _signalhandler = [] | ||||
def setsignalhandler(): | def setsignalhandler(): | ||||
'''Register a termination handler for console events including | """Register a termination handler for console events including | ||||
CTRL+C. python signal handlers do not work well with socket | CTRL+C. python signal handlers do not work well with socket | ||||
operations. | operations. | ||||
''' | """ | ||||
def handler(event): | def handler(event): | ||||
_kernel32.ExitProcess(1) | _kernel32.ExitProcess(1) | ||||
if _signalhandler: | if _signalhandler: | ||||
return # already registered | return # already registered | ||||
h = _SIGNAL_HANDLER(handler) | h = _SIGNAL_HANDLER(handler) | ||||
_signalhandler.append(h) # needed to prevent garbage collection | _signalhandler.append(h) # needed to prevent garbage collection | ||||
if not _kernel32.GetConsoleScreenBufferInfo(screenbuf, ctypes.byref(csbi)): | if not _kernel32.GetConsoleScreenBufferInfo(screenbuf, ctypes.byref(csbi)): | ||||
return width, height | return width, height | ||||
width = csbi.srWindow.Right - csbi.srWindow.Left # don't '+ 1' | width = csbi.srWindow.Right - csbi.srWindow.Left # don't '+ 1' | ||||
height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1 | height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1 | ||||
return width, height | return width, height | ||||
def enablevtmode(): | def enablevtmode(): | ||||
'''Enable virtual terminal mode for the associated console. Return True if | """Enable virtual terminal mode for the associated console. Return True if | ||||
enabled, else False.''' | enabled, else False.""" | ||||
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4 | ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4 | ||||
handle = _kernel32.GetStdHandle( | handle = _kernel32.GetStdHandle( | ||||
_STD_OUTPUT_HANDLE | _STD_OUTPUT_HANDLE | ||||
) # don't close the handle | ) # don't close the handle | ||||
if handle == _INVALID_HANDLE_VALUE: | if handle == _INVALID_HANDLE_VALUE: | ||||
return False | return False |
def _isatty(fp): | def _isatty(fp): | ||||
try: | try: | ||||
return fp.isatty() | return fp.isatty() | ||||
except AttributeError: | except AttributeError: | ||||
return False | return False | ||||
class winstdout(object): | class winstdout(object): | ||||
'''Some files on Windows misbehave. | """Some files on Windows misbehave. | ||||
When writing to a broken pipe, EINVAL instead of EPIPE may be raised. | When writing to a broken pipe, EINVAL instead of EPIPE may be raised. | ||||
When writing too many bytes to a console at the same, a "Not enough space" | When writing too many bytes to a console at the same, a "Not enough space" | ||||
error may happen. Python 3 already works around that. | error may happen. Python 3 already works around that. | ||||
''' | """ | ||||
def __init__(self, fp): | def __init__(self, fp): | ||||
self.fp = fp | self.fp = fp | ||||
self.throttle = not pycompat.ispy3 and _isatty(fp) | self.throttle = not pycompat.ispy3 and _isatty(fp) | ||||
def __getattr__(self, key): | def __getattr__(self, key): | ||||
return getattr(self.fp, key) | return getattr(self.fp, key) | ||||
# if you change this stub into a real check, please try to implement the | # if you change this stub into a real check, please try to implement the | ||||
# username and groupname functions above, too. | # username and groupname functions above, too. | ||||
def isowner(st): | def isowner(st): | ||||
return True | return True | ||||
def findexe(command): | def findexe(command): | ||||
'''Find executable for command searching like cmd.exe does. | """Find executable for command searching like cmd.exe does. | ||||
If command is a basename then PATH is searched for command. | If command is a basename then PATH is searched for command. | ||||
PATH isn't searched if command is an absolute or relative path. | PATH isn't searched if command is an absolute or relative path. | ||||
An extension from PATHEXT is found and added if not present. | An extension from PATHEXT is found and added if not present. | ||||
If command isn't found None is returned.''' | If command isn't found None is returned.""" | ||||
pathext = encoding.environ.get(b'PATHEXT', b'.COM;.EXE;.BAT;.CMD') | pathext = encoding.environ.get(b'PATHEXT', b'.COM;.EXE;.BAT;.CMD') | ||||
pathexts = [ext for ext in pathext.lower().split(pycompat.ospathsep)] | pathexts = [ext for ext in pathext.lower().split(pycompat.ospathsep)] | ||||
if os.path.splitext(command)[1].lower() in pathexts: | if os.path.splitext(command)[1].lower() in pathexts: | ||||
pathexts = [b''] | pathexts = [b''] | ||||
def findexisting(pathcommand): | def findexisting(pathcommand): | ||||
"""Will append extension (if needed) and return existing file""" | """Will append extension (if needed) and return existing file""" | ||||
for ext in pathexts: | for ext in pathexts: | ||||
return executable | return executable | ||||
return findexisting(os.path.expanduser(os.path.expandvars(command))) | return findexisting(os.path.expanduser(os.path.expandvars(command))) | ||||
_wantedkinds = {stat.S_IFREG, stat.S_IFLNK} | _wantedkinds = {stat.S_IFREG, stat.S_IFLNK} | ||||
def statfiles(files): | def statfiles(files): | ||||
'''Stat each file in files. Yield each stat, or None if a file | """Stat each file in files. Yield each stat, or None if a file | ||||
does not exist or has a type we don't care about. | does not exist or has a type we don't care about. | ||||
Cluster and cache stat per directory to minimize number of OS stat calls.''' | Cluster and cache stat per directory to minimize number of OS stat calls.""" | ||||
dircache = {} # dirname -> filename -> status | None if file does not exist | dircache = {} # dirname -> filename -> status | None if file does not exist | ||||
getkind = stat.S_IFMT | getkind = stat.S_IFMT | ||||
for nf in files: | for nf in files: | ||||
nf = normcase(nf) | nf = normcase(nf) | ||||
dir, base = os.path.split(nf) | dir, base = os.path.split(nf) | ||||
if not dir: | if not dir: | ||||
dir = b'.' | dir = b'.' | ||||
cache = dircache.get(dir, None) | cache = dircache.get(dir, None) | ||||
def __init__(self, path): | def __init__(self, path): | ||||
pass | pass | ||||
def cacheable(self): | def cacheable(self): | ||||
return False | return False | ||||
def lookupreg(key, valname=None, scope=None): | def lookupreg(key, valname=None, scope=None): | ||||
''' Look up a key/value name in the Windows registry. | """Look up a key/value name in the Windows registry. | ||||
valname: value name. If unspecified, the default value for the key | valname: value name. If unspecified, the default value for the key | ||||
is used. | is used. | ||||
scope: optionally specify scope for registry lookup, this can be | scope: optionally specify scope for registry lookup, this can be | ||||
a sequence of scopes to look up in order. Default (CURRENT_USER, | a sequence of scopes to look up in order. Default (CURRENT_USER, | ||||
LOCAL_MACHINE). | LOCAL_MACHINE). | ||||
''' | """ | ||||
if scope is None: | if scope is None: | ||||
scope = (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE) | scope = (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE) | ||||
elif not isinstance(scope, (list, tuple)): | elif not isinstance(scope, (list, tuple)): | ||||
scope = (scope,) | scope = (scope,) | ||||
for s in scope: | for s in scope: | ||||
try: | try: | ||||
with winreg.OpenKey(s, encoding.strfromlocal(key)) as hkey: | with winreg.OpenKey(s, encoding.strfromlocal(key)) as hkey: | ||||
name = valname and encoding.strfromlocal(valname) or valname | name = valname and encoding.strfromlocal(valname) or valname |
if done: | if done: | ||||
break | break | ||||
def createalternatelocationresponseframe(stream, requestid, location): | def createalternatelocationresponseframe(stream, requestid, location): | ||||
data = { | data = { | ||||
b'status': b'redirect', | b'status': b'redirect', | ||||
b'location': {b'url': location.url, b'mediatype': location.mediatype,}, | b'location': { | ||||
b'url': location.url, | |||||
b'mediatype': location.mediatype, | |||||
}, | |||||
} | } | ||||
for a in ( | for a in ( | ||||
'size', | 'size', | ||||
'fullhashes', | 'fullhashes', | ||||
'fullhashseed', | 'fullhashseed', | ||||
'serverdercerts', | 'serverdercerts', | ||||
'servercadercerts', | 'servercadercerts', | ||||
payload=payload, | payload=payload, | ||||
encoded=encoded, | encoded=encoded, | ||||
) | ) | ||||
def createcommanderrorresponse(stream, requestid, message, args=None): | def createcommanderrorresponse(stream, requestid, message, args=None): | ||||
# TODO should this be using a list of {'msg': ..., 'args': {}} so atom | # TODO should this be using a list of {'msg': ..., 'args': {}} so atom | ||||
# formatting works consistently? | # formatting works consistently? | ||||
m = {b'status': b'error', b'error': {b'message': message,}} | m = { | ||||
b'status': b'error', | |||||
b'error': { | |||||
b'message': message, | |||||
}, | |||||
} | |||||
if args: | if args: | ||||
m[b'error'][b'args'] = args | m[b'error'][b'args'] = args | ||||
overall = b''.join(cborutil.streamencode(m)) | overall = b''.join(cborutil.streamencode(m)) | ||||
yield stream.makeframe( | yield stream.makeframe( | ||||
requestid=requestid, | requestid=requestid, | ||||
typeid=FRAME_TYPE_COMMAND_RESPONSE, | typeid=FRAME_TYPE_COMMAND_RESPONSE, | ||||
flags=FLAG_COMMAND_RESPONSE_EOS, | flags=FLAG_COMMAND_RESPONSE_EOS, | ||||
payload=overall, | payload=overall, | ||||
) | ) | ||||
def createerrorframe(stream, requestid, msg, errtype): | def createerrorframe(stream, requestid, msg, errtype): | ||||
# TODO properly handle frame size limits. | # TODO properly handle frame size limits. | ||||
assert len(msg) <= DEFAULT_MAX_FRAME_SIZE | assert len(msg) <= DEFAULT_MAX_FRAME_SIZE | ||||
payload = b''.join( | payload = b''.join( | ||||
cborutil.streamencode({b'type': errtype, b'message': [{b'msg': msg}],}) | cborutil.streamencode( | ||||
{ | |||||
b'type': errtype, | |||||
b'message': [{b'msg': msg}], | |||||
} | |||||
) | |||||
) | ) | ||||
yield stream.makeframe( | yield stream.makeframe( | ||||
requestid=requestid, | requestid=requestid, | ||||
typeid=FRAME_TYPE_ERROR_RESPONSE, | typeid=FRAME_TYPE_ERROR_RESPONSE, | ||||
flags=0, | flags=0, | ||||
payload=payload, | payload=payload, | ||||
) | ) | ||||
return b'noop', {} | return b'noop', {} | ||||
# If we buffered all our responses, emit those. | # If we buffered all our responses, emit those. | ||||
def makegen(): | def makegen(): | ||||
for gen in self._bufferedframegens: | for gen in self._bufferedframegens: | ||||
for frame in gen: | for frame in gen: | ||||
yield frame | yield frame | ||||
return b'sendframes', {b'framegen': makegen(),} | return b'sendframes', { | ||||
b'framegen': makegen(), | |||||
} | |||||
def _handlesendframes(self, framegen): | def _handlesendframes(self, framegen): | ||||
if self._deferoutput: | if self._deferoutput: | ||||
self._bufferedframegens.append(framegen) | self._bufferedframegens.append(framegen) | ||||
return b'noop', {} | return b'noop', {} | ||||
else: | else: | ||||
return b'sendframes', {b'framegen': framegen,} | return b'sendframes', { | ||||
b'framegen': framegen, | |||||
} | |||||
def onservererror(self, stream, requestid, msg): | def onservererror(self, stream, requestid, msg): | ||||
ensureserverstream(stream) | ensureserverstream(stream) | ||||
def sendframes(): | def sendframes(): | ||||
for frame in createerrorframe( | for frame in createerrorframe( | ||||
stream, requestid, msg, errtype=b'server' | stream, requestid, msg, errtype=b'server' | ||||
): | ): | ||||
for name in STREAM_ENCODERS_ORDER: | for name in STREAM_ENCODERS_ORDER: | ||||
if name in self._sendersettings[b'contentencodings']: | if name in self._sendersettings[b'contentencodings']: | ||||
s.setencoder(self._ui, name) | s.setencoder(self._ui, name) | ||||
break | break | ||||
return s | return s | ||||
def _makeerrorresult(self, msg): | def _makeerrorresult(self, msg): | ||||
return b'error', {b'message': msg,} | return b'error', { | ||||
b'message': msg, | |||||
} | |||||
def _makeruncommandresult(self, requestid): | def _makeruncommandresult(self, requestid): | ||||
entry = self._receivingcommands[requestid] | entry = self._receivingcommands[requestid] | ||||
if not entry[b'requestdone']: | if not entry[b'requestdone']: | ||||
self._state = b'errored' | self._state = b'errored' | ||||
raise error.ProgrammingError( | raise error.ProgrammingError( | ||||
b'should not be called without requestdone set' | b'should not be called without requestdone set' | ||||
b'command': request[b'name'], | b'command': request[b'name'], | ||||
b'args': request[b'args'], | b'args': request[b'args'], | ||||
b'redirect': request.get(b'redirect'), | b'redirect': request.get(b'redirect'), | ||||
b'data': entry[b'data'].getvalue() if entry[b'data'] else None, | b'data': entry[b'data'].getvalue() if entry[b'data'] else None, | ||||
}, | }, | ||||
) | ) | ||||
def _makewantframeresult(self): | def _makewantframeresult(self): | ||||
return b'wantframe', {b'state': self._state,} | return b'wantframe', { | ||||
b'state': self._state, | |||||
} | |||||
def _validatecommandrequestframe(self, frame): | def _validatecommandrequestframe(self, frame): | ||||
new = frame.flags & FLAG_COMMAND_REQUEST_NEW | new = frame.flags & FLAG_COMMAND_REQUEST_NEW | ||||
continuation = frame.flags & FLAG_COMMAND_REQUEST_CONTINUATION | continuation = frame.flags & FLAG_COMMAND_REQUEST_CONTINUATION | ||||
if new and continuation: | if new and continuation: | ||||
self._state = b'errored' | self._state = b'errored' | ||||
return self._makeerrorresult( | return self._makeerrorresult( | ||||
if not self._hasmultiplesend: | if not self._hasmultiplesend: | ||||
self._cansend = False | self._cansend = False | ||||
self._canissuecommands = False | self._canissuecommands = False | ||||
return ( | return ( | ||||
request, | request, | ||||
b'sendframes', | b'sendframes', | ||||
{b'framegen': self._makecommandframes(request),}, | { | ||||
b'framegen': self._makecommandframes(request), | |||||
}, | |||||
) | ) | ||||
def flushcommands(self): | def flushcommands(self): | ||||
"""Request that all queued commands be sent. | """Request that all queued commands be sent. | ||||
If any commands are buffered, this will instruct the caller to send | If any commands are buffered, this will instruct the caller to send | ||||
them over the wire. If no commands are buffered it instructs the client | them over the wire. If no commands are buffered it instructs the client | ||||
to no-op. | to no-op. | ||||
self._cansend = False | self._cansend = False | ||||
def makeframes(): | def makeframes(): | ||||
while self._pendingrequests: | while self._pendingrequests: | ||||
request = self._pendingrequests.popleft() | request = self._pendingrequests.popleft() | ||||
for frame in self._makecommandframes(request): | for frame in self._makecommandframes(request): | ||||
yield frame | yield frame | ||||
return b'sendframes', {b'framegen': makeframes(),} | return b'sendframes', { | ||||
b'framegen': makeframes(), | |||||
} | |||||
def _makecommandframes(self, request): | def _makecommandframes(self, request): | ||||
"""Emit frames to issue a command request. | """Emit frames to issue a command request. | ||||
As a side-effect, update request accounting to reflect its changed | As a side-effect, update request accounting to reflect its changed | ||||
state. | state. | ||||
""" | """ | ||||
self._activerequests[request.requestid] = request | self._activerequests[request.requestid] = request | ||||
request.state = b'sending' | request.state = b'sending' | ||||
if not self._protocolsettingssent and self._clientcontentencoders: | if not self._protocolsettingssent and self._clientcontentencoders: | ||||
self._protocolsettingssent = True | self._protocolsettingssent = True | ||||
payload = b''.join( | payload = b''.join( | ||||
cborutil.streamencode( | cborutil.streamencode( | ||||
{b'contentencodings': self._clientcontentencoders,} | { | ||||
b'contentencodings': self._clientcontentencoders, | |||||
} | |||||
) | ) | ||||
) | ) | ||||
yield self._outgoingstream.makeframe( | yield self._outgoingstream.makeframe( | ||||
requestid=request.requestid, | requestid=request.requestid, | ||||
typeid=FRAME_TYPE_SENDER_PROTOCOL_SETTINGS, | typeid=FRAME_TYPE_SENDER_PROTOCOL_SETTINGS, | ||||
flags=FLAG_SENDER_PROTOCOL_SETTINGS_EOS, | flags=FLAG_SENDER_PROTOCOL_SETTINGS_EOS, | ||||
payload=payload, | payload=payload, |
NARROWCAP = b'exp-narrow-1' | NARROWCAP = b'exp-narrow-1' | ||||
ELLIPSESCAP1 = b'exp-ellipses-1' | ELLIPSESCAP1 = b'exp-ellipses-1' | ||||
ELLIPSESCAP = b'exp-ellipses-2' | ELLIPSESCAP = b'exp-ellipses-2' | ||||
SUPPORTED_ELLIPSESCAP = (ELLIPSESCAP1, ELLIPSESCAP) | SUPPORTED_ELLIPSESCAP = (ELLIPSESCAP1, ELLIPSESCAP) | ||||
# All available wire protocol transports. | # All available wire protocol transports. | ||||
TRANSPORTS = { | TRANSPORTS = { | ||||
SSHV1: {b'transport': b'ssh', b'version': 1,}, | SSHV1: { | ||||
b'transport': b'ssh', | |||||
b'version': 1, | |||||
}, | |||||
SSHV2: { | SSHV2: { | ||||
b'transport': b'ssh', | b'transport': b'ssh', | ||||
# TODO mark as version 2 once all commands are implemented. | # TODO mark as version 2 once all commands are implemented. | ||||
b'version': 1, | b'version': 1, | ||||
}, | }, | ||||
b'http-v1': {b'transport': b'http', b'version': 1,}, | b'http-v1': { | ||||
HTTP_WIREPROTO_V2: {b'transport': b'http', b'version': 2,}, | b'transport': b'http', | ||||
b'version': 1, | |||||
}, | |||||
HTTP_WIREPROTO_V2: { | |||||
b'transport': b'http', | |||||
b'version': 2, | |||||
}, | |||||
} | } | ||||
class bytesresponse(object): | class bytesresponse(object): | ||||
"""A wire protocol response consisting of raw bytes.""" | """A wire protocol response consisting of raw bytes.""" | ||||
def __init__(self, data): | def __init__(self, data): | ||||
self.data = data | self.data = data |
util as interfaceutil, | util as interfaceutil, | ||||
) | ) | ||||
from .utils import hashutil | from .utils import hashutil | ||||
urlreq = util.urlreq | urlreq = util.urlreq | ||||
def batchable(f): | def batchable(f): | ||||
'''annotation for batchable methods | """annotation for batchable methods | ||||
Such methods must implement a coroutine as follows: | Such methods must implement a coroutine as follows: | ||||
@batchable | @batchable | ||||
def sample(self, one, two=None): | def sample(self, one, two=None): | ||||
# Build list of encoded arguments suitable for your wire protocol: | # Build list of encoded arguments suitable for your wire protocol: | ||||
encargs = [('one', encode(one),), ('two', encode(two),)] | encargs = [('one', encode(one),), ('two', encode(two),)] | ||||
# Create future for injection of encoded result: | # Create future for injection of encoded result: | ||||
encresref = future() | encresref = future() | ||||
# Return encoded arguments and future: | # Return encoded arguments and future: | ||||
yield encargs, encresref | yield encargs, encresref | ||||
# Assuming the future to be filled with the result from the batched | # Assuming the future to be filled with the result from the batched | ||||
# request now. Decode it: | # request now. Decode it: | ||||
yield decode(encresref.value) | yield decode(encresref.value) | ||||
The decorator returns a function which wraps this coroutine as a plain | The decorator returns a function which wraps this coroutine as a plain | ||||
method, but adds the original method as an attribute called "batchable", | method, but adds the original method as an attribute called "batchable", | ||||
which is used by remotebatch to split the call into separate encoding and | which is used by remotebatch to split the call into separate encoding and | ||||
decoding phases. | decoding phases. | ||||
''' | """ | ||||
def plain(*args, **opts): | def plain(*args, **opts): | ||||
batchable = f(*args, **opts) | batchable = f(*args, **opts) | ||||
encargsorres, encresref = next(batchable) | encargsorres, encresref = next(batchable) | ||||
if not encresref: | if not encresref: | ||||
return encargsorres # a local result in this case | return encargsorres # a local result in this case | ||||
self = args[0] | self = args[0] | ||||
cmd = pycompat.bytesurl(f.__name__) # ensure cmd is ascii bytestr | cmd = pycompat.bytesurl(f.__name__) # ensure cmd is ascii bytestr | ||||
opts[key] = value | opts[key] = value | ||||
f = self._callcompressable(b"getbundle", **pycompat.strkwargs(opts)) | f = self._callcompressable(b"getbundle", **pycompat.strkwargs(opts)) | ||||
if any((cap.startswith(b'HG2') for cap in bundlecaps)): | if any((cap.startswith(b'HG2') for cap in bundlecaps)): | ||||
return bundle2.getunbundler(self.ui, f) | return bundle2.getunbundler(self.ui, f) | ||||
else: | else: | ||||
return changegroupmod.cg1unpacker(f, b'UN') | return changegroupmod.cg1unpacker(f, b'UN') | ||||
def unbundle(self, bundle, heads, url): | def unbundle(self, bundle, heads, url): | ||||
'''Send cg (a readable file-like object representing the | """Send cg (a readable file-like object representing the | ||||
changegroup to push, typically a chunkbuffer object) to the | changegroup to push, typically a chunkbuffer object) to the | ||||
remote server as a bundle. | remote server as a bundle. | ||||
When pushing a bundle10 stream, return an integer indicating the | When pushing a bundle10 stream, return an integer indicating the | ||||
result of the push (see changegroup.apply()). | result of the push (see changegroup.apply()). | ||||
When pushing a bundle20 stream, return a bundle20 stream. | When pushing a bundle20 stream, return a bundle20 stream. | ||||
`url` is the url the client thinks it's pushing to, which is | `url` is the url the client thinks it's pushing to, which is | ||||
visible to hooks. | visible to hooks. | ||||
''' | """ | ||||
if heads != [b'force'] and self.capable(b'unbundlehash'): | if heads != [b'force'] and self.capable(b'unbundlehash'): | ||||
heads = wireprototypes.encodelist( | heads = wireprototypes.encodelist( | ||||
[b'hashed', hashutil.sha1(b''.join(sorted(heads))).digest()] | [b'hashed', hashutil.sha1(b''.join(sorted(heads))).digest()] | ||||
) | ) | ||||
else: | else: | ||||
heads = wireprototypes.encodelist(heads) | heads = wireprototypes.encodelist(heads) | ||||
def _calltwowaystream(self, cmd, fp, **args): | def _calltwowaystream(self, cmd, fp, **args): | ||||
"""execute <cmd> on server | """execute <cmd> on server | ||||
The command will send a stream to the server and get a stream in reply. | The command will send a stream to the server and get a stream in reply. | ||||
""" | """ | ||||
raise NotImplementedError() | raise NotImplementedError() | ||||
def _abort(self, exception): | def _abort(self, exception): | ||||
"""clearly abort the wire protocol connection and raise the exception | """clearly abort the wire protocol connection and raise the exception""" | ||||
""" | |||||
raise NotImplementedError() | raise NotImplementedError() |
) | ) | ||||
output = output.getvalue() if output else b'' | output = output.getvalue() if output else b'' | ||||
return wireprototypes.bytesresponse(b'%d\n%s' % (int(r), output)) | return wireprototypes.bytesresponse(b'%d\n%s' % (int(r), output)) | ||||
@wireprotocommand(b'stream_out', permission=b'pull') | @wireprotocommand(b'stream_out', permission=b'pull') | ||||
def stream(repo, proto): | def stream(repo, proto): | ||||
'''If the server supports streaming clone, it advertises the "stream" | """If the server supports streaming clone, it advertises the "stream" | ||||
capability with a value representing the version and flags of the repo | capability with a value representing the version and flags of the repo | ||||
it is serving. Client checks to see if it understands the format. | it is serving. Client checks to see if it understands the format. | ||||
''' | """ | ||||
return wireprototypes.streamreslegacy(streamclone.generatev1wireproto(repo)) | return wireprototypes.streamreslegacy(streamclone.generatev1wireproto(repo)) | ||||
@wireprotocommand(b'unbundle', b'heads', permission=b'push') | @wireprotocommand(b'unbundle', b'heads', permission=b'push') | ||||
def unbundle(repo, proto, heads): | def unbundle(repo, proto, heads): | ||||
their_heads = wireprototypes.decodelist(heads) | their_heads = wireprototypes.decodelist(heads) | ||||
with proto.mayberedirectstdio() as output: | with proto.mayberedirectstdio() as output: |
@wireprotocommand( | @wireprotocommand( | ||||
b'changesetdata', | b'changesetdata', | ||||
args={ | args={ | ||||
b'revisions': { | b'revisions': { | ||||
b'type': b'list', | b'type': b'list', | ||||
b'example': [ | b'example': [ | ||||
{b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],} | { | ||||
b'type': b'changesetexplicit', | |||||
b'nodes': [b'abcdef...'], | |||||
} | |||||
], | ], | ||||
}, | }, | ||||
b'fields': { | b'fields': { | ||||
b'type': b'set', | b'type': b'set', | ||||
b'default': set, | b'default': set, | ||||
b'example': {b'parents', b'revision'}, | b'example': {b'parents', b'revision'}, | ||||
b'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'}, | b'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'}, | ||||
}, | }, | ||||
@wireprotocommand( | @wireprotocommand( | ||||
b'filedata', | b'filedata', | ||||
args={ | args={ | ||||
b'haveparents': { | b'haveparents': { | ||||
b'type': b'bool', | b'type': b'bool', | ||||
b'default': lambda: False, | b'default': lambda: False, | ||||
b'example': True, | b'example': True, | ||||
}, | }, | ||||
b'nodes': {b'type': b'list', b'example': [b'0123456...'],}, | b'nodes': { | ||||
b'type': b'list', | |||||
b'example': [b'0123456...'], | |||||
}, | |||||
b'fields': { | b'fields': { | ||||
b'type': b'set', | b'type': b'set', | ||||
b'default': set, | b'default': set, | ||||
b'example': {b'parents', b'revision'}, | b'example': {b'parents', b'revision'}, | ||||
b'validvalues': {b'parents', b'revision', b'linknode'}, | b'validvalues': {b'parents', b'revision', b'linknode'}, | ||||
}, | }, | ||||
b'path': {b'type': b'bytes', b'example': b'foo.txt',}, | b'path': { | ||||
b'type': b'bytes', | |||||
b'example': b'foo.txt', | |||||
}, | |||||
}, | }, | ||||
permission=b'pull', | permission=b'pull', | ||||
# TODO censoring a file revision won't invalidate the cache. | # TODO censoring a file revision won't invalidate the cache. | ||||
# Figure out a way to take censoring into account when deriving | # Figure out a way to take censoring into account when deriving | ||||
# the cache key. | # the cache key. | ||||
cachekeyfn=makecommandcachekeyfn(b'filedata', 1, allargs=True), | cachekeyfn=makecommandcachekeyfn(b'filedata', 1, allargs=True), | ||||
) | ) | ||||
def filedata(repo, proto, haveparents, nodes, fields, path): | def filedata(repo, proto, haveparents, nodes, fields, path): | ||||
b'pathfilter': { | b'pathfilter': { | ||||
b'type': b'dict', | b'type': b'dict', | ||||
b'default': lambda: None, | b'default': lambda: None, | ||||
b'example': {b'include': [b'path:tests']}, | b'example': {b'include': [b'path:tests']}, | ||||
}, | }, | ||||
b'revisions': { | b'revisions': { | ||||
b'type': b'list', | b'type': b'list', | ||||
b'example': [ | b'example': [ | ||||
{b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],} | { | ||||
b'type': b'changesetexplicit', | |||||
b'nodes': [b'abcdef...'], | |||||
} | |||||
], | ], | ||||
}, | }, | ||||
}, | }, | ||||
permission=b'pull', | permission=b'pull', | ||||
# TODO censoring a file revision won't invalidate the cache. | # TODO censoring a file revision won't invalidate the cache. | ||||
# Figure out a way to take censoring into account when deriving | # Figure out a way to take censoring into account when deriving | ||||
# the cache key. | # the cache key. | ||||
cachekeyfn=makecommandcachekeyfn(b'filesdata', 1, allargs=True), | cachekeyfn=makecommandcachekeyfn(b'filesdata', 1, allargs=True), | ||||
) | ) | ||||
def knownv2(repo, proto, nodes): | def knownv2(repo, proto, nodes): | ||||
result = b''.join(b'1' if n else b'0' for n in repo.known(nodes)) | result = b''.join(b'1' if n else b'0' for n in repo.known(nodes)) | ||||
yield result | yield result | ||||
@wireprotocommand( | @wireprotocommand( | ||||
b'listkeys', | b'listkeys', | ||||
args={b'namespace': {b'type': b'bytes', b'example': b'ns',},}, | args={ | ||||
b'namespace': { | |||||
b'type': b'bytes', | |||||
b'example': b'ns', | |||||
}, | |||||
}, | |||||
permission=b'pull', | permission=b'pull', | ||||
) | ) | ||||
def listkeysv2(repo, proto, namespace): | def listkeysv2(repo, proto, namespace): | ||||
keys = repo.listkeys(encoding.tolocal(namespace)) | keys = repo.listkeys(encoding.tolocal(namespace)) | ||||
keys = { | keys = { | ||||
encoding.fromlocal(k): encoding.fromlocal(v) | encoding.fromlocal(k): encoding.fromlocal(v) | ||||
for k, v in pycompat.iteritems(keys) | for k, v in pycompat.iteritems(keys) | ||||
} | } | ||||
yield keys | yield keys | ||||
@wireprotocommand( | @wireprotocommand( | ||||
b'lookup', | b'lookup', | ||||
args={b'key': {b'type': b'bytes', b'example': b'foo',},}, | args={ | ||||
b'key': { | |||||
b'type': b'bytes', | |||||
b'example': b'foo', | |||||
}, | |||||
}, | |||||
permission=b'pull', | permission=b'pull', | ||||
) | ) | ||||
def lookupv2(repo, proto, key): | def lookupv2(repo, proto, key): | ||||
key = encoding.tolocal(key) | key = encoding.tolocal(key) | ||||
# TODO handle exception. | # TODO handle exception. | ||||
node = repo.lookup(key) | node = repo.lookup(key) | ||||
yield node | yield node | ||||
def manifestdatacapabilities(repo, proto): | def manifestdatacapabilities(repo, proto): | ||||
batchsize = repo.ui.configint( | batchsize = repo.ui.configint( | ||||
b'experimental', b'server.manifestdata.recommended-batch-size' | b'experimental', b'server.manifestdata.recommended-batch-size' | ||||
) | ) | ||||
return { | return { | ||||
b'recommendedbatchsize': batchsize, | b'recommendedbatchsize': batchsize, | ||||
} | } | ||||
@wireprotocommand( | @wireprotocommand( | ||||
b'manifestdata', | b'manifestdata', | ||||
args={ | args={ | ||||
b'nodes': {b'type': b'list', b'example': [b'0123456...'],}, | b'nodes': { | ||||
b'type': b'list', | |||||
b'example': [b'0123456...'], | |||||
}, | |||||
b'haveparents': { | b'haveparents': { | ||||
b'type': b'bool', | b'type': b'bool', | ||||
b'default': lambda: False, | b'default': lambda: False, | ||||
b'example': True, | b'example': True, | ||||
}, | }, | ||||
b'fields': { | b'fields': { | ||||
b'type': b'set', | b'type': b'set', | ||||
b'default': set, | b'default': set, | ||||
b'example': {b'parents', b'revision'}, | b'example': {b'parents', b'revision'}, | ||||
b'validvalues': {b'parents', b'revision'}, | b'validvalues': {b'parents', b'revision'}, | ||||
}, | }, | ||||
b'tree': {b'type': b'bytes', b'example': b'',}, | b'tree': { | ||||
b'type': b'bytes', | |||||
b'example': b'', | |||||
}, | |||||
}, | }, | ||||
permission=b'pull', | permission=b'pull', | ||||
cachekeyfn=makecommandcachekeyfn(b'manifestdata', 1, allargs=True), | cachekeyfn=makecommandcachekeyfn(b'manifestdata', 1, allargs=True), | ||||
extracapabilitiesfn=manifestdatacapabilities, | extracapabilitiesfn=manifestdatacapabilities, | ||||
) | ) | ||||
def manifestdata(repo, proto, haveparents, nodes, fields, tree): | def manifestdata(repo, proto, haveparents, nodes, fields, tree): | ||||
store = repo.manifestlog.getstorage(tree) | store = repo.manifestlog.getstorage(tree) | ||||
for extra in followingdata: | for extra in followingdata: | ||||
yield extra | yield extra | ||||
@wireprotocommand( | @wireprotocommand( | ||||
b'pushkey', | b'pushkey', | ||||
args={ | args={ | ||||
b'namespace': {b'type': b'bytes', b'example': b'ns',}, | b'namespace': { | ||||
b'key': {b'type': b'bytes', b'example': b'key',}, | b'type': b'bytes', | ||||
b'old': {b'type': b'bytes', b'example': b'old',}, | b'example': b'ns', | ||||
b'new': {b'type': b'bytes', b'example': b'new',}, | }, | ||||
b'key': { | |||||
b'type': b'bytes', | |||||
b'example': b'key', | |||||
}, | |||||
b'old': { | |||||
b'type': b'bytes', | |||||
b'example': b'old', | |||||
}, | |||||
b'new': { | |||||
b'type': b'bytes', | |||||
b'example': b'new', | |||||
}, | |||||
}, | }, | ||||
permission=b'push', | permission=b'push', | ||||
) | ) | ||||
def pushkeyv2(repo, proto, namespace, key, old, new): | def pushkeyv2(repo, proto, namespace, key, old, new): | ||||
# TODO handle ui output redirection | # TODO handle ui output redirection | ||||
yield repo.pushkey( | yield repo.pushkey( | ||||
encoding.tolocal(namespace), | encoding.tolocal(namespace), | ||||
encoding.tolocal(key), | encoding.tolocal(key), |
# this overhead can slow down execution. | # this overhead can slow down execution. | ||||
_DISALLOW_THREAD_UNSAFE = pycompat.iswindows | _DISALLOW_THREAD_UNSAFE = pycompat.iswindows | ||||
else: | else: | ||||
_STARTUP_COST = 1e30 | _STARTUP_COST = 1e30 | ||||
_DISALLOW_THREAD_UNSAFE = False | _DISALLOW_THREAD_UNSAFE = False | ||||
def worthwhile(ui, costperop, nops, threadsafe=True): | def worthwhile(ui, costperop, nops, threadsafe=True): | ||||
'''try to determine whether the benefit of multiple processes can | """try to determine whether the benefit of multiple processes can | ||||
outweigh the cost of starting them''' | outweigh the cost of starting them""" | ||||
if not threadsafe and _DISALLOW_THREAD_UNSAFE: | if not threadsafe and _DISALLOW_THREAD_UNSAFE: | ||||
return False | return False | ||||
linear = costperop * nops | linear = costperop * nops | ||||
workers = _numworkers(ui) | workers = _numworkers(ui) | ||||
benefit = linear - (_STARTUP_COST * workers + linear / workers) | benefit = linear - (_STARTUP_COST * workers + linear / workers) | ||||
return benefit >= 0.15 | return benefit >= 0.15 | ||||
def worker( | def worker( | ||||
ui, costperarg, func, staticargs, args, hasretval=False, threadsafe=True | ui, costperarg, func, staticargs, args, hasretval=False, threadsafe=True | ||||
): | ): | ||||
'''run a function, possibly in parallel in multiple worker | """run a function, possibly in parallel in multiple worker | ||||
processes. | processes. | ||||
returns a progress iterator | returns a progress iterator | ||||
costperarg - cost of a single task | costperarg - cost of a single task | ||||
func - function to run. It is expected to return a progress iterator. | func - function to run. It is expected to return a progress iterator. | ||||
staticargs - arguments to pass to every invocation of the function | staticargs - arguments to pass to every invocation of the function | ||||
args - arguments to split into chunks, to pass to individual | args - arguments to split into chunks, to pass to individual | ||||
workers | workers | ||||
hasretval - when True, func and the current function return an progress | hasretval - when True, func and the current function return an progress | ||||
iterator then a dict (encoded as an iterator that yield many (False, ..) | iterator then a dict (encoded as an iterator that yield many (False, ..) | ||||
then a (True, dict)). The dicts are joined in some arbitrary order, so | then a (True, dict)). The dicts are joined in some arbitrary order, so | ||||
overlapping keys are a bad idea. | overlapping keys are a bad idea. | ||||
threadsafe - whether work items are thread safe and can be executed using | threadsafe - whether work items are thread safe and can be executed using | ||||
a thread-based worker. Should be disabled for CPU heavy tasks that don't | a thread-based worker. Should be disabled for CPU heavy tasks that don't | ||||
release the GIL. | release the GIL. | ||||
''' | """ | ||||
enabled = ui.configbool(b'worker', b'enabled') | enabled = ui.configbool(b'worker', b'enabled') | ||||
if enabled and worthwhile(ui, costperarg, len(args), threadsafe=threadsafe): | if enabled and worthwhile(ui, costperarg, len(args), threadsafe=threadsafe): | ||||
return _platformworker(ui, func, staticargs, args, hasretval) | return _platformworker(ui, func, staticargs, args, hasretval) | ||||
return func(*staticargs + (args,)) | return func(*staticargs + (args,)) | ||||
def _posixworker(ui, func, staticargs, args, hasretval): | def _posixworker(ui, func, staticargs, args, hasretval): | ||||
workers = _numworkers(ui) | workers = _numworkers(ui) | ||||
if status < 0: | if status < 0: | ||||
os.kill(os.getpid(), -status) | os.kill(os.getpid(), -status) | ||||
raise error.WorkerError(status) | raise error.WorkerError(status) | ||||
if hasretval: | if hasretval: | ||||
yield True, retval | yield True, retval | ||||
def _posixexitstatus(code): | def _posixexitstatus(code): | ||||
'''convert a posix exit status into the same form returned by | """convert a posix exit status into the same form returned by | ||||
os.spawnv | os.spawnv | ||||
returns None if the process was stopped instead of exiting''' | returns None if the process was stopped instead of exiting""" | ||||
if os.WIFEXITED(code): | if os.WIFEXITED(code): | ||||
return os.WEXITSTATUS(code) | return os.WEXITSTATUS(code) | ||||
elif os.WIFSIGNALED(code): | elif os.WIFSIGNALED(code): | ||||
return -(os.WTERMSIG(code)) | return -(os.WTERMSIG(code)) | ||||
def _windowsworker(ui, func, staticargs, args, hasretval): | def _windowsworker(ui, func, staticargs, args, hasretval): | ||||
class Worker(threading.Thread): | class Worker(threading.Thread): | ||||
if pycompat.iswindows: | if pycompat.iswindows: | ||||
_platformworker = _windowsworker | _platformworker = _windowsworker | ||||
else: | else: | ||||
_platformworker = _posixworker | _platformworker = _posixworker | ||||
_exitstatus = _posixexitstatus | _exitstatus = _posixexitstatus | ||||
def partition(lst, nslices): | def partition(lst, nslices): | ||||
'''partition a list into N slices of roughly equal size | """partition a list into N slices of roughly equal size | ||||
The current strategy takes every Nth element from the input. If | The current strategy takes every Nth element from the input. If | ||||
we ever write workers that need to preserve grouping in input | we ever write workers that need to preserve grouping in input | ||||
we should consider allowing callers to specify a partition strategy. | we should consider allowing callers to specify a partition strategy. | ||||
mpm is not a fan of this partitioning strategy when files are involved. | mpm is not a fan of this partitioning strategy when files are involved. | ||||
In his words: | In his words: | ||||
the revlog ordering on disk by sorting the revlogs by hash and suddenly | the revlog ordering on disk by sorting the revlogs by hash and suddenly | ||||
performance of my kernel checkout benchmark dropped by ~10x because the | performance of my kernel checkout benchmark dropped by ~10x because the | ||||
"working set" of sectors visited no longer fit in the drive's cache and | "working set" of sectors visited no longer fit in the drive's cache and | ||||
the workload switched from streaming to random I/O. | the workload switched from streaming to random I/O. | ||||
What we should really be doing is have workers read filenames from a | What we should really be doing is have workers read filenames from a | ||||
ordered queue. This preserves locality and also keeps any worker from | ordered queue. This preserves locality and also keeps any worker from | ||||
getting more than one file out of balance. | getting more than one file out of balance. | ||||
''' | """ | ||||
for i in range(nslices): | for i in range(nslices): | ||||
yield lst[i::nslices] | yield lst[i::nslices] |
pythonlib = dllbasename[:-4] | pythonlib = dllbasename[:-4] | ||||
# Copy the pythonXY.dll next to the binary so that it runs | # Copy the pythonXY.dll next to the binary so that it runs | ||||
# without tampering with PATH. | # without tampering with PATH. | ||||
fsdecode = lambda x: x | fsdecode = lambda x: x | ||||
if sys.version_info[0] >= 3: | if sys.version_info[0] >= 3: | ||||
fsdecode = os.fsdecode | fsdecode = os.fsdecode | ||||
dest = os.path.join( | dest = os.path.join( | ||||
os.path.dirname(self.hgtarget), fsdecode(dllbasename), | os.path.dirname(self.hgtarget), | ||||
fsdecode(dllbasename), | |||||
) | ) | ||||
if not os.path.exists(dest): | if not os.path.exists(dest): | ||||
shutil.copy(buf.value, dest) | shutil.copy(buf.value, dest) | ||||
if not pythonlib: | if not pythonlib: | ||||
log.warn( | log.warn( | ||||
'could not determine Python DLL filename; assuming pythonXY' | 'could not determine Python DLL filename; assuming pythonXY' | ||||
# Screen out egg related commands to prevent egg generation. But allow | # Screen out egg related commands to prevent egg generation. But allow | ||||
# mercurial.egg-info generation, since that is part of modern | # mercurial.egg-info generation, since that is part of modern | ||||
# packaging. | # packaging. | ||||
excl = {'bdist_egg'} | excl = {'bdist_egg'} | ||||
return filter(lambda x: x not in excl, install.get_sub_commands(self)) | return filter(lambda x: x not in excl, install.get_sub_commands(self)) | ||||
class hginstalllib(install_lib): | class hginstalllib(install_lib): | ||||
''' | """ | ||||
This is a specialization of install_lib that replaces the copy_file used | This is a specialization of install_lib that replaces the copy_file used | ||||
there so that it supports setting the mode of files after copying them, | there so that it supports setting the mode of files after copying them, | ||||
instead of just preserving the mode that the files originally had. If your | instead of just preserving the mode that the files originally had. If your | ||||
system has a umask of something like 027, preserving the permissions when | system has a umask of something like 027, preserving the permissions when | ||||
copying will lead to a broken install. | copying will lead to a broken install. | ||||
Note that just passing keep_permissions=False to copy_file would be | Note that just passing keep_permissions=False to copy_file would be | ||||
insufficient, as it might still be applying a umask. | insufficient, as it might still be applying a umask. | ||||
''' | """ | ||||
def run(self): | def run(self): | ||||
realcopyfile = file_util.copy_file | realcopyfile = file_util.copy_file | ||||
def copyfileandsetmode(*args, **kwargs): | def copyfileandsetmode(*args, **kwargs): | ||||
src, dst = args[0], args[1] | src, dst = args[0], args[1] | ||||
dst, copied = realcopyfile(*args, **kwargs) | dst, copied = realcopyfile(*args, **kwargs) | ||||
if copied: | if copied: | ||||
file_util.copy_file = copyfileandsetmode | file_util.copy_file = copyfileandsetmode | ||||
try: | try: | ||||
install_lib.run(self) | install_lib.run(self) | ||||
finally: | finally: | ||||
file_util.copy_file = realcopyfile | file_util.copy_file = realcopyfile | ||||
class hginstallscripts(install_scripts): | class hginstallscripts(install_scripts): | ||||
''' | """ | ||||
This is a specialization of install_scripts that replaces the @LIBDIR@ with | This is a specialization of install_scripts that replaces the @LIBDIR@ with | ||||
the configured directory for modules. If possible, the path is made relative | the configured directory for modules. If possible, the path is made relative | ||||
to the directory for scripts. | to the directory for scripts. | ||||
''' | """ | ||||
def initialize_options(self): | def initialize_options(self): | ||||
install_scripts.initialize_options(self) | install_scripts.initialize_options(self) | ||||
self.install_lib = None | self.install_lib = None | ||||
def finalize_options(self): | def finalize_options(self): | ||||
install_scripts.finalize_options(self) | install_scripts.finalize_options(self) | ||||
] | ] | ||||
class RustCompilationError(CCompilerError): | class RustCompilationError(CCompilerError): | ||||
"""Exception class for Rust compilation errors.""" | """Exception class for Rust compilation errors.""" | ||||
class RustExtension(Extension): | class RustExtension(Extension): | ||||
"""Base classes for concrete Rust Extension classes. | """Base classes for concrete Rust Extension classes.""" | ||||
""" | |||||
rusttargetdir = os.path.join('rust', 'target', 'release') | rusttargetdir = os.path.join('rust', 'target', 'release') | ||||
def __init__( | def __init__( | ||||
self, mpath, sources, rustlibname, subcrate, py3_features=None, **kw | self, mpath, sources, rustlibname, subcrate, py3_features=None, **kw | ||||
): | ): | ||||
Extension.__init__(self, mpath, sources, **kw) | Extension.__init__(self, mpath, sources, **kw) | ||||
srcdir = self.rustsrcdir = os.path.join('rust', subcrate) | srcdir = self.rustsrcdir = os.path.join('rust', subcrate) | ||||
'mercurial/cext/manifest.c', | 'mercurial/cext/manifest.c', | ||||
'mercurial/cext/parsers.c', | 'mercurial/cext/parsers.c', | ||||
'mercurial/cext/pathencode.c', | 'mercurial/cext/pathencode.c', | ||||
'mercurial/cext/revlog.c', | 'mercurial/cext/revlog.c', | ||||
], | ], | ||||
include_dirs=common_include_dirs, | include_dirs=common_include_dirs, | ||||
extra_compile_args=common_cflags, | extra_compile_args=common_cflags, | ||||
depends=common_depends | depends=common_depends | ||||
+ ['mercurial/cext/charencode.h', 'mercurial/cext/revlog.h',], | + [ | ||||
'mercurial/cext/charencode.h', | |||||
'mercurial/cext/revlog.h', | |||||
], | |||||
), | ), | ||||
Extension( | Extension( | ||||
'mercurial.cext.osutil', | 'mercurial.cext.osutil', | ||||
['mercurial/cext/osutil.c'], | ['mercurial/cext/osutil.c'], | ||||
include_dirs=common_include_dirs, | include_dirs=common_include_dirs, | ||||
extra_compile_args=common_cflags + osutil_cflags, | extra_compile_args=common_cflags + osutil_cflags, | ||||
extra_link_args=osutil_ldflags, | extra_link_args=osutil_ldflags, | ||||
depends=common_depends, | depends=common_depends, | ||||
msvccompilerclass.initialize(self) | msvccompilerclass.initialize(self) | ||||
# "warning LNK4197: export 'func' specified multiple times" | # "warning LNK4197: export 'func' specified multiple times" | ||||
self.ldflags_shared.append('/ignore:4197') | self.ldflags_shared.append('/ignore:4197') | ||||
self.ldflags_shared_debug.append('/ignore:4197') | self.ldflags_shared_debug.append('/ignore:4197') | ||||
msvccompiler.MSVCCompiler = HackedMSVCCompiler | msvccompiler.MSVCCompiler = HackedMSVCCompiler | ||||
packagedata = { | packagedata = { | ||||
'mercurial': ['locale/*/LC_MESSAGES/hg.mo', 'dummycert.pem',], | 'mercurial': [ | ||||
'mercurial.defaultrc': ['*.rc',], | 'locale/*/LC_MESSAGES/hg.mo', | ||||
'mercurial.helptext': ['*.txt',], | 'dummycert.pem', | ||||
'mercurial.helptext.internals': ['*.txt',], | ], | ||||
'mercurial.defaultrc': [ | |||||
'*.rc', | |||||
], | |||||
'mercurial.helptext': [ | |||||
'*.txt', | |||||
], | |||||
'mercurial.helptext.internals': [ | |||||
'*.txt', | |||||
], | |||||
} | } | ||||
def ordinarypath(p): | def ordinarypath(p): | ||||
return p and p[0] != '.' and p[-1] != '~' | return p and p[0] != '.' and p[-1] != '~' | ||||
for root in ('templates',): | for root in ('templates',): |
) | ) | ||||
from mercurial.hgweb import server | from mercurial.hgweb import server | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'badserver', b'closeafteraccept', default=False, | b'badserver', | ||||
b'closeafteraccept', | |||||
default=False, | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'badserver', b'closeafterrecvbytes', default=b'0', | b'badserver', | ||||
b'closeafterrecvbytes', | |||||
default=b'0', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'badserver', b'closeaftersendbytes', default=b'0', | b'badserver', | ||||
b'closeaftersendbytes', | |||||
default=b'0', | |||||
) | ) | ||||
configitem( | configitem( | ||||
b'badserver', b'closebeforeaccept', default=False, | b'badserver', | ||||
b'closebeforeaccept', | |||||
default=False, | |||||
) | ) | ||||
# We can't adjust __class__ on a socket instance. So we define a proxy type. | # We can't adjust __class__ on a socket instance. So we define a proxy type. | ||||
class socketproxy(object): | class socketproxy(object): | ||||
__slots__ = ( | __slots__ = ( | ||||
'_orig', | '_orig', | ||||
'_logfp', | '_logfp', | ||||
'_closeafterrecvbytes', | '_closeafterrecvbytes', |
rustext.__name__ # force actual import (see hgdemandimport) | rustext.__name__ # force actual import (see hgdemandimport) | ||||
except ImportError: | except ImportError: | ||||
rustext = None | rustext = None | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'fakedirstatewritetime', b'fakenow', default=None, | b'fakedirstatewritetime', | ||||
b'fakenow', | |||||
default=None, | |||||
) | ) | ||||
parsers = policy.importmod('parsers') | parsers = policy.importmod('parsers') | ||||
rustmod = policy.importrust('parsers') | rustmod = policy.importrust('parsers') | ||||
def pack_dirstate(fakenow, orig, dmap, copymap, pl, now): | def pack_dirstate(fakenow, orig, dmap, copymap, pl, now): | ||||
# execute what original parsers.pack_dirstate should do actually | # execute what original parsers.pack_dirstate should do actually |
# extension to emulate invoking 'patch.internalpatch()' at the time | # extension to emulate invoking 'patch.internalpatch()' at the time | ||||
# specified by '[fakepatchtime] fakenow' | # specified by '[fakepatchtime] fakenow' | ||||
from __future__ import absolute_import | from __future__ import absolute_import | ||||
from mercurial import ( | from mercurial import ( | ||||
extensions, | extensions, | ||||
patch as patchmod, | patch as patchmod, | ||||
registrar, | registrar, | ||||
) | ) | ||||
from mercurial.utils import dateutil | from mercurial.utils import dateutil | ||||
configtable = {} | configtable = {} | ||||
configitem = registrar.configitem(configtable) | configitem = registrar.configitem(configtable) | ||||
configitem( | configitem( | ||||
b'fakepatchtime', b'fakenow', default=None, | b'fakepatchtime', | ||||
b'fakenow', | |||||
default=None, | |||||
) | ) | ||||
def internalpatch( | def internalpatch( | ||||
orig, | orig, | ||||
ui, | ui, | ||||
repo, | repo, | ||||
patchobj, | patchobj, |
revlog.REVIDX_FLAGS_ORDER.extend(flags) | revlog.REVIDX_FLAGS_ORDER.extend(flags) | ||||
# Teach exchange to use changegroup 3 | # Teach exchange to use changegroup 3 | ||||
for k in bundlecaches._bundlespeccontentopts.keys(): | for k in bundlecaches._bundlespeccontentopts.keys(): | ||||
bundlecaches._bundlespeccontentopts[k][b"cg.version"] = b"03" | bundlecaches._bundlespeccontentopts[k][b"cg.version"] = b"03" | ||||
# Register flag processors for each extension | # Register flag processors for each extension | ||||
flagutil.addflagprocessor( | flagutil.addflagprocessor( | ||||
REVIDX_NOOP, (noopdonothingread, noopdonothing, validatehash,) | REVIDX_NOOP, | ||||
( | |||||
noopdonothingread, | |||||
noopdonothing, | |||||
validatehash, | |||||
), | |||||
) | ) | ||||
flagutil.addflagprocessor( | flagutil.addflagprocessor( | ||||
REVIDX_BASE64, (b64decode, b64encode, bypass,), | REVIDX_BASE64, | ||||
( | |||||
b64decode, | |||||
b64encode, | |||||
bypass, | |||||
), | |||||
) | ) | ||||
flagutil.addflagprocessor( | flagutil.addflagprocessor( | ||||
REVIDX_GZIP, (gzipdecompress, gzipcompress, bypass) | REVIDX_GZIP, (gzipdecompress, gzipcompress, bypass) | ||||
) | ) |
@check('black', 'the black formatter for python') | @check('black', 'the black formatter for python') | ||||
def has_black(): | def has_black(): | ||||
blackcmd = 'black --version' | blackcmd = 'black --version' | ||||
version_regex = b'black, version ([0-9a-b.]+)' | version_regex = b'black, version ([0-9a-b.]+)' | ||||
version = matchoutput(blackcmd, version_regex) | version = matchoutput(blackcmd, version_regex) | ||||
sv = distutils.version.StrictVersion | sv = distutils.version.StrictVersion | ||||
return version and sv(_bytes2sys(version.group(1))) >= sv('19.10b0') | return version and sv(_bytes2sys(version.group(1))) >= sv('20.8b1') | ||||
@check('pytype', 'the pytype type checker') | @check('pytype', 'the pytype type checker') | ||||
def has_pytype(): | def has_pytype(): | ||||
pytypecmd = 'pytype --version' | pytypecmd = 'pytype --version' | ||||
version = matchoutput(pytypecmd, b'[0-9a-b.]+') | version = matchoutput(pytypecmd, b'[0-9a-b.]+') | ||||
sv = distutils.version.StrictVersion | sv = distutils.version.StrictVersion | ||||
return version and sv(_bytes2sys(version.group(0))) >= sv('2019.10.17') | return version and sv(_bytes2sys(version.group(0))) >= sv('2019.10.17') |
except Exception: | except Exception: | ||||
traceback.print_exc(file=sys.stdout) | traceback.print_exc(file=sys.stdout) | ||||
sys.exit(1) | sys.exit(1) | ||||
return accept | return accept | ||||
def roundtrips(data, decode, encode): | def roundtrips(data, decode, encode): | ||||
"""helper to tests function that must do proper encode/decode roundtripping | """helper to tests function that must do proper encode/decode roundtripping""" | ||||
""" | |||||
@given(data) | @given(data) | ||||
def testroundtrips(value): | def testroundtrips(value): | ||||
encoded = encode(value) | encoded = encode(value) | ||||
decoded = decode(encoded) | decoded = decode(encoded) | ||||
if decoded != value: | if decoded != value: | ||||
raise ValueError( | raise ValueError( | ||||
"Round trip failed: %s(%r) -> %s(%r) -> %r" | "Round trip failed: %s(%r) -> %s(%r) -> %r" | ||||
print("Round trip OK") | print("Round trip OK") | ||||
# strategy for generating bytestring that might be an issue for Mercurial | # strategy for generating bytestring that might be an issue for Mercurial | ||||
bytestrings = ( | bytestrings = ( | ||||
st.builds( | st.builds( | ||||
lambda s, e: s.encode(e), | lambda s, e: s.encode(e), | ||||
st.text(), | st.text(), | ||||
st.sampled_from(['utf-8', 'utf-16',]), | st.sampled_from( | ||||
[ | |||||
'utf-8', | |||||
'utf-16', | |||||
] | |||||
), | |||||
) | ) | ||||
) | st.binary() | ) | st.binary() |
hgconf = parser.add_argument_group('Mercurial Configuration') | hgconf = parser.add_argument_group('Mercurial Configuration') | ||||
hgconf.add_argument( | hgconf.add_argument( | ||||
"--chg", | "--chg", | ||||
action="store_true", | action="store_true", | ||||
help="install and use chg wrapper in place of hg", | help="install and use chg wrapper in place of hg", | ||||
) | ) | ||||
hgconf.add_argument( | hgconf.add_argument( | ||||
"--chg-debug", action="store_true", help="show chg debug logs", | "--chg-debug", | ||||
action="store_true", | |||||
help="show chg debug logs", | |||||
) | ) | ||||
hgconf.add_argument("--compiler", help="compiler to build with") | hgconf.add_argument("--compiler", help="compiler to build with") | ||||
hgconf.add_argument( | hgconf.add_argument( | ||||
'--extra-config-opt', | '--extra-config-opt', | ||||
action="append", | action="append", | ||||
default=[], | default=[], | ||||
help='set the given config opt in the test hgrc', | help='set the given config opt in the test hgrc', | ||||
) | ) | ||||
"""Tasks to perform after run().""" | """Tasks to perform after run().""" | ||||
for entry in self._daemonpids: | for entry in self._daemonpids: | ||||
killdaemons(entry) | killdaemons(entry) | ||||
self._daemonpids = [] | self._daemonpids = [] | ||||
if self._keeptmpdir: | if self._keeptmpdir: | ||||
log( | log( | ||||
'\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' | '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' | ||||
% (_bytes2sys(self._testtmp), _bytes2sys(self._threadtmp),) | % ( | ||||
_bytes2sys(self._testtmp), | |||||
_bytes2sys(self._threadtmp), | |||||
) | |||||
) | ) | ||||
else: | else: | ||||
try: | try: | ||||
shutil.rmtree(self._testtmp) | shutil.rmtree(self._testtmp) | ||||
except OSError: | except OSError: | ||||
# unreadable directory may be left in $TESTTMP; fix permission | # unreadable directory may be left in $TESTTMP; fix permission | ||||
# and try again | # and try again | ||||
makecleanable(self._testtmp) | makecleanable(self._testtmp) | ||||
if os.altsep: | if os.altsep: | ||||
_l = l.replace(b'\\', b'/') | _l = l.replace(b'\\', b'/') | ||||
if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l: | if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l: | ||||
return True, True | return True, True | ||||
return retry, True | return retry, True | ||||
@staticmethod | @staticmethod | ||||
def parsehghaveoutput(lines): | def parsehghaveoutput(lines): | ||||
'''Parse hghave log lines. | """Parse hghave log lines. | ||||
Return tuple of lists (missing, failed): | Return tuple of lists (missing, failed): | ||||
* the missing/unknown features | * the missing/unknown features | ||||
* the features for which existence check failed''' | * the features for which existence check failed""" | ||||
missing = [] | missing = [] | ||||
failed = [] | failed = [] | ||||
for line in lines: | for line in lines: | ||||
if line.startswith(TTest.SKIPPED_PREFIX): | if line.startswith(TTest.SKIPPED_PREFIX): | ||||
line = line.splitlines()[0] | line = line.splitlines()[0] | ||||
missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :])) | missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :])) | ||||
elif line.startswith(TTest.FAILED_PREFIX): | elif line.startswith(TTest.FAILED_PREFIX): | ||||
line = line.splitlines()[0] | line = line.splitlines()[0] | ||||
if options.color == 'auto': | if options.color == 'auto': | ||||
self.color = pygmentspresent and self.stream.isatty() | self.color = pygmentspresent and self.stream.isatty() | ||||
elif options.color == 'never': | elif options.color == 'never': | ||||
self.color = False | self.color = False | ||||
else: # 'always', for testing purposes | else: # 'always', for testing purposes | ||||
self.color = pygmentspresent | self.color = pygmentspresent | ||||
def onStart(self, test): | def onStart(self, test): | ||||
""" Can be overriden by custom TestResult | """Can be overriden by custom TestResult""" | ||||
""" | |||||
def onEnd(self): | def onEnd(self): | ||||
""" Can be overriden by custom TestResult | """Can be overriden by custom TestResult""" | ||||
""" | |||||
def addFailure(self, test, reason): | def addFailure(self, test, reason): | ||||
self.failures.append((test, reason)) | self.failures.append((test, reason)) | ||||
if self._options.first: | if self._options.first: | ||||
self.stop() | self.stop() | ||||
else: | else: | ||||
with iolock: | with iolock: | ||||
'extensions.logexceptions=%s' % logexceptions.decode('utf-8') | 'extensions.logexceptions=%s' % logexceptions.decode('utf-8') | ||||
) | ) | ||||
vlog("# Using TESTDIR", _bytes2sys(self._testdir)) | vlog("# Using TESTDIR", _bytes2sys(self._testdir)) | ||||
vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR'])) | vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR'])) | ||||
vlog("# Using HGTMP", _bytes2sys(self._hgtmp)) | vlog("# Using HGTMP", _bytes2sys(self._hgtmp)) | ||||
vlog("# Using PATH", os.environ["PATH"]) | vlog("# Using PATH", os.environ["PATH"]) | ||||
vlog( | vlog( | ||||
"# Using", _bytes2sys(IMPL_PATH), _bytes2sys(osenvironb[IMPL_PATH]), | "# Using", | ||||
_bytes2sys(IMPL_PATH), | |||||
_bytes2sys(osenvironb[IMPL_PATH]), | |||||
) | ) | ||||
vlog("# Writing to directory", _bytes2sys(self._outputdir)) | vlog("# Writing to directory", _bytes2sys(self._outputdir)) | ||||
try: | try: | ||||
return self._runtests(testdescs) or 0 | return self._runtests(testdescs) or 0 | ||||
finally: | finally: | ||||
time.sleep(0.1) | time.sleep(0.1) | ||||
self._cleanup() | self._cleanup() |
# replace a single chunk | # replace a single chunk | ||||
testfilefixup(case0, b'', [b'', b'']) | testfilefixup(case0, b'', [b'', b'']) | ||||
testfilefixup(case0, b'2', [b'', b'2']) | testfilefixup(case0, b'2', [b'', b'2']) | ||||
testfilefixup(case0, b'22', [b'', b'22']) | testfilefixup(case0, b'22', [b'', b'22']) | ||||
testfilefixup(case0, b'222', [b'', b'222']) | testfilefixup(case0, b'222', [b'', b'222']) | ||||
# input case 1: 3 lines, each commit adds one line | # input case 1: 3 lines, each commit adds one line | ||||
case1 = buildcontents([(b'1', [1, 2, 3]), (b'2', [2, 3]), (b'3', [3]),]) | case1 = buildcontents( | ||||
[ | |||||
(b'1', [1, 2, 3]), | |||||
(b'2', [2, 3]), | |||||
(b'3', [3]), | |||||
] | |||||
) | |||||
# 1:1 line mapping | # 1:1 line mapping | ||||
testfilefixup(case1, b'123', case1) | testfilefixup(case1, b'123', case1) | ||||
testfilefixup(case1, b'12c', [b'', b'1', b'12', b'12c']) | testfilefixup(case1, b'12c', [b'', b'1', b'12', b'12c']) | ||||
testfilefixup(case1, b'1b3', [b'', b'1', b'1b', b'1b3']) | testfilefixup(case1, b'1b3', [b'', b'1', b'1b', b'1b3']) | ||||
testfilefixup(case1, b'1bc', [b'', b'1', b'1b', b'1bc']) | testfilefixup(case1, b'1bc', [b'', b'1', b'1b', b'1bc']) | ||||
testfilefixup(case1, b'a23', [b'', b'a', b'a2', b'a23']) | testfilefixup(case1, b'a23', [b'', b'a', b'a2', b'a23']) | ||||
testfilefixup(case1, b'a2c', [b'', b'a', b'a2', b'a2c']) | testfilefixup(case1, b'a2c', [b'', b'a', b'a2', b'a2c']) | ||||
testfilefixup(case1, b'123c', [b'', b'1', b'12', b'123c']) | testfilefixup(case1, b'123c', [b'', b'1', b'12', b'123c']) | ||||
testfilefixup(case1, b'a123', [b'', b'a1', b'a12', b'a123']) | testfilefixup(case1, b'a123', [b'', b'a1', b'a12', b'a123']) | ||||
# (confusing) insertions | # (confusing) insertions | ||||
testfilefixup(case1, b'1a23', case1) | testfilefixup(case1, b'1a23', case1) | ||||
testfilefixup(case1, b'12b3', case1) | testfilefixup(case1, b'12b3', case1) | ||||
# input case 2: delete in the middle | # input case 2: delete in the middle | ||||
case2 = buildcontents([(b'11', [1, 2]), (b'22', [1]), (b'33', [1, 2]),]) | case2 = buildcontents( | ||||
[ | |||||
(b'11', [1, 2]), | |||||
(b'22', [1]), | |||||
(b'33', [1, 2]), | |||||
] | |||||
) | |||||
# deletion (optimize code should make it 2 chunks) | # deletion (optimize code should make it 2 chunks) | ||||
testfilefixup( | testfilefixup( | ||||
case2, b'', [b'', b'22', b''], fixups=[(4, 0, 2, 0, 0), (4, 2, 4, 0, 0)] | case2, b'', [b'', b'22', b''], fixups=[(4, 0, 2, 0, 0), (4, 2, 4, 0, 0)] | ||||
) | ) | ||||
# 1:1 line mapping | # 1:1 line mapping | ||||
testfilefixup(case2, b'aaaa', [b'', b'aa22aa', b'aaaa']) | testfilefixup(case2, b'aaaa', [b'', b'aa22aa', b'aaaa']) | ||||
# non 1:1 edits | # non 1:1 edits | ||||
# note: unlike case0, the chunk is not "continuous" and no edit allowed | # note: unlike case0, the chunk is not "continuous" and no edit allowed | ||||
testfilefixup(case2, b'aaa', case2) | testfilefixup(case2, b'aaa', case2) | ||||
# input case 3: rev 3 reverts rev 2 | # input case 3: rev 3 reverts rev 2 | ||||
case3 = buildcontents([(b'1', [1, 2, 3]), (b'2', [2]), (b'3', [1, 2, 3]),]) | case3 = buildcontents( | ||||
[ | |||||
(b'1', [1, 2, 3]), | |||||
(b'2', [2]), | |||||
(b'3', [1, 2, 3]), | |||||
] | |||||
) | |||||
# 1:1 line mapping | # 1:1 line mapping | ||||
testfilefixup(case3, b'13', case3) | testfilefixup(case3, b'13', case3) | ||||
testfilefixup(case3, b'1b', [b'', b'1b', b'12b', b'1b']) | testfilefixup(case3, b'1b', [b'', b'1b', b'12b', b'1b']) | ||||
testfilefixup(case3, b'a3', [b'', b'a3', b'a23', b'a3']) | testfilefixup(case3, b'a3', [b'', b'a3', b'a23', b'a3']) | ||||
testfilefixup(case3, b'ab', [b'', b'ab', b'a2b', b'ab']) | testfilefixup(case3, b'ab', [b'', b'ab', b'a2b', b'ab']) | ||||
# non 1:1 edits | # non 1:1 edits | ||||
testfilefixup(case3, b'a', case3) | testfilefixup(case3, b'a', case3) | ||||
testfilefixup(case3, b'abc', case3) | testfilefixup(case3, b'abc', case3) | ||||
# deletion | # deletion | ||||
testfilefixup(case3, b'', [b'', b'', b'2', b'']) | testfilefixup(case3, b'', [b'', b'', b'2', b'']) | ||||
# insertion | # insertion | ||||
testfilefixup(case3, b'a13c', [b'', b'a13c', b'a123c', b'a13c']) | testfilefixup(case3, b'a13c', [b'', b'a13c', b'a123c', b'a13c']) | ||||
# input case 4: a slightly complex case | # input case 4: a slightly complex case | ||||
case4 = buildcontents( | case4 = buildcontents( | ||||
[ | [ | ||||
(b'1', [1, 2, 3]), | (b'1', [1, 2, 3]), | ||||
(b'2', [2, 3]), | (b'2', [2, 3]), | ||||
(b'3', [1, 2,]), | ( | ||||
b'3', | |||||
[ | |||||
1, | |||||
2, | |||||
], | |||||
), | |||||
(b'4', [1, 3]), | (b'4', [1, 3]), | ||||
(b'5', [3]), | (b'5', [3]), | ||||
(b'6', [2, 3]), | (b'6', [2, 3]), | ||||
(b'7', [2]), | (b'7', [2]), | ||||
(b'8', [2, 3]), | (b'8', [2, 3]), | ||||
(b'9', [3]), | (b'9', [3]), | ||||
] | ] | ||||
) | ) | ||||
testfilefixup(case4, b'1245689', case4) | testfilefixup(case4, b'1245689', case4) | ||||
testfilefixup(case4, b'1a2456bbb', case4) | testfilefixup(case4, b'1a2456bbb', case4) | ||||
testfilefixup(case4, b'1abc5689', case4) | testfilefixup(case4, b'1abc5689', case4) | ||||
testfilefixup(case4, b'1ab5689', [b'', b'134', b'1a3678', b'1ab5689']) | testfilefixup(case4, b'1ab5689', [b'', b'134', b'1a3678', b'1ab5689']) | ||||
testfilefixup(case4, b'aa2bcd8ee', [b'', b'aa34', b'aa23d78', b'aa2bcd8ee']) | testfilefixup(case4, b'aa2bcd8ee', [b'', b'aa34', b'aa23d78', b'aa2bcd8ee']) | ||||
testfilefixup(case4, b'aa2bcdd8ee', [b'', b'aa34', b'aa23678', b'aa24568ee']) | testfilefixup(case4, b'aa2bcdd8ee', [b'', b'aa34', b'aa23678', b'aa24568ee']) | ||||
testfilefixup(case4, b'aaaaaa', case4) | testfilefixup(case4, b'aaaaaa', case4) | ||||
testfilefixup(case4, b'aa258b', [b'', b'aa34', b'aa2378', b'aa258b']) | testfilefixup(case4, b'aa258b', [b'', b'aa34', b'aa2378', b'aa258b']) | ||||
testfilefixup(case4, b'25bb', [b'', b'34', b'23678', b'25689']) | testfilefixup(case4, b'25bb', [b'', b'34', b'23678', b'25689']) | ||||
testfilefixup(case4, b'27', [b'', b'34', b'23678', b'245689']) | testfilefixup(case4, b'27', [b'', b'34', b'23678', b'245689']) | ||||
testfilefixup(case4, b'28', [b'', b'34', b'2378', b'28']) | testfilefixup(case4, b'28', [b'', b'34', b'2378', b'28']) | ||||
testfilefixup(case4, b'', [b'', b'34', b'37', b'']) | testfilefixup(case4, b'', [b'', b'34', b'37', b'']) | ||||
# input case 5: replace a small chunk which is near a deleted line | # input case 5: replace a small chunk which is near a deleted line | ||||
case5 = buildcontents([(b'12', [1, 2]), (b'3', [1]), (b'4', [1, 2]),]) | case5 = buildcontents( | ||||
[ | |||||
(b'12', [1, 2]), | |||||
(b'3', [1]), | |||||
(b'4', [1, 2]), | |||||
] | |||||
) | |||||
testfilefixup(case5, b'1cd4', [b'', b'1cd34', b'1cd4']) | testfilefixup(case5, b'1cd4', [b'', b'1cd34', b'1cd4']) | ||||
# input case 6: base "changeset" is immutable | # input case 6: base "changeset" is immutable | ||||
case6 = [b'1357', b'0125678'] | case6 = [b'1357', b'0125678'] | ||||
testfilefixup(case6, b'0125678', case6) | testfilefixup(case6, b'0125678', case6) | ||||
testfilefixup(case6, b'0a25678', case6) | testfilefixup(case6, b'0a25678', case6) |
) | ) | ||||
if pycompat.ispy3: | if pycompat.ispy3: | ||||
long = int | long = int | ||||
xrange = range | xrange = range | ||||
def buildgraph(rng, nodes=100, rootprob=0.05, mergeprob=0.2, prevprob=0.7): | def buildgraph(rng, nodes=100, rootprob=0.05, mergeprob=0.2, prevprob=0.7): | ||||
'''nodes: total number of nodes in the graph | """nodes: total number of nodes in the graph | ||||
rootprob: probability that a new node (not 0) will be a root | rootprob: probability that a new node (not 0) will be a root | ||||
mergeprob: probability that, excluding a root a node will be a merge | mergeprob: probability that, excluding a root a node will be a merge | ||||
prevprob: probability that p1 will be the previous node | prevprob: probability that p1 will be the previous node | ||||
return value is a graph represented as an adjacency list. | return value is a graph represented as an adjacency list. | ||||
''' | """ | ||||
graph = [None] * nodes | graph = [None] * nodes | ||||
for i in xrange(nodes): | for i in xrange(nodes): | ||||
if i == 0 or rng.random() < rootprob: | if i == 0 or rng.random() < rootprob: | ||||
graph[i] = [nullrev] | graph[i] = [nullrev] | ||||
elif i == 1: | elif i == 1: | ||||
graph[i] = [0] | graph[i] = [0] | ||||
elif rng.random() < mergeprob: | elif rng.random() < mergeprob: | ||||
if i == 2 or rng.random() < prevprob: | if i == 2 or rng.random() < prevprob: | ||||
) | ) | ||||
): | ): | ||||
print("%% removeancestorsfrom(), example %d" % (i + 1)) | print("%% removeancestorsfrom(), example %d" % (i + 1)) | ||||
missanc = ancestor.incrementalmissingancestors(graph.get, bases) | missanc = ancestor.incrementalmissingancestors(graph.get, bases) | ||||
missanc.removeancestorsfrom(revs) | missanc.removeancestorsfrom(revs) | ||||
print("remaining (sorted): %s" % sorted(list(revs))) | print("remaining (sorted): %s" % sorted(list(revs))) | ||||
for i, (bases, revs) in enumerate( | for i, (bases, revs) in enumerate( | ||||
(({10}, {11}), ({11}, {10}), ({7}, {9, 11}),) | ( | ||||
({10}, {11}), | |||||
({11}, {10}), | |||||
({7}, {9, 11}), | |||||
) | |||||
): | ): | ||||
print("%% missingancestors(), example %d" % (i + 1)) | print("%% missingancestors(), example %d" % (i + 1)) | ||||
missanc = ancestor.incrementalmissingancestors(graph.get, bases) | missanc = ancestor.incrementalmissingancestors(graph.get, bases) | ||||
print("return %s" % missanc.missingancestors(revs)) | print("return %s" % missanc.missingancestors(revs)) | ||||
print("% removeancestorsfrom(), bigger graph") | print("% removeancestorsfrom(), bigger graph") | ||||
vecgraph = [ | vecgraph = [ | ||||
[-1, -1], | [-1, -1], |
def hello(self): | def hello(self): | ||||
return b"Ready." | return b"Ready." | ||||
# equivalent of localrepo.localrepository | # equivalent of localrepo.localrepository | ||||
class localthing(thing): | class localthing(thing): | ||||
def foo(self, one, two=None): | def foo(self, one, two=None): | ||||
if one: | if one: | ||||
return b"%s and %s" % (one, two,) | return b"%s and %s" % ( | ||||
one, | |||||
two, | |||||
) | |||||
return b"Nope" | return b"Nope" | ||||
def bar(self, b, a): | def bar(self, b, a): | ||||
return b"%s und %s" % (b, a,) | return b"%s und %s" % ( | ||||
b, | |||||
a, | |||||
) | |||||
def greet(self, name=None): | def greet(self, name=None): | ||||
return b"Hello, %s" % name | return b"Hello, %s" % name | ||||
@contextlib.contextmanager | @contextlib.contextmanager | ||||
def commandexecutor(self): | def commandexecutor(self): | ||||
e = localrepo.localcommandexecutor(self) | e = localrepo.localcommandexecutor(self) | ||||
try: | try: | ||||
return self.server.perform(req) | return self.server.perform(req) | ||||
def _submitbatch(self, cmds): | def _submitbatch(self, cmds): | ||||
req = [] | req = [] | ||||
for name, args in cmds: | for name, args in cmds: | ||||
args = b','.join(n + b'=' + escapearg(v) for n, v in args) | args = b','.join(n + b'=' + escapearg(v) for n, v in args) | ||||
req.append(name + b':' + args) | req.append(name + b':' + args) | ||||
req = b';'.join(req) | req = b';'.join(req) | ||||
res = self._submitone(b'batch', [(b'cmds', req,)]) | res = self._submitone( | ||||
b'batch', | |||||
[ | |||||
( | |||||
b'cmds', | |||||
req, | |||||
) | |||||
], | |||||
) | |||||
for r in res.split(b';'): | for r in res.split(b';'): | ||||
yield r | yield r | ||||
@contextlib.contextmanager | @contextlib.contextmanager | ||||
def commandexecutor(self): | def commandexecutor(self): | ||||
e = wireprotov1peer.peerexecutor(self) | e = wireprotov1peer.peerexecutor(self) | ||||
try: | try: | ||||
yield e | yield e | ||||
finally: | finally: | ||||
e.close() | e.close() | ||||
@wireprotov1peer.batchable | @wireprotov1peer.batchable | ||||
def foo(self, one, two=None): | def foo(self, one, two=None): | ||||
encargs = [(b'one', mangle(one),), (b'two', mangle(two),)] | encargs = [ | ||||
( | |||||
b'one', | |||||
mangle(one), | |||||
), | |||||
( | |||||
b'two', | |||||
mangle(two), | |||||
), | |||||
] | |||||
encresref = wireprotov1peer.future() | encresref = wireprotov1peer.future() | ||||
yield encargs, encresref | yield encargs, encresref | ||||
yield unmangle(encresref.value) | yield unmangle(encresref.value) | ||||
@wireprotov1peer.batchable | @wireprotov1peer.batchable | ||||
def bar(self, b, a): | def bar(self, b, a): | ||||
encresref = wireprotov1peer.future() | encresref = wireprotov1peer.future() | ||||
yield [(b'b', mangle(b),), (b'a', mangle(a),)], encresref | yield [ | ||||
( | |||||
b'b', | |||||
mangle(b), | |||||
), | |||||
( | |||||
b'a', | |||||
mangle(a), | |||||
), | |||||
], encresref | |||||
yield unmangle(encresref.value) | yield unmangle(encresref.value) | ||||
# greet is coded directly. It therefore does not support batching. If it | # greet is coded directly. It therefore does not support batching. If it | ||||
# does appear in a batch, the batch is split around greet, and the call to | # does appear in a batch, the batch is split around greet, and the call to | ||||
# greet is done in its own roundtrip. | # greet is done in its own roundtrip. | ||||
def greet(self, name=None): | def greet(self, name=None): | ||||
return unmangle(self._submitone(b'greet', [(b'name', mangle(name),)])) | return unmangle( | ||||
self._submitone( | |||||
b'greet', | |||||
[ | |||||
( | |||||
b'name', | |||||
mangle(name), | |||||
) | |||||
], | |||||
) | |||||
) | |||||
# demo remote usage | # demo remote usage | ||||
myproxy = remotething(myserver) | myproxy = remotething(myserver) | ||||
print() | print() | ||||
bprint(b"== Remote") | bprint(b"== Remote") | ||||
use(myproxy) | use(myproxy) |
): | ): | ||||
cborutil.decodeall(b'\x9f\xff') | cborutil.decodeall(b'\x9f\xff') | ||||
def testfromiter1(self): | def testfromiter1(self): | ||||
source = [b'foo'] | source = [b'foo'] | ||||
self.assertEqual( | self.assertEqual( | ||||
list(cborutil.streamencodearrayfromiter(source)), | list(cborutil.streamencodearrayfromiter(source)), | ||||
[b'\x9f', b'\x43', b'foo', b'\xff',], | [ | ||||
b'\x9f', | |||||
b'\x43', | |||||
b'foo', | |||||
b'\xff', | |||||
], | |||||
) | ) | ||||
dest = b''.join(cborutil.streamencodearrayfromiter(source)) | dest = b''.join(cborutil.streamencodearrayfromiter(source)) | ||||
self.assertEqual(cbor.loads(dest), source) | self.assertEqual(cbor.loads(dest), source) | ||||
with self.assertRaisesRegex( | with self.assertRaisesRegex( | ||||
cborutil.CBORDecodeError, 'indefinite length uint not allowed' | cborutil.CBORDecodeError, 'indefinite length uint not allowed' | ||||
): | ): | ||||
'indefinite length bytestrings not ' 'allowed as array values', | 'indefinite length bytestrings not ' 'allowed as array values', | ||||
): | ): | ||||
cborutil.decodeall(encoded) | cborutil.decodeall(encoded) | ||||
class SetTests(TestCase): | class SetTests(TestCase): | ||||
def testempty(self): | def testempty(self): | ||||
self.assertEqual( | self.assertEqual( | ||||
list(cborutil.streamencode(set())), [b'\xd9\x01\x02', b'\x80',] | list(cborutil.streamencode(set())), | ||||
[ | |||||
b'\xd9\x01\x02', | |||||
b'\x80', | |||||
], | |||||
) | ) | ||||
self.assertEqual(cborutil.decodeall(b'\xd9\x01\x02\x80'), [set()]) | self.assertEqual(cborutil.decodeall(b'\xd9\x01\x02\x80'), [set()]) | ||||
def testset(self): | def testset(self): | ||||
source = {b'foo', None, 42} | source = {b'foo', None, 42} | ||||
encoded = b''.join(cborutil.streamencode(source)) | encoded = b''.join(cborutil.streamencode(source)) | ||||
) | ) | ||||
with self.assertRaisesRegex( | with self.assertRaisesRegex( | ||||
cborutil.CBORDecodeError, | cborutil.CBORDecodeError, | ||||
'indefinite length bytestrings not ' 'allowed as set values', | 'indefinite length bytestrings not ' 'allowed as set values', | ||||
): | ): | ||||
cborutil.decodeall(encoded) | cborutil.decodeall(encoded) | ||||
encoded = b''.join([b'\xd9\x01\x02', b'\x81', b'\x80',]) # empty array | encoded = b''.join( | ||||
[ | |||||
b'\xd9\x01\x02', | |||||
b'\x81', | |||||
b'\x80', | |||||
] | |||||
) # empty array | |||||
with self.assertRaisesRegex( | with self.assertRaisesRegex( | ||||
cborutil.CBORDecodeError, 'collections not allowed as set values' | cborutil.CBORDecodeError, 'collections not allowed as set values' | ||||
): | ): | ||||
cborutil.decodeall(encoded) | cborutil.decodeall(encoded) | ||||
encoded = b''.join([b'\xd9\x01\x02', b'\x81', b'\xa0',]) # empty map | encoded = b''.join( | ||||
[ | |||||
b'\xd9\x01\x02', | |||||
b'\x81', | |||||
b'\xa0', | |||||
] | |||||
) # empty map | |||||
with self.assertRaisesRegex( | with self.assertRaisesRegex( | ||||
cborutil.CBORDecodeError, 'collections not allowed as set values' | cborutil.CBORDecodeError, 'collections not allowed as set values' | ||||
): | ): | ||||
cborutil.decodeall(encoded) | cborutil.decodeall(encoded) | ||||
encoded = b''.join( | encoded = b''.join( | ||||
[ | [ | ||||
) | ) | ||||
with self.assertRaisesRegex( | with self.assertRaisesRegex( | ||||
cborutil.CBORDecodeError, | cborutil.CBORDecodeError, | ||||
'indefinite length bytestrings not ' 'allowed as map keys', | 'indefinite length bytestrings not ' 'allowed as map keys', | ||||
): | ): | ||||
cborutil.decodeall(encoded) | cborutil.decodeall(encoded) | ||||
encoded = b''.join([b'\xa1', b'\x80', b'\x43foo',]) # empty array | encoded = b''.join( | ||||
[ | |||||
b'\xa1', | |||||
b'\x80', | |||||
b'\x43foo', | |||||
] | |||||
) # empty array | |||||
with self.assertRaisesRegex( | with self.assertRaisesRegex( | ||||
cborutil.CBORDecodeError, 'collections not supported as map keys' | cborutil.CBORDecodeError, 'collections not supported as map keys' | ||||
): | ): | ||||
cborutil.decodeall(encoded) | cborutil.decodeall(encoded) | ||||
def testillegalvalue(self): | def testillegalvalue(self): | ||||
encoded = b''.join( | encoded = b''.join( | ||||
class DecodeallTests(TestCase): | class DecodeallTests(TestCase): | ||||
def testemptyinput(self): | def testemptyinput(self): | ||||
self.assertEqual(cborutil.decodeall(b''), []) | self.assertEqual(cborutil.decodeall(b''), []) | ||||
def testpartialinput(self): | def testpartialinput(self): | ||||
encoded = b''.join( | encoded = b''.join( | ||||
[b'\x82', b'\x01',] # array of 2 elements # integer 1 | [ | ||||
b'\x82', | |||||
b'\x01', | |||||
] # array of 2 elements # integer 1 | |||||
) | ) | ||||
with self.assertRaisesRegex( | with self.assertRaisesRegex( | ||||
cborutil.CBORDecodeError, 'input data not complete' | cborutil.CBORDecodeError, 'input data not complete' | ||||
): | ): | ||||
cborutil.decodeall(encoded) | cborutil.decodeall(encoded) | ||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
import silenttestrunner | import silenttestrunner | ||||
silenttestrunner.main(__name__) | silenttestrunner.main(__name__) |
fileset = 'set:(**.py)' | fileset = 'set:(**.py)' | ||||
cwd = os.path.dirname(os.environ["TESTDIR"]) | cwd = os.path.dirname(os.environ["TESTDIR"]) | ||||
if not os.path.isdir(os.path.join(cwd, ".hg")): | if not os.path.isdir(os.path.join(cwd, ".hg")): | ||||
sys.exit(0) | sys.exit(0) | ||||
files = subprocess.check_output( | files = subprocess.check_output( | ||||
"hg files --print0 \"%s\"" % fileset, shell=True, cwd=cwd, | "hg files --print0 \"%s\"" % fileset, | ||||
shell=True, | |||||
cwd=cwd, | |||||
).split(b'\0') | ).split(b'\0') | ||||
if sys.version_info[0] >= 3: | if sys.version_info[0] >= 3: | ||||
cwd = os.fsencode(cwd) | cwd = os.fsencode(cwd) | ||||
mods_tested = set() | mods_tested = set() | ||||
for f in files: | for f in files: | ||||
if not f: | if not f: |
) | ) | ||||
def testsimpleedits(self): | def testsimpleedits(self): | ||||
ll = linelog.linelog() | ll = linelog.linelog() | ||||
# Initial revision: add lines 0, 1, and 2 | # Initial revision: add lines 0, 1, and 2 | ||||
ll.replacelines(1, 0, 0, 0, 3) | ll.replacelines(1, 0, 0, 0, 3) | ||||
self.assertEqual( | self.assertEqual( | ||||
[(l.rev, l.linenum) for l in ll.annotate(1)], | [(l.rev, l.linenum) for l in ll.annotate(1)], | ||||
[(1, 0), (1, 1), (1, 2),], | [ | ||||
(1, 0), | |||||
(1, 1), | |||||
(1, 2), | |||||
], | |||||
) | ) | ||||
# Replace line 1 with a new line | # Replace line 1 with a new line | ||||
ll.replacelines(2, 1, 2, 1, 2) | ll.replacelines(2, 1, 2, 1, 2) | ||||
self.assertEqual( | self.assertEqual( | ||||
[(l.rev, l.linenum) for l in ll.annotate(2)], | [(l.rev, l.linenum) for l in ll.annotate(2)], | ||||
[(1, 0), (2, 1), (1, 2),], | [ | ||||
(1, 0), | |||||
(2, 1), | |||||
(1, 2), | |||||
], | |||||
) | ) | ||||
# delete a line out of 2 | # delete a line out of 2 | ||||
ll.replacelines(3, 1, 2, 0, 0) | ll.replacelines(3, 1, 2, 0, 0) | ||||
self.assertEqual( | self.assertEqual( | ||||
[(l.rev, l.linenum) for l in ll.annotate(3)], [(1, 0), (1, 2),] | [(l.rev, l.linenum) for l in ll.annotate(3)], | ||||
[ | |||||
(1, 0), | |||||
(1, 2), | |||||
], | |||||
) | ) | ||||
# annotation of 1 is unchanged | # annotation of 1 is unchanged | ||||
self.assertEqual( | self.assertEqual( | ||||
[(l.rev, l.linenum) for l in ll.annotate(1)], | [(l.rev, l.linenum) for l in ll.annotate(1)], | ||||
[(1, 0), (1, 1), (1, 2),], | [ | ||||
(1, 0), | |||||
(1, 1), | |||||
(1, 2), | |||||
], | |||||
) | ) | ||||
ll.annotate(3) # set internal state to revision 3 | ll.annotate(3) # set internal state to revision 3 | ||||
start = ll.getoffset(0) | start = ll.getoffset(0) | ||||
end = ll.getoffset(1) | end = ll.getoffset(1) | ||||
self.assertEqual(ll.getalllines(start, end), [(1, 0), (2, 1), (1, 1),]) | self.assertEqual( | ||||
self.assertEqual(ll.getalllines(), [(1, 0), (2, 1), (1, 1), (1, 2),]) | ll.getalllines(start, end), | ||||
[ | |||||
(1, 0), | |||||
(2, 1), | |||||
(1, 1), | |||||
], | |||||
) | |||||
self.assertEqual( | |||||
ll.getalllines(), | |||||
[ | |||||
(1, 0), | |||||
(2, 1), | |||||
(1, 1), | |||||
(1, 2), | |||||
], | |||||
) | |||||
def testparseclinelogfile(self): | def testparseclinelogfile(self): | ||||
# This data is what the replacements in testsimpleedits | # This data is what the replacements in testsimpleedits | ||||
# produce when fed to the original linelog.c implementation. | # produce when fed to the original linelog.c implementation. | ||||
data = ( | data = ( | ||||
b'\x00\x00\x00\x0c\x00\x00\x00\x0f' | b'\x00\x00\x00\x0c\x00\x00\x00\x0f' | ||||
b'\x00\x00\x00\x00\x00\x00\x00\x02' | b'\x00\x00\x00\x00\x00\x00\x00\x02' | ||||
b'\x00\x00\x00\x05\x00\x00\x00\x06' | b'\x00\x00\x00\x05\x00\x00\x00\x06' | ||||
b'\x00\x00\x00\x06\x00\x00\x00\x00' | b'\x00\x00\x00\x06\x00\x00\x00\x00' | ||||
b'\x00\x00\x00\x00\x00\x00\x00\x07' | b'\x00\x00\x00\x00\x00\x00\x00\x07' | ||||
b'\x00\x00\x00\x06\x00\x00\x00\x02' | b'\x00\x00\x00\x06\x00\x00\x00\x02' | ||||
b'\x00\x00\x00\x00\x00\x00\x00\x00' | b'\x00\x00\x00\x00\x00\x00\x00\x00' | ||||
b'\x00\x00\x00\t\x00\x00\x00\t' | b'\x00\x00\x00\t\x00\x00\x00\t' | ||||
b'\x00\x00\x00\x00\x00\x00\x00\x0c' | b'\x00\x00\x00\x00\x00\x00\x00\x0c' | ||||
b'\x00\x00\x00\x08\x00\x00\x00\x05' | b'\x00\x00\x00\x08\x00\x00\x00\x05' | ||||
b'\x00\x00\x00\x06\x00\x00\x00\x01' | b'\x00\x00\x00\x06\x00\x00\x00\x01' | ||||
b'\x00\x00\x00\x00\x00\x00\x00\x05' | b'\x00\x00\x00\x00\x00\x00\x00\x05' | ||||
b'\x00\x00\x00\x0c\x00\x00\x00\x05' | b'\x00\x00\x00\x0c\x00\x00\x00\x05' | ||||
b'\x00\x00\x00\n\x00\x00\x00\x01' | b'\x00\x00\x00\n\x00\x00\x00\x01' | ||||
b'\x00\x00\x00\x00\x00\x00\x00\t' | b'\x00\x00\x00\x00\x00\x00\x00\t' | ||||
) | ) | ||||
llc = linelog.linelog.fromdata(data) | llc = linelog.linelog.fromdata(data) | ||||
self.assertEqual( | self.assertEqual( | ||||
[(l.rev, l.linenum) for l in llc.annotate(1)], | [(l.rev, l.linenum) for l in llc.annotate(1)], | ||||
[(1, 0), (1, 1), (1, 2),], | [ | ||||
(1, 0), | |||||
(1, 1), | |||||
(1, 2), | |||||
], | |||||
) | ) | ||||
self.assertEqual( | self.assertEqual( | ||||
[(l.rev, l.linenum) for l in llc.annotate(2)], | [(l.rev, l.linenum) for l in llc.annotate(2)], | ||||
[(1, 0), (2, 1), (1, 2),], | [ | ||||
(1, 0), | |||||
(2, 1), | |||||
(1, 2), | |||||
], | |||||
) | ) | ||||
self.assertEqual( | self.assertEqual( | ||||
[(l.rev, l.linenum) for l in llc.annotate(3)], [(1, 0), (1, 2),] | [(l.rev, l.linenum) for l in llc.annotate(3)], | ||||
[ | |||||
(1, 0), | |||||
(1, 2), | |||||
], | |||||
) | ) | ||||
# Check we emit the same bytecode. | # Check we emit the same bytecode. | ||||
ll = linelog.linelog() | ll = linelog.linelog() | ||||
# Initial revision: add lines 0, 1, and 2 | # Initial revision: add lines 0, 1, and 2 | ||||
ll.replacelines(1, 0, 0, 0, 3) | ll.replacelines(1, 0, 0, 0, 3) | ||||
# Replace line 1 with a new line | # Replace line 1 with a new line | ||||
ll.replacelines(2, 1, 2, 1, 2) | ll.replacelines(2, 1, 2, 1, 2) | ||||
# delete a line out of 2 | # delete a line out of 2 |
def postreleasefn(self, success): | def postreleasefn(self, success): | ||||
self._postreleasecalled = True | self._postreleasecalled = True | ||||
def assertacquirecalled(self, called): | def assertacquirecalled(self, called): | ||||
self._testcase.assertEqual( | self._testcase.assertEqual( | ||||
self._acquirecalled, | self._acquirecalled, | ||||
called, | called, | ||||
'expected acquire to be %s but was actually %s' | 'expected acquire to be %s but was actually %s' | ||||
% (self._tocalled(called), self._tocalled(self._acquirecalled),), | % ( | ||||
self._tocalled(called), | |||||
self._tocalled(self._acquirecalled), | |||||
), | |||||
) | ) | ||||
def resetacquirefn(self): | def resetacquirefn(self): | ||||
self._acquirecalled = False | self._acquirecalled = False | ||||
def assertreleasecalled(self, called): | def assertreleasecalled(self, called): | ||||
self._testcase.assertEqual( | self._testcase.assertEqual( | ||||
self._releasecalled, | self._releasecalled, | ||||
called, | called, | ||||
'expected release to be %s but was actually %s' | 'expected release to be %s but was actually %s' | ||||
% (self._tocalled(called), self._tocalled(self._releasecalled),), | % ( | ||||
self._tocalled(called), | |||||
self._tocalled(self._releasecalled), | |||||
), | |||||
) | ) | ||||
def assertpostreleasecalled(self, called): | def assertpostreleasecalled(self, called): | ||||
self._testcase.assertEqual( | self._testcase.assertEqual( | ||||
self._postreleasecalled, | self._postreleasecalled, | ||||
called, | called, | ||||
'expected postrelease to be %s but was actually %s' | 'expected postrelease to be %s but was actually %s' | ||||
% ( | % ( | ||||
self._tocalled(called), | self._tocalled(called), | ||||
self._tocalled(self._postreleasecalled), | self._tocalled(self._postreleasecalled), | ||||
), | ), | ||||
) | ) | ||||
def assertlockexists(self, exists): | def assertlockexists(self, exists): | ||||
actual = self.vfs.lexists(testlockname) | actual = self.vfs.lexists(testlockname) | ||||
self._testcase.assertEqual( | self._testcase.assertEqual( | ||||
actual, | actual, | ||||
exists, | exists, | ||||
'expected lock to %s but actually did %s' | 'expected lock to %s but actually did %s' | ||||
% (self._toexists(exists), self._toexists(actual),), | % ( | ||||
self._toexists(exists), | |||||
self._toexists(actual), | |||||
), | |||||
) | ) | ||||
def _tocalled(self, called): | def _tocalled(self, called): | ||||
if called: | if called: | ||||
return 'called' | return 'called' | ||||
else: | else: | ||||
return 'not called' | return 'not called' | ||||
HASH_1 = b'1' * 40 | HASH_1 = b'1' * 40 | ||||
BIN_HASH_1 = binascii.unhexlify(HASH_1) | BIN_HASH_1 = binascii.unhexlify(HASH_1) | ||||
HASH_2 = b'f' * 40 | HASH_2 = b'f' * 40 | ||||
BIN_HASH_2 = binascii.unhexlify(HASH_2) | BIN_HASH_2 = binascii.unhexlify(HASH_2) | ||||
HASH_3 = b'1234567890abcdef0987654321deadbeef0fcafe' | HASH_3 = b'1234567890abcdef0987654321deadbeef0fcafe' | ||||
BIN_HASH_3 = binascii.unhexlify(HASH_3) | BIN_HASH_3 = binascii.unhexlify(HASH_3) | ||||
A_SHORT_MANIFEST = ( | A_SHORT_MANIFEST = ( | ||||
b'bar/baz/qux.py\0%(hash2)s%(flag2)s\n' b'foo\0%(hash1)s%(flag1)s\n' | b'bar/baz/qux.py\0%(hash2)s%(flag2)s\n' b'foo\0%(hash1)s%(flag1)s\n' | ||||
) % {b'hash1': HASH_1, b'flag1': b'', b'hash2': HASH_2, b'flag2': b'l',} | ) % { | ||||
b'hash1': HASH_1, | |||||
b'flag1': b'', | |||||
b'hash2': HASH_2, | |||||
b'flag2': b'l', | |||||
} | |||||
A_DEEPER_MANIFEST = ( | A_DEEPER_MANIFEST = ( | ||||
b'a/b/c/bar.py\0%(hash3)s%(flag1)s\n' | b'a/b/c/bar.py\0%(hash3)s%(flag1)s\n' | ||||
b'a/b/c/bar.txt\0%(hash1)s%(flag1)s\n' | b'a/b/c/bar.txt\0%(hash1)s%(flag1)s\n' | ||||
b'a/b/c/foo.py\0%(hash3)s%(flag1)s\n' | b'a/b/c/foo.py\0%(hash3)s%(flag1)s\n' | ||||
b'a/b/c/foo.txt\0%(hash2)s%(flag2)s\n' | b'a/b/c/foo.txt\0%(hash2)s%(flag2)s\n' | ||||
b'a/b/d/baz.py\0%(hash3)s%(flag1)s\n' | b'a/b/d/baz.py\0%(hash3)s%(flag1)s\n' | ||||
b'a/b/d/qux.py\0%(hash1)s%(flag2)s\n' | b'a/b/d/qux.py\0%(hash1)s%(flag2)s\n' | ||||
self.assertIn('Manifest did not end in a newline.', str(v)) | self.assertIn('Manifest did not end in a newline.', str(v)) | ||||
def testHugeManifest(self): | def testHugeManifest(self): | ||||
m = self.parsemanifest(A_HUGE_MANIFEST) | m = self.parsemanifest(A_HUGE_MANIFEST) | ||||
self.assertEqual(HUGE_MANIFEST_ENTRIES, len(m)) | self.assertEqual(HUGE_MANIFEST_ENTRIES, len(m)) | ||||
self.assertEqual(len(m), len(list(m))) | self.assertEqual(len(m), len(list(m))) | ||||
def testMatchesMetadata(self): | def testMatchesMetadata(self): | ||||
'''Tests matches() for a few specific files to make sure that both | """Tests matches() for a few specific files to make sure that both | ||||
the set of files as well as their flags and nodeids are correct in | the set of files as well as their flags and nodeids are correct in | ||||
the resulting manifest.''' | the resulting manifest.""" | ||||
m = self.parsemanifest(A_HUGE_MANIFEST) | m = self.parsemanifest(A_HUGE_MANIFEST) | ||||
match = matchmod.exact([b'file1', b'file200', b'file300']) | match = matchmod.exact([b'file1', b'file200', b'file300']) | ||||
m2 = m._matches(match) | m2 = m._matches(match) | ||||
w = (b'file1\0%sx\n' b'file200\0%sl\n' b'file300\0%s\n') % ( | w = (b'file1\0%sx\n' b'file200\0%sl\n' b'file300\0%s\n') % ( | ||||
HASH_2, | HASH_2, | ||||
HASH_1, | HASH_1, | ||||
HASH_1, | HASH_1, | ||||
) | ) | ||||
self.assertEqual(w, m2.text()) | self.assertEqual(w, m2.text()) | ||||
def testMatchesNonexistentFile(self): | def testMatchesNonexistentFile(self): | ||||
'''Tests matches() for a small set of specific files, including one | """Tests matches() for a small set of specific files, including one | ||||
nonexistent file to make sure in only matches against existing files. | nonexistent file to make sure in only matches against existing files. | ||||
''' | """ | ||||
m = self.parsemanifest(A_DEEPER_MANIFEST) | m = self.parsemanifest(A_DEEPER_MANIFEST) | ||||
match = matchmod.exact( | match = matchmod.exact( | ||||
[b'a/b/c/bar.txt', b'a/b/d/qux.py', b'readme.txt', b'nonexistent'] | [b'a/b/c/bar.txt', b'a/b/d/qux.py', b'readme.txt', b'nonexistent'] | ||||
) | ) | ||||
m2 = m._matches(match) | m2 = m._matches(match) | ||||
self.assertEqual( | self.assertEqual( | ||||
[b'a/b/c/bar.txt', b'a/b/d/qux.py', b'readme.txt'], m2.keys() | [b'a/b/c/bar.txt', b'a/b/d/qux.py', b'readme.txt'], m2.keys() | ||||
) | ) | ||||
def testMatchesNonexistentDirectory(self): | def testMatchesNonexistentDirectory(self): | ||||
'''Tests matches() for a relpath match on a directory that doesn't | """Tests matches() for a relpath match on a directory that doesn't | ||||
actually exist.''' | actually exist.""" | ||||
m = self.parsemanifest(A_DEEPER_MANIFEST) | m = self.parsemanifest(A_DEEPER_MANIFEST) | ||||
match = matchmod.match( | match = matchmod.match( | ||||
util.localpath(b'/repo'), b'', [b'a/f'], default=b'relpath' | util.localpath(b'/repo'), b'', [b'a/f'], default=b'relpath' | ||||
) | ) | ||||
m2 = m._matches(match) | m2 = m._matches(match) | ||||
self.assertEqual([], m2.keys()) | self.assertEqual([], m2.keys()) | ||||
def testMatchesExactLarge(self): | def testMatchesExactLarge(self): | ||||
'''Tests matches() for files matching a large list of exact files. | """Tests matches() for files matching a large list of exact files.""" | ||||
''' | |||||
m = self.parsemanifest(A_HUGE_MANIFEST) | m = self.parsemanifest(A_HUGE_MANIFEST) | ||||
flist = m.keys()[80:300] | flist = m.keys()[80:300] | ||||
match = matchmod.exact(flist) | match = matchmod.exact(flist) | ||||
m2 = m._matches(match) | m2 = m._matches(match) | ||||
self.assertEqual(flist, m2.keys()) | self.assertEqual(flist, m2.keys()) | ||||
def testMatchesFull(self): | def testMatchesFull(self): | ||||
'''Tests matches() for what should be a full match.''' | '''Tests matches() for what should be a full match.''' | ||||
m = self.parsemanifest(A_DEEPER_MANIFEST) | m = self.parsemanifest(A_DEEPER_MANIFEST) | ||||
match = matchmod.match(util.localpath(b'/repo'), b'', [b'']) | match = matchmod.match(util.localpath(b'/repo'), b'', [b'']) | ||||
m2 = m._matches(match) | m2 = m._matches(match) | ||||
self.assertEqual(m.keys(), m2.keys()) | self.assertEqual(m.keys(), m2.keys()) | ||||
def testMatchesDirectory(self): | def testMatchesDirectory(self): | ||||
'''Tests matches() on a relpath match on a directory, which should | """Tests matches() on a relpath match on a directory, which should | ||||
match against all files within said directory.''' | match against all files within said directory.""" | ||||
m = self.parsemanifest(A_DEEPER_MANIFEST) | m = self.parsemanifest(A_DEEPER_MANIFEST) | ||||
match = matchmod.match( | match = matchmod.match( | ||||
util.localpath(b'/repo'), b'', [b'a/b'], default=b'relpath' | util.localpath(b'/repo'), b'', [b'a/b'], default=b'relpath' | ||||
) | ) | ||||
m2 = m._matches(match) | m2 = m._matches(match) | ||||
self.assertEqual( | self.assertEqual( | ||||
[ | [ | ||||
b'a/b/c/bar.py', | b'a/b/c/bar.py', | ||||
b'a/b/c/bar.txt', | b'a/b/c/bar.txt', | ||||
b'a/b/c/foo.py', | b'a/b/c/foo.py', | ||||
b'a/b/c/foo.txt', | b'a/b/c/foo.txt', | ||||
b'a/b/d/baz.py', | b'a/b/d/baz.py', | ||||
b'a/b/d/qux.py', | b'a/b/d/qux.py', | ||||
b'a/b/d/ten.txt', | b'a/b/d/ten.txt', | ||||
b'a/b/dog.py', | b'a/b/dog.py', | ||||
b'a/b/fish.py', | b'a/b/fish.py', | ||||
], | ], | ||||
m2.keys(), | m2.keys(), | ||||
) | ) | ||||
def testMatchesExactPath(self): | def testMatchesExactPath(self): | ||||
'''Tests matches() on an exact match on a directory, which should | """Tests matches() on an exact match on a directory, which should | ||||
result in an empty manifest because you can't perform an exact match | result in an empty manifest because you can't perform an exact match | ||||
against a directory.''' | against a directory.""" | ||||
m = self.parsemanifest(A_DEEPER_MANIFEST) | m = self.parsemanifest(A_DEEPER_MANIFEST) | ||||
match = matchmod.exact([b'a/b']) | match = matchmod.exact([b'a/b']) | ||||
m2 = m._matches(match) | m2 = m._matches(match) | ||||
self.assertEqual([], m2.keys()) | self.assertEqual([], m2.keys()) | ||||
def testMatchesCwd(self): | def testMatchesCwd(self): | ||||
'''Tests matches() on a relpath match with the current directory ('.') | """Tests matches() on a relpath match with the current directory ('.') | ||||
when not in the root directory.''' | when not in the root directory.""" | ||||
m = self.parsemanifest(A_DEEPER_MANIFEST) | m = self.parsemanifest(A_DEEPER_MANIFEST) | ||||
match = matchmod.match( | match = matchmod.match( | ||||
util.localpath(b'/repo'), b'a/b', [b'.'], default=b'relpath' | util.localpath(b'/repo'), b'a/b', [b'.'], default=b'relpath' | ||||
) | ) | ||||
m2 = m._matches(match) | m2 = m._matches(match) | ||||
self.assertEqual( | self.assertEqual( | ||||
[ | [ | ||||
b'a/b/c/bar.py', | b'a/b/c/bar.py', | ||||
b'a/b/c/bar.txt', | b'a/b/c/bar.txt', | ||||
b'a/b/c/foo.py', | b'a/b/c/foo.py', | ||||
b'a/b/c/foo.txt', | b'a/b/c/foo.txt', | ||||
b'a/b/d/baz.py', | b'a/b/d/baz.py', | ||||
b'a/b/d/qux.py', | b'a/b/d/qux.py', | ||||
b'a/b/d/ten.txt', | b'a/b/d/ten.txt', | ||||
b'a/b/dog.py', | b'a/b/dog.py', | ||||
b'a/b/fish.py', | b'a/b/fish.py', | ||||
], | ], | ||||
m2.keys(), | m2.keys(), | ||||
) | ) | ||||
def testMatchesWithPattern(self): | def testMatchesWithPattern(self): | ||||
'''Tests matches() for files matching a pattern that reside | """Tests matches() for files matching a pattern that reside | ||||
deeper than the specified directory.''' | deeper than the specified directory.""" | ||||
m = self.parsemanifest(A_DEEPER_MANIFEST) | m = self.parsemanifest(A_DEEPER_MANIFEST) | ||||
match = matchmod.match(util.localpath(b'/repo'), b'', [b'a/b/*/*.txt']) | match = matchmod.match(util.localpath(b'/repo'), b'', [b'a/b/*/*.txt']) | ||||
m2 = m._matches(match) | m2 = m._matches(match) | ||||
self.assertEqual( | self.assertEqual( | ||||
[b'a/b/c/bar.txt', b'a/b/c/foo.txt', b'a/b/d/ten.txt'], m2.keys() | [b'a/b/c/bar.txt', b'a/b/c/foo.txt', b'a/b/d/ten.txt'], m2.keys() | ||||
) | ) |
self.assertEqual(m.visitchildrenset(b'dir'), b'this') | self.assertEqual(m.visitchildrenset(b'dir'), b'this') | ||||
self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'all') | self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'all') | ||||
# OPT: This should probably be 'all' if its parent is? | # OPT: This should probably be 'all' if its parent is? | ||||
self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this') | self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this') | ||||
self.assertEqual(m.visitchildrenset(b'folder'), set()) | self.assertEqual(m.visitchildrenset(b'folder'), set()) | ||||
def testVisitdirRootfilesin(self): | def testVisitdirRootfilesin(self): | ||||
m = matchmod.match( | m = matchmod.match( | ||||
util.localpath(b'/repo'), b'', patterns=[b'rootfilesin:dir/subdir'], | util.localpath(b'/repo'), | ||||
b'', | |||||
patterns=[b'rootfilesin:dir/subdir'], | |||||
) | ) | ||||
assert isinstance(m, matchmod.patternmatcher) | assert isinstance(m, matchmod.patternmatcher) | ||||
self.assertFalse(m.visitdir(b'dir/subdir/x')) | self.assertFalse(m.visitdir(b'dir/subdir/x')) | ||||
self.assertFalse(m.visitdir(b'folder')) | self.assertFalse(m.visitdir(b'folder')) | ||||
# FIXME: These should probably be True. | # FIXME: These should probably be True. | ||||
self.assertFalse(m.visitdir(b'')) | self.assertFalse(m.visitdir(b'')) | ||||
self.assertFalse(m.visitdir(b'dir')) | self.assertFalse(m.visitdir(b'dir')) | ||||
self.assertFalse(m.visitdir(b'dir/subdir')) | self.assertFalse(m.visitdir(b'dir/subdir')) | ||||
def testVisitchildrensetRootfilesin(self): | def testVisitchildrensetRootfilesin(self): | ||||
m = matchmod.match( | m = matchmod.match( | ||||
util.localpath(b'/repo'), b'', patterns=[b'rootfilesin:dir/subdir'], | util.localpath(b'/repo'), | ||||
b'', | |||||
patterns=[b'rootfilesin:dir/subdir'], | |||||
) | ) | ||||
assert isinstance(m, matchmod.patternmatcher) | assert isinstance(m, matchmod.patternmatcher) | ||||
self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set()) | self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set()) | ||||
self.assertEqual(m.visitchildrenset(b'folder'), set()) | self.assertEqual(m.visitchildrenset(b'folder'), set()) | ||||
# FIXME: These should probably be {'dir'}, {'subdir'} and 'this', | # FIXME: These should probably be {'dir'}, {'subdir'} and 'this', | ||||
# respectively, or at least 'this' for all three. | # respectively, or at least 'this' for all three. | ||||
self.assertEqual(m.visitchildrenset(b''), set()) | self.assertEqual(m.visitchildrenset(b''), set()) | ||||
self.assertEqual(m.visitchildrenset(b'dir'), set()) | self.assertEqual(m.visitchildrenset(b'dir'), set()) |
d = r | d = r | ||||
for j in c: | for j in c: | ||||
d = b''.join((d[:j], d[j : j + 1].upper(), d[j + 1 :])) | d = b''.join((d[:j], d[j : j + 1].upper(), d[j + 1 :])) | ||||
combos.add(d) | combos.add(d) | ||||
return sorted(combos) | return sorted(combos) | ||||
def buildprobtable(fp, cmd='hg manifest tip'): | def buildprobtable(fp, cmd='hg manifest tip'): | ||||
'''Construct and print a table of probabilities for path name | """Construct and print a table of probabilities for path name | ||||
components. The numbers are percentages.''' | components. The numbers are percentages.""" | ||||
counts = collections.defaultdict(lambda: 0) | counts = collections.defaultdict(lambda: 0) | ||||
for line in os.popen(cmd).read().splitlines(): | for line in os.popen(cmd).read().splitlines(): | ||||
if line[-2:] in ('.i', '.d'): | if line[-2:] in ('.i', '.d'): | ||||
line = line[:-2] | line = line[:-2] | ||||
if line.startswith('data/'): | if line.startswith('data/'): | ||||
line = line[5:] | line = line[5:] | ||||
for c in line: | for c in line: |
if len(args) > 4: | if len(args) > 4: | ||||
meta = args[4] | meta = args[4] | ||||
packer.add(filename, node, base, content, metadata=meta) | packer.add(filename, node, base, content, metadata=meta) | ||||
path = packer.close() | path = packer.close() | ||||
return self.datapackreader(path) | return self.datapackreader(path) | ||||
def _testAddSingle(self, content): | def _testAddSingle(self, content): | ||||
"""Test putting a simple blob into a pack and reading it out. | """Test putting a simple blob into a pack and reading it out.""" | ||||
""" | |||||
filename = b"foo" | filename = b"foo" | ||||
node = self.getHash(content) | node = self.getHash(content) | ||||
revisions = [(filename, node, nullid, content)] | revisions = [(filename, node, nullid, content)] | ||||
pack = self.createPack(revisions) | pack = self.createPack(revisions) | ||||
if self.paramsavailable: | if self.paramsavailable: | ||||
self.assertEqual( | self.assertEqual( | ||||
pack.params.fanoutprefix, basepack.SMALLFANOUTPREFIX | pack.params.fanoutprefix, basepack.SMALLFANOUTPREFIX | ||||
for filename, node, base, content in revisions: | for filename, node, base, content in revisions: | ||||
entry = pack.getdelta(filename, node) | entry = pack.getdelta(filename, node) | ||||
self.assertEqual((content, filename, base, {}), entry) | self.assertEqual((content, filename, base, {}), entry) | ||||
chain = pack.getdeltachain(filename, node) | chain = pack.getdeltachain(filename, node) | ||||
self.assertEqual(content, chain[0][4]) | self.assertEqual(content, chain[0][4]) | ||||
def testAddDeltas(self): | def testAddDeltas(self): | ||||
"""Test putting multiple delta blobs into a pack and read the chain. | """Test putting multiple delta blobs into a pack and read the chain.""" | ||||
""" | |||||
revisions = [] | revisions = [] | ||||
filename = b"foo" | filename = b"foo" | ||||
lastnode = nullid | lastnode = nullid | ||||
for i in range(10): | for i in range(10): | ||||
content = b"abcdef%d" % i | content = b"abcdef%d" % i | ||||
node = self.getHash(content) | node = self.getHash(content) | ||||
revisions.append((filename, node, lastnode, content)) | revisions.append((filename, node, lastnode, content)) | ||||
lastnode = node | lastnode = node | ||||
pack = self.createPack(revisions) | pack = self.createPack(revisions) | ||||
entry = pack.getdelta(filename, revisions[0][1]) | entry = pack.getdelta(filename, revisions[0][1]) | ||||
realvalue = (revisions[0][3], filename, revisions[0][2], {}) | realvalue = (revisions[0][3], filename, revisions[0][2], {}) | ||||
self.assertEqual(entry, realvalue) | self.assertEqual(entry, realvalue) | ||||
# Test that the chain for the final entry has all the others | # Test that the chain for the final entry has all the others | ||||
chain = pack.getdeltachain(filename, node) | chain = pack.getdeltachain(filename, node) | ||||
for i in range(10): | for i in range(10): | ||||
content = b"abcdef%d" % i | content = b"abcdef%d" % i | ||||
self.assertEqual(content, chain[-i - 1][4]) | self.assertEqual(content, chain[-i - 1][4]) | ||||
def testPackMany(self): | def testPackMany(self): | ||||
"""Pack many related and unrelated objects. | """Pack many related and unrelated objects.""" | ||||
""" | |||||
# Build a random pack file | # Build a random pack file | ||||
revisions = [] | revisions = [] | ||||
blobs = {} | blobs = {} | ||||
random.seed(0) | random.seed(0) | ||||
for i in range(100): | for i in range(100): | ||||
filename = b"filename-%d" % i | filename = b"filename-%d" % i | ||||
filerevs = [] | filerevs = [] | ||||
for j in range(random.randint(1, 100)): | for j in range(random.randint(1, 100)): | ||||
for name, node, x, content, origmeta in revisions: | for name, node, x, content, origmeta in revisions: | ||||
parsedmeta = pack.getmeta(name, node) | parsedmeta = pack.getmeta(name, node) | ||||
# flag == 0 should be optimized out | # flag == 0 should be optimized out | ||||
if origmeta[constants.METAKEYFLAG] == 0: | if origmeta[constants.METAKEYFLAG] == 0: | ||||
del origmeta[constants.METAKEYFLAG] | del origmeta[constants.METAKEYFLAG] | ||||
self.assertEqual(parsedmeta, origmeta) | self.assertEqual(parsedmeta, origmeta) | ||||
def testGetMissing(self): | def testGetMissing(self): | ||||
"""Test the getmissing() api. | """Test the getmissing() api.""" | ||||
""" | |||||
revisions = [] | revisions = [] | ||||
filename = b"foo" | filename = b"foo" | ||||
lastnode = nullid | lastnode = nullid | ||||
for i in range(10): | for i in range(10): | ||||
content = b"abcdef%d" % i | content = b"abcdef%d" % i | ||||
node = self.getHash(content) | node = self.getHash(content) | ||||
revisions.append((filename, node, lastnode, content)) | revisions.append((filename, node, lastnode, content)) | ||||
lastnode = node | lastnode = node |
for filename, node, p1, p2, linknode, copyfrom in revisions: | for filename, node, p1, p2, linknode, copyfrom in revisions: | ||||
packer.add(filename, node, p1, p2, linknode, copyfrom) | packer.add(filename, node, p1, p2, linknode, copyfrom) | ||||
path = packer.close() | path = packer.close() | ||||
return historypack.historypack(path) | return historypack.historypack(path) | ||||
def testAddSingle(self): | def testAddSingle(self): | ||||
"""Test putting a single entry into a pack and reading it out. | """Test putting a single entry into a pack and reading it out.""" | ||||
""" | |||||
filename = b"foo" | filename = b"foo" | ||||
node = self.getFakeHash() | node = self.getFakeHash() | ||||
p1 = self.getFakeHash() | p1 = self.getFakeHash() | ||||
p2 = self.getFakeHash() | p2 = self.getFakeHash() | ||||
linknode = self.getFakeHash() | linknode = self.getFakeHash() | ||||
revisions = [(filename, node, p1, p2, linknode, None)] | revisions = [(filename, node, p1, p2, linknode, None)] | ||||
pack = self.createPack(revisions) | pack = self.createPack(revisions) | ||||
for filename, node, p1, p2, linknode, copyfrom in revisions: | for filename, node, p1, p2, linknode, copyfrom in revisions: | ||||
ap1, ap2, alinknode, acopyfrom = ancestors[node] | ap1, ap2, alinknode, acopyfrom = ancestors[node] | ||||
self.assertEqual(ap1, p1) | self.assertEqual(ap1, p1) | ||||
self.assertEqual(ap2, p2) | self.assertEqual(ap2, p2) | ||||
self.assertEqual(alinknode, linknode) | self.assertEqual(alinknode, linknode) | ||||
self.assertEqual(acopyfrom, copyfrom) | self.assertEqual(acopyfrom, copyfrom) | ||||
def testPackMany(self): | def testPackMany(self): | ||||
"""Pack many related and unrelated ancestors. | """Pack many related and unrelated ancestors.""" | ||||
""" | |||||
# Build a random pack file | # Build a random pack file | ||||
allentries = {} | allentries = {} | ||||
ancestorcounts = {} | ancestorcounts = {} | ||||
revisions = [] | revisions = [] | ||||
random.seed(0) | random.seed(0) | ||||
for i in range(100): | for i in range(100): | ||||
filename = b"filename-%d" % i | filename = b"filename-%d" % i | ||||
entries = [] | entries = [] | ||||
for filename, node, p1, p2, linknode, copyfrom in revisions: | for filename, node, p1, p2, linknode, copyfrom in revisions: | ||||
ap1, ap2, alinknode, acopyfrom = pack.getnodeinfo(filename, node) | ap1, ap2, alinknode, acopyfrom = pack.getnodeinfo(filename, node) | ||||
self.assertEqual(ap1, p1) | self.assertEqual(ap1, p1) | ||||
self.assertEqual(ap2, p2) | self.assertEqual(ap2, p2) | ||||
self.assertEqual(alinknode, linknode) | self.assertEqual(alinknode, linknode) | ||||
self.assertEqual(acopyfrom, copyfrom) | self.assertEqual(acopyfrom, copyfrom) | ||||
def testGetMissing(self): | def testGetMissing(self): | ||||
"""Test the getmissing() api. | """Test the getmissing() api.""" | ||||
""" | |||||
revisions = [] | revisions = [] | ||||
filename = b"foo" | filename = b"foo" | ||||
for i in range(10): | for i in range(10): | ||||
node = self.getFakeHash() | node = self.getFakeHash() | ||||
p1 = self.getFakeHash() | p1 = self.getFakeHash() | ||||
p2 = self.getFakeHash() | p2 = self.getFakeHash() | ||||
linknode = self.getFakeHash() | linknode = self.getFakeHash() | ||||
revisions.append((filename, node, p1, p2, linknode, None)) | revisions.append((filename, node, p1, p2, linknode, None)) |
def newrevlog(name=b'_testrevlog.i', recreate=False): | def newrevlog(name=b'_testrevlog.i', recreate=False): | ||||
if recreate: | if recreate: | ||||
tvfs.tryunlink(name) | tvfs.tryunlink(name) | ||||
rlog = revlog.revlog(tvfs, name) | rlog = revlog.revlog(tvfs, name) | ||||
return rlog | return rlog | ||||
def appendrev(rlog, text, tr, isext=False, isdelta=True): | def appendrev(rlog, text, tr, isext=False, isdelta=True): | ||||
'''Append a revision. If isext is True, set the EXTSTORED flag so flag | """Append a revision. If isext is True, set the EXTSTORED flag so flag | ||||
processor will be used (and rawtext is different from text). If isdelta is | processor will be used (and rawtext is different from text). If isdelta is | ||||
True, force the revision to be a delta, otherwise it's full text. | True, force the revision to be a delta, otherwise it's full text. | ||||
''' | """ | ||||
nextrev = len(rlog) | nextrev = len(rlog) | ||||
p1 = rlog.node(nextrev - 1) | p1 = rlog.node(nextrev - 1) | ||||
p2 = node.nullid | p2 = node.nullid | ||||
if isext: | if isext: | ||||
flags = revlog.REVIDX_EXTSTORED | flags = revlog.REVIDX_EXTSTORED | ||||
else: | else: | ||||
flags = revlog.REVIDX_DEFAULT_FLAGS | flags = revlog.REVIDX_DEFAULT_FLAGS | ||||
# Change storedeltachains temporarily, to override revlog's delta decision | # Change storedeltachains temporarily, to override revlog's delta decision | ||||
rlog._storedeltachains = isdelta | rlog._storedeltachains = isdelta | ||||
try: | try: | ||||
rlog.addrevision(text, tr, nextrev, p1, p2, flags=flags) | rlog.addrevision(text, tr, nextrev, p1, p2, flags=flags) | ||||
return nextrev | return nextrev | ||||
except Exception as ex: | except Exception as ex: | ||||
abort('rev %d: failed to append: %s' % (nextrev, ex)) | abort('rev %d: failed to append: %s' % (nextrev, ex)) | ||||
finally: | finally: | ||||
# Restore storedeltachains. It is always True, see revlog.__init__ | # Restore storedeltachains. It is always True, see revlog.__init__ | ||||
rlog._storedeltachains = True | rlog._storedeltachains = True | ||||
def addgroupcopy(rlog, tr, destname=b'_destrevlog.i', optimaldelta=True): | def addgroupcopy(rlog, tr, destname=b'_destrevlog.i', optimaldelta=True): | ||||
'''Copy revlog to destname using revlog.addgroup. Return the copied revlog. | """Copy revlog to destname using revlog.addgroup. Return the copied revlog. | ||||
This emulates push or pull. They use changegroup. Changegroup requires | This emulates push or pull. They use changegroup. Changegroup requires | ||||
repo to work. We don't have a repo, so a dummy changegroup is used. | repo to work. We don't have a repo, so a dummy changegroup is used. | ||||
If optimaldelta is True, use optimized delta parent, so the destination | If optimaldelta is True, use optimized delta parent, so the destination | ||||
revlog could probably reuse it. Otherwise it builds sub-optimal delta, and | revlog could probably reuse it. Otherwise it builds sub-optimal delta, and | ||||
the destination revlog needs more work to use it. | the destination revlog needs more work to use it. | ||||
This exercises some revlog.addgroup (and revlog._addrevision(text=None)) | This exercises some revlog.addgroup (and revlog._addrevision(text=None)) | ||||
code path, which is not covered by "appendrev" alone. | code path, which is not covered by "appendrev" alone. | ||||
''' | """ | ||||
class dummychangegroup(object): | class dummychangegroup(object): | ||||
@staticmethod | @staticmethod | ||||
def deltachunk(pnode): | def deltachunk(pnode): | ||||
pnode = pnode or node.nullid | pnode = pnode or node.nullid | ||||
parentrev = rlog.rev(pnode) | parentrev = rlog.rev(pnode) | ||||
r = parentrev + 1 | r = parentrev + 1 | ||||
if r >= len(rlog): | if r >= len(rlog): | ||||
dlog = newrevlog(destname, recreate=True) | dlog = newrevlog(destname, recreate=True) | ||||
dummydeltas = dummychangegroup().deltaiter() | dummydeltas = dummychangegroup().deltaiter() | ||||
dlog.addgroup(dummydeltas, linkmap, tr) | dlog.addgroup(dummydeltas, linkmap, tr) | ||||
return dlog | return dlog | ||||
def lowlevelcopy(rlog, tr, destname=b'_destrevlog.i'): | def lowlevelcopy(rlog, tr, destname=b'_destrevlog.i'): | ||||
'''Like addgroupcopy, but use the low level revlog._addrevision directly. | """Like addgroupcopy, but use the low level revlog._addrevision directly. | ||||
It exercises some code paths that are hard to reach easily otherwise. | It exercises some code paths that are hard to reach easily otherwise. | ||||
''' | """ | ||||
dlog = newrevlog(destname, recreate=True) | dlog = newrevlog(destname, recreate=True) | ||||
for r in rlog: | for r in rlog: | ||||
p1 = rlog.node(r - 1) | p1 = rlog.node(r - 1) | ||||
p2 = node.nullid | p2 = node.nullid | ||||
if r == 0 or (rlog.flags(r) & revlog.REVIDX_EXTSTORED): | if r == 0 or (rlog.flags(r) & revlog.REVIDX_EXTSTORED): | ||||
text = rlog.rawdata(r) | text = rlog.rawdata(r) | ||||
cachedelta = None | cachedelta = None | ||||
else: | else: | ||||
ifh.close() | ifh.close() | ||||
return dlog | return dlog | ||||
# Utilities to generate revisions for testing | # Utilities to generate revisions for testing | ||||
def genbits(n): | def genbits(n): | ||||
'''Given a number n, generate (2 ** (n * 2) + 1) numbers in range(2 ** n). | """Given a number n, generate (2 ** (n * 2) + 1) numbers in range(2 ** n). | ||||
i.e. the generated numbers have a width of n bits. | i.e. the generated numbers have a width of n bits. | ||||
The combination of two adjacent numbers will cover all possible cases. | The combination of two adjacent numbers will cover all possible cases. | ||||
That is to say, given any x, y where both x, and y are in range(2 ** n), | That is to say, given any x, y where both x, and y are in range(2 ** n), | ||||
there is an x followed immediately by y in the generated sequence. | there is an x followed immediately by y in the generated sequence. | ||||
''' | """ | ||||
m = 2 ** n | m = 2 ** n | ||||
# Gray Code. See https://en.wikipedia.org/wiki/Gray_code | # Gray Code. See https://en.wikipedia.org/wiki/Gray_code | ||||
gray = lambda x: x ^ (x >> 1) | gray = lambda x: x ^ (x >> 1) | ||||
reversegray = {gray(i): i for i in range(m)} | reversegray = {gray(i): i for i in range(m)} | ||||
# Generate (n * 2) bit gray code, yield lower n bits as X, and look for | # Generate (n * 2) bit gray code, yield lower n bits as X, and look for | ||||
# the next unused gray code where higher n bits equal to X. | # the next unused gray code where higher n bits equal to X. | ||||
def gentext(rev): | def gentext(rev): | ||||
'''Given a revision number, generate dummy text''' | '''Given a revision number, generate dummy text''' | ||||
return b''.join(b'%d\n' % j for j in range(-1, rev % 5)) | return b''.join(b'%d\n' % j for j in range(-1, rev % 5)) | ||||
def writecases(rlog, tr): | def writecases(rlog, tr): | ||||
'''Write some revisions interested to the test. | """Write some revisions interested to the test. | ||||
The test is interested in 3 properties of a revision: | The test is interested in 3 properties of a revision: | ||||
- Is it a delta or a full text? (isdelta) | - Is it a delta or a full text? (isdelta) | ||||
This is to catch some delta application issues. | This is to catch some delta application issues. | ||||
- Does it have a flag of EXTSTORED? (isext) | - Does it have a flag of EXTSTORED? (isext) | ||||
This is to catch some flag processor issues. Especially when | This is to catch some flag processor issues. Especially when | ||||
interacted with revlog deltas. | interacted with revlog deltas. | ||||
(r.delta, r.ext, r.empty, (r+1).delta, (r+1).ext, (r+1).empty) | (r.delta, r.ext, r.empty, (r+1).delta, (r+1).ext, (r+1).empty) | ||||
for r in range(len(rlog) - 1) | for r in range(len(rlog) - 1) | ||||
)) is 64. | )) is 64. | ||||
Where "r.delta", "r.ext", and "r.empty" are booleans matching properties | Where "r.delta", "r.ext", and "r.empty" are booleans matching properties | ||||
mentioned above. | mentioned above. | ||||
Return expected [(text, rawtext)]. | Return expected [(text, rawtext)]. | ||||
''' | """ | ||||
result = [] | result = [] | ||||
for i, x in enumerate(genbits(3)): | for i, x in enumerate(genbits(3)): | ||||
isdelta, isext, isempty = bool(x & 1), bool(x & 2), bool(x & 4) | isdelta, isext, isempty = bool(x & 1), bool(x & 2), bool(x & 4) | ||||
if isempty: | if isempty: | ||||
text = b'' | text = b'' | ||||
else: | else: | ||||
text = gentext(i) | text = gentext(i) | ||||
rev = appendrev(rlog, text, tr, isext=isext, isdelta=isdelta) | rev = appendrev(rlog, text, tr, isext=isext, isdelta=isdelta) |
# this would fail already without appropriate ancestor.__package__ | # this would fail already without appropriate ancestor.__package__ | ||||
from mercurial.rustext.ancestor import LazyAncestors | from mercurial.rustext.ancestor import LazyAncestors | ||||
from mercurial.testing import revlog as revlogtesting | from mercurial.testing import revlog as revlogtesting | ||||
@unittest.skipIf( | @unittest.skipIf( | ||||
rustext is None, "rustext module revlog relies on is not available", | rustext is None, | ||||
"rustext module revlog relies on is not available", | |||||
) | ) | ||||
class RustRevlogIndexTest(revlogtesting.RevlogBasedTestBase): | class RustRevlogIndexTest(revlogtesting.RevlogBasedTestBase): | ||||
def test_heads(self): | def test_heads(self): | ||||
idx = self.parseindex() | idx = self.parseindex() | ||||
rustidx = revlog.MixedIndex(idx) | rustidx = revlog.MixedIndex(idx) | ||||
self.assertEqual(rustidx.headrevs(), idx.headrevs()) | self.assertEqual(rustidx.headrevs(), idx.headrevs()) | ||||
def test_get_cindex(self): | def test_get_cindex(self): |
safetext = st.text( | safetext = st.text( | ||||
st.characters( | st.characters( | ||||
min_codepoint=1, max_codepoint=127, blacklist_categories=('Cc', 'Cs') | min_codepoint=1, max_codepoint=127, blacklist_categories=('Cc', 'Cs') | ||||
), | ), | ||||
min_size=1, | min_size=1, | ||||
).map(lambda s: s.encode('utf-8')) | ).map(lambda s: s.encode('utf-8')) | ||||
extensions = st.sampled_from(('shelve', 'mq', 'blackbox',)) | extensions = st.sampled_from( | ||||
( | |||||
'shelve', | |||||
'mq', | |||||
'blackbox', | |||||
) | |||||
) | |||||
@contextmanager | @contextmanager | ||||
def acceptableerrors(*args): | def acceptableerrors(*args): | ||||
"""Sometimes we know an operation we're about to perform might fail, and | """Sometimes we know an operation we're about to perform might fail, and | ||||
we're OK with some of the failures. In those cases this may be used as a | we're OK with some of the failures. In those cases this may be used as a | ||||
context manager and will swallow expected failures, as identified by | context manager and will swallow expected failures, as identified by | ||||
substrings of the error message Mercurial emits.""" | substrings of the error message Mercurial emits.""" | ||||
"--config %s=" % (extensionconfigkey(ext),), | "--config %s=" % (extensionconfigkey(ext),), | ||||
"", | "", | ||||
) | ) | ||||
o.write(l + os.linesep) | o.write(l + os.linesep) | ||||
with open(tf, 'r') as r: | with open(tf, 'r') as r: | ||||
t = r.read() | t = r.read() | ||||
assert ext not in t, t | assert ext not in t, t | ||||
output = subprocess.check_output( | output = subprocess.check_output( | ||||
[runtests, tf, "--local",], stderr=subprocess.STDOUT | [ | ||||
runtests, | |||||
tf, | |||||
"--local", | |||||
], | |||||
stderr=subprocess.STDOUT, | |||||
) | ) | ||||
assert "Ran 1 test" in output, output | assert "Ran 1 test" in output, output | ||||
except subprocess.CalledProcessError as e: | except subprocess.CalledProcessError as e: | ||||
note(e.output) | note(e.output) | ||||
if self.failed or e is not None: | if self.failed or e is not None: | ||||
with open(savefile, "wb") as o: | with open(savefile, "wb") as o: | ||||
o.write(ttest) | o.write(ttest) | ||||
if e is not None: | if e is not None: | ||||
content=st.one_of( | content=st.one_of( | ||||
st.binary(), st.text().map(lambda x: x.encode('utf-8')) | st.binary(), st.text().map(lambda x: x.encode('utf-8')) | ||||
), | ), | ||||
) | ) | ||||
def gencontent(self, content): | def gencontent(self, content): | ||||
return content | return content | ||||
@rule( | @rule( | ||||
target=branches, name=safetext, | target=branches, | ||||
name=safetext, | |||||
) | ) | ||||
def genbranch(self, name): | def genbranch(self, name): | ||||
return name | return name | ||||
@rule(target=paths, source=paths) | @rule(target=paths, source=paths) | ||||
def lowerpath(self, source): | def lowerpath(self, source): | ||||
return source.lower() | return source.lower() | ||||
return | return | ||||
with open(path, 'wb') as o: | with open(path, 'wb') as o: | ||||
o.write(content) | o.write(content) | ||||
self.log.append( | self.log.append( | ||||
( | ( | ||||
"$ python -c 'import binascii; " | "$ python -c 'import binascii; " | ||||
"print(binascii.unhexlify(\"%s\"))' > %s" | "print(binascii.unhexlify(\"%s\"))' > %s" | ||||
) | ) | ||||
% (binascii.hexlify(content), pipes.quote(path),) | % ( | ||||
binascii.hexlify(content), | |||||
pipes.quote(path), | |||||
) | |||||
) | ) | ||||
@rule(path=paths) | @rule(path=paths) | ||||
def addpath(self, path): | def addpath(self, path): | ||||
if os.path.exists(path): | if os.path.exists(path): | ||||
self.hg("add", "--", path) | self.hg("add", "--", path) | ||||
@rule(path=paths) | @rule(path=paths) | ||||
def forgetpath(self, path): | def forgetpath(self, path): | ||||
if os.path.exists(path): | if os.path.exists(path): | ||||
with acceptableerrors("file is already untracked",): | with acceptableerrors( | ||||
"file is already untracked", | |||||
): | |||||
self.hg("forget", "--", path) | self.hg("forget", "--", path) | ||||
@rule(s=st.none() | st.integers(0, 100)) | @rule(s=st.none() | st.integers(0, 100)) | ||||
def addremove(self, s): | def addremove(self, s): | ||||
args = ["addremove"] | args = ["addremove"] | ||||
if s is not None: | if s is not None: | ||||
args.extend(["-s", str(s)]) | args.extend(["-s", str(s)]) | ||||
self.hg(*args) | self.hg(*args) | ||||
def currentrepo(self): | def currentrepo(self): | ||||
return os.path.basename(os.getcwd()) | return os.path.basename(os.getcwd()) | ||||
@property | @property | ||||
def config(self): | def config(self): | ||||
return self.configperrepo.setdefault(self.currentrepo, {}) | return self.configperrepo.setdefault(self.currentrepo, {}) | ||||
@rule( | @rule( | ||||
target=repos, source=repos, name=reponames, | target=repos, | ||||
source=repos, | |||||
name=reponames, | |||||
) | ) | ||||
def clone(self, source, name): | def clone(self, source, name): | ||||
if not os.path.exists(os.path.join("..", name)): | if not os.path.exists(os.path.join("..", name)): | ||||
self.cd("..") | self.cd("..") | ||||
self.hg("clone", source, name) | self.hg("clone", source, name) | ||||
self.cd(name) | self.cd(name) | ||||
return name | return name | ||||
@rule( | @rule( | ||||
target=repos, name=reponames, | target=repos, | ||||
name=reponames, | |||||
) | ) | ||||
def fresh(self, name): | def fresh(self, name): | ||||
if not os.path.exists(os.path.join("..", name)): | if not os.path.exists(os.path.join("..", name)): | ||||
self.cd("..") | self.cd("..") | ||||
self.mkdirp(name) | self.mkdirp(name) | ||||
self.cd(name) | self.cd(name) | ||||
self.hg("init") | self.hg("init") | ||||
return name | return name | ||||
@rule(name=repos) | @rule(name=repos) | ||||
def switch(self, name): | def switch(self, name): | ||||
self.cd(os.path.join("..", name)) | self.cd(os.path.join("..", name)) | ||||
assert self.currentrepo == name | assert self.currentrepo == name | ||||
assert os.path.exists(".hg") | assert os.path.exists(".hg") | ||||
@rule(target=repos) | @rule(target=repos) | ||||
def origin(self): | def origin(self): | ||||
return "repo1" | return "repo1" | ||||
@rule() | @rule() | ||||
def pull(self, repo=repos): | def pull(self, repo=repos): | ||||
with acceptableerrors( | with acceptableerrors( | ||||
"repository default not found", "repository is unrelated", | "repository default not found", | ||||
"repository is unrelated", | |||||
): | ): | ||||
self.hg("pull") | self.hg("pull") | ||||
@rule(newbranch=st.booleans()) | @rule(newbranch=st.booleans()) | ||||
def push(self, newbranch): | def push(self, newbranch): | ||||
with acceptableerrors( | with acceptableerrors( | ||||
"default repository not configured", "no changes found", | "default repository not configured", | ||||
"no changes found", | |||||
): | ): | ||||
if newbranch: | if newbranch: | ||||
self.hg("push", "--new-branch") | self.hg("push", "--new-branch") | ||||
else: | else: | ||||
with acceptableerrors("creates new branches"): | with acceptableerrors("creates new branches"): | ||||
self.hg("push") | self.hg("push") | ||||
# Section: Simple side effect free "check" operations | # Section: Simple side effect free "check" operations | ||||
'a branch of the same name already exists', | 'a branch of the same name already exists', | ||||
'is reserved', | 'is reserved', | ||||
): | ): | ||||
self.hg("branch", "--", branch) | self.hg("branch", "--", branch) | ||||
@rule(branch=branches, clean=st.booleans()) | @rule(branch=branches, clean=st.booleans()) | ||||
def update(self, branch, clean): | def update(self, branch, clean): | ||||
with acceptableerrors( | with acceptableerrors( | ||||
'unknown revision', 'parse error', | 'unknown revision', | ||||
'parse error', | |||||
): | ): | ||||
if clean: | if clean: | ||||
self.hg("update", "-C", "--", branch) | self.hg("update", "-C", "--", branch) | ||||
else: | else: | ||||
self.hg("update", "--", branch) | self.hg("update", "--", branch) | ||||
# Section: Extension management | # Section: Extension management | ||||
def hasextension(self, extension): | def hasextension(self, extension): | ||||
self.underlying.close() | self.underlying.close() | ||||
def extensionconfigkey(extension): | def extensionconfigkey(extension): | ||||
return "extensions." + extension | return "extensions." + extension | ||||
settings.register_profile( | settings.register_profile( | ||||
'default', settings(timeout=300, stateful_step_count=50, max_examples=10,) | 'default', | ||||
settings( | |||||
timeout=300, | |||||
stateful_step_count=50, | |||||
max_examples=10, | |||||
), | |||||
) | ) | ||||
settings.register_profile( | settings.register_profile( | ||||
'fast', | 'fast', | ||||
settings( | settings( | ||||
timeout=10, | timeout=10, | ||||
stateful_step_count=20, | stateful_step_count=20, | ||||
max_examples=5, | max_examples=5, |
for f in meta[b'framegen']: | for f in meta[b'framegen']: | ||||
pass | pass | ||||
outstream = framing.outputstream(2) | outstream = framing.outputstream(2) | ||||
outstream.setencoder(globalui, b'zlib') | outstream.setencoder(globalui, b'zlib') | ||||
response1 = b''.join( | response1 = b''.join( | ||||
cborutil.streamencode( | cborutil.streamencode( | ||||
{b'status': b'ok', b'extra': b'response1' * 10,} | { | ||||
b'status': b'ok', | |||||
b'extra': b'response1' * 10, | |||||
} | |||||
) | ) | ||||
) | ) | ||||
response2 = b''.join( | response2 = b''.join( | ||||
cborutil.streamencode( | cborutil.streamencode( | ||||
{b'status': b'error', b'extra': b'response2' * 10,} | { | ||||
b'status': b'error', | |||||
b'extra': b'response2' * 10, | |||||
} | |||||
) | ) | ||||
) | ) | ||||
action, meta = sendframe( | action, meta = sendframe( | ||||
reactor, | reactor, | ||||
ffs( | ffs( | ||||
b'%d 2 stream-begin stream-settings eos cbor:b"zlib"' | b'%d 2 stream-begin stream-settings eos cbor:b"zlib"' | ||||
% request1.requestid | % request1.requestid | ||||
for f in meta[b'framegen']: | for f in meta[b'framegen']: | ||||
pass | pass | ||||
outstream = framing.outputstream(2) | outstream = framing.outputstream(2) | ||||
outstream.setencoder(globalui, b'zstd-8mb') | outstream.setencoder(globalui, b'zstd-8mb') | ||||
response1 = b''.join( | response1 = b''.join( | ||||
cborutil.streamencode( | cborutil.streamencode( | ||||
{b'status': b'ok', b'extra': b'response1' * 10,} | { | ||||
b'status': b'ok', | |||||
b'extra': b'response1' * 10, | |||||
} | |||||
) | ) | ||||
) | ) | ||||
response2 = b''.join( | response2 = b''.join( | ||||
cborutil.streamencode( | cborutil.streamencode( | ||||
{b'status': b'error', b'extra': b'response2' * 10,} | { | ||||
b'status': b'error', | |||||
b'extra': b'response2' * 10, | |||||
} | |||||
) | ) | ||||
) | ) | ||||
action, meta = sendframe( | action, meta = sendframe( | ||||
reactor, | reactor, | ||||
ffs( | ffs( | ||||
b'%d 2 stream-begin stream-settings eos cbor:b"zstd-8mb"' | b'%d 2 stream-begin stream-settings eos cbor:b"zstd-8mb"' | ||||
% request1.requestid | % request1.requestid |
), | ), | ||||
], | ], | ||||
) | ) | ||||
def testtextoutput2simpleatoms(self): | def testtextoutput2simpleatoms(self): | ||||
stream = framing.stream(1) | stream = framing.stream(1) | ||||
val = list( | val = list( | ||||
framing.createtextoutputframe( | framing.createtextoutputframe( | ||||
stream, 1, [(b'foo', [], []), (b'bar', [], []),] | stream, | ||||
1, | |||||
[ | |||||
(b'foo', [], []), | |||||
(b'bar', [], []), | |||||
], | |||||
) | ) | ||||
) | ) | ||||
self.assertEqual( | self.assertEqual( | ||||
val, | val, | ||||
[ | [ | ||||
ffs( | ffs( | ||||
b'1 1 stream-begin text-output 0 ' | b'1 1 stream-begin text-output 0 ' | ||||
b"cbor:[{b'msg': b'foo'}, {b'msg': b'bar'}]" | b"cbor:[{b'msg': b'foo'}, {b'msg': b'bar'}]" | ||||
) | ) | ||||
], | ], | ||||
) | ) | ||||
def testtextoutput1arg(self): | def testtextoutput1arg(self): | ||||
stream = framing.stream(1) | stream = framing.stream(1) | ||||
val = list( | val = list( | ||||
framing.createtextoutputframe( | framing.createtextoutputframe( | ||||
stream, 1, [(b'foo %s', [b'val1'], []),] | stream, | ||||
1, | |||||
[ | |||||
(b'foo %s', [b'val1'], []), | |||||
], | |||||
) | ) | ||||
) | ) | ||||
self.assertEqual( | self.assertEqual( | ||||
val, | val, | ||||
[ | [ | ||||
ffs( | ffs( | ||||
b'1 1 stream-begin text-output 0 ' | b'1 1 stream-begin text-output 0 ' | ||||
b"cbor:[{b'msg': b'foo %s', b'args': [b'val1']}]" | b"cbor:[{b'msg': b'foo %s', b'args': [b'val1']}]" | ||||
) | ) | ||||
], | ], | ||||
) | ) | ||||
def testtextoutput2arg(self): | def testtextoutput2arg(self): | ||||
stream = framing.stream(1) | stream = framing.stream(1) | ||||
val = list( | val = list( | ||||
framing.createtextoutputframe( | framing.createtextoutputframe( | ||||
stream, 1, [(b'foo %s %s', [b'val', b'value'], []),] | stream, | ||||
1, | |||||
[ | |||||
(b'foo %s %s', [b'val', b'value'], []), | |||||
], | |||||
) | ) | ||||
) | ) | ||||
self.assertEqual( | self.assertEqual( | ||||
val, | val, | ||||
[ | [ | ||||
ffs( | ffs( | ||||
b'1 1 stream-begin text-output 0 ' | b'1 1 stream-begin text-output 0 ' | ||||
b"cbor:[{b'msg': b'foo %s %s', b'args': [b'val', b'value']}]" | b"cbor:[{b'msg': b'foo %s %s', b'args': [b'val', b'value']}]" | ||||
) | ) | ||||
], | ], | ||||
) | ) | ||||
def testtextoutput1label(self): | def testtextoutput1label(self): | ||||
stream = framing.stream(1) | stream = framing.stream(1) | ||||
val = list( | val = list( | ||||
framing.createtextoutputframe( | framing.createtextoutputframe( | ||||
stream, 1, [(b'foo', [], [b'label']),] | stream, | ||||
1, | |||||
[ | |||||
(b'foo', [], [b'label']), | |||||
], | |||||
) | ) | ||||
) | ) | ||||
self.assertEqual( | self.assertEqual( | ||||
val, | val, | ||||
[ | [ | ||||
ffs( | ffs( | ||||
b'1 1 stream-begin text-output 0 ' | b'1 1 stream-begin text-output 0 ' | ||||
b"cbor:[{b'msg': b'foo', b'labels': [b'label']}]" | b"cbor:[{b'msg': b'foo', b'labels': [b'label']}]" | ||||
) | ) | ||||
], | ], | ||||
) | ) | ||||
def testargandlabel(self): | def testargandlabel(self): | ||||
stream = framing.stream(1) | stream = framing.stream(1) | ||||
val = list( | val = list( | ||||
framing.createtextoutputframe( | framing.createtextoutputframe( | ||||
stream, 1, [(b'foo %s', [b'arg'], [b'label']),] | stream, | ||||
1, | |||||
[ | |||||
(b'foo %s', [b'arg'], [b'label']), | |||||
], | |||||
) | ) | ||||
) | ) | ||||
self.assertEqual( | self.assertEqual( | ||||
val, | val, | ||||
[ | [ | ||||
ffs( | ffs( | ||||
b'1 1 stream-begin text-output 0 ' | b'1 1 stream-begin text-output 0 ' |
results = list(sendframes(reactor, frames)) | results = list(sendframes(reactor, frames)) | ||||
self.assertaction(results[-1], b'runcommand') | self.assertaction(results[-1], b'runcommand') | ||||
self.assertEqual( | self.assertEqual( | ||||
results[-1][1], | results[-1][1], | ||||
{ | { | ||||
b'requestid': 1, | b'requestid': 1, | ||||
b'command': b'command', | b'command': b'command', | ||||
b'args': {b'key': b'val', b'foo': b'bar',}, | b'args': { | ||||
b'key': b'val', | |||||
b'foo': b'bar', | |||||
}, | |||||
b'redirect': None, | b'redirect': None, | ||||
b'data': b'value1value2', | b'data': b'value1value2', | ||||
}, | }, | ||||
) | ) | ||||
def testnewandcontinuation(self): | def testnewandcontinuation(self): | ||||
result = self._sendsingleframe( | result = self._sendsingleframe( | ||||
makereactor(), | makereactor(), | ||||
), | ), | ||||
], | ], | ||||
) | ) | ||||
) | ) | ||||
self.assertaction(results[0], b'wantframe') | self.assertaction(results[0], b'wantframe') | ||||
self.assertaction(results[1], b'error') | self.assertaction(results[1], b'error') | ||||
self.assertEqual( | self.assertEqual( | ||||
results[1][1], {b'message': b'request with ID 1 already received',} | results[1][1], | ||||
{ | |||||
b'message': b'request with ID 1 already received', | |||||
}, | |||||
) | ) | ||||
def testinterleavedcommands(self): | def testinterleavedcommands(self): | ||||
cbor1 = b''.join( | cbor1 = b''.join( | ||||
cborutil.streamencode( | cborutil.streamencode( | ||||
{ | { | ||||
b'name': b'command1', | b'name': b'command1', | ||||
b'args': {b'foo': b'bar', b'key1': b'val',}, | b'args': { | ||||
b'foo': b'bar', | |||||
b'key1': b'val', | |||||
}, | |||||
} | } | ||||
) | ) | ||||
) | ) | ||||
cbor3 = b''.join( | cbor3 = b''.join( | ||||
cborutil.streamencode( | cborutil.streamencode( | ||||
{ | { | ||||
b'name': b'command3', | b'name': b'command3', | ||||
b'args': {b'biz': b'baz', b'key': b'val',}, | b'args': { | ||||
b'biz': b'baz', | |||||
b'key': b'val', | |||||
}, | |||||
} | } | ||||
) | ) | ||||
) | ) | ||||
results = list( | results = list( | ||||
sendframes( | sendframes( | ||||
makereactor(), | makereactor(), | ||||
[ | [ | ||||
), | ), | ||||
ffs(b'1 1 0 command-data 0 data'), | ffs(b'1 1 0 command-data 0 data'), | ||||
] | ] | ||||
results = list(sendframes(makereactor(), frames)) | results = list(sendframes(makereactor(), frames)) | ||||
self.assertEqual(len(results), 2) | self.assertEqual(len(results), 2) | ||||
self.assertaction(results[0], b'wantframe') | self.assertaction(results[0], b'wantframe') | ||||
self.assertaction(results[1], b'error') | self.assertaction(results[1], b'error') | ||||
self.assertEqual( | self.assertEqual( | ||||
results[1][1], {b'message': b'command data frame without flags',} | results[1][1], | ||||
{ | |||||
b'message': b'command data frame without flags', | |||||
}, | |||||
) | ) | ||||
def testframefornonreceivingrequest(self): | def testframefornonreceivingrequest(self): | ||||
"""Receiving a frame for a command that is not receiving is illegal.""" | """Receiving a frame for a command that is not receiving is illegal.""" | ||||
results = list( | results = list( | ||||
sendframes( | sendframes( | ||||
makereactor(), | makereactor(), | ||||
[ | [ | ||||
"""Receiving a request ID that matches a request that isn't finished.""" | """Receiving a request ID that matches a request that isn't finished.""" | ||||
reactor = makereactor() | reactor = makereactor() | ||||
stream = framing.stream(1) | stream = framing.stream(1) | ||||
list(sendcommandframes(reactor, stream, 1, b'command1', {})) | list(sendcommandframes(reactor, stream, 1, b'command1', {})) | ||||
results = list(sendcommandframes(reactor, stream, 1, b'command1', {})) | results = list(sendcommandframes(reactor, stream, 1, b'command1', {})) | ||||
self.assertaction(results[0], b'error') | self.assertaction(results[0], b'error') | ||||
self.assertEqual( | self.assertEqual( | ||||
results[0][1], {b'message': b'request with ID 1 is already active',} | results[0][1], | ||||
{ | |||||
b'message': b'request with ID 1 is already active', | |||||
}, | |||||
) | ) | ||||
def testduplicaterequestonactivecommandnosend(self): | def testduplicaterequestonactivecommandnosend(self): | ||||
"""Same as above but we've registered a response but haven't sent it.""" | """Same as above but we've registered a response but haven't sent it.""" | ||||
reactor = makereactor() | reactor = makereactor() | ||||
instream = framing.stream(1) | instream = framing.stream(1) | ||||
list(sendcommandframes(reactor, instream, 1, b'command1', {})) | list(sendcommandframes(reactor, instream, 1, b'command1', {})) | ||||
outstream = reactor.makeoutputstream() | outstream = reactor.makeoutputstream() | ||||
reactor.oncommandresponsereadyobjects(outstream, 1, [b'response']) | reactor.oncommandresponsereadyobjects(outstream, 1, [b'response']) | ||||
# We've registered the response but haven't sent it. From the | # We've registered the response but haven't sent it. From the | ||||
# perspective of the reactor, the command is still active. | # perspective of the reactor, the command is still active. | ||||
results = list(sendcommandframes(reactor, instream, 1, b'command1', {})) | results = list(sendcommandframes(reactor, instream, 1, b'command1', {})) | ||||
self.assertaction(results[0], b'error') | self.assertaction(results[0], b'error') | ||||
self.assertEqual( | self.assertEqual( | ||||
results[0][1], {b'message': b'request with ID 1 is already active',} | results[0][1], | ||||
{ | |||||
b'message': b'request with ID 1 is already active', | |||||
}, | |||||
) | ) | ||||
def testduplicaterequestaftersend(self): | def testduplicaterequestaftersend(self): | ||||
"""We can use a duplicate request ID after we've sent the response.""" | """We can use a duplicate request ID after we've sent the response.""" | ||||
reactor = makereactor() | reactor = makereactor() | ||||
instream = framing.stream(1) | instream = framing.stream(1) | ||||
list(sendcommandframes(reactor, instream, 1, b'command1', {})) | list(sendcommandframes(reactor, instream, 1, b'command1', {})) | ||||
outstream = reactor.makeoutputstream() | outstream = reactor.makeoutputstream() | ||||
reactor._sendersettings[b'contentencodings'], [b'a', b'b'] | reactor._sendersettings[b'contentencodings'], [b'a', b'b'] | ||||
) | ) | ||||
def testprotocolsettingsmultipleframes(self): | def testprotocolsettingsmultipleframes(self): | ||||
reactor = makereactor() | reactor = makereactor() | ||||
data = b''.join( | data = b''.join( | ||||
cborutil.streamencode( | cborutil.streamencode( | ||||
{b'contentencodings': [b'value1', b'value2'],} | { | ||||
b'contentencodings': [b'value1', b'value2'], | |||||
} | |||||
) | ) | ||||
) | ) | ||||
results = list( | results = list( | ||||
sendframes( | sendframes( | ||||
reactor, | reactor, | ||||
[ | [ | ||||
ffs( | ffs( | ||||
self.assertEqual(len(results), 1) | self.assertEqual(len(results), 1) | ||||
self.assertaction(results[0], b'runcommand') | self.assertaction(results[0], b'runcommand') | ||||
result = self._sendsingleframe( | result = self._sendsingleframe( | ||||
reactor, ffs(b'0 1 0 sender-protocol-settings eos ') | reactor, ffs(b'0 1 0 sender-protocol-settings eos ') | ||||
) | ) | ||||
self.assertaction(result, b'error') | self.assertaction(result, b'error') | ||||
self.assertEqual( | self.assertEqual( | ||||
result[1], {b'message': b'expected command request frame; got 8',} | result[1], | ||||
{ | |||||
b'message': b'expected command request frame; got 8', | |||||
}, | |||||
) | ) | ||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
import silenttestrunner | import silenttestrunner | ||||
silenttestrunner.main(__name__) | silenttestrunner.main(__name__) |
self.assertEqual(r.dispatchparts, []) | self.assertEqual(r.dispatchparts, []) | ||||
self.assertIsNone(r.dispatchpath) | self.assertIsNone(r.dispatchpath) | ||||
self.assertIsNone(r.reponame) | self.assertIsNone(r.reponame) | ||||
self.assertEqual(r.querystring, b'') | self.assertEqual(r.querystring, b'') | ||||
self.assertEqual(len(r.qsparams), 0) | self.assertEqual(len(r.qsparams), 0) | ||||
self.assertEqual(len(r.headers), 0) | self.assertEqual(len(r.headers), 0) | ||||
def testcustomport(self): | def testcustomport(self): | ||||
r = parse(DEFAULT_ENV, extra={'SERVER_PORT': '8000',}) | r = parse( | ||||
DEFAULT_ENV, | |||||
extra={ | |||||
'SERVER_PORT': '8000', | |||||
}, | |||||
) | |||||
self.assertEqual(r.url, b'http://testserver:8000') | self.assertEqual(r.url, b'http://testserver:8000') | ||||
self.assertEqual(r.baseurl, b'http://testserver:8000') | self.assertEqual(r.baseurl, b'http://testserver:8000') | ||||
self.assertEqual(r.advertisedurl, b'http://testserver:8000') | self.assertEqual(r.advertisedurl, b'http://testserver:8000') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver:8000') | self.assertEqual(r.advertisedbaseurl, b'http://testserver:8000') | ||||
r = parse( | r = parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
extra={'SERVER_PORT': '4000', 'wsgi.url_scheme': 'https',}, | extra={ | ||||
'SERVER_PORT': '4000', | |||||
'wsgi.url_scheme': 'https', | |||||
}, | |||||
) | ) | ||||
self.assertEqual(r.url, b'https://testserver:4000') | self.assertEqual(r.url, b'https://testserver:4000') | ||||
self.assertEqual(r.baseurl, b'https://testserver:4000') | self.assertEqual(r.baseurl, b'https://testserver:4000') | ||||
self.assertEqual(r.advertisedurl, b'https://testserver:4000') | self.assertEqual(r.advertisedurl, b'https://testserver:4000') | ||||
self.assertEqual(r.advertisedbaseurl, b'https://testserver:4000') | self.assertEqual(r.advertisedbaseurl, b'https://testserver:4000') | ||||
def testhttphost(self): | def testhttphost(self): | ||||
r = parse(DEFAULT_ENV, extra={'HTTP_HOST': 'altserver',}) | r = parse( | ||||
DEFAULT_ENV, | |||||
extra={ | |||||
'HTTP_HOST': 'altserver', | |||||
}, | |||||
) | |||||
self.assertEqual(r.url, b'http://altserver') | self.assertEqual(r.url, b'http://altserver') | ||||
self.assertEqual(r.baseurl, b'http://altserver') | self.assertEqual(r.baseurl, b'http://altserver') | ||||
self.assertEqual(r.advertisedurl, b'http://testserver') | self.assertEqual(r.advertisedurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver') | self.assertEqual(r.advertisedbaseurl, b'http://testserver') | ||||
def testscriptname(self): | def testscriptname(self): | ||||
r = parse(DEFAULT_ENV, extra={'SCRIPT_NAME': '',}) | r = parse( | ||||
DEFAULT_ENV, | |||||
extra={ | |||||
'SCRIPT_NAME': '', | |||||
}, | |||||
) | |||||
self.assertEqual(r.url, b'http://testserver') | self.assertEqual(r.url, b'http://testserver') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedurl, b'http://testserver') | self.assertEqual(r.advertisedurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver') | self.assertEqual(r.advertisedbaseurl, b'http://testserver') | ||||
self.assertEqual(r.apppath, b'') | self.assertEqual(r.apppath, b'') | ||||
self.assertEqual(r.dispatchparts, []) | self.assertEqual(r.dispatchparts, []) | ||||
self.assertIsNone(r.dispatchpath) | self.assertIsNone(r.dispatchpath) | ||||
r = parse(DEFAULT_ENV, extra={'SCRIPT_NAME': '/script',}) | r = parse( | ||||
DEFAULT_ENV, | |||||
extra={ | |||||
'SCRIPT_NAME': '/script', | |||||
}, | |||||
) | |||||
self.assertEqual(r.url, b'http://testserver/script') | self.assertEqual(r.url, b'http://testserver/script') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedurl, b'http://testserver/script') | self.assertEqual(r.advertisedurl, b'http://testserver/script') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver') | self.assertEqual(r.advertisedbaseurl, b'http://testserver') | ||||
self.assertEqual(r.apppath, b'/script') | self.assertEqual(r.apppath, b'/script') | ||||
self.assertEqual(r.dispatchparts, []) | self.assertEqual(r.dispatchparts, []) | ||||
self.assertIsNone(r.dispatchpath) | self.assertIsNone(r.dispatchpath) | ||||
r = parse(DEFAULT_ENV, extra={'SCRIPT_NAME': '/multiple words',}) | r = parse( | ||||
DEFAULT_ENV, | |||||
extra={ | |||||
'SCRIPT_NAME': '/multiple words', | |||||
}, | |||||
) | |||||
self.assertEqual(r.url, b'http://testserver/multiple%20words') | self.assertEqual(r.url, b'http://testserver/multiple%20words') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedurl, b'http://testserver/multiple%20words') | self.assertEqual(r.advertisedurl, b'http://testserver/multiple%20words') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver') | self.assertEqual(r.advertisedbaseurl, b'http://testserver') | ||||
self.assertEqual(r.apppath, b'/multiple words') | self.assertEqual(r.apppath, b'/multiple words') | ||||
self.assertEqual(r.dispatchparts, []) | self.assertEqual(r.dispatchparts, []) | ||||
self.assertIsNone(r.dispatchpath) | self.assertIsNone(r.dispatchpath) | ||||
def testpathinfo(self): | def testpathinfo(self): | ||||
r = parse(DEFAULT_ENV, extra={'PATH_INFO': '',}) | r = parse( | ||||
DEFAULT_ENV, | |||||
extra={ | |||||
'PATH_INFO': '', | |||||
}, | |||||
) | |||||
self.assertEqual(r.url, b'http://testserver') | self.assertEqual(r.url, b'http://testserver') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedurl, b'http://testserver') | self.assertEqual(r.advertisedurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver') | self.assertEqual(r.advertisedbaseurl, b'http://testserver') | ||||
self.assertEqual(r.apppath, b'') | self.assertEqual(r.apppath, b'') | ||||
self.assertEqual(r.dispatchparts, []) | self.assertEqual(r.dispatchparts, []) | ||||
self.assertEqual(r.dispatchpath, b'') | self.assertEqual(r.dispatchpath, b'') | ||||
r = parse(DEFAULT_ENV, extra={'PATH_INFO': '/pathinfo',}) | r = parse( | ||||
DEFAULT_ENV, | |||||
extra={ | |||||
'PATH_INFO': '/pathinfo', | |||||
}, | |||||
) | |||||
self.assertEqual(r.url, b'http://testserver/pathinfo') | self.assertEqual(r.url, b'http://testserver/pathinfo') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedurl, b'http://testserver/pathinfo') | self.assertEqual(r.advertisedurl, b'http://testserver/pathinfo') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver') | self.assertEqual(r.advertisedbaseurl, b'http://testserver') | ||||
self.assertEqual(r.apppath, b'') | self.assertEqual(r.apppath, b'') | ||||
self.assertEqual(r.dispatchparts, [b'pathinfo']) | self.assertEqual(r.dispatchparts, [b'pathinfo']) | ||||
self.assertEqual(r.dispatchpath, b'pathinfo') | self.assertEqual(r.dispatchpath, b'pathinfo') | ||||
r = parse(DEFAULT_ENV, extra={'PATH_INFO': '/one/two/',}) | r = parse( | ||||
DEFAULT_ENV, | |||||
extra={ | |||||
'PATH_INFO': '/one/two/', | |||||
}, | |||||
) | |||||
self.assertEqual(r.url, b'http://testserver/one/two/') | self.assertEqual(r.url, b'http://testserver/one/two/') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedurl, b'http://testserver/one/two/') | self.assertEqual(r.advertisedurl, b'http://testserver/one/two/') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver') | self.assertEqual(r.advertisedbaseurl, b'http://testserver') | ||||
self.assertEqual(r.apppath, b'') | self.assertEqual(r.apppath, b'') | ||||
self.assertEqual(r.dispatchparts, [b'one', b'two']) | self.assertEqual(r.dispatchparts, [b'one', b'two']) | ||||
self.assertEqual(r.dispatchpath, b'one/two') | self.assertEqual(r.dispatchpath, b'one/two') | ||||
def testscriptandpathinfo(self): | def testscriptandpathinfo(self): | ||||
r = parse( | r = parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
extra={'SCRIPT_NAME': '/script', 'PATH_INFO': '/pathinfo',}, | extra={ | ||||
'SCRIPT_NAME': '/script', | |||||
'PATH_INFO': '/pathinfo', | |||||
}, | |||||
) | ) | ||||
self.assertEqual(r.url, b'http://testserver/script/pathinfo') | self.assertEqual(r.url, b'http://testserver/script/pathinfo') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedurl, b'http://testserver/script/pathinfo') | self.assertEqual(r.advertisedurl, b'http://testserver/script/pathinfo') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver') | self.assertEqual(r.advertisedbaseurl, b'http://testserver') | ||||
self.assertEqual(r.apppath, b'/script') | self.assertEqual(r.apppath, b'/script') | ||||
self.assertEqual(r.dispatchparts, [b'pathinfo']) | self.assertEqual(r.dispatchparts, [b'pathinfo']) | ||||
parse(DEFAULT_ENV, reponame=b'repo') | parse(DEFAULT_ENV, reponame=b'repo') | ||||
with self.assertRaisesRegex( | with self.assertRaisesRegex( | ||||
error.ProgrammingError, 'PATH_INFO does not begin with repo ' 'name' | error.ProgrammingError, 'PATH_INFO does not begin with repo ' 'name' | ||||
): | ): | ||||
parse( | parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
reponame=b'repo', | reponame=b'repo', | ||||
extra={'PATH_INFO': '/pathinfo',}, | extra={ | ||||
'PATH_INFO': '/pathinfo', | |||||
}, | |||||
) | ) | ||||
with self.assertRaisesRegex( | with self.assertRaisesRegex( | ||||
error.ProgrammingError, 'reponame prefix of PATH_INFO' | error.ProgrammingError, 'reponame prefix of PATH_INFO' | ||||
): | ): | ||||
parse( | parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
reponame=b'repo', | reponame=b'repo', | ||||
extra={'PATH_INFO': '/repoextra/path',}, | extra={ | ||||
'PATH_INFO': '/repoextra/path', | |||||
}, | |||||
) | ) | ||||
r = parse( | r = parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
reponame=b'repo', | reponame=b'repo', | ||||
extra={'PATH_INFO': '/repo/path1/path2',}, | extra={ | ||||
'PATH_INFO': '/repo/path1/path2', | |||||
}, | |||||
) | ) | ||||
self.assertEqual(r.url, b'http://testserver/repo/path1/path2') | self.assertEqual(r.url, b'http://testserver/repo/path1/path2') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedurl, b'http://testserver/repo/path1/path2') | self.assertEqual(r.advertisedurl, b'http://testserver/repo/path1/path2') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver') | self.assertEqual(r.advertisedbaseurl, b'http://testserver') | ||||
self.assertEqual(r.apppath, b'/repo') | self.assertEqual(r.apppath, b'/repo') | ||||
self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | ||||
self.assertEqual(r.dispatchpath, b'path1/path2') | self.assertEqual(r.dispatchpath, b'path1/path2') | ||||
self.assertEqual(r.reponame, b'repo') | self.assertEqual(r.reponame, b'repo') | ||||
r = parse( | r = parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
reponame=b'prefix/repo', | reponame=b'prefix/repo', | ||||
extra={'PATH_INFO': '/prefix/repo/path1/path2',}, | extra={ | ||||
'PATH_INFO': '/prefix/repo/path1/path2', | |||||
}, | |||||
) | ) | ||||
self.assertEqual(r.url, b'http://testserver/prefix/repo/path1/path2') | self.assertEqual(r.url, b'http://testserver/prefix/repo/path1/path2') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual( | self.assertEqual( | ||||
r.advertisedurl, b'http://testserver/prefix/repo/path1/path2' | r.advertisedurl, b'http://testserver/prefix/repo/path1/path2' | ||||
) | ) | ||||
self.assertEqual(r.advertisedbaseurl, b'http://testserver') | self.assertEqual(r.advertisedbaseurl, b'http://testserver') | ||||
self.assertEqual(r.dispatchparts, []) | self.assertEqual(r.dispatchparts, []) | ||||
self.assertIsNone(r.dispatchpath) | self.assertIsNone(r.dispatchpath) | ||||
self.assertIsNone(r.reponame) | self.assertIsNone(r.reponame) | ||||
# With only PATH_INFO defined. | # With only PATH_INFO defined. | ||||
r = parse( | r = parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
altbaseurl=b'http://altserver', | altbaseurl=b'http://altserver', | ||||
extra={'PATH_INFO': '/path1/path2',}, | extra={ | ||||
'PATH_INFO': '/path1/path2', | |||||
}, | |||||
) | ) | ||||
self.assertEqual(r.url, b'http://testserver/path1/path2') | self.assertEqual(r.url, b'http://testserver/path1/path2') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedurl, b'http://altserver/path1/path2') | self.assertEqual(r.advertisedurl, b'http://altserver/path1/path2') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://altserver') | self.assertEqual(r.advertisedbaseurl, b'http://altserver') | ||||
self.assertEqual(r.urlscheme, b'http') | self.assertEqual(r.urlscheme, b'http') | ||||
self.assertEqual(r.apppath, b'') | self.assertEqual(r.apppath, b'') | ||||
self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | ||||
self.assertEqual(r.dispatchparts, []) | self.assertEqual(r.dispatchparts, []) | ||||
self.assertIsNone(r.dispatchpath) | self.assertIsNone(r.dispatchpath) | ||||
self.assertIsNone(r.reponame) | self.assertIsNone(r.reponame) | ||||
# PATH_INFO + path on alt URL. | # PATH_INFO + path on alt URL. | ||||
r = parse( | r = parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
altbaseurl=b'http://altserver/altpath', | altbaseurl=b'http://altserver/altpath', | ||||
extra={'PATH_INFO': '/path1/path2',}, | extra={ | ||||
'PATH_INFO': '/path1/path2', | |||||
}, | |||||
) | ) | ||||
self.assertEqual(r.url, b'http://testserver/path1/path2') | self.assertEqual(r.url, b'http://testserver/path1/path2') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual( | self.assertEqual( | ||||
r.advertisedurl, b'http://altserver/altpath/path1/path2' | r.advertisedurl, b'http://altserver/altpath/path1/path2' | ||||
) | ) | ||||
self.assertEqual(r.advertisedbaseurl, b'http://altserver') | self.assertEqual(r.advertisedbaseurl, b'http://altserver') | ||||
self.assertEqual(r.urlscheme, b'http') | self.assertEqual(r.urlscheme, b'http') | ||||
self.assertEqual(r.apppath, b'/altpath') | self.assertEqual(r.apppath, b'/altpath') | ||||
self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | ||||
self.assertEqual(r.dispatchpath, b'path1/path2') | self.assertEqual(r.dispatchpath, b'path1/path2') | ||||
self.assertIsNone(r.reponame) | self.assertIsNone(r.reponame) | ||||
# PATH_INFO + path on alt URL with trailing slash. | # PATH_INFO + path on alt URL with trailing slash. | ||||
r = parse( | r = parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
altbaseurl=b'http://altserver/altpath/', | altbaseurl=b'http://altserver/altpath/', | ||||
extra={'PATH_INFO': '/path1/path2',}, | extra={ | ||||
'PATH_INFO': '/path1/path2', | |||||
}, | |||||
) | ) | ||||
self.assertEqual(r.url, b'http://testserver/path1/path2') | self.assertEqual(r.url, b'http://testserver/path1/path2') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual( | self.assertEqual( | ||||
r.advertisedurl, b'http://altserver/altpath//path1/path2' | r.advertisedurl, b'http://altserver/altpath//path1/path2' | ||||
) | ) | ||||
self.assertEqual(r.advertisedbaseurl, b'http://altserver') | self.assertEqual(r.advertisedbaseurl, b'http://altserver') | ||||
self.assertEqual(r.urlscheme, b'http') | self.assertEqual(r.urlscheme, b'http') | ||||
self.assertEqual(r.apppath, b'/altpath/') | self.assertEqual(r.apppath, b'/altpath/') | ||||
self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | ||||
self.assertEqual(r.dispatchpath, b'path1/path2') | self.assertEqual(r.dispatchpath, b'path1/path2') | ||||
self.assertIsNone(r.reponame) | self.assertIsNone(r.reponame) | ||||
# Local SCRIPT_NAME is ignored. | # Local SCRIPT_NAME is ignored. | ||||
r = parse( | r = parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
altbaseurl=b'http://altserver', | altbaseurl=b'http://altserver', | ||||
extra={'SCRIPT_NAME': '/script', 'PATH_INFO': '/path1/path2',}, | extra={ | ||||
'SCRIPT_NAME': '/script', | |||||
'PATH_INFO': '/path1/path2', | |||||
}, | |||||
) | ) | ||||
self.assertEqual(r.url, b'http://testserver/script/path1/path2') | self.assertEqual(r.url, b'http://testserver/script/path1/path2') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual(r.advertisedurl, b'http://altserver/path1/path2') | self.assertEqual(r.advertisedurl, b'http://altserver/path1/path2') | ||||
self.assertEqual(r.advertisedbaseurl, b'http://altserver') | self.assertEqual(r.advertisedbaseurl, b'http://altserver') | ||||
self.assertEqual(r.urlscheme, b'http') | self.assertEqual(r.urlscheme, b'http') | ||||
self.assertEqual(r.apppath, b'') | self.assertEqual(r.apppath, b'') | ||||
self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | ||||
self.assertEqual(r.dispatchpath, b'path1/path2') | self.assertEqual(r.dispatchpath, b'path1/path2') | ||||
self.assertIsNone(r.reponame) | self.assertIsNone(r.reponame) | ||||
# Use remote's path for script name, app path | # Use remote's path for script name, app path | ||||
r = parse( | r = parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
altbaseurl=b'http://altserver/altroot', | altbaseurl=b'http://altserver/altroot', | ||||
extra={'SCRIPT_NAME': '/script', 'PATH_INFO': '/path1/path2',}, | extra={ | ||||
'SCRIPT_NAME': '/script', | |||||
'PATH_INFO': '/path1/path2', | |||||
}, | |||||
) | ) | ||||
self.assertEqual(r.url, b'http://testserver/script/path1/path2') | self.assertEqual(r.url, b'http://testserver/script/path1/path2') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual( | self.assertEqual( | ||||
r.advertisedurl, b'http://altserver/altroot/path1/path2' | r.advertisedurl, b'http://altserver/altroot/path1/path2' | ||||
) | ) | ||||
self.assertEqual(r.advertisedbaseurl, b'http://altserver') | self.assertEqual(r.advertisedbaseurl, b'http://altserver') | ||||
self.assertEqual(r.urlscheme, b'http') | self.assertEqual(r.urlscheme, b'http') | ||||
self.assertEqual(r.apppath, b'/altroot') | self.assertEqual(r.apppath, b'/altroot') | ||||
self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | self.assertEqual(r.dispatchparts, [b'path1', b'path2']) | ||||
self.assertEqual(r.dispatchpath, b'path1/path2') | self.assertEqual(r.dispatchpath, b'path1/path2') | ||||
self.assertIsNone(r.reponame) | self.assertIsNone(r.reponame) | ||||
# reponame is factored in properly. | # reponame is factored in properly. | ||||
r = parse( | r = parse( | ||||
DEFAULT_ENV, | DEFAULT_ENV, | ||||
reponame=b'repo', | reponame=b'repo', | ||||
altbaseurl=b'http://altserver/altroot', | altbaseurl=b'http://altserver/altroot', | ||||
extra={'SCRIPT_NAME': '/script', 'PATH_INFO': '/repo/path1/path2',}, | extra={ | ||||
'SCRIPT_NAME': '/script', | |||||
'PATH_INFO': '/repo/path1/path2', | |||||
}, | |||||
) | ) | ||||
self.assertEqual(r.url, b'http://testserver/script/repo/path1/path2') | self.assertEqual(r.url, b'http://testserver/script/repo/path1/path2') | ||||
self.assertEqual(r.baseurl, b'http://testserver') | self.assertEqual(r.baseurl, b'http://testserver') | ||||
self.assertEqual( | self.assertEqual( | ||||
r.advertisedurl, b'http://altserver/altroot/repo/path1/path2' | r.advertisedurl, b'http://altserver/altroot/repo/path1/path2' | ||||
) | ) | ||||
self.assertEqual(r.advertisedbaseurl, b'http://altserver') | self.assertEqual(r.advertisedbaseurl, b'http://altserver') |