0
0
Fork 0

feat(git_branch_set_upstream_to): ship the missing canonical step

RTFM. The helpme-codesys.com Git scripting page
(https://content.helpme-codesys.com/en/CODESYS%20Git/_git_using_scripting.html)
spells out the canonical "init -> commit -> remote_add -> push"
sequence and explicitly inserts a mandatory step between remote_add
and push:

  "Set Upstream Before Push: After adding a remote, use
   project.git.branch_set_upstream_to(origin_remote) before pushing."

The previous commit (8a6059b) shipped git_remote_add + git_push but
left this step labelled "out of scope," which guaranteed the very
first end-to-end smoke test would fail with:

  sLocalBranchName: The branch 'master' ('refs/heads/master')
  does not track an upstream branch.

Confirmed against the GitSmokeTest project (C:\Temp\GitSmokeTest)
on 2026-04-25 immediately after the commit landed.

This commit adds git_branch_set_upstream_to as a thin wrapper over
project.git.branch_set_upstream_to(remoteName, branchName?) using
the simplest of the four overloads in
Stubs/scriptengine/GitScriptProject.pyi. branchName defaults to the
current branch (per the stub default arg). Defensive checks +
HasGitLicense rewrite mirror the rest of the git_* tool family.

Tool description carries the doc citation up front and the exact
error message you get without it, so the next user sees the missing
step before having to discover it experimentally.
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-25 20:45:43 +02:00
parent 8a6059b9a0
commit e3e5f58039
2 changed files with 92 additions and 0 deletions

View file

@ -0,0 +1,68 @@
import sys, scriptengine as script_engine, traceback
REMOTE_NAME = "{REMOTE_NAME}"
BRANCH_NAME = "{BRANCH_NAME}" # may be empty -> defaults to current branch
try:
print("DEBUG: git_branch_set_upstream_to: Project='%s', Remote='%s', Branch='%s'" % (
PROJECT_FILE_PATH, REMOTE_NAME, BRANCH_NAME))
if not REMOTE_NAME.strip():
raise ValueError("Remote name is empty.")
primary_project = ensure_project_open(PROJECT_FILE_PATH)
git = getattr(primary_project, 'git', None)
if git is None:
raise RuntimeError(
"Project '%s' is not bound to a Git repository (primary_project.git "
"is None). Run git_init first." % PROJECT_FILE_PATH)
if not hasattr(git, 'branch_set_upstream_to'):
attrs = sorted([a for a in dir(git) if not a.startswith('_')])
raise AttributeError(
"project.git does not expose branch_set_upstream_to(). "
"Available: %s" % attrs)
if BRANCH_NAME:
print("DEBUG: calling git.branch_set_upstream_to('%s', '%s')" % (
REMOTE_NAME, BRANCH_NAME))
git.branch_set_upstream_to(REMOTE_NAME, BRANCH_NAME)
else:
print("DEBUG: calling git.branch_set_upstream_to('%s') -- defaults to current branch" % REMOTE_NAME)
git.branch_set_upstream_to(REMOTE_NAME)
print("DEBUG: branch_set_upstream_to returned without exception.")
current = "?"
if hasattr(git, 'branch_show_current'):
try:
current = str(git.branch_show_current())
except Exception:
pass
if BRANCH_NAME:
print("Branch '%s' now tracks upstream '%s'" % (BRANCH_NAME, REMOTE_NAME))
else:
print("Current branch '%s' now tracks upstream '%s'" % (current, REMOTE_NAME))
print("SCRIPT_SUCCESS: git_branch_set_upstream_to complete.")
sys.exit(0)
except Exception as e:
detailed = traceback.format_exc()
raw = "%s" % e
if 'HasGitLicense' in raw or 'HasGitLicense' in detailed:
msg = (
"CODESYS Git scripting requires an active CODESYS Professional "
"Developer Edition subscription license. The plug-in is installed "
"but the runtime 'HasGitLicense' rule returned False, so every "
"project.git.* operation (init/commit/status/...) is gated. "
"Activate a Professional Developer Edition subscription on this "
"CODESYS install (see https://store.codesys.com/en/codesys-git.html, "
"sections 'Additional Requirements' and 'Licensing'). Underlying "
"error from CODESYS: %s" % e
)
else:
msg = "Error in git_branch_set_upstream_to for project '%s': %s\n%s" % (
PROJECT_FILE_PATH, e, detailed)
print(msg)
print("SCRIPT_ERROR: %s" % msg)
sys.exit(1)

View file

@ -1139,6 +1139,30 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
}
);
s.tool(
'git_branch_set_upstream_to',
"Sets the upstream tracking ref of a local branch to a configured remote via project.git.branch_set_upstream_to(remoteName, branchName?). MANDATORY before the first push to a fresh remote -- per the helpme-codesys.com Git scripting docs (https://content.helpme-codesys.com/en/CODESYS%20Git/_git_using_scripting.html), 'After adding a remote, use project.git.branch_set_upstream_to(origin_remote) before pushing'. Without this call, push() fails with 'The branch X does not track an upstream branch.' Requires an existing git binding on the project, a configured remote (use git_remote_add), and an active CODESYS Professional Developer Edition subscription license -- without the subscription, the tool fails fast with a clear PDE-required message.",
{
projectFilePath: z.string().describe("Path to the project file."),
remoteName: z.string().min(1).describe("Remote name to track. Conventionally 'origin'."),
branchName: z.string().optional().describe("Local branch to configure. Optional; defaults to the current branch."),
},
async (args: { projectFilePath: string; remoteName: string; branchName?: string }) => {
const escaped = resolvePath(args.projectFilePath, workspaceDir);
const script = scriptManager.prepareScriptWithHelpers(
'git_branch_set_upstream_to',
{
PROJECT_FILE_PATH: escaped,
REMOTE_NAME: args.remoteName,
BRANCH_NAME: args.branchName || '',
},
['ensure_project_open']
);
const result = await executor.executeScript(script);
return formatToolResponse(result, `git_branch_set_upstream_to complete for ${args.projectFilePath}.`);
}
);
s.tool(
'git_push',
"Pushes the local branch to a configured remote via project.git.push(). If username + token are both provided, uses the 3-arg overload push(branch, user, SecureString(token)) and derives the current branch when branchName is omitted; otherwise calls push(branch) or push() and relies on git config / Windows Credential Manager / cached credentials. SECURITY NOTE: when token is supplied, it is templated into the IronPython script that the watcher executes -- briefly resident in the watcher's command file on disk. Prefer cached credentials (omit token) when feasible. Requires an existing git binding on the project, a configured remote (use git_remote_add), and an active CODESYS Professional Developer Edition subscription license -- without the subscription, the tool fails fast with a clear PDE-required message.",