Neels Hofmeyr has uploaded this change for review. ( https://gerrit.osmocom.org/11560
Change subject: replace src/* git scripts with a single src/gits ...................................................................... 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 --- M src/README D src/e D src/g D src/git_branch_summary.py A src/gits D src/s D src/st 7 files changed, 353 insertions(+), 298 deletions(-) git pull ssh://gerrit.osmocom.org:29418/osmo-dev refs/changes/60/11560/1 diff --git a/src/README b/src/README index a2fbe81..f066561 100644 --- a/src/README +++ b/src/README @@ -11,12 +11,11 @@ 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 @@ ----------------------------------------------------------------------------- -./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 @@ ----------------------------------------------------------------------------- -./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 @@ ----------------------------------------------------------------------------- -./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 <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 diff --git a/src/gits b/src/gits new file mode 100755 index 0000000..7d7f253 --- /dev/null +++ b/src/gits @@ -0,0 +1,342 @@ +#!/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): + status_lines = git_output(git_dir, 'status').splitlines() + + 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 + + print('branch: %s\n%r%s' % (on_branch, 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 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 cmd_status(): + if len(sys.argv) > 2: + error('no arguments allowed') + + infos = [git_branch_summary(git_dir) for git_dir in git_dirs()] + print(format_summaries(infos)) + +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) + + if local_mods: + do_commit = ask(git_dir, 'Local mods.', + 'c commit to this branch', + '<name> commit to new branch',) + + 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(): + 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) + +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 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 -- To view, visit https://gerrit.osmocom.org/11560 To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings Gerrit-Project: osmo-dev Gerrit-Branch: master Gerrit-MessageType: newchange Gerrit-Change-Id: I579e7af26d76d5c5d83b2349695456bc7b54f5a2 Gerrit-Change-Number: 11560 Gerrit-PatchSet: 1 Gerrit-Owner: Neels Hofmeyr <nhofm...@sysmocom.de>