housebroken-cli 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/PKG-INFO +13 -11
  2. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/README.md +10 -8
  3. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/bin/housebroken +4 -2
  4. housebroken_cli-0.2.0/hatch_build.py +24 -0
  5. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/pyproject.toml +6 -2
  6. housebroken_cli-0.2.0/scripts/comment-census.sh +171 -0
  7. housebroken_cli-0.2.0/scripts/diff-defaults.sh +222 -0
  8. housebroken_cli-0.2.0/scripts/notes.sh +83 -0
  9. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/pr-sweep.sh +71 -8
  10. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/skills/housebroken/SKILL.md +18 -2
  11. housebroken_cli-0.2.0/src/housebroken/__init__.py +1 -0
  12. housebroken_cli-0.1.0/scripts/comment-census.sh +0 -83
  13. housebroken_cli-0.1.0/src/housebroken/__init__.py +0 -1
  14. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/.gitignore +0 -0
  15. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/LICENSE +0 -0
  16. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/check-ai-policy.sh +0 -0
  17. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/distinct-outside.sh +0 -0
  18. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/file-pr.sh +0 -0
  19. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/fork-hygiene.sh +0 -0
  20. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/prior-art.sh +0 -0
  21. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/verify-filed-pr.sh +0 -0
  22. {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/src/housebroken/cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: housebroken-cli
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Agent-written pull requests that do not make a mess in someone else's house.
5
5
  Project-URL: Repository, https://github.com/lenamonj/housebroken
6
6
  License-Expression: MIT
@@ -11,7 +11,7 @@ Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
 
13
13
  <p align="center">
14
- <img src="assets/door.jpg" width="560" alt="A white ceramic robot dog sits at an open, lit door, a scroll in its mouth, its paws stopped at a glowing amber line on the threshold">
14
+ <img src="https://raw.githubusercontent.com/lenamonj/housebroken/main/assets/door.jpg" width="560" alt="A white ceramic robot dog sits at an open, lit door, a scroll in its mouth, its paws stopped at a glowing amber line on the threshold">
15
15
  </p>
16
16
 
17
17
  <h1 align="center">housebroken</h1>
@@ -28,13 +28,13 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  <p align="center">
30
30
  <img src="https://img.shields.io/badge/gates-12-E8A23B?style=flat-square" alt="12 gates">
31
- <img src="https://img.shields.io/badge/merged%20upstream-24%20PRs%20in%2021%20projects-E8A23B?style=flat-square" alt="24 merged pull requests across 21 projects">
31
+ <img src="https://img.shields.io/badge/merged%20upstream-29%20PRs%20in%2023%20projects-E8A23B?style=flat-square" alt="29 merged pull requests across 23 projects">
32
32
  <img src="https://img.shields.io/badge/closures%20turned%20into%20gates-5-E8A23B?style=flat-square" alt="5 closures turned into gates">
33
33
  <img src="https://img.shields.io/badge/shellcheck-clean-111111?style=flat-square" alt="shellcheck clean">
34
34
  </p>
35
35
 
36
36
  <p align="center">
37
- <strong>24 merged upstream &middot; 21 projects &middot; 5 closures, each one now a gate</strong><br>
37
+ <strong>29 merged upstream &middot; 23 projects &middot; 5 closures, each one now a gate</strong><br>
38
38
  <sub>Derived from GitHub on 7 September 2026: one account, every patch written by an agent, every filing governed by these rules as they were learned. Apple, Microsoft, Google, Apache and JetBrains are among the mergers. <a href="#numbers">The numbers</a> &middot; <a href="#how-it-works">the twelve gates</a>.</sub>
39
39
  </p>
40
40
 
@@ -68,9 +68,9 @@ Every rule here was learned on real repositories with real maintainers. Between
68
68
 
69
69
  | | count |
70
70
  |---|--:|
71
- | pull requests filed | 102 |
72
- | merged | 24, across 21 projects |
73
- | open, waiting on a maintainer | 73 |
71
+ | pull requests filed | 115 |
72
+ | merged | 29, across 23 projects |
73
+ | open, waiting on a maintainer | 81 |
74
74
  | closed without merging | 5 |
75
75
 
76
76
  The merged patches include ones accepted by Apple, Microsoft, Google, Apache, JetBrains, and the URL parser that Node.js ships. The fastest merge came twelve minutes after filing. Several came the same day.
@@ -79,9 +79,9 @@ The five closures matter more than the merges. Each became a gate. Two of the fi
79
79
 
80
80
  ## How it works
81
81
 
82
- <img align="right" width="230" src="assets/treatise.jpg" alt="An obsidian book with chrome corners titled The Housebroken Agent, a treatise on manners for machines calling at the homes of maintainers">
82
+ <img align="right" width="230" src="https://raw.githubusercontent.com/lenamonj/housebroken/main/assets/treatise.jpg" alt="An obsidian book with chrome corners titled The Housebroken Agent, a treatise on manners for machines calling at the homes of maintainers">
83
83
 
84
- A pull request passes through the door in order. Each step is a script or a rule, and each names the closure that put it there.
84
+ A pull request passes through the door in order. Each step is a script or a rule, and each names the closure that put it there. The verdicts themselves, in the maintainers' words, are in [docs/lessons.md](https://github.com/lenamonj/housebroken/blob/main/docs/lessons.md).
85
85
 
86
86
  **1. Read the house rules before knocking.**
87
87
  `check-ai-policy.sh` reads the repository's contribution policy on its development branch and in the organization's `.github` repository, and prints the sentence, not a verdict. Some projects ask contributors not to use AI for pull request text; those are never filed. Some accept pull requests only for issues they have labelled; those get an issue with the fix offered.
@@ -122,10 +122,10 @@ A repository gets at most three pull requests, each one finding, each meeting ev
122
122
 
123
123
  ## Install
124
124
 
125
- You need `bash`, `gh` (signed in) and `jq`. Then:
125
+ You need `bash`, `gh` (signed in) and `jq`. Then, from PyPI; `pipx install housebroken-cli` and `uv tool install housebroken-cli` work the same way:
126
126
 
127
127
  ```
128
- uv tool install housebroken-cli
128
+ pip install housebroken-cli
129
129
  housebroken install-skill
130
130
  ```
131
131
 
@@ -145,9 +145,11 @@ uv tool install git+https://github.com/lenamonj/housebroken
145
145
  | `scripts/distinct-outside.sh` | outside contributors merged in 120 days |
146
146
  | `scripts/prior-art.sh` | issues and pull requests on the touched files, type stated, rulings quoted |
147
147
  | `scripts/comment-census.sh` | added code versus added comments, per branch |
148
+ | `scripts/diff-defaults.sh` | default arguments and one-line wrappers added to keep call sites untouched |
148
149
  | `scripts/file-pr.sh` | the only way a pull request gets filed |
149
150
  | `scripts/verify-filed-pr.sh` | the filed pull request is what was meant, and CI settled |
150
151
  | `scripts/pr-sweep.sh` | every open pull request where the ball is in your court |
152
+ | `scripts/notes.sh` | what this repository's maintainers have asked for before |
151
153
  | `scripts/fork-hygiene.sh` | branches deleted after merge or close, orphan forks listed |
152
154
 
153
155
  The scripts are bash and need `gh` and `jq`. They came out of one operator's workshop and some still carry that operator's assumptions; each script's header says what it assumes. Generalizing them is the current work.
@@ -16,13 +16,13 @@
16
16
 
17
17
  <p align="center">
18
18
  <img src="https://img.shields.io/badge/gates-12-E8A23B?style=flat-square" alt="12 gates">
19
- <img src="https://img.shields.io/badge/merged%20upstream-24%20PRs%20in%2021%20projects-E8A23B?style=flat-square" alt="24 merged pull requests across 21 projects">
19
+ <img src="https://img.shields.io/badge/merged%20upstream-29%20PRs%20in%2023%20projects-E8A23B?style=flat-square" alt="29 merged pull requests across 23 projects">
20
20
  <img src="https://img.shields.io/badge/closures%20turned%20into%20gates-5-E8A23B?style=flat-square" alt="5 closures turned into gates">
21
21
  <img src="https://img.shields.io/badge/shellcheck-clean-111111?style=flat-square" alt="shellcheck clean">
22
22
  </p>
23
23
 
24
24
  <p align="center">
25
- <strong>24 merged upstream &middot; 21 projects &middot; 5 closures, each one now a gate</strong><br>
25
+ <strong>29 merged upstream &middot; 23 projects &middot; 5 closures, each one now a gate</strong><br>
26
26
  <sub>Derived from GitHub on 7 September 2026: one account, every patch written by an agent, every filing governed by these rules as they were learned. Apple, Microsoft, Google, Apache and JetBrains are among the mergers. <a href="#numbers">The numbers</a> &middot; <a href="#how-it-works">the twelve gates</a>.</sub>
27
27
  </p>
28
28
 
@@ -56,9 +56,9 @@ Every rule here was learned on real repositories with real maintainers. Between
56
56
 
57
57
  | | count |
58
58
  |---|--:|
59
- | pull requests filed | 102 |
60
- | merged | 24, across 21 projects |
61
- | open, waiting on a maintainer | 73 |
59
+ | pull requests filed | 115 |
60
+ | merged | 29, across 23 projects |
61
+ | open, waiting on a maintainer | 81 |
62
62
  | closed without merging | 5 |
63
63
 
64
64
  The merged patches include ones accepted by Apple, Microsoft, Google, Apache, JetBrains, and the URL parser that Node.js ships. The fastest merge came twelve minutes after filing. Several came the same day.
@@ -69,7 +69,7 @@ The five closures matter more than the merges. Each became a gate. Two of the fi
69
69
 
70
70
  <img align="right" width="230" src="assets/treatise.jpg" alt="An obsidian book with chrome corners titled The Housebroken Agent, a treatise on manners for machines calling at the homes of maintainers">
71
71
 
72
- A pull request passes through the door in order. Each step is a script or a rule, and each names the closure that put it there.
72
+ A pull request passes through the door in order. Each step is a script or a rule, and each names the closure that put it there. The verdicts themselves, in the maintainers' words, are in [docs/lessons.md](docs/lessons.md).
73
73
 
74
74
  **1. Read the house rules before knocking.**
75
75
  `check-ai-policy.sh` reads the repository's contribution policy on its development branch and in the organization's `.github` repository, and prints the sentence, not a verdict. Some projects ask contributors not to use AI for pull request text; those are never filed. Some accept pull requests only for issues they have labelled; those get an issue with the fix offered.
@@ -110,10 +110,10 @@ A repository gets at most three pull requests, each one finding, each meeting ev
110
110
 
111
111
  ## Install
112
112
 
113
- You need `bash`, `gh` (signed in) and `jq`. Then:
113
+ You need `bash`, `gh` (signed in) and `jq`. Then, from PyPI; `pipx install housebroken-cli` and `uv tool install housebroken-cli` work the same way:
114
114
 
115
115
  ```
116
- uv tool install housebroken-cli
116
+ pip install housebroken-cli
117
117
  housebroken install-skill
118
118
  ```
119
119
 
@@ -133,9 +133,11 @@ uv tool install git+https://github.com/lenamonj/housebroken
133
133
  | `scripts/distinct-outside.sh` | outside contributors merged in 120 days |
134
134
  | `scripts/prior-art.sh` | issues and pull requests on the touched files, type stated, rulings quoted |
135
135
  | `scripts/comment-census.sh` | added code versus added comments, per branch |
136
+ | `scripts/diff-defaults.sh` | default arguments and one-line wrappers added to keep call sites untouched |
136
137
  | `scripts/file-pr.sh` | the only way a pull request gets filed |
137
138
  | `scripts/verify-filed-pr.sh` | the filed pull request is what was meant, and CI settled |
138
139
  | `scripts/pr-sweep.sh` | every open pull request where the ball is in your court |
140
+ | `scripts/notes.sh` | what this repository's maintainers have asked for before |
139
141
  | `scripts/fork-hygiene.sh` | branches deleted after merge or close, orphan forks listed |
140
142
 
141
143
  The scripts are bash and need `gh` and `jq`. They came out of one operator's workshop and some still carry that operator's assumptions; each script's header says what it assumes. Generalizing them is the current work.
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # housebroken - one command name for the eight gate scripts.
2
+ # housebroken - one command name for the nine gate scripts.
3
3
  #
4
4
  # The skill file, and any future adapter for another agent host, must be able
5
5
  # to say `housebroken prior-art owner/repo term` without knowing where the
@@ -13,7 +13,7 @@
13
13
  # housebroken version
14
14
  #
15
15
  # Environment:
16
- # HOUSEBROKEN_SCRIPTS directory holding the eight *.sh gate scripts. When
16
+ # HOUSEBROKEN_SCRIPTS directory holding the nine *.sh gate scripts. When
17
17
  # unset, the scripts directory next to this file
18
18
  # (../scripts), then this file's own directory, then
19
19
  # $HOUSEBROKEN_HOME/scripts are tried in that order.
@@ -51,6 +51,7 @@ usage: housebroken <subcommand> [args...]
51
51
  9 file the only way a pull request gets filed
52
52
  10 verify the filed pull request is what was meant, and CI settled
53
53
  10 sweep every open pull request where the ball is in your court
54
+ 10 notes what this repository's maintainers have asked for before
54
55
  11 hygiene delete fork branches after merge or close, list orphan forks
55
56
 
56
57
  help this list
@@ -90,6 +91,7 @@ case "$cmd" in
90
91
  file) script=file-pr.sh ;;
91
92
  verify) script=verify-filed-pr.sh ;;
92
93
  sweep) script=pr-sweep.sh ;;
94
+ notes) script=notes.sh ;;
93
95
  hygiene) script=fork-hygiene.sh ;;
94
96
  *)
95
97
  echo "housebroken: unknown subcommand: $cmd (try: housebroken help)" >&2
@@ -0,0 +1,24 @@
1
+ """Rewrite the README for PyPI at build time.
2
+
3
+ PyPI renders the project description from README.md but cannot resolve
4
+ relative paths, so images under assets/ and links to files in the repository
5
+ render broken there. This hook turns every relative src, srcset and markdown
6
+ link into an absolute GitHub URL in the built metadata only; the README in the
7
+ repository stays relative, which is what GitHub wants.
8
+ """
9
+ import os
10
+ import re
11
+
12
+ from hatchling.metadata.plugin.interface import MetadataHookInterface
13
+
14
+
15
+ class ReadmeHook(MetadataHookInterface):
16
+ def update(self, metadata):
17
+ repo = self.config["repo"]
18
+ raw = f"https://raw.githubusercontent.com/{repo}/main/"
19
+ blob = f"https://github.com/{repo}/blob/main/"
20
+ with open(os.path.join(self.root, "README.md"), encoding="utf-8") as f:
21
+ text = f.read()
22
+ text = re.sub(r'((?:src|srcset)=")(?!https?://)([^"]+)"', lambda m: f'{m.group(1)}{raw}{m.group(2)}"', text)
23
+ text = re.sub(r'\]\((?!https?://|#|mailto:)([^)\s]+)\)', lambda m: f"]({blob}{m.group(1)})", text)
24
+ metadata["readme"] = {"content-type": "text/markdown", "text": text}
@@ -4,9 +4,9 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "housebroken-cli"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Agent-written pull requests that do not make a mess in someone else's house."
9
- readme = "README.md"
9
+ dynamic = ["readme"]
10
10
  license = "MIT"
11
11
  requires-python = ">=3.10"
12
12
  dependencies = []
@@ -38,3 +38,7 @@ include = [
38
38
  "README.md",
39
39
  "LICENSE",
40
40
  ]
41
+
42
+ [tool.hatch.metadata.hooks.custom]
43
+ path = "hatch_build.py"
44
+ repo = "lenamonj/housebroken"
@@ -0,0 +1,171 @@
1
+ #!/bin/bash
2
+ # comment-census.sh - added code lines against added comment lines, per branch,
3
+ # plus a neighbourhood check on every added comment.
4
+ #
5
+ # A patch that carries a paragraph of comments into a file that has none reads
6
+ # as machine-written and gets closed. This counts, for every branch in every
7
+ # clone under a directory, the added non-comment lines and the added comment
8
+ # lines, so a pull request can be made to match the comment density of the file
9
+ # it lands in. Three maintainers asked for exactly this, in three separate
10
+ # reviews, before it became a script.
11
+ #
12
+ # A fourth asked for more. On google/benchmark #2294 the maintainer called a
13
+ # single comment above a two-line fix "unnecessary comment" in a file whose
14
+ # overall ratio was healthy: he judged the comment against the function around
15
+ # it, not against the file. So every added comment line is also checked against
16
+ # its neighbourhood - the 20 lines above and the 20 lines below it on the
17
+ # branch, excluding the added lines themselves. If none of those lines is a
18
+ # comment, the added comment is flagged whatever the file ratio says. A shebang
19
+ # or a licence header in the first 15 lines does not rescue a comment added
20
+ # further down.
21
+ #
22
+ # Usage:
23
+ # bash comment-census.sh [CLONE_ROOT]
24
+ # bash comment-census.sh --help
25
+ #
26
+ # CLONE_ROOT is any directory whose immediate subdirectories are git clones.
27
+ # Defaults to $HOUSEBROKEN_CLONES, then to $HOUSEBROKEN_HOME/clones.
28
+ #
29
+ # Environment:
30
+ # HOUSEBROKEN_CLONES default clone root
31
+ # HOUSEBROKEN_HOME workshop root, default $HOME/.housebroken
32
+ #
33
+ # Assumes: each clone has an origin remote whose default branch is discoverable
34
+ # (origin/HEAD, else origin/main, else origin/master); every other local branch
35
+ # is a candidate patch. Tests, markdown and lock files are excluded from the
36
+ # code and comment counts. Comment detection covers //, /* */, * and #.
37
+ # Neighbourhood detection picks the comment family from the file extension:
38
+ # //, /* */ for .c .cc .cpp .h .hpp .go .rs .java .kt .swift .ts .js, # for
39
+ # .py .rb .sh .toml .yaml .yml, and ; for .asm .s .lisp .el .clj. A file with
40
+ # any other extension gets no neighbourhood check.
41
+ set -u
42
+
43
+ usage() {
44
+ sed -n '2,/^set -u$/p' "$0" | sed 's/^# \{0,1\}//; /^set -u$/d'
45
+ }
46
+
47
+ case "${1:-}" in
48
+ --help|-h) usage; exit 0 ;;
49
+ esac
50
+
51
+ HOUSEBROKEN_HOME="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
52
+ root="${1:-${HOUSEBROKEN_CLONES:-$HOUSEBROKEN_HOME/clones}}"
53
+
54
+ if [ ! -d "$root" ]; then
55
+ echo "comment-census.sh: not a directory: $root" >&2
56
+ echo "give a clone root as an argument or set HOUSEBROKEN_CLONES" >&2
57
+ exit 1
58
+ fi
59
+
60
+ paths=(. ':(exclude)*test*' ':(exclude)*Test*' ':(exclude)*_test.go' ':(exclude)Tests/*' ':(exclude)*.md' ':(exclude)*.sum')
61
+
62
+ # Comment family for an extension, as an ERE. Returns 1 for unknown extensions.
63
+ comment_re() {
64
+ case "$1" in
65
+ c|cc|cpp|h|hpp|go|rs|java|kt|swift|ts|js) printf '%s' '^[[:space:]]*(//|/[*]|[*]|[*]/)' ;;
66
+ py|rb|sh|toml|yaml|yml) printf '%s' '^[[:space:]]*#' ;;
67
+ asm|s|lisp|el|clj) printf '%s' '^[[:space:]]*;' ;;
68
+ *) return 1 ;;
69
+ esac
70
+ }
71
+
72
+ flagfile=$(mktemp) || exit 1
73
+ trap 'rm -f "$flagfile"' EXIT
74
+
75
+ flag_scan() {
76
+ local d="$1" base="$2" br="$3" name="$4"
77
+ local map content addedset cur row f ln text e re lo hi body
78
+ local rows=()
79
+ map=$(mktemp) || return 1
80
+ content=$(mktemp) || { rm -f "$map"; return 1; }
81
+ addedset=$(mktemp) || { rm -f "$map" "$content"; return 1; }
82
+
83
+ git -C "$d" diff --unified=0 "$base..$br" -- "${paths[@]}" | awk '
84
+ /^\+\+\+ / { file=""; if (substr($0,1,6) == "+++ b/") file=substr($0,7); next }
85
+ /^--- / { next }
86
+ /^@@ / { split($3, a, ","); ln=substr(a[1],2)+0; next }
87
+ /^\+/ { if (file != "") { print file "\t" ln "\t" substr($0,2); ln++ } next }
88
+ /^ / { ln++ }
89
+ ' > "$map"
90
+
91
+ mapfile -t rows < "$map"
92
+ cur=""
93
+ for row in ${rows[@]+"${rows[@]}"}; do
94
+ IFS=$'\t' read -r f ln text <<< "$row"
95
+ e="${f##*.}"
96
+ re=$(comment_re "$e") || continue
97
+ printf '%s\n' "$text" | grep -qE "$re" || continue
98
+ if [ "$f" != "$cur" ]; then
99
+ cur="$f"
100
+ git -C "$d" show "$br:$f" > "$content" 2>/dev/null || : > "$content"
101
+ awk -F'\t' -v want="$f" '$1 == want { print $2 }' "$map" > "$addedset"
102
+ fi
103
+ lo=$((ln - 20))
104
+ [ "$lo" -lt 1 ] && lo=1
105
+ hi=$((ln + 20))
106
+ body=0
107
+ [ "$ln" -gt 15 ] && body=1
108
+ if ! awk -v lo="$lo" -v hi="$hi" -v body="$body" -v self="$ln" -v re="$re" -v af="$addedset" '
109
+ BEGIN { found=0; while ((getline a < af) > 0) added[a+0]=1 }
110
+ NR < lo { next }
111
+ NR > hi { exit }
112
+ NR == self { next }
113
+ added[NR] { next }
114
+ body == 1 && NR <= 15 { next }
115
+ $0 ~ re { found=1; exit }
116
+ END { exit (found ? 0 : 1) }
117
+ ' "$content"; then
118
+ printf 'FLAG %s %s %s:%s %s\n' "$name" "$br" "$f" "$ln" "$text" >> "$flagfile"
119
+ fi
120
+ done
121
+
122
+ rm -f "$map" "$content" "$addedset"
123
+ }
124
+
125
+ printf '%-28s %-36s %5s %5s %5s\n' repo branch code cmnt tests
126
+
127
+ found=0
128
+ for d in "$root"/*/; do
129
+ [ -d "$d/.git" ] || continue
130
+ found=1
131
+ name=$(basename "$d")
132
+ base=$(git -C "$d" symbolic-ref -q --short refs/remotes/origin/HEAD || true)
133
+ if [ -z "$base" ]; then
134
+ if git -C "$d" rev-parse --verify -q origin/main >/dev/null; then
135
+ base=origin/main
136
+ elif git -C "$d" rev-parse --verify -q origin/master >/dev/null; then
137
+ base=origin/master
138
+ else
139
+ echo "comment-census.sh: no default branch in $d, skipped" >&2
140
+ continue
141
+ fi
142
+ fi
143
+ default_branch="${base#origin/}"
144
+ while read -r br; do
145
+ [ -n "$br" ] || continue
146
+ [ "$br" = "$default_branch" ] && continue
147
+ [ "$(git -C "$d" rev-list --count "$base..$br")" = "0" ] && continue
148
+ added=$(git -C "$d" diff "$base..$br" -- "${paths[@]}" | grep '^+' | grep -v '^+++' || true)
149
+ if [ -z "$added" ]; then
150
+ code=0; cmnt=0
151
+ else
152
+ code=$(printf '%s\n' "$added" | grep -v -E '^\+\s*(//|/\*|\*|#|///|\*/)' | grep -c -v -E '^\+\s*$' || true)
153
+ cmnt=$(printf '%s\n' "$added" | grep -c -E '^\+\s*(//|/\*|\*|///|\*/)' || true)
154
+ fi
155
+ tests=$(git -C "$d" diff "$base..$br" --numstat | awk '/[Tt]est/{s+=$1} END{print s+0}')
156
+ printf '%-28s %-36s %5s %5s %5s\n' "$name" "$br" "$code" "$cmnt" "$tests"
157
+ flag_scan "$d" "$base" "$br" "$name"
158
+ done < <(git -C "$d" for-each-ref --format='%(refname:short)' refs/heads/)
159
+ done
160
+
161
+ if [ "$found" = 0 ]; then
162
+ echo "comment-census.sh: no git clones found under $root" >&2
163
+ exit 1
164
+ fi
165
+
166
+ if [ -s "$flagfile" ]; then
167
+ echo
168
+ cat "$flagfile"
169
+ fi
170
+ echo
171
+ echo "$(grep -c '^FLAG ' "$flagfile" || true) comment lines added into comment-free neighbourhoods"
@@ -0,0 +1,222 @@
1
+ #!/bin/bash
2
+ # diff-defaults.sh - added default arguments and one-line forwarding wrappers.
3
+ #
4
+ # On google/benchmark #2294 the maintainer asked "why are there defaults here?"
5
+ # about two default arguments added so that ten existing call sites did not
6
+ # have to change. A default argument, an overload or a one-line wrapper that
7
+ # exists only to keep call sites untouched hides the change from the reader and
8
+ # leaves the old behaviour reachable. The fix is to touch the call sites. This
9
+ # script reads the added lines of a diff and reports both shapes so the author
10
+ # sees them before the maintainer does. It reports, it does not judge: the exit
11
+ # status is 0 whether or not anything was found.
12
+ #
13
+ # Usage:
14
+ # bash diff-defaults.sh [CLONE_ROOT]
15
+ # bash diff-defaults.sh --diff FILE
16
+ # bash diff-defaults.sh --diff -
17
+ # bash diff-defaults.sh --help
18
+ #
19
+ # CLONE_ROOT is any directory whose immediate subdirectories are git clones;
20
+ # every branch is diffed against its clone's default branch. With --diff, a
21
+ # unified diff is read from FILE, or from standard input when FILE is -.
22
+ #
23
+ # Environment:
24
+ # HOUSEBROKEN_CLONES default clone root
25
+ # HOUSEBROKEN_HOME workshop root, default $HOME/.housebroken
26
+ #
27
+ # Output is one line per hit, "file:line kind text", where kind is
28
+ # default-arg or forwarding, followed by a summary count.
29
+ #
30
+ # Languages: C, C++, Python, Kotlin, Swift, TypeScript and JavaScript, by
31
+ # extension (.c .cc .cpp .h .hpp .py .kt .swift .ts .js). Other files are
32
+ # ignored.
33
+ #
34
+ # Heuristic. Working one added line at a time, with no parser and no view of
35
+ # the surrounding function, the script calls a line a signature when it holds
36
+ # an assignment whose left side ends in an identifier and one of these is true:
37
+ # - the assignment sits between the line's first ( and its last ), and
38
+ # nothing after that ) opens another paren: void Run(int n, bool v = 0);
39
+ # - the line opens a parameter list it does not close, and the assignment is
40
+ # after the (
41
+ # - the line closes a parameter list it did not open, and the assignment is
42
+ # before the )
43
+ # - the trimmed line ends with a comma, the shape of a parameter continuation
44
+ # A forwarding wrapper is an added line that defines a name and calls that same
45
+ # name again on the same line, with a return, an =>, an = or a { between them.
46
+ #
47
+ # Known misses. False positives: a keyword or designated argument written with
48
+ # = inside a call, "foo(a, b = 5);"; a brace-initialiser or array element
49
+ # ending in a comma, though a leading . is skipped; a recursive one-line
50
+ # function, which on one line looks exactly like a forwarding wrapper. False
51
+ # negatives: a default on a signature split so that neither the identifier nor
52
+ # the = lands on an added line; a forwarding wrapper whose body is on the next
53
+ # line; a default introduced by a line the diff records as unchanged context;
54
+ # C++ default template arguments and Python keyword-only markers are not
55
+ # modelled.
56
+ set -u
57
+
58
+ usage() {
59
+ sed -n '2,/^set -u$/p' "$0" | sed 's/^# \{0,1\}//; /^set -u$/d'
60
+ }
61
+
62
+ mode="clones"
63
+ diff_src=""
64
+ case "${1:-}" in
65
+ --help|-h) usage; exit 0 ;;
66
+ --diff)
67
+ if [ $# -lt 2 ] || [ -z "$2" ]; then
68
+ echo "diff-defaults.sh: --diff needs a file or -" >&2
69
+ exit 2
70
+ fi
71
+ mode="diff"
72
+ diff_src="$2"
73
+ ;;
74
+ --*)
75
+ echo "diff-defaults.sh: unknown option: $1" >&2
76
+ exit 2
77
+ ;;
78
+ esac
79
+
80
+ analyzer=$(cat <<'AWK'
81
+ function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
82
+ function ext(f, n, a) { n = split(f, a, "."); return (n > 1) ? a[n] : "" }
83
+ function supported(e) {
84
+ return (e=="c" || e=="cc" || e=="cpp" || e=="h" || e=="hpp" || e=="py" ||
85
+ e=="kt" || e=="swift" || e=="ts" || e=="js")
86
+ }
87
+ function countch(s, c, i, n, k) { n=length(s); k=0; for (i=1;i<=n;i++) if (substr(s,i,1)==c) k++; return k }
88
+ function lastidx(s, c, i, n, k) { n=length(s); k=0; for (i=1;i<=n;i++) if (substr(s,i,1)==c) k=i; return k }
89
+ # position of the first plain assignment =, skipping ==, !=, +=, and friends
90
+ function eqpos(s, i, n, p) {
91
+ n = length(s)
92
+ for (i = 1; i <= n; i++) {
93
+ if (substr(s,i,1) != "=") continue
94
+ if (substr(s,i+1,1) == "=") continue
95
+ p = (i > 1) ? substr(s,i-1,1) : ""
96
+ if (p ~ /[=!<>+*\/%&|^~-]/) continue
97
+ return i
98
+ }
99
+ return 0
100
+ }
101
+ function is_default(t, s, eq, pre, oc, cc, fo, lc, tail) {
102
+ s = trim(t)
103
+ if (substr(s,1,1) == ".") return 0
104
+ eq = eqpos(s)
105
+ if (eq == 0) return 0
106
+ pre = trim(substr(s, 1, eq - 1))
107
+ if (pre !~ /[A-Za-z0-9_]$/) return 0
108
+ if (trim(substr(s, eq + 1)) == "") return 0
109
+ oc = countch(s, "("); cc = countch(s, ")")
110
+ fo = index(s, "("); lc = lastidx(s, ")")
111
+ if (fo > 0 && lc > fo && eq > fo && eq < lc) {
112
+ tail = substr(s, lc + 1)
113
+ if (index(tail, "(") == 0) return 1
114
+ }
115
+ if (oc > cc && fo > 0 && eq > fo) return 1
116
+ if (cc > oc && lc > 0 && eq < lc) return 1
117
+ if (substr(s, length(s), 1) == ",") return 1
118
+ return 0
119
+ }
120
+ function defname(s, fo, j, e) {
121
+ fo = index(s, "(")
122
+ if (fo == 0) return ""
123
+ j = fo - 1
124
+ while (j > 0 && (substr(s,j,1) == " " || substr(s,j,1) == "\t")) j--
125
+ e = j
126
+ while (j > 0 && substr(s,j,1) ~ /[A-Za-z0-9_]/) j--
127
+ return substr(s, j + 1, e - j)
128
+ }
129
+ function ncalls(s, name, i, n, L, p, j, k) {
130
+ n = length(s); L = length(name); k = 0
131
+ if (L == 0) return 0
132
+ for (i = 1; i <= n - L + 1; i++) {
133
+ if (substr(s,i,L) != name) continue
134
+ p = (i > 1) ? substr(s,i-1,1) : ""
135
+ if (p ~ /[A-Za-z0-9_]/) continue
136
+ j = i + L
137
+ while (substr(s,j,1) == " " || substr(s,j,1) == "\t") j++
138
+ if (substr(s,j,1) == "(") k++
139
+ }
140
+ return k
141
+ }
142
+ function is_forward(t, s, name) {
143
+ s = trim(t)
144
+ name = defname(s)
145
+ if (name == "") return 0
146
+ if (name ~ /^(if|for|while|switch|return|catch|def|fun|func|function|new|sizeof)$/) return 0
147
+ if (ncalls(s, name) < 2) return 0
148
+ if (s ~ /return/ || index(s, "=>") > 0 || index(s, "=") > 0 || index(s, "{") > 0) return 1
149
+ return 0
150
+ }
151
+ /^\+\+\+ / { file = ""; if (substr($0,1,6) == "+++ b/") file = substr($0,7); next }
152
+ /^--- / { next }
153
+ /^@@ / { split($3, a, ","); ln = substr(a[1],2) + 0; next }
154
+ /^\\/ { next }
155
+ /^\+/ {
156
+ if (file != "") {
157
+ t = substr($0, 2)
158
+ if (supported(ext(file))) {
159
+ if (is_forward(t)) print file ":" ln " forwarding " t
160
+ else if (is_default(t)) print file ":" ln " default-arg " t
161
+ }
162
+ ln++
163
+ }
164
+ next
165
+ }
166
+ /^-/ { next }
167
+ /^ / { ln++ }
168
+ AWK
169
+ )
170
+
171
+ hits=$(mktemp) || exit 1
172
+ trap 'rm -f "$hits"' EXIT
173
+
174
+ if [ "$mode" = diff ]; then
175
+ if [ "$diff_src" = "-" ]; then
176
+ awk "$analyzer" >> "$hits"
177
+ else
178
+ if [ ! -f "$diff_src" ]; then
179
+ echo "diff-defaults.sh: not a file: $diff_src" >&2
180
+ exit 2
181
+ fi
182
+ awk "$analyzer" "$diff_src" >> "$hits"
183
+ fi
184
+ else
185
+ HOUSEBROKEN_HOME="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
186
+ root="${1:-${HOUSEBROKEN_CLONES:-$HOUSEBROKEN_HOME/clones}}"
187
+ if [ ! -d "$root" ]; then
188
+ echo "diff-defaults.sh: not a directory: $root" >&2
189
+ echo "give a clone root as an argument or set HOUSEBROKEN_CLONES" >&2
190
+ exit 2
191
+ fi
192
+ found=0
193
+ for d in "$root"/*/; do
194
+ [ -d "$d/.git" ] || continue
195
+ found=1
196
+ base=$(git -C "$d" symbolic-ref -q --short refs/remotes/origin/HEAD || true)
197
+ if [ -z "$base" ]; then
198
+ if git -C "$d" rev-parse --verify -q origin/main >/dev/null; then
199
+ base=origin/main
200
+ elif git -C "$d" rev-parse --verify -q origin/master >/dev/null; then
201
+ base=origin/master
202
+ else
203
+ echo "diff-defaults.sh: no default branch in $d, skipped" >&2
204
+ continue
205
+ fi
206
+ fi
207
+ default_branch="${base#origin/}"
208
+ while read -r br; do
209
+ [ -n "$br" ] || continue
210
+ [ "$br" = "$default_branch" ] && continue
211
+ [ "$(git -C "$d" rev-list --count "$base..$br")" = "0" ] && continue
212
+ git -C "$d" diff --unified=0 "$base..$br" | awk "$analyzer" >> "$hits"
213
+ done < <(git -C "$d" for-each-ref --format='%(refname:short)' refs/heads/)
214
+ done
215
+ if [ "$found" = 0 ]; then
216
+ echo "diff-defaults.sh: no git clones found under $root" >&2
217
+ exit 2
218
+ fi
219
+ fi
220
+
221
+ cat "$hits"
222
+ echo "$(grep -c . "$hits" || true) default arguments or forwarding wrappers added"
@@ -0,0 +1,83 @@
1
+ #!/bin/bash
2
+ # notes.sh - what a repository's maintainers have asked for before.
3
+ #
4
+ # Preferences outlive one pull request. google/benchmark wants no comment and
5
+ # no default arguments; Kotlin/kotlinx-datetime wants the test in the JVM suite
6
+ # that compares against java.time; apple/swift-log wants the package's own Lock
7
+ # in tests; cloudflare/circl answers its review bots itself. A note is written
8
+ # once, when the maintainer says it, and read before the next pull request and
9
+ # before every rework.
10
+ #
11
+ # Usage:
12
+ # bash notes.sh owner/repo # print the notes for one repository
13
+ # bash notes.sh owner/repo add "<text>" # append a dated line
14
+ # bash notes.sh --list # repositories that have notes
15
+ # bash notes.sh --help
16
+ #
17
+ # Environment:
18
+ # HOUSEBROKEN_HOME workshop root, default $HOME/.housebroken. A repository's
19
+ # notes live in $HOUSEBROKEN_HOME/notes/<owner>-<repo>.md
20
+ #
21
+ # Assumes: bash. No network, no gh, no jq.
22
+ set -u
23
+
24
+ usage() {
25
+ sed -n '2,/^set -u$/p' "$0" | sed 's/^# \{0,1\}//; /^set -u$/d'
26
+ }
27
+
28
+ NOTES_DIR="${HOUSEBROKEN_HOME:-$HOME/.housebroken}/notes"
29
+
30
+ list_repos() {
31
+ found=0
32
+ for f in "$NOTES_DIR"/*.md; do
33
+ [ -f "$f" ] || continue
34
+ # the header line names the repository; the file name cannot, because an
35
+ # owner and a repository may both contain a hyphen
36
+ name="$(sed -n '1s/^# notes: //p' "$f")"
37
+ [ -n "$name" ] || name="$(basename "$f" .md)"
38
+ echo "$name"
39
+ found=1
40
+ done
41
+ [ "$found" = 1 ] || echo "no repositories have notes yet"
42
+ }
43
+
44
+ case "${1:-}" in
45
+ --help|-h) usage; exit 0 ;;
46
+ --list) list_repos; exit 0 ;;
47
+ "") echo "notes.sh: expected owner/repo, --list or --help" >&2; usage >&2; exit 2 ;;
48
+ esac
49
+
50
+ target="$1"
51
+ case "$target" in
52
+ */*/*|/*|*/) echo "notes.sh: expected owner/repo, got: $target" >&2; exit 2 ;;
53
+ */*) ;;
54
+ *) echo "notes.sh: expected owner/repo, got: $target" >&2; exit 2 ;;
55
+ esac
56
+ file="$NOTES_DIR/${target%%/*}-${target#*/}.md"
57
+ shift
58
+
59
+ if [ $# -eq 0 ]; then
60
+ if [ -f "$file" ]; then
61
+ cat "$file"
62
+ else
63
+ echo "no notes for $target"
64
+ fi
65
+ exit 0
66
+ fi
67
+
68
+ [ "$1" = "add" ] || { echo "notes.sh: unknown action: $1" >&2; usage >&2; exit 2; }
69
+ shift
70
+ text="${1:-}"
71
+ [ -n "$text" ] || { echo "notes.sh: add needs the note text" >&2; exit 2; }
72
+
73
+ if ! mkdir -p "$NOTES_DIR"; then
74
+ echo "notes.sh: cannot create $NOTES_DIR" >&2
75
+ exit 1
76
+ fi
77
+ if [ ! -f "$file" ]; then
78
+ printf '# notes: %s\n' "$target" > "$file" || {
79
+ echo "notes.sh: cannot write $file" >&2; exit 1; }
80
+ fi
81
+ printf -- '- %s: %s\n' "$(date -u +%Y-%m-%d)" "$text" >> "$file" || {
82
+ echo "notes.sh: cannot append to $file" >&2; exit 1; }
83
+ echo "noted for $target in $file"
@@ -7,13 +7,21 @@
7
7
  # changes, CI has a failing check, or the PR is CONFLICTING (needs a rebase).
8
8
  # Read-only; acting is the operator's.
9
9
  #
10
+ # A merge or a close drops a PR out of the open list without a word, so each
11
+ # run writes its open set to a state file and the next run reports what left
12
+ # it.
13
+ #
10
14
  # Usage:
11
- # bash pr-sweep.sh # prints only PRs where the ball is in our court
12
- # bash pr-sweep.sh --all # prints every open PR with its state
15
+ # bash pr-sweep.sh # only PRs where the ball is in our court
16
+ # bash pr-sweep.sh --all # every open PR with its state
17
+ # bash pr-sweep.sh --state <path> # a state file other than the default
13
18
  # bash pr-sweep.sh --help
14
19
  #
15
20
  # Environment:
16
21
  # HOUSEBROKEN_USER GitHub login to sweep, default the authenticated user
22
+ # HOUSEBROKEN_HOME workshop root, default $HOME/.housebroken. The state file
23
+ # is $HOUSEBROKEN_HOME/sweep-state.json unless --state
24
+ # says otherwise.
17
25
  #
18
26
  # Assumes: gh is installed and authenticated, and that the PRs of interest are
19
27
  # the ones authored by that login. Exits non-zero when gh fails or the search
@@ -25,12 +33,28 @@ usage() {
25
33
  }
26
34
 
27
35
  ALL=0
28
- case "${1:-}" in
29
- --help|-h) usage; exit 0 ;;
30
- --all) ALL=1 ;;
31
- "") ;;
32
- *) echo "pr-sweep.sh: unknown argument: $1" >&2; usage >&2; exit 2 ;;
33
- esac
36
+ STATE=""
37
+ while [ $# -gt 0 ]; do
38
+ case "$1" in
39
+ --help|-h) usage; exit 0 ;;
40
+ --all) ALL=1 ;;
41
+ --state)
42
+ shift
43
+ [ $# -gt 0 ] || { echo "pr-sweep.sh: --state needs a path" >&2; exit 2; }
44
+ STATE="$1"
45
+ ;;
46
+ *) echo "pr-sweep.sh: unknown argument: $1" >&2; usage >&2; exit 2 ;;
47
+ esac
48
+ shift
49
+ done
50
+
51
+ if [ -z "$STATE" ]; then
52
+ STATE="${HOUSEBROKEN_HOME:-$HOME/.housebroken}/sweep-state.json"
53
+ fi
54
+ if ! mkdir -p "$(dirname "$STATE")"; then
55
+ echo "pr-sweep.sh: cannot create the directory of $STATE" >&2
56
+ exit 1
57
+ fi
34
58
 
35
59
  ME="${HOUSEBROKEN_USER:-$(gh api user --jq .login)}"
36
60
  if [ -z "$ME" ]; then
@@ -44,6 +68,45 @@ if ! list="$(gh search prs --author "$ME" --state open --limit 100 --json reposi
44
68
  exit 1
45
69
  fi
46
70
  [ -n "$list" ] || { echo "pr-sweep.sh: no open PRs found for $ME" >&2; exit 1; }
71
+
72
+ open_ids="$(printf '%s\n' "$list" | tr -d '\r' | awk -F'\t' 'NF > 1 { print $1 "#" $2 }' | sort)"
73
+ if [ -f "$STATE" ]; then
74
+ # jq on Windows writes CRLF; a stray CR would make every stored id look new
75
+ if ! last="$(jq -r '.lastRun // "an unknown time"' "$STATE" | tr -d '\r')"; then
76
+ echo "pr-sweep.sh: cannot read $STATE" >&2
77
+ exit 1
78
+ fi
79
+ echo "--- merged or closed since $last"
80
+ gone="$(comm -23 <(jq -r '.open[]?' "$STATE" | tr -d '\r' | sort) <(printf '%s\n' "$open_ids"))"
81
+ while read -r id; do
82
+ [ -n "$id" ] || continue
83
+ gone_repo="${id%#*}"; gone_num="${id##*#}"
84
+ if ! v="$(gh pr view "$gone_num" --repo "$gone_repo" --json state,mergedAt,closedAt,mergedBy)"; then
85
+ echo "pr-sweep.sh: gh pr view $id failed" >&2
86
+ continue
87
+ fi
88
+ case "$(printf '%s' "$v" | jq -r '.state // "-"' | tr -d '\r')" in
89
+ MERGED)
90
+ printf 'MERGED %s by %s at %s\n' "$id" \
91
+ "$(printf '%s' "$v" | jq -r '.mergedBy.login // "-"' | tr -d '\r')" \
92
+ "$(printf '%s' "$v" | jq -r '.mergedAt // "-"' | tr -d '\r')"
93
+ ;;
94
+ CLOSED)
95
+ printf 'CLOSED %s at %s\n' "$id" \
96
+ "$(printf '%s' "$v" | jq -r '.closedAt // "-"' | tr -d '\r')"
97
+ ;;
98
+ esac
99
+ done <<< "$gone"
100
+ else
101
+ echo "--- no previous sweep recorded"
102
+ fi
103
+
104
+ if ! printf '%s\n' "$open_ids" | jq -R -s --arg t "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
105
+ '{lastRun: $t, open: (split("\n") | map(select(length > 0)))}' > "$STATE"; then
106
+ echo "pr-sweep.sh: cannot write $STATE" >&2
107
+ exit 1
108
+ fi
109
+
47
110
  n=0; act=0
48
111
  while IFS=$'\t' read -r repo num updated title; do
49
112
  [ -n "$repo" ] || continue
@@ -76,6 +76,8 @@ report it in one line and do not proceed to the next step.
76
76
  criteria in machine-readable form; run them.
77
77
 
78
78
  6. **Match the house style.**
79
+ Run `housebroken notes owner/repo` before writing the pull request and
80
+ again before any rework, and follow what is there.
79
81
  One finding, one pull request, the smallest diff that fixes it. No
80
82
  comment in code that has none: run `housebroken census <clone root>` and
81
83
  compare the added-comment ratio with the file. The body is under 120 words,
@@ -113,7 +115,9 @@ report it in one line and do not proceed to the next step.
113
115
  user's voice, after reading the whole thread. Rework what is asked for as
114
116
  a second commit so the reviewer sees the change. When the maintainer is
115
117
  right, concede in one sentence and let them close it. Never nudge a
116
- silent maintainer without the user's explicit word.
118
+ silent maintainer without the user's explicit word. After answering a
119
+ review, record what the maintainer asked for with
120
+ `housebroken notes owner/repo add "..."`.
117
121
 
118
122
  11. **Clean up.**
119
123
  `housebroken hygiene` deletes the fork branch of every merged or closed
@@ -127,12 +131,24 @@ report it in one line and do not proceed to the next step.
127
131
  maintainers cap open pull requests from non-collaborators and counts
128
132
  agent-opened ones against that cap.
129
133
 
134
+ 13. **Write it down before you leave.**
135
+ A run through the door ends with its ledger, not with the pull
136
+ request. A house rule learned about the repository goes into
137
+ `housebroken notes owner/repo add "..."` the same session. A
138
+ maintainer's verdict on a filed pull request is a lesson for everyone:
139
+ it goes into docs/lessons.md in the housebroken repository the same
140
+ day, quoted verbatim with the rule it became, by pull request when the
141
+ repository is not yours. A gate that was wrong, missing, or done by
142
+ hand goes on the housebroken repository as an issue before the run
143
+ ends. A lesson that lives only in a chat is lost.
144
+
130
145
  ## What you say to the user
131
146
 
132
147
  Before filing: the repository, the finding in one sentence, the prior-art
133
148
  result in one sentence, the red-then-green result, the agreement the
134
149
  organization wants, and the exact command you are about to run. After
135
- filing: the pull request URL and the verify result. On a refusal at any
150
+ filing: the pull request URL and the verify result. At the end of the run:
151
+ what went into the ledger, or that nothing did. On a refusal at any
136
152
  step: which step, and the one sentence that explains it.
137
153
 
138
154
  ## Never
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -1,83 +0,0 @@
1
- #!/bin/bash
2
- # comment-census.sh - added code lines against added comment lines, per branch.
3
- #
4
- # A patch that carries a paragraph of comments into a file that has none reads
5
- # as machine-written and gets closed. This counts, for every branch in every
6
- # clone under a directory, the added non-comment lines and the added comment
7
- # lines, so a pull request can be made to match the comment density of the file
8
- # it lands in. Three maintainers asked for exactly this, in three separate
9
- # reviews, before it became a script.
10
- #
11
- # Usage:
12
- # bash comment-census.sh [CLONE_ROOT]
13
- # bash comment-census.sh --help
14
- #
15
- # CLONE_ROOT is any directory whose immediate subdirectories are git clones.
16
- # Defaults to $HOUSEBROKEN_CLONES, then to $HOUSEBROKEN_HOME/clones.
17
- #
18
- # Environment:
19
- # HOUSEBROKEN_CLONES default clone root
20
- # HOUSEBROKEN_HOME workshop root, default $HOME/.housebroken
21
- #
22
- # Assumes: each clone has an origin remote whose default branch is discoverable
23
- # (origin/HEAD, else origin/main, else origin/master); every other local branch
24
- # is a candidate patch. Tests, markdown and lock files are excluded from the
25
- # code and comment counts. Comment detection covers //, /* */, * and #.
26
- set -u
27
-
28
- usage() {
29
- sed -n '2,/^set -u$/p' "$0" | sed 's/^# \{0,1\}//; /^set -u$/d'
30
- }
31
-
32
- case "${1:-}" in
33
- --help|-h) usage; exit 0 ;;
34
- esac
35
-
36
- HOUSEBROKEN_HOME="${HOUSEBROKEN_HOME:-$HOME/.housebroken}"
37
- root="${1:-${HOUSEBROKEN_CLONES:-$HOUSEBROKEN_HOME/clones}}"
38
-
39
- if [ ! -d "$root" ]; then
40
- echo "comment-census.sh: not a directory: $root" >&2
41
- echo "give a clone root as an argument or set HOUSEBROKEN_CLONES" >&2
42
- exit 1
43
- fi
44
-
45
- printf '%-28s %-36s %5s %5s %5s\n' repo branch code cmnt tests
46
-
47
- found=0
48
- for d in "$root"/*/; do
49
- [ -d "$d/.git" ] || continue
50
- found=1
51
- name=$(basename "$d")
52
- base=$(git -C "$d" symbolic-ref -q --short refs/remotes/origin/HEAD || true)
53
- if [ -z "$base" ]; then
54
- if git -C "$d" rev-parse --verify -q origin/main >/dev/null; then
55
- base=origin/main
56
- elif git -C "$d" rev-parse --verify -q origin/master >/dev/null; then
57
- base=origin/master
58
- else
59
- echo "comment-census.sh: no default branch in $d, skipped" >&2
60
- continue
61
- fi
62
- fi
63
- default_branch="${base#origin/}"
64
- while read -r br; do
65
- [ -n "$br" ] || continue
66
- [ "$br" = "$default_branch" ] && continue
67
- [ "$(git -C "$d" rev-list --count "$base..$br")" = "0" ] && continue
68
- added=$(git -C "$d" diff "$base..$br" -- . ':(exclude)*test*' ':(exclude)*Test*' ':(exclude)*_test.go' ':(exclude)Tests/*' ':(exclude)*.md' ':(exclude)*.sum' | grep '^+' | grep -v '^+++' || true)
69
- if [ -z "$added" ]; then
70
- code=0; cmnt=0
71
- else
72
- code=$(printf '%s\n' "$added" | grep -v -E '^\+\s*(//|/\*|\*|#|///|\*/)' | grep -c -v -E '^\+\s*$' || true)
73
- cmnt=$(printf '%s\n' "$added" | grep -c -E '^\+\s*(//|/\*|\*|///|\*/)' || true)
74
- fi
75
- tests=$(git -C "$d" diff "$base..$br" --numstat | awk '/[Tt]est/{s+=$1} END{print s+0}')
76
- printf '%-28s %-36s %5s %5s %5s\n' "$name" "$br" "$code" "$cmnt" "$tests"
77
- done < <(git -C "$d" for-each-ref --format='%(refname:short)' refs/heads/)
78
- done
79
-
80
- if [ "$found" = 0 ]; then
81
- echo "comment-census.sh: no git clones found under $root" >&2
82
- exit 1
83
- fi
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
File without changes