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..ba5cc29 --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +## git-gloss ✨ makes git logs show PR/issue/reviewer links + +`git-gloss` automatically adds [git notes](https://web.archive.org/web/20180121193320/http://git-scm.com/blog/2010/08/25/notes.html) to all your git logs — with GitHub PR/issue/reviewer/author links. + +### How to use git-gloss + +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. + +Then, when you run `git log`, the log output for each commit will look something like this: + +``` +commit 9812031a02e539f08a6936e9c17d919a44c912b8 +Author: Jonatan Klemets +Date: 12 months ago + + LibWeb: Implement spec-compliant integer parsing + + This patch adds two new methods named `parse_integer` and `parse_non_negative_integer` + inside the `Web::HTML` namespace that uses `StringUtils` under the hood but adds a bit + more logic to make it spec compliant. + +Notes: + Author: https://github.com/Jon4t4n 🔰 + Commit: https://github.com/SerenityOS/serenity/commit/9812031a02 + Pull-request: https://github.com/SerenityOS/serenity/pull/20140 + Issue: https://github.com/SerenityOS/serenity/issues/19937 + Reviewed-by: https://github.com/AtkinsSJ ✅ + Reviewed-by: https://github.com/nico +``` + +🔰 – indicates this is author’s first commit to the repo\ +✅ – indicates a review approval + +#### How long does it take? + +`git-gloss` can process at most 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 at least 45 minutes 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. + +#### How to fix errors + +`git-gloss` doesn’t yet have error handling for the case where a call to the GitHub API fails. So you can keep a log of its output, and review it after `git-gloss` finishes. You can create a log by invoking `git-gloss` like this: + +``` +git-gloss 2>&1 | tee $(mktemp -p .) +``` + +That creates a log file in the current directory with a name such as `tmp.gndYHHPKgA` — using the `mktemp` and `tee` utilities, which are standard in any Linux/Unix environment (including the macOS Terminal/shell environment). + +After `git-log` finishes (or even as it’s running), you can review the log for error output that looks like this: + +``` +1320/56162 https://github.com/LadybirdBrowser/ladybird/commit/67c727177e +jq: parse error: Invalid numeric literal at line 1, column 314 +jq: parse error: Invalid numeric literal at line 1, column 314 +jq: error (at :1): Cannot index object with number +jq: parse error: Invalid numeric literal at line 1, column 315 +``` + +For each case like that which you see in the log output, you’ll need to complete the following steps: + +1. Remove any note which `git-gloss` may have added for the given commit: + + ``` + git notes remove 67c727177e + ``` + +2. (Re)add a note for the given commit, by running `git-gloss` with the commit hash specified: + + ``` + git-gloss 67c727177e + ``` + +#### How to share the notes + +Once `git-gloss` finishes running, here’s how you can share the notes with everyone in your GitHub project: + +1. Push the notes back to your project remote at GitHub by running this command: + + ``` + git push origin 'refs/notes/*' + ``` + +2. Others in your project can then fetch the notes from GitHub by running this command: + + ``` + git fetch origin 'refs/notes/*:refs/notes/*' + ``` + + Alternatively, rather than running the above command manually, others in the project can update their git configuration by runing the following command; + + ``` + git config --add remote.origin.fetch 'refs/notes/*:refs/notes/*' + ``` + + That will cause all notes to be fetched from the remote every time they use `git fetch` or `git pull`. + +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)); 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. + +* `OTHER_OTHER_REPO` + + You can use this in addition to `OTHER_REPO` — if you have a third repo with a shared history; example: + + ``` + export OTHER_OTHER_REPO=LadybirdBrowser/ancient-history + ``` + +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_OTHER_REPO=LadybirdBrowser/ancient-history OTHER_REPO=SerenityOS/serenity \ + GREP=rg GIT=/opt/homebrew/bin/git ./git-gloss +``` + +## Make a “git gloss” command + +Clone the `git-gloss` repo and add its directory to your `$PATH`: + + ```bash + git clone https://github.com/sideshowbarker/git-gloss.git + cd git-gloss + echo export PATH=\"$PATH:$PWD\" >> ~/.bash_profile + ``` + + Now you can just type `git gloss` in any repo/clone directory, to add notes to the logs for that repo. diff --git a/git-gloss b/git-gloss new file mode 100755 index 0000000..cd3b8c9 --- /dev/null +++ b/git-gloss @@ -0,0 +1,157 @@ +#!/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/') + +startRepo=$(echo "$repoURL" | rev | cut -d '/' -f-1 | rev) +startOwner=$(echo "$repoURL" | rev | cut -d '/' -f2 | rev) +if [[ $repoURL == *"github"* && -z "$startRepo" ]]; 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 + +otherRepo=$(echo "$OTHER_REPO" | rev | cut -d '/' -f-1 | rev) +otherOwner=$(echo "$OTHER_REPO" | rev | cut -d '/' -f2 | rev) +otherOtherRepo=$(echo "$OTHER_OTHER_REPO" | rev | cut -d '/' -f-1 | rev) +otherOtherOwner=$(echo "$OTHER_OTHER_REPO" | rev | cut -d '/' -f2 | rev) + +if ! [[ -x "$(command -v jq)" ]]; then + echo + echo -e \ + "Error: You must have jq installed in order to use this tool." >&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 +totalCommitsCount=$(wc -l < "$commitsWithoutNotes" | xargs) +if [ "$totalCommitsCount" -eq "0" ]; then + commitsWithoutNotes=$allCommits +fi +echo "Found $totalCommitsCount commits to process." +echo +currentCommitNumber=0 + +addNote () { + sleep .1 + ((currentCommitNumber++)) + printf "%${#totalCommitsCount}d" "$currentCommitNumber" + echo -n "/$totalCommitsCount " + shortCommitHash=$(git rev-parse --short "$1") + echo "https://github.com/$owner/$repo/commit/$shortCommitHash" + commit=$(gh api "/repos/$owner/$repo/commits/$1" 2>&1) + pullRequest=$(gh api "/repos/$owner/$repo/commits/$1/pulls" 2>&1) + if [[ "$pullRequest" == "[]" + || $(echo "$pullRequest" | jq ".status" 2>/dev/null) == '"422"' ]]; then + if [[ -n "$otherRepo" ]]; then + otherCommit=$(gh api "/repos/$otherOwner/$otherRepo/commits/$1" 2>&1) + if [[ $(echo "$otherCommit" | jq ".status" 2>/dev/null) != '"422"' ]]; then + pullRequest=$(gh api "/repos/$otherOwner/$otherRepo/commits/$1/pulls" 2>&1) + owner="$otherOwner"; repo="$otherRepo" + elif [[ -n "$otherOtherRepo" ]]; then + otherOtherCommit=$(gh api "/repos/$otherOtherOwner/$otherOtherRepo/commits/$1" 2>&1) + if [[ $(echo "$otherOtherCommit" | jq ".status" 2>/dev/null) != '"422"' ]]; then + pullRequest=$(gh api "/repos/$otherOtherOwner/$otherOtherRepo/commits/$1/pulls" 2>&1) + owner="$otherOtherOwner"; repo="$otherOtherRepo" + fi + fi + fi + fi + author=$(echo "$commit" | jq ".author.login" | tr -d '"') + authorEmail=$(echo "$commit" | jq ".commit.author.email" | tr -d '"') + 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/$author 🔰" "$1" + else + $GIT notes append --no-separator -m "Author: https://github.com/$author" "$1" + fi + $GIT notes append --no-separator -m "Commit: https://github.com/$owner/$repo/commit/$shortCommitHash" "$1" + if [[ "$pullRequest" == "[]" + || $(echo "$pullRequest" | jq ".status" 2>/dev/null) == '"422"' ]]; then + return 0 + fi + prNumber=$(echo "$pullRequest" | jq '.[0].number') + if [[ -n "$prNumber" && "$prNumber" != "null" ]]; then + $GIT notes append --no-separator -m "Pull-request: https://github.com/$owner/$repo/pull/$prNumber" "$1" + # 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" != "$author" + && "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 +} +if [[ -z "$*" ]]; then + while read -r line; do + owner=$startOwner + repo=$startRepo + if [[ -z $($GIT notes show "$line" 2>/dev/null) ]]; then + addNote "$line" + fi + done < "$commitsWithoutNotes" +else + for sha in "$@"; do + owner=$startOwner + repo=$startRepo + if [[ -z $($GIT notes show "$sha" 2>/dev/null) ]]; then + addNote "$sha" + fi + done +fi