Skip to content

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.read permission; mutating operations require git-control.write. Note this is by operation, not HTTP verb — several read-only endpoints are POST (they take a JSON body) but still only need read (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:

bash
# 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

LevelEndpointsNotes
safe (read-only)project-info, status, log, branches, validate-checkout, flow-units, flow-diff, file-diff, commit-diff, commit-branches, ssh-keysnever modify the repo
writes working tree / historyadd, unstage, commit, checkout, pull, merge, revert, cherry-pick, abort, create-branch, orphan-branch, rename-branch, set-upstream, revert-flow-unit, ssh-keyreversible 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:

json
{
  "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:

json
{
  "repoPath": "/path/to/repo"  // Optional - defaults to active Node-RED project
}

Response Structure

Success responses include:

json
{
  "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

json
{
  "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: true when the branch is both ahead of and behind its remote (a plain push will be rejected — pull first, or force-push).
  • mergeState.inProgress / kind: a merge / cherry-pick / revert / rebase left mid-flight (usually by a conflict). kind is also the subcommand /abort uses. See POST /rosepetal-git/abort.

Repository Operations

POST /rosepetal-git/log

Fetches commit history with metadata for graph visualization.

Request

json
{
  "repoPath": "/optional/path",
  "maxCount": 50,
  "from": "commit-hash",  // Optional
  "to": "commit-hash"     // Optional
}

Response 200

json
{
  "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

json
{
  "repoPath": "/optional/path"
}

Response 200

json
{
  "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

json
{
  "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

json
{
  "commitRef": "main"  // Branch name or commit hash
}

Response 200

json
{
  "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 is true; 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

json
{
  "targetRef": "abc123"
}

Response 200

json
{
  "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

json
{
  "stageAll": true,          // Stage all changes
  "files": ["file1", "file2"] // Or specific files
}

Response 200

json
{
  "success": true,
  "operation": "add",
  "result": "..."
}

POST /rosepetal-git/unstage

Unstages files from the staging area.

Request

json
{
  "unstageAll": true,         // Unstage all
  "files": ["file1", "file2"] // Or specific files
}

Response 200

json
{
  "success": true,
  "operation": "unstage",
  "result": "..."
}

POST /rosepetal-git/commit

Creates a new commit. Automatically creates a branch if in detached HEAD state.

Request

json
{
  "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

json
{
  "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

json
{
  "commitRef": "HEAD~1",
  "resetMode": "mixed",  // soft, mixed, or hard
  "safeMode": true       // Blocks hard resets when true
}

Response 200

json
{
  "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). A mixed/soft reset never rewrites the working tree, so this is always false for them; only a hard reset can flip it to true.

Error 500 (when safeMode blocks hard reset)

json
{
  "success": false,
  "error": "Hard reset is disabled in safe mode"
}

POST /rosepetal-git/discard

Discards uncommitted changes to specific files.

Request

json
{
  "files": ["flows.json", "package.json"]
}

Response 200

json
{
  "success": true,
  "operation": "discard",
  "files": ["flows.json", "package.json"],
  "result": "..."
}

Remote Operations

POST /rosepetal-git/fetch

Fetches updates from remote without merging.

Response 200

json
{
  "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

json
{ "allowMerge": false }
  • allowMerge (default false): 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):

json
{ "success": true, "operation": "pull", "upToDate": true, "changed": false, "flowFileChanged": false, "conflicted": false, "conflictedFiles": [] }

Diverged, merge not yet confirmed (the fetch already ran):

json
{ "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):

json
{ "success": true, "operation": "pull", "merged": false, "conflicted": false, "conflictedFiles": [], "changed": true, "flowFileChanged": true, "result": "..." }
  • needsMerge: the branch diverged and allowMerge was not set — nothing was merged; re-call with allowMerge: true to proceed.
  • changed: whether HEAD moved. flowFileChanged: whether the flow file changed — the editor resyncs from disk only when true and there's no conflict.
  • conflicted / conflictedFiles: a confirmed merge can still conflict — see the conflict note under merge.
  • 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:

json
{
  "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:

json
{
  "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). null only 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:

json
{ "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

json
{
  "confirmed": true
}

Response 400 (when not confirmed)

json
{
  "success": false,
  "error": "Force push requires explicit confirmation",
  "requiresConfirmation": true
}

Response 200

json
{
  "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

json
{ "name": "feature/x", "startPoint": "main", "checkout": true }

startPoint (optional) is any ref to branch from. checkout defaults to true.

Response 200

json
{ "success": true, "operation": "create-branch", "branch": "feature/x", "checkedOut": true, "startPoint": "main", "flowFileChanged": false }
  • flowFileChanged: when checkout is true and startPoint is 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

json
{ "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

json
{ "name": "feature/x", "force": true, "confirmed": true }

POST /rosepetal-git/rename-branch

Rename a branch. Omit from to rename the current branch.

Request

json
{ "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

json
{ "branch": "main", "remote": "origin", "remoteBranch": "main" }

Response 200

json
{ "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

json
{ "ref": "feature/x", "noFastForward": false }

Response 200

json
{ "success": true, "operation": "merge", "ref": "feature/x", "conflicted": false, "conflictedFiles": [], "flowFileChanged": true, "result": "..." }

merge, pull, revert, and cherry-pick can leave the repo mid-operation on a conflict. Instead of throwing, they return 200 with "conflicted": true and "conflictedFiles": [...]. The flow file then contains conflict markers (invalid JSON) — do not redeploy from it. Resolve the files and commit, or call POST /rosepetal-git/abort to undo the operation.

merge, pull, and revert also return flowFileChanged (see checkout): the editor only needs to resync from disk when it is true and 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

json
{ "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

json
{ "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

json
{}

Response 200

json
{ "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

json
{ "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

json
{ "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

json
{ "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

json
{ "commitRef": "abc123" }

Response 200

json
{ "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

json
{ "commitRef": "abc123" }

Response 200

json
{ "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

json
{ "ref": "HEAD" }

Response 200

json
{
  "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

json
{ "base": "HEAD", "head": null, "detail": false }

Response 200

json
{
  "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

json
{ "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

json
{
  "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

json
{ "nodeIds": ["abc123", "def456"], "ref": "HEAD" }

Response 200

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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:

  1. Calling the Git operation endpoint
  2. POST /flows with header Node-RED-Deployment-Type: reload, which tells the Node-RED runtime to re-read its flow file from disk
  3. 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.