Skip to content

Commit

Permalink
refactor: 'less' notification viewing logic (#86)
Browse files Browse the repository at this point in the history
* refactor: Extract view_in_pager function from select_notif

* chore: Refactor gh_rest_api function usage and improve conditional checks

* docs: Update comment for clarity in view_in_pager function

* chore: update debug log path and remove redirection

* chore: redirect stdout and stderr to terminal and file

* fix: correct API endpoint for comment count

* fix: protect against '.updated_at' being null

* fix: surround title with escaped quotes in discussion GraphQL query
  • Loading branch information
LangLangBart authored Jul 17, 2024
1 parent 3722449 commit de732ab
Showing 1 changed file with 87 additions and 46 deletions.
133 changes: 87 additions & 46 deletions gh-notify
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ set -o errexit -o nounset -o pipefail
# NotificationReason:
# assign, author, comment, invitation, manual, mention, review_requested, security_alert, state_change, subscribed, team_mention, ci_activity
# NotificationSubjectTypes:
# CheckSuite, Commit, Discussion, Issue, PullRequest, Release,
# RepositoryVulnerabilityAlert, ...
# CheckSuite, Commit, Discussion, Issue, PullRequest, Release, RepositoryVulnerabilityAlert, ...

# ====================== set variables =======================

Expand Down Expand Up @@ -43,7 +42,7 @@ export GH_NOTIFY_PER_PAGE_LIMIT=50
# Assign 'GH_NOTIFY_DEBUG_MODE' with 'true' to see more information
export GH_NOTIFY_DEBUG_MODE=${GH_NOTIFY_DEBUG_MODE:-false}
if $GH_NOTIFY_DEBUG_MODE; then
export gh_notify_debug_log="${BASH_SOURCE%/*}/gh_notify_debug.log"
export gh_notify_debug_log="${BASH_SOURCE[0]%/*}/gh_notify_debug.log"

# Tell the user where we saved the debug information
trap 'echo [DEBUG] $gh_notify_debug_log' EXIT
Expand All @@ -54,7 +53,8 @@ if $GH_NOTIFY_DEBUG_MODE; then
# Unset GH_FORCE_TTY to avoid unnecessary color codes in the debug file
unset GH_FORCE_TTY

# Redirect stdout and stderr to the terminal and a file
# Redirect stdout and stderr to the terminal and a file, in fzf 0.52.0+ the UI is no longer
# written to stderr: https://github.com/junegunn/fzf/discussions/3792
exec &> >(tee -a "$gh_notify_debug_log")

# [DISABLED] 'GH_DEBUG' sends the output to file descriptor 2, but these error messages can be
Expand Down Expand Up @@ -189,9 +189,13 @@ done

# ===================== helper functions ==========================

gh_rest_api() {
command gh api --header "$GH_REST_API_VERSION" --method GET --cache=0s "$@"
}

get_notifs() {
local page_num="$1"
command gh api --header "$GH_REST_API_VERSION" --method GET notifications --cache=0s \
gh_rest_api notifications \
--field per_page="$GH_NOTIFY_PER_PAGE_LIMIT" --field page="$page_num" \
--field participating="$only_participating_flag" --field all="$include_all_flag" \
--jq \
Expand All @@ -207,7 +211,15 @@ get_notifs() {
def colored(text; color):
colors[color] + text + colors.reset;
.[] | {
updated_short: .updated_at | fromdateiso8601 | strftime("%Y-%m"),
updated_short:
# for some reason ".updated_at" can be null
if .updated_at then
.updated_at | fromdateiso8601 | strftime("%Y-%m")
else
# Github Discussion launched in 2020
# https://resources.github.com/devops/process/planning/discussions/
"2020"
end,
# UTC time ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ
# https://docs.github.com/en/rest/overview/resources-in-the-rest-api#timezones
iso8601: now | strftime("%Y-%m-%dT%H:%M:%SZ"),
Expand All @@ -217,15 +229,21 @@ get_notifs() {
repo_full_name: .repository.full_name,
unread_symbol: colored((if .unread then "\u25cf" else "\u00a0" end); "magenta"),
# make sure each outcome has an equal number of fields separated by spaces
timefmt: colored(((if .unread then .last_read_at // .updated_at else .updated_at end) | fromdateiso8601) as $time_sec |
# difference is less than one hour
if ((now - $time_sec) / 3600) < 1 then
(now - $time_sec) / 60 | floor | tostring + "min ago"
# difference is less than 24 hours
elif ((now - $time_sec) / 3600) < 24 then
(now - $time_sec) / 3600 | floor | tostring + "h ago"
timefmt: colored(
# for some reason ".updated_at" can be null
if (.unread and .last_read_at) or .updated_at then
((if .unread then .last_read_at // .updated_at else .updated_at end) | fromdateiso8601) as $time_sec |
# difference is less than one hour
if ((now - $time_sec) / 3600) < 1 then
(now - $time_sec) / 60 | floor | tostring + "min ago"
# difference is less than 24 hours
elif ((now - $time_sec) / 3600) < 24 then
(now - $time_sec) / 3600 | floor | tostring + "h ago"
else
$time_sec | strflocaltime("%d/%b %H:%M")
end
else
$time_sec | strflocaltime("%d/%b %H:%M")
"Not available"
end; "gray"),
owner_abbreviated: colored(
if (.repository.owner.login | length) > 10 then
Expand Down Expand Up @@ -337,11 +355,8 @@ process_url() {
# https://blog.cuviper.com/2013/11/10/how-short-can-git-abbreviate/
command basename "$url" | command head -c 12
elif command grep -q "Release" <<<"$type"; then
if IFS=$'\t' read -r number prerelease < <(command gh api "$url" \
--cache=100h \
--header "$GH_REST_API_VERSION" \
--method GET \
--jq '[.tag_name, .prerelease] | @tsv'); then
if IFS=$'\t' read -r number prerelease < <(gh_rest_api "$url" \
--cache=100h --jq '[.tag_name, .prerelease] | @tsv'); then
if "$prerelease"; then
echo "$number Pre-release"
else
Expand Down Expand Up @@ -372,7 +387,7 @@ process_discussion() {
command gh api graphql \
--cache=100h \
--raw-field query="$graphql_query_discussion" \
--raw-field filter="$title in:title updated:>=$updated_short repo:$repo_full_name" \
--raw-field filter="\"$title\" in:title updated:>=$updated_short repo:$repo_full_name" \
--jq '.data.search.nodes | "#\(.[].number)"' || die "Failed GraphQL discussion query."
}

Expand Down Expand Up @@ -424,16 +439,16 @@ open_in_browser() {

view_notification() {
local all_comments date time repo_full_name type number
if [ "$1" = "--all_comments" ]; then
if [[ $1 == "--all_comments" ]]; then
shift
all_comments="1"
fi
IFS=' ' read -r _ _ _ _ repo_full_name _ date time _ type number _ <<<"$1"
printf "[%s %s - %s]\n" "$date" "$time" "$type"
case "$type" in
Commit)
command gh api --header "$GH_REST_API_VERSION" --cache=24h \
--method GET "repos/$repo_full_name/commits/$number" --jq '.files[].patch' | highlight_output
gh_rest_api --cache=24h "repos/$repo_full_name/commits/$number" \
--jq '.files[].patch' | highlight_output
;;
Issue)
# use the '--comments' flag only if 'all_comments' exists and is not null
Expand All @@ -451,41 +466,66 @@ view_notification() {
esac
}

view_in_pager() {
local repo_full_name type number unhashed_num total_comments
local issue_or_pr="issues"
IFS=' ' read -r _ _ _ _ repo_full_name _ _ _ _ type number _ <<<"$1"
declare -a less_args
# The long option (--+…) for resetting the option to its default setting is broken in less
# version 643, so use only the short version. Ref: https://github.com/gwsw/less/issues/452
less_args=(
"--clear-screen" # to be painted from the top line down
"--RAW-CONTROL-CHARS" # Raw color codes in output (don't remove color codes)
"-+F" # disable exiting if the entire file can be displayed on the screen
"-+X" # reset screen clearing prevention
)

# Move to the end of the file only for Issues or PRs that have comments.
case "$type" in
Issue | PullRequest)
unhashed_num=$(command tr -d "#" <<<"$number")
[[ $type == "PullRequest" ]] && issue_or_pr="pulls"

if total_comments=$(gh_rest_api \
"repos/${repo_full_name}/${issue_or_pr}/${unhashed_num}" \
--jq '.comments' 2>/dev/null); then
if ((total_comments > 0)); then
less_args+=(
"+G" # start at the end of the file
)
fi
fi
;;
esac

# Redirect 'less' output to '/dev/tty' to interact with the terminal when in command
# substitution '$()'. Ref: https://github.com/junegunn/fzf/issues/1360#issuecomment-966054123
view_notification --all_comments "$1" | command less "${less_args[@]}" >/dev/tty
}

mark_all_read() {
local iso_time
IFS=' ' read -r iso_time _ <<<"$1"
# https://docs.github.com/en/rest/activity/notifications#mark-notifications-as-read
command gh api --silent --header "$GH_REST_API_VERSION" --method PUT notifications \
gh_rest_api --silent --method PUT notifications \
--raw-field last_read_at="$iso_time" --field read=true
}

mark_individual_read() {
local thread_id thread_state
IFS=' ' read -r _ thread_id thread_state _ <<<"$1"
if [ "$thread_state" = "UNREAD" ]; then
command gh api --silent --header "$GH_REST_API_VERSION" --method PATCH "notifications/threads/${thread_id}"
if [[ $thread_state == "UNREAD" ]]; then
gh_rest_api --silent --method PATCH "notifications/threads/${thread_id}"
fi
}

select_notif() {
declare -a less_args
# The long option (--+…) for resetting the option to its default setting is broken in
# less version 643, so only use the short version.
# Ref: https://github.com/gwsw/less/issues/452
less_args=(
"--clear-screen" # to be painted from the top line down
"--RAW-CONTROL-CHARS" # Raw color codes in output (don't remove color codes)
"-+F" # reset exiting if the entire file can be displayed on the first screen
"-+X" # reset screen clearing prevention
)

local output expected_key selected_line repo_full_name type num
# make functions available in child processes
# 'SHELL="$(which bash)"' is needed to use exported functions when the default shell
# is not bash
# Export functions to child processes. 'fzf' executes commands with $SHELL -c; to ensure
# compatibility when the default shell is not bash, set 'SHELL="$(which bash)"'.
export -f print_help_text print_notifs get_notifs
export -f process_page process_discussion process_url
export -f highlight_output open_in_browser view_notification
export -f process_page process_discussion process_url gh_rest_api
export -f highlight_output open_in_browser view_notification view_in_pager
export -f mark_all_read mark_individual_read
# The 'die' function is not exported because 'fzf' warns you about the error in
# a failed 'print_notifs' call, but does not display the message.
Expand All @@ -505,7 +545,7 @@ select_notif() {
--bind "${GH_NOTIFY_VIEW_PATCH_KEY}:toggle-preview+change-preview:if command grep -q PullRequest <<<{10}; then command gh pr diff {11} --patch --repo {5} | highlight_output; else view_notification {}; fi" \
--bind "${GH_NOTIFY_RELOAD_KEY}:reload:print_notifs || true" \
--bind "${GH_NOTIFY_MARK_READ_KEY}:execute-silent(mark_individual_read {})+reload:print_notifs || true" \
--bind "${GH_NOTIFY_VIEW_KEY}:execute:view_notification --all_comments {} | less ${less_args[*]} >/dev/tty" \
--bind "${GH_NOTIFY_VIEW_KEY}:execute:view_in_pager {}" \
--bind "${GH_NOTIFY_TOGGLE_PREVIEW_KEY}:toggle-preview+change-preview:view_notification {}" \
--bind "${GH_NOTIFY_TOGGLE_HELP_KEY}:toggle-preview+change-preview:print_help_text" \
--border horizontal \
Expand Down Expand Up @@ -543,7 +583,8 @@ select_notif() {
command gh issue comment "$num" --repo "$repo_full_name"
mark_individual_read "$selected_line" || die "Failed to mark the notification as read."
else
printf "Writing comments is only supported for %bIssues%b and %bPullRequests%b.\n" "$WHITE_BOLD" "$NC" "$WHITE_BOLD" "$NC"
printf "Writing comments is only supported for %bIssues%b and %bPullRequests%b.\n" \
"$WHITE_BOLD" "$NC" "$WHITE_BOLD" "$NC"
exit 1
fi
;;
Expand Down Expand Up @@ -661,7 +702,7 @@ gh_notify() {
break
fi
done
if [ -z "$python_executable" ]; then
if [[ -z $python_executable ]]; then
die "install 'python' or use the -s flag"
fi

Expand All @@ -673,7 +714,7 @@ gh_notify() {
fi

notifs="$(print_notifs)"
if [ -z "$notifs" ]; then
if [[ -z $notifs ]]; then
echo "$FINAL_MSG"
exit 0
elif ! $print_static_flag; then
Expand Down

0 comments on commit de732ab

Please sign in to comment.