Skip to content

Commit 3fd2cdc

Browse files
darukutsupbrisbin
authored andcommitted
feat: add ability to downgrade aur package using git
This is fully opt-in by the `--git` option (or equivalent env var). If enabled, we check to see if the configured cache directory represents the git repository. This would be the case if the user is downgrading an AUR package installed via `yay`. In such a repository, we can hunt for commits that represent the older versions of the package and present them as options to downgrade to. If selected, we can check out that commit and build the package at that version (if necessary) and install it. The logic is isolated to a `search_git` function, for handling locating commits that represent versions and output potential package names that include "gitpkg" in them, and a `build_pkg` function that looks for such package names and builds them. We have to be careful to use `su` so that `makepkg` is not run as root. The only changes to `downgrade` outside of these new functions is to regexs used to locate and parse package names, but this is well covered by tests.
1 parent e7c1590 commit 3fd2cdc

File tree

5 files changed

+116
-5
lines changed

5 files changed

+116
-5
lines changed

dist/completion/downgrade/fish

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ complete -c $cmd -l pacman -xa "(complete -C(commandline -ct))" -d 'pacman comma
44
complete -c $cmd -l pacman-conf -r -d 'pacman configuration file'
55
complete -c $cmd -l pacman-cache -r -d 'pacman cache directory'
66
complete -c $cmd -l pacman-log -r -d 'pacman log file'
7+
complete -c $cmd -l git -d 'uses git for choosing version, viable only for aur packages'
78
complete -c $cmd -l maxdepth -x -d 'maximum depth to search for cached packages'
89
complete -c $cmd -l ala-url -x -d 'location of ALA server'
910
complete -c $cmd -l ala-only -d 'only use ALA server'

dist/completion/downgrade/zsh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ _downgrade () {
2020
'--pacman-conf[pacman configuration file]:pacman config file:_files' \
2121
'*--pacman-cache[pacman cache directory]:pacman cache directory:_path_files -/' \
2222
'--pacman-log[pacman log file]:pacman log file:_files' \
23+
'--git[uses git for choosing version, viable only for aur packages]' \
2324
'--maxdepth[maximum depth to search for cached packages]:maximum search depth' \
2425
'--ala-url[location of ALA server]:ala url' \
2526
'--ala-only[only use ALA server]' \

doc/downgrade.8.ronn

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ on the ALA.
7272
Pacman log file, default value is extracted from pacman configuration file, or
7373
otherwise defaults to _/var/log/pacman.log_.
7474

75+
* `--git`:
76+
If the pacman cache contains the source repository for the package (e.g. if
77+
you use yay and are downgrading an AUR package), present any version-like
78+
tags as available for downgrading (together with cached packages if
79+
present). If selected, build and install the package from sources checked
80+
out at that tag.
81+
82+
See [EXAMPLES].
83+
7584
* `--maxdepth`=<INTEGER>:
7685
Maximum depth to search for cached packages, defaults to _1_.
7786

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

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

180+
Present cached `downgrade` pkgbuilds together with tags with version greater
181+
than 9
182+
183+
# downgrade --git --pacman-cache ~/.cache/yay --maxdepth 2 'downgrade>9.0.0'
184+
171185
## AUTHORS
172186

173187
* Patrick Brisbin \<pbrisbin@gmail.com>

src/downgrade

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ $(gettext "Options"):
1919
$(gettext "pacman log file,")
2020
$(gettext "default value taken from pacman configuration file,")
2121
$(gettext "or otherwise defaults to") "/var/log/pacman.log"
22+
--git $(gettext "uses git for choosing version, viable only for aur packages")
2223
--maxdepth <$(gettext "integer")>
2324
$(gettext "maximum depth to search for cached packages, defaults to") 1
2425
--ala-url <url>
@@ -167,8 +168,10 @@ matches_name_version_filter() {
167168
return 0
168169
fi
169170

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

174177
case "$operator" in
@@ -196,6 +199,34 @@ matches_name_version_filter() {
196199
esac
197200
}
198201

202+
search_git() {
203+
local name=$1 pairs path
204+
205+
# Delay this defaulting so #read_pacman_conf behavior is tested
206+
if ((!${#PACMAN_CACHE[@]})); then
207+
mapfile -t PACMAN_CACHE < <(read_pacman_conf CacheDir)
208+
fi
209+
210+
for cache in "${PACMAN_CACHE[@]}"; do
211+
path="$cache/$name" # directory name
212+
if ! [[ -d "$path/.git" ]]; then
213+
{
214+
gettext "\`$path\" not a git directory, skipping..."
215+
echo
216+
} >&2
217+
continue
218+
fi
219+
220+
# TODO: in future could be enhanced by counting every commit
221+
# that DOES NOT change version as minor like 0.0.1-2 instead of any
222+
# pair = abcd12 1.0
223+
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')
224+
225+
# shellcheck disable=SC2086
226+
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
227+
done
228+
}
229+
199230
search_ala() {
200231
local name=$1 uriname pkgfile_re index
201232

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

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

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

260294
# Remote or local file
261-
if [[ $path =~ ^/ ]]; then
295+
if [[ $path =~ ^/ && ! ($path =~ gitpkg) ]]; then
262296
location="$(dirname "$path")"
263297
timestamp=$(stat -c '%y' "$path" | cut -d' ' -f1)
264298
else
@@ -293,7 +327,7 @@ extract_version_parts() {
293327
s|^.\{'${#pkgname}'\}-\?||;
294328
295329
# Strip package extension
296-
s|\.pkg\(\.tar\)\?\(\.[a-z0-9A-Z]\+\)\?$||;
330+
s|\.\(git\)\?pkg\(\.tar\)\?\(\.[a-z0-9A-Z]\+\)\?$||;
297331
298332
# (epoch:)?version(-release)?(-arch)? -> epoch,version,release,arch
299333
s|\(\([^:]*\):\)\?\([^-]*\)\(-\([^-]*\)\)\?\(-\(.*\)\)\?|\2,\3,\5,\7|;
@@ -320,6 +354,11 @@ process_term() {
320354

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

357+
# this is already sorted
358+
if ((DOWNGRADE_FROM_GIT)); then
359+
candidates=($(search_git "$name" | filter_packages "$name" "$operator" "$version") "${candidates[@]}")
360+
fi
361+
323362
if (("${#candidates[@]}" == 0)); then
324363
{
325364
gettext "No results found"
@@ -440,6 +479,9 @@ parse_options() {
440479
exit 1
441480
fi
442481
;;
482+
--git)
483+
DOWNGRADE_FROM_GIT=1
484+
;;
443485
--maxdepth)
444486
if [[ -n "$2" ]]; then
445487
shift
@@ -523,6 +565,34 @@ parse_options() {
523565
fi
524566
}
525567

568+
build_pkg() {
569+
local item="$1" path commit_hash build_failed versionreg hashreg gitpkgreg
570+
if [[ "$item" =~ .*gitpkg.tar.gz ]]; then
571+
# version: 2025.11.12.r15.g0eed3fe
572+
versionreg="([a-zA-Z0-9]+\.?)*"
573+
hashreg='[a-z0-9]{6,}'
574+
#hash_or_minor="-([0-9]+|[a-z0-9]{6,})" # leaving this as a note, only hash should appear tho
575+
gitpkgreg="(.*)-$versionreg-($hashreg)-(any|$DOWNGRADE_ARCH).gitpkg.tar.gz"
576+
path=$(dirname "$item")
577+
commit_hash=$(sed -r "s/$gitpkgreg/\3/" <<<"$item")
578+
579+
pushd "$path" >/dev/null
580+
su -P "$(stat -c"%U" PKGBUILD)" -c "git checkout $commit_hash &>/dev/null && makepkg -fs | tee makepkg.log"
581+
build_failed=$?
582+
# NOTE: when force-canceling, users might need to manually do checkout master
583+
# otherwise they won't be able to update that pkg using yay or whatever I guess...
584+
su "$(stat -c"%U" PKGBUILD)" -c "git switch - &>/dev/null"
585+
if ((build_failed)); then
586+
exit 1
587+
fi
588+
589+
item="$(find "$path" -maxdepth 1 -type f -name "$(sed -nr '/.*Finished making: ([^ ]+) ([^ ]+) .*/{s//\1-\2/;p}' makepkg.log)*" -print -quit)"
590+
rm makepkg.log
591+
popd >/dev/null
592+
fi
593+
printf "%s\0" "$item" >>new_to_install.downgrade
594+
}
595+
526596
cli() {
527597
local conf_args=()
528598

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

537607
# Proceed with rest of workflow
538608
main "${terms[@]}"
609+
if ((DOWNGRADE_FROM_GIT)); then
610+
for item in "${to_install[@]}"; do
611+
build_pkg "$item"
612+
done
613+
mapfile -d '' to_install <new_to_install.downgrade
614+
rm new_to_install.downgrade
615+
fi
616+
539617
pacman -U "${pacman_args[@]}" "${to_install[@]}"
540618
prompt_to_ignore "${to_ignore[@]}"
541619
}
@@ -545,6 +623,7 @@ PACMAN="pacman"
545623
PACMAN_CONF="/etc/pacman.conf"
546624
DOWNGRADE_ARCH="$(pacman-conf Architecture | head -n 1)"
547625
DOWNGRADE_ALA_URL="https://archive.archlinux.org"
626+
DOWNGRADE_FROM_GIT=0
548627
DOWNGRADE_FROM_ALA=1
549628
DOWNGRADE_FROM_CACHE=1
550629
DOWNGRADE_MAXDEPTH=1

test/search_git.t

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
$ source "$TESTDIR/helper.sh"
2+
> cache=$(mktemp -d)
3+
> git clone -q "https://aur.archlinux.org/downgrade.git" "$cache/downgrade"
4+
> write_pacman_conf "[options]" "CacheDir = $cache"
5+
> export DOWNGRADE_FROM_GIT=1
6+
7+
List packages based on git tags
8+
9+
$ search_git 'downgrade' | head -n 2
10+
/tmp/*/downgrade-5.1.3-0bdb507-any.gitpkg.tar.gz (glob)
11+
/tmp/*/downgrade-5.1.4-dee7bd9-any.gitpkg.tar.gz (glob)
12+
13+
Outputs appropriately for filter_packages
14+
15+
$ search_git 'downgrade' | filter_packages 'downgrade' '=~' '^7' | head -n 1
16+
/tmp/*/downgrade-7.0.0-513f504-any.gitpkg.tar.gz (glob)

0 commit comments

Comments
 (0)