#!/bin/bash # # SPDX-License-Identifier: GPL-3.0-or-later [[ -z ${DEVTOOLS_INCLUDE_SEARCH_SH:-} ]] || return 0 DEVTOOLS_INCLUDE_SEARCH_SH=1 _DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@} # shellcheck source=src/lib/common.sh source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh # shellcheck source=src/lib/cache.sh source "${_DEVTOOLS_LIBRARY_DIR}"/lib/cache.sh # shellcheck source=src/lib/api/gitlab.sh source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh # shellcheck source=src/lib/valid-search.sh source "${_DEVTOOLS_LIBRARY_DIR}"/lib/valid-search.sh # shellcheck source=src/lib/util/term.sh source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/term.sh source /usr/share/makepkg/util/util.sh source /usr/share/makepkg/util/message.sh set -eo pipefail pkgctl_search_usage() { local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}} cat <<- _EOF_ Usage: ${COMMAND} [OPTIONS] QUERY Search for an expression across the GitLab packaging group. To use a filter, include it in your query. You may use wildcards (*) to use glob matching. Available filters for the blobs scope: path, extension Every usage of the search command must be authenticated. Consult the 'pkgctl auth' command to authenticate with GitLab or view the authentication status. SEARCH TIPS Syntax Description Example ─────────────────────────────────────── " Exact search "gem sidekiq" ~ Fuzzy search J~ Doe | Or display | banner + And display +banner - Exclude display -banner * Partial bug error 50* \\ Escape \\*md # Issue ID #23456 ! Merge request !23456 OPTIONS -h, --help Show this help text FILTER OPTIONS --no-default-filter Do not apply default filter (like -path:keys/pgp/*.asc) OUTPUT OPTIONS --json Enable printing in JSON; Shorthand for '--format json' -F, --format FORMAT Controls the formatting of the results; FORMAT is 'pretty', 'plain', or 'json' (default: pretty) -N, --no-line-number Don't show line numbers when formatting results EXAMPLES $ ${COMMAND} linux $ ${COMMAND} --json '"pytest -v" +PYTHONPATH' _EOF_ } pkgctl_search_check_option_group_format() { local option=$1 local output_format=$2 if [[ -n ${output_format} ]]; then die "The argument '%s' cannot be used with one or more of the other specified arguments" "${option}" exit 1 fi return 0 } pkgctl_search() { if (( $# < 1 )); then pkgctl_search_usage exit 0 fi # options local search local output_format= local use_default_filter=1 local line_numbers=1 # variables local bat_style="header,grid" local default_filter="-path:keys/pgp/*.asc" local graphql_lookup_batch=200 local output result query entries from until length local project_name_cache_file project_name_lookup project_ids project_id project_name project_slice local mapping_output path startline currentline data line while (( $# )); do case $1 in -h|--help) pkgctl_search_usage exit 0 ;; --no-default-filter) use_default_filter=0 shift ;; --json) pkgctl_search_check_option_group_format "$1" "${output_format}" output_format=json shift ;; -F|--format) (( $# <= 1 )) && die "missing argument for %s" "$1" pkgctl_search_check_option_group_format "$1" "${output_format}" output_format="${2}" if ! in_array "${output_format}" "${valid_search_output_format[@]}"; then die "Unknown output format: %s" "${output_format}" fi shift 2 ;; -N|--no-line-number) line_numbers=0 shift ;; --) shift break ;; -*) die "invalid argument: %s" "$1" ;; *) break ;; esac done if (( $# == 0 )); then pkgctl_search_usage exit 1 fi # assign search parameter search="${*}" if (( use_default_filter )); then search+=" ${default_filter}" fi # assign default output format if [[ -z ${output_format} ]]; then output_format=pretty fi # check for optional dependencies if [[ ${output_format} == pretty ]] && ! command -v bat &>/dev/null; then warning "Failed to find optional dependency 'bat': falling back to plain output" output_format=plain fi # populate line numbers option if (( line_numbers )); then bat_style="numbers,${bat_style}" fi # call the gitlab search API status_dir=$(mktemp --tmpdir="${WORKDIR}" --directory pkgctl-gitlab-api.XXXXXXXXXX) printf "📡 Querying GitLab search API..." > "${status_dir}/status" term_spinner_start "${status_dir}" output=$(gitlab_api_search "${search}" "${status_dir}/status") term_spinner_stop "${status_dir}" msg_success "Querying GitLab search API" # collect project ids whose name needs to be looked up project_name_cache_file=$(get_cache_file gitlab/project_id_to_name) lock 11 "${project_name_cache_file}" "Locking project name cache" mapfile -t project_ids < <( jq --raw-output '[.[].project_id] | unique[]' <<< "${output}" | \ grep --invert-match --file <(awk '{ print $1 }' < "${project_name_cache_file}" )) # look up project names tmp_file=$(mktemp --tmpdir="${WORKDIR}" pkgctl-gitlab-api-spinner.tmp.XXXXXXXXXX) printf "📡 Querying GitLab project names..." > "${status_dir}/status" term_spinner_start "${status_dir}" local entries="${#project_ids[@]}" local until=0 while (( until < entries )); do from=${until} until=$(( until + graphql_lookup_batch )) if (( until > entries )); then until=${entries} fi length=$(( until - from )) percentage=$(( 100 * until / entries )) printf "📡 Querying GitLab project names: %s/%s [%s] %%spinner%%" \ "${BOLD}${until}" "${entries}" "${percentage}%${ALL_OFF}" \ > "${tmp_file}" mv "${tmp_file}" "${status_dir}/status" project_slice=("${project_ids[@]:${from}:${length}}") printf -v projects '"gid://gitlab/Project/%s",' "${project_slice[@]}" query='{ projects(after: "" ids: ['"${projects}"']) { pageInfo { startCursor endCursor hasNextPage } nodes { id name } } }' mapping_output=$(gitlab_api_get_project_name_mapping "${query}") # update cache while read -r project_id project_name; do printf "%s %s\n" "${project_id}" "${project_name}" >> "${project_name_cache_file}" done < <(jq --raw-output \ '.[] | "\(.id | rindex("/") as $lastSlash | .[$lastSlash+1:]) \(.name)"' \ <<< "${mapping_output}") done term_spinner_stop "${status_dir}" msg_success "Querying GitLab project names" # read project_id to name mapping from cache declare -A project_name_lookup=() while read -r project_id project_name; do project_name_lookup[${project_id}]=${project_name} done < "${project_name_cache_file}" # close project name cache lock lock_close 11 # output mode JSON if [[ ${output_format} == json ]]; then jq --from-file <( for project_id in $(jq '.[].project_id' <<< "${output}"); do project_name=${project_name_lookup[${project_id}]} printf 'map(if .project_id == %s then . + {"project_name": "%s"} else . end) | ' \ "${project_id}" "${project_name}" done printf . ) <<< "${output}" exit 0 fi # pretty print each result while read -r result; do # read properties from search result mapfile -t data < <(jq --raw-output ".data" <<< "${result}") { read -r project_id; read -r path; read -r startline; } < <( jq --raw-output ".project_id, .path, .startline" <<< "${result}" ) project_name=${project_name_lookup[${project_id}]} # remove trailing newline for multiline results if (( ${#data[@]} > 1 )) && [[ ${data[-1]} == "" ]]; then unset "data[${#data[@]}-1]" fi # output mode plain if [[ ${output_format} == plain ]]; then printf "%s%s%s\n" "${PURPLE}" "${project_name}/${path}" "${ALL_OFF}" currentline=${startline} for line in "${data[@]}"; do if (( line_numbers )); then line="${DARK_GREEN}${currentline}${ALL_OFF}: ${line}" currentline=$(( currentline + 1 )) fi printf "%s\n" "${line}" done printf "\n" continue fi # prepend empty lines to match startline if (( startline > 1 )); then mapfile -t data < <( printf '%.0s\n' $(seq 1 "$(( startline - 1 ))") printf "%s\n" "${data[@]}" ) fi bat \ --file-name="${project_name}/${path}" \ --line-range "${startline}:" \ --paging=never \ --force-colorization \ --style "${bat_style}" \ --map-syntax "PKGBUILD:Bourne Again Shell (bash)" \ --map-syntax ".SRCINFO:INI" \ --map-syntax "*install:Bourne Again Shell (bash)" \ --map-syntax "*sysusers*:Bourne Again Shell (bash)" \ --map-syntax "*tmpfiles*:Bourne Again Shell (bash)" \ --map-syntax "*.hook:INI" \ <(printf "%s\n" "${data[@]}") done < <(jq --compact-output '.[]' <<< "${output}") }