diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8394c08
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 sideshowbarker
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b0d2e2e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,92 @@
+## git-gloss ✨
+
+Annotate git log output with GitHub PR/Issue/Reviewer metadata
+
+### How to run
+
+You can download and run `git-gloss` within a directory having a GitHub repo clone by doing this:
+
+```
+curl -fsSLO https://sideshowbarker.github.io/git-gloss/git-gloss && bash ./git-gloss
+```
+
+That will add [git notes](https://web.archive.org/web/20180121193320/http://git-scm.com/blog/2010/08/25/notes.html) locally for all commits in the local commit history with an associated GitHub pull request.
+
+It can process about 22 commits per minute — or about 1300 commits per hour.
+
+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.
+
+For example, if your repo has somewhere around 1000 commits, it’ll take about an hour to finish. If your repo has somewhere around 10,000 commits, it’ll take more than 7 hours. And so on.
+
+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 for example, if you have a repo with somewhere around 10,000 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.
+
+### Sharing the notes
+
+Once `git-gloss` finishes running, here’s how you can share the notes with everyone in your project:
+
+1. Push the notes back to your project remote by running this command:
+
+ ```
+ git push origin 'refs/notes/*'
+ ```
+
+2. Others in your project can then fetch the notes from the remote by running this command:
+
+ ```
+ git fetch origin 'refs/notes/*:refs/notes/*'
+ ```
+
+3. Run `git-gloss` again to add notes for any new commits made after the last time you ran `git-gloss`.
+
+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).
+
+### Dependencies
+
+* `git` – any version with support for the `--no-separator` option for the `git notes` command
+
+* `jq` – https://jqlang.github.io/jq/ (JSON processor)
+
+* `gh` – https://cli.github.com/ (GitHub CLI)
+
+* `grep` – on macOS in particular, a grep program other than the Apple-provided one is recommended (for instance, GNU grep or [ripgrep](https://github.com/BurntSushi/ripgrep#installation)); for example:
+
+ ```
+ brew install ripgrep
+ brew install grep
+ ```
+
+### Environment variables
+
+You can affect the `git-gloss` behavior using the following environment variables:
+
+* `GIT`
+
+ 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:
+
+ ```
+ export GIT=/opt/homebrew/bin/git
+ ```
+
+* `GREP`
+
+ 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:
+
+ ```
+ export GREP=/opt/homebrew/bin/rg
+ ```
+
+* `OTHER_REPO`
+
+ 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:
+
+ ```
+ export OTHER_REPO=SerenityOS/serenity
+ ```
+
+ 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.
+
+Rather than exporting each of those environment variables to your shell, you can instead specify them all at the same time in the invocation you use for running `git-gloss` — like this:
+
+```
+OTHER_REPO=SerenityOS/serenity GREP=rg GIT=/opt/homebrew/bin/git ./git-gloss
+```
diff --git a/git-gloss b/git-gloss
new file mode 100755
index 0000000..52ebfcc
--- /dev/null
+++ b/git-gloss
@@ -0,0 +1,125 @@
+#!/bin/bash
+
+GIT=${GIT:-git}
+GREP=${GREP:-grep}
+
+remoteURLproperty=remote.origin.url
+if [ -n "$(GIT config --get remote.upstream.url)" ]; then
+ remoteURLproperty=remote.upstream.url
+fi
+
+repoURL=$(GIT config --get $remoteURLproperty | sed -r \
+ 's/.*(\@|\/\/)(.*)(\:|\/)([^:\/]*)\/([^\/]*)\.git/https:\/\/\2\/\4\/\5/')
+
+repo=$(echo "$repoURL" | rev | cut -d '/' -f-1 | rev)
+owner=$(echo "$repoURL" | rev | cut -d '/' -f2 | rev)
+if [[ $repoURL == *"github"* && -z "$repo" ]]; then
+ echo
+ echo -n -e \
+ "Error: This tool must be run from within a clone " >&2
+ echo -e "of a GitHub repo. Stopping." >&2
+ exit 1;
+fi
+
+if ! [[ -x "$(command -v jq)" ]]; then
+ echo
+ echo -ne \
+ "Error: You must have jq installed in order to use this tool. " >&2
+ echo -e \
+ " https://jqlang.github.io/jq/download/" >&2
+ exit 1;
+fi
+
+if ! [[ -x "$(command -v gh)" ]]; then
+ echo
+ echo -ne \
+ "Error: You must have the GitHub CLI installed in order to use "
+ echo -e \
+ "this tool. https://cli.github.com/" >&2
+ exit 1;
+fi
+
+allCommits=$(mktemp)
+commitsWithNotes=$(mktemp)
+commitsWithoutNotes=$(mktemp)
+$GIT log --pretty=format:"%H" > "$allCommits"
+$GIT notes list | xargs | tr " " "\n" > "$commitsWithNotes"
+if [[ -s "$commitsWithNotes" ]]; then
+ $GREP -Fxv -f "$commitsWithNotes" "$allCommits" > "$commitsWithoutNotes"
+else
+ commitsWithoutNotes=$allCommits
+fi
+count=$(wc -l < "$commitsWithoutNotes" | xargs)
+if [ "$count" -eq "0" ]; then
+ commitsWithoutNotes=$allCommits
+fi
+echo "Found $count commits to process."
+
+addNotes () {
+ pullRequest=$(gh api "/repos/$owner/$repo/commits/$1/pulls")
+ prNumber=$(echo "$pullRequest" | jq '.[0].number')
+ prAuthor=$(echo "$pullRequest" | jq '.[0].user.login' | tr -d '"')
+ if [[ -n "$prNumber" && "$prNumber" != "null" ]]; then
+ commit=$(gh api "/repos/$owner/$repo/commits/$1")
+ authorEmail=$(echo "$commit" | jq ".commit.author.email" | tr -d '"')
+ $GIT notes append --no-separator -m "PR: https://github.com/$owner/$repo/pull/$prNumber" "$1"
+ authorFirstCommit=$(git log --format="%H" --no-use-mailmap --author="$authorEmail" | tail -1)
+ if [[ "$authorFirstCommit" == "$1" ]]; then
+ $GIT notes append --no-separator -m "Author: https://github.com/$prAuthor 🔰" "$1"
+ else
+ $GIT notes append --no-separator -m "Author: https://github.com/$prAuthor" "$1"
+ fi
+ #sleep .5
+ # shellcheck disable=SC2016
+ issues=$(gh api graphql \
+ -F owner="$owner" -F repo="$repo" -F pr="$prNumber" -f query='
+ query ($owner: String!, $repo: String!, $pr: Int!) {
+ repository(owner: $owner, name: $repo) {
+ pullRequest(number: $pr) {
+ closingIssuesReferences(first: 100) {
+ nodes {
+ number
+ }
+ }
+ }
+ }
+ }' --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number')
+ if [[ -n "$issues" ]]; then
+ for issue in $issues; do
+ $GIT notes append --no-separator -m "Issue: https://github.com/$owner/$repo/issues/$issue" "$1"
+ done;
+ fi
+ reviewerDataFile=$(mktemp)
+ reviews=$(gh api "/repos/$owner/$repo/pulls/$prNumber/reviews" 2>/dev/null | jq "sort_by(.state)")
+ if [[ -n "$reviews" && $(echo "$reviews" | jq ".status" 2>/dev/null) != '"404"' ]]; then
+ echo "$reviews" | jq -c -r '.[]' | while read -r review; do
+ reviewer=$(echo "$review" | jq -r ".user.login")
+ state=$(echo "$review" | jq -r ".state")
+ association=$(echo "$review" | jq -r ".author_association")
+ case "$association" in
+ OWNER | MEMBER | CONTRIBUTOR | COLLABORATOR | OUTSIDE_COLLABORATOR)
+ if [[ -z $($GREP "$reviewer" "$reviewerDataFile") \
+ && "$reviewer" != "$prAuthor"
+ && "github-actions[bot]" != "$reviewer" ]]; then
+ if [[ "$state" == "APPROVED" ]]; then
+ echo "Reviewed-by: https://github.com/$reviewer ✅" >> "$reviewerDataFile";
+ else
+ echo "Reviewed-by: https://github.com/$reviewer" >> "$reviewerDataFile";
+ fi
+ fi
+ esac
+ done
+ reviewerData=$(sort "$reviewerDataFile" | uniq)
+ echo "$reviewerData" > "$reviewerDataFile"
+ [[ -s "$reviewerDataFile" ]] && while read -r line; do
+ [[ -n "$line" ]] && $GIT notes append --no-separator -m "$line" "$1"
+ done < "$reviewerDataFile"
+ fi
+ fi
+}
+while read -r line; do
+ echo "https://github.com/$owner/$repo/commit/$line"
+ if [[ -z $($GIT notes show "$line" 2>/dev/null) ]]; then
+ addNotes "$line"
+ fi
+done < "$commitsWithoutNotes"