#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2023 Blender Authors # # SPDX-License-Identifier: GPL-2.0-or-later import collections import os import subprocess import sys # Hashes to be ignored # # The system sometimes fails to match commits and suggests to back-port # revision which was already ported. In order to solve that we can: # # - Explicitly ignore some of the commits. # - Move the synchronization point forward. IGNORE_HASHES = { } # Start revisions from both repositories. CYCLES_START_COMMIT = b"b941eccba81bbb1309a0eb4977fc3a77796f4ada" # blender-v2.92 BLENDER_START_COMMIT = b"02948a2cab44f74ed101fc1b2ad9fe4431123e85" # v2.92 # Prefix which is common for all the subjects. GIT_SUBJECT_COMMON_PREFIX = b"Subject: [PATCH] " # Marker which indicates begin of new file in the patch set. GIT_FILE_SECTION_MARKER = b"diff --git" # Marker of the end of the patch-set. GIT_PATCHSET_END_MARKER = b"-- " # Prefix of topic to be omitted SUBJECT_SKIP_PREFIX = ( b"Cycles: ", b"cycles: ", b"Cycles Standalone: ", b"Cycles standalone: ", b"cycles standalone: ", ) def subject_strip(common_prefix, subject): for prefix in SUBJECT_SKIP_PREFIX: full_prefix = common_prefix + prefix if subject.startswith(full_prefix): subject = subject[len(full_prefix):].capitalize() subject = common_prefix + subject break return subject def replace_file_prefix(path, prefix, replace_prefix): tokens = path.split(b' ') prefix_len = len(prefix) for i, t in enumerate(tokens): for x in (b"a/", b"b/"): if t.startswith(x + prefix): tokens[i] = x + replace_prefix + t[prefix_len + 2:] return b' '.join(tokens) def cleanup_patch(patch, accept_prefix, replace_prefix): assert accept_prefix[0] != b'/' assert replace_prefix[0] != b'/' full_accept_prefix = GIT_FILE_SECTION_MARKER + b" a/" + accept_prefix with open(patch, "rb") as f: content = f.readlines() clean_content = [] do_skip = False for line in content: if line.startswith(GIT_SUBJECT_COMMON_PREFIX): # Skip possible prefix like "Cycles:", we already know change is # about Cycles since it's being committed to a Cycles repository. line = subject_strip(GIT_SUBJECT_COMMON_PREFIX, line) # Dots usually are omitted in the topic line = line.replace(b".\n", b"\n") elif line.startswith(GIT_FILE_SECTION_MARKER): if not line.startswith(full_accept_prefix): do_skip = True else: do_skip = False line = replace_file_prefix(line, accept_prefix, replace_prefix) elif line.startswith(GIT_PATCHSET_END_MARKER): do_skip = False elif line.startswith(b"---") or line.startswith(b"+++"): line = replace_file_prefix(line, accept_prefix, replace_prefix) if not do_skip: clean_content.append(line) with open(patch, "wb") as f: f.writelines(clean_content) # Get mapping from commit subject to commit hash. # # It'll actually include timestamp of the commit to the map key, so commits with # the same subject wouldn't conflict with each other. def commit_map_get(repository, path, start_commit): command = (b"git", b"--git-dir=" + os.path.join(repository, b'.git'), b"--work-tree=" + repository, b"log", b"--format=%H %at %s", b"--reverse", start_commit + b'..HEAD', b'--', os.path.join(repository, path), b':(exclude)' + os.path.join(repository, b'intern/cycles/blender')) lines = subprocess.check_output(command).split(b"\n") commit_map = collections.OrderedDict() for line in lines: if line: commit_sha, stamped_subject = line.split(b' ', 1) stamp, subject = stamped_subject.split(b' ', 1) subject = subject_strip(b"", subject).rstrip(b".") stamped_subject = stamp + b" " + subject if commit_sha in IGNORE_HASHES: continue commit_map[stamped_subject] = commit_sha return commit_map # Get difference between two lists of commits. # Returns two lists: first are the commits to be ported from Cycles to Blender, # second one are the commits to be ported from Blender to Cycles. def commits_get_difference(cycles_map, blender_map): cycles_to_blender = [] for stamped_subject, commit_hash in cycles_map.items(): if stamped_subject not in blender_map: cycles_to_blender.append(commit_hash) blender_to_cycles = [] for stamped_subject, commit_hash in blender_map.items(): if stamped_subject not in cycles_map: blender_to_cycles.append(commit_hash) return cycles_to_blender, blender_to_cycles # Transfer commits from one repository to another. # Doesn't do actual commit just for the safety. def transfer_commits(commit_hashes, from_repository, to_repository, dst_is_cycles): patch_index = 1 for commit_hash in commit_hashes: command = ( b"git", b"--git-dir=" + os.path.join(from_repository, b'.git'), b"--work-tree=" + from_repository, b"format-patch", b"-1", b"--start-number", bytes(str(patch_index), 'utf-8'), b"-o", to_repository, commit_hash, b'--', b':(exclude)' + os.path.join(from_repository, b'intern/cycles/blender'), ) patch_file = subprocess.check_output(command).rstrip(b"\n") if dst_is_cycles: cleanup_patch(patch_file, b"intern/cycles", b"src") else: cleanup_patch(patch_file, b"src", b"intern/cycles") patch_index += 1 def main(): if len(sys.argv) != 3: print("Usage: %s /path/to/cycles/ /path/to/blender/" % sys.argv[0]) return cycles_repository = sys.argv[1].encode() blender_repository = sys.argv[2].encode() cycles_map = commit_map_get(cycles_repository, b'', CYCLES_START_COMMIT) blender_map = commit_map_get(blender_repository, b"intern/cycles", BLENDER_START_COMMIT) diff = commits_get_difference(cycles_map, blender_map) transfer_commits(diff[0], cycles_repository, blender_repository, False) transfer_commits(diff[1], blender_repository, cycles_repository, True) print("Missing commits were saved to the blender and cycles repositories.") print("Check them and if they're all fine run:") print("") print(" git am *.patch") if __name__ == '__main__': main()