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 self.reactos_index.remove(old_reactos_path) 190 # here we check if the file exists. We don't complain, because applying the patch already failed anyway 191 elif os.path.isfile(os.path.join(self.reactos_src, new_reactos_path)): 192 self.reactos_index.add(new_reactos_path) 193 194 complete_patch += blob_patch.text 195 196 modified_files = True 197 else: 198 ignored_files += [delta.old_file.path, delta.new_file.path] 199 200 if not modified_files: 201 # We applied nothing 202 return False, '' 203 204 print('Applied patches from wine commit ' + str(wine_commit.id)) 205 206 if ignored_files: 207 warning_message += 'WARNING : some files were ignored: ' + ' '.join(ignored_files) + '\n' 208 209 if not in_staging: 210 self.module_cfg['tags']['wine'] = str(wine_commit.id) 211 with open(self.module + '.cfg', 'w') as file_output: 212 yaml.dump(self.module_cfg, file_output) 213 self.reactos_index.add(f'sdk/tools/winesync/{self.module}.cfg') 214 else: 215 # Add the staging patch 216 # do not save the wine commit ID in <module>.cfg, as it's a local one for staging patches 217 if not os.path.isdir(os.path.join(self.reactos_src, self.staged_patch_dir)): 218 os.mkdir(os.path.join(self.reactos_src, self.staged_patch_dir)) 219 with open(patch_path, 'w') as file_output: 220 file_output.write(complete_patch) 221 self.reactos_index.add(os.path.join(self.staged_patch_dir, patch_file_name)) 222 223 self.reactos_index.write() 224 225 commit_msg = f'[WINESYNC] {wine_commit.message}\n' 226 if (in_staging): 227 commit_msg += f'wine-staging patch by {wine_commit.author.name} <{wine_commit.author.email}>' 228 else: 229 commit_msg += f'wine commit id {str(wine_commit.id)} by {wine_commit.author.name} <{wine_commit.author.email}>' 230 231 self.reactos_repo.create_commit('HEAD', 232 pygit2.Signature('winesync', 'ros-dev@reactos.org'), 233 self.reactos_repo.default_signature, 234 commit_msg, 235 self.reactos_index.write_tree(), 236 [self.reactos_repo.head.target]) 237 238 if (warning_message != ''): 239 warning_message += 'If needed, amend the current commit in your reactos tree and start this script again' 240 241 if not in_staging: 242 warning_message += f'\n' \ 243 f'You can see the details of the wine commit here:\n' \ 244 f' https://source.winehq.org/git/wine.git/commit/{str(wine_commit.id)}\n' 245 else: 246 warning_message += f'\n' \ 247 f'Do not forget to run\n' \ 248 f' git diff HEAD^ \':(exclude)sdk/tools/winesync/{patch_file_name}\' > sdk/tools/winesync/{patch_file_name}\n' \ 249 f'after your correction and then\n' \ 250 f' git add sdk/tools/winesync/{patch_file_name}\n' \ 251 f'before running "git commit --amend"' 252 253 return True, warning_message 254 255 def revert_staged_patchset(self): 256 # revert all of this in one commmit 257 staged_patch_dir_path = os.path.join(self.reactos_src, self.staged_patch_dir) 258 if not os.path.isdir(staged_patch_dir_path): 259 return True 260 261 has_patches = False 262 263 for patch_file_name in sorted(os.listdir(staged_patch_dir_path), reverse=True): 264 patch_path = os.path.join(staged_patch_dir_path, patch_file_name) 265 if not os.path.isfile(patch_path): 266 continue 267 268 has_patches = True 269 270 with open(patch_path, 'rb') as patch_file: 271 try: 272 subprocess.run(['git', '-C', self.reactos_src, 'apply', '-R', '--reject'], stdin=patch_file, check=True) 273 except subprocess.CalledProcessError as err: 274 print(f'Error while reverting patch {patch_file_name}') 275 print('Please check, remove the offending patch with git rm, and relaunch this script') 276 return False 277 278 self.reactos_index.remove(os.path.join(self.staged_patch_dir, patch_file_name)) 279 self.reactos_index.write() 280 os.remove(patch_path) 281 282 if not has_patches: 283 return True 284 285 self.reactos_index.add_all([f for f in self.module_cfg['files'].values()]) 286 self.reactos_index.add_all([f'{d}/*.*' for d in self.module_cfg['directories'].values()]) 287 self.reactos_index.write() 288 289 self.reactos_repo.create_commit( 290 'HEAD', 291 self.reactos_repo.default_signature, 292 self.reactos_repo.default_signature, 293 f'[WINESYNC]: revert wine-staging patchset for {self.module}', 294 self.reactos_index.write_tree(), 295 [self.reactos_repo.head.target]) 296 return True 297 298 def sync_to_wine(self, wine_tag, wine_staging_tag): 299 # Get our target commit 300 wine_target_commit = self.wine_repo.revparse_single(wine_tag) 301 if isinstance(wine_target_commit, pygit2.Tag): 302 wine_target_commit = wine_target_commit.target 303 # print(f'wine target commit is {wine_target_commit}') 304 305 # get the wine commit id where we left 306 in_staging = False 307 wine_last_sync = self.wine_repo.revparse_single(self.module_cfg['tags']['wine']) 308 if (isinstance(wine_last_sync, pygit2.Tag)): 309 if not self.revert_staged_patchset(): 310 return 311 wine_last_sync = wine_last_sync.target 312 if (isinstance(wine_last_sync, pygit2.Commit)): 313 wine_last_sync = wine_last_sync.id 314 315 # create a branch to keep things clean 316 wine_branch_name = self.create_or_checkout_wine_branch(wine_tag, wine_staging_tag) 317 318 finished_sync = True 319 staging_patch_index = 1 320 321 # walk each commit between last sync and the asked tag/revision 322 wine_commit_walker = self.wine_repo.walk(self.wine_repo.head.target, pygit2.GIT_SORT_TOPOLOGICAL | pygit2.GIT_SORT_REVERSE) 323 wine_commit_walker.hide(wine_last_sync) 324 for wine_commit in wine_commit_walker: 325 applied_patch, warning_message = self.sync_wine_commit(wine_commit, in_staging, staging_patch_index) 326 327 if str(wine_commit.id) == str(wine_target_commit): 328 print('We are now in staging territory') 329 in_staging = True 330 331 if not applied_patch: 332 continue 333 334 if in_staging: 335 staging_patch_index += 1 336 337 if warning_message != '': 338 print("THERE WERE SOME ISSUES WHEN APPLYING THE PATCH\n\n") 339 print(warning_message) 340 print("\n") 341 finished_sync = False 342 break 343 344 # we're done without error 345 if finished_sync: 346 # update wine tag and commit 347 self.module_cfg['tags']['wine'] = wine_tag 348 with open(self.module + '.cfg', 'w') as file_output: 349 yaml.dump(self.module_cfg, file_output) 350 351 self.reactos_index.add(f'sdk/tools/winesync/{self.module}.cfg') 352 self.reactos_index.write() 353 self.reactos_repo.create_commit( 354 'HEAD', 355 self.reactos_repo.default_signature, 356 self.reactos_repo.default_signature, 357 f'[WINESYNC]: {self.module} is now in sync with wine-staging {wine_tag}', 358 self.reactos_index.write_tree(), 359 [self.reactos_repo.head.target]) 360 361 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') 362 363def main(): 364 parser = argparse.ArgumentParser() 365 parser.add_argument('module', help='The module you want to sync. <module>.cfg must exist in the current directory') 366 parser.add_argument('wine_tag', help='The wine tag or commit id to sync to') 367 parser.add_argument('wine_staging_tag', help='The wine staging tag or commit id to pick wine staged patches from') 368 369 args = parser.parse_args() 370 371 syncator = wine_sync(args.module) 372 373 return syncator.sync_to_wine(args.wine_tag, args.wine_staging_tag) 374 375 376if __name__ == '__main__': 377 main() 378