cmd/gitbay/migrate.go

e91fdfe0cc2f9bfdcf99bf1a316d219bed8e2ebf
gitbay/cmd/gitbay/migrate.go history · blame · raw

125 lines · 4315 bytes

  1package main
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"fmt"
  7	"os"
  8	"os/exec"
  9	"strings"
 10
 11	"github.com/spf13/cobra"
 12
 13	"gitbay.org/gitbay/internal/protocol"
 14	"gitbay.org/gitbay/internal/toolpath"
 15)
 16
 17func migrateCmd() *cobra.Command {
 18	var from string
 19	var fromPort int
 20	cmd := &cobra.Command{
 21		Use:   "migrate",
 22		Short: "move your account here from another gitbay instance: gitbay migrate --from <host>",
 23		Long: `Migrate authenticates to the source instance with your own SSH key,
 24exports your account bundle (profile, emails, repos with settings,
 25issues, MRs, comments), replays it on this instance, then mirrors each
 26repository's git data client-side: clone from the source, push here.
 27Keys are never transferred and emails arrive unverified — trust is
 28per-instance. Re-running resumes; nothing imports twice.`,
 29		Args: cobra.NoArgs,
 30		RunE: func(cmd *cobra.Command, args []string) error {
 31			os.Exit(runMigrate(from, fromPort))
 32			return nil
 33		},
 34	}
 35	cmd.Flags().StringVar(&from, "from", "", "source instance host (required)")
 36	cmd.Flags().IntVar(&fromPort, "from-port", 22, "source instance SSH port")
 37	return cmd
 38}
 39
 40func sourceSSH(host string, port int, extra ...string) *exec.Cmd {
 41	args := []string{}
 42	if port != 0 && port != 22 {
 43		args = append(args, "-p", fmt.Sprint(port))
 44	}
 45	args = append(args, "git@"+host, "--")
 46	args = append(args, extra...)
 47	return exec.Command(toolpath.Look("ssh"), args...)
 48}
 49
 50func runMigrate(from string, fromPort int) int {
 51	if from == "" {
 52		fmt.Fprintln(os.Stderr, "gitbay: --from <host> is required")
 53		return protocol.ExitUsage
 54	}
 55	t, err := resolveTarget()
 56	if err != nil {
 57		fmt.Fprintln(os.Stderr, "gitbay:", err)
 58		return protocol.ExitFailure
 59	}
 60	if t.inst.Host == from {
 61		fmt.Fprintln(os.Stderr, "gitbay: --from is this instance; migrate runs on the TARGET with the source in --from")
 62		return protocol.ExitUsage
 63	}
 64
 65	fmt.Fprintf(os.Stderr, "exporting account bundle from %s ...\n", from)
 66	exp := sourceSSH(from, fromPort, "account", "export")
 67	var bundleBuf, expErr bytes.Buffer
 68	exp.Stdout, exp.Stderr = &bundleBuf, &expErr
 69	if err := exp.Run(); err != nil {
 70		fmt.Fprintf(os.Stderr, "gitbay: export from %s failed: %v\n%s", from, err, expErr.String())
 71		return protocol.ExitProtocol
 72	}
 73
 74	var b struct {
 75		Username string `json:"username"`
 76		Repos    []struct {
 77			Name string `json:"name"`
 78		} `json:"repos"`
 79	}
 80	if err := json.Unmarshal(bundleBuf.Bytes(), &b); err != nil {
 81		fmt.Fprintf(os.Stderr, "gitbay: bundle from %s does not parse: %v\n", from, err)
 82		return protocol.ExitProtocol
 83	}
 84
 85	fmt.Fprintf(os.Stderr, "replaying metadata on %s ...\n", t.inst.Host)
 86	if code := runSSH(t, []string{"account", "import-bundle", "--source", from}, &bundleBuf); code != 0 {
 87		return code
 88	}
 89
 90	// Git data travels client-side: your key authenticates both ends.
 91	for _, r := range b.Repos {
 92		repoPath := b.Username + "/" + r.Name
 93		fmt.Fprintf(os.Stderr, "mirroring %s ...\n", repoPath)
 94		tmp, err := os.MkdirTemp("", "gitbay-migrate-*")
 95		if err != nil {
 96			fmt.Fprintln(os.Stderr, "gitbay:", err)
 97			return protocol.ExitFailure
 98		}
 99		srcURL := fmt.Sprintf("ssh://git@%s/%s.git", from, repoPath)
100		if fromPort != 0 && fromPort != 22 {
101			srcURL = fmt.Sprintf("ssh://git@%s:%d/%s.git", from, fromPort, repoPath)
102		}
103		clone := exec.Command(toolpath.Look("git"), "clone", "--quiet", "--mirror", srcURL, tmp+"/r")
104		clone.Stderr = os.Stderr
105		if err := clone.Run(); err != nil {
106			fmt.Fprintf(os.Stderr, "gitbay: cloning %s failed; fix and re-run migrate (it resumes)\n", repoPath)
107			os.RemoveAll(tmp)
108			return protocol.ExitFailure
109		}
110		push := exec.Command(toolpath.Look("git"), "-C", tmp+"/r", "push", "--quiet", t.inst.CloneURL(repoPath),
111			"+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*")
112		if len(t.inst.SSHOptions) > 0 {
113			push.Env = append(os.Environ(), "GIT_SSH_COMMAND=ssh "+strings.Join(quoteAll(t.inst.SSHOptions), " "))
114		}
115		push.Stderr = os.Stderr
116		err = push.Run()
117		os.RemoveAll(tmp)
118		if err != nil {
119			fmt.Fprintf(os.Stderr, "gitbay: pushing %s failed; fix and re-run migrate (it resumes)\n", repoPath)
120			return protocol.ExitFailure
121		}
122	}
123	fmt.Fprintf(os.Stderr, "migration complete: %d repositories. Re-register extra keys, re-verify emails,\nand re-apply any deferred policies printed above.\n", len(b.Repos))
124	return protocol.ExitOK
125}