Skip to content

Instantly share code, notes, and snippets.

@nongvantinh
Last active May 26, 2025 06:41
Show Gist options
  • Save nongvantinh/c290c5d439a29211d990aadcbdfa390f to your computer and use it in GitHub Desktop.
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…
#!/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()
@nongvantinh
Copy link
Author

nongvantinh commented May 20, 2025

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

  1. Choose a Directory:

    • Use a directory already in your PATH (e.g., C:\Users\<YourUsername>\Scripts).
    • Or create a new directory, such as C:\Scripts.
    New-Item -Path "C:\Scripts" -ItemType Directory
  2. Move the Script:

    • Rename the script to a simpler name (e.g., git_merge_tool.py).
    • Move it to the chosen directory.

    Example:

    Move-Item -Path "git_merge_tool.py" -Destination "C:\Scripts\git_merge_tool.py"
  3. Add the Directory to PATH (if not already in PATH):

    • Open the Start Menu and search for "Environment Variables."
    • Click Edit the system environment variables.
    • In the System Properties window, click Environment Variables.
    • Under System variables or User variables, find the Path variable and click Edit.
    • Add the directory (e.g., C:\Scripts) to the list.

    Click OK to save and close all dialogs.

    Or just use Powershell for short:

    [System.Environment]::SetEnvironmentVariable('PATH', $env:PATH + ';C:\Scripts', [System.EnvironmentVariableTarget]::Machine)

2. Associate .py Files with Python

Ensure Python is installed and .py files are associated with the Python interpreter:

  1. Open a Command Prompt and type:

    python --version

    If Python is not recognized, install Python from python.org.

  2. During installation, ensure the Add Python to PATH option is checked.

  3. Verify .py files are associated with Python:

    • Right-click any .py file.
    • Select Open with > Choose another app.
    • Select the Python interpreter and check Always use this app to open .py files.

3. Test the Script

You can now invoke the script globally by typing its name in the Command Prompt or PowerShell:

git_merge_tool.py --development <development_branch> [--dry-run] [-f feature] [-f development]

4. Optional: Create a Batch File for Simplicity

If you want to invoke the script without typing .py, create a batch file wrapper:

  1. Create a new file named git_merge_tool.bat in the same directory as the script.

  2. Add the following content to the batch file:

    @echo off
    python C:\Scripts\git_merge_tool.py %*
  3. Save the file.

Now you can invoke the script as:

git_merge_tool --development <development-branch>

Summary for Windows

  1. Move the script to a directory in your PATH (e.g., C:\Scripts).
  2. Ensure Python is installed and .py files are associated with Python.
  3. Optionally, create a batch file for easier invocation.
  4. Test the script globally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment