# Test CLI plan mode end to end

`efd5740`→[main](/content/gh/entireio/git-sync/commits/main/index.html)·

Soph·3mo ago·1 file·+350 added/-0 removed

## Sessions

5424186859f4View transcript

[?\
Can you take a look at the go code (wasm) in /Users/soph/Work/entire/devenv/entire-io-worktree1 based a bit on that I wonder if something like this can be build:Codex·GPT-5.4·1 step](/content/gh/entireio/git-sync/session/019d6d29-8cf7-7fe3-adc9-8c3e4d9d5603#timeline-5424186859f4/index.html)

## Changes

1

- cmd/git-sync

- Mmain\_test.go+350

```
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"sync"
	"testing"
	"time"

"github.com/go-git/go-billy/v5"
	"github.com/go-git/go-billy/v5/memfs"
	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing/format/pktline"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/protocol/packp"
	"github.com/go-git/go-git/v5/plumbing/transport"
	transportclient "github.com/go-git/go-git/v5/plumbing/transport/client"
	transporthttp "github.com/go-git/go-git/v5/plumbing/transport/http"
	transportserver "github.com/go-git/go-git/v5/plumbing/transport/server"
	"github.com/go-git/go-git/v5/storage/memory"
	"github.com/soph/git-sync/internal/syncer"
)

const testBranch = "master"

func TestMarshalOutput_JSONShape(t *testing.T) {
	data, err := marshalOutput(syncer.FetchResult{
		SourceURL:      "https://example.com/source.git",

t.Fatalf("unexpected FetchedObjects: %#v", got)
	}
}

func TestRun_Plan_JSONDoesNotPush(t *testing.T) {
	sourceRepo, sourceFS := newSourceRepo(t)
	makeCommits(t, sourceRepo, sourceFS, 3)

targetRepo, err := git.Init(memory.NewStorage(), nil)
	if err != nil {
		t.Fatalf("init target repo: %v", err)
	}

sourceServer := newSmartHTTPRepoServer(t, sourceRepo)
	targetServer := newSmartHTTPRepoServer(t, targetRepo)
	defer sourceServer.Close()
	defer targetServer.Close()

output, err := captureStdout(func() error {
		return run(context.Background(), []string{
			"plan",
			"--json",
			"--stats",
			sourceServer.RepoURL(),
			targetServer.RepoURL(),
		})
	})
	if err != nil {
		t.Fatalf("run plan: %v", err)
	}

var result syncer.Result
	if err := json.Unmarshal([]byte(output), &result); err != nil {
		t.Fatalf("decode plan json: %v\noutput=%s", err, output)
	}
	if !result.DryRun {
		t.Fatalf("expected dry-run result")
	}
	if len(result.Plans) == 0 {
		t.Fatalf("expected plan entries")
	}
	if result.Pushed != 0 {
		t.Fatalf("expected no pushed refs, got %+v", result)
	}
	if targetServer.Count("git-receive-pack") != 0 {
		t.Fatalf("expected no receive-pack POSTs, got %d", targetServer.Count("git-receive-pack"))
	}
}

func captureStdout(fn func() error) (string, error) {
	old := os.Stdout
	r, w, err := os.Pipe()
	if err != nil {
		return "", err
	}
	os.Stdout = w
	defer func() { os.Stdout = old }()

runErr := fn()
	_ = w.Close()

var buf bytes.Buffer
	if _, err := io.Copy(&buf, r); err != nil {
		return "", err
	}
	_ = r.Close()
	return strings.TrimSpace(buf.String()), runErr
}

func newSourceRepo(t *testing.T) (*git.Repository, billy.Filesystem) {
	t.Helper()

fs := memfs.New()
	repo, err := git.Init(memory.NewStorage(), fs)
	if err != nil {
		t.Fatalf("init source repo: %v", err)
	}

return repo, fs
}

func makeCommits(t *testing.T, repo *git.Repository, fs billy.Filesystem, count int) {
	t.Helper()

wt, err := repo.Worktree()
	if err != nil {
		t.Fatalf("open worktree: %v", err)
	}

for i := 0; i < count; i++ {
		content := strings.Repeat(fmt.Sprintf("line %d %d\n", i, time.Now().UnixNano()), 24)
		file, err := fs.Create("tracked.txt")
		if err != nil {
			t.Fatalf("create file: %v", err)
		}
		if _, err := io.WriteString(file, content); err != nil {
			t.Fatalf("write file: %v", err)
		}
		if err := file.Close(); err != nil {
			t.Fatalf("close file: %v", err)
		}

if _, err := wt.Add("tracked.txt"); err != nil {
			t.Fatalf("add file: %v", err)
		}

_, err = wt.Commit(fmt.Sprintf("commit %d", i), &git.CommitOptions{
			Author:    &objectSignature,
			Committer: &objectSignature,
		})
		if err != nil {
			t.Fatalf("commit: %v", err)
		}
	}
}

var objectSignature = signature()

func signature() object.Signature {
	return object.Signature{
		Name:  "test",
		Email: "test@example.com",
		When:  time.Unix(1, 0).UTC(),
	}
}

type smartHTTPRepoServer struct {
	t        *testing.T
	server   *httptest.Server
	repo     *git.Repository
	repoPath string

mu           sync.Mutex
	receivePacks int
}

func newSmartHTTPRepoServer(t *testing.T, repo *git.Repository) *smartHTTPRepoServer {
	t.Helper()

s := &smartHTTPRepoServer{
		t:        t,
		repo:     repo,
		repoPath: "/repo.git",
	}
	s.server = httptest.NewServer(http.HandlerFunc(s.handle))
	return s
}

func (s *smartHTTPRepoServer) Close() {
	s.server.Close()
}

func (s *smartHTTPRepoServer) RepoURL() string {
	return s.server.URL + s.repoPath
}

func (s *smartHTTPRepoServer) Count(service string) int {
	s.mu.Lock()
	defer s.mu.Unlock()
	if service == "git-receive-pack" {
		return s.receivePacks
	}
	return 0
}

func (s *smartHTTPRepoServer) handle(w http.ResponseWriter, r *http.Request) {
	switch {
	case r.Method == http.MethodGet && r.URL.Path == s.repoPath+"/info/refs":
		s.handleInfoRefs(w, r)
	case r.Method == http.MethodPost && r.URL.Path == s.repoPath+"/git-upload-pack":
		s.handleUploadPack(w, r)
	case r.Method == http.MethodPost && r.URL.Path == s.repoPath+"/git-receive-pack":
		s.handleReceivePack(w, r)
	default:
		http.NotFound(w, r)
	}
}

func (s *smartHTTPRepoServer) handleInfoRefs(w http.ResponseWriter, r *http.Request) {
	service := r.URL.Query().Get("service")
	if service != transport.UploadPackServiceName && service != transport.ReceivePackServiceName {
		http.Error(w, "missing service", http.StatusBadRequest)
		return
	}

session, err := s.newSession(service)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

var ar *packp.AdvRefs
	switch service {
	case transport.UploadPackServiceName:
		ar, err = session.(transport.UploadPackSession).AdvertisedReferencesContext(r.Context())
	case transport.ReceivePackServiceName:
		ar, err = session.(transport.ReceivePackSession).AdvertisedReferencesContext(r.Context())
	}
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

ar.Prefix = [][]byte{
		[]byte("# service=" + service),
		pktline.Flush,
	}

var buf bytes.Buffer
	if err := ar.Encode(&buf); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", service))
	if _, err := w.Write(buf.Bytes()); err != nil {
		s.t.Fatalf("write advertised refs: %v", err)
	}
}

func (s *smartHTTPRepoServer) handleUploadPack(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer r.Body.Close()

session, err := s.newSession(transport.UploadPackServiceName)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

req := packp.NewUploadPackRequest()
	if err := req.Decode(bytes.NewReader(body)); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

resp, err := session.(transport.UploadPackSession).UploadPack(r.Context(), req)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

var buf bytes.Buffer
	if err := resp.Encode(&buf); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

w.Header().Set("Content-Type", "application/x-git-upload-pack-result")
	if _, err := w.Write(buf.Bytes()); err != nil {
		s.t.Fatalf("write upload-pack response: %v", err)
	}
}

func (s *smartHTTPRepoServer) handleReceivePack(w http.ResponseWriter, r *http.Request) {
	s.mu.Lock()
	s.receivePacks++
	s.mu.Unlock()

body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer r.Body.Close()

session, err := s.newSession(transport.ReceivePackServiceName)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

req := packp.NewReferenceUpdateRequest()
	if err := req.Decode(bytes.NewReader(body)); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

report, err := session.(transport.ReceivePackSession).ReceivePack(r.Context(), req)
	if report != nil {
		var buf bytes.Buffer
		if encErr := report.Encode(&buf); encErr == nil {
			w.Header().Set("Content-Type", "application/x-git-receive-pack-result")
			_, _ = w.Write(buf.Bytes())
		}
	}
	if err != nil {
		return
	}
}

func (s *smartHTTPRepoServer) newSession(service string) (interface{}, error) {
	loader := transportserver.MapLoader{}

endpoint, err := transport.NewEndpoint(s.RepoURL())
	if err != nil {
		return nil, err
	}
	loader[endpoint.String()] = s.repo.Storer

srv := transportserver.NewServer(loader)
	switch service {
	case transport.UploadPackServiceName:
		return srv.NewUploadPackSession(endpoint, nil)
	case transport.ReceivePackServiceName:
		return srv.NewReceivePackSession(endpoint, nil)
	default:
		return nil, fmt.Errorf("unknown service %q", service)
	}
}

func TestMain(m *testing.M) {
	originalHTTP := transportclient.Protocols["http"]
	originalHTTPS := transportclient.Protocols["https"]

customHTTP := transporthttp.NewClient(&http.Client{})
	transportclient.InstallProtocol("http", customHTTP)
	transportclient.InstallProtocol("https", customHTTP)

code := m.Run()

transportclient.InstallProtocol("http", originalHTTP)
	transportclient.InstallProtocol("https", originalHTTPS)

os.Exit(code)
}
```
