blob: 7f5c5a8778290d3e0db601d875900e06008f58bf [file] [log] [blame]
#!/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()