1
0

git-ident-filter.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. #!/usr/bin/env python
  2. #ident "@(#)$Format:LocalFoodAI_lanfr144: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. # -----------------------------------------------------------------------------
  20. # STEP 2: PARSE COMMAND LINE ARGUMENTS
  21. # -----------------------------------------------------------------------------
  22. mode = sys.argv[1] if len(sys.argv) > 1 else "smudge"
  23. # -----------------------------------------------------------------------------
  24. # STEP 3: HELPERS FOR DYNAMIC DETAILS & SANITIZATION
  25. # -----------------------------------------------------------------------------
  26. def sanitize_name(name):
  27. """Standardizes variations of Lange Francois to a safe ASCII format, preventing charset decode errors."""
  28. if not name:
  29. return "Francois Lange"
  30. name_lower = name.lower()
  31. if "fran" in name_lower or "lange" in name_lower or "lanfr" in name_lower:
  32. return "Francois Lange"
  33. return name
  34. def get_git_repo_details():
  35. """Extracts username and project name dynamically from the origin git remote URL."""
  36. try:
  37. url = subprocess.check_output(["git", "remote", "get-url", "origin"], stderr=subprocess.DEVNULL).decode().strip()
  38. # Parse username and project name (e.g. from https://git.btshub.lu/lanfr/LocalFoodAI_lanfr144.git)
  39. match = re.search(r'[:/]([^:/]+)/([^/]+?)(?:\.git)?$', url)
  40. if match:
  41. username = match.group(1)
  42. project_name = match.group(2)
  43. return username, project_name
  44. except Exception:
  45. pass
  46. return "lanfr", "LocalFoodAI_lanfr144"
  47. # -----------------------------------------------------------------------------
  48. # STEP 4: DEFINE METADATA RETRIEVAL FUNCTION
  49. # -----------------------------------------------------------------------------
  50. def get_git_info(file_path):
  51. """Retrieves commit metadata for the specific file using git log, or falls back to system context."""
  52. try:
  53. cmd = [
  54. "git", "log", "-1",
  55. "--date=format:%Y/%m/%d %H:%M:%S",
  56. "--format=%an|%ae|%ad|%cn|%ce|%cd|%H|%D|%N",
  57. "--", file_path
  58. ]
  59. out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip()
  60. if out:
  61. parts = out.split('|')
  62. if len(parts) == 9:
  63. parts[0] = sanitize_name(parts[0])
  64. parts[3] = sanitize_name(parts[3])
  65. return parts
  66. except Exception:
  67. pass
  68. author_name = "Francois Lange"
  69. try:
  70. author_email = subprocess.check_output(["git", "config", "user.email"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "lanfr144@school.lu"
  71. except Exception:
  72. author_email = "lanfr144@school.lu"
  73. now_str = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
  74. return [author_name, author_email, now_str, author_name, author_email, now_str, "Not Committed Yet", "local", "none"]
  75. # -----------------------------------------------------------------------------
  76. # STEP 5: EXECUTE SPECIFIED FILTER MODE
  77. # -----------------------------------------------------------------------------
  78. if mode == "clean":
  79. # -------------------------------------------------------------------------
  80. # CLEAN MODE (Staging phase / git add)
  81. # -------------------------------------------------------------------------
  82. content = sys.stdin.read()
  83. file_name = os.path.basename(sys.argv[2]) if len(sys.argv) > 2 else "app.py"
  84. # Get project name dynamically from repo URL
  85. _, project_name = get_git_repo_details()
  86. pattern = r'\$F' + r'ormat:[^\r\n$]+\$'
  87. repl = f"$F" + f"ormat:{project_name}:{file_name}:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
  88. cleaned = re.sub(pattern, repl, content)
  89. sys.stdout.write(cleaned)
  90. else:
  91. # -------------------------------------------------------------------------
  92. # SMUDGE MODE (Checkout / pull phase)
  93. # -------------------------------------------------------------------------
  94. try:
  95. # Get project details dynamically from repo URL
  96. username, project_name = get_git_repo_details()
  97. file_name = sys.argv[2] if len(sys.argv) > 2 else "unknown_file"
  98. content = sys.stdin.read()
  99. info = get_git_info(file_name)
  100. 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]}$"
  101. # Regex replacement targeting the dynamic format placeholders
  102. pattern = r'\$F' + r'ormat:[^:]+:[^:]+:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N\$'
  103. smudged = re.sub(pattern, replacement, content)
  104. sys.stdout.write(smudged)
  105. except Exception:
  106. sys.stdout.write(sys.stdin.read())