# Copyright (c) 2025 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import contextlib import json import os import re import tempfile import gclient_utils import lockfile import scm @contextlib.contextmanager def _AtomicFileWriter(path, mode='w'): """Atomic file writer context manager.""" # Create temp file in same directory to ensure atomic rename works fd, temp_path = tempfile.mkstemp(dir=os.path.dirname(path), text='b' not in mode) try: with os.fdopen(fd, mode) as f: yield f # Atomic rename gclient_utils.safe_replace(temp_path, path) except Exception: # Cleanup on failure if os.path.exists(temp_path): os.remove(temp_path) raise class GerritCache(object): """Simple JSON file-based cache for Gerrit API results.""" def __init__(self, root_dir): self.root_dir = root_dir self.cache_path = self._get_cache_path() @staticmethod def get_repo_code_owners_enabled_key(host, project): return 'code-owners.%s.%s.enabled' % (host, project) @staticmethod def get_host_code_owners_enabled_key(host): return 'code-owners.%s.enabled' % host def _get_cache_path(self): path = os.environ.get('DEPOT_TOOLS_GERRIT_CACHE_PATH') if path: return path try: path = scm.GIT.GetConfig(self.root_dir, 'depot-tools.gerrit-cache-path') if path: return path except Exception: pass try: if self.root_dir: # Use a deterministic name based on the repo path sanitized_path = re.sub(r'[^a-zA-Z0-9]', '_', os.path.abspath(self.root_dir)) filename = 'depot_tools_gerrit_cache_%s.json' % sanitized_path path = os.path.join(tempfile.gettempdir(), filename) else: fd, path = tempfile.mkstemp(prefix='depot_tools_gerrit_cache_', suffix='.json') os.close(fd) if not os.path.exists(path): # Initialize with empty JSON object with open(path, 'w') as f: json.dump({}, f) try: scm.GIT.SetConfig(self.root_dir, 'depot-tools.gerrit-cache-path', path) except Exception: # If we can't set config (e.g. not a git repo and no env var # set), just return the temp path. It will be a per-process # cache in that case. pass return path except Exception: # Fallback to random temp file if everything else fails fd, path = tempfile.mkstemp(prefix='gerrit_cache_', suffix='.json') os.close(fd) return path def get(self, key): if not self.cache_path: return None try: with lockfile.lock(self.cache_path, timeout=1): if os.path.exists(self.cache_path): with open(self.cache_path, 'r') as f: try: data = json.load(f) return data.get(key) except ValueError: # Corrupt cache file, treat as miss return None except Exception: # Ignore cache errors return None def set(self, key, value): if not self.cache_path: return try: with lockfile.lock(self.cache_path, timeout=1): data = {} if os.path.exists(self.cache_path): with open(self.cache_path, 'r') as f: try: data = json.load(f) except ValueError: # Corrupt cache, start fresh data = {} data[key] = value with _AtomicFileWriter(self.cache_path, 'w') as f: json.dump(data, f) except Exception: # Ignore cache errors pass def getBoolean(self, key): """Returns the value for key as a boolean, or None if missing.""" val = self.get(key) if val is None: return None return bool(val) def setBoolean(self, key, value): """Sets the value for key as a boolean.""" self.set(key, bool(value))