Build a Realistic Git History Generator
Write a Python script that generates 90 days of realistic Git commit history.
Introduction
30 Second Summary
Your Git history tells a story. Every commit, every timestamp, and every file change paints a picture of how a developer works over time. Understanding how that story gets constructed gives you powerful insight into Git's internals.
In this project, you will build a Python script that generates a complete Git repository with 90 days of realistic commit history, then packages it into a zip file ready to push anywhere.
What You'll Build
A Python script that automates Git operations to create a repository simulating 3 months of sustained development on a task management web app.
By the end of this project, you'll have:
- A Python script that generates 60-120 realistic commits spanning 90 days with natural patterns.
- A generated Git repository with real, incremental file changes across HTML, CSS, and JavaScript.
- A zip file containing the finished repo, ready to extract and push to GitHub or GitLab.
- Secret Mission: Add feature branch history with merge commits for extra realism.
Are there any prerequisites?
You should have basic Python knowledge (variables, loops, functions, and file I/O). No prior Git internals experience is needed.
Before We Start
Your generator script needs two tools to do its job: Python for the automation logic and Git for creating commits with custom timestamps.
In this step, you'll confirm both tools are ready, create your project folder, and verify Python can talk to Git through the subprocess module.
In this step, get ready to:
- Verify Python and Git are installed and configured.
- Create the project folder and script file.
- Confirm Python can invoke Git commands.
Verify Python and Git installations
The script relies on Python 3.8+ and Git 2.x. Both must be accessible from your terminal for the automation to work.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check your Python version by running this command:
python --version
✔️ I see a version number
You should see something like Python 3.11.4. Any version 3.8 or above works.
ⓧ Command not found
Python is not installed or not in your system PATH.
- Download Python from python.org/downloads.
- During installation, check the box that says Add Python to PATH.
- After installing, close and reopen your terminal, then run python --version again.
- On macOS, you may need to use python3 --version instead.
- Check your Git version by running this command:
git --version
✔️ I see a version number
You should see something like git version 2.43.0. Any modern Git version works.
ⓧ Command not found
Git is not installed yet.
- Download Git from git-scm.com/downloads.
- Install with the default options.
- Close and reopen your terminal, then run git --version again.
- Verify Git knows your identity by running these commands:
git config --global user.name
git config --global user.email
Both commands should print a value. If either returns blank, configure them now.
✔️ I see my name and email
Git is configured and ready to go.
ⓧ One or both are blank
- Set your Git name by running this command (replace with your own name):
git config --global user.name [[GITNAME="Your Name"]]
- Set your Git email by running this command (replace with your own email):
git config --global user.email [[GITEMAIL="your.email@example.com"]]
Why does Git need my name and email?
Every Git commit records who made it. The generated repository uses its own author name ("Alex Chen") for the fake history, but Git requires a global config to be present before it will create any commits.
Create the project folder and script file
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir git-history-generator
cd git-history-generator
- Create the script file by running this command:
macOS
touch generate_history.py
Windows
echo. > generate_history.py
- Confirm the file exists by running:
ls
You should see generate_history.py in the output.
- Open the git-history-generator folder in your text editor.
Open from terminal
If you use VS Code, run code . from inside the folder to open it directly.
Folder not showing up?
Make sure you ran cd ~/Desktop first. Without it, the folder could be anywhere your terminal last opened to.
help me find my git-history-generator folder
Test that Python can call Git
The entire generator works by calling Git commands from Python using the subprocess module. A quick test confirms the two tools can communicate.
- Open generate_history.py in your text editor and paste the following test code:
import subprocess
# Run 'git --version' and capture the output
result = subprocess.run(["git", "--version"], capture_output=True, text=True)
print(result.stdout)
What does this code do?
subprocess.run() launches an external program from Python. The capture_output=True argument tells Python to collect whatever Git prints instead of letting it pass straight to the terminal.
text=True converts the output from raw bytes to a readable string. This is the same pattern the full generator script will use for every Git operation.
- Save generate_history.py.
- Run the script by executing this command in your terminal:
python generate_history.py
You should see your Git version printed, like git version 2.43.0.
Seeing an error instead?
- If you see FileNotFoundError, Git is not in your system PATH. Close your terminal, reopen it, and try again.
- On macOS, try python3 generate_history.py if python alone fails.
- Make sure your terminal is inside the git-history-generator folder where your script lives.
help me debug why Python can't call Git
- Now that the test passed, clear all the code from generate_history.py so the file is empty.
- Save the empty file.
The test code was only to confirm the connection works. You'll write the real script starting in the next step.
Your environment is locked in. Next up, you'll write the configuration and schedule generator that decides when each commit happens across 90 days.
Set Up Your Environment
Before you can generate a fake Git history, you need two tools working together: Python to orchestrate everything, and Git to create the actual commits.
In this step, you'll verify both are installed, create your project folder, and confirm Python can talk to Git programmatically.
In this step, get ready to:
- Verify Python and Git are installed and configured.
- Create your project folder and script file.
- Confirm Python can invoke Git commands via subprocess.
Verify Python and Git installations
Your generator script relies on Python 3.8+ for f-strings and pathlib, and Git for creating commits.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check your Python version by running this command:
python --version
✔️ I see Python 3.8 or higher
You're all set with Python.
ⓧ Command not found or version is below 3.8
- Download Python from python.org/downloads.
- During installation, check the box that says Add Python to PATH.
- After installation, close and reopen your terminal, then run python --version again.
- Check your Git version by running this command:
git --version
✔️ I see a version number
Git is ready to go.
ⓧ Command not found
- Download Git from git-scm.com/downloads.
- Install with the default options.
- Close and reopen your terminal, then run git --version again.
- Verify Git knows your identity by running these commands:
git config --global user.name
git config --global user.email
You should see your name and email printed back.
Seeing blank output?
If either command returns nothing, Git doesn't have your identity configured yet. Run these commands to set them (replace with your own name and email):
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
help me configure Git user.name and user.email
Create the project folder and script file
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir git-history-generator
cd git-history-generator
- Create the script file by running this command:
echo. > generate_history.py
- Confirm the file exists by running:
ls
You should see generate_history.py in the output.
- Open the git-history-generator folder in your text editor.
Open from the terminal
If you use VS Code, you can run code . to open the current folder directly.
Test that Python can call Git
Your script will control Git entirely through Python's subprocess module. This lets Python run terminal commands and capture their output.
- In your text editor, open generate_history.py and add this quick test:
import subprocess
result = subprocess.run(["git", "--version"], capture_output=True, text=True)
print(result.stdout)
- Save generate_history.py.
- Run the test by executing this command in your terminal:
python generate_history.py
You should see your Git version printed (e.g. git version 2.43.0).
What does this test prove?
If Python can run git --version and capture the output, it can run any Git command. Your entire generator script will use this same pattern to create commits.
Seeing FileNotFoundError?
This means Python cannot find the git command. Make sure Git is installed and added to your system PATH. Close and reopen your terminal after installing Git.
help me fix FileNotFoundError when running git from Python subprocess
Now that the test passes, it's time to replace that test code with the real starting point for your script.
- Delete everything in generate_history.py and replace it with the following imports and configuration:
import os
import subprocess
import random
import shutil
import zipfile
from datetime import datetime, timedelta
from pathlib import Path
# === CONFIGURATION ===
REPO_NAME = "taskflow"
OUTPUT_ZIP = "taskflow-repo.zip"
DAYS_OF_HISTORY = 90
AUTHOR_NAME = "Alex Chen"
AUTHOR_EMAIL = "alex.chen@example.com"
WEEKDAY_COMMIT_CHANCE = 0.85
WEEKEND_COMMIT_CHANCE = 0.30
WEEKDAY_COMMITS = (1, 5)
WEEKEND_COMMITS = (1, 2)
WORK_START_HOUR = 9
WORK_END_HOUR = 19
What does this configuration control?
These constants define how realistic your generated history will look.
- REPO_NAME and OUTPUT_ZIP set the output folder and zip file names.
- WEEKDAY_COMMIT_CHANCE (85%) vs WEEKEND_COMMIT_CHANCE (30%) creates a natural weekday-heavy pattern.
- WEEKDAY_COMMITS allows 1-5 commits per day, while weekends cap at 1-2.
- WORK_START_HOUR and WORK_END_HOUR ensure commits only appear during 9am-7pm.
- Save generate_history.py.
✔️ Awesome, I've got everything!
Double check you've saved your file.
ⓧ I'd like to double check the full code
import os
import subprocess
import random
import shutil
import zipfile
from datetime import datetime, timedelta
from pathlib import Path
# === CONFIGURATION ===
REPO_NAME = "taskflow"
OUTPUT_ZIP = "taskflow-repo.zip"
DAYS_OF_HISTORY = 90
AUTHOR_NAME = "Alex Chen"
AUTHOR_EMAIL = "alex.chen@example.com"
WEEKDAY_COMMIT_CHANCE = 0.85
WEEKEND_COMMIT_CHANCE = 0.30
WEEKDAY_COMMITS = (1, 5)
WEEKEND_COMMITS = (1, 2)
WORK_START_HOUR = 9
WORK_END_HOUR = 19
Your environment is ready and your script has its configuration backbone. Next up, you'll use these constants to generate a realistic schedule of commit timestamps.
Build the Commit Schedule Generator
Your configuration constants tell the script what the generated history should look like. Now you need to tell it what content to commit and when to commit it.
A convincing commit history needs two things: a realistic timeline of commit timestamps and a believable evolution of project files. In this step, you'll define the fake project's development phases and build a function that generates a natural-looking commit schedule spread across 90 days.
In this step, get ready to:
- Define 6 project evolution phases with real HTML, CSS, and JavaScript content.
- Create a list of filler commit messages for between-phase commits.
- Write a function that generates 60-120 realistic commit timestamps.
Define the project evolution phases
Each commit in a real project changes actual files. Your generator simulates this by defining 6 "phases" of a fake task management app called TaskFlow. Each phase represents a stage of development and contains the complete file contents at that point.
- In generate_history.py, find the last configuration line (WORK_END_HOUR = 19) and add the following code below it:
# === PROJECT EVOLUTION PHASES ===
PHASES = [
{
"name": "initial-setup",
"weight": 0.05,
"files": {
"README.md": "# TaskFlow\n\nA simple task management app.\n",
"index.html": """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskFlow</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>TaskFlow</h1>
<div id="app"></div>
<script src="app.js"></script>
</body>
</html>""",
"style.css": """* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
padding: 2rem;
}
h1 {
color: #333;
margin-bottom: 1rem;
}""",
"app.js": "// TaskFlow App\nconsole.log('TaskFlow initialized');\n",
},
"messages": [
"Initial commit",
"feat: add project structure",
"feat: add basic HTML skeleton",
"style: add base CSS reset",
]
},
{
"name": "core-features",
"weight": 0.25,
"files": {
"app.js": """// TaskFlow App
const app = document.getElementById('app');
let tasks = [];
function createTask(title) {
const task = {
id: Date.now(),
title: title,
completed: false,
createdAt: new Date().toISOString()
};
tasks.push(task);
render();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
render();
}
function toggleTask(id) {
tasks = tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
render();
}
function render() {
app.innerHTML = `
<form id="task-form">
<input type="text" id="task-input" placeholder="Add a new task..." />
<button type="submit">Add</button>
</form>
<ul id="task-list">
${tasks.map(t => `
<li class="${t.completed ? 'completed' : ''}">
<input type="checkbox" ${t.completed ? 'checked' : ''}
onchange="toggleTask(${t.id})" />
<span>${t.title}</span>
<button onclick="deleteTask(${t.id})">Delete</button>
</li>
`).join('')}
</ul>
`;
document.getElementById('task-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('task-input');
if (input.value.trim()) {
createTask(input.value.trim());
input.value = '';
}
});
}
render();
""",
"index.html": """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskFlow</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>TaskFlow</h1>
<p class="subtitle">Stay organized, stay productive</p>
<div id="app"></div>
</div>
<script src="app.js"></script>
</body>
</html>""",
"style.css": """* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f0f2f5;
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 600px;
margin: 0 auto;
}
h1 {
color: #1a1a2e;
font-size: 2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: #666;
margin-bottom: 1.5rem;
}
#task-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
#task-form input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
#task-form input:focus {
outline: none;
border-color: #4a90d9;
}
#task-form button {
padding: 0.75rem 1.5rem;
background: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
#task-form button:hover {
background: #357abd;
}
#task-list {
list-style: none;
}
#task-list li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: white;
border-radius: 8px;
margin-bottom: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: transform 0.1s;
}
#task-list li:hover {
transform: translateX(4px);
}
#task-list li.completed span {
text-decoration: line-through;
color: #999;
}
#task-list li span {
flex: 1;
}
#task-list li button {
padding: 0.25rem 0.75rem;
background: #ff4757;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
#task-list li button:hover {
background: #e84040;
}""",
},
"messages": [
"feat: implement task creation",
"feat: add task deletion",
"feat: add toggle completion",
"feat: implement render function",
"style: add form styling",
"style: add task list styles",
"fix: prevent empty task submission",
"refactor: extract task operations",
"style: add hover effects on tasks",
"feat: add subtitle to header",
]
},
{
"name": "local-storage",
"weight": 0.40,
"files": {
"app.js": """// TaskFlow App
const app = document.getElementById('app');
const STORAGE_KEY = 'taskflow-tasks';
let tasks = loadTasks();
function loadTasks() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('Failed to load tasks:', e);
return [];
}
}
function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
function createTask(title) {
const task = {
id: Date.now(),
title: title,
completed: false,
createdAt: new Date().toISOString()
};
tasks.push(task);
saveTasks();
render();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
saveTasks();
render();
}
function toggleTask(id) {
tasks = tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
saveTasks();
render();
}
function getTaskCount() {
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
return { total, completed, remaining: total - completed };
}
function render() {
const count = getTaskCount();
app.innerHTML = `
<form id="task-form">
<input type="text" id="task-input" placeholder="Add a new task..." />
<button type="submit">Add</button>
</form>
<div class="task-stats">
<span>${count.remaining} remaining</span>
<span>${count.completed} completed</span>
</div>
<ul id="task-list">
${tasks.map(t => `
<li class="${t.completed ? 'completed' : ''}" data-id="${t.id}">
<input type="checkbox" ${t.completed ? 'checked' : ''}
onchange="toggleTask(${t.id})" />
<span>${t.title}</span>
<button onclick="deleteTask(${t.id})">Delete</button>
</li>
`).join('')}
</ul>
`;
document.getElementById('task-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('task-input');
if (input.value.trim()) {
createTask(input.value.trim());
input.value = '';
}
});
}
render();
""",
"README.md": """# TaskFlow
A simple, clean task management app built with vanilla JavaScript.
## Features
- Create, complete, and delete tasks
- Persistent storage using localStorage
- Task completion tracking
- Clean, responsive UI
## Getting Started
Open `index.html` in your browser. No build step required.
## Tech Stack
- HTML5
- CSS3
- Vanilla JavaScript
- localStorage for persistence
""",
},
"messages": [
"feat: add localStorage persistence",
"feat: load tasks from storage on startup",
"fix: handle corrupted localStorage data",
"feat: add task count statistics",
"refactor: extract storage logic",
"docs: update README with features",
"fix: save tasks after toggle",
"feat: show remaining vs completed count",
]
},
{
"name": "filtering-and-polish",
"weight": 0.60,
"files": {
"app.js": """// TaskFlow App
const app = document.getElementById('app');
const STORAGE_KEY = 'taskflow-tasks';
let tasks = loadTasks();
let currentFilter = 'all';
function loadTasks() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('Failed to load tasks:', e);
return [];
}
}
function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
function createTask(title) {
const task = {
id: Date.now(),
title: title,
completed: false,
createdAt: new Date().toISOString()
};
tasks.push(task);
saveTasks();
render();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
saveTasks();
render();
}
function toggleTask(id) {
tasks = tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
saveTasks();
render();
}
function setFilter(filter) {
currentFilter = filter;
render();
}
function getFilteredTasks() {
switch (currentFilter) {
case 'active':
return tasks.filter(t => !t.completed);
case 'completed':
return tasks.filter(t => t.completed);
default:
return tasks;
}
}
function clearCompleted() {
tasks = tasks.filter(t => !t.completed);
saveTasks();
render();
}
function getTaskCount() {
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
return { total, completed, remaining: total - completed };
}
function render() {
const count = getTaskCount();
const filtered = getFilteredTasks();
app.innerHTML = `
<form id="task-form">
<input type="text" id="task-input" placeholder="Add a new task..." />
<button type="submit">Add</button>
</form>
<div class="filters">
<button class="${currentFilter === 'all' ? 'active' : ''}"
onclick="setFilter('all')">All (${count.total})</button>
<button class="${currentFilter === 'active' ? 'active' : ''}"
onclick="setFilter('active')">Active (${count.remaining})</button>
<button class="${currentFilter === 'completed' ? 'active' : ''}"
onclick="setFilter('completed')">Completed (${count.completed})</button>
</div>
<ul id="task-list">
${filtered.map(t => `
<li class="${t.completed ? 'completed' : ''}" data-id="${t.id}">
<input type="checkbox" ${t.completed ? 'checked' : ''}
onchange="toggleTask(${t.id})" />
<span>${t.title}</span>
<button onclick="deleteTask(${t.id})">Delete</button>
</li>
`).join('')}
</ul>
${count.completed > 0 ? `
<button class="clear-btn" onclick="clearCompleted()">
Clear completed (${count.completed})
</button>
` : ''}
`;
document.getElementById('task-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('task-input');
if (input.value.trim()) {
createTask(input.value.trim());
input.value = '';
}
});
}
render();
""",
"style.css": """* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f0f2f5;
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 600px;
margin: 0 auto;
}
h1 {
color: #1a1a2e;
font-size: 2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: #666;
margin-bottom: 1.5rem;
}
#task-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
#task-form input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
#task-form input:focus {
outline: none;
border-color: #4a90d9;
}
#task-form button {
padding: 0.75rem 1.5rem;
background: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
#task-form button:hover {
background: #357abd;
}
.filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.filters button {
padding: 0.5rem 1rem;
background: white;
border: 1px solid #ddd;
border-radius: 20px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.filters button:hover {
border-color: #4a90d9;
color: #4a90d9;
}
.filters button.active {
background: #4a90d9;
color: white;
border-color: #4a90d9;
}
#task-list {
list-style: none;
}
#task-list li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: white;
border-radius: 8px;
margin-bottom: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: transform 0.1s, box-shadow 0.2s;
}
#task-list li:hover {
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
}
#task-list li.completed span {
text-decoration: line-through;
color: #999;
}
#task-list li span {
flex: 1;
}
#task-list li button {
padding: 0.25rem 0.75rem;
background: #ff4757;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
opacity: 0;
transition: opacity 0.2s;
}
#task-list li:hover button {
opacity: 1;
}
#task-list li button:hover {
background: #e84040;
}
.clear-btn {
display: block;
margin-top: 1rem;
padding: 0.5rem 1rem;
background: none;
border: 1px solid #ff4757;
color: #ff4757;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.clear-btn:hover {
background: #ff4757;
color: white;
}""",
},
"messages": [
"feat: add task filtering (all/active/completed)",
"feat: add clear completed button",
"style: add filter button styles",
"style: show delete button only on hover",
"refactor: extract getFilteredTasks helper",
"fix: update filter counts on change",
"style: add pill-style filter buttons",
"style: improve hover shadow on tasks",
]
},
{
"name": "accessibility-and-docs",
"weight": 0.80,
"files": {
"index.html": """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskFlow - Task Management</title>
<meta name="description" content="A simple, clean task management app">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>TaskFlow</h1>
<p class="subtitle">Stay organized, stay productive</p>
</header>
<main id="app" role="main" aria-label="Task management"></main>
</div>
<script src="app.js"></script>
</body>
</html>
""",
"CONTRIBUTING.md": """# Contributing to TaskFlow
Thanks for your interest in contributing!
## Development Setup
1. Clone the repository
2. Open `index.html` in your browser
3. Make changes and refresh to test
## Guidelines
- Keep it simple - no build tools required
- Test in multiple browsers
- Follow existing code style
- Write descriptive commit messages
## Reporting Issues
Open an issue with:
- Browser and version
- Steps to reproduce
- Expected vs actual behavior
""",
".gitignore": """.DS_Store
Thumbs.db
*.log
node_modules/
.vscode/
.idea/
""",
},
"messages": [
"feat: add ARIA labels for accessibility",
"feat: add meta description",
"docs: add CONTRIBUTING.md",
"chore: add .gitignore",
"refactor: use semantic HTML elements",
"fix: improve screen reader support",
"docs: update page title",
]
},
{
"name": "final-polish",
"weight": 0.95,
"files": {
"README.md": """# TaskFlow
A simple, clean task management app built with vanilla JavaScript. No frameworks, no build step, just open and use.
## Features
- Create, complete, and delete tasks
- Filter by status (all, active, completed)
- Persistent storage using localStorage
- Task completion tracking with stats
- Clear completed tasks in bulk
- Accessible (ARIA labels, semantic HTML)
- Responsive design
## Getting Started
```bash
git clone <your-repo-url>
cd taskflow
# Open index.html in your browser
```
No dependencies. No build step. Just open `index.html`.
## Project Structure
```
taskflow/
index.html - Main HTML page
style.css - All styles
app.js - Application logic
```
## Browser Support
- Chrome 80+
- Firefox 75+
- Safari 13+
- Edge 80+
## License
MIT
""",
"LICENSE": """MIT License
Copyright (c) 2026 TaskFlow
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.
""",
},
"messages": [
"docs: rewrite README with full documentation",
"chore: add MIT license",
"docs: add browser support info",
"docs: add project structure to README",
"fix: minor typo in README",
]
},
]
What does this code do?
The PHASES list defines 6 stages of the fake TaskFlow app's development. Each phase has three key parts:
- weight controls when in the timeline this phase appears (0.05 = first 5%, 0.95 = last 5%).
- files contains the complete file contents at that stage, so each commit creates real, meaningful diffs.
- messages provides realistic commit messages using conventional prefixes like feat:, fix:, and style:.
Why real file content instead of empty files?
Tools like fake-git-history only modify a single file per commit. Anyone inspecting the diffs would immediately see the history is fake. Your generator creates actual incremental code evolution across HTML, CSS, and JavaScript files, making the diffs look authentic.
Between the major phase milestones, the generator still needs to make commits. Filler messages cover those gaps with realistic-sounding small changes.
- Add the FILLER_MESSAGES list directly below the closing ] of the PHASES list:
FILLER_MESSAGES = [
"fix: resolve edge case in task toggle",
"refactor: simplify render logic",
"style: adjust spacing on mobile",
"fix: handle special characters in task title",
"refactor: use const where possible",
"style: tweak button border radius",
"fix: prevent duplicate task IDs",
"perf: reduce unnecessary re-renders",
"style: adjust font sizes for readability",
"fix: correct checkbox alignment",
"refactor: clean up event listeners",
"style: improve color contrast",
"fix: handle empty state gracefully",
"chore: remove console.log statements",
"style: add subtle animation on add",
"fix: trim whitespace from input",
"refactor: rename variables for clarity",
"style: darken text for accessibility",
"fix: ensure IDs are unique across sessions",
"perf: debounce localStorage writes",
]
What does this code do?
These 20 filler messages get randomly selected for commits that fall between phase transitions. They use varied conventional commit prefixes (fix:, refactor:, style:, perf:, chore:) to avoid repetitive-looking commit logs.
- Save generate_history.py.
Write the schedule generation function
The schedule function decides exactly when each commit happens. It loops through every day in the 90-day range and uses probability to determine whether commits occur on that day and how many.
- Add the generate_schedule function below the FILLER_MESSAGES list:
def generate_schedule(start_date, end_date):
"""Generate a realistic commit schedule over the date range."""
schedule = []
current = start_date
while current <= end_date:
is_weekday = current.weekday() < 5
chance = WEEKDAY_COMMIT_CHANCE if is_weekday else WEEKEND_COMMIT_CHANCE
if random.random() < chance:
commit_range = WEEKDAY_COMMITS if is_weekday else WEEKEND_COMMITS
num_commits = random.randint(*commit_range)
for _ in range(num_commits):
hour = random.randint(WORK_START_HOUR, WORK_END_HOUR)
minute = random.randint(0, 59)
second = random.randint(0, 59)
commit_time = current.replace(hour=hour, minute=minute, second=second)
schedule.append(commit_time)
current += timedelta(days=1)
schedule.sort()
return schedule
What does this code do?
- current.weekday() < 5 checks if the day is Monday through Friday (Python uses 0=Monday, 4=Friday).
- random.random() < chance gives each weekday an 85% probability of having commits and each weekend day a 30% chance.
- random.randint(*commit_range) unpacks the tuple (1, 5) or (1, 2) to pick a random number of commits for that day.
- current.replace(hour=hour, ...) creates a timestamp on that day during working hours (9am-7pm) with randomized minutes and seconds.
- schedule.sort() ensures all commit times are in chronological order before returning.
- Save generate_history.py.
Seeing a syntax error?
- Make sure there are no extra blank lines or indentation mismatches inside the function.
- Check that the def line starts at column 0 (no leading spaces).
- Verify the FILLER_MESSAGES list above ends with a closing ] before the function starts.
Help me fix a syntax error in my generate_schedule function
Test the schedule output
Before building the rest of the generator, you should verify the schedule function produces a realistic number of commits. A 90-day range with these probability settings should generate roughly 60-120 commits.
- Add the following test code at the very bottom of generate_history.py:
# Temporary test - remove after verifying
end_date = datetime.now()
start_date = end_date - timedelta(days=DAYS_OF_HISTORY)
schedule = generate_schedule(start_date, end_date)
print(f"Generated {len(schedule)} commits over {DAYS_OF_HISTORY} days")
print(f"Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
- Save generate_history.py.
- Run the script by executing this command:
python generate_history.py
- You should see output like Generated 87 commits over 90 days (your exact number will vary since it uses randomness, but it should be between 60 and 120).
Run it a few times
Try running the script 2-3 times. Each run produces a different count because of the random probability checks. If you consistently see numbers way outside the 60-120 range, double-check your WEEKDAY_COMMIT_CHANCE and WEEKEND_COMMIT_CHANCE values.
Seeing 0 commits or a NameError?
- If you see 0 commits, check that WEEKDAY_COMMIT_CHANCE = 0.85 (not 0.0085 or similar).
- If you see NameError: name 'datetime' is not defined, check your imports at the top include from datetime import datetime, timedelta.
- If you see NameError: name 'random' is not defined, confirm import random is at the top of the file.
Help me debug my generate_schedule function output
✔️ Awesome, I've got everything!
Great. Make sure you've saved your file, then delete the temporary test code at the bottom (the 5 lines starting with # Temporary test). You won't need it going forward.
ⓧ I'd like to double check the full code
Here is the complete generate_history.py file as it should look at the end of this step (without the temporary test code):
import os
import subprocess
import random
import shutil
import zipfile
from datetime import datetime, timedelta
from pathlib import Path
# === CONFIGURATION ===
REPO_NAME = "taskflow"
OUTPUT_ZIP = "taskflow-repo.zip"
DAYS_OF_HISTORY = 90
AUTHOR_NAME = "Alex Chen"
AUTHOR_EMAIL = "alex.chen@example.com"
WEEKDAY_COMMIT_CHANCE = 0.85
WEEKEND_COMMIT_CHANCE = 0.30
WEEKDAY_COMMITS = (1, 5)
WEEKEND_COMMITS = (1, 2)
WORK_START_HOUR = 9
WORK_END_HOUR = 19
# === PROJECT EVOLUTION PHASES ===
PHASES = [
{
"name": "initial-setup",
"weight": 0.05,
"files": {
"README.md": "# TaskFlow\n\nA simple task management app.\n",
"index.html": """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskFlow</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>TaskFlow</h1>
<div id="app"></div>
<script src="app.js"></script>
</body>
</html>""",
"style.css": """* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
padding: 2rem;
}
h1 {
color: #333;
margin-bottom: 1rem;
}""",
"app.js": "// TaskFlow App\nconsole.log('TaskFlow initialized');\n",
},
"messages": [
"Initial commit",
"feat: add project structure",
"feat: add basic HTML skeleton",
"style: add base CSS reset",
]
},
{
"name": "core-features",
"weight": 0.25,
"files": {
"app.js": """// TaskFlow App
const app = document.getElementById('app');
let tasks = [];
function createTask(title) {
const task = {
id: Date.now(),
title: title,
completed: false,
createdAt: new Date().toISOString()
};
tasks.push(task);
render();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
render();
}
function toggleTask(id) {
tasks = tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
render();
}
function render() {
app.innerHTML = `
<form id="task-form">
<input type="text" id="task-input" placeholder="Add a new task..." />
<button type="submit">Add</button>
</form>
<ul id="task-list">
${tasks.map(t => `
<li class="${t.completed ? 'completed' : ''}">
<input type="checkbox" ${t.completed ? 'checked' : ''}
onchange="toggleTask(${t.id})" />
<span>${t.title}</span>
<button onclick="deleteTask(${t.id})">Delete</button>
</li>
`).join('')}
</ul>
`;
document.getElementById('task-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('task-input');
if (input.value.trim()) {
createTask(input.value.trim());
input.value = '';
}
});
}
render();
""",
"index.html": """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskFlow</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>TaskFlow</h1>
<p class="subtitle">Stay organized, stay productive</p>
<div id="app"></div>
</div>
<script src="app.js"></script>
</body>
</html>""",
"style.css": """* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f0f2f5;
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 600px;
margin: 0 auto;
}
h1 {
color: #1a1a2e;
font-size: 2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: #666;
margin-bottom: 1.5rem;
}
#task-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
#task-form input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
#task-form input:focus {
outline: none;
border-color: #4a90d9;
}
#task-form button {
padding: 0.75rem 1.5rem;
background: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
#task-form button:hover {
background: #357abd;
}
#task-list {
list-style: none;
}
#task-list li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: white;
border-radius: 8px;
margin-bottom: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: transform 0.1s;
}
#task-list li:hover {
transform: translateX(4px);
}
#task-list li.completed span {
text-decoration: line-through;
color: #999;
}
#task-list li span {
flex: 1;
}
#task-list li button {
padding: 0.25rem 0.75rem;
background: #ff4757;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
#task-list li button:hover {
background: #e84040;
}""",
},
"messages": [
"feat: implement task creation",
"feat: add task deletion",
"feat: add toggle completion",
"feat: implement render function",
"style: add form styling",
"style: add task list styles",
"fix: prevent empty task submission",
"refactor: extract task operations",
"style: add hover effects on tasks",
"feat: add subtitle to header",
]
},
{
"name": "local-storage",
"weight": 0.40,
"files": {
"app.js": """// TaskFlow App
const app = document.getElementById('app');
const STORAGE_KEY = 'taskflow-tasks';
let tasks = loadTasks();
function loadTasks() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('Failed to load tasks:', e);
return [];
}
}
function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
function createTask(title) {
const task = {
id: Date.now(),
title: title,
completed: false,
createdAt: new Date().toISOString()
};
tasks.push(task);
saveTasks();
render();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
saveTasks();
render();
}
function toggleTask(id) {
tasks = tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
saveTasks();
render();
}
function getTaskCount() {
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
return { total, completed, remaining: total - completed };
}
function render() {
const count = getTaskCount();
app.innerHTML = `
<form id="task-form">
<input type="text" id="task-input" placeholder="Add a new task..." />
<button type="submit">Add</button>
</form>
<div class="task-stats">
<span>${count.remaining} remaining</span>
<span>${count.completed} completed</span>
</div>
<ul id="task-list">
${tasks.map(t => `
<li class="${t.completed ? 'completed' : ''}" data-id="${t.id}">
<input type="checkbox" ${t.completed ? 'checked' : ''}
onchange="toggleTask(${t.id})" />
<span>${t.title}</span>
<button onclick="deleteTask(${t.id})">Delete</button>
</li>
`).join('')}
</ul>
`;
document.getElementById('task-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('task-input');
if (input.value.trim()) {
createTask(input.value.trim());
input.value = '';
}
});
}
render();
""",
"README.md": """# TaskFlow
A simple, clean task management app built with vanilla JavaScript.
## Features
- Create, complete, and delete tasks
- Persistent storage using localStorage
- Task completion tracking
- Clean, responsive UI
## Getting Started
Open `index.html` in your browser. No build step required.
## Tech Stack
- HTML5
- CSS3
- Vanilla JavaScript
- localStorage for persistence
""",
},
"messages": [
"feat: add localStorage persistence",
"feat: load tasks from storage on startup",
"fix: handle corrupted localStorage data",
"feat: add task count statistics",
"refactor: extract storage logic",
"docs: update README with features",
"fix: save tasks after toggle",
"feat: show remaining vs completed count",
]
},
{
"name": "filtering-and-polish",
"weight": 0.60,
"files": {
"app.js": """// TaskFlow App
const app = document.getElementById('app');
const STORAGE_KEY = 'taskflow-tasks';
let tasks = loadTasks();
let currentFilter = 'all';
function loadTasks() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('Failed to load tasks:', e);
return [];
}
}
function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
function createTask(title) {
const task = {
id: Date.now(),
title: title,
completed: false,
createdAt: new Date().toISOString()
};
tasks.push(task);
saveTasks();
render();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
saveTasks();
render();
}
function toggleTask(id) {
tasks = tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
saveTasks();
render();
}
function setFilter(filter) {
currentFilter = filter;
render();
}
function getFilteredTasks() {
switch (currentFilter) {
case 'active':
return tasks.filter(t => !t.completed);
case 'completed':
return tasks.filter(t => t.completed);
default:
return tasks;
}
}
function clearCompleted() {
tasks = tasks.filter(t => !t.completed);
saveTasks();
render();
}
function getTaskCount() {
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
return { total, completed, remaining: total - completed };
}
function render() {
const count = getTaskCount();
const filtered = getFilteredTasks();
app.innerHTML = `
<form id="task-form">
<input type="text" id="task-input" placeholder="Add a new task..." />
<button type="submit">Add</button>
</form>
<div class="filters">
<button class="${currentFilter === 'all' ? 'active' : ''}"
onclick="setFilter('all')">All (${count.total})</button>
<button class="${currentFilter === 'active' ? 'active' : ''}"
onclick="setFilter('active')">Active (${count.remaining})</button>
<button class="${currentFilter === 'completed' ? 'active' : ''}"
onclick="setFilter('completed')">Completed (${count.completed})</button>
</div>
<ul id="task-list">
${filtered.map(t => `
<li class="${t.completed ? 'completed' : ''}" data-id="${t.id}">
<input type="checkbox" ${t.completed ? 'checked' : ''}
onchange="toggleTask(${t.id})" />
<span>${t.title}</span>
<button onclick="deleteTask(${t.id})">Delete</button>
</li>
`).join('')}
</ul>
${count.completed > 0 ? `
<button class="clear-btn" onclick="clearCompleted()">
Clear completed (${count.completed})
</button>
` : ''}
`;
document.getElementById('task-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('task-input');
if (input.value.trim()) {
createTask(input.value.trim());
input.value = '';
}
});
}
render();
""",
"style.css": """* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f0f2f5;
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 600px;
margin: 0 auto;
}
h1 {
color: #1a1a2e;
font-size: 2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: #666;
margin-bottom: 1.5rem;
}
#task-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
#task-form input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
#task-form input:focus {
outline: none;
border-color: #4a90d9;
}
#task-form button {
padding: 0.75rem 1.5rem;
background: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
#task-form button:hover {
background: #357abd;
}
.filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.filters button {
padding: 0.5rem 1rem;
background: white;
border: 1px solid #ddd;
border-radius: 20px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.filters button:hover {
border-color: #4a90d9;
color: #4a90d9;
}
.filters button.active {
background: #4a90d9;
color: white;
border-color: #4a90d9;
}
#task-list {
list-style: none;
}
#task-list li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: white;
border-radius: 8px;
margin-bottom: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: transform 0.1s, box-shadow 0.2s;
}
#task-list li:hover {
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
}
#task-list li.completed span {
text-decoration: line-through;
color: #999;
}
#task-list li span {
flex: 1;
}
#task-list li button {
padding: 0.25rem 0.75rem;
background: #ff4757;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
opacity: 0;
transition: opacity 0.2s;
}
#task-list li:hover button {
opacity: 1;
}
#task-list li button:hover {
background: #e84040;
}
.clear-btn {
display: block;
margin-top: 1rem;
padding: 0.5rem 1rem;
background: none;
border: 1px solid #ff4757;
color: #ff4757;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.clear-btn:hover {
background: #ff4757;
color: white;
}""",
},
"messages": [
"feat: add task filtering (all/active/completed)",
"feat: add clear completed button",
"style: add filter button styles",
"style: show delete button only on hover",
"refactor: extract getFilteredTasks helper",
"fix: update filter counts on change",
"style: add pill-style filter buttons",
"style: improve hover shadow on tasks",
]
},
{
"name": "accessibility-and-docs",
"weight": 0.80,
"files": {
"index.html": """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskFlow - Task Management</title>
<meta name="description" content="A simple, clean task management app">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>TaskFlow</h1>
<p class="subtitle">Stay organized, stay productive</p>
</header>
<main id="app" role="main" aria-label="Task management"></main>
</div>
<script src="app.js"></script>
</body>
</html>
""",
"CONTRIBUTING.md": """# Contributing to TaskFlow
Thanks for your interest in contributing!
## Development Setup
1. Clone the repository
2. Open `index.html` in your browser
3. Make changes and refresh to test
## Guidelines
- Keep it simple - no build tools required
- Test in multiple browsers
- Follow existing code style
- Write descriptive commit messages
## Reporting Issues
Open an issue with:
- Browser and version
- Steps to reproduce
- Expected vs actual behavior
""",
".gitignore": """.DS_Store
Thumbs.db
*.log
node_modules/
.vscode/
.idea/
""",
},
"messages": [
"feat: add ARIA labels for accessibility",
"feat: add meta description",
"docs: add CONTRIBUTING.md",
"chore: add .gitignore",
"refactor: use semantic HTML elements",
"fix: improve screen reader support",
"docs: update page title",
]
},
{
"name": "final-polish",
"weight": 0.95,
"files": {
"README.md": """# TaskFlow
A simple, clean task management app built with vanilla JavaScript. No frameworks, no build step, just open and use.
## Features
- Create, complete, and delete tasks
- Filter by status (all, active, completed)
- Persistent storage using localStorage
- Task completion tracking with stats
- Clear completed tasks in bulk
- Accessible (ARIA labels, semantic HTML)
- Responsive design
## Getting Started
```bash
git clone <your-repo-url>
cd taskflow
# Open index.html in your browser
```
No dependencies. No build step. Just open `index.html`.
## Project Structure
```
taskflow/
index.html - Main HTML page
style.css - All styles
app.js - Application logic
```
## Browser Support
- Chrome 80+
- Firefox 75+
- Safari 13+
- Edge 80+
## License
MIT
""",
"LICENSE": """MIT License
Copyright (c) 2026 TaskFlow
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.
""",
},
"messages": [
"docs: rewrite README with full documentation",
"chore: add MIT license",
"docs: add browser support info",
"docs: add project structure to README",
"fix: minor typo in README",
]
},
]
FILLER_MESSAGES = [
"fix: resolve edge case in task toggle",
"refactor: simplify render logic",
"style: adjust spacing on mobile",
"fix: handle special characters in task title",
"refactor: use const where possible",
"style: tweak button border radius",
"fix: prevent duplicate task IDs",
"perf: reduce unnecessary re-renders",
"style: adjust font sizes for readability",
"fix: correct checkbox alignment",
"refactor: clean up event listeners",
"style: improve color contrast",
"fix: handle empty state gracefully",
"chore: remove console.log statements",
"style: add subtle animation on add",
"fix: trim whitespace from input",
"refactor: rename variables for clarity",
"style: darken text for accessibility",
"fix: ensure IDs are unique across sessions",
"perf: debounce localStorage writes",
]
def generate_schedule(start_date, end_date):
"""Generate a realistic commit schedule over the date range."""
schedule = []
current = start_date
while current <= end_date:
is_weekday = current.weekday() < 5
chance = WEEKDAY_COMMIT_CHANCE if is_weekday else WEEKEND_COMMIT_CHANCE
if random.random() < chance:
commit_range = WEEKDAY_COMMITS if is_weekday else WEEKEND_COMMITS
num_commits = random.randint(*commit_range)
for _ in range(num_commits):
hour = random.randint(WORK_START_HOUR, WORK_END_HOUR)
minute = random.randint(0, 59)
second = random.randint(0, 59)
commit_time = current.replace(hour=hour, minute=minute, second=second)
schedule.append(commit_time)
current += timedelta(days=1)
schedule.sort()
return schedule
Your schedule generator is working and your project phases are defined. Next up, you'll build the Git automation engine that uses both to create actual commits with backdated timestamps.
Create the Project Evolution Plan
Your commit schedule answers when commits happen. Now you need to answer what each commit contains.
The PHASES list and filler messages you wrote in the previous step define how the fake TaskFlow app evolves over time. In this step, you will write a helper function that maps any point on the 90-day timeline to the correct phase of development.
In this step, get ready to:
- Understand how the phase weight system controls file evolution.
- Write a helper function that picks the correct phase for any commit.
Explore the phase structure
Each phase in your PHASES list represents a stage of the fake project's development. The key field is weight, which marks the timeline position (from 0.0 to 1.0) where that phase ends.
How do weights work?
The weights divide the 90-day timeline into segments. A weight of 0.05 means that phase covers the first 5% of the timeline (about 4.5 days). A weight of 0.25 means that phase covers everything from the previous boundary up to 25% of the timeline.
Your six phases progress from initial setup (0-5%) through core features, localStorage, filtering, accessibility, and final polish (up to 95%). This mimics how a real project starts bare and gains complexity over weeks.
Each phase also includes a files dictionary with complete file contents at that stage, and a messages list with realistic conventional commit messages.
Understand the filler messages
Between major phase transitions, commits still need unique messages. The FILLER_MESSAGES list you wrote contains 20 generic but realistic messages using conventional commit prefixes like fix:, refactor:, style:, and perf:.
Why use conventional commit prefixes?
Real developers use standardized prefixes to categorize commits at a glance. When someone runs git log --oneline on your generated repo, the mix of feat:, fix:, and chore: messages looks exactly like a maintained project.
Write the phase-selection helper
The generator will calculate a progress value between 0.0 and 1.0 for each commit based on its position in the 90-day timeline. You need a function that converts this progress value into the correct phase index.
- In generate_history.py, scroll to the bottom of the file (below the generate_schedule function).
- Add the get_phase_for_progress function by pasting the following code:
def get_phase_for_progress(progress):
"""Determine which phase a commit belongs to based on timeline progress."""
for i, phase in enumerate(PHASES):
if progress <= phase["weight"]:
return i
return len(PHASES) - 1
What does this code do?
- The function takes a progress float (0.0 to 1.0) representing how far through the 90-day timeline a commit is.
- It loops through each phase and checks if the progress is within that phase's weight boundary.
- If no phase matches (progress is beyond all weights), it returns the last phase index as a fallback.
For example, a commit at 10% progress (0.10) returns phase index 1 (core-features, weight 0.25). A commit at 50% returns phase index 3 (filtering-and-polish, weight 0.60).
- Save generate_history.py.
- Test the function by temporarily adding a print statement at the bottom of your file and running it:
python -c "exec(open('generate_history.py').read()); print(get_phase_for_progress(0.10), get_phase_for_progress(0.50), get_phase_for_progress(0.90))"
You should see 1 3 5 printed, confirming the function maps progress values to the correct phase indices.
Seeing a NameError or wrong numbers?
- If you see NameError: name 'PHASES' is not defined, make sure the get_phase_for_progress function is placed BELOW the PHASES list in your file.
- If the numbers don't match, double-check the weight values in your PHASES list. They should be 0.05, 0.25, 0.40, 0.60, 0.80, 0.95 in order.
- Still stuck? Help me debug my get_phase_for_progress function.
✔️ Awesome, I've got everything!
Great. Double check you've saved generate_history.py.
ⓧ I'd like to double check the full code
Here is what your generate_history.py should look like at this point. The PHASES list and FILLER_MESSAGES are collapsed for readability since they haven't changed.
import os
import subprocess
import random
import shutil
import zipfile
from datetime import datetime, timedelta
from pathlib import Path
# === CONFIGURATION ===
REPO_NAME = "taskflow"
OUTPUT_ZIP = "taskflow-repo.zip"
DAYS_OF_HISTORY = 90
AUTHOR_NAME = "Alex Chen"
AUTHOR_EMAIL = "alex.chen@example.com"
WEEKDAY_COMMIT_CHANCE = 0.85
WEEKEND_COMMIT_CHANCE = 0.30
WEEKDAY_COMMITS = (1, 5)
WEEKEND_COMMITS = (1, 2)
WORK_START_HOUR = 9
WORK_END_HOUR = 19
# === PROJECT EVOLUTION PHASES ===
PHASES = [
# ... (all 6 phases as you wrote them in the previous step)
]
FILLER_MESSAGES = [
# ... (all 20 filler messages as you wrote them)
]
def generate_schedule(start_date, end_date):
"""Generate a realistic commit schedule over the date range."""
schedule = []
current = start_date
while current <= end_date:
is_weekday = current.weekday() < 5
chance = WEEKDAY_COMMIT_CHANCE if is_weekday else WEEKEND_COMMIT_CHANCE
if random.random() < chance:
commit_range = WEEKDAY_COMMITS if is_weekday else WEEKEND_COMMITS
num_commits = random.randint(*commit_range)
for _ in range(num_commits):
hour = random.randint(WORK_START_HOUR, WORK_END_HOUR)
minute = random.randint(0, 59)
second = random.randint(0, 59)
commit_time = current.replace(hour=hour, minute=minute, second=second)
schedule.append(commit_time)
current += timedelta(days=1)
schedule.sort()
return schedule
def get_phase_for_progress(progress):
"""Determine which phase a commit belongs to based on timeline progress."""
for i, phase in enumerate(PHASES):
if progress <= phase["weight"]:
return i
return len(PHASES) - 1
You now have all the data and logic needed to decide what each commit should contain. Next up, you will build the Git automation engine that uses these phases to create real commits with backdated timestamps.
Implement the Git Automation Engine
You have a schedule of commit times and a plan for how files evolve. Now you need the engine that actually executes Git commands, writes files to disk, and stamps each commit with the correct date.
This step connects everything together. You will write utility functions that wrap Git operations, then build the main loop that iterates through your schedule and creates real, backdated commits with meaningful file changes.
In this step, get ready to:
- Write helper functions for running Git commands and writing files.
- Build the main generation loop that creates backdated commits.
- Add progress output so you can watch commits being created.
Write the run_git helper function
Every commit in your generated repository requires multiple Git commands. A wrapper function keeps your code clean and handles environment variables for date overriding.
- In generate_history.py, scroll to the bottom of the file (below the get_phase_for_progress function).
- Add the following run_git function:
def run_git(args, cwd, env=None):
"""Run a git command."""
# Start with the full system environment (keeps PATH intact)
full_env = os.environ.copy()
# Merge in any custom env vars (like GIT_AUTHOR_DATE)
if env:
full_env.update(env)
result = subprocess.run(
["git"] + args,
cwd=cwd,
env=full_env,
capture_output=True,
text=True
)
# Warn on errors, but ignore "nothing to commit" (expected for filler)
if result.returncode != 0 and "nothing to commit" not in result.stderr:
print(f" Warning: git {' '.join(args)} returned: {result.stderr.strip()}")
return result
What does this code do?
This function wraps Python's subprocess.run to execute Git commands from your script.
- os.environ.copy() grabs the full system environment so Git can find its own executable via PATH.
- The env parameter lets you pass extra environment variables (like GIT_AUTHOR_DATE) that override commit timestamps.
- The error check ignores "nothing to commit" messages since filler commits may occasionally produce empty diffs.
Why merge environments instead of passing just the date?
The env parameter in subprocess.run replaces the entire child process environment. If you only passed your date variables, Git would lose access to PATH and crash. Merging with os.environ.copy() preserves system variables while injecting your custom ones.
- Save generate_history.py.
Write the file-writing helper
Each commit needs to write files into the generated repository. This helper handles creating parent directories automatically so your phase files land in the right place.
- Below the run_git function you just added, add this function:
def write_file(repo_path, filename, content):
"""Write a file to the repo, creating directories if needed."""
filepath = repo_path / filename
# Create parent folders (e.g. src/) if they don't exist
filepath.parent.mkdir(parents=True, exist_ok=True)
filepath.write_text(content, encoding="utf-8")
What does this code do?
This uses pathlib.Path to build file paths in a cross-platform way. The repo_path / filename syntax uses Path's / operator to join paths correctly on both Windows and macOS.
- Save generate_history.py.
Write the filler change helper
Between major phase transitions, commits still need to produce real diffs. This function makes tiny version-comment changes to app.js so Git always has something new to commit.
- Below write_file, add the following function:
def make_filler_change(repo_path, phase_index):
"""Make a small filler change to an existing file."""
app_js = repo_path / "app.js"
if app_js.exists():
content = app_js.read_text(encoding="utf-8")
lines = content.split("\n")
# Update or insert a version comment at the top of the file
version_line = f"// v0.{phase_index + 1}.{random.randint(0, 9)}"
if lines[0].startswith("// v"):
lines[0] = version_line
else:
lines.insert(0, version_line)
app_js.write_text("\n".join(lines), encoding="utf-8")
What does this code do?
- It opens app.js from the generated repo and reads its contents.
- It generates a random version string like // v0.3.7 using the current phase number.
- If the first line is already a version comment, it replaces it. Otherwise it inserts one at the top.
- This ensures every filler commit has a real diff in the Git history, not an empty commit.
- Save generate_history.py.
Seeing a syntax error when you save?
- Check that the f-string f"// v0.{phase_index + 1}.{random.randint(0, 9)}" has matching curly braces.
- Make sure your indentation is consistent (4 spaces per level, no tabs mixed in).
Help me debug a syntax error in my make_filler_change function.
Build the main generation loop
The main() function ties everything together. It initializes a fresh Git repository, generates the commit schedule, then loops through each scheduled time to create backdated commits with the right files.
- Below the make_filler_change function, add the start of the main() function with the repository setup:
def main():
print("=" * 50)
print(" TaskFlow Git History Generator")
print("=" * 50)
repo_path = Path.cwd() / REPO_NAME
# Clean up any previous run
if repo_path.exists():
print(f"\nRemoving existing {REPO_NAME} directory...")
shutil.rmtree(repo_path)
# Create fresh repo and configure it
repo_path.mkdir()
run_git(["init"], cwd=repo_path)
run_git(["config", "user.name", AUTHOR_NAME], cwd=repo_path)
run_git(["config", "user.email", AUTHOR_EMAIL], cwd=repo_path)
# Calculate the 90-day date range ending today
end_date = datetime.now()
start_date = end_date - timedelta(days=DAYS_OF_HISTORY)
print(f"\nGenerating history from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
# Generate the commit schedule
schedule = generate_schedule(start_date, end_date)
print(f"Planned {len(schedule)} commits over {DAYS_OF_HISTORY} days\n")
What does this setup section do?
- shutil.rmtree(repo_path) deletes the output folder from any previous run so you start fresh.
- git config user.name and user.email are set at the repo level, not globally. This means the generated commits use "Alex Chen" as the author without touching your real Git config.
- The date range is calculated backwards from today, so the most recent commits land on today's date.
Now add the commit generation loop. This is the core engine that creates each backdated commit.
- Still inside the main() function, add the following code directly after the print(f"Planned {len(schedule)}...") line:
# Track which phases have been applied
last_phase_applied = -1
phase_message_indices = {i: 0 for i in range(len(PHASES))}
for i, commit_time in enumerate(schedule):
# Calculate how far through the timeline this commit is (0.0 to 1.0)
elapsed = (commit_time - start_date).total_seconds()
total = (end_date - start_date).total_seconds()
progress = elapsed / total if total > 0 else 0
# Determine which phase this commit belongs to
phase_index = get_phase_for_progress(progress)
# If entering a new phase, write all its files
if phase_index > last_phase_applied:
for p in range(last_phase_applied + 1, phase_index + 1):
phase = PHASES[p]
for filename, content in phase["files"].items():
write_file(repo_path, filename, content)
last_phase_applied = phase_index
# Pick the next unused message from this phase
phase = PHASES[phase_index]
msg_idx = phase_message_indices[phase_index]
if msg_idx < len(phase["messages"]):
message = phase["messages"][msg_idx]
phase_message_indices[phase_index] = msg_idx + 1
else:
message = random.choice(FILLER_MESSAGES)
else:
# Still in the same phase - use remaining messages or make filler changes
phase = PHASES[phase_index]
msg_idx = phase_message_indices[phase_index]
if msg_idx < len(phase["messages"]):
message = phase["messages"][msg_idx]
phase_message_indices[phase_index] = msg_idx + 1
# Write a random file from this phase to create a diff
files_list = list(phase["files"].items())
if files_list:
filename, content = random.choice(files_list)
write_file(repo_path, filename, content)
else:
# No messages left - make a filler change
make_filler_change(repo_path, phase_index)
message = random.choice(FILLER_MESSAGES)
# Create the commit with backdated timestamps
date_str = commit_time.strftime("%Y-%m-%dT%H:%M:%S")
env = {
"GIT_AUTHOR_DATE": date_str,
"GIT_COMMITTER_DATE": date_str,
}
run_git(["add", "-A"], cwd=repo_path)
run_git(["commit", "-m", message, "--allow-empty"], cwd=repo_path, env=env)
What does this loop do?
For each scheduled commit time, the loop calculates how far through the 90-day timeline that commit falls (as a 0.0 to 1.0 progress value). It uses this to determine the current project phase.
- When entering a new phase, all of that phase's files are written to disk at once, simulating a feature being built.
- When still in the same phase, it either uses the next available phase message (writing a random file for a diff) or makes a filler change.
- GIT_AUTHOR_DATE and GIT_COMMITTER_DATE are both set to the scheduled time. GitHub uses author date for the contribution graph, but setting both ensures the history looks correct no matter how it is inspected.
- The date format YYYY-MM-DDTHH:MM:SS (ISO 8601) works identically on Windows, macOS, and Linux.
- Save generate_history.py.
Seeing IndentationError?
The loop code must be indented one level inside the main() function (4 spaces). The for loop body needs 8 spaces, and the if/else blocks inside the loop need 12 spaces.
Help me fix an indentation error in my main() function.
Add progress output
Generating 60-120 commits takes 30-90 seconds. Without progress output, you would not know if the script is working or frozen.
- In generate_history.py, find the last two lines inside the for loop (inside main()):
run_git(["add", "-A"], cwd=repo_path)
run_git(["commit", "-m", message, "--allow-empty"], cwd=repo_path, env=env)
- Add the following progress indicator directly below those lines, still inside the for loop (indented 8 spaces):
# Print progress every 20 commits
if (i + 1) % 20 == 0 or i == len(schedule) - 1:
print(f" [{i + 1}/{len(schedule)}] commits created...")
What does this code do?
This prints a progress update every 20 commits and on the very last commit. The output looks like [20/87] commits created... so you can see the script is running.
- Save generate_history.py.
- Run a quick syntax check by running:
python -c "import ast; ast.parse(open('generate_history.py').read()); print('Syntax OK')"
You should see Syntax OK printed. This confirms Python can parse your entire file without errors.
Seeing a SyntaxError?
- Check that the progress lines are inside the for loop, not after it. They should be indented 8 spaces (same level as the run_git calls above them).
- Make sure all f-strings have matching quotes and curly braces.
- Verify that the main() function's def main(): line has no indentation (it should be at column 0).
Help me fix a syntax error in generate_history.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved generate_history.py and the syntax check passed.
ⓧ I'd like to double check the full code
Here is the full generate_history.py file as it should look at the end of this step. The PHASES and FILLER_MESSAGES content is collapsed for readability.
import os
import subprocess
import random
import shutil
import zipfile
from datetime import datetime, timedelta
from pathlib import Path
# === CONFIGURATION ===
REPO_NAME = "taskflow"
OUTPUT_ZIP = "taskflow-repo.zip"
DAYS_OF_HISTORY = 90
AUTHOR_NAME = "Alex Chen"
AUTHOR_EMAIL = "alex.chen@example.com"
WEEKDAY_COMMIT_CHANCE = 0.85
WEEKEND_COMMIT_CHANCE = 0.30
WEEKDAY_COMMITS = (1, 5)
WEEKEND_COMMITS = (1, 2)
WORK_START_HOUR = 9
WORK_END_HOUR = 19
# === PROJECT EVOLUTION PHASES ===
PHASES = [
{
"name": "initial-setup",
"weight": 0.05,
"files": { ... },
"messages": [ ... ]
},
... (all 6 phases)
]
FILLER_MESSAGES = [ ... ]
def generate_schedule(start_date, end_date):
schedule = []
current = start_date
while current <= end_date:
is_weekday = current.weekday() < 5
chance = WEEKDAY_COMMIT_CHANCE if is_weekday else WEEKEND_COMMIT_CHANCE
if random.random() < chance:
commit_range = WEEKDAY_COMMITS if is_weekday else WEEKEND_COMMITS
num_commits = random.randint(*commit_range)
for _ in range(num_commits):
hour = random.randint(WORK_START_HOUR, WORK_END_HOUR)
minute = random.randint(0, 59)
second = random.randint(0, 59)
commit_time = current.replace(hour=hour, minute=minute, second=second)
schedule.append(commit_time)
current += timedelta(days=1)
schedule.sort()
return schedule
def get_phase_for_progress(progress):
for i, phase in enumerate(PHASES):
if progress <= phase["weight"]:
return i
return len(PHASES) - 1
def run_git(args, cwd, env=None):
"""Run a git command."""
full_env = os.environ.copy()
if env:
full_env.update(env)
result = subprocess.run(
["git"] + args,
cwd=cwd,
env=full_env,
capture_output=True,
text=True
)
if result.returncode != 0 and "nothing to commit" not in result.stderr:
print(f" Warning: git {' '.join(args)} returned: {result.stderr.strip()}")
return result
def write_file(repo_path, filename, content):
"""Write a file to the repo, creating directories if needed."""
filepath = repo_path / filename
filepath.parent.mkdir(parents=True, exist_ok=True)
filepath.write_text(content, encoding="utf-8")
def make_filler_change(repo_path, phase_index):
"""Make a small filler change to an existing file."""
app_js = repo_path / "app.js"
if app_js.exists():
content = app_js.read_text(encoding="utf-8")
lines = content.split("\n")
version_line = f"// v0.{phase_index + 1}.{random.randint(0, 9)}"
if lines[0].startswith("// v"):
lines[0] = version_line
else:
lines.insert(0, version_line)
app_js.write_text("\n".join(lines), encoding="utf-8")
def main():
print("=" * 50)
print(" TaskFlow Git History Generator")
print("=" * 50)
repo_path = Path.cwd() / REPO_NAME
if repo_path.exists():
print(f"\nRemoving existing {REPO_NAME} directory...")
shutil.rmtree(repo_path)
repo_path.mkdir()
run_git(["init"], cwd=repo_path)
run_git(["config", "user.name", AUTHOR_NAME], cwd=repo_path)
run_git(["config", "user.email", AUTHOR_EMAIL], cwd=repo_path)
end_date = datetime.now()
start_date = end_date - timedelta(days=DAYS_OF_HISTORY)
print(f"\nGenerating history from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
schedule = generate_schedule(start_date, end_date)
print(f"Planned {len(schedule)} commits over {DAYS_OF_HISTORY} days\n")
last_phase_applied = -1
phase_message_indices = {i: 0 for i in range(len(PHASES))}
for i, commit_time in enumerate(schedule):
elapsed = (commit_time - start_date).total_seconds()
total = (end_date - start_date).total_seconds()
progress = elapsed / total if total > 0 else 0
phase_index = get_phase_for_progress(progress)
if phase_index > last_phase_applied:
for p in range(last_phase_applied + 1, phase_index + 1):
phase = PHASES[p]
for filename, content in phase["files"].items():
write_file(repo_path, filename, content)
last_phase_applied = phase_index
phase = PHASES[phase_index]
msg_idx = phase_message_indices[phase_index]
if msg_idx < len(phase["messages"]):
message = phase["messages"][msg_idx]
phase_message_indices[phase_index] = msg_idx + 1
else:
message = random.choice(FILLER_MESSAGES)
else:
phase = PHASES[phase_index]
msg_idx = phase_message_indices[phase_index]
if msg_idx < len(phase["messages"]):
message = phase["messages"][msg_idx]
phase_message_indices[phase_index] = msg_idx + 1
files_list = list(phase["files"].items())
if files_list:
filename, content = random.choice(files_list)
write_file(repo_path, filename, content)
else:
make_filler_change(repo_path, phase_index)
message = random.choice(FILLER_MESSAGES)
date_str = commit_time.strftime("%Y-%m-%dT%H:%M:%S")
env = {
"GIT_AUTHOR_DATE": date_str,
"GIT_COMMITTER_DATE": date_str,
}
run_git(["add", "-A"], cwd=repo_path)
run_git(["commit", "-m", message, "--allow-empty"], cwd=repo_path, env=env)
if (i + 1) % 20 == 0 or i == len(schedule) - 1:
print(f" [{i + 1}/{len(schedule)}] commits created...")
Your automation engine is ready. Next up, you will add zip packaging and run the full generator to create your 90-day repository.
Add Zip Packaging and Run the Generator
Your generation engine is built and ready to create commits. The last piece is packaging the finished repository into a portable zip file that you can extract and push anywhere.
In this step, you will write a zip function that captures the entire repository (including the .git folder), wire it into your script, and run the full generator to produce your 90-day commit history.
In this step, get ready to:
- Write a zip creation function that packages the generated repository.
- Run the full generator and produce a 90-day commit history.
- Verify the output with Git log commands and inspect the final app.
Write the zip creation function
The generated repository needs to be portable. Python's built-in zipfile module can walk the entire directory tree and compress it into a single file.
- In generate_history.py, find the make_filler_change function and add the following new function below it:
def create_zip(repo_path, output_name):
"""Create a zip file of the repository including .git directory."""
print(f"\nPackaging into {output_name}...")
with zipfile.ZipFile(output_name, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(repo_path):
for file in files:
file_path = Path(root) / file
arcname = file_path.relative_to(repo_path.parent)
zf.write(file_path, arcname)
print(f"Done! Created {output_name}")
What does this code do?
- zipfile.ZipFile(output_name, 'w', zipfile.ZIP_DEFLATED) creates a new compressed zip archive.
- os.walk(repo_path) recursively visits every file in the repository, including the hidden .git folder. This preserves the full commit history inside the zip.
- relative_to(repo_path.parent) ensures the zip contains a top-level taskflow/ folder instead of absolute paths.
- You won't see a change yet. The next chunk wires this function into your main loop.
Wire everything together and run
The main() function currently ends after creating all commits. You need to call your zip function and add the script's entry point so Python knows where to start.
- In generate_history.py, scroll to the bottom of the main() function. Find this line:
if (i + 1) % 20 == 0 or i == len(schedule) - 1:
print(f" [{i + 1}/{len(schedule)}] commits created...")
- Add the following code below it, still inside the main() function (same indentation level as the for loop):
# Create zip
output_zip = Path.cwd() / OUTPUT_ZIP
if output_zip.exists():
output_zip.unlink()
create_zip(repo_path, OUTPUT_ZIP)
# Summary
print(f"\n{'=' * 50}")
print(f" Generation Complete!")
print(f"{'=' * 50}")
print(f" Repository: ./{REPO_NAME}/")
print(f" Zip file: ./{OUTPUT_ZIP}")
print(f" Commits: {len(schedule)}")
print(f" Timespan: {DAYS_OF_HISTORY} days")
print(f"\n To inspect: cd {REPO_NAME} && git log --oneline")
print(f" To push: cd {REPO_NAME} && git remote add origin <URL> && git push")
if __name__ == "__main__":
main()
What does this code do?
- output_zip.unlink() deletes any existing zip from a previous run so you always get a fresh archive.
- The summary block prints useful next steps (how to inspect and push the repo).
- if __name__ == "__main__" is Python's standard entry point. It ensures main() only runs when you execute the script directly, not when it's imported.
- Save generate_history.py.
- Run the full generator by running this command:
python generate_history.py
This takes a while
The script creates 60-120 individual git commits, each with file writes and subprocess calls. Expect it to run for 30-90 seconds depending on your machine speed. You will see progress indicators every 20 commits.
- Wait for the script to finish. You should see a "Generation Complete!" summary with the commit count and file paths.
Seeing an error?
- If you see PermissionError when removing the old directory, close any file explorer or terminal windows that have the taskflow/ folder open, then run the script again.
- If you see FileNotFoundError: [Errno 2] No such file or directory: 'git', Git is not in your PATH. Reinstall Git and make sure "Add to PATH" is checked.
- If the script runs but commits show warnings, those are usually harmless (empty diffs on filler commits). The --allow-empty flag ensures the script continues regardless.
Help me debug my generate_history.py script
Verify the output
The script should have produced a taskflow/ directory and a taskflow-repo.zip file. Now you will inspect the generated history to confirm it looks realistic.
- Navigate into the generated repo by running:
cd taskflow
- View the full commit list by running:
git log --oneline
- You should see 60-120 commits with varied messages (feat:, fix:, refactor:, style:, docs:, chore: prefixes).
- Verify the dates span 90 days by running:
git log --format="%ad %s" --date=short
- You should see dates ranging from roughly 90 days ago to today.
- Confirm more commits land on weekdays by running:
git log --format="%ad" --date=format:"%A"
- You should see a higher concentration of Monday through Friday entries compared to Saturday and Sunday.
- Navigate back to the parent folder and confirm the zip file exists by running:
cd ..
ls taskflow-repo.zip
- You should see taskflow-repo.zip listed in the output.
- Open taskflow/index.html in your browser to confirm the final TaskFlow app loads and displays correctly.
You should see a styled task management app with a header, input form, filter buttons, and a clean layout.
Git log shows no commits?
- Make sure you ran cd taskflow first. Running git log from the parent folder will fail because it is not a Git repository.
- If the taskflow/ folder does not exist, the script likely failed. Run python generate_history.py again from the git-history-generator/ folder and look for error messages.
Help me troubleshoot my generated Git history
✔️ Awesome, I've got everything!
Great. Double check you have saved generate_history.py and that both taskflow/ and taskflow-repo.zip exist in your project folder.
ⓧ I'd like to double check the full code
import os
import subprocess
import random
import shutil
import zipfile
from datetime import datetime, timedelta
from pathlib import Path
# === CONFIGURATION ===
REPO_NAME = "taskflow"
OUTPUT_ZIP = "taskflow-repo.zip"
DAYS_OF_HISTORY = 90
AUTHOR_NAME = "Alex Chen"
AUTHOR_EMAIL = "alex.chen@example.com"
WEEKDAY_COMMIT_CHANCE = 0.85
WEEKEND_COMMIT_CHANCE = 0.30
WEEKDAY_COMMITS = (1, 5)
WEEKEND_COMMITS = (1, 2)
WORK_START_HOUR = 9
WORK_END_HOUR = 19
# === PROJECT EVOLUTION PHASES ===
PHASES = [
{
"name": "initial-setup",
"weight": 0.05,
"files": {
"README.md": "# TaskFlow\n\nA simple task management app.\n",
"index.html": """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskFlow</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>TaskFlow</h1>
<div id="app"></div>
<script src="app.js"></script>
</body>
</html>""",
"style.css": """* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
padding: 2rem;
}
h1 {
color: #333;
margin-bottom: 1rem;
}""",
"app.js": "// TaskFlow App\nconsole.log('TaskFlow initialized');\n",
},
"messages": [
"Initial commit",
"feat: add project structure",
"feat: add basic HTML skeleton",
"style: add base CSS reset",
]
},
{
"name": "core-features",
"weight": 0.25,
"files": {
"app.js": """// TaskFlow App
const app = document.getElementById('app');
let tasks = [];
function createTask(title) {
const task = {
id: Date.now(),
title: title,
completed: false,
createdAt: new Date().toISOString()
};
tasks.push(task);
render();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
render();
}
function toggleTask(id) {
tasks = tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
render();
}
function render() {
app.innerHTML = `
<form id="task-form">
<input type="text" id="task-input" placeholder="Add a new task..." />
<button type="submit">Add</button>
</form>
<ul id="task-list">
${tasks.map(t => `
<li class="${t.completed ? 'completed' : ''}">
<input type="checkbox" ${t.completed ? 'checked' : ''}
onchange="toggleTask(${t.id})" />
<span>${t.title}</span>
<button onclick="deleteTask(${t.id})">Delete</button>
</li>
`).join('')}
</ul>
`;
document.getElementById('task-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('task-input');
if (input.value.trim()) {
createTask(input.value.trim());
input.value = '';
}
});
}
render();
""",
"index.html": """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskFlow</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>TaskFlow</h1>
<p class="subtitle">Stay organized, stay productive</p>
<div id="app"></div>
</div>
<script src="app.js"></script>
</body>
</html>""",
"style.css": """* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f0f2f5;
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 600px;
margin: 0 auto;
}
h1 {
color: #1a1a2e;
font-size: 2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: #666;
margin-bottom: 1.5rem;
}
#task-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
#task-form input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
#task-form input:focus {
outline: none;
border-color: #4a90d9;
}
#task-form button {
padding: 0.75rem 1.5rem;
background: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
#task-form button:hover {
background: #357abd;
}
#task-list {
list-style: none;
}
#task-list li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: white;
border-radius: 8px;
margin-bottom: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: transform 0.1s;
}
#task-list li:hover {
transform: translateX(4px);
}
#task-list li.completed span {
text-decoration: line-through;
color: #999;
}
#task-list li span {
flex: 1;
}
#task-list li button {
padding: 0.25rem 0.75rem;
background: #ff4757;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
#task-list li button:hover {
background: #e84040;
}""",
},
"messages": [
"feat: implement task creation",
"feat: add task deletion",
"feat: add toggle completion",
"feat: implement render function",
"style: add form styling",
"style: add task list styles",
"fix: prevent empty task submission",
"refactor: extract task operations",
"style: add hover effects on tasks",
"feat: add subtitle to header",
]
},
{
"name": "local-storage",
"weight": 0.40,
"files": {
"app.js": """// TaskFlow App
const app = document.getElementById('app');
const STORAGE_KEY = 'taskflow-tasks';
let tasks = loadTasks();
function loadTasks() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('Failed to load tasks:', e);
return [];
}
}
function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
function createTask(title) {
const task = {
id: Date.now(),
title: title,
completed: false,
createdAt: new Date().toISOString()
};
tasks.push(task);
saveTasks();
render();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
saveTasks();
render();
}
function toggleTask(id) {
tasks = tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
saveTasks();
render();
}
function getTaskCount() {
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
return { total, completed, remaining: total - completed };
}
function render() {
const count = getTaskCount();
app.innerHTML = `
<form id="task-form">
<input type="text" id="task-input" placeholder="Add a new task..." />
<button type="submit">Add</button>
</form>
<div class="task-stats">
<span>${count.remaining} remaining</span>
<span>${count.completed} completed</span>
</div>
<ul id="task-list">
${tasks.map(t => `
<li class="${t.completed ? 'completed' : ''}" data-id="${t.id}">
<input type="checkbox" ${t.completed ? 'checked' : ''}
onchange="toggleTask(${t.id})" />
<span>${t.title}</span>
<button onclick="deleteTask(${t.id})">Delete</button>
</li>
`).join('')}
</ul>
`;
document.getElementById('task-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('task-input');
if (input.value.trim()) {
createTask(input.value.trim());
input.value = '';
}
});
}
render();
""",
"README.md": """# TaskFlow
A simple, clean task management app built with vanilla JavaScript.
## Features
- Create, complete, and delete tasks
- Persistent storage using localStorage
- Task completion tracking
- Clean, responsive UI
## Getting Started
Open `index.html` in your browser. No build step required.
## Tech Stack
- HTML5
- CSS3
- Vanilla JavaScript
- localStorage for persistence
""",
},
"messages": [
"feat: add localStorage persistence",
"feat: load tasks from storage on startup",
"fix: handle corrupted localStorage data",
"feat: add task count statistics",
"refactor: extract storage logic",
"docs: update README with features",
"fix: save tasks after toggle",
"feat: show remaining vs completed count",
]
},
{
"name": "filtering-and-polish",
"weight": 0.60,
"files": {
"app.js": """// TaskFlow App
const app = document.getElementById('app');
const STORAGE_KEY = 'taskflow-tasks';
let tasks = loadTasks();
let currentFilter = 'all';
function loadTasks() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('Failed to load tasks:', e);
return [];
}
}
function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
function createTask(title) {
const task = {
id: Date.now(),
title: title,
completed: false,
createdAt: new Date().toISOString()
};
tasks.push(task);
saveTasks();
render();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
saveTasks();
render();
}
function toggleTask(id) {
tasks = tasks.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
saveTasks();
render();
}
function setFilter(filter) {
currentFilter = filter;
render();
}
function getFilteredTasks() {
switch (currentFilter) {
case 'active':
return tasks.filter(t => !t.completed);
case 'completed':
return tasks.filter(t => t.completed);
default:
return tasks;
}
}
function clearCompleted() {
tasks = tasks.filter(t => !t.completed);
saveTasks();
render();
}
function getTaskCount() {
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
return { total, completed, remaining: total - completed };
}
function render() {
const count = getTaskCount();
const filtered = getFilteredTasks();
app.innerHTML = `
<form id="task-form">
<input type="text" id="task-input" placeholder="Add a new task..." />
<button type="submit">Add</button>
</form>
<div class="filters">
<button class="${currentFilter === 'all' ? 'active' : ''}"
onclick="setFilter('all')">All (${count.total})</button>
<button class="${currentFilter === 'active' ? 'active' : ''}"
onclick="setFilter('active')">Active (${count.remaining})</button>
<button class="${currentFilter === 'completed' ? 'active' : ''}"
onclick="setFilter('completed')">Completed (${count.completed})</button>
</div>
<ul id="task-list">
${filtered.map(t => `
<li class="${t.completed ? 'completed' : ''}" data-id="${t.id}">
<input type="checkbox" ${t.completed ? 'checked' : ''}
onchange="toggleTask(${t.id})" />
<span>${t.title}</span>
<button onclick="deleteTask(${t.id})">Delete</button>
</li>
`).join('')}
</ul>
${count.completed > 0 ? `
<button class="clear-btn" onclick="clearCompleted()">
Clear completed (${count.completed})
</button>
` : ''}
`;
document.getElementById('task-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('task-input');
if (input.value.trim()) {
createTask(input.value.trim());
input.value = '';
}
});
}
render();
""",
"style.css": """* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f0f2f5;
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 600px;
margin: 0 auto;
}
h1 {
color: #1a1a2e;
font-size: 2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: #666;
margin-bottom: 1.5rem;
}
#task-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
#task-form input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
#task-form input:focus {
outline: none;
border-color: #4a90d9;
}
#task-form button {
padding: 0.75rem 1.5rem;
background: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
#task-form button:hover {
background: #357abd;
}
.filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.filters button {
padding: 0.5rem 1rem;
background: white;
border: 1px solid #ddd;
border-radius: 20px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.filters button:hover {
border-color: #4a90d9;
color: #4a90d9;
}
.filters button.active {
background: #4a90d9;
color: white;
border-color: #4a90d9;
}
#task-list {
list-style: none;
}
#task-list li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: white;
border-radius: 8px;
margin-bottom: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: transform 0.1s, box-shadow 0.2s;
}
#task-list li:hover {
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
}
#task-list li.completed span {
text-decoration: line-through;
color: #999;
}
#task-list li span {
flex: 1;
}
#task-list li button {
padding: 0.25rem 0.75rem;
background: #ff4757;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
opacity: 0;
transition: opacity 0.2s;
}
#task-list li:hover button {
opacity: 1;
}
#task-list li button:hover {
background: #e84040;
}
.clear-btn {
display: block;
margin-top: 1rem;
padding: 0.5rem 1rem;
background: none;
border: 1px solid #ff4757;
color: #ff4757;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.clear-btn:hover {
background: #ff4757;
color: white;
}""",
},
"messages": [
"feat: add task filtering (all/active/completed)",
"feat: add clear completed button",
"style: add filter button styles",
"style: show delete button only on hover",
"refactor: extract getFilteredTasks helper",
"fix: update filter counts on change",
"style: add pill-style filter buttons",
"style: improve hover shadow on tasks",
]
},
{
"name": "accessibility-and-docs",
"weight": 0.80,
"files": {
"index.html": """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskFlow - Task Management</title>
<meta name="description" content="A simple, clean task management app">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>TaskFlow</h1>
<p class="subtitle">Stay organized, stay productive</p>
</header>
<main id="app" role="main" aria-label="Task management"></main>
</div>
<script src="app.js"></script>
</body>
</html>
""",
"CONTRIBUTING.md": """# Contributing to TaskFlow
Thanks for your interest in contributing!
## Development Setup
1. Clone the repository
2. Open `index.html` in your browser
3. Make changes and refresh to test
## Guidelines
- Keep it simple - no build tools required
- Test in multiple browsers
- Follow existing code style
- Write descriptive commit messages
## Reporting Issues
Open an issue with:
- Browser and version
- Steps to reproduce
- Expected vs actual behavior
""",
".gitignore": """.DS_Store
Thumbs.db
*.log
node_modules/
.vscode/
.idea/
""",
},
"messages": [
"feat: add ARIA labels for accessibility",
"feat: add meta description",
"docs: add CONTRIBUTING.md",
"chore: add .gitignore",
"refactor: use semantic HTML elements",
"fix: improve screen reader support",
"docs: update page title",
]
},
{
"name": "final-polish",
"weight": 0.95,
"files": {
"README.md": """# TaskFlow
A simple, clean task management app built with vanilla JavaScript. No frameworks, no build step, just open and use.
## Features
- Create, complete, and delete tasks
- Filter by status (all, active, completed)
- Persistent storage using localStorage
- Task completion tracking with stats
- Clear completed tasks in bulk
- Accessible (ARIA labels, semantic HTML)
- Responsive design
## Getting Started
```bash
git clone <your-repo-url>
cd taskflow
# Open index.html in your browser
```
No dependencies. No build step. Just open `index.html`.
## Project Structure
```
taskflow/
index.html - Main HTML page
style.css - All styles
app.js - Application logic
```
## Browser Support
- Chrome 80+
- Firefox 75+
- Safari 13+
- Edge 80+
## License
MIT
""",
"LICENSE": """MIT License
Copyright (c) 2026 TaskFlow
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.
""",
},
"messages": [
"docs: rewrite README with full documentation",
"chore: add MIT license",
"docs: add browser support info",
"docs: add project structure to README",
"fix: minor typo in README",
]
},
]
FILLER_MESSAGES = [
"fix: resolve edge case in task toggle",
"refactor: simplify render logic",
"style: adjust spacing on mobile",
"fix: handle special characters in task title",
"refactor: use const where possible",
"style: tweak button border radius",
"fix: prevent duplicate task IDs",
"perf: reduce unnecessary re-renders",
"style: adjust font sizes for readability",
"fix: correct checkbox alignment",
"refactor: clean up event listeners",
"style: improve color contrast",
"fix: handle empty state gracefully",
"chore: remove console.log statements",
"style: add subtle animation on add",
"fix: trim whitespace from input",
"refactor: rename variables for clarity",
"style: darken text for accessibility",
"fix: ensure IDs are unique across sessions",
"perf: debounce localStorage writes",
]
def generate_schedule(start_date, end_date):
"""Generate a realistic commit schedule over the date range."""
schedule = []
current = start_date
while current <= end_date:
is_weekday = current.weekday() < 5
chance = WEEKDAY_COMMIT_CHANCE if is_weekday else WEEKEND_COMMIT_CHANCE
if random.random() < chance:
commit_range = WEEKDAY_COMMITS if is_weekday else WEEKEND_COMMITS
num_commits = random.randint(*commit_range)
for _ in range(num_commits):
hour = random.randint(WORK_START_HOUR, WORK_END_HOUR)
minute = random.randint(0, 59)
second = random.randint(0, 59)
commit_time = current.replace(hour=hour, minute=minute, second=second)
schedule.append(commit_time)
current += timedelta(days=1)
schedule.sort()
return schedule
def get_phase_for_progress(progress):
"""Determine which phase a commit belongs to based on timeline progress."""
for i, phase in enumerate(PHASES):
if progress <= phase["weight"]:
return i
return len(PHASES) - 1
def run_git(args, cwd, env=None):
"""Run a git command."""
full_env = os.environ.copy()
if env:
full_env.update(env)
result = subprocess.run(
["git"] + args,
cwd=cwd,
env=full_env,
capture_output=True,
text=True
)
if result.returncode != 0 and "nothing to commit" not in result.stderr:
print(f" Warning: git {' '.join(args)} returned: {result.stderr.strip()}")
return result
def write_file(repo_path, filename, content):
"""Write a file to the repo, creating directories if needed."""
filepath = repo_path / filename
filepath.parent.mkdir(parents=True, exist_ok=True)
filepath.write_text(content, encoding="utf-8")
def make_filler_change(repo_path, phase_index):
"""Make a small filler change to an existing file."""
app_js = repo_path / "app.js"
if app_js.exists():
content = app_js.read_text(encoding="utf-8")
lines = content.split("\n")
version_line = f"// v0.{phase_index + 1}.{random.randint(0, 9)}"
if lines[0].startswith("// v"):
lines[0] = version_line
else:
lines.insert(0, version_line)
app_js.write_text("\n".join(lines), encoding="utf-8")
def create_zip(repo_path, output_name):
"""Create a zip file of the repository including .git directory."""
print(f"\nPackaging into {output_name}...")
with zipfile.ZipFile(output_name, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(repo_path):
for file in files:
file_path = Path(root) / file
arcname = file_path.relative_to(repo_path.parent)
zf.write(file_path, arcname)
print(f"Done! Created {output_name}")
def main():
print("=" * 50)
print(" TaskFlow Git History Generator")
print("=" * 50)
repo_path = Path.cwd() / REPO_NAME
# Clean up previous run
if repo_path.exists():
print(f"\nRemoving existing {REPO_NAME} directory...")
shutil.rmtree(repo_path)
# Create and init repo
repo_path.mkdir()
run_git(["init"], cwd=repo_path)
run_git(["config", "user.name", AUTHOR_NAME], cwd=repo_path)
run_git(["config", "user.email", AUTHOR_EMAIL], cwd=repo_path)
# Calculate date range
end_date = datetime.now()
start_date = end_date - timedelta(days=DAYS_OF_HISTORY)
print(f"\nGenerating history from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
# Generate commit schedule
schedule = generate_schedule(start_date, end_date)
print(f"Planned {len(schedule)} commits over {DAYS_OF_HISTORY} days\n")
# Track phase progress
last_phase_applied = -1
phase_message_indices = {i: 0 for i in range(len(PHASES))}
for i, commit_time in enumerate(schedule):
# Calculate progress through timeline (0.0 to 1.0)
elapsed = (commit_time - start_date).total_seconds()
total = (end_date - start_date).total_seconds()
progress = elapsed / total if total > 0 else 0
# Determine current phase
phase_index = get_phase_for_progress(progress)
# If entering a new phase, write its files
if phase_index > last_phase_applied:
for p in range(last_phase_applied + 1, phase_index + 1):
phase = PHASES[p]
for filename, content in phase["files"].items():
write_file(repo_path, filename, content)
last_phase_applied = phase_index
# Use a phase message
phase = PHASES[phase_index]
msg_idx = phase_message_indices[phase_index]
if msg_idx < len(phase["messages"]):
message = phase["messages"][msg_idx]
phase_message_indices[phase_index] = msg_idx + 1
else:
message = random.choice(FILLER_MESSAGES)
else:
# Use remaining phase messages or make filler changes
phase = PHASES[phase_index]
msg_idx = phase_message_indices[phase_index]
if msg_idx < len(phase["messages"]):
message = phase["messages"][msg_idx]
phase_message_indices[phase_index] = msg_idx + 1
# Write a subset of phase files
files_list = list(phase["files"].items())
if files_list:
filename, content = random.choice(files_list)
write_file(repo_path, filename, content)
else:
# Filler commit
make_filler_change(repo_path, phase_index)
message = random.choice(FILLER_MESSAGES)
# Stage and commit with backdated timestamp
date_str = commit_time.strftime("%Y-%m-%dT%H:%M:%S")
env = {
"GIT_AUTHOR_DATE": date_str,
"GIT_COMMITTER_DATE": date_str,
}
run_git(["add", "-A"], cwd=repo_path)
run_git(["commit", "-m", message, "--allow-empty"], cwd=repo_path, env=env)
# Progress indicator
if (i + 1) % 20 == 0 or i == len(schedule) - 1:
print(f" [{i + 1}/{len(schedule)}] commits created...")
# Create zip
output_zip = Path.cwd() / OUTPUT_ZIP
if output_zip.exists():
output_zip.unlink()
create_zip(repo_path, OUTPUT_ZIP)
# Summary
print(f"\n{'=' * 50}")
print(f" Generation Complete!")
print(f"{'=' * 50}")
print(f" Repository: ./{REPO_NAME}/")
print(f" Zip file: ./{OUTPUT_ZIP}")
print(f" Commits: {len(schedule)}")
print(f" Timespan: {DAYS_OF_HISTORY} days")
print(f"\n To inspect: cd {REPO_NAME} && git log --oneline")
print(f" To push: cd {REPO_NAME} && git remote add origin <URL> && git push")
if __name__ == "__main__":
main()
Secret mission
Add Feature Branch Merge History
Real repositories don't just have commits on main. They have feature branches, pull requests, and merge commits. Can you make the generated history even more convincing by simulating branch-based development?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your files to experiment further, or delete them to free up disk space.
Resources you used:
- git-history-generator/ project folder (contains your generator script).
- taskflow/ generated repository with full Git history.
- taskflow-repo.zip packaged zip file of the repository.
- Python 3 and Git (pre-existing installations).
Keep everything running
No action needed. Choose this if you want to keep experimenting with your generator script or regenerate the repository with different settings.
- Your generate_history.py script stays in the git-history-generator/ folder on your Desktop.
- You can re-run the script anytime to regenerate a fresh repository with new random commits.
- Edit the configuration constants at the top of the script to change the author name, date range, or commit frequency.
Pause - I'll come back to this later
Free up disk space by removing the generated output while keeping your script for later.
- Delete the taskflow/ folder (you can regenerate it anytime by running the script again).
- Delete taskflow-repo.zip if you no longer need the packaged version.
- Keep generate_history.py so you can regenerate everything later.
Delete - I don't want to use this again
Remove all project files completely. You can always rebuild the script from scratch using this project.
- Delete the entire git-history-generator/ folder from your Desktop.
- Delete taskflow-repo.zip if it exists in the same directory.
- Python and Git remain installed on your system for other projects.
Nice Work!
Nice Work!
You just built a Python-powered Git history generator from scratch. Your script creates a repository that looks like 3 months of sustained, authentic development work.
You learned how to:
- Build a script that generates 60-120+ realistic commits spanning 90 days, with natural weekday/weekend patterns and working-hour timestamps.
- Manipulate GIT_AUTHOR_DATE and GIT_COMMITTER_DATE to backdate commits, and automate Git operations via Python's subprocess module.
- Create a phase-based file evolution system that produces real, meaningful diffs across HTML, CSS, and JavaScript files.
- Secret Mission: Added feature branch history with merge commits for extra authenticity.
Ready to quiz yourself?