Import gerrit and jira search scripts

Change-Id: I3f52a37d1b875835a521a89a1a58d7e16966483f
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..a4e6bee
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "lf/onf-common"]
+	path = lf/onf-common
+	url = https://github.com/joey-onf/onf-common.git
diff --git a/jira/jira-search.sh b/jira/jira-search.sh
new file mode 100755
index 0000000..31c22ce
--- /dev/null
+++ b/jira/jira-search.sh
@@ -0,0 +1,623 @@
+#!/bin/bash
+## --------------------------------------------------------------------
+## Intent: Construct a jira ticket query with attributes
+## --------------------------------------------------------------------
+
+# set -euo pipefail
+#source ~/.sandbox/trainlab-common/common.sh '--common-args-begin--'
+
+##-------------------##
+##---]  GLOBALS  [---##
+##-------------------##
+declare -g -a text=()
+declare -g -a text_and=()
+declare -g -a text_or=()
+
+declare -g -a urls_raw=()
+declare -g -a urls_filt=()
+
+declare -g -a labels_incl=()
+declare -g -a labels_excl=()
+
+declare -g -a projects=()
+
+path="$(realpath $0 --canonicalize-existing)"
+source "${path%\.sh}/utils.sh"
+source "$pgmlib/fixversion.sh"
+source "$pgmlib/resolved.sh"
+
+## --------------------------------------------------------------------
+## --------------------------------------------------------------------
+function error()
+{
+    echo "ERROR ${FUNCNAME[1]}: $@"
+    exit 1
+}
+
+## -----------------------------------------------------------------------
+## -----------------------------------------------------------------------
+function html_encode()
+{
+    local -n ref=$1; shift
+    local tmp="$ref"
+
+    tmp="${tmp//[[:space:]]/%20}"
+    tmp="${tmp//\"/%22}"
+    tmp="${tmp//\'/%27}"
+
+    ref="$tmp"
+    return
+}
+
+## -----------------------------------------------------------------------
+## Intent: Insert a conjunction into the stream when prior statements exist
+## -----------------------------------------------------------------------
+function conjunction()
+{
+    return
+    
+    local -n ref=$1; shift
+    [[ $# -gt 0 ]] && { local literal="$1"; shift; }
+
+    ## -------------------------------
+    ## Conjunction if prior statements
+    ## -------------------------------
+    if [ ${#ref[@]} -gt 0 ]; then
+        if [[ -v literal ]]; then
+            ref+=("$literal")
+        elif [[ -v bool_and ]]; then
+            ref+=('AND')
+        else
+            ref+=('OR')
+        fi
+    fi
+
+    return
+}
+
+## -----------------------------------------------------------------------
+## Intent: Helper method
+## -----------------------------------------------------------------------
+## Usage : local path="$(join_by '/' 'lib' "${fields[@]}")"
+## -----------------------------------------------------------------------
+function join_by()
+{
+    local d=${1-} f=${2-}; if shift 2; then printf %s "$f" "${@/#/$d}"; fi;
+}
+
+## --------------------------------------------------------------------
+## Intent: Query by component name filter
+## --------------------------------------------------------------------
+## Value: helm-charts
+## --------------------------------------------------------------------
+function do_components()
+{
+    declare -n args=$1; shift
+    declare -n ans=$1; shift
+
+    # [ -z ${args+word} ] && { args=(); }
+    
+    if [[ ${#args[@]} -gt 0 ]]; then
+
+        local modifier
+        if [[ -v bool_not ]]; then
+            modifier='NOT IN'
+        else
+            modifier='IN'
+        fi
+        ans+=("component ${modifier} (${args[@]})")
+        # alt: comp='foo' OR comp='bar'
+    fi
+
+    return
+}
+
+## --------------------------------------------------------------------
+## Intent: Query filter by labels assigned to a ticket:
+##   o pods, failing, testing
+## --------------------------------------------------------------------
+# "project in (UKSCR, COMPRG) AND issuetype = Bug AND labels in (BAT)" and
+## --------------------------------------------------------------------
+function do_labels()
+{
+    declare -n incl=$1; shift # was args=
+    declare -n excl=$1; shift
+    declare -n ans=$1; shift
+
+    ## --------------------------------
+    ## Conjunction if stream tokens > 0
+    ## --------------------------------
+    conjunction ans
+
+    declare -a tokens=()
+
+    ## -----------------------------
+    ## -----------------------------
+    if [[ ${#incl[@]} -gt 0 ]]; then
+
+        local modifier
+        if [[ -v bool_not ]]; then
+            modifier='NOT IN'
+        else
+            modifier='IN'
+        fi
+
+        local labels=$(join_by ',' "${incl[@]}")
+        local -a tmp=(\
+                      '('\
+                          'label IS EMPTY' \
+                          'OR' \
+                          "labels ${modifier} ($labels)" \
+                          ')'\
+            )
+        tokens+=("${tmp[@]}")
+    fi
+
+    conjunction tokens 'AND'
+
+    ## -----------------------------
+    ## -----------------------------
+    if [[ ${#excl[@]} -gt 0 ]]; then
+        local labels=$(join_by ',' "${excl[@]}")
+        tokens+=('(' "labels NOT IN ($labels)" ')')
+    fi
+
+    ans+=("${tokens[@]}")
+    return
+}
+
+## --------------------------------------------------------------------
+## Intent: Modify search query by project type (SEBA, VOL)
+## --------------------------------------------------------------------
+function do_projects()
+{
+    declare -n ref=$1; shift
+
+    [[ ${#projects[@]} -eq 0 ]] && { return; }
+
+    local terms="$(join_by ',' "${projects[@]}")"
+#    local -a buffer=('(' 'project' 'IN' "($terms)" ')')
+#    ref+=("$(join_by '%20' "${buffer[@]}")")
+    ref+=("(project IN ($terms))")
+    return
+}
+
+## --------------------------------------------------------------------
+## Intent: Query by compound text filters
+## --------------------------------------------------------------------
+function do_text()
+{
+    local -n ref=$1; shift
+    local -n ans=$1; shift
+    local val
+
+    ## Accumulate
+    if [[ ${#ref[@]} -gt 0 ]]; then
+
+        if [[ -v bool_and ]]; then
+            text_and+=("${ref[@]}")
+        else
+            text_or+=("${ref[@]}")
+        fi
+    fi
+
+    ## Append terms: AND
+    if [[ ${#text_and[@]} -gt 0 ]]; then
+        declare -a term=()
+        for val in "${text_and[@]}";
+        do
+            term+=("text ~ \"$val\"")
+        done
+        val=$(join_by ' AND ' "${term[@]}")
+        ans+=("($val)")
+    fi
+
+    ## Append terms: OR
+    if [[ ${#text_or[@]} -gt 0 ]]; then
+        declare -a term=()
+        for val in "${text_or[@]}";
+        do
+            term+=("text ~ \"$val\"")
+        done
+        val=$(join_by ' OR ' "${term[@]}")
+        ans+=("($val)")
+    fi
+
+    return
+}
+
+## --------------------------------------------------------------------
+## Intent: Query by assigned or requestor
+## --------------------------------------------------------------------
+## Note: Simple for now but support query by a list of suers
+## --------------------------------------------------------------------
+function do_user()
+{
+    declare -n ans=$1; shift
+
+    [[ -v argv_nobody ]] && return
+
+    local user='currentUser()'
+    if [[ -v argv_user ]]; then
+        user="$argv_user"
+    fi
+
+    if [[ -v argv_assigned ]]; then
+        ans+=("assignee=${user}")
+    fi
+
+    if [[ -v argv_reported ]]; then
+        ans+=("reporter=${user}")
+    fi
+
+    return
+}
+
+## --------------------------------------------------------------------
+## Intent: Combine filter arguments into a search query
+## --------------------------------------------------------------------
+function gen_filter()
+{
+    declare -n ans=$1; shift
+    declare -n args=$1; shift
+
+    ## -----------------------------------
+    ## Begin by joining major search terms
+    ## -----------------------------------
+    declare -a _tmp=()
+    local val
+    for val in "${args[@]}";
+    do
+        _tmp+=("$val" 'AND')
+    done
+    unset _tmp[-1]
+
+    ## -----------------------
+    ## Massage with html codes
+    ## -----------------------
+    ans="$(join_by '%20' "${_tmp[@]}")"
+    return
+}
+
+## --------------------------------------------------------------------
+## Intent: Combine filter arguments into a search query
+## --------------------------------------------------------------------
+function gen_url()
+{
+    declare -n ans=$1; shift
+    declare -n args=$1; shift
+
+    ## Which jira server to query (?)
+    [[ ! -v server ]] && declare -g server='jira.opennetworking.org'
+    tmp_url="https://${server}/issues/?jql="
+    tmp="${tmp_url}${args}"
+    ans="${tmp// /%20}"
+    return
+}
+
+## --------------------------------------------------------------------
+## Intent: Dispaly command usage
+## --------------------------------------------------------------------
+function usage()
+{
+    cat <<EOH
+Usage: $0 VOL-xxxx
+  --debug       Enable script debug mode
+  --dry-run     Simulate
+
+  VOL-{xxxx}    View a jira ticket by ID
+
+[SERVER]
+  --onf         jira.opennetworking.org (default)
+  --opencord    jira.opencord.org
+
+[WHAT]
+  --component   Search by component name assigned to ticket
+  --label       Search by label name assigned to ticket.
+  --text        Search string(s)
+
+[FIXVERSION] - Voltha-v2.12
+  --fixversion-incl
+  --fixversion-excl
+  --fixversion-is-empty
+  --fixversion-not-empty
+
+[RESOLVED] - tokens={Declined, Won't Fix}
+  --resolved-start ccyy-mm-dd
+  --resolved-end   ccyy-mm-dd
+  --resolved-incl {token(s)}
+  --resolved-excl {token(s)}
+  --resolved-is-empty   Query for open tickets
+  --resolved-not-empty
+
+[USER(s)]
+  --me          Tickets assigned to or reported by me.
+  --user [u]    Tickets assigned to this user.
+  --nobody      Raw query, no filtering by user
+
+[BY-USER]
+  --assigned    Tickets assided to user
+  --reported    Tickets created by user
+
+[BOOL]
+  --and            Join terms using 'AND'
+  --or             Join terms using 'OR'
+
+[MEMBER]
+  --in             (default) Items belong (--component IN)
+  --not-in         Negate item set (--component NOT IN)
+
+[Contains]
+  --text     [t]   (join modifer: --and, --or)
+  --text-and [t]   All of these terms
+  --text-or  [t]   Any of these terms
+
+[RANGE]
+  --newer [d]   Search for tickets created < [n] days ago.
+  --older [d]   Search for tickets created > [n] days ago.
+
+[ALIASES]
+  --all         Query for all unresolved tickets
+
+[USAGE]
+  $0 --assigned
+     o Display all tickets assigned to my login
+  $0 --requested --user tux
+     o Display all tickets requested by user tux
+  $0 --reported --or --text 'bbsim' --text 'release'
+     o Search for tickets that contain strings bbsim or release
+  $0 --cord --text-and 'release' --text-and 'voltctl'
+     o Search jira.opencord for tickets that contain release and voltctl
+  $0 --text 'bitergia' --text 'Jira' -and
+     o Search jira.opennetworking for tickets containing string bitergia and Jira
+
+  $0 --cord --label failing --label pod
+     o Search jira.opencord for tests failing due to pod/hardware issuses.
+
+  $0 --proj VOL --fixversion "VOLTHA v2.12" --resolved-is-empty
+     o Query for unresolved release tickets
+EOH
+
+    return
+}
+
+## --------------------------------------------------------------------
+# classpath=$(join_by ':' "${mypath[@]}")
+## --------------------------------------------------------------------
+function join_by()
+{
+    local d=${1-} f=${2-}; if shift 2; then printf %s "$f" "${@/#/$d}"; fi;
+}
+
+##----------------##
+##---]  MAIN  [---##
+##----------------##
+declare -a suffix0=()
+
+# declare -g -i debug=1
+
+while [ $# -gt 0 ]; do
+
+    if [ ${#suffix0[@]} -gt 0 ]; then
+        suffix0+=('AND')
+    fi
+
+    arg="$1"; shift
+    [[ -v debug ]] && echo "** argv=[$arg] [$*]"
+
+    case "$arg" in
+
+        -*help) usage; exit 0 ;;
+
+        ##-----------------##
+        ##---]  MODES  [---##
+        ##-----------------##
+        -*debug)   declare -g -i debug=1 ;;
+        --dry-run) declare -g -i dry_run=1 ;;
+
+        ##-------------------##
+        ##---]  BY USER  [---##
+        ##-------------------##
+        --assigned) declare -g -i argv_assigned=1 ;;
+        --reported) declare -g -i argv_reported=1 ;;
+        --me)       declare -g -i argv_me=1       ;;
+        --nobody)   declare -g -i argv_nobody=1   ;;
+        --user)
+            arg="$1"; shift
+            declare -g argv_user="$arg"
+            ;;
+
+        ##------------------##
+        ##---]  SERVER  [---##
+        ##------------------##
+        -*onf) declare server='jira.opennetworking.org'; error "FOUND --onf" ;;
+        -*cord) declare server='jira.opencord.org' ;;
+
+        ##---------------------##
+        ##---]  SEARCH-BY  [---##
+        ##---------------------##
+        --component|--comp*)
+            arg="$1"; shift
+            [[ ! -v components ]] && declare -g -a components=()
+            components+=("$arg")
+            ;;
+
+        --label-excl)
+            arg="$1"; shift
+            labels_excl+=("$arg")
+            ;;
+
+        --label|--label-incl)
+            arg="$1"; shift
+            labels_incl+=("$arg")
+            ;;
+
+        ##-----------------------##
+        ##---]  Text Search  [---##
+        ##-----------------------##
+        # jsearch.sh --text-and bbsim --text-and release
+        -*text-and) text_and+=("$1"); shift ;;
+        -*text-or) text_or+=("$1");   shift ;;
+
+        # % js --and --text jenkins --text cord
+        # text ~ "Jira Software"
     # [WORDs]
+        # text ~ "\"Jira Software\""
 # [STRING]
+        -*text)
+            arg="$1"; shift
+            if [[ -v bool_and ]]; then
+                text_and+=("$arg")
+            elif [[ -v bool_or ]]; then
+                text_or+=("$arg")
+            else
+                text+=("$arg")
+            fi
+            ;;
+
+        --all) set -- '--resolved-is-none' "$@" ;; # alias: --[un-]resolved
+
+        --proj*) projects+=("$1"); shift ;;
+
+        --fixversion-*)
+            # function get_jql_fixversion()
+            case "$arg" in
+                  *excl)
+                      [[ ! -v fixversion_excl ]] && { declare -g -a fixversion_excl=(); }
+                      val="\"$1\""; shift
+                      html_encode val
+                      fixversion_excl+=("$val");
+                      ;;
+
+                  *incl)
+                      [[ ! -v fixversion_incl ]] && { declare -g -a fixversion_incl=(); }
+                      val="\"$1\""; shift
+                      html_encode val
+                      fixversion_incl+=("$val");
+                      ;;
+
+                  *not-empty) declare -g -i fixversion_not_empty=1 ;;
+                   *is-empty) declare -g -i fixversion_is_empty=1  ;;
+
+                  *) error "Detected invalid --fixversion-* modifier" ;;
+            esac
+            ;;
+
+        --resolved-*)
+            # function get_jql_reasons()
+            case "$arg" in
+
+                *start) declare -g resolved_start="$1"; shift ;;
+                  *end) declare -g resolved_end="$1";   shift ;;
+
+                *not-empty) declare -g resolved_not_empty="$1" ;;
+                    *empty) declare -g resolved_is_empty="$1"  ;;
+
+                  *excl)
+                      [[ ! -v resolved_excl ]] && { declare -g -a resolved_excl=(); }
+                      val="\"$1\""; shift
+                      html_encode val
+                      resolved_excl+=("$val");
+                      ;;
+                  *incl)
+                      [[ ! -v resolved_incl ]] && { declare -g -a resolved_incl=(); }
+                      val="\"$1\""; shift
+                      html_encode val
+                      resolved_incl+=("$val");
+                      ;;
+                 *) ;;
+                     *) error "Detected invalid --resolved-* modifier" ;;
+            esac
+            ;;
+
+        -*newer)
+            arg="$1"; shift
+            suffix0+=("created <= '-${arg}d'") ;;
+
+        -*older)
+            arg="$1"; shift
+            suffix0+=("created >= '-${arg}d'") ;;
+
+        ##----------------##
+        ##---]  BOOL  [---##
+        ##----------------##
+        --[aA][nN][dD]) declare -g -i bool_and=1 ;;
+        --[oO][rR])     declare -g -i bool_or=1  ;;
+
+        ##------------------##
+        ##---]  MEMBER  [---##
+        ##------------------##
+        --[iI][nN])     declare -g -i bool_in=1  ;;
+        --[nN][oO][tT]) declare -g -i bool_not=1 ;;
+
+        [A-Z][A-Z][A-Z]-[0-9]*)
+            case "$arg" in
+                CORD-[0-9]*)
+                    url="https://jira.opencord.org/browse/${arg}"
+                    urls_raw+=('--new-window' "$url")
+                    ;;
+
+                INF-[0-9]*)
+                    url="https://jira.opennetworking.org/browse/${arg}"
+                    urls_raw+=('--new-window' "$url")
+                    ;;
+
+                VOL-[0-9]*)
+                    url="https://jira.opencord.org/browse/${arg}"
+                    urls_raw+=('--new-window' "$url")
+                    ;;
+
+                *) error "Detected invalid ticket [$arg]" ;;
+
+            esac
+            ;;
+
+        # -----------------------------------------------------------------------
+        # https://support.atlassian.com/jira-software-cloud/docs/search-syntax-for-text-fields/
+        # -----------------------------------------------------------------------
+        # +jira atlassian -- must contain jira, atlassian is optional
+        # -japan          -- exclude term
+        # [STEM] summary ~ "customize"    -- finds stem 'custom' in the Summary field
+        *)
+            declare -p text_and
+            error "Detected unknown argument $arg"
+            ;;
+    esac
+done
+
+## ----------------------
+## Construct query filter
+## ----------------------
+do_user                           suffix0
+do_projects                       suffix0
+[[ -v components ]] && { do_components components suffix0; }
+do_labels labels_incl labels_excl suffix0
+do_text  text                     suffix0
+do_resolved                       suffix0
+do_fixversion                     suffix0
+
+filter=''
+gen_filter filter suffix0
+
+if [[ ! -v urls_raw ]]; then
+    url=''
+    gen_url url filter
+    urls_filt+=("$url")
+elif [ ${#urls_raw} -eq 0 ]; then
+    url=''
+    gen_url url filter
+    urls_filt+=("$url")
+fi
+
+[[ -v debug ]] && [[ -v url ]] && echo "URL: $url"
+# browser="${BROWSER:-/snap/bin/firefox}"
+# browser="${BROWSER:-/opt/firefox/current/firefox}"
+browser="${BROWSER:-opera}"
+echo "$browser ${urls_filt[@]} ${urls_raw[@]}"
+
+if [[ ! -v dry_run ]]; then
+    "$browser" "${urls_filt[@]}" "${urls_raw[@]}" >/dev/null 2>/dev/null &
+fi
+
+# [SEE ALSO]
+#   o https://support.atlassian.com/jira-software-cloud/docs/advanced-search-reference-jql-fields/
+
+# [EOF]
diff --git a/jira/jira-search/fixversion.sh b/jira/jira-search/fixversion.sh
new file mode 100644
index 0000000..46058af
--- /dev/null
+++ b/jira/jira-search/fixversion.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+## --------------------------------------------------------------------
+## Intent: Retrieve a list of reason query strings
+## --------------------------------------------------------------------
+function get_jql_fixversion()
+{
+    local -n ref=$1; shift
+
+    ref+=('VOLTHA-v2.12')
+    return
+}
+
+## --------------------------------------------------------------------
+## Intent: Modify search query by release fix version string
+## --------------------------------------------------------------------
+function do_fixversion()
+{
+    declare -n ans=$1; shift
+    declare -g fixversion
+
+    if [[ -v fixversion_excl ]]; then
+        filter="$(join_by ',' "${fixversion_excl[@]}")"
+        ans+=("(fixVersion NOT IN ($filter))")
+    fi
+    
+    if [[ -v fixversion_incl ]]; then
+        filter="$(join_by ',' "${fixversion_incl[@]}")"
+        ans+=("(fixVersion IN ($filter))")
+    fi
+
+    [[ -v fixversion_not_empty ]] && { ans+=('(fixVersion IS NOT EMPTY)'); }
+    [[ -v fixversion_is_empty ]] \
+        && { ans+=('(fixversion IS EMPTY)'); } \
+        || { true; }
+
+    return
+}
+
+: # ($?=0) for source $include
+
+# [EOF]
diff --git a/jira/jira-search/resolved.sh b/jira/jira-search/resolved.sh
new file mode 100644
index 0000000..9d2d563
--- /dev/null
+++ b/jira/jira-search/resolved.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+
+## --------------------------------------------------------------------
+## Intent: Retrieve a list of reason query strings
+## --------------------------------------------------------------------
+function get_jql_reasons()
+{
+    local -n ref=$1; shift
+
+    ref+=('Cannot Reproduce')
+    ref+=('Duplicate')
+    ref+=('Fixed')
+    ref+=('Incomplete')
+    ref+=("Won't Do")
+    ref+=("Won't Fix")
+    return
+}
+
+## --------------------------------------------------------------------
+## Intent: Modify search query by ticket resolution
+## --------------------------------------------------------------------
+function do_resolved()
+{
+    declare -n ans=$1; shift
+   # declare -g resolved
+
+    [[ -v resolved_start ]] && { ans+=("(Resolved >= $resolved_start)"); }
+    [[ -v resolved_end ]]   && { ans+=("(Resolved <= $resolved_end)"); }
+
+    if [[ -v resolved_excl ]]; then
+        filter="$(join_by ',' "${resolved_excl[@]}")"
+        declare -p filter
+        ans+=( "(resolution NOT IN ($filter))" )
+    fi
+
+    if [[ -v resolved_incl ]]; then
+        filter="$(join_by ',' "${resolved_incl[@]}")"
+        ans+=( "(resolution IN ($filter))" )
+    fi
+
+    [[ -v resolved_not_empty ]] && { ans+=('(resolved IS NOT EMPTY)'); }
+    [[ -v resolved_is_empty ]] \
+        && { ans+=('(resolved IS EMPTY)'); } \
+        || { true; }
+
+    return
+}
+
+: # ($?=0) for source $include
+
+# [EOF]
diff --git a/jira/jira-search/utils.sh b/jira/jira-search/utils.sh
new file mode 100644
index 0000000..8936a45
--- /dev/null
+++ b/jira/jira-search/utils.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+## -----------------------------------------------------------------------
+## Intent: Parse command line paths
+## -----------------------------------------------------------------------
+function program_paths()
+{
+    declare -g pgm="$(readlink --canonicalize-existing "$0")"
+    declare -g pgmbin="${pgm%/*}"
+    declare -g pgmlib="${pgmbin}/jira-search"
+
+    readonly pgm
+    readonly pgmbin
+    readonly pgmlib
+    return
+}
+program_paths
+           
+: # ($?=0) for source $include
+
+# [EOF]
diff --git a/jira/resolved.sh b/jira/resolved.sh
new file mode 100755
index 0000000..e828ef5
--- /dev/null
+++ b/jira/resolved.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+declare -a args=()
+args+=('--project' 'SEBA')
+args+=('--project' 'VOL')
+
+args+=('--opencord')
+args+=('--resolved-start' '2022-12-31')
+args+=('--resolved-end'   '2024-12-3'1)
+
+args+=('--resolved-excl'  "Duplicate")
+args+=('--resolved-excl'  "Won't Do")
+args+=('--resolved-excl'  "Won't Fix")
+
+args+=('--resolved-not-empty')
+
+# args+=('--fixversion-excl'  'VOLTHA v2.13')
+args+=('--fixversion-is-empty')
+# args+=('--unresolved')
+
+jira-search.sh "${args[@]}"
+
+# [EOF]
diff --git a/lf/onf-common b/lf/onf-common
new file mode 160000
index 0000000..ddb24ea
--- /dev/null
+++ b/lf/onf-common
@@ -0,0 +1 @@
+Subproject commit ddb24ead39da336a063ec9ed78662593d0c0e26f