Skip to content

Commit c103509

Browse files
authored
Merge pull request #1166 from GitGuardian/feat/display-warning-if-cache-unignored
feat(cache): automatically add .cache_ggshield to .gitignore
2 parents 03efce7 + 1e45d75 commit c103509

5 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!--
2+
A new scriv changelog fragment.
3+
4+
Uncomment the section that is right (remove the HTML comment wrapper).
5+
For top level release notes, leave all the headers commented out.
6+
-->
7+
8+
<!--
9+
### Removed
10+
11+
- A bullet item for the Removed category.
12+
13+
-->
14+
15+
### Added
16+
17+
- Display a warning if .cache_ggshield is not ignored in a git repository.
18+
19+
<!--
20+
### Changed
21+
22+
- A bullet item for the Changed category.
23+
24+
-->
25+
<!--
26+
### Deprecated
27+
28+
- A bullet item for the Deprecated category.
29+
30+
-->
31+
<!--
32+
### Fixed
33+
34+
- A bullet item for the Fixed category.
35+
36+
-->
37+
<!--
38+
### Security
39+
40+
- A bullet item for the Security category.
41+
42+
-->

ggshield/core/cache.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ggshield.core.constants import CACHE_PATH
77
from ggshield.core.errors import UnexpectedError
88
from ggshield.core.types import IgnoredMatch
9+
from ggshield.utils.git_shell import gitignore, is_gitignored
910

1011

1112
SECRETS_CACHE_KEY = "last_found_secrets"
@@ -19,6 +20,9 @@ def __init__(self) -> None:
1920
self.load_cache()
2021

2122
def load_cache(self) -> None:
23+
if self.cache_path.is_file() and is_gitignored(self.cache_path) is False:
24+
gitignore(self.cache_path)
25+
2226
if not self.cache_path.is_file() or self.cache_path.stat().st_size == 0:
2327
return
2428

ggshield/utils/git_shell.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,3 +487,41 @@ def get_default_branch(wd: Optional[Union[str, Path]] = None) -> str:
487487
return git(["config", "init.defaultBranch"], cwd=wd).strip()
488488

489489
return default_branch
490+
491+
492+
def is_gitignored(path: Path) -> Optional[bool]:
493+
"""
494+
Returns True if the path is ignored by git, False if it is not,
495+
or None if git is not available or the directory is not a git repository.
496+
:param path: path to the file or directory to check
497+
"""
498+
working_dir = Path.cwd()
499+
if not is_git_available() or not is_git_dir(working_dir):
500+
return None
501+
else:
502+
res = git(["check-ignore", "--", path.as_posix()], cwd=working_dir, check=False)
503+
return res == path.as_posix()
504+
505+
506+
def gitignore(path: Path):
507+
"""
508+
Tries to add a path to the gitignore file.
509+
:param path: path to the file or directory to add to .gitignore
510+
"""
511+
working_dir = Path.cwd()
512+
513+
if not is_git_available() or not is_git_dir(working_dir):
514+
return
515+
516+
repo_root = _git_rev_parse(option="--show-toplevel", wd=working_dir)
517+
if repo_root is not None:
518+
try:
519+
with open(Path(repo_root) / ".gitignore", "a") as f:
520+
f.write("\n# Added by ggshield\n")
521+
f.write(path.as_posix() + "\n")
522+
except OSError:
523+
logger.debug(
524+
"Failed to add %s to .gitignore in %s",
525+
path,
526+
working_dir,
527+
)

tests/unit/core/test_cache.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import json
22
import os
3+
from pathlib import Path
34

45
import pytest
56
import yaml
67

78
from ggshield.core.cache import Cache
89
from ggshield.core.config import Config
910
from ggshield.core.types import IgnoredMatch
11+
from ggshield.utils.git_shell import is_gitignored
12+
from ggshield.utils.os import cd
13+
from tests.repository import Repository
1014

1115

1216
@pytest.mark.usefixtures("isolated_fs")
@@ -86,3 +90,21 @@ def test_max_commits_for_hook_setting(self):
8690

8791
config = Config()
8892
assert config.user_config.max_commits_for_hook == 75
93+
94+
95+
def test_auto_ignore_cache_file(tmp_path):
96+
"""
97+
GIVEN a cache file in a git repository
98+
WHEN it is not ignored by git
99+
THEN the cache file is automatically added to the gitignore file
100+
"""
101+
Repository.create(tmp_path)
102+
103+
with open(tmp_path / ".cache_ggshield", "w") as file:
104+
json.dump({"last_found_secrets": [{"name": "", "match": "XXX"}]}, file)
105+
106+
with cd(str(tmp_path)):
107+
assert not is_gitignored(Path(".cache_ggshield"))
108+
109+
Cache()
110+
assert is_gitignored(Path(".cache_ggshield"))

tests/unit/utils/test_git_shell.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import os
23
import platform
34
import subprocess
@@ -24,8 +25,10 @@
2425
get_staged_filepaths,
2526
git,
2627
git_ls_unstaged,
28+
gitignore,
2729
is_git_available,
2830
is_git_dir,
31+
is_gitignored,
2932
is_valid_git_commit_ref,
3033
simplify_git_url,
3134
)
@@ -680,3 +683,87 @@ def test_git_command_includes_longpaths_on_windows(mock_run):
680683
assert (
681684
not longpaths_included
682685
), f"core.longpaths=true found in command: {command}"
686+
687+
688+
def test_check_if_path_is_gitignored(tmp_path):
689+
# GIVEN a repository
690+
repo = Repository.create(tmp_path)
691+
repo.create_commit()
692+
693+
# WHEN checking if the path is ignored
694+
with cd(str(repo.path)):
695+
not_ignored = is_gitignored(Path("*.pyc"))
696+
with open(repo.path / ".gitignore", "w") as f:
697+
f.write("*.pyc")
698+
ignored = is_gitignored(Path("*.pyc"))
699+
700+
# THEN the correct value is returned
701+
assert ignored
702+
assert not_ignored is False
703+
704+
705+
def test_check_if_path_is_gitignored_no_repo(tmp_path):
706+
# GIVEN a directory that is not a git repository
707+
# WHEN checking if the path is ignored
708+
with cd(str(tmp_path)):
709+
no_git = is_gitignored(Path("*.pyc"))
710+
711+
# THEN None is returned
712+
assert no_git is None
713+
714+
715+
def test_gitignore(tmp_path):
716+
# GIVEN a repository
717+
repo = Repository.create(tmp_path)
718+
repo.create_commit()
719+
720+
# WHEN adding a path to the gitignore
721+
with cd(str(repo.path)):
722+
gitignore(Path("*.pyc"))
723+
724+
# THEN the path is added to the gitignore file
725+
gitignore_content = (repo.path / ".gitignore").read_text()
726+
assert "\n*.pyc\n" in gitignore_content
727+
728+
729+
def test_gitignore_with_existing_gitignore(tmp_path):
730+
# GIVEN a repository with an existing gitignore file
731+
repo = Repository.create(tmp_path)
732+
repo.create_commit()
733+
with open(tmp_path / ".gitignore", "w") as f:
734+
f.write("node_modules/")
735+
736+
# WHEN adding a path to the gitignore
737+
with cd(str(repo.path)):
738+
gitignore(Path("*.pyc"))
739+
740+
# THEN the path is added to the gitignore file
741+
gitignore_content = (repo.path / ".gitignore").read_text()
742+
assert "\n*.pyc\n" in gitignore_content
743+
744+
745+
def test_gitignore_in_non_git_directory(tmp_path):
746+
# GIVEN a non-git directory
747+
# WHEN adding a path to the gitignore
748+
with cd(str(tmp_path)):
749+
gitignore(Path("*.pyc"))
750+
751+
# THEN the path is not added to the gitignore file
752+
assert not (tmp_path / ".gitignore").exists()
753+
754+
755+
def test_gitignore_write_error(caplog, tmp_path):
756+
# GIVEN a repository with a gitignore file that cannot be written
757+
repo = Repository.create(tmp_path)
758+
repo.create_commit()
759+
with open(tmp_path / ".gitignore", "w") as f:
760+
f.write("node_modules/")
761+
(tmp_path / ".gitignore").chmod(0o000)
762+
763+
with caplog.at_level(logging.DEBUG):
764+
# WHEN adding a path to the gitignore
765+
with cd(str(repo.path)):
766+
gitignore(Path("*.pyc"))
767+
768+
# THEN the error is logged
769+
assert "Failed to add *.pyc to .gitignore in" in caplog.text

0 commit comments

Comments
 (0)