├── .gitignore ├── screenshot.png ├── Makefile ├── LICENSE.txt ├── README.md ├── mgitstatus.1.md ├── mgitstatus.1 └── mgitstatus /.gitignore: -------------------------------------------------------------------------------- 1 | build.sla 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fboender/multi-git-status/HEAD/screenshot.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/sh 2 | 3 | PREFIX ?= /usr/local 4 | 5 | .PHONY: all 6 | all: man 7 | 8 | mgitstatus.1: mgitstatus.1.md 9 | pandoc ./$< -s -t man > $@ 10 | 11 | .PHONY: man 12 | man: mgitstatus.1 13 | 14 | .PHONY: test 15 | test: 16 | # SC1117 Backslash is literal in... 17 | # SC2059 Don't use variables in the printf format string. But we need to or colors won't work 18 | # SC2012 Use find instead of ls, but we need to extract the user id of the .git dir 19 | shellcheck -e SC1117,SC2059,SC2012 mgitstatus 20 | 21 | .PHONY: install 22 | install: 23 | install -d $(DESTDIR)$(PREFIX)/bin 24 | install -d $(DESTDIR)$(PREFIX)/man/man1 25 | install -m 755 mgitstatus $(DESTDIR)$(PREFIX)/bin/ 26 | install mgitstatus.1 $(DESTDIR)$(PREFIX)/man/man1/ 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2022 Ferry Boender 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mgitstatus 2 | ========== 3 | 4 | Show uncommitted, untracked and unpushed changes in multiple Git 5 | repositories. Scan for .git dirs up to **DEPTH** directories deep. 6 | The default is 2. If **DEPTH** is 0, the scan is infinitely deep. 7 | 8 | ![](https://raw.githubusercontent.com/fboender/multi-git-status/master/screenshot.png) 9 | 10 | mgitstatus shows: 11 | 12 | * **Uncommitted changes** if there are unstaged or uncommitted changes on the 13 | checked out branch. 14 | * **Untracked files** if there are untracked files which are not ignored. 15 | * **Needs push (BRANCH)** if the branch is tracking a (remote) branch which is 16 | behind. 17 | * **Needs upstream (BRANCH)** if a branch does not have a local or remote 18 | upstream branch configured. Changes in the branch may otherwise never be 19 | pushed or merged. 20 | * **Needs pull (BRANCH)** if the branch is tracking a (remote) branch which is 21 | ahead. This requires that the local git repo already knows about the remote 22 | changes (i.e. you've done a `fetch`), or that you specify the `-f` option. 23 | mgitstatus does NOT contact the remote by default. 24 | * **X stashes** if there are stashes. 25 | 26 | Since there are a lot of different states a git repository can be in, 27 | mgitstatus makes no guarantees that *all* states are taken into account. 28 | 29 | mgitstatus can also list dirs that are not a repo, if given the `-w` 30 | switch. To ignore certain repos, set the `mgitstatus.ignore` git config flag 31 | for that repo to `true`. (See "usage" below for an example). 32 | 33 | 34 | # Usage 35 | 36 | Usage: mgitstatus [--version] [-w] [-e] [-f] [--throttle SEC] [-c] [-d/--depth=2] [--no-depth] [--flatten] [--no-X] [DIR [DIR]...] 37 | 38 | mgitstatus shows uncommitted, untracked and unpushed changes in multiple Git 39 | repositories. By default, mgitstatus scans two directories deep. This can be 40 | changed with the -d (--depth) option. If DEPTH is 0, the scan is infinitely 41 | deep. 42 | 43 | --version Show version 44 | -w Warn about dirs that are not Git repositories 45 | -e Exclude repos that are 'ok' 46 | -f Do a 'git fetch' on each repo (slow for many repos) 47 | --throttle SEC Wait SEC seconds between each 'git fetch' (-f option) 48 | -c Force color output (preserve colors when using pipes) 49 | -d, --depth=2 Scan this many directories deep 50 | --no-depth Do not recurse into directories (incompatible with -d) 51 | --flatten Show only one status per line 52 | 53 | You can limit output with the following options: 54 | 55 | --no-push 56 | --no-pull 57 | --no-upstream 58 | --no-uncommitted 59 | --no-untracked 60 | --no-stashes 61 | --no-ok (same as -e) 62 | 63 | The following example scans all directories under the current dir, with a 64 | depth of 2. That means the current dir and all directories directly under it. 65 | 66 | ~/Projects/fboender $ mgitstatus 67 | ./mgitstatus: ok 68 | ./mdpreview: ok 69 | ./snippets: ok 70 | ./boxes: ok 71 | ./ansible-cmdb: Uncommitted changes Untracked files 72 | ./scriptform: Uncommitted changes 73 | 74 | For more examples, see the [manual page](mgitstatus.1.md). 75 | 76 | # Installation 77 | 78 | mgitstatus requires make. 79 | 80 | The following steps will install mgitstatus: 81 | 82 | # Clone the repo 83 | $ git clone https://github.com/fboender/multi-git-status.git 84 | $ cd multi-git-status 85 | 86 | # Install globally (all users) 87 | $ sudo make install 88 | 89 | # Install locally (only your user) 90 | $ PREFIX=~/.local make install 91 | 92 | # License 93 | 94 | Copyright 2016-2022, Ferry Boender (et al). 95 | 96 | Licensed under the MIT license. For more information, see the LICENSE.txt file. 97 | -------------------------------------------------------------------------------- /mgitstatus.1.md: -------------------------------------------------------------------------------- 1 | % MGITSTATUS(1) 2 | % Ferry Boender 3 | % Mar 2022 4 | 5 | # NAME 6 | 7 | mgitstatus - Show uncommitted, untracked and unpushed changes for multiple Git repos. 8 | 9 | # SYNOPSIS 10 | 11 | **mgitstatus** [**\--version**] [**-w**] [**-e**] [**-f**] [**\--throttle** SEC] [**\-c**] [**-d/\--depth**=2] [**\--flatten**] [**\--no-X**] [**DIR** [**DIR**]...] 12 | 13 | # DESCRIPTION 14 | 15 | **mgitstatus** shows uncommitted, untracked and unpushed changes in multiple 16 | Git repositories. By default, **mgitstatus** scans two directories deep. This 17 | can be changed with the `-d` (`--depth`) option. If **DEPTH** is 0, the scan 18 | is infinitely deep. 19 | 20 | mgitstatus shows: 21 | 22 | - **Uncommitted changes** if there are unstaged or uncommitted changes on the 23 | checked out branch. 24 | 25 | - **Untracked files** if there are untracked files which are not ignored. 26 | 27 | - **Needs push (BRANCH)** if the branch is tracking a (remote) branch which is 28 | behind. 29 | 30 | - **Needs upstream (BRANCH)** if a branch does not have a local or remote 31 | upstream branch configured. Changes in the branch may otherwise never be 32 | pushed or merged. 33 | 34 | - **Needs pull (BRANCH)** if the branch is tracking a (remote) branch which is 35 | ahead. This requires that the local git repo already knows about the remote 36 | changes (i.e. you've done a fetch), or that you specify the -f option. 37 | mgitstatus does NOT contact the remote by default. 38 | 39 | - **X stashes** if there are stashes. 40 | 41 | Since there are a lot of different states a git repository can be in, 42 | mgitstatus makes no guarantees that all states are taken into account. 43 | 44 | # OPTIONS 45 | 46 | **\--version** 47 | : Show version 48 | 49 | **-w** 50 | : Warn about dirs that are not Git repositories 51 | 52 | **-e** 53 | : Exclude repos that are 'ok' 54 | 55 | **-f** 56 | : Do a 'git fetch' on each repo (slow for many repos) 57 | 58 | **\--throttle SEC** 59 | : Wait SEC seconds between each 'git fetch' (-f option) 60 | 61 | **-c** 62 | : Force color output (preserve colors when using pipes) 63 | 64 | **-d, \--depth=2** 65 | : Scan this many directories deep. Default is 2. If **0**, the scan is infinitely deep 66 | 67 | **\--no-depth** 68 | : Do not recurse into directories (incompatible with -d) 69 | 70 | **\--flatten** 71 | : Flatten output by only showing one status per line. If a repo has multiple statuses, multiple lines are shown for that repo. This aids in grepability. 72 | 73 | You can limit output with the following options: 74 | 75 | **\--no-push** 76 | : Do not show branches that need a push. 77 | 78 | **\--no-pull** 79 | : Do not show branches that need a pull. 80 | 81 | **\--no-upstream** 82 | : Do not show branches that need an upstream. 83 | 84 | **\--no-uncommitted** 85 | : Do not show branches that have unstaged or uncommitted changes. 86 | 87 | **\--no-untracked** 88 | : Do not show branches that have untracked files. 89 | 90 | **\--no-stashes** 91 | : Do now show stashes 92 | 93 | **\--no-ok** 94 | : Do now show repos that are 'ok' (same as -e) 95 | 96 | 97 | # EXAMPLES 98 | 99 | The following command scans two directories deep for Git projects and shows 100 | their status: 101 | 102 | $ mgitstatus 103 | ./fboender/sla: ok 104 | ./fboender/multi-git-status: Needs push (master) Untracked files 105 | ./other/peewee: ok 106 | 107 | To scan deeper (three dirs instead of two) in the current dir: 108 | 109 | $ mgitstatus -d 3 110 | 111 | The following command scans three levels deep in `/opt/deploy/` and hides 112 | repos that are 'ok'. It does not show stashes: 113 | 114 | $ mgitstatus -e --no-stashes -d 3 /opt/deploy 115 | 116 | To ignore a repo, set the `mgitstatus.ignore` git configuration option for 117 | that repo to `true`. E.g.: 118 | 119 | $ cd stupidrepo 120 | $ git config --local mgitstatus.ignore true 121 | 122 | Sort output by repo name while retaining colors: 123 | 124 | $ mgitstatus -c | sort 125 | ./ansible: ok 126 | ./ansible-cmdb: Needs push (master) Uncommitted changes Untracked files 127 | ./davis: ok 128 | ./espy: Uncommitted changes Untracked files 129 | ./garner: Untracked files 130 | ./garner-chains: ok 131 | ./mdpreview: ok 132 | 133 | Sort output by repo status while retaining colors: 134 | 135 | $ mgitstatus -c --flatten -e | sort -k2 136 | ./fboender/ansible-cmdb: Uncommitted changes 137 | ./fboender/espy: Uncommitted changes 138 | ./fboender/multi-git-status: Uncommitted changes 139 | ./fboender/ansible-cmdb: Needs push (master) 140 | ./fboender/ansible-cmdb: Untracked files 141 | ./fboender/espy: Untracked files 142 | ./fboender/garner: Untracked files 143 | ./fboender/multi-git-status: Untracked files 144 | 145 | Force color output and flatten the output so we can grep for things: 146 | 147 | $ mgitstatus --flatten -c | grep Uncommitted 148 | ./fboender/multi-git-status: Uncommitted changes 149 | ./fboender/ansible-cmdb: Uncommitted changes 150 | ./fboender/espy: Uncommitted changes 151 | 152 | 153 | # COPYRIGHT 154 | 155 | Copyright 2016-2022, Ferry Boender (et al). 156 | 157 | Licensed under the MIT license. For more information, see the LICENSE.txt file. 158 | -------------------------------------------------------------------------------- /mgitstatus.1: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 2.5 2 | .\" 3 | .TH "MGITSTATUS" "1" "Mar 2022" "" "" 4 | .hy 5 | .SH NAME 6 | .PP 7 | mgitstatus \- Show uncommitted, untracked and unpushed changes for 8 | multiple Git repos. 9 | .SH SYNOPSIS 10 | .PP 11 | \f[B]mgitstatus\f[R] [\f[B]\-\-version\f[R]] [\f[B]\-w\f[R]] 12 | [\f[B]\-e\f[R]] [\f[B]\-f\f[R]] [\f[B]\-\-throttle\f[R] SEC] 13 | [\f[B]\-c\f[R]] [\f[B]\-d/\-\-depth\f[R]=2] [\f[B]\-\-flatten\f[R]] 14 | [\f[B]\-\-no\-X\f[R]] [\f[B]DIR\f[R] [\f[B]DIR\f[R]]\&...] 15 | .SH DESCRIPTION 16 | .PP 17 | \f[B]mgitstatus\f[R] shows uncommitted, untracked and unpushed changes 18 | in multiple Git repositories. 19 | By default, \f[B]mgitstatus\f[R] scans two directories deep. 20 | This can be changed with the \f[C]\-d\f[R] (\f[C]\-\-depth\f[R]) option. 21 | If \f[B]DEPTH\f[R] is 0, the scan is infinitely deep. 22 | .PP 23 | mgitstatus shows: 24 | .IP \[bu] 2 25 | \f[B]Uncommitted changes\f[R] if there are unstaged or uncommitted 26 | changes on the checked out branch. 27 | .IP \[bu] 2 28 | \f[B]Untracked files\f[R] if there are untracked files which are not 29 | ignored. 30 | .IP \[bu] 2 31 | \f[B]Needs push (BRANCH)\f[R] if the branch is tracking a (remote) 32 | branch which is behind. 33 | .IP \[bu] 2 34 | \f[B]Needs upstream (BRANCH)\f[R] if a branch does not have a local or 35 | remote upstream branch configured. 36 | Changes in the branch may otherwise never be pushed or merged. 37 | .IP \[bu] 2 38 | \f[B]Needs pull (BRANCH)\f[R] if the branch is tracking a (remote) 39 | branch which is ahead. 40 | This requires that the local git repo already knows about the remote 41 | changes (i.e.\ you\[cq]ve done a fetch), or that you specify the \-f 42 | option. 43 | mgitstatus does NOT contact the remote by default. 44 | .IP \[bu] 2 45 | \f[B]X stashes\f[R] if there are stashes. 46 | .PP 47 | Since there are a lot of different states a git repository can be in, 48 | mgitstatus makes no guarantees that all states are taken into account. 49 | .SH OPTIONS 50 | .TP 51 | .B \f[B]\-\-version\f[R] 52 | Show version 53 | .TP 54 | .B \f[B]\-w\f[R] 55 | Warn about dirs that are not Git repositories 56 | .TP 57 | .B \f[B]\-e\f[R] 58 | Exclude repos that are `ok' 59 | .TP 60 | .B \f[B]\-f\f[R] 61 | Do a `git fetch' on each repo (slow for many repos) 62 | .TP 63 | .B \f[B]\-\-throttle SEC\f[R] 64 | Wait SEC seconds between each `git fetch' (\-f option) 65 | .TP 66 | .B \f[B]\-c\f[R] 67 | Force color output (preserve colors when using pipes) 68 | .TP 69 | .B \f[B]\-d, \-\-depth=2\f[R] 70 | Scan this many directories deep. 71 | Default is 2. 72 | If \f[B]0\f[R], the scan is infinitely deep 73 | .TP 74 | .B \f[B]\-\-no\-depth\f[R] 75 | Do not recurse into directories (incompatible with \-d) 76 | .TP 77 | .B \f[B]\-\-flatten\f[R] 78 | Flatten output by only showing one status per line. 79 | If a repo has multiple statuses, multiple lines are shown for that repo. 80 | This aids in grepability. 81 | .PP 82 | You can limit output with the following options: 83 | .TP 84 | .B \f[B]\-\-no\-push\f[R] 85 | Do not show branches that need a push. 86 | .TP 87 | .B \f[B]\-\-no\-pull\f[R] 88 | Do not show branches that need a pull. 89 | .TP 90 | .B \f[B]\-\-no\-upstream\f[R] 91 | Do not show branches that need an upstream. 92 | .TP 93 | .B \f[B]\-\-no\-uncommitted\f[R] 94 | Do not show branches that have unstaged or uncommitted changes. 95 | .TP 96 | .B \f[B]\-\-no\-untracked\f[R] 97 | Do not show branches that have untracked files. 98 | .TP 99 | .B \f[B]\-\-no\-stashes\f[R] 100 | Do now show stashes 101 | .TP 102 | .B \f[B]\-\-no\-ok\f[R] 103 | Do now show repos that are `ok' (same as \-e) 104 | .SH EXAMPLES 105 | .PP 106 | The following command scans two directories deep for Git projects and 107 | shows their status: 108 | .IP 109 | .nf 110 | \f[C] 111 | $ mgitstatus 112 | \&./fboender/sla: ok 113 | \&./fboender/multi\-git\-status: Needs push (master) Untracked files 114 | \&./other/peewee: ok 115 | \f[R] 116 | .fi 117 | .PP 118 | To scan deeper (three dirs instead of two) in the current dir: 119 | .IP 120 | .nf 121 | \f[C] 122 | $ mgitstatus \-d 3 123 | \f[R] 124 | .fi 125 | .PP 126 | The following command scans three levels deep in \f[C]/opt/deploy/\f[R] 127 | and hides repos that are `ok'. 128 | It does not show stashes: 129 | .IP 130 | .nf 131 | \f[C] 132 | $ mgitstatus \-e \-\-no\-stashes \-d 3 /opt/deploy 133 | \f[R] 134 | .fi 135 | .PP 136 | To ignore a repo, set the \f[C]mgitstatus.ignore\f[R] git configuration 137 | option for that repo to \f[C]true\f[R]. 138 | E.g.: 139 | .IP 140 | .nf 141 | \f[C] 142 | $ cd stupidrepo 143 | $ git config \-\-local mgitstatus.ignore true 144 | \f[R] 145 | .fi 146 | .PP 147 | Sort output by repo name while retaining colors: 148 | .IP 149 | .nf 150 | \f[C] 151 | $ mgitstatus \-c | sort 152 | \&./ansible: ok 153 | \&./ansible\-cmdb: Needs push (master) Uncommitted changes Untracked files 154 | \&./davis: ok 155 | \&./espy: Uncommitted changes Untracked files 156 | \&./garner: Untracked files 157 | \&./garner\-chains: ok 158 | \&./mdpreview: ok 159 | \f[R] 160 | .fi 161 | .PP 162 | Sort output by repo status while retaining colors: 163 | .IP 164 | .nf 165 | \f[C] 166 | $ mgitstatus \-c \-\-flatten \-e | sort \-k2 167 | \&./fboender/ansible\-cmdb: Uncommitted changes 168 | \&./fboender/espy: Uncommitted changes 169 | \&./fboender/multi\-git\-status: Uncommitted changes 170 | \&./fboender/ansible\-cmdb: Needs push (master) 171 | \&./fboender/ansible\-cmdb: Untracked files 172 | \&./fboender/espy: Untracked files 173 | \&./fboender/garner: Untracked files 174 | \&./fboender/multi\-git\-status: Untracked files 175 | \f[R] 176 | .fi 177 | .PP 178 | Force color output and flatten the output so we can grep for things: 179 | .IP 180 | .nf 181 | \f[C] 182 | $ mgitstatus \-\-flatten \-c | grep Uncommitted 183 | \&./fboender/multi\-git\-status: Uncommitted changes 184 | \&./fboender/ansible\-cmdb: Uncommitted changes 185 | \&./fboender/espy: Uncommitted changes 186 | \f[R] 187 | .fi 188 | .SH COPYRIGHT 189 | .PP 190 | Copyright 2016\-2022, Ferry Boender (et al). 191 | .PP 192 | Licensed under the MIT license. 193 | For more information, see the LICENSE.txt file. 194 | .SH AUTHORS 195 | Ferry Boender. 196 | -------------------------------------------------------------------------------- /mgitstatus: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # MIT license 4 | 5 | VERSION="2.3" 6 | DEBUG=${MG_DEBUG:-0} 7 | 8 | usage () { 9 | cat << EOF >&2 10 | 11 | Usage: $0 [--version] [-w] [-e] [-f] [--throttle SEC] [-c] [-d/--depth=2] [--flatten] [--no-X] [DIR [DIR]...] 12 | 13 | mgitstatus shows uncommitted, untracked and unpushed changes in multiple Git 14 | repositories. By default, mgitstatus scans two directories deep. This can be 15 | changed with the -d (--depth) option. If DEPTH is 0, the scan is infinitely 16 | deep. 17 | 18 | -b Show currently checked out branch 19 | -c Force color output (preserve colors when using pipes) 20 | -d, --depth=2 Scan this many directories deep 21 | -e Exclude repos that are 'ok' 22 | -f Do a 'git fetch' on each repo (slow for many repos) 23 | -h, --help Show this help message 24 | --flatten Show only one status per line 25 | --no-depth Do not recurse into directories (incompatible with -d) 26 | --throttle SEC Wait SEC seconds between each 'git fetch' (-f option) 27 | --version Show version 28 | -w Warn about dirs that are not Git repositories 29 | 30 | You can limit output with the following options: 31 | 32 | --no-push 33 | --no-pull 34 | --no-upstream 35 | --no-uncommitted 36 | --no-untracked 37 | --no-stashes 38 | --no-ok (same as -e) 39 | 40 | EOF 41 | } 42 | 43 | # Handle commandline options 44 | WARN_NOT_REPO=0 45 | EXCLUDE_OK=0 46 | DO_FETCH=0 47 | FORCE_COLOR=0 48 | FLATTEN=0 49 | NO_PUSH=0 50 | NO_PULL=0 51 | NO_UPSTREAM=0 52 | NO_UNCOMMITTED=0 53 | NO_UNTRACKED=0 54 | NO_STASHES=0 55 | NO_DEPTH=0 56 | THROTTLE=0 57 | DEPTH=2 58 | SHOW_CUR_BRANCH=0 59 | 60 | while [ -n "$1" ]; do 61 | # Stop reading when we've run out of options. 62 | [ "$(printf "%s" "$1" | cut -c 1)" != "-" ] && break 63 | 64 | if [ "$1" = "-b" ]; then 65 | SHOW_CUR_BRANCH=1 66 | fi 67 | if [ "$1" = "-c" ]; then 68 | FORCE_COLOR=1 69 | fi 70 | if [ "$1" = "-d" ] || [ "$1" = "--depth" ]; then 71 | DEPTH="$2" 72 | echo "$DEPTH" | grep -E "^[0-9]+$" > /dev/null 2>&1 73 | IS_NUM="$?" 74 | if [ "$IS_NUM" -ne 0 ]; then 75 | echo "Invalid value for 'depth' (must be a number): $DEPTH" >&2 76 | exit 1 77 | fi 78 | # Shift one extra param 79 | shift 80 | fi 81 | if [ "$1" = "-e" ]; then 82 | EXCLUDE_OK=1 83 | fi 84 | if [ "$1" = "-f" ]; then 85 | DO_FETCH=1 86 | fi 87 | if [ "$1" = "--flatten" ]; then 88 | FLATTEN=1 89 | fi 90 | if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then 91 | usage 92 | exit 1 93 | fi 94 | if [ "$1" = "--no-push" ]; then 95 | NO_PUSH=1 96 | fi 97 | if [ "$1" = "--no-pull" ]; then 98 | NO_PULL=1 99 | fi 100 | if [ "$1" = "--no-upstream" ]; then 101 | NO_UPSTREAM=1 102 | fi 103 | if [ "$1" = "--no-uncommitted" ]; then 104 | NO_UNCOMMITTED=1 105 | fi 106 | if [ "$1" = "--no-untracked" ]; then 107 | NO_UNTRACKED=1 108 | fi 109 | if [ "$1" = "--no-stashes" ]; then 110 | NO_STASHES=1 111 | fi 112 | if [ "$1" = "--no-ok" ]; then 113 | # Same as -e, but -e violates the principle of least astonishment. 114 | EXCLUDE_OK=1 115 | fi 116 | if [ "$1" = "--no-depth" ]; then 117 | # Same as -e, but -e violates the principle of least astonishment. 118 | NO_DEPTH=1 119 | fi 120 | if [ "$1" = "--throttle" ]; then 121 | THROTTLE="$2" 122 | echo "$THROTTLE" | grep -E "^[0-9]+$" > /dev/null 2>&1 123 | IS_NUM="$?" 124 | if [ "$IS_NUM" -ne 0 ]; then 125 | echo "Invalid value for 'throttle' (must be a number): $THROTTLE" >&2 126 | exit 1 127 | fi 128 | # Shift one extra param 129 | shift 130 | fi 131 | if [ "$1" = "--version" ]; then 132 | echo "v$VERSION" 133 | exit 0 134 | fi 135 | if [ "$1" = "-w" ]; then 136 | WARN_NOT_REPO=1 137 | fi 138 | 139 | shift 140 | done 141 | 142 | 143 | if [ -t 1 ] || [ "$FORCE_COLOR" -eq 1 ]; then 144 | # Our output is not being redirected, so we can use colors. 145 | C_RED="\033[1;31m" 146 | C_GREEN="\033[1;32m" 147 | C_YELLOW="\033[1;33m" 148 | C_BLUE="\033[1;34m" 149 | C_PURPLE="\033[1;35m" 150 | C_CYAN="\033[1;36m" 151 | C_RESET="\033[0;10m" 152 | fi 153 | 154 | C_OK="$C_GREEN" 155 | C_LOCKED="$C_RED" 156 | C_NEEDS_PUSH="$C_YELLOW" 157 | C_NEEDS_PULL="$C_BLUE" 158 | C_NEEDS_COMMIT="$C_RED" 159 | C_NEEDS_UPSTREAM="$C_PURPLE" 160 | C_UNTRACKED="$C_CYAN" 161 | C_STASHES="$C_YELLOW" 162 | C_UNSAFE="$C_PURPLE" 163 | 164 | # Get current username so we can check .git dir ownership. 165 | ID="$(id -n -u)" 166 | 167 | # Find all .git dirs, up to DEPTH levels deep. If DEPTH is 0, the scan is 168 | # infinitely deep 169 | FIND_OPTS="" 170 | if [ "$DEPTH" -ne 0 ]; then 171 | FIND_OPTS="$FIND_OPTS -maxdepth $DEPTH" 172 | fi 173 | if [ "$NO_DEPTH" -eq 1 ]; then 174 | # Do not recurse at all. Really, this should have been the '-d 0' option, 175 | # but that's already used for infinite recursion, and we don't want to 176 | # break backwards compatibility. 177 | FIND_OPTS="$FIND_OPTS -maxdepth 0" 178 | fi 179 | 180 | 181 | # Go through positional arguments (DIRs) or '.' if no argumnets are given 182 | for DIR in "${@:-"."}"; do 183 | # We *want* to expand parameters, so disable shellcheck for this error: 184 | # shellcheck disable=SC2086 185 | find -L "$DIR" $FIND_OPTS -type d | while read -r PROJ_DIR 186 | do 187 | GIT_DIR="$PROJ_DIR/.git" 188 | GIT_CONF="$PROJ_DIR/.git/config" 189 | 190 | # Check if the repo is safe (https://github.blog/2022-04-12-git-security-vulnerability-announced/) 191 | if [ -d "$GIT_DIR" ]; then 192 | GIT_DIR_OWNER="$(ls -ld "$GIT_DIR" | awk 'NR==1 {print $3}')" 193 | if [ "$ID" != "$GIT_DIR_OWNER" ]; then 194 | printf "${PROJ_DIR}: ${C_UNSAFE}Unsafe ownership, owned by someone else. Skipping.${C_RESET}\n" 195 | continue 196 | fi 197 | fi 198 | 199 | # Check git config for this project to see if we should ignore this repo. 200 | IGNORE=$(git config -f "$GIT_CONF" --bool mgitstatus.ignore) 201 | if [ "$IGNORE" = "true" ]; then 202 | continue 203 | fi 204 | 205 | # If this dir is not a repo, and WARN_NOT_REPO is 1, tell the user. 206 | if [ ! -d "$GIT_DIR" ]; then 207 | if [ "$WARN_NOT_REPO" -eq 1 ] && [ "$PROJ_DIR" != "." ]; then 208 | printf "${PROJ_DIR}: not a git repo\n" 209 | fi 210 | continue 211 | fi 212 | 213 | [ $DEBUG -eq 1 ] && echo "${PROJ_DIR}" 214 | 215 | # Check if repo is locked 216 | if [ -f "$GIT_DIR/index.lock" ]; then 217 | printf "${PROJ_DIR}: ${C_LOCKED}Locked. Skipping.${C_RESET}\n" 218 | continue 219 | fi 220 | 221 | # Do a 'git fetch' if requested 222 | if [ "$DO_FETCH" -eq 1 ]; then 223 | git --work-tree "$(dirname "$GIT_DIR")" --git-dir "$GIT_DIR" fetch -q >/dev/null 224 | fi 225 | 226 | # Refresh the index, or we might get wrong results. 227 | git --work-tree "$(dirname "$GIT_DIR")" --git-dir "$GIT_DIR" update-index -q --refresh >/dev/null 2>&1 228 | 229 | # Get current branch, if "-b" is specified 230 | if [ "$SHOW_CUR_BRANCH" -eq 1 ]; then 231 | CUR_BRANCH=" ($(git --git-dir "$GIT_DIR" rev-parse --abbrev-ref HEAD))" 232 | else 233 | CUR_BRANCH="" 234 | fi 235 | 236 | # Find all remote branches that have been checked out and figure out if 237 | # they need a push or pull. We do this with various tests and put the name 238 | # of the branches in NEEDS_XXXX, seperated by newlines. After we're done, 239 | # we remove duplicates from NEEDS_XXX. 240 | NEEDS_PUSH_BRANCHES="" 241 | NEEDS_PULL_BRANCHES="" 242 | NEEDS_UPSTREAM_BRANCHES="" 243 | 244 | for REF_HEAD in $(cd "$GIT_DIR/refs/heads" && find . -type 'f' | sed "s/^\.\///"); do 245 | # Check if this branch is tracking an upstream (local/remote branch) 246 | UPSTREAM=$(git --git-dir "$GIT_DIR" rev-parse --abbrev-ref --symbolic-full-name "$REF_HEAD@{u}" 2>/dev/null) 247 | EXIT_CODE="$?" 248 | if [ "$EXIT_CODE" -eq 0 ]; then 249 | # Branch is tracking a remote branch. Find out how much behind / 250 | # ahead it is of that remote branch. 251 | CNT_AHEAD_BEHIND=$(git --git-dir "$GIT_DIR" rev-list --left-right --count "$REF_HEAD...$UPSTREAM") 252 | CNT_AHEAD=$(echo "$CNT_AHEAD_BEHIND" | awk '{ print $1 }') 253 | CNT_BEHIND=$(echo "$CNT_AHEAD_BEHIND" | awk '{ print $2 }') 254 | 255 | [ $DEBUG -eq 1 ] && echo "CNT_AHEAD_BEHIND: $CNT_AHEAD_BEHIND" 256 | [ $DEBUG -eq 1 ] && echo "CNT_AHEAD: $CNT_AHEAD" 257 | [ $DEBUG -eq 1 ] && echo "CNT_BEHIND: $CNT_BEHIND" 258 | 259 | if [ "$CNT_AHEAD" -gt 0 ]; then 260 | NEEDS_PUSH_BRANCHES="${NEEDS_PUSH_BRANCHES}\n$REF_HEAD" 261 | fi 262 | if [ "$CNT_BEHIND" -gt 0 ]; then 263 | NEEDS_PULL_BRANCHES="${NEEDS_PULL_BRANCHES}\n$REF_HEAD" 264 | fi 265 | 266 | # Check if this branch is a branch off another branch. and if it needs 267 | # to be updated. 268 | REV_LOCAL=$(git --git-dir "$GIT_DIR" rev-parse --verify "$REF_HEAD" 2>/dev/null) 269 | REV_REMOTE=$(git --git-dir "$GIT_DIR" rev-parse --verify "$UPSTREAM" 2>/dev/null) 270 | REV_BASE=$(git --git-dir "$GIT_DIR" merge-base "$REF_HEAD" "$UPSTREAM" 2>/dev/null) 271 | 272 | [ $DEBUG -eq 1 ] && echo "REV_LOCAL: $REV_LOCAL" 273 | [ $DEBUG -eq 1 ] && echo "REV_REMOTE: $REV_REMOTE" 274 | [ $DEBUG -eq 1 ] && echo "REV_BASE: $REV_BASE" 275 | 276 | if [ "$REV_LOCAL" = "$REV_REMOTE" ]; then 277 | : # NOOP 278 | else 279 | if [ "$REV_LOCAL" = "$REV_BASE" ]; then 280 | NEEDS_PULL_BRANCHES="${NEEDS_PULL_BRANCHES}\n$REF_HEAD" 281 | fi 282 | if [ "$REV_REMOTE" = "$REV_BASE" ]; then 283 | NEEDS_PUSH_BRANCHES="${NEEDS_PUSH_BRANCHES}\n$REF_HEAD" 284 | fi 285 | fi 286 | else 287 | # Branch does not have an upstream (local/remote branch). 288 | NEEDS_UPSTREAM_BRANCHES="${NEEDS_UPSTREAM_BRANCHES}\n$REF_HEAD" 289 | fi 290 | done 291 | 292 | # Remove duplicates from NEEDS_XXXX and make comma-seperated 293 | NEEDS_PUSH_BRANCHES=$(printf "$NEEDS_PUSH_BRANCHES" | sort | uniq | tr '\n' ',' | sed "s/^,\(.*\),$/\1/") 294 | NEEDS_PULL_BRANCHES=$(printf "$NEEDS_PULL_BRANCHES" | sort | uniq | tr '\n' ',' | sed "s/^,\(.*\),$/\1/") 295 | NEEDS_UPSTREAM_BRANCHES=$(printf "$NEEDS_UPSTREAM_BRANCHES" | sort | uniq | tr '\n' ',' | sed "s/^,\(.*\),$/\1/") 296 | 297 | # Find out if there are unstaged, uncommitted or untracked changes 298 | UNSTAGED=$(git --work-tree "$(dirname "$GIT_DIR")" --git-dir "$GIT_DIR" diff-index --quiet HEAD -- 2>/dev/null; echo $?) 299 | UNCOMMITTED=$(git --work-tree "$(dirname "$GIT_DIR")" --git-dir "$GIT_DIR" diff-files --quiet --ignore-submodules --; echo $?) 300 | UNTRACKED=$(git --work-tree "$(dirname "$GIT_DIR")" --git-dir "$GIT_DIR" ls-files --exclude-standard --others) 301 | cd "$(dirname "$GIT_DIR")" || exit 302 | STASHES=$(git stash list | wc -l) 303 | cd "$OLDPWD" || exit 304 | 305 | [ $DEBUG -eq 1 ] && echo "UNSTAGED: $UNSTAGED" 306 | [ $DEBUG -eq 1 ] && echo "UNCOMMITTED: $UNCOMMITTED" 307 | [ $DEBUG -eq 1 ] && echo "UNTRACKED: $UNTRACKED" 308 | [ $DEBUG -eq 1 ] && echo "STASHES: $STASHES" 309 | 310 | # Build up the status string if not flattening. Otherwise, print 311 | # results immediately. 312 | IS_OK=0 # 0 = Repo needs something, 1 = Repo needs nothing ('ok') 313 | STATUS_NEEDS="" 314 | if [ -n "$NEEDS_PUSH_BRANCHES" ] && [ "$NO_PUSH" -eq 0 ]; then 315 | THIS_STATUS="${C_NEEDS_PUSH}Needs push ($NEEDS_PUSH_BRANCHES)${C_RESET}" 316 | STATUS_NEEDS="${STATUS_NEEDS}${THIS_STATUS} " 317 | [ "$FLATTEN" -eq 1 ] && printf "${PROJ_DIR}$CUR_BRANCH: $THIS_STATUS\n" 318 | fi 319 | if [ -n "$NEEDS_PULL_BRANCHES" ] && [ "$NO_PULL" -eq 0 ]; then 320 | THIS_STATUS="${C_NEEDS_PULL}Needs pull ($NEEDS_PULL_BRANCHES)${C_RESET}" 321 | STATUS_NEEDS="${STATUS_NEEDS}${THIS_STATUS} " 322 | [ "$FLATTEN" -eq 1 ] && printf "${PROJ_DIR}$CUR_BRANCH: $THIS_STATUS\n" 323 | fi 324 | if [ -n "$NEEDS_UPSTREAM_BRANCHES" ] && [ "$NO_UPSTREAM" -eq 0 ]; then 325 | THIS_STATUS="${C_NEEDS_UPSTREAM}Needs upstream ($NEEDS_UPSTREAM_BRANCHES)${C_RESET}" 326 | STATUS_NEEDS="${STATUS_NEEDS}${THIS_STATUS} " 327 | [ "$FLATTEN" -eq 1 ] && printf "${PROJ_DIR}$CUR_BRANCH: $THIS_STATUS\n" 328 | fi 329 | if [ "$UNSTAGED" -ne 0 ] || [ "$UNCOMMITTED" -ne 0 ] && [ "$NO_UNCOMMITTED" -eq 0 ]; then 330 | THIS_STATUS="${C_NEEDS_COMMIT}Uncommitted changes${C_RESET}" 331 | STATUS_NEEDS="${STATUS_NEEDS}${THIS_STATUS} " 332 | [ "$FLATTEN" -eq 1 ] && printf "${PROJ_DIR}$CUR_BRANCH: $THIS_STATUS\n" 333 | fi 334 | if [ "$UNTRACKED" != "" ] && [ "$NO_UNTRACKED" -eq 0 ]; then 335 | THIS_STATUS="${C_UNTRACKED}Untracked files${C_RESET}" 336 | STATUS_NEEDS="${STATUS_NEEDS}${THIS_STATUS} " 337 | [ "$FLATTEN" -eq 1 ] && printf "${PROJ_DIR}$CUR_BRANCH: $THIS_STATUS\n" 338 | fi 339 | if [ "$STASHES" -ne 0 ] && [ "$NO_STASHES" -eq 0 ]; then 340 | THIS_STATUS="${C_STASHES}$STASHES stashes${C_RESET}" 341 | STATUS_NEEDS="${STATUS_NEEDS}${THIS_STATUS} " 342 | [ "$FLATTEN" -eq 1 ] && printf "${PROJ_DIR}$CUR_BRANCH: $THIS_STATUS\n" 343 | fi 344 | if [ "$STATUS_NEEDS" = "" ]; then 345 | IS_OK=1 346 | THIS_STATUS="${C_OK}ok${C_RESET}" 347 | STATUS_NEEDS="${STATUS_NEEDS}${THIS_STATUS} " 348 | [ "$FLATTEN" -eq 1 ] && [ "$EXCLUDE_OK" -ne 1 ] && printf "${PROJ_DIR}$CUR_BRANCH: $THIS_STATUS\n" 349 | fi 350 | 351 | if [ "$FLATTEN" -ne 1 ]; then 352 | # Print the output, unless repo is 'ok' and -e was specified 353 | if [ "$IS_OK" -ne 1 ] || [ "$EXCLUDE_OK" -ne 1 ]; then 354 | printf "${PROJ_DIR}$CUR_BRANCH: $STATUS_NEEDS\n" 355 | fi 356 | fi 357 | 358 | # Throttle if requested 359 | if [ "$DO_FETCH" -eq 1 ] && [ "$THROTTLE" -ne 0 ]; then 360 | sleep "$THROTTLE" 361 | fi 362 | done 363 | done 364 | --------------------------------------------------------------------------------