Browse Source

[#1] chore: make git-ident-filter line-restricted and automate remote model deployment

Lange François 3 weeks ago
parent
commit
24634c07f4
2 changed files with 59 additions and 17 deletions
  1. 57 16
      local_tools/git-ident-filter.py
  2. 2 1
      scripts/deploy_to_server.py

+ 57 - 16
local_tools/git-ident-filter.py

@@ -6,78 +6,119 @@ import subprocess
 import re
 import re
 from datetime import datetime
 from datetime import datetime
 
 
-# Force LF-only line endings on Windows for stdin and stdout to prevent automatic CRLF translation
+# -----------------------------------------------------------------------------
+# STEP 1: LINE ENDINGS CONFIGURATION FOR WINDOWS/UNIX COMPATIBILITY
+# -----------------------------------------------------------------------------
+# On Windows, python streams sometimes default to automatically translating LF to CRLF.
+# Since Git filters must handle raw file streams, we force standard Unix LF ('\n') 
+# line endings on stdin and stdout to prevent Git from raising corrupt content errors.
 if hasattr(sys.stdin, 'reconfigure'):
 if hasattr(sys.stdin, 'reconfigure'):
     sys.stdin.reconfigure(newline='\n')
     sys.stdin.reconfigure(newline='\n')
 if hasattr(sys.stdout, 'reconfigure'):
 if hasattr(sys.stdout, 'reconfigure'):
     sys.stdout.reconfigure(newline='\n')
     sys.stdout.reconfigure(newline='\n')
 
 
-# Detect execution mode (clean or smudge) passed by Git
+# -----------------------------------------------------------------------------
+# STEP 2: PARSE COMMAND LINE ARGUMENTS
+# -----------------------------------------------------------------------------
+# The Git configuration passes arguments to this script:
+# sys.argv[1] is either 'clean' or 'smudge' (determining execution mode)
+# sys.argv[2] (for smudge mode) is the relative file path of the file being smudged
 mode = sys.argv[1] if len(sys.argv) > 1 else "smudge"
 mode = sys.argv[1] if len(sys.argv) > 1 else "smudge"
 
 
+# -----------------------------------------------------------------------------
+# STEP 3: DEFINE METADATA RETRIEVAL FUNCTION
+# -----------------------------------------------------------------------------
 def get_git_info(file_path):
 def get_git_info(file_path):
     """Retrieves commit metadata for the specific file using git log, or falls back to system context."""
     """Retrieves commit metadata for the specific file using git log, or falls back to system context."""
     try:
     try:
-        # 1. Query git log for the last commit details of the specific file
+        # A. Query git log for the last commit details of the specific file.
+        # We specify the YYYY/MM/DD HH:MM:SS format using '--date=format:...'.
+        # We use a pipe '|' delimiter to separate fields in the output format.
         cmd = [
         cmd = [
             "git", "log", "-1",
             "git", "log", "-1",
             "--date=format:%Y/%m/%d %H:%M:%S",
             "--date=format:%Y/%m/%d %H:%M:%S",
             "--format=%an|%ae|%ad|%cn|%ce|%cd|%H|%D|%N",
             "--format=%an|%ae|%ad|%cn|%ce|%cd|%H|%D|%N",
             "--", file_path
             "--", file_path
         ]
         ]
+        
+        # Run the git command, capture stdout, ignore stderr, decode bytes to string.
         out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip()
         out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip()
         if out:
         if out:
+            # Split the pipe-delimited string back into an array of fields
             parts = out.split('|')
             parts = out.split('|')
             if len(parts) == 9:
             if len(parts) == 9:
                 return parts
                 return parts
     except Exception:
     except Exception:
+        # If git log fails (e.g., file not tracked yet), proceed to fallback logic below.
         pass
         pass
     
     
-    # 2. Fallback: Query local Git configuration if file is not committed yet
+    # B. Fallback: Query local Git configuration if file is not committed yet.
     try:
     try:
+        # Retrieve the user's local git user.name
         author_name = subprocess.check_output(["git", "config", "user.name"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "system"
         author_name = subprocess.check_output(["git", "config", "user.name"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "system"
+        # Retrieve the user's local git user.email
         author_email = subprocess.check_output(["git", "config", "user.email"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "system@mail.com"
         author_email = subprocess.check_output(["git", "config", "user.email"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "system@mail.com"
     except Exception:
     except Exception:
+        # If Git is not installed or configured, fallback to default system strings.
         author_name = "system"
         author_name = "system"
         author_email = "system@mail.com"
         author_email = "system@mail.com"
         
         
+    # Get current system date & time formatted consistently
     now_str = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
     now_str = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
+    # Return placeholder list in the same structure as git log output
     return [author_name, author_email, now_str, author_name, author_email, now_str, "Not Committed Yet", "local", "none"]
     return [author_name, author_email, now_str, author_name, author_email, now_str, "Not Committed Yet", "local", "none"]
 
 
+# -----------------------------------------------------------------------------
+# STEP 4: EXECUTE SPECIFIED FILTER MODE
+# -----------------------------------------------------------------------------
 if mode == "clean":
 if mode == "clean":
-    # CLEAN Mode: Replaces any smudged $Format:...$ tag back to the standard neutral git representation
+    # -------------------------------------------------------------------------
+    # CLEAN MODE (Staging phase / git add)
+    # -------------------------------------------------------------------------
+    # Read the file's contents directly from standard input (passed by Git)
     content = sys.stdin.read()
     content = sys.stdin.read()
+    
     # Non-greedy substitution to restore standard placeholder format for Git storage.
     # Non-greedy substitution to restore standard placeholder format for Git storage.
     # We construct the search pattern and replacement dynamically to avoid matching our own code.
     # We construct the search pattern and replacement dynamically to avoid matching our own code.
-    pattern = r'\$Format' + r':([^%:\r\n]+):([^%:\r\n]+):[^\r\n$]*?\$'
-    cleaned = re.sub(pattern, r'$Format:\1:\2:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$', content)
+    pattern = r'\$F' + r'ormat:[^\r\n$]+\$'
+    repl = r'$F' + r'ormat:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$'
+    
+    # Run regular expression search and replace
+    cleaned = re.sub(pattern, repl, content)
+    
+    # Write the cleaned output directly to stdout so Git can write it to the index
     sys.stdout.write(cleaned)
     sys.stdout.write(cleaned)
 
 
 else:
 else:
-    # SMUDGE Mode: Dynamically injects actual project, file path, and commit metadata into the file
+    # -------------------------------------------------------------------------
+    # SMUDGE MODE (Checkout / pull phase)
+    # -------------------------------------------------------------------------
     try:
     try:
         # Get absolute path of repository to find project directory name
         # Get absolute path of repository to find project directory name
         toplevel = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], stderr=subprocess.DEVNULL).decode().strip()
         toplevel = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], stderr=subprocess.DEVNULL).decode().strip()
         project_name = os.path.basename(toplevel)
         project_name = os.path.basename(toplevel)
 
 
-        # Get the relative path of the file being smudged
+        # Get the relative path of the file being smudged (passed as 2nd CLI arg by git configuration)
         file_name = sys.argv[2] if len(sys.argv) > 2 else "unknown_file"
         file_name = sys.argv[2] if len(sys.argv) > 2 else "unknown_file"
-        file_name = file_name.replace('\\', '/')
 
 
-        # Read the file content sent by Git on stdin
+        # Read the raw file content sent by Git on stdin
         content = sys.stdin.read()
         content = sys.stdin.read()
 
 
-        # Query git log metadata or local fallbacks
+        # Query git log metadata or local config fallbacks
         info = get_git_info(file_name)
         info = get_git_info(file_name)
 
 
-        # Format replacement string using dynamic project and file name
-        replacement = f"$Format" + f":{project_name}:{file_name}:{info[0]}:{info[1]}:{info[2]}:{info[3]}:{info[4]}:{info[5]}:{info[6]}:{info[7]}:{info[8]}$"
+        # Format replacement string using dynamic project name and relative file path
+        # This replaces the placeholder metadata fields with actual git variables
+        replacement = f"$F" + f"ormat:{project_name}:{file_name}:{info[0]}:{info[1]}:{info[2]}:{info[3]}:{info[4]}:{info[5]}:{info[6]}:{info[7]}:{info[8]}$"
 
 
         # Regex replacement targeting the dynamic format placeholders
         # Regex replacement targeting the dynamic format placeholders
-        pattern = r'\$Format' + r':([^%:\r\n]+):([^%:\r\n]+):%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N\$'
+        # Pattern explanation: Matches "$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+        pattern = r'\$F' + r'ormat:LocalFoodAI:app\.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N\$'
         smudged = re.sub(pattern, replacement, content)
         smudged = re.sub(pattern, replacement, content)
+        
+        # Write smudged file contents to stdout so Git can output the file onto the filesystem
         sys.stdout.write(smudged)
         sys.stdout.write(smudged)
 
 
     except Exception:
     except Exception:
-        # Security fallback: If executed outside Git repo, write stream unchanged
+        # Safety fallback: If execution fails (e.g. outside git repository), write stream unchanged
         sys.stdout.write(sys.stdin.read())
         sys.stdout.write(sys.stdin.read())

+ 2 - 1
scripts/deploy_to_server.py

@@ -34,7 +34,8 @@ def deploy():
         ssh.connect(host, username=user, password=password, timeout=10)
         ssh.connect(host, username=user, password=password, timeout=10)
         print("Connected successfully!")
         print("Connected successfully!")
         
         
-        command = "cd food_project && git stash && rm -f git_version.txt git_id.txt && git -c filter.ident-dynamic.clean= -c filter.ident-dynamic.smudge= pull && git stash clear && docker-compose up -d --build"
+        local_model = os.environ.get('LLM_MODEL', 'llama3.2-vision:11b')
+        command = f"cd food_project && git stash && rm -f git_version.txt git_id.txt && git -c filter.ident-dynamic.clean= -c filter.ident-dynamic.smudge= pull && git stash clear && sed -i 's/^LLM_MODEL=.*/LLM_MODEL={local_model}/' .env && docker-compose up -d --build"
         print(f"Executing: {command}")
         print(f"Executing: {command}")
         
         
         stdin, stdout, stderr = ssh.exec_command(command)
         stdin, stdout, stderr = ssh.exec_command(command)