diff --git a/script/apply-patches b/script/apply-patches index f36309a1..4024053c 100755 --- a/script/apply-patches +++ b/script/apply-patches @@ -20,7 +20,7 @@ def main(): args = parse_args() for folder in args.folders: - error = apply_patches_for_dir(folder) + error = apply_patches_for_dir(folder, args.commit) if error: sys.stderr.write(error + '\n') sys.stderr.flush() @@ -29,14 +29,14 @@ def main(): return 0 -def apply_patches_for_dir(directory): +def apply_patches_for_dir(directory, commit): for root, dirs, files in os.walk(directory): config = PatchesConfig.from_directory(root) patches_list = config.get_patches_list() if patches_list is None: continue - (success, failed_patches) = patches_list.apply() + (success, failed_patches) = patches_list.apply(commit=commit) if not success: patch_path = failed_patches[0].get_file_path() return '{0} failed to apply'.format(os.path.basename(patch_path)) @@ -45,6 +45,8 @@ def apply_patches_for_dir(directory): def parse_args(): parser = argparse.ArgumentParser(description='Apply all required patches.') + parser.add_argument('--commit', default=False, action='store_true', + help='Commit a patch after it has been applied') parser.add_argument('-t', '--target_arch', help='Target architecture') diff --git a/script/lib/git.py b/script/lib/git.py index 19eab02a..e3d2c59e 100644 --- a/script/lib/git.py +++ b/script/lib/git.py @@ -4,26 +4,60 @@ Everything in here should be project agnostic, shouldn't rely on project's struc and make any assumptions about the passed arguments or calls outcomes. """ +import os import subprocess from util import scoped_cwd -def apply(repo, patch_path, reverse=False): +def is_repo_root(path): + path_exists = os.path.exists(path) + if not path_exists: + return False + + git_folder_path = os.path.join(path, '.git') + git_folder_exists = os.path.exists(git_folder_path) + + return git_folder_exists + + +def get_repo_root(path): + """Finds a closest ancestor folder which is a repo root.""" + norm_path = os.path.normpath(path) + norm_path_exists = os.path.exists(norm_path) + if not norm_path_exists: + return None + + if is_repo_root(norm_path): + return norm_path + + parent_path = os.path.dirname(norm_path) + + # Check if we're in the root folder already. + if parent_path == norm_path: + return None + + return get_repo_root(parent_path) + + +def apply(repo, patch_path, directory=None, index=False, reverse=False): args = ['git', 'apply', - '--directory', repo, '--ignore-space-change', '--ignore-whitespace', '--whitespace', 'fix' ] + if directory: + args += ['--directory', directory] + if index: + args += ['--index'] if reverse: args += ['--reverse'] args += ['--', patch_path] - return_code = subprocess.call(args) - applied_successfully = (return_code == 0) - - return applied_successfully + with scoped_cwd(repo): + return_code = subprocess.call(args) + applied_successfully = (return_code == 0) + return applied_successfully def get_patch(repo, commit_hash): @@ -42,3 +76,23 @@ def get_head_commit(repo): with scoped_cwd(repo): return subprocess.check_output(args).strip() + + +def commit(repo, author, message): + """ Commit whatever in the index is now.""" + + # Let's setup committer info so git won't complain about it being missing. + # TODO: Is there a better way to set committer's name and email? + env = os.environ.copy() + env['GIT_COMMITTER_NAME'] = 'Anonymous Committer' + env['GIT_COMMITTER_EMAIL'] = 'anonymous@electronjs.org' + + args = ['git', 'commit', + '--author', author, + '--message', message + ] + + with scoped_cwd(repo): + return_code = subprocess.call(args, env=env) + committed_successfully = (return_code == 0) + return committed_successfully diff --git a/script/lib/patches.py b/script/lib/patches.py index 87a32334..ac2cd76c 100644 --- a/script/lib/patches.py +++ b/script/lib/patches.py @@ -1,22 +1,46 @@ import os import sys +import git from config import VENDOR_DIR PYYAML_LIB_DIR = os.path.join(VENDOR_DIR, 'pyyaml', 'lib') sys.path.append(PYYAML_LIB_DIR) import yaml -from git import apply as git_apply - class Patch: - def __init__(self, file_path, repo_path): + def __init__(self, file_path, repo_path, paths_prefix=None, author='Anonymous ', description=None): + self.author = author + self.description = description self.file_path = file_path + self.paths_prefix = paths_prefix self.repo_path = repo_path - def apply(self, reverse=False): - return git_apply(self.repo_path, self.file_path, reverse=reverse) + def apply(self, reverse=False, commit=False): + # Add the change to index only if we're going to commit it later. + patch_applied = git.apply(self.repo_path, self.file_path, directory=self.paths_prefix, index=commit, reverse=reverse) + + if not patch_applied: + return False + + if commit: + message = self.__get_commit_message(reverse) + patch_committed = git.commit(self.repo_path, author=self.author, message=message) + return patch_committed + + return True + + def __get_commit_message(self, reverse): + message = self.description + + if message is None: + message = os.path.basename(self.file_path) + + if reverse: + message = 'Revert: ' + message + + return message def reverse(self): return self.apply(reverse=True) @@ -32,12 +56,12 @@ class PatchesList: def __len__(self): return len(self.patches) - def apply(self, reverse=False, stop_on_error=True): + def apply(self, reverse=False, stop_on_error=True, commit=False): all_patches_applied = True failed_patches = [] for patch in self.patches: - applied_successfully = patch.apply(reverse=reverse) + applied_successfully = patch.apply(reverse=reverse, commit=commit) if not applied_successfully: all_patches_applied = False @@ -74,25 +98,44 @@ class PatchesConfig: return contents - def __create_patch(self, raw_data, base_directory, repo_path): + def __create_patch(self, raw_data, base_directory, repo_path, paths_prefix): + author = raw_data['author'] + if author is None: # Shouldn't actually happen. + author = 'Anonymous ' + relative_file_path = raw_data['file'] absolute_file_path = os.path.join(base_directory, relative_file_path) - return Patch(absolute_file_path, repo_path) + # Use a patch file path as a commit summary + # and optional description as a commit body. + description = relative_file_path + if raw_data['description'] is not None: + description += '\n\n' + raw_data['description'] + + return Patch(absolute_file_path, repo_path, paths_prefix=paths_prefix, author=author, description=description) def get_patches_list(self): config_contents = self.__parse() if config_contents is None: return None - repo_path = config_contents['repo'] - if sys.platform == 'win32': - repo_path = repo_path.replace('/', '\\') + project_root = git.get_repo_root(self.path) + assert(project_root) + + relative_repo_path = os.path.normpath(config_contents['repo']) + absolute_repo_path = os.path.join(project_root, relative_repo_path) + + # If the 'repo' path is not really a git repository, + # then use that path as a prefix for patched files. + paths_prefix = None + if not git.is_repo_root(absolute_repo_path): + absolute_repo_path = project_root + paths_prefix = relative_repo_path patches_data = config_contents['patches'] base_directory = os.path.dirname(self.path) - patches = [self.__create_patch(data, base_directory, repo_path) for data in patches_data] + patches = [self.__create_patch(data, base_directory, absolute_repo_path, paths_prefix) for data in patches_data] patches_list = PatchesList(patches) return patches_list diff --git a/script/patch.py b/script/patch.py index 448af745..126c0596 100755 --- a/script/patch.py +++ b/script/patch.py @@ -38,7 +38,7 @@ def main(): def apply_patches(repo_path, patches_paths, force=False, reverse=False): - patches = [Patch(patch_path, repo_path) for patch_path in patches_paths] + patches = [Patch(os.path.abspath(patch_path), os.path.abspath(repo_path)) for patch_path in patches_paths] patches_list = PatchesList(patches) stop_on_error = not force return patches_list.apply(reverse=reverse, stop_on_error=stop_on_error)