Automate backdated Git history (repositories, branches, commits, PRs, merges) using git and the GitHub CLI (gh).
This tool lets you create a repository that appears to have evolved over time: you specify the dates and the CLI writes
commits and merge commits carrying those timestamps. PRs are orchestrated through gh for a realistic workflow.
Important limitation: GitHub does not allow backdating PR creation, review, or comment timestamps.
legends backdates commits and merge commits. PRs, reviews, and comments will show the time you performed the action.
- legends
- Create repo with a backdated initial commit (local +
gh repo createpush). - Create branch and stamp its “birth” with a backdated empty commit.
- Backdated commits on any branch (
--allow-empty,--add-all,--touchsupported). - Open PR via GitHub CLI.
- Backdated merge commit (performed locally with
--no-ff --no-commit, then committed with a chosen date). - One-shot
commit-all: backdated commit → open PR → (optional review/comment) → backdated merge → cleanup. - Dry-run mode (log commands without executing).
- Configurable defaults via YAML and environment variables (with clear precedence).
- Minimal dependencies (stdlib + optional
PyYAMLfor YAML config).
- Git commit timestamps are controlled by environment variables:
GIT_AUTHOR_DATEandGIT_COMMITTER_DATEare set to your desired past datetime.- legends normalizes input like
YYYY-MM-DD,YYYY-MM-DD HH:MM[:SS], ISO8601 (withZor+/-HH:MM), treating naïve values as local time, then storing in UTC.
- Merge commits are created locally with those same variables and then pushed. GitHub recognizes the merge and will close the PR as merged.
- PRs, reviews, and comments are created through
gh. Their timestamps cannot be backdated, but the merge commit itself is backdated and shows in the repo’s history accordingly.
GitHub renders Mermaid diagrams in Markdown. If viewing locally, use a Markdown preview with Mermaid support.
flowchart TD
A["Run: legends commit-all"] --> B["Checkout base branch"]
B --> C["Create/checkout feature branch"]
C --> D["Create change (or allow-empty)"]
D --> E["Backdated commit on branch"]
E --> F["Push branch (upstream if first push)"]
F --> G["Open PR via gh"]
G --> H{"Optional review/comment?"}
H -->|Yes| I["gh pr review/comment"]
H -->|No| J["Skip"]
I --> K["Checkout base and pull"]
J --> K
K --> L["Merge (no-ff, no-commit)"]
L --> M["Create merge commit (backdated)"]
M --> N["Push base"]
N --> O["Close PR and delete branch"]
sequenceDiagram
participant Dev as Developer
participant CLI as legends
participant Git as git
participant GH as gh
Dev->>CLI: commit-all --branch BRANCH --commit-date ... --merge-date ...
CLI->>Git: checkout base, create/checkout branch
CLI->>Git: commit (env: GIT_*_DATE=commit-date)
CLI->>Git: push branch (set upstream if needed)
CLI->>GH: pr create --head BRANCH --base BASE
opt Optional
CLI->>GH: pr review --approve / pr comment
end
CLI->>Git: checkout base and pull
CLI->>Git: merge --no-ff --no-commit BRANCH
CLI->>Git: commit (env: GIT_*_DATE=merge-date)
CLI->>Git: push base
CLI->>GH: pr close --delete-branch
Based on
config/timeline.example.yaml(for inspiration; you can build a small runner to drive these steps).
gantt
title Project timeline (example)
dateFormat YYYY-MM-DD
axisFormat %b %d, %Y
section Init
Initial commit :milestone, ic, 2024-12-01, 1d
section feature-login
Branch start :active, fls, 2025-02-01, 1d
Implement login backend :fl1, 2025-02-10, 1d
Add frontend for login :fl2, 2025-02-15, 1d
Merge to main :milestone, flm, 2025-03-01, 1d
section feature-profile
Branch start :fps, 2025-03-05, 1d
Profile service skeleton :fp1, 2025-03-08, 1d
Avatar upload + resizing :fp2, 2025-03-12, 1d
Merge to main :milestone, fpm, 2025-03-20, 1d
section fix-auth-timeout
Branch start :fas, 2025-04-01, 1d
Fix token refresh race condition :fa1, 2025-04-02, 1d
Merge to main :milestone, fam, 2025-04-03, 1d
flowchart LR
A["Project defaults"] --> B["YAML file (--config)"]
B --> C["Environment variables"]
C --> D["CLI flags"]
D --> E["Effective runtime config"]
- Precedence: CLI flags → Environment → YAML → Defaults.
- Git (on
PATH) - GitHub CLI (
gh) with auth:gh auth login(orGITHUB_TOKEN/GH_TOKEN) - Python 3.9+
- Optional: PyYAML (auto-installed by the package) for reading YAML config
Using the included Makefile:
make install # creates .venv and installs legends in editable mode
. .venv/bin/activate # activate the virtualenv
legends --helpOr manually:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -e .Create a repo whose initial commit is backdated to Dec 1, 2024 (private by default):
legends create-repo my-backdated-repo \
--owner your-username-or-org \
--date "2024-12-01T12:00:00" \
--private \
--description "Backdated repo demo"Create a feature branch with a backdated birth commit:
legends create-branch feature-login \
--base main \
--date "2025-02-01T09:00:00" \
--message "chore(feature-login): branch birth @ 2025-02-01T09:00:00" \
--pushMake backdated commits on that branch:
legends commit --branch feature-login \
--date "2025-02-10T15:00:00" --message "Implement login backend" --add-all --push
legends commit --branch feature-login \
--date "2025-02-15T10:30:00" --message "Add frontend for login" --add-all --pushOpen a PR and merge with a backdated merge commit:
legends open-pr --branch feature-login --base main \
--title "Feature: User Login" \
--body "Adds backend + UI for user login."
legends merge-pr --branch feature-login --base main \
--date "2025-03-01T17:00:00" --delete-branchRun the one-shot flow:
legends commit-all \
--base main \
--branch feature-login \
--commit-date "2025-02-10T15:00:00" \
--message "Implement login" \
--pr-title "Feature: Login" \
--pr-body "Adds backend + UI." \
--merge-date "2025-03-01T17:00:00" \
--delete-branchRun
legends --helpfor details; highlights below.
Create a repo from the local folder with a backdated initial commit and push it via gh.
legends create-repo <name> \
[--owner <org-or-user>] [--description <text>] \
[--branch <main|trunk|…>] [--message "Initial commit"] \
[--date "<ISO or YYYY-MM-DD HH:MM:SS>"] \
[--author-name <name>] [--author-email <email>] \
[--private|--public] [--remote origin]Create a branch from base and record its birth via an empty backdated commit.
legends create-branch <branch> \
[--base main] --date "<ISO>" [--message "..."] [--push]Make a backdated commit on a branch; can stage all or --touch a file.
legends commit --branch <branch> --date "<ISO>" \
--message "..." [--allow-empty] [--add-all] [--touch <path>] [--push]Open a PR via GitHub CLI.
legends open-pr --branch <head> [--base main] [--title ...] [--body ...] [--draft]Perform a local, backdated merge commit and push; closes the PR & optionally deletes the branch.
legends merge-pr [--branch <head> | --pr <number>] \
[--base main] --date "<ISO>" [--message "..."] [--delete-branch|--no-delete-branch]One-shot: backdated commit → open PR → optional review/comment → backdated merge → cleanup.
legends commit-all --branch <branch> [--base main] \
--commit-date "<ISO>" --message "..." \
[--touch <path>] [--allow-empty] \
[--pr-title ...] [--pr-body ...] [--draft] \
--merge-date "<ISO>" [--delete-branch]
[--review] [--review-body "..."] [--comment "..."]Built-in defaults: config/defaults.yaml
base_branch: main
remote_name: origin
visibility: private
owner: null
author:
name: ""
email: ""
token_env: GITHUB_TOKEN
# dry_run: false- Use
--config <path/to/yaml>to load a file with the same keys. - Precedence: CLI flags → Environment → YAML → Defaults.
See config/timeline.example.yaml for a year-like schedule (repo init, three features & merges).
You can write a tiny wrapper that reads this YAML and invokes the CLI in order.
- Auth:
GITHUB_TOKENorGH_TOKEN(or rungh auth logininteractively). - CLI defaults (override anything in YAML):
GHB_VISIBILITY=private|publicGHB_BASE_BRANCH(default base branch)GHB_REMOTE(default remote name)GHB_OWNER(default GitHub owner/org)GHB_DRY_RUN=1|trueto log without executing
- Identity overrides (used if set; otherwise your git global config is used):
GIT_AUTHOR_NAME,GIT_AUTHOR_EMAILGIT_COMMITTER_NAME,GIT_COMMITTER_EMAIL
make install,make verify-env,make run-help- Workflow helpers:
make create-repomake create-branchmake commitmake open-prmake merge-prmake commit-all
- Scripts:
scripts/verify_env.sh– checks git, gh, auth, identityscripts/install.sh– venv + install (with--devextras)scripts/bootstrap_repo.sh– create a repo w/ backdated initial commitscripts/commit_all.sh– one-shot flow wrapper
Create three branches and simulate a small quarter of activity (dates are illustrative):
# Repo created "last December"
legends create-repo quarterly-demo --date "2024-12-01T10:00:00" --private
# Feature A
legends create-branch feature-a --date "2025-02-01T09:00:00" --push
legends commit --branch feature-a --date "2025-02-05T14:00:00" --message "feat(a): add core" --allow-empty --push
legends open-pr --branch feature-a --title "Feature A"
legends merge-pr --branch feature-a --date "2025-02-20T16:00:00" --delete-branch
# Feature B (one-shot)
legends commit-all --branch feature-b --commit-date "2025-03-10T11:00:00" \
--message "feat(b): initial" --pr-title "Feature B" \
--merge-date "2025-03-18T18:00:00" --delete-branch
# Hotfix
legends commit-all --branch hotfix-timeout --commit-date "2025-04-02T09:00:00" \
--message "fix: token refresh race" --pr-title "Fix: auth token refresh race" \
--merge-date "2025-04-03T13:00:00" --delete-branchQ: Will these backdated commits affect my GitHub contribution graph?
A: Yes—contributions are based on commit metadata and email attribution. Use responsibly.
Q: Can I backdate PR creation/merge timestamps shown on the PR page?
A: No. GitHub records those server-side when the action occurs. Only commits (including merge commits) can be backdated.
Q: Do I need to force-push?
A: Not for normal use. Only if you later rewrite history. Be careful with --force on shared repos.
Q: What date formats are accepted?
A: YYYY-MM-DD, YYYY-MM-DD HH:MM[:SS], ISO8601 (...T...), with optional Z or an offset like +02:00. Naïve times are treated as local time and normalized to UTC internally.
- Run
scripts/verify_env.shto check prerequisites and auth. - Ensure
git config user.nameanduser.emailare set (or provide--author-*/ env overrides). - If
ghprompts for login, rungh auth loginor setGITHUB_TOKEN/GH_TOKEN. - If
gh pr createcannot resolve a PR number immediately, we query it viagh pr list --head <branch>. - On merge: we use
git merge --no-ff --no-commitfollowed by a backdatedgit commit -m ...thengit pushthe base branch.
- PRs welcome for: a YAML timeline runner, richer templates, additional safety checks, and Windows support.
- Dev setup:
scripts/install.sh --dev, runruff,black, andpytestas needed.
MIT – see LICENSE.
pyproject.tomldeclaresreadme = "README.md"– keep this file at the project root.- Non-Python templates are included under
gh_backdate/templates/in the wheel.