This document describes the internal architecture of SGD for contributors and maintainers. It follows the execution pipeline from CLI invocation to output generation.
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
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.
Entry: ConfigValidator.validateConfig() (src/utils/configValidator.ts)
Validates and normalizes user inputs:
- Resolves symbolic git refs (
HEAD, branch names) to full SHA viagit rev-parse - Validates that
fromandtoSHAs exist in the repository - Defaults
apiVersionfromsfdx-project.jsonor 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.
Entry: getDefinition(config) (src/metadata/metadataManager.ts)
Builds a MetadataRepository — the central lookup table mapping file paths to Salesforce metadata type definitions.
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"]
- SDR registry — standard Salesforce metadata types from
@salesforce/source-deploy-retrieve, adapted viaSdrMetadataAdapter - Internal registry (
src/metadata/internalRegistry.ts) — SGD-specific overrides and additions (highest priority, overrides SDR byxmlName) - Additional registry (user-provided via
--additional-metadata-registry) — custom types, lowest priority
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 |
| 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 |
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 singlegit diff --name-statuscall. 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 withA,M,D, orR<score>. - Whitespace-ignore path: three (or four, when rename detection is on) parallel
git diff --numstatcalls in a singlePromise.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-Mis enabled) uses-zto sidestep numstat's brace/arrow rename-path encoding, emitting<added>\t<deleted>\t\0<src>\0<dst>\0triplets that are stride-3-parsed intoR\t<src>\t<dst>lines matching the default-path format. Only--numstatcomputes a real content diff under the whitespace flags —--name-statuswould still mark whitespace-only changes asMbecause it works off raw blob SHAs.
Then:
- Filters lines through the metadata registry — only paths that resolve to a known metadata type are kept
- Applies ignore patterns (
IgnoreHelper) — separate global and destructive-only ignore files - The legacy
_getRenamedElementsFQN 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.
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-fileis not provided; always hard-codesrecordTypes/(Salesforce API limitation)
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.
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}"]
The resolveHandler() method applies these tiers in order, returning the first match:
| Tier | Signal | Handler | Example |
|---|---|---|---|
| 1. Explicit override | xmlName in handlerMap |
Varies | Flow → FlowHandler |
| 2. Folder-based | inFolder: true |
InFolderHandler |
Document, EmailTemplate |
| 3. Adapter-based | adapter from SDR strategies |
InResourceHandler / InBundleHandler |
bundle → InResource |
| 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.
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
StandardHandler defines the fixed algorithm skeleton:
_isProcessable()— gate: does this file match the expected suffix?- Switch on change type →
collectAddition()/collectDeletion()/collectModification() - 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.
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
pruneOnlyis 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
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.xmlfiles — uses StandardHandler behavior directly
Detects the format by file extension and routes accordingly.
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.
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.
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.
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.
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).
Extends: InResourceHandler Used by: DigitalExperienceBundle
Like InResourceHandler but element names use two path segments (bundleType/bundleName) instead of one.
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.
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.
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.
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.
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.
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.
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.
Extends: StandardHandler Used by: Flow
Standard behavior plus a warning on deletion — deleting a Flow requires manual deactivation first (Salesforce API limitation).
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
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 grepwith pathspec globs (<source>/*<extension><metaFileSuffix>) to find.translation-meta.xmlfiles containingflowDefinitionselements matching deployed flows. This avoids requiring the tree index. Produces pruned translation files as computed content. - IncludeProcessor: handles
--includeand--include-destructiveflags. Lists all files in source directories, filters through include patterns, then processes matching lines throughDiffLineInterpreteras synthetic additions/deletions.
Processors (isCollector = false) run last via executeRemaining(), in registration order:
- PackageGenerator: writes
package.xml(fromChangeSet.forPackageManifest()),destructiveChanges.xml(fromChangeSet.forDestructiveManifest()— already coalesced to drop delete entries that are re-added or re-modified in the same diff), and the required companion emptypackage.xmlfor destructive deployments. - ChangesManifestProcessor: opt-in via
--changes-manifest. SerializesChangeSet.byChangeKind()into a JSON file alongside the xml manifests, grouped byChangeKind(add/modify/delete, plusrenameas{from, to}pairs when git-Mdetects component renames). Powered by thechangeKindfield carried on everyManifestElementfor add/modify/delete and byRenameResolverfeedingChangeSet.recordRenamefor rename pairs.
Each processor is wrapped in error isolation — failures produce warnings rather than crashing the pipeline.
Every ManifestElement produced by a handler is tagged with a ChangeKind. This tag is set at three sites:
StandardHandler._collectManifestElementderives the kind from the git change type (A→add,M→modify,D→delete) via theCHANGE_KIND_BY_GIT_TYPEmap. All handlers using the default manifest builder inherit this.InFileHandler._collectManifestFromComparisontakes the kind as a parameter so sub-elements get the correct label fromMetadataDiff.compare()— which returns three disjoint buckets:added(key absent infrom),modified(key present but content differs),deleted(key absent into). 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 byPackageGenerator,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; therenamebucket 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.
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.
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 ascauseConfigError— invalid user configurationMetadataRegistryError— invalid additional metadata registry
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.
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.
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.
| 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 |
| 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 |
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 |
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 |
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 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 |