Test CLI plan mode end to end · Entire
Test CLI plan mode end to end
efd5740→main·
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)
}