Skip to content

Instantly share code, notes, and snippets.

@tbnorth
Last active May 10, 2023 13:27
Show Gist options
  • Save tbnorth/936a7ac488e7cbacaf4729a23fd34419 to your computer and use it in GitHub Desktop.
Save tbnorth/936a7ac488e7cbacaf4729a23fd34419 to your computer and use it in GitHub Desktop.
Recursively show detailed status of git repos.
find . \( -name HEAD -o -name .git \) -print -prune \
| xargs -L1 dirname \
| xargs -IF sh -c ' \
echo; echo -n F ""; \
(cd F; git rev-parse --abbrev-ref HEAD); \
echo "$((cd F; git ls-files --others --exclude-standard) | wc -l) untracked"; \
(cd F; git remote -v) | sed "s/(.*)//" | sort -u; \
(cd F; git cherry -v 2>/dev/null); \
[ -d F/.git ] && (cd F; git -c core.fileMode=false status -uno -s) \
' \
| less +g -p'(M|\+)'
# find -prune as well as -print to not find .git/HEAD and .git/logs/HEAD
# -L1 in the first xargs works with older dirname commands that
# only take one arg.
# use (cd X; cmd) rather the git -C for old git versions with no -C
# sed / sort just collapses fetch / push lines from git remote -v
# less params - search (highlight) M and + (modified / unpushed), but start at top
# show local unpushed branches
git branch --format "%(refname:short) %(upstream)" | grep '\s$'
# or
git for-each-ref --format="%(upstream:remotename) %(refname:short) %(upstream:track)" refs/heads | sort
# git 2.41+
git for-each-ref --format="%(upstream:remotename) %(refname:short) %(ahead-behind:HEAD) %(upstream:track)" refs/heads | sort
# pre 2.41 https://stackoverflow.com/a/7774433/1072212
git for-each-ref --format="%(refname:short) %(upstream:short)" refs/heads | \
while read local remote
do
[ -z "$remote" ] && continue
git rev-list --left-right "${local}...${remote}" -- 2>/dev/null >/tmp/git_upstream_status_delta || continue
LEFT_AHEAD=$(grep -c '^<' /tmp/git_upstream_status_delta)
RIGHT_AHEAD=$(grep -c '^>' /tmp/git_upstream_status_delta)
echo "$local (ahead $LEFT_AHEAD) | (behind $RIGHT_AHEAD) $remote"
done
# or, more useful formatting:
echo; git for-each-ref --format="%(refname:short) %(upstream:short)" refs/heads | while read local remote; do [ -z "$remote" ] && continue; git rev-list --left-right "${local}...${remote}" -- 2>/dev/null >/tmp/git_upstream_status_delta || continue; LEFT_AHEAD=$(grep -c '^<' /tmp/git_upstream_status_delta); RIGHT_AHEAD=$(grep -c '^>' /tmp/git_upstream_status_delta); echo "+$LEFT_AHEAD-$RIGHT_AHEAD $local $remote"; done | sort -r; echo
# push all local branches to a new remote
git push newremote refs/remotes/oldremote/*:refs/heads/*
# https://www.metaltoad.com/blog/git-push-all-branches-new-remote
# to check deletions ~= insertions (i.e. reordering of functions / bibtex records etc.)
git diff | grep ^- | wc -l; git diff | grep ^+ | wc -l
# fingerprint entire repo.
git log --all --format=%H | sort | sha1sum
# to process dirs with uncommitted changes
read -p Dir: DIR; pushd $DIR; git diff; read -p Why: WHY; git commit -am"$WHY" && git push; popd
# pull in each git repo dir found
find . \( -name HEAD -o -name .git \) -print -prune \
| xargs -L1 dirname \
| xargs -IF sh -c ' \
echo; echo -n F ""; \
(cd F; git pull origin $(git rev-parse --abbrev-ref HEAD) ); \
'
# other git maintenance commands
# fix eol change
# git ls-files -m | xargs dos2unix misses some, so
git status --porcelain | sed -n '/^ M/ {s/^ M//; p}' | xargs -IF dos2unix $(git rev-parse --show-toplevel)/F
# fix mode change, *from git root*
git diff --summary | sed -r 's/mode change 100([^ ]*).* (.*)/chmod \1 \2/' | bash
# fix trailing whitespace
git ls-files -m | xargs sed -i 's/[ \t]\+$//g'
# sha1 comparison, first 7 chars
sha1sum /some/path/* /some/other/path/* | sort | sed 's/^\(.\{7\}\).\{33\}/\1/'
# find unpushed stuff, again avoid -C for older gits
find . -name .git -type d | xargs dirname | \
xargs -IF bash -c 'cd F; BRANCH=$(git rev-parse --abbrev-ref HEAD); \
echo F $BRANCH; \
git --no-pager log -1 --pretty=oneline origin/$BRANCH..$BRANCH' \
| less
# check for references to untracked files from other files
git status --porcelain | sed -n '/^?? .*[^/]$/ {s%.*/%%; p}' | xargs -IF grep -ri F .
# move untracked files to attic
mkdir -p attic
git status --porcelain | sed -n '/^?? / {s%^...%%; p}' >attic/to_add.lst
rsync --remove-source-files --verbose --recursive --backup --suffix `date '+.%Y%m%d%H%M%S'` --files-from=attic/to_add.lst --exclude attic . attic/
# list repos. without readme files
find . -name .git | sed 's/\.git$//' | xargs -IF bash -c "find F -maxdepth 1 -iname readme.\* | egrep -q . || echo F"
# change extension (not really git specific)
ls | sed 's/\(.*\)\..*/git mv \1.Rmd \1.md/' | bash
# compare pairs of files
ls *txt | sed 'x; G; s/\n/ /; /^ / d' | xargs -L1 echo diff
# copy files to numbered list
find . -type f -mtime -4 -name \*[0-9] | cat -n | sed -E 's%(\S+)\s+(\S+)%cp \2 ~/skipmail/\1%' | bash
# edit files in a commit
git diff-tree --no-commit-id --name-only -r HEAD | xargs -L1 sed -i 's/"temp_set": 0/"temp_set": 25/g'
cat > gitemailfix.sh << EOT
git filter-branch --env-filter '
WRONG_EMAIL="wrong@example.com"
NEW_NAME="New Name Value"
NEW_EMAIL="correct@example.com"
if [ "\$GIT_COMMITTER_EMAIL" = "\$WRONG_EMAIL" ]
then
export GIT_COMMITTER_NAME="\$NEW_NAME"
export GIT_COMMITTER_EMAIL="\$NEW_EMAIL"
fi
if [ "\$GIT_AUTHOR_EMAIL" = "\$WRONG_EMAIL" ]
then
export GIT_AUTHOR_NAME="\$NEW_NAME"
export GIT_AUTHOR_EMAIL="\$NEW_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags
EOT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment