Skip to content

Instantly share code, notes, and snippets.

@mrtysn
Last active May 26, 2025 13:10
Show Gist options
  • Save mrtysn/04e0c8e1f29668203d7d23fbb05ab630 to your computer and use it in GitHub Desktop.
Save mrtysn/04e0c8e1f29668203d7d23fbb05ab630 to your computer and use it in GitHub Desktop.
#!/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