webui: add a flag to log handling errors

Michael Muré created

Change summary

api/graphql/graphql_test.go        |   2 
api/graphql/handler.go             |   6 
api/graphql/tracer.go              |  67 ++++++++++++
commands/webui.go                  |  26 +++-
doc/man/git-bug-webui.1            |   4 
doc/md/git-bug_webui.md            |   1 
misc/completion/bash/git-bug       | 177 +++++++++++++++++++------------
misc/completion/fish/git-bug       |   3 
misc/completion/powershell/git-bug |   5 
misc/completion/zsh/git-bug        |  32 +++++
10 files changed, 238 insertions(+), 85 deletions(-)

Detailed changes

api/graphql/graphql_test.go 🔗

@@ -22,7 +22,7 @@ func TestQueries(t *testing.T) {
 	_, err := mrc.RegisterDefaultRepository(repo)
 	require.NoError(t, err)
 
-	handler := NewHandler(mrc)
+	handler := NewHandler(mrc, nil)
 
 	c := client.New(handler)
 

api/graphql/handler.go 🔗

@@ -20,11 +20,15 @@ type Handler struct {
 	io.Closer
 }
 
-func NewHandler(mrc *cache.MultiRepoCache) Handler {
+func NewHandler(mrc *cache.MultiRepoCache, errorOut io.Writer) Handler {
 	rootResolver := resolvers.NewRootResolver(mrc)
 	config := graph.Config{Resolvers: rootResolver}
 	h := handler.NewDefaultServer(graph.NewExecutableSchema(config))
 
+	if errorOut != nil {
+		h.Use(&Tracer{Out: errorOut})
+	}
+
 	return Handler{
 		Handler: h,
 		Closer:  rootResolver,

api/graphql/tracer.go 🔗

@@ -0,0 +1,67 @@
+package graphql
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/99designs/gqlgen/graphql"
+
+	"github.com/MichaelMure/git-bug/util/colors"
+)
+
+// adapted from https://github.com/99designs/gqlgen/blob/master/graphql/handler/debug/tracer.go
+
+type Tracer struct {
+	Out io.Writer
+}
+
+var _ interface {
+	graphql.HandlerExtension
+	graphql.ResponseInterceptor
+} = &Tracer{}
+
+func (a Tracer) ExtensionName() string {
+	return "error tracer"
+}
+
+func (a *Tracer) Validate(schema graphql.ExecutableSchema) error {
+	return nil
+}
+
+func stringify(value interface{}) string {
+	valueJson, err := json.MarshalIndent(value, "  ", "  ")
+	if err == nil {
+		return string(valueJson)
+	}
+
+	return fmt.Sprint(value)
+}
+
+func (a Tracer) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
+	resp := next(ctx)
+
+	if len(resp.Errors) == 0 {
+		return resp
+	}
+
+	rctx := graphql.GetOperationContext(ctx)
+
+	_, _ = fmt.Fprintln(a.Out, "GraphQL Request {")
+	for _, line := range strings.Split(rctx.RawQuery, "\n") {
+		_, _ = fmt.Fprintln(a.Out, " ", colors.Cyan(line))
+	}
+	for name, value := range rctx.Variables {
+		_, _ = fmt.Fprintf(a.Out, "  var %s = %s\n", name, colors.Yellow(stringify(value)))
+	}
+
+	_, _ = fmt.Fprintln(a.Out, "  resp:", colors.Green(stringify(resp)))
+	for _, err := range resp.Errors {
+		_, _ = fmt.Fprintln(a.Out, "  error:", colors.Bold(err.Path.String()+":"), colors.Red(err.Message))
+	}
+	_, _ = fmt.Fprintln(a.Out, "}")
+	_, _ = fmt.Fprintln(a.Out)
+	return resp
+}

commands/webui.go 🔗

@@ -3,6 +3,7 @@ package commands
 import (
 	"context"
 	"fmt"
+	"io"
 	"log"
 	"net"
 	"net/http"
@@ -30,12 +31,13 @@ import (
 const webUIOpenConfigKey = "git-bug.webui.open"
 
 type webUIOptions struct {
-	host     string
-	port     int
-	open     bool
-	noOpen   bool
-	readOnly bool
-	query    string
+	host      string
+	port      int
+	open      bool
+	noOpen    bool
+	readOnly  bool
+	logErrors bool
+	query     string
 }
 
 func newWebUICommand() *cobra.Command {
@@ -52,7 +54,7 @@ Available git config:
 `,
 		PreRunE: loadRepo(env),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return runWebUI(env, options, args)
+			return runWebUI(env, options)
 		},
 	}
 
@@ -64,12 +66,13 @@ Available git config:
 	flags.BoolVar(&options.noOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser")
 	flags.IntVarP(&options.port, "port", "p", 0, "Port to listen to (default to random available port)")
 	flags.BoolVar(&options.readOnly, "read-only", false, "Whether to run the web UI in read-only mode")
+	flags.BoolVar(&options.logErrors, "log-errors", false, "Whether to log errors")
 	flags.StringVarP(&options.query, "query", "q", "", "The query to open in the web UI bug list")
 
 	return cmd
 }
 
-func runWebUI(env *Env, opts webUIOptions, args []string) error {
+func runWebUI(env *Env, opts webUIOptions) error {
 	if opts.port == 0 {
 		var err error
 		opts.port, err = freeport.GetFreePort()
@@ -106,7 +109,12 @@ func runWebUI(env *Env, opts webUIOptions, args []string) error {
 		return err
 	}
 
-	graphqlHandler := graphql.NewHandler(mrc)
+	var errOut io.Writer
+	if opts.logErrors {
+		errOut = env.err
+	}
+
+	graphqlHandler := graphql.NewHandler(mrc, errOut)
 
 	// Routes
 	router.Path("/playground").Handler(playground.Handler("git-bug", "/graphql"))

doc/man/git-bug-webui.1 🔗

@@ -41,6 +41,10 @@ Available git config:
 \fB--read-only\fP[=false]
 	Whether to run the web UI in read-only mode
 
+.PP
+\fB--log-errors\fP[=false]
+	Whether to log errors
+
 .PP
 \fB-q\fP, \fB--query\fP=""
 	The query to open in the web UI bug list

doc/md/git-bug_webui.md 🔗

@@ -22,6 +22,7 @@ git-bug webui [flags]
       --no-open        Prevent the automatic opening of the web UI in the default browser
   -p, --port int       Port to listen to (default to random available port)
       --read-only      Whether to run the web UI in read-only mode
+      --log-errors     Whether to log errors
   -q, --query string   The query to open in the web UI bug list
   -h, --help           help for webui
 ```

misc/completion/bash/git-bug 🔗

@@ -56,7 +56,7 @@ __git-bug_get_completion_results() {
         directive=0
     fi
     __git-bug_debug "The completion directive is: ${directive}"
-    __git-bug_debug "The completions are: ${out[*]}"
+    __git-bug_debug "The completions are: ${out}"
 }
 
 __git-bug_process_completion_results() {
@@ -89,13 +89,18 @@ __git-bug_process_completion_results() {
         fi
     fi
 
+    # Separate activeHelp from normal completions
+    local completions=()
+    local activeHelp=()
+    __git-bug_extract_activeHelp
+
     if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
         # File extension filtering
         local fullFilter filter filteringCmd
 
-        # Do not use quotes around the $out variable or else newline
+        # Do not use quotes around the $completions variable or else newline
         # characters will be kept.
-        for filter in ${out[*]}; do
+        for filter in ${completions[*]}; do
             fullFilter+="$filter|"
         done
 
@@ -107,7 +112,7 @@ __git-bug_process_completion_results() {
 
         # Use printf to strip any trailing newline
         local subdir
-        subdir=$(printf "%s" "${out[0]}")
+        subdir=$(printf "%s" "${completions[0]}")
         if [ -n "$subdir" ]; then
             __git-bug_debug "Listing directories in $subdir"
             pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
@@ -121,6 +126,43 @@ __git-bug_process_completion_results() {
 
     __git-bug_handle_special_char "$cur" :
     __git-bug_handle_special_char "$cur" =
+
+    # Print the activeHelp statements before we finish
+    if [ ${#activeHelp} -ne 0 ]; then
+        printf "\n";
+        printf "%s\n" "${activeHelp[@]}"
+        printf "\n"
+
+        # The prompt format is only available from bash 4.4.
+        # We test if it is available before using it.
+        if (x=${PS1@P}) 2> /dev/null; then
+            printf "%s" "${PS1@P}${COMP_LINE[@]}"
+        else
+            # Can't print the prompt.  Just print the
+            # text the user had typed, it is workable enough.
+            printf "%s" "${COMP_LINE[@]}"
+        fi
+    fi
+}
+
+# Separate activeHelp lines from real completions.
+# Fills the $activeHelp and $completions arrays.
+__git-bug_extract_activeHelp() {
+    local activeHelpMarker="_activeHelp_ "
+    local endIndex=${#activeHelpMarker}
+
+    while IFS='' read -r comp; do
+        if [ "${comp:0:endIndex}" = "$activeHelpMarker" ]; then
+            comp=${comp:endIndex}
+            __git-bug_debug "ActiveHelp found: $comp"
+            if [ -n "$comp" ]; then
+                activeHelp+=("$comp")
+            fi
+        else
+            # Not an activeHelp line but a normal completion
+            completions+=("$comp")
+        fi
+    done < <(printf "%s\n" "${out}")
 }
 
 __git-bug_handle_completion_types() {
@@ -132,17 +174,16 @@ __git-bug_handle_completion_types() {
         # If the user requested inserting one completion at a time, or all
         # completions at once on the command-line we must remove the descriptions.
         # https://github.com/spf13/cobra/issues/1508
-        local tab comp
-        tab=$(printf '\t')
+        local tab=$'\t' comp
         while IFS='' read -r comp; do
+            [[ -z $comp ]] && continue
             # Strip any description
             comp=${comp%%$tab*}
             # Only consider the completions that match
-            comp=$(compgen -W "$comp" -- "$cur")
-            if [ -n "$comp" ]; then
+            if [[ $comp == "$cur"* ]]; then
                 COMPREPLY+=("$comp")
             fi
-        done < <(printf "%s\n" "${out[@]}")
+        done < <(printf "%s\n" "${completions[@]}")
         ;;
 
     *)
@@ -153,44 +194,37 @@ __git-bug_handle_completion_types() {
 }
 
 __git-bug_handle_standard_completion_case() {
-    local tab comp
-    tab=$(printf '\t')
+    local tab=$'\t' comp
+
+    # Short circuit to optimize if we don't have descriptions
+    if [[ "${completions[*]}" != *$tab* ]]; then
+        IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur")
+        return 0
+    fi
 
     local longest=0
+    local compline
     # Look for the longest completion so that we can format things nicely
-    while IFS='' read -r comp; do
+    while IFS='' read -r compline; do
+        [[ -z $compline ]] && continue
         # Strip any description before checking the length
-        comp=${comp%%$tab*}
+        comp=${compline%%$tab*}
         # Only consider the completions that match
-        comp=$(compgen -W "$comp" -- "$cur")
+        [[ $comp == "$cur"* ]] || continue
+        COMPREPLY+=("$compline")
         if ((${#comp}>longest)); then
             longest=${#comp}
         fi
-    done < <(printf "%s\n" "${out[@]}")
-
-    local completions=()
-    while IFS='' read -r comp; do
-        if [ -z "$comp" ]; then
-            continue
-        fi
-
-        __git-bug_debug "Original comp: $comp"
-        comp="$(__git-bug_format_comp_descriptions "$comp" "$longest")"
-        __git-bug_debug "Final comp: $comp"
-        completions+=("$comp")
-    done < <(printf "%s\n" "${out[@]}")
-
-    while IFS='' read -r comp; do
-        COMPREPLY+=("$comp")
-    done < <(compgen -W "${completions[*]}" -- "$cur")
+    done < <(printf "%s\n" "${completions[@]}")
 
     # If there is a single completion left, remove the description text
     if [ ${#COMPREPLY[*]} -eq 1 ]; then
         __git-bug_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
-        comp="${COMPREPLY[0]%% *}"
+        comp="${COMPREPLY[0]%%$tab*}"
         __git-bug_debug "Removed description from single completion, which is now: ${comp}"
-        COMPREPLY=()
-        COMPREPLY+=("$comp")
+        COMPREPLY[0]=$comp
+    else # Format the descriptions
+        __git-bug_format_comp_descriptions $longest
     fi
 }
 
@@ -209,45 +243,48 @@ __git-bug_handle_special_char()
 
 __git-bug_format_comp_descriptions()
 {
-    local tab
-    tab=$(printf '\t')
-    local comp="$1"
-    local longest=$2
-
-    # Properly format the description string which follows a tab character if there is one
-    if [[ "$comp" == *$tab* ]]; then
-        desc=${comp#*$tab}
-        comp=${comp%%$tab*}
-
-        # $COLUMNS stores the current shell width.
-        # Remove an extra 4 because we add 2 spaces and 2 parentheses.
-        maxdesclength=$(( COLUMNS - longest - 4 ))
-
-        # Make sure we can fit a description of at least 8 characters
-        # if we are to align the descriptions.
-        if [[ $maxdesclength -gt 8 ]]; then
-            # Add the proper number of spaces to align the descriptions
-            for ((i = ${#comp} ; i < longest ; i++)); do
-                comp+=" "
-            done
-        else
-            # Don't pad the descriptions so we can fit more text after the completion
-            maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
-        fi
+    local tab=$'\t'
+    local comp desc maxdesclength
+    local longest=$1
+
+    local i ci
+    for ci in ${!COMPREPLY[*]}; do
+        comp=${COMPREPLY[ci]}
+        # Properly format the description string which follows a tab character if there is one
+        if [[ "$comp" == *$tab* ]]; then
+            __git-bug_debug "Original comp: $comp"
+            desc=${comp#*$tab}
+            comp=${comp%%$tab*}
+
+            # $COLUMNS stores the current shell width.
+            # Remove an extra 4 because we add 2 spaces and 2 parentheses.
+            maxdesclength=$(( COLUMNS - longest - 4 ))
+
+            # Make sure we can fit a description of at least 8 characters
+            # if we are to align the descriptions.
+            if [[ $maxdesclength -gt 8 ]]; then
+                # Add the proper number of spaces to align the descriptions
+                for ((i = ${#comp} ; i < longest ; i++)); do
+                    comp+=" "
+                done
+            else
+                # Don't pad the descriptions so we can fit more text after the completion
+                maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
+            fi
 
-        # If there is enough space for any description text,
-        # truncate the descriptions that are too long for the shell width
-        if [ $maxdesclength -gt 0 ]; then
-            if [ ${#desc} -gt $maxdesclength ]; then
-                desc=${desc:0:$(( maxdesclength - 1 ))}
-                desc+="…"
+            # If there is enough space for any description text,
+            # truncate the descriptions that are too long for the shell width
+            if [ $maxdesclength -gt 0 ]; then
+                if [ ${#desc} -gt $maxdesclength ]; then
+                    desc=${desc:0:$(( maxdesclength - 1 ))}
+                    desc+="…"
+                fi
+                comp+="  ($desc)"
             fi
-            comp+="  ($desc)"
+            COMPREPLY[ci]=$comp
+            __git-bug_debug "Final comp: $comp"
         fi
-    fi
-
-    # Must use printf to escape all special characters
-    printf "%q" "${comp}"
+    done
 }
 
 __start_git-bug()

misc/completion/fish/git-bug 🔗

@@ -18,7 +18,8 @@ function __git_bug_perform_completion
     __git_bug_debug "args: $args"
     __git_bug_debug "last arg: $lastArg"
 
-    set -l requestComp "$args[1] __complete $args[2..-1] $lastArg"
+    # Disable ActiveHelp which is not supported for fish shell
+    set -l requestComp "GIT_BUG_ACTIVE_HELP=0 $args[1] __complete $args[2..-1] $lastArg"
 
     __git_bug_debug "Calling $requestComp"
     set -l results (eval $requestComp 2> /dev/null)

misc/completion/powershell/git-bug 🔗

@@ -44,6 +44,7 @@ Register-ArgumentCompleter -CommandName 'git-bug' -ScriptBlock {
     # Prepare the command to request completions for the program.
     # Split the command at the first space to separate the program and arguments.
     $Program,$Arguments = $Command.Split(" ",2)
+
     $RequestComp="$Program __completeNoDesc $Arguments"
     __git-bug_debug "RequestComp: $RequestComp"
 
@@ -73,11 +74,13 @@ Register-ArgumentCompleter -CommandName 'git-bug' -ScriptBlock {
     }
 
     __git-bug_debug "Calling $RequestComp"
+    # First disable ActiveHelp which is not supported for Powershell
+    $env:GIT_BUG_ACTIVE_HELP=0
+
     #call the command store the output in $out and redirect stderr and stdout to null
     # $Out is an array contains each line per element
     Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null
 
-
     # get directive from last line
     [int]$Directive = $Out[-1].TrimStart(':')
     if ($Directive -eq "") {

misc/completion/zsh/git-bug 🔗

@@ -1,4 +1,4 @@
-#compdef _git-bug git-bug
+#compdef git-bug
 
 # zsh completion for git-bug                              -*- shell-script -*-
 
@@ -86,7 +86,24 @@ _git-bug()
         return
     fi
 
+    local activeHelpMarker="_activeHelp_ "
+    local endIndex=${#activeHelpMarker}
+    local startIndex=$((${#activeHelpMarker}+1))
+    local hasActiveHelp=0
     while IFS='\n' read -r comp; do
+        # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker)
+        if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then
+            __git-bug_debug "ActiveHelp found: $comp"
+            comp="${comp[$startIndex,-1]}"
+            if [ -n "$comp" ]; then
+                compadd -x "${comp}"
+                __git-bug_debug "ActiveHelp will need delimiter"
+                hasActiveHelp=1
+            fi
+
+            continue
+        fi
+
         if [ -n "$comp" ]; then
             # If requested, completions are returned with a description.
             # The description is preceded by a TAB character.
@@ -94,7 +111,7 @@ _git-bug()
             # We first need to escape any : as part of the completion itself.
             comp=${comp//:/\\:}
 
-            local tab=$(printf '\t')
+            local tab="$(printf '\t')"
             comp=${comp//$tab/:}
 
             __git-bug_debug "Adding completion: ${comp}"
@@ -103,6 +120,17 @@ _git-bug()
         fi
     done < <(printf "%s\n" "${out[@]}")
 
+    # Add a delimiter after the activeHelp statements, but only if:
+    # - there are completions following the activeHelp statements, or
+    # - file completion will be performed (so there will be choices after the activeHelp)
+    if [ $hasActiveHelp -eq 1 ]; then
+        if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then
+            __git-bug_debug "Adding activeHelp delimiter"
+            compadd -x "--"
+            hasActiveHelp=0
+        fi
+    fi
+
     if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
         __git-bug_debug "Activating nospace."
         noSpace="-S ''"