From 4673ad6c89bbdca632b22edfc2ef35486b7a635b Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sat, 1 Jul 2023 15:21:32 +0200 Subject: feat(search): add subcommand to search across the packaging group 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. This command uses bats for pretty printing the results including line numbers and syntax highlighting. Component: pkgctl search Co-authored-by: Christian Heusel Co-authored-by: Levente Polyak --- src/lib/api/gitlab.sh | 174 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 167 insertions(+), 7 deletions(-) (limited to 'src/lib/api/gitlab.sh') diff --git a/src/lib/api/gitlab.sh b/src/lib/api/gitlab.sh index e5f4237..e4b8a9d 100644 --- a/src/lib/api/gitlab.sh +++ b/src/lib/api/gitlab.sh @@ -13,13 +13,63 @@ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/config.sh set -e +graphql_api_call() { + local outfile=$1 + local request=$2 + local node_type=$3 + local data=$4 + local hasNextPage cursor + + # empty token + if [[ -z "${GITLAB_TOKEN}" ]]; then + msg_error " api call failed: No token provided" + return 1 + fi + + [[ -z ${WORKDIR:-} ]] && setup_workdir + api_workdir=$(mktemp --tmpdir="${WORKDIR}" --directory pkgctl-gitlab-api.XXXXXXXXXX) + + # normalize graphql data and prepare query + data="${data//\"/\\\"}" + data='{ + "query": "'"${data}"'" + }' + data="${data//$'\t'/ }" + data="${data//$'\n'/}" + + cursor="" + hasNextPage=true + while [[ ${hasNextPage} == true ]]; do + data=$(sed -E 's|after: \\"[a-zA-Z0-9]*\\"|after: \\"'"${cursor}"'\\"|' <<< "${data}") + result="${api_workdir}/result.${cursor}" + + if ! curl --request "${request}" \ + --url "https://${GITLAB_HOST}/api/graphql" \ + --header "Authorization: Bearer ${GITLAB_TOKEN}" \ + --header "Content-Type: application/json" \ + --data "${data}" \ + --output "${result}" \ + --silent; then + msg_error " api call failed: $(cat "${outfile}")" + return 1 + fi + + hasNextPage=$(jq --raw-output ".data | .${node_type} | .pageInfo | .hasNextPage" < "${result}") + cursor=$(jq --raw-output ".data | .${node_type} | .pageInfo | .endCursor" < "${result}") + + cp "${result}" "${api_workdir}/tmp" + jq ".data.${node_type}.nodes" "${api_workdir}/tmp" > "${result}" + done + + jq --slurp add "${api_workdir}"/result.* > "${outfile}" + return 0 +} gitlab_api_call() { local outfile=$1 local request=$2 local endpoint=$3 local data=${4:-} - local error # empty token if [[ -z "${GITLAB_TOKEN}" ]]; then @@ -38,27 +88,102 @@ gitlab_api_call() { return 1 fi + if ! gitlab_check_api_errors "${outfile}"; then + return 1 + fi + + return 0 +} + +gitlab_api_call_paged() { + local outfile=$1 + local request=$2 + local endpoint=$3 + local data=${4:-} + local result header + + # empty token + if [[ -z "${GITLAB_TOKEN}" ]]; then + msg_error " api call failed: No token provided" + return 1 + fi + + [[ -z ${WORKDIR:-} ]] && setup_workdir + api_workdir=$(mktemp --tmpdir="${WORKDIR}" --directory pkgctl-gitlab-api.XXXXXXXXXX) + + next_page=1 + while [[ -n "${next_page}" ]]; do + result="${api_workdir}/result.${next_page}" + header="${api_workdir}/header" + if ! curl --request "${request}" \ + --get \ + --url "https://${GITLAB_HOST}/api/v4/${endpoint}&per_page=100&page=${next_page}" \ + --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \ + --header "Content-Type: application/json" \ + --data-urlencode "${data}" \ + --dump-header "${header}" \ + --output "${result}" \ + --silent; then + msg_error " api call failed: $(cat "${result}")" + return 1 + fi + + if ! gitlab_check_api_errors "${result}"; then + return 1 + fi + + next_page=$(grep "x-next-page" "${header}" | tr -d '\r' | awk '{ print $2 }') + done + + jq --slurp add "${api_workdir}"/result.* > "${outfile}" + return 0 +} + +gitlab_check_api_errors() { + local file=$1 + local error + + # search API only returns an array, no errors + if [[ $(jq --raw-output 'type' < "${file}") == "array" ]]; then + return 0 + fi + # check for general purpose api error - if error=$(jq --raw-output --exit-status '.error' < "${outfile}"); then + if error=$(jq --raw-output --exit-status '.error' < "${file}"); then msg_error " api call failed: ${error}" return 1 fi # check for api specific error messages - if ! jq --raw-output --exit-status '.id' < "${outfile}" >/dev/null; then - if jq --raw-output --exit-status '.message | keys[]' < "${outfile}" &>/dev/null; then + if ! jq --raw-output --exit-status '.id' < "${file}" >/dev/null; then + if jq --raw-output --exit-status '.message | keys[]' < "${file}" &>/dev/null; then while read -r error; do msg_error " api call failed: ${error}" - done < <(jq --raw-output --exit-status '.message|to_entries|map("\(.key) \(.value[])")[]' < "${outfile}") - elif error=$(jq --raw-output --exit-status '.message' < "${outfile}"); then + done < <(jq --raw-output --exit-status '.message|to_entries|map("\(.key) \(.value[])")[]' < "${file}") + elif error=$(jq --raw-output --exit-status '.message' < "${file}"); then msg_error " api call failed: ${error}" fi return 1 fi - return 0 } +graphql_check_api_errors() { + local file=$1 + local error + + # early exit if we do not have errors + if ! jq --raw-output --exit-status '.errors[]' < "${file}" &>/dev/null; then + return 0 + fi + + # check for api specific error messages + while read -r error; do + msg_error " api call failed: ${error}" + done < <(jq --raw-output --exit-status '.errors[].message' < "${file}") + return 1 +} + gitlab_api_get_user() { local outfile username @@ -81,6 +206,23 @@ gitlab_api_get_user() { return 0 } +gitlab_api_get_project_name_mapping() { + local query=$1 + local outfile + + [[ -z ${WORKDIR:-} ]] && setup_workdir + outfile=$(mktemp --tmpdir="${WORKDIR}" pkgctl-gitlab-api.XXXXXXXXXX) + + # query user details + if ! graphql_api_call "${outfile}" POST projects "${query}"; then + msg_warn " Invalid token provided?" + exit 1 + fi + + cat "${outfile}" + return 0 +} + # Convert arbitrary project names to GitLab valid path names. # # GitLab has several limitations on project and group names and also maintains @@ -130,3 +272,21 @@ gitlab_api_create_project() { printf "%s" "${path}" return 0 } + +# TODO: parallelize +# https://docs.gitlab.com/ee/api/search.html#scope-blobs +gitlab_api_search() { + local search=$1 + local outfile + + [[ -z ${WORKDIR:-} ]] && setup_workdir + outfile=$(mktemp --tmpdir="${WORKDIR}" pkgctl-gitlab-api.XXXXXXXXXX) + + if ! gitlab_api_call_paged "${outfile}" GET "/groups/archlinux%2fpackaging%2fpackages/search?scope=blobs" "search=${search}"; then + return 1 + fi + + cat "${outfile}" + + return 0 +} -- cgit v1.2.3-70-g09d2 From 67fdb58758db553d2c081cf16fbfb54e8d4e932d Mon Sep 17 00:00:00 2001 From: Levente Polyak Date: Thu, 18 Jan 2024 19:44:11 +0100 Subject: feat(search): add status spinner to long running GitLab calls This helps people to be slightly more patient as the progress status update includes the current percentage. Component: pkgctl search Signed-off-by: Levente Polyak --- src/lib/api/gitlab.sh | 22 +++++++++++++++++----- src/lib/search.sh | 24 +++++++++++++++++++----- 2 files changed, 36 insertions(+), 10 deletions(-) (limited to 'src/lib/api/gitlab.sh') diff --git a/src/lib/api/gitlab.sh b/src/lib/api/gitlab.sh index e4b8a9d..115e58c 100644 --- a/src/lib/api/gitlab.sh +++ b/src/lib/api/gitlab.sh @@ -97,9 +97,10 @@ gitlab_api_call() { gitlab_api_call_paged() { local outfile=$1 - local request=$2 - local endpoint=$3 - local data=${4:-} + local status_file=$2 + local request=$3 + local endpoint=$4 + local data=${5:-} local result header # empty token @@ -110,9 +111,18 @@ gitlab_api_call_paged() { [[ -z ${WORKDIR:-} ]] && setup_workdir api_workdir=$(mktemp --tmpdir="${WORKDIR}" --directory pkgctl-gitlab-api.XXXXXXXXXX) + tmp_file=$(mktemp --tmpdir="${api_workdir}" spinner.tmp.XXXXXXXXXX) + + local next_page=1 + local total_pages=1 - next_page=1 while [[ -n "${next_page}" ]]; do + percentage=$(( 100 * next_page / total_pages )) + printf "📡 Querying GitLab: %s/%s [%s] %%spinner%%" \ + "${BOLD}${next_page}" "${total_pages}" "${percentage}%${ALL_OFF}" \ + > "${tmp_file}" + mv "${tmp_file}" "${status_file}" + result="${api_workdir}/result.${next_page}" header="${api_workdir}/header" if ! curl --request "${request}" \ @@ -133,6 +143,7 @@ gitlab_api_call_paged() { fi next_page=$(grep "x-next-page" "${header}" | tr -d '\r' | awk '{ print $2 }') + total_pages=$(grep "x-total-pages" "${header}" | tr -d '\r' | awk '{ print $2 }') done jq --slurp add "${api_workdir}"/result.* > "${outfile}" @@ -277,12 +288,13 @@ gitlab_api_create_project() { # https://docs.gitlab.com/ee/api/search.html#scope-blobs gitlab_api_search() { local search=$1 + local status_file=$2 local outfile [[ -z ${WORKDIR:-} ]] && setup_workdir outfile=$(mktemp --tmpdir="${WORKDIR}" pkgctl-gitlab-api.XXXXXXXXXX) - if ! gitlab_api_call_paged "${outfile}" GET "/groups/archlinux%2fpackaging%2fpackages/search?scope=blobs" "search=${search}"; then + if ! gitlab_api_call_paged "${outfile}" "${status_file}" GET "/groups/archlinux%2fpackaging%2fpackages/search?scope=blobs" "search=${search}"; then return 1 fi diff --git a/src/lib/search.sh b/src/lib/search.sh index e737dfa..d3bad68 100644 --- a/src/lib/search.sh +++ b/src/lib/search.sh @@ -14,6 +14,8 @@ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/cache.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 @@ -167,9 +169,12 @@ pkgctl_search() { fi # call the gitlab search API - stat_busy "Querying gitlab search api" - output=$(gitlab_api_search "${search}") - stat_done + 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) @@ -179,7 +184,9 @@ pkgctl_search() { grep --invert-match --file <(awk '{ print $1 }' < "${project_name_cache_file}" )) # look up project names - stat_busy "Querying 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 @@ -190,6 +197,12 @@ pkgctl_search() { 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='{ @@ -214,7 +227,8 @@ pkgctl_search() { '.[] | "\(.id | rindex("/") as $lastSlash | .[$lastSlash+1:]) \(.name)"' \ <<< "${mapping_output}") done - stat_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=() -- cgit v1.2.3-70-g09d2