Last active
May 26, 2025 13:10
-
-
Save mrtysn/04e0c8e1f29668203d7d23fbb05ab630 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Script to clean Unity-generated meta files and pull latest changes | |
# Also updates the 'foundation' submodule to the latest commit | |
# Usage: ./clean_and_pull.sh [path/to/unity/project] [--auto-discard] [--yes] [--force-fetch] [--branch=name] [--submodule-branch=name] | |
# | |
# Options: | |
# --auto-discard Automatically discard all local changes without prompting | |
# --yes, -y Automatically answer yes to all prompts | |
# --force-fetch Force fetch even if recent fetch detected (slower but always fresh) | |
# --branch=NAME Target branch for main repository (default: development) | |
# --submodule-branch=NAME Target branch for submodule (default: main) | |
# Set project directory | |
PROJECT_DIR="/Users/mert/dev/merge-of-wonders" # Default path | |
# Set defaults for automatic operation | |
AUTO_DISCARD=false | |
AUTO_YES=false | |
FAST_MODE=true # Use cached data when possible (5 min threshold) | |
TARGET_BRANCH="development" # Default main repository branch | |
SUBMODULE_BRANCH="main" # Default submodule branch | |
# Parse arguments | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
--auto-discard) | |
AUTO_DISCARD=true | |
shift | |
;; | |
--yes|-y) | |
AUTO_YES=true | |
shift | |
;; | |
--force-fetch) | |
FAST_MODE=false | |
shift | |
;; | |
--branch=*) | |
TARGET_BRANCH="${1#*=}" | |
shift | |
;; | |
--submodule-branch=*) | |
SUBMODULE_BRANCH="${1#*=}" | |
shift | |
;; | |
--help|-h) | |
echo "Unity Git Clean and Pull Script" | |
echo "Usage: $0 [path/to/unity/project] [OPTIONS]" | |
echo "" | |
echo "OPTIONS:" | |
echo " --auto-discard Automatically discard all local changes without prompting" | |
echo " --yes, -y Automatically answer yes to all prompts" | |
echo " --force-fetch Force fetch even if recent fetch detected (slower but always fresh)" | |
echo " --branch=NAME Target branch for main repository (default: development)" | |
echo " --submodule-branch=NAME Target branch for submodule (default: main)" | |
echo " --help, -h Show this help message" | |
echo "" | |
echo "EXAMPLES:" | |
echo " $0 # Use defaults (development + main branches)" | |
echo " $0 --branch=feature/new-ui # Sync to feature branch" | |
echo " $0 --force-fetch --yes # Always fetch latest, auto-confirm prompts" | |
echo " $0 --branch=hotfix --submodule-branch=develop # Custom branches for both repos" | |
echo "" | |
echo "BEHAVIOR:" | |
echo " - Uses 5-minute fetch caching by default for speed" | |
echo " - Handles local changes (discard/stash/keep options)" | |
echo " - Automatically switches to target branches" | |
echo " - Cleans Unity-generated meta files" | |
echo " - Updates both main repository and foundation submodule" | |
exit 0 | |
;; | |
--fast) | |
echo "Warning: --fast is deprecated. Caching is enabled by default, use --force-fetch to disable." | |
shift | |
;; | |
-*) | |
echo "Unknown option $1" | |
echo "Available options: --auto-discard, --yes/-y, --force-fetch, --branch=NAME, --submodule-branch=NAME, --help/-h" | |
echo "Use --help for detailed usage information." | |
exit 1 | |
;; | |
*) | |
# Positional argument - treat as project directory | |
PROJECT_DIR="$1" | |
shift | |
;; | |
esac | |
done | |
# Function to check if fetch is recent (less than 5 minutes ago) | |
# Works for both regular repos and submodules | |
is_fetch_recent() { | |
local fetch_head_file | |
# Check if we're in a submodule (has .git file instead of .git directory) | |
if [ -f ".git" ]; then | |
# Submodule: .git is a file pointing to the real git directory | |
local git_dir=$(cat .git | sed 's/gitdir: //') | |
fetch_head_file="$git_dir/FETCH_HEAD" | |
elif [ -d ".git" ]; then | |
# Regular repo: .git is a directory | |
fetch_head_file=".git/FETCH_HEAD" | |
else | |
# Not in a git repo | |
return 1 | |
fi | |
if [ -f "$fetch_head_file" ]; then | |
# Use a more reliable method to get file modification time | |
# Try different approaches for macOS | |
local fetch_time | |
# Method 1: Try stat with -t flag (should work on most macOS versions) | |
fetch_time=$(stat -t "%s" -f "%m" "$fetch_head_file" 2>/dev/null) | |
# Method 2: If that fails, try using ls and date | |
if [ -z "$fetch_time" ] || ! [[ "$fetch_time" =~ ^[0-9]+$ ]]; then | |
# Get the modification time using ls and convert to epoch | |
local mod_time_str=$(ls -l "$fetch_head_file" | awk '{print $6" "$7" "$8}') | |
fetch_time=$(date -j -f "%b %d %H:%M" "$mod_time_str" "+%s" 2>/dev/null) | |
fi | |
# Method 3: If still failing, use Python as fallback | |
if [ -z "$fetch_time" ] || ! [[ "$fetch_time" =~ ^[0-9]+$ ]]; then | |
fetch_time=$(python3 -c "import os; print(int(os.path.getmtime('$fetch_head_file')))" 2>/dev/null) | |
fi | |
if [ -n "$fetch_time" ] && [[ "$fetch_time" =~ ^[0-9]+$ ]]; then | |
local current_time=$(date +%s) | |
local diff=$((current_time - fetch_time)) | |
if [ $diff -lt 300 ]; then # 5 minutes = 300 seconds | |
return 0 # Recent | |
fi | |
fi | |
fi | |
return 1 # Not recent or doesn't exist | |
} | |
echo "===== Unity Git Clean and Pull Script =====" | |
echo "Project directory: $PROJECT_DIR" | |
cd "$PROJECT_DIR" || { echo "Error: Could not change to directory $PROJECT_DIR"; exit 1; } | |
# Verify we're in a git repository | |
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then | |
echo "Error: Not in a git repository" | |
exit 1 | |
fi | |
# Function to check for any local changes (staged, unstaged, or untracked that matter) | |
has_local_changes() { | |
# Check for staged changes | |
if ! git diff --cached --quiet; then | |
return 0 # Has changes | |
fi | |
# Check for unstaged changes | |
if ! git diff --quiet; then | |
return 0 # Has changes | |
fi | |
# Check for untracked files that aren't gitignored | |
if [ -n "$(git ls-files --others --exclude-standard)" ]; then | |
return 0 # Has changes | |
fi | |
return 1 # No changes | |
} | |
# Function to handle local changes | |
handle_local_changes() { | |
local context="$1" # "main" or "submodule" | |
local return_to_dir="$2" # Directory to return to on error (for submodules) | |
echo "You have local changes in $context that need to be handled" | |
if [ "$AUTO_DISCARD" = true ]; then | |
CHANGE_OPTION=1 | |
echo "Auto-discarding all local changes (--auto-discard flag is set)" | |
else | |
echo "1) Discard all changes (recommended for clean sync)" | |
echo "2) Stash changes (save them for later)" | |
echo "3) Keep changes (may cause conflicts)" | |
read -p "Choose an option [1-3] (default: 1): " CHANGE_OPTION | |
CHANGE_OPTION=${CHANGE_OPTION:-1} | |
fi | |
case $CHANGE_OPTION in | |
1) | |
echo "Discarding all local changes in $context..." | |
git reset --hard HEAD || { | |
echo "Error: Could not reset changes in $context" | |
[ -n "$return_to_dir" ] && cd "$return_to_dir" | |
exit 1 | |
} | |
git clean -fd || { | |
echo "Error: Could not clean untracked files in $context" | |
[ -n "$return_to_dir" ] && cd "$return_to_dir" | |
exit 1 | |
} | |
echo "All local changes in $context have been discarded" | |
return 0 | |
;; | |
2) | |
git stash push -m "Auto-stash from clean_and_pull script - $context" || { | |
echo "Error: Could not stash changes in $context" | |
[ -n "$return_to_dir" ] && cd "$return_to_dir" | |
exit 1 | |
} | |
echo "Changes stashed in $context" | |
return 1 # Return 1 to indicate stash was created | |
;; | |
3) | |
echo "Warning: Proceeding with local changes in $context" | |
return 2 # Return 2 to indicate changes were kept | |
;; | |
*) | |
echo "Invalid option. Proceeding with option 1 (discard all changes)" | |
git reset --hard HEAD || { | |
echo "Error: Could not reset changes in $context" | |
[ -n "$return_to_dir" ] && cd "$return_to_dir" | |
exit 1 | |
} | |
git clean -fd || { | |
echo "Error: Could not clean untracked files in $context" | |
[ -n "$return_to_dir" ] && cd "$return_to_dir" | |
exit 1 | |
} | |
echo "All local changes in $context have been discarded" | |
return 0 | |
;; | |
esac | |
} | |
# Display git status before cleaning | |
echo "Current git status:" | |
STATUS_OUTPUT=$(git status --short) | |
if [ -z "$STATUS_OUTPUT" ]; then | |
echo "β Working directory is clean" | |
else | |
echo "$STATUS_OUTPUT" | |
fi | |
echo "------------------------" | |
# Store current branch info | |
CURRENT_BRANCH=$(git branch --show-current) | |
NEEDS_BRANCH_SWITCH=false | |
if [ "$CURRENT_BRANCH" != "$TARGET_BRANCH" ]; then | |
NEEDS_BRANCH_SWITCH=true | |
else | |
echo "β Already on target branch ($TARGET_BRANCH)" | |
fi | |
# Handle local changes first if they exist | |
MAIN_STASH_CREATED=false | |
if has_local_changes; then | |
handle_local_changes "main repository" | |
case $? in | |
1) MAIN_STASH_CREATED=true ;; | |
2) NEEDS_BRANCH_SWITCH=false ;; # Can't switch branches with conflicting changes | |
esac | |
fi | |
# Handle branch switching if needed and safe | |
if [ "$NEEDS_BRANCH_SWITCH" = true ]; then | |
echo "Warning: You are on branch '$CURRENT_BRANCH', not '$TARGET_BRANCH'" | |
if [ "$AUTO_YES" = true ]; then | |
SWITCH="y" | |
echo "Auto-switching to $TARGET_BRANCH branch (--yes flag is set)" | |
else | |
read -p "Do you want to switch to $TARGET_BRANCH branch? (y/n): " SWITCH | |
fi | |
if [ "$SWITCH" = "y" ]; then | |
git checkout "$TARGET_BRANCH" || { echo "Error: Could not switch to $TARGET_BRANCH branch"; exit 1; } | |
else | |
if [ "$AUTO_YES" = true ]; then | |
CONTINUE="y" | |
echo "Auto-continuing anyway (--yes flag is set)" | |
else | |
read -p "Continue anyway? (y/n): " CONTINUE | |
fi | |
if [ "$CONTINUE" != "y" ]; then | |
echo "Operation canceled" | |
exit 0 | |
fi | |
fi | |
fi | |
echo "Cleaning untracked meta files..." | |
# List of patterns to clean (add more as needed) | |
META_PATTERNS=( | |
"Assets/Packages/Microsoft.*.meta" | |
"Assets/Packages/System.*.meta" | |
) | |
# Create a temporary file for git clean command | |
TEMP_FILE=$(mktemp) | |
for pattern in "${META_PATTERNS[@]}"; do | |
git ls-files --others --exclude-standard | grep "$pattern" >> "$TEMP_FILE" | |
done | |
# If there are files to clean | |
if [ -s "$TEMP_FILE" ]; then | |
echo "The following files will be removed:" | |
cat "$TEMP_FILE" | |
# Confirm before deletion | |
if [ "$AUTO_YES" = true ]; then | |
CONFIRM="y" | |
echo "Auto-confirming removal (--yes flag is set)" | |
else | |
read -p "Continue with removal? (y/n): " CONFIRM | |
fi | |
if [ "$CONFIRM" = "y" ]; then | |
while IFS= read -r file; do | |
rm -f "$file" | |
echo "Removed: $file" | |
done < "$TEMP_FILE" | |
echo "Cleanup completed" | |
else | |
echo "Cleanup skipped" | |
fi | |
else | |
echo "No matching untracked files found" | |
fi | |
rm -f "$TEMP_FILE" | |
# Fetch optimization based on cache age | |
SHOULD_FETCH=true | |
if [ "$FAST_MODE" = true ]; then | |
if is_fetch_recent; then | |
echo "β‘ Using cached fetch data (< 5 min old, use --force-fetch for fresh data)" | |
SHOULD_FETCH=false | |
else | |
echo "π Cache expired, fetching latest changes..." | |
fi | |
else | |
echo "π Force fetch mode - always getting latest data" | |
fi | |
if [ "$SHOULD_FETCH" = true ]; then | |
echo "Fetching latest changes..." | |
git fetch origin || { echo "Error: Could not fetch from origin"; exit 1; } | |
else | |
echo "β Using cached fetch data" | |
fi | |
# Check if local and remote commits differ before pulling | |
LOCAL_COMMIT=$(git rev-parse HEAD) | |
REMOTE_COMMIT=$(git rev-parse "origin/$TARGET_BRANCH" 2>/dev/null || echo "unknown") | |
if [ "$LOCAL_COMMIT" = "$REMOTE_COMMIT" ] && [ "$SHOULD_FETCH" = false ]; then | |
echo "β Already up to date (skipping pull)" | |
else | |
echo "Pulling latest changes from origin/$TARGET_BRANCH..." | |
git pull origin "$TARGET_BRANCH" || { echo "Error: Could not pull from origin/$TARGET_BRANCH"; exit 1; } | |
fi | |
# Pop the stash if we stashed changes | |
if [ "$MAIN_STASH_CREATED" = true ]; then | |
echo "Reapplying stashed changes..." | |
git stash pop || { echo "Warning: Could not pop stash - you may need to resolve conflicts manually"; } | |
echo "Stashed changes reapplied" | |
fi | |
# Update the foundation submodule | |
echo "Checking for 'foundation' submodule..." | |
# Check for different possible locations of the Foundation submodule | |
if [ -d "foundation" ] && [ -e "foundation/.git" ]; then | |
SUBMODULE_PATH="foundation" | |
elif [ -d "Foundation" ] && [ -e "Foundation/.git" ]; then | |
SUBMODULE_PATH="Foundation" | |
elif [ -d "Assets/Foundation" ] && [ -e "Assets/Foundation/.git" -o -d "$PROJECT_DIR/.git/modules/Assets/Foundation" ]; then | |
SUBMODULE_PATH="Assets/Foundation" | |
else | |
# Try to find the submodule using git | |
SUBMODULE_PATH=$(git config --file .gitmodules --get-regexp path | grep -i foundation | sed 's/.*path = //') | |
fi | |
if [ -n "$SUBMODULE_PATH" ]; then | |
echo "===== Updating 'foundation' submodule at path: $SUBMODULE_PATH =====" | |
cd "$SUBMODULE_PATH" || { echo "Error: Could not change to foundation directory at $SUBMODULE_PATH"; exit 1; } | |
# Verify we're in a git repository | |
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then | |
echo "Error: foundation is not a valid git repository" | |
cd "$PROJECT_DIR" | |
exit 1 | |
fi | |
# Get current branch of the submodule | |
SUB_CURRENT_BRANCH=$(git branch --show-current) | |
echo "Current foundation branch: $SUB_CURRENT_BRANCH" | |
# Submodule fetch optimization | |
SUB_SHOULD_FETCH=true | |
if [ "$FAST_MODE" = true ]; then | |
if is_fetch_recent; then | |
echo "β‘ Using cached submodule data (< 5 min old)" | |
SUB_SHOULD_FETCH=false | |
else | |
echo "π Submodule cache expired, fetching..." | |
fi | |
else | |
echo "π Force fetching submodule data" | |
fi | |
if [ "$SUB_SHOULD_FETCH" = true ]; then | |
echo "Fetching latest changes for the foundation submodule..." | |
git fetch origin || { echo "Error: Could not fetch from origin in submodule"; cd "$PROJECT_DIR"; exit 1; } | |
else | |
echo "β Using cached submodule fetch data" | |
fi | |
# Handle submodule changes | |
SUB_STASH_CREATED=false | |
if has_local_changes; then | |
handle_local_changes "foundation submodule" "$PROJECT_DIR" | |
case $? in | |
1) SUB_STASH_CREATED=true ;; | |
esac | |
fi | |
# Switch to target branch if not already on it | |
if [ "$SUB_CURRENT_BRANCH" != "$SUBMODULE_BRANCH" ]; then | |
echo "Switching to $SUBMODULE_BRANCH branch in foundation submodule..." | |
git checkout "$SUBMODULE_BRANCH" || { echo "Error: Could not switch to $SUBMODULE_BRANCH branch in submodule"; cd "$PROJECT_DIR"; exit 1; } | |
fi | |
# Optimize submodule pull based on commit comparison | |
SUB_LOCAL_COMMIT=$(git rev-parse HEAD) | |
SUB_REMOTE_COMMIT=$(git rev-parse "origin/$SUBMODULE_BRANCH" 2>/dev/null || echo "unknown") | |
if [ "$SUB_LOCAL_COMMIT" = "$SUB_REMOTE_COMMIT" ] && [ "$SUB_SHOULD_FETCH" = false ]; then | |
echo "β Submodule already up to date (skipping pull)" | |
else | |
echo "Pulling latest changes from origin/$SUBMODULE_BRANCH for foundation submodule..." | |
git pull origin "$SUBMODULE_BRANCH" || { echo "Error: Could not pull from origin/$SUBMODULE_BRANCH in submodule"; cd "$PROJECT_DIR"; exit 1; } | |
fi | |
# Pop the stash if we stashed changes | |
if [ "$SUB_STASH_CREATED" = true ]; then | |
echo "Reapplying stashed changes in submodule..." | |
git stash pop || { echo "Warning: Could not pop stash in submodule - you may need to resolve conflicts manually"; } | |
echo "Stashed changes reapplied in submodule" | |
fi | |
# Return to the main project directory | |
cd "$PROJECT_DIR" || { echo "Error: Could not return to main project directory"; exit 1; } | |
echo "Foundation submodule updated successfully" | |
elif [ -f ".gitmodules" ] && grep -qi "foundation" ".gitmodules"; then | |
echo "Foundation submodule exists but isn't initialized" | |
if [ "$AUTO_YES" = true ]; then | |
INIT_SUB="y" | |
echo "Auto-initializing submodule (--yes flag is set)" | |
else | |
read -p "Do you want to initialize and update the submodule? (y/n): " INIT_SUB | |
fi | |
if [ "$INIT_SUB" = "y" ]; then | |
# Extract the submodule path from .gitmodules | |
SUBMODULE_PATH=$(git config --file .gitmodules --get-regexp path | grep -i foundation | sed 's/.*path = //') | |
if [ -n "$SUBMODULE_PATH" ]; then | |
echo "Initializing submodule at path: $SUBMODULE_PATH" | |
git submodule update --init --recursive "$SUBMODULE_PATH" || { echo "Error: Could not initialize foundation submodule"; exit 1; } | |
echo "Foundation submodule initialized and updated" | |
else | |
echo "Could not determine submodule path from .gitmodules" | |
git submodule update --init --recursive || { echo "Error: Could not initialize all submodules"; exit 1; } | |
echo "All submodules initialized and updated" | |
fi | |
fi | |
else | |
echo "No 'foundation' submodule found in this project (.gitmodules check)" | |
# One final check - use git directly to list submodules | |
if git submodule status | grep -qi "foundation"; then | |
echo "Submodule detected through git submodule status" | |
if [ "$AUTO_YES" = true ]; then | |
INIT_SUB="y" | |
echo "Auto-initializing all submodules (--yes flag is set)" | |
else | |
read -p "Do you want to initialize and update all submodules? (y/n): " INIT_SUB | |
fi | |
if [ "$INIT_SUB" = "y" ]; then | |
git submodule update --init --recursive || { echo "Error: Could not initialize all submodules"; exit 1; } | |
echo "All submodules initialized and updated" | |
fi | |
else | |
echo "No 'foundation' submodule found in this project" | |
fi | |
fi | |
echo "===== Operation completed successfully =====" | |
echo "Your local branch is now up-to-date with origin/$TARGET_BRANCH" | |
if [ -n "$SUBMODULE_PATH" ]; then | |
echo "And foundation submodule at path '$SUBMODULE_PATH' is up-to-date with origin/$SUBMODULE_BRANCH" | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment