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.
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/PKG-INFO +13 -11
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/README.md +10 -8
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/bin/housebroken +4 -2
- housebroken_cli-0.2.0/hatch_build.py +24 -0
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/pyproject.toml +6 -2
- housebroken_cli-0.2.0/scripts/comment-census.sh +171 -0
- housebroken_cli-0.2.0/scripts/diff-defaults.sh +222 -0
- housebroken_cli-0.2.0/scripts/notes.sh +83 -0
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/pr-sweep.sh +71 -8
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/skills/housebroken/SKILL.md +18 -2
- housebroken_cli-0.2.0/src/housebroken/__init__.py +1 -0
- housebroken_cli-0.1.0/scripts/comment-census.sh +0 -83
- housebroken_cli-0.1.0/src/housebroken/__init__.py +0 -1
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/.gitignore +0 -0
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/LICENSE +0 -0
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/check-ai-policy.sh +0 -0
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/distinct-outside.sh +0 -0
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/file-pr.sh +0 -0
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/fork-hygiene.sh +0 -0
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/prior-art.sh +0 -0
- {housebroken_cli-0.1.0 → housebroken_cli-0.2.0}/scripts/verify-filed-pr.sh +0 -0
- {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.
|
|
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-
|
|
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>
|
|
37
|
+
<strong>29 merged upstream · 23 projects · 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> · <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 |
|
|
72
|
-
| merged |
|
|
73
|
-
| open, waiting on a maintainer |
|
|
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
|
-
|
|
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-
|
|
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>
|
|
25
|
+
<strong>29 merged upstream · 23 projects · 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> · <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 |
|
|
60
|
-
| merged |
|
|
61
|
-
| open, waiting on a maintainer |
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Agent-written pull requests that do not make a mess in someone else's house."
|
|
9
|
-
|
|
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
|
|
12
|
-
# bash pr-sweep.sh --all
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|