Skip to content

Instantly share code, notes, and snippets.

@xZise
Last active August 29, 2015 14:12
Show Gist options
  • Save xZise/975251c90e531347fee7 to your computer and use it in GitHub Desktop.
Save xZise/975251c90e531347fee7 to your computer and use it in GitHub Desktop.
Remove branches which have been merged
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Clean up the git branches by removing branches whose change-id got merged."""
from __future__ import unicode_literals
import argparse
import collections
import itertools
import json
import re
import subprocess
import sys
if sys.version_info[0] > 2:
from urllib.parse import urlparse
else:
from urlparse import urlparse
class Commit(object):
def __init__(self, commit_hash, change_id):
self.commit_hash = commit_hash
self.change_id = change_id
@classmethod
def parse_message(cls, message):
message = message.splitlines()
commit_hash = message[0][len('commit '):]
# skip header (4 lines) and reversed order
message = message[:3:-1]
# find first change-id
change_id = None
for line in message:
if not line:
break
match = re.match('^ *Change-Id: (I[0-9A-Fa-f]{40})$', line)
if match:
if change_id:
print('Found multiple Change-IDs in commit message of '
'"{0}".'.format(commit_hash))
else:
change_id = match.group(1)
if not change_id:
print('No Change-IDs found in commit message of '
'"{0}".'.format(commit_hash))
return cls(commit_hash, change_id)
def exec_proc(*args, **kwargs):
kwargs.setdefault('stdout', subprocess.PIPE)
kwargs.setdefault('stderr', subprocess.STDOUT)
proc = subprocess.Popen(args, **kwargs)
out, err = proc.communicate()
if sys.version_info[0] >= 3:
out = out.decode("utf8")
return out
def ssh_query(change_ids, additional_parameter):
params = ['OR'] * (len(change_ids) * 2 - 1)
params[0::2] = change_ids
params = ['ssh', '-p', port, host, 'gerrit', 'query',
'--format=JSON'] + list(additional_parameter) + params
data = {}
for line in exec_proc(*params).splitlines()[:-1]:
status = json.loads(line)
data[status['id']] = status
return data
NEVER_DELETE = 0
ALWAYS_ASK = 1
NOT_REVIEW_ASK = 2
ALWAYS_DELETE = 3
parser = argparse.ArgumentParser()
parser.add_argument('--master-branch', default='master')
parser.add_argument('--remote', default='gerrit')
delete_mode = parser.add_mutually_exclusive_group()
delete_mode.add_argument('--always-delete', dest='delete_mode', action='store_const',
const=ALWAYS_DELETE)
delete_mode.add_argument('--not-review-ask', dest='delete_mode', action='store_const',
const=NOT_REVIEW_ASK)
delete_mode.add_argument('--always-ask', dest='delete_mode', action='store_const',
const=ALWAYS_ASK)
online_mode = parser.add_mutually_exclusive_group()
online_mode.add_argument('--load-additional-data', '-L', dest='online', action='store_const', const=True)
online_mode.add_argument('--offline', dest='online', action='store_const', const=False)
args = parser.parse_args()
if args.delete_mode is None:
args.delete_mode = NEVER_DELETE
if args.online is not False:
url = urlparse(exec_proc(
'git', 'config', 'remote.{0}.url'.format(args.remote)).strip())
host = '{0}@{1}'.format(url.username, url.hostname)
port = str(url.port)
branches = []
git_branch_output = exec_proc('git', 'branch', '--no-color')
# remove the ' ' or '* ' in front of the list
branches = set(branch[2:] for branch in git_branch_output.splitlines())
if args.master_branch not in branches:
print('The master branch "{0}" was not found.'.format(args.master_branch))
sys.exit(1)
# Don't scan the master branch
branches.difference_update([args.master_branch])
branches = sorted(branches)
change_ids = set()
branch_ids = {}
for branch in branches:
# get newest change-id
message = exec_proc('git', 'log', '--pretty=medium', '--no-color', '-n', '1', branch)
commit = Commit.parse_message(message)
if commit.change_id:
change_ids.add(commit.change_id)
else:
print('Branch "{0}" is going to be skipped.'.format(branch))
branch_ids[branch] = commit
print('Found {0} branch(es) and {1} change ids'.format(len(branches), len(change_ids)))
if change_ids:
if args.online is not False:
print('Query server for {0} change id(s)…'.format(len(change_ids)))
change_id_data = ssh_query(change_ids, [])
open_change_ids = set(change_id for change_id, status in change_id_data.items()
if status['open'])
if args.online is True and open_change_ids:
print('Query server for additional data of {0} change '
'id(s)…'.format(len(open_change_ids)))
change_id_data.update(ssh_query(open_change_ids, ['--patch-sets']))
else:
change_id_data = {}
for change_id in change_ids:
messages = exec_proc(
'git', 'log', '--pretty=medium', '--no-color',
'--grep=Change-Id: {0}'.format(change_id), args.master_branch)
parts = re.split('commit ([0-9a-f]{40})', messages)
commits = [None] * (len(parts) // 2)
for i in range(len(commits)): # parts is always #commits*2 + 1
commits[i] = parts[i * 2 + 1] + parts[(i + 1) * 2]
assert(None not in commits)
for commit_entry in commits:
commit = Commit.parse_message(commit_entry)
if commit.change_id == change_id:
change_id_data[change_id] = {'open': False,
'status': 'MERGED'}
break
else:
change_id_data[change_id] = {'open': True}
if len(change_id_data) % 10 == 0 and len(change_id_data) < len(change_ids):
print('Process {0}th entry.'.format(len(change_id_data)))
else:
change_id_data = {}
for branch in branches:
commit = branch_ids[branch]
status = change_id_data.get(commit.change_id)
if status and not status['open']:
if args.delete_mode is NEVER_DELETE:
print('[X] Branch "{0}" got closed: {1}'.format(branch, status['status']))
else:
assert(args.delete_mode > 0)
delete = args.delete_mode is ALWAYS_DELETE or (args.delete_mode is NOT_REVIEW_ASK and branch.startswith('review/'))
if args.delete_mode is ALWAYS_ASK or not delete:
answer = None
while answer not in ['y', 'n']:
answer = input('Delete branch "{0}" [y/n]?'.format(branch)).lower()
delete = answer == 'y'
if not delete:
print('[N] Branch "{0}" got closed but not deleted: {1}'.format(branch, status['status']))
else:
print('[D] Branch "{0}" got closed and deleted: {1}'.format(branch, status['status']))
print(exec_proc('git', 'branch', '-D', branch).rstrip('\n'))
elif not status:
print('[!] Branch "{0}" was not submitted.'.format(branch))
else:
if 'patchSets' in status:
updated = None
for number, patch_set in enumerate(status['patchSets'], 1):
assert(number == int(patch_set['number']))
if updated:
updated = False
break
if patch_set['revision'] == commit.commit_hash:
updated = True
else:
updated = True
if updated:
print('[ ] Branch "{0}" did not get merged.'.format(branch))
elif updated is False:
print('[U] Branch "{0}" could be updated.'.format(branch))
else:
print('[¦] Branch "{0}" is not a patch set revision.'.format(branch))
@jayvdb
Copy link

jayvdb commented Jan 29, 2015

odd... I ran this on a very old work area, .. 17 branches listed, but it says they had 16 change ids. So I ran it with --always-delete, half expecting it to leave one branch behind, but then there were none.

Found 17 branch(es) and 16 change ids
Query server for 16 change id(s)...
[X] Branch "anywiki" got closed: MERGED
[X] Branch "api-random-fix" got closed: MERGED
[X] Branch "archivebot-break-less" got closed: MERGED
[X] Branch "config-dict-keyerror" got closed: MERGED
[X] Branch "disable-long-test-methods" got closed: ABANDONED
[X] Branch "move-itempage-save-test" got closed: MERGED
[X] Branch "mwexception" got closed: MERGED
[X] Branch "random-fix" got closed: MERGED
[X] Branch "reenable_wikibase_tests" got closed: MERGED
[X] Branch "revdel_dup_tests" got closed: MERGED
[X] Branch "review/john_vandenberg/link-tests" got closed: MERGED
[X] Branch "review/ladsgroup/161256" got closed: MERGED
[X] Branch "review/mpaa/tokenwallet" got closed: MERGED
[X] Branch "review/xzise/bug/70760" got closed: MERGED
[X] Branch "shell-preload-library" got closed: MERGED
[X] Branch "test-dry-site" got closed: MERGED
[X] Branch "test-isolate-site-object" got closed: MERGED

$ git branch-status --always-delete
Found 17 branch(es) and 16 change ids
Query server for 16 change id(s)...
[D] Branch "anywiki" got closed and deleted: MERGED
Deleted branch anywiki (was 4dc2ce9).
[D] Branch "api-random-fix" got closed and deleted: MERGED
Deleted branch api-random-fix (was 7736f7d).
[D] Branch "archivebot-break-less" got closed and deleted: MERGED
Deleted branch archivebot-break-less (was 06ebec7).
[D] Branch "config-dict-keyerror" got closed and deleted: MERGED
Deleted branch config-dict-keyerror (was 0d6a3d7).
[D] Branch "disable-long-test-methods" got closed and deleted: ABANDONED
Deleted branch disable-long-test-methods (was ac56d63).
[D] Branch "move-itempage-save-test" got closed and deleted: MERGED
Deleted branch move-itempage-save-test (was aec6beb).
[D] Branch "mwexception" got closed and deleted: MERGED
Deleted branch mwexception (was c02e247).
[D] Branch "random-fix" got closed and deleted: MERGED
Deleted branch random-fix (was 5d9249e).
[D] Branch "reenable_wikibase_tests" got closed and deleted: MERGED
Deleted branch reenable_wikibase_tests (was 072ca4b).
[D] Branch "revdel_dup_tests" got closed and deleted: MERGED
Deleted branch revdel_dup_tests (was f55e6b3).
[D] Branch "review/john_vandenberg/link-tests" got closed and deleted: MERGED
Deleted branch review/john_vandenberg/link-tests (was cf3f49f).
[D] Branch "review/ladsgroup/161256" got closed and deleted: MERGED
Deleted branch review/ladsgroup/161256 (was 8894b7f).
[D] Branch "review/mpaa/tokenwallet" got closed and deleted: MERGED
Deleted branch review/mpaa/tokenwallet (was d337098).
[D] Branch "review/xzise/bug/70760" got closed and deleted: MERGED
Deleted branch review/xzise/bug/70760 (was d65ac7b).
[D] Branch "shell-preload-library" got closed and deleted: MERGED
Deleted branch shell-preload-library (was f31510d).
[D] Branch "test-dry-site" got closed and deleted: MERGED
Deleted branch test-dry-site (was 23eda36).
[D] Branch "test-isolate-site-object" got closed and deleted: MERGED
Deleted branch test-isolate-site-object (was 08a3f30).

$ git branch-status
Found 0 branch(es) and 0 change ids


Also, I cant seriously use this yet, as I occasionally, stupidly perhaps, commit more work to a changeset which latter gets merged without my additional work. I need it to check my rev id has the same rev id as the committed changeset. I'm up for hacking at my own stupid problem, but would prefer to contribute once it is in a repo. IMO, even if we dont stick with gerrit, this will be good tool for other gerrit users, and we may as well develop it as much as possible while we're constantly using gerrit.

@jayvdb
Copy link

jayvdb commented Jan 30, 2015

A slightly different tool has just been published.
https://github.com/jdlrobson/GerritCommandLine
Worth integrating this into that tool, or not? I think not, but it would be good to get this published as the 'git gerrit' tool, for functionality that interacts with a git workarea, whereas the other tool is for functionality that looks at an entire repository

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