Last active
August 29, 2015 14:12
-
-
Save xZise/975251c90e531347fee7 to your computer and use it in GitHub Desktop.
Remove branches which have been merged
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 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)) |
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
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.