2121 -e Exclude repos that are 'ok'
2222 -f Do a 'git fetch' on each repo (slow for many repos)
2323 -h, --help Show this help message
24+ -l, --list FILE Use list of repo paths from FILE instead of searching
25+ -s, --save-list FILE Search for repos and save paths to FILE, then exit
2426 --flatten Show only one status per line
2527 --no-depth Do not recurse into directories (incompatible with -d)
2628 --throttle SEC Wait SEC seconds between each 'git fetch' (-f option)
@@ -36,7 +38,9 @@ You can limit output with the following options:
3638 --no-untracked
3739 --no-stashes
3840 --no-ok (same as -e)
39-
41+
42+ The list file may contain comments (lines starting with '#') and empty lines.
43+
4044EOF
4145}
4246
@@ -56,6 +60,8 @@ NO_DEPTH=0
5660THROTTLE=0
5761DEPTH=2
5862SHOW_CUR_BRANCH=0
63+ LIST=" "
64+ SAVE_LIST=" "
5965
6066while [ -n " $1 " ]; do
6167 # Stop reading when we've run out of options.
@@ -135,6 +141,14 @@ while [ -n "$1" ]; do
135141 if [ " $1 " = " -w" ]; then
136142 WARN_NOT_REPO=1
137143 fi
144+ if [ " $1 " = " -l" ] || [ " $1 " = " --list" ]; then
145+ LIST=" $2 "
146+ shift
147+ fi
148+ if [ " $1 " = " -s" ] || [ " $1 " = " --save-list" ]; then
149+ SAVE_LIST=" $2 "
150+ shift
151+ fi
138152
139153 shift
140154done
@@ -161,6 +175,35 @@ C_UNTRACKED="$C_CYAN"
161175C_STASHES=" $C_YELLOW "
162176C_UNSAFE=" $C_PURPLE "
163177
178+ # Handle --save-list: scan for repos and save to file, then exit
179+ if [ -n " $SAVE_LIST " ]; then
180+ temp=" $( mktemp) "
181+ (
182+ # Preserve existing entries if file exists
183+ if [ -f " $SAVE_LIST " ]; then
184+ cat " $SAVE_LIST "
185+ fi
186+ # Scan directories for git repos
187+ for DIR in " ${@:- " ." } " ; do
188+ # shellcheck disable=SC2086
189+ find -L " $DIR " $FIND_OPTS -type d -name " *.git" -prune 2> /dev/null | while read -r GIT_DIR
190+ do
191+ PROJ_DIR=" $( cd " $GIT_DIR " && cd .. && pwd) "
192+ GIT_CONF=" $GIT_DIR /config"
193+ # Check if repo should be ignored
194+ IGNORE=$( git config -f " $GIT_CONF " --bool mgitstatus.ignore 2> /dev/null)
195+ if [ " $IGNORE " = " true" ]; then
196+ continue
197+ fi
198+ echo " $PROJ_DIR "
199+ done
200+ done
201+ ) | awk ' $0 ~ /^\s*(#|$)/ || !seen[$0]++' > " $temp "
202+ mv " $temp " " $SAVE_LIST "
203+ echo " Saved repository list to $SAVE_LIST "
204+ exit 0
205+ fi
206+
164207# Get current username so we can check .git dir ownership.
165208ID=" $( id -n -u) "
166209
@@ -195,13 +238,12 @@ find_git_work_tree()(
195238 fi
196239)
197240
198- # Go through positional arguments (DIRs) or '.' if no arguments are given
199- for DIR in " ${@:- " ." } " ; do
200- # We *want* to expand parameters, so disable shellcheck for this error:
201- # shellcheck disable=SC2086
202- find -L " $DIR " $FIND_OPTS -type d -name " *.git" -prune | while read -r GIT_DIR
241+ # Process repos either from list file or by scanning directories
242+ if [ -n " $LIST " ]; then
243+ # Read from list file - each line is a project directory
244+ grep -v ' ^\s*#' " $LIST " | grep -v ' ^\s*$' | while read -r PROJ_DIR
203245 do
204- PROJ_DIR =" $( find_git_work_tree " $GIT_DIR " ) "
246+ GIT_DIR =" $PROJ_DIR /.git "
205247 GIT_CONF=" $GIT_DIR /config"
206248
207249 # Check if the repo is safe (https://github.blog/2022-04-12-git-security-vulnerability-announced/)
@@ -254,7 +296,7 @@ for DIR in "${@:-"."}"; do
254296 # they need a push or pull. We do this with various tests and put the name
255297 # of the branches in NEEDS_XXXX, seperated by newlines. After we're done,
256298 # we remove duplicates from NEEDS_XXX.
257- NEEDS_PUSH_BRANCHES=" "
299+ NEEDS_PUSH_BRANCHES=" "
258300 NEEDS_PULL_BRANCHES=" "
259301 NEEDS_UPSTREAM_BRANCHES=" "
260302
@@ -375,4 +417,186 @@ for DIR in "${@:-"."}"; do
375417 sleep " $THROTTLE "
376418 fi
377419 done
378- done
420+ else
421+ # Scan directories - go through positional arguments (DIRs) or '.' if no arguments are given
422+ for DIR in " ${@:- " ." } " ; do
423+ # We *want* to expand parameters, so disable shellcheck for this error:
424+ # shellcheck disable=SC2086
425+ find -L " $DIR " $FIND_OPTS -type d -name " *.git" -prune | while read -r GIT_DIR
426+ do
427+ PROJ_DIR=" $( find_git_work_tree " $GIT_DIR " ) "
428+ GIT_CONF=" $GIT_DIR /config"
429+
430+ # Check if the repo is safe (https://github.blog/2022-04-12-git-security-vulnerability-announced/)
431+ if [ -d " $GIT_DIR " ]; then
432+ GIT_DIR_OWNER=" $( ls -ld " $GIT_DIR " | awk ' NR==1 {print $3}' ) "
433+ if [ " $ID " != " $GIT_DIR_OWNER " ]; then
434+ printf " ${PROJ_DIR} : ${C_UNSAFE} Unsafe ownership, owned by someone else. Skipping.${C_RESET} \n"
435+ continue
436+ fi
437+ fi
438+
439+ # Check git config for this project to see if we should ignore this repo.
440+ IGNORE=$( git config -f " $GIT_CONF " --bool mgitstatus.ignore)
441+ if [ " $IGNORE " = " true" ]; then
442+ continue
443+ fi
444+
445+ # If this dir is not a repo, and WARN_NOT_REPO is 1, tell the user.
446+ if [ ! -d " $GIT_DIR " ]; then
447+ if [ " $WARN_NOT_REPO " -eq 1 ] && [ " $PROJ_DIR " != " ." ]; then
448+ printf " ${PROJ_DIR} : not a git repo\n"
449+ fi
450+ continue
451+ fi
452+
453+ [ $DEBUG -eq 1 ] && echo " ${PROJ_DIR} "
454+
455+ # Check if repo is locked
456+ if [ -f " $GIT_DIR /index.lock" ]; then
457+ printf " ${PROJ_DIR} : ${C_LOCKED} Locked. Skipping.${C_RESET} \n"
458+ continue
459+ fi
460+
461+ # Do a 'git fetch' if requested
462+ if [ " $DO_FETCH " -eq 1 ]; then
463+ git --work-tree " $PROJ_DIR " --git-dir " $GIT_DIR " fetch -q > /dev/null
464+ fi
465+
466+ # Refresh the index, or we might get wrong results.
467+ git --work-tree " $PROJ_DIR " --git-dir " $GIT_DIR " update-index -q --refresh > /dev/null 2>&1
468+
469+ # Get current branch, if "-b" is specified
470+ if [ " $SHOW_CUR_BRANCH " -eq 1 ]; then
471+ CUR_BRANCH=" ($( git --git-dir " $GIT_DIR " rev-parse --abbrev-ref HEAD) )"
472+ else
473+ CUR_BRANCH=" "
474+ fi
475+
476+ # Find all remote branches that have been checked out and figure out if
477+ # they need a push or pull. We do this with various tests and put the name
478+ # of the branches in NEEDS_XXXX, seperated by newlines. After we're done,
479+ # we remove duplicates from NEEDS_XXX.
480+ NEEDS_PUSH_BRANCHES=" "
481+ NEEDS_PULL_BRANCHES=" "
482+ NEEDS_UPSTREAM_BRANCHES=" "
483+
484+ for REF_HEAD in $( git --git-dir " ${GIT_DIR} " for-each-ref --format=' %(refname:short)' refs/heads/) ; do
485+ # Check if this branch is tracking an upstream (local/remote branch)
486+ UPSTREAM=$( git --git-dir " $GIT_DIR " rev-parse --abbrev-ref --symbolic-full-name " $REF_HEAD @{u}" 2> /dev/null)
487+ EXIT_CODE=" $? "
488+ if [ " $EXIT_CODE " -eq 0 ]; then
489+ # Branch is tracking a remote branch. Find out how much behind /
490+ # ahead it is of that remote branch.
491+ CNT_AHEAD_BEHIND=$( git --git-dir " $GIT_DIR " rev-list --left-right --count " $REF_HEAD ...$UPSTREAM " )
492+ CNT_AHEAD=$( echo " $CNT_AHEAD_BEHIND " | awk ' { print $1 }' )
493+ CNT_BEHIND=$( echo " $CNT_AHEAD_BEHIND " | awk ' { print $2 }' )
494+
495+ [ $DEBUG -eq 1 ] && echo " CNT_AHEAD_BEHIND: $CNT_AHEAD_BEHIND "
496+ [ $DEBUG -eq 1 ] && echo " CNT_AHEAD: $CNT_AHEAD "
497+ [ $DEBUG -eq 1 ] && echo " CNT_BEHIND: $CNT_BEHIND "
498+
499+ if [ " $CNT_AHEAD " -gt 0 ]; then
500+ NEEDS_PUSH_BRANCHES=" ${NEEDS_PUSH_BRANCHES} \n$REF_HEAD "
501+ fi
502+ if [ " $CNT_BEHIND " -gt 0 ]; then
503+ NEEDS_PULL_BRANCHES=" ${NEEDS_PULL_BRANCHES} \n$REF_HEAD "
504+ fi
505+
506+ # Check if this branch is a branch off another branch. and if it needs
507+ # to be updated.
508+ REV_LOCAL=$( git --git-dir " $GIT_DIR " rev-parse --verify " $REF_HEAD " 2> /dev/null)
509+ REV_REMOTE=$( git --git-dir " $GIT_DIR " rev-parse --verify " $UPSTREAM " 2> /dev/null)
510+ REV_BASE=$( git --git-dir " $GIT_DIR " merge-base " $REF_HEAD " " $UPSTREAM " 2> /dev/null)
511+
512+ [ $DEBUG -eq 1 ] && echo " REV_LOCAL: $REV_LOCAL "
513+ [ $DEBUG -eq 1 ] && echo " REV_REMOTE: $REV_REMOTE "
514+ [ $DEBUG -eq 1 ] && echo " REV_BASE: $REV_BASE "
515+
516+ if [ " $REV_LOCAL " = " $REV_REMOTE " ]; then
517+ : # NOOP
518+ else
519+ if [ " $REV_LOCAL " = " $REV_BASE " ]; then
520+ NEEDS_PULL_BRANCHES=" ${NEEDS_PULL_BRANCHES} \n$REF_HEAD "
521+ fi
522+ if [ " $REV_REMOTE " = " $REV_BASE " ]; then
523+ NEEDS_PUSH_BRANCHES=" ${NEEDS_PUSH_BRANCHES} \n$REF_HEAD "
524+ fi
525+ fi
526+ else
527+ # Branch does not have an upstream (local/remote branch).
528+ NEEDS_UPSTREAM_BRANCHES=" ${NEEDS_UPSTREAM_BRANCHES} \n$REF_HEAD "
529+ fi
530+ done
531+
532+ # Remove duplicates from NEEDS_XXXX and make comma-seperated
533+ NEEDS_PUSH_BRANCHES=$( printf " $NEEDS_PUSH_BRANCHES " | sort | uniq | tr ' \n' ' ,' | sed " s/^,\(.*\),$/\1/" )
534+ NEEDS_PULL_BRANCHES=$( printf " $NEEDS_PULL_BRANCHES " | sort | uniq | tr ' \n' ' ,' | sed " s/^,\(.*\),$/\1/" )
535+ NEEDS_UPSTREAM_BRANCHES=$( printf " $NEEDS_UPSTREAM_BRANCHES " | sort | uniq | tr ' \n' ' ,' | sed " s/^,\(.*\),$/\1/" )
536+
537+ # Find out if there are unstaged, uncommitted or untracked changes
538+ UNSTAGED=$( git --work-tree " $PROJ_DIR " --git-dir " $GIT_DIR " diff-index --quiet HEAD -- 2> /dev/null; echo $? )
539+ UNCOMMITTED=$( git --work-tree " $PROJ_DIR " --git-dir " $GIT_DIR " diff-files --quiet --ignore-submodules --; echo $? )
540+ UNTRACKED=$( git --work-tree " $PROJ_DIR " --git-dir " $GIT_DIR " ls-files --exclude-standard --others)
541+ STASHES=$( git --work-tree " $PROJ_DIR " --git-dir " $GIT_DIR " stash list | wc -l)
542+
543+ [ $DEBUG -eq 1 ] && echo " UNSTAGED: $UNSTAGED "
544+ [ $DEBUG -eq 1 ] && echo " UNCOMMITTED: $UNCOMMITTED "
545+ [ $DEBUG -eq 1 ] && echo " UNTRACKED: $UNTRACKED "
546+ [ $DEBUG -eq 1 ] && echo " STASHES: $STASHES "
547+
548+ # Build up the status string if not flattening. Otherwise, print
549+ # results immediately.
550+ IS_OK=0 # 0 = Repo needs something, 1 = Repo needs nothing ('ok')
551+ STATUS_NEEDS=" "
552+ if [ -n " $NEEDS_PUSH_BRANCHES " ] && [ " $NO_PUSH " -eq 0 ]; then
553+ THIS_STATUS=" ${C_NEEDS_PUSH} Needs push ($NEEDS_PUSH_BRANCHES )${C_RESET} "
554+ STATUS_NEEDS=" ${STATUS_NEEDS}${THIS_STATUS} "
555+ [ " $FLATTEN " -eq 1 ] && printf " ${PROJ_DIR} $CUR_BRANCH : $THIS_STATUS \n"
556+ fi
557+ if [ -n " $NEEDS_PULL_BRANCHES " ] && [ " $NO_PULL " -eq 0 ]; then
558+ THIS_STATUS=" ${C_NEEDS_PULL} Needs pull ($NEEDS_PULL_BRANCHES )${C_RESET} "
559+ STATUS_NEEDS=" ${STATUS_NEEDS}${THIS_STATUS} "
560+ [ " $FLATTEN " -eq 1 ] && printf " ${PROJ_DIR} $CUR_BRANCH : $THIS_STATUS \n"
561+ fi
562+ if [ -n " $NEEDS_UPSTREAM_BRANCHES " ] && [ " $NO_UPSTREAM " -eq 0 ]; then
563+ THIS_STATUS=" ${C_NEEDS_UPSTREAM} Needs upstream ($NEEDS_UPSTREAM_BRANCHES )${C_RESET} "
564+ STATUS_NEEDS=" ${STATUS_NEEDS}${THIS_STATUS} "
565+ [ " $FLATTEN " -eq 1 ] && printf " ${PROJ_DIR} $CUR_BRANCH : $THIS_STATUS \n"
566+ fi
567+ if [ " $UNSTAGED " -ne 0 ] || [ " $UNCOMMITTED " -ne 0 ] && [ " $NO_UNCOMMITTED " -eq 0 ]; then
568+ THIS_STATUS=" ${C_NEEDS_COMMIT} Uncommitted changes${C_RESET} "
569+ STATUS_NEEDS=" ${STATUS_NEEDS}${THIS_STATUS} "
570+ [ " $FLATTEN " -eq 1 ] && printf " ${PROJ_DIR} $CUR_BRANCH : $THIS_STATUS \n"
571+ fi
572+ if [ " $UNTRACKED " != " " ] && [ " $NO_UNTRACKED " -eq 0 ]; then
573+ THIS_STATUS=" ${C_UNTRACKED} Untracked files${C_RESET} "
574+ STATUS_NEEDS=" ${STATUS_NEEDS}${THIS_STATUS} "
575+ [ " $FLATTEN " -eq 1 ] && printf " ${PROJ_DIR} $CUR_BRANCH : $THIS_STATUS \n"
576+ fi
577+ if [ " $STASHES " -ne 0 ] && [ " $NO_STASHES " -eq 0 ]; then
578+ THIS_STATUS=" ${C_STASHES} $STASHES stashes${C_RESET} "
579+ STATUS_NEEDS=" ${STATUS_NEEDS}${THIS_STATUS} "
580+ [ " $FLATTEN " -eq 1 ] && printf " ${PROJ_DIR} $CUR_BRANCH : $THIS_STATUS \n"
581+ fi
582+ if [ " $STATUS_NEEDS " = " " ]; then
583+ IS_OK=1
584+ THIS_STATUS=" ${C_OK} ok${C_RESET} "
585+ STATUS_NEEDS=" ${STATUS_NEEDS}${THIS_STATUS} "
586+ [ " $FLATTEN " -eq 1 ] && [ " $EXCLUDE_OK " -ne 1 ] && printf " ${PROJ_DIR} $CUR_BRANCH : $THIS_STATUS \n"
587+ fi
588+
589+ if [ " $FLATTEN " -ne 1 ]; then
590+ # Print the output, unless repo is 'ok' and -e was specified
591+ if [ " $IS_OK " -ne 1 ] || [ " $EXCLUDE_OK " -ne 1 ]; then
592+ printf " ${PROJ_DIR} $CUR_BRANCH : $STATUS_NEEDS \n"
593+ fi
594+ fi
595+
596+ # Throttle if requested
597+ if [ " $DO_FETCH " -eq 1 ] && [ " $THROTTLE " -ne 0 ]; then
598+ sleep " $THROTTLE "
599+ fi
600+ done
601+ done
602+ fi
0 commit comments