HTTP API Reference
The plugin exposes Git control operations via HTTP Admin API endpoints. All routes live on the Node-RED admin server (usually http://localhost:1880) under the base path /rosepetal-git.
Authentication & Permissions
- Routes honor Node-RED admin authentication when it is enabled.
- Read-only operations require the
git-control.readpermission; mutating operations requiregit-control.write. Note this is by operation, not HTTP verb — several read-only endpoints arePOST(they take a JSON body) but still only needread(e.g.status,branches,flow-diff,file-diff).
Calling the API as an agent / external tool
When admin auth is disabled, call the endpoints directly — no token needed.
When admin auth is enabled, obtain a bearer token once and send it on every request:
# 1. Get a token
curl -s http://localhost:1880/auth/token \
-d 'client_id=node-red-admin' \
-d 'grant_type=password' \
-d 'scope=*' \
-d 'username=admin' \
-d 'password=secret'
# -> { "access_token": "....", "token_type": "Bearer", ... }
# 2. Use it
curl -s http://localhost:1880/rosepetal-git/project-info \
-H 'Authorization: Bearer <access_token>'Bearer-token requests are not subject to CSRF (CSRF only guards cookie-based editor sessions). All POST bodies are JSON (Content-Type: application/json).
Danger levels
| Level | Endpoints | Notes |
|---|---|---|
| safe (read-only) | project-info, status, log, branches, validate-checkout, flow-units, flow-diff, file-diff, commit-diff, commit-branches, ssh-keys | never modify the repo |
| writes working tree / history | add, unstage, commit, checkout, pull, merge, revert, cherry-pick, abort, create-branch, orphan-branch, rename-branch, set-upstream, revert-flow-unit, ssh-key | reversible via git |
destructive (needs confirmed: true) | force-push, discard-all, delete-branch (when force: true), reset (hard, when safeMode: false) | can discard work / rewrite history |
All responses are JSON. Errors return:
{
"success": false,
"error": "Human-readable error message",
"details": "Technical error details (optional)",
"suggestion": "Actionable suggestion for resolution (optional)"
}Common Patterns
Request Structure
All POST endpoints accept JSON bodies with at minimum:
{
"repoPath": "/path/to/repo" // Optional - defaults to active Node-RED project
}Response Structure
Success responses include:
{
"success": true,
"operation": "operation-name",
...operation-specific data
}Endpoints
Project Information
GET /rosepetal-git/project-info
Returns metadata about the active Node-RED project and its Git state.
Response 200
{
"success": true,
"projectName": "my-project",
"projectPath": "/home/user/.node-red/projects/my-project",
"currentBranch": "main",
"lastKnownBranch": "main",
"remotes": [
{
"name": "origin",
"refs": {
"fetch": "git@github.com:user/repo.git",
"push": "git@github.com:user/repo.git"
}
}
],
"user": {
"name": "John Doe",
"email": "john@example.com"
},
"sshKeyConfigured": true,
"tracking": "origin/main",
"ahead": 2,
"behind": 0,
"isDetachedHead": false,
"hasTracking": true,
"conflicted": [],
"diverged": false,
"mergeState": { "inProgress": false, "kind": null }
}conflicted: paths with unresolved merge conflicts (empty when clean).diverged:truewhen the branch is both ahead of and behind its remote (a plain push will be rejected — pull first, orforce-push).mergeState.inProgress/kind: amerge/cherry-pick/revert/rebaseleft mid-flight (usually by a conflict).kindis also the subcommand/abortuses. SeePOST /rosepetal-git/abort.
Repository Operations
POST /rosepetal-git/log
Fetches commit history with metadata for graph visualization.
Request
{
"repoPath": "/optional/path",
"maxCount": 50,
"from": "commit-hash", // Optional
"to": "commit-hash" // Optional
}Response 200
{
"success": true,
"operation": "log",
"total": 50,
"commits": [
{
"hash": "abc123def456...",
"parents": ["parent-hash-1", "parent-hash-2"],
"date": "2025-01-14T10:30:00Z",
"message": "Commit message",
"author": "John Doe",
"email": "john@example.com",
"pushed": true,
"isHead": false,
"branches": ["main", "feature-branch"]
}
]
}POST /rosepetal-git/status
Returns working tree status with file changes.
Request
{
"repoPath": "/optional/path"
}Response 200
{
"success": true,
"operation": "status",
"status": {
"current": "main",
"tracking": "origin/main",
"ahead": 2,
"behind": 0,
"files": [
{
"path": "flows.json",
"index": "M",
"working_dir": " "
}
],
"staged": ["flows.json"],
"modified": [],
"created": [],
"deleted": [],
"not_added": []
}
}POST /rosepetal-git/branches
Lists all local and unique remote branches.
Response 200
{
"success": true,
"operation": "branches",
"current": "main",
"all": ["main", "develop", "feature-branch"],
"branches": {
"main": {
"current": true,
"commit": "abc123...",
"label": "main"
}
}
}Branch Operations
POST /rosepetal-git/checkout
Checks out a commit or branch.
Request
{
"commitRef": "main" // Branch name or commit hash
}Response 200
{
"success": true,
"operation": "checkout",
"ref": "main",
"flowFileChanged": true,
"result": "..."
}flowFileChanged: whether the on-disk flow file actually changed. The editor only needs to reload flows from disk (and restart) when this istrue; a checkout that doesn't touch the flow file requires no resync.
POST /rosepetal-git/validate-checkout
Validates whether a checkout is safe. The only blocker is uncommitted working changes (which a checkout could overwrite). Unpushed commits are not a blocker — switching away never loses commits a branch still points to; unpushedCount is reported for information only.
Request
{
"targetRef": "abc123"
}Response 200
{
"success": true,
"canCheckout": false,
"blockers": ["uncommitted_changes"],
"uncommittedCount": 3,
"unpushedCount": 2,
"isDetachedHead": false,
"currentRef": "def456",
"targetRef": "abc123",
"currentBranch": "main"
}Commit Operations
POST /rosepetal-git/add
Stages files for commit.
Request
{
"stageAll": true, // Stage all changes
"files": ["file1", "file2"] // Or specific files
}Response 200
{
"success": true,
"operation": "add",
"result": "..."
}POST /rosepetal-git/unstage
Unstages files from the staging area.
Request
{
"unstageAll": true, // Unstage all
"files": ["file1", "file2"] // Or specific files
}Response 200
{
"success": true,
"operation": "unstage",
"result": "..."
}POST /rosepetal-git/commit
Creates a new commit. Automatically creates a branch if in detached HEAD state.
Request
{
"message": "Commit message",
"stageAll": false
}stageAll (optional, default false): when true, stage every change (git add -A, including untracked files and deletions) before committing. The sidebar sends true so a commit captures all current changes in one step; without it, only already-staged content is committed.
Response 200
{
"success": true,
"operation": "commit",
"commit": "abc123...",
"summary": {
"changes": 2,
"insertions": 10,
"deletions": 5
},
"branch": "from-abc123",
"createdBranch": true,
"message": "Created commit and new branch 'from-abc123'"
}POST /rosepetal-git/reset
Resets to a specific commit (soft/mixed/hard).
Request
{
"commitRef": "HEAD~1",
"resetMode": "mixed", // soft, mixed, or hard
"safeMode": true // Blocks hard resets when true
}Response 200
{
"success": true,
"operation": "reset",
"mode": "mixed",
"commit": "HEAD~1",
"flowFileChanged": false,
"result": "..."
}flowFileChanged: whether the on-disk flow file changed (and thus whether the editor must resync). Amixed/softreset never rewrites the working tree, so this is alwaysfalsefor them; only ahardreset can flip it totrue.
Error 500 (when safeMode blocks hard reset)
{
"success": false,
"error": "Hard reset is disabled in safe mode"
}POST /rosepetal-git/discard
Discards uncommitted changes to specific files.
Request
{
"files": ["flows.json", "package.json"]
}Response 200
{
"success": true,
"operation": "discard",
"files": ["flows.json", "package.json"],
"result": "..."
}Remote Operations
POST /rosepetal-git/fetch
Fetches updates from remote without merging.
Response 200
{
"success": true,
"operation": "fetch",
"result": "..."
}POST /rosepetal-git/pull
Fetches the upstream and integrates it, but never auto-merges. It fast-forwards when possible; if the branch has diverged (a merge commit would be required) it stops and reports needsMerge instead — call again with allowMerge: true to perform the merge (--no-edit, so it never blocks on an editor).
Request
{ "allowMerge": false }allowMerge(defaultfalse): permit creating a merge commit when the branch has diverged. Has no effect on a fast-forward or no-op pull.
Response 200 — varies by outcome (all share success/operation):
Up to date (nothing behind the upstream):
{ "success": true, "operation": "pull", "upToDate": true, "changed": false, "flowFileChanged": false, "conflicted": false, "conflictedFiles": [] }Diverged, merge not yet confirmed (the fetch already ran):
{ "success": true, "operation": "pull", "needsMerge": true, "ahead": 2, "behind": 3, "changed": false, "flowFileChanged": false, "conflicted": false, "conflictedFiles": [] }Fast-forward, or a confirmed merge (merged: true only for the merge case):
{ "success": true, "operation": "pull", "merged": false, "conflicted": false, "conflictedFiles": [], "changed": true, "flowFileChanged": true, "result": "..." }needsMerge: the branch diverged andallowMergewas not set — nothing was merged; re-call withallowMerge: trueto proceed.changed: whetherHEADmoved.flowFileChanged: whether the flow file changed — the editor resyncs from disk only whentrueand there's no conflict.conflicted/conflictedFiles: a confirmed merge can still conflict — see the conflict note undermerge.- Upstream defaults to the same-named remote branch: if the branch has no upstream but
origin/<branch>exists, pull links to it automatically. If no such remote branch exists, it errors asking you to push the branch first.
POST /rosepetal-git/push
Pushes the current branch's commits to the remote.
When the branch already has an upstream, this is a plain git push:
{
"success": true,
"operation": "push",
"branch": "main",
"setUpstream": false,
"upstream": "origin/main"
}When the branch has no upstream yet (its remote branch doesn't exist), it runs git push -u <remote> <branch>, which creates the remote branch and sets it as the tracking upstream:
{
"success": true,
"operation": "push",
"branch": "feature-branch",
"setUpstream": true,
"upstream": "origin/feature-branch"
}setUpstream: whether this push created the upstream link.upstream: the tracking ref (after the push).nullonly if the branch somehow still has no upstream.
Optional request body — publish to an explicit remote branch (creates it if missing and sets it as upstream, overriding any prior one). Used by the tracking picker's "create origin/<name>" option:
{ "targetRemote": "origin", "targetBranch": "feature-branch" }This runs git push -u <targetRemote> HEAD:<targetBranch>, so it works even when the branch currently tracks a differently-named upstream.
POST /rosepetal-git/force-push
Force pushes with --force-with-lease (requires confirmation).
Request
{
"confirmed": true
}Response 400 (when not confirmed)
{
"success": false,
"error": "Force push requires explicit confirmation",
"requiresConfirmation": true
}Response 200
{
"success": true,
"operation": "force-push",
"result": "...",
"warning": "Force push completed - Git history has been rewritten"
}Branch & Ref Operations
All accept the optional repoPath. Refs must not begin with - (rejected as unsafe). Operations that create a commit resolve the author identity from Node-RED user settings or git config.
POST /rosepetal-git/create-branch
Create a branch, optionally checking it out.
Request
{ "name": "feature/x", "startPoint": "main", "checkout": true }startPoint (optional) is any ref to branch from. checkout defaults to true.
Response 200
{ "success": true, "operation": "create-branch", "branch": "feature/x", "checkedOut": true, "startPoint": "main", "flowFileChanged": false }flowFileChanged: whencheckoutis true andstartPointis an earlier commit, the working tree is rewritten; this reports whether the flow file actually changed (so the editor only resyncs when needed).
POST /rosepetal-git/orphan-branch
Create a branch with no history (git checkout --orphan). With keepContent: true (default) the working tree carries over so the first commit captures current content; false clears the index to start empty.
Request
{ "name": "clean-slate", "keepContent": true }POST /rosepetal-git/delete-branch
Delete a branch. A safe delete (force omitted) fails if the branch has unmerged commits. Force-delete requires confirmed: true.
Request
{ "name": "feature/x", "force": true, "confirmed": true }POST /rosepetal-git/rename-branch
Rename a branch. Omit from to rename the current branch.
Request
{ "from": "old-name", "to": "new-name" }POST /rosepetal-git/set-upstream
Link a local branch to a remote branch (its tracking upstream). Defaults the branch to the current one and the remote to origin.
Request
{ "branch": "main", "remote": "origin", "remoteBranch": "main" }Response 200
{ "success": true, "operation": "set-upstream", "branch": "main", "upstream": "origin/main" }POST /rosepetal-git/merge
Merge a ref into the current branch. noFastForward: true forces a merge commit.
Request
{ "ref": "feature/x", "noFastForward": false }Response 200
{ "success": true, "operation": "merge", "ref": "feature/x", "conflicted": false, "conflictedFiles": [], "flowFileChanged": true, "result": "..." }
merge,pull,revert, andcherry-pickcan leave the repo mid-operation on a conflict. Instead of throwing, they return200with"conflicted": trueand"conflictedFiles": [...]. The flow file then contains conflict markers (invalid JSON) — do not redeploy from it. Resolve the files and commit, or callPOST /rosepetal-git/abortto undo the operation.
merge,pull, andrevertalso returnflowFileChanged(seecheckout): the editor only needs to resync from disk when it istrueand there is no conflict. Merges/pulls run headless (--no-edit), so a merge commit never blocks on an editor.
POST /rosepetal-git/revert
Create a new commit that undoes a previous commit (git revert, history-safe). noCommit: true stages the revert without committing. May report a conflict (see note above).
Request
{ "commitRef": "abc123", "noCommit": false }POST /rosepetal-git/cherry-pick
Apply a commit onto the current branch. noCommit: true stages without committing. May report a conflict (see note above).
Request
{ "commitRef": "abc123", "noCommit": false }POST /rosepetal-git/abort
Abort whatever operation is in progress (merge / cherry-pick / revert / rebase — see mergeState in project-info), returning the repo to its pre-operation state. Errors with 400 if nothing is in progress.
Request
{}Response 200
{ "success": true, "operation": "abort", "kind": "merge" }POST /rosepetal-git/discard-all
Discard all working changes: hard reset to HEAD and remove untracked files (clean -fd, respecting .gitignore). Requires confirmed: true.
Request
{ "confirmed": true }Diff Operations
POST /rosepetal-git/file-diff
Unified text diff for a file between two refs (or a ref and the working tree).
Request
{ "file": "package.json", "base": "HEAD", "head": null }Omit head (or send null) to diff against the working tree. Omit file to diff the whole tree.
Response 200
{ "success": true, "operation": "file-diff", "base": "HEAD", "head": "working", "file": "package.json", "diff": "diff --git a/package.json..." }POST /rosepetal-git/commit-diff
List the files changed in a commit (name-status, vs its parent; the root commit lists all files as additions).
Request
{ "commitRef": "abc123" }Response 200
{ "success": true, "operation": "commit-diff", "commit": "abc123", "files": [ { "status": "M", "path": "flows.json" } ] }POST /rosepetal-git/commit-branches
List the branches that contain a commit (answers "what branch is this commit on?"). Local and remote are returned separately; origin/HEAD and the detached-HEAD marker are filtered out.
Request
{ "commitRef": "abc123" }Response 200
{ "success": true, "operation": "commit-branches", "commit": "abc123", "branches": ["main", "feature/x"], "remoteBranches": ["origin/main"], "current": "main" }Flow-Aware Operations
Node-RED stores all flows in a single flows.json (a flat array of node objects). These endpoints present it as logical units so you can diff and revert one flow at a time without touching the others. The file on disk stays a single file — the decomposition is logical.
A unit is one of:
- flow — a tab and every node on it (
id= the tab's id) - subflow — a subflow definition and its internal nodes (
id= the subflow's id) - config — all global config nodes, grouped into a single unit (
id="__config__")
A ref of the literal string "WORKING" (or an omitted ref) means the working tree on disk.
POST /rosepetal-git/flow-units
List the logical units present at a ref (or the working tree).
Request
{ "ref": "HEAD" }Response 200
{
"success": true,
"operation": "flow-units",
"ref": "HEAD",
"units": [
{ "id": "a1b2c3d4", "kind": "flow", "label": "Main", "nodeCount": 12 },
{ "id": "e5f6...", "kind": "subflow", "label": "Resize Image", "nodeCount": 4 },
{ "id": "__config__", "kind": "config", "label": "Configuration", "nodeCount": 3 }
]
}POST /rosepetal-git/flow-diff
Per-unit diff between two refs (defaults: base = HEAD, head = working tree). Each unit reports a status (added / removed / modified / unchanged) and the nodes added/removed/modified within it. Pass detail: true to include full node bodies (base + head for modified nodes) instead of summaries.
Request
{ "base": "HEAD", "head": null, "detail": false }Response 200
{
"success": true,
"operation": "flow-diff",
"base": "HEAD",
"head": "working",
"changed": 1,
"units": [
{
"id": "a1b2c3d4",
"kind": "flow",
"label": "Main",
"status": "modified",
"counts": { "added": 1, "removed": 0, "modified": 2 },
"added": [ { "id": "n9", "type": "function", "name": "transform" } ],
"removed": [],
"modified": [ { "id": "n2", "type": "mqtt out" } ]
}
]
}To see what a past commit changed at flow level, diff it against its parent: { "base": "abc123^", "head": "abc123" }.
POST /rosepetal-git/revert-flow-unit
Revert a single unit to its version at ref, leaving every other unit untouched. Writes the working flows.json but does not commit — the result shows up as a normal working-tree change to review and commit (or reload via the flow-reload pattern below).
Request
{ "unitId": "a1b2c3d4", "ref": "HEAD", "includeDependencies": false }If the reverted flow references config nodes or subflows that differ, they are reported under dependencies. Set includeDependencies: true to also revert those units (the whole config unit is reverted if any config node is referenced).
Response 200
{
"success": true,
"operation": "revert-flow-unit",
"unitId": "a1b2c3d4",
"ref": "HEAD",
"revertedUnits": ["a1b2c3d4"],
"dependencies": {
"config": [ { "id": "c1", "type": "mqtt-broker" } ],
"subflows": [ { "id": "e5f6...", "label": "Resize Image" } ]
},
"file": "flows.json",
"warning": "The reverted flow references config/subflow units that were not reverted; they may be out of sync."
}Important: After a flow revert, reload flows from disk (see Flow Reload Pattern) so the editor and runtime pick up the change.
POST /rosepetal-git/revert-flow-nodes
Revert individual nodes (by id) to their version at ref, leaving the rest of the flow file untouched. A modified node is reset, a node added in the working tree is removed, and a deleted node is re-added. Writes the working flows.json without committing.
Request
{ "nodeIds": ["abc123", "def456"], "ref": "HEAD" }Response 200
{
"success": true,
"operation": "revert-flow-nodes",
"nodeIds": ["abc123"],
"ref": "HEAD",
"file": "flows.json",
"dependencies": { "config": [], "subflows": [] },
"warning": null
}Like a flow revert, this can leave a dangling wire/config reference if you revert a node but not what it points to; dependencies reports referenced config/subflow units. Reload flows from disk afterward.
Error Handling
The plugin provides context-aware error messages with actionable suggestions:
Merge Conflict
{
"success": false,
"error": "Merge conflict detected during pull",
"details": "CONFLICT (content): Merge conflict in flows.json",
"suggestion": "Your local changes conflict with changes from the remote. You need to:\n1. Pull the changes\n2. Resolve conflicts manually\n3. Commit the resolved changes"
}Push Rejected
{
"success": false,
"error": "Push rejected: Remote has newer commits",
"details": "! [rejected] main -> main (fetch first)",
"suggestion": "Pull the remote changes first, then push:\n1. Click the Pull button\n2. Resolve any merge conflicts\n3. Click Push again"
}Authentication Failed
{
"success": false,
"error": "Authentication failed",
"details": "Permission denied (publickey)",
"suggestion": "Git authentication error:\n1. Check Node-RED SSH keys are configured\n2. Verify repository permissions\n3. Check remote URL is correct"
}Integration Notes
Flow Reload Pattern
After destructive Git operations (checkout, reset, pull, discard), the UI keeps the editor in sync with disk by:
- Calling the Git operation endpoint
POST /flowswith headerNode-RED-Deployment-Type: reload, which tells the Node-RED runtime to re-read its flow file from disk- Reloading the editor with
window.location.reload()
This prevents editor state from diverging from disk state.
Detached HEAD Safety
When committing in detached HEAD state, the endpoint automatically creates a new branch named from-<hash> to prevent orphaned commits.
SSH Key Configuration
The plugin uses Node-RED's project SSH keys (in ~/.node-red/projects/.sshkeys/) rather than the user's ~/.ssh keys. The key is resolved per project: an explicit selection, else a key whose name matches the project, else the only key present.