#!/bin/sh VERSION=1.2 # # Simple git based voting system # # if $VOTE_DIR is no defined the checkout happens in the shell-script directory # # To setup a new shared VOTE_DIR, use: # freefall> git init --bare /path/to/votes.git # local> git clone freefall:/path/to/votes.git votes # local> cd votes # local> git checkout -b main # Create the voters file (username:Name ) # local> git add voters # local> git commit -m "Initialize votes directory" # local> git push -u origin main:main # # # The mark_* and color_* values can be overriden in ~/.voterc # col_reset="\033[0m" # Color used to to show the output of shelled-out git calls. col_git="\033[32m" # Color for highlights within the script col_highlight="\033[33m" mark_ys="👍" mark_no="👎" mark_ab="💁" mark_dn="💤" mark_op="🔓" mark_cl="🔒" ### base_dir="$(dirname $(realpath $0))" vote_dir="${VOTE_DIR:-${base_dir}/votes}" ### text_ys="y" text_no="n" text_ab="a" text_dn="-" text_op="o" text_op="c" ### if [ -f ~/.voterc ] ; then . ~/.voterc ; fi ### __message () { echo -e "$@" } __error () { echo -e "$@" >&2 exit 1 } __usage() { __error "No command given. Available commands are:\n\n" \ "- help\t\tShow this message:\n" \ "\t\t${col_highlight}% vote help${col_reset}\n\n" \ "- init\t\tInitialize a repository:\n" \ "\t\t${col_highlight}% vote init ${col_reset}\n" \ "- update\tUpdate the repository:\n" \ "\t\t${col_highlight}% vote update${col_reset}\n\n" \ "- list\t\tList current ongoing votes:\n" \ "\t\t${col_highlight}% vote list${col_reset}\n" \ "- all\t\tList all votes:\n" \ "\t\t${col_highlight}% vote all${col_reset}\n" \ "- info\t\tShow information on a vote:\n" \ "\t\t${col_highlight}% vote info ${col_reset}\n\n" \ "- create\tCreate a new vote:\n" \ "\t\t${col_highlight}% vote create ${col_reset}\n" \ "- close\tMark a vote as closed:\n" \ "\t\t${col_highlight}% vote close ${col_reset}\n" \ "- reopen\tReopen a vote:\n" \ "\t\t${col_highlight}% vote reopen ${col_reset}\n\n" \ "- changedate\tChange the Due Date to something new\n" \ "\t\t${col_highlight}% vote changedate ${col_reset}\n" \ "- changesubject\tChange the Subject to something new\n" \ "\t\t${col_highlight}% vote changesubject ${col_reset}\n\n" \ "- vote\t\tPlace a vote:\n" \ "\t\t${col_highlight}% vote vote ${col_reset}\n" \ "- votefor\tPlace a vote for another person:\n" \ "\t\t${col_highlight}% vote votefor ${col_reset}\n\n" \ "The checkout directory can be overridden using then ${col_highlight}VOTE_DIR${col_reset}\n" \ "\n" \ "Vote version ${VERSION}." } __want_args() { local count=$1 shift if [ $# -ne ${count} ] ; then __error "Expected ${count} arguments, but got $#." fi } __username () { __want_args 1 "$@" local vote_dir="$1" username=$(git -C ${vote_dir} config user.email | awk -F '@' '{print $1}') if [ $? -eq 0 ] ; then echo "${username}" return 0 fi __error "Could not evaluate git user name" } __setup_voters() { __want_args 2 "$@" local votes_dir="$1" local voters="$2" if [ -d ${votes_dir} ] ; then if [ -f ${voters} ] ; then for voter in $(awk -F ':' '{print $1}' ${voters}) ; do ln -s zzz ${votes_dir}/${voter} done fi fi } # Managing votes create_vote () { __want_args 4 "$@" local vote_dir="$1" update_votes "${vote_dir}" local subject="$2" local duedate="$3" local description="$4" if [ $? -ne 0 ] ; then __error "Could not update votes prior to creation" fi local vid=$(__next_vid "${vote_dir}") local voters=$(awk -F ':' 'BEGIN{OFS=":"}{print " ",$0}' ${vote_dir}/voters | column -t -s ':') local commit_message=$(printf "<${vid}> [CREATE] ${subject} (${duedate})\n\n${description}\n\nVoters:\n${voters}\n") echo -e "${col_git}" cd ${vote_dir} && \ mkdir -p ${vid} && \ cd ${vid} && \ echo "${subject}" > subject && \ echo "${duedate}" > duedate && \ echo "${description} " > description && \ mkdir votes && \ __setup_voters $(realpath votes) $(realpath ${vote_dir}/voters) && \ git add subject duedate description votes && \ git commit -m "${commit_message}" && \ git push echo -e "${col_reset}" __message "" __pretty_print ${vote_dir} ${vid} } change_date () { __want_args 3 "$@" local vote_dir="$1" update_votes "${vote_dir}" local vid=$(__make_vid "$2") local newdate="$3" open=$(__is_open_vote ${vote_dir} ${vid}) if [ $? -ne 0 ] ; then __error "Cannot modify a closed vote. ${vote_dir} ${vid}" fi local olddate=$(cat ${vote_dir}/${vid}/duedate) local commit_message=$(printf "<${vid}> [CHANGE DATE] ${subject} (${olddate}->${newdate})\n") echo -e "${col_git}" cd ${vote_dir} && \ cd ${vid} && \ echo "${newdate}" > duedate && \ git add duedate && \ git commit -m "${commit_message}" && \ git push echo -e "${col_reset}" list_votes ${vote_dir} } change_subject () { __want_args 3 "$@" local vote_dir="$1" update_votes "${vote_dir}" local vid=$(__make_vid "$2") local newsubject="$3" open=$(__is_open_vote ${vote_dir} ${vid}) if [ $? -ne 0 ] ; then __error "Cannot modify a closed vote. ${vote_dir} ${vid}" fi local oldsubject=$(cat ${vote_dir}/${vid}/subject) local commit_message=$(printf "<${vid}> [CHANGE DATE] ${oldsubject} -> ${newsubject}\n") echo -e "${col_git}" cd ${vote_dir} && \ cd ${vid} && \ echo "${newsubject}" > subject && \ git add subject && \ git commit -m "${commit_message}" && \ git push echo -e "${col_reset}" list_votes ${vote_dir} } info_vote () { __want_args 2 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") __pretty_print ${vote_dir} ${vid} } __is_open_vote () { __want_args 2 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") if [ -f ${vote_dir}/${vid}/closeddate ] ; then return 1 fi return 0 } __set_vote() { __want_args 4 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") local username="$3" local value="$4" open=$(__is_open_vote ${vote_dir} ${vid}) if [ $? -ne 0 ] ; then __error "Cannot vote on a closed vote. ${vote_dir} ${vid}" fi if [ ! -L ${vote_dir}/${vid}/votes/${username} ] ; then __error "Invalid voter ${username}" fi local target="" case "${value}" in "yes") target="yes" ;; "no") target="no" ;; "abstain") target="abstain" ;; *) __error "Invalid vote '${value}' (valid: yes, no, abstain)" ;; esac result=$(cd ${vote_dir}/${vid}/votes && ln -sf ${value} ${username}) return $? } __short_result () { __want_args 2 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") echo $(__tally_vote ${vote_dir} ${vid}) return 0 } __pretty_print () { __want_args 2 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") local subject=$(cat ${vote_dir}/${vid}/subject) local y_voters="" local n_voters="" local a_voters="" local d_voters="" for vote in $(find -s ${vote_dir}/${vid}/votes -type l) ; do value=$(readlink ${vote}) voter=$(basename ${vote}) case "${value}" in "yes") y_voters="${y_voters}${y_voters:+, }${voter}" ;; "no") n_voters="${n_voters}${n_voters:+, }${voter}" ;; "abstain") a_voters="${a_voters}${a_voters:+, }${voter}" ;; "zzz") d_voters="${d_voters}${d_voters:+, }${voter}" ;; *) __error "Invalid vote value ${value} in ${vote} (${vid})" esac done result=$(printf "Vote on ${subject}.\n\nYes:\t\t${y_voters}\nNo:\t\t${n_voters}\nAbstained:\t${a_voters}\n\nDid not vote:\t${d_voters}") echo "${result}" return 0 } reopen_vote () { __want_args 2 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") open=$(__is_open_vote ${vote_dir} ${vid}) if [ $? -eq 0 ] ; then __error "Vote already open" fi local closeddate=$(cat ${vote_dir}/${vid}/closeddate) rm ${vote_dir}/${vid}/closeddate local tally=$(__short_result ${vote_dir} ${vid}) local pretty=$(__pretty_print ${vote_dir} ${vid}) local commit_message=$(printf "<${vid}> [REOPEN] ${tally}\n\n${pretty}\nOld Closed Date:\t${closeddate}") echo -e "${col_git}" cd ${vote_dir} && \ git add ${vote_dir}/${vid}/closeddate && \ git commit -m "${commit_message}" && \ git push echo -e "${col_reset}" __pretty_print ${vote_dir} ${vid} return 0 } close_vote () { __want_args 2 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") open=$(__is_open_vote ${vote_dir} ${vid}) if [ $? -ne 0 ] ; then __error "Vote already closed" fi echo $(date -u '+%Y%m%d') > ${vote_dir}/${vid}/closeddate local tally=$(__short_result ${vote_dir} ${vid}) local pretty=$(__pretty_print ${vote_dir} ${vid}) local commit_message=$(printf "<${vid}> [CLOSE] ${tally}\n\n${pretty}") echo -e "${col_git}" cd ${vote_dir} && \ git add ${vote_dir}/${vid}/closeddate && \ git commit -m "${commit_message}" && \ git push echo -e "${col_reset}" __pretty_print ${vote_dir} ${vid} return 0 } # Interacting with votes init_votes () { __want_args 2 "$@" local vote_dir="$1" local url="$2" if [ -d "${vote_dir}" ] ; then __error "Directory '${vote_dir}' already exists." fi echo -e "${col_git}" git clone ${url} ${vote_dir} if [ $? -ne 0 ] ; then echo -e "${col_reset}" __error "Could not clone ${url} to ${vote_dir}" fi echo -e "${col_reset}" list_all_votes ${vote_dir} return 0 } update_votes () { __want_args 1 "$@" local vote_dir="$1" __check_git ${vote_dir} if [ $? -ne 0 ] ; then __error "Directory '${vote_dir}' is not a repository." fi echo -e "${col_git}" cd "${vote_dir}" && git pull --rebase --autostash if [ $? -ne 0 ] ; then echo -e "${col_reset}" __error "Failed to update '${vote_dir}' -- please try manual merge." fi echo -e "${col_reset}" list_votes ${vote_dir} return 0 } __list_vids () { local vote_dir="$1" vids=$(find -E "${vote_dir}" -type d -regex ".*/[0-9][0-9][0-9][0-9]$" | sort | awk -F '/' '{print $NF}') if [ $? -ne 0 ] ; then __error "Failed to enumerate vids" fi echo "${vids}" return 0 } __last_vid () { local vote_dir="$1" result=$(__list_vids "${vote_dir}" | tail -n1) echo ${result} } __make_vid () { __want_args 1 "$@" echo $(printf "%04d" $(expr $1 + 0)) return 0 } __next_vid () { __want_args 1 "$@" local vote_dir="$1" last=$(__last_vid "${vote_dir}") if [ -z ${last} ] ; then last=0 fi echo $(__make_vid $(expr ${last} + 1)) return 0 } __list_vote_entry () { __want_args 3 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") local include_closed="$3" if [ ! -d ${vote_dir}/${vid} ] ; then __error "Failed to read info on ${vid} from ${vote_dir}" fi local state=${mark_cl} __is_open_vote ${vote_dir} ${vid} if [ $? -eq 0 ] ; then state=${mark_op} fi local duedate=$(cat ${vote_dir}/${vid}/duedate) local subject=$(cut -c 1-50 ${vote_dir}/${vid}/subject) local votes=$(__tally_vote ${vote_dir} ${vid}) local own_vote=$(__own_vote ${vote_dir} ${vid}) local closed_info="" if [ "${include_closed}" = "yes" ] ; then if [ -f ${vote_dir}/${vid}/closeddate ] ; then closed_info="|$(cat ${vote_dir}/${vid}/closeddate)" else closed_info="|" fi fi echo "${state}|${duedate}|${vid}|${subject}|${votes}|${own_vote}${closed_info}" } __vote_mark () { __want_args 1 "$@" local result=${mark_dn} case "$1" in "yes") result=${mark_ys} ;; "no") result=${mark_no} ;; "abstain") result=${mark_ab} ;; esac echo "${result}" } __vote_text() { __want_args 1 "$@" local result=${text_dn} case "$1" in "yes") result=${text_ys} ;; "no") result=${text_no} ;; "abstain") result=${text_ab} ;; esac echo "${result}" } __get_vote () { __want_args 3 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") local username="$3" if [ ! -d ${vote_dir}/${vid} ] ; then __error "Failed to read info on ${vid} from ${vote_dir}" fi local uservote="" local vote=${vote_dir}/${vid}/votes/${username} voted=1 local result=${mark_dn} if [ -L ${vote} ] ; then vote_value=$(readlink ${vote}) if [ "${vote_value}" != "zzz" ] ; then voted=0 fi result=$(__vote_mark ${vote_value}) fi echo "${result}" return ${voted} } __own_vote () { __want_args 2 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") local username=$(__username ${vote_dir}) local result=$(__get_vote ${vote_dir} ${vid} ${username}) echo "${result}" return 0 } __tally_vote () { __want_args 2 "$@" local vote_dir="$1" local vid=$(__make_vid "$2") if [ ! -d ${vote_dir}/${vid} ] ; then __error "Failed to read info on ${vid} from ${vote_dir}" fi local count_yes=0 local count_no=0 local count_abstain=0 local count_dnv=0 for vote in $(find ${vote_dir}/${vid}/votes -type l) ; do value=$(readlink ${vote}) voter=$(basename ${vote}) case "${value}" in "yes") count_yes=$(expr ${count_yes} + 1) ;; "no") count_no=$(expr ${count_no} + 1) ;; "abstain") count_abstain=$(expr ${count_abstain} + 1) ;; "zzz") count_dnv=$(expr ${count_dnv} + 1) ;; *) __error "Invalid vote value ${value} in ${vote} (${vid})" esac done result=$(printf "${mark_ys}%2d, ${mark_no}%2d, ${mark_ab}%2d, ${mark_dn}%2d" ${count_yes} ${count_no} ${count_abstain} ${count_dnv}) echo "${result}" return 0 } list_votes () { __want_args 1 "$@" local vote_dir="$1" result="" for vid in $(__list_vids "$@") ; do __is_open_vote ${vote_dir} ${vid} if [ $? -eq 0 ] ; then result="${result}\n$(__list_vote_entry ${vote_dir} ${vid} no)" fi done result=$(echo -e "${result}" | sort -r) result=$(printf "State|Due Date|VID|Subject|Tally|Own Vote\n${result}") echo "${result}" | column -t -s '|' } list_all_votes () { __want_args 1 "$@" local vote_dir="$1" result="" for vid in $(__list_vids "$@") ; do result="${result}\n$(__list_vote_entry ${vote_dir} ${vid} yes)" done result=$(echo -e "${result}" | sort -r) result=$(printf "State|Due Date|VID|Subject|Tally|Own Vote|Closed On\n${result}") echo "${result}" | column -t -s '|' } place_vote () { __vote "$@" } place_vote_for () { __vote_for "$@" } __vote_for () { __want_args 4 "$@" local vote_dir="$1" update_votes "${vote_dir}" local vid=$(__make_vid "$2") local value="$3" local username="$4" if [ ! -L ${vote_dir}/${vid}/votes/${username} ] ; then __error "Invalid voter ${username}" fi local author=$(awk -F : "/${username}/{print \$NF}" ${vote_dir}/voters) if [ -z "${author}" ] ; then __error "Could not read author for ${username}" fi local action="" local msg="" oldvote=$(__get_vote ${vote_dir} ${vid} ${username}) if [ $? -eq 0 ] ; then action="[CHANGE VOTE]" message="${oldvote} -> $(__vote_text ${value})" else action="[SET VOTE]" message="$(__vote_text ${value})" fi local foreignvote="" gituser=$(__username ${vote_dir}) if [ ${username} != ${gituser} ] ; then foreignvote=" (vote placed by ${gituser})" fi __set_vote "${vote_dir}" "${vid}" "${username}" "${value}" if [ $? -ne 0 ] ; then __error "Could not place vote ${value} on ${vid}" fi local subject=$(cut -c 1-50 ${vote_dir}/${vid}/subject) local commit_message=$(printf "<${vid}> ${action} ${subject} :: ${username} ${message}${foreignvote}\n\n$(__pretty_print ${vote_dir} ${vid})") echo -e "${col_git}" cd ${vote_dir} && \ git add ${vid}/votes/${username} && \ git commit -m "${commit_message}" --author="${author}" && \ git push echo -e "${col_reset}" __message "" __pretty_print ${vote_dir} ${vid} } __vote () { __want_args 3 "$@" local username=$(__username ${vote_dir}) if [ -z "${username}" ] ; then __error "Could not read your git username" fi __vote_for "$@" ${username} } # utils __check_git () { local git_dir=$(realpath "$1") if [ ! -d "${git_dir}" ] ; then return 1 fi ( cd "${git_dir}" git rev-parse --is-inside-work-tree > /dev/null 2>&1 if [ $? -ne 0 ] ; then return 1 fi ) return 0 } if [ -z $1 ] ; then __usage fi command=$1 shift case "${command}" in "help") __usage ;; "init") init_votes ${vote_dir} "$@" ;; "update") update_votes ${vote_dir} ;; "list") list_votes ${vote_dir} ;; "all") list_all_votes ${vote_dir} ;; "create") create_vote ${vote_dir} "$@" ;; "info") info_vote ${vote_dir} "$@" ;; "close") close_vote ${vote_dir} "$@" ;; "reopen") reopen_vote ${vote_dir} "$@" ;; "changedate") change_date ${vote_dir} "$@" ;; "changesubject") change_subject ${vote_dir} "$@" ;; "vote") place_vote ${vote_dir} "$@" ;; "votefor") place_vote_for ${vote_dir} "$@" ;; *) __usage ;; esac