reactos/sdk/tools/winesync/winesync.py
Hermès Bélusca-Maïto 53cc92613f
[WINESYNC] Support improvements for staging patches (#5898)
- Simplify patch directory usage;
- Fix the path shown in the warning message.

The staging patch path in the warning message didn't show the correct
sub-directory where the patch resides.
2023-12-18 22:21:41 +01:00

408 lines
18 KiB
Python

#!/usr/bin/env python3
import sys
import os
import posixpath
import string
import argparse
import subprocess
import fnmatch
import pygit2
import yaml
def string_to_valid_file_name(to_convert):
valid_chars = '-_.()' + string.ascii_letters + string.digits
result = ''
for c in to_convert:
if c in valid_chars:
result += c
else:
result += '_'
# strip final dot, if any
if result.endswith('.'):
return result[:-1]
return result
class wine_sync:
def __init__(self, module):
if os.path.isfile('winesync.cfg'):
with open('winesync.cfg', 'r') as file_input:
config = yaml.safe_load(file_input)
self.reactos_src = config['repos']['reactos']
self.wine_src = config['repos']['wine']
self.wine_staging_src = config['repos']['wine-staging']
else:
config = { }
self.reactos_src = input('Please enter the path to the reactos git tree: ')
self.wine_src = input('Please enter the path to the wine git tree: ')
self.wine_staging_src = input('Please enter the path to the wine-staging git tree: ')
config['repos'] = { 'reactos': self.reactos_src,
'wine': self.wine_src,
'wine-staging': self.wine_staging_src }
with open('winesync.cfg', 'w') as file_output:
yaml.dump(config, file_output)
self.wine_repo = pygit2.Repository(self.wine_src)
self.wine_staging_repo = pygit2.Repository(self.wine_staging_src)
self.reactos_repo = pygit2.Repository(self.reactos_src)
# the standard author signature we will use
self.winesync_author_signature = pygit2.Signature('winesync', 'ros-dev@reactos.org')
# read the index from the reactos tree
self.reactos_index = self.reactos_repo.index
self.reactos_index.read()
# get the actual state for the asked module
self.module = module
with open(module + '.cfg', 'r') as file_input:
self.module_cfg = yaml.safe_load(file_input)
self.staged_patch_dir = posixpath.join('sdk', 'tools', 'winesync', self.module + '_staging')
def create_or_checkout_wine_branch(self, wine_tag, wine_staging_tag):
# build the wine branch name
wine_branch_name = 'winesync-' + wine_tag
if wine_staging_tag:
wine_branch_name += '-' + wine_staging_tag
branch = self.wine_repo.lookup_branch(wine_branch_name)
if branch is None:
# get our target commits
wine_target_commit = self.wine_repo.revparse_single(wine_tag)
if isinstance(wine_target_commit, pygit2.Tag):
wine_target_commit = wine_target_commit.target
if isinstance(wine_target_commit, pygit2.Commit):
wine_target_commit = wine_target_commit.id
# do the same for the wine-staging tree
if wine_staging_tag:
wine_staging_target_commit = self.wine_staging_repo.revparse_single(wine_staging_tag)
if isinstance(wine_staging_target_commit, pygit2.Tag):
wine_staging_target_commit = wine_staging_target_commit.target
if isinstance(wine_staging_target_commit, pygit2.Commit):
wine_staging_target_commit = wine_staging_target_commit.id
self.wine_repo.branches.local.create(wine_branch_name, self.wine_repo.revparse_single('HEAD'))
self.wine_repo.checkout(self.wine_repo.lookup_branch(wine_branch_name))
self.wine_repo.reset(wine_target_commit, pygit2.GIT_RESET_HARD)
# do the same for the wine-staging tree
if wine_staging_tag:
self.wine_staging_repo.branches.local.create(wine_branch_name, self.wine_staging_repo.revparse_single('HEAD'))
self.wine_staging_repo.checkout(self.wine_staging_repo.lookup_branch(wine_branch_name))
self.wine_staging_repo.reset(wine_staging_target_commit, pygit2.GIT_RESET_HARD)
# run the wine-staging script
if subprocess.call(['python', self.wine_staging_src + '/staging/patchinstall.py', 'DESTDIR=' + self.wine_src, '--all', '--backend=git-am']):
# the new script failed (it doesn't exist?), try the old one
subprocess.call(['bash', '-c', self.wine_staging_src + '/patches/patchinstall.sh DESTDIR=' + self.wine_src + ' --all --backend=git-am'])
# delete the branch we created
self.wine_staging_repo.checkout(self.wine_staging_repo.lookup_branch('master'))
self.wine_staging_repo.branches.delete(wine_branch_name)
else:
self.wine_repo.checkout(self.wine_repo.lookup_branch(wine_branch_name))
return wine_branch_name
# Helper function for resolving wine tree path to reactos one
# Note: it doesn't care about the fact that the file actually exists or not
def wine_to_reactos_path(self, wine_path):
if self.module_cfg['files'] and (wine_path in self.module_cfg['files']):
# we have a direct mapping
return self.module_cfg['files'][wine_path]
if not '/' in wine_path:
# root files should have a direct mapping
return None
wine_dir, wine_file = os.path.split(wine_path)
if self.module_cfg['directories'] and (wine_dir in self.module_cfg['directories']):
# we have a mapping for the directory
return posixpath.join(self.module_cfg['directories'][wine_dir], wine_file)
# no match
return None
def sync_wine_commit(self, wine_commit, in_staging, staging_patch_index):
# Get the diff object
diff = self.wine_repo.diff(wine_commit.parents[0], wine_commit)
modified_files = False
ignored_files = []
warning_message = ''
complete_patch = ''
if in_staging:
# see if we already applied this patch
patch_file_name = f'{staging_patch_index:04}-{string_to_valid_file_name(wine_commit.message.splitlines()[0])}.diff'
patch_dir = os.path.join(self.reactos_src, self.staged_patch_dir)
patch_path = os.path.join(patch_dir, patch_file_name)
if os.path.isfile(patch_path):
print(f'Skipping patch as {patch_path} already exists')
return True, ''
for delta in diff.deltas:
if delta.status == pygit2.GIT_DELTA_ADDED:
# check if we should care
new_reactos_path = self.wine_to_reactos_path(delta.new_file.path)
if not new_reactos_path is None:
warning_message += 'file ' + delta.new_file.path + ' is added to the wine tree!\n'
old_reactos_path = '/dev/null'
else:
old_reactos_path = None
elif delta.status == pygit2.GIT_DELTA_DELETED:
# check if we should care
old_reactos_path = self.wine_to_reactos_path(delta.old_file.path)
if not old_reactos_path is None:
warning_message += 'file ' + delta.old_file.path + ' is removed from the wine tree!\n'
new_reactos_path = '/dev/null'
else:
new_reactos_path = None
elif delta.new_file.path.endswith('Makefile.in'):
warning_message += 'file ' + delta.new_file.path + ' was modified!\n'
# no need to warn that those are ignored, we just did.
continue
else:
new_reactos_path = self.wine_to_reactos_path(delta.new_file.path)
old_reactos_path = self.wine_to_reactos_path(delta.old_file.path)
if (new_reactos_path is not None) or (old_reactos_path is not None):
# print('Must apply diff: ' + old_reactos_path + ' --> ' + new_reactos_path)
if delta.status == pygit2.GIT_DELTA_ADDED:
new_blob = self.wine_repo.get(delta.new_file.id)
blob_patch = pygit2.Patch.create_from(
old=None,
new=new_blob,
new_as_path=new_reactos_path)
elif delta.status == pygit2.GIT_DELTA_DELETED:
old_blob = self.wine_repo.get(delta.old_file.id)
blob_patch = pygit2.Patch.create_from(
old=old_blob,
new=None,
old_as_path=old_reactos_path)
else:
new_blob = self.wine_repo.get(delta.new_file.id)
old_blob = self.wine_repo.get(delta.old_file.id)
blob_patch = pygit2.Patch.create_from(
old=old_blob,
new=new_blob,
old_as_path=old_reactos_path,
new_as_path=new_reactos_path)
# print(str(wine_commit.id))
# print(blob_patch.text)
# this doesn't work
# reactos_diff = pygit2.Diff.parse_diff(blob_patch.text)
# reactos_repo.apply(reactos_diff)
try:
subprocess.run(['git', '-C', self.reactos_src, 'apply', '--reject'], input=blob_patch.data, check=True)
except subprocess.CalledProcessError as err:
warning_message += 'Error while applying patch to ' + new_reactos_path + '\n'
if delta.status == pygit2.GIT_DELTA_DELETED:
try:
self.reactos_index.remove(old_reactos_path)
except IOError as err:
warning_message += 'Error while removing file ' + old_reactos_path + '\n'
# here we check if the file exists. We don't complain, because applying the patch already failed anyway
elif os.path.isfile(os.path.join(self.reactos_src, new_reactos_path)):
self.reactos_index.add(new_reactos_path)
complete_patch += blob_patch.text
modified_files = True
else:
ignored_files += [delta.old_file.path, delta.new_file.path]
if not modified_files:
# We applied nothing
return False, ''
print('Applied patches from wine commit ' + str(wine_commit.id))
if ignored_files:
warning_message += 'WARNING: some files were ignored: ' + ' '.join(ignored_files) + '\n'
if not in_staging:
self.module_cfg['tags']['wine'] = str(wine_commit.id)
with open(self.module + '.cfg', 'w') as file_output:
yaml.dump(self.module_cfg, file_output)
self.reactos_index.add(f'sdk/tools/winesync/{self.module}.cfg')
else:
# Add the staging patch
# do not save the wine commit ID in <module>.cfg, as it's a local one for staging patches
if not os.path.isdir(patch_dir):
os.mkdir(patch_dir)
with open(patch_path, 'w') as file_output:
file_output.write(complete_patch)
self.reactos_index.add(posixpath.join(self.staged_patch_dir, patch_file_name))
self.reactos_index.write()
commit_msg = f'[WINESYNC] {wine_commit.message}\n'
if (in_staging):
commit_msg += f'wine-staging patch by {wine_commit.author.name} <{wine_commit.author.email}>'
else:
commit_msg += f'wine commit id {str(wine_commit.id)} by {wine_commit.author.name} <{wine_commit.author.email}>'
self.reactos_repo.create_commit(
'HEAD',
self.winesync_author_signature,
self.reactos_repo.default_signature,
commit_msg,
self.reactos_index.write_tree(),
[self.reactos_repo.head.target])
if (warning_message != ''):
warning_message += 'If needed, amend the current commit in your reactos tree and start this script again'
if not in_staging:
warning_message += f'\n' \
f'You can see the details of the wine commit here:\n' \
f' https://source.winehq.org/git/wine.git/commit/{str(wine_commit.id)}\n'
else:
patch_file_path = posixpath.join(self.staged_patch_dir, patch_file_name)
warning_message += f'\n' \
f'Do not forget to run\n' \
f' git diff HEAD^ \':(exclude){patch_file_path}\' > {patch_file_path}\n' \
f'after your correction and then\n' \
f' git add {patch_file_path}\n' \
f'before running "git commit --amend"'
return True, warning_message
def revert_staged_patchset(self):
# revert all of this in one commit
staged_patch_dir_path = posixpath.join(self.reactos_src, self.staged_patch_dir)
if not os.path.isdir(staged_patch_dir_path):
return True
has_patches = False
for patch_file_name in sorted(os.listdir(staged_patch_dir_path), reverse=True):
patch_path = os.path.join(staged_patch_dir_path, patch_file_name)
if not os.path.isfile(patch_path):
continue
has_patches = True
with open(patch_path, 'rb') as patch_file:
try:
subprocess.run(['git', '-C', self.reactos_src, 'apply', '-R', '--ignore-whitespace', '--reject'], stdin=patch_file, check=True)
except subprocess.CalledProcessError as err:
print(f'Error while reverting patch {patch_file_name}')
print('Please check, remove the offending patch with git rm, and relaunch this script')
return False
self.reactos_index.remove(posixpath.join(self.staged_patch_dir, patch_file_name))
self.reactos_index.write()
os.remove(patch_path)
if not has_patches:
return True
# Note: these path lists may be empty or None, in which case
# we should not call index.add_all(), otherwise we would add
# any untracked file present in the repository.
if self.module_cfg['files']:
self.reactos_index.add_all([f for f in self.module_cfg['files'].values()])
if self.module_cfg['directories']:
self.reactos_index.add_all([f'{d}/*.*' for d in self.module_cfg['directories'].values()])
self.reactos_index.write()
self.reactos_repo.create_commit(
'HEAD',
self.winesync_author_signature,
self.reactos_repo.default_signature,
f'[WINESYNC]: revert wine-staging patchset for {self.module}',
self.reactos_index.write_tree(),
[self.reactos_repo.head.target])
return True
def sync_to_wine(self, wine_tag, wine_staging_tag):
# Get our target commit
wine_target_commit = self.wine_repo.revparse_single(wine_tag)
if isinstance(wine_target_commit, pygit2.Tag):
wine_target_commit = wine_target_commit.target
if isinstance(wine_target_commit, pygit2.Commit):
wine_target_commit = wine_target_commit.id
# print(f'wine target commit is {wine_target_commit}')
# get the wine commit id where we left
in_staging = False
wine_last_sync = self.wine_repo.revparse_single(self.module_cfg['tags']['wine'])
if isinstance(wine_last_sync, pygit2.Tag):
if not self.revert_staged_patchset():
return
wine_last_sync = wine_last_sync.target
if isinstance(wine_last_sync, pygit2.Commit):
wine_last_sync = wine_last_sync.id
# create a branch to keep things clean
wine_branch_name = self.create_or_checkout_wine_branch(wine_tag, wine_staging_tag)
finished_sync = True
staging_patch_index = 1
# walk each commit between last sync and the asked tag/revision
wine_commit_walker = self.wine_repo.walk(self.wine_repo.head.target, pygit2.GIT_SORT_TOPOLOGICAL | pygit2.GIT_SORT_REVERSE)
wine_commit_walker.hide(wine_last_sync)
for wine_commit in wine_commit_walker:
applied_patch, warning_message = self.sync_wine_commit(wine_commit, in_staging, staging_patch_index)
if str(wine_commit.id) == str(wine_target_commit):
print('We are now in staging territory')
in_staging = True
if not applied_patch:
continue
if in_staging:
staging_patch_index += 1
if warning_message != '':
print("THERE WERE SOME ISSUES WHEN APPLYING THE PATCH\n\n")
print(warning_message)
print("\n")
finished_sync = False
break
# we're done without error
if finished_sync:
# update wine tag and commit
self.module_cfg['tags']['wine'] = wine_tag
with open(self.module + '.cfg', 'w') as file_output:
yaml.dump(self.module_cfg, file_output)
self.reactos_index.add(f'sdk/tools/winesync/{self.module}.cfg')
self.reactos_index.write()
self.reactos_repo.create_commit(
'HEAD',
self.winesync_author_signature,
self.reactos_repo.default_signature,
f'[WINESYNC]: {self.module} is now in sync with wine-staging {wine_tag}',
self.reactos_index.write_tree(),
[self.reactos_repo.head.target])
print('The branch ' + wine_branch_name + ' was created in your wine repository. You might want to delete it, but you should keep it in case you want to sync more module up to this wine version')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('module', help='The module you want to sync. <module>.cfg must exist in the current directory.')
parser.add_argument('wine_tag', help='The wine tag or commit id to sync to.')
parser.add_argument('wine_staging_tag', nargs='?', default=None, help='The optional wine staging tag or commit id to pick wine staged patches from.')
args = parser.parse_args()
syncator = wine_sync(args.module)
return syncator.sync_to_wine(args.wine_tag, args.wine_staging_tag)
if __name__ == '__main__':
main()