feat(lanes): git login from the dashboard, merge and conflict resolution (#45)
Part A — a fetch/pull/push against a remote that needs a login no longer dies on "terminal prompts disabled": the server answers 409 EGITAUTH with the host, the git bar asks for a username + password/access token and resends the same action. AgentHub keeps its OWN credential store (<CLAUDE_HOME>/agenthub-git-credentials/, 0700/0600), one login per host, shared by every user of the install, managed from Settings -> Git credentials. A login is only sent to one of the repo's own remote hosts, saved only after git accepted it, never returned by any route, and reaches git through an exclusive credential helper (never argv). Part B — merge a branch (or the diverged upstream) from the git bar; a merge that stops on conflicts lists them in the Changes panel with Keep mine / Take theirs / Mark resolved, then Complete merge or Abort merge (confirm gate). Clean tree, repo-root lane and an existing ref only; pull/switch/hunk actions are blocked mid-merge; staging a conflicted file with markers left is refused. Rebase/reset/stash stay out.
This commit is contained in:
+30
-2
@@ -30,7 +30,11 @@ const STATUS_FILE_CAP = 20;
|
||||
* these variables. This module's whole job is to run git safely against repos other
|
||||
* than the one enclosing the current working directory.
|
||||
*/
|
||||
async function git(cwd, args, { timeout = 0, maxBuffer = 8 * 1024 * 1024, literal = false } = {}) {
|
||||
async function git(
|
||||
cwd,
|
||||
args,
|
||||
{ timeout = 0, maxBuffer = 8 * 1024 * 1024, literal = false, env: extraEnv = {} } = {}
|
||||
) {
|
||||
// Build a clean environment: copy process.env but scrub git hook variables
|
||||
// that could point to the outer repo's git directory or index.
|
||||
const env = { ...process.env };
|
||||
@@ -60,6 +64,8 @@ async function git(cwd, args, { timeout = 0, maxBuffer = 8 * 1024 * 1024, litera
|
||||
delete env.GIT_ICASE_PATHSPECS;
|
||||
if (literal) env.GIT_LITERAL_PATHSPECS = "1";
|
||||
else delete env.GIT_LITERAL_PATHSPECS;
|
||||
// Caller-supplied last: the git credential helper's inputs (git-credentials.js).
|
||||
Object.assign(env, extraEnv);
|
||||
|
||||
try {
|
||||
const result = await execFileAsync("git", args, {
|
||||
@@ -696,7 +702,29 @@ async function gitFacts(dir) {
|
||||
const { dirty, untracked, files } = await laneFiles(dir);
|
||||
const branch = (await git(dir, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
||||
const subject = (await git(dir, ["log", "-1", "--format=%s"])).stdout.trim();
|
||||
return { branch, head, subject, dirty, untracked, files, ...(await trackingFacts(dir)) };
|
||||
return {
|
||||
branch,
|
||||
head,
|
||||
subject,
|
||||
dirty,
|
||||
untracked,
|
||||
files,
|
||||
...(await trackingFacts(dir)),
|
||||
...(await mergeFacts(dir)),
|
||||
};
|
||||
}
|
||||
|
||||
/** A merge in progress and how many files it still holds unmerged (repo-wide). */
|
||||
async function mergeFacts(dir) {
|
||||
try {
|
||||
await git(dir, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]);
|
||||
} catch {
|
||||
return { merging: false, conflicts: 0 };
|
||||
}
|
||||
const out = await git(dir, ["diff", "--name-only", "--diff-filter=U", "-z"]).catch(() => ({
|
||||
stdout: "",
|
||||
}));
|
||||
return { merging: true, conflicts: out.stdout.split("\0").filter(Boolean).length };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user