diff --git a/src/README b/src/README index a2fbe81..f066561 100644 --- a/src/README +++ b/src/README @@ -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 diff --git a/src/e b/src/e deleted file mode 100755 index 4d32bf1..0000000 --- a/src/e +++ /dev/null @@ -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:]) diff --git a/src/g b/src/g deleted file mode 100755 index bb9b693..0000000 --- a/src/g +++ /dev/null @@ -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() diff --git a/src/git_branch_summary.py b/src/git_branch_summary.py deleted file mode 100755 index 9d81f8b..0000000 --- a/src/git_branch_summary.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python - -import sys, subprocess, re - -if len(sys.argv) < 2: - print("Usage: %s [...]\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 diff --git a/src/gits b/src/gits new file mode 100755 index 0000000..81083f1 --- /dev/null +++ b/src/gits @@ -0,0 +1,386 @@ +#!/usr/bin/env python3 +# +# (C) 2018 by Neels Hofmeyr +# 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 . + +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', + ' commit to new branch', + ' 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?', + " 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?', + ' no', + ' 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), + ' 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), + ' 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 diff --git a/src/s b/src/s deleted file mode 100755 index f897bc1..0000000 --- a/src/s +++ /dev/null @@ -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 diff --git a/src/st b/src/st deleted file mode 100755 index a47de6b..0000000 --- a/src/st +++ /dev/null @@ -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