Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorLevente Polyak <anthraxx@archlinux.org>2022-10-10 00:37:51 +0200
committerLevente Polyak <anthraxx@archlinux.org>2023-05-19 22:27:12 +0200
commitb5d5402e439f5edfd642fb4f680d596f5992e874 (patch)
tree856facc3e379ed590e260e7c39cafaebea9e60be /src/lib
parenta8be7423efb287edd5ef80002a75a853fc0c9c1d (diff)
src: modularize repo layout into a library
This will greatly help us to structure the functionality and commands in a more sane way. We will distribute the sources as actual libraries and reuse code with imports instead of processing everything with m4 and duplicating a lot of code.
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/archroot.sh62
-rw-r--r--src/lib/common.sh289
-rw-r--r--src/lib/repo.sh90
-rw-r--r--src/lib/repo/clone.sh139
-rw-r--r--src/lib/repo/configure.sh169
-rw-r--r--src/lib/repo/web.sh84
-rw-r--r--src/lib/valid-repos.sh32
-rw-r--r--src/lib/valid-tags.sh26
8 files changed, 891 insertions, 0 deletions
diff --git a/src/lib/archroot.sh b/src/lib/archroot.sh
new file mode 100644
index 0000000..d7917da
--- /dev/null
+++ b/src/lib/archroot.sh
@@ -0,0 +1,62 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+:
+
+# shellcheck disable=2034
+CHROOT_VERSION='v4'
+
+##
+# usage : check_root $keepenv
+##
+orig_argv=("${BASH_SOURCE[0]}" "$@")
+check_root() {
+ local keepenv=$1
+
+ (( EUID == 0 )) && return
+ if type -P sudo >/dev/null; then
+ exec sudo --preserve-env=$keepenv -- "${orig_argv[@]}"
+ else
+ exec su root -c "$(printf ' %q' "${orig_argv[@]}")"
+ fi
+}
+
+##
+# usage : is_btrfs( $path )
+# return : whether $path is on a btrfs
+##
+is_btrfs() {
+ [[ -e "$1" && "$(stat -f -c %T "$1")" == btrfs ]]
+}
+
+##
+# usage : is_subvolume( $path )
+# return : whether $path is a the root of a btrfs subvolume (including
+# the top-level subvolume).
+##
+is_subvolume() {
+ [[ -e "$1" && "$(stat -f -c %T "$1")" == btrfs && "$(stat -c %i "$1")" == 256 ]]
+}
+
+##
+# usage : subvolume_delete_recursive( $path )
+#
+# Find all btrfs subvolumes under and including $path and delete them.
+##
+subvolume_delete_recursive() {
+ local subvol
+
+ is_subvolume "$1" || return 0
+
+ while IFS= read -d $'\0' -r subvol; do
+ if ! subvolume_delete_recursive "$subvol"; then
+ return 1
+ fi
+ done < <(find "$1" -mindepth 1 -xdev -depth -inum 256 -print0)
+ if ! btrfs subvolume delete "$1" &>/dev/null; then
+ error "Unable to delete subvolume %s" "$subvol"
+ return 1
+ fi
+
+ return 0
+}
diff --git a/src/lib/common.sh b/src/lib/common.sh
new file mode 100644
index 0000000..f977726
--- /dev/null
+++ b/src/lib/common.sh
@@ -0,0 +1,289 @@
+#!/hint/bash
+#
+# This may be included with or without `set -euE`
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_COMMON_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_COMMON_SH="$(set +o|grep nounset)"
+
+set +u +o posix
+# shellcheck disable=1091
+. /usr/share/makepkg/util.sh
+$DEVTOOLS_INCLUDE_COMMON_SH
+
+# Avoid any encoding problems
+export LANG=C
+
+# Set buildtool properties
+export BUILDTOOL=devtools
+export BUILDTOOLVER=m4_devtools_version
+
+# Set common properties
+export PACMAN_KEYRING_DIR=/etc/pacman.d/gnupg
+export GITLAB_HOST=gitlab.archlinux.org
+export GIT_REPO_SPEC_VERSION=1
+export GIT_PACKAGING_NAMESPACE=archlinux/packaging/packages
+export GIT_PACKAGING_NAMESPACE_ID=11323
+export GIT_PACKAGING_URL_SSH="ssh://git@${GITLAB_HOST}/${GIT_PACKAGING_NAMESPACE}"
+export GIT_PACKAGING_URL_HTTPS="https://${GITLAB_HOST}/${GIT_PACKAGING_NAMESPACE}"
+export PACKAGING_REPO_RELEASE_HOST=repos.archlinux.org
+
+# check if messages are to be printed using color
+if [[ -t 2 && "$TERM" != dumb ]]; then
+ colorize
+else
+ # shellcheck disable=2034
+ declare -gr ALL_OFF='' BOLD='' BLUE='' GREEN='' RED='' YELLOW=''
+fi
+
+stat_busy() {
+ local mesg=$1; shift
+ # shellcheck disable=2059
+ printf "${GREEN}==>${ALL_OFF}${BOLD} ${mesg}...${ALL_OFF}" "$@" >&2
+}
+
+stat_progress() {
+ # shellcheck disable=2059
+ printf "${BOLD}.${ALL_OFF}" >&2
+}
+
+stat_done() {
+ # shellcheck disable=2059
+ printf "${BOLD}done${ALL_OFF}\n" >&2
+}
+
+_setup_workdir=false
+setup_workdir() {
+ [[ -z ${WORKDIR:-} ]] && WORKDIR=$(mktemp -d --tmpdir "${0##*/}.XXXXXXXXXX")
+ _setup_workdir=true
+ trap 'trap_abort' INT QUIT TERM HUP
+ trap 'trap_exit' EXIT
+}
+
+cleanup() {
+ if [[ -n ${WORKDIR:-} ]] && $_setup_workdir; then
+ rm -rf "$WORKDIR"
+ fi
+ exit "${1:-0}"
+}
+
+abort() {
+ error 'Aborting...'
+ cleanup 255
+}
+
+trap_abort() {
+ trap - EXIT INT QUIT TERM HUP
+ abort
+}
+
+trap_exit() {
+ local r=$?
+ trap - EXIT INT QUIT TERM HUP
+ cleanup $r
+}
+
+die() {
+ (( $# )) && error "$@"
+ cleanup 255
+}
+
+##
+# usage : lock( $fd, $file, $message, [ $message_arguments... ] )
+##
+lock() {
+ # Only reopen the FD if it wasn't handed to us
+ if ! [[ "/dev/fd/$1" -ef "$2" ]]; then
+ mkdir -p -- "$(dirname -- "$2")"
+ eval "exec $1>"'"$2"'
+ fi
+
+ if ! flock -n "$1"; then
+ stat_busy "${@:3}"
+ flock "$1"
+ stat_done
+ fi
+}
+
+##
+# usage : slock( $fd, $file, $message, [ $message_arguments... ] )
+##
+slock() {
+ # Only reopen the FD if it wasn't handed to us
+ if ! [[ "/dev/fd/$1" -ef "$2" ]]; then
+ mkdir -p -- "$(dirname -- "$2")"
+ eval "exec $1>"'"$2"'
+ fi
+
+ if ! flock -sn "$1"; then
+ stat_busy "${@:3}"
+ flock -s "$1"
+ stat_done
+ fi
+}
+
+##
+# usage : lock_close( $fd )
+##
+lock_close() {
+ local fd=$1
+ # https://github.com/koalaman/shellcheck/issues/862
+ # shellcheck disable=2034
+ exec {fd}>&-
+}
+
+##
+# usage: pkgver_equal( $pkgver1, $pkgver2 )
+##
+pkgver_equal() {
+ if [[ $1 = *-* && $2 = *-* ]]; then
+ # if both versions have a pkgrel, then they must be an exact match
+ [[ $1 = "$2" ]]
+ else
+ # otherwise, trim any pkgrel and compare the bare version.
+ [[ ${1%%-*} = "${2%%-*}" ]]
+ fi
+}
+
+##
+# usage: find_cached_package( $pkgname, $pkgver, $arch )
+#
+# $pkgver can be supplied with or without a pkgrel appended.
+# If not supplied, any pkgrel will be matched.
+##
+shopt -s extglob
+find_cached_package() {
+ local searchdirs=("$PWD" "$PKGDEST") results=()
+ local targetname=$1 targetver=$2 targetarch=$3
+ local dir pkg packages pkgbasename name ver rel arch r results
+
+ for dir in "${searchdirs[@]}"; do
+ [[ -d $dir ]] || continue
+
+ shopt -s extglob nullglob
+ mapfile -t packages < <(printf "%s\n" "$dir"/"${targetname}"-"${targetver}"-*"${targetarch}".pkg.tar?(.!(sig|*.*)))
+ shopt -u extglob nullglob
+
+ for pkg in "${packages[@]}"; do
+ [[ -f $pkg ]] || continue
+
+ # avoid adding duplicates of the same inode
+ for r in "${results[@]}"; do
+ [[ $r -ef $pkg ]] && continue 2
+ done
+
+ # split apart package filename into parts
+ pkgbasename=${pkg##*/}
+ pkgbasename=${pkgbasename%.pkg.tar*}
+
+ arch=${pkgbasename##*-}
+ pkgbasename=${pkgbasename%-"$arch"}
+
+ rel=${pkgbasename##*-}
+ pkgbasename=${pkgbasename%-"$rel"}
+
+ ver=${pkgbasename##*-}
+ name=${pkgbasename%-"$ver"}
+
+ if [[ $targetname = "$name" && $targetarch = "$arch" ]] &&
+ pkgver_equal "$targetver" "$ver-$rel"; then
+ results+=("$pkg")
+ fi
+ done
+ done
+
+ case ${#results[*]} in
+ 0)
+ return 1
+ ;;
+ 1)
+ printf '%s\n' "${results[0]}"
+ return 0
+ ;;
+ *)
+ error 'Multiple packages found:'
+ printf '\t%s\n' "${results[@]}" >&2
+ return 1
+ esac
+}
+shopt -u extglob
+
+check_package_validity(){
+ local pkgfile=$1
+ if grep -q "packager = Unknown Packager" <(bsdtar -xOqf "$pkgfile" .PKGINFO); then
+ die "PACKAGER was not set when building package"
+ fi
+ hashsum=sha256sum
+ pkgbuild_hash=$(awk -v"hashsum=$hashsum" -F' = ' '$1 == "pkgbuild_"hashsum {print $2}' <(bsdtar -xOqf "$pkgfile" .BUILDINFO))
+ if [[ "$pkgbuild_hash" != "$($hashsum PKGBUILD|cut -d' ' -f1)" ]]; then
+ die "PKGBUILD $hashsum mismatch: expected $pkgbuild_hash"
+ fi
+}
+
+
+# usage: grep_pkginfo pkgfile pattern
+grep_pkginfo() {
+ local _ret=()
+ mapfile -t _ret < <(bsdtar -xOqf "$1" ".PKGINFO" | grep "^${2} = ")
+ printf '%s\n' "${_ret[@]#${2} = }"
+}
+
+
+# Get the package name
+getpkgname() {
+ local _name
+
+ _name="$(grep_pkginfo "$1" "pkgname")"
+ if [[ -z $_name ]]; then
+ error "Package '%s' has no pkgname in the PKGINFO. Fail!" "$1"
+ exit 1
+ fi
+
+ echo "$_name"
+}
+
+
+# Get the package base or name as fallback
+getpkgbase() {
+ local _base
+
+ _base="$(grep_pkginfo "$1" "pkgbase")"
+ if [[ -z $_base ]]; then
+ getpkgname "$1"
+ else
+ echo "$_base"
+ fi
+}
+
+
+getpkgdesc() {
+ local _desc
+
+ _desc="$(grep_pkginfo "$1" "pkgdesc")"
+ if [[ -z $_desc ]]; then
+ error "Package '%s' has no pkgdesc in the PKGINFO. Fail!" "$1"
+ exit 1
+ fi
+
+ echo "$_desc"
+}
+
+
+get_tag_from_pkgver() {
+ local pkgver=$1
+ local tag=${pkgver}
+
+ tag=${tag/:/-}
+ tag=${tag//~/.}
+ echo "${tag}"
+}
+
+
+is_debug_package() {
+ local pkgfile=${1} pkgbase pkgname pkgdesc
+ pkgbase="$(getpkgbase "${pkgfile}")"
+ pkgname="$(getpkgname "${pkgfile}")"
+ pkgdesc="$(getpkgdesc "${pkgfile}")"
+ [[ ${pkgdesc} == "Detached debugging symbols for "* && ${pkgbase}-debug = "${pkgname}" ]]
+}
diff --git a/src/lib/repo.sh b/src/lib/repo.sh
new file mode 100644
index 0000000..8b8df11
--- /dev/null
+++ b/src/lib/repo.sh
@@ -0,0 +1,90 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_REPO_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_REPO_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+
+set -e
+
+
+pkgctl_repo_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [COMMAND] [OPTIONS]
+
+ Manage Git packaging repositories and helps with their configuration
+ according to distro specs.
+
+ Git author information and the used signing key is set up from
+ makepkg.conf read from any valid location like /etc or XDG_CONFIG_HOME.
+ The configure command can be used to synchronize the distro specs and
+ makepkg.conf settings for previously cloned repositories.
+
+ The unprivileged option can be used for cloning packaging repositories
+ without SSH access using read-only HTTPS.
+
+ COMMANDS
+ clone Clone a package repository
+ configure Configure a clone according to distro specs
+ web Open the packaging repository's website
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} clone libfoo linux libbar
+ $ ${COMMAND} clone --maintainer mynickname
+ $ ${COMMAND} configure *
+ $ ${COMMAND} web linux
+_EOF_
+}
+
+pkgctl_repo() {
+ if (( $# < 1 )); then
+ pkgctl_repo_usage
+ exit 0
+ fi
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_repo_usage
+ exit 0
+ ;;
+ clone)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/repo/clone.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/clone.sh
+ pkgctl_repo_clone "$@"
+ exit 0
+ ;;
+ configure)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/repo/configure.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/configure.sh
+ pkgctl_repo_configure "$@"
+ exit 0
+ ;;
+ web)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/repo/web.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/web.sh
+ pkgctl_repo_web "$@"
+ exit 0
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ die "invalid command: %s" "$1"
+ ;;
+ esac
+ done
+}
diff --git a/src/lib/repo/clone.sh b/src/lib/repo/clone.sh
new file mode 100644
index 0000000..42dc383
--- /dev/null
+++ b/src/lib/repo/clone.sh
@@ -0,0 +1,139 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_REPO_CLONE_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_REPO_CLONE_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/api/gitlab.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
+# shellcheck source=src/lib/repo/configure.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/configure.sh
+
+source /usr/share/makepkg/util/message.sh
+
+set -e
+
+
+pkgctl_repo_clone_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PKGBASE]...
+
+ Clone Git packaging repositories from the canonical namespace.
+
+ The configure command is subsequently invoked to synchronize the distro
+ specs and makepkg.conf settings. The unprivileged option can be used
+ for cloning packaging repositories without SSH access using read-only
+ HTTPS.
+
+ OPTIONS
+ -m, --maintainer=NAME Clone all packages of the named maintainer
+ -u, --unprivileged Clone package with read-only access and without
+ packager info as Git author
+ --universe Clone all existing packages, useful for cache warming
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} libfoo linux libbar
+ $ ${COMMAND} --maintainer mynickname
+_EOF_
+}
+
+pkgctl_repo_clone() {
+ if (( $# < 1 )); then
+ pkgctl_repo_clone_usage
+ exit 0
+ fi
+
+ # options
+ local GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_SSH}
+ local CLONE_ALL=0
+ local MAINTAINER=
+ local CONFIGURE_OPTIONS=()
+ local pkgbases
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_repo_clone_usage
+ exit 0
+ ;;
+ -u|--unprivileged)
+ GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_HTTPS}
+ CONFIGURE_OPTIONS+=("$1")
+ shift
+ ;;
+ -m|--maintainer)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ MAINTAINER="$2"
+ shift 2
+ ;;
+ --maintainer=*)
+ MAINTAINER="${1#*=}"
+ shift
+ ;;
+ --universe)
+ CLONE_ALL=1
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ pkgbases=("$@")
+ break
+ ;;
+ esac
+ done
+
+ # Query packages of a maintainer
+ if [[ -n ${MAINTAINER} ]]; then
+ stat_busy "Query packages"
+ max_pages=$(curl --silent --location --fail --retry 3 --retry-delay 3 "https://archlinux.org/packages/search/json/?sort=name&maintainer=${MAINTAINER}" | jq -r '.num_pages')
+ if [[ ! ${max_pages} =~ ([[:digit:]]) ]]; then
+ stat_done
+ warning "found no packages for maintainer ${MAINTAINER}"
+ exit 0
+ fi
+ mapfile -t pkgbases < <(for page in $(seq "${max_pages}"); do
+ curl --silent --location --fail --retry 3 --retry-delay 3 "https://archlinux.org/packages/search/json/?sort=name&maintainer=${MAINTAINER}&page=${page}" | jq -r '.results[].pkgbase'
+ stat_progress
+ done | sort --unique)
+ stat_done
+ fi
+
+ # Query all released packages
+ if (( CLONE_ALL )); then
+ stat_busy "Query all released packages"
+ max_pages=$(curl --silent --location --fail --retry 3 --retry-delay 3 "https://archlinux.org/packages/search/json/?sort=name" | jq -r '.num_pages')
+ if [[ ! ${max_pages} =~ ([[:digit:]]) ]]; then
+ stat_done
+ die "failed to query packages"
+ fi
+ mapfile -t pkgbases < <(for page in $(seq "${max_pages}"); do
+ curl --silent --location --fail --retry 3 --retry-delay 3 "https://archlinux.org/packages/search/json/?sort=name&page=${page}" | jq -r '.results[].pkgbase'
+ stat_progress
+ done | sort --unique)
+ stat_done
+ fi
+
+ for pkgbase in "${pkgbases[@]}"; do
+ if [[ ! -d ${pkgbase} ]]; then
+ msg "Cloning ${pkgbase} ..."
+ remote_url="${GIT_REPO_BASE_URL}/${pkgbase}.git"
+ git clone --origin origin "${remote_url}" "${pkgbase}"
+ else
+ warning "Skip cloning ${pkgbase}: Directory exists"
+ fi
+
+ pkgctl_repo_configure "${CONFIGURE_OPTIONS[@]}" "${pkgbase}"
+ done
+}
diff --git a/src/lib/repo/configure.sh b/src/lib/repo/configure.sh
new file mode 100644
index 0000000..5c99562
--- /dev/null
+++ b/src/lib/repo/configure.sh
@@ -0,0 +1,169 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_REPO_CONFIGURE_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_REPO_CONFIGURE_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/api/gitlab.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
+
+source /usr/share/makepkg/util/config.sh
+source /usr/share/makepkg/util/message.sh
+
+set -e
+
+
+pkgctl_repo_configure_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PATH]...
+
+ Configure Git packaging repositories according to distro specs and
+ makepkg.conf settings.
+
+ Git author information and the used signing key is set up from
+ makepkg.conf read from any valid location like /etc or XDG_CONFIG_HOME.
+ The unprivileged option can be used for cloning packaging repositories
+ without SSH access using read-only HTTPS.
+
+ OPTIONS
+ -u, --unprivileged Configure read-only repo without packager info as Git author
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} configure *
+_EOF_
+}
+
+pkgctl_repo_configure() {
+ # options
+ local GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_SSH}
+ local UNPRIVILEGED=0
+ local PACKAGER_NAME=
+ local PACKAGER_EMAIL=
+ local paths=()
+
+ # variables
+ local path realpath pkgbase remote_url
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_repo_configure_usage
+ exit 0
+ ;;
+ -u|--unprivileged)
+ GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_HTTPS}
+ UNPRIVILEGED=1
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ paths=("$@")
+ break
+ ;;
+ esac
+ done
+
+ # check if invoked without any path from within a packaging repo
+ if (( ${#paths[@]} == 0 )); then
+ if [[ -f PKGBUILD ]]; then
+ paths=(".")
+ else
+ pkgctl_repo_configure_usage
+ exit 1
+ fi
+ fi
+
+ # Load makepkg.conf variables to be available
+ # shellcheck disable=2119
+ load_makepkg_config
+
+ # Check official packaging identity before setting Git author
+ if (( ! UNPRIVILEGED )); then
+ if [[ $PACKAGER == *"Unknown Packager"* ]]; then
+ die "Packager must be set in makepkg.conf"
+ fi
+ packager_pattern="(.+) <(.+@.+)>"
+ if [[ ! $PACKAGER =~ $packager_pattern ]]; then
+ die "Invalid Packager format '${PACKAGER}' in makepkg.conf"
+ fi
+
+ PACKAGER_NAME=$(echo "${PACKAGER}"|sed -E "s/${packager_pattern}/\1/")
+ PACKAGER_EMAIL=$(echo "${PACKAGER}"|sed -E "s/${packager_pattern}/\2/")
+
+ if [[ ! $PACKAGER_EMAIL =~ .+@archlinux.org ]]; then
+ die "Packager email '${PACKAGER_EMAIL}' is not an @archlinux.org address"
+ fi
+ fi
+
+ msg "Collected packager settings"
+ msg2 "name : ${PACKAGER_NAME}"
+ msg2 "email : ${PACKAGER_EMAIL}"
+ msg2 "gpg-key : ${GPGKEY:-undefined}"
+
+ # TODO: print which protocol got auto detected, ssh https
+
+ for path in "${paths[@]}"; do
+ if ! realpath=$(realpath -e "${path}"); then
+ error "No such directory: ${path}"
+ continue
+ fi
+
+ pkgbase=$(basename "${realpath}")
+ pkgbase=${pkgbase%.git}
+ msg "Configuring ${pkgbase}"
+
+ if [[ ! -d "${path}/.git" ]]; then
+ error "Not a Git repository: ${path}"
+ continue
+ fi
+
+ remote_url="${GIT_REPO_BASE_URL}/${pkgbase}.git"
+ if ! git -C "${path}" remote add origin "${remote_url}" &>/dev/null; then
+ git -C "${path}" remote set-url origin "${remote_url}"
+ fi
+
+ # move the master branch to main
+ if [[ $(git -C "${path}" symbolic-ref --short HEAD) == master ]]; then
+ git -C "${path}" branch --move main
+ git -C "${path}" config branch.main.merge refs/heads/main
+ fi
+
+ git -C "${path}" config devtools.version "${GIT_REPO_SPEC_VERSION}"
+ git -C "${path}" config pull.rebase true
+ git -C "${path}" config branch.autoSetupRebase always
+ git -C "${path}" config branch.main.remote origin
+ git -C "${path}" config branch.main.rebase true
+
+ git -C "${path}" config transfer.fsckobjects true
+ git -C "${path}" config fetch.fsckobjects true
+ git -C "${path}" config receive.fsckobjects true
+
+ if (( ! UNPRIVILEGED )); then
+ git -C "${path}" config user.name "${PACKAGER_NAME}"
+ git -C "${path}" config user.email "${PACKAGER_EMAIL}"
+ git -C "${path}" config commit.gpgsign true
+ if [[ -n $GPGKEY ]]; then
+ git -C "${path}" config user.signingKey "${GPGKEY}"
+ else
+ warning "Missing makepkg.conf configuration: GPGKEY"
+ fi
+ fi
+
+ if ! git ls-remote origin &>/dev/null; then
+ warning "configured remote origin may not exist, run:"
+ msg2 "pkgctl repo create ${pkgbase}"
+ fi
+ done
+}
diff --git a/src/lib/repo/web.sh b/src/lib/repo/web.sh
new file mode 100644
index 0000000..3fa214d
--- /dev/null
+++ b/src/lib/repo/web.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_REPO_WEB_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_REPO_WEB_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/api/gitlab.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
+
+set -e
+
+
+pkgctl_repo_web_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PKGBASE]...
+
+ Open the packaging repository's website via xdg-open. If called with
+ no arguments, open the package cloned in the current working directory.
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} web linux
+_EOF_
+}
+
+pkgctl_repo_web() {
+ local pkgbases=()
+ local path giturl pkgbase
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_repo_web_usage
+ exit 0
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ pkgbases=("$@")
+ break
+ ;;
+ esac
+ done
+
+ # Check if web mode has xdg-open
+ if ! command -v xdg-open &>/dev/null; then
+ die "The web command requires 'xdg-open'"
+ fi
+
+ # Check if used without pkgnames in a packaging directory
+ if (( ! $# )); then
+ path=${PWD}
+ if [[ ! -d "${path}/.git" ]]; then
+ die "Not a Git repository: ${path}"
+ fi
+
+ giturl=$(git -C "${path}" remote get-url origin)
+ if [[ ${giturl} != *${GIT_PACKAGING_NAMESPACE}* ]]; then
+ die "Not a packaging repository: ${path}"
+ fi
+
+ pkgbase=$(basename "${giturl}")
+ pkgbase=${pkgbase%.git}
+ pkgbases=("${pkgbase}")
+ fi
+
+ for pkgbase in "${pkgbases[@]}"; do
+ path=$(gitlab_project_name_to_path "${pkgbase}")
+ xdg-open "${GIT_PACKAGING_URL_HTTPS}/${path}"
+ done
+}
diff --git a/src/lib/valid-repos.sh b/src/lib/valid-repos.sh
new file mode 100644
index 0000000..9ac9639
--- /dev/null
+++ b/src/lib/valid-repos.sh
@@ -0,0 +1,32 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+:
+
+# shellcheck disable=2034
+_repos=(
+ staging
+ testing
+ core
+ extra
+ community-staging
+ community-testing
+ community
+ multilib-staging
+ multilib-testing
+ multilib
+ gnome-unstable
+ kde-unstable
+)
+
+# shellcheck disable=2034
+_build_repos=(
+ staging
+ testing
+ extra
+ multilib-staging
+ multilib-testing
+ multilib
+ gnome-unstable
+ kde-unstable
+)
diff --git a/src/lib/valid-tags.sh b/src/lib/valid-tags.sh
new file mode 100644
index 0000000..d628fd1
--- /dev/null
+++ b/src/lib/valid-tags.sh
@@ -0,0 +1,26 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+:
+
+# shellcheck disable=2034
+_arch=(
+ x86_64
+ any
+)
+
+# shellcheck disable=2034
+_tags=(
+ core-x86_64 core-any
+ extra-x86_64 extra-any
+ multilib-x86_64
+ staging-x86_64 staging-any
+ testing-x86_64 testing-any
+ multilib-testing-x86_64
+ multilib-staging-x86_64
+ community-x86_64 community-any
+ community-staging-x86_64 community-staging-any
+ community-testing-x86_64 community-testing-any
+ kde-unstable-x86_64 kde-unstable-any
+ gnome-unstable-x86_64 gnome-unstable-any
+)