Skip to content

Latest commit

 

History

History
547 lines (372 loc) · 32.8 KB

File metadata and controls

547 lines (372 loc) · 32.8 KB

SFDX-Git-Delta — Design Document

This document describes the internal architecture of SGD for contributors and maintainers. It follows the execution pipeline from CLI invocation to output generation.

Pipeline Overview

SGD processes git diffs through a linear pipeline with six stages:

flowchart TD
    CLI["CLI Command<br/>(delta.ts)"] --> V["1. Config Validation<br/>(ConfigValidator)"]
    V --> M["2. Metadata Registry<br/>(MetadataRepository)"]
    M --> G["3. Git Diff Collection<br/>(RepoGitDiff)"]
    G --> I["4. Diff Interpretation<br/>(DiffLineInterpreter → Handlers)"]
    I --> P["5. Post-Processing<br/>(PostProcessorManager)"]
    P --> IO["6. I/O Execution<br/>(IOExecutor + PackageGenerator)"]

    style CLI fill:#e1f5fe
    style IO fill:#e8f5e9
Loading

The pipeline is orchestrated by src/main.ts, which receives a Config object and returns a Work result containing the accumulated manifests and warnings.

Key design principle: collection is separated from execution. Handlers produce HandlerResult objects (manifest entries + copy operations) that are aggregated first, then written to disk only at the end. This allows deduplication and conflict resolution before any I/O occurs.


Stage 1: Config Validation

Entry: ConfigValidator.validateConfig() (src/utils/configValidator.ts)

Validates and normalizes user inputs:

  • Resolves symbolic git refs (HEAD, branch names) to full SHA via git rev-parse
  • Validates that from and to SHAs exist in the repository
  • Defaults apiVersion from sfdx-project.json or the latest SDR version; caps at the max supported version
  • Sanitizes file paths (output dir, source dirs, ignore files)

Fatal errors (ConfigError) at this stage abort the pipeline entirely. This is one of only two places where exceptions propagate to the CLI layer.


Stage 2: Metadata Registry

Entry: getDefinition(config) (src/metadata/metadataManager.ts)

Builds a MetadataRepository — the central lookup table mapping file paths to Salesforce metadata type definitions.

Registry Priority Chain

flowchart LR
    SDR["@salesforce/source-deploy-retrieve<br/>registry"] --> Merge["MetadataDefinitionMerger"]
    IR["internalRegistry<br/>(SGD overrides)"] --> Merge
    AR["additionalMetadataRegistry<br/>(user-provided)"] --> Merge
    Merge --> Repo["MetadataRepositoryImpl"]
Loading
  1. SDR registry — standard Salesforce metadata types from @salesforce/source-deploy-retrieve, adapted via SdrMetadataAdapter
  2. Internal registry (src/metadata/internalRegistry.ts) — SGD-specific overrides and additions (highest priority, overrides SDR by xmlName)
  3. Additional registry (user-provided via --additional-metadata-registry) — custom types, lowest priority

Multi-Index Lookup

MetadataRepositoryImpl maintains three lookup indexes for fast path resolution:

Index Key Use case
extIndex File extension (.cls, .trigger) Primary lookup for most types
dirIndex Directory name (classes, triggers) Fallback when extension is ambiguous — picks deepest match, stops at inFolder types
xmlNameIndex XML name (ApexClass, ApexTrigger) Direct lookup by type name, exposed as getByXmlName(xmlName) on the interface for handler-resolution and parent-type lookup

Key Metadata Fields

Field Purpose
xmlName Salesforce API type name
suffix File extension without dot
directoryName Expected parent directory
metaFile Whether a companion -meta.xml file exists
inFolder Whether the type uses folder-based organization
content[] Sub-types sharing the same directory (Dashboard → DashboardFolder)
xmlTag + key In-file diff semantics for sub-element types
adapter SDR strategies.adapter value (e.g. bundle, mixedContent, digitalExperience) — drives dynamic handler resolution
decomposition SDR strategies.decomposition value (e.g. folderPerType) — used for child type handler heuristics
pruneOnly Type is only ever added to destructiveChanges, never package
excluded Sub-element type is not independently packageable

Stage 3: Git Diff Collection

Entry: RepoGitDiff.getLines() (src/utils/repoGitDiff.ts)

Collects diff lines between the from and to commits. GitAdapter.getDiffLines has two code paths because git's --name-status output does not honour whitespace-ignore flags. Rename detection (-M) is gated on config.changesManifest — default sgd runs pass --no-renames so line shape matches the pre-feature output; setting --changes-manifest opts into -M. When enabled, R<score>\tfrom\tto lines (or their numstat equivalent) are emitted and RepoGitDiff._expandRenames splits them into synthetic A/D lines while recording each {fromPath, toPath} pair for RenameResolver to resolve later.

  • Default path (no --ignore-whitespace): a single git diff --name-status call. With rename detection off: --no-renames --diff-filter=AMD. With detection on: -M --diff-filter=AMDR. Output is already <STATUS>\t<path>, so each line starts with A, M, D, or R<score>.
  • Whitespace-ignore path: three (or four, when rename detection is on) parallel git diff --numstat calls in a single Promise.all, one per --diff-filter. A/M/D calls emit the standard <added>\t<deleted>\t<path> shape; their leading counts are rewritten to the status prefix. The R call (only when -M is enabled) uses -z to sidestep numstat's brace/arrow rename-path encoding, emitting <added>\t<deleted>\t\0<src>\0<dst>\0 triplets that are stride-3-parsed into R\t<src>\t<dst> lines matching the default-path format. Only --numstat computes a real content diff under the whitespace flags — --name-status would still mark whitespace-only changes as M because it works off raw blob SHAs.

Then:

  1. Filters lines through the metadata registry — only paths that resolve to a known metadata type are kept
  2. Applies ignore patterns (IgnoreHelper) — separate global and destructive-only ignore files
  3. The legacy _getRenamedElements FQN match is preserved as a safety net for file-move-same-component cases (different file paths resolving to the same Salesforce component); the deletion side is dropped so the component appears only as an addition. True component renames (different FQNs) surface via git -M.

Ignore System

IgnoreHelper wraps the ignore library (gitignore spec) with a dual-instance pattern:

  • globalIgnore: applied to all diff lines
  • destructiveIgnore: applied only to deletions; falls back to globalIgnore if --ignore-destructive-file is not provided; always hard-codes recordTypes/ (Salesforce API limitation)

Stage 4: Diff Interpretation & Handler Hierarchy

Entry: DiffLineInterpreter.process(lines) (src/service/diffLineInterpreter.ts)

Each diff line is dispatched to a type-specific handler via an async queue capped at getConcurrencyThreshold(). The TypeHandlerFactory selects the handler class using a multi-tier resolution chain that combines explicit overrides with dynamic resolution from SDR registry attributes.

Dispatch Flow

flowchart TD
    Line["Diff line<br/>'A force-app/main/.../MyClass.cls'"] --> TF["TypeHandlerFactory"]
    TF --> ME["MetadataBoundaryResolver<br/>creates MetadataElement"]
    TF --> T1{"xmlName in<br/>handlerMap?"}
    T1 -->|Yes| SH["Explicit Handler Override"]
    T1 -->|No| T2{"inFolder?"}
    T2 -->|Yes| IF["InFolderHandler"]
    T2 -->|No| T3{"adapter in<br/>adapterHandlerMap?"}
    T3 -->|Yes| AH["Adapter-Based Handler"]
    T3 -->|No| T4{"has parentXmlName?"}
    T4 -->|Yes| CH["Child Type Heuristics"]
    T4 -->|No| T5{"parent of<br/>InFile children?"}
    T5 -->|Yes| IFH["InFileHandler"]
    T5 -->|No| DH["StandardHandler"]
    CH --> C["handler.collect()"]
    SH --> C
    IF --> C
    AH --> C
    IFH --> C
    DH --> C
    C --> HR["HandlerResult<br/>{manifests, copies, warnings}"]
Loading

Handler Resolution Tiers

The resolveHandler() method applies these tiers in order, returning the first match:

Tier Signal Handler Example
1. Explicit override xmlName in handlerMap Varies FlowFlowHandler
2. Folder-based inFolder: true InFolderHandler Document, EmailTemplate
3. Adapter-based adapter from SDR strategies InResourceHandler / InBundleHandler bundleInResource
4. Child heuristics xmlTag + key + non-adapter parent DecomposedHandler WorkflowAlert
4b. Child heuristics no xmlTag + folderPerType parent CustomObjectChildHandler ListView
5. InFile parent has children with xmlTag+key InFileHandler Workflow
6. Fallback none of the above StandardHandler ApexClass

This design means most new SDR metadata types are handled automatically without code changes. Only types requiring specialized behavior need explicit overrides in handlerMap.

MetadataBoundaryResolver creates a MetadataElement — a value object capturing the parsed identity of the diff line: base path, extension, parent folder, component name, and path segments after the type directory. Resolution follows a tiered strategy to minimize git I/O: (1) flat paths (single segment after the type directory) use MetadataElement.fromPath() directly; (2) types with no suffix (LWC, Aura) use fromPath() since the scan cannot identify them; (3) depth-2 paths where the file contains the metadata suffix extract the component name directly from the file name without git I/O; (4) deeper paths delegate to scanAndCreateElement() which calls getFilesPath(typeDir) — a single recursive git ls-tree -r per type directory, cached hierarchically — then builds a Set of component names from meta files and matches path segments inner-to-outer. This handles intermediate folders between the type directory and the component (e.g., permissionsets/marketing/Admin/... where marketing is an organizational folder, not the component). For paths where the type directory is not in the path, a fallback walk-up strategy lists directory siblings via listDirAtRevision and matches against known metadata suffixes, with results cached per revision via dirCache.

Handler Hierarchy

classDiagram
    class StandardHandler {
        +collect() HandlerResult
        +collectAddition() HandlerResult
        +collectDeletion() HandlerResult
        +collectModification() HandlerResult
        #_isProcessable() bool
        #_getElementName() string
        #_delegateFileCopy() bool
    }

    StandardHandler <|-- InFileHandler
    StandardHandler <|-- InFolderHandler
    StandardHandler <|-- InResourceHandler
    StandardHandler <|-- SharedFolderHandler
    StandardHandler <|-- DecomposedHandler
    StandardHandler <|-- ContainedDecomposedHandler
    StandardHandler <|-- CustomObjectHandler
    StandardHandler <|-- CustomObjectChildHandler
    StandardHandler <|-- FlowHandler

    InFileHandler <|-- CustomLabelHandler
    InFolderHandler <|-- ReportingFolderHandler
    InResourceHandler <|-- BundleHandler
    InResourceHandler <|-- LwcHandler
    InResourceHandler <|-- ObjectTranslationHandler
    SharedFolderHandler <|-- BotHandler
    DecomposedHandler <|-- CustomFieldHandler
Loading

StandardHandler — Template Method

StandardHandler defines the fixed algorithm skeleton:

  1. _isProcessable() — gate: does this file match the expected suffix?
  2. Switch on change type → collectAddition() / collectDeletion() / collectModification()
  3. Errors are caught and converted to warnings — a single broken file does not abort processing

Subclasses override specific hooks to customize behavior. Even thin subclasses that override a single method justify their existence because they are selected at runtime based on metadata type definitions.

Handler Reference

InFileHandler

Extends: StandardHandler Used by: AssignmentRules, AutoResponseRules, EscalationRules, GlobalValueSetTranslation, MarketingAppExtension, MatchingRules, Profile, SharingRules, StandardValueSetTranslation, Translations, Workflow

Handles metadata types where multiple deployable sub-elements are stored in a single XML file. Uses MetadataDiff to compare both revisions of the file and produce per-sub-element manifest entries. Copies a pruned XML (computed content) containing only changed sub-elements instead of the full file.

Key behavior:

  • Additions/modifications: XML diff produces fine-grained manifest entries per changed sub-element
  • Deletions: if pruneOnly is set, treats as standard deletion; otherwise re-diffs to extract sub-element removals
  • File copy: disabled for standard git-copy; uses computed content (pruned XML) instead

CustomLabelHandler

Extends: InFileHandler Used by: CustomLabel

Handles CustomLabel which can exist in two formats:

  • Monolithic: single CustomLabels.labels-meta.xml — uses InFile XML diff behavior
  • Decomposed: individual .label-meta.xml files — uses StandardHandler behavior directly

Detects the format by file extension and routes accordingly.

InFolderHandler

Extends: StandardHandler Used by: Document, EmailTemplate (and any type with inFolder: true not explicitly overridden)

Handles metadata stored in named folders. When a file changes, the handler also copies the folder's -meta.xml descriptor and any companion files sharing the same base name (e.g. thumbnails). Element names use the Folder/MemberName format.

ReportingFolderHandler

Extends: InFolderHandler Used by: Dashboard, Report

Handles reporting types in shared directories where the actual Salesforce type is determined by file extension. An unrecognized extension silently produces an empty result. The manifest type name comes from the resolved extension, not the directory.

SharedFolderHandler

Extends: StandardHandler Used by: VirtualDiscovery, VirtualModeration, VirtualWave

Handles metadata in a shared directory where the type is resolved per file extension. Similar to ReportingFolderHandler but without the folder-meta copy logic.

BotHandler

Extends: SharedFolderHandler Used by: VirtualBot

Extends shared folder behavior: changing any sub-file also forces inclusion of the parent Bot manifest entry and its .bot-meta.xml.

InResourceHandler

Extends: StandardHandler Used by: ExperienceBundle, GenAiPlannerBundle, LightningTypeBundle, StaticResource, WaveTemplateBundle (and any type with adapter: "bundle" or adapter: "mixedContent" not explicitly overridden)

Handles bundle-like resources where changing any file within the bundle triggers the entire bundle to be redeployed. On deletion, checks if the bundle root still has content — if yes, treats as modification instead of deletion (the bundle still exists with remaining files).

BundleHandler

Extends: InResourceHandler Used by: DigitalExperienceBundle

Like InResourceHandler but element names use two path segments (bundleType/bundleName) instead of one.

LwcHandler

Extends: InResourceHandler Used by: AuraDefinitionBundle, GenAiFunction, LightningComponentBundle

Like InResourceHandler but skips files directly in the type directory (e.g. top-level __tests__); only processes files inside a named component sub-folder.

ObjectTranslationHandler

Extends: InResourceHandler Used by: CustomFieldTranslation, CustomObjectTranslation

Field translation files are not independently deployable. The handler produces a pruned version of the parent objectTranslation file containing only changed field translations, emitted as computed content.

DecomposedHandler

Extends: StandardHandler Used by: SharingCriteriaRule, SharingGuestRule, SharingOwnerRule, Territory2, Territory2Rule, WorkflowAlert, WorkflowFieldUpdate, WorkflowFlowAction, WorkflowKnowledgePublish, WorkflowOutboundMessage, WorkflowRule, WorkflowSend, WorkflowTask (and any child type with xmlTag + key whose parent has no adapter)

Handles metadata stored as individual files in sub-folders of a parent type. Element names are qualified as ParentName.ChildName. On addition, also copies the parent type's -meta.xml.

CustomFieldHandler

Extends: DecomposedHandler Used by: CustomField

Like DecomposedHandler but the parent copy is conditional: only copies the parent CustomObject when the field contains <type>MasterDetail</type>, because Master Detail fields require the parent object in the same deployment.

ContainedDecomposedHandler

Extends: StandardHandler Used by: PermissionSet

Handles types that can exist in either monolithic format (single file) or decomposed format (folder with sub-files). Detects the format at construction time. Locates the PermissionSet directory using a fixed offset from the file path's end, supporting arbitrary nesting depth (e.g., permissionsets/marketing/Admin/fieldPermissions/...). On deletion in decomposed format, checks if the holder folder still has content — if yes, treats as modification (redeploy the PS); if no, treats as true deletion.

CustomObjectHandler

Extends: StandardHandler Used by: CustomObject, Territory2Model

On addition, scans the object's fields/ subfolder for Master Detail fields and includes them in the copy set — Master Detail fields cannot be deployed in a subsequent step.

CustomObjectChildHandler

Extends: StandardHandler Used by: BusinessProcess, CompactLayout, FieldSet, Index, ListView, RecordType, SharingReason, ValidationRule, WebLink (and any child type without xmlTag whose parent has decomposition: "folderPerType")

Handles child types living in CustomObject sub-folders. Element names are qualified as ObjectName.ChildName.

FlowHandler

Extends: StandardHandler Used by: Flow

Standard behavior plus a warning on deletion — deleting a Flow requires manual deactivation first (Salesforce API limitation).


Stage 5: Post-Processing Chain

Entry: PostProcessorManager (src/post-processor/postProcessorManager.ts)

After handlers produce their results, post-processors run in two phases:

flowchart TD
    HR["Handler Results"] --> A["ChangeSet.from()"]
    A --> C["Collectors phase<br/>(transformAndCollect)"]
    C --> M["Merge + re-aggregate"]
    M --> IO["I/O Execution"]
    IO --> P["Processors phase<br/>(process)"]

    subgraph Collectors
        FT["FlowTranslationProcessor"]
        IP["IncludeProcessor"]
    end

    subgraph Processors
        PG["PackageGenerator"]
        CM["ChangesManifestProcessor"]
    end

    C --> FT
    C --> IP
    P --> PG
    P --> CM
Loading

Two-Phase Execution

Collectors (isCollector = true) run first via collectAll(). They produce additional HandlerResult data that gets merged into the main result:

  • FlowTranslationProcessor: when Flows are being deployed, uses git grep with pathspec globs (<source>/*<extension><metaFileSuffix>) to find .translation-meta.xml files containing flowDefinitions elements matching deployed flows. This avoids requiring the tree index. Produces pruned translation files as computed content.
  • IncludeProcessor: handles --include and --include-destructive flags. Lists all files in source directories, filters through include patterns, then processes matching lines through DiffLineInterpreter as synthetic additions/deletions.

Processors (isCollector = false) run last via executeRemaining(), in registration order:

  • PackageGenerator: writes package.xml (from ChangeSet.forPackageManifest()), destructiveChanges.xml (from ChangeSet.forDestructiveManifest() — already coalesced to drop delete entries that are re-added or re-modified in the same diff), and the required companion empty package.xml for destructive deployments.
  • ChangesManifestProcessor: opt-in via --changes-manifest. Serializes ChangeSet.byChangeKind() into a JSON file alongside the xml manifests, grouped by ChangeKind (add / modify / delete, plus rename as {from, to} pairs when git -M detects component renames). Powered by the changeKind field carried on every ManifestElement for add/modify/delete and by RenameResolver feeding ChangeSet.recordRename for rename pairs.

Each processor is wrapped in error isolation — failures produce warnings rather than crashing the pipeline.

Change-kind pipeline

Every ManifestElement produced by a handler is tagged with a ChangeKind. This tag is set at three sites:

  • StandardHandler._collectManifestElement derives the kind from the git change type (Aadd, Mmodify, Ddelete) via the CHANGE_KIND_BY_GIT_TYPE map. All handlers using the default manifest builder inherit this.
  • InFileHandler._collectManifestFromComparison takes the kind as a parameter so sub-elements get the correct label from MetadataDiff.compare() — which returns three disjoint buckets: added (key absent in from), modified (key present but content differs), deleted (key absent in to). The keyless-element case is bucketed as modified.
  • Direct constructors (BotHandler, FlowTranslationProcessor) stamp the kind explicitly at their push site.

ChangeSet.from(manifests) is the single ingestion point. Each ManifestElement carries two orthogonal axes — target (deployment contract: Package vs DestructiveChanges) and changeKind (review semantics: Add / Modify / Delete) — and the ChangeSet stores both:

  • byTarget: Record<ManifestTarget, Manifest> drives the xml manifests.
  • byKind: Record<AddKind, Manifest> drives the review-oriented JSON.

The two axes are not redundant: a single element can be (target=Package, changeKind=Delete), which is what InFileHandler stamps when a container file (e.g. CustomLabels) is deleted but child elements survive — the deployment must still list the container under package.xml while the JSON manifest surfaces a delete for reviewer visibility. Views route on the correct axis:

  • forPackageManifest()byTarget[Package] ∪ rename-target per type (used by PackageGenerator, FlowTranslationProcessor).
  • forDestructiveManifest()byTarget[DestructiveChanges] ∪ rename-source, minus anything that winds up in the package view (drops cancelled deletions and covers rename semantics in a single coalesce).
  • byChangeKind() — per-kind record. Rename participants are removed from the add/delete buckets so every emitted entry lives in exactly one user-visible bucket; the rename bucket contains {from, to} pairs deduplicated per type.

Rename detection uses git's -M flag, gated on config.changesManifest. Default sgd runs pass --no-renames + --diff-filter=AMD so line shape matches the pre-feature output; only --changes-manifest <file> opts into -M + --diff-filter=AMDR (fast path) or the fourth --numstat -M -z --diff-filter=R call (ignore-whitespace path). When enabled, RepoGitDiff splits each R<score>\tfrom\tto line into a synthetic D\tfrom + A\tto pair so the existing handler pipeline processes them normally, while capturing the pair. RenameResolver resolves each pair's paths back through TypeHandlerFactory to recover (type, from-member, to-member) — bundle renames re-emitted per file collapse to a single entry via the Map-keyed recordRename dedupe.

Package.xml remains byte-identical to the pre-feature output because InFileHandler still routes both added and modified sub-elements to ManifestTarget.Package; the ChangeSet merely tags them differently.


Stage 6: I/O Execution

Entry: IOExecutor.execute(copies) (src/adapter/ioExecutor.ts)

Executes the accumulated copy operations with concurrency bounded by getConcurrencyThreshold(). Three operation kinds:

Kind Description
GitCopy Reads a single file from a specific git revision via the batch git cat-file process and writes it to the output directory. Does not require the tree index.
GitDirCopy Enumerates all files under a directory path via getFilesPath (requires tree index), then copies each file via git cat-file. Used by handlers that need to copy entire directories (e.g., ContainedDecomposedHandler for decomposed PermissionSet holder folders). Executed serially because the outer execute() already parallelizes across operations.
ComputedContent Writes a string (typically pruned XML from InFile/ObjectTranslation handlers) directly to the output directory

GitAdapter.getBufferContent() handles LFS detection: if the buffer starts with an LFS pointer signature, it reads the actual object from the local LFS cache instead.


Cross-Cutting Concerns

Error Handling

SGD follows a warnings-not-exceptions philosophy for per-file errors:

Layer Strategy
Config validation Fatal: throws ConfigError / MetadataRegistryError → propagates to CLI
Handlers (collect()) Catches all errors → converts to warnings in HandlerResult
Post-processors Each wrapped in _safeProcess → failures become warnings
Git operations Debug-logged, return empty/false → silent degradation
XML parsing Produces "MalformedXML" warning with file path and revision

This ensures a single broken file never aborts processing of the entire diff.

Error types form a hierarchy:

  • SgdError (base) — wraps original error as cause
  • ConfigError — invalid user configuration
  • MetadataRegistryError — invalid additional metadata registry

Concurrency

Every parallel operation uses getConcurrencyThreshold() from src/utils/concurrencyUtils.ts, which returns min(availableParallelism(), 6). The cap at 6 is a deliberate CI/CD safety constraint — the plugin runs on small CI machines.

The async library's queue, eachLimit, mapLimit, and filterLimit are used throughout. Unbounded Promise.all is never used for file or git operations.

Logging

Two complementary mechanisms:

lazy template tag (src/utils/LoggingService.ts): defers string interpolation until the log level is active. Expressions are evaluated eagerly by JavaScript, so expensive computations must be wrapped as arrow functions:

Logger.debug(lazy`result: ${() => JSON.stringify(largeObject)}`)

@log decorator (src/utils/LoggingDecorator.ts): emits Logger.trace entry/exit on decorated methods. Works for both sync and async functions. Applied pervasively across handlers, adapters, and processors.

Git Adapter

GitAdapter (src/adapter/GitAdapter.ts) wraps simple-git with a singleton pattern keyed by the composite string <repo>\0<to> — so spread copies of the same config (for example ioExecutor's per-revision {...config, to: rev}) share one adapter instance and one git cat-file subprocess rather than spawning a new one per call. It minimizes git subprocess spawns via two strategies:

Tree index: The tree index is a path-segment trie (TreeIndex in src/adapter/treeIndex.ts) per revision, populated exclusively by preBuildTreeIndex(revision, scopePaths) in src/main.ts. It serves pathExists, getFilesPath, and listDirAtRevision lookups in O(path-depth) without additional subprocess calls — if no index was pre-built for a revision, these methods return empty results. The trie replaces an earlier flat Set<string> that required O(n) prefix scans; prefix and directory-listing operations now traverse only the relevant sub-tree. The index is scoped to reduce heap pressure: preBuildTreeIndex runs git ls-tree --name-only -r <revision> -- <path1> <path2> ... to index only the metadata directories that handlers actually need. Both revisions (config.to and config.from) are indexed in parallel via Promise.all. Scope computation (computeTreeIndexScope in src/utils/treeIndexScope.ts) analyzes diff lines against the metadata registry to determine which type directories require tree-index lookups — only types using InResource, InFolder, ReportingFolder, InBundle, Lwc, CustomObject, ContainedDecomposed, or MetadataBoundaryResolver deep-path resolution need the index. When --include or --include-destructive is set, the scope defaults to config.source since the include processor may touch any type. When generateDelta is false, the tree index is never built.

Batch cat-file: GitBatchCatFile (src/adapter/gitBatchCatFile.ts) spawns a single long-lived git cat-file --batch child process per adapter instance. File content reads write <revision>:<path>\n to stdin and parse the binary response from stdout using a FIFO queue. This replaces individual git show subprocess spawns.

Lifecycle: GitAdapter.closeAll() terminates all batch processes and clears the singleton instances map. It is called in a finally block in src/main.ts to prevent orphaned child processes on both success and error paths. If the git cat-file process exits unexpectedly, a close event handler rejects all pending promises to prevent hangs.

Memory note: The batch cat-file process buffers each blob's content entirely in memory before resolving. There is no upper bound on individual blob size — repositories with very large binary blobs (multi-GB) could cause high memory consumption. This is acceptable because SGD operates on trusted repositories and the previous git show implementation had the same characteristic.


Extensibility Points

For Users

Mechanism Purpose
--additional-metadata-registry JSON file defining custom metadata types (Zod-validated)
--ignore-file / --ignore-destructive-file Gitignore-format exclusion patterns
--include-file / --include-destructive-file Force-include paths regardless of diff
--source-dir (multiple) Scope diff to specific directories

For Developers

Extension point How to extend
New metadata type handler Most types are auto-resolved via SDR registry attributes (adapter, decomposition, inFolder, xmlTag+key). Only add an explicit entry to handlerMap in TypeHandlerFactory when a type needs behavior that differs from what SDR signals would select.
New post-processor Add a BaseProcessor subclass to registeredProcessors in postProcessorManager.ts
Metadata type override Add definition to internalRegistry.ts with special flags (pruneOnly, excluded, xmlTag, etc.)
Programmatic API import sgd from 'sfdx-git-delta' — call await sgd(config) directly, receiving the Work object

Key Types Reference

Config (src/types/config.ts)

All user inputs flowing through the pipeline:

Field Type Description
from / to string Git commit SHAs (the diff range)
output string Directory for generated manifests
source string[] Source paths to scan
repo string Git repository root
apiVersion number Salesforce API version
generateDelta boolean Whether to copy files (not just manifests)
ignore / ignoreDestructive string Gitignore-style filter file paths
include / includeDestructive string Force-include file paths
ignoreWhitespace boolean Skip whitespace-only changes

Work (src/types/work.ts)

Mutable context accumulating outputs:

Field Type Description
config Config The configuration
diffs { package: Map, destructiveChanges: Map } Accumulated manifest maps (type → Set<member>)
warnings Error[] Non-fatal warnings

HandlerResult (src/types/handlerResult.ts)

Universal handler/processor output:

Field Type Description
manifests ManifestElement[] Entries for package.xml or destructiveChanges.xml
copies CopyOperation[] GitCopy, GitDirCopy, or ComputedContent operations
warnings Error[] Non-fatal warnings

Metadata (src/schemas/metadata.ts)

Metadata type definition (Zod-validated):

Field Type Description
xmlName string Salesforce API type name
suffix string File extension without dot
directoryName string Expected parent directory
metaFile boolean Companion -meta.xml exists
inFolder boolean Folder-based organization
content Metadata[] Sub-types sharing the directory
xmlTag + key string In-file diff semantics
adapter string SDR strategies.adapter — drives handler auto-resolution
decomposition string SDR strategies.decomposition — child type heuristics
pruneOnly boolean Only in destructiveChanges
excluded boolean Not independently packageable