Last active
September 30, 2024 11:36
-
-
Save robUx4/487fa3eb18fbabd4adfabafff44a1939 to your computer and use it in GitHub Desktop.
Script to delete merged branches in git
This file contains 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 | |
# SPDX-License-Identifier: ISC | |
# Copyright © 2024 VideoLabs, VLC authors and VideoLAN | |
# | |
# Authors: Steve Lhomme <[email protected]> | |
import argparse | |
import subprocess | |
import sys | |
branches = [] | |
# test branch (uncomment to test a particular branch) | |
# branches = ['ndk26'] | |
def exit_err(errcode:int, err:str, line:int): | |
print(str(line) + ': ' + err) | |
exit(errcode) | |
# Argument parsing | |
parser = argparse.ArgumentParser( | |
description="Delete local branches merged upstream") | |
parser.add_argument('--dry-run', '-d', action='store_true', | |
help='do not delete branches, just shows what would be deleted') | |
parser.add_argument('--quiet', '-q', action='store_true', | |
help='only log branches that will be deleted') | |
parser.add_argument('--verbose', '-v', action='store_true', | |
help='show the commands that are run') | |
parser.add_argument('upstream', nargs='?', type=str, | |
help='upstream branch to compare to (guessed if not provided)') | |
args = parser.parse_args() | |
def CURRENT_LINE(): | |
return sys._getframe(1).f_lineno | |
def call_git(cmd): | |
if args.verbose: | |
print('git ' + ' '.join(cmd)) | |
call = subprocess.Popen(['git'] + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
stdout_bin, stderr_bin = call.communicate() | |
errcode = call.wait() | |
return stdout_bin.decode('utf-8'), stderr_bin, errcode | |
if branches: | |
print('!! testing only branch(es) {} !!'.format(' '.join(branches))) | |
else: | |
# delete branches that have been fully merged upstream | |
branch_list, err, errcode = call_git(['branch', '--list', '--sort=-committerdate', '--format="%(refname:short)"']) | |
if errcode != 0: | |
exit_err(errcode, err.decode('utf-8'), CURRENT_LINE()) | |
branches = branch_list.replace('\n',' ').replace('"','').split() | |
if args.upstream and args.upstream != '': | |
remote_split = args.upstream.split('/') | |
test_remote_bin, err, errcode = call_git(['remote', 'show', '-n', remote_split[0]]) | |
if errcode != 0: | |
exit_err(errcode, err.decode('utf-8'), CURRENT_LINE()) | |
upstream_default_branch = args.upstream | |
default_branch = upstream_default_branch[len(remote_split[0]) + 1:] | |
else: | |
# get the first remote branch | |
all_remote_names_bin, err, errcode = call_git(['config', '--get-regexp', 'remote.*.fetch']) | |
if errcode != 0: | |
exit_err(errcode, err.decode('utf-8'), CURRENT_LINE()) | |
all_remotes_split = all_remote_names_bin.split('\n') | |
remote_0 = all_remotes_split[0][len('remote.'):] | |
origin_cut = remote_0.find('.fetch +refs') | |
remote_split = [ remote_0[:origin_cut] ] | |
all_remote_info_bin, err, errcode = call_git(['symbolic-ref', '--short', 'refs/remotes/' + remote_split[0] + '/HEAD']) | |
if errcode != 0: | |
exit_err(errcode, err.decode('utf-8'), CURRENT_LINE()) | |
all_remote_info = all_remote_info_bin.split('\n') | |
upstream_default_branch = all_remote_info[0] | |
if upstream_default_branch.startswith(remote_split[0] + '/'): | |
default_branch = upstream_default_branch[len(remote_split[0]) + 1:] | |
print('Using default upstream {}/{}'.format(remote_split[0], default_branch)) | |
else: | |
exit_err(1, 'Could not guess the default branch') | |
for branch in branches: | |
if branch == default_branch: | |
continue | |
if not args.quiet: | |
print('* {}'.format(branch)) | |
# empty if no upstream | |
# upstream_branch_bin, err, errcode = call_git(['branch', '--list', '--format="%(upstream)"', branch]) | |
upstream_branch_bin, err, errcode = call_git(['branch', '--list', '--format="%(upstream:short)"', branch]) | |
upstream_branch = upstream_branch_bin.replace('\n','').replace('"','') | |
matches_remote = '' | |
if upstream_branch: | |
upstream_exists_bin, err, errcode = call_git(['rev-parse', '--quiet', '--verify', upstream_branch]) | |
upstream_exists = upstream_exists_bin.replace('\n','').replace('"','') | |
if upstream_exists: | |
matches_remote_bin, err, errcode = call_git(['merge-base', '--is-ancestor', upstream_branch, branch]) | |
matches_remote = matches_remote_bin | |
upstream_branch_full_bin, err, errcode = call_git(['branch', '--list', '--format="%(upstream)"', branch]) | |
upstream_branch_full = upstream_branch_full_bin.replace('\n','').replace('"','') | |
merged_upstream_bin, err, errcode = call_git(['branch', '--merged', upstream_branch, '--remotes']) | |
# merged_upstream_bin, err, errcode = call_git(['branch', '--no-merged', upstream_branch]) | |
merged_upstream = merged_upstream_bin | |
merged_remote = merged_upstream.find(upstream_branch_full) == -1 | |
if merged_remote: | |
print('matches remote {}'.format(upstream_branch_full)) | |
else: | |
print('upstream {} was deleted, unset-upstream'.format(upstream_branch)) | |
if not args.dry_run: | |
merged_remote_bin, err, errcode = call_git(['branch', '--unset-upstream', branch]) | |
if errcode != 0: | |
exit_err(errcode, err.decode('utf-8'), CURRENT_LINE()) | |
# remove remote first (verify it merges cleanly first) | |
all_merged_bin, err, errcode = call_git(['cherry', upstream_default_branch, branch]) | |
all_merged_list = all_merged_bin.split('\n') | |
merged_added = 0 | |
merged_deleted = 0 | |
for merged in all_merged_list: | |
if merged == '': | |
continue | |
if merged.startswith('+ '): | |
merged_added = merged_added +1 | |
elif merged.startswith('- '): | |
merged_deleted = merged_deleted +1 | |
# if merged_added == 0 and merged_deleted == 0: | |
if merged_added == 0: | |
print('Branch {} can be removed'.format(branch)) | |
if upstream_branch and not args.quiet: | |
if merged_remote: | |
print('Remote {} can be removed (merged)"'.format(upstream_branch)) | |
if matches_remote != '': | |
print('Remote {} can be removed (local merged)"'.format(upstream_branch)) | |
if not args.dry_run: | |
delete_branch_bin, err, errcode = call_git(['branch', '-D', branch]) | |
if errcode != 0: | |
exit_err(errcode, err.decode('utf-8'), CURRENT_LINE()) | |
elif not args.quiet: | |
print('Branch {} has not been merged (+{}-{} commits differ)'.format(branch, merged_added, merged_deleted)) | |
if not args.quiet: | |
print('') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment