Remove the open command

`open` and `view` shared one implementation and differed in a single
line: `open` also made the project the active one. Only `view` is left,
with the behaviour it always had.

Dropping it from builtinCmds is the part worth noting: a reserved word
cannot be shadowed by an alias, so `open` is now free for one --
`alias open '!xdg-open $1'` works, which it could not before. That also
made a completion test wrong, since it used `open` as its example of a
name a builtin owns; it uses `status` now.

The `runInDir(d, "open", ...)` calls stay: those are macOS's open(1),
which is how an Xcode workspace gets opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 18:35:45 +02:00
co-authored by Claude Opus 5
parent 0d0d28560e
commit 65342bcd7c
8 changed files with 17 additions and 20 deletions
+5 -4
View File
@@ -40,7 +40,7 @@ stand in. Outside `base` no project is selected. `mgsh <project>` starts the
interactive shell with that project preselected. interactive shell with that project preselected.
The commands available directly from the shell are `clone`, `init`, `log`, The commands available directly from the shell are `clone`, `init`, `log`,
`push`, `pushremote`, `release`, `list`, `tag`, `archive`, `show`, `open`, `push`, `pushremote`, `release`, `list`, `tag`, `archive`, `show`, `view`,
`pull`, `fetch`, `status`, `diff`, `overview`, `config`, `count`, `login` and `pull`, `fetch`, `status`, `diff`, `overview`, `config`, `count`, `login` and
`cloneall`; every other command is interactive-only. `cloneall`; every other command is interactive-only.
@@ -52,7 +52,7 @@ project, its git branch and a `*` dirty marker:
``` ```
Features: command history (`~/.mgsh_history`), Tab completion (commands, local Features: command history (`~/.mgsh_history`), Tab completion (commands, local
projects for `cd`/`open`, server repos for `clone`/`show`, branches/tags for projects for `cd`/`view`, server repos for `clone`/`show`, branches/tags for
`checkout`/`tag`, mirror targets for `pushremote`/`release`, filesystem paths for `checkout`/`tag`, mirror targets for `pushremote`/`release`, filesystem paths for
`dist`, and shell-style completion after `!` and for aliases that expand to `dist`, and shell-style completion after `!` and for aliases that expand to
one), and colored `list`/`log`/error output. one), and colored `list`/`log`/error output.
@@ -106,6 +106,7 @@ Run `help` for the full list. Highlights:
| command | description | | command | description |
|---------------------------|------------------------------------------------| |---------------------------|------------------------------------------------|
| `cd [project]` | change project (no argument: back to the base) | | `cd [project]` | change project (no argument: back to the base) |
| `view [project]` | open a project in Xcode / the configured editor |
| `push [comment]` | commit everything and push to the server | | `push [comment]` | commit everything and push to the server |
| `pushremote [desc]` | mirror the repo to a public server (gitea/github/gitlab) | | `pushremote [desc]` | mirror the repo to a public server (gitea/github/gitlab) |
| `pull` / `fetch` | pull / fetch from the server | | `pull` / `fetch` | pull / fetch from the server |
@@ -409,7 +410,7 @@ gitpath = /home/git
gitname = Your Name gitname = Your Name
gitemail = you@example.com gitemail = you@example.com
pushdefault = matching pushdefault = matching
editor = code # fallback opener for `open` editor = code # fallback opener for `view`
alias co 'checkout $1' alias co 'checkout $1'
``` ```
@@ -435,7 +436,7 @@ setting.
| `gitname` | global | `user.name` written to the **global** git config at startup | | `gitname` | global | `user.name` written to the **global** git config at startup |
| `gitemail` | global | `user.email` written to the global git config | | `gitemail` | global | `user.email` written to the global git config |
| `pushdefault` | global | `push.default` written to the global git config | | `pushdefault` | global | `push.default` written to the global git config |
| `editor` | project | opener used by `open`/`view` when the project has no Xcode workspace (default `coda`) | | `editor` | project | opener used by `view` when the project has no Xcode workspace (default `coda`) |
| `remote.<name>.url` | project | base URL of the mirror target `<name>` | | `remote.<name>.url` | project | base URL of the mirror target `<name>` |
| `remote.<name>.key` | project | API token for that target | | `remote.<name>.key` | project | API token for that target |
| `remote.<name>.type` | project | `gitea`\|`github`\|`gitlab`; auto-detected from the url when unset | | `remote.<name>.type` | project | `gitea`\|`github`\|`gitlab`; auto-detected from the url when unset |
+1 -1
View File
@@ -46,7 +46,7 @@ var builtinCmds = map[string]bool{
"diff": true, "pull": true, "fetch": true, "push": true, "edit": true, "diff": true, "pull": true, "fetch": true, "push": true, "edit": true,
"pushremote": true, "overview": true, "archive": true, "init": true, "pushremote": true, "overview": true, "archive": true, "init": true,
"login": true, "cd": true, "checkout": true, "clone": true, "cloneall": true, "login": true, "cd": true, "checkout": true, "clone": true, "cloneall": true,
"open": true, "view": true, "count": true, "tag": true, "alias": true, "view": true, "count": true, "tag": true, "alias": true,
"unalias": true, "config": true, "release": true, "unalias": true, "config": true, "release": true,
} }
+2 -5
View File
@@ -573,7 +573,7 @@ func runCommandDepth(line string, depth int) bool {
} }
} }
case "open", "view": // open project in Xcode / editor case "view": // open a project in Xcode / the configured editor
prj := PRJ prj := PRJ
if w := word(words, 1); w != "" { if w := word(words, 1); w != "" {
prj = w prj = w
@@ -608,9 +608,6 @@ func runCommandDepth(line string, depth int) bool {
} }
runInDir(d, editor, d) runInDir(d, editor, d)
} }
if words[0] == "open" {
PRJ = prj
}
case "count": // count source lines in the project case "count": // count source lines in the project
if !requireProject() { if !requireProject() {
@@ -809,7 +806,7 @@ const gitignore = `.DS_Store
var helpItems = []struct{ cmd, desc string }{ var helpItems = []struct{ cmd, desc string }{
{"cd [project]", "change project (no argument: back to the base)"}, {"cd [project]", "change project (no argument: back to the base)"},
{"open [project]", "open project"}, {"view [project]", "open project in Xcode / the editor"},
{"init", "make new repository from current directory"}, {"init", "make new repository from current directory"},
{"push [comment]", "push changes to git server"}, {"push [comment]", "push changes to git server"},
{"pushremote [@name] [desc]", "mirror repo to the public server(s) (gitea/github/gitlab)"}, {"pushremote [@name] [desc]", "mirror repo to the public server(s) (gitea/github/gitlab)"},
+1 -2
View File
@@ -45,14 +45,13 @@ func runeSuffixes(cands []string, prefix string) ([][]rune, int) {
} }
// builtinCompleter is the command tree. Command names complete at the start of // builtinCompleter is the command tree. Command names complete at the start of
// the line; cd/open/view complete local project names; clone/show complete // the line; cd/view complete local project names; clone/show complete
// repository names cached from the git server; checkout/tag complete branch and // repository names cached from the git server; checkout/tag complete branch and
// tag names; pushremote/release complete mirror targets; dist completes // tag names; pushremote/release complete mirror targets; dist completes
// filesystem paths. // filesystem paths.
func builtinCompleter() *readline.PrefixCompleter { func builtinCompleter() *readline.PrefixCompleter {
return readline.NewPrefixCompleter( return readline.NewPrefixCompleter(
readline.PcItem("cd", readline.PcItemDynamic(dynLocalProjects)), readline.PcItem("cd", readline.PcItemDynamic(dynLocalProjects)),
readline.PcItem("open", readline.PcItemDynamic(dynLocalProjects)),
readline.PcItem("view", readline.PcItemDynamic(dynLocalProjects)), readline.PcItem("view", readline.PcItemDynamic(dynLocalProjects)),
readline.PcItem("clone", readline.PcItem("clone",
readline.PcItem("-a", readline.PcItemDynamic(dynServerArchives)), readline.PcItem("-a", readline.PcItemDynamic(dynServerArchives)),
+1 -1
View File
@@ -33,7 +33,7 @@ type Config struct {
GitName string // git user.name to set globally ("" = leave alone) GitName string // git user.name to set globally ("" = leave alone)
GitEmail string // git user.email to set globally ("" = leave alone) GitEmail string // git user.email to set globally ("" = leave alone)
PushDefault string // git push.default to set globally ("" = leave alone) PushDefault string // git push.default to set globally ("" = leave alone)
Editor string // editor/opener used as fallback by `open` ("" = coda) Editor string // editor/opener used as fallback by `view` ("" = coda)
Mirror string // truthy -> `push` also mirrors via `pushremote` Mirror string // truthy -> `push` also mirrors via `pushremote`
SecretScan string // falsy -> `push` skips the credential scan SecretScan string // falsy -> `push` skips the credential scan
Remotes []RemoteTarget Remotes []RemoteTarget
+1 -1
View File
@@ -145,7 +145,7 @@ func parseArgs() (int, string, bool) {
} }
cls := map[string]int{ cls := map[string]int{
"clone": 2, "init": 2, "log": 2, "clone": 2, "init": 2, "log": 2,
"push": 1, "pushremote": 1, "list": 1, "tag": 1, "archive": 1, "show": 1, "open": 1, "push": 1, "pushremote": 1, "list": 1, "tag": 1, "archive": 1, "show": 1, "view": 1,
"pull": 1, "fetch": 1, "status": 1, "diff": 1, "overview": 1, "pull": 1, "fetch": 1, "status": 1, "diff": 1, "overview": 1,
"config": 1, "count": 1, "login": 1, "cloneall": 1, "release": 1, "config": 1, "count": 1, "login": 1, "cloneall": 1, "release": 1,
} }
+5 -5
View File
@@ -214,10 +214,10 @@ func TestShellCandidatesThroughAlias(t *testing.T) {
shellTree(t) shellTree(t)
fakeCommands(t, "vi", "ls") fakeCommands(t, "vi", "ls")
withAliases(t, map[string]string{ withAliases(t, map[string]string{
"ll": "!ls -la", "ll": "!ls -la",
"e": "!vi $1", "e": "!vi $1",
"co": "checkout $1", // a builtin, not a shell command "co": "checkout $1", // expands to a builtin, not a shell command
"open": "!xdg-open $1", // shadows a builtin: must not count "status": "!git status", // shadows a builtin: must not count
}) })
// arguments of a shell alias complete against the filesystem // arguments of a shell alias complete against the filesystem
@@ -246,7 +246,7 @@ func TestShellCandidatesThroughAlias(t *testing.T) {
t.Error("an alias expanding to a builtin was treated as a shell line") t.Error("an alias expanding to a builtin was treated as a shell line")
} }
// nor is a name that a builtin owns, since runCommand never expands those // nor is a name that a builtin owns, since runCommand never expands those
if _, _, ok := shellLine("open ma"); ok { if _, _, ok := shellLine("status ma"); ok {
t.Error("a builtin name was resolved through an alias") t.Error("a builtin name was resolved through an alias")
} }
// nor an undefined name // nor an undefined name
+1 -1
View File
@@ -1 +1 @@
4.0.46 4.0.48