How Git Legends Works – Under the Hood of Git History Backdating

How Git Legends Works: The Technology Behind the Tool

Git Legends uses a combination of clever Git techniques and GitHub integrations to manipulate your repository’s history. In simple terms, it lets you rewrite commit dates and branch histories so that your project looks like it had activity on specific past dates. This is incredibly useful for developers who need to simulate a long timeline of commits or fill in missing history. Under the hood, everything is done with Git’s native features and the GitHub API – there’s no magic, just smart automation. The tone of Git Legends is casually technical – it’s built by developers for developers (think software engineers, full-stack devs, DevOps engineers, open-source maintainers, and Git power-users). Let’s break down the key techniques that make this tool possible.

Using GIT_AUTHOR_DATE and GIT_COMMITTER_DATE for Backdated Commits

Git commits normally record two timestamps: an author date and a committer date. By default, both are set to the current time when you run git commit. Git Legends leverages Git’s built-in ability to override these values. By setting the environment variables GIT_AUTHOR_DATE and GIT_COMMITTER_DATE before each commit (or using Git’s --date flag), the tool makes Git assign a specific past timestamp to that commit. This means you can create a commit today that appears to have been made months or years ago. For example, Git Legends might run a command under the hood like:

In this case, the new commit would show December 4, 2024 as its timestamp. GitHub will display the commit date you set, not the date you pushed it. This technique uses Git’s official features, so it’s reliable and won’t corrupt your repo. (It’s the same method you’d use manually with git commit --date or by exporting the env variables.) Git Legends wraps this logic so you can simply provide a --date option in the CLI, and it handles setting these environment variables behind the scenes for you.

Different Dates for Author vs. Committer: In most cases, Git Legends sets both author and committer dates the same (to your chosen backdate) for consistency. However, Git does allow them to differ. The author date is meant to be when the work was originally done, and the committer date is when it was added to the repo. Git Legends defaults to keeping them identical for a natural look, unless you intentionally want to distinguish them.

Custom Author Identity: Along with dates, Git Legends can also set the author name and email for each commit (via GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, etc.). This means you can simulate commits from different contributors. For example, one commit can look like it was authored by “Alice alice@example.com” in January, and another by “Bob bob@example.com” in February. This is great for populating a fake project history with multiple personas (like showing a team’s activity). The CLI provides options to specify author info per commit, which it then applies through these environment variables.

GIT_COMMITTER_DATE="2024-12-04T09:30:00" \
GIT_AUTHOR_DATE="2024-12-04T09:30:00" \
git commit -m "Backdated commit" --allow-empty
# Overrides default timestamps to create a commit that looks like it happened in the past.
$ legends branch create feature-login --base main --date 2025-02-01
Internal Command:git checkout -b feature-login main
GIT_AUTHOR_DATE="2025-02-01T09:00:00" \
GIT_COMMITTER_DATE="2025-02-01T09:00:00" \
git commit --allow-empty -m "Initial commit on feature-login"

Empty Commits for Backdated Branch Creation

Git doesn’t timestamp the moment a branch is created, but we often infer a branch’s start time from its first commit. To make a new branch look like it started in the past, Git Legends uses empty commits as branch markers. An empty commit is a commit with no file changes (no additions or modifications). Git allows this via the --allow-empty flag on git commit. Git Legends creates a new branch (off of a base branch you specify, usually main) and immediately makes an empty commit on that branch dated to your chosen start date.

For example, if you run:

Git Legends will internally do something like:

This creates a commit on feature-login with timestamp Feb 1, 2025, but with no changes (just a message). Pushing this branch to GitHub makes it appear as if the branch was created on Feb 1, 2025 – anyone looking at the branch’s history will see that date on the first commit. By doing this, Legends CLI effectively backdates the branch’s creation. You can then proceed to add real commits on that branch on later dates (which we’ll do next). The empty commit trick is key for branch timelines, and it’s done in a safe way that Git fully supports (empty commits are often used to trigger CI or mark events, so we’re repurposing the idea to mark branch start dates).

Backdating Merge Commits with Local Git Merges

Merging a pull request via the GitHub web interface will timestamp the merge commit with the actual time you click “Merge”. That’s not suitable for our backdating purposes. Git Legends performs merges locally in Git to control the timestamp of the merge commit. Here’s how it works:

When you instruct Git Legends to merge a feature branch into the main branch (with a specified past date), the tool will check out the main branch on your local repo and do a non-fast-forward merge using git merge --no-ff. It sets the GIT_AUTHOR_DATE/GIT_COMMITTER_DATE for this merge commit as well, so the merge commit gets a custom timestamp (say, you want the merge to look like it happened on March 1, 2025). The merge commit message is crafted to mimic GitHub’s style, for example: “Merge pull request #42 from feature-login”. You can even include a merge description if desired, but typically Git Legends keeps it simple and authentic-looking.

After creating this merge commit locally, Git Legends pushes it to the GitHub repository’s main branch. Now a few cool things happen on GitHub: The main branch’s history now contains a merge commit at your specified date (e.g. Mar 1, 2025) with two parents (one parent is the previous main commit, and the other is the tip of the feature branch). This looks just like a normal merge in Git history, except it’s dated in the past.

GitHub will automatically recognize that the commits from the feature branch are now in the main branch. As a result, the pull request for that branch (which was opened earlier) gets marked as “Merged” and closed, even though we merged via the CLI rather than the web UI. In other words, by pushing the merge commit ourselves, we effectively merged the PR. This auto-closure happens because the commit that the PR was supposed to introduce is already in the target branch. (No need to click the merge button on GitHub – we bypassed it.)

One caveat: On the pull request’s page, GitHub will list the “Merged on” date as the time we pushed the merge (which is actually now). This is because GitHub records the action of closing the PR at the current time. However, the commit timeline (the commit dates shown in the repository’s history and graphs) will reflect the backdated timestamp we set on the merge commit. So anyone browsing the repo or looking at the network graph will see the merge at Mar 1, 2025 in the history, consistent with your story.

This approach lets us backdate merge commits and still use pull requests for a collaborative look. Git Legends essentially uses Git’s normal merge mechanics but with custom dates. It’s important to note that this is a regular merge commit in Git – no special hacks beyond setting the date metadata. It will show up in git log with the parents and message as usual. By doing it locally, we retain full control over the timestamp.

$ legends pr merge --branch feature-login --date 2025-03-01 --delete-branch
# Merges feature-login into main with a merge commit dated Mar 1, 2025.
Merged
GitHub automatically detects the pushed merge commit and marks the PR as closed/merged without using the web UI button.
$ gh repo create my-project --source=. --public --push
$ gh pr create --base main --head feature-login --title "Feature: User Login" --body "Adds user login functionality."
$ gh pr review --approve --body "LGTM"

GitHub CLI and API Integration (Automating PRs and Reviews)

Git Legends doesn’t stop at offline Git manipulation – it also hooks into GitHub’s API (often via the official GitHub CLI gh) to automate repository actions like creating repos, opening pull requests, and even simulating code reviews. This is where the tool interacts with GitHub as if it were a user:

Repository Creation: If you start a new project with Git Legends, it can create a remote GitHub repository for you via the GitHub CLI. For example, it might run a command equivalent to gh repo create my-project --source=. --public --push to create the repo and push the initial commit in one go. This saves you from manually creating the repo on GitHub. (You’ll need to be authenticated to GitHub for this to work – more on auth in a moment.)

Opening Pull Requests: Git Legends uses the GitHub CLI to open PRs from your feature branches into the main branch. This is done with a command like gh pr create --base main --head feature-login --title "Feature: User Login" --body "Adds user login functionality." behind the scenes. This creates a pull request on GitHub just as if you went through the web form. The CLI will output the new PR’s URL (or number) on success, and Git Legends captures that. The PR will list all the commits you made on the feature branch (with their backdated timestamps). It essentially automates the PR process so you don’t have to leave the terminal.

Comments and Reviews: While commit dates can be faked, note that PR comments and review timestamps cannot be arbitrarily set via the API – GitHub will timestamp those with the actual current time. Git Legends can post a comment or approval on the PR if you want to simulate a review (for example, auto-approving your own PR for completeness). It might call something like gh pr review --approve --body "LGTM" to approve. The content of the review is up to you, but remember, it will show as happening now. If you want to avoid that discrepancy, you might choose to skip adding comments, or just be aware that the PR’s conversation dates will be real-time.

Closing and Cleaning Up: After a merge, Git Legends can also handle cleanup. It can delete the merged branch on GitHub to keep your repo tidy (equivalent to clicking “Delete branch” after merging a PR). This uses the GitHub API or CLI (e.g., gh pr close <PR> --delete-branch). Since our merge commit is already pushed, the PR is effectively merged; closing it via the API is just a formality if needed. Usually, pushing the merge commit will auto-close the PR as mentioned. The tool is careful to only delete branches you specify to delete.

Authentication and Setup: To use these GitHub features, you do need to authenticate. Git Legends expects you to have the GitHub CLI logged in (via gh auth login) or a Personal Access Token set in the environment. If you’re already logged in with gh, you’re all set. Otherwise, you can provide a token. The integration with GitHub is what allows Legends to go beyond just local Git fiddling and actually reflect the changes on GitHub.com – creating that realistic historical record in your GitHub repository.

Safety, Dry-Run, and Re-runnable Scripts

Rewriting Git history can be a bit scary, so Git Legends is built with safety checks and idempotent design to protect your real work:

Dry-Run Mode: You can run most commands in a “preview” mode (dry-run) where the tool will output the git commands it would run, without actually executing them. This is extremely useful to see what’s going to happen before you modify any repo. For instance, you’ll see that it plans to set certain dates and run specific merges. Once you’re confident, you can run it for real.

No Force-Push Unless You Say So: Git Legends tries to avoid destructive operations. If you are working with a fresh repository that it created, it will handle the initial push. If you use it on an existing repo, it will warn you if a history rewrite (and thus a force-push) might be needed. It won’t force-push unless you explicitly allow it. This ensures you don’t accidentally overwrite teammates’ work or remote history. In general, using Git Legends on a brand new repo or a throwaway test repo is the safest way to experiment, then you can integrate into real projects carefully.

Idempotent Workflow: The CLI is designed so that you can run the same sequence of steps repeatedly without creating conflicts. For example, if a step fails and you run the command again, it will either pick up where it left off or clean up and redo the step. Creating an initial commit with a set date, making a branch, etc., can be retried. If something went wrong (say your internet dropped during a gh pr create call), you can re-run the command and Git Legends will handle the fact that maybe the PR already exists or the branch was already pushed. It checks the state and avoids duplicating actions. This makes it robust for scripting: you can compose a series of Git Legends commands in a shell script or CI job, and if one part fails, you can fix the issue and rerun without ending up with weird duplicate history.

Local Only Option: If you purely want to script commit histories without touching GitHub, you can. Every GitHub-related feature is typically optional. You could use Git Legends to script out a local repo’s history, review it, and only push to GitHub when you’re satisfied. This staged approach ensures you’re comfortable with the result. (In fact, under the hood, everything happens locally first – nothing hits the remote until you push or the CLI uses an API call – so you always have a chance to inspect the local repo state.)

In short, Git Legends tries to make these powerful history manipulations as safe and user-friendly as possible. It’s meant to give you confidence that you won’t mess up your repo while bending time in Git. The tone is friendly – the tool might print hints or warnings like “(Dry Run) Would create commit on 2025-02-01, but not actually doing it.” to guide you.

# Preview mode - no changes made
$ legends commit --dry-run --date 2025-02-01
(Dry Run) Would create commit on 2025-02-01, but not actually doing it.
# 1. Initialize
legends init my-project --date 2024-01-15
# 2. Create Branches
legends branch create feature-A --base main --date 2024-03-01
# 3. Add Backdated Commits
legends commit --branch feature-A --date 2024-03-10
# 4. Open & Merge PRs
legends pr open --branch feature-A ...
legends pr merge --branch feature-A --date 2024-05-15

Example Workflow: Simulating a Long Project History

Let’s put it all together with an example scenario. Suppose you want to create a repository that looks as though it had development activity throughout 2024 and 2025, with two feature branches that were developed in 2024. Create their branches with backdated start points:

This creates a new Git repository locally, adds an initial commit dated Jan 15, 2024, and (if configured) creates a remote GitHub repo my-project and pushes the commit. Now your repo’s history starts in early 2024.

Create Two Feature Branches in the Past: Let’s say you want two parallel features, Feature A and Feature B, that were developed in 2024. Create their branches with backdated start points:

Now feature-A branch has an empty initial commit on Mar 1, 2024, and feature-B starts on Apr 1, 2024. This implies feature A work began in March and feature B in April.

Add Commits to Each Feature Branch: You can switch to each branch and add a series of commits spread over time. For instance:

These commands create three commits on feature-A, dated March 10, March 20, and April 5 of 2024, with the given messages. Then do something similar on feature-B:

Now each branch’s commit history shows a progression of work on those dates. (You can, of course, add as many commits and any content you want – e.g., actually editing files. Git Legends will commit whatever is in your working directory at those steps, or make empty commits if you prefer placeholders.)

Open Pull Requests for the Features: After pushing these branches to GitHub (Git Legends pushes after each commit by default, or you can push at the end), you can create pull requests:

This will use the GitHub CLI to open two PRs on the repo: one for merging feature-A into main, and one for feature-B into main. Each PR will list the commits we created, showing the dates in March/April/May 2024. You can optionally use GitHub’s interface or CLI to add reviewers or comments (for realism, maybe you comment “Ready for review” on each). Even if you don’t, the PR exists to document the merge.

Merge the Pull Requests with Backdated Merges: Now for the grand finale – merging those features into main with historical dates. Say we want Feature A to have been merged on May 15, 2024, and Feature B on June 1, 2024. We run:

Under the hood, Git Legends will check out main, merge feature-A with a commit dated May 15, 2024, push it, and mark the PR as merged (deleting the branch if we asked). Then do the same for feature-B with a merge commit dated June 1, 2024. After this, the main branch history on GitHub shows two merge commits on those dates. The pull request pages will show that they’re merged (on the current date, but that’s a minor detail) and the feature branches are cleaned up.

Outcome – A “Time-Traveled” Repository: You now have a repository whose commit history spans from Jan to June 2024. The initial commit in January, a feature branch A with activity in March/April and merged in May, and feature B with activity in April/May merged in June. If someone looks at the repository on GitHub, they’ll see commits and merges across those months. The contribution graph for whichever authors you used will have entries on those dates (since Git uses the commit dates for that graph). Essentially, you’ve created a legendary timeline for your project within a few minutes! And because all of this used valid Git operations, the repo remains functional and credible – clone it, and you have the exact history as shown.

This example was just two feature branches; you could create many more, or longer timelines. You could even simulate an open-source project by using different author names for different commits (to fake multiple contributors), or backdate an entire existing project if you forgot to use version control until now.

Getting Started with Git Legends

Installation

Git Legends is distributed as a Python package. You can install it with pip:

$ pip install legends

This provides the legends command. Alternatively, you can grab the standalone binary from the GitHub releases if available. Make sure you have Git installed on your system, and if you plan to use GitHub integration features, you should also have the GitHub CLI (gh) installed.

Authentication (for GitHub features)

If you’ll be creating repos or PRs through Git Legends, log in to your GitHub account via the CLI. Run gh auth login and follow the prompts, or set a GITHUB_TOKEN environment variable with a Personal Access Token. This step isn’t needed for purely local history manipulation, but it’s required for anything that touches GitHub (like opening pull requests automatically).

Usage and Commands

Once installed, run legends --help to see the available commands. The CLI is organized by tasks:

  • legends init – initialize a new repo with a backdated first commit.
  • legends branch create – create a new branch with a past start date.
  • legends commit – add a commit to a branch on a specified date (and push it).
  • legends pr open – open a pull request on GitHub for a branch.
  • legends pr merge – perform a merge (locally with backdate, then push) and close the PR.
  • legends workflow run – a higher-level command to run an entire sequence (branch > commits > PR > merge) in one go, if you want to automate the whole thing at once.

Each command has its own flags. For example, legends commit lets you specify --date, --message, --author info, etc. You can chain these commands to script complex histories. For a full reference, check out the documentation on our website or the README in the GitHub repo.

Documentation and Support

We have more detailed docs and examples on the Git Legends GitHub repository (including an FAQ). You can find sample scripts to accomplish various scenarios, like importing a timeline from a CSV of dates, or simulating a year’s worth of activity in one go. If you run into any issues or have questions, feel free to open an issue on GitHub or join our developer Discord channel. Since Git Legends is an open-source project, we also welcome contributions – if you have ideas for new features or improvements, you can send a pull request!

Git Legends empowers you to take control of Git history in a way that wasn’t easily possible before. Whether you’re a software engineer reconstructing lost commits, a DevOps engineer creating realistic test repositories, or an open-source developer presenting a project timeline, this tool gives you a friendly yet powerful interface to bend Git’s timeline to your will. And it does so using Git’s own mechanisms, so the end result is indistinguishable from a “natural” commit history. Give it a try, and craft your own legendary commit story!