1#!/usr/bin/env python3 2 3import sys 4import os 5import string 6import argparse 7import subprocess 8import fnmatch 9import pygit2 10import yaml 11 12def string_to_valid_file_name(to_convert): 13 valid_chars = '-_.()' + string.ascii_letters + string.digits 14 result = '' 15 for c in to_convert: 16 if c in valid_chars: 17 result += c 18 else: 19 result += '_' 20 # strip final dot, if any 21 if result.endswith('.'): 22 return result[:-1] 23 return result 24 25class wine_sync: 26 def __init__(self, module): 27 if os.path.isfile('winesync.cfg'): 28 with open('winesync.cfg', 'r') as file_input: 29 config = yaml.safe_load(file_input) 30 self.reactos_src = config['repos']['reactos'] 31 self.wine_src = config['repos']['wine'] 32 self.wine_staging_src = config['repos']['wine-staging'] 33 else: 34 config = { } 35 self.reactos_src = input('Please enter the path to the reactos git tree: ') 36 self.wine_src = input('Please enter the path to the wine git tree: ') 37 self.wine_staging_src = input('Please enter the path to the wine-staging git tree: ') 38 config['repos'] = { 'reactos' : self.reactos_src, 39 'wine': self.wine_src, 40 'wine-staging': self.wine_staging_src } 41 with open('winesync.cfg', 'w') as file_output: 42 yaml.dump(config, file_output) 43 44 self.wine_repo = pygit2.Repository(self.wine_src) 45 self.wine_staging_repo = pygit2.Repository(self.wine_staging_src) 46 self.reactos_repo = pygit2.Repository(self.reactos_src) 47 48 # read the index from the reactos tree 49 self.reactos_index = self.reactos_repo.index 50 self.reactos_index.read() 51 52 # get the actual state for the asked module 53 self.module = module 54 with open(module + '.cfg', 'r') as file_input: 55 self.module_cfg = yaml.safe_load(file_input) 56 57 self.staged_patch_dir = os.path.join('sdk', 'tools', 'winesync', self.module + '_staging') 58 59 def create_or_checkout_wine_branch(self, wine_tag, wine_staging_tag): 60 wine_branch_name = 'winesync-' + wine_tag + '-' + wine_staging_tag 61 branch = self.wine_repo.lookup_branch(wine_branch_name) 62 if branch is None: 63 # get our target commits 64 wine_target_commit = self.wine_repo.revparse_single(wine_tag) 65 if isinstance(wine_target_commit, pygit2.Tag): 66 wine_target_commit = wine_target_commit.target 67 68 wine_staging_target_commit = self.wine_staging_repo.revparse_single(wine_staging_tag) 69 if isinstance(wine_staging_target_commit, pygit2.Tag): 70 wine_staging_target_commit = wine_staging_target_commit.target 71 72 self.wine_repo.branches.local.create(wine_branch_name, self.wine_repo.revparse_single('HEAD')) 73 self.wine_repo.checkout(self.wine_repo.lookup_branch(wine_branch_name)) 74 self.wine_repo.reset(wine_target_commit, pygit2.GIT_RESET_HARD) 75 76 # do the same for the wine-staging tree 77 self.wine_staging_repo.branches.local.create(wine_branch_name, self.wine_staging_repo.revparse_single('HEAD')) 78 self.wine_staging_repo.checkout(self.wine_staging_repo.lookup_branch(wine_branch_name)) 79 self.wine_staging_repo.reset(wine_staging_target_commit, pygit2.GIT_RESET_HARD) 80 81 # run the wine-staging script 82 subprocess.call(['bash', '-c', self.wine_staging_src + '/patches/patchinstall.sh DESTDIR=' + self.wine_src + ' --all --backend=git-am']) 83 84 # delete the branch we created 85 self.wine_staging_repo.checkout(self.wine_staging_repo.lookup_branch('master')) 86 self.wine_staging_repo.branches.delete(wine_branch_name) 87 else: 88 self.wine_repo.checkout(self.wine_repo.lookup_branch(wine_branch_name)) 89 90 return wine_branch_name 91 92 # helper function for resolving wine tree path to reactos one 93 # Note : it doesn't care about the fact that the file actually exists or not 94 def wine_to_reactos_path(self, wine_path): 95 if wine_path in self.module_cfg['files']: 96 # we have a direct mapping 97 return self.module_cfg['files'][wine_path] 98 99 if not '/' in wine_path: 100 # root files should have a direct mapping 101 return None 102 103 wine_dir, wine_file = os.path.split(wine_path) 104 if wine_dir in self.module_cfg['directories']: 105 # we have a mapping for the directory 106 return os.path.join(self.module_cfg['directories'][wine_dir], wine_file) 107 108 # no match 109 return None 110 111 def sync_wine_commit(self, wine_commit, in_staging, staging_patch_index): 112 # Get the diff object 113 diff = self.wine_repo.diff(wine_commit.parents[0], wine_commit) 114 115 modified_files = False 116 ignored_files = [] 117 warning_message = '' 118 complete_patch = '' 119 120 if in_staging: 121 # see if we already applied this patch 122 patch_file_name = f'{staging_patch_index:04}-{string_to_valid_file_name(wine_commit.message.splitlines()[0])}.diff' 123 patch_path = os.path.join(self.reactos_src, self.staged_patch_dir, patch_file_name) 124 if os.path.isfile(patch_path): 125 print(f'Skipping patch as {patch_path} already exists') 126 return True, '' 127 128 for delta in diff.deltas: 129 if delta.status == pygit2.GIT_DELTA_ADDED: 130 # check if we should care 131 new_reactos_path = self.wine_to_reactos_path(delta.new_file.path) 132 if not new_reactos_path is None: 133 warning_message += 'file ' + delta.new_file.path + ' is added to the wine tree !\n' 134 old_reactos_path = '/dev/null' 135 else: 136 old_reactos_path = None 137 elif delta.status == pygit2.GIT_DELTA_DELETED: 138 # check if we should care 139 old_reactos_path = self.wine_to_reactos_path(delta.old_file.path) 140 if not old_reactos_path is None: 141 warning_message += 'file ' + delta.old_file.path + ' is removed from the wine tree !\n' 142 new_reactos_path = '/dev/null' 143 else: 144 new_reactos_path = None 145 elif delta.new_file.path.endswith('Makefile.in'): 146 warning_message += 'file ' + delta.new_file.path + ' was modified !\n' 147 # no need to warn that those are ignored, we just did. 148 continue 149 else: 150 new_reactos_path = self.wine_to_reactos_path(delta.new_file.path) 151 old_reactos_path = self.wine_to_reactos_path(delta.old_file.path) 152 153 if (new_reactos_path is not None) or (old_reactos_path is not None): 154 # print('Must apply diff: ' + old_reactos_path + ' --> ' + new_reactos_path) 155 if delta.status == pygit2.GIT_DELTA_ADDED: 156 new_blob = self.wine_repo.get(delta.new_file.id) 157 blob_patch = pygit2.Patch.create_from( 158 old=None, 159 new=new_blob, 160 new_as_path=new_reactos_path) 161 elif delta.status == pygit2.GIT_DELTA_DELETED: 162 old_blob = self.wine_repo.get(delta.old_file.id) 163 blob_patch = pygit2.Patch.create_from( 164 old=old_blob, 165 new=None, 166 old_as_path=old_reactos_path) 167 else: 168 new_blob = self.wine_repo.get(delta.new_file.id) 169 old_blob = self.wine_repo.get(delta.old_file.id) 170 171 blob_patch = pygit2.Patch.create_from( 172 old=old_blob, 173 new=new_blob, 174 old_as_path=old_reactos_path, 175 new_as_path=new_reactos_path) 176 177 # print(str(wine_commit.id)) 178 # print(blob_patch.text) 179 180 # this doesn't work 181 # reactos_diff = pygit2.Diff.parse_diff(blob_patch.text) 182 # reactos_repo.apply(reactos_diff) 183 try: 184 subprocess.run(['git', '-C', self.reactos_src, 'apply', '--reject'], input=blob_patch.data, check=True) 185 except subprocess.CalledProcessError as err: 186 warning_message += 'Error while applying patch to ' + new_reactos_path + '\n' 187 188 if delta.status == pygit2.GIT_DELTA_DELETED: 189 try: 190 self.reactos_index.remove(old_reactos_path) 191 except IOError as err: 192 warning_message += 'Error while removing file ' + old_reactos_path + '\n' 193 # here we check if the file exists. We don't complain, because applying the patch already failed anyway 194 elif os.path.isfile(os.path.join(self.reactos_src, new_reactos_path)): 195 self.reactos_index.add(new_reactos_path) 196 197 complete_patch += blob_patch.text 198 199 modified_files = True 200 else: 201 ignored_files += [delta.old_file.path, delta.new_file.path] 202 203 if not modified_files: 204 # We applied nothing 205 return False, '' 206 207 print('Applied patches from wine commit ' + str(wine_commit.id)) 208 209 if ignored_files: 210 warning_message += 'WARNING : some files were ignored: ' + ' '.join(ignored_files) + '\n' 211 212 if not in_staging: 213 self.module_cfg['tags']['wine'] = str(wine_commit.id) 214 with open(self.module + '.cfg', 'w') as file_output: 215 yaml.dump(self.module_cfg, file_output) 216 self.reactos_index.add(f'sdk/tools/winesync/{self.module}.cfg') 217 else: 218 # Add the staging patch 219 # do not save the wine commit ID in <module>.cfg, as it's a local one for staging patches 220 if not os.path.isdir(os.path.join(self.reactos_src, self.staged_patch_dir)): 221 os.mkdir(os.path.join(self.reactos_src, self.staged_patch_dir)) 222 with open(patch_path, 'w') as file_output: 223 file_output.write(complete_patch) 224 self.reactos_index.add(os.path.join(self.staged_patch_dir, patch_file_name)) 225 226 self.reactos_index.write() 227 228 commit_msg = f'[WINESYNC] {wine_commit.message}\n' 229 if (in_staging): 230 commit_msg += f'wine-staging patch by {wine_commit.author.name} <{wine_commit.author.email}>' 231 else: 232 commit_msg += f'wine commit id {str(wine_commit.id)} by {wine_commit.author.name} <{wine_commit.author.email}>' 233 234 self.reactos_repo.create_commit('HEAD', 235 pygit2.Signature('winesync', 'ros-dev@reactos.org'), 236 self.reactos_repo.default_signature, 237 commit_msg, 238 self.reactos_index.write_tree(), 239 [self.reactos_repo.head.target]) 240 241 if (warning_message != ''): 242 warning_message += 'If needed, amend the current commit in your reactos tree and start this script again' 243 244 if not in_staging: 245 warning_message += f'\n' \ 246 f'You can see the details of the wine commit here:\n' \ 247 f' https://source.winehq.org/git/wine.git/commit/{str(wine_commit.id)}\n' 248 else: 249 warning_message += f'\n' \ 250 f'Do not forget to run\n' \ 251 f' git diff HEAD^ \':(exclude)sdk/tools/winesync/{patch_file_name}\' > sdk/tools/winesync/{patch_file_name}\n' \ 252 f'after your correction and then\n' \ 253 f' git add sdk/tools/winesync/{patch_file_name}\n' \ 254 f'before running "git commit --amend"' 255 256 return True, warning_message 257 258 def revert_staged_patchset(self): 259 # revert all of this in one commmit 260 staged_patch_dir_path = os.path.join(self.reactos_src, self.staged_patch_dir) 261 if not os.path.isdir(staged_patch_dir_path): 262 return True 263 264 has_patches = False 265 266 for patch_file_name in sorted(os.listdir(staged_patch_dir_path), reverse=True): 267 patch_path = os.path.join(staged_patch_dir_path, patch_file_name) 268 if not os.path.isfile(patch_path): 269 continue 270 271 has_patches = True 272 273 with open(patch_path, 'rb') as patch_file: 274 try: 275 subprocess.run(['git', '-C', self.reactos_src, 'apply', '-R', '--reject'], stdin=patch_file, check=True) 276 except subprocess.CalledProcessError as err: 277 print(f'Error while reverting patch {patch_file_name}') 278 print('Please check, remove the offending patch with git rm, and relaunch this script') 279 return False 280 281 self.reactos_index.remove(os.path.join(self.staged_patch_dir, patch_file_name)) 282 self.reactos_index.write() 283 os.remove(patch_path) 284 285 if not has_patches: 286 return True 287 288 self.reactos_index.add_all([f for f in self.module_cfg['files'].values()]) 289 self.reactos_index.add_all([f'{d}/*.*' for d in self.module_cfg['directories'].values()]) 290 self.reactos_index.write() 291 292 self.reactos_repo.create_commit( 293 'HEAD', 294 self.reactos_repo.default_signature, 295 self.reactos_repo.default_signature, 296 f'[WINESYNC]: revert wine-staging patchset for {self.module}', 297 self.reactos_index.write_tree(), 298 [self.reactos_repo.head.target]) 299 return True 300 301 def sync_to_wine(self, wine_tag, wine_staging_tag): 302 # Get our target commit 303 wine_target_commit = self.wine_repo.revparse_single(wine_tag) 304 if isinstance(wine_target_commit, pygit2.Tag): 305 wine_target_commit = wine_target_commit.target 306 # print(f'wine target commit is {wine_target_commit}') 307 308 # get the wine commit id where we left 309 in_staging = False 310 wine_last_sync = self.wine_repo.revparse_single(self.module_cfg['tags']['wine']) 311 if (isinstance(wine_last_sync, pygit2.Tag)): 312 if not self.revert_staged_patchset(): 313 return 314 wine_last_sync = wine_last_sync.target 315 if (isinstance(wine_last_sync, pygit2.Commit)): 316 wine_last_sync = wine_last_sync.id 317 318 # create a branch to keep things clean 319 wine_branch_name = self.create_or_checkout_wine_branch(wine_tag, wine_staging_tag) 320 321 finished_sync = True 322 staging_patch_index = 1 323 324 # walk each commit between last sync and the asked tag/revision 325 wine_commit_walker = self.wine_repo.walk(self.wine_repo.head.target, pygit2.GIT_SORT_TOPOLOGICAL | pygit2.GIT_SORT_REVERSE) 326 wine_commit_walker.hide(wine_last_sync) 327 for wine_commit in wine_commit_walker: 328 applied_patch, warning_message = self.sync_wine_commit(wine_commit, in_staging, staging_patch_index) 329 330 if str(wine_commit.id) == str(wine_target_commit): 331 print('We are now in staging territory') 332 in_staging = True 333 334 if not applied_patch: 335 continue 336 337 if in_staging: 338 staging_patch_index += 1 339 340 if warning_message != '': 341 print("THERE WERE SOME ISSUES WHEN APPLYING THE PATCH\n\n") 342 print(warning_message) 343 print("\n") 344 finished_sync = False 345 break 346 347 # we're done without error 348 if finished_sync: 349 # update wine tag and commit 350 self.module_cfg['tags']['wine'] = wine_tag 351 with open(self.module + '.cfg', 'w') as file_output: 352 yaml.dump(self.module_cfg, file_output) 353 354 self.reactos_index.add(f'sdk/tools/winesync/{self.module}.cfg') 355 self.reactos_index.write() 356 self.reactos_repo.create_commit( 357 'HEAD', 358 self.reactos_repo.default_signature, 359 self.reactos_repo.default_signature, 360 f'[WINESYNC]: {self.module} is now in sync with wine-staging {wine_tag}', 361 self.reactos_index.write_tree(), 362 [self.reactos_repo.head.target]) 363 364 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') 365 366def main(): 367 parser = argparse.ArgumentParser() 368 parser.add_argument('module', help='The module you want to sync. <module>.cfg must exist in the current directory') 369 parser.add_argument('wine_tag', help='The wine tag or commit id to sync to') 370 parser.add_argument('wine_staging_tag', help='The wine staging tag or commit id to pick wine staged patches from') 371 372 args = parser.parse_args() 373 374 syncator = wine_sync(args.module) 375 376 return syncator.sync_to_wine(args.wine_tag, args.wine_staging_tag) 377 378 379if __name__ == '__main__': 380 main() 381