Skip to content

Instantly share code, notes, and snippets.

@blackjack
Last active August 4, 2016 12:45
Show Gist options
  • Save blackjack/a021a6ffb1f3e76467e8ee420f809103 to your computer and use it in GitHub Desktop.
Save blackjack/a021a6ffb1f3e76467e8ee420f809103 to your computer and use it in GitHub Desktop.
Tomerge - tool that shows what should you merge
#!/bin/bash
set -e
sudo cp tomerge /usr/local/bin
cp tomerge.yaml ~/.config/tomerge.yaml
$EDITOR ~/.config/tomerge.yaml
#!/usr/bin/python
"""
A script that shows what commits should you merge
Some parameter defaults can be set in config file (see tomerge.yaml)
Examples:
%(name)s cmdaemon\t\t\tWill show you all not merged commits in cmdaemon between trunk and latest release
%(name)s -sb 7.2 -db 7.1 cmdaemon\tWill show you all not merged commits in cmdaemon between 7.2 and 7.1 branches
%(name)s cmdaemon src/openstack\tWill do the same but will only affect commits in src/openstack
%(name)s -s 1234 cluster_tools\t\tWill hide commit 1234 from the output
%(name)s -ds node-installer\t\tWill also show diffstat of not merged commits in node-installer
"""
import argparse
import os
import sys
import yaml
from pkg_resources import parse_version
from xml.etree import ElementTree
import subprocess
import textwrap
import readline
import getpass
import pwd
you = pwd.getpwuid(os.getuid()).pw_name
config_file = os.path.expanduser('~/.config/tomerge.yaml')
header_separator = '------------------------------------------------------------------------'
def getCredentials(default_username):
def rlinput(prompt, prefill=''):
readline.set_startup_hook(lambda: readline.insert_text(prefill))
try:
return raw_input(prompt)
finally:
readline.set_startup_hook()
username = rlinput('Username: ', default_username)
while True:
password = getpass.getpass('Password: ')
if password:
break
return username, password
def parseConfig(args, config_file):
if os.path.isfile(config_file):
config = yaml.load(open(config_file, 'r'))
if config:
if 'author' in config and not args.author:
args.author = config['author']
if 'skip' in config and args.project in config['skip'] and args.skip is None:
args.skip = config['skip'][args.project]
if 'projects_dir' in config:
args.projects_dir = config['projects_dir']
if 'no_credentials' in config and not args.no_credentials:
args.no_credentials = config['no_credentials']
else:
return None
# get latest released version of a project
def guessBranch(project_dir):
dirs = next(os.walk(project_dir + '/branches'))[1]
return sorted(dirs, key=parse_version)[-1]
def prepareArguments():
args = argparse.ArgumentParser(description=__doc__ % {'name': sys.argv[0]},
formatter_class=argparse.RawDescriptionHelpFormatter)
args.add_argument('project', help='Project to user')
args.add_argument('path', nargs='?', type=str, default='',
help='Path suffix to check for not merged commits e.g. src/cloudstorage (default: project root)')
args.add_argument('-sb', '--source-branch', type=str, default='trunk',
help='Source branch (default: trunk)')
args.add_argument('-db', '--dest-branch', type=str, default='',
help='Destination branch (default: latest released version)')
args.add_argument('-a', '--author', type=str,
help='Username used to filter commits (default: %s)' % you)
args.add_argument('-s', '--skip', type=str,
help='List of comma-separated commit to hide from output')
args.add_argument('-nc', '--no-credentials',
action='store_true', help='Do not ask for credentials')
args.add_argument('--projects-dir', type=str, default=os.getcwd(),
help='Path to a directory with your projects (default: current directory)')
group = args.add_mutually_exclusive_group()
group.add_argument('-d', '--diff', action='store_true',
help='Show diff along with the log entries')
group.add_argument('-ds', '--diffstat', action='store_true',
help='Show diffstat along with the log entries')
args = args.parse_args()
# split skip-list
if args.skip is not None:
args.skip = args.skip.split(',')
parseConfig(args, config_file)
if args.skip is None:
args.skip = list()
else:
args.skip = [s if not s.startswith('r') else s[1:]
for s in map(str, args.skip)]
args.projects_dir = os.path.expanduser(args.projects_dir)
project_dir = args.projects_dir + '/' + args.project
if not os.path.isdir(project_dir):
print "No project %s in directory %s" % (args.project, project_dir)
sys.exit(1)
if not args.dest_branch:
args.dest_branch = guessBranch(project_dir)
if args.source_branch != 'trunk':
args.source_branch = 'branches/' + args.source_branch
if args.dest_branch != 'trunk':
args.dest_branch = 'branches/' + args.dest_branch
return args
def svn():
if args.no_credentials:
return ['svn']
else:
return ['svn', '--username', args.username, '--password', args.password]
def getNotMergedCommits(src_repo, dest_repo):
p = subprocess.Popen(svn() + ['mergeinfo', '--show-revs', 'eligible',
'--non-interactive', src_repo, dest_repo], stdout=subprocess.PIPE)
out, _ = p.communicate()
if p.wait() != 0:
sys.exit(1)
return ','.join(out.splitlines())
def getLogMessages(revision_list, src_repo):
p = subprocess.Popen(svn() + ['log', '--xml', '-c',
revision_list, src_repo], stdout=subprocess.PIPE)
out, _ = p.communicate()
if p.wait() != 0:
sys.exit(1)
return out
def getDiff(revision_list, src_repo):
# somewhat ugly 'svn log --diff' parser
p = subprocess.Popen(svn() + ['log', '--diff', '-c',
','.join(revision_list), src_repo], stdout=subprocess.PIPE)
out, _ = p.communicate()
if p.wait() != 0:
sys.exit(1)
out = out.splitlines()
iterator = out.__iter__()
current_revision = None
diff = dict()
try:
for line in iterator:
if line == header_separator:
n = iterator.next()
current_revision = n.split(' | ')[0][1:]
while True:
n = iterator.next()
if n.startswith('Index:'):
diff[current_revision] = line + '\n'
break
else:
diff[current_revision] += line + '\n'
except StopIteration:
pass
return diff
def getDiffstat(diff_string):
p = subprocess.Popen(
['diffstat', '-C'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
out, _ = p.communicate(diff_string)
p.wait()
return out
args = prepareArguments()
if not args.no_credentials:
args.username, args.password = getCredentials(
you if not args.author else args.author)
src_repo = 'svn://dev/%s/%s' % (args.project, args.source_branch)
dest_repo = 'svn://dev/%s/%s' % (args.project, args.dest_branch)
if args.path:
src_repo = src_repo + '/' + args.path
dest_repo = dest_repo + '/' + args.path
not_merged = getNotMergedCommits(src_repo, dest_repo)
if not not_merged:
print 'Nothing to merge'
sys.exit(0)
log = ElementTree.fromstring(getLogMessages(not_merged, src_repo))
yours = dict()
for logentry in log.findall('logentry'):
author = logentry.find('author').text
revision = logentry.attrib['revision']
if author != args.author or revision in args.skip:
continue
yours[revision] = {
'date': logentry.find('date').text,
'msg': logentry.find('msg').text
}
diff = dict()
if args.diff or args.diffstat:
diff = getDiff(yours.keys(), src_repo)
for rev, vals in sorted(yours.items()):
print header_separator
print '\033[92mr%s\033[0m | \033[94m%s\033[0m' % (rev, vals['date'])
print
print textwrap.fill(vals['msg'], len(header_separator))
print
if args.diff:
print diff[rev]
elif args.diffstat:
print getDiffstat(diff[rev])
if not yours:
print "Nothing to merge"
print "Skipped revisions:", len(args.skip)
sys.exit(0)
print header_separator
print
print 'To merge:'
print 'cd %s/%s/%s' % (args.projects_dir, args.project, args.dest_branch)
print 'svn up'
merge = 'svn merge -c %s %s .' % (','.join(sorted(yours.keys())), src_repo)
print merge
print "svn commit -m '// %s'" % merge
# An example config file for 'tomerge' tool
# This file should be placed to ~/.config/tomerge.yaml
#
author: oleksandr # Commit author to be used as a filter
projects_dir: ~/projects # Path to all of your projects
no_credentials: False # Set this to true if you have password-less svn setup
skip:
cmdaemon:
# List of revisions to be hidden from the output
- 9000
- r100500 # Can be prefixed with 'r' as well
cluster-tools:
- 12345
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment