mirror of https://gerrit.osmocom.org/osmo-dev
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: I579e7af26d76d5c5d83b2349695456bc7b54f5a2changes/60/11560/11
parent
0cbe590462
commit
b459b6c2ae
@ -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:]) |
@ -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() |
@ -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 |
@ -0,0 +1,386 @@ |
||||
#!/usr/bin/env python3 |
||||
# |
||||
# (C) 2018 by Neels Hofmeyr <neels@hofmeyr.de> |
||||
# All rights reserved. |
||||
# |
||||
# This program is free software: you can redistribute it and/or modify |
||||
# it under the terms of the GNU General Public License as published by |
||||
# the Free Software Foundation, either version 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
import sys |
||||
import subprocess |
||||
import re |
||||
import argparse |
||||
import os |
||||
import shlex |
||||
|
||||
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, ... |
||||
''' |
||||
|
||||
re_status_mods = re.compile('^\t(modified|deleted):.*') |
||||
re_status_branch_name = re.compile('On branch ([^ ]*)') |
||||
re_branch_name = re.compile('^..([^ ]+) .*') |
||||
re_ahead_behind = re.compile('ahead [0-9]+|behind [0-9]+') |
||||
|
||||
|
||||
def error(*msgs): |
||||
sys.stderr.write(''.join(msgs)) |
||||
sys.stderr.write('\n') |
||||
exit(1) |
||||
|
||||
|
||||
def cmd_to_str(cmd): |
||||
return ' '.join(shlex.quote(c) for c in cmd) |
||||
|
||||
|
||||
def git(git_dir, *args, may_fail=False, section_marker=False, show_cmd=True): |
||||
sys.stdout.flush() |
||||
sys.stderr.flush() |
||||
|
||||
if section_marker: |
||||
print('\n===== %s =====' % git_dir) |
||||
sys.stdout.flush() |
||||
|
||||
cmd = ['git', '-C', git_dir] + list(args) |
||||
if show_cmd: |
||||
print('+ %s' % cmd_to_str(cmd)) |
||||
sys.stdout.flush() |
||||
|
||||
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))) |
||||
|
||||
|
||||
def git_output(git_dir, *args): |
||||
return subprocess.check_output(['git', '-C', git_dir, ] + list(args)).decode('utf-8') |
||||
|
||||
|
||||
def git_branch(git_dir): |
||||
status = git_output(git_dir, 'status', '--long') |
||||
m = re_status_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 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: [git_dir, branch-info0, branch-info1,...] |
||||
infos are are arbitrary strings like "master[-1]"''' |
||||
|
||||
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() |
||||
|
||||
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_behind = re_ahead_behind.findall(line) |
||||
if not ahead_behind and not current_branch: |
||||
# skip branches that are "not interesting" |
||||
continue |
||||
ahead_behind = [ |
||||
x.replace('ahead ', '+').replace('behind ', '-') for x in ahead_behind] |
||||
|
||||
branch_info = name |
||||
if ahead_behind: |
||||
branch_info = branch_info + ('[%s]' % '|'.join(ahead_behind)) |
||||
|
||||
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 print_status(): |
||||
infos = [git_branch_summary(git_dir) for git_dir in git_dirs()] |
||||
print(format_summaries(infos)) |
||||
|
||||
|
||||
def cmd_do(argv): |
||||
for git_dir in git_dirs(): |
||||
git(git_dir, *argv, may_fail=True, section_marker=True) |
||||
|
||||
|
||||
def cmd_sh(cmd): |
||||
if not cmd: |
||||
error('which command do you want to run?') |
||||
for git_dir in git_dirs(): |
||||
print('\n===== %s =====' % git_dir) |
||||
print('+ %s' % cmd_to_str(cmd)) |
||||
sys.stdout.flush() |
||||
subprocess.call(cmd, cwd=git_dir) |
||||
sys.stdout.flush() |
||||
sys.stderr.flush() |
||||
|
||||
|
||||
class SkipThisRepo(Exception): |
||||
pass |
||||
|
||||
|
||||
def ask(git_dir, *question, valid_answers=('*',)): |
||||
while True: |
||||
print('\n' + '\n '.join(question)) |
||||
print(' ' + '\n '.join(( |
||||
's skip this repo', |
||||
't show in tig', |
||||
'g show in gitk', |
||||
))) |
||||
|
||||
answer = sys.stdin.readline().strip() |
||||
if answer == 's': |
||||
raise SkipThisRepo() |
||||
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): |
||||
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 SkipThisRepo() |
||||
|
||||
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 SkipThisRepo() |
||||
|
||||
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 SkipThisRepo() |
||||
|
||||
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 SkipThisRepo: |
||||
print('\nSkipping %r' % git_dir) |
||||
skipped.append(git_dir) |
||||
|
||||
print('\n\n==========\nrebase done.\n') |
||||
print_status() |
||||
if skipped: |
||||
print('\nskipped: %s' % ' '.join(skipped)) |
||||
|
||||
|
||||
def parse_args(): |
||||
parser = argparse.ArgumentParser(description=doc) |
||||
sub = parser.add_subparsers(title='action', dest='action') |
||||
sub.required = True |
||||
|
||||
# status |
||||
sub.add_parser('status', aliases=['st', 's'], |
||||
help='show a branch summary and indicate modifications') |
||||
|
||||
# fetch |
||||
fetch = sub.add_parser('fetch', aliases=['f'], |
||||
help="run 'git fetch' in each clone (use before rebase)") |
||||
fetch.add_argument('remainder', nargs=argparse.REMAINDER, |
||||
help='additional arguments to be passed to git fetch') |
||||
|
||||
# rebase |
||||
sub.add_parser('rebase', aliases=['r', 're'], |
||||
help='interactively ff-merge master, rebase current branches') |
||||
|
||||
# sh |
||||
sh = sub.add_parser('sh', |
||||
help='run shell command in each clone (`gits sh echo hi`)') |
||||
sh.add_argument('remainder', nargs=argparse.REMAINDER, |
||||
help='command to run in each clone') |
||||
|
||||
# do |
||||
do = sub.add_parser('do', |
||||
help='run git command in each clone (`gits do clean -dxf`)') |
||||
do.add_argument('remainder', nargs=argparse.REMAINDER, |
||||
help='git command to run in each clone') |
||||
return parser.parse_args() |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
args = parse_args() |
||||
if args.action in ['status', 's', 'st']: |
||||
print_status() |
||||
elif args.action in ['fetch', 'f']: |
||||
cmd_do(['fetch'] + args.remainder) |
||||
elif args.action in ['rebase', 'r']: |
||||
cmd_rebase() |
||||
elif args.action == 'sh': |
||||
cmd_sh(args.remainder) |
||||
elif args.action == 'do': |
||||
cmd_do(args.remainder) |
||||
|
||||
# vim: shiftwidth=4 expandtab tabstop=4 |
@ -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 |
Loading…
Reference in new issue