Skip to content

Commit ace33d1

Browse files
committed
Helpful message if not logged in
1 parent 840c956 commit ace33d1

4 files changed

Lines changed: 164 additions & 9 deletions

File tree

Examples/ExampleGitReview/GitRunner.cpp

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <array>
55
#include <cstdio>
66
#include <cstring>
7+
#include <cwchar>
78
#include <sstream>
89
#include <stdexcept>
910
#include <thread>
@@ -84,7 +85,38 @@ namespace gitReview
8485
into.append(buf, static_cast<size_t>(n));
8586
}
8687

87-
static GitRunResult runGitImpl(const std::string &repoRoot, const std::vector<std::string> &args, const std::string *stdinData)
88+
static bool isEnvName(const wchar_t *entry, const wchar_t *name)
89+
{
90+
const size_t nameLen = wcslen(name);
91+
return _wcsnicmp(entry, name, nameLen) == 0 && entry[nameLen] == L'=';
92+
}
93+
94+
static std::vector<wchar_t> makeNoTerminalPromptEnvironment()
95+
{
96+
std::vector<wchar_t> block;
97+
LPWCH raw = GetEnvironmentStringsW();
98+
if (!raw)
99+
return block;
100+
101+
for (LPWCH p = raw; *p; p += wcslen(p) + 1)
102+
{
103+
if (isEnvName(p, L"GIT_TERMINAL_PROMPT") || isEnvName(p, L"GCM_INTERACTIVE"))
104+
continue;
105+
const size_t len = wcslen(p);
106+
block.insert(block.end(), p, p + len + 1);
107+
}
108+
FreeEnvironmentStringsW(raw);
109+
110+
const wchar_t gitPrompt[] = L"GIT_TERMINAL_PROMPT=0";
111+
const wchar_t gcmInteractive[] = L"GCM_INTERACTIVE=never";
112+
block.insert(block.end(), gitPrompt, gitPrompt + wcslen(gitPrompt) + 1);
113+
block.insert(block.end(), gcmInteractive, gcmInteractive + wcslen(gcmInteractive) + 1);
114+
block.push_back(L'\0');
115+
return block;
116+
}
117+
118+
static GitRunResult runGitImpl(const std::string &repoRoot, const std::vector<std::string> &args, const std::string *stdinData,
119+
bool noTerminalPrompt)
88120
{
89121
GitRunResult result;
90122

@@ -138,8 +170,12 @@ namespace gitReview
138170
PROCESS_INFORMATION pi{};
139171
std::vector<wchar_t> cmdBuf(cmdLine.begin(), cmdLine.end());
140172
cmdBuf.push_back(L'\0');
173+
std::vector<wchar_t> envBlock;
174+
if (noTerminalPrompt)
175+
envBlock = makeNoTerminalPromptEnvironment();
141176

142-
const BOOL ok = CreateProcessW(nullptr, cmdBuf.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi);
177+
const BOOL ok = CreateProcessW(nullptr, cmdBuf.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT,
178+
noTerminalPrompt && !envBlock.empty() ? envBlock.data() : nullptr, nullptr, &si, &pi);
143179

144180
CloseHandle(hStdoutWrite);
145181
CloseHandle(hStderrWrite);
@@ -191,7 +227,8 @@ namespace gitReview
191227
#else
192228
// ── POSIX helpers ────────────────────────────────────────────────
193229

194-
static GitRunResult runGitImpl(const std::string &repoRoot, const std::vector<std::string> &args, const std::string *stdinData)
230+
static GitRunResult runGitImpl(const std::string &repoRoot, const std::vector<std::string> &args, const std::string *stdinData,
231+
bool noTerminalPrompt)
195232
{
196233
GitRunResult result;
197234

@@ -250,6 +287,21 @@ namespace gitReview
250287
dup2(inPipe[0], STDIN_FILENO);
251288
close(inPipe[0]);
252289
}
290+
else if (noTerminalPrompt)
291+
{
292+
const int devNull = open("/dev/null", O_RDONLY);
293+
if (devNull >= 0)
294+
{
295+
dup2(devNull, STDIN_FILENO);
296+
close(devNull);
297+
}
298+
}
299+
300+
if (noTerminalPrompt)
301+
{
302+
setenv("GIT_TERMINAL_PROMPT", "0", 1);
303+
setenv("GCM_INTERACTIVE", "never", 1);
304+
}
253305

254306
std::vector<std::string> argvStorage;
255307
argvStorage.reserve(args.size() + 3);
@@ -328,12 +380,17 @@ namespace gitReview
328380

329381
GitRunResult runGit(const std::string &repoRoot, const std::vector<std::string> &args)
330382
{
331-
return runGitImpl(repoRoot, args, nullptr);
383+
return runGitImpl(repoRoot, args, nullptr, false);
332384
}
333385

334386
GitRunResult runGitWithStdin(const std::string &repoRoot, const std::vector<std::string> &args, const std::string &stdinData)
335387
{
336-
return runGitImpl(repoRoot, args, &stdinData);
388+
return runGitImpl(repoRoot, args, &stdinData, false);
389+
}
390+
391+
GitRunResult runGitNoTerminalPrompt(const std::string &repoRoot, const std::vector<std::string> &args)
392+
{
393+
return runGitImpl(repoRoot, args, nullptr, true);
337394
}
338395

339396
std::string redactGitOutput(const std::string &text)

Examples/ExampleGitReview/GitRunner.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ namespace gitReview
1919
/// Like runGit, but feeds \p stdinData to the child's stdin and closes it.
2020
GitRunResult runGitWithStdin(const std::string &repoRoot, const std::vector<std::string> &args, const std::string &stdinData);
2121

22+
/// Like runGit, but disables interactive terminal credential prompts for the child process.
23+
GitRunResult runGitNoTerminalPrompt(const std::string &repoRoot, const std::vector<std::string> &args);
24+
2225
/// Redacts credentialed URLs and obvious token patterns from Git
2326
/// output before it is shown in the UI.
2427
std::string redactGitOutput(const std::string &text);

Examples/ExampleGitReview/ReviewGui.cpp

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,60 @@ namespace gitReview
106106
return "";
107107
}
108108

109+
std::string trimModalLine(std::string s)
110+
{
111+
while (!s.empty() && (s.front() == ' ' || s.front() == '\t'))
112+
s.erase(s.begin());
113+
while (!s.empty() && (s.back() == ' ' || s.back() == '\t' || s.back() == '\r'))
114+
s.pop_back();
115+
return s;
116+
}
117+
118+
bool isCopyableModalLine(const std::string &line)
119+
{
120+
const std::string trimmed = trimModalLine(line);
121+
return trimmed.rfind("http://", 0) == 0 || trimmed.rfind("https://", 0) == 0 || trimmed.rfind("gh ", 0) == 0;
122+
}
123+
124+
void drawCopyableModalLine(const std::string &line, int id)
125+
{
126+
std::string text = trimModalLine(line);
127+
std::vector<char> buffer(text.begin(), text.end());
128+
buffer.push_back('\0');
129+
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 42.f);
130+
ImGui::InputText(("##modalCopy" + std::to_string(id)).c_str(), buffer.data(), buffer.size(), ImGuiInputTextFlags_ReadOnly);
131+
}
132+
133+
void drawModalBody(const std::string &body)
134+
{
135+
size_t lineStart = 0;
136+
int copyId = 0;
137+
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 42.f);
138+
while (lineStart <= body.size())
139+
{
140+
const size_t lineEnd = body.find('\n', lineStart);
141+
const std::string line = body.substr(lineStart, lineEnd == std::string::npos ? std::string::npos : lineEnd - lineStart);
142+
if (isCopyableModalLine(line))
143+
{
144+
ImGui::PopTextWrapPos();
145+
drawCopyableModalLine(line, copyId++);
146+
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 42.f);
147+
}
148+
else if (line.empty())
149+
{
150+
ImGui::Spacing();
151+
}
152+
else
153+
{
154+
ImGui::TextUnformatted(line.c_str());
155+
}
156+
if (lineEnd == std::string::npos)
157+
break;
158+
lineStart = lineEnd + 1;
159+
}
160+
ImGui::PopTextWrapPos();
161+
}
162+
109163
void drawModalIfAny(ReviewAppState &app)
110164
{
111165
if (app.modalOpen)
@@ -116,9 +170,7 @@ namespace gitReview
116170
{
117171
ImGui::TextUnformatted(app.modalTitle.c_str());
118172
ImGui::Separator();
119-
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 42.f);
120-
ImGui::TextUnformatted(app.modalBody.c_str());
121-
ImGui::PopTextWrapPos();
173+
drawModalBody(app.modalBody);
122174
if (ImGui::Button("OK", ImVec2(120, 0)))
123175
{
124176
app.modalOpen = false;

Examples/ExampleGitReview/ReviewSession.cpp

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,41 @@ namespace gitReview
6868
[](unsigned char a, unsigned char b) { return std::tolower(a) == std::tolower(b); }) != haystack.end();
6969
}
7070

71+
bool isGitHubHttpsRemoteUrl(const std::string &url)
72+
{
73+
const std::string trimmed = trimCopy(url);
74+
return (containsCaseInsensitive(trimmed, "https://github.com/") || containsCaseInsensitive(trimmed, "http://github.com/"));
75+
}
76+
77+
std::string githubHttpsCliSetupInstructions(const std::string &originUrl)
78+
{
79+
std::string msg =
80+
"GitHub authentication is not configured for this HTTPS remote, and this app will not let git open an interactive username/password prompt.\n\n";
81+
if (!originUrl.empty())
82+
msg += "origin: " + redactGitOutput(trimCopy(originUrl)) + "\n\n";
83+
msg +=
84+
"Set up GitHub HTTPS authentication with GitHub CLI:\n\n"
85+
"1. Install GitHub CLI if needed:\n"
86+
" https://cli.github.com/\n\n"
87+
"2. Log in for HTTPS Git operations:\n"
88+
" gh auth login --hostname github.com --web --git-protocol https\n\n"
89+
"3. Let gh configure Git's credential helper:\n"
90+
" gh auth setup-git\n\n"
91+
"4. Check the login:\n"
92+
" gh auth status --hostname github.com\n\n"
93+
"5. Push again from ExampleGitReview.";
94+
return msg;
95+
}
96+
97+
bool isCredentialPromptFailure(const std::string &err)
98+
{
99+
return containsCaseInsensitive(err, "terminal prompts disabled") ||
100+
containsCaseInsensitive(err, "could not read Username") ||
101+
containsCaseInsensitive(err, "could not read Password") ||
102+
containsCaseInsensitive(err, "authentication failed") ||
103+
containsCaseInsensitive(err, "credential");
104+
}
105+
71106
/// Sanitize git output for user-facing display.
72107
std::string sanitizedGitError(const GitRunResult &r)
73108
{
@@ -807,9 +842,17 @@ namespace gitReview
807842
{
808843
err.clear();
809844
const std::string root = repoRootString(app);
810-
GitRunResult r = runGit(root, { "push" });
845+
GitRunResult origin = runGit(root, { "remote", "get-url", "origin" });
846+
const bool githubHttpsOrigin = origin.exitCode == 0 && isGitHubHttpsRemoteUrl(origin.standardOut);
847+
GitRunResult r = githubHttpsOrigin
848+
? runGitNoTerminalPrompt(root, { "-c", "credential.interactive=false", "push" })
849+
: runGit(root, { "push" });
811850
if (r.exitCode != 0)
851+
{
812852
err = sanitizedGitError(r);
853+
if (githubHttpsOrigin && isCredentialPromptFailure(err))
854+
err = githubHttpsCliSetupInstructions(origin.standardOut);
855+
}
813856
}
814857

815858
const std::vector<DiffRow> &cachedDiffRows(ReviewAppState &app)

0 commit comments

Comments
 (0)