Skip to content

Commit 55d098c

Browse files
tobixenclaude
andcommitted
Integrate list file support from PR fboender#27
Add -l/--list FILE option to read repo paths from a file instead of scanning. Add -s/--save-list FILE option to scan and save repo paths to a file. The list file supports comments (lines starting with #) and empty lines. Based on PR fboender#27 by cipherbrain, adapted to work with the find_git_work_tree approach from PR fboender#53. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a63ca1c commit 55d098c

1 file changed

Lines changed: 233 additions & 9 deletions

File tree

mgitstatus

Lines changed: 233 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ deep.
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+
4044
EOF
4145
}
4246

@@ -56,6 +60,8 @@ NO_DEPTH=0
5660
THROTTLE=0
5761
DEPTH=2
5862
SHOW_CUR_BRANCH=0
63+
LIST=""
64+
SAVE_LIST=""
5965

6066
while [ -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
140154
done
@@ -161,6 +175,35 @@ C_UNTRACKED="$C_CYAN"
161175
C_STASHES="$C_YELLOW"
162176
C_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.
165208
ID="$(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

Comments
 (0)