#!/bin/bash

## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

set -x
set -e
set -o pipefail
set -o errtrace

true "INFO: Currently running script: $BASH_SOURCE $@"

MYDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

if [ "$MYDIR" = "/usr/bin" ]; then
   true "INFO: Run from: /usr/bin"
   ## XXX: hardcoded path
   derivative_maker_source_code_dir="$HOME/derivative-maker"
else
   true "INFO: Run from: source code folder"
   derivative_maker_source_code_dir="$(cd -- "$MYDIR" && cd -- "../../../../../" && pwd)"
fi

## required by: make-helper-one.bsh
cd "$derivative_maker_source_code_dir"

source "${derivative_maker_source_code_dir}/help-steps/colors"
source "${derivative_maker_source_code_dir}/packages/kicksecure/genmkfile/usr/share/genmkfile/make-helper-one.bsh"

## BEGIN Initialization functions {

check_prerequisites() {
   local empty_directories cache_dirs

   if ! test -d "${derivative_maker_source_code_dir}"; then
      printf "%s\n" "FATAL ERROR: derivative_maker_source_code_dir '${derivative_maker_source_code_dir}' does not exist or is not a directory."
      exit 1
   fi

   # Any arbitrary package under derivative-maker/packages/kicksecure will
   # work here, live-config-dist was chosen arbitrarily.
   if ! test -d "${derivative_maker_source_code_dir}/packages/kicksecure/live-config-dist"; then
      printf "%s\n" "FATAL ERROR: git submodules under derivative_maker_source_code_dir '${derivative_maker_source_code_dir}' not populated."
      exit 1
   fi

   test -x "${MYDIR}/dm-check-unicode"

   if ! "${MYDIR}/dm-check-unicode"; then
      printf "%s\n" "FATAL ERROR: derivative_maker_source_code_dir '${derivative_maker_source_code_dir}' contains Unicode or is mislocated."
      exit 1
   fi

   cache_dirs="$(
      find "${derivative_maker_source_code_dir}" \
      -not -ipath '*/.git/*' \
      -type d \
      \( \
      -name '.pytest_cache' -o \
      -name '.mypy_cache'   -o \
      -name '__pycache__' \
      \)
   )"

   if [ -n "${cache_dirs}" ]; then
      printf "%s\n" \
        "FATAL ERROR: derivative_maker_source_code_dir '${derivative_maker_source_code_dir}' contains cache directories. Found: '${cache_dirs}'"
      exit 1
   fi

   empty_directories="$(find "${derivative_maker_source_code_dir}" -not -ipath '*.git*' -type d -empty)"

   if ! [ "${empty_directories}" = "" ]; then
      printf "%s\n" "${red}${bold}$0: ERROR:${reset} Empty directory found! empty_directories:
'${empty_directories}'"
      exit 1
   fi

   [ "${derivative_maker_source_code_dir}" = "$HOME/derivative-maker" ] || {
      printf "%s\n" "WARNING: derivative_maker_source_code_dir '${derivative_maker_source_code_dir}' is not located at '$HOME/derivative-maker'. The script may break."
   }

   [ -d "${derivative_binary_dir}" ] || {
      printf "%s\n" "WARNING: derivative_binary_dir '${derivative_binary_dir}' does not exist or is not a directory. Some features will be disabled."
      ## Soft failure.
      #exit 1
   }

   main pkg_git_check_current_branch

   true
}

## Ensures that the system is prepared to run the script (right now this just
## makes sure directories that need to exist actually exist).
prepare_system() {
   mkdir --parents -- "${announcements_drafts_dir}"
   mkdir --parents -- "${package_documentation_dir}"
}

## Shows debugging information for all non-volatile global variables.
## TODO Make sure this is up-to-date!
show_debug_variable_info() {
   local library_item

   true "Global tunables:"
   true "derivative_binary_dir: ${derivative_binary_dir}"
   true "derivative_maker_source_code_dir: ${derivative_maker_source_code_dir}"
   true "derivative_version_old_main: ${derivative_version_old_main}"
   true "derivative_version_new_main: ${derivative_version_new_main}"
   true "derivative_release_type: ${derivative_release_type}"
   true "makefile_generic_version: ${makefile_generic_version}"
   true "packaging_files_diff_template_package_relative_path: ${packaging_files_diff_template_package_relative_path}"
   true ""
   true "Global variables:"
   true "announcements_drafts_dir: ${announcements_drafts_dir}"
   true "derivative_maker_dir_name: ${derivative_maker_dir_name}"
   true "make_cowbuilder_dist_dir: ${make_cowbuilder_dist_dir}"
   true "package_documentation_dir: ${package_documentation_dir}"
   true "MYDIR: ${MYDIR}"
}

## . END Initialization functions }

## BEGIN Utility functions {

## Takes a short commit message as input. Returns 0 if the commit message is
## not one of several common ones that should be ignored. Returns 1 otherwise.
commit_filter() {
   set +x

   local commit_msg_short text_any_case text_lower_case

   commit_msg_short="$1"
   [ -z "${commit_msg_short}" ] && {
      printf "%s\n" "${red}${bold}FATAL ERROR: No argument provided to ${FUNCNAME[0]}!${reset}"
      set -x
      exit 1
   }

   commit_msg_short="${commit_msg_short,,}"

   if printf "%s\n" "${commit_msg_short}" | grep --ignore-case --quiet -- 'Merge remote-tracking branch' ; then
      set -x
      return 1
   fi
   if printf "%s\n" "${commit_msg_short}" | grep --ignore-case --quiet -- 'Merge pull request' ; then
      set -x
      return 1
   fi
   if printf "%s\n" "${commit_msg_short}" | grep --ignore-case --quiet -- 'Merge branch' ; then
      set -x
      return 1
   fi

   for text_any_case in \
      'add debian install file (generated using "genmkfile debinstfile")' \
      'use long option names' \
      'Insert empty new line' \
      "Merge branch 'master' into master" \
      'Remove whitespace' \
      'lintian' \
      'genmkfile manpages' \
      'LC_ALL=C' \
      'LANG=C' \
      'signed commit' \
      'rm_conffile' \
      'Typo' \
      'Typo fixes' \
      'end of options' \
      'end-of-options' \
      'todo' \
      'sponge' \
      'Clarify' \
      'aa-logprof' \
      'bookworm aa-logprof' \
      'improved error handling' \
      'lower debugging' \
      'printf' \
      'stprint' \
      'stecho' \
      'release-upgrade' \
      'Kicksecure' \
      'kicksecure' \
      'rename' \
      'split' \
      'split project source code' \
      'split source code' \
      'split Kicksecure and Whonix packages' \
      're-generated man pages' \
      'update lintian tag name' \
      'genmkfile debinstfile' \
      'update-path' \
      'output, refactoring' \
      'man' \
      'manpage' \
      'lintian' \
      'fix comment' \
      'genmkfile manpages' \
      'update comment' \
      'fix linitian warning' \
      'PEP8' \
      'intentd' \
      'improve error handling' \
      'refactoring; output' \
      'refactoring; fix' \
      'fix; refactoring' \
      'refactoring; debugging' \
      'output; refactoring' \
      'autopep8' \
      'anondate' \
      'apparmor' \
      'update path' \
      'update link' \
      'package description' \
      'update copyright year' \
      'remove white spaces from file names' \
      'Added creation of upstream changelog to debian/rules' \
      'add debian install file' \
      'remove genmkfile' \
      '# On branch master nothing to commit (working directory clean)' \
      'copyright' \
      'fix lintian warnings' \
      'fix lintian warning' \
      'fixed debian/changelog' \
      'fix path' \
      'remove trailing spaces' \
      'typos' \
      'minor' \
      'typo' \
      'update Depends' \
      'update copyright' \
      'remove faketime from Build-Depends:' \
      'remove debian/gain-root-command workaround' \
      'packaging simplification config-package-dev (>= 5.1) -> config-package-dev' \
      'packaging, bumped Standards-Version from 3.9.6 to 3.9.8 for jessie support' \
      'bumped Standards-Version' \
      'bump version number' \
      'bumped version number' \
      'bumped changelog version' \
      'packaging' \
      'shellcheck' \
      'verbose' \
      'safe-rm' \
      'sanity test' \
      'fix' \
      'comment' \
      'comments' \
      'output' \
      'news' \
      'readme' \
      'updated generic makefile' \
      'bumped compat from 8 to 9' \
      'Updated debian/changelog.' \
      'Fixed changelog date.' \
      'updated makefile generic to version 1.3' \
      'updated makefile generic to version 1.4' \
      'updated makefile generic to version 1.5' \
      'added changelog.upstream' \
      'quotes' \
      'refactoring' \
      'debugging' \
      'lintian warning copyright fix' \
      'https://www.kicksecure.com/wiki/Dev/Licensing' \
      'port to debian buster' \
      'port to debian bullseye' \
      'surpress lintian warning' \
      'add error handler' \
      'use full path' \
      'colors' \
      'build' \
      'improve --dry-run' \
      'migration' \
      'add newline at the end' \
      'update' \
      'fix debian/watch lintian warning debian-watch-contains-dh_make-template' \
      'code simplification' \
      'simplification' \
      'update path to pre.bsh' \
      'sudo' \
      'gksudo' \
      'tbb_hardcoded_version update' \
      'work towards nounset' \
      'nounset' \
      'dev-adrelanos' \
      'improvements' \
      'stricter shell options' \
      'use long options' \
      'remove trailing whitespace' \
      'remove trailing whitespaces' \
      'pylint' \
      'use end of options' \
      'usrmerge' \
      'unicode' \
      'improve pkexec test' \
      'only one TM' \
      'debhelper' \
      'use long option name' \
      'Adjust persistent mode wording' \
      'identd style' \
      'ident style' \
      'ident' \
      'autopep' \
      'add more ignore patterns' \
      'exit non-zero in more cases of warnings and errors' \
      'check journal: exclude more patterns' \
      'description' \
      'buster' \
      'bullseye' \
      'bookworm' \
      'cleanup' \
      'license' \
      'update path to pre.bsh' \
      'anon-shared-helper-scripts -> helper-scripts' \
      'enable debugging' \
      'Update control' \
      'fix output' \
      'local' \
      'man page' \
      'coypright' \
      'formatting' \
      'improve error handler' \
      'cleanup' \
      'fix lintian warning' \
      'local' \
      'bump' \
      'trailing spaces' \
      'coypright' \
      'description' \
      'shuffle' \
      'use pre.bsh' \
      'set -e' \
      'update control' \
      'disable debugging' \
      'set -o pipefail' \
      'style' \
      'upgrade license from GPLv2+ to GPLv3+' \
      'CI fix' \
      'errtrace' \
      'add link' \
      'simplify boot menu names' \
      'better boot menu entry naming' \
      'bugfixes' \
      'long option names' \
      'newline at the end' \
      'use end-of-options' \
      'delete empty file' \
      'wayland' \
      'nftabels' \
      'chmod +x' \
      'CI' \
      'rename variable' \
      'ISO' \
      'wrap-and-sort' \
      'Depends: pkexec' \
      'sysmaint' \
      'Port to Trixie.' \
      "Merge branch 'master' into arraybolt3/trixie" \
      'Add Wayland compatibility' \
      'remove duplication' \
      '--no-install-recommends' \
      'test' \
      'duplicate' \
      'pipe' \
      'add' \
      'dev' \
      'indent' \
      'stcat' \
      'improvement' \
      'improve lockfile' \
      'trixie' \
      'python3 -su' \
      'bump breaks version' \
      'idempotent' \
      'bump version' \
      'more fixes' \
      'long option name' \
      'long option names' \
      'no longer depend on sudo' \
      'newline' \
      'Remove link' \
      'Remove duplicate comment' \
      'add root_check' \
      'add root check' \
      'ignore' \
      'ignore more journal messages' \
      'refactoring; comments' \
      'add `wc -l` test' \
      'check apparmor' \
      'Adjust for Wayland and LXQt compatibility' \
      'Adjust for metapackage restructure' \
      'update minimum_unixtime' \
      'forky' \
      'debian-tor' \
      'links' \
      'use TEMPDIR' \
      'regenerate' \
      'remove trailing space' \
      'remove trailing spaces' \
      'Depends: python3' \
      '/usr/bin/touch' \
      'newlines' \
      'line endings' \
      'Add Wayland support' \
      'warning' \
      'lockfile' \
      'consistency' \
      're-generate man pages (generated using "genmkfile manpages")' \
      'add todo' \
      'todo' \
      '-' \
      '.' \
      ; do
         ## Done above already.
         #commit_msg_short="${commit_msg_short,,}"
         if [ "${text_any_case,,}" = "${commit_msg_short}" ]; then
            set -x
            return 1
         fi
   done

   set -x
}

## Takes a package repository name as an argument. Returns 0 if the current
## package reponame being handled is NOT the same as the argument. Returns 1
## otherwise. Intended to be used in commands as
## `repo_skip 'package-name' || return 0`.
repo_skip() {
   if [ "${batch_current_package_reponame}" = "$1" ]; then
      true "Skipping ${batch_current_package_reponame}."
      return 1
   fi
}

repo_skip_non_package() {
   repo_skip 'derivative-maker' || return 1
   repo_skip 'qubes-template-kicksecure' || return 1
   repo_skip 'qubes-template-whonix' || return 1
   repo_skip 'virtualbox' || return 1
   repo_skip 'Whonix-Installer' || return 1
   repo_skip 'Whonix-Starter' || return 1
}

## Resets all batch_meta_* global state variables to default values.
reset_batch_meta_globals() {
   batch_meta_category_list=()
   batch_meta_debian_control_web_link=''
   batch_meta_file_name_without_reponame=''
   batch_meta_file_web_link=''
   batch_meta_gateway_only='n'
   batch_meta_installed_by_default='y'
   batch_meta_non_qubes_whonix_only='n'
   batch_meta_project_list=()
   batch_meta_qubes_whonix_only='n'
   batch_meta_relative_file_name=''
   batch_meta_repo_web_link=''
   batch_meta_workstation_only='n'
   unset batch_meta_file_header_done_list
   declare -A -g batch_meta_file_header_done_list
}

## XXX: Duplicated.
nothing_to_commit() {
   local nothing_to_commit_msg_one git_status_last_line
   nothing_to_commit_msg_one="nothing to commit, working tree clean"

   git_status_last_line="$(git status | tail -n1)" || true
   if [ "${git_status_last_line}" = "${nothing_to_commit_msg_one}" ]; then
      return 0
   fi
   return 1
}

## Extracts a package description from a debian/control file, writing it to
## the specified file using a special template for the headline. The template
## can be an arbitrary string, but must contain the string XXX_REPLACE_ME_XXX
## to indicate where the first line of the description should be inserted.
extract_description_from_debian_control() {
   local source_file target_file headline_template line first_word \
     description_found headline_written

   source_file="${1:-}"
   target_file="${2:-}"
   headline_template="${3:-}"
   if [ -z "${source_file}" ]; then
      printf "%s\n" "${red}${bold}FATAL ERROR: Empty source_file parameter provided!${reset}"
      exit 1
   elif [ -z "${target_file}" ]; then
      printf "%s\n" "${red}${bold}FATAL ERROR: Empty target_file parameter provided!${reset}"
      exit 1
   elif [ -z "${headline_template}" ]; then
      printf "%s\n" "${red}${bold}FATAL ERROR: Empty headline_template parameter provided!${reset}"
      exit 1
   elif ! grep --quiet 'XXX_REPLACE_ME_XXX' <<< "${headline_template}" ; then
      printf "%s\n" "${red}${bold}FATAL ERROR: headline_template parameter lacks XXX_REPLACE_ME_XXX placeholder!${reset}"
      exit 1
   fi

   description_found='n'
   headline_written='n'

   while IFS= read -r line; do
      read -r first_word _ <<< "${line}"
      if [ "${first_word}" = 'Description:' ]; then
         description_found='y'
      elif [ "${line:0:1}" != ' ' ] && [ "${description_found}" = 'y' ]; then
         break
      fi

      if [ "${description_found}" = 'y' ]; then
         if [ -z "${line}" ]; then
            break;
         fi
         if [ "${headline_written}" = 'n' ]; then
            headline="$(awk -F':' '{ print $2 }' <<< "${line}")"
            ## Strip off the one leading space if it exists. TODO: Is there
            ## ever a situation where this doesn't exist?
            if [ "${headline:0:1}" = ' ' ]; then
               headline="${headline:1}"
            fi
            headline_template="${headline_template//'XXX_REPLACE_ME_XXX'/"${headline}"}"
            printf "%s\n" "${headline_template}" | tee -a -- "${target_file}" >/dev/null
            printf "%s\n" '' | tee -a -- "${target_file}" >/dev/null
            headline_written='y'
            continue
         fi
         if [ "${line}" = ' .' ]; then
            printf "%s\n" '' | tee -a -- "${target_file}" >/dev/null
         else
            # Strip all leading whitespace
            read -r line <<< "${line}"
            printf "%s\n" "${line}" | tee -a -- "${target_file}" >/dev/null
         fi
      fi
   done < "${source_file}"
}

## Creates and appends to package description files.
internal_descr_writer() {
   local project category description_file control_file line first_word \
     description_found headline_written

   for project in "${batch_meta_project_list[@]}"; do
      for category in "${batch_meta_category_list[@]}"; do
         description_file="${package_documentation_dir}/${batch_current_package_reponame}_${category}_${project}.mediawiki"

         ## Create the file if it does not yet exist
         if ! [ -e "${description_file}" ]; then
            control_file="${batch_current_package_path}/debian/control"
            if ! [ -f "${control_file}" ]; then
               printf "%s\n" "${red}${bold}FATAL ERROR: control_file '${control_file}' does not exist or is not a regular file!${reset}"
               exit 1
            fi

            touch "${description_file}"

            printf "%s\n" "== ${batch_current_package_reponame} ==" | tee -a "${description_file}"
            printf "%s\n" '' | tee -a -- "${description_file}" >/dev/null
            printf "%s\n" "* ${batch_meta_repo_web_link}" | tee -a -- "${description_file}" >/dev/null
            printf "%s\n" "* [${batch_meta_debian_control_web_link} debian/control]" | tee -a -- "${description_file}" >/dev/null

            extract_description_from_debian_control "${control_file}" "${description_file}" '=== XXX_REPLACE_ME_XXX ==='

         ## If the file does exist, make sure it's a real file
         elif ! [ -f "${description_file}" ]; then
            printf "%s\n" "${red}${bold}FATAL ERROR: ${description_file} exists but is not a regular file!${reset}"
            exit 1
         fi

         if ! [ "${batch_meta_file_header_done_list["${batch_meta_relative_file_name}_${batch_current_package_reponame}_${category}_${project}"]}" = 'y' ]; then
            printf "%s\n" "=== ${batch_meta_file_name_without_reponame} ===" | tee -a -- "${description_file}" >/dev/null
            printf "%s\n" '' | tee -a -- "${description_file}" >/dev/null
            printf "%s\n" "* [${batch_meta_file_web_link} ${batch_meta_file_name_without_reponame}]" | tee -a -- "${description_file}" >/dev/null
            [ "${batch_meta_gateway_only}" = 'y' ] && printf "%s\n" '* gateway only<!--gateway-only-->' | tee -a -- "${description_file}" >/dev/null
            [ "${batch_meta_workstation_only}" = 'y' ] && printf "%s\n" '* workstation only<!--workstation-only-->' | tee -a -- "${description_file}" >/dev/null
            [ "${batch_meta_non_qubes_whonix_only}" = 'y' ] && printf "%s\n" '* Non-Qubes-Whonix only' | tee -a -- "${description_file}" >/dev/null
            [ "${batch_meta_qubes_whonix_only}" = 'y' ] && printf "%s\n" '* Qubes-Whonix only' | tee -a -- "${description_file}" >/dev/null
            [ "${batch_meta_installed_by_default}" = 'n' ] && printf "%s\n" '* Not installed by default.' | tee -a -- "${description_file}" >/dev/null
            printf "%s\n" '' | tee -a -- "${description_file}" >/dev/null

            batch_meta_file_header_done_list["${batch_meta_relative_file_name}_${batch_current_package_reponame}_${category}_${project}"]='y'
         fi

         printf "%s\n" "$*" | tee -a -- "${description_file}" >/dev/null
      done
   done
}

## Extracts a list of unique info fields from package documentation file
## names. The info fields that can be searched for are 'category' and
##'project'. This takes advantage of the fact that in package documentation
## filenames, the reponames, categories, and projects are separated by
## underscores and can thus be extracted easily that way.
get_package_documentation_info_list() {
   local info_type info_idx info_list file_name_full_path file_name_only \
      file_info info_from_list info_match_found

   info_type="${1:-}"
   if [ "${info_type}" = 'category' ]; then
      info_idx='2'
   elif [ "${info_type}" = 'project' ]; then
      info_idx='3'
   else
      printf "%s\n" "${red}${bold}FATAL ERROR: info_type is not 'category' or 'project'!${reset}"
      exit 1
   fi

   info_list=()

   for file_name_full_path in "${package_documentation_dir}/"* ; do
      file_name_only="${file_name_full_path##*/}"
      file_info="$(cut -d'_' -f"${info_idx}" <<< "${file_name_only}")"
      if [[ "${file_info}" =~ \.merged\.mediawiki ]] \
         || [[ "${file_info}" =~ \.allmerged\.mediawiki ]]; then
         continue
      fi
      file_info="$(str_replace '.mediawiki' '' <<< "${file_info}")"
      info_match_found='n'
      for info_from_list in "${info_list[@]}"; do
         if [ "${file_info}" = "${info_from_list}" ]; then
            info_match_found='y'
            break
         fi
      done
      if [ "${info_match_found}" = 'n' ]; then
         info_list+=( "${file_info}" )
         printf "%s\n" "${file_info}"
      fi
   done
}

## Sets the list of Github remotes for the the current package, as defined in
## batch_current_package_reponame. See pkg_git_remotes_add for a good
## definition of the remotes typically used Kicksecure and Whonix packages.
## TODO: Use this function as the single source of truth for most of these
## remote names, rather than duplicating info.
set_batch_current_package_remote_list() {
   batch_current_package_remote_list=( "github-${batch_current_project_name,,}" )
   ## derivative-maker has a couple extra remotes
   if [ "${batch_current_package_reponame}" = 'derivative-maker' ]; then
      batch_current_package_remote_list+=( 'github-kicksecure' 'github-whonix' )
   fi
}

## Diffs two Git branches against each other, reporting whether they have
## differences or not. Returns 0 when differences are found, 1 otherwise.
internal_git_diff() {
   local git_branch_one git_branch_two git_exit_code git_diff_output

   git_branch_one="${1:-}"
   git_branch_two="${2:-}"
   if [ -z "${git_branch_one}" ]; then
      printf "%s\n" "${red}${bold}FATAL ERROR: git_branch_one argument is empty!${reset}"
      exit 1
   elif [ -z "${git_branch_two}" ]; then
      printf "%s\n" "${red}${bold}FATAL ERROR: git_branch_two argument is empty!${reset}"
      exit 1
   fi

   git_exit_code='0'
   git_diff_output="$(git diff --stat "${git_branch_one}" "${git_branch_two}")" || { git_exit_code="$?"; true; };

   if [ "${git_exit_code}" = '128' ]; then
      true "ERROR: Something is wrong. See above. Press enter to continue."
      read -r _
   fi

   if [ -z "${git_diff_output}" ]; then
      if [ "${git_exit_code}" = '0' ]; then
         return 1
      fi
   fi

   return 0
}

## . END Utility functions }

## BEGIN Commands {
#
# NOTE: Commands may assume that $PWD is the directory in which they should
# make changes, unless they are excluded from the batch processing mechanism.

## Commits changes to all project README files.
pkg_readme_creator_commit() {
   local readme_file

   repo_skip 'derivative-maker' || return 0

   ## global readme file
   if [ -f 'README_generic.md' ]; then
      ## When there is a README_generic.md, this is a way to express
      ## "do not create a generic README.md for that package, because it
      ## already has a real readme."
      readme_file='README_generic.md'
   else
      readme_file='README.md'
   fi

   git add "${readme_file}" || true

   if nothing_to_commit; then
      return 0
   fi

   git diff --cached

   git commit -m 'readme' || true
}

## Regenerates the README files for all projects except derivative-maker and
## tirdad, using the description from 'debian/control' and a master README
## template. The files are placed at `README.md` or `README_generic.md`,
## depending on which kind of README the project uses.
pkg_readme_creator_do() {
   local control_file generic_readme_template_file readme_file line \
     first_word description_found headline_written headline search_str \
     replace_str

   repo_skip 'derivative-maker' || return 0

   ## TODO
   repo_skip 'lxqt-wayland-session' || return 0

   control_file='debian/control'
   if ! [ -f "${control_file}" ]; then
      true "No file"
      return
   fi

   generic_readme_template_file="${derivative_maker_source_code_dir}/packages/kicksecure/developer-meta-files/README_generic_template_file.md"
   if ! [ -f "${generic_readme_template_file}" ]; then
      printf "%s\n" "${red}${bold}FATAL ERROR: generic_readme_template_file '${generic_readme_template_file}' does not exist!${reset}"
      exit 1
   fi

   ## global readme file
   if [ -f 'README_generic.md' ]; then
      ## When there is a README_generic.md, this is a way to express
      ## "do not create a generic README.md for that package, because it
      ## already has a real readme."
      readme_file='README_generic.md'
   else
      readme_file='README.md'
   fi

   description_found='n'
   headline_written='n'

   safe-rm --force "${readme_file}"

   extract_description_from_debian_control "${control_file}" "${readme_file}" '# XXX_REPLACE_ME_XXX #'

   printf "%s\n" "" | tee -a -- "${readme_file}" >/dev/null

   cat -- "${generic_readme_template_file}" | tee -a -- "${readme_file}" >/dev/null

   search_str='%%project_clearnet%%'
   replace_str="${batch_current_project_website}"
   str_replace "${search_str}" "${replace_str}" "${readme_file}"

   search_str='%%package-name%%'
   replace_str="${batch_current_package_reponame}"
   str_replace "${search_str}" "${replace_str}" "${readme_file}"

   true "Done: ${readme_file}"
}

## Harvests metadata from individual files under all projects except
## derivative-maker, and converts it to package documentation in the form of
## MediaWiki files. Places documentation under package_documentation_dir.
## TODO: Only handles the first metadata section of each file. Some files have
## multiple metadata sections.
pkg_descr_creator() {
   local file_name file_list skip_file_list base_name do_skip_file \
     skip_file_name in_meta_section description_found line first_word \
     second_word third_word temp

   repo_skip 'derivative-maker' || return 0

   skip_file_list=(
      'changelog.upstream'
      'CONTRIBUTING.md'
      'COPYING'
      'GPLv3'
      'Makefile'
      'README.md'
   )

   if [ "${batch_func_init_done}" = 'n' ]; then
      safe-rm -f -- "${package_documentation_dir}"/*.mediawiki
      batch_func_init_done='y'
   fi

   readarray -t file_list < <(find "${batch_current_package_path}" -type f -not -iwholename '*.git*')
   for file_name in "${file_list[@]}"; do
      ## Skip binary files for better performance.
      if ! isutf8 -q "${file_name}" ; then
         continue
      fi

      true "file_name: ${file_name}"

      base_name="${file_name##*/}"
      do_skip_file='n'
      for skip_file_name in "${skip_file_list[@]}"; do
         if [ "${skip_file_name}" = "${base_name}" ]; then
            do_skip_file='y'
            break
         fi
      done
      if [ "${do_skip_file}" = 'y' ]; then
         continue
      fi

      if ! [ -f "${file_name}" ]; then
         printf "%s\n" "${red}${bold}FATAL ERROR: pkg_descr_creator: file_name '${file_name}' does not exist!${reset}"
         exit 1
      fi

      reset_batch_meta_globals

      batch_meta_relative_file_name="$(str_replace "${derivative_maker_source_code_dir}/packages/${batch_current_project_name,,}/" '' <<< "${file_name}")"
      batch_meta_file_name_without_reponame="/$(cut -d'/' -f2- <<< "${batch_meta_relative_file_name}")"
      batch_meta_repo_web_link="https://github.com/${batch_current_project_name}/${batch_current_package_reponame}"
      batch_meta_debian_control_web_link="https://github.com/${batch_current_project_name}/${batch_current_package_reponame}/blob/master/debian/control"
      batch_meta_file_web_link="https://github.com/${batch_current_project_name}/${batch_current_package_reponame}/blob/master${batch_meta_file_name_without_reponame}"

      in_meta_section='n'
      description_found='n'
      while read -r line; do
         true "line: ${line}"
         read -r first_word second_word third_word _ <<< "${line}" || true

         if [ "${first_word}" = '####' ]; then
            true "second_word: '${second_word}'"
            if [ "${second_word}" = 'meta' ]; then
               if [ "${third_word}" = 'start' ]; then
                  in_meta_section='y'
                  printf "%s\n" '##################################################'
                  printf "%s\n" "${file_name}"
                  continue
               elif [ "${third_word}" = 'end' ]; then
                  in_meta_section='n'
                  break
               else
                  printf "%s\n" "${red}${bold}FATAL ERROR: Unexpected meta subcommand third_word '${third_word}'!${reset}"
                  exit 1
               fi
            fi

            if [ "${in_meta_section}" = 'y' ]; then
               if [ "${second_word}" = 'project' ]; then
                  ## TODO: Consider rewriting this to handle a comma-separated
                  ## list of projects
                  temp="$(str_replace '#### project ' '' <<< "${line}") " # extra space needed for readarray
                  temp="$(str_replace ' and' '' <<< "${temp}")"
                  readarray -d' ' -t batch_meta_project_list <<< "${temp}"
                  # Remove a mangled entry at the end.
                  batch_meta_project_list=( "${batch_meta_project_list[@]:0:((${#batch_meta_project_list[@]} - 1))}" )
                  printf "%s\n" "batch_meta_project_list: '${batch_meta_project_list[*]}'"
                  continue
               elif [ "${second_word}" = 'non_qubes_whonix_only' ]; then
                  if [ "${third_word}" = 'yes' ]; then
                     batch_meta_non_qubes_whonix_only='y'
                     continue
                  fi
               elif [ "${second_word}" = 'qubes_whonix_only' ]; then
                  if [ "${third_word}" = 'yes' ]; then
                     batch_meta_qubes_whonix_only='y'
                     continue
                  fi
               elif [ "${second_word}" = 'gateway_only' ]; then
                  if [ "${third_word}" = 'yes' ]; then
                     batch_meta_gateway_only='y'
                     continue
                  fi
               elif [ "${second_word}" = 'workstation_only' ]; then
                  if [ "${third_word}" = 'yes' ]; then
                     batch_meta_workstation_only='y'
                     continue
                  fi
               elif [ "${second_word}" = 'installed_by_default' ]; then
                  if [ "${third_word}" = 'no' ]; then
                     batch_meta_installed_by_default='n'
                     continue
                  fi
               elif [ "${second_word}" = 'category' ]; then
                  ## TODO: Consider rewriting this to handle a comma-separated
                  ## list of categories
                  temp="$(str_replace '#### category ' '' <<< "${line}") " # extra space needed for readarray
                  temp="$(str_replace ' and' '' <<< "${temp}")"
                  readarray -d' ' -t batch_meta_category_list <<< "${temp}"
                  # Remove a mangled entry at the end.
                  batch_meta_category_list=( "${batch_meta_category_list[@]:0:((${#batch_meta_category_list[@]} - 1))}" )
                  printf "%s\n" "batch_meta_category_list: '${batch_meta_category_list[*]}'"
                  continue
               elif [ "${second_word}" = 'description' ]; then
                  description_found='y'
                  continue
               else
                  ## this looks like a metadata field line and we're in the
                  ## meta section, but the field keyword was not recognized.
                  continue
               fi
            else
               ## this looks like a metadata field line, but we're not in the
               ## meta section and a 'meta' keyword was not encountered
               continue
            fi
         fi

         if [ "${in_meta_section}" = 'y' ]; then
            ## At this point, we're in the meta section, but the current line
            ## doesn't look like a metadata field line. Usually this means
            ## that we're in a description zone.
            if [ "${description_found}" = 'y' ]; then
               if [ "${line}" = '' ]; then
                  internal_descr_writer ''
                  continue
               fi

               ## translate a line consisting entirely of '##' to a newline
               if [ "${line}" = '##' ]; then
                  internal_descr_writer ''
                  continue
               fi

               ## Lines prefixed by a single '#' and lines with no '#'s
               ## prefixing them at all are code, anything else should be
               ## treated as normal text.
               if [ "${line:0:1}" = '#' ] && [ "${line:1:1}" != '#' ]; then
                  line="* <code>${line}</code>"
                  internal_descr_writer "${line}"
                  continue
               elif [ "${line:0:1}" != '#' ]; then
                  line="* <code>${line}</code>"
                  internal_descr_writer "${line}"
                  continue
               else
                  line="$(str_replace '## ' '' <<< "${line}")"
                  internal_descr_writer "${line}"
               fi
            fi
         fi
      done < "${file_name}"
   done
}

## Merges together package documentation files for all packages sharing a
## project, category, and target machine type in common. pkg_descr_creator
## must be run first. Note that this function does NOT go through the batch
## processing mechanism.
pkg_descr_merger() {
   local project_list category_list project category file_name_full_path \
      file_name_only package_name machine merged_file

   readarray -t project_list < <(get_package_documentation_info_list 'project')
   readarray -t category_list < <(get_package_documentation_info_list 'category')

   for project in "${project_list[@]}"; do
      for category in "${category_list[@]}"; do
         for file_name_full_path in "${package_documentation_dir}/"*"_${category}_${project}.mediawiki" ; do
            ## If we hit on a combination of category and project that never
            ## occurs, file_name_full_path will contain a bogus filename that
            ## needs to be skipped.
            if [ ! -f "${file_name_full_path}" ]; then
               continue
            fi

            file_name_only="${file_name_full_path##*/}"
            package_name="$(str_replace "_${category}_${project}.mediawiki" '' <<< "${file_name_only}")"

            if [ "${project}" = 'Kicksecure' ]; then
               machine='all'
            elif [ "${project}" = 'Whonix' ]; then
               if grep --quiet '\-gw-' <<< "${package_name}" ; then
                  machine='gateway'
               elif grep --quiet '\-ws-' <<< "${package_name}" ; then
                  machine='workstation'
               else
                  machine='shared'
               fi

               ## TODO: don't hardcode here
               [ "${package_name}" = 'onion-grater' ] && machine='gateway'
               [ "${package_name}" = 'anon-apps-config' ] && machine='workstation'
               [ "${package_name}" = 'bindp' ] && machine='workstation'
            else
               printf "%s\n" "${red}${bold}FATAL ERROR: Unrecognized project '${project}'!${reset}"
               exit 1
            fi

            merged_file="${package_documentation_dir}/${machine}_${category}_${project}.merged.mediawiki"

            if ! [ "${merge_file_reset_list["${merged_file}"]}" = 'y' ] ; then
               safe-rm -f -- "${merged_file}"
               merge_file_reset_list["${merged_file}"]='y'
            fi

            cat -- "${file_name_full_path}" | tee -a -- "${merged_file}" >/dev/null
         done
      done
   done
}

## Merges together the merged files output by pkg_descr_merger, so that the
## final resulting files contain documentation for all packages sharing a
## project and category in common. Runs pkg_descr_merger, so you can run this
## without running pkg_descr_merger first. Note that this function does NOT go
## through the batch processing mechanism.
pkg_descr_merge_all() {
   local project_list category_list machine_list project category machine \
      all_merged_file file_name_full_path

   readarray -t project_list < <(get_package_documentation_info_list 'project')
   readarray -t category_list < <(get_package_documentation_info_list 'category')

   pkg_descr_merger

   machine_list=( 'all' 'gateway' 'workstation' 'shared' )

   for project in "${project_list[@]}"; do
      for category in "${category_list[@]}"; do
         all_merged_file="${package_documentation_dir}/${category}_${project}.allmerged.mediawiki"
         safe-rm -f -- "${all_merged_file}"

         for machine in "${machine_list[@]}"; do
            file_name_full_path="${package_documentation_dir}/${machine}_${category}_${project}.merged.mediawiki"
            if ! [ -f "${file_name_full_path}" ]; then
               continue
            fi

            true "file_name_full_path: ${file_name_full_path}"
            true "all_merged_file: ${all_merged_file}"

            if [ "${machine}" = 'all' ]; then
               printf "%s\n" '= Kicksecure =' | tee -a -- "${all_merged_file}" >/dev/null
            elif [ "${machine}" = 'gateway' ]; then
               printf "%s\n" '= Whonix-Gateway =' | tee -a -- "${all_merged_file}" >/dev/null
            elif [ "${machine}" = 'workstation' ]; then
               printf "%s\n" '= Whonix-Workstation =' | tee -a -- "${all_merged_file}" >/dev/null
            elif [ "${machine}" = 'shared' ]; then
               printf "%s\n" '= Shared by Whonix-Gateway and Whonix-Workstation =' | tee -a -- "${all_merged_file}" >/dev/null
            fi

            cat -- "${file_name_full_path}" | tee -a -- "${all_merged_file}" >/dev/null
         done
      done
   done
}

## Echos repository links for all repositories.
pkg_links_echo() {
   printf "%s\n" "https://github.com/${batch_current_project_name}/${batch_current_package_reponame}"
   #printf "%s\n" "https://github.com/adrelanos/${batch_current_package_reponame}"
}

## Echos Markdown-formatted links for all repositories.
pkg_links_markdown_echo() {
   printf "%s\n" "[${batch_current_package_reponame}](https://github.com/${batch_current_project_name}/${batch_current_package_reponame})"
}

## Echos links to all commits made by adrelanos for all repositories.
pkg_echo_commits_adrelanos() {
   printf "%s\n" "https://github.com/${batch_current_project_name}/${batch_current_package_reponame}/commits?author=adrelanos"
}

## Echos the names of all packages.
pkg_names_echo() {
   printf "%s\n" "${batch_current_package_reponame}"
}

## Echos the names of all packages, along with the project each package is
## under.
pkg_names_test() {
   printf "%s\n" "${batch_current_package_reponame} | ${batch_current_project_name}"
}

## Opens all package links in Tor Browser.
pkg_links_open() {
   torbrowser --new-tab "$(pkg_links_echo)"
}

## Sets up standard Git remotes for all repos.
pkg_git_remotes_add() {
   git remote rm origin || true

   git remote add     adrelanos         "git@github.com:adrelanos/${batch_current_package_reponame}.git" || true
   git remote set-url adrelanos         "git@github.com:adrelanos/${batch_current_package_reponame}.git" || true

   git remote add     ArrayBolt3         "git@github.com:ArrayBolt3/${batch_current_package_reponame}.git" || true
   git remote set-url ArrayBolt3         "git@github.com:ArrayBolt3/${batch_current_package_reponame}.git" || true

   if [ "${batch_current_project_name,,}" = "whonix" ]; then
      git remote add     github-whonix     "git@github.com:Whonix/${batch_current_package_reponame}.git" || true
      git remote set-url github-whonix     "git@github.com:Whonix/${batch_current_package_reponame}.git" || true

      #git remote add     gitlab-whonix     "git@gitlab.com:whonix/${batch_current_package_reponame}.git" || true
      #git remote set-url gitlab-whonix     "git@gitlab.com:whonix/${batch_current_package_reponame}.git" || true
   elif [ "${batch_current_project_name,,}" = "kicksecure" ]; then
      git remote add     github-kicksecure "git@github.com:Kicksecure/${batch_current_package_reponame}.git" || true
      git remote set-url github-kicksecure "git@github.com:Kicksecure/${batch_current_package_reponame}.git" || true

      #git remote add     gitlab-kicksecure "git@gitlab.com:kicksecure/${batch_current_package_reponame}.git" || true
      #git remote set-url gitlab-kicksecure "git@gitlab.com:kicksecure/${batch_current_package_reponame}.git" || true
   elif [ "${batch_current_project_name,,}" = "derivative-maker" ]; then
      git remote add     github-derivative-maker "git@github.com:derivative-maker/${batch_current_package_reponame}.git" || true
      git remote set-url github-derivative-maker "git@github.com:derivative-maker/${batch_current_package_reponame}.git" || true

      git remote add     github-kicksecure "git@github.com:Kicksecure/${batch_current_package_reponame}.git" || true
      git remote set-url github-kicksecure "git@github.com:Kicksecure/${batch_current_package_reponame}.git" || true

      git remote add     github-whonix "git@github.com:Whonix/${batch_current_package_reponame}.git" || true
      git remote set-url github-whonix "git@github.com:Whonix/${batch_current_package_reponame}.git" || true

      #git remote add     gitlab-derivative-maker "git@gitlab.com:derivative-maker/${batch_current_package_reponame}.git" || true
      #git remote set-url gitlab-derivative-maker "git@gitlab.com:derivative-maker/${batch_current_package_reponame}.git" || true
   else
      printf "%s\n" "ERROR: batch_current_project_name wrong! (1)"
   fi

   true
}

pkg_git_check_current_branch() {
   local current_branch expected_branch
   current_branch="$(git branch --show-current)"

   if [ "$batch_current_package_reponame" = "qubes-template-kicksecure" ]; then
      expected_branch="trixie"
   elif [ "$batch_current_package_reponame" = "qubes-template-whonix" ]; then
      expected_branch="trixie"
   else
      expected_branch="master"
   fi

   if [ "$current_branch" = "$expected_branch" ]; then
      return 0
   fi

   printf "%s\n" "unexpected branch! batch_current_package_reponame: '$batch_current_package_reponame' | current_branch: '$current_branch'"
   return 1
}

## Creates a 'trixie' branch for all repos.
pkg_git_branch_trixie() {
   git branch trixie || true
   true
}

## Checks out the 'master' branch of all repos.
pkg_git_branch_checkout_master() {
   git checkout master
   true
}

## Checks out the 'bullseye' branch of all repos.
pkg_git_branch_checkout_bullseye() {
   git checkout bullseye
   true
}

## Checks out the 'trixie' branch of all repos.
pkg_git_branch_checkout_trixie() {
   git checkout trixie
   true
}

pkg_git_branch_fetch_all() {
   local origin_name package_remote_item git_pid

   #origin_name="github-${batch_current_project_name,,}"
   origin_name="ArrayBolt3"

   for package_remote_item in "${origin_name}"; do
      git fetch "${package_remote_item}" &
      git_pid="$!"
      git_pid_list_str="${git_pid_list_str} ${git_pid}"
   done

   #wait $git_pid_list_str

   true
}

pkg_git_diff_and_merge_branch() {
   #repo_skip 'derivative-maker' || return 0

   #origin_branch="github-${batch_current_project_name,,}/master"
   origin_branch="ArrayBolt3/arraybolt3/trixie"

   ## Check if branch exists.
   if ! git --no-pager diff --stat "${origin_branch}" &>/dev/null ; then
      ## No such branch.
      return 0
   fi
   ## Branch exists.

   if [ "$(git --no-pager diff -C --stat ..."${origin_branch}")" = "" ]; then
      ## No diff. Branch already merged.
      return 0
   fi

   if [ "$(git --no-pager diff -C --stat "${origin_branch}")" = "" ]; then
      ## No diff. Branch already merged.
      return 0
   fi

   printf '%s\n' "${batch_current_package_reponame}"
   git --no-pager diff -C --stat "${origin_branch}"
   git --no-pager diff -C "${origin_branch}"
   git --no-pager kdiff -C "${origin_branch}"

   git merge "${origin_branch}"

   printf '%s\n' "--------------------------------------------------"
   true

   ## Manually re-run script.
   exit
}

## Detects packages with Bash profile.d files, and adds symlink lines to zsh's
## zprofile.d directory to the Debian packaging if needed.
pkg_profile_d_zsh_creator() {
   local file_name_list file_name_without_slash profile_d_symlink \
      basename_symlink_with_file_extension \
      basename_symlink_without_file_extension

   if ! test -d 'etc/profile.d' ; then
      return 0
   fi

   readarray -t file_name_list < <(find etc/profile.d/*)

   for file_name_without_slash in "${file_name_list[@]}"; do
      profile_d_symlink="/${file_name_without_slash}"
      basename_symlink_with_file_extension="${profile_d_symlink##*/}"
      basename_symlink_without_file_extension="${basename_symlink_with_file_extension%.*}"
      package_links_file="debian/${batch_current_package_reponame}.links"
      z_profile_d_symlink="/etc/zprofile.d/${basename_symlink_without_file_extension}.zsh"
      symlink_config_entry="$profile_d_symlink $z_profile_d_symlink"
      if [ -f "${package_links_file}" ]; then
         if grep --quiet "${symlink_config_entry}" "${package_links_file}" ; then
            true 'already exists.'
            continue
         fi
      fi
      printf "%s\n" "${symlink_config_entry}" | tee -a -- "${package_links_file}" >/dev/null
   done
   true
}

## Updated CONTRIBUTING.md in all repos except derivative-maker, kloak, and
## corridor.
pkg_copy_contributing_file() {
   repo_skip 'derivative-maker' || return 0
   repo_skip 'kloak' || return 0

   cp "${derivative_maker_source_code_dir}/CONTRIBUTING.md" "${batch_current_package_path}/CONTRIBUTING.md"
   git add "${batch_current_package_path}/CONTRIBUTING.md"
   git commit -m "update"

   true
}

## Gets rid of all 'debian/compat' files in all repos.
pkg_compat_delete() {
   git rm "debian/compat"

   true
}

## Updates all packages that ship systemd services and Build-Depend on
## debhelper 13 or later to depend on debhelper 13.11.6 or later.
pkg_build_depends_systemd() {
   local search replace

   repo_skip 'derivative-maker' || return 0

   if ! find . -not -iwholename '*.git*' | grep --fixed-strings 'usr/lib/systemd/system' | grep --color '.service$' ; then
      return 0
   fi

   search='Build-Depends: debhelper (>= 13)'
   replace='Build-Depends: debhelper (>= 13.11.6)'
   str_replace "${search}" "${replace}" "${batch_current_package_path}/debian/control"

   true
}

## Previously was used to do mass package changes for porting packages from
## Bullseye to Bookworm. Retained for future reference.
# pkg_debhelper_bump() {
#    local one two
#    one='debhelper (>= 12)'
#    two='debhelper (>= 13), debhelper-compat (= 13)'
#    str_replace "$one" "$two" "${batch_current_package_path}/debian/control"
#
#    one='http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/'
#    two='https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/'
#    str_replace "$one" "$two" debian/copyright
#    str_replace "$one" "$two" COPYING
#
#    one="Standards-Version: 3.9.8"
#    two="Standards-Version: 4.6.2
#
#    str_replace "$one" "$two" "${batch_current_package_path}/debian/control"
#
#    one="debian-watch-may-check-gpg-signature"
#    two="debian-watch-does-not-check-openpgp-signature"
#    str_replace "$one" "$two" "${batch_current_package_path}/debian/source/lintian-overrides"
#
#    one="ruby-ronn"
#    two="ronn"
#    str_replace "$one" "$two" "${batch_current_package_path}/debian/control"
#
#    one='Priority: extra'
#    two='Priority: optional'
#    str_replace "$one" "$two" "${batch_current_package_path}/debian/control"
#
#    one='--with=config-package --with=systemd'
#    two='--with=config-package'
#    str_replace "$one" "$two" "${batch_current_package_path}/debian/rules"
#
#    git add "${batch_current_package_path}/debian/rules"
#    git commit -m "port to debian bookworm" || true
# }

## Generates diffs for specific files between each package and an arbitrary
## template package. Also checks for the existence of several files, and
## synchronizes files that should be identical.
pkg_packaging_files_diff() {
   local compare_with_full_path contributing_md_absent_package_list \
      package_lacks_contributing_md contributing_md_absent_package \
      check_file_list check_file

   repo_skip 'derivative-maker' || return 0

   compare_with_full_path="$(realpath "../../${packaging_files_diff_template_package_relative_path}")"

   if [ "${batch_func_init_done}" = 'n' ]; then
      if ! [ -f "${compare_with_full_path}/COPYING" ]; then
         printf "%s\n" "${red}${bold}FATAL ERROR: '${compare_with_full_path}/COPYING' does not exist!"
         exit 1
      fi
      batch_func_init_done='y'
   fi

   ## Don't bother looking for CONTRIBUTING.md for the following list of
   ## packages. For all others, make sure that CONTRIBUTING.md exists.
   contributing_md_absent_package_list=(
      'mediawiki-shell'
      'tirdad'
      'tor-ctrl'
      'kloak'
   )

   package_lacks_contributing_md='n'
   for contributing_md_absent_package in "${contributing_md_absent_package_list[@]}"; do
      if [ "${batch_current_package_reponame}" = "${contributing_md_absent_package}" ]; then
         package_lacks_contributing_md='y'
         break
      fi
   done
   if ! [ "${package_lacks_contributing_md}" = 'y' ]; then
      if ! [ -f "${batch_current_package_path}/CONTRIBUTING.md" ]; then
         printf "%s\n" "${red}${bold}FATAL ERROR: Package '${batch_current_package_reponame}' is missing a CONTRIBUTING.md file!${reset}"
         exit 1
      fi
   fi

   if ! [ -f "${batch_current_package_path}/README.md" ]; then
      if ! [ -f "${batch_current_package_path}/README.mediawiki" ]; then
         ## TODO: Should README_generic.d be taken into account here?
         printf "%s\n" "${red}${bold}FATAL ERROR: Neither '${batch_current_package_path}/README.md' nor '${batch_current_package_path}/README.mediawiki' exist!${reset}"
         exit 1
      fi
   fi

   diff "${compare_with_full_path}/debian/source/format" "${batch_current_package_path}/debian/source/format"

   if [ "${batch_current_package_reponame}" = 'corridor' ]; then
      ## Corridor has no COPYING but a LICENSE-ISC file.
      if ! [ -f "${batch_current_package_path}/LICENSE-ISC" ]; then
         printf "%s\n" "${red}${bold}FATAL ERROR: '${batch_current_package_path}/LICENSE-ISC' does not exist!${reset}"
         return 1
      fi
#  elif [ "${batch_current_package_reponame}" = 'hardened_malloc' ]; then
#     ## hardened_malloc has no COPYING but a LICENSE file.
#     if ! [ -f "${batch_current_package_path}/LICENSE" ]; then
#        printf "%s\n" "${red}${bold}FATAL ERROR: '${batch_current_package_path}/LICENSE' does not exist!${reset}"
#        return 1
#     fi
   else
      cp "${batch_current_package_path}/debian/copyright" "${batch_current_package_path}/COPYING"

      ## COPYING should always match debian/copyright.
      diff "${batch_current_package_path}/debian/copyright" "${batch_current_package_path}/COPYING"

      ## Would show license files that are different.
      ## This is useful to manually enable sometimes.
      #diff "${compare_with_full_path}/COPYING" "${batch_current_package_path}/COPYING"

#       if diff ~/old-copying "${batch_current_package_path}/COPYING" &>/dev/null ; then
#          cp "$HOME/derivative-maker/COPYING" "${batch_current_package_path}/COPYING"
#          cp "$HOME/derivative-maker/COPYING" "${batch_current_package_path}/debian/copyright"
#       else
#          ## TODO: delete /tmp/list-of-packages
#          printf "%s\n" "${batch_current_package_reponame}" | tee -a -- /tmp/list-of-packages >/dev/null
#          mousepad "${batch_current_package_path}/COPYING"
#          mousepad "${batch_current_package_path}/debian/copyright"
#       fi

   fi

   check_file_list=(
      "debian/changelog"
      "debian/control"
      "debian/copyright"
      "debian/rules"
      "debian/watch"
   )
   for check_file in "${check_file_list[@]}"; do
      if ! [ -f "${batch_current_package_path}/${check_file}" ]; then
         printf "%s\n" "${red}${bold}FATAL ERROR: check_file '${batch_current_package_path}/${check_file}' does not exist!"
         exit 1
      fi
   done

   true
}

## Fetches remote changes from all Git remotes for all repos.
pkg_git_fetch_remotes() {
   local package_remote_item git_pid

   for package_remote_item in "${batch_current_package_remote_list[@]}"; do
      git fetch "${package_remote_item}" &
      git_pid="$!"
      git_pid_list_str="${git_pid_list_str} ${git_pid}"
   done
   true
}

## Pushes local changes to all Git remotes for all repos.
pkg_git_push_remotes() {
   local branch_name_list package_remote_item branch_name_item git_pid

   if [ "$batch_current_package_reponame" = "qubes-template-kicksecure" ]; then
      branch_name_list=( "master" "bookworm" "trixie" )
   elif [ "$batch_current_package_reponame" = "qubes-template-whonix" ]; then
      branch_name_list=( "master" "bookworm" "trixie" )
   else
      branch_name_list=( "master" )
   fi

   for package_remote_item in "${batch_current_package_remote_list[@]}"; do
      for branch_name_item in "${branch_name_list[@]}"; do
         if internal_git_diff "${branch_name_item}" "${package_remote_item}/${branch_name_item}"; then
            git push "${package_remote_item}" "${branch_name_item}" &
            git_pid="$!"
            git_pid_list_str="${git_pid_list_str} ${git_pid}"
            ## TODO: Disable.
            ## Debugging.
            ## Word splitting is used here intentionally.
            # shellcheck disable=SC2086
            wait $git_pid_list_str
         fi
      done
   done

   true
}

## Pushes the most recent tag for all Git repos to all Git remotes of each
## repo.
pkg_git_push_tags() {
   local git_tag_latest package_remote_item git_pid

   git_tag_latest="$(git describe --tags "$(git rev-list --tags --max-count=1)")"

   ## https://stackoverflow.com/questions/72375239/git-offline-check-if-git-tag-was-already-pushed-to-remote

   for package_remote_item in "${batch_current_package_remote_list[@]}"; do
      git push "${package_remote_item}" "${git_tag_latest}" &
      git_pid="$!"
      git_pid_list_str="${git_pid_list_str} ${git_pid}"
   done

   true
}

## Checks out the changelog.upstream file of all repos.
git_reset_changelog_upstream() {
   git checkout changelog.upstream
}

## Commits changes to the readme files of all repos.
pkg_git_commit_readme() {
   local msg

   if [ -f 'README_generic.md' ]; then
      git add 'README_generic.md'
   elif [ -f 'README.md' ]; then
      git add 'README.md'
   elif [ -f 'README.mediawiki' ]; then
      git add 'README.mediawiki'
   fi

   #git diff --cached
   msg='readme'
   git commit -m "${msg}" || true
   git status
}

## Commits changes to the changelog files of all repos.
pkg_git_commit_changelog() {
   local msg

   git add 'debian/changelog'
   if [ -f 'changelog.upstream' ]; then
      git add 'changelog.upstream'
   fi
   msg='bumped changelog version'
   git commit -m "${msg}" || true
   git status
   true
}

## Creates a new commit called 'copyright' for all repos.
pkg_git_commit_copyright() {
   local msg

   git add 'debian/copyright'
   if [ -f 'COPYING' ]; then
      git add 'COPYING'
   fi
   msg='copyright'
   git commit -m "${msg}" || true
   git status
   true
}

pkg_git_commit_debian_control() {
   local msg

   git add 'debian/control'
   msg='bumped Standards-Version'
   git commit -m "${msg}" || true
   git status
   true
}

## Commits all changes to all repos with a tunable-set message.
pkg_git_commit_all() {
   echo TODO
   exit 1
   git add -A
   git commit -m "${git_commit_all_msg}" || true
   git status
   true
}

## Regenerate and commit manpages for all repos.
pkg_git_manpages() {
   local msg

   if ! [ -d 'man' ]; then
      return 0
   fi

   genmkfile manpages

   msg='re-generate man pages (generated using "genmkfile manpages")'
   git add 'man'/* || true
   git add 'auto-generated-man-pages'/* || true
   git commit -m "${msg}" || true
   git status
   true
}

## Generate and commit debian install files for all repos.
pkg_git_debinstfile() {
   local msg

   repo_skip 'derivative-maker' || return 0

   ## Skipped because the install file for corridor is maintained manually.
   repo_skip 'corridor' || return 0

   genmkfile debinstfile

   msg='add debian install file (generated using "genmkfile debinstfile")'
   printf "%s\n" "PWD: '$PWD'"
   git add "debian/${batch_current_package_reponame}.install" || true
   git commit -m "${msg}" || true
   git status
   true
}

## Commits packaging-related files for all repos. TODO: Perhaps don't hardcode
## the word 'trixie' here? Could be broken out into a tunable
pkg_git_commit_packaging() {
   local msg

   repo_skip 'derivative-maker' || return 0

   if nothing_to_commit; then
      ## TODO: should we just get rid of this `true` call?
      true "press enter to continue_not"
      #read -r temp
      return
   fi

   #git add 'Makefile'
   #git add 'make-helper.bsh'
   #git add 'debian/changelog'
   #git add 'changelog.upstream'
   #git add 'debian'/*'.install'
   git add 'debian/control'
   #git add -A
   msg='trixie'
   git commit -m "${msg}"
   git status
   true
}

## Add a dh_installchangelogs entry to all packages that don't already have
## one. Note that this was modified from the original to avoid writing
## duplicates.
pkg_add_dh_changelogs_override_to_debian_rules() {
   repo_skip 'derivative-maker' || return 0

   if grep --quiet 'override_dh_installchangelogs' 'debian/rules'; then
      return 0
    fi

   printf "%s\n" "
override_dh_installchangelogs:
  dh_installchangelogs changelog.upstream upstream" | tee -a -- "debian/rules" >/dev/null
   true
}

## Hard-reset all repos except for derivative-maker and developer-meta-files.
pkg_git_reset() {
   repo_skip 'derivative-maker' || return 0
   repo_skip 'developer-meta-files' || return 0
   git reset --hard
   git clean -dff
}

## Does a git diff followed by a git commit on all repos with changes, except
## for derivative-maker.
pkg_git_diff_and_commit() {
   repo_skip 'derivative-maker' || return 0

   ## Remove extra new lines.
   ## Thanks to llua http://unix.stackexchange.com/a/81689
   #a=$(<debian/rules); printf '%s\n' "$a" > debian/rules
   #continue

   if nothing_to_commit; then
      true "press enter to continue_not"
      #read -r temp
      return
   fi

   git add -A
   git diff --cached

   true "press enter to continue"
   #read -r temp

   git add -A
   ## TODO
   git commit -m "typo"

   if nothing_to_commit; then
      true "press enter to continue_not"
      #read -r temp
      return
   fi

   true
}

## Opens the debian/control file of all repos in Mousepad.
pkg_debian_control_open() {
   mousepad "${batch_current_package_path}/debian/control" &

   true
}

## Finds all Bash scripts in all repos and syntax-checks them.
pkg_bash_sanity_test() {
   local grep_opts

   grep_opts=(
      '--exclude-dir=.git'
      '--exclude-dir=auto-generated-man-pages'
      '--exclude-dir=man'
      '--recursive'
      '-l'
   )

   if ! grep "${grep_opts[@]}" --quiet '#!/bin/bash$' ; then
      return 0
   fi

   grep "${grep_opts[@]}" '#!/bin/bash$' | xargs -n 1 bash -n

   true
}

## Rewrites the Git submodules file for the derivative-maker repo.
pkg_git_submodule_file_writer() {
   local url

   repo_skip 'derivative-maker' || return 0

   if [ "${batch_func_init_done}" = 'n' ]; then
      safe-rm -- "${derivative_maker_source_code_dir}/.gitmodules"
      batch_func_init_done='y'
      printf "%s\n" '## This file is autogenerated by:
## autogenerated by dm-packaging-helper-script function pkg_git_submodule_file_writer

## BEGIN hardcoded part BEGIN ##

[submodule "live-build"]
        path = live-build
        url  = https://github.com/Kicksecure/live-build.git
        #url = https://gitlab.com/Kicksecure/live-build.git

[submodule "grml-debootstrap"]
        path = grml-debootstrap
        url  = https://github.com/Kicksecure/grml-debootstrap.git
        #url = https://gitlab.com/Kicksecure/grml-debootstrap.git

[submodule "grml-debootstraptest"]
        path = grml-debootstraptest
        url  = https://github.com/Kicksecure/grml-debootstraptest.git
        #url = https://gitlab.com/Kicksecure/grml-debootstraptest.git

[submodule "whonix-installer"]
        path = windows/Whonix-Installer
        url  = https://github.com/Whonix/Whonix-Installer.git
        #url = https://gitlab.com/whonix/Whonix-Installer.git

[submodule "whonix-starter"]
        path = windows/Whonix-Starter
        url  = https://github.com/Whonix/Whonix-Starter.git
        #url = https://gitlab.com/whonix/Whonix-Starter.git

[submodule "qubes-template-kicksecure"]
        path = qubes/qubes-template-kicksecure
        url  = https://github.com/Kicksecure/qubes-template-kicksecure.git
        #url = https://gitlab.com/Kicksecure/qubes-template-kicksecure.git

[submodule "qubes-template-whonix"]
        path = qubes/qubes-template-whonix
        url  = https://github.com/Whonix/qubes-template-whonix.git
        #url = https://gitlab.com/Whonix/qubes-template-whonix.git

## END hardcoded part END ##
' | tee -a -- "${derivative_maker_source_code_dir}/.gitmodules" >/dev/null
   fi

   url="\
        url = https://github.com/${batch_current_project_name}/${batch_current_package_reponame}.git
        #url = https://gitlab.com/${batch_current_project_name,,}/${batch_current_package_reponame}.git"

   printf "%s\n" "\
[submodule \"${batch_current_package_reponame}\"]
        path = packages/${batch_current_project_name,,}/${batch_current_package_reponame}
$url" \
         | tee -a -- "${derivative_maker_source_code_dir}/.gitmodules" >/dev/null

   printf "%s\n" "" | tee -a -- "${derivative_maker_source_code_dir}/.gitmodules" >/dev/null
}

## Generates a release announcement.
pkg_git_packages_git_log_writer() {
   local package_header_written package_version_old package_version_new \
      new_package commit_msg_short_list commit_hash commit_msg_short \
      committer_person commit_msg_full log_msg temp_folder_path

   if [ "${batch_func_init_done}" = 'n' ]; then
      post_run_hook_list+=( 'generate_announcement' )
      safe-rm -f -- "${announcements_drafts_dir}/derivative-maker_giant_git_log.txt"
      safe-rm -f -- "${announcements_drafts_dir}/kicksecure_giant_git_log.txt"
      safe-rm -f -- "${announcements_drafts_dir}/whonix_giant_git_log.txt"
      batch_func_init_done='y'
   fi

   repo_skip 'virtualbox' || return 0

   temp_folder_path="${PWD#"/home/user/derivative-maker/"}"

   if [ "${batch_current_package_reponame}" != 'derivative-maker' ]; then
      pushd .. >/dev/null
      if package_version_old="$(git rev-parse "${derivative_version_old_main}:${temp_folder_path}")" ; then
         new_package='n'
      else
         new_package='y'
      fi
      package_version_new="$(git rev-parse "${derivative_version_new_main}:${temp_folder_path}")"
      popd >/dev/null
   else
      new_package='n'
      package_version_old="${derivative_version_old_main}"
      package_version_new="${derivative_version_new_main}"
   fi

   if [ "${new_package}" = 'n' ]; then
      if [ "${package_version_old}" = "${package_version_new}" ]; then
         true "skipping because package_version_old = package_version_new"
         return 0
      else
         commit_msg_short_list="$(git --no-pager log --pretty='%H %s' "${package_version_old}..${package_version_new}")"
      fi
   else
      commit_msg_short_list="$(git --no-pager log --pretty='%H %s')"
   fi

   package_header_written='n'

   while read -r commit_hash commit_msg_short ; do
      if ! commit_filter "${commit_msg_short}" ; then
         true "SKIP: ${commit_msg_short}"
         continue
      fi
      true "OK: ${commit_msg_short}"

      committer_person="$(git log --format='%an' -n 1 "${commit_hash}")"
      commit_msg_full="$(git log --format='%B' -n 1 "${commit_hash}")"
      ## Remove trailing spaces.
      commit_msg_full="${commit_msg_full%"${commit_msg_full##*[![:space:]]}"}"
      commit_msg_full="$(printf "%s\n" "${commit_msg_full}" | sed '/^[[:space:]]*$/d')"
      ## Replace new lines with spaces to unbreak links for multi line comments.
      commit_msg_full="$(printf "%s\n" "${commit_msg_full}" | tr '\n' ' ')"
      ## Remove trailing spaces.
      commit_msg_full="${commit_msg_full%"${commit_msg_full##*[![:space:]]}"}"

      if [ "${committer_person}" = 'madaidan' ]; then
         committer_person="@${committer_person}"
      elif [ "${committer_person}" = 'Gavin Pacini' ]; then
         committer_person='@GavinPacini'
      elif [ "${committer_person}" = 'JeremyRand' ]; then
         committer_person='@JeremyRand'
      elif [ "${committer_person}" = 'HulaHoop0' ]; then
         committer_person='@HulaHoop'
      elif [ "${committer_person}" = 'Raja Grewal' ]; then
         committer_person='@raja'
      elif [ "${committer_person}" = 'raja-grewal' ]; then
         committer_person='@raja'
      elif [ "${committer_person}" = 'Aaron Rainbolt' ]; then
         committer_person='@ArrayBolt3'
      elif [ "${committer_person}" = 'TNT BOM BOM' ]; then
         committer_person='@nurmagoz'
      fi

      if [ "${committer_person}" = 'Patrick Schleizer' ]; then
         credit_msg=""
      else
         credit_msg=" (Thanks to ${committer_person}!)"
      fi

      log_msg="${commit_msg_full}"
      log_msg+="${credit_msg}"

      if [ "${package_header_written}" = 'n' ]; then
         printf "%s\n" "* ${batch_current_package_reponame}:" | tee -a -- "${batch_current_package_changelog}" >/dev/null
         package_header_written='y'
      fi

      printf "%s\n" "  * ${log_msg}" | tee -a -- "${batch_current_package_changelog}" >/dev/null
   done <<< "${commit_msg_short_list}"

   if [ "${package_header_written}" = 'y' ]; then
      ## Add newline but only if there were actually changelog lines.
      printf "%s\n" '' | tee -a -- "${batch_current_package_changelog}" >/dev/null
   fi

   true
}

## Signs git tags for all repos except derivative-maker using genmkfile.
pkg_git_sign_tags() {
   repo_skip 'derivative-maker' || return 0

   true "${cyan}batch_current_package_reponame: ${under}${batch_current_package_reponame}${eunder}${reset} 1/4 TODO"
   genmkfile git-commit-verify
   true "${cyan}batch_current_package_reponame: ${under}${batch_current_package_reponame}${eunder}${reset} 1/4 done"

   true "${cyan}batch_current_package_reponame: ${under}${batch_current_package_reponame}${eunder}${reset} 2/4 TODO"
   if ! genmkfile git-verify 2>/dev/null ; then
      true "${cyan}batch_current_package_reponame: ${under}${batch_current_package_reponame}${eunder}${reset} 2/4 done"

      true "${cyan}batch_current_package_reponame: ${under}${batch_current_package_reponame}${eunder}${reset} 3/4 TODO"
      genmkfile git-tag-sign
      true "${cyan}batch_current_package_reponame: ${under}${batch_current_package_reponame}${eunder}${reset} 3/4 done"
   fi

   genmkfile git-verify

   true
}

## Verifies that the head commit of all repos is signed.
pkg_verify_signed_commit() {
   git verify-commit HEAD

   true
}

## Verifies that the head commit and tag of all repos except derivative-maker
## is signed. Uses genmkfile.
pkg_verify_signed_commit_and_tag() {
   repo_skip 'derivative-maker' || return 0

   genmkfile git-verify
   true
}

## Finds packages that have modifications made to them since the last version
## bump, and displays their repo names. Also communicates that the package in
## question needs a version bump via a global variable, which is consumed by
## other functions in this script.
## NOTE: When calling this directly, redirect STDERR to /dev/null with
## 2>/dev/null.
pkg_need_version_bump_show() {
   local last_log_entry

   batch_current_package_needs_version_bump='n'

   repo_skip 'derivative-maker' || return 0

   last_log_entry="$(git log --format=%s -1)"

   if [ 'bumped changelog version' = "${last_log_entry}" ]; then
      if [ "$batch_meta_force_version_bump" = "y" ]; then
         true "Forced version bump."
      else
         return 0
      fi
   fi

   if ! nothing_to_commit; then
      true "uncommitted changes #1"
      return 1
   fi

   ## Verify that the commit prior the next changelog commit is signed.
   pkg_verify_signed_commit

   pkg_bash_sanity_test

   batch_current_package_needs_version_bump='y'

   printf "%s\n" "${batch_current_package_reponame}"
   true
}

## Bumps the debian/changelog version and rewrites the changelog.upstream file
## for all repos except derivative-maker. Generally called by other batch
## functions when necessary, calling this directly is probably a bad idea.
pkg_upstream_and_debian_changelog_bump() {
   repo_skip 'derivative-maker' || return 0

   ## version_numbers_by_upstream is set by make-helper-overrides.bsh by
   ## individual packages.
   printf "%s\n" "version_numbers_by_upstream: '${version_numbers_by_upstream}'"

   ## TODO:
   ## Not the cleanest way. Other variables might be unwanted.
   local version_numbers_by_upstream

   ## Complex, slow.
   #make_init ## includes make_get_variables
   ## sets maybe: version_numbers_by_upstream
   make_source_overrides_file
   make_source_overrides_folder

   ## If version_numbers_by_upstream=true then 'genmkfile deb-uachl-bumpup-major'
   ## would be bumping only the package revision number. Not the version number.
   ## In that case reprepro ('genmkfile reprepro-add') would then detect the same
   ## source tarball version but with different content (checksum) and therefore
   ## refuse adding it.
   if [ "${version_numbers_by_upstream}" = "true" ]; then
      ## XXX: manual
      genmkfile deb-uachl-bumpup-manual
   else
      genmkfile deb-uachl-bumpup-major
   fi

   genmkfile make_deb-uachl-commit-changelog

   true
}

## Does changelog bumps on all repos that need it except derivative-maker,
## then pushes the modified repos.
## NOTE: When calling this directly, redirect STDERR to /dev/null with
## 2>/dev/null.
pkg_need_version_bump_do() {
   repo_skip 'derivative-maker' || return 0

   ## sets: batch_current_package_needs_version_bump
   pkg_need_version_bump_show

   if [ ! "${batch_current_package_needs_version_bump}" = 'y' ]; then
      return 0
   fi

   printf "%s\n" "${FUNCNAME[0]}: needs version bump: '${batch_current_package_reponame}'"

   pkg_upstream_and_debian_changelog_bump
   ## Verify that the changelog bump commit is signed.
   pkg_verify_signed_commit

   ## Includes "genmkfile git-commit-verify".
   pkg_git_sign_tags

   pkg_git_push_remotes
   pkg_git_push_tags

   true
}

## For all packages that have modifications since the last changelog bump:
##
## * Regenerates manpages
## * Rewrites the Debian install file
## * Does version number bumps
## * Pushes the changes to Git
## * Builds the package
## * Adds the built package to a reprepro repository
## * Cleans up after the build
##
## NOTE: When calling this directly, redirect STDERR to /dev/null with
## 2>/dev/null.
pkg_need_version_bump_and_pkg_build_and_reprepro_add() {
   repo_skip_non_package || return 0

   #if [ "${batch_current_package_reponame}" = "kloak" ]; then
   #   continue_yes=true
   #fi
   #if [ ! "$continue_yes" = "true" ]; then
   #   return 0
   #fi

   ## sets: batch_current_package_needs_version_bump
   pkg_need_version_bump_show

   if [ ! "${batch_current_package_needs_version_bump}" = 'y' ]; then
      return 0
   fi

   printf "%s\n" "${FUNCNAME[0]}: needs version bump: '${batch_current_package_reponame}'"

   pkg_git_manpages
   pkg_git_debinstfile
   ## TODO
   #pkg_readme_creator_do
   #pkg_readme_creator_commit

   ## sets: needs_version_bump
   pkg_need_version_bump_do

   if [ ! "${batch_current_package_needs_version_bump}" = 'y' ]; then
      printf "%s\n" "${red}${bold}FATAL ERROR: batch_current_package_needs_version_bump before true but now not!?${reset}"
      exit 1
   fi

   ## At this stage:
   ## - the commit prior the changelog bump is verified
   ## - the changelog bump commit is signed and verified
   ## - the new git tag is signed and verified
   ## - the new git tag has already been pushed
   ## - the git branch has already been pushed

   export make_lintian=true
   export make_use_cowbuilder=true
   genmkfile deb-pkg
   genmkfile reprepro-remove
   genmkfile reprepro-add
   genmkfile deb-cleanup

   true
}

## Builds all packages.
pkg_only_build_all() {
   repo_skip_non_package || return 0

   export make_lintian=true
   export make_use_cowbuilder=true
   genmkfile deb-pkg

   true
}

## Adds all packages to a reprepro repository.
pkg_only_reprepro_add() {
   repo_skip_non_package || return 0

   export make_use_cowbuilder=true
   genmkfile reprepro-add

   true
}

## Creates a lintian-overrides file for all repositories, ignoring
## debian-watch-does-not-check-openpgp-signature warnings.
pkg_add_lintian_watch_gpg_override() {
   repo_skip_non_package || return 0

   mkdir --parents './debian/source'
   if [ -f './debian/source/lintian-overrides' ] && ! grep 'debian-watch-does-not-check-openpgp-signature' './debian/source/lintian-overrides' ; then
      printf "%s\n" "\
## https://forums.whonix.org/t/genmkfile-lintian-debian-watch-may-check-gpg-signature-build-issue/19124
debian-watch-does-not-check-openpgp-signature" >> "./debian/source/lintian-overrides"
   fi
}

## Commits the lintian-overrides file generated by
## pkg_add_lintian_watch_gpg_override.
pkg_git_commit_lintian_watch_gpg_override() {
   local msg

   repo_skip_non_package || return 0

   git add './debian/source/lintian-overrides' || true
   msg="\
added debian/source/lintian-overrides with debian-watch-does-not-check-openpgp-signature to fix lintian warning
https://forums.whonix.org/t/genmkfile-lintian-debian-watch-may-check-gpg-signature-build-issue/19124"
   git commit -m "${msg}" || true
   true
}

## Creates debian/watch files for all repos.
debian_watch_file_create() {
   local msg

   printf "%s\n" "\
## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

version=4
opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/${batch_current_package_reponame}-\$1\.tar\.gz/ \\
  https://github.com/${batch_current_project_name}/${batch_current_package_reponame}/tags .*/v?(\d\S+)\.tar\.gz" > "${batch_current_package_path}/debian/watch"

   git add "${batch_current_package_path}/debian/watch" || true
   msg="\
fix debian/watch lintian warning debian-watch-contains-dh_make-template"
   git commit -m "${msg}" || true
   true
}

## . END Commands }

## BEGIN Post-run hooks {

wait_for_git_processes() {
   if [ "${git_pid_list_str}" != '' ]; then
      ## Word splitting is used here intentionally.
      # shellcheck disable=SC2086
      wait $git_pid_list_str
   fi
}

generate_announcement() {
   local changelog_file derivative_name project_name project_website temp \
      derivative_version_new_main_short release_type_long end_text

   ## TODO: Should this dynamically change depending on the project? Also,
   ## should it be inserted into the announcement automatically?
   changelog_file="${announcements_drafts_dir}/kicksecure_giant_git_log.txt"

   for derivative_name in "${derivative_name_list[@]}"; do
      if [ "${derivative_name}" = 'kicksecure' ]; then
         project_name='Kicksecure'
         project_website='kicksecure.com'
      elif [ "${derivative_name}" = 'whonix' ]; then
         project_name='Whonix'
         project_website='whonix.org'
      fi

      temp="${derivative_version_new_main}"
      temp="$(str_replace "-developers-only" "" <<< "${temp}")"
      temp="$(str_replace "-testers-only" "" <<< "${temp}")"
      temp="$(str_replace "-stable-only" "" <<< "${temp}")"

      derivative_version_new_main_short="$temp"

      if [ "${derivative_release_type}" = 'testers' ]; then
         release_type_long='Testers Wanted'
         header_text="# Testers Wanted!

Download the Testers-Only version of ${project_name}:

https://www.${project_website}/wiki/VirtualBox_Testers_Only_Version"
      end_text="(This testers wanted announcement might in future be [transformed](https://forums.whonix.org/t/transform-whonix-testers-wanted-forum-news-post-into-whonix-release-forum-news-post-ok/11405) into a stable release announcement if no major issues are found during the testing period.)"
      elif [ "${derivative_release_type}" = 'point' ]; then
         release_type_long="Point Release"
         header_text="# Download

https://www.${project_website}/wiki/Download

([What is a point release?](https://www.kicksecure.com/wiki/Point_Release))"
      end_text="(This forum post was previously a call for testers. No release critical bugs where found during the testing period. This forum post was therefore [transformed](https://forums.whonix.org/t/transform-whonix-testers-wanted-forum-news-post-into-whonix-release-forum-news-post-ok/11405) into a stable release announcement. See edit history.)"
      else
         error "invalid derivative_release_type!"
      fi

      printf "%s\n" "\
${project_name} ${derivative_version_new_main_short} - ${release_type_long}!

${header_text}

----

# Upgrade

Alternatively, an in-place release upgrade is possible using the [${project_name} repository](https://www.${project_website}/wiki/Project-APT-Repository).

----

This release would not have been possible without the numerous supporters of ${project_name}!

----

Please Donate!

https://www.${project_website}/wiki/Donate

----

Please Contribute!

https://www.${project_website}/wiki/Contribute

----

# Major Changes

TODO

${changelog_file}

----

# Full difference of all changes

[https://github.com/${project_name}/derivative-maker/compare/${derivative_version_old_main}...${derivative_version_new_main}](https://github.com/${project_name}/derivative-maker/compare/${derivative_version_old_main}...${derivative_version_new_main})

----

${end_text}
" | \
      tee "${announcements_drafts_dir}/${project_name}.txt" >/dev/null
   done
}

## . END Post-run hooks }

## BEGIN Main logic {

run_batch_command() {
   local batch_command arg_list

   batch_command="$1"
   shift
   arg_list=( "$@" )

   [ -z "${batch_command}" ] && {
      printf "%s\n" "${red}${bold}FATAL ERROR: No command provided to ${FUNCNAME[0]}!${reset}"
      exit 1
   }

   (( batch_run_counter++ )) || true
   true "${FUNCNAME[0]}: batch_current_package_path: ${batch_current_package_path} | batch_run_counter: ${batch_run_counter}"
   true "${cyan}INFO: ${FUNCNAME[0]}: batch_current_package_path: ${batch_current_package_path}"
   true "${cyan}INFO: ${FUNCNAME[0]}: batch_current_package_reponame: ${batch_current_package_reponame}${reset}"
   true "${cyan}INFO: ${FUNCNAME[0]}: batch_current_project_name: ${batch_current_project_name}${reset}"
   true "${cyan}INFO: ${FUNCNAME[0]}: batch_current_package_changelog: ${batch_current_package_changelog}${reset}"

   set_batch_current_package_remote_list

   pushd "${batch_current_package_path}" >/dev/null
   "${batch_command}" "$@"
   popd >/dev/null
}

run_batch() {
   local derivative_name post_run_hook

   for derivative_name in "${derivative_name_list[@]}" ; do
      for batch_current_package_path in "${derivative_maker_source_code_dir}/packages/${derivative_name}"/*; do
         if [ "${derivative_name}" = 'kicksecure' ]; then
            batch_current_project_name='Kicksecure'
            batch_current_project_website='kicksecure.com'
          elif [ "${derivative_name}" = 'whonix' ]; then
             batch_current_project_name='Whonix'
             batch_current_project_website='whonix.org'
          else
             printf "%s\n" 'ERROR: Project name detection failed!'
             exit 1
          fi
          if ! [ -d "${batch_current_package_path}" ]; then
             continue
          fi
          batch_current_package_changelog="${announcements_drafts_dir}/${batch_current_project_name,,}_giant_git_log.txt"
          batch_current_package_reponame="$(basename "${batch_current_package_path}")"
          run_batch_command "$@"
      done
   done

   # Special-case for qubes-template-kicksecure
   batch_current_project_name='Kicksecure'
   batch_current_package_reponame='qubes-template-kicksecure'
   batch_current_project_website='kicksecure.com'
   batch_current_package_changelog="${announcements_drafts_dir}/kicksecure_giant_git_log.txt"
   batch_current_package_path="qubes/qubes-template-kicksecure"
   run_batch_command "$@"

   # Special-case for qubes-template-whonix
   batch_current_project_name='Whonix'
   batch_current_package_reponame='qubes-template-whonix'
   batch_current_project_website='whonix.org'
   batch_current_package_changelog="${announcements_drafts_dir}/whonix_giant_git_log.txt"
   batch_current_package_path="qubes/qubes-template-whonix"
   run_batch_command "$@"

   # Special-case for virtualbox
   batch_current_project_name='Kicksecure'
   batch_current_package_reponame='virtualbox'
   batch_current_project_website='kicksecure.com'
   batch_current_package_changelog="${announcements_drafts_dir}/kicksecure_giant_git_log.txt"
   batch_current_package_path="windows/virtualbox"
   run_batch_command "$@"

   # Special-case for Whonix-Installer
   batch_current_project_name='Whonix'
   batch_current_package_reponame='Whonix-Installer'
   batch_current_project_website='whonix.org'
   batch_current_package_changelog="${announcements_drafts_dir}/whonix_giant_git_log.txt"
   batch_current_package_path="windows/Whonix-Installer"
   run_batch_command "$@"

   # Special-case for Whonix-Starter
   batch_current_project_name='Whonix'
   batch_current_package_reponame='Whonix-Starter'
   batch_current_project_website='whonix.org'
   batch_current_package_changelog="${announcements_drafts_dir}/whonix_giant_git_log.txt"
   batch_current_package_path="windows/Whonix-Starter"
   run_batch_command "$@"

   # Special-case for derivative-maker
   batch_current_project_name='derivative-maker'
   batch_current_package_reponame='derivative-maker'
   batch_current_project_website='kicksecure.com'
   batch_current_package_changelog="${announcements_drafts_dir}/kicksecure_giant_git_log.txt"
   batch_current_package_path="${derivative_maker_source_code_dir}"
   run_batch_command "$@"

   # Post-run hooks
   # Note that this does nothing if post_run_hook_list is empty.
   for post_run_hook in "${post_run_hook_list[@]}"; do
      "${post_run_hook}"
   done
}

main() {
   local target_cmd arg_list

   target_cmd="$1"
   shift || true
   arg_list=( "$@" )

   if [ "$target_cmd" = '' ]; then
      true "INFO: Available functions..."
      typeset -f | awk '/ \(\) $/ && !/^main / {print $1}' | grep -E "^pkg_*"
      true "INFO: syntax: ./$0 function-name arguments"
      exit 0
   fi

   case "${target_cmd}" in
      'commit_filter')
         [ -z "${arg_list[0]}" ] && {
            printf "%s\n" "${red}${bold}FATAL ERROR: commit_filter requires an argument${reset}"
            exit 1
         }
         commit_filter "${arg_list[@]}"
         ;;
      'pkg_descr_merger')
         pkg_descr_merger
         ;;
      'pkg_descr_merge_all')
         pkg_descr_merge_all
         ;;
      *)
         run_batch "${target_cmd}"
         ;;
   esac
}

## . END Main logic }

## Global tunables, adjust these as necessary
derivative_binary_dir="$HOME/derivative-binary"
derivative_name_list=( 'kicksecure' 'whonix' )
derivative_version_old_main='18.0.8.4-developers-only'
derivative_version_new_main='18.0.8.7-developers-only'
derivative_release_type='point'
#derivative_release_type='testers'
makefile_generic_version='1.5'
packaging_files_diff_template_package_relative_path='kicksecure/anon-apt-sources-list'

## Global static variables, do not adjust
announcements_drafts_dir="${derivative_binary_dir}/announcements-drafts"
derivative_maker_dir_name="$(basename "${derivative_maker_source_code_dir}")"
make_cowbuilder_dist_dir="${derivative_maker_source_code_dir}/genmkfile-packages-result"
package_documentation_dir="${derivative_binary_dir}/package_documentation"

## Global state variables, do not adjust
batch_current_package_changelog=''
batch_current_package_needs_version_bump='n'
batch_current_package_path=''
batch_current_package_reponame=''
batch_current_project_name=''
batch_current_project_website=''
batch_current_package_remote_list=()
batch_func_init_done='n'
batch_meta_category_list=()
batch_meta_debian_control_web_link=''
batch_meta_file_name_without_reponame=''
batch_meta_file_web_link=''
batch_meta_gateway_only='n'
batch_meta_installed_by_default='y' # yes, this defaults to 'y'
batch_meta_non_qubes_whonix_only='n'
batch_meta_project_list=()
batch_meta_qubes_whonix_only='n'
batch_meta_relative_file_name=''
batch_meta_repo_web_link=''
batch_meta_workstation_only='n'
batch_meta_force_version_bump='n'

declare -A -g batch_meta_file_header_done_list
batch_run_counter=0
git_pid_list_str=''
declare -A -g merge_file_reset_list
post_run_hook_list=( 'wait_for_git_processes' )

## Initialization and launch
check_prerequisites
prepare_system
show_debug_variable_info
main "$@"

true "END: $0"
