diff --git a/README.md b/README.md index 37b3774..4df1e3c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This tool provides advanced Git feature management, allowing you to associate feature information with commits and files. -## Installation + Preaparation +## Installation + Preparation 1. Install the tool using pip: 1. Download the latest release file (currently, the project is not stored in the PIP Index) @@ -29,19 +29,19 @@ This tool provides advanced Git feature management, allowing you to associate fe ## Commands Overview -### `git feature-status` +### `git feature status` Displays the current feature status, including staged, unstaged, and untracked files with their associated features. **Usage**: ```bash -git feature-status +git feature status ``` -### `git feature-add` +### `git feature add` This command helps to associate feature information with a commit that does not yet exist. You can either add the information while adding the files or add features to the staging area. If you prefer to keep your workflows as usual and add feature information solely to commits that you already created, you don't need the git hooks and can jump to `git feature-commit`. -#### `git feature-add by-add` +#### `git feature add` Associates specified features with staged files. You can stage specific files or all tracked changes. @@ -51,62 +51,65 @@ Associates specified features with staged files. You can stage specific files or **Usage**: ```bash -git feature-add by-add --all -git feature-add by-add --files ... +git feature add --all +git feature add --files ... ``` -#### `git feature-add from-staged` +#### `git feature add-from-staged` Uses staged files to associate them with feature information. **Usage**: ```bash -git feature-add from-staged +git feature add-from-staged ``` -### `git feature-commit` +### `git feature commit` Assign features to a commit retroactively. To find all commits that have not yet features assigned, see --- **Usage**: ```bash -git feature-commit +git feature commit ``` -### `git feature-blame` +### `git feature blame` Displays the feature associations for each line of a specified file, similar to `git blame`. **Usage**: ```bash -git feature-blame +git feature blame ``` -### `git feature-info` +### `git feature info` Displays detailed information about a feature, including associated commits, files, authors, and branches. **Options**: - `--authors`: Lists the authors who contributed to the feature. - `--files`: Lists the files associated with the feature. -- `--branches`: Lists the branches where the feature is found. +- `--branches`: Lists the branches where the feature is present. +- `--updatable`: Check if the feature has updates available on other branches and list the update options. +- `--branch `: Specify a branch for checking updates (used with `--updatable`). **Usage**: ```bash -git feature-info -git feature-info --authors --files --branches +git feature info +git feature info --authors --files --branches ``` -### `git feature-commits` +### `git feature commits` Lists all commits associated with a feature or shows commits that are missing feature associations. -**Options**: -- `--missing`: Lists commits that do not have any associated feature. +**Commands**: +- `missing`: Lists commits that do not have any associated feature. +- `list`: List commits that are associated to features. **Usage**: ```bash -git feature-commits -git feature-commits --missing +git feature commits list +git feature commits missing ``` --- @@ -115,38 +118,38 @@ git feature-commits --missing 1. **Check Feature Status**: ```bash - git feature-status + git feature status ``` 2. **Add Features to All Files**: ```bash - git feature-add by-add --all "new-feature" + git feature add --all "new-feature" ``` 3. **Add Features to Specific Files**: ```bash - git feature-add by-add --files src/main.py "feature-x" + git feature add --files src/main.py "feature-x" ``` 4. **Use Staged Files for Feature Information**: ```bash - git feature-add from-staged + git feature add-from-staged ``` 5. **Show Feature Associations for a File**: ```bash - git feature-blame src/main.py + git feature blame src/main.py ``` 6. **Display Feature Information**: ```bash - git feature-info feature-x --authors --files --branches + git feature info feature-x --authors --files --branches ``` 7. **List All Commits With and Without Features**: ```bash - git feature-commits - git feature-commits --missing + git feature commits list + git feature commits missing ``` --- diff --git a/git_tool/__main__.py b/git_tool/__main__.py index 687f249..69b1c5e 100644 --- a/git_tool/__main__.py +++ b/git_tool/__main__.py @@ -1,15 +1,28 @@ import typer -from git_tool.ci.subcommands.feature_add import app as feature_add -from git_tool.ci.subcommands.feature_blame import app as feature_blame -from git_tool.ci.subcommands.feature_commit import app as feature_commit -from git_tool.ci.subcommands.feature_info import app as feature_info -from git_tool.ci.subcommands.feature_status import app as feature_status - -app = typer.Typer(name="features") -app.add_typer(feature_add, name="add") -app.add_typer(feature_blame, name="blame") -app.add_typer(feature_commit, name="commit") -app.add_typer(feature_info, name="info") -app.add_typer(feature_status, name="status") + +from git_tool.ci.subcommands.feature_add import feature_add_by_add +from git_tool.ci.subcommands.feature_add_from_staged import features_from_staging_area +from git_tool.ci.subcommands.feature_blame import feature_blame +from git_tool.ci.subcommands.feature_commit import feature_commit +from git_tool.ci.subcommands.feature_commit_msg import feature_commit_msg +from git_tool.ci.subcommands.feature_commits import app as feature_commits +from git_tool.ci.subcommands.feature_info import inspect_feature +from git_tool.ci.subcommands.feature_info_all import all_feature_info +from git_tool.ci.subcommands.feature_pre_commit import feature_pre_commit +from git_tool.ci.subcommands.feature_status import feature_status + + +app = typer.Typer(name="feature", no_args_is_help=True) # "git feature --help" does not work, but "git-feature --help" does +app.command(name="add", help="Stage files and associate them with the provided features.")(feature_add_by_add) +app.command(name="add-from-staged", help="Associate staged files with features.")(features_from_staging_area) +app.command(name="blame", help="Display features associated with file lines.")(feature_blame) +app.command(name="commit", help="Associate an existing commit with one or more features.")(feature_commit) +app.command(name="commit-msg", help="Generate feature information for the commit message.")(feature_commit_msg) +app.add_typer(feature_commits, name="commits", help="Use with the subcommand 'list' or 'missing' to show commits with or without associated features.") +app.command(name="info", help="Show information of a specific feature.")(inspect_feature) +app.command(name="info-all", help="List all available features in the project.")(all_feature_info) +app.command(name="pre-commit", help="Check if all staged changes are properly associated with features.")(feature_pre_commit) +app.command(name="status", help="Display unstaged and staged changes with associated features.")(feature_status) + app() diff --git a/git_tool/ci/subcommands/feature_add.py b/git_tool/ci/subcommands/feature_add.py index 62efe6c..26e65f4 100644 --- a/git_tool/ci/subcommands/feature_add.py +++ b/git_tool/ci/subcommands/feature_add.py @@ -14,7 +14,7 @@ app = typer.Typer(no_args_is_help=True) -@app.command("by-add", no_args_is_help=True) +@app.command("add", help="Stage files and associate them with the provided features.", no_args_is_help=True) def feature_add_by_add( feature_names: list[str] = typer.Argument( None, help="List of feature names to associate with the staged files" @@ -52,48 +52,6 @@ def feature_add_by_add( typer.echo("No features provided.", err=True) -@app.command("from-staged", help="Associate staged files with features") -def features_from_staging_area(): - """ - Use the staged files to add feature information. - """ - feature_names = read_features_from_staged() - if feature_names: - typer.echo(f"Features in staging area: {feature_names}") - write_staged_featureset(feature_names) - else: - typer.echo("No features found in staging area.", err=True) - - -def read_features_from_staged(type: str = "Union") -> list[str]: - """ - Retrieve the features from the current staging area. - - Keyword Arguments: - type -- Can be Union or Intersection. Describes how multiple features will be combined (default: {"Union"}) - - Returns: - List of features. - """ - typer.echo("Using staged files") - - staged_files = get_files_by_git_change().get("staged_files", []) - feature_sets = [set(get_features_for_file(f)) for f in staged_files] - - if not feature_sets: - return [] - - if type == "Union": - combined_features = set.union(*feature_sets) - elif type == "Intersection": - combined_features = set.intersection(*feature_sets) - else: - raise ValueError( - "Invalid type specified. Use 'Union' or 'Intersection'." - ) - - return list(combined_features) - def stage_files(selected_files: list[str]) -> bool: """ diff --git a/git_tool/ci/subcommands/feature_add_from_staged.py b/git_tool/ci/subcommands/feature_add_from_staged.py new file mode 100644 index 0000000..66f528b --- /dev/null +++ b/git_tool/ci/subcommands/feature_add_from_staged.py @@ -0,0 +1,56 @@ +import typer + +from git_tool.feature_data.git_status_per_feature import ( + get_features_for_file, + get_files_by_git_change, +) +from git_tool.feature_data.models_and_context.feature_state import ( + reset_staged_featureset, + write_staged_featureset, +) + + +app = typer.Typer() + + +@app.command(name="add-from-staged", help="Associate staged files with features.") +def features_from_staging_area(): + """ + Use the staged files to add feature information. + """ + feature_names = read_features_from_staged() + if feature_names: + typer.echo(f"Features in staging area: {feature_names}") + write_staged_featureset(feature_names) + else: + typer.echo("No features found in staging area.", err=True) + + +def read_features_from_staged(type: str = "Union") -> list[str]: + """ + Retrieve the features from the current staging area. + + Keyword Arguments: + type -- Can be Union or Intersection. Describes how multiple features will be combined (default: {"Union"}) + + Returns: + List of features. + """ + typer.echo("Using staged files") + + staged_files = get_files_by_git_change().get("staged_files", []) + feature_sets = [set(get_features_for_file(f)) for f in staged_files] + + if not feature_sets: + return [] + + if type == "Union": + combined_features = set.union(*feature_sets) + elif type == "Intersection": + combined_features = set.intersection(*feature_sets) + else: + raise ValueError( + "Invalid type specified. Use 'Union' or 'Intersection'." + ) + + return list(combined_features) \ No newline at end of file diff --git a/git_tool/ci/subcommands/feature_blame.py b/git_tool/ci/subcommands/feature_blame.py index 423506d..900ef31 100644 --- a/git_tool/ci/subcommands/feature_blame.py +++ b/git_tool/ci/subcommands/feature_blame.py @@ -1,10 +1,12 @@ from pathlib import Path +from typing import Any + import typer from git import Repo -from git_tool.feature_data.git_status_per_feature import get_features_for_file +from git_tool.feature_data.read_feature_data.parse_data import get_features_touched_by_commit from git_tool.feature_data.models_and_context.repo_context import ( repo_context, -) # Assuming this exists in your code +) app = typer.Typer(no_args_is_help=True) @@ -17,93 +19,95 @@ def read_file_lines(file_path: Path) -> list[str]: return f.readlines() -def run_git_blame( +def get_line_to_blame_mapping( repo: Repo, file_path: Path, start_line: int, end_line: int -) -> dict[int, str]: +) -> dict[int, tuple[str, str]]: """ - Uses gitpython's blame functionality to map line numbers to commit hashes. - This function works on the specified range of lines. + Returns a mapping of line numbers to (commit hash, blame line). """ blame_output = repo.git.blame( - "-L", f"{start_line},{end_line}", "--line-porcelain", str(file_path) + "-L", f"{start_line},{end_line}", "--date=short", str(file_path) ) - line_to_commit = {} - current_commit = None + line_to_blame = {} line_number = start_line for line in blame_output.splitlines(): - if line.startswith("author "): - continue - if line.startswith("summary "): - continue - if line.startswith("filename "): + if line.startswith(":"): continue + blame_part = line.split(" ", 1) + short_hash = blame_part[0] + blame_text = blame_part[1] if len(blame_part) > 1 else "" + full_hash = repo.git.rev_parse(short_hash) + line_to_blame[line_number] = (short_hash, blame_text) + line_number += 1 - # New commit hash - if line.startswith( - (" ", "\t") - ): # If the line starts with a space, it is a line of the file - line_to_commit[line_number] = current_commit - line_number += 1 - else: - current_commit = line.split()[0] - - return line_to_commit + return line_to_blame -def get_commit_feature_mapping() -> dict[str, str]: +def get_commit_to_features_mapping(line_to_commit: dict[int, tuple[str, str]]) -> dict[str, str]: """ Returns a mapping of commit hashes to features. - This is a placeholder for your actual implementation, where you'd - map each commit hash to its associated feature. """ - # Example mapping: Replace with your real data source - return { - "abcd123": "Feature A", - "efgh456": "Feature B", - "ijkl789": "Feature C", + unique_commits = {commit for commit, _ in line_to_commit.values()} + + commit_to_features = { + commit_id: ", ".join(get_features_touched_by_commit(commit_id)) + for commit_id in unique_commits } + return commit_to_features -def get_features_for_lines( + +def get_line_to_features_mapping( repo: Repo, file_path: Path, start_line: int, end_line: int -) -> dict[int, str]: +) -> tuple[dict[int, Any], dict[int, tuple[str, str]]]: """ - Returns a dictionary mapping line numbers to features based on the commits - that modified each line in the specified line range. + Returns a mapping of line numbers to features. """ - # Step 1: Get the commit for each line using 'git blame' - line_to_commit = run_git_blame(repo, file_path, start_line, end_line) - - # Step 2: Get the mapping of commits to features - commit_to_feature = get_commit_feature_mapping() - - # Step 3: Map each line to its corresponding feature - line_to_feature = { - line: commit_to_feature.get(commit_hash, "UNKNOWN") - for line, commit_hash in line_to_commit.items() + # Get the commit for each line using 'git blame' + line_to_blame = get_line_to_blame_mapping(repo, file_path, start_line, end_line) + # for debugging: print("Step 1: ", line_to_blame) + + # Get the features for each commit + commit_to_features = get_commit_to_features_mapping(line_to_blame) + # for debugging: print("Step 2: ", commit_to_features) + + # Map each line to its corresponding feature + line_to_features = { + line: commit_to_features.get(commit_hash, "UNKNOWN") + for line, (commit_hash, _) in line_to_blame.items() } + # for debugging: print("Step 3: ", line_to_features) - return line_to_feature + return line_to_features, line_to_blame def print_feature_blame_output( lines: list[str], - features_by_line: dict[int, str], + mappings: tuple[dict[int, Any], dict[int, tuple[str, str]]], start_line: int, end_line: int, ): """ Prints the feature blame output similar to git blame. """ + line_to_features, line_to_blame = mappings + # Get the max width of feature strings for alignment + max_feature_width = max( + (len(line_to_features.get(commit, "UNKNOWN")) for commit in line_to_features.values()), + default=15, + ) + for i in range(start_line, end_line + 1): - line = lines[i - 1] # Adjust for 0-based indexing - feature = features_by_line.get(i, "UNKNOWN") - typer.echo(f"{feature:<15} {i:>4} {line.strip()}") + line = lines[i - 1] # Adjust because list is 0-indexed, but line numbers start from 1 + commit_hash, blame_text = line_to_blame.get(i) + blame_text = blame_text.replace("(", "", 1) + feature = line_to_features.get(i, "UNKNOWN") + typer.echo(f"{feature:<15} ({commit_hash} {blame_text}") -@app.command(no_args_is_help=True, name=None) +@app.command(help="Display features associated with file lines.", no_args_is_help=True, name=None) def feature_blame( filename: str = typer.Argument( ..., help="The file to display feature blame for." @@ -120,44 +124,40 @@ def feature_blame( if not file_path.exists(): typer.echo(f"Error: file '{filename}' not found.") raise typer.Exit(code=1) - file_features = get_features_for_file( - file_path=file_path, use_annotations=False - ) - typer.echo(f"Features associated with the file '{filename}':\n") - for i, feature in enumerate(file_features, 1): - typer.echo(f"{i}. {feature}") + + # Read the file contents + lines = read_file_lines(file_path) + + # Default to the entire file if no line argument is provided + start_line = 1 + end_line = len(lines) if line: - typer.echo("Linebased blames are not supported yet", err=True) - return - lines = read_file_lines(file_path) - - # Default to the entire file if no line argument is provided - start_line = 1 - end_line = len(lines) - - if line: - if "-" in line: - # Handle a range of lines - start_line, end_line = map(int, line.split("-")) - else: - # Handle a single line - start_line = end_line = int(line) - - # Ensure the line range is valid - if start_line < 1 or end_line > len(lines): - typer.echo("Error: Line number out of range.") - raise typer.Exit(code=1) - - with repo_context() as repo: # Use repo_context for the git operations - feature_to_line_mapping = get_features_for_lines( - repo, file_path, start_line, end_line - ) - - print_feature_blame_output( - lines, feature_to_line_mapping, start_line, end_line + if "-" in line: + # Handle a range of lines + start_line, end_line = map(int, line.split("-")) + else: + # Handle a single line + start_line = end_line = int(line) + + # Ensure the line range is valid + if start_line < 1 or end_line > len(lines): + typer.echo("Error: Line number out of range.") + raise typer.Exit(code=1) + + if start_line > end_line: + typer.echo("Error: Start line must be less than end line.") + raise typer.Exit(code=1) + + with repo_context() as repo: # Use repo_context for the git operations + feature_to_line_mapping = get_line_to_features_mapping( + repo, file_path, start_line, end_line ) + print_feature_blame_output( + lines, feature_to_line_mapping, start_line, end_line + ) + if __name__ == "__main__": app() diff --git a/git_tool/ci/subcommands/feature_commit.py b/git_tool/ci/subcommands/feature_commit.py index fbd6493..2e142eb 100644 --- a/git_tool/ci/subcommands/feature_commit.py +++ b/git_tool/ci/subcommands/feature_commit.py @@ -23,7 +23,7 @@ @app.command( name="commit", - help="Associate an existing commit with one or more features", + help="Associate an existing commit with one or more features.", no_args_is_help=True, ) def feature_commit( diff --git a/git_tool/ci/subcommands/feature_info.py b/git_tool/ci/subcommands/feature_info.py index 879908f..70f3a27 100644 --- a/git_tool/ci/subcommands/feature_info.py +++ b/git_tool/ci/subcommands/feature_info.py @@ -10,9 +10,6 @@ get_files_for_commit, ) from git_tool.feature_data.git_status_per_feature import get_commits_for_feature -from git_tool.feature_data.read_feature_data.parse_data import ( - _get_feature_uuids, -) def print_list_w_indent(stuff: list, indent: int = 1) -> None: @@ -20,16 +17,7 @@ def print_list_w_indent(stuff: list, indent: int = 1) -> None: typer.echo("\t" * indent + item) -app = typer.Typer( - help="Displaying feature information for the entire git repo", - no_args_is_help=True, -) - -info_app = typer.Typer( - no_args_is_help=True, help="Inspect feature details. This includes " -) -# app.add_typer(info_app, name="info") -# app.add_typer(currently_staged_app, name="currently-staged") +app = typer.Typer(help="Displaying feature information for the entire git repo", no_args_is_help=True) # TODO sollte zu git feature-status @@ -40,7 +28,7 @@ def print_list_w_indent(stuff: list, indent: int = 1) -> None: # return -@app.command(name="feature", help="Show feature-specific information.") +@app.command(name="info") def inspect_feature( feature: str = typer.Argument(..., help="Inspect a particular feature"), authors: bool = typer.Option( @@ -64,13 +52,14 @@ def inspect_feature( ): typer.echo(f"Collecting information for feature {feature}") try: + typer.echo("Commit IDs") commit_ids = [x.hexsha for x in get_commits_for_feature(feature)] print_list_w_indent(commit_ids) except Exception: commit_ids = [] typer.echo(f"No commit-ids for feature {feature} found") if branches: - typer.echo(f"Branches (* indicates current branch)") + typer.echo("Branches (* indicates current branch)") branches = set( [ branch @@ -80,11 +69,11 @@ def inspect_feature( ) print_list_w_indent(branches) if authors: - typer.echo(f"Authors") + typer.echo("Authors") authors = set([get_author_for_commit(c) for c in commit_ids]) print_list_w_indent(authors) if files: - typer.echo(f"Files") + typer.echo("Files") files = set( file for c in commit_ids for file in get_files_for_commit(c) ) @@ -146,18 +135,5 @@ def inspect_feature( ) -@app.command( - name="all", - help="List all available features in the project. \ - Use these names with 'git feature-info ' to inspect details.", -) -def all_feature_info(): - """ - Inspects feature information. - """ - typer.echo("All Features") - print_list_w_indent(_get_feature_uuids()) - - if __name__ == "__main__": app() diff --git a/git_tool/ci/subcommands/feature_info_all.py b/git_tool/ci/subcommands/feature_info_all.py new file mode 100644 index 0000000..4bd86c5 --- /dev/null +++ b/git_tool/ci/subcommands/feature_info_all.py @@ -0,0 +1,22 @@ +import typer + +from git_tool.feature_data.read_feature_data.parse_data import ( + _get_feature_uuids, +) + + +app = typer.Typer() + + +@app.command(name="info-all") +def all_feature_info(): + """ + Inspects feature information. + """ + typer.echo("All Features") + print_list_w_indent(_get_feature_uuids()) + + +def print_list_w_indent(stuff: list, indent: int = 1) -> None: + for item in stuff: + typer.echo("\t" * indent + item) \ No newline at end of file diff --git a/git_tool/ci/subcommands/feature_status.py b/git_tool/ci/subcommands/feature_status.py index b675503..7800182 100644 --- a/git_tool/ci/subcommands/feature_status.py +++ b/git_tool/ci/subcommands/feature_status.py @@ -19,17 +19,15 @@ ) -@app.command( - help="Displays the current status of files in the working directory, showing staged, unstaged, and untracked changes along with their associated features. \ - Files without associated features will be highlighted, and suggestions for adding features will be provided.", -) +@app.command(name="status") def feature_status( help: bool = typer.Option( None, "--help", "-h", is_eager=True, help="Show this message and exit." ) ): """ - Displays unstaged and staged changes with associated features. + Displays the current status of files in the working directory, showing staged, unstaged, and untracked changes along with their associated features. + Files without associated features will be highlighted, and suggestions for adding features will be provided. """ if help: typer.echo(app.rich_help_panel) diff --git a/git_tool/feature_data/analyze_feature_data/feature_utils.py b/git_tool/feature_data/analyze_feature_data/feature_utils.py index fd23887..8a627dc 100644 --- a/git_tool/feature_data/analyze_feature_data/feature_utils.py +++ b/git_tool/feature_data/analyze_feature_data/feature_utils.py @@ -96,12 +96,12 @@ def get_uuid_for_featurename(name: str) -> uuid.UUID: # TODO this is not implemented correctly return name - +# Usages: FEATURE INFO def get_current_branchname() -> str: with repo_context() as repo: return repo.active_branch - +# Usages: FEATURE INFO def get_commits_for_feature_on_other_branches( feature_commits: set[str], current_branch: str = get_current_branchname(), @@ -166,7 +166,7 @@ def get_all_features() -> list[str]: folders = folder_string.splitlines() return folders - +# Usages: FEATURE COMMITS def get_commits_with_feature() -> list[str]: """ Return a list of short ids of commits that are assoicated diff --git a/git_tool/feature_data/git_status_per_feature.py b/git_tool/feature_data/git_status_per_feature.py index 34b6c31..4c195f8 100644 --- a/git_tool/feature_data/git_status_per_feature.py +++ b/git_tool/feature_data/git_status_per_feature.py @@ -23,7 +23,7 @@ class GitChanges(TypedDict): GitStatusEntry = namedtuple("GitStatusEntry", ["status", "file_path"]) - +# Usages: FEATURE ADD, ADD-FROM-STAGED, PRE-COMMIT, STATUS def get_files_by_git_change() -> GitChanges: """ Retrieves files sorted by the type of git change (staged, unstaged, untracked). @@ -64,7 +64,7 @@ def find_annotations_for_file(file: str): """ raise NotImplementedError - +# Usage: FEATURE ADD-FROM-STAGED, BLAME, STATUS def get_features_for_file( file_path: str, use_annotations: bool = False ) -> List[str]: @@ -102,7 +102,7 @@ def get_features_for_file( features.append(feature_name) return features - +# Usages: FEATURE INFO, FEATURE STATUS def get_commits_for_feature(feature_uuid: str) -> list[Commit]: with repo_context() as repo: diff --git a/git_tool/feature_data/models_and_context/feature_state.py b/git_tool/feature_data/models_and_context/feature_state.py index 708f6b3..0f0f131 100644 --- a/git_tool/feature_data/models_and_context/feature_state.py +++ b/git_tool/feature_data/models_and_context/feature_state.py @@ -35,6 +35,7 @@ def read_staged_featureset() -> List[str]: features = set(line.strip() for line in f.readlines()) return list(features) +# Usage: FEATURE ADD, ADD-FROM-STAGED def write_staged_featureset(features: List[str]): """ Write the list of staged features to the FEATUREINFO file. diff --git a/git_tool/feature_data/read_feature_data/parse_data.py b/git_tool/feature_data/read_feature_data/parse_data.py index 001bdd6..29c4433 100644 --- a/git_tool/feature_data/read_feature_data/parse_data.py +++ b/git_tool/feature_data/read_feature_data/parse_data.py @@ -15,7 +15,7 @@ repo_context, ) - +# Usages: FEATURE INFO-ALL def _get_feature_uuids() -> list[str]: """ Each feature has its own folder where the foldername is equivalent to the uuid that the @@ -91,7 +91,7 @@ def get_feature_log(feature_uuid: str): ) ) - +# Usages: compare_branches.py (potentially FEATURE BLAME) def get_features_touched_by_commit(commit: Commit) -> Set[str]: """ Retrieves the set of features touched by a given commit. @@ -202,23 +202,6 @@ def extract_facts_from_commit(commit: Commit) -> List[FeatureFactModel]: return facts -def get_features_touched_by_commit(commit: Commit) -> Set[str]: - """ - Retrieves the set of features touched by a given commit. - - Args: - commit (Commit): The commit to analyze. - - Returns: - Set[str]: Set of features touched by the commit. - """ - feature_facts = extract_facts_from_commit(commit) - features = set() - for fact in feature_facts: - features.update(fact.features) - return features - - if __name__ == "__main__": # get_metadata("abc") # logging.info("Get Metadata success") diff --git a/git_tool/helpdoc/git-feature.1 b/git_tool/helpdoc/git-feature.1 new file mode 100644 index 0000000..f14d574 --- /dev/null +++ b/git_tool/helpdoc/git-feature.1 @@ -0,0 +1,26 @@ +.TH git-feature 1 "12 April 2025" "Kim Nguyen" "git-feature(1) Manual Page" +.SH NAME +git-feature - Manage feature metadata and inspect feature-related information + +.SH SYNOPSIS +.P +\fB git feature status \fP +.P +\fB git feature add \fP [-a | --all | -f | --files ] +.P +\fB git feature add-from-staged \fP +.P +\fB git feature commit \fP [--features ] +.P +\fB git feature commits missing \fP +.P +\fB git feature commits list \fP +.P +\fB git feature blame \fP +.P +\fB git feature info-all \fP +.P +\fBgit feature info\fP [--authors | --files | --branches | --updatable | --branch ] + +.SH COMMANDS +With no arguments, shows the help text. Several subcommands are available to perform operations on the feature metadata. \ No newline at end of file diff --git a/git_tool/helpdoc/git-feature.html b/git_tool/helpdoc/git-feature.html new file mode 100644 index 0000000..04424df --- /dev/null +++ b/git_tool/helpdoc/git-feature.html @@ -0,0 +1,2334 @@ + + + + + + + + git-feature(1) + + + + + +
+
+

SYNOPSIS

+
+
+
git feature status
+git feature add <feature_names> (-a | --all | -f <file_name> | --files <file_name>)
+git feature add-from-staged
+git feature commit <commit_id> [--features <feature_name>]
+git feature commits list
+git feature commits missing
+git feature blame <file_name>
+git feature info-all [--push] <name> <newurl> [<oldurl>]
+git feature info [--authors | --files | --branches | --updatable | --branch <branch_name>]
+
+
+
+
+

DESCRIPTION

+
+
+

Associate features with commits and files in a repository, manage feature metadata, and inspect feature-related information.

+
+
+
+
+

COMMANDS

+
+
+

With no subcommands, shows the help text. Several subcommands are available to track and manage feature information.

+
+
+
+
status
+
+

Displays the current status of files in the working directory, showing staged, unstaged, and untracked changes + along with their associated features. This command is intended to be used with the following commands to add + feature information while staging files. It can also be used to aid in the selection of files for the next commit.

+
+
add
+
+

Stage specific <file_names> or all files and associate them with the provided features.

+
+

With -a or --all, you can stage all tracked changes and associate them with the features.

+
+
+

With -f or --files, you can stage a <file_name> and associate it with the features. + You can use this option multiple times to stage multiple files.

+
+
+
add-from-staged
+
+

Associate features with the currently staged files. Currently, it derives the feature list from the previously selected + features for those files and is not fully implemented yet.

+
+
commit
+
+

Associates feature information with an existing commit. This is useful when you want to add feature metadata to a commit + after it has been created.

+
+

With --features, a feature name is manually specified. You can use this option multiple times to associate + multiple features. If this is provided, staged feature information will be ignored. Otherwise, the program will try + to derive the information.

+
+
+
commits
+
+

Provides information about commits that are either already associated, or have not yet been associated with a feature. + This is especially useful when transitioning to feature-based workflows or when tracing commits for specific features. + It helps identifying which commits have feature metadata associated and which do not, ensuring that no commit is left + untracked.

+
+

With the list subcommand, show a list of commits that are already associated with features.

+
+
+

With the missing subcommand, show a list of commits that have not yet been associated with features.

+
+
+
blame
+
+

Display features associated with <file_name> lines. Currently, it shows the associated file and has not yet been fully + implemented yet.

+
+
info-all
+
+

List all available features in the project.

+
+
info
+
+

Shows detailed information about the feature <feature_name>, including authors, files, branches, and update status.

+
+

With --authors, show all authors who contributed to this feature.

+
+
+

With --files, list all files associated with this feature.

+
+
+

With --branches, show all branches where this feature is present.

+
+
+

With --branch <branch_name>, show all branches where this feature is present.

+
+
+
+
+
+
+
+

EXAMPLE WORKFLOWS

+
+
+
    +
  • +

    Assess the file location for feature authentication, inspect its commit history, and list its authors

    +
    +
    +
    $ git feature info "authentication" --files
    +    Files associated with 'authentication':
    +    - src/auth/login.py
    +    - src/auth/logout.py
    +    - src/auth/utils.py
    +
    +$ git feature info "authentication" --log
    +    Recent commits for 'authentication':
    +    − mno7890 : Improved password hashing algorithm
    +    − abc1234 : Implemented login functionality
    +    ...
    +$ git show mno7890
    +    commit mno7890
    +    Author: Bob <bob@company.com >
    +    Date: Mon Apr 01 11:12:13 2024
    +    Improved password hashing algorithm
    +
    +    The one used before had a problem.
    +    diff −−git a/src/auth/utils.pyb/src/auth/utils.py
    +    <diff of the commit>
    +
    +$ git feature info "authentication" --authors
    +    Top contributors to 'authentication':
    +    − Alice Smith (5 commits)
    +    − Bob Johnson (3 commits)
    +
    +
    +
  • +
  • +

    Analyze and stage changes, associate with feature add and commit the changes

    +
    +
    +
    $ git feature status
    +    Unstaged changes:
    +        src/main.py [add]
    +        src/operations.py [subtract, add]
    +        src/utils.py [core]
    +
    +$ git feature add "core" --file src/utils.py
    +    Feature 'core' associated with staged changes.
    +
    +$ git feature status
    +    Unstaged changes:
    +        src/main.py [add]
    +        src/operations.py [subtract, add]
    +
    +    Staged changes:
    +        src/utils.py [core]
    +
    +$ git commit
    +    [main abc1234] Implemented new functionality for core
    +    Feature: core
    +    1 file changed, 50 insertions(+), 10 deletions(-)
    +
    +
    +
    +
  • +
  • +

    Assign feature core to an already existing commit

    +
    +
    +
    $ git log -n 1 --oneline
    +    abc1234 (HEAD -> main) Implemented new functionality for core
    +
    +$ git feature commit "core" abc1234
    +
    +
    +
    +
  • +
  • +

    You find an odd code snippet. Find out the author and which feature it's associated with.

    +
    +
    +
    $ git feature blame --line 3-4
    +    op-select, core (abcd123 Jane Doe 2024-09-10 11) #weird code
    +    op-select, core (abcd123 Jane Doe 2024-09-10 12) #more nonsense
    +
    +
    +
    +
  • +
+
+
+
+
+

GIT

+
+
+

A Git extension built by Tabea Röthemeyer and improved by Kim Nguyen in collaboration with the Software + Engineering Faculty of the Ruhr-Universität Bochum.

+
+
+
+
+ + + \ No newline at end of file diff --git a/git_tool/hooks/post-commit b/git_tool/hooks/post-commit index 468ef09..cc00aae 100755 --- a/git_tool/hooks/post-commit +++ b/git_tool/hooks/post-commit @@ -4,7 +4,7 @@ echo "Running feature-post-commit" COMMIT_ID=$(git rev-parse HEAD) -git feature-commit $COMMIT_ID +git feature commit $COMMIT_ID if [ $? -ne 0 ]; then echo "Error: git feature-commit failed." diff --git a/git_tool/hooks/pre-commit b/git_tool/hooks/pre-commit index 3220412..3e84af9 100755 --- a/git_tool/hooks/pre-commit +++ b/git_tool/hooks/pre-commit @@ -1,7 +1,7 @@ #!/bin/bash # Pre-Commit Hook: Assert that feature changes were staged and the script does not find reasons to not continue echo "Running feature-pre-commit" -git feature-pre-commit +git feature pre-commit if [ $? -ne 0 ]; then echo "Error: Pre-commit checks failed." diff --git a/git_tool/hooks/prepare-commit-msg b/git_tool/hooks/prepare-commit-msg index 0129d24..71cd664 100755 --- a/git_tool/hooks/prepare-commit-msg +++ b/git_tool/hooks/prepare-commit-msg @@ -5,7 +5,7 @@ COMMIT_MSG_FILE=$1 COMMIT_SOURCE=$2 SHA1=$3 -FEATURE_MSG=$(git feature-commit-msg) +FEATURE_MSG=$(git feature commit-msg) if [ $? -ne 0 ]; then echo "Error: Failed to generate feature commit message." diff --git a/git_tool/scripts_for_experiment/set_hooks_path.py b/git_tool/scripts_for_experiment/set_hooks_path.py new file mode 100644 index 0000000..dbd3ea5 --- /dev/null +++ b/git_tool/scripts_for_experiment/set_hooks_path.py @@ -0,0 +1,42 @@ +import subprocess +import os +import sys +import git_tool + +''' + This script sets the hooks path for git. + It has been added to the pyproject.toml file as "feature-init-hooks" + Originally, the user had to set the hooks path manually, but not anymore. +''' + +def main(): + try: + # Step 1: Get the path to the installed git_tool package + package_path = os.path.abspath(git_tool.__file__) + print(f"Located git_tool at: {package_path}") + + # Step 2: Strip __init__.py and append 'hook' + base_path = os.path.dirname(package_path) + hook_path = os.path.join(base_path, "hooks") + print(f"Using hooks directory: {hook_path}") + + # Step 3: Set git hooksPath + subprocess.run(["git", "config", "core.hooksPath", hook_path], check=True) + print("Git hooks path set successfully.") + + # Step 4: Check the current hooks path + result = subprocess.run( + ["git", "rev-parse", "--git-path", "hooks"], + check=True, + stdout=subprocess.PIPE, + text=True + ) + current_hook_path = result.stdout.strip() + print(f"Git is now using hooks from: {current_hook_path}") + + except subprocess.CalledProcessError as e: + print("Git command failed:", e) + sys.exit(1) + except Exception as e: + print("Something went wrong:", e) + sys.exit(1) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index af19bd0..42e177b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [project] name = "git_tool" -authors = [{ name = "Tabea Röthemeyer", email = "tabea.roethemeyer@rub.de" }] +authors = [{ name = "Tabea Röthemeyer", email = "tabea.roethemeyer@rub.de" }, { name = "Kim Nguyen", email = "ho.nguyen@rub.de"}] license = { file = "LICENSE" } description = "Support feature-oriented development workflows with git" readme = "README.md" -version = "1.0.10" +version = "1.0.11" requires-python = ">=3.10" classifiers = [ "Programming Language :: Python :: 3", @@ -14,6 +14,7 @@ classifiers = [ ] dependencies = [ "typer>=0.12,<0.13", + "click <8.1.0", "pydantic[email]>=2.8,<3.0", "GitPython>=3.1,<4.0", "python-dotenv>=1.0,<2.0", @@ -21,14 +22,8 @@ dependencies = [ ] [project.scripts] -git-feature-status = "git_tool.ci.subcommands.feature_status:app" -git-feature-info = "git_tool.ci.subcommands.feature_info:app" -git-feature-add = "git_tool.ci.subcommands.feature_add:app" -git-feature-blame = "git_tool.ci.subcommands.feature_blame:app" -git-feature-commit = "git_tool.ci.subcommands.feature_commit:app" -git-feature-commit-msg = "git_tool.ci.subcommands.feature_commit_msg:app" -git-feature-pre-commit = "git_tool.ci.subcommands.feature_pre_commit:app" -git-feature-commits = "git_tool.ci.subcommands.feature_commits:app" +git-feature = "git_tool.__main__.py:app" +feature-init-hooks = "git_tool.scripts_for_experiment.set_hooks_path:main" [build-system] requires = ["hatchling"]