1
0
Fork 0
mirror of https://github.com/ohmyzsh/ohmyzsh.git synced 2024-10-16 11:40:46 +00:00

Compare commits

...

19 commits

Author SHA1 Message Date
Markus Hofbauer
68bb213bc1
Merge 21f33c476c into f11cc8fea1 2024-09-23 09:13:09 +02:00
Markus Hofbauer
21f33c476c move tool to .github workflows 2024-02-19 18:39:33 +01:00
Markus Hofbauer
cdf32e5c04 fix plugin path 2024-02-19 15:39:55 +01:00
Markus Hofbauer
e46423299e explicitly use master 2024-02-19 15:37:05 +01:00
Markus Hofbauer
4595a6ffe4 better use checkout 2024-02-19 15:31:48 +01:00
Markus Hofbauer
c9282c5613 remove known collisions 2024-02-19 15:19:49 +01:00
Markus Hofbauer
7875e78af7 removed resolved collision 2024-02-19 12:54:59 +01:00
Markus Hofbauer
a157b80a72 read known collisions from file 2024-02-19 12:54:59 +01:00
Markus Hofbauer
68f45fe079 add filter for known collisions 2024-02-19 12:54:59 +01:00
Markus Hofbauer
b4a077fdd8 refactor to pytest 2024-02-19 12:54:59 +01:00
Markus Hofbauer
4880743ab7 fix CI 2024-02-19 12:54:59 +01:00
Markus Hofbauer
114240d9fa disable failing of collision checker temporarily 2024-02-19 12:54:59 +01:00
Markus Hofbauer
b456dc5342 print all collisions found before failing 2024-02-19 12:54:59 +01:00
Markus Hofbauer
ccfdc8dd97 improve error message 2024-02-19 12:54:59 +01:00
Markus Hofbauer
821ee1ebec rename test to tests 2024-02-19 12:54:59 +01:00
Markus Hofbauer
d0c2053a1d update actions to latest major version 2024-02-19 12:54:59 +01:00
Markus Hofbauer
0d13d8b5fb run python unit tests in CI 2024-02-19 12:54:59 +01:00
Markus Hofbauer
de10614369 add collision check to github actions 2024-02-19 12:54:59 +01:00
Markus Hofbauer
d05b5b194e first draft alias collision 2024-02-19 12:54:59 +01:00
5 changed files with 526 additions and 0 deletions

View file

@ -0,0 +1,163 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# default known collisions file
known_collisions.json

View file

@ -0,0 +1,166 @@
"""Check for alias collisions within the codebase"""
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from pathlib import Path
from dataclasses import dataclass
from typing import List, Dict
import itertools
import re
import json
ERROR_MESSAGE_TEMPLATE = (
"Alias `%s` defined in `%s` already exists as alias `%s` in `%s`."
)
KNOWN_COLLISIONS_PATH = Path(__file__).resolve().parent / "known_collisions.json"
def dir_path(path_string: str) -> Path:
if Path(path_string).is_dir():
return Path(path_string)
else:
raise NotADirectoryError(path_string)
def parse_arguments():
parser = ArgumentParser(
description=__doc__,
formatter_class=ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"folder",
type=dir_path,
help="Folder to check",
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--known-collisions",
type=Path,
default=None,
help="Json-serialized list of known collision to compare to",
)
group.add_argument(
"--known-collisions-output-path",
type=Path,
default=KNOWN_COLLISIONS_PATH,
help="Output path for a json-serialized list of known collisions",
)
return parser.parse_args()
@dataclass(frozen=True)
class Alias:
alias: str
value: str
module: Path
def to_dict(self) -> Dict:
return {
"alias": self.alias,
"value": self.value,
"module": str(self.module),
}
@dataclass(frozen=True)
class Collision:
existing_alias: Alias
new_alias: Alias
def is_new_collision(self, known_collision_aliases: List[str]) -> bool:
return self.new_alias.alias not in known_collision_aliases
@classmethod
def from_dict(cls, collision_dict: Dict) -> "Collision":
return cls(
Alias(**collision_dict["existing_alias"]),
Alias(**collision_dict["new_alias"]),
)
def to_dict(self) -> Dict:
return {
"existing_alias": self.existing_alias.to_dict(),
"new_alias": self.new_alias.to_dict(),
}
def find_aliases_in_file(file: Path) -> List[Alias]:
matches = re.findall(r"^alias (.*)='(.*)'", file.read_text(), re.M)
return [Alias(match[0], match[1], file) for match in matches]
def load_known_collisions(collision_file: Path) -> List[Collision]:
collision_list = json.loads(collision_file.read_text())
return [Collision.from_dict(collision_dict) for collision_dict in collision_list]
def find_all_aliases(path: Path) -> list:
aliases = [find_aliases_in_file(file) for file in path.rglob("*.zsh")]
return list(itertools.chain(*aliases))
def check_for_duplicates(aliases: List[Alias]) -> List[Collision]:
elements = {}
collisions = []
for alias in aliases:
if alias.alias in elements:
existing = elements[alias.alias]
collisions.append(Collision(existing, alias))
else:
elements[alias.alias] = alias
return collisions
def print_collisions(collisions: Dict[Alias, Alias]) -> None:
if collisions:
print(f"Found {len(collisions)} alias collisions:\n")
for collision in collisions:
print(
ERROR_MESSAGE_TEMPLATE
% (
f"{collision.new_alias.alias}={collision.new_alias.value}",
collision.new_alias.module.name,
f"{collision.existing_alias.alias}={collision.existing_alias.value}",
collision.existing_alias.module.name,
)
)
print("\nConsider renaming your aliases.")
else:
print("Found no collisions")
def check_for_new_collisions(
known_collisions: Path, collisions: List[Collision]
) -> List[Collision]:
known_collisions = load_known_collisions(known_collisions)
known_collision_aliases = [
collision.new_alias.alias for collision in known_collisions
]
return [
collision
for collision in collisions
if collision.is_new_collision(known_collision_aliases)
]
def main() -> int:
"""main"""
args = parse_arguments()
aliases = find_all_aliases(args.folder)
collisions = check_for_duplicates(aliases)
if args.known_collisions is not None:
new_collisions = check_for_new_collisions(args.known_collisions, collisions)
print_collisions(new_collisions)
return -1 if new_collisions else 0
args.known_collisions_output_path.write_text(
json.dumps([collision.to_dict() for collision in collisions])
)
return 0
if __name__ == "__main__":
exit(main())

View file

@ -0,0 +1,3 @@
pyfakefs
pytest
pytest-sugar

View file

@ -0,0 +1,166 @@
from pathlib import Path
from pyfakefs.fake_filesystem import FakeFilesystem
import pytest
from check_alias_collision import (
dir_path,
find_all_aliases,
find_aliases_in_file,
check_for_duplicates,
Alias,
Collision,
load_known_collisions,
)
THREE_ALIASES = """
alias g='git'
alias ga='git add'
alias gaa='git add --all'
"""
CONDITIONAL_ALIAS = """
is-at-least 2.8 "$git_version" \
&& alias gfa='git fetch --all --prune --jobs=10' \
|| alias gfa='git fetch --all --prune'
"""
ONE_KNOWN_COLLISION = """
[
{
"existing_alias": {
"alias": "gcd",
"value": "git checkout $(git_develop_branch)",
"module": "plugins/git/git.plugin.zsh"
},
"new_alias": {
"alias": "gcd",
"value": "git checkout $(git config gitflow.branch.develop)",
"module": "plugins/git-flow/git-flow.plugin.zsh"
}
}
]
"""
def test_dir_path__is_dir__input_path(fs: FakeFilesystem) -> None:
fs.create_dir("test")
assert Path("test") == dir_path("test")
def test_dir_path__is_file__raise_not_a_directory_error(fs: FakeFilesystem) -> None:
fs.create_file("test")
with pytest.raises(NotADirectoryError):
dir_path("test")
def test_dir_path__does_not_exist__raise_not_a_directory_error(
fs: FakeFilesystem,
) -> None:
with pytest.raises(NotADirectoryError):
dir_path("test")
def test_find_all_aliases__empty_folder_should_return_empty_list(
fs: FakeFilesystem,
) -> None:
fs.create_dir("test")
result = find_all_aliases(Path("test"))
assert [] == result
def test_find_aliases_in_file__empty_text_should_return_empty_list(
fs: FakeFilesystem,
) -> None:
fs.create_file("empty.zsh")
result = find_aliases_in_file(Path("empty.zsh"))
assert [] == result
def test_find_aliases_in_file__one_alias_should_find_one(fs: FakeFilesystem) -> None:
fs.create_file("one.zsh", contents="alias g='git'")
result = find_aliases_in_file(Path("one.zsh"))
assert [Alias("g", "git", Path("one.zsh"))] == result
def test_find_aliases_in_file__three_aliases_should_find_three(
fs: FakeFilesystem,
) -> None:
fs.create_file("three.zsh", contents=THREE_ALIASES)
result = find_aliases_in_file(Path("three.zsh"))
assert [
Alias("g", "git", Path("three.zsh")),
Alias("ga", "git add", Path("three.zsh")),
Alias("gaa", "git add --all", Path("three.zsh")),
] == result
def test_find_aliases_in_file__one_conditional_alias_should_find_none(
fs: FakeFilesystem,
) -> None:
fs.create_file("conditional.zsh", contents=CONDITIONAL_ALIAS)
result = find_aliases_in_file(Path("conditional.zsh"))
assert [] == result
def test_check_for_duplicates__no_duplicates_should_return_empty_dict() -> None:
result = check_for_duplicates(
[
Alias("g", "git", Path("git.zsh")),
Alias("ga", "git add", Path("git.zsh")),
Alias("gaa", "git add --all", Path("git.zsh")),
]
)
assert result == []
def test_check_for_duplicates__duplicates_should_have_one_collision() -> None:
result = check_for_duplicates(
[
Alias("gc", "git commit", Path("git.zsh")),
Alias("gc", "git clone", Path("git.zsh")),
]
)
assert result == [
Collision(
Alias("gc", "git commit", Path("git.zsh")),
Alias("gc", "git clone", Path("git.zsh")),
)
]
def test_is_new_collision__new_alias_not_in_known_collisions__should_return_true() -> (
None
):
known_collisions = ["gc", "gd"]
new_alias = Alias("ga", "git add", Path("git.zsh"))
collision = Collision(Alias("gd", "git diff", Path("git.zsh")), new_alias)
assert collision.is_new_collision(known_collisions) is True
def test_is_new_collision__new_alias_in_known_collisions__should_return_false() -> None:
known_collisions = ["gc", "gd", "ga"]
new_alias = Alias("ga", "git add", Path("git.zsh"))
collision = Collision(Alias("gd", "git diff", Path("git.zsh")), new_alias)
assert collision.is_new_collision(known_collisions) is False
def test_load_known_collisions__empty_file__should_return_empty_list(
fs: FakeFilesystem,
) -> None:
empty_list = Path("empty.json")
fs.create_file(empty_list, contents="[]")
result = load_known_collisions(empty_list)
assert [] == result
def test_load_known_collisions__one_collision__should_return_one_collision(
fs: FakeFilesystem,
) -> None:
known_collisions_file = Path("known_collisions.json")
fs.create_file(known_collisions_file, contents=ONE_KNOWN_COLLISION)
result = load_known_collisions(known_collisions_file)
assert 1 == len(result)
assert "gcd" == result[0].existing_alias.alias

View file

@ -36,3 +36,31 @@ jobs:
./themes/*.zsh-theme; do
zsh -n "$file" || return 1
done
collisions:
name: Check alias collisions
runs-on: ubuntu-latest
steps:
- name: Set up git repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r .github/workflows/alias_collision/requirements.txt
- name: Run unit tests
run: |
cd .github/workflows/alias_collision/
python -m pytest
- name: Checkout target branch
uses: actions/checkout@v4
with:
path: ohmyzsh-target-branch
ref: master
- name: Check for alias collisions on target branch
run: python .github/workflows/alias_collision/check_alias_collision.py ohmyzsh-target-branch/plugins --known-collisions-output-path known_alias_collisions.json
- name: Compare known collisions to new collisions on source branch
run: python .github/workflows/alias_collision/check_alias_collision.py plugins --known-collisions known_alias_collisions.json