Skip to content

Instantly share code, notes, and snippets.

@robUx4
Last active September 30, 2024 11:36
Show Gist options
  • Save robUx4/487fa3eb18fbabd4adfabafff44a1939 to your computer and use it in GitHub Desktop.
Save robUx4/487fa3eb18fbabd4adfabafff44a1939 to your computer and use it in GitHub Desktop.
Script to delete merged branches in git
#!/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