diff --git a/cmd/git-remote-offloading/README.md b/cmd/git-remote-offloading/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/cmd/git-remote-offloading/main.go b/cmd/git-remote-offloading/main.go new file mode 100644 index 0000000000000000000000000000000000000000..7c44afa80d6a118ad45a32f2020352a825f14fe5 --- /dev/null +++ b/cmd/git-remote-offloading/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "strings" + + "gitlab.com/gitlab-org/gitaly/v16/internal/log" + labkittracing "gitlab.com/gitlab-org/labkit/tracing" +) + +type offloadingCmdArg struct { + // remoteName is the name of the remote defined in gitconfig + remoteName string + sinkURL string + // prefix string + // cacheDir string + // gitDir string +} + +type remoteCommand struct { + exec func(context.Context, *offloadingCmdArg, io.Writer) error +} + +// remoteCommandSupported contains all the "capabilities" supported by git-remote-offloading +// EXCEPT the "capabilities" command itself, for two reasons: +// 1. The "capabilities" command doesn't include itself in its output +// 2. Including "capabilities" in this map would create a circular dependency, +// as the capabilities handler uses this map to generate its output, +// which would cause a compilation error +var remoteCommandSupported = map[string]remoteCommand{ + "list": {}, +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Since the environment is sanitized at the moment, we're only + // using this to extract the correlation ID. The finished() call + // to clean up the tracing will be a NOP here. + ctx, finished := labkittracing.ExtractFromEnv(ctx) + defer finished() + logger, err := log.Configure(os.Stdout, "json", "info") + if err != nil { + fmt.Printf("new subprocess logger: %q", err) + os.Exit(1) + } + + logger.Info("starting git remote helper offloading") + + if err := run(ctx, os.Args); err != nil { + logger.WithError(err).Error("executing git remote helper offloading") + os.Exit(1) + } +} + +func run(ctx context.Context, args []string) error { + scanner := bufio.NewScanner(os.Stdin) + remoteRepoUrl, remoteName, err := parseRemoteArgs(args[1:]) + if err != nil { + return err + } + offloadingCfg := &offloadingCmdArg{ + remoteName: remoteName, + sinkURL: remoteRepoUrl, + } + for scanner.Scan() { + line := scanner.Text() + if line == "" || line == "\n" { + // Exit loop if an empty line is entered + break + } + lineSplit := strings.SplitN(line, " ", 2) + cmdName := lineSplit[0] + if cmdName == "capabilities" { + if err := capabilities(ctx, offloadingCfg, os.Stdout); err != nil { + return err + } + } + if cmd, ok := remoteCommandSupported[cmdName]; ok { + err := cmd.exec(ctx, offloadingCfg, os.Stdout) + if err != nil { + return err + } + } + } + return nil +} + +func parseRemoteArgs(args []string) (remoteRepoUrl string, remoteName string, err error) { + if len(args) == 1 { + // name of the remote or URL + remoteRepoUrl = args[0] + remoteName = "offload" + + } else if len(args) == 2 { + // optional URL or address + remoteName = args[0] + remoteRepoUrl = args[1] + } else { + return "", "", fmt.Errorf("expected 1 or 2 arguments, got %d", len(args)) + } + return remoteRepoUrl, remoteName, nil +} + +// capabilities is the handler for the "capabilities" command, it lists all the +// "capabilities" supported by git-remote-offloading. +func capabilities(ctx context.Context, arg *offloadingCmdArg, resultWriter io.Writer) error { + for capability := range remoteCommandSupported { + if _, err := fmt.Fprintln(resultWriter, capability); err != nil { + return fmt.Errorf("print capability %q: %w", capability, err) + } + } + return nil +} diff --git a/cmd/git-remote-offloading/main_test.go b/cmd/git-remote-offloading/main_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3f517634baa9e9c27ebd319052bbd05c003d3920 --- /dev/null +++ b/cmd/git-remote-offloading/main_test.go @@ -0,0 +1,24 @@ +package main + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" + "golang.org/x/exp/maps" +) + +func TestCapabilitiesCmd(t *testing.T) { + t.Parallel() + var resultBuf bytes.Buffer + ctx := testhelper.Context(t) + err := capabilities(ctx, nil, &resultBuf) + require.NoError(t, err) + + expectedCapabilities := maps.Keys(remoteCommandSupported) + actualCapabilities := strings.Split(strings.TrimSpace(resultBuf.String()), "\n") + + require.ElementsMatch(t, expectedCapabilities, actualCapabilities) +} diff --git a/cmd/git-remote-offloading/testhelper_test.go b/cmd/git-remote-offloading/testhelper_test.go new file mode 100644 index 0000000000000000000000000000000000000000..432115dc0309f4dd75780a0cc22dee780378aab5 --- /dev/null +++ b/cmd/git-remote-offloading/testhelper_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "testing" + + "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" +) + +func TestMain(m *testing.M) { + testhelper.Run(m) +}