├── .gitattributes ├── LICENSE ├── README.md ├── _config.yml ├── _layouts └── default.html ├── assets ├── css │ ├── style.scss │ └── tocbot.css ├── images │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico └── js │ ├── anchor.min.js │ ├── script.js │ └── tocbot.min.js └── git-gloss /.gitattributes: -------------------------------------------------------------------------------- 1 | _config.yml linguist-documentation 2 | _layouts/** linguist-documentation 3 | assets/** linguist-documentation 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 sideshowbarker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## git-gloss ✨ makes git logs show PR/issue/review links 2 | 3 | `git-gloss` automatically adds [git notes](https://scottchacon.com/2010/08/25/notes/) to all your git logs — with GitHub PR/issue/reviewer/author links. 4 | 5 | --- 6 | 7 | ### How to use git-gloss 8 | 9 | You can download and run `git-gloss` within a directory having a GitHub repo clone by just doing this: 10 | 11 | ``` 12 | curl -fsSLO https://sideshowbarker.github.io/git-gloss/git-gloss && bash ./git-gloss 13 | ``` 14 | 15 | That will add [git notes](https://scottchacon.com/2010/08/25/notes/) locally for all commits in the local commit history with an associated GitHub pull request. 16 | 17 | Then, when you run `git log`, the log output for each commit will look something like this: 18 | 19 | ``` 20 | commit 9812031a02e539f08a6936e9c17d919a44c912b8 21 | Author: Jonatan Klemets 22 | Date: Sun Jul 23 19:38:04 2023 +0300 23 | 24 | LibWeb: Implement spec-compliant integer parsing 25 | 26 | This patch adds two new methods named `parse_non_negative_integer` and 27 | `parse_integer` inside the `Web::HTML` namespace that uses `StringUtils` 28 | under the hood but adds a bit more logic to make it spec compliant. 29 | 30 | Notes: 31 | Author: https://github.com/Jon4t4n 🔰 32 | Commit: https://github.com/SerenityOS/serenity/commit/9812031a02 33 | Pull-request: https://github.com/SerenityOS/serenity/pull/20140 34 | Issue: https://github.com/SerenityOS/serenity/issues/19937 35 | Reviewed-by: https://github.com/AtkinsSJ ✅ 36 | Reviewed-by: https://github.com/nico 37 | ``` 38 | 39 | 🔰 – indicates this is author’s first commit to the repo\ 40 | ✅ – indicates a review approval 41 | 42 | You can also run `git-gloss` on any subset of a rep’s commit history — by giving it zero or more commit hashes: 43 | 44 | ``` 45 | ./git-gloss 9812031a02 ebc5b33b77a 418f9ceadd 46 | ``` 47 | 48 | --- 49 | 50 | > [!TIP] 51 | > If you want to put notes in the logs for multiple repos, see the [Add a “git gloss” command](#add-git-gloss-command) section for a how-to on setting up a new `git gloss` command that you can run just as you would any other `git` command. 52 | 53 | --- 54 | 55 | ### How to share the notes 56 | 57 | Once `git-gloss` finishes running, here’s how you can share the notes with everyone in your GitHub project: 58 | 59 | 1. Push the notes back to your project remote at GitHub by running this command: 60 | 61 | ``` 62 | git push origin 'refs/notes/*' 63 | ``` 64 | 65 | 2. Others in your project can then fetch the notes from GitHub by running this command: 66 | 67 | ``` 68 | git fetch origin 'refs/notes/*:refs/notes/*' 69 | ``` 70 | 71 | Alternatively, rather than running the above command manually, others in the project can update their git configuration by running the following command; 72 | 73 | ``` 74 | git config --add remote.origin.fetch '+refs/notes/*:refs/notes/*' 75 | ``` 76 | 77 | That will cause all notes to be fetched from the remote every time they use `git fetch` or `git pull`. 78 | 79 | 3. Run `git-gloss` again to add notes for any new commits made after the last time you ran `git-gloss`. 80 | 81 | 4. Keep your project’s notes up to date by repeating steps 1 to 3 at a regular cadence (e.g., once day or so). 82 | 83 | Alternatively — to have steps 3 and 4 get done for you automatically, every time anyone from the project pushes to the main branch — you can set up a GitHub Actions workflow, with the file contents like this: 84 | 85 | ```yml 86 | name: Push notes 87 | on: 88 | push: 89 | branches: 90 | - master 91 | permissions: 92 | contents: write 93 | jobs: 94 | build: 95 | runs-on: ubuntu-latest 96 | steps: 97 | - uses: actions/checkout@v4 98 | with: 99 | fetch-depth: 0 100 | - uses: fregante/setup-git-user@v2 101 | - run: | 102 | git fetch origin "refs/notes/*:refs/notes/*" 103 | curl -fsSLO https://sideshowbarker.github.io/git-gloss/git-gloss && bash ./git-gloss 104 | git push origin "refs/notes/*" 105 | env: 106 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 107 | ``` 108 | 109 | > [!IMPORTANT] 110 | > It’s especially important that your workflow file include the following: 111 | > 112 | > ``` 113 | > with: 114 | > fetch-depth: 0 115 | > ``` 116 | > 117 | > …as shown in the [Fetch all history for all tags and branches](https://github.com/actions/checkout?tab=readme-ov-file#fetch-all-history-for-all-tags-and-branches) example in the actions/checkout documentation. 118 | > 119 | > `git-gloss` needs access to the entire commit history — and that requires setting `fetch-depth: 0`. Otherwise, without that set, the actions/checkout action fetches only 1 commit from the history. 120 | > 121 | > Among the problems with fetching only one commit: If your project uses the [Rebase and merge](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github#rebasing-and-merging-your-commits) method, it’s possible that each push to the repo may contain multiple commits — and if so, without `fetch-depth: 0` set, only one of the commits in the push would get processed by `git-gloss`, and the rest would be ignored. 122 | 123 | --- 124 | 125 | ### How long does it take? 126 | 127 | `git-gloss` seems able to process at most about 1000 commits per hour — about 17 or 18 commits per minute. 128 | 129 | So, the first time you run it in a repo with many commits, it’ll take a long time — hours, or even a day or more. 130 | 131 | For example, if your repo has somewhere around 1000 commits, it’ll take at least 1 hour to finish. If your repo has somewhere around 10,000 commits, it’ll take more than 10 hours. And so on. 132 | 133 | > [!NOTE] 134 | > You can stop `git-gloss` at any time with Ctrl-C. After stopping it, when you run it again, it will start off wherever it left off. So, if you have a repo with somewhere around 2000 commits, and you stopped `git-gloss` after it was running for about an hour, then it will run for about another hour before it finishes. 135 | 136 | ### How to “back up” notes 137 | 138 | Given how long (multiple hours) it can take `git-gloss` to add all notes for a large history, you should do this: 139 | 140 | 1. Periodically stop `git-gloss` (say, once an hour), using Ctrl-C. 141 | 142 | 2. Run the following command to push your (partial) notes to your project repo: 143 | 144 | ``` 145 | git push origin 'refs/notes/*' 146 | ``` 147 | 148 | 3. Restart `git-gloss`, to continue adding more notes. 149 | 150 | 4. Repeat steps 1 to 3 periodically (say, once an hour) until `git-gloss` finishes. 151 | 152 | > [!IMPORTANT] 153 | > Doing the steps above will ensure that you have a “backup” of the notes you’ve generated so far — and if ever needed, you can then “restore” your notes from that backup by running the following command: 154 | > 155 | > ``` 156 | > git fetch origin 'refs/notes/*:refs/notes/*' 157 | > ``` 158 | 159 | ### Why is it so slow? 160 | 161 | For each commit `git-gloss` processes, it makes 4 calls to GitHub API endpoints — requiring network resources and time. And in a typical environment, for each commit the total time needed (mostly due to those calls) seems to to work out to at least 3.5 seconds or so — which means it can process only about 17 or 18 commits a minute. 162 | 163 | Regardless, the GitHub API has [rate limits](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api) that prevent making more than 5000 requests per hour — which works out to about 83 requests per minute. And so, because `git-gloss` makes 4 requests for each commit, that also limits it to being able to make only enough requests per minute for 20–21 commits at most (20 ⨉ 4 ⨉ 60 = 4800). 164 | 165 | ### How to handle problems 166 | 167 | It’s possible to end up making enough requests in your environment that you exceed the 5000-requests-per-hour limit, especially if you happen to be running other applications that make GitHub API calls at the same time you’re running `git-gloss`. When you do exceed the limit while running `git-gloss`, you’ll see it log a message like this: 168 | 169 | > ⚠️ ***Attention: Hit the GitHub API rate limit. Sleeping for 933 seconds.*** 170 | 171 | If you do see that message, you don’t need to worry; instead, `git-gloss` will automatically wait for the specified number of seconds — and then, after that, it will start sending GitHub API requests once again. 172 | 173 | `git-gloss` tries to gracefully handle all errors, and you may occasionally see it log a message like this: 174 | 175 | > ⚠️ ***Attention: Failed to fetch issues from GitHub; re-trying.*** 176 | 177 | If you do see that message, you don’t need to worry; instead, `git-gloss` will automatically retry the GitHub API request — and will keep re-trying it until it succeeds. 178 | 179 | But it’s possible you may run into other errors — unhandled errors. So you can keep an output log, and review it after `git-gloss` finishes. You can create a `git-gloss` output log like this: 180 | 181 | ``` 182 | tmpfile=$(mktemp) && echo "Logging output to $tmpfile"; 183 | git-gloss 2>&1 | tee tmpfile 184 | ``` 185 | 186 | That creates a log file and sends the output to both the terminal and the log file (using the `mktemp` and `tee` utilities, which are standard in any Linux/Unix environment — including the macOS Terminal/shell environment). 187 | 188 | After `git-log` finishes (or even as it’s running), you can review the log for any unhandled error output. 189 | 190 | > [!NOTE] 191 | > You should [raise an issue](https://github.com/sideshowbarker/git-gloss/issues/new) if you do find any unhandled errors. 192 | 193 | For each unhandled error you find, you’ll need to complete the following steps: 194 | 195 | 1. Remove any note which `git-gloss` may have added for the given commit: 196 | 197 | ``` 198 | git notes remove 67c727177e 199 | ``` 200 | 201 | 2. (Re)add a note for the given commit, by running `git-gloss` with the commit hash specified: 202 | 203 | ``` 204 | git-gloss 67c727177e 205 | ``` 206 | 207 | > [!CAUTION] 208 | > `git-gloss` provides no way to undo its actions and remove all notes it added. The only practical way to undo its actions may be to completely remove all notes, including any you may have added by other means. 209 | > 210 | > But if you haven’t added notes by any other means: To remove all `git-gloss`-added notes, run this: 211 | > 212 | > ``` 213 | > git update-ref -d refs/notes/commits 214 | > ``` 215 | 216 | ### How to fix “loose objects” 217 | 218 | After running `git-gloss` for some time, if you then do other git operations in the same clone where you’ve got `git-gloss` running, you may see git reporting the following series of messages: 219 | 220 | ``` 221 | Auto packing the repository in background for optimum performance. 222 | See "git help gc" for manual housekeeping. 223 | warning: The last gc run reported the following. Please correct the root cause 224 | and remove .git/gc.log 225 | Automatic cleanup will not be performed until the file is removed. 226 | 227 | warning: There are too many unreachable loose objects; run 'git prune' to remove them. 228 | ``` 229 | 230 | > [!CAUTION] 231 | > **Absolutely never, ever, under any circumstances run `git prune` at the same time `git gloss` is running.** 232 | > 233 | > Invoking `git prune` while `git-gloss` is running may cause corruption of whatever notes data you’ve generated so far — and may even bork your entire local git environment for the clone in such a way as to prevent you from being able to successfully run any other git commands at all. 234 | 235 | If the reason you’re reading this section is that you *did* run `git prune` — and you’re now trying to figure out how what to do — then: **don’t worry**, because you *can* recover from it. Here’s how → Run the following command: 236 | 237 | > ``` 238 | > git update-ref -d refs/notes/commits 239 | > ``` 240 | 241 | That will remove all the notes you’ve added *locally* so far. But if you’ve periodically been pushing [backups](#how-to-back-up-notes), then you can next do this: 242 | 243 | > ``` 244 | > git fetch origin 'refs/notes/*:refs/notes/*' 245 | > ``` 246 | 247 | …and that *may* successfully recover all the notes you had added up to the point where you last backed up. 248 | 249 | Regardless, your next step is just [re-run `git-gloss`](#how-to-use-git-gloss) so that it can again start adding notes. 250 | 251 | But note that having too many loose objects in a repo is not really a problem that has any serious effects; it’s not something to spend any time worrying about. 252 | 253 | So the only real “problem” to fix here is: How to make git stop emitting that series of warning messages. 254 | 255 | And the way to do that is just this: 256 | 257 | **Once `git gloss` has completely finished**, *then at that time* you can safely run `git prune` (and you should). 258 | 259 | But otherwise, prior to `git gloss` completely finishing, **do not spend any time worrying about**; having too many loose objects in a repo for a while (or even for a long time) doesn’t cause any real problems worth worrying about. 260 | 261 | ### Dependencies 262 | 263 | * `git` – any version ([v2.42+](https://stackoverflow.com/a/76633969/)) with support for the `--no-separator` option for the `git notes` command 264 | 265 | * `jq` – [https://jqlang.github.io/jq/](https://jqlang.github.io/jq/) (JSON processor) 266 | 267 | * `gh` – [https://cli.github.com/](https://cli.github.com/) (GitHub CLI), with the [`GH_TOKEN` or `GITHUB_TOKEN`](https://cli.github.com/manual/gh_help_environment) environment variables set 268 | 269 | * `grep` – any grep-compatible program 270 | 271 | > [!IMPORTANT] 272 | > On macOS in particular, [GNU grep](https://apple.stackexchange.com/a/193300) — rather than than the Apple-provided `grep` — is recommended,for performance reasons; example: 273 | > 274 | > ``` 275 | > brew install grep 276 | > ``` 277 | 278 | ### Environment variables 279 | 280 | You can affect the `git-gloss` behavior using the environment variables described in this section. 281 | 282 | > [!TIP] 283 | > Rather than separately exporting each environment variable to your shell, you can instead specify them all at the same time in the invocation you use for running `git-gloss` — like this: 284 | > 285 | > ``` 286 | > GIT=/opt/homebrew/bin/git GREP=/opt/homebrew/bin/ggrep \ 287 | > OTHER_REPO=SerenityOS/serenity > ./git-gloss 288 | >``` 289 | 290 | #### `GIT` 291 | 292 | You can use this to specify a path to a different `git` binary — for instance, in the case where you have multiple different `git` versions on your system; example: 293 | 294 | ``` 295 | export GIT=/opt/homebrew/bin/git 296 | ``` 297 | 298 | > [!NOTE] 299 | > Because `git-gloss` calls the `git notes` command with the `--no-separator` option — which was added in git [version 2.42+](https://stackoverflow.com/a/76633969/) — the git version you use with `git-gloss` must be version 2.42 or later. 300 | 301 | #### `GREP` 302 | 303 | You can use this to specify a path to any grep-compatible binary on your system; for instance, to avoid using the Apple-provided `grep` on macOS; example: 304 | 305 | ``` 306 | export GREP=/opt/homebrew/bin/ggrep 307 | ``` 308 | 309 | > [!IMPORTANT] 310 | > On macOS in particular, [GNU grep](https://apple.stackexchange.com/a/193300) — rather than than the Apple-provided `grep` — is recommended, for performance reasons; example: 311 | > 312 | > ``` 313 | > brew install grep 314 | > ``` 315 | 316 | #### `OTHER_REPO` 317 | 318 | You can use this to specify an `[owner]/[repo]` repo other than the current repo; e.g., a repo the current repo shares part of its commit history with (because the current repo was created from an older repo); example: 319 | 320 | ``` 321 | export OTHER_REPO=SerenityOS/serenity 322 | ``` 323 | 324 | If you specify an `OTHER_REPO` value, then if `git-gloss` can’t find any pull request for a particular commit in the current repo, it will then look for a pull request in the repo you specified in the `OTHER_REPO` value. 325 | 326 | ### Add “git gloss” command 327 | 328 | Clone the `git-gloss` repo and add its directory to your `$PATH`: 329 | 330 | ```bash 331 | git clone https://github.com/sideshowbarker/git-gloss.git 332 | cd git-gloss 333 | echo export PATH=\"$PATH:$PWD\" >> ~/.bash_profile 334 | ``` 335 | 336 | Now you can just type `git gloss` in any repo/clone directory, to add notes to the logs for that repo. 337 | 338 | ### Notes 339 | 340 | You can see how much space your notes tree is taking up by running this command: 341 | 342 | ``` 343 | git ls-tree -r $(git rev-parse refs/notes/commits) \ 344 | | awk '{print $3}' | git cat-file --batch-check='%(objectsize:disk)' \ 345 | | awk '{s+=$1} END {printf "%.2f MB\n", s / 1048576}' 346 | ``` 347 | 348 | You’re likely to find that it takes up about 1MB to 1.3MB for every 20,000 commits in the repo history. 349 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | markdown: kramdown 2 | remote_theme: pages-themes/hacker@v0.2.0 3 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% include head-custom.html %} 15 | 16 | {% seo %} 17 | 18 | 19 | 20 |
21 | 22 |
23 | {{ content }} 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | body { 6 | font-size: 16px; 7 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; 8 | position: relative; 9 | margin-bottom: 500px; 10 | } 11 | @media print { 12 | .toc { 13 | display: none; 14 | } 15 | #main_content, section { 16 | max-width: none !important; 17 | width: 1000px !important; 18 | } 19 | } 20 | #footer { 21 | font-size: 13px; 22 | } 23 | #sticky { 24 | display: none; 25 | } 26 | @media (max-width: 600px) { 27 | .mobile { 28 | display: block; 29 | } 30 | #sticky { 31 | display: block; 32 | position: fixed; 33 | bottom: 0; 34 | left: 0; 35 | height: 60px; 36 | width: 100%; 37 | padding-top: 12px; 38 | padding-bottom: 60px; 39 | background: rgb(106, 106, 94); 40 | text-align: center; 41 | margin-bottom: 0; 42 | font-size: 14px; 43 | } 44 | } 45 | .close-button { 46 | border: none; 47 | display: inline-block; 48 | padding-right: 10px; 49 | padding-bottom: 0; 50 | vertical-align: middle; 51 | overflow: hidden; 52 | text-decoration: none; 53 | color: inherit; 54 | background-color: inherit; 55 | text-align: center; 56 | cursor: pointer; 57 | white-space: nowrap; 58 | font-size: 20px !important; 59 | } 60 | .topright { 61 | position: absolute; 62 | right: 0; 63 | top: 0; 64 | } 65 | .container { 66 | min-height: 100%; 67 | margin-left: auto; 68 | margin-bottom: 20px; 69 | } 70 | @media (max-width: 1310px) { 71 | .container { 72 | width: 60%; 73 | max-width: 90%; 74 | margin-left: 50px; 75 | } 76 | } 77 | @media (max-width: 1000px) { 78 | .container { 79 | width: 50%; 80 | margin-left: 30px; 81 | } 82 | } 83 | @media (max-width: 800px) { 84 | .container { 85 | width: 40%; 86 | } 87 | } 88 | @media (max-width: 600px) { 89 | .container { 90 | width: 90%; 91 | } 92 | .toc { 93 | display: none !important; 94 | } 95 | } 96 | .toc.toc-right { 97 | right: calc((100% - 48rem - 4rem)/2); 98 | } 99 | .toc.toc-right { 100 | right: 0; 101 | } 102 | .toc { 103 | height: 100%; 104 | width: 331px; 105 | transform: translateX(0); 106 | } 107 | .transition--300 { 108 | transition: all 300ms ease-in-out; 109 | } 110 | .is-position-fixed { 111 | position: fixed !important; 112 | top: 0; 113 | } 114 | .z-1 { 115 | z-index: 1; 116 | } 117 | .pt5 { 118 | padding-top: 4rem; 119 | } 120 | .pa4 { 121 | padding: 2rem; 122 | } 123 | .absolute { 124 | position: absolute; 125 | } 126 | .relative { 127 | position: relative; 128 | } 129 | article, aside, footer, header, nav, section { 130 | display: block; 131 | } 132 | * { 133 | box-sizing: border-box; 134 | } 135 | ul li { 136 | list-style-type: "\00BB " !important; 137 | list-style-image: none !important; 138 | padding-left: 3px; 139 | } 140 | .toc > .toc-list:first-child > li:first-child { 141 | list-style: none !important; 142 | } 143 | .toc-list { 144 | padding-left: 16px !important; 145 | } 146 | .is-active-link::before { 147 | color: rgb(184, 233, 90) !important; 148 | } 149 | .is-active-link { 150 | color: rgb(184, 233, 90) !important; 151 | text-shadow: rgba(0, 0, 0, 0.1) 0px 1px 1px, rgba(86, 111, 15, 0.1) 0px 0px 5px, rgba(86, 111, 15, 0.1) 0px 0px 10px !important; 152 | } 153 | section { 154 | max-width: 859px; 155 | border-bottom: 1px dashed #b5e853 !important; 156 | padding-bottom: 16px; 157 | margin-bottom: 16px !important; 158 | } 159 | 160 | #main_content h1 { 161 | font-size: 27px !important; 162 | } 163 | 164 | h1 { 165 | margin-top: 32px; 166 | border-bottom: 1px dashed #b5e853; 167 | padding-bottom: 16px; 168 | } 169 | h2, h3, h4 { 170 | margin: 60px 0 6px; 171 | } 172 | h3 { 173 | margin-top: 32px; 174 | } 175 | #main_content h4 { 176 | font-size: 16px !important; 177 | margin-top: 20px; 178 | } 179 | #why-not-just-wds + p + h4 { 180 | font-size: 12.8px !important; 181 | } 182 | h2 + h2, h3 + h3, h4 + h4 { 183 | margin-top: 0px !important; 184 | } 185 | h2 + p, h3 + p, h4 + p { 186 | margin-top: 8px; 187 | } 188 | h1, h2, h3 { 189 | line-height: 1.1; 190 | } 191 | blockquote { 192 | margin-left: 3px; 193 | border-left: .25em solid rgb(184, 233, 90) !important; 194 | border-left-color: rgb(184, 233, 90) !important; 195 | } 196 | // blockquote p:first-of-type::first-line { 197 | // color: magenta; 198 | // } 199 | a { 200 | text-decoration: none; 201 | } 202 | code { 203 | font-size: 15px; 204 | color: orange !important; 205 | } 206 | -------------------------------------------------------------------------------- /assets/css/tocbot.css: -------------------------------------------------------------------------------- 1 | .toc{overflow-y:auto}.toc>.toc-list{overflow:hidden;position:relative}.toc>.toc-list li{list-style:none}.toc-list{margin:0;padding-left:10px}a.toc-link{color:currentColor;height:100%}.is-collapsible{max-height:1000px;overflow:hidden;transition:all 300ms ease-in-out}.is-collapsed{max-height:0}.is-position-fixed{position:fixed !important;top:0}.is-active-link{font-weight:700}.toc-link::before{background-color:#eee;content:" ";display:inline-block;height:inherit;left:0;margin-top:-1px;position:absolute;width:2px}.is-active-link::before{background-color:#54bc4b}/*# sourceMappingURL=tocbot.css.map */ 2 | -------------------------------------------------------------------------------- /assets/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sideshowbarker/git-gloss/a69253695c7619b356cf13f71d194d485dbbfba2/assets/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sideshowbarker/git-gloss/a69253695c7619b356cf13f71d194d485dbbfba2/assets/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sideshowbarker/git-gloss/a69253695c7619b356cf13f71d194d485dbbfba2/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sideshowbarker/git-gloss/a69253695c7619b356cf13f71d194d485dbbfba2/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sideshowbarker/git-gloss/a69253695c7619b356cf13f71d194d485dbbfba2/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sideshowbarker/git-gloss/a69253695c7619b356cf13f71d194d485dbbfba2/assets/images/favicon.ico -------------------------------------------------------------------------------- /assets/js/anchor.min.js: -------------------------------------------------------------------------------- 1 | // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat 2 | // 3 | // AnchorJS - v5.0.0 - 2023-01-18 4 | // https://www.bryanbraun.com/anchorjs/ 5 | // Copyright (c) 2023 Bryan Braun; Licensed MIT 6 | // 7 | // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat 8 | !function(A,e){"use strict";"function"==typeof define&&define.amd?define([],e):"object"==typeof module&&module.exports?module.exports=e():(A.AnchorJS=e(),A.anchors=new A.AnchorJS)}(globalThis,function(){"use strict";return function(A){function u(A){A.icon=Object.prototype.hasOwnProperty.call(A,"icon")?A.icon:"",A.visible=Object.prototype.hasOwnProperty.call(A,"visible")?A.visible:"hover",A.placement=Object.prototype.hasOwnProperty.call(A,"placement")?A.placement:"right",A.ariaLabel=Object.prototype.hasOwnProperty.call(A,"ariaLabel")?A.ariaLabel:"Anchor",A.class=Object.prototype.hasOwnProperty.call(A,"class")?A.class:"",A.base=Object.prototype.hasOwnProperty.call(A,"base")?A.base:"",A.truncate=Object.prototype.hasOwnProperty.call(A,"truncate")?Math.floor(A.truncate):64,A.titleText=Object.prototype.hasOwnProperty.call(A,"titleText")?A.titleText:""}function d(A){var e;if("string"==typeof A||A instanceof String)e=[].slice.call(document.querySelectorAll(A));else{if(!(Array.isArray(A)||A instanceof NodeList))throw new TypeError("The selector provided to AnchorJS was invalid.");e=[].slice.call(A)}return e}this.options=A||{},this.elements=[],u(this.options),this.add=function(A){var e,t,o,i,n,s,a,r,l,c,h,p=[];if(u(this.options),0!==(e=d(A=A||"h2, h3, h4, h5, h6")).length){for(null===document.head.querySelector("style.anchorjs")&&((A=document.createElement("style")).className="anchorjs",A.appendChild(document.createTextNode("")),void 0===(h=document.head.querySelector('[rel="stylesheet"],style'))?document.head.appendChild(A):document.head.insertBefore(A,h),A.sheet.insertRule(".anchorjs-link{opacity:0;text-decoration:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}",A.sheet.cssRules.length),A.sheet.insertRule(":hover>.anchorjs-link,.anchorjs-link:focus{opacity:1}",A.sheet.cssRules.length),A.sheet.insertRule("[data-anchorjs-icon]::after{content:attr(data-anchorjs-icon)}",A.sheet.cssRules.length),A.sheet.insertRule('@font-face{font-family:anchorjs-icons;src:url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype")}',A.sheet.cssRules.length)),h=document.querySelectorAll("[id]"),t=[].map.call(h,function(A){return A.id}),i=0;i\]./()*\\\n\t\b\v\u00A0]/g,"-").replace(/-{2,}/g,"-").substring(0,this.options.truncate).replace(/^-+|-+$/gm,"").toLowerCase()},this.hasAnchorJSLink=function(A){var e=A.firstChild&&-1<(" "+A.firstChild.className+" ").indexOf(" anchorjs-link "),A=A.lastChild&&-1<(" "+A.lastChild.className+" ").indexOf(" anchorjs-link ");return e||A||!1}}}); 9 | // @license-end -------------------------------------------------------------------------------- /assets/js/script.js: -------------------------------------------------------------------------------- 1 | const replaceOnDocument = (pattern, string, {target = document.body} = {}) => { 2 | // Handle `string` — see the last section 3 | [ 4 | target, 5 | ...target.querySelectorAll("*:not(script):not(noscript):not(style)") 6 | ].forEach(({childNodes: [...nodes]}) => nodes 7 | .filter(({nodeType}) => nodeType === Node.TEXT_NODE) 8 | .forEach((textNode) => textNode.textContent = textNode.textContent.replace(pattern, string))); 9 | }; 10 | replaceOnDocument(/\[!IMPORTANT\]/g, "👋 Important: "); 11 | replaceOnDocument(/\[!NOTE\]/g, "👉 Note: "); 12 | replaceOnDocument(/\[!TIP\]/g, "💡 Tip: "); 13 | replaceOnDocument(/\[!CAUTION\]/g, "⚠️ Caution: "); 14 | 15 | anchors.options.placement = 'left'; 16 | document.addEventListener('DOMContentLoaded', function(event) { anchors.add(); }); 17 | 18 | tocbot.init({ 19 | // Where to render the table of contents. 20 | tocSelector: '.js-toc', 21 | // Where to grab the headings to build the table of contents. 22 | contentSelector: '.js-toc-content', 23 | // Which headings to grab inside of the contentSelector element. 24 | headingSelector: 'h1, h2, h3, h4', 25 | // For headings inside relative or absolute positioned containers within content. 26 | hasInnerContainers: true, 27 | orderedList: false, 28 | // Show the entire table of contents, fully expanded 29 | collapseDepth: 6, 30 | }); 31 | -------------------------------------------------------------------------------- /assets/js/tocbot.min.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={539:(e,t,n)=>{var o,l,r;l=[],void 0===(r="function"==typeof(o=function(e){"use strict";const t=!!(e&&e.document&&e.document.querySelector&&e.addEventListener);if("undefined"==typeof window&&!t)return;const o=n(496);return e.tocbot=o,o}(void 0!==n.g?n.g:window||n.g))?o.apply(t,l):o)||(e.exports=r)},496:(e,t,n)=>{"use strict";function o(e){var t,n=[].forEach,o=[].some,l=document.body,r=!0,i=" ";function s(t,o){var l,r,a,u=o.appendChild((l=t,r=document.createElement("li"),a=document.createElement("a"),e.listItemClass&&r.setAttribute("class",e.listItemClass),e.onClick&&(a.onclick=e.onClick),e.includeTitleTags&&a.setAttribute("title",l.textContent),e.includeHtml&&l.childNodes.length?n.call(l.childNodes,(function(e){a.appendChild(e.cloneNode(!0))})):a.textContent=l.textContent,a.setAttribute("href",e.basePath+"#"+l.id),a.setAttribute("class",e.linkClass+i+"node-name--"+l.nodeName+i+e.extraLinkClasses),r.appendChild(a),r));if(t.children.length){var d=c(t.isCollapsed);t.children.forEach((function(e){s(e,d)})),u.appendChild(d)}}function c(t){var n=e.orderedList?"ol":"ul",o=document.createElement(n),l=e.listClass+i+e.extraListClasses;return t&&(l=(l=l+i+e.collapsibleClass)+i+e.isCollapsedClass),o.setAttribute("class",l),o}function a(t){var n=0;return null!==t&&(n=t.offsetTop,e.hasInnerContainers&&(n+=a(t.offsetParent))),n}function u(e,t){return e&&e.className!==t&&(e.className=t),e}function d(t){return t&&-1!==t.className.indexOf(e.collapsibleClass)&&-1!==t.className.indexOf(e.isCollapsedClass)?(u(t,t.className.replace(i+e.isCollapsedClass,"")),d(t.parentNode.parentNode)):t}return{enableTocAnimation:function(){r=!0},disableTocAnimation:function(t){var n=t.target||t.srcElement;"string"==typeof n.className&&-1!==n.className.indexOf(e.linkClass)&&(r=!1)},render:function(e,n){var o=c(!1);if(n.forEach((function(e){s(e,o)})),null!==(t=e||t))return t.firstChild&&t.removeChild(t.firstChild),0===n.length?t:t.appendChild(o)},updateToc:function(s){var c;c=e.scrollContainer&&document.querySelector(e.scrollContainer)?document.querySelector(e.scrollContainer).scrollTop:document.documentElement.scrollTop||l.scrollTop,e.positionFixedSelector&&function(){var n;n=e.scrollContainer&&document.querySelector(e.scrollContainer)?document.querySelector(e.scrollContainer).scrollTop:document.documentElement.scrollTop||l.scrollTop;var o=document.querySelector(e.positionFixedSelector);"auto"===e.fixedSidebarOffset&&(e.fixedSidebarOffset=t.offsetTop),n>e.fixedSidebarOffset?-1===o.className.indexOf(e.positionFixedClass)&&(o.className+=i+e.positionFixedClass):o.className=o.className.replace(i+e.positionFixedClass,"")}();var f,m=s;if(r&&null!==t&&m.length>0){o.call(m,(function(t,n){return a(t)>c+e.headingsOffset+10?(f=m[0===n?n:n-1],!0):n===m.length-1?(f=m[m.length-1],!0):void 0}));var h=t.querySelector("."+e.activeLinkClass),p=t.querySelector("."+e.linkClass+".node-name--"+f.nodeName+'[href="'+e.basePath+"#"+f.id.replace(/([ #;&,.+*~':"!^$[\]()=>|/\\@])/g,"\\$1")+'"]');if(h===p)return;var C=t.querySelectorAll("."+e.linkClass);n.call(C,(function(t){u(t,t.className.replace(i+e.activeLinkClass,""))}));var g=t.querySelectorAll("."+e.listItemClass);n.call(g,(function(t){u(t,t.className.replace(i+e.activeListItemClass,""))})),p&&-1===p.className.indexOf(e.activeLinkClass)&&(p.className+=i+e.activeLinkClass);var v=p&&p.parentNode;v&&-1===v.className.indexOf(e.activeListItemClass)&&(v.className+=i+e.activeListItemClass);var S=t.querySelectorAll("."+e.listClass+"."+e.collapsibleClass);n.call(S,(function(t){-1===t.className.indexOf(e.isCollapsedClass)&&(t.className+=i+e.isCollapsedClass)})),p&&p.nextSibling&&-1!==p.nextSibling.className.indexOf(e.isCollapsedClass)&&u(p.nextSibling,p.nextSibling.className.replace(i+e.isCollapsedClass,"")),d(p&&p.parentNode.parentNode)}}}}n.r(t),n.d(t,{_buildHtml:()=>s,_headingsArray:()=>a,_options:()=>f,_parseContent:()=>c,_scrollListener:()=>u,destroy:()=>h,init:()=>m,refresh:()=>p});const l={tocSelector:".js-toc",contentSelector:".js-toc-content",headingSelector:"h1, h2, h3",ignoreSelector:".js-toc-ignore",hasInnerContainers:!1,linkClass:"toc-link",extraLinkClasses:"",activeLinkClass:"is-active-link",listClass:"toc-list",extraListClasses:"",isCollapsedClass:"is-collapsed",collapsibleClass:"is-collapsible",listItemClass:"toc-list-item",activeListItemClass:"is-active-li",collapseDepth:0,scrollSmooth:!0,scrollSmoothDuration:420,scrollSmoothOffset:0,scrollEndCallback:function(e){},headingsOffset:1,throttleTimeout:50,positionFixedSelector:null,positionFixedClass:"is-position-fixed",fixedSidebarOffset:"auto",includeHtml:!1,includeTitleTags:!1,onClick:function(e){},orderedList:!0,scrollContainer:null,skipRendering:!1,headingLabelCallback:!1,ignoreHiddenElements:!1,headingObjectCallback:null,basePath:"",disableTocScrollSync:!1,tocScrollOffset:0};function r(e){var t=e.duration,n=e.offset,o=location.hash?l(location.href):location.href;function l(e){return e.slice(0,e.lastIndexOf("#"))}document.body.addEventListener("click",(function(r){var i;"a"!==(i=r.target).tagName.toLowerCase()||!(i.hash.length>0||"#"===i.href.charAt(i.href.length-1))||l(i.href)!==o&&l(i.href)+"#"!==o||r.target.className.indexOf("no-smooth-scroll")>-1||"#"===r.target.href.charAt(r.target.href.length-2)&&"!"===r.target.href.charAt(r.target.href.length-1)||-1===r.target.className.indexOf(e.linkClass)||function(e,t){var n,o,l=window.pageYOffset,r={duration:t.duration,offset:t.offset||0,callback:t.callback,easing:t.easing||function(e,t,n,o){return(e/=o/2)<1?n/2*e*e+t:-n/2*(--e*(e-2)-1)+t}},i=document.querySelector('[id="'+decodeURI(e).split("#").join("")+'"]')||document.querySelector('[id="'+e.split("#").join("")+'"]'),s="string"==typeof e?r.offset+(e?i&&i.getBoundingClientRect().top||0:-(document.documentElement.scrollTop||document.body.scrollTop)):e,c="function"==typeof r.duration?r.duration(s):r.duration;function a(e){o=e-n,window.scrollTo(0,r.easing(o,l,s,c)),o0&&(!(c=n(s))||i!==c.headingLevel);)c&&void 0!==c.children&&(s=c.children),a--;i>=e.collapseDepth&&(r.isCollapsed=!0),s.push(r)}(r,t.nest),t}),{nest:[]})},selectHeadings:function(t,n){var o=n;e.ignoreSelector&&(o=n.split(",").map((function(t){return t.trim()+":not("+e.ignoreSelector+")"})));try{return t.querySelectorAll(o)}catch(e){return console.warn("Headers not found with selector: "+o),null}}}}(f),h();const t=function(e){try{return e.contentElement||document.querySelector(e.contentSelector)}catch(t){return console.warn("Contents element not found: "+e.contentSelector),null}}(f);if(null===t)return;const n=v(f);if(null===n)return;if(a=c.selectHeadings(t,f.headingSelector),null===a)return;const m=c.nestHeadingsArray(a).nest;if(f.skipRendering)return this;s.render(n,m),u=g((function(e){s.updateToc(a),!f.disableTocScrollSync&&function(e){var t=e.tocElement||document.querySelector(e.tocSelector);if(t&&t.scrollHeight>t.clientHeight){var n=t.querySelector("."+e.activeListItemClass);if(n){var o=t.scrollTop,l=o+t.clientHeight,r=n.offsetTop,s=r+n.clientHeight;rl-e.tocScrollOffset-i&&(t.scrollTop+=s-l+e.tocScrollOffset+2*i)}}}(f);const t=e&&e.target&&e.target.scrollingElement&&0===e.target.scrollingElement.scrollTop;(e&&(0===e.eventPhase||null===e.currentTarget)||t)&&(s.updateToc(a),f.scrollEndCallback&&f.scrollEndCallback(e))}),f.throttleTimeout),u(),f.scrollContainer&&document.querySelector(f.scrollContainer)?(document.querySelector(f.scrollContainer).addEventListener("scroll",u,!1),document.querySelector(f.scrollContainer).addEventListener("resize",u,!1)):(document.addEventListener("scroll",u,!1),document.addEventListener("resize",u,!1));let p=null;d=g((function(e){f.scrollSmooth&&s.disableTocAnimation(e),s.updateToc(a),p&&clearTimeout(p),p=setTimeout((function(){s.enableTocAnimation()}),f.scrollSmoothDuration)}),f.throttleTimeout),f.scrollContainer&&document.querySelector(f.scrollContainer)?document.querySelector(f.scrollContainer).addEventListener("click",d,!1):document.addEventListener("click",d,!1)}function h(){const e=v(f);null!==e&&(f.skipRendering||e&&(e.innerHTML=""),f.scrollContainer&&document.querySelector(f.scrollContainer)?(document.querySelector(f.scrollContainer).removeEventListener("scroll",u,!1),document.querySelector(f.scrollContainer).removeEventListener("resize",u,!1),s&&document.querySelector(f.scrollContainer).removeEventListener("click",d,!1)):(document.removeEventListener("scroll",u,!1),document.removeEventListener("resize",u,!1),s&&document.removeEventListener("click",d,!1)))}function p(e){h(),m(e||f)}const C=Object.prototype.hasOwnProperty;function g(e,t,n){let o,l;return t||(t=250),function(){const r=n||this,i=+new Date,s=arguments;o&&i{for(var o in t)n.o(t,o)&&!n.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n(539)})(); -------------------------------------------------------------------------------- /git-gloss: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git-gloss() { cat </dev/null) 21 | if [[ $(echo "$response" | jq ".status" 2>/dev/null) == '"403"' ]]; then 22 | response=$(gh api "/rate_limit") 23 | reset_time=$(echo "$response" | jq ".rate.reset") 24 | current_time=$(date +%s) 25 | seconds_to_sleep=$((reset_time - current_time + 1)) 26 | >&2 echo -n "⚠️ Attention: Hit the GitHub API rate limit. " 27 | >&2 echo -n "Sleeping for $seconds_to_sleep seconds. " 28 | sleep "$seconds_to_sleep" 29 | response=$(gh api "$1" 2>/dev/null) 30 | fi 31 | echo "$response" 32 | } 33 | 34 | remote_url_property=remote.origin.url 35 | if [ -n "$($GIT config --get remote.upstream.url)" ]; then 36 | remote_url_property=remote.upstream.url 37 | fi 38 | 39 | repo_url=$($GIT config --get $remote_url_property | sed -r \ 40 | 's/.*(\@|\/\/)(.*)(\:|\/)([^:\/]*)\/([^\/]*)\.git/https:\/\/\2\/\4\/\5/') 41 | 42 | start_repo=$(echo "$repo_url" | rev | cut -d '/' -f-1 | rev) 43 | start_owner=$(echo "$repo_url" | rev | cut -d '/' -f2 | rev) 44 | if [[ $repo_url == *"github"* && -z "$start_repo" ]]; then 45 | echo 46 | echo -n -e "error: This tool must be run from within a clone " >&2 47 | echo -e "of a GitHub repo. Stopping." >&2 48 | exit 1; 49 | fi 50 | 51 | # shellcheck disable=SC2153 52 | other_repo=$(echo "$OTHER_REPO" | rev | cut -d '/' -f-1 | rev) 53 | other_owner=$(echo "$OTHER_REPO" | rev | cut -d '/' -f2 | rev) 54 | 55 | if ! [[ -x "$(command -v gh)" ]]; then 56 | echo 57 | echo -ne "error: You need the GitHub CLI " >&2 58 | echo "installed in order to use this tool. Stopping." >&2 59 | exit 1; 60 | fi 61 | if ! [[ -x "$(command -v jq)" ]]; then 62 | echo 63 | echo -ne "error: You need jq " >&2 64 | echo "installed in order to use this tool. Stopping." >&2 65 | exit 1; 66 | fi 67 | all_commits=$(mktemp) 68 | commits_with_notes=$(mktemp) 69 | commits_without_notes=$(mktemp) 70 | $GIT log --pretty=format:"%H" > "$all_commits" 71 | $GIT notes list | xargs | tr " " "\n" > "$commits_with_notes" 72 | if [[ -s "$commits_with_notes" ]]; then 73 | $GREP -Fxv -f "$commits_with_notes" "$all_commits" \ 74 | > "$commits_without_notes" 75 | else 76 | commits_without_notes=$all_commits 77 | fi 78 | total_commits_count=$(wc -l < "$commits_without_notes" | xargs) 79 | if [ "$total_commits_count" -eq "0" ]; then 80 | commits_without_notes=$all_commits 81 | fi 82 | current_commit_number=0 83 | 84 | add_note() { 85 | ((current_commit_number++)) 86 | printf "%${#total_commits_count}d" "$current_commit_number" 87 | echo -n "/$total_commits_count " 88 | short_commit_hash=$(git rev-parse --short "$1") 89 | echo -n "$short_commit_hash " 90 | commit=$(gh_api "/repos/$owner/$repo/commits/$1") 91 | if [[ $(echo "$commit" | jq ".status" 2>/dev/null) == '"422"' ]]; then 92 | echo -n "https://github.com/$owner/$repo/commit/$short_commit_hash" 93 | attention "Failed to fetch commit from GitHub; re-trying" 94 | add_note "$1" 95 | fi 96 | pull_request=$(gh_api "/repos/$owner/$repo/commits/$1/pulls" 2>/dev/null) 97 | if [[ "$pull_request" == "[]" 98 | || $(echo "$pull_request" \ 99 | | jq ".status" 2>/dev/null) == '"422"' ]]; then 100 | if [[ -n "$other_repo" ]]; then 101 | other_commit=$(gh_api \ 102 | "/repos/$other_owner/$other_repo/commits/$1" 2>/dev/null) 103 | if [[ $(echo "$other_commit" \ 104 | | jq ".status" 2>/dev/null) != '"422"' ]]; then 105 | pull_request=$(gh_api \ 106 | "/repos/$other_owner/$other_repo/commits/$1/pulls" 2>/dev/null) 107 | owner="$other_owner"; repo="$other_repo" 108 | fi 109 | fi 110 | fi 111 | echo -n "https://github.com/$owner/$repo/commit/$short_commit_hash" 112 | committer=$(echo "$commit" | jq ".committer.login") 113 | committer=$(echo "$committer" | tr -d '"') 114 | # .author.login for commits is sometimes null; so, first get it from PR 115 | author=$(echo "$pull_request" | jq ".[0].user.login") 116 | if [[ "$author" == "null" ]]; then 117 | author=$(echo "$commit" | jq ".author.login") 118 | fi 119 | author=$(echo "$author" | tr -d '"') 120 | author_email=$(echo "$commit" | jq ".commit.author.email") 121 | author_email=$(echo "$author_email" | tr -d '"') 122 | author_first_commit=$(git log \ 123 | --format="%H" --no-use-mailmap --author="$author_email" | tail -1) 124 | if [[ "$author" != "null" ]]; then 125 | if [[ "$author_first_commit" == "$1" ]]; then 126 | $GIT notes append --no-separator \ 127 | -m "Author: https://github.com/$author 🔰" "$1" 128 | else 129 | $GIT notes append --no-separator \ 130 | -m "Author: https://github.com/$author" "$1" 131 | fi 132 | elif [[ "$committer" != "null" ]]; then 133 | # Some GitHub commits actually have neither a GitHub profile link for 134 | # the commit author, nor a GitHub profile link for the committer; 135 | # example: https://github.com/SerenityOS/serenity/commit/800242ed4e4 136 | $GIT notes append --no-separator \ 137 | -m "Committer: https://github.com/$committer" "$1" 138 | fi 139 | $GIT notes append --no-separator -m \ 140 | "Commit: https://github.com/$owner/$repo/commit/$short_commit_hash" "$1" 141 | if [[ "$pull_request" == "[]" 142 | || $(echo "$pull_request" \ 143 | | jq ".status" 2>/dev/null) == '"422"' ]]; then 144 | echo " ✅" 145 | return 0 146 | fi 147 | pr_number=$(echo "$pull_request" | jq '.[0].number' 2>/dev/null) 148 | if [[ -n "$pr_number" && "$pr_number" != "null" ]]; then 149 | $GIT notes append --no-separator -m \ 150 | "Pull-request: https://github.com/$owner/$repo/pull/$pr_number" "$1" 151 | # shellcheck disable=SC2016 152 | query='query ($owner: String!, $repo: String!, $pr: Int!) { 153 | repository(owner: $owner, name: $repo) { 154 | pullRequest(number: $pr) { 155 | closingIssuesReferences(first: 100) { 156 | nodes { 157 | number 158 | } 159 | } 160 | } 161 | } 162 | }' 163 | issues_response=$(gh_api graphql \ 164 | -F owner="$owner" -F repo="$repo" -F pr="$pr_number" \ 165 | -f query="$query") 166 | return_code=$? 167 | if [[ "$return_code" -ne 0 ]]; then 168 | attention "Failed to fetch issues from GitHub; re-trying" 169 | $GIT notes remove "$short_commit_hash" 2>/dev/null 170 | add_note "$short_commit_hash" 171 | fi 172 | issues=$(echo "$issues_response" | jq \ 173 | '.data.repository.pullRequest.closingIssuesReferences.nodes[].number' \ 174 | 2>/dev/null 175 | ) 176 | if [[ -n "$issues" ]]; then 177 | for issue in $issues; do 178 | $GIT notes append --no-separator \ 179 | -m "Issue: https://github.com/$owner/$repo/issues/$issue" "$1" 180 | done; 181 | fi 182 | reviewer_data_file=$(mktemp) 183 | reviews=$(gh_api \ 184 | "/repos/$owner/$repo/pulls/$pr_number/reviews") 185 | if [[ $(echo "$reviews" | jq ".status" 2>/dev/null) == '"404"' ]]; then 186 | attention "Failed to fetch reviews from GitHub; re-trying" 187 | $GIT notes remove "$1" 2>/dev/null 188 | add_note "$1" 189 | fi 190 | reviews=$(echo "$reviews" | jq "sort_by(.state)") 191 | if [[ -n "$reviews" ]]; then 192 | echo "$reviews" | jq -c -r '.[]' | while read -r review; do 193 | reviewer=$(echo "$review" | jq -r ".user.login") 194 | state=$(echo "$review" | jq -r ".state") 195 | association=$(echo "$review" | jq -r ".author_association") 196 | case "$association" in 197 | OWNER | MEMBER | COLLABORATOR | OUTSIDE_COLLABORATOR | CONTRIBUTOR) 198 | if [[ -z $($GREP "$reviewer" "$reviewer_data_file") \ 199 | && "$reviewer" != "$author" 200 | && "github-actions[bot]" != "$reviewer" ]]; then 201 | if [[ "$state" == "APPROVED" ]]; then 202 | echo "Reviewed-by: https://github.com/$reviewer ✅" \ 203 | >> "$reviewer_data_file"; 204 | else 205 | echo "Reviewed-by: https://github.com/$reviewer" >> \ 206 | "$reviewer_data_file"; 207 | fi 208 | fi 209 | esac 210 | done 211 | reviewer_data=$(sort "$reviewer_data_file" | uniq) 212 | echo "$reviewer_data" > "$reviewer_data_file" 213 | [[ -s "$reviewer_data_file" ]] && while read -r line; do 214 | [[ -n "$line" ]] && $GIT notes append --no-separator -m "$line" "$1" 215 | done < "$reviewer_data_file" 216 | fi 217 | fi 218 | echo " ✅" 219 | } 220 | if [[ $total_commits_count == "0" ]]; then 221 | echo "Found no commits to process. Stopping." 222 | exit 0 223 | fi 224 | if [[ -z "$*" ]]; then 225 | s="s" && [[ $total_commits_count == "1" ]] && s="" 226 | echo "Found $total_commits_count commit$s to process." 227 | echo 228 | while read -r sha; do 229 | owner=$start_owner 230 | repo=$start_repo 231 | if [[ -z $($GIT notes show "$sha" 2>/dev/null) ]]; then 232 | add_note "$sha" 233 | fi 234 | done < "$commits_without_notes" 235 | else 236 | total_commits_count=$# 237 | s="s" && [[ $total_commits_count == "1" ]] && s="" 238 | echo "Found $total_commits_count commit$s to process." 239 | echo 240 | for sha in "$@"; do 241 | owner=$start_owner 242 | repo=$start_repo 243 | if [[ -z $($GIT notes show "$sha" 2>/dev/null) ]]; then 244 | add_note "$sha" 245 | fi 246 | done 247 | fi 248 | --------------------------------------------------------------------------------