489 lines
12 KiB
Bash
Executable File
489 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Author: Lucas Frérot
|
|
# Affiliation:
|
|
# Sorbonne Université, CNRS, Institut Jean Le Rond d'Alembert,
|
|
# F-75005 Paris, France
|
|
#
|
|
# Copyright © 2024 Lucas Frérot
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
# Sane bash options
|
|
set -o errexit
|
|
set -o nounset
|
|
set -o pipefail
|
|
|
|
# Colors
|
|
readonly RED='\033[0;31m'
|
|
readonly ORANGE='\033[0;33m'
|
|
readonly GREEN='\033[0;32m'
|
|
readonly BLUE='\033[0;34m'
|
|
readonly CYAN='\033[0;36m'
|
|
readonly NC='\033[0m'
|
|
|
|
# Gitea
|
|
readonly ENDPOINT="https://git.dalembert.upmc.fr/api/v1"
|
|
|
|
# Lab info
|
|
readonly AFFILIATION="Sorbonne Université, CNRS, Institut Jean Le Rond d'Alembert, F-75005 Paris, France"
|
|
|
|
# Files
|
|
readonly MANDATORY_FILES=("README.md"
|
|
"AUTHORS"
|
|
"COPYING"
|
|
"make_all_figures"
|
|
"tests/run_all_tests")
|
|
|
|
# ----------------- Logging commands -----------------------
|
|
|
|
# Print error and exit
|
|
error() {
|
|
printf "${RED}error${NC}: %b\\n" "$@" 1>&2
|
|
exit 1
|
|
}
|
|
|
|
# Print warning
|
|
warning() {
|
|
printf "${ORANGE}warning${NC}: %b\\n" "$@" 1>&2
|
|
}
|
|
|
|
# Print info
|
|
info() {
|
|
printf "${GREEN}info${NC}: %b\\n" "$@" 1>&2
|
|
}
|
|
|
|
# Enter value
|
|
enter() {
|
|
printf "${BLUE}input${NC}: %b" "$@" 1>&2
|
|
local input_var=''
|
|
read input_var
|
|
printf "${input_var}"
|
|
}
|
|
|
|
# Test if a variable name is set
|
|
is_set() {
|
|
local var_name="$1"
|
|
eval "! [[ -z \"\${${var_name}+x}\" ]]"
|
|
return $?
|
|
}
|
|
|
|
# Set var to first value if unset
|
|
alt_var() {
|
|
local value_if_unset="$1"
|
|
local variable="$2"
|
|
if is_set "${variable}"; then
|
|
printf "%s" "$(eval "printf \"%s\" \"\${${variable}}\"")"
|
|
else
|
|
printf "%s" "${value_if_unset}"
|
|
fi
|
|
}
|
|
|
|
# Enter value if unset
|
|
cond_enter() {
|
|
local input="$1"
|
|
local var_name="$2"
|
|
local value="$(alt_var "" "${var_name}")"
|
|
|
|
if [[ "${value}" == "" ]]; then
|
|
value="$(enter "${input}")"
|
|
fi
|
|
|
|
printf "%s" "${value}"
|
|
}
|
|
|
|
# Check that command exists
|
|
has_command() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
# Check curl and jq to process API calls to gitea
|
|
check_api_prerequisites() {
|
|
if ! has_command curl; then
|
|
error "curl not found, please install"
|
|
fi
|
|
|
|
if ! has_command jq; then
|
|
error "jq not found, please install"
|
|
fi
|
|
}
|
|
|
|
# ----------------- Gitea API commands -----------------------
|
|
get_gitea_token(){
|
|
if [[ -f token ]]; then
|
|
read TOKEN < token
|
|
else
|
|
TOKEN="$(enter "gitea token: ")"
|
|
fi
|
|
}
|
|
|
|
gitea() {
|
|
check_api_prerequisites
|
|
|
|
if ! is_set TOKEN; then
|
|
get_gitea_token
|
|
fi
|
|
|
|
readonly TOKEN
|
|
|
|
local method="$1"
|
|
local request="$2"
|
|
local data=""
|
|
|
|
if [[ $# > 2 ]]; then
|
|
data="$3"
|
|
fi
|
|
|
|
\curl -s -X "${request}" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: token ${TOKEN}" \
|
|
-d "${data}" \
|
|
"${ENDPOINT}/${method}"
|
|
}
|
|
|
|
# ----------------- Git commands -----------------------
|
|
|
|
# Set value of git config parameter
|
|
set_git_config() {
|
|
local param="$1"
|
|
local value=''
|
|
while ! [[ -n "${value}" ]]; do
|
|
value="$(enter "new value for ${param}: ")"
|
|
done
|
|
\git config "${param}" "${value}"
|
|
info "setting new value for ${param}: '$(\git config "${param}")'"
|
|
printf "${value}"
|
|
}
|
|
|
|
# Get value of git config parameter, set if unset
|
|
get_git_config() {
|
|
local param="$1"
|
|
local value="$(\git config "${param}")"
|
|
if ! [[ -n "${value}" ]]; then
|
|
warning "git ${param} is unset"
|
|
value="$(set_git_config "${param}")"
|
|
fi
|
|
printf "${value}"
|
|
}
|
|
|
|
# Check git configuration and correct if necessary
|
|
check_git_config() {
|
|
if ! has_command git; then
|
|
error "git not found, please install"
|
|
fi
|
|
|
|
readonly USER="$(alt_var "$(get_git_config user.name)" USER)"
|
|
readonly EMAIL="$(alt_var "$(get_git_config user.email)" EMAIL)"
|
|
info "found git credentials:\\n - user.name: '${USER}'\\n - user.email: '${EMAIL}'"
|
|
}
|
|
|
|
# Initialize git repository
|
|
init_repo() {
|
|
check_git_config
|
|
info "$(\git init)"
|
|
}
|
|
|
|
# ----------------- Tree commands -----------------------
|
|
|
|
# Make bash script stub
|
|
script_stub() {
|
|
local script_name="$1"
|
|
local start_phrase="$2"
|
|
|
|
if ! [[ -f "${script_name}" ]]; then
|
|
mkdir -p "$(dirname "${script_name}")"
|
|
cat << STUB > "${script_name}"
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
main() {
|
|
printf "${start_phrase}\\n" 1>&2
|
|
# put code here
|
|
}
|
|
|
|
main "\$@"
|
|
STUB
|
|
chmod +x "${script_name}"
|
|
fi
|
|
|
|
info "wrote '${script_name}'"
|
|
}
|
|
|
|
# Fetch licence
|
|
fetch_licence() {
|
|
local licence="COPYING"
|
|
|
|
if ! [[ -f "${licence}" ]]; then
|
|
if has_command curl; then
|
|
info "setting licence to GPL by default, see https://www.gnu.org/licenses for more options"
|
|
\curl -s "https://www.gnu.org/licenses/gpl-3.0.txt" > "${licence}"
|
|
info "wrote '${licence}'"
|
|
else
|
|
warning "please choose a free software licence, see https://www.gnu.org/licenses"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Create README
|
|
create_readme() {
|
|
local readme="${MANDATORY_FILES[0]}"
|
|
|
|
if ! [[ -f "${readme}" ]]; then
|
|
local project_name="$(cond_enter "project name: " PROJECT_NAME)"
|
|
local project_desc="$(cond_enter "project short description: " PROJECT_DESC)"
|
|
|
|
cat << READMEMSG > "${readme}"
|
|
# ${project_name}
|
|
|
|
${project_desc}
|
|
|
|
## Dependencies
|
|
|
|
Here are the dependencies to build and run the code:
|
|
|
|
- <dependencies_list>
|
|
|
|
## Running the code
|
|
|
|
Here is how to run the code:
|
|
|
|
\`\`\`
|
|
./make_all_figures
|
|
\`\`\`
|
|
|
|
## Tests
|
|
|
|
Here is how to run the tests:
|
|
|
|
\`\`\`
|
|
./tests/run_all_tests
|
|
\`\`\`
|
|
READMEMSG
|
|
|
|
info "wrote '${readme}'"
|
|
fi
|
|
}
|
|
|
|
create_authors_file() {
|
|
if ! [[ -f AUTHORS ]]; then
|
|
printf "%s\\n" "${USER} <${EMAIL}> ${AFFILIATION}" > AUTHORS
|
|
fi
|
|
info "wrote 'AUTHORS'"
|
|
}
|
|
|
|
# Create required files
|
|
create_tenet_file_tree() {
|
|
create_readme
|
|
create_authors_file
|
|
fetch_licence
|
|
script_stub tests/run_all_tests "Running all tests..."
|
|
script_stub make_all_figures "Generating figures..."
|
|
}
|
|
|
|
# Check tree structure
|
|
check-tree() {
|
|
local directory="$1"
|
|
local check_failed=0
|
|
pushd "${directory}" >/dev/null
|
|
|
|
# Test directory is a git repo
|
|
if ! \git status > /dev/null 2>&1; then
|
|
warning "git failed: $PWD is not a repo"
|
|
check_failed=1
|
|
fi
|
|
|
|
# Test config values
|
|
if ! ( [[ "$(\git config user.name)" != "" ]] \
|
|
&& [[ "$(\git config user.email)" != "" ]] ); then
|
|
warning "git config failed"
|
|
check_failed=1
|
|
fi
|
|
|
|
# Test file structure
|
|
for file in "${MANDATORY_FILES[@]}"; do
|
|
if ! [[ -f "${file}" ]]; then
|
|
warning "file ${file} is missing"
|
|
check_failed=1
|
|
fi
|
|
done
|
|
popd >/dev/null
|
|
return ${check_failed}
|
|
}
|
|
|
|
# ----------------- Gitea commands -----------------------
|
|
|
|
get_gitea_owner() {
|
|
gitea /user GET | jq -r '.login'
|
|
}
|
|
|
|
# Check if repository exists
|
|
get_gitea_repo() {
|
|
local owner="$1"
|
|
local repo_name="$2"
|
|
gitea "/repos/${owner}/${repo_name}" GET
|
|
}
|
|
|
|
# Create a repository on gitea
|
|
create_gitea_repo() {
|
|
local owner="$1"
|
|
local repo_name="$2"
|
|
local get_response="$(get_gitea_repo "${owner}" "${repo_name}")"
|
|
|
|
# Check if repo already exits
|
|
if [[ "$(jq '.id' <<< "${get_response}")" == "null" ]]; then
|
|
info "creating repository '${repo_name}' on gitea"
|
|
gitea /user/repos POST "{ \"name\": \"${repo_name}\", \"private\": true }"
|
|
else
|
|
info "found repository '${repo_name}' on gitea"
|
|
printf "%s" "${get_response}"
|
|
fi
|
|
}
|
|
|
|
# API call to setup the software heritage webhook
|
|
setup_software_heritage_hook() {
|
|
local owner="$1"
|
|
local repo_name="$2"
|
|
local remote_url="$3"
|
|
#gitea "/repos/${owner}/${repo_name}/hooks" GET
|
|
gitea "/repos/${owner}/${repo_name}/hooks" POST \
|
|
"{ \"type\": \"gitea\",
|
|
\"config\": {
|
|
\"content_type\": \"json\",
|
|
\"url\": \"https://archive.softwareheritage.org/api/1/origin/save/git/url/${remote_url}\"
|
|
},
|
|
\"events\": [\"release\"],
|
|
\"active\": true
|
|
}"
|
|
info "created Software Hertiage webhook for releases in gitea"
|
|
}
|
|
|
|
# Setup remote
|
|
setup_dalembert_gitea() {
|
|
local repo_name="$(basename <<< "${PWD}")"
|
|
local owner="$(get_gitea_owner)"
|
|
local repo_json="$(create_gitea_repo "${repo_name}")"
|
|
local remote_ssh="$(jq '.ssh_url' <<< "${repo_json}")"
|
|
local remote_http="$(jq '.clone_url' <<< "${repo_json}")"
|
|
|
|
if [[ "$(\git remote | \grep -c '^dalembert$' || true)" == "0" ]]; then
|
|
\git remote add dalembert "${remote_ssh}"
|
|
info "created remote dalembert with '${remote_ssh}"
|
|
else
|
|
\git remote set-url dalembert "${remote_ssh}"
|
|
info "set remote dalembert with '${remote_ssh}"
|
|
fi
|
|
|
|
setup_software_heritage_hook "${owner}" "${repo_name}" "${remote_http}"
|
|
}
|
|
|
|
# ----------------- Script subcommands commands -----------------------
|
|
|
|
init-tree() {
|
|
declare desc="usage: rtenets init-tree <directory>"
|
|
|
|
local directory="$1"
|
|
(
|
|
cd "${directory}"
|
|
init_repo
|
|
create_tenet_file_tree
|
|
)
|
|
}
|
|
|
|
init-gitea() {
|
|
declare desc="usage: rtenets init-gitea <directory>"
|
|
}
|
|
|
|
init-workflow() {
|
|
declare desc="usage: rtenets init-workflow <directory>"
|
|
}
|
|
|
|
# Create git repository
|
|
create() {
|
|
declare desc="usage: rtenets create <directory>"
|
|
local repo_name="$1"
|
|
info "recursively creating directory '${repo_name}'"
|
|
mkdir -p "${repo_name}"
|
|
(
|
|
info "initializing repository '${repo_name}'"
|
|
cd "${repo_name}"
|
|
init_repo
|
|
create_tenet_file_tree
|
|
)
|
|
}
|
|
|
|
# Print usage and exit
|
|
usage() {
|
|
cat <<USAGE
|
|
usage: $0 [--help,-h] [--version,-v] command [args...]
|
|
|
|
Available commands:
|
|
- create: create and populate a repository
|
|
- init-tree: populate a repository with README.md, AUTHORS, COPYING and test/
|
|
- init-gitea: setup a remote on a gitea server with SoftwareHeritage hook
|
|
- init-workflow: setup a Python virtual environment and Snakemake template
|
|
- check: verify tenent compliance for a repository
|
|
USAGE
|
|
}
|
|
|
|
version() {
|
|
cat <<VERSION
|
|
rtenets 0.0.1
|
|
|
|
Copyright © 2024 Lucas Frérot
|
|
This program comes with ABSOLUTELY NO WARRANTY;
|
|
This is free software, and you are welcome to redistribute it
|
|
under certain conditions;
|
|
VERSION
|
|
}
|
|
|
|
main() {
|
|
local other_args=()
|
|
local help_mode=0
|
|
for i in "$@"; do
|
|
case $i in
|
|
-v|--version)
|
|
version
|
|
return 0
|
|
;;
|
|
-h|--help)
|
|
help_mode=1
|
|
;;
|
|
*)
|
|
other_args+=("${i}")
|
|
;;
|
|
esac
|
|
done
|
|
|
|
local command="${other_args[0]}"
|
|
|
|
if [[ "${help_mode}" == 1 ]]; then
|
|
type "${command}" | sed -n -e 's/^.*declare desc="\(.*\)";/\1/p'
|
|
fi
|
|
|
|
# Subcommands trick
|
|
# https://blogsh.github.io/2020/03/21/subcommands-in-bash-scripts.html
|
|
declare -A COMMANDS=(
|
|
[default]=usage
|
|
[init-tree]=init-tree
|
|
[init-gitea]=init-gitea
|
|
[init-workflow]=init-workflow
|
|
[create]=create
|
|
[check-tree]=check-tree
|
|
)
|
|
|
|
"${COMMANDS[${command:-default}]:-${COMMANDS[default]}}" "${other_args[@]:1}"
|
|
}
|
|
|
|
# Avoid executing main if sourced
|
|
# https://stackoverflow.com/questions/2683279/how-to-detect-if-a-script-is-being-sourced
|
|
( return 0 2>/dev/null ) && true || main "$@"
|