From 9e201ef5fcb2500e0e71b1be857700fcbdf2a5d9 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 23 Feb 2025 17:10:03 +0800 Subject: [PATCH] Verify repository integrity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In accordance with the lecture guidelines, this test script has been consolidated to ensure that the GitHub repository complies with certain rules—such as having 'master' as the default branch—and to validate that each commit includes a Change-Id. Change-Id: Ie11907e72d2e907cec356bec66c01dbad3cef9af --- Makefile | 1 + scripts/check-repo.sh | 130 ++++++++++++++++++++++++++++++++++++++++++ scripts/common.sh | 106 ++++++++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100755 scripts/check-repo.sh create mode 100644 scripts/common.sh 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