cmd/gitbay/migrate.go
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}