merge

package
v2.937.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2026 License: AGPL-3.0 Imports: 11 Imported by: 0

Documentation

Overview

Package merge implements the 3-way merge subsystem for SDK generation.

This package provides the core merge algorithm and data structures for preserving user modifications across regenerations. The implementation is split between this package (pure merge logic) and the patches package (integration with Git and filesystem).

Architecture Overview

The merge system uses Git's object database as an efficient content-addressable store for pristine (generated) versions of files. This enables:

  • 3-way merges between base (previous pristine), current (user's version), and new (regenerated)
  • Detection of user modifications via checksum comparison
  • Efficient storage via Git's delta compression
  • Conflict detection and resolution using standard Git conflict markers

Key Components

  • VirtualFile: Represents a generated file held in memory before writing
  • MergeContext: Contains all inputs for the merge operation
  • MergeResult: Contains outputs including conflicts and no-op detection
  • Git: Interface for Git operations (implemented by patches.GitAdapter)
  • ConflictsError: Returned when merge conflicts require manual resolution

Merge Algorithm

The Merge function performs a 4-step algorithm:

  1. Immediate Blobbing: Write all generated content to Git object database. Each file is hashed (SHA1) and stored as a blob, providing deduplication.

  2. Determinism Check: Build a tree hash from all tracked files. If tree hash matches previous generation, this is a no-op run (return early).

  3. 3-Way Merge Loop: For each file, determine the appropriate action: - New file: Write directly - Clean file (disk matches last write): Overwrite with new version - Dirty file (user modified): Perform 3-way merge - Deleted file (user deleted): Respect deletion, update tracking only - Moved file (user moved): Write to new location

  4. Commit & Push: Create a Git commit linking the tree, push to remote. This is handled by the caller (patches.Subsystem) which owns UUID generation.

3-Way Text Merge

The [merge3Way] function uses line-based diffing to merge changes:

  • Convert files to line-based rune strings for efficient comparison
  • Compute diffs from base to current (user changes) and base to new (generator changes)
  • Walk both diff lists, merging non-overlapping changes automatically
  • Generate conflict markers for overlapping changes

Conflict format:

<<<<<<< Current (Your changes)
[user's version]
=======
[generated version]
>>>>>>> New (Generated by Speakeasy)

File Move Detection

Users can move generated files to new locations. The system detects this via:

  • Embedded @generated-id comments in file headers (e.g., "// @generated-id: a1b2c3d4e5f6")
  • Scanning the output directory for files with these IDs
  • Matching scanned IDs against computed IDs (based on original paths)

When a move is detected, the generator writes to the new location, respecting the user's file organization.

No-Op Detection

The system detects "no-op" runs where regeneration produces identical output. This is determined by comparing the tree hash of generated content against the previous pristine tree hash stored in the lockfile. When no-op is detected:

  • No files are written
  • No commit is created
  • The lockfile is unchanged

This prevents unnecessary churn in version control and CI pipelines.

Integration Flow

  1. Generator renders templates, collecting VirtualFile entries in memory
  2. Generator calls patches.Subsystem.PerformMerge() with virtual files
  3. PerformMerge builds MergeContext and calls Merge
  4. Merge performs the algorithm, writing merged files via FileSystem
  5. PerformMerge creates commit, pushes to remote, updates lockfile
  6. If conflicts exist, ConflictsError is returned after files are written

Lockfile State

The gen.lock file tracks state for the merge system:

persistentEdits:
  generation_id: "uuid"           # Unique ID for this generation run
  pristine_commit_hash: "sha1"    # Git commit hash of pristine tree
  pristine_tree_hash: "sha1"      # Git tree hash for no-op detection
trackedFiles:
  path/to/file.go:
    id: "a1b2c3d4e5f6"           # Short ID embedded in file header
    last_write_checksum: "sha1:..." # Hash of file as last written
    pristine_git_object: "sha1"  # Git blob hash of pristine version
    moved_to: "new/path.go"      # Present if user moved the file
    deleted: true                # Present if user deleted the file

Error Handling

The system is designed to be resilient:

  • Network failures during push are soft failures (code still works locally)
  • Missing Git objects trigger fetch attempts before falling back
  • Scan failures for file IDs are logged but don't block generation
  • Conflicts are written as markers, allowing manual resolution

References

For implementation details, see:

  • Merge: Main merge entry point
  • [merge3Way]: Text merge algorithm
  • Git: Git operations interface
  • patches.Subsystem: Integration with Generator (in openapi-generation/internal/patches)
  • patches.GitAdapter: Git interface implementation (in speakeasy/internal/patches)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Merge3Way

func Merge3Way(base, current, newContent []byte) ([]byte, bool)

Merge3Way performs the same 3-way text merge used by the persistent-edits subsystem and returns conflict-marked content when overlaps are detected.

Types

type ConflictsError

type ConflictsError struct {
	Files []string
}

ConflictsError is returned when merge conflicts are detected. The Files slice contains relative paths of all conflicting files. The CLI should render this as a git-status style message and exit non-zero.

func (*ConflictsError) Error

func (e *ConflictsError) Error() string

type Git

type Git interface {

	// HasObject checks if a blob or commit exists in the local object DB.
	HasObject(hash string) bool

	// ReadBlob returns the content of a specific blob hash.
	ReadBlob(hash string) ([]byte, error)

	// FetchSnapshot ensures the history for a specific UUID exists locally.
	// This triggers the network call to origin.
	FetchSnapshot(uuid string) error

	// RepoRoot returns the root directory of the git repository.
	// This is the directory containing .git (the worktree root).
	// Returns empty string if not in a git repository.
	RepoRoot() string

	// WriteObject hashes content into the DB (blob) and returns the SHA1.
	WriteObject(content []byte) (string, error)

	// CreateSnapshotTree builds a Git Tree object from a map of "path" -> "blobHash".
	// Returns the tree hash.
	CreateSnapshotTree(fileHashes map[string]string) (string, error)

	// CommitSnapshot creates a commit object.
	// parentHash: The Previous Commit Hash (links the history).
	// Returns the commit hash.
	CommitSnapshot(treeHash, parentHash, message string) (string, error)

	// PushSnapshot syncs the ref to the server synchronously.
	// The push must complete before returning to ensure the commit is available
	// for future generations that may reference it as a parent.
	PushSnapshot(commitHash, uuid string) error

	// SetConflictState sets up git's index to show a file as conflicted.
	// This writes the base, ours, and theirs versions as blobs and creates
	// stage 1, 2, 3 index entries, enabling standard git conflict resolution:
	//   - git status shows "both modified"
	//   - git mergetool can resolve conflicts
	//   - git checkout --ours/--theirs works
	//   - git add marks as resolved
	//
	// Parameters:
	//   - path: relative file path
	//   - base: content from common ancestor (stage 1)
	//   - ours: content from current/HEAD version (stage 2)
	//   - theirs: content from incoming/generated version (stage 3)
	//   - isExecutable: whether the file should be marked executable
	//
	// If base is nil, it indicates a new file conflict (no common ancestor).
	SetConflictState(path string, base, ours, theirs []byte, isExecutable bool) error
}

Git abstracts the object database and network operations. The CLI implements this interface and injects it into the Generator.

type MergeContext

type MergeContext struct {
	// Generated files from template rendering
	VirtualFiles map[string]*VirtualFile

	// TrackedFiles contains the files that should be included in the tree hash
	// for no-op detection. This comes from the FileTracker and excludes untracked
	// files like CONTRIBUTING.md and README.md, ensuring stability across runs.
	TrackedFiles config.TrackedFiles

	// Current lockfile state
	LockFile *config.LockFile

	// Interface implementations
	FileSystem filesystem.FileSystem
	Git        Git

	// Enabled
	Enabled bool

	// OutDir is the output directory for this target.
	// All file operations are relative to this directory.
	OutDir string

	// PathRemapping maps computed paths to actual paths on disk.
	// When a user moves a file from path A to path B, and we generate for path A,
	// PathRemapping[A] = B tells us to look for the file at B instead.
	PathRemapping map[string]string
}

MergeContext contains all the context needed to perform a 3-way merge

type MergeResult

type MergeResult struct {
	// The tree hash of the generated content (for no-op detection)
	TreeHash string

	// The commit hash (if a commit was created)
	CommitHash string

	// The new generation ID
	GenerationID string

	// Whether this was a no-op (no changes needed)
	IsNoOp bool

	// List of files that had conflicts
	ConflictFiles []string
}

MergeResult contains the result of the merge operation

func Merge

func Merge(ctx *MergeContext) (*MergeResult, error)

Merge performs the complete 3-way merge algorithm: Step 1: Immediate Blobbing - Write all generated content to Git object database Step 2: Determinism Check - Compare tree hash to detect no-op runs Step 3: 3-Way Merge Loop - For each file, perform dirty check and merge if needed Step 4: Commit & Push - Create commit, update lockfile, push snapshot

type SnapshotRefError

type SnapshotRefError struct {
	GenerationID string
	BlobHash     string
	Cause        error
}

SnapshotRefError is returned when persistentEdits is enabled and the merge-base blob cannot be recovered. The three-way merge cannot proceed without the base, so generation is aborted rather than writing conflict markers or silently losing customer edits.

func (*SnapshotRefError) Error

func (e *SnapshotRefError) Error() string

func (*SnapshotRefError) Unwrap

func (e *SnapshotRefError) Unwrap() error

type VirtualFile

type VirtualFile struct {
	Path     string      // Relative path (e.g., "pkg/models/user.go")
	Content  []byte      // File content
	Mode     fs.FileMode // File mode (e.g., 0644 or 0755)
	IsBinary bool        // True if binary file (.png, .jar, etc.)
}

VirtualFile represents a file to be generated, held in memory before writing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL