Last active
August 4, 2016 12:45
-
-
Save blackjack/a021a6ffb1f3e76467e8ee420f809103 to your computer and use it in GitHub Desktop.
Tomerge - tool that shows what should you merge
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
#!/bin/bash | |
set -e | |
sudo cp tomerge /usr/local/bin | |
cp tomerge.yaml ~/.config/tomerge.yaml | |
$EDITOR ~/.config/tomerge.yaml |
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/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 textwrap.fill(vals['msg'], len(header_separator)) | |
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 '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 |
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
# 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