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