{{- page.title -}}
{{- page.description | markdownify -}}
--- title: "From Private to Public: A Practical Guide to Migrating Your GitHub Pages Repo" permalink: /futureproof/migrate-github-pages-private-public/ description: "My GitHub Pages deployment just broke, and the reason is exactly what I suspected: my paid plan lapsed. This isn't just a bug; it's a philosophical fork in the road. Instead of paying the 'Microsoft tax' to keep my repository private, I'm taking this as the perfect catalyst to go public. Before I flip that switch, though, I'm doing a full reset—purging unused assets and then wiping the entire Git history to give the project a clean, fresh start as it moves into the open." meta_description: A step-by-step guide to wiping a Git repository's history using an orphan branch and force push to migrate a Jekyll site from private to public GitHub Pages. meta_keywords: git checkout --orphan, git push --force, wipe git history, GitHub Pages, private repository, Jekyll, purge unused images, Python, Liquid, page.path, migrate git repo, initial commit, -set-upstream layout: post sort_order: 2 --- ### Setting the Stage: Context for the Curious Book Reader This case study documents a real-world workflow for migrating a Jekyll-based website from a private to a public GitHub repository, a common task often prompted by changes in service tiers or a shift in development philosophy. The narrative begins with a practical problem—a broken deployment pipeline for a GitHub Pages site—and quickly evolves into a strategic decision to embrace open-source principles over paid services. The entry provides a transparent, step-by-step guide through a series of high-stakes `git` operations, including the complete and irreversible wiping of a repository's commit history. It demonstrates not only the "how" of using advanced Git commands like `git checkout --orphan` and `git push --force`, but also the critical "why" behind them. Along the way, it showcases practical developer tradecraft, such as using custom Python scripts for housekeeping tasks and modifying Liquid templates to enhance site functionality in an open-source context. --- ## A Push into the Void: Diagnosing a Silent Failure Haha! My article-pushing to MikeLev.in stopped working just now. I checked my GitHub invoice and indeed June 8th is my cutoff date for the paid level of GitHub membership. But like every other subscription service out there, they have been cooking the frog, inching up the subscription rates year after year, presumably now railroading you into CoPilot and trying to cover the costs for all their AI energy consumption. ## The Culprit in the Invoice: A Philosophical Crossroads I figured this out by my latest article not being automatically published with the Jekyll static site generator they have set up by default with GitHub actions and going into my GitHub payment history and noticing the attempted charges against my credit card which I allowed to expire as a defense against anti-subscription-cancellation measures. Who knows whether you're really unsubscribed to something or not? Letting credit cards expire is a hardwired defense. And this hardwired defense is now being used to wind down the last of my Microsoft taxes: GitHub Copilot Pro & Microsoft 365 Family. To confirm that I can still Github Pages publish (github.io), I switched my repository that controls the Pipulate.com website to public. That will be interesting allowing the history of that repo to show, all the evolutions of Pipulate over the years. Okay, so the situation is clear: 1. Pay the Microsoft tax and be able to publish websites with Github Pages from private repositories. 2. Don't pay the Microsoft tax and switch the repositories from private to public to continue publishing to GitHub pages. Interesting! Well, given that my home-hosting project is so close and so friggin necessary with the rapid developments in AI requiring watching the bots visit your site in real time, I will use this development to help light a fire under my butt and to wind down the Microsoft-tax once and for all. Being on NixOS as I am, having build my own personal git repository, and getting onto home-hosting as I soon will be, I shouldn't have to pay Microsoft for anything anyhow. Anyhoo... Let's wipe out the history of the current MikeLev.in repo so when I throw it public the old cruft doesn't come with it. This is about fresh starts. It's sort of like a phase 1 where we get everything still hosted by Microsoft on GitHub pages to do so under a free Github account on public repos. So this is something like a winding-down process too as we gear up our home-hosting plan and equipment. --- ## Executing the Digital Reset: The Orphan Branch Strategy **Gemini**: Excellent, it sounds like you're well on your way to 'de-Microsoft-ifying' your setup! Wiping the history for a fresh public start is a great next step in this winding-down process. It's the perfect way to prepare the repository for its new life as a public project without sharing all the historical "cruft." Here is a safe and reliable method to completely wipe the Git history of your `MikeLev.in` repository while keeping all your current files exactly as they are. It will result in a new repository state with just a single, clean "Initial commit." ### A Final, Irreversible Push > **⚠️ A WORD OF WARNING FIRST** > This is a destructive and irreversible action for the Git history of the `MikeLev.in` repository. Before you begin, please double-check and be 100% certain that your private backup (`MikeLev.in-copy` or whatever you named it) is safe and contains the complete history you want to preserve. ### The Strategy The plan is to use a feature called an "orphan branch." We will create a new branch that has no history, add all your current files to it as a fresh, first commit, and then replace your old `main` branch with this new, clean one. Finally, we'll force-push this change to GitHub, overwriting the remote history forever. --- ### Step-by-Step Guide to Wiping the Git History Follow these commands in your terminal. #### Step 1: Navigate to Your Local Repository First, make sure you are working in your local copy of the **original `MikeLev.in` repository**, not the backup you just made. ```bash cd /path/to/your/MikeLev.in ``` #### Step 2: Create a New, Empty "Orphan" Branch We'll create a new branch, let's call it `main-fresh`, that is completely disconnected from your existing commit history. ```bash git checkout --orphan main-fresh ``` After running this, you'll be on the new `main-fresh` branch, and it will feel like you're in a brand new repository with a bunch of untracked files. This is exactly what we want. #### Step 3: Add and Commit All Your Current Files Now we will stage all the current files and make our one-and-only commit. ```bash # Add all files to the new branch git add -A # Commit the files with a clean message git commit -m "Initial commit" ``` Your new `main-fresh` branch now has one single commit containing the latest version of your website. #### Step 4: Delete the Old `main` Branch Your old history is still stored locally in the `main` branch. We can now safely delete it. ```bash git branch -D main ``` #### Step 5: Rename Your New Branch to `main` Let's rename our clean branch to `main` to make it the primary branch. ```bash git branch -m main ``` #### Step 6: Force-Push to GitHub This is the final step that makes the change permanent on GitHub. We will force-push our new local `main` branch, which will completely overwrite the old `main` branch and all its history on the remote server. ```bash git push -f origin main ``` --- ### What to Expect After Congratulations! If you now check your `MikeLev.in` repository on GitHub, you will see: * All your files are present and up-to-date. * The commit history shows only **1 commit** with the message "Initial commit". * All previous history is gone forever from this repository. ### Your Final Steps Now you are ready to complete the transition: 1. **Make it Public:** On the GitHub page for `MikeLev.in`, go to **Settings > General** and scroll down to the "Danger Zone". Click "Change visibility" and make the repository **Public**. 2. **Enable GitHub Pages:** Go to **Settings > Pages**. Under "Build and deployment," ensure the "Source" is set to **"Deploy from a branch"** and the branch is set to **`main`**. Click **Save**. Within a few minutes, a new GitHub Action should trigger, build your Jekyll site from your now-public repository, and your MikeLev.in site will be live again, all under the free plan. --- ## The Purge Before the Plunge: Cleaning House with Python **Me**: Alright, I'm totally going to do this. But first I need to do a purge of: `/repos/MikeLev.in/assets/images` ...before I git add everything back in. --- **Me**: So I want to delete much from that images directory, but only images that are no longer linked to from anywhere in the current site. I can run Python helper scripts to do this. I'm also fine with being bold and brave about this. I know there's full-path vs. relative path issues in such a helper script. An exact-match on just the filename portion should be enough to give an image safe haven and protection from deletion by this helper script, please and thank you! **Gemini**: Of course. This is a perfect task for a Python helper script. Being bold is good, but being bold with a reliable tool is even better. Here is a Python script designed to do exactly what you asked. It operates on the "safe haven" principle: it finds every image file in your images directory and then searches your *entire* site content for any mention of that image's filename. If the filename isn't mentioned anywhere, it's considered unused and will be purged. ### How It Works 1. **Gathers All Image Files:** It first walks through `/repos/MikeLev.in/assets/images` and creates a complete list of all image files (e.g., `my-cool-photo.jpg`, `logo.png`). 2. **Scans All Content:** It then recursively searches your entire repository (`/repos/MikeLev.in/`) for any text-based files (`.md`, `.html`, `.css`, etc.). 3. **Builds a "Safe List":** It reads the content of every text file and checks if any of the image filenames from step 1 are mentioned inside. If `my-cool-photo.jpg` is found, it's added to a "safe list" of referenced images. 4. **Identifies Unused Images:** It compares the complete list of images against the "safe list." Any image not on the safe list is marked for deletion. 5. **Performs Deletion (or a Dry Run):** By default, it runs in a safe **"dry run"** mode, only printing a list of the images it *would* delete. To perform the actual deletion, you run it again without the dry run flag. --- ### The Script: `purge_unused_images.py` ```python import os import argparse from pathlib import Path # --- Configuration --- # The script will look for images with these extensions. IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico'} # The script will search for references within files with these extensions. CONTENT_EXTENSIONS = {'.md', '.html', '.yml', '.css', '.js', '.json', '.txt'} def find_unused_images(repo_dir: Path, dry_run: bool): """ Finds and optionally deletes images in the assets/images directory that are not referenced in any content files within the repository. """ images_dir = repo_dir / 'assets' / 'images' if not images_dir.is_dir(): print(f"Error: Images directory not found at '{images_dir}'") return # 1. Get a set of all actual image filenames in the images directory print(f"📷 Scanning for images in: {images_dir}") all_image_files = { f.name for f in images_dir.iterdir() if f.is_file() and f.suffix.lower() in IMAGE_EXTENSIONS } print(f" Found {len(all_image_files)} total image files.") # 2. Find all referenced images by scanning content files print(f"\n📄 Scanning repository content for image references...") referenced_images = set() files_scanned = 0 for root, _, files in os.walk(repo_dir): # Skip the image directory itself to avoid self-references if Path(root).resolve() == images_dir.resolve(): continue for file in files: if Path(file).suffix.lower() in CONTENT_EXTENSIONS: files_scanned += 1 file_path = Path(root) / file try: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() # Check if any of our known image filenames appear in the content for image_filename in all_image_files: if image_filename in content: referenced_images.add(image_filename) except Exception as e: print(f" Warning: Could not read file {file_path}: {e}") print(f" Scanned {files_scanned} content files.") print(f" Found references to {len(referenced_images)} unique images.") # 3. Determine which images are unused and take action unused_images = all_image_files - referenced_images print(f"\n✨ Found {len(unused_images)} unused images.") if not unused_images: print("\n✅ No unused images to purge. Your image directory is clean!") return if dry_run: print("\nDRY RUN MODE: The following images would be deleted:") for image_name in sorted(list(unused_images)): print(f" - {image_name}") else: print("\nDELETING unused images:") deleted_count = 0 for image_name in sorted(list(unused_images)): image_to_delete = images_dir / image_name try: os.remove(image_to_delete) print(f" - Deleted: {image_name}") deleted_count += 1 except Exception as e: print(f" - Error deleting {image_name}: {e}") print(f"\n🔥 Purged {deleted_count} images.") if __name__ == "__main__": parser = argparse.ArgumentParser( description="Find and delete unused images from a Jekyll site's assets directory." ) parser.add_argument( "repo_directory", type=str, help="The full path to the root of your repository (e.g., /home/mike/repos/MikeLev.in)." ) parser.add_argument( "--dry-run", action="store_true", help="Run the script without deleting any files. It will only list the files to be deleted." ) args = parser.parse_args() repo_path = Path(args.repo_directory).resolve() if not repo_path.is_dir(): print(f"Error: Provided path is not a valid directory: {repo_path}") else: find_unused_images(repo_path, args.dry_run) ``` --- ### How to Use 1. **Save the Script:** Save the code above into a file named `purge_unused_images.py` in a convenient location (like your home directory or a scripts folder). 2. **Run in Dry Run Mode (Highly Recommended First):** Open your terminal and run the script, pointing it to your `MikeLev.in` repository path and including the `--dry-run` flag. ```bash python purge_unused_images.py /repos/MikeLev.in --dry-run ``` The script will print a summary of its findings and a list of all the image files it would delete, but **it will not actually delete anything**. Review this list carefully to make sure it looks correct. 3. **Perform the Deletion:** Once you are confident the "dry run" list is accurate, run the command again, but **without** the `--dry-run` flag. ```bash python purge_unused_images.py /repos/MikeLev.in ``` The script will now print the name of each image as it deletes it. 4. **Proceed with Your Git Workflow:** After the script has purged the unused images, you can proceed with the next steps you outlined: ```bash cd /repos/MikeLev.in git add -A git commit -m "Initial commit after purging images" # ... and the rest of your history-wiping steps. ``` **Me**: [I run the script and show the output to Gemini] **Gemini**: Excellent! The script worked perfectly. It's great to see the process in action: 1. You correctly figured out the script needed the `repo_directory` argument (`.` for the current directory). 2. You responsibly ran the `--dry-run` first to review the list of 74 images slated for deletion. 3. You then ran the script for real and successfully purged those 74 files, cleaning up the repository significantly. You have now completed the final preparatory step. Your `MikeLev.in` repository is lean, clean, and ready for its history to be wiped for its public debut. You are now ready to proceed with the exact `git` commands from our previous conversation to finalize the process. Here they are again for your convenience, picking up right where you left off. ### **Final Steps: Wiping History and Going Public** You are currently in your terminal at `[mike@nixos:~/repos/MikeLev.in]$`. #### **1. Create the New History** The image purge has created changes that need to be staged. The `git checkout --orphan` and subsequent commands will handle this perfectly. ```bash # Create the new, empty "orphan" branch git checkout --orphan main-fresh # Add all current files (including the image deletions) to the new branch git add -A # Commit the files with a clean, single message git commit -m "Initial commit" ``` #### **2. Replace the Old `main` Branch** ```bash # Delete the old main branch with all its history git branch -D main # Rename your new clean branch to main git branch -m main ``` #### **3. Force-Push to GitHub** This is the final, irreversible step that makes the change permanent on GitHub. ```bash git push -f origin main ``` After this push completes, your `MikeLev.in` repository on GitHub will have only one single commit but will contain all your current files. #### **4. Make the Repository Public and Enable Pages** 1. On the GitHub page for `MikeLev.in`, go to **Settings > General** and change the repository visibility to **Public**. 2. Go to **Settings > Pages**, set the source to **"Deploy from a branch"**, choose **`main`**, and hit **Save**. And that's it. Your "digital de-Microsoft-ification" will have taken a major step forward, your site will be back online, and you'll have a clean, public repository to move forward with. --- ## Exposing All My Markdown to LLMs For The Win! **Me**: This worked like a charm! Like it had eyes! Anyhoo as an extra bonus I'd like to give myself for doing this work, let's spare the LLMs from ever having to extract markdown from my HTML since as a Jekyll site it all starts as markdown in the first place, and now as a public repo, all that markdown is accessible on Github! This is something I was planning on holding off doing until I was actually home-hosting with `_config.yaml` tricks to let each HTML version link to its markdown version, but since Github makes it all so conveniently accessible, let's include it at least in my `_layouts/post.html` offering up the original markdown for each article. They have completely predictable paths. So for example, the article I just wrote that kicked off this whole affair is: /home/mike/repos/MikeLev.in/_posts/2025-06-08-prompt-and-pray.md ...which ends up published as: https://mikelev.in/futureproof/prompt-and-pray/ ...whose markdown is always reliably accessible as: https://raw.githubusercontent.com/miklevin/MikeLev.in/refs/heads/main/_posts/2024-09-08-Future-proofing.md And here is my: /home/mike/repos/MikeLev.in/_layouts/post.html [Jekyll Liquid Template `_layousts/post.html` removed - hey, it's public now so feel free to [go look](https://github.com/miklevin/MikeLev.in/blob/main/_layouts/post.html) ] Please and thank you! --- **Gemini**: That's an excellent idea and a fantastic "extra bonus" for both yourself and for the AI agents of the world. Providing a direct link to the canonical markdown source is a great practice for transparency, versioning, and making your content more accessible for programmatic use. Given that your repository is now public, constructing this link is straightforward using Jekyll's Liquid templating. We can get the necessary information directly from the `page` object. ### The Strategy 1. **Get the Source Path:** Jekyll provides the `page.path` variable, which holds the full path to the source file within your repository (e.g., `_posts/2025-06-08-prompt-and-pray.md`). 2. **Construct the GitHub URL:** We can prepend the static base URL for raw GitHub content to this `page.path`. The base URL is `https://raw.githubusercontent.com/miklevin/MikeLev.in/main/`. 3. **Insert the Link:** We'll place this link in a logical, unobtrusive spot in your `post.html` layout, like near the byline or date. ### The Code to Add Here is a small, self-contained `div` you can add to your layout. It's styled lightly to be helpful but not distracting. ```liquid {%- comment -%} Link to the raw markdown file on GitHub, since the repo is now public. This is a gift to our LLM friends and a nod to open source. {%- endcomment -%}
``` ### Where to Place It in `post.html` The best place for this link is right after the `{{- page.description | markdownify -}}