Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,5 @@ jobs:

steps:
- uses: actions/checkout@v6
- run: pacman -Sy --needed --noconfirm just python-cram
- run: |
useradd ci
runuser -u ci -- just test
- run: pacman -Sy --needed --noconfirm git just python-cram
- run: just test
1 change: 1 addition & 0 deletions dist/completion/downgrade/fish
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ complete -c $cmd -l pacman -xa "(complete -C(commandline -ct))" -d 'pacman comma
complete -c $cmd -l pacman-conf -r -d 'pacman configuration file'
complete -c $cmd -l pacman-cache -r -d 'pacman cache directory'
complete -c $cmd -l pacman-log -r -d 'pacman log file'
complete -c $cmd -l git -d 'uses git for choosing version, viable only for aur packages'
complete -c $cmd -l maxdepth -x -d 'maximum depth to search for cached packages'
complete -c $cmd -l ala-url -x -d 'location of ALA server'
complete -c $cmd -l ala-only -d 'only use ALA server'
Expand Down
1 change: 1 addition & 0 deletions dist/completion/downgrade/zsh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ _downgrade () {
'--pacman-conf[pacman configuration file]:pacman config file:_files' \
'*--pacman-cache[pacman cache directory]:pacman cache directory:_path_files -/' \
'--pacman-log[pacman log file]:pacman log file:_files' \
'--git[uses git for choosing version, viable only for aur packages]' \
'--maxdepth[maximum depth to search for cached packages]:maximum search depth' \
'--ala-url[location of ALA server]:ala url' \
'--ala-only[only use ALA server]' \
Expand Down
14 changes: 14 additions & 0 deletions doc/downgrade.8.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ on the ALA.
Pacman log file, default value is extracted from pacman configuration file, or
otherwise defaults to _/var/log/pacman.log_.

* `--git`:
If the pacman cache contains the source repository for the package (e.g. if
you use yay and are downgrading an AUR package), present any version-like
tags as available for downgrading (together with cached packages if
present). If selected, build and install the package from sources checked
out at that tag.

See [EXAMPLES].

* `--maxdepth`=<INTEGER>:
Maximum depth to search for cached packages, defaults to _1_.

Expand Down Expand Up @@ -168,6 +177,11 @@ Non-interactively downgrade `foo` to `1.0.0-1`

# downgrade --latest --prefer-cache --ignore never 'foo=1.0.0-1'

Present cached `downgrade` pkgbuilds together with tags with version greater
than 9

# downgrade --git --pacman-cache ~/.cache/yay --maxdepth 2 'downgrade>9.0.0'

## AUTHORS

* Patrick Brisbin \<pbrisbin@gmail.com>
Expand Down
89 changes: 84 additions & 5 deletions src/downgrade
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ $(gettext "Options"):
$(gettext "pacman log file,")
$(gettext "default value taken from pacman configuration file,")
$(gettext "or otherwise defaults to") "/var/log/pacman.log"
--git $(gettext "uses git for choosing version, viable only for aur packages")
--maxdepth <$(gettext "integer")>
$(gettext "maximum depth to search for cached packages, defaults to") 1
--ala-url <url>
Expand Down Expand Up @@ -167,8 +168,10 @@ matches_name_version_filter() {
return 0
fi

version_regex="[^-]+-[0-9.]+"
pkg_version=$(sed -r "s/.*$name-($version_regex)-(any|$DOWNGRADE_ARCH)\\.pkg\\.tar\\.(gz|xz|zst)/\1/g" <<<"$pkg")
# version: 2025.11.12.r15.g0eed3fe
version_regex="[a-zA-Z0-9.]+"
hash_or_minor="([0-9]+|[a-z0-9]{6,})"
pkg_version=$(sed -r "s/.*$name-($version_regex)(-$hash_or_minor)?-(any|$DOWNGRADE_ARCH)\\.(git)?pkg\\.tar\\.(gz|xz|zst)/\1\2/g" <<<"$pkg")
cmp=$(vercmp "$pkg_version" "$search_version")

case "$operator" in
Expand Down Expand Up @@ -196,6 +199,34 @@ matches_name_version_filter() {
esac
}

search_git() {
local name=$1 pairs path

# Delay this defaulting so #read_pacman_conf behavior is tested
if ((!${#PACMAN_CACHE[@]})); then
mapfile -t PACMAN_CACHE < <(read_pacman_conf CacheDir)
fi

for cache in "${PACMAN_CACHE[@]}"; do
path="$cache/$name" # directory name
if ! [[ -d "$path/.git" ]]; then
{
gettext "\`$path\" not a git directory, skipping..."
echo
} >&2
continue
fi

# TODO: in future could be enhanced by counting every commit
# that DOES NOT change version as minor like 0.0.1-2 instead of any
# pair = abcd12 1.0
pairs=$(git -C "$path" log -p --oneline --format="GIT: %h" PKGBUILD | grep -e 'GIT:.*' -e '+pkgver' | grep -Pzo 'GIT: .*\n\+pkgver=.*\n' | sed 's/GIT: \|+pkgver=//' | tr -d '\0')

# shellcheck disable=SC2086
echo "$path/$name" $pairs | awk '{for(i=2;i<NF;i+=2) printf "%s-%s-%s-any.gitpkg.tar.gz\n", $1, $(i+1), $i}' | sort -V
done
}

search_ala() {
local name=$1 uriname pkgfile_re index

Expand All @@ -217,7 +248,10 @@ search_ala() {
search_cache() {
local name=$1 pkgfile_re index

pkgfile_re="$name-[^-]+-[0-9.]+-(any|$DOWNGRADE_ARCH)\\.pkg\\.tar\\.(gz|xz|zst)"
# version: 2025.11.12.r15.g0eed3fe
version_regex="[a-zA-Z0-9.]+"
hash_or_minor="([0-9]+|[a-z0-9]{6,})"
pkgfile_re="$name-($version_regex)(-$hash_or_minor)?-(any|$DOWNGRADE_ARCH)\\.pkg\\.tar\\.(gz|xz|zst)"

# Delay this defaulting so #read_pacman_conf behavior is tested
if ((!${#PACMAN_CACHE[@]})); then
Expand Down Expand Up @@ -258,7 +292,7 @@ output_package() {
fi

# Remote or local file
if [[ $path =~ ^/ ]]; then
if [[ $path =~ ^/ && ! ($path =~ gitpkg) ]]; then
location="$(dirname "$path")"
timestamp=$(stat -c '%y' "$path" | cut -d' ' -f1)
else
Expand Down Expand Up @@ -293,7 +327,7 @@ extract_version_parts() {
s|^.\{'${#pkgname}'\}-\?||;

# Strip package extension
s|\.pkg\(\.tar\)\?\(\.[a-z0-9A-Z]\+\)\?$||;
s|\.\(git\)\?pkg\(\.tar\)\?\(\.[a-z0-9A-Z]\+\)\?$||;

# (epoch:)?version(-release)?(-arch)? -> epoch,version,release,arch
s|\(\([^:]*\):\)\?\([^-]*\)\(-\([^-]*\)\)\?\(-\(.*\)\)\?|\2,\3,\5,\7|;
Expand All @@ -320,6 +354,11 @@ process_term() {

candidates=($(printf '%s\n' "${candidates[@]}" | sort_packages))

# this is already sorted
if ((DOWNGRADE_FROM_GIT)); then
candidates=($(search_git "$name" | filter_packages "$name" "$operator" "$version") "${candidates[@]}")
fi

if (("${#candidates[@]}" == 0)); then
{
gettext "No results found"
Expand Down Expand Up @@ -440,6 +479,9 @@ parse_options() {
exit 1
fi
;;
--git)
DOWNGRADE_FROM_GIT=1
;;
--maxdepth)
if [[ -n "$2" ]]; then
shift
Expand Down Expand Up @@ -523,6 +565,34 @@ parse_options() {
fi
}

build_pkg() {
local item="$1" path commit_hash build_failed versionreg hashreg gitpkgreg
if [[ "$item" =~ .*gitpkg.tar.gz ]]; then
# version: 2025.11.12.r15.g0eed3fe
versionreg="([a-zA-Z0-9]+\.?)*"
hashreg='[a-z0-9]{6,}'
#hash_or_minor="-([0-9]+|[a-z0-9]{6,})" # leaving this as a note, only hash should appear tho
gitpkgreg="(.*)-$versionreg-($hashreg)-(any|$DOWNGRADE_ARCH).gitpkg.tar.gz"
path=$(dirname "$item")
commit_hash=$(sed -r "s/$gitpkgreg/\3/" <<<"$item")

pushd "$path" >/dev/null
su -P "$(stat -c"%U" PKGBUILD)" -c "git checkout $commit_hash &>/dev/null && makepkg -fs | tee makepkg.log"
build_failed=$?
# NOTE: when force-canceling, users might need to manually do checkout master
# otherwise they won't be able to update that pkg using yay or whatever I guess...
su "$(stat -c"%U" PKGBUILD)" -c "git switch - &>/dev/null"
if ((build_failed)); then
exit 1
fi

item="$(find "$path" -maxdepth 1 -type f -name "$(sed -nr '/.*Finished making: ([^ ]+) ([^ ]+) .*/{s//\1-\2/;p}' makepkg.log)*" -print -quit)"
rm makepkg.log
popd >/dev/null
fi
printf "%s\0" "$item" >>new_to_install.downgrade
}

cli() {
local conf_args=()

Expand All @@ -536,6 +606,14 @@ cli() {

# Proceed with rest of workflow
main "${terms[@]}"
if ((DOWNGRADE_FROM_GIT)); then
for item in "${to_install[@]}"; do
build_pkg "$item"
done
mapfile -d '' to_install <new_to_install.downgrade
rm new_to_install.downgrade
fi

pacman -U "${pacman_args[@]}" "${to_install[@]}"
prompt_to_ignore "${to_ignore[@]}"
}
Expand All @@ -545,6 +623,7 @@ PACMAN="pacman"
PACMAN_CONF="/etc/pacman.conf"
DOWNGRADE_ARCH="$(pacman-conf Architecture | head -n 1)"
DOWNGRADE_ALA_URL="https://archive.archlinux.org"
DOWNGRADE_FROM_GIT=0
DOWNGRADE_FROM_ALA=1
DOWNGRADE_FROM_CACHE=1
DOWNGRADE_MAXDEPTH=1
Expand Down
2 changes: 1 addition & 1 deletion test/main/one-choice.t
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ It doesn't present packages when there's only one choice
> DOWNGRADE_IGNORE=always
> DOWNGRADE_FROM_CACHE=1
> search_cache() { printf "%s\n" "$TMP/foo.pkg"; }
> main foo 2>/tmp/downgradechoicelog; exit_code=$?
> main foo 2>/dev/null; exit_code=$?
> printf "ignore: %s\n" "${to_ignore[@]}"
> printf "install: %s\n" "${to_install[@]}"
> printf "exit code: %s\n" "$exit_code"
Expand Down
45 changes: 26 additions & 19 deletions test/pacignore/parse_failure.t
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,32 @@ Check that pacignore fails if no argument provided
[options]
IgnorePkg = foo bar

Check that pacignore fails if no root access provided for add or rm

$ PACMAN_CONF="$(write_pacman_conf "[options]" "IgnorePkg = foo bar")"
> pacignore add 2>&1
> printf "exit_code=%s\n" "$?"
> cat "$PACMAN_CONF"
pacignore must be run as root for this subcommand
exit_code=1
[options]
IgnorePkg = foo bar

$ PACMAN_CONF="$(write_pacman_conf "[options]" "IgnorePkg = foo bar")"
> pacignore rm 2>&1
> printf "exit_code=%s\n" "$?"
> cat "$PACMAN_CONF"
pacignore must be run as root for this subcommand
exit_code=1
[options]
IgnorePkg = foo bar
# TODO: Because CI is running on an archlinux container on GitHub Actions, the
# tests must run as root there.
#
# https://docs.github.com/en/actions/reference/workflows-and-actions/dockerfile-support#user
#
# That means this test won't work there.
#
# Check that pacignore fails if no root access provided for add or rm
#
# $ PACMAN_CONF="$(write_pacman_conf "[options]" "IgnorePkg = foo bar")"
# > pacignore add 2>&1
# > printf "exit_code=%s\n" "$?"
# > cat "$PACMAN_CONF"
# pacignore must be run as root for this subcommand
# exit_code=1
# [options]
# IgnorePkg = foo bar
#
# $ PACMAN_CONF="$(write_pacman_conf "[options]" "IgnorePkg = foo bar")"
# > pacignore rm 2>&1
# > printf "exit_code=%s\n" "$?"
# > cat "$PACMAN_CONF"
# pacignore must be run as root for this subcommand
# exit_code=1
# [options]
# IgnorePkg = foo bar

Check that parsing fails if no package is package is specified for add or rm

Expand Down
16 changes: 16 additions & 0 deletions test/search_git.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
$ source "$TESTDIR/helper.sh"
> cache=$(mktemp -d)
> git clone -q "https://aur.archlinux.org/downgrade.git" "$cache/downgrade"
> write_pacman_conf "[options]" "CacheDir = $cache"
> export DOWNGRADE_FROM_GIT=1

List packages based on git tags

$ search_git 'downgrade' | head -n 2
/tmp/*/downgrade-5.1.3-0bdb507-any.gitpkg.tar.gz (glob)
/tmp/*/downgrade-5.1.4-dee7bd9-any.gitpkg.tar.gz (glob)

Outputs appropriately for filter_packages

$ search_git 'downgrade' | filter_packages 'downgrade' '=~' '^7' | head -n 1
/tmp/*/downgrade-7.0.0-513f504-any.gitpkg.tar.gz (glob)