git-ident-filter.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. #!/usr/bin/env python
  2. #ident "@(#)$Format:LocalFoodAI:git-ident-filter.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
  3. import sys
  4. import os
  5. import subprocess
  6. import re
  7. from datetime import datetime
  8. # -----------------------------------------------------------------------------
  9. # STEP 1: LINE ENDINGS CONFIGURATION FOR WINDOWS/UNIX COMPATIBILITY
  10. # -----------------------------------------------------------------------------
  11. # On Windows, python streams sometimes default to automatically translating LF to CRLF.
  12. # Since Git filters must handle raw file streams, we force standard Unix LF ('\n')
  13. # line endings on stdin and stdout to prevent Git from raising corrupt content errors.
  14. if hasattr(sys.stdin, 'reconfigure'):
  15. sys.stdin.reconfigure(newline='\n')
  16. if hasattr(sys.stdout, 'reconfigure'):
  17. sys.stdout.reconfigure(newline='\n')
  18. # -----------------------------------------------------------------------------
  19. # STEP 2: PARSE COMMAND LINE ARGUMENTS
  20. # -----------------------------------------------------------------------------
  21. # The Git configuration passes arguments to this script:
  22. # sys.argv[1] is either 'clean' or 'smudge' (determining execution mode)
  23. # sys.argv[2] (for smudge mode) is the relative file path of the file being smudged
  24. mode = sys.argv[1] if len(sys.argv) > 1 else "smudge"
  25. # -----------------------------------------------------------------------------
  26. # STEP 3: DEFINE METADATA RETRIEVAL FUNCTION
  27. # -----------------------------------------------------------------------------
  28. def get_git_info(file_path):
  29. """Retrieves commit metadata for the specific file using git log, or falls back to system context."""
  30. try:
  31. # A. Query git log for the last commit details of the specific file.
  32. # We specify the YYYY/MM/DD HH:MM:SS format using '--date=format:...'.
  33. # We use a pipe '|' delimiter to separate fields in the output format.
  34. cmd = [
  35. "git", "log", "-1",
  36. "--date=format:%Y/%m/%d %H:%M:%S",
  37. "--format=%an|%ae|%ad|%cn|%ce|%cd|%H|%D|%N",
  38. "--", file_path
  39. ]
  40. # Run the git command, capture stdout, ignore stderr, decode bytes to string.
  41. out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip()
  42. if out:
  43. # Split the pipe-delimited string back into an array of fields
  44. parts = out.split('|')
  45. if len(parts) == 9:
  46. return parts
  47. except Exception:
  48. # If git log fails (e.g., file not tracked yet), proceed to fallback logic below.
  49. pass
  50. # B. Fallback: Query local Git configuration if file is not committed yet.
  51. try:
  52. # Retrieve the user's local git user.name
  53. author_name = subprocess.check_output(["git", "config", "user.name"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "system"
  54. # Retrieve the user's local git user.email
  55. author_email = subprocess.check_output(["git", "config", "user.email"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "system@mail.com"
  56. except Exception:
  57. # If Git is not installed or configured, fallback to default system strings.
  58. author_name = "system"
  59. author_email = "system@mail.com"
  60. # Get current system date & time formatted consistently
  61. now_str = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
  62. # Return placeholder list in the same structure as git log output
  63. return [author_name, author_email, now_str, author_name, author_email, now_str, "Not Committed Yet", "local", "none"]
  64. # -----------------------------------------------------------------------------
  65. # STEP 4: EXECUTE SPECIFIED FILTER MODE
  66. # -----------------------------------------------------------------------------
  67. if mode == "clean":
  68. # -------------------------------------------------------------------------
  69. # CLEAN MODE (Staging phase / git add)
  70. # -------------------------------------------------------------------------
  71. # Read the file's contents directly from standard input (passed by Git)
  72. content = sys.stdin.read()
  73. # Get the basename of the file being cleaned
  74. file_name = os.path.basename(sys.argv[2]) if len(sys.argv) > 2 else "app.py"
  75. # Non-greedy substitution to restore standard placeholder format for Git storage.
  76. # We construct the search pattern and replacement dynamically to avoid matching our own code.
  77. pattern = r'\$F' + r'ormat:[^\r\n$]+\$'
  78. repl = f"$F" + f"ormat:LocalFoodAI:{file_name}:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
  79. # Run regular expression search and replace
  80. cleaned = re.sub(pattern, repl, content)
  81. # Write the cleaned output directly to stdout so Git can write it to the index
  82. sys.stdout.write(cleaned)
  83. else:
  84. # -------------------------------------------------------------------------
  85. # SMUDGE MODE (Checkout / pull phase)
  86. # -------------------------------------------------------------------------
  87. try:
  88. # Get absolute path of repository to find project directory name
  89. toplevel = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], stderr=subprocess.DEVNULL).decode().strip()
  90. project_name = os.path.basename(toplevel)
  91. # Get the relative path of the file being smudged (passed as 2nd CLI arg by git configuration)
  92. file_name = sys.argv[2] if len(sys.argv) > 2 else "unknown_file"
  93. # Read the raw file content sent by Git on stdin
  94. content = sys.stdin.read()
  95. # Query git log metadata or local config fallbacks
  96. info = get_git_info(file_name)
  97. # Format replacement string using dynamic project name and relative file path
  98. # This replaces the placeholder metadata fields with actual git variables
  99. replacement = f"$F" + f"ormat:{project_name}:{os.path.basename(file_name)}:{info[0]}:{info[1]}:{info[2]}:{info[3]}:{info[4]}:{info[5]}:{info[6]}:{info[7]}:{info[8]}$"
  100. # Regex replacement targeting the dynamic format placeholders
  101. # Pattern explanation: Matches "$Format:LocalFoodAI:git-ident-filter.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
  102. pattern = r'\$F' + r'ormat:[^:]+:[^:]+:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N\$'
  103. smudged = re.sub(pattern, replacement, content)
  104. # Write smudged file contents to stdout so Git can output the file onto the filesystem
  105. sys.stdout.write(smudged)
  106. except Exception:
  107. # Safety fallback: If execution fails (e.g. outside git repository), write stream unchanged
  108. sys.stdout.write(sys.stdin.read())