blob: 47101e8a527003677fb78649b47eb253aaedf62d [file] [log] [blame] [edit]
# 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))