| #!/usr/bin/env python3 | |
| """ | |
| Semi-automated python script to maintain WIP master branches forked | |
| from third-party tools. | |
| The user needs to provide a location of the git project that needs to be | |
| updated. In case the directory does not exist, the user needs to provide | |
| the URL of the git repository. | |
| This script takes into account all the branches marked under the `wip/` | |
| namespace, rebases them on top of the master branch, and performs an | |
| Octopus Merge on the master+wip-next branch. | |
| In case of conflicts between different branches, the user is given access | |
| to the shell, from which he can solve all the issues. | |
| Once all the conflicting files are fixed, the script automatically performs | |
| the last steps of the conflict solving. | |
| The user can also choose to let the script to push force on the master+wip-next | |
| branch. | |
| """ | |
| import os | |
| import subprocess | |
| import argparse | |
| import git | |
| def yes_or_no_input(): | |
| # raw_input returns the empty string for "enter" | |
| yes = {'yes', 'y', 'ye', ''} | |
| no = {'no', 'n'} | |
| choice = input().lower() | |
| if choice in yes: | |
| return True | |
| elif choice in no: | |
| return False | |
| def solve_conflicts(g, branch=""): | |
| need_fix = set(g.diff("--name-only").split("\n")) | |
| help_msg = """ | |
| CONFLICT {} | |
| Entered in conflict-fixing mode, a shell will be spawned. | |
| Solve conflicts in the following files: | |
| """.format(branch) | |
| for f in need_fix: | |
| help_msg += "{}/{}\n".format(g.working_dir, f) | |
| help_msg += """ | |
| After having fixed all the conflicts exit the spawned shell the following command | |
| $ exit | |
| """ | |
| print(help_msg) | |
| os.system('/bin/bash') | |
| g.add(".") | |
| def merge_branches(location, branches): | |
| """ | |
| Merge one or more branches into the current branch. | |
| The branches have to be in string format | |
| Returns: | |
| - True: if merge was successful | |
| - False: if merge was unsuccessful | |
| """ | |
| try: | |
| subprocess.check_call( | |
| "cd {} && git merge {} && cd -".format(location, branches), | |
| shell=True | |
| ) | |
| except subprocess.CalledProcessError: | |
| print("Something went wrong during the merge!") | |
| return False | |
| pass | |
| return True | |
| def rebase_continue_rec(g): | |
| try: | |
| g.rebase("--continue") | |
| except Exception: | |
| solve_conflicts(g) | |
| rebase_continue_rec(g) | |
| def rebase_branch(g, branch): | |
| g.checkout(branch) | |
| try: | |
| g.rebase('master') | |
| except Exception: | |
| solve_conflicts(g, branch) | |
| rebase_continue_rec(g) | |
| def revert_to_master(g, repo, remote): | |
| """ Preserving history, revert {remote}/master+wip to {remote}/master | |
| """ | |
| g.reset(['--hard', '{}/master'.format(remote)]) | |
| g.reset(['{}/master+wip'.format(remote)]) | |
| g.add(['.']) | |
| g.commit( | |
| [ | |
| '-sm', 'Revert master+wip to master ({})'.format( | |
| repo.commit('{}/master'.format(remote)).hexsha | |
| ) | |
| ] | |
| ) | |
| # This removes files that got left behind during the reset's above. | |
| # Without this, some stray files may cause git to error when attempting | |
| # the merge. | |
| g.clean(['-fx']) | |
| g.merge(['master']) | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument( | |
| '--location', | |
| required=True, | |
| help="Absolute path to the git repository." | |
| ) | |
| parser.add_argument('--url', help="Optional url of the repository.") | |
| parser.add_argument( | |
| '--remote', help="Optional remote repository to use instead of origin" | |
| ) | |
| parser.add_argument( | |
| '--branch_name', | |
| required=True, | |
| help="Name of branch to push to github with new master+wip" | |
| ) | |
| args = parser.parse_args() | |
| location = args.location | |
| url = args.url | |
| remote = args.remote | |
| assert os.path.exists( | |
| location | |
| ) or url, "The git repository does not exists, and no URL has been provided" | |
| if not os.path.exists(location): | |
| git.repo.base.Repo.clone_from(url, location) | |
| if not remote: | |
| remote = 'origin' | |
| repo = git.Repo("{}/.git".format(location)) | |
| g = git.cmd.Git(location) | |
| g.fetch("-p") | |
| all_branches = g.branch("-r") | |
| # Remove spaces and special characters from branches | |
| for string in [' ', '*']: | |
| all_branches = all_branches.replace(string, '') | |
| all_branches = all_branches.split('\n') | |
| # Consider only branches in `origin` | |
| origin_branches = [] | |
| for branch in all_branches: | |
| if "HEAD" not in branch and "{}/".format(remote) in branch: | |
| origin_branches.append(branch.replace('{}/'.format(remote), '')) | |
| # Create new integration point on master. | |
| g.checkout(['master']) | |
| g.commit(['--allow-empty', '-sm', 'New integration point for master+wip.']) | |
| branches = [] | |
| for branch in origin_branches: | |
| if branch.startswith("wip/"): | |
| print("Updating branch: ", branch) | |
| rebase_branch(g, branch) | |
| branches.append(branch) | |
| assert args.branch_name not in ['master+wip', 'master'], \ | |
| ('Branch name "{}" should not be "master+wip" nor "master"') | |
| try: | |
| g.checkout(['-b', args.branch_name]) | |
| except Exception: | |
| print("Branch {} already exists!".format(args.branch_name)) | |
| g.checkout(args.branch_name) | |
| revert_to_master(g, repo, remote) | |
| if branches: | |
| branches_string = ' '.join(branches) | |
| result = merge_branches(location, branches_string) | |
| else: | |
| result = True | |
| if not result: | |
| revert_to_master(g, repo, remote) | |
| for branch in branches: | |
| result = merge_branches(location, branch) | |
| if not result: | |
| solve_conflicts(g, branch) | |
| g.commit( | |
| "-sm \"Sequential merge of conflicting branch {}\"". | |
| format(branch) | |
| ) | |
| else: | |
| repo.index.commit( | |
| """Octopus merge | |
| This is an Octopus Merge commit of the following branches: | |
| {branches} | |
| Signed-off-by: {user} <{email}> | |
| """.format( | |
| branches='\n'.join(branches), | |
| user=repo.config_reader().get_value('user', 'name'), | |
| email=repo.config_reader().get_value('user', 'email') | |
| ), | |
| ) | |
| # Pushing to remote | |
| print( | |
| "Push on remote {} {} branch? [Y/n]".format(remote, args.branch_name) | |
| ) | |
| if yes_or_no_input(): | |
| g.push(['{}'.format(remote), args.branch_name]) | |
| else: | |
| print("Warning: did not push to {}".format(remote)) | |
| print("Octopus merge ready to be tested!") | |
| if __name__ == "__main__": | |
| main() |