Last active
May 26, 2025 06:41
-
-
Save nongvantinh/c290c5d439a29211d990aadcbdfa390f to your computer and use it in GitHub Desktop.
This script automates the process of merging a feature branch into a development branch with rebasing. It checks out the feature branch, rebases it onto the development branch, and then merges it into the development branch. It also validates that the branches exist before performing any operations. The script can be run in dry-run mode to simul…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# USAGE: | |
# This script automates the process of merging a feature branch into a development branch | |
# with rebasing. It checks out the feature branch, rebases it onto the development branch, | |
# and then merges it into the development branch. It also validates that the branches exist | |
# before performing any operations. The script can be run in dry-run mode to simulate the | |
# operations without making any changes to the repository. | |
# 1. Make the script executable: | |
# chmod +x git_merge_tool.py | |
# 2. Move the script to a directory in your PATH, e.g., ~/.local/bin: | |
# mkdir -p ~/.local/bin | |
# cp git_merge_tool.py ~/.local/bin/git_merge_tool | |
# 3. Add the directory to your PATH if it's not already: | |
# echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc | |
# source ~/.bashrc | |
# 4. Run the script: | |
# git_merge_tool.py --development <development_branch> [--dry-run] [--push-remote <upstream>] [-f feature] [-f development] | |
import argparse | |
import subprocess | |
import sys | |
import logging | |
logging.basicConfig(level=logging.INFO, format="%(message)s") | |
def run_git_command(command, description, dry_run=False): | |
"""Run a Git command and handle errors.""" | |
logging.info(description) | |
if dry_run: | |
logging.info(f"Dry-run: {' '.join(command)}") | |
return | |
try: | |
result = subprocess.run(command, capture_output=True, text=True, check=True) | |
logging.info(result.stdout.strip()) | |
except subprocess.CalledProcessError as e: | |
logging.error(f"Error during: {description}") | |
logging.error(e.stderr.strip()) | |
sys.exit(1) | |
def rebase_branch(branch_name, onto_branch, upstream_remote, origin_remote, force_push=None, dry_run=False): | |
"""Rebase a branch onto another branch.""" | |
run_git_command( | |
["git", "checkout", branch_name], | |
f"Checking out branch '{branch_name}'...", | |
dry_run | |
) | |
run_git_command( | |
["git", "pull", "--rebase", upstream_remote, onto_branch], | |
f"Rebasing branch '{branch_name}' onto '{onto_branch}' from remote '{upstream_remote}'...", | |
dry_run | |
) | |
run_git_command( | |
["git", "push", origin_remote, branch_name] + (["-f"] if force_push else []), | |
f"Pushing rebased branch '{branch_name}' to remote '{origin_remote}'...", | |
dry_run | |
) | |
def merge_feature_branch(feature_branch, development_branch, origin_remote, push_remote, dry_run=False): | |
"""Merge the feature branch into the development branch.""" | |
run_git_command( | |
["git", "checkout", development_branch], | |
f"Checking out development branch '{development_branch}'...", | |
dry_run | |
) | |
run_git_command( | |
["git", "merge", "--no-ff", feature_branch], | |
f"Merging feature branch '{feature_branch}' into '{development_branch}'...", | |
dry_run | |
) | |
run_git_command( | |
["git", "push", push_remote, development_branch], | |
f"Pushing changes to remote upstream development branch '{development_branch}' on '{push_remote}'...", | |
dry_run | |
) | |
run_git_command( | |
["git", "push", origin_remote, development_branch], | |
f"Pushing changes to remote origin development branch '{development_branch}' on '{origin_remote}'...", | |
dry_run | |
) | |
if feature_branch != development_branch: | |
run_git_command( | |
["git", "push", "-d", origin_remote, feature_branch], | |
f"Deleting merged branch '{feature_branch}' on '{origin_remote}'...", | |
dry_run | |
) | |
def validate_branch(branch_name, dry_run=False): | |
"""Validate that the branch exists in the repository.""" | |
run_git_command( | |
["git", "rev-parse", "--verify", branch_name], | |
f"Validating branch '{branch_name}' exists...", | |
dry_run | |
) | |
def get_current_branch(): | |
"""Get the name of the current Git branch.""" | |
try: | |
result = subprocess.run( | |
["git", "branch", "--show-current"], | |
capture_output=True, | |
text=True, | |
check=True | |
) | |
return result.stdout.strip() | |
except subprocess.CalledProcessError as e: | |
logging.error("Failed to get the current branch name.") | |
logging.error(e.stderr.strip()) | |
sys.exit(1) | |
def get_git_remotes(): | |
"""Get the list of available Git remotes.""" | |
try: | |
result = subprocess.run( | |
["git", "remote"], | |
capture_output=True, | |
text=True, | |
check=True | |
) | |
return result.stdout.strip().splitlines() | |
except subprocess.CalledProcessError as e: | |
logging.error("Failed to get the list of Git remotes.") | |
logging.error(e.stderr.strip()) | |
sys.exit(1) | |
def main(): | |
git_remotes = get_git_remotes() | |
if not git_remotes: | |
logging.error("No Git remotes found. Please configure at least one remote.") | |
sys.exit(1) | |
default_remote = git_remotes[0] | |
default_push_remote = git_remotes[-1] | |
parser = argparse.ArgumentParser(description="Merge feature branch to development branch with rebasing.") | |
parser.add_argument( | |
'--development', | |
type=str, | |
required=True, | |
help='Name of the development branch to merge into' | |
) | |
parser.add_argument( | |
'--dry-run', | |
action='store_true', | |
help='Perform a dry run without making any changes' | |
) | |
parser.add_argument( | |
'-f', '--force', | |
action='append', | |
choices=['feature', 'development'], | |
default=[], | |
help="Force push the specified branch ('feature' or 'development'). Can be used multiple times." | |
) | |
parser.add_argument( | |
'--upstream', | |
type=str, | |
choices=git_remotes, | |
default=default_push_remote, | |
help=f"Specify the remote repository to rebase from (default: '{default_push_remote}'). Allowed values: {', '.join(git_remotes)}" | |
) | |
parser.add_argument( | |
'--origin', | |
type=str, | |
choices=git_remotes, | |
default=default_remote, | |
help=f"Specify the remote repository to push to (default: '{default_remote}'). Allowed values: {', '.join(git_remotes)}" | |
) | |
parser.add_argument( | |
'--push-remote', | |
type=str, | |
choices=git_remotes, | |
default=default_push_remote, | |
help=f"Specify the remote repository to push the merged branch to (default: '{default_push_remote}'). Allowed values: {', '.join(git_remotes)}" | |
) | |
args = parser.parse_args() | |
development_branch = args.development | |
dry_run = args.dry_run | |
force_push = args.force | |
upstream_remote = args.upstream | |
origin_remote = args.origin | |
push_remote = args.push_remote | |
feature_branch = get_current_branch() | |
if not feature_branch: | |
logging.error("Could not determine the current branch. Ensure you are on a valid Git branch.") | |
sys.exit(1) | |
validate_branch(feature_branch, dry_run) | |
validate_branch(development_branch, dry_run) | |
force_push_feature = 'feature' in force_push | |
force_push_development = 'development' in force_push | |
rebase_branch(feature_branch, development_branch, upstream_remote, origin_remote, force_push_feature, dry_run) | |
rebase_branch(development_branch, development_branch, upstream_remote, origin_remote, force_push_development, dry_run) | |
merge_feature_branch(feature_branch, development_branch, origin_remote, push_remote, dry_run) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note: The Powershell that uses to run the commands below should be opened as Administrator.
On Windows, you can make the script globally accessible by following these steps:
1. Add the Script to a Directory in the PATH
Choose a Directory:
PATH
(e.g.,C:\Users\<YourUsername>\Scripts
).C:\Scripts
.Move the Script:
Example:
Add the Directory to PATH (if not already in PATH):
Path
variable and click Edit.C:\Scripts
) to the list.Click OK to save and close all dialogs.
Or just use Powershell for short:
2. Associate
.py
Files with PythonEnsure Python is installed and
.py
files are associated with the Python interpreter:Open a Command Prompt and type:
If Python is not recognized, install Python from python.org.
During installation, ensure the Add Python to PATH option is checked.
Verify
.py
files are associated with Python:.py
file.3. Test the Script
You can now invoke the script globally by typing its name in the Command Prompt or PowerShell:
4. Optional: Create a Batch File for Simplicity
If you want to invoke the script without typing
.py
, create a batch file wrapper:Create a new file named
git_merge_tool.bat
in the same directory as the script.Add the following content to the batch file:
Save the file.
Now you can invoke the script as:
Summary for Windows
PATH
(e.g.,C:\Scripts
)..py
files are associated with Python.