Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions submissions/git-narrate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.pyc
*.pyd
*.pyo
*.egg-info/

# Distribution / packaging
.Python
build/
dist/
wheels/
*.egg-info/
.tox/
.venv/
venv/
env/

# Editors
.vscode/
.idea/

# OS
.DS_Store
.Trashes
Thumbs.db

# Logs and data
*.log
*.sqlite3
*.db

# AI related
.env
history.html
timeline.png
contributors.png
21 changes: 21 additions & 0 deletions submissions/git-narrate/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Sithum Sathsara Rajapakshe | 000x

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
76 changes: 76 additions & 0 deletions submissions/git-narrate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Git-Narrate User Guide 📖

Welcome to Git-Narrate! This guide will help you get started with turning your project's history into an exciting story.

## What is Git-Narrate?

Imagine your project is like a movie, and every change you make is a scene. Git-Narrate is like a movie director that watches all these scenes and creates a story about how your project was made. It looks at your project's `git` history (the log of all your changes) and writes a narrative about it.

## Key Features

* **Comprehensive Repository Analysis**: Git-Narrate delves into your Git repository to extract detailed information about commits, branches, tags, and contributors.
* **AI-Powered Storytelling**: Leverage the power of AI to transform raw Git data into a rich, engaging, and accurate narrative of your project's development journey.
* **Flexible Output Formats**: Generate your project's story in Markdown, HTML, or plain text, suitable for various uses like documentation, web display, or simple readability.
* **Visual Insights**: Create insightful visualizations, including a commit activity timeline and a contributor activity chart, to better understand your project's evolution and team contributions.
* **Interactive Command-Line Interface**: A user-friendly CLI guides you through the process with clear prompts for repository path, output preferences, and visualization options.

## Getting Started

### 1. Installation

To use Git-Narrate, you first need to install it on your computer. Open your terminal or command prompt and type the following command:

```bash
pip install git-narrate
```

This will download and install Git-Narrate so you can use it from anywhere on your computer.

### 2. Running Git-Narrate

Once installed, you can run Git-Narrate on any of your projects that use `git`.

1. **Navigate to your project folder:**
Open your terminal and go to the folder of the project you want to analyze. For example:
```bash
cd /path/to/your/project
```

2. **Run the command:**
Now, simply run the `git-narrate` command:
```bash
git-narrate
```
The application will then guide you through the process by asking for the following inputs:
* **Path to your Git repository**: You can enter the path to your repository (e.g., `/path/to/your/project`) or simply press Enter to use the current directory (`.`).
* **Output format**: Choose between Markdown, HTML, or plain text for your story.
* **Output file path**: Specify where you want to save the generated story file.
* **Generate visualization charts**: Confirm if you want to create `timeline.png` and `contributors.png` charts.

After you provide these inputs, Git-Narrate will generate the story and any requested visualizations.

## Fun Things You Can Do

Git-Narrate will prompt you for your preferences, allowing you to:

* **Choose Output Format**: Select `html` to generate a story that looks like a webpage (e.g., `git_story.html`).
* **Generate Visualizations**: Opt to create `timeline.png` (commit activity over time) and `contributors.png` (contributor activity) charts.


## For Developers: A Quick Look Under the Hood

If you're a developer and want to contribute to Git-Narrate, here's a quick overview of how it works:

* **`analyzer.py`**: This is the heart of the tool. It uses `GitPython` to read the `.git` folder and extract all the data about commits, branches, tags, and contributors.
* **`narrator.py`**: This module takes the data from the analyzer and turns it into a story. It has different functions to create Markdown, HTML, or plain text stories.
* **`ai_narrator.py`**: This module sends the project data to the Z.ai API and gets back a more detailed story.
* **`visualizer.py`**: This module uses `matplotlib` to create the timeline and contributor charts.
* **`cli.py`**: This file defines the command-line interface using `click`, so you can run `git-narrate` with different options.

### Contributing

We welcome contributions! If you want to help make Git-Narrate even better, please check out our [Contributing Guide](https://github.com/000xs/Git-Narrate/blob/main/CONTRIBUTING.md).

### License

Git-Narrate is licensed under the MIT License. You can find more details in the [LICENSE](https://github.com/000xs/Git-Narrate/blob/main/LICENSE) file.
9 changes: 9 additions & 0 deletions submissions/git-narrate/git_narrate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
Git-Narrate: The Repository Storyteller

A tool that analyzes a git repository and generates a human-readable story of its development.
"""

__version__ = "1.0.3"
__author__ = "Sithum Sathsara Rajapakshe | 000x"
__email__ = "SITHUMSS9122@gmail.com"
149 changes: 149 additions & 0 deletions submissions/git-narrate/git_narrate/analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import git
from datetime import datetime
from typing import List, Dict, Any
from pathlib import Path
from collections import defaultdict

class RepoAnalyzer:
def __init__(self, repo_path: Path):
self.repo_path = repo_path
self.repo = git.Repo(repo_path)

def analyze(self) -> Dict[str, Any]:
"""Perform complete repository analysis."""
return {
"commits": self._get_commits(),
"branches": self._get_branches(),
"tags": self._get_tags(),
"contributors": self._get_contributors(),
"repo_name": self._get_project_name_from_readme() or self.repo_path.name,
"readme_content": self._get_readme_content()
}

def _get_commits(self) -> List[Dict[str, Any]]:
"""Extract commit history."""
commits = []
for commit in self.repo.iter_commits("--all"):
changed_files = list(commit.stats.files.keys())

# Get full content of changed files
file_contents = {}
for file_path in changed_files:
try:
file_contents[file_path] = self.repo.git.show(f"{commit.hexsha}:{file_path}")
except git.exc.GitCommandError:
# This can happen for deleted files, etc.
file_contents[file_path] = None

commits.append({
"sha": commit.hexsha,
"author": commit.author.name,
"email": commit.author.email,
"date": datetime.fromtimestamp(commit.committed_date),
"message": commit.message.strip(),
"files_changed": len(changed_files),
"insertions": commit.stats.total["insertions"],
"deletions": commit.stats.total["deletions"],
"is_merge": len(commit.parents) > 1,
"file_contents": file_contents,
"category": self._categorize_commit(commit.message.strip())
})
return commits

def _categorize_commit(self, message: str) -> str:
"""Categorize commit based on its message."""
message = message.lower()
if message.startswith("feat"):
return "feature"
if message.startswith("fix"):
return "fix"
if message.startswith("docs"):
return "documentation"
if message.startswith("style"):
return "style"
if message.startswith("refactor"):
return "refactor"
if message.startswith("test"):
return "test"
if message.startswith("chore"):
return "chore"
return "other"

def _get_branches(self) -> List[Dict[str, Any]]:
"""Get branch information."""
branches = []
for branch in self.repo.branches:
branches.append({
"name": branch.name,
"commit": branch.commit.hexsha,
"is_remote": branch.is_remote
})
return branches

def _get_tags(self) -> List[Dict[str, Any]]:
"""Get tag information."""
tags = []
for tag in self.repo.tags:
tags.append({
"name": tag.name,
"commit": tag.commit.hexsha,
"date": datetime.fromtimestamp(tag.tag.tagged_date) if tag.tag else None
})
return tags

def _get_contributors(self) -> Dict[str, Dict[str, Any]]:
"""Get contributor statistics."""
contributors = defaultdict(lambda: {
"commits": 0,
"insertions": 0,
"deletions": 0,
"first_commit": None,
"last_commit": None
})

for commit in self._get_commits():
author = commit["author"]
contributors[author]["commits"] += 1
contributors[author]["insertions"] += commit["insertions"]
contributors[author]["deletions"] += commit["deletions"]

if not contributors[author]["first_commit"] or commit["date"] < contributors[author]["first_commit"]:
contributors[author]["first_commit"] = commit["date"]
if not contributors[author]["last_commit"] or commit["date"] > contributors[author]["last_commit"]:
contributors[author]["last_commit"] = commit["date"]

return dict(contributors)

def _get_readme_content(self) -> str:
"""Extract content from README file if it exists."""
readme_paths = [
self.repo_path / "README.md",
self.repo_path / "README.rst",
self.repo_path / "README.txt",
self.repo_path / "readme.md",
self.repo_path / "readme.rst",
self.repo_path / "readme.txt"
]

for path in readme_paths:
if path.exists() and path.is_file():
try:
with open(path, 'r', encoding='utf-8') as f:
return f.read()
except Exception:
continue

return ""

def _get_project_name_from_readme(self) -> str:
"""Extract project name from the first H1 heading in README.md."""
readme = self._get_readme_content()
if not readme:
return ""

lines = readme.split('\n')
for line in lines:
stripped_line = line.strip()
if stripped_line.startswith('# '):
return stripped_line.lstrip('# ').strip()
return ""
84 changes: 84 additions & 0 deletions submissions/git-narrate/git_narrate/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import click
import questionary
from pathlib import Path
from rich.console import Console
from rich.status import Status
import git # Import git for exception handling
from .analyzer import RepoAnalyzer
from .narrator import RepoNarrator
from .visualizer import create_html_story
import pyfiglet

console = Console(force_terminal=True)

@click.command()
def main():
"""Generate a human-readable story of a git repository's development."""

# Display ASCII art welcome
ascii_art = pyfiglet.figlet_format("Git-Narrate")
console.print(f"[bold green]{ascii_art}[/bold green]")
console.print("[bold green]Welcome to Git-Narrate! Let's create your project's story.[/bold green]")

repo_path_str = console.input(
"[bold cyan]Enter the path to your Git repository('/path/repo')[/bold cyan] [default: .]: "
) or "."
repo_path = Path(repo_path_str)

output_format = questionary.select(
"Choose output format:",
choices=["markdown", "html", "text"],
default="html"
).ask()

output_extension = "md" if output_format == "markdown" else "html" if output_format == "html" else "txt" if output_format == "text" else "txt"
output_default = repo_path / "git_story"
output_str = questionary.text(
f"Enter output file path(/path/[filename])",
default=str(output_default)
).ask()
output_path = Path(f"{output_str}.{output_extension}")

# Analyze repository
console.print("[bold blue]Please be patient while the repository content is being examined.[/bold blue]")
try:
with click.progressbar(
length=100,
label=f"Analyzing repository at {repo_path}...",
show_percent=False,
show_eta=False,
bar_template="%(label)s %(bar)s | %(info)s",
fill_char=click.style("█", fg="green"),
empty_char=" ",
) as bar:
analyzer = RepoAnalyzer(repo_path)
repo_data = analyzer.analyze()
bar.update(100) # Complete the progress bar once analysis is done
except git.exc.NoSuchPathError:
console.print(f"[bold red]Error: Repository not found at '{repo_path}'. Please ensure the path is correct and it's a valid Git repository.[/bold red]")
return
except Exception as e:
console.print(f"[bold red]An unexpected error occurred during repository analysis: {e}[/bold red]")
return

# Generate narrative (always AI-powered)
with Status(f"[bold green]Generating AI-powered narrative...", spinner="dots", console=console) as status:
narrator = RepoNarrator(repo_data)
story_md = narrator.generate_story()
status.stop()

# Save narrative
try:
if output_format == 'html':
create_html_story(story_md, output_path, repo_data["repo_name"])
else:
with open(output_path, "w", encoding="utf-8") as f:
f.write(story_md)
console.print(f"[bold green]Narrative saved to {output_path}[/bold green]")
except PermissionError:
console.print(f"[bold red]Error: Permission denied to write to '{output_path}'. Please check file permissions or choose a different path.[/bold red]")
except Exception as e:
console.print(f"[bold red]An unexpected error occurred: {e}[/bold red]")

if __name__ == "__main__":
main()
Loading