Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Builds manygit for macOS + Linux (Intel & Apple Silicon) and publishes a
# GitHub Release. Triggered by pushing a v* tag (see .github/workflows/release.yml).
# Builds manygit for macOS, Linux and Windows (Intel & Apple/Arm silicon) and
# publishes a GitHub Release. Triggered by pushing a v* tag (see
# .github/workflows/release.yml).
version: 2

before:
Expand All @@ -15,6 +16,7 @@ builds:
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
Expand All @@ -23,6 +25,10 @@ builds:

archives:
- id: manygit
# Every OS ships the same tar.gz, windows included: Windows 10 1803+ and
# Windows 11 both bundle tar.exe, so install.ps1 and the self-updater can
# both just extract it — no second archive format to keep in sync. Inside
# a windows archive goreleaser names the binary manygit.exe automatically.
formats: [tar.gz]
# e.g. manygit_darwin_arm64.tar.gz — the installer and self-updater expect
# exactly this name.
Expand All @@ -41,10 +47,11 @@ checksum:
# loaded until the user trusts it. Nothing here can waive that — it is a decision
# Homebrew reserves for the machine running the code.
#
# Casks are macOS-only by design; Linux installs through install.sh. Note the
# cask points at the SAME archive install.sh downloads — there is no separate
# brew build — which is why manygit works out who owns its binary at runtime
# rather than from a build flag. See docs/adr/0001-runtime-install-ownership-detection.md.
# Casks are macOS-only by design; Linux installs through install.sh, Windows
# through install.ps1 (or install.sh under Git Bash/MSYS/WSL). Note the cask
# points at the SAME archive install.sh downloads — there is no separate brew
# build — which is why manygit works out who owns its binary at runtime rather
# than from a build flag. See docs/adr/0001-runtime-install-ownership-detection.md.
homebrew_casks:
- name: manygit
binaries:
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ you can run `manygit` from anywhere. On each launch it checks for a newer releas
and offers to update itself (`--no-update-check`, or `MANYGIT_NO_UPDATE_CHECK=1`,
skips that).

**Windows — installer script:**

```powershell
irm https://raw.githubusercontent.com/rabeeh-ta/manygit/main/install.ps1 | iex
```

Drops `manygit.exe` into `%LocalAppData%\manygit\bin` and adds it to your PATH.
Same self-update behavior as above. Already have bash — Git Bash, MSYS2, Cygwin
or WSL? `install.sh` detects Windows there too and installs the same binary.

A Homebrew install works differently: Homebrew owns that binary, so manygit never
replaces it. It tells you a release is out — at most once a day, after you quit —
and leaves `brew update && brew upgrade --cask manygit` to you. The
Expand All @@ -58,6 +68,8 @@ tag:
curl -fsSL https://raw.githubusercontent.com/rabeeh-ta/manygit/main/install.sh | bash -s -- v1.0.7
```

On Windows: `$env:MANYGIT_VERSION = "v1.0.7"; irm .../install.ps1 | iex`.

The self-updater only ever moves *forward*, so a downgrade goes through the
installer. Since the launch check will then offer to pull you back to the newest
release, answer `n` or use `--no-update-check` to stay put.
Expand Down Expand Up @@ -118,7 +130,7 @@ Actions apply to the **highlighted** repo (the `>` cursor).
| `F` | show only repos with changes / ahead / behind |
| `/` | filter the focused list by what it shows — repos match on name **and** current branch (`/master` finds every repo on master, and the tag too while `t` is on); branches match on name (type `feat` to find a remote branch among hundreds); scripts on name |
| `:` | **plain-English git** — type a request (`rebase current onto master`, `sync everything in other/`), your AI harness turns it into git commands, you confirm them, manygit runs them. Repo/folder/branch names autocomplete with `tab`, and `@script.sh` pulls a file in as context (`@scripts/update-all.sh only the frontend apps from this`). Output goes to the Output pane. `↑`/`↓` recall this session's earlier requests; `alt+backspace` / `ctrl+w` delete a word, `ctrl+u` the line |
| `!` | **shell in the highlighted repo** — opens a `$manygit:<group>/<repo>` prompt naming the folder you're in; type a bash command and it runs there, streaming into the Output pane. The prompt **stays open**, so it's a shell you sit in: run, read, run again. The bottom bar shows which repo you're in while you type, so it's never ambiguous. `esc` leaves the shell and lands you back on Repos — it doesn't stop a running command; `ctrl+c` does that. `↑`/`↓` recall this session's earlier commands; `alt+backspace` / `ctrl+w` delete a word, `ctrl+u` the line. Non-interactive, so `vim`, `top` and anything else wanting a terminal won't work |
| `!` | **shell in the highlighted repo** — opens a `$manygit:<group>/<repo>` prompt naming the folder you're in; type a bash command and it runs there (via `cmd.exe` on Windows if bash isn't on PATH), streaming into the Output pane. The prompt **stays open**, so it's a shell you sit in: run, read, run again. The bottom bar shows which repo you're in while you type, so it's never ambiguous. `esc` leaves the shell and lands you back on Repos — it doesn't stop a running command; `ctrl+c` does that. `↑`/`↓` recall this session's earlier commands; `alt+backspace` / `ctrl+w` delete a word, `ctrl+u` the line. Non-interactive, so `vim`, `top` and anything else wanting a terminal won't work |
| `o` | open the repo in your editor |
| `z` | zoom the focused pane |
| `esc` | back out one layer of state: the diff, then Changes, then zoom, then the `/` and `F` filters |
Expand Down Expand Up @@ -200,9 +212,10 @@ is a plain push — and the only destructive thing it offers *on its own* is
discarding a repo's changes (`d` / `D`), which always asks you to confirm first.

`!` is the deliberate exception: it runs whatever bash command you type in the
highlighted repo, with no confirm and no blocklist, because a shell escape that
second-guessed you would be useless. What it does instead is leave a record — the
command is echoed into the Output pane with the repo it ran in, before any output
highlighted repo (falling back to `cmd.exe` on Windows if bash isn't on PATH),
with no confirm and no blocklist, because a shell escape that second-guessed
you would be useless. What it does instead is leave a record — the command is
echoed into the Output pane with the repo it ran in, before any output
arrives — and `ctrl+c` stops it mid-run.

`:` deliberately widens that, since merging, rebasing and tagging are the point of
Expand Down
38 changes: 32 additions & 6 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"name": "manygit",
"applicationCategory": "DeveloperApplication",
"applicationSubCategory": "Version Control",
"operatingSystem": "macOS, Linux",
"operatingSystem": "macOS, Linux, Windows",
"url": "https://rabeeh-ta.github.io/manygit/",
"downloadUrl": "https://github.com/rabeeh-ta/manygit/releases",
"codeRepository": "https://github.com/rabeeh-ta/manygit",
Expand Down Expand Up @@ -103,7 +103,7 @@
"name": "Is there a CLI tool to manage multiple git repositories at once?",
"acceptedAnswer": {
"@type": "Answer",
"text": "manygit is one. You point it at a folder, it finds every git repo nested up to three levels underneath, and puts them all on a single screen with each repo's branch and whether it is ahead, behind, or dirty. You then fetch, pull, push, or switch branches on the repo under the cursor without cd-ing between them. It is a free single binary for macOS and Linux."
"text": "manygit is one. You point it at a folder, it finds every git repo nested up to three levels underneath, and puts them all on a single screen with each repo's branch and whether it is ahead, behind, or dirty. You then fetch, pull, push, or switch branches on the repo under the cursor without cd-ing between them. It is a free single binary for macOS, Linux, and Windows."
}
},
{
Expand Down Expand Up @@ -262,7 +262,7 @@ <h1 class="mg-h" tabindex="-1">fine. the whole pitch:</h1>
<div class="shell hero__grid">
<div class="lane">
<div>single binary</div>
<div>macOS · Linux</div>
<div>macOS · Linux · Windows</div>
</div>
<div>
<h1>Which of your repos are <em>dirty</em> right now?</h1>
Expand Down Expand Up @@ -296,7 +296,7 @@ <h1>Which of your repos are <em>dirty</em> right now?</h1>
copy
</button>
</div>
<a class="chip" href="#install">Linux and other ways to install</a>
<a class="chip" href="#install">Linux, Windows, and other ways to install</a>
</div>
</div>
</div>
Expand Down Expand Up @@ -429,6 +429,30 @@ <h3>macOS and Linux &mdash; installer script</h3>
every launch and offers to update itself &mdash; the last time you'll run this.
</p>

<h3>Windows &mdash; installer script</h3>
<div class="cmd">
<div class="cmd__txt" tabindex="0" role="group" aria-label="command, scrolls horizontally">
<span class="cmd__sigil">$</span
>irm https://raw.githubusercontent.com/rabeeh-ta/manygit/main/install.ps1 | iex
</div>
<button
class="cmd__copy"
type="button"
data-copy="irm https://raw.githubusercontent.com/rabeeh-ta/manygit/main/install.ps1 | iex"
aria-label="copy the Windows installer-script command"
>
copy
</button>
</div>

<p>
PowerShell. Drops <code>manygit.exe</code> into
<code>%LocalAppData%\manygit\bin</code> and adds it to your PATH, with the same
self-update behavior as the script above. Already on Git Bash, MSYS2, Cygwin or
WSL? <code>install.sh</code> detects Windows there too and installs the same
binary.
</p>

<h3>With Go</h3>
<div class="cmd">
<div class="cmd__txt" tabindex="0" role="group" aria-label="command, scrolls horizontally">
Expand Down Expand Up @@ -601,7 +625,8 @@ <h2 class="lane">Keys</h2>
<td>
Shell in the highlighted repo. Opens a prompt naming where you are —
<code>$manygit:apps/api-gateway</code>. Type a bash command —
<i>git status -sb</i>, <i>npm test</i> — and it runs there, streaming
<i>git status -sb</i>, <i>npm test</i> — and it runs there (falling back
to <code>cmd.exe</code> on Windows if bash isn't on PATH), streaming
into the Output pane. The prompt stays open, so it is a shell you sit
in: run, read, run again. The bottom bar shows which repo you are in
while you type. <b>esc</b> leaves the shell and lands on Repos;
Expand Down Expand Up @@ -808,7 +833,8 @@ <h2 class="lane">Config</h2>
</p>
<p>
<b>!</b> is the deliberate exception: it runs the bash command you type, in the
highlighted repo, with no confirm and no blocklist — a shell escape that
highlighted repo (falling back to <code>cmd.exe</code> on Windows if bash isn't
on PATH), with no confirm and no blocklist — a shell escape that
second-guessed you would be useless. What it does instead is leave a record:
the command is echoed into the Output pane with the repo it ran in, before any
output arrives, and <b>ctrl+c</b> stops it mid-run.
Expand Down
26 changes: 18 additions & 8 deletions docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
> then fetches, pulls, pushes, and switches branches on the one under the cursor,
> without cd-ing between them.

manygit is a single Go binary for macOS and Linux, built with Bubble Tea and
Lip Gloss. It is free and open source at https://github.com/rabeeh-ta/manygit
manygit is a single Go binary for macOS, Linux and Windows, built with Bubble
Tea and Lip Gloss. It is free and open source at
https://github.com/rabeeh-ta/manygit

## What problem it solves

Expand Down Expand Up @@ -53,6 +54,13 @@ macOS and Linux, via the installer script:

curl -fsSL https://raw.githubusercontent.com/rabeeh-ta/manygit/main/install.sh | bash

Windows, via PowerShell:

irm https://raw.githubusercontent.com/rabeeh-ta/manygit/main/install.ps1 | iex

(Git Bash, MSYS2, Cygwin and WSL can use install.sh instead — it detects Windows
there too and installs the same binary.)

The installer-script build self-updates: it offers to replace itself when a newer
release exists. A Homebrew or `go install` build is owned by that package manager,
so manygit never replaces it — it reports the new release at most once a day, after
Expand All @@ -78,7 +86,8 @@ It walks the folder three levels deep by default, skips `node_modules`, `vendor`
## Panes

- `1` Repos — every repo, its current branch, and its status
- `2` Scripts — `*.sh` files near the root, runnable in place
- `2` Scripts — script files near the root, runnable in place (`*.sh`
everywhere; `*.ps1`, `*.cmd`, `*.bat` also count on Windows)
- `3` Branches — the highlighted repo's local and remote branches
- `4` PRs — GitHub pull requests (needs the `gh` CLI, signed in)
- `5` Graph — a colored commit graph for the highlighted repo
Expand Down Expand Up @@ -110,8 +119,8 @@ Actions apply to the repo under the `>` cursor.
- `!` — shell in the highlighted repo. Opens a prompt reading `$manygit:apps/api-gateway`
(the key is `!`, vim's shell-escape convention; the prompt itself renders as a shell
prompt naming where you are — the repo's path relative to the scanned root, shortened
from the left with `…` on a narrow terminal). Type a bash command and it runs there,
with its
from the left with `…` on a narrow terminal). Type a bash command and it runs there
(falling back to `cmd.exe` on Windows if bash isn't on PATH), with its
combined stdout and stderr streaming into the Output pane. The prompt stays open after
each command, so it is a shell you sit in rather than a one-shot. The bottom bar shows
the repo name while you type, so it is always clear which folder the command lands in.
Expand Down Expand Up @@ -177,9 +186,10 @@ is a plain push. The only destructive action it offers on its own is discarding
repo's changes (`d` / `D`), which always asks first.

`!` is the deliberate exception: it runs the bash command you type in the
highlighted repo, with no confirm and no blocklist, because a shell escape that
second-guessed you would be useless. The command is echoed into the Output pane
with the repo it ran in, so there is a record, and `ctrl+c` stops it mid-run.
highlighted repo (falling back to `cmd.exe` on Windows if bash isn't on PATH),
with no confirm and no blocklist, because a shell escape that second-guessed
you would be useless. The command is echoed into the Output pane with the repo
it ran in, so there is a record, and `ctrl+c` stops it mid-run.

`:` widens that, deliberately. It can merge, rebase, tag and push, because that
is what it is for — but only commands it has shown you and you have confirmed,
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/x/exp/teatest v0.0.0-20260629091435-9c70f75e26a4
golang.org/x/sys v0.36.0
gopkg.in/yaml.v3 v3.0.1
)

Expand All @@ -27,6 +28,5 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.28.0 // indirect
)
101 changes: 101 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# manygit installer for native Windows (PowerShell). Usage:
# irm https://raw.githubusercontent.com/rabeeh-ta/manygit/main/install.ps1 | iex
#
# Installs the latest release by default. To pin a version (e.g. to roll back),
# set $env:MANYGIT_VERSION before running, or download this script and pass it
# as -Version:
# $env:MANYGIT_VERSION = "v1.0.7"; irm .../install.ps1 | iex
#
# On Git Bash, MSYS2, Cygwin or WSL, use install.sh instead — it detects
# Windows the same way and installs the same binary.
param(
[string]$Version = $env:MANYGIT_VERSION
)

$ErrorActionPreference = 'Stop'

$repo = 'rabeeh-ta/manygit'
$exe = 'manygit.exe'
$installDir = if ($env:MANYGIT_INSTALL_DIR) { $env:MANYGIT_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'manygit\bin' }

function Die($msg) {
Write-Error "error: $msg"
exit 1
}

if (-not (Get-Command tar -ErrorAction SilentlyContinue)) {
Die "tar.exe is required (bundled with Windows 10 1803+ and Windows 11)"
}

switch ($env:PROCESSOR_ARCHITECTURE) {
'AMD64' { $arch = 'amd64' }
'ARM64' { $arch = 'arm64' }
default { Die "unsupported architecture: $env:PROCESSOR_ARCHITECTURE" }
}

# An explicit version (-Version or MANYGIT_VERSION) pins the install;
# otherwise the newest release wins. Pinning is how you roll back to an
# earlier build.
$pinned = [bool]$Version
if ($pinned) {
$tag = if ($Version.StartsWith('v')) { $Version } else { "v$Version" }
try {
Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases/tags/$tag" -UseBasicParsing | Out-Null
} catch {
Die "no release tagged $tag (see https://github.com/$repo/releases)"
}
} else {
$latest = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases/latest" -UseBasicParsing
$tag = $latest.tag_name
if (-not $tag) { Die "no published release found for $repo yet" }
}

$asset = "manygit_windows_${arch}.tar.gz"
$url = "https://github.com/$repo/releases/download/$tag/$asset"

Write-Host "Installing $exe $tag (windows/$arch)..."

$tmp = Join-Path ([System.IO.Path]::GetTempPath()) "manygit-install-$tag-$arch"
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
try {
$archivePath = Join-Path $tmp $asset
try {
Invoke-WebRequest -Uri $url -OutFile $archivePath -UseBasicParsing
} catch {
Die "download failed: $url"
}
# tar.exe (bundled since Windows 10 1803) reads .tar.gz directly — no
# separate zip archive to maintain just for this platform.
tar -xzf $archivePath -C $tmp
if ($LASTEXITCODE -ne 0) { Die "could not extract $asset" }
$extracted = Join-Path $tmp $exe
if (-not (Test-Path $extracted)) { Die "archive did not contain $exe" }

New-Item -ItemType Directory -Force -Path $installDir | Out-Null
$dest = Join-Path $installDir $exe
Move-Item -Force $extracted $dest
Write-Host "Installed to $dest"
} finally {
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}

# A pinned version is usually a rollback, and manygit's launch check would
# offer to pull it straight back to newest. Say so instead of letting it
# surprise them.
if ($pinned) {
Write-Host "Pinned to $tag. manygit checks for a newer release on launch -- answer `"n`","
Write-Host "or use --no-update-check / MANYGIT_NO_UPDATE_CHECK=1 to stay on this version."
}

# Put installDir on the user's PATH if it isn't already.
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$entries = @()
if ($userPath) { $entries = $userPath -split ';' }
if ($entries -notcontains $installDir) {
$newPath = if ($userPath) { "$installDir;$userPath" } else { $installDir }
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
$env:Path = "$installDir;$env:Path"
Write-Host "Added $installDir to your PATH. Open a new terminal to pick it up everywhere."
}

Write-Host "Done. Run: manygit"
Loading