Define mirror targets one way: remote.<name>.<field>
There were two spellings for the same thing -- a flat remoteurl/remotekey/remotetype/remotevisibility set for a single server, and remote.<name>.* blocks for several. The flat one is gone; every target, including a lone one, is now a named block with the fields url, key, type and visibility. An existing ~/.mgshrc is converted on the next start. Only the key is rewritten, so values, comments, alignment, commented-out lines and the file's 0600 mode survive untouched, and mgsh prints each rename rather than doing it quietly. The target is named "public", which is what the old settings called the git remote they created, so a converted setup keeps pushing to the same place under the same remote name. A file that carries both spellings keeps what the new one says. The environment follows the same shape: MGSH_REMOTEURL and friends are replaced by MGSH_REMOTE_<NAME>_<FIELD>, so MGSH_REMOTE_GITLAB_KEY sets remote.gitlab.key. The field is read from the end of the variable name, which leaves target names free to contain underscores. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -259,38 +259,37 @@ alias ec '!echo $1' # ec hello -> echo hello (shell)
|
||||
|
||||
Besides the internal ssh git server, `pushremote` mirrors the active project to
|
||||
one or more public hosting servers (Gitea, GitHub or GitLab) over their REST
|
||||
API. A single server is configured flat:
|
||||
API. Each server is one `remote.<name>.<field>` block:
|
||||
|
||||
```ini
|
||||
remoteurl = https://git.example.com # base URL of the server
|
||||
remotekey = <personal-access-token> # API token
|
||||
# remotetype = gitea # optional; auto-detected from remoteurl
|
||||
# remotevisibility = private # visibility of created repos (default private)
|
||||
# mirror = true # `push` also mirrors via pushremote
|
||||
remote.gitea.url = https://git.example.com
|
||||
remote.gitea.key = <personal-access-token>
|
||||
remote.gitea.type = gitea # optional; auto-detected from the url
|
||||
remote.gitea.visibility = private # or public (default private)
|
||||
|
||||
remote.gitlab.url = https://gitlab.example.com
|
||||
remote.gitlab.key = <personal-access-token>
|
||||
remote.gitlab.type = gitlab
|
||||
remote.gitlab.visibility = public
|
||||
|
||||
# remotes = gitea, gitlab # optional: restrict and order the set
|
||||
# mirror = true # `push` also mirrors via pushremote
|
||||
```
|
||||
|
||||
Several servers get one `remote.<name>.*` block each:
|
||||
|
||||
```ini
|
||||
remote.gitea.url = https://git.example.com
|
||||
remote.gitea.key = <personal-access-token>
|
||||
|
||||
remote.hub.url = https://github.com
|
||||
remote.hub.key = <personal-access-token>
|
||||
remote.hub.visibility = public
|
||||
|
||||
# remotes = gitea, hub # optional: restrict and order the set
|
||||
```
|
||||
`<name>` is yours to pick; there is no other spelling. Older versions had a flat
|
||||
`remoteurl`/`remotekey` pair for a single server — mgsh converts those to
|
||||
`remote.public.*` in place on the next start and says so, keeping the git remote
|
||||
name those versions used.
|
||||
|
||||
| command | pushes to |
|
||||
|------------------------|-----------------------------------------------|
|
||||
| `pushremote` | every configured target, in order |
|
||||
| `pushremote @hub` | only `hub` |
|
||||
| `pushremote @hub @gitea` | those two |
|
||||
| `pushremote @gitea` | only `gitea` |
|
||||
| `pushremote @gitea @gitlab` | those two |
|
||||
| `pushremote a fix` | every target, description "a fix" |
|
||||
|
||||
Each target owns a git remote of the same name in the repository (the flat form
|
||||
uses `public`, as before), so `git push hub` keeps working outside mgsh. A
|
||||
Each target owns a git remote of the same name in the repository, so
|
||||
`git push gitlab` keeps working outside mgsh. A
|
||||
target that fails does not stop the others; with more than one target
|
||||
`pushremote` prints an `n/m remotes updated` summary. `remotes = …` restricts
|
||||
and orders the set, which is mostly useful in a project `.mgshrc` — see below.
|
||||
@@ -304,9 +303,10 @@ The token is sent as a one-shot HTTP auth header: it is never written into the
|
||||
repo's git config, and it reaches git through the environment rather than the
|
||||
command line, so it does not show up in the process table. Because `~/.mgshrc`
|
||||
then holds a credential, mgsh creates it mode `600` and warns at startup if an
|
||||
existing file is readable by others. The provider is auto-detected from `remoteurl` (`github.com` →
|
||||
GitHub, `gitlab*` → GitLab, otherwise Gitea) and can be forced with
|
||||
`remotetype`. Set `mirror = true` to have every `push` mirror automatically.
|
||||
existing file is readable by others. The provider is auto-detected from the url
|
||||
(`github.com` → GitHub, `gitlab*` → GitLab, otherwise Gitea) and can be forced
|
||||
with `remote.<name>.type`. Set `mirror = true` to have every `push` mirror
|
||||
automatically.
|
||||
|
||||
### Releases
|
||||
|
||||
@@ -390,8 +390,10 @@ alias co 'checkout $1'
|
||||
### Settings reference
|
||||
|
||||
Every setting can also be given as an environment variable named `MGSH_<KEY>`
|
||||
(e.g. `MGSH_GITHOST`), which wins over both files. "Scope" says whether a
|
||||
project `.mgshrc` may override the setting.
|
||||
(e.g. `MGSH_GITHOST`), which wins over both files; a mirror field is
|
||||
`MGSH_REMOTE_<NAME>_<FIELD>`, so `MGSH_REMOTE_GITLAB_KEY` sets
|
||||
`remote.gitlab.key`. "Scope" says whether a project `.mgshrc` may override the
|
||||
setting.
|
||||
|
||||
| setting | scope | meaning |
|
||||
|---|---|---|
|
||||
@@ -405,14 +407,10 @@ project `.mgshrc` may override the setting.
|
||||
| `gitemail` | global | `user.email` 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`) |
|
||||
| `remoteurl` | project | base URL of a single mirror server (target name `public`) |
|
||||
| `remotekey` | project | API token for `remoteurl` |
|
||||
| `remotetype` | project | `gitea`\|`github`\|`gitlab`; auto-detected from the URL when unset |
|
||||
| `remotevisibility` | project | `private` (default) or `public` for repositories created by `pushremote` |
|
||||
| `remote.<name>.url` | project | base URL of the named mirror target |
|
||||
| `remote.<name>.url` | project | base URL of the mirror target `<name>` |
|
||||
| `remote.<name>.key` | project | API token for that target |
|
||||
| `remote.<name>.type` | project | provider override for that target |
|
||||
| `remote.<name>.visibility` | project | visibility for that target |
|
||||
| `remote.<name>.type` | project | `gitea`\|`github`\|`gitlab`; auto-detected from the url when unset |
|
||||
| `remote.<name>.visibility` | project | `private` (default) or `public` for repositories `pushremote` creates |
|
||||
| `remotes` | project | comma- or space-separated list restricting and ordering the mirror targets |
|
||||
| `mirror` | project | truthy (`1`/`true`/`yes`/`on`) → every `push` also mirrors |
|
||||
| `secretscan` | project | `off` disables the credential check `push` runs before committing (on by default; only an explicit `off` disables it) |
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
@@ -33,10 +34,6 @@ type Config struct {
|
||||
GitEmail string // git user.email 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)
|
||||
RemoteURL string // public mirror server base URL (Gitea/GitHub/GitLab)
|
||||
RemoteKey string // API token for the mirror server (used by `pushremote`)
|
||||
RemoteType string // "gitea"|"github"|"gitlab" (auto-detected when empty)
|
||||
RemoteVis string // visibility of created repos: "private" (default)|"public"
|
||||
Mirror string // truthy -> `push` also mirrors via `pushremote`
|
||||
SecretScan string // falsy -> `push` skips the credential scan
|
||||
Remotes []RemoteTarget
|
||||
@@ -54,22 +51,25 @@ type RemoteTarget struct {
|
||||
Vis string // "private" (default) | "public"
|
||||
}
|
||||
|
||||
// legacyRemoteName is the target name for the flat remoteurl/remotekey pair,
|
||||
// matching the git remote that earlier versions created.
|
||||
// legacyRemoteName is the target the pre-4.1 flat remoteurl/remotekey settings
|
||||
// are migrated to. It matches the git remote those versions created, so a
|
||||
// converted configuration keeps pushing to the same place.
|
||||
const legacyRemoteName = "public"
|
||||
|
||||
// legacyRemoteKeys maps the old flat spelling onto the named-target form. A
|
||||
// mirror target is defined one way now, not two.
|
||||
var legacyRemoteKeys = map[string]string{
|
||||
"remoteurl": "remote." + legacyRemoteName + ".url",
|
||||
"remotekey": "remote." + legacyRemoteName + ".key",
|
||||
"remotetype": "remote." + legacyRemoteName + ".type",
|
||||
"remotevisibility": "remote." + legacyRemoteName + ".visibility",
|
||||
}
|
||||
|
||||
// mirrorTargets returns the usable mirror targets in configured order, plus the
|
||||
// names of targets that are defined but unusable (missing url or key) so the
|
||||
// caller can complain about them instead of silently skipping.
|
||||
func (c Config) mirrorTargets() (usable []RemoteTarget, incomplete []string) {
|
||||
var all []RemoteTarget
|
||||
if c.RemoteURL != "" || c.RemoteKey != "" {
|
||||
all = append(all, RemoteTarget{
|
||||
Name: legacyRemoteName, URL: c.RemoteURL, Key: c.RemoteKey,
|
||||
Type: c.RemoteType, Vis: c.RemoteVis,
|
||||
})
|
||||
}
|
||||
all = append(all, c.Remotes...)
|
||||
all := c.Remotes
|
||||
|
||||
// `remotes = a, b` narrows and orders the set — a project .mgshrc uses it
|
||||
// to mirror to only some of the globally configured servers.
|
||||
@@ -147,11 +147,54 @@ func loadConfig() Config {
|
||||
m := parseConfig(string(data))
|
||||
applyConfig(&c, m)
|
||||
warnConfigPerms(path, m)
|
||||
migrateRemoteKeys(path, string(data))
|
||||
}
|
||||
applyEnv(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// migrateRemoteKeys converts the pre-4.1 flat remote settings in a config file
|
||||
// to the remote.<name>.<field> spelling, so a mirror target is defined one way
|
||||
// and not two. Only the key is rewritten: values, comments, blank lines and the
|
||||
// file's permissions stay exactly as they are, and commented-out lines are left
|
||||
// alone. Reports what it changed rather than doing it silently.
|
||||
func migrateRemoteKeys(path, data string) {
|
||||
lines := strings.Split(data, "\n")
|
||||
var renamed []string
|
||||
|
||||
for i, ln := range lines {
|
||||
trimmed := strings.TrimLeft(ln, " \t")
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
sep := strings.IndexAny(trimmed, "=:")
|
||||
if sep < 0 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimRight(trimmed[:sep], " \t")
|
||||
dotted, ok := legacyRemoteKeys[strings.ToLower(key)]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
indent := ln[:len(ln)-len(trimmed)]
|
||||
gap := trimmed[len(key):sep] // whatever alignment was there
|
||||
lines[i] = indent + dotted + gap + trimmed[sep:]
|
||||
renamed = append(renamed, key+" → "+dotted)
|
||||
}
|
||||
if len(renamed) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")), configMode); err != nil {
|
||||
errorln("could not update " + path + ": " + err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println(col(cGray, path+": mirror settings renamed to the remote.<name>.* form"))
|
||||
for _, r := range renamed {
|
||||
fmt.Println(col(cGray, " "+r))
|
||||
}
|
||||
}
|
||||
|
||||
// projectGlobalOnly lists settings a project-level .mgshrc must not change:
|
||||
// `base` decides where projects live in the first place, and the git identity
|
||||
// keys are written to the user's *global* git config at startup — applying
|
||||
@@ -279,13 +322,15 @@ func writeConfigTemplate(path string) {
|
||||
b.WriteString("# pushdefault = matching\n")
|
||||
b.WriteString("# editor = code\n\n")
|
||||
b.WriteString("# --- public mirrors for `pushremote` ---\n")
|
||||
b.WriteString("# One block per server; `pushremote` pushes to all of them,\n")
|
||||
b.WriteString("# `pushremote @hub` to a single one.\n")
|
||||
b.WriteString("# remote.hub.url = https://github.com\n")
|
||||
b.WriteString("# remote.hub.key = <personal-access-token>\n")
|
||||
b.WriteString("# remote.hub.visibility = public\n")
|
||||
b.WriteString("# remotes = hub # optional: restrict/order the set\n")
|
||||
b.WriteString("# mirror = true # `push` also mirrors\n\n")
|
||||
b.WriteString("# One 'remote.<name>.*' block per server. `pushremote` pushes to all\n")
|
||||
b.WriteString("# of them, `pushremote @gitlab` to a single one. <name> is also the\n")
|
||||
b.WriteString("# git remote created in the repository.\n")
|
||||
b.WriteString("# remote.gitlab.url = https://gitlab.example.com\n")
|
||||
b.WriteString("# remote.gitlab.key = <personal-access-token>\n")
|
||||
b.WriteString("# remote.gitlab.type = gitlab # optional; detected from the url\n")
|
||||
b.WriteString("# remote.gitlab.visibility = private # or public (default private)\n")
|
||||
b.WriteString("# remotes = gitlab # optional: restrict/order the set\n")
|
||||
b.WriteString("# mirror = true # `push` also mirrors\n\n")
|
||||
b.WriteString("# A project may override any of these (except base and the git\n")
|
||||
b.WriteString("# identity) in its own <project>/.mgshrc.\n")
|
||||
|
||||
@@ -365,14 +410,36 @@ func applyConfig(c *Config, m map[string]string) {
|
||||
set("gitemail", &c.GitEmail)
|
||||
set("pushdefault", &c.PushDefault)
|
||||
set("editor", &c.Editor)
|
||||
set("remoteurl", &c.RemoteURL)
|
||||
set("remotekey", &c.RemoteKey)
|
||||
set("remotetype", &c.RemoteType)
|
||||
set("remotevisibility", &c.RemoteVis)
|
||||
set("remotes", &c.RemoteNames)
|
||||
set("mirror", &c.Mirror)
|
||||
set("secretscan", &c.SecretScan)
|
||||
applyRemoteTargets(c, m)
|
||||
applyRemoteTargets(c, foldLegacyRemoteKeys(m))
|
||||
}
|
||||
|
||||
// foldLegacyRemoteKeys rewrites the pre-4.1 flat remote settings into the
|
||||
// named-target form, so a configuration that has not been converted yet still
|
||||
// works while it is being read. The file itself is converted by
|
||||
// migrateRemoteKeys; this only makes the current run behave.
|
||||
func foldLegacyRemoteKeys(m map[string]string) map[string]string {
|
||||
folded, copied := m, false
|
||||
for old, dotted := range legacyRemoteKeys {
|
||||
v, ok := m[old]
|
||||
if !ok || v == "" {
|
||||
continue
|
||||
}
|
||||
if _, taken := m[dotted]; taken {
|
||||
continue // an explicit new-style setting always wins
|
||||
}
|
||||
if !copied { // copy on first write, never touch the caller's map
|
||||
folded = make(map[string]string, len(m))
|
||||
for k, val := range m {
|
||||
folded[k] = val
|
||||
}
|
||||
copied = true
|
||||
}
|
||||
folded[dotted] = v
|
||||
}
|
||||
return folded
|
||||
}
|
||||
|
||||
// remoteFieldRe matches a named mirror target setting: remote.<name>.<field>.
|
||||
@@ -439,11 +506,55 @@ func applyEnv(c *Config) {
|
||||
env("MGSH_GITEMAIL", &c.GitEmail)
|
||||
env("MGSH_PUSHDEFAULT", &c.PushDefault)
|
||||
env("MGSH_EDITOR", &c.Editor)
|
||||
env("MGSH_REMOTEURL", &c.RemoteURL)
|
||||
env("MGSH_REMOTEKEY", &c.RemoteKey)
|
||||
env("MGSH_REMOTETYPE", &c.RemoteType)
|
||||
env("MGSH_REMOTEVISIBILITY", &c.RemoteVis)
|
||||
env("MGSH_REMOTES", &c.RemoteNames)
|
||||
env("MGSH_MIRROR", &c.Mirror)
|
||||
env("MGSH_SECRETSCAN", &c.SecretScan)
|
||||
applyRemoteEnv(c)
|
||||
}
|
||||
|
||||
// remoteFields are the settings a mirror target is made of.
|
||||
var remoteFields = []string{"url", "key", "type", "visibility"}
|
||||
|
||||
// applyRemoteEnv reads MGSH_REMOTE_<NAME>_<FIELD>, the environment spelling of
|
||||
// a remote.<name>.<field> setting — MGSH_REMOTE_GITLAB_KEY for
|
||||
// remote.gitlab.key. The field is taken from the end, so a target name may
|
||||
// contain underscores itself.
|
||||
func applyRemoteEnv(c *Config) {
|
||||
const prefix = "MGSH_REMOTE_"
|
||||
|
||||
// sorted, so a target these variables introduce lands in the push order the
|
||||
// same way on every run
|
||||
envs := os.Environ()
|
||||
sort.Strings(envs)
|
||||
|
||||
for _, kv := range envs {
|
||||
eq := strings.IndexByte(kv, '=')
|
||||
if eq < 0 {
|
||||
continue
|
||||
}
|
||||
name, value := kv[:eq], kv[eq+1:]
|
||||
if value == "" || !strings.HasPrefix(name, prefix) {
|
||||
continue
|
||||
}
|
||||
rest := name[len(prefix):]
|
||||
us := strings.LastIndexByte(rest, '_')
|
||||
if us <= 0 {
|
||||
continue
|
||||
}
|
||||
target, field := strings.ToLower(rest[:us]), strings.ToLower(rest[us+1:])
|
||||
if !slices.Contains(remoteFields, field) {
|
||||
continue // MGSH_REMOTES and anything else that merely starts alike
|
||||
}
|
||||
t := &c.Remotes[c.remoteIndex(target)]
|
||||
switch field {
|
||||
case "url":
|
||||
t.URL = value
|
||||
case "key":
|
||||
t.Key = value
|
||||
case "type":
|
||||
t.Type = value
|
||||
case "visibility":
|
||||
t.Vis = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+99
-1
@@ -468,7 +468,7 @@ remote.broken.url = https://nowhere.example # no key -> unusable
|
||||
}
|
||||
|
||||
func TestMirrorTargetsLegacyAndSelection(t *testing.T) {
|
||||
// the flat remoteurl/remotekey pair stays supported, as target "public"
|
||||
// the pre-4.1 flat pair still loads, folded onto the target "public"
|
||||
var c Config
|
||||
applyConfig(&c, parseConfig("remoteurl = https://git.example.com\nremotekey = tok\n"))
|
||||
usable, _ := c.mirrorTargets()
|
||||
@@ -1049,3 +1049,101 @@ func TestParseDuSizes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrateRemoteKeys rewrites the pre-4.1 flat spelling in place. Only the
|
||||
// key changes: values, comments and everything else stay byte for byte.
|
||||
func TestMigrateRemoteKeys(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, ".mgshrc")
|
||||
before := `# my config
|
||||
base = /home/me/src
|
||||
remoteurl = https://git.example.com # the mirror
|
||||
remotekey = s3cr3t-token
|
||||
remotevisibility: public
|
||||
# remotetype = gitea (commented out, must stay put)
|
||||
alias co 'checkout $1'
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(before), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := captureStdout(t, func() { migrateRemoteKeys(path, before) })
|
||||
|
||||
got := readFile(t, path)
|
||||
for _, want := range []string{
|
||||
"remote.public.url = https://git.example.com # the mirror",
|
||||
"remote.public.key = s3cr3t-token",
|
||||
" remote.public.visibility: public",
|
||||
"# remotetype = gitea (commented out, must stay put)",
|
||||
"base = /home/me/src",
|
||||
"alias co 'checkout $1'",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("migrated file missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, "\nremoteurl") || strings.Contains(got, "\nremotekey") {
|
||||
t.Errorf("old spelling left behind:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(out, "remoteurl → remote.public.url") {
|
||||
t.Errorf("migration was not reported: %q", out)
|
||||
}
|
||||
// the file keeps its private mode
|
||||
if fi, err := os.Stat(path); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if fi.Mode().Perm() != 0o600 {
|
||||
t.Errorf("mode after migration = %04o, want 0600", fi.Mode().Perm())
|
||||
}
|
||||
|
||||
// running again changes nothing and says nothing
|
||||
second := captureStdout(t, func() { migrateRemoteKeys(path, readFile(t, path)) })
|
||||
if strings.TrimSpace(second) != "" {
|
||||
t.Errorf("a converted file was migrated again: %q", second)
|
||||
}
|
||||
if readFile(t, path) != got {
|
||||
t.Error("a second migration changed the file")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLegacyKeysDoNotOverrideExplicitOnes: a config carrying both spellings must
|
||||
// keep what the new one says.
|
||||
func TestLegacyKeysDoNotOverrideExplicitOnes(t *testing.T) {
|
||||
var c Config
|
||||
applyConfig(&c, parseConfig(
|
||||
"remoteurl = https://old.example\nremote.public.url = https://new.example\n"+
|
||||
"remote.public.key = tok\n"))
|
||||
targets, _ := c.mirrorTargets()
|
||||
if len(targets) != 1 || targets[0].URL != "https://new.example" {
|
||||
t.Errorf("targets = %+v, want the remote.public.url value", targets)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemoteEnvOverrides: MGSH_REMOTE_<NAME>_<FIELD> is the environment
|
||||
// spelling of remote.<name>.<field>.
|
||||
func TestRemoteEnvOverrides(t *testing.T) {
|
||||
t.Setenv("MGSH_REMOTE_GITLAB_URL", "https://gitlab.example")
|
||||
t.Setenv("MGSH_REMOTE_GITLAB_KEY", "env-token")
|
||||
t.Setenv("MGSH_REMOTE_GITLAB_VISIBILITY", "public")
|
||||
t.Setenv("MGSH_REMOTE_MY_HUB_URL", "https://hub.example") // name with an underscore
|
||||
t.Setenv("MGSH_REMOTE_MY_HUB_KEY", "hub-token")
|
||||
t.Setenv("MGSH_REMOTES", "") // must not be mistaken for a target field
|
||||
|
||||
var c Config
|
||||
applyConfig(&c, parseConfig("remote.gitlab.url = https://from-file.example\nremote.gitlab.key = file-token\n"))
|
||||
applyEnv(&c)
|
||||
|
||||
targets, incomplete := c.mirrorTargets()
|
||||
if len(incomplete) != 0 {
|
||||
t.Fatalf("incomplete targets: %v", incomplete)
|
||||
}
|
||||
byName := map[string]RemoteTarget{}
|
||||
for _, tg := range targets {
|
||||
byName[tg.Name] = tg
|
||||
}
|
||||
if g := byName["gitlab"]; g.URL != "https://gitlab.example" || g.Key != "env-token" || g.Vis != "public" {
|
||||
t.Errorf("env did not override the file: %+v", g)
|
||||
}
|
||||
// the field is taken from the end, so the name may contain underscores
|
||||
if h := byName["my_hub"]; h.URL != "https://hub.example" || h.Key != "hub-token" {
|
||||
t.Errorf("MGSH_REMOTE_MY_HUB_* = %+v, want target my_hub", h)
|
||||
}
|
||||
}
|
||||
|
||||
+17
-16
@@ -23,24 +23,25 @@ gitpath = /home/git
|
||||
# editor = code
|
||||
|
||||
# --- pushremote: mirror to public servers (gitea/github/gitlab) via their API ---
|
||||
# A single server, the flat form (this target is named "public"):
|
||||
# remoteurl = https://git.example.com
|
||||
# remotekey = <personal-access-token>
|
||||
# remotetype = gitea # optional; auto-detected from remoteurl
|
||||
# remotevisibility = private # visibility of created repos (default private)
|
||||
# One "remote.<name>.<field>" block per server, with the fields url, key, type
|
||||
# and visibility. <name> is yours to pick and becomes the git remote created in
|
||||
# the repository, so `git push gitlab` keeps working outside mgsh.
|
||||
#
|
||||
# Or any number of named servers. `pushremote` pushes to all of them in the
|
||||
# order given, `pushremote @hub` to a single one. Each target gets a git remote
|
||||
# of the same name in the repository.
|
||||
# remote.gitea.url = https://git.example.com
|
||||
# remote.gitea.key = <personal-access-token>
|
||||
# remote.hub.url = https://github.com
|
||||
# remote.hub.key = <personal-access-token>
|
||||
# remote.hub.type = github # optional; auto-detected from the url
|
||||
# remote.hub.visibility = public # default private
|
||||
# remotes = gitea, hub # optional: restrict and order the set
|
||||
# `pushremote` pushes to every configured server in the order given,
|
||||
# `pushremote @gitlab` to a single one.
|
||||
#
|
||||
# mirror = true # `push` also mirrors via pushremote
|
||||
# remote.gitea.url = https://git.example.com
|
||||
# remote.gitea.key = <personal-access-token>
|
||||
# remote.gitea.type = gitea # optional; auto-detected from the url
|
||||
# remote.gitea.visibility = private # or public (default private)
|
||||
#
|
||||
# remote.gitlab.url = https://gitlab.example.com
|
||||
# remote.gitlab.key = <personal-access-token>
|
||||
# remote.gitlab.type = gitlab
|
||||
# remote.gitlab.visibility = public
|
||||
#
|
||||
# remotes = gitea, gitlab # optional: restrict and order the set
|
||||
# mirror = true # `push` also mirrors via pushremote
|
||||
|
||||
# --- safety ---
|
||||
# `push` checks the staged diff for private keys and API tokens before it
|
||||
|
||||
@@ -4,23 +4,18 @@ package main
|
||||
// git hosting server (Gitea, GitHub or GitLab), creating the repository via the
|
||||
// server's REST API when it does not exist yet.
|
||||
//
|
||||
// Configuration (in ~/.mgshrc, a project .mgshrc, or MGSH_* env) — either a
|
||||
// single flat target:
|
||||
// Configuration (in ~/.mgshrc, a project .mgshrc, or MGSH_* env): one
|
||||
// remote.<name>.<field> block per server, which `pushremote` mirrors to in turn.
|
||||
//
|
||||
// remoteurl = https://git.example.com base URL of the server
|
||||
// remotekey = <api-token> personal access token
|
||||
// remotetype = gitea|github|gitlab optional; auto-detected from the URL
|
||||
// remote.gitlab.url = https://gitlab.example.com
|
||||
// remote.gitlab.key = <api-token>
|
||||
// remote.gitlab.type = gitlab optional; detected from the url
|
||||
// remote.gitlab.visibility = public or private (the default)
|
||||
// remotes = gitlab optional: restrict/order the set
|
||||
//
|
||||
// or any number of named ones, which `pushremote` mirrors to in turn:
|
||||
//
|
||||
// remote.gitea.url = https://git.example.com
|
||||
// remote.gitea.key = <api-token>
|
||||
// remote.hub.url = https://github.com
|
||||
// remote.hub.key = <api-token>
|
||||
// remote.hub.visibility = public
|
||||
// remotes = gitea, hub optional: restrict/order the set
|
||||
//
|
||||
// Each target owns a git remote of the same name in the repository.
|
||||
// Each target owns a git remote of its own name in the repository. There is no
|
||||
// second spelling: the pre-4.1 flat remoteurl/remotekey pair is migrated to
|
||||
// remote.public.* on load.
|
||||
//
|
||||
// The token is used for the API calls and, via an HTTP Basic auth header, for
|
||||
// the git push. It is never written into the repository's git config, and it is
|
||||
@@ -287,7 +282,7 @@ func handlePushRemote(args string) {
|
||||
targets = pickRemotes(targets, names)
|
||||
if len(targets) == 0 {
|
||||
if len(names) == 0 { // an unknown @name already reported itself
|
||||
errorln("pushremote needs 'remoteurl'/'remotekey' or a 'remote.<name>.*' block in " + configFile())
|
||||
errorln("pushremote needs a 'remote.<name>.url' and 'remote.<name>.key' in " + configFile())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -147,7 +147,6 @@ func configKeys() []string {
|
||||
keys := []string{
|
||||
"base", "githost", "gitport", "gituser", "gitpath", "gitkey",
|
||||
"gitname", "gitemail", "pushdefault", "editor",
|
||||
"remoteurl", "remotekey", "remotetype", "remotevisibility",
|
||||
"remotes", "mirror", "secretscan",
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4.0.40
|
||||
4.0.42
|
||||
|
||||
Reference in New Issue
Block a user