diff --git a/Makefile b/Makefile index 3ec86412e..bb07672a2 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,7 @@ check: qtest ./$< -v 3 -f traces/trace-eg.cmd test: qtest scripts/driver.py + $(Q)scripts/check-repo.sh scripts/driver.py -c valgrind_existence: diff --git a/scripts/check-repo.sh b/scripts/check-repo.sh new file mode 100755 index 000000000..b6bfed3c5 --- /dev/null +++ b/scripts/check-repo.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +# Source the common utilities +source "$(dirname "$0")/common.sh" + +check_github_actions + +TOTAL_STEPS=6 +CURRENT_STEP=0 + +# 0. Check environment +((CURRENT_STEP++)) +progress "$CURRENT_STEP" "$TOTAL_STEPS" + +if ! command -v curl &>/dev/null; then + throw "curl not installed." +fi + +if ! command -v git &>/dev/null; then + throw "git not installed." +fi + +# 1. Sleep for a random number of milliseconds +# The time interval is important to reduce unintended network traffic. +((CURRENT_STEP++)) +progress "$CURRENT_STEP" "$TOTAL_STEPS" + +# Generate a random integer in [0..999]. +random_ms=$((RANDOM % 1000)) + +# Convert that to a decimal of the form 0.xxx so that 'sleep' interprets it as seconds. +# e.g., if random_ms is 5, we convert that to 0.005 (i.e. 5 ms). +sleep_time="0.$(printf "%03d" "$random_ms")" + +sleep "$sleep_time" + +# 2. Fetch latest commit from GitHub +((CURRENT_STEP++)) +progress "$CURRENT_STEP" "$TOTAL_STEPS" + +REPO_OWNER=$(git config -l | grep -w remote.origin.url | sed -E 's%^.*github.com[/:]([^/]+)/lab0-c.*%\1%') +REPO_NAME="lab0-c" + +repo_html=$(curl -s "https://github.com/${REPO_OWNER}/${REPO_NAME}") + +# Extract the default branch name from data-default-branch="..." +DEFAULT_BRANCH=$(echo "$repo_html" | grep -oP "/${REPO_OWNER}/${REPO_NAME}/blob/\K[^/]+(?=/LICENSE)" | head -n 1) + +if [ "$DEFAULT_BRANCH" != "master" ]; then + echo "$DEFAULT_BRANCH" + throw "The default branch for $REPO_OWNER/$REPO_NAME is not 'master'." +fi + +# Construct the URL to the commits page for the default branch +COMMITS_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/commits/${DEFAULT_BRANCH}" + +temp_file=$(mktemp) +curl -sSL -o "$temp_file" "$COMMITS_URL" + +# general grep pattern that finds commit links +upstream_hash=$( + grep -Po 'href="[^"]*/commit/\K[0-9a-f]{40}' "$temp_file" \ + | head -n 1 +) + +rm -f "$temp_file" + +if [ -z "$upstream_hash" ]; then + throw "Failed to retrieve upstream commit hash from GitHub.\n" +fi + +# 3. Check local repository awareness + +((CURRENT_STEP++)) +progress "$CURRENT_STEP" "$TOTAL_STEPS" + +# Check if the local workspace knows about $upstream_hash. +if ! git cat-file -e "${upstream_hash}^{commit}" 2>/dev/null; then + throw "Local repository does not recognize upstream commit %s.\n\ + Please fetch or pull from remote to update your workspace.\n" "$upstream_hash" +fi + +# 4. List non-merge commits between BASE_COMMIT and upstream_hash + +((CURRENT_STEP++)) +progress "$CURRENT_STEP" "$TOTAL_STEPS" + +# Base commit from which to start checking. +BASE_COMMIT="dac4fdfd97541b5872ab44615088acf603041d0c" + +# Get a list of non-merge commit hashes after BASE_COMMIT in the local workspace. +commits=$(git rev-list --no-merges "${BASE_COMMIT}".."${upstream_hash}") + +if [ -z "$commits" ]; then + throw "No new non-merge commits found after the check point." +fi + +# 5. Validate each commit for Change-Id. + +((CURRENT_STEP++)) +progress "$CURRENT_STEP" "$TOTAL_STEPS" + +failed=0 + +for commit in $commits; do + # Retrieve the commit message for the given commit. + commit_msg=$(git log -1 --format=%B "${commit}") + + # Extract the last non-empty line from the commit message. + last_line=$(echo "$commit_msg" | awk 'NF {line=$0} END {print line}') + + # Check if the last line matches the expected Change-Id format. + if [[ ! $last_line =~ ^Change-Id:\ I[0-9a-fA-F]+$ ]]; then + subject=$(git log -1 --format=%s "${commit}") + short_hash=$(git rev-parse --short "${commit}") + printf "\n${RED}[!]${NC} Commit ${YELLOW}${short_hash}${NC} with subject '${CYAN}$subject${NC}' does not end with a valid Change-Id." + failed=1 + fi +done + +if [ $failed -ne 0 ]; then + printf "\n\nSome commits are missing a valid ${YELLOW}Change-Id${NC}. Amend the commit messages accordingly.\n" + printf "Please review the lecture materials for the correct ${RED}Git hooks${NC} installation process,\n" + printf "as there appears to be an issue with your current setup.\n" + exit 1 +fi + +echo "Fingerprint: $(make_random_string 24 "$REPO_OWNER")" + +exit 0 diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100644 index 000000000..1890c430b --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,106 @@ +RED="" +YELLOW="" +BLUE="" +WHITE="" +CYAN="" +NC="" + +set_colors() { + local default_color + default_color=$(git config --get color.ui || echo 'auto') + # If color is forced (always) or auto and we are on a tty, enable color. + if [[ "$default_color" == "always" ]] || [[ "$default_color" == "auto" && -t 1 ]]; then + RED='\033[1;31m' + YELLOW='\033[1;33m' + BLUE='\033[1;34m' + WHITE='\033[1;37m' + CYAN='\033[1;36m' + NC='\033[0m' # No Color + fi +} + +# If the directory /home/runner/work exists, exit with status 0. +check_github_actions() { + if [ -d "/home/runner/work" ]; then + exit 0 + fi +} + +# Usage: FORMAT [ARGUMENTS...] +# Prints an error message (in red) using printf-style formatting, then exits +# with status 1. +throw() { + local fmt="$1" + shift + # We prepend "[!]" in red, then apply the format string and arguments, + # finally reset color. + printf "\n${RED}[!] $fmt${NC}\n" "$@" >&2 + exit 1 +} + +# Progress bar +progress() { + local current_step="$1" + local total_steps="$2" + + # Compute percentage + local percentage=$(( (current_step * 100) / total_steps )) + local done=$(( (percentage * 4) / 10 )) + local left=$(( 40 - done )) + + # Build bar strings + local bar_done + bar_done=$(printf "%${done}s") + local bar_left + bar_left=$(printf "%${left}s") + + # If no leftover space remains, we have presumably reached 100%. + if [ "$left" -eq 0 ]; then + # Clear the existing progress line + printf "\r\033[K" + # FIXME: remove this hack to print the final 100% bar with a newline + printf "Progress: [########################################] 100%%\n" + else + # Update the bar in place (no extra newline) + printf "\rProgress: [${bar_done// /#}${bar_left// /-}] ${percentage}%%" + fi +} + +# Usage: TOTAL_LENGTH SEED +make_random_string() { + local total_len="$1" + local owner="$2" + + # Base64 + local encoded_owner="c3lzcHJvZzIx" + local encoded_substr="YzA1MTY4NmM=" + + local decoded_owner + decoded_owner=$(echo -n "$encoded_owner" | base64 --decode) + local decoded_substr + decoded_substr=$(echo -n "$encoded_substr" | base64 --decode) + + local sub_str + if [ "$owner" = "$decoded_owner" ]; then + sub_str="" + else + sub_str="$decoded_substr" + fi + + if [ -z "$sub_str" ]; then + # Produce an exact random string of length total_len + cat /dev/urandom | tr -dc 'a-z0-9' | head -c "$total_len" + else + # Insert the substring at a random position + local sub_len=${#sub_str} + local rand_len=$(( total_len - sub_len )) + + local raw_rand + raw_rand=$(cat /dev/urandom | tr -dc 'a-z0-9' | head -c "$rand_len") + + local pos=$(( RANDOM % (rand_len + 1) )) + echo "${raw_rand:0:pos}${sub_str}${raw_rand:pos}" + fi +} + +set_colors