Problem
The repository ships a complete, frozen copy of Oh My Zsh at GentlemanZsh/.oh-my-zsh (1019 files, ~11 MB), and the installer copies it file by file over ~/.oh-my-zsh, whether or not a previous installation exists.
Oh My Zsh is a Git repository that updates itself. Overwriting its tracked files makes every difference between the bundled copy and upstream's actual state register as a local modification to a tracked file. The next Oh My Zsh auto-update then conflicts.
Where it happens
installer/internal/tui/installer.go:804:
if err := system.CopyDir(filepath.Join(repoDir, "GentlemanZsh", ".oh-my-zsh"), filepath.Join(homeDir, ".oh-my-zsh")); err != nil {
system.CopyDir (installer/internal/system/exec.go:306) walks the source tree and writes every file with CopyFile. It overwrites the destination without comparing versions, without excluding anything, and without checking whether Oh My Zsh is already installed on the system. Files that exist in the copy but no longer exist upstream are left behind, resurrected.
Causal chain
- The user has Oh My Zsh installed the official way, so
~/.oh-my-zsh is a Git clone.
- The installer overwrites tracked files:
lib/, plugins/, themes/, tools/, oh-my-zsh.sh.
- Those files become dirty relative to the Git index.
tools/upgrade.sh runs with rebase.autoStash true (line 226) and executes git pull --quiet --rebase (line 245).
- The autostash stores the modifications the installer left behind, rebases onto upstream, and the pop collides with upstream's own changes to the same files.
- The Oh My Zsh update is left broken and requires manual conflict resolution.
Evidence
Real installation performed about a month ago, on top of a pre-existing Oh My Zsh installed the official way.
HEAD commit left in ~/.oh-my-zsh after the first auto-update following the installation:
3036697 resolve: bundler stash pop conflict (upstream 59a9740)
+ legacy Gentleman.Dots bundle_install() commented
Comparison between the bundled copy and the live clone: 38 divergences. Tracked files that differ include:
plugins/bundler/bundler.plugin.zsh
plugins/flutter/flutter.plugin.zsh
plugins/flutter/README.md
plugins/rails/rails.plugin.zsh
plugins/rails/_rails
plugins/rails/README.md
plugins/pm2/_pm2
Directories upstream deleted that the bundled copy recreated, and that now sit as untracked files inside the clone:
?? plugins/fig/
?? plugins/rbfu/
Secondary defect
The bundled copy contains no .git. On a clean installation, on a system without a prior Oh My Zsh, the result is a ~/.oh-my-zsh that is not a Git repository. That Oh My Zsh can never update, and it fails silently: omz update has no remote to pull from.
There are two scenarios and neither ends well:
| Prior state |
Current result |
| Oh My Zsh already installed (Git clone) |
Rebase conflicts on every auto-update |
| No Oh My Zsh |
Oh My Zsh without Git, unable to update, no warning |
Expected behavior
The installation should leave a working, updatable Oh My Zsh in both scenarios, and contribute only Gentleman.Dots' own configuration.
Actual behavior
The installation leaves an Oh My Zsh whose official update produces rebase conflicts, or an Oh My Zsh with no ability to update at all.
Proposal
Oh My Zsh is genuinely required: GentlemanZsh/.zshrc:117 runs source $ZSH/oh-my-zsh.sh. What is not required is keeping it vendored inside the repository. The project's configuration enables a single plugin (GentlemanZsh/.zshrc:113-115):
plugins=(
command-not-found
)
The 1019 bundled files exist to serve the Oh My Zsh core plus one plugin. The proposal has two parts.
1. Detect before writing
The installer should check whether ~/.oh-my-zsh already exists. If it does, leave it alone: that installation belongs to Oh My Zsh and its update cycle, not to the installer.
2. Delegate installation to the official installer
When ~/.oh-my-zsh does not exist, install Oh My Zsh through its official path instead of copying the frozen snapshot:
RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
All three variables are documented in Oh My Zsh's own tools/install.sh:28-35. KEEP_ZSHRC=yes is required so the official installer does not replace .zshrc with its template; Gentleman.Dots copies its own immediately afterwards and keeps control of the configuration.
This introduces no new dependency. The installer already clones repositories and runs remote installers in several places, among them:
installer/internal/tui/installer.go:122 — clones Gentleman.Dots itself
installer/internal/tui/installer.go:935 — git clone https://github.com/tmux-plugins/tpm
installer/internal/tui/installer.go:337 — clones Alacritty
installer/internal/tui/installer.go:157 — Homebrew installer via curl
The tpm case is the exact precedent for the proposed pattern: a third-party tool fetched from its origin instead of being vendored into the repository.
Resulting flow
Does ~/.oh-my-zsh exist?
├── Yes → leave it untouched
└── No → official Oh My Zsh installer
Then, in both cases: copy .zshrc, .p10k.zsh and whatever belongs in custom/
3. Drop the bundled copy
With the above in place, GentlemanZsh/.oh-my-zsh no longer serves a purpose, except for whatever genuinely belongs under $ZSH_CUSTOM (~/.oh-my-zsh/custom/), which is the extension point Oh My Zsh provides for exactly this kind of third-party configuration.
Verification: the bundled copy contains no project-owned code
Before proposing its removal, I checked that there is no project-owned function or intentional modification inside the copy. The method: hash every bundled file and verify whether that exact blob exists in the official Oh My Zsh repository's history.
h=$(git hash-object "GentlemanZsh/.oh-my-zsh/$f")
git -C ~/.oh-my-zsh cat-file -e "$h" # if it exists, the file is unmodified upstream
Result across all 1019 files: 1015 match an identical upstream blob. The remaining 4 are plugins/fig/ and plugins/rbfu/, also upstream files, whose blobs are missing only because their deletion predates the locally available history.
Two further confirmations:
rg -i gentleman GentlemanZsh/.oh-my-zsh returns no matches.
- The directory has 3 commits in the entire repository history, all of them imports (
fix: add oh-my-zsh files directly, fix: new try, fix: add oh-my-zsh directory). There was never a deliberate edit.
Therefore, dropping the copy loses no project functionality and there is nothing to migrate into $ZSH_CUSTOM.
A note on bundle_install()
While resolving these conflicts it is easy to mistake old Oh My Zsh code for Gentleman.Dots code. A concrete case: in the bundled copy, plugins/bundler/bundler.plugin.zsh contains a bundle_install() function alongside alias bi="bundle_install". That function is deprecated Oh My Zsh code, not a Gentleman.Dots function: upstream removed it and now defines alias bi="bundle install". The blob for that file exists verbatim in upstream history.
Worth flagging, because anyone resolving the conflict by hand may keep or label as project-owned code that should simply take upstream's version.
Alternatives considered
|
Current |
Detect only, keep the copy |
Detect + official installer |
| Existing installation |
❌ conflicts |
✅ |
✅ |
| Clean installation, updatable |
❌ |
❌ frozen Oh My Zsh, no Git |
✅ |
| Third-party copy to maintain |
Yes |
Yes |
No |
| Repository size |
— |
unchanged |
−11 MB |
| New dependencies |
— |
none |
none |
Keeping the copy would only be justified to support offline installation, and that is not the case here: the installer fetches itself from GitHub and depends on the network for package managers, fonts and terminals.
Note on remediating existing installations
Anyone who already installed and has the Git clone can restore upstream's clean state, bearing in mind this discards any local modification inside ~/.oh-my-zsh:
git -C ~/.oh-my-zsh fetch origin
git -C ~/.oh-my-zsh reset --hard origin/master
git -C ~/.oh-my-zsh clean -fd
Environment
- Gentleman.Dots v2.12.2
- Linux (WSL2)
- Zsh with Oh My Zsh previously installed the official way
- Affected installer branch:
case "zsh" in installer/internal/tui/installer.go:770-807
Problem
The repository ships a complete, frozen copy of Oh My Zsh at
GentlemanZsh/.oh-my-zsh(1019 files, ~11 MB), and the installer copies it file by file over~/.oh-my-zsh, whether or not a previous installation exists.Oh My Zsh is a Git repository that updates itself. Overwriting its tracked files makes every difference between the bundled copy and upstream's actual state register as a local modification to a tracked file. The next Oh My Zsh auto-update then conflicts.
Where it happens
installer/internal/tui/installer.go:804:system.CopyDir(installer/internal/system/exec.go:306) walks the source tree and writes every file withCopyFile. It overwrites the destination without comparing versions, without excluding anything, and without checking whether Oh My Zsh is already installed on the system. Files that exist in the copy but no longer exist upstream are left behind, resurrected.Causal chain
~/.oh-my-zshis a Git clone.lib/,plugins/,themes/,tools/,oh-my-zsh.sh.tools/upgrade.shruns withrebase.autoStash true(line 226) and executesgit pull --quiet --rebase(line 245).Evidence
Real installation performed about a month ago, on top of a pre-existing Oh My Zsh installed the official way.
HEAD commit left in
~/.oh-my-zshafter the first auto-update following the installation:Comparison between the bundled copy and the live clone: 38 divergences. Tracked files that differ include:
Directories upstream deleted that the bundled copy recreated, and that now sit as untracked files inside the clone:
Secondary defect
The bundled copy contains no
.git. On a clean installation, on a system without a prior Oh My Zsh, the result is a~/.oh-my-zshthat is not a Git repository. That Oh My Zsh can never update, and it fails silently:omz updatehas no remote to pull from.There are two scenarios and neither ends well:
Expected behavior
The installation should leave a working, updatable Oh My Zsh in both scenarios, and contribute only Gentleman.Dots' own configuration.
Actual behavior
The installation leaves an Oh My Zsh whose official update produces rebase conflicts, or an Oh My Zsh with no ability to update at all.
Proposal
Oh My Zsh is genuinely required:
GentlemanZsh/.zshrc:117runssource $ZSH/oh-my-zsh.sh. What is not required is keeping it vendored inside the repository. The project's configuration enables a single plugin (GentlemanZsh/.zshrc:113-115):The 1019 bundled files exist to serve the Oh My Zsh core plus one plugin. The proposal has two parts.
1. Detect before writing
The installer should check whether
~/.oh-my-zshalready exists. If it does, leave it alone: that installation belongs to Oh My Zsh and its update cycle, not to the installer.2. Delegate installation to the official installer
When
~/.oh-my-zshdoes not exist, install Oh My Zsh through its official path instead of copying the frozen snapshot:RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"All three variables are documented in Oh My Zsh's own
tools/install.sh:28-35.KEEP_ZSHRC=yesis required so the official installer does not replace.zshrcwith its template; Gentleman.Dots copies its own immediately afterwards and keeps control of the configuration.This introduces no new dependency. The installer already clones repositories and runs remote installers in several places, among them:
installer/internal/tui/installer.go:122— clones Gentleman.Dots itselfinstaller/internal/tui/installer.go:935—git clone https://github.com/tmux-plugins/tpminstaller/internal/tui/installer.go:337— clones Alacrittyinstaller/internal/tui/installer.go:157— Homebrew installer viacurlThe tpm case is the exact precedent for the proposed pattern: a third-party tool fetched from its origin instead of being vendored into the repository.
Resulting flow
3. Drop the bundled copy
With the above in place,
GentlemanZsh/.oh-my-zshno longer serves a purpose, except for whatever genuinely belongs under$ZSH_CUSTOM(~/.oh-my-zsh/custom/), which is the extension point Oh My Zsh provides for exactly this kind of third-party configuration.Verification: the bundled copy contains no project-owned code
Before proposing its removal, I checked that there is no project-owned function or intentional modification inside the copy. The method: hash every bundled file and verify whether that exact blob exists in the official Oh My Zsh repository's history.
Result across all 1019 files: 1015 match an identical upstream blob. The remaining 4 are
plugins/fig/andplugins/rbfu/, also upstream files, whose blobs are missing only because their deletion predates the locally available history.Two further confirmations:
rg -i gentleman GentlemanZsh/.oh-my-zshreturns no matches.fix: add oh-my-zsh files directly,fix: new try,fix: add oh-my-zsh directory). There was never a deliberate edit.Therefore, dropping the copy loses no project functionality and there is nothing to migrate into
$ZSH_CUSTOM.A note on
bundle_install()While resolving these conflicts it is easy to mistake old Oh My Zsh code for Gentleman.Dots code. A concrete case: in the bundled copy,
plugins/bundler/bundler.plugin.zshcontains abundle_install()function alongsidealias bi="bundle_install". That function is deprecated Oh My Zsh code, not a Gentleman.Dots function: upstream removed it and now definesalias bi="bundle install". The blob for that file exists verbatim in upstream history.Worth flagging, because anyone resolving the conflict by hand may keep or label as project-owned code that should simply take upstream's version.
Alternatives considered
Keeping the copy would only be justified to support offline installation, and that is not the case here: the installer fetches itself from GitHub and depends on the network for package managers, fonts and terminals.
Note on remediating existing installations
Anyone who already installed and has the Git clone can restore upstream's clean state, bearing in mind this discards any local modification inside
~/.oh-my-zsh:Environment
case "zsh"ininstaller/internal/tui/installer.go:770-807