-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathgit-status-all.py
160 lines (142 loc) · 4.67 KB
/
git-status-all.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python
# file: git-check-all.py
# vim:fileencoding=utf-8:ft=python
#
# Copyright © 2022 R.F. Smith <[email protected]>.
# SPDX-License-Identifier: MIT
# Created: 2022-01-22T17:36:02+0100
# Last modified: 2025-01-19T12:28:00+0100
"""
Run ``git status`` on all the user's git repositories under the current
working directory.
Report repositories that have uncommitted changes or that are ahead of their remote.
"""
import argparse
import os
import subprocess as sp
import sys
import logging
import concurrent.futures as cf
from signal import signal, SIGPIPE, SIG_DFL
# Ignore BrokenPipeError
signal(SIGPIPE, SIG_DFL)
def main():
"""
Entry point of git-status-all.
"""
args = setup()
if not args.directories:
args.directories = [""]
cwd = os.getcwd() + os.sep
args.directories = [
d if d.startswith(os.sep) else cwd + d for d in args.directories
]
exec = cf.ThreadPoolExecutor()
flist = []
for d in args.directories:
for (dirpath, dirnames, filenames) in os.walk(d):
if any(w in dirpath for w in args.ignore):
continue
# Do not check archived or external repos.
if "attic" in dirpath or "github" in dirpath or "gitlab" in dirpath:
continue
if ".git" in dirnames:
flist.append(exec.submit(runstatus, dirpath, args.verbose))
for fut in cf.as_completed(flist):
rv = fut.result()
if rv:
print(rv)
def setup():
"""Parse command-line arguments. Check for required programs."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--log",
default="info",
choices=["debug", "info", "warning", "error"],
help="logging level (defaults to 'info')",
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="also report on repos that are OK"
)
parser.add_argument(
"-i",
"--ignore",
action="append",
default=[],
help="directories that contain IGNORE are ignored (can be use multiple times)",
)
parser.add_argument(
"directories", nargs="*", help="one or more directories to process"
)
args = parser.parse_args(sys.argv[1:])
logging.basicConfig(
level=getattr(logging, args.log.upper(), None),
format="%(levelname)s: %(message)s",
)
logging.debug(f"Command line arguments = {sys.argv}")
logging.debug(f"Parsed arguments = {args}")
# Check for required programs.
try:
sp.run(["git"], stdout=sp.DEVNULL, stderr=sp.DEVNULL)
logging.debug("found “git”")
except FileNotFoundError:
logging.error("the program “git” cannot be found")
sys.exit(1)
return args
def runstatus(d, verbose=False):
"""
Run git status in the specified directory.
Report status and if branch is ahead of remotes.
Arguments:
d: Directory to run the checks in.
verbose: Boolean to enable verbose messages.
Returns:
String containing the status of the directory.
"""
os.chdir(d)
home = os.environ["HOME"]
idx = d.index(home) + len(home)
d = '~' + d[idx:]
outp = gitcmd("status", True)
notclean = ""
if b"working tree clean" not in outp:
notclean = "\033[31mnot clean\033[0m"
if b"is ahead of" in outp:
if notclean:
notclean = "\033[31mnot clean\033[0m, \033[35mahead of remote branch\033[0m"
else:
notclean = "\033[35mahead of remote branch\033[0m"
if notclean:
return f"'{d}' is {notclean}."
elif verbose:
return f"'{d}' is \033[32mOK\033[0m."
else:
return ""
def gitcmd(cmds, output=False):
"""
Run the specified git subcommand.
Arguments:
cmds: command string or list of strings of command and arguments.
output: flag to specify if the output should be captured and
returned, or just the return value.
Returns:
The return value or the output of the git command.
"""
if isinstance(cmds, str):
if " " in cmds:
raise ValueError("No spaces in single command allowed.")
cmds = [cmds]
else:
if not isinstance(cmds, (list, tuple)):
raise ValueError("args should be a list or tuple")
if not all(isinstance(x, str) for x in cmds):
raise ValueError("args should be a list or tuple of strings")
cmds = ["git"] + cmds
if output:
cp = sp.run(cmds, stdout=sp.PIPE, stderr=sp.STDOUT)
return cp.stdout
else:
cp = sp.run(cmds, stdout=sp.DEVNULL, stderr=sp.DEVNULL)
return cp.returncode
if __name__ == "__main__":
main()