<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lance Nehring</title>
    <description>The latest articles on DEV Community by Lance Nehring (@lance_nehring).</description>
    <link>https://dev.to/lance_nehring</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1903613%2Fdd93d705-3acf-4fce-9682-619fd0501996.jpg</url>
      <title>DEV Community: Lance Nehring</title>
      <link>https://dev.to/lance_nehring</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lance_nehring"/>
    <language>en</language>
    <item>
      <title>Git Worktrees: Replace Your Pile of Clones with One Manageable Repository</title>
      <dc:creator>Lance Nehring</dc:creator>
      <pubDate>Sun, 26 Jul 2026 15:23:28 +0000</pubDate>
      <link>https://dev.to/lance_nehring/git-worktrees-replace-your-pile-of-clones-with-one-manageable-repository-hc0</link>
      <guid>https://dev.to/lance_nehring/git-worktrees-replace-your-pile-of-clones-with-one-manageable-repository-hc0</guid>
      <description>&lt;h2&gt;
  
  
  Table Of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What is a Git worktree?&lt;/li&gt;
&lt;li&gt;Why use worktrees instead of several clones?&lt;/li&gt;
&lt;li&gt;A practical directory convention&lt;/li&gt;
&lt;li&gt;Everyday Git worktree commands&lt;/li&gt;
&lt;li&gt;Consolidating several independent clones&lt;/li&gt;
&lt;li&gt;Safety rules before starting&lt;/li&gt;
&lt;li&gt;Phase 1: Inventory every clone&lt;/li&gt;
&lt;li&gt;Phase 2: Choose the canonical repository&lt;/li&gt;
&lt;li&gt;Phase 3: Decide what each clone should become&lt;/li&gt;
&lt;li&gt;Phase 4: Convert one clone&lt;/li&gt;
&lt;li&gt;Moving a worktree&lt;/li&gt;
&lt;li&gt;Recovering a deleted .git worktree file&lt;/li&gt;
&lt;li&gt;When should a worktree be locked?&lt;/li&gt;
&lt;li&gt;When should git worktree prune be used?&lt;/li&gt;
&lt;li&gt;Final validation&lt;/li&gt;
&lt;li&gt;Quick reference&lt;/li&gt;
&lt;li&gt;Closing thoughts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have you ever ended up with a directory structure like this?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/src/project
~/src2/project
~/src3/project
~/src4/project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each directory started innocently enough.&lt;/p&gt;

&lt;p&gt;One was for &lt;code&gt;main&lt;/code&gt;. Another was for a feature branch. A third contained a half-finished experiment. The fourth had several untracked test files you were afraid to lose.&lt;/p&gt;

&lt;p&gt;Eventually, each clone had its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stale view of the remote repository,&lt;/li&gt;
&lt;li&gt;duplicated Git history,&lt;/li&gt;
&lt;li&gt;local-only commits,&lt;/li&gt;
&lt;li&gt;modified files,&lt;/li&gt;
&lt;li&gt;ignored test artifacts,&lt;/li&gt;
&lt;li&gt;and unknown relationship to the others.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Git worktrees are designed to solve this problem.&lt;/p&gt;

&lt;p&gt;A worktree gives you multiple checked-out working directories backed by one shared Git repository. Each working directory can have its own branch and uncommitted changes, while commits, branches, tags, remotes, and fetched objects remain shared.&lt;/p&gt;

&lt;p&gt;This article covers two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How to use Git worktrees during normal development.&lt;/li&gt;
&lt;li&gt;How to safely consolidate several independent clones into one worktree-based layout without losing local work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The shell examples are written to work in both &lt;strong&gt;Bash and zsh&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a Git worktree?
&lt;/h2&gt;

&lt;p&gt;A normal Git clone contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the object database,&lt;/li&gt;
&lt;li&gt;commit history,&lt;/li&gt;
&lt;li&gt;branches,&lt;/li&gt;
&lt;li&gt;tags,&lt;/li&gt;
&lt;li&gt;remotes,&lt;/li&gt;
&lt;li&gt;remote-tracking references,&lt;/li&gt;
&lt;li&gt;and one checked-out working directory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A linked worktree adds another checked-out working directory to that same repository.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/src/project                         main
~/src/project-FEATURE-123             FEATURE-123
~/src/project-HOTFIX-456              HOTFIX-456
~/src/project-large-restructure       large-restructure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each worktree has its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checked-out branch or detached &lt;code&gt;HEAD&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;modified files,&lt;/li&gt;
&lt;li&gt;untracked files,&lt;/li&gt;
&lt;li&gt;ignored files,&lt;/li&gt;
&lt;li&gt;staging area,&lt;/li&gt;
&lt;li&gt;merge state,&lt;/li&gt;
&lt;li&gt;and rebase state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worktrees share:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;commit objects,&lt;/li&gt;
&lt;li&gt;local branches,&lt;/li&gt;
&lt;li&gt;tags,&lt;/li&gt;
&lt;li&gt;remotes,&lt;/li&gt;
&lt;li&gt;remote-tracking references,&lt;/li&gt;
&lt;li&gt;and the results of &lt;code&gt;git fetch&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction is important.&lt;/p&gt;

&lt;p&gt;A worktree is not merely another directory pointing at the same files. It is an independent working area backed by shared repository metadata.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why use worktrees instead of several clones?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Commits are immediately visible everywhere
&lt;/h3&gt;

&lt;p&gt;Suppose you commit something in a feature worktree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project-FEATURE-123"&lt;/span&gt; commit &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="s2"&gt;"Complete FEATURE-123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commit and updated branch are immediately visible from the main worktree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project"&lt;/span&gt; log FEATURE-123 &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no local push-and-fetch cycle between directories.&lt;/p&gt;

&lt;h3&gt;
  
  
  One fetch updates every worktree
&lt;/h3&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project"&lt;/span&gt; fetch origin &lt;span class="nt"&gt;--prune&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every linked worktree immediately sees the updated &lt;code&gt;origin/*&lt;/code&gt; references.&lt;/p&gt;

&lt;p&gt;With independent clones, every clone must be fetched separately, and each clone can have a different idea of what &lt;code&gt;origin/main&lt;/code&gt; means.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git prevents one branch from being checked out twice
&lt;/h3&gt;

&lt;p&gt;Git normally refuses to check out the same local branch in two linked worktrees at once.&lt;/p&gt;

&lt;p&gt;That protects you from independently modifying one branch from two directories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Repository history is not duplicated
&lt;/h3&gt;

&lt;p&gt;Each worktree has a separate checked-out filesystem tree, but the object database and Git history are shared.&lt;/p&gt;

&lt;p&gt;For a large repository, that can save substantial disk space.&lt;/p&gt;

&lt;h3&gt;
  
  
  You get a central inventory
&lt;/h3&gt;

&lt;p&gt;Run this from any worktree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/user/src/project                    81f9d2a [main]
/home/user/src/project-FEATURE-123        f27ae91 [FEATURE-123]
/home/user/src/project-HOTFIX-456         6b812b0 [HOTFIX-456]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For script-friendly output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list &lt;span class="nt"&gt;--porcelain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  A practical directory convention
&lt;/h2&gt;

&lt;p&gt;Git does not impose a directory naming convention, but this one is easy to understand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/src/&amp;lt;repository&amp;gt;                 main
~/src/&amp;lt;repository&amp;gt;-&amp;lt;branch&amp;gt;        another branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/src/project
~/src/project-FEATURE-123
~/src/project-HOTFIX-456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Branch names containing &lt;code&gt;/&lt;/code&gt; should usually be converted to filesystem-friendly names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature/new-api  -&amp;gt;  project-feature-new-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worktree directory name does not need to match the branch name exactly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Everyday Git worktree commands
&lt;/h2&gt;

&lt;p&gt;Before getting into migration, here are the commands you will use during normal development.&lt;/p&gt;

&lt;h3&gt;
  
  
  List worktrees
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add an existing local branch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add &lt;span class="se"&gt;\&lt;/span&gt;
    ../project-FEATURE-123 &lt;span class="se"&gt;\&lt;/span&gt;
    FEATURE-123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a new branch and worktree
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-b&lt;/span&gt; FEATURE-123 &lt;span class="se"&gt;\&lt;/span&gt;
    ../project-FEATURE-123 &lt;span class="se"&gt;\&lt;/span&gt;
    main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates &lt;code&gt;FEATURE-123&lt;/code&gt; from &lt;code&gt;main&lt;/code&gt; and checks it out in the new directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a worktree from a remote branch
&lt;/h3&gt;

&lt;p&gt;Update the remote-tracking references:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin &lt;span class="nt"&gt;--prune&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create the local branch and worktree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-b&lt;/span&gt; FEATURE-123 &lt;span class="se"&gt;\&lt;/span&gt;
    ../project-FEATURE-123 &lt;span class="se"&gt;\&lt;/span&gt;
    origin/FEATURE-123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a detached worktree for temporary testing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    ../project-test &lt;span class="se"&gt;\&lt;/span&gt;
    main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A detached worktree is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;running a build against an old commit,&lt;/li&gt;
&lt;li&gt;reviewing a release tag,&lt;/li&gt;
&lt;li&gt;reproducing a bug,&lt;/li&gt;
&lt;li&gt;or performing disposable testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a named branch instead if you may want to retain commits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Work normally inside a worktree
&lt;/h3&gt;

&lt;p&gt;Once created, a worktree behaves like an ordinary Git working directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../project-FEATURE-123

git status
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit
git pull &lt;span class="nt"&gt;--rebase&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do not need special worktree versions of ordinary Git commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rebase a feature branch onto updated main
&lt;/h3&gt;

&lt;p&gt;Update the main worktree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; ../project fetch origin &lt;span class="nt"&gt;--prune&lt;/span&gt;

git &lt;span class="nt"&gt;-C&lt;/span&gt; ../project merge &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--ff-only&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then rebase the feature worktree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; ../project-FEATURE-123 rebase main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the worktrees share local branches, the feature worktree immediately sees the updated &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remove a worktree
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree remove ../project-FEATURE-123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git normally refuses to remove a worktree containing uncommitted changes.&lt;/p&gt;

&lt;p&gt;This is much safer than deleting the directory manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  Consolidating several independent clones
&lt;/h2&gt;

&lt;p&gt;Suppose you currently have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/src/project
~/src2/project
~/src3/project
~/src4/project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The target layout is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/src/project                         canonical repository and main
~/src/project-FEATURE-123             linked worktree
~/src/project-HOTFIX-456              linked worktree
~/src/project-large-restructure       linked worktree
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The safe approach is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inventory every clone.&lt;/li&gt;
&lt;li&gt;Choose one canonical repository.&lt;/li&gt;
&lt;li&gt;Update the canonical repository carefully.&lt;/li&gt;
&lt;li&gt;Convert one clone at a time.&lt;/li&gt;
&lt;li&gt;Import the source clone's exact branch and commit.&lt;/li&gt;
&lt;li&gt;Rename the source clone to a backup.&lt;/li&gt;
&lt;li&gt;Create a linked worktree.&lt;/li&gt;
&lt;li&gt;Copy the complete working-directory state.&lt;/li&gt;
&lt;li&gt;Compare the old clone and new worktree.&lt;/li&gt;
&lt;li&gt;Delete the backup only after testing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not try to automate the entire migration in one opaque script.&lt;/p&gt;

&lt;p&gt;Convert one work area, verify it, and then continue.&lt;/p&gt;




&lt;h2&gt;
  
  
  Safety rules before starting
&lt;/h2&gt;

&lt;p&gt;A few rules prevent most migration disasters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the source clone as a backup
&lt;/h3&gt;

&lt;p&gt;Do not delete an old clone during migration.&lt;/p&gt;

&lt;p&gt;Rename it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source_repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only delete the backup after the new worktree has been compared, built, and tested.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not use &lt;code&gt;exit&lt;/code&gt; in pasted interactive snippets
&lt;/h3&gt;

&lt;p&gt;A command such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is reasonable inside a standalone script.&lt;/p&gt;

&lt;p&gt;It is not friendly in a command block intended to be pasted into an interactive terminal because it may close the shell session.&lt;/p&gt;

&lt;p&gt;The examples in this article print errors and rely on the engineer to stop before continuing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use braced variables in Bash and zsh
&lt;/h3&gt;

&lt;p&gt;When text immediately follows a variable, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:refs/heads/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;:refs/heads/&lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The braced form is valid in both Bash and zsh.&lt;/p&gt;

&lt;p&gt;This matters particularly when a variable is followed by &lt;code&gt;:&lt;/code&gt;. Zsh can interpret the colon as part of parameter-expansion syntax and produce a malformed Git refspec.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remember that a linked worktree has a &lt;code&gt;.git&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;A normal clone has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.git/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A linked worktree usually has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;.git&lt;/code&gt; entry is a text file pointing back to the shared repository metadata.&lt;/p&gt;

&lt;p&gt;This difference becomes important when using &lt;code&gt;rsync&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: Inventory every clone
&lt;/h2&gt;

&lt;p&gt;Define the known clone paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;repos&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project"&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src2/project"&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src3/project"&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src4/project"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This array syntax works in both Bash and zsh.&lt;/p&gt;

&lt;p&gt;Inventory each clone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;repo &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;echo
    echo&lt;/span&gt; &lt;span class="s2"&gt;"================================================================"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Repository: &lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"================================================================"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse &lt;span class="nt"&gt;--git-dir&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"NOT A GIT REPOSITORY"&lt;/span&gt;
        &lt;span class="k"&gt;continue
    fi

    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"Branch:       "&lt;/span&gt;
    git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; branch &lt;span class="nt"&gt;--show-current&lt;/span&gt;

    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"HEAD:         "&lt;/span&gt;
    git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse HEAD

    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"Origin:       "&lt;/span&gt;
    git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; remote get-url origin 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"(none)"&lt;/span&gt;

    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"Upstream:     "&lt;/span&gt;
    git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--abbrev-ref&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--symbolic-full-name&lt;/span&gt; &lt;span class="s1"&gt;'@{upstream}'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"(none)"&lt;/span&gt;

    &lt;span class="nb"&gt;echo
    echo&lt;/span&gt; &lt;span class="s2"&gt;"Status:"&lt;/span&gt;
    git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; status &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--short&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--branch&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--untracked-files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all

    &lt;span class="nb"&gt;echo
    echo&lt;/span&gt; &lt;span class="s2"&gt;"Recent commits:"&lt;/span&gt;
    git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; log &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--decorate&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each clone, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;directory,&lt;/li&gt;
&lt;li&gt;checked-out branch,&lt;/li&gt;
&lt;li&gt;exact &lt;code&gt;HEAD&lt;/code&gt; commit,&lt;/li&gt;
&lt;li&gt;origin URL,&lt;/li&gt;
&lt;li&gt;upstream branch,&lt;/li&gt;
&lt;li&gt;modified files,&lt;/li&gt;
&lt;li&gt;deleted files,&lt;/li&gt;
&lt;li&gt;untracked files,&lt;/li&gt;
&lt;li&gt;and local-only commits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Do not forget ignored files
&lt;/h3&gt;

&lt;p&gt;Normal &lt;code&gt;git status&lt;/code&gt; hides ignored files.&lt;/p&gt;

&lt;p&gt;Check them separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;repo &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;echo
    echo&lt;/span&gt; &lt;span class="s2"&gt;"===== &lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;: ignored files ====="&lt;/span&gt;

    git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; status &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--short&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--ignored&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;matching &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--untracked-files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all |
        &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^!!'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ignored files can still be important.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local configuration,&lt;/li&gt;
&lt;li&gt;test credentials,&lt;/li&gt;
&lt;li&gt;IDE settings,&lt;/li&gt;
&lt;li&gt;generated test fixtures,&lt;/li&gt;
&lt;li&gt;build caches,&lt;/li&gt;
&lt;li&gt;and environment-specific inventory files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need to decide whether each one should be preserved.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2: Choose the canonical repository
&lt;/h2&gt;

&lt;p&gt;Normally, the clone already used for &lt;code&gt;main&lt;/code&gt; becomes the canonical repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;canonical&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fetch the current remote state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; fetch origin &lt;span class="nt"&gt;--prune&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect local and remote &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; status &lt;span class="nt"&gt;--short&lt;/span&gt; &lt;span class="nt"&gt;--branch&lt;/span&gt;

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; log &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--decorate&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--graph&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-10&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    main origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check for untracked-file collisions
&lt;/h3&gt;

&lt;p&gt;Before fast-forwarding, inspect incoming paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; diff &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name-status&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    HEAD..origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A future tracked file may collide with a local untracked file at the same path.&lt;/p&gt;

&lt;p&gt;Git will normally refuse to overwrite it, but the safer workflow is to preserve the local file explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;pre_update_backup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/project-pre-update-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d-%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pre_update_backup&lt;/span&gt;&lt;span class="s2"&gt;/path/to"&lt;/span&gt;

&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;/path/to/local-file.yml"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pre_update_backup&lt;/span&gt;&lt;span class="s2"&gt;/path/to/local-file.yml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then fast-forward only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; merge &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--ff-only&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare the saved local file with the newly tracked version before restoring anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3: Decide what each clone should become
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Clone on a different branch with useful work
&lt;/h3&gt;

&lt;p&gt;Convert it into a linked worktree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Another clone of &lt;code&gt;main&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Git normally permits a local branch to be checked out in only one worktree.&lt;/p&gt;

&lt;p&gt;If the duplicate clone is clean and contains nothing unique, retire it.&lt;/p&gt;

&lt;p&gt;Check for local-only commits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source_repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; log &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    origin/main..main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check tracked, untracked, and ignored files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source_repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; status &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--short&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--ignored&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;matching &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--untracked-files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the duplicate &lt;code&gt;main&lt;/code&gt; clone is being used as a second test environment, create either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a local alias branch, or&lt;/li&gt;
&lt;li&gt;a detached worktree.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local alias example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; branch &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nb"&gt;local&lt;/span&gt;/second-main-testing &lt;span class="se"&gt;\&lt;/span&gt;
    main

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; worktree add &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project-second-main-testing"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nb"&gt;local&lt;/span&gt;/second-main-testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detached example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; worktree add &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project-main-test"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a named branch if commits may need to be retained.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 4: Convert one clone
&lt;/h2&gt;

&lt;p&gt;The following example converts one source clone into one linked worktree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set migration variables
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;canonical&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project"&lt;/span&gt;
&lt;span class="nv"&gt;source_repo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src2/project"&lt;/span&gt;
&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"FEATURE-123"&lt;/span&gt;
&lt;span class="nv"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project-FEATURE-123"&lt;/span&gt;
&lt;span class="nv"&gt;backup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src2/project.pre-worktree-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d-%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;migration_ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"refs/remotes/migrate/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source_repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; branch &lt;span class="nt"&gt;--show-current&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source_repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse HEAD

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source_repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; status &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--short&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--branch&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--untracked-files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;git branch --show-current&lt;/code&gt; prints nothing, the source clone is in a detached &lt;code&gt;HEAD&lt;/code&gt; state. Investigate before continuing unless a detached result is intentional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Import the exact source branch
&lt;/h3&gt;

&lt;p&gt;Do not assume the branch in the source clone matches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the canonical repository's local branch,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;origin/&amp;lt;branch&amp;gt;&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;or the latest remote commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Import the source clone's exact branch into a temporary reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; fetch &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source_repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"refs/heads/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;migration_ref&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also imports local-only commits that have never been pushed.&lt;/p&gt;

&lt;p&gt;If the local branch does not already exist in the canonical repository, create it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; show-ref &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--verify&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="s2"&gt;"refs/heads/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Local branch already exists:"&lt;/span&gt;
    git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Creating local branch from imported source"&lt;/span&gt;
    git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; branch &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$migration_ref&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now compare the two branch tips:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Source clone:  '&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source_repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse HEAD

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Canonical:     '&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hashes must be identical before continuing.&lt;/p&gt;

&lt;p&gt;Do not blindly reset either repository if they differ. Investigate why the branch histories are different.&lt;/p&gt;

&lt;p&gt;After verification, remove the temporary migration reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; update-ref &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$migration_ref&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rename the old clone
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Backup path: &lt;/span&gt;&lt;span class="nv"&gt;$backup&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source_repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm that the backup is still a valid Git repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse HEAD

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; status &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--short&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--branch&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--untracked-files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the linked worktree
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; worktree add &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare the starting commits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Old clone:     '&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse HEAD

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'New worktree:  '&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, the hashes must match.&lt;/p&gt;

&lt;p&gt;Do not copy the old working files onto a worktree based on a different commit. The resulting diff would mix branch differences with uncommitted working-directory changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restore the complete working-directory state
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;rsync&lt;/code&gt; to reproduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;modified tracked files,&lt;/li&gt;
&lt;li&gt;deleted tracked files,&lt;/li&gt;
&lt;li&gt;untracked files,&lt;/li&gt;
&lt;li&gt;ignored files,&lt;/li&gt;
&lt;li&gt;and local generated files.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--delete&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/.git'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The anchored exclusion is critical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/.git'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not use only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'.git/'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trailing slash pattern protects a &lt;code&gt;.git&lt;/code&gt; directory, but a linked worktree has a &lt;code&gt;.git&lt;/code&gt; file. With &lt;code&gt;--delete&lt;/code&gt;, the wrong exclusion can delete that file and temporarily break the worktree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restore upstream tracking
&lt;/h3&gt;

&lt;p&gt;If the branch exists on &lt;code&gt;origin&lt;/code&gt;, restore its upstream relationship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; show-ref &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--verify&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="s2"&gt;"refs/remotes/origin/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;then
    &lt;/span&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; branch &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--set-upstream-to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"origin/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No origin/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; branch remains local-only"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compare Git-visible state
&lt;/h3&gt;

&lt;p&gt;Capture the old and new status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    status &lt;span class="nt"&gt;--porcelain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v1 &lt;span class="nt"&gt;--untracked-files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/project-old.status

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    status &lt;span class="nt"&gt;--porcelain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v1 &lt;span class="nt"&gt;--untracked-files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/project-new.status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    /tmp/project-old.status &lt;span class="se"&gt;\&lt;/span&gt;
    /tmp/project-new.status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No output means Git sees the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;modified files,&lt;/li&gt;
&lt;li&gt;deleted files,&lt;/li&gt;
&lt;li&gt;and untracked files.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Compare the complete filesystem
&lt;/h3&gt;

&lt;p&gt;Use a checksum-based dry run to include ignored files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--checksum&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--delete&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/.git'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No output means the filesystems match, excluding Git metadata.&lt;/p&gt;

&lt;p&gt;At this point, build and test the new worktree before deleting the backup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Moving a worktree
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Same-filesystem move
&lt;/h3&gt;

&lt;p&gt;Use Git's worktree-aware move command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; worktree move &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old_path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new_path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cross-device move
&lt;/h3&gt;

&lt;p&gt;A move between filesystems may fail with an error similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cross-device link
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This often happens when moving between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;internal storage and an external disk,&lt;/li&gt;
&lt;li&gt;two mounted volumes,&lt;/li&gt;
&lt;li&gt;or separate filesystem partitions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Move the directory using the operating system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old_path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new_path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then repair Git's recorded paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; worktree repair &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new_path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; worktree list

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new_path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse HEAD

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new_path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; status &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--short&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--branch&lt;/span&gt;

file &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new_path&lt;/span&gt;&lt;span class="s2"&gt;/.git"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new_path&lt;/span&gt;&lt;span class="s2"&gt;/.git"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.git&lt;/code&gt; file should contain a pointer resembling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gitdir: /path/to/canonical/.git/worktrees/project-FEATURE-123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not rely only on the messages printed by &lt;code&gt;git worktree repair&lt;/code&gt;. The final verification commands determine whether the repair succeeded.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recovering a deleted &lt;code&gt;.git&lt;/code&gt; worktree file
&lt;/h2&gt;

&lt;p&gt;If an &lt;code&gt;rsync --delete&lt;/code&gt; operation removed the linked worktree's &lt;code&gt;.git&lt;/code&gt; file, Git commands in that directory may report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fatal: not a git repository
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repair it from the canonical repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; worktree repair &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;file &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;/.git"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;/.git"&lt;/span&gt;

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse HEAD

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; status &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--short&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--branch&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--untracked-files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For future copies, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/.git'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  When should a worktree be locked?
&lt;/h2&gt;

&lt;p&gt;Locking is useful when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the canonical repository remains available,&lt;/li&gt;
&lt;li&gt;one linked worktree lives elsewhere,&lt;/li&gt;
&lt;li&gt;and that linked worktree may temporarily disappear.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/src/project                         canonical repository on internal storage
/Volumes/External/project-testing     linked worktree on removable storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lock the removable worktree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree lock &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--reason&lt;/span&gt; &lt;span class="s2"&gt;"Stored on removable volume"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    /Volumes/External/project-testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Git not to treat the missing worktree as an abandoned entry eligible for pruning.&lt;/p&gt;

&lt;p&gt;Unlock it before intentionally moving or removing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree unlock &lt;span class="se"&gt;\&lt;/span&gt;
    /Volumes/External/project-testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When locking is unnecessary
&lt;/h3&gt;

&lt;p&gt;Suppose the canonical repository and every linked worktree are together on the same external drive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/Volumes/External/src/project
/Volumes/External/src/project-FEATURE-123
/Volumes/External/src/project-HOTFIX-456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the drive is disconnected, the entire repository is unavailable. Git cannot run maintenance against it.&lt;/p&gt;

&lt;p&gt;Locking every worktree does not provide meaningful additional protection in that arrangement.&lt;/p&gt;

&lt;p&gt;Reconnect the drive at the same mount path and continue working normally.&lt;/p&gt;




&lt;h2&gt;
  
  
  When should &lt;code&gt;git worktree prune&lt;/code&gt; be used?
&lt;/h2&gt;

&lt;p&gt;Git stores administrative metadata for each linked worktree in the canonical repository.&lt;/p&gt;

&lt;p&gt;If a worktree directory is removed outside Git, its administrative record may remain behind.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Someone ran &lt;code&gt;rm -rf&lt;/code&gt; on a worktree directory.&lt;/li&gt;
&lt;li&gt;A temporary build worktree was deleted manually.&lt;/li&gt;
&lt;li&gt;A storage volume was permanently retired.&lt;/li&gt;
&lt;li&gt;A cross-device migration left an obsolete worktree record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Preview stale records first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree prune &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after confirming that every listed worktree is permanently gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;prune&lt;/code&gt; removes obsolete administrative records. It does not delete an existing, accessible worktree directory.&lt;/p&gt;

&lt;p&gt;Do not prune simply because a removable drive is temporarily disconnected.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reconnect the drive, or&lt;/li&gt;
&lt;li&gt;lock the removable linked worktree if the canonical repository remains available elsewhere.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final validation
&lt;/h2&gt;

&lt;p&gt;List the registered worktrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; worktree list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;worktree &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project-FEATURE-123"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/src/project-HOTFIX-456"&lt;/span&gt;
&lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;echo
    echo&lt;/span&gt; &lt;span class="s2"&gt;"===== &lt;/span&gt;&lt;span class="nv"&gt;$worktree&lt;/span&gt;&lt;span class="s2"&gt; ====="&lt;/span&gt;

    git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$worktree&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; status &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--short&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--branch&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--untracked-files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the shared and per-worktree metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--show-toplevel&lt;/span&gt;

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--git-common-dir&lt;/span&gt;

git &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; rev-parse &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--git-dir&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build and test every converted worktree.&lt;/p&gt;

&lt;p&gt;Only then remove its backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the path carefully before running that command.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List worktrees&lt;/span&gt;
git worktree list

&lt;span class="c"&gt;# Machine-readable list&lt;/span&gt;
git worktree list &lt;span class="nt"&gt;--porcelain&lt;/span&gt;

&lt;span class="c"&gt;# Add an existing local branch&lt;/span&gt;
git worktree add ../project-FEATURE FEATURE

&lt;span class="c"&gt;# Create a new branch and worktree&lt;/span&gt;
git worktree add &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-b&lt;/span&gt; FEATURE &lt;span class="se"&gt;\&lt;/span&gt;
    ../project-FEATURE &lt;span class="se"&gt;\&lt;/span&gt;
    main

&lt;span class="c"&gt;# Create a detached testing worktree&lt;/span&gt;
git worktree add &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    ../project-test &lt;span class="se"&gt;\&lt;/span&gt;
    main

&lt;span class="c"&gt;# Move a worktree on the same filesystem&lt;/span&gt;
git worktree move ../old-location ../new-location

&lt;span class="c"&gt;# Repair after a manual or cross-device move&lt;/span&gt;
git worktree repair ../new-location

&lt;span class="c"&gt;# Lock one removable worktree while the canonical repo remains available&lt;/span&gt;
git worktree lock &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--reason&lt;/span&gt; &lt;span class="s2"&gt;"Removable storage"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    ../project-FEATURE

&lt;span class="c"&gt;# Unlock before intentionally moving or removing it&lt;/span&gt;
git worktree unlock ../project-FEATURE

&lt;span class="c"&gt;# Remove a registered worktree&lt;/span&gt;
git worktree remove ../project-FEATURE

&lt;span class="c"&gt;# Preview obsolete worktree records&lt;/span&gt;
git worktree prune &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;

&lt;span class="c"&gt;# Remove confirmed-obsolete records&lt;/span&gt;
git worktree prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Git worktrees are not an exotic Git feature reserved for unusual workflows.&lt;/p&gt;

&lt;p&gt;They are useful whenever you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maintain several active branches,&lt;/li&gt;
&lt;li&gt;compare old and new releases,&lt;/li&gt;
&lt;li&gt;run concurrent builds,&lt;/li&gt;
&lt;li&gt;preserve a long-running experiment,&lt;/li&gt;
&lt;li&gt;review another branch without disturbing current work,&lt;/li&gt;
&lt;li&gt;or replace a growing collection of independent clones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The safest migration strategy is intentionally conservative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserve the source clone,&lt;/li&gt;
&lt;li&gt;import its exact commit,&lt;/li&gt;
&lt;li&gt;create the worktree at that commit,&lt;/li&gt;
&lt;li&gt;copy the working-directory state,&lt;/li&gt;
&lt;li&gt;compare everything,&lt;/li&gt;
&lt;li&gt;test it,&lt;/li&gt;
&lt;li&gt;and only then remove the backup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the migration is complete, everyday work becomes simpler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one fetch,&lt;/li&gt;
&lt;li&gt;one repository history,&lt;/li&gt;
&lt;li&gt;one set of remotes,&lt;/li&gt;
&lt;li&gt;and as many independent working directories as you actually need.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
      <category>tutorial</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Adventures with K0S in AWS</title>
      <dc:creator>Lance Nehring</dc:creator>
      <pubDate>Fri, 09 Aug 2024 14:22:47 +0000</pubDate>
      <link>https://dev.to/lance_nehring/adventures-with-k0s-in-aws-4ho9</link>
      <guid>https://dev.to/lance_nehring/adventures-with-k0s-in-aws-4ho9</guid>
      <description>&lt;p&gt;At the time of this writing (August 2024), K0S is at version v1.30.3.  There's a tremendous about of outdated and incorrect information on the Internet (which impacts AI, if you're into asking AmazonQ or ChatGPT questions), so be aware of the date of this article. My goal is to keep it current - we'll see how that goes.&lt;/p&gt;

&lt;p&gt;This isn't actually a tutorial - the end state is not desirable and the information is too dense. This is a more of an "engineering notebook" - akin to what my fellow graybeards may recall from engineering school.&lt;/p&gt;

&lt;p&gt;My plan was to establish a Kubernetes presence on AWS without the incurring the costs of Amazon's EKS. I wanted a lightweight, but fully functional, K8S installation that I could stand up and tear down to prove out orchestration and deployment of containerized projects that come along.... such as those for a startup company where attention to cloud cost is paramount.  I'm certainly not against EKS for those situations where the cost is justified and I have used it heavily in the past.&lt;/p&gt;

&lt;p&gt;Picking through the various smaller K8S projects out there, I've settled on &lt;a href="https://docs.k0sproject.io/stable/" rel="noopener noreferrer"&gt;K0S&lt;/a&gt; since it's supposed to be "The Zero Friction Kubernetes". The features I'm after with this experiment are similar to what I've used with EKS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ability to pull images from ECR&lt;/li&gt;
&lt;li&gt;use the AWS cloud provider functionality to get those AWS specific things: using tags to annotate subnets, worker nodes, route tables, etc for use by the K0S installation.&lt;/li&gt;
&lt;li&gt;use the pod identity agent to address pods that require certain privileges within AWS via IAM roles&lt;/li&gt;
&lt;li&gt;use an ingress controller to manage the provisioning and lifecycle of AWS ELBs - namely NLBs and ALBs. K0S has a tool called "k0sctl" to manage installation, but it requires SSH access to the nodes. I have no other use for SSH and don't need to expand the attack surface, so I won't install it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Establish a VPC for testing
&lt;/h2&gt;

&lt;p&gt;I won't cover the mechanics of creating the VPC, subnets, Internet gateway, route table, security groups, NACLs, etc. I personally use the IaC tool &lt;a href="https://www.terraform.io/" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; whenever possible.  There is a learning curve to use something like Terraform (and learning HCL), but the benefits are enormous - especially when you need consistency so you don't waste time chasing ghosts resulting from misconfigured infrastructure.&lt;/p&gt;

&lt;p&gt;I'm using a VPC with a class B private CIDR (172.16.0.0/16) in the us-east-1 region, enabled for DNS hostnames and DNS resolution. I created 3 public subnets (each with a 20 bit subnet mask) even though we're only using 1 subnet to start with. The main route table needs a route for 0.0.0.0/0 that goes to the Internet Gateway for the VPC. I didn't create any private subnets in order to reduce the cost and need for any NAT gateways for this experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create your EC2 instance
&lt;/h2&gt;

&lt;p&gt;Similarly, I won't cover the mechanics of creating an EC2 instance. Terraform comes in really handy here since you may find yourself repeatedly doing &lt;code&gt;terraform apply&lt;/code&gt; and &lt;code&gt;terraform destroy&lt;/code&gt; as you start and stop your experiments.  I'm using a "t3a.large" node to start with using the latest AL2023 AMI - to have enough vCPU, memory, and networking to keep us out of harms way, without costing too much (in case we forget to destroy the instance after testing).  Also, I'm not bothering to set up SSH to get a shell on the instance and I'm using &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html" rel="noopener noreferrer"&gt;AWS System Manager&lt;/a&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Establish an IAM role for the EC2 instance to use as its instance profile
&lt;/h2&gt;

&lt;p&gt;We're going to do ourselves a giant favor in terms of security and start using IAM roles immediately.  Many articles related to AWS just talk about putting credentials in some "~/.aws/credentials" file. Yes, you can do that, but you immediately create an issue that will fail a security audit, and you're actually making your life harder by having to track and secure those credentials. So don't cheat and use your personal IAM access keys, or &lt;a href="https://en.wikipedia.org/wiki/Krampus" rel="noopener noreferrer"&gt;Krampus&lt;/a&gt; will find you.&lt;br&gt;
You can use Terraform for this as well.  Effectively you need an IAM role, I named mine "k0s_instance" and attached these AWS managed policies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AmazonEC2ContainerRegistryReadOnly&lt;/li&gt;
&lt;li&gt;AmazonEKS_CNI_Policy  (for when we experiment with the AWS CNI)
You'll also need to create 2 customer managed policies and attach those to the role.   The policy permissions information is from the docs here: &lt;a href="https://cloud-provider-aws.sigs.k8s.io/prerequisites/#iam-policies" rel="noopener noreferrer"&gt;https://cloud-provider-aws.sigs.k8s.io/prerequisites/#iam-policies&lt;/a&gt;  I named mine "k0s_control_plane_policy" and "k0s_node_policy".&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Install k0s
&lt;/h2&gt;

&lt;p&gt;We're going to start at the smallest cluster - a single node. All of the following installation steps are done in a "root" shell on the EC2 instance. This means the control plane and worker artifacts will be running on the same node. There are side effects with node selection and tolerations that we'll run into, but we'll address that later.&lt;br&gt;
&lt;a href="https://docs.k0sproject.io/stable/install/" rel="noopener noreferrer"&gt;https://docs.k0sproject.io/stable/install/&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -sSLf https://get.k0s.sh | sudo sh
k0s sysinfo
mkdir -p /etc/k0s
k0s config create &amp;gt; /etc/k0s/k0s.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Get the ECR credential provider binary
&lt;/h2&gt;

&lt;p&gt;This link can help you determine what releases are available: &lt;a href="https://github.com/kubernetes/cloud-provider-aws/releases" rel="noopener noreferrer"&gt;https://github.com/kubernetes/cloud-provider-aws/releases&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://cloud-provider-aws.sigs.k8s.io/credential_provider/" rel="noopener noreferrer"&gt;AWS credential provider documentation&lt;/a&gt; is very light. You get to create a configuration file and then search the K0S docs for how to manipulate the kubelet arguments to use that file.  This &lt;a href="https://medium.com/@sajjadzaheri/how-to-authenticate-aws-ecr-on-any-kubernetes-cluster-the-right-way-26b6ee190125" rel="noopener noreferrer"&gt;article&lt;/a&gt; is for K3S, but shows that the configuration file can be YAML instead of JSON - something that isn't mentioned in the credential provider docs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RELEASE=v1.30.3
curl -OL https://storage.googleapis.com/k8s-staging-provider-aws/releases/${RELEASE}/linux/amd64/ecr-credential-provider-linux-amd64
mv ecr-credential-provider-linux-amd64 /etc/k0s/ecr-credential-provider
chmod 0755 /etc/k0s/ecr-credential-provider
cat &amp;lt;&amp;lt; EOF &amp;gt; /etc/k0s/custom-credential-providers.yaml
apiVersion: kubelet.config.k8s.io/v1
kind: CredentialProviderConfig
providers:
- name: ecr-credential-provider
  matchImages:
  - "*.dkr.ecr.*.amazonaws.com"
  - "*.dkr.ecr.*.amazonaws.com.cn"
  apiVersion: credentialprovider.kubelet.k8s.io/v1
  defaultCacheDuration: '0'
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Edit the k0s.yaml
&lt;/h2&gt;

&lt;p&gt;Here is where we actually set the kubelet argments so that the ECR credential provider will work.  The clues for this were from this &lt;a href="https://gist.github.com/schakko/d53deb3e75309ea5577693a21cb3cbc3" rel="noopener noreferrer"&gt;Gist&lt;/a&gt;. Combining that information with this K0S &lt;a href="https://docs.k0sproject.io/stable/worker-node-config/" rel="noopener noreferrer"&gt;doc&lt;/a&gt;, we discover that it is possible to use "--kubelet-extra-args" on the k0s command line to set the "--extra-args for the kubelet. &lt;br&gt;
Also, there seems like no possible way to get the default K0S CNI of kuberouter to work in AWS. I don't know the root cause - possibly there's CIDR conflicts with what I chose for my VPC CIDR - but it was a simple change to set the "spec.network.provider" value to "calico" in the "/etc/k0s/k0s.yaml" file that we created. Calico worked fine for me without further configuration.&lt;br&gt;
So, for now we're using Calico as the CNI. I feel like I should be able to use the AWS VPC CNI plugin, but that has not yet been successful for me. &lt;em&gt;This may need to be revisited if the AWS Load Balancer Controller requires it.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k0s install controller --single --enable-cloud-provider --kubelet-extra-args="--image-credential-provider-config=/etc/k0s/custom-credential-providers.yaml --image-credential-provider-bin-dir=/etc/k0s" -c /etc/k0s/k0s.yaml
systemctl daemon-reload
k0s start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will "start" the cluster, which we need to do to get the our kubectl configured that we'll do next.  The single node cluster won't truly start yet - and that's ok for now.   You'll notice Pods in the pending state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k0s status
k0s kubectl get pod -A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install and configure kubectl
&lt;/h2&gt;

&lt;p&gt;Here we grab a version of kubectl that matches our kubernetes version so that we maximize compatibility.  We use the k0s command to generate a valid config file and put it in the expected place.  Note the this file contains the "keys to the kingdom" as far as the k0s installation is concerned, so treat it appropriately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k0s version
curl -LO https://dl.k8s.io/release/v1.30.3/bin/linux/amd64/kubectl
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
mkdir -p ~/.kube
k0s kubeconfig admin &amp;gt; ~/.kube/config
chmod 0600 ~/.kube/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install helm and add the stable and cloud-provider-aws repos
&lt;/h2&gt;

&lt;p&gt;We're embracing helm charts for repeatable, stable, versioned installations of everything we can.  Install the latest version of helm and setup a few repos that we intend to use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dnf install -y git
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm repo add stable https://charts.helm.sh/stable
helm repo add aws-cloud-controller-manager https://kubernetes.github.io/cloud-provider-aws
helm repo add eks https://aws.github.io/eks-charts
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install the helm chart for the aws-cloud-controller-manager
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AWS Tags
&lt;/h3&gt;

&lt;p&gt;The documentation for the &lt;a href="https://cloud-provider-aws.sigs.k8s.io/prerequisites/" rel="noopener noreferrer"&gt;AWS Cloud Provider&lt;/a&gt; is rather underwhelming. Especially frustrating is the lack of direct information about tagging AWS resources. There's some information here that can help: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://repost.aws/knowledge-center/eks-vpc-subnet-discovery" rel="noopener noreferrer"&gt;https://repost.aws/knowledge-center/eks-vpc-subnet-discovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/49802306/aws-integration-on-kubernetes" rel="noopener noreferrer"&gt;https://stackoverflow.com/questions/49802306/aws-integration-on-kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.golinuxcloud.com/setup-kubernetes-cluster-on-aws-ec2/" rel="noopener noreferrer"&gt;https://www.golinuxcloud.com/setup-kubernetes-cluster-on-aws-ec2/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using a kubernetes cluster name of "testcluster", the tags we start with are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;
Tags for EC2 instance, VPC, subnets:
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubernetes.io/cluster/testcluster&lt;/td&gt;
&lt;td&gt;owned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;
Additional tags for subnets:
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubernetes.io/role/elb&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubernetes.io/role/alb-ingress&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubernetes.io/role/internal-elb&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  AWS Cloud Provider
&lt;/h3&gt;

&lt;p&gt;I found it necessary to edit the node selector and tolerations of the daemonset to be able to get the pod scheduled in the case of this single node deployment. I was also unable to get AWS route tables annotated to the point where the aws-cloud-controller-manager would be happy about configuring cloud routes.  Not sure what "cloud routes" are supposed to be, but for now, I've disabled that feature.  There's more on it &lt;a href="https://blog.scottlowe.org/2021/10/12/using-the-external-aws-cloud-provider-for-kubernetes/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;
We are doing all this in the custom helm values file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat &amp;lt;&amp;lt; EOF &amp;gt; /etc/k0s/accm-values.yaml
---
args:
  - --v=2
  - --cloud-provider=aws
  - --configure-cloud-routes=false
nodeSelector:
  node-role.kubernetes.io/control-plane: "true"
tolerations:
- key: node.cloudprovider.kubernetes.io/uninitialized
  value: "true"
  effect: NoSchedule
EOF
helm -n kube-system upgrade --install aws-cloud-controller-manager aws-cloud-controller-manager/aws-cloud-controller-manager --values /etc/k0s/accm-values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install pod identity agent
&lt;/h2&gt;

&lt;p&gt;Unfortunately, I didn't see a helm chart for the eks-pod-identity-agent hosted on a Helm repo.  So we're forced to clone the git repo and install the helm chart from that work area.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat &amp;lt;&amp;lt; EOF &amp;gt; /etc/k0s/epia-values.yaml
---
clusterName: testcluster
env:
  AWS_REGION: us-east-1
EOF
git clone https://github.com/aws/eks-pod-identity-agent.git
cd eks-pod-identity-agent/
helm install eks-pod-identity-agent --namespace kube-system ./charts/eks-pod-identity-agent --values ./charts/eks-pod-identity-agent/values.yaml --values /etc/k0s/epia-values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Is it working so far?
&lt;/h2&gt;

&lt;p&gt;Kubectl should be happy with the node and the pods. It can take a few minutes for the pods to reach a "Running" state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get node -o wide
kubectl get pod -A -o wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From what I've seen, the worker node (our only node, at this point) must have IP addresses assigned, else the &lt;code&gt;kubectl logs&lt;/code&gt; command will fail if you try to inspect logs from the pods/containers.  I found that you can find logs in the "/var/log/container" container directory of the EC2 instance that I'm using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ingress Controller
&lt;/h2&gt;

&lt;p&gt;Here's where things take another complicated twist.  The AWS Cloud Controller Manager contains a legacy AWS load balancer controller capable of managing legacy ELBs and NLBs.  The code for the NLB management shows an older and newer API... I'm not sure what the switch is between the two, but it may very well be the "--v=2" argument that was passed to the aws-cloud-controller-manager.  Oddly the newer API for NLBs is not capable of configuring for proxy protocol, whereas the docs suggest that it does, and so does the code for the older API.&lt;br&gt;
It appears that this legacy code in the aws-cloud-controller-manager is bascially EOL - you can still use it, but broken things are not getting fixed.  The push seems to be with a follow-on project, the AWS Load Balancer Controller.  It is absolutely confusing, but I did find an &lt;a href="https://www.doit.com/demystifying-the-kubernetes-aws-load-balancer-controller/" rel="noopener noreferrer"&gt;article&lt;/a&gt; to explain it better.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install the Nginx Ingress Controller
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://kubernetes.github.io/ingress-nginx/" rel="noopener noreferrer"&gt;https://kubernetes.github.io/ingress-nginx/&lt;/a&gt;&lt;br&gt;
The available customization values can be found with this nifty helm command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm show values ingress-nginx --repo https://kubernetes.github.io/ingress-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're creating a special configuration here based on: &lt;a href="https://kubernetes.github.io/ingress-nginx/deploy/#network-load-balancer-nlb" rel="noopener noreferrer"&gt;https://kubernetes.github.io/ingress-nginx/deploy/#network-load-balancer-nlb&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The idea is that the Nginx ingress controller is behind an NLB that accepts HTTPS and HTTP traffic (TCP:443 and TCP:80).  The HTTPS traffic has the SSL terminated at the NLB using the certificate given the the annotation.  That traffic of HTTPS origin, is then fed to the nginx controller as HTTP traffic.  The traffic of HTTP origin is sent by the NLB to a "tohttps" port (TCP:2443) at the nginx controller, that merely responds to the client with a code 308 permanent redirect - to force the client to use HTTPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The "$" in the http-snippet are escaped in this heredoc to protect them from the shell.&lt;/li&gt;
&lt;li&gt;The shell variable CERT_ARN must be set to whatever certificate ARN that you have in your AWS Certificate Manager that you intend to use.&lt;/li&gt;
&lt;li&gt;Since this annotation using the legacy AWS load balancer controller, only a single certificate ARN can be specified.&lt;/li&gt;
&lt;li&gt;The "proxy-real-ip-cidr" is set to the CIDR of the VPC I'm using.  You can force proxy protocol to work, by uncommenting the comments in the heredoc.  The controller will not actually enable proxy protocol on the NLB's target groups, so you'll have to use the AWS console and do that manually. It can work, but it's not solution for production.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CERT_ARN="xxxxxxxx"
cat &amp;lt;&amp;lt; EOF &amp;gt; /etc/k0s/nic-values.yaml
---
controller:
  service:
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp
      service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
      service.beta.kubernetes.io/aws-load-balancer-type: nlb
      service.beta.kubernetes.io/aws-load-balancer-ssl-cert: ${CERT_ARN}
      service.beta.kubernetes.io/aws-load-balancer-ssl-ports: https
#      service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*'
    targetPorts:
      http: tohttps
      https: http
  config:
#    use-proxy-protocol: "true"
    use-forwarded-headers: "true"
    proxy-real-ip-cidr: "172.16.0.0/16"
    http-snippet: |
      server {
        listen 2443;
        return 308 https://\$host\$request_uri;
      }
  containerPort:
    tohttps: 2443
EOF
helm upgrade -i ingress-nginx ingress-nginx/ingress-nginx --values /etc/k0s/nic-values.yaml -n ingress-nginx --create-namespace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Simple application for testing
&lt;/h2&gt;

&lt;p&gt;Once the nginx ingress controller is running, we can attempt to test. For simplicity, this application is just a yaml manifest instead of a helm chart (there's likely a better way to do this). You can adjust the ingress host to something other than "web.example.com" - to potentially match that SSL cert that you're using - or not, depending on whether your testing can handle SSL name mismatch errors.&lt;/p&gt;

&lt;p&gt;"simple-web-server-with-ingress.yaml":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: Namespace
metadata:
  name: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-server
  namespace: web
spec:
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: httpd
        image: httpd:2.4.53-alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web-server-service
  namespace: web
spec:
  selector:
    app: web
  ports:
    - protocol: TCP
      port: 5000
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-server-ingress
  namespace: web
spec:
  ingressClassName: nginx
  rules:
  - host: web.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-server-service
            port:
              number: 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply with &lt;code&gt;kubectl apply -f simple-web-server-with-ingress.yaml&lt;/code&gt;.  It will take a few minutes for the NLB to finish provisioning and pass initial health checks. You can monitor the progress in the AWS EC2 console.  The ingress and service can be seen with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get ingress -A -o wide
kubectl get service -A -o wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can attempt to curl to the NLB's DNS address (same as the address shown by kubectl). I'm giving curl the "-k" option to ignore the SSL cert mismatch, and I'm also setting a "Host" HTTP header, since the ingress is explicitly for "web.example.com".&lt;/p&gt;

&lt;p&gt;So, when I execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -k -H 'Host: web.example.com' https://xxxxxxxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I get the expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;It works!&amp;lt;/h1&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I try to similarly curl using HTTP instead of HTTPS, as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -k -H 'Host: web.example.com' http://xxxxxxxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I get the expected 308 permanent redirect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;308 Permanent Redirect&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;center&amp;gt;&amp;lt;h1&amp;gt;308 Permanent Redirect&amp;lt;/h1&amp;gt;&amp;lt;/center&amp;gt;
&amp;lt;hr&amp;gt;&amp;lt;center&amp;gt;nginx&amp;lt;/center&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that since I didn't change "web.example.com" to some DNS that I own, that if I tell curl to follow the redirect as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -L -k -H 'Host: web.example.com' http://xxxxxxxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I get the expected error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl: (6) Could not resolve host: web.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusions at this point
&lt;/h2&gt;

&lt;p&gt;We've shown that it is possible to get K0S working on a single node in AWS.  Using the nginx ingress controller can work for an NLB, but there are issues that make it undesirable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's using a legacy AWS load balancer controller contained in the AWS cloud manager controller, which means:

&lt;ul&gt;
&lt;li&gt;Risk of that code being removed at some unknown point in the future&lt;/li&gt;
&lt;li&gt;Current documentation doesn't match the actual features&lt;/li&gt;
&lt;li&gt;NLBs are not configurable for TLS SNI or proxy protocol&lt;/li&gt;
&lt;li&gt;No support for ALBs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;The nginx ingress controller has a somewhat complicated configuration to accomplish a common HTTP to HTTPS redirect.&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Moving forward
&lt;/h2&gt;

&lt;p&gt;In the current &lt;a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.8/deploy/installation/#additional-requirements-for-non-eks-clusters" rel="noopener noreferrer"&gt;AWS Load Balancer Controller&lt;/a&gt; docs we find this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Additional requirements for non-EKS clusters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensure subnets are tagged appropriately for auto-discovery to work&lt;/li&gt;
&lt;li&gt;For IP targets, pods must have IPs from the VPC subnets. You can configure the amazon-vpc-cni-k8s plugin for this purpose.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm going to revisit using the amazon-vpc-cni-k8s plugin.  I'm thinking that I missed the kubelet configuration requirements when experimenting before and didn't actually have it installed properly.  It appears that they may be components that require installation directly on the worker node - like with the ECR credential provider.  We'll see - every day is a learning experience.&lt;/p&gt;

&lt;p&gt;Has anyone else tried to use K0S in this way? or have advice/clarifications/questions that I may (or may not) be able to answer?&lt;/p&gt;

</description>
      <category>k0s</category>
      <category>aws</category>
      <category>kubernetes</category>
    </item>
  </channel>
</rss>
