replace src/* git scripts with a single src/gits

I keep re-using this functionality in completely unrelated realms, and decided
to unify the oddly named scripts in a single 'gits' meta-repos tool, so I can
just symlink this script into my ~/bin and use it everywhere.

Change-Id: I579e7af26d76d5c5d83b2349695456bc7b54f5a2
This commit is contained in:
Neels Hofmeyr 2018-10-31 21:35:36 +01:00
parent 5c882f66e9
commit 7d46f2ea30
7 changed files with 373 additions and 298 deletions

View File

@ -11,12 +11,11 @@ There are some handy scripts I use for my daily Osmocom development:
Pass a patch number seen on gerrit to fetch the latest patch set into
your git clone. See top comment in the script.
./g run a git command in each source tree
./e run an arbitrary shell command in each source tree
./st show a brief branch and local mods status for each source tree
./s walk through each source tree and use gitk as well as user interaction
to quickly fast-forward / reset changes coming in from upstream. (This
is potentially dangerous, but safe if you only hit enter every time.)
gits Conveniently manage several git clones:
- run a git or shell command in each source tree
- show a brief branch and local mods status for each source tree
- merge / rebase / fast-forward each source tree interactively
See ./gits help
Examples:
@ -54,7 +53,7 @@ Switched to a new branch '3787_1'
-----------------------------------------------------------------------------
./g fetch # run 'git fetch' in each clone = fetch all from upstream
./gits fetch # run 'git fetch' in each clone = fetch all from upstream
===== libasn1c =====
remote: Counting objects: 29, done
@ -90,7 +89,7 @@ From ssh://go/libosmocore
-----------------------------------------------------------------------------
./st # any modifications / updates? (e.g. useful after './g fetch')
./gits st # any modifications / updates? (e.g. useful after './g fetch')
# (checks only 'master' and the current checked-out branch)
libasn1c master
@ -116,13 +115,13 @@ libosmo-netif master
-----------------------------------------------------------------------------
./e rm .version # in each source tree, remove the local .version file
./gits sh rm .version # in each source tree, remove the local .version file
-----------------------------------------------------------------------------
./s # interactively try to fast-forward to upstream and/or save
# local modifications.
# If you just hit Enter all the time, nothing will be changed.
./gits rebase # interactively try to fast-forward to upstream and/or save
# local modifications.
# If you just hit Enter all the time, nothing dangerous will happen.
libosmocore

15
src/e
View File

@ -1,15 +0,0 @@
#!/usr/bin/env python3
import os
import os.path
import sys
import subprocess
base_dir = os.getcwd()
for p in list(os.listdir('.')):
subdir = os.path.join(base_dir, p)
if not os.path.isdir(os.path.join(subdir, '.git')):
continue
print("\n===== %s =====" % p)
os.chdir(subdir)
subprocess.call(sys.argv[1:])

17
src/g
View File

@ -1,17 +0,0 @@
#!/usr/bin/env python3
import sys
import os
import subprocess
git_subdirs = []
for subdir in os.listdir():
if not os.path.isdir(os.path.join(subdir, '.git')):
continue
print('\n===== %s =====' % subdir)
sys.stdout.flush()
subprocess.call(['git', '-C', subdir] + sys.argv[1:])
sys.stdout.flush()
sys.stderr.flush()

View File

@ -1,68 +0,0 @@
#!/usr/bin/env python
import sys, subprocess, re
if len(sys.argv) < 2:
print("Usage: %s <git_dir> [...]\nThis is mostly here for helping the 'st' script." % sys.argv[0])
exit(1)
interesting_branch_names = [ 'master', 'sysmocom/iu', 'sysmocom/sccp', 'aper-prefix-onto-upstream' ]
re_branch_name = re.compile('^..([^ ]+) .*')
re_ahead = re.compile('ahead [0-9]+|behind [0-9]+')
def branch_name(line):
m = re_branch_name.match(line)
return m.group(1)
interesting = []
def do_one_git(git_dir):
global interesting
branch_strs = subprocess.check_output(('git', '-C', git_dir, 'branch', '-vv')).decode('utf-8').splitlines()
interesting_branches = []
for line in branch_strs:
name = branch_name(line)
current_branch = False
if line.startswith('*'):
current_branch = True
elif name not in interesting_branch_names:
continue
ahead = re_ahead.findall(line)
if not ahead and not current_branch:
continue
ahead = [x.replace('ahead ', '+').replace('behind ', '-') for x in ahead]
br = (current_branch, name, ahead)
if current_branch:
interesting_branches.insert(0, br)
else:
interesting_branches.append(br)
status = subprocess.check_output(('git', '-C', git_dir, 'status')).decode()
has_mods = 'modified:' in status
interesting.append((git_dir, has_mods, interesting_branches))
for git_dir in sys.argv[1:]:
do_one_git(git_dir)
first_col = max([len(git_dir) for git_dir, _, _ in interesting])
first_col_fmt = '%' + str(first_col) + 's'
for git_dir, has_mods, interesting_branches in interesting:
strs = [first_col_fmt % git_dir,]
if has_mods:
strs.append('MODS')
for current_branch, name, ahead in interesting_branches:
br = []
br.append(name)
if ahead:
br.append('[%s]' % '|'.join(ahead))
strs.append(''.join(br))
print(' '.join(strs))
# vim: shiftwidth=2 expandtab tabstop=2

362
src/gits Executable file
View File

@ -0,0 +1,362 @@
#!/usr/bin/env python3
import sys, subprocess, re, argparse, os
doc = '''gits: conveniently manage several git subdirectories.
Instead of doing the 'cd foo; git status; cd ../bar; git status' dance, this
helps to save your time with: status, fetch, rebase, ...
See 'gits help'
'''
re_status_mods = re.compile('^\t(modified|deleted):.*')
def error(*msgs):
sys.stderr.write(''.join(msgs))
sys.stderr.write('\n')
exit(1)
def usage(*msgs):
global doc
error(doc, '\n\n', *msgs)
def git(git_dir, *args, may_fail=False, section_marker=False, show_cmd=True):
if section_marker:
print('\n===== %s =====' % git_dir)
sys.stdout.flush()
cmd = ['git', '-C', git_dir] + list(args)
if show_cmd:
print(' '.join(cmd))
rc = subprocess.call(cmd)
if rc and not may_fail:
error('git returned error! command: git -C %r %s' % (git_dir, ' '.join(repr(arg) for arg in args)))
if section_marker:
sys.stdout.flush()
sys.stderr.flush()
def git_output(git_dir, *args):
return subprocess.check_output(['git', '-C', git_dir,] + list(args)).decode('utf-8')
def git_branch(git_dir):
re_branch_name = re.compile('On branch ([^ ]*)')
status = git_output(git_dir, 'status')
m = re_branch_name.find(status)
if not m:
error('No current branch in %r' % git_dir)
return m.group(1)
def git_status(git_dir, verbose=False):
status_lines = git_output(git_dir, 'status').splitlines()
if verbose and len(status_lines):
print(status_lines[0])
on_branch = None
branch_status_str = None
local_mods = False
ON_BRANCH = 'On branch '
STATUS = 'Your branch'
for l in status_lines:
if l.startswith(ON_BRANCH):
if on_branch:
error('cannot parse status, more than one branch?')
on_branch = l[len(ON_BRANCH):]
elif l.startswith(STATUS):
if 'Your branch is up to date' in l:
branch_status_str = l
elif 'Your branch is ahead' in l:
branch_status_str = 'ahead: ' + l
elif 'Your branch is behind' in l:
branch_status_str = 'behind: ' + l
elif 'have diverged' in l:
branch_status_str = 'diverged: ' + l
else:
error('unknown status str: %r' % l)
else:
m = re_status_mods.match(l)
if m:
local_mods = True
if verbose:
print('%s%s' % (branch_status_str, '\nLOCAL MODS' if local_mods else ''))
return (on_branch, branch_status_str, local_mods)
def git_branch_summary(git_dir):
'''return a list of strings: [branchname, info0, info1,...]'''
interesting_branch_names = [ 'master' ]
strs = [git_dir,]
on_branch, branch_status_str, has_mods = git_status(git_dir)
if has_mods:
strs.append('MODS')
branch_strs = git_output(git_dir, 'branch', '-vv').splitlines()
re_branch_name = re.compile('^..([^ ]+) .*')
re_ahead = re.compile('ahead [0-9]+|behind [0-9]+')
for line in branch_strs:
m = re_branch_name.match(line)
name = m.group(1)
current_branch = False
if line.startswith('*'):
current_branch = True
elif name not in interesting_branch_names:
continue
ahead = re_ahead.findall(line)
if not ahead and not current_branch:
continue
ahead = [x.replace('ahead ', '+').replace('behind ', '-') for x in ahead]
branch_info = [name]
if ahead:
branch_info.append('[%s]' % '|'.join(ahead))
strs.append(''.join(branch_info))
return strs
def format_summaries(summaries, sep0=' ', sep1=' '):
first_col = max([len(row[0]) for row in summaries])
first_col_fmt = '%' + str(first_col) + 's'
lines = []
for row in summaries:
lines.append('%s%s%s' % (first_col_fmt % row[0], sep0, sep1.join(row[1:])))
return '\n'.join(lines)
def git_dirs():
dirs = []
for sub in os.listdir():
git_path = os.path.join(sub, '.git')
if not os.path.isdir(git_path):
continue
dirs.append(sub)
if not dirs:
error('No subdirectories found that are git clones')
return list(sorted(dirs))
def cmd_help():
global commands
global aliases
if len(sys.argv) > 2:
error('no arguments allowed')
lines = []
for name, cmd in commands.items():
names = [name,]
for alias_name, alias_for in aliases.items():
if alias_for == name:
names.append(alias_name)
line = ['|'.join(names), ]
func, doc = cmd
line.append(doc)
lines.append(line)
lines.append(('<git-command>', "Run arbitrary git command in each clone, shortcut for 'do'"))
print(format_summaries(lines, ': '))
def print_status():
infos = [git_branch_summary(git_dir) for git_dir in git_dirs()]
print(format_summaries(infos))
def cmd_status():
if len(sys.argv) > 2:
error('no arguments allowed')
print_status()
def cmd_do(argv=None):
if argv is None:
argv = sys.argv[2:]
for git_dir in git_dirs():
git(git_dir, *argv, may_fail=True, section_marker=True)
def cmd_sh():
cmd = sys.argv[2:]
for git_dir in git_dirs():
print('\n===== %s =====' % git_dir)
sys.stdout.flush()
subprocess.call(cmd, cwd=git_dir)
sys.stdout.flush()
sys.stderr.flush()
class SkipThisRepos(Exception):
pass
def ask(git_dir, *question, valid_answers=('*',)):
while True:
print('\n' + '\n '.join(question))
print(' ' + '\n '.join((
's skip this repos',
't show in tig',
'g show in gitk',
)))
answer = sys.stdin.readline().strip()
if answer == 's':
raise SkipThisRepos()
if answer == 't':
subprocess.call(('tig', '--all'), cwd=git_dir)
continue
if answer == 'g':
subprocess.call(('gitk', '--all'), cwd=git_dir)
continue
for v in valid_answers:
if v == answer:
return answer
if v == '*':
return answer
if v == '+' and len(answer) > 0:
return answer
def rebase(git_dir):
orig_branch, branch_status_str, local_mods = git_status(git_dir, verbose=True)
if orig_branch is None:
print('Not on a branch: %s' % git_dir)
raise SkipThisRepos()
if local_mods:
do_commit = ask(git_dir, 'Local mods.',
'c commit to this branch',
'<name> commit to new branch',
'<empty> skip')
if not do_commit:
raise SkipThisRepos()
if do_commit == 'c':
git(git_dir, 'commit', '-am', 'wip', may_fail=True)
else:
git(git_dir, 'checkout', '-b', do_commit)
git(git_dir, 'commit', '-am', 'wip', may_fail=True)
git(git_dir, 'checkout', orig_branch)
_, _, local_mods = git_status(git_dir)
if local_mods:
print('There still are local modifications')
raise SkipThisRepos()
if branch_status_str is None:
print('there is no upstream branch for %r' % orig_branch)
elif branch_status_str.startswith('behind'):
if 'and can be fast-forwarded' in branch_status_str:
print('fast-forwarding...')
git(git_dir, 'merge')
else:
do_merge = ask(git_dir, 'Behind. git merge?',
"<empty> don't merge",
'ok git merge',
valid_answers=('', 'ok')
)
if do_merge == 'ok':
git(git_dir, 'merge')
elif branch_status_str.startswith('ahead'):
do_commit = ask(git_dir, 'Ahead. commit to new branch?',
'<empty> no',
'<name> create new branch',
)
if do_commit:
git(git_dir, 'checkout', '-b', do_commit)
git(git_dir, 'commit', '-am', 'wip', may_fail=True)
git(git_dir, 'checkout', orig_branch)
do_reset = ask(git_dir, '%s: git reset --hard origin/%s?' % (orig_branch, orig_branch),
'<empty> no',
'OK yes (write OK in caps!)',
valid_answers=('', 'OK'))
if do_reset == 'OK':
git(git_dir, 'reset', '--hard', 'origin/%s' % orig_branch)
elif branch_status_str.startswith('diverged'):
do_reset = ask(git_dir, 'Diverged.',
'%s: git reset --hard origin/%s?' % (orig_branch, orig_branch),
'<empty> no',
'OK yes (write OK in caps!)',
valid_answers=('', 'OK'))
if do_reset == 'OK':
git(git_dir, 'reset', '--hard', 'origin/%s' % orig_branch)
return orig_branch
def cmd_rebase():
skipped = []
for git_dir in git_dirs():
try:
print('\n\n===== %s =====' % git_dir)
sys.stdout.flush()
branch = rebase(git_dir)
if branch != 'master':
git(git_dir, 'checkout', 'master')
rebase(git_dir)
git(git_dir, 'checkout', branch)
except SkipThisRepos:
print('\nSkipping %r' % git_dir)
skipped.append(git_dir)
print('\n\n==========\nrebase done.\n')
print_status()
if skipped:
print('\nskipped: %s' % ' '.join(skipped))
commands = {
'help': (cmd_help, 'List commands.'),
'status': (cmd_status, 'Show a branch summary and indicate modifications.'),
'rebase': (cmd_rebase, 'Interactively merge master and rebase current branch.'),
'sh': (cmd_sh, 'Run arbitrary shell command in each clone'),
'do': (cmd_do, 'Run arbitrary git command in each clone'),
}
aliases = {
'st': 'status',
'r': 'rebase',
}
if __name__ == '__main__':
if len(sys.argv) < 2:
usage('Pass at least one argument to tell me what to do.')
command_str = sys.argv[1]
alias_for = aliases.get(command_str)
if alias_for:
command_str = alias_for
command = commands.get(command_str)
if command:
func, doc = command
func()
else:
# run arbitrary git command
cmd_do(sys.argv[1:])
# vim: shiftwidth=2 expandtab tabstop=2

176
src/s
View File

@ -1,176 +0,0 @@
#!/usr/bin/env bash
fastforwards=""
Git() {
echo "git $@"
git $@
if [ "$?" != "0" ]; then
echo "GIT RETURNED ERROR!"
exit 1
fi
}
Git_may_fail() {
git $@
}
Git_branch() {
echo "$(Git -C "$dir" status)" | grep 'On branch' | sed 's/On branch //'
}
gitk_start() {
if [ -n "$DISPLAY" ]; then
gitk --all &
gitk_started="1"
fi
}
status() {
st="$(Git status)"
mods="$(echo "$st" | grep 'modified:')"
stline="$(echo "$st" | grep '\(behind\|ahead\|up-to-date\|diverged\)')"
echo "$br"
echo "$stline"
}
dance() {
echo
echo
br="$(Git_branch)"
echo "$dir"
cd "$dir"
status
if [ -z "$mods" -a -n "$(echo "$stline" | grep up-to-date)" ]; then
return 0
fi
gitk_start
if [ -n "$mods" ]; then
echo "Local mods"
echo "$mods"
echo
echo "commit to new branch? (enter name, empty = no)"
read wipbranch
if [ -n "$wipbranch" ]; then
Git checkout -b "$wipbranch"
Git_may_fail commit -am wip
#Git push --set-upstream origin "$wipbranch"
Git checkout "$br"
else
echo "commit to this branch $br ? (empty = no, 'ok' = yes)"
read ok
if [ "x$ok" = xok ]; then
Git commit -am wip
#Git push
fi
fi
status
if [ -n "$mods" ]; then
return 0
fi
fi
if [ -n "$(echo "$stline" | grep behind)" ]; then
if [ -n "$(echo "$stline" | grep "and can be fast-forwarded")" ]; then
echo "fast forwarding..."
fastforwards="${fastforwards} $dir/$br:$(Git_may_fail rev-parse --short HEAD)"
ok="ok"
else
echo "Behind. git merge? (empty = no, 'ok' = yes)"
read ok
fi
if [ "x$ok" = xok ]; then
Git merge
fi
elif [ -n "$(echo "$stline" | grep ahead)" ]; then
echo "Ahead. commit to new branch? (enter name, empty = no)"
read wipbranch
if [ -n "$wipbranch" ]; then
Git checkout -b "$wipbranch"
Git_may_fail commit -am wip
#Git push --set-upstream origin "$wipbranch"
Git checkout "$br"
fi
echo "$br: git reset --hard origin/$br ? (empty = no, 'OK' IN CAPS = yes)"
read ok
if [ "x$ok" = xOK ]; then
Git reset --hard "origin/$br"
fi
return 0
elif [ -n "$(echo "$stline" | grep diverged)" ]; then
echo "Diverged. git reset --hard origin/$br ? (empty = no, 'OK' IN CAPS = yes)"
read ok
if [ "x$ok" = xOK ]; then
wipbranch="neels/wip_$(date +%Y%m%d_%H%M)"
Git checkout -b "$wipbranch"
Git_may_fail commit -am wip
Git checkout "$br"
Git reset --hard "origin/$br"
fi
elif [ -z "$(echo "$stline" | grep up-to-date)" ]; then
echo "Nothing to do."
echo "$st"
fi
}
kill_gitk() {
if [ "$gitk_started" = "1" ]; then
kill %1
gitk_started="0"
fi
}
basedir="$(pwd)"
gitk_started="0"
for gitdir in */.git ; do
cd "$basedir"
dir="$(dirname "$gitdir")"
orig_branch="$(Git_branch)"
kill_gitk
dance
cd "$basedir"
if [ "$orig_branch" != master ]; then
kill_gitk
git -C "$dir" checkout master || continue
dance
cd "$basedir"
pwd
git -C "$dir" checkout "$orig_branch"
fi
# if [ "$dir" = "openbsc" ]; then
# kill_gitk
# Git checkout "sysmocom/iu"
# dance
# fi
sleep .1
done
kill_gitk
echo
echo
./st
if [ -n "$fastforwards" ]; then
echo
echo "FAST-FORWARDED: $fastforwards"
fi
# vim: shiftwidth=2 expandtab

10
src/st
View File

@ -1,10 +0,0 @@
#!/bin/sh
git_dirs() {
for gitdir in */.git ; do
echo "$(dirname "$gitdir")"
done
}
./git_branch_summary.py $(git_dirs)
# vim: shiftwidth=2 expandtab