Not sure
This commit is contained in:
@@ -86,7 +86,7 @@ target_compile_definitions(gitree PRIVATE
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(gitree PRIVATE dwmapi urlmon windowscodecs bcrypt)
|
||||
target_link_libraries(gitree PRIVATE dwmapi urlmon windowscodecs bcrypt crypt32)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
#include <izo/Time.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -99,6 +101,64 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> repositoryRemotes(git_repository *repository)
|
||||
{
|
||||
std::vector<std::string> remotes;
|
||||
git_strarray names{};
|
||||
if (git_remote_list(&names, repository) == 0)
|
||||
{
|
||||
for (size_t i = 0; i < names.count; ++i)
|
||||
remotes.emplace_back(names.strings[i]);
|
||||
git_strarray_dispose(&names);
|
||||
}
|
||||
return remotes;
|
||||
}
|
||||
|
||||
std::string preferredRemoteName(git_repository *repository)
|
||||
{
|
||||
const std::vector<std::string> remotes = repositoryRemotes(repository);
|
||||
if (remotes.empty())
|
||||
return {};
|
||||
const auto origin = std::find(remotes.begin(), remotes.end(), "origin");
|
||||
return origin != remotes.end() ? *origin : remotes.front();
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string encodeBase64(std::string_view value)
|
||||
{
|
||||
static constexpr std::string_view alphabet =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string encoded;
|
||||
encoded.reserve(((value.size() + 2) / 3) * 4);
|
||||
for (size_t index = 0; index < value.size(); index += 3)
|
||||
{
|
||||
const uint32_t a = static_cast<unsigned char>(value[index]);
|
||||
const uint32_t b = index + 1 < value.size() ? static_cast<unsigned char>(value[index + 1]) : 0;
|
||||
const uint32_t c = index + 2 < value.size() ? static_cast<unsigned char>(value[index + 2]) : 0;
|
||||
const uint32_t chunk = (a << 16) | (b << 8) | c;
|
||||
encoded.push_back(alphabet[(chunk >> 18) & 0x3F]);
|
||||
encoded.push_back(alphabet[(chunk >> 12) & 0x3F]);
|
||||
encoded.push_back(index + 1 < value.size() ? alphabet[(chunk >> 6) & 0x3F] : '=');
|
||||
encoded.push_back(index + 2 < value.size() ? alphabet[chunk & 0x3F] : '=');
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::vector<std::string> withAuthOverrideArguments(std::vector<std::string> arguments,
|
||||
const std::optional<GitAuthOverride> &auth)
|
||||
{
|
||||
if (!auth || auth->username.empty())
|
||||
return arguments;
|
||||
const std::string header = "http.extraHeader=Authorization: Basic " +
|
||||
encodeBase64(auth->username + ":" + auth->password);
|
||||
arguments.insert(arguments.begin(), {
|
||||
"-c", "credential.interactive=never",
|
||||
"-c", header,
|
||||
});
|
||||
return arguments;
|
||||
}
|
||||
|
||||
void addBadge(RepositoryView &repository, const git_oid *oid, RefBadge badge)
|
||||
{
|
||||
if (!oid)
|
||||
@@ -753,10 +813,11 @@ bool GitManager::reload(RepositoryView &repository, std::string &error)
|
||||
}
|
||||
|
||||
bool GitManager::runGit(RepositoryView &repository, const std::vector<std::string> &arguments,
|
||||
const char *success_message, std::string &message, bool reload_repository)
|
||||
const char *success_message, std::string &message, bool reload_repository,
|
||||
const std::optional<GitAuthOverride> &auth)
|
||||
{
|
||||
std::string command_output;
|
||||
if (!captureGit(repository, arguments, command_output, message))
|
||||
if (!captureGit(repository, withAuthOverrideArguments(arguments, auth), command_output, message))
|
||||
return false;
|
||||
if (reload_repository)
|
||||
{
|
||||
@@ -772,7 +833,8 @@ bool GitManager::runGit(RepositoryView &repository, const std::vector<std::strin
|
||||
}
|
||||
|
||||
bool GitManager::captureGit(RepositoryView &repository, const std::vector<std::string> &arguments,
|
||||
std::string &command_output, std::string &error)
|
||||
std::string &command_output, std::string &error,
|
||||
const std::string *stdin_data)
|
||||
{
|
||||
if (!repository.repo)
|
||||
{
|
||||
@@ -789,10 +851,24 @@ bool GitManager::captureGit(RepositoryView &repository, const std::vector<std::s
|
||||
return false;
|
||||
}
|
||||
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
HANDLE input_read = nullptr;
|
||||
HANDLE input_write = nullptr;
|
||||
if (stdin_data && !CreatePipe(&input_read, &input_write, &security, 0))
|
||||
{
|
||||
DeleteFileW(output_path);
|
||||
error = "Unable to create Git command input pipe";
|
||||
return false;
|
||||
}
|
||||
if (input_write)
|
||||
SetHandleInformation(input_write, HANDLE_FLAG_INHERIT, 0);
|
||||
HANDLE output = CreateFileW(output_path, GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, &security, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, nullptr);
|
||||
if (output == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
if (input_read)
|
||||
CloseHandle(input_read);
|
||||
if (input_write)
|
||||
CloseHandle(input_write);
|
||||
DeleteFileW(output_path);
|
||||
error = "Unable to capture Git command output";
|
||||
return false;
|
||||
@@ -809,18 +885,34 @@ bool GitManager::captureGit(RepositoryView &repository, const std::vector<std::s
|
||||
startup.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.hStdOutput = output;
|
||||
startup.hStdError = output;
|
||||
startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||
startup.hStdInput = input_read ? input_read : GetStdHandle(STD_INPUT_HANDLE);
|
||||
PROCESS_INFORMATION process{};
|
||||
const BOOL started = CreateProcessW(nullptr, mutable_command.data(), nullptr, nullptr, TRUE,
|
||||
CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process);
|
||||
DWORD exit_code = 1;
|
||||
if (started)
|
||||
{
|
||||
if (input_read)
|
||||
{
|
||||
CloseHandle(input_read);
|
||||
input_read = nullptr;
|
||||
}
|
||||
if (input_write && stdin_data)
|
||||
{
|
||||
DWORD bytes_written = 0;
|
||||
WriteFile(input_write, stdin_data->data(), static_cast<DWORD>(stdin_data->size()), &bytes_written, nullptr);
|
||||
CloseHandle(input_write);
|
||||
input_write = nullptr;
|
||||
}
|
||||
WaitForSingleObject(process.hProcess, INFINITE);
|
||||
GetExitCodeProcess(process.hProcess, &exit_code);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
}
|
||||
if (input_read)
|
||||
CloseHandle(input_read);
|
||||
if (input_write)
|
||||
CloseHandle(input_write);
|
||||
FlushFileBuffers(output);
|
||||
SetFilePointer(output, 0, nullptr, FILE_BEGIN);
|
||||
command_output.clear();
|
||||
@@ -851,7 +943,7 @@ bool GitManager::prepareCredentials(RepositoryView &repository, std::string &err
|
||||
return true;
|
||||
|
||||
bool uses_https = false;
|
||||
for (const std::string &remote_name : repository.remotes)
|
||||
for (const std::string &remote_name : repositoryRemotes(repository.repo))
|
||||
{
|
||||
git_remote *remote = nullptr;
|
||||
if (git_remote_lookup(&remote, repository.repo, remote_name.c_str()) == 0)
|
||||
@@ -892,9 +984,8 @@ bool GitManager::prepareCredentials(RepositoryView &repository, std::string &err
|
||||
|
||||
if (!gcm_available)
|
||||
{
|
||||
error = "This HTTPS remote needs authentication, but no credential helper is configured. "
|
||||
"Install Git Credential Manager or configure credential.helper in Git.";
|
||||
return false;
|
||||
repository.credentials_checked = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string configure_output;
|
||||
@@ -905,6 +996,78 @@ bool GitManager::prepareCredentials(RepositoryView &repository, std::string &err
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GitManager::remoteUrl(RepositoryView &repository, const std::string &remote_name, std::string &error)
|
||||
{
|
||||
git_remote *remote = nullptr;
|
||||
if (git_remote_lookup(&remote, repository.repo, remote_name.c_str()) != 0)
|
||||
{
|
||||
error = lastError("Unable to find remote");
|
||||
return {};
|
||||
}
|
||||
const char *url = git_remote_url(remote);
|
||||
const std::string remote_url = url ? url : "";
|
||||
git_remote_free(remote);
|
||||
if (remote_url.empty())
|
||||
error = "Remote has no URL";
|
||||
return remote_url;
|
||||
}
|
||||
|
||||
bool GitManager::storeCredential(RepositoryView &repository, const std::string &remote_url,
|
||||
const std::string &username, const std::string &password,
|
||||
std::string &error)
|
||||
{
|
||||
if (username.empty())
|
||||
{
|
||||
error = "Username is required";
|
||||
return false;
|
||||
}
|
||||
if (!prepareCredentials(repository, error))
|
||||
return false;
|
||||
std::string helper;
|
||||
std::string helper_error;
|
||||
if (!captureGit(repository, {"config", "--get-all", "credential.helper"}, helper, helper_error) || helper.empty())
|
||||
{
|
||||
error = helper_error.empty() ? "No Git credential helper is configured" : helper_error;
|
||||
return false;
|
||||
}
|
||||
const size_t scheme = remote_url.find("://");
|
||||
if (scheme == std::string::npos)
|
||||
{
|
||||
error = "Remote URL is invalid";
|
||||
return false;
|
||||
}
|
||||
const size_t authority_begin = scheme + 3;
|
||||
const size_t path_begin = remote_url.find('/', authority_begin);
|
||||
const std::string protocol = remote_url.substr(0, scheme);
|
||||
const std::string host = path_begin == std::string::npos
|
||||
? remote_url.substr(authority_begin)
|
||||
: remote_url.substr(authority_begin, path_begin - authority_begin);
|
||||
const std::string path = path_begin == std::string::npos ? "/" : remote_url.substr(path_begin);
|
||||
std::ostringstream input;
|
||||
input << "protocol=" << protocol << "\n";
|
||||
input << "host=" << host << "\n";
|
||||
input << "path=" << path << "\n";
|
||||
input << "username=" << username << "\n";
|
||||
input << "password=" << password << "\n\n";
|
||||
std::string output;
|
||||
std::string payload = input.str();
|
||||
return captureGit(repository, {"credential", "approve"}, output, error, &payload);
|
||||
}
|
||||
|
||||
bool GitManager::isAuthenticationFailure(std::string_view message)
|
||||
{
|
||||
std::string lowered(message);
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char value)
|
||||
{ return static_cast<char>(std::tolower(value)); });
|
||||
return lowered.find(" 403") != std::string::npos ||
|
||||
lowered.find(" 401") != std::string::npos ||
|
||||
lowered.find("forbidden") != std::string::npos ||
|
||||
lowered.find("authentication failed") != std::string::npos ||
|
||||
lowered.find("access denied") != std::string::npos ||
|
||||
lowered.find("could not read username") != std::string::npos ||
|
||||
lowered.find("terminal prompts disabled") != std::string::npos;
|
||||
}
|
||||
|
||||
bool GitManager::applyPatch(RepositoryView &repository, const std::string &patch, bool cached,
|
||||
bool reverse, std::string &error)
|
||||
{
|
||||
@@ -935,22 +1098,24 @@ bool GitManager::applyPatch(RepositoryView &repository, const std::string &patch
|
||||
return applied;
|
||||
}
|
||||
|
||||
bool GitManager::fetch(RepositoryView &repository, const std::string &remote, std::string &error)
|
||||
bool GitManager::fetch(RepositoryView &repository, const std::string &remote, std::string &error,
|
||||
const std::optional<GitAuthOverride> &auth)
|
||||
{
|
||||
if (!prepareCredentials(repository, error))
|
||||
return false;
|
||||
const std::vector<std::string> args = remote.empty()
|
||||
? std::vector<std::string>{"fetch", "--all", "--prune"}
|
||||
: std::vector<std::string>{"fetch", remote, "--prune"};
|
||||
return runGit(repository, args, "Fetch complete", error);
|
||||
return runGit(repository, args, "Fetch complete", error, true, auth);
|
||||
}
|
||||
|
||||
bool GitManager::pull(RepositoryView &repository, int mode, std::string &error)
|
||||
bool GitManager::pull(RepositoryView &repository, int mode, std::string &error,
|
||||
const std::optional<GitAuthOverride> &auth)
|
||||
{
|
||||
if (!prepareCredentials(repository, error))
|
||||
return false;
|
||||
if (mode == 0)
|
||||
return fetch(repository, {}, error);
|
||||
return fetch(repository, {}, error, auth);
|
||||
std::vector<std::string> args{"pull"};
|
||||
if (mode == 1)
|
||||
args.push_back("--ff");
|
||||
@@ -958,26 +1123,25 @@ bool GitManager::pull(RepositoryView &repository, int mode, std::string &error)
|
||||
args.push_back("--ff-only");
|
||||
else if (mode == 3)
|
||||
args.push_back("--rebase");
|
||||
return runGit(repository, args, "Pull complete", error);
|
||||
return runGit(repository, args, "Pull complete", error, true, auth);
|
||||
}
|
||||
|
||||
bool GitManager::push(RepositoryView &repository, std::string &error)
|
||||
bool GitManager::push(RepositoryView &repository, std::string &error,
|
||||
const std::optional<GitAuthOverride> &auth)
|
||||
{
|
||||
if (!prepareCredentials(repository, error))
|
||||
return false;
|
||||
if (runGit(repository, {"push"}, "Push complete", error))
|
||||
if (runGit(repository, {"push"}, "Push complete", error, true, auth))
|
||||
return true;
|
||||
if (repository.remotes.empty())
|
||||
const std::string remote = preferredRemoteName(repository.repo);
|
||||
if (remote.empty())
|
||||
return false;
|
||||
const std::string remote = std::find(repository.remotes.begin(), repository.remotes.end(), "origin") !=
|
||||
repository.remotes.end()
|
||||
? "origin"
|
||||
: repository.remotes.front();
|
||||
return runGit(repository, {"push", "--set-upstream", remote, repository.branch},
|
||||
"Push complete; upstream configured", error);
|
||||
"Push complete; upstream configured", error, true, auth);
|
||||
}
|
||||
|
||||
bool GitManager::pushBranch(RepositoryView &repository, const std::string &branch, std::string &error)
|
||||
bool GitManager::pushBranch(RepositoryView &repository, const std::string &branch, std::string &error,
|
||||
const std::optional<GitAuthOverride> &auth)
|
||||
{
|
||||
if (!prepareCredentials(repository, error))
|
||||
return false;
|
||||
@@ -1009,20 +1173,16 @@ bool GitManager::pushBranch(RepositoryView &repository, const std::string &branc
|
||||
|
||||
if (!remote_name.empty() && !remote_branch_name.empty())
|
||||
return runGit(repository, {"push", remote_name, branch + ":" + remote_branch_name},
|
||||
"Push complete", error);
|
||||
"Push complete", error, true, auth);
|
||||
|
||||
if (repository.remotes.empty())
|
||||
const std::string remote = preferredRemoteName(repository.repo);
|
||||
if (remote.empty())
|
||||
{
|
||||
error = "No remote is configured for this repository";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string remote = std::find(repository.remotes.begin(), repository.remotes.end(), "origin") !=
|
||||
repository.remotes.end()
|
||||
? "origin"
|
||||
: repository.remotes.front();
|
||||
return runGit(repository, {"push", "--set-upstream", remote, branch + ":" + branch},
|
||||
"Push complete; upstream configured", error);
|
||||
"Push complete; upstream configured", error, true, auth);
|
||||
}
|
||||
|
||||
bool GitManager::stash(RepositoryView &repository, std::string &error)
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "models/repository.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class GitManager
|
||||
{
|
||||
public:
|
||||
#include "models/repository.h"
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct GitAuthOverride
|
||||
{
|
||||
std::string remote_url;
|
||||
std::string username;
|
||||
std::string password;
|
||||
};
|
||||
|
||||
class GitManager
|
||||
{
|
||||
public:
|
||||
GitManager();
|
||||
~GitManager();
|
||||
|
||||
@@ -21,12 +29,16 @@ public:
|
||||
bool checkoutBranch(RepositoryView &repository, const std::string &branch, std::string &error);
|
||||
bool mergeBranch(RepositoryView &repository, const std::string &source_branch,
|
||||
const std::string &target_branch, std::string &error);
|
||||
bool fetch(RepositoryView &repository, const std::string &remote, std::string &error);
|
||||
bool pull(RepositoryView &repository, int mode, std::string &error);
|
||||
bool push(RepositoryView &repository, std::string &error);
|
||||
bool pushBranch(RepositoryView &repository, const std::string &branch, std::string &error);
|
||||
bool stash(RepositoryView &repository, std::string &error);
|
||||
bool popStash(RepositoryView &repository, std::string &error);
|
||||
bool fetch(RepositoryView &repository, const std::string &remote, std::string &error,
|
||||
const std::optional<GitAuthOverride> &auth = std::nullopt);
|
||||
bool pull(RepositoryView &repository, int mode, std::string &error,
|
||||
const std::optional<GitAuthOverride> &auth = std::nullopt);
|
||||
bool push(RepositoryView &repository, std::string &error,
|
||||
const std::optional<GitAuthOverride> &auth = std::nullopt);
|
||||
bool pushBranch(RepositoryView &repository, const std::string &branch, std::string &error,
|
||||
const std::optional<GitAuthOverride> &auth = std::nullopt);
|
||||
bool stash(RepositoryView &repository, std::string &error);
|
||||
bool popStash(RepositoryView &repository, std::string &error);
|
||||
bool undoCommit(RepositoryView &repository, std::string &error);
|
||||
bool redoCommit(RepositoryView &repository, std::string &error);
|
||||
bool stageAll(RepositoryView &repository, std::string &error);
|
||||
@@ -50,13 +62,19 @@ public:
|
||||
bool updateSubmodule(RepositoryView &repository, const std::string &name, std::string &error);
|
||||
std::string worktreePath(RepositoryView &repository, const std::string &name, std::string &error);
|
||||
bool loadCommitChanges(RepositoryView &repository, int commit_index, std::string &error);
|
||||
bool loadCommitFiles(RepositoryView &repository, int commit_index, std::string &error);
|
||||
bool captureGit(RepositoryView &repository, const std::vector<std::string> &arguments,
|
||||
std::string &output, std::string &error);
|
||||
bool applyPatch(RepositoryView &repository, const std::string &patch, bool cached,
|
||||
bool reverse, std::string &error);
|
||||
|
||||
private:
|
||||
bool loadCommitFiles(RepositoryView &repository, int commit_index, std::string &error);
|
||||
bool captureGit(RepositoryView &repository, const std::vector<std::string> &arguments,
|
||||
std::string &output, std::string &error,
|
||||
const std::string *stdin_data = nullptr);
|
||||
bool applyPatch(RepositoryView &repository, const std::string &patch, bool cached,
|
||||
bool reverse, std::string &error);
|
||||
std::string remoteUrl(RepositoryView &repository, const std::string &remote_name, std::string &error);
|
||||
bool storeCredential(RepositoryView &repository, const std::string &remote_url,
|
||||
const std::string &username, const std::string &password,
|
||||
std::string &error);
|
||||
static bool isAuthenticationFailure(std::string_view message);
|
||||
|
||||
private:
|
||||
static std::string lastError(const char *fallback);
|
||||
static std::string formatTime(git_time_t value, int offset_minutes);
|
||||
void loadToolbarHistoryActions(RepositoryView &repository);
|
||||
@@ -65,8 +83,9 @@ private:
|
||||
void loadRefBadges(RepositoryView &repository);
|
||||
void computeGraphLanes(RepositoryView &repository);
|
||||
bool loadRepositoryData(RepositoryView &repository, std::string &error);
|
||||
void loadWorkingTree(RepositoryView &repository);
|
||||
bool prepareCredentials(RepositoryView &repository, std::string &error);
|
||||
bool runGit(RepositoryView &repository, const std::vector<std::string> &arguments,
|
||||
const char *success_message, std::string &message, bool reload = true);
|
||||
};
|
||||
void loadWorkingTree(RepositoryView &repository);
|
||||
bool prepareCredentials(RepositoryView &repository, std::string &error);
|
||||
bool runGit(RepositoryView &repository, const std::vector<std::string> &arguments,
|
||||
const char *success_message, std::string &message, bool reload = true,
|
||||
const std::optional<GitAuthOverride> &auth = std::nullopt);
|
||||
};
|
||||
|
||||
@@ -10,8 +10,15 @@ extern "C"
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <wincrypt.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
std::filesystem::path roaming_directory()
|
||||
@@ -28,6 +35,89 @@ namespace
|
||||
const ikv_node_t *value = object ? ikv_object_get(object, key) : nullptr;
|
||||
return value && ikv_node_type(value) == type ? value : nullptr;
|
||||
}
|
||||
|
||||
std::optional<std::pair<std::string, std::string>> splitCredentialPayload(const std::string &payload)
|
||||
{
|
||||
const size_t separator = payload.find('\n');
|
||||
if (separator == std::string::npos)
|
||||
return std::nullopt;
|
||||
return std::pair<std::string, std::string>{
|
||||
payload.substr(0, separator),
|
||||
payload.substr(separator + 1)};
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string hexEncode(const std::string &value)
|
||||
{
|
||||
static constexpr char digits[] = "0123456789abcdef";
|
||||
std::string encoded;
|
||||
encoded.reserve(value.size() * 2);
|
||||
for (const unsigned char byte : value)
|
||||
{
|
||||
encoded.push_back(digits[(byte >> 4) & 0x0F]);
|
||||
encoded.push_back(digits[byte & 0x0F]);
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
std::optional<std::string> hexDecode(const std::string &value)
|
||||
{
|
||||
if (value.size() % 2 != 0)
|
||||
return std::nullopt;
|
||||
auto decode_nibble = [](char character) -> int
|
||||
{
|
||||
if (character >= '0' && character <= '9')
|
||||
return character - '0';
|
||||
if (character >= 'a' && character <= 'f')
|
||||
return 10 + (character - 'a');
|
||||
if (character >= 'A' && character <= 'F')
|
||||
return 10 + (character - 'A');
|
||||
return -1;
|
||||
};
|
||||
std::string decoded;
|
||||
decoded.reserve(value.size() / 2);
|
||||
for (size_t index = 0; index < value.size(); index += 2)
|
||||
{
|
||||
const int high = decode_nibble(value[index]);
|
||||
const int low = decode_nibble(value[index + 1]);
|
||||
if (high < 0 || low < 0)
|
||||
return std::nullopt;
|
||||
decoded.push_back(static_cast<char>((high << 4) | low));
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
std::optional<std::string> protectCredentialString(const std::string &value)
|
||||
{
|
||||
DATA_BLOB input{};
|
||||
input.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(value.data()));
|
||||
input.cbData = static_cast<DWORD>(value.size());
|
||||
DATA_BLOB output{};
|
||||
if (!CryptProtectData(&input, L"Gitree Git Credential", nullptr, nullptr, nullptr,
|
||||
CRYPTPROTECT_UI_FORBIDDEN, &output))
|
||||
return std::nullopt;
|
||||
std::string protected_value(reinterpret_cast<const char *>(output.pbData), output.cbData);
|
||||
LocalFree(output.pbData);
|
||||
return hexEncode(protected_value);
|
||||
}
|
||||
|
||||
std::optional<std::string> unprotectCredentialString(const std::string &value)
|
||||
{
|
||||
const auto binary = hexDecode(value);
|
||||
if (!binary)
|
||||
return std::nullopt;
|
||||
DATA_BLOB input{};
|
||||
input.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(binary->data()));
|
||||
input.cbData = static_cast<DWORD>(binary->size());
|
||||
DATA_BLOB output{};
|
||||
if (!CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr,
|
||||
CRYPTPROTECT_UI_FORBIDDEN, &output))
|
||||
return std::nullopt;
|
||||
std::string plain(reinterpret_cast<const char *>(output.pbData), output.cbData);
|
||||
LocalFree(output.pbData);
|
||||
return plain;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
UserData::UserData()
|
||||
@@ -42,6 +132,16 @@ UserData::~UserData()
|
||||
save();
|
||||
}
|
||||
|
||||
std::string UserData::credentialScope(const std::string &remote_url)
|
||||
{
|
||||
const size_t scheme = remote_url.find("://");
|
||||
if (scheme == std::string::npos)
|
||||
return remote_url;
|
||||
const size_t authority_begin = scheme + 3;
|
||||
const size_t authority_end = remote_url.find_first_of("/?#", authority_begin);
|
||||
return remote_url.substr(0, authority_end == std::string::npos ? remote_url.size() : authority_end);
|
||||
}
|
||||
|
||||
std::filesystem::path UserData::dataPath() const
|
||||
{
|
||||
return directory_ / "user_data.ikv2b";
|
||||
@@ -126,6 +226,24 @@ void UserData::load()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const ikv_node_t *credentials = object_value(root, "credentials", IKV_ARRAY))
|
||||
{
|
||||
for (uint32_t index = 0; index < ikv_array_size(credentials); ++index)
|
||||
{
|
||||
const ikv_node_t *entry = ikv_array_get(credentials, index);
|
||||
if (ikv_node_type(entry) != IKV_OBJECT)
|
||||
continue;
|
||||
const ikv_node_t *scope = object_value(entry, "scope", IKV_STRING);
|
||||
const ikv_node_t *secret = object_value(entry, "secret", IKV_STRING);
|
||||
if (!scope || !secret)
|
||||
continue;
|
||||
const char *scope_value = ikv_as_string(scope);
|
||||
const char *secret_value = ikv_as_string(secret);
|
||||
if (!scope_value || !secret_value || !*scope_value || !*secret_value)
|
||||
continue;
|
||||
encrypted_credentials_[scope_value] = secret_value;
|
||||
}
|
||||
}
|
||||
ikv_free(root);
|
||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||
@@ -212,6 +330,24 @@ void UserData::load()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const ikv_node_t *credentials = object_value(root, "credentials", IKV_ARRAY))
|
||||
{
|
||||
for (uint32_t index = 0; index < ikv_array_size(credentials); ++index)
|
||||
{
|
||||
const ikv_node_t *entry = ikv_array_get(credentials, index);
|
||||
if (ikv_node_type(entry) != IKV_OBJECT)
|
||||
continue;
|
||||
const ikv_node_t *scope = object_value(entry, "scope", IKV_STRING);
|
||||
const ikv_node_t *secret = object_value(entry, "secret", IKV_STRING);
|
||||
if (!scope || !secret)
|
||||
continue;
|
||||
const char *scope_value = ikv_as_string(scope);
|
||||
const char *secret_value = ikv_as_string(secret);
|
||||
if (!scope_value || !secret_value || !*scope_value || !*secret_value)
|
||||
continue;
|
||||
encrypted_credentials_[scope_value] = secret_value;
|
||||
}
|
||||
}
|
||||
ikv_free(root);
|
||||
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
||||
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
||||
@@ -280,6 +416,24 @@ void UserData::load()
|
||||
save();
|
||||
}
|
||||
|
||||
std::optional<SavedGitCredential> UserData::remoteCredential(const std::string &remote_url) const
|
||||
{
|
||||
const auto iterator = encrypted_credentials_.find(credentialScope(remote_url));
|
||||
if (iterator == encrypted_credentials_.end())
|
||||
return std::nullopt;
|
||||
#ifdef _WIN32
|
||||
const auto plain = unprotectCredentialString(iterator->second);
|
||||
if (!plain)
|
||||
return std::nullopt;
|
||||
const auto split = splitCredentialPayload(*plain);
|
||||
if (!split)
|
||||
return std::nullopt;
|
||||
return SavedGitCredential{remote_url, split->first, split->second};
|
||||
#else
|
||||
return std::nullopt;
|
||||
#endif
|
||||
}
|
||||
|
||||
void UserData::addRecentRepository(const std::string &path)
|
||||
{
|
||||
if (path.empty())
|
||||
@@ -325,6 +479,29 @@ void UserData::setRepositorySession(std::vector<std::string> paths, size_t activ
|
||||
save();
|
||||
}
|
||||
|
||||
void UserData::storeRemoteCredential(const std::string &remote_url, const std::string &username,
|
||||
const std::string &password)
|
||||
{
|
||||
const std::string scope = credentialScope(remote_url);
|
||||
if (scope.empty() || username.empty())
|
||||
return;
|
||||
#ifdef _WIN32
|
||||
const auto protected_value = protectCredentialString(username + "\n" + password);
|
||||
if (!protected_value)
|
||||
return;
|
||||
encrypted_credentials_[scope] = *protected_value;
|
||||
save();
|
||||
#else
|
||||
(void)password;
|
||||
#endif
|
||||
}
|
||||
|
||||
void UserData::clearRemoteCredential(const std::string &remote_url)
|
||||
{
|
||||
encrypted_credentials_.erase(credentialScope(remote_url));
|
||||
save();
|
||||
}
|
||||
|
||||
void UserData::save() const
|
||||
{
|
||||
std::filesystem::create_directories(directory_);
|
||||
@@ -362,6 +539,14 @@ void UserData::save() const
|
||||
for (const auto &path : open_repositories_)
|
||||
ikv_array_add_string(tabs, path.c_str());
|
||||
|
||||
ikv_node_t *credentials = ikv_object_add_array(root, "credentials", IKV_OBJECT);
|
||||
for (const auto &[scope, secret] : encrypted_credentials_)
|
||||
{
|
||||
ikv_node_t *entry = ikv_array_add_object(credentials);
|
||||
ikv_object_set_string(entry, "scope", scope.c_str());
|
||||
ikv_object_set_string(entry, "secret", secret.c_str());
|
||||
}
|
||||
|
||||
ikvb_write_file_version(dataPath().string().c_str(), root, IKV_VERSION_2);
|
||||
ikv_free(root);
|
||||
removeLegacyFiles();
|
||||
|
||||
@@ -2,9 +2,18 @@
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct SavedGitCredential
|
||||
{
|
||||
std::string remote_url;
|
||||
std::string username;
|
||||
std::string password;
|
||||
};
|
||||
|
||||
class UserData
|
||||
{
|
||||
public:
|
||||
@@ -24,6 +33,8 @@ public:
|
||||
[[nodiscard]] float commitTableColumnWidth(size_t index) const { return commit_table_column_widths_.at(index); }
|
||||
[[nodiscard]] int pullMode() const { return pull_mode_; }
|
||||
[[nodiscard]] int zoomPercent() const { return zoom_percent_; }
|
||||
[[nodiscard]] std::optional<SavedGitCredential> remoteCredential(const std::string &remote_url) const;
|
||||
static std::string credentialScope(const std::string &remote_url);
|
||||
|
||||
void setSidebarWidth(float width) { sidebar_width_ = width; }
|
||||
void setDetailsWidth(float width) { details_width_ = width; }
|
||||
@@ -45,6 +56,9 @@ public:
|
||||
void addRecentlyClosed(const std::string &path);
|
||||
std::string takeRecentlyClosed();
|
||||
void setRepositorySession(std::vector<std::string> paths, size_t active_repository);
|
||||
void storeRemoteCredential(const std::string &remote_url, const std::string &username,
|
||||
const std::string &password);
|
||||
void clearRemoteCredential(const std::string &remote_url);
|
||||
void save() const;
|
||||
|
||||
private:
|
||||
@@ -65,4 +79,5 @@ private:
|
||||
std::array<float, 4> commit_table_column_widths_ = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
int pull_mode_ = 1;
|
||||
int zoom_percent_ = 100;
|
||||
std::unordered_map<std::string, std::string> encrypted_credentials_;
|
||||
};
|
||||
|
||||
@@ -518,27 +518,48 @@ std::vector<MinimapSegment> buildMinimapSegments(const std::string& text, Syntax
|
||||
return segments;
|
||||
}
|
||||
|
||||
void drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& size, float scale,
|
||||
float rendered_content_height, float visible_start, float visible_height, float max_scroll_y) {
|
||||
ImU32 dimMinimapColor(ImU32 color) {
|
||||
ImVec4 rgba = ImGui::ColorConvertU32ToFloat4(color);
|
||||
rgba.x *= 0.58f;
|
||||
rgba.y *= 0.58f;
|
||||
rgba.z *= 0.58f;
|
||||
rgba.w *= 0.82f;
|
||||
return ImGui::ColorConvertFloat4ToU32(rgba);
|
||||
}
|
||||
|
||||
float minimapTotalUnits(const std::vector<MinimapEntry>& entries) {
|
||||
float total_units = 0.0f;
|
||||
for (const MinimapEntry& entry : entries) total_units += std::max(0.10f, entry.units);
|
||||
return std::max(1.0f, total_units);
|
||||
}
|
||||
|
||||
void drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& viewport_size, float scale,
|
||||
float rendered_content_height, float visible_start, float visible_height, float max_scroll_y,
|
||||
float content_height_override = 0.0f) {
|
||||
const ImVec2 minimum = ImGui::GetCursorScreenPos();
|
||||
ImGui::InvisibleButton("##code_minimap", size);
|
||||
const float total_units = minimapTotalUnits(entries);
|
||||
const float min_unit_height = scaled(0.72f, scale);
|
||||
const float ideal_unit_height = scaled(2.05f, scale);
|
||||
const float unit_height = content_height_override > 0.0f
|
||||
? std::max(min_unit_height, content_height_override / total_units)
|
||||
: ideal_unit_height;
|
||||
const float content_height = std::max(content_height_override, total_units * unit_height);
|
||||
const ImVec2 content_size{viewport_size.x, content_height};
|
||||
ImGui::InvisibleButton("##code_minimap", content_size);
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
const bool active = ImGui::IsItemActive();
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
draw->AddRectFilled(minimum, {minimum.x + size.x, minimum.y + size.y}, IM_COL32(31, 33, 39, 235), scaled(4.0f, scale));
|
||||
draw->AddRect(minimum, {minimum.x + size.x, minimum.y + size.y},
|
||||
hovered || active ? IM_COL32(82, 90, 103, 255) : IM_COL32(56, 61, 71, 255), scaled(4.0f, scale));
|
||||
draw->AddRectFilled(minimum, {minimum.x + viewport_size.x, minimum.y + content_size.y},
|
||||
IM_COL32(26, 28, 33, 188), scaled(3.0f, scale));
|
||||
draw->AddRect(minimum, {minimum.x + viewport_size.x, minimum.y + content_size.y},
|
||||
hovered || active ? IM_COL32(70, 76, 88, 180) : IM_COL32(46, 50, 58, 140), scaled(3.0f, scale));
|
||||
|
||||
if (entries.empty() || size.y <= 2.0f) return;
|
||||
if (entries.empty() || content_size.y <= 1.0f) return;
|
||||
|
||||
const float content_left = minimum.x + scaled(5.0f, scale);
|
||||
const float content_right = minimum.x + size.x - scaled(5.0f, scale);
|
||||
const float content_right = minimum.x + viewport_size.x - scaled(5.0f, scale);
|
||||
const float content_width = std::max(1.0f, content_right - content_left);
|
||||
float total_units = 0.0f;
|
||||
for (const MinimapEntry& entry : entries) total_units += std::max(0.10f, entry.units);
|
||||
total_units = std::max(1.0f, total_units);
|
||||
const float unit_height = size.y / total_units;
|
||||
const float line_height = std::max(1.0f, std::min(scaled(3.2f, scale), unit_height));
|
||||
const float line_height = std::max(scaled(0.65f, scale), std::min(scaled(2.1f, scale), unit_height));
|
||||
float y = minimum.y;
|
||||
for (const MinimapEntry& entry : entries) {
|
||||
const float entry_units = std::max(0.10f, entry.units);
|
||||
@@ -552,29 +573,29 @@ void drawMinimap(const std::vector<MinimapEntry>& entries, const ImVec2& size, f
|
||||
}
|
||||
const float width = std::max(scaled(1.0f, scale), content_width * (segment.weight / total_weight));
|
||||
draw->AddRectFilled({x, y}, {std::min(content_right, x + width), y + line_height},
|
||||
segment.color, scaled(1.0f, scale));
|
||||
dimMinimapColor(segment.color), scaled(1.0f, scale));
|
||||
x += width;
|
||||
}
|
||||
y += entry_units * unit_height;
|
||||
}
|
||||
|
||||
const float content_height = std::max(rendered_content_height, 1.0f);
|
||||
const float clamped_visible_height = std::clamp(visible_height, 0.0f, content_height);
|
||||
const float clamped_visible_start = std::clamp(visible_start, 0.0f, std::max(0.0f, content_height - clamped_visible_height));
|
||||
const float viewport_height = std::clamp(size.y * (clamped_visible_height / content_height),
|
||||
scaled(18.0f, scale), size.y);
|
||||
const float viewport_y = minimum.y + size.y * (clamped_visible_start / content_height);
|
||||
const float rendered_height = std::max(rendered_content_height, 1.0f);
|
||||
const float clamped_visible_height = std::clamp(visible_height, 0.0f, rendered_height);
|
||||
const float clamped_visible_start = std::clamp(visible_start, 0.0f, std::max(0.0f, rendered_height - clamped_visible_height));
|
||||
const float viewport_height = std::clamp(content_size.y * (clamped_visible_height / rendered_height),
|
||||
scaled(14.0f, scale), content_size.y);
|
||||
const float viewport_y = minimum.y + content_size.y * (clamped_visible_start / rendered_height);
|
||||
if ((hovered || active) && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
const float mouse_ratio = std::clamp((ImGui::GetIO().MousePos.y - minimum.y) / std::max(1.0f, size.y), 0.0f, 1.0f);
|
||||
const float target = std::clamp(mouse_ratio * content_height - clamped_visible_height * 0.5f, 0.0f, max_scroll_y);
|
||||
const float mouse_ratio = std::clamp((ImGui::GetIO().MousePos.y - minimum.y) / std::max(1.0f, content_size.y), 0.0f, 1.0f);
|
||||
const float target = std::clamp(mouse_ratio * rendered_height - clamped_visible_height * 0.5f, 0.0f, max_scroll_y);
|
||||
ImGui::SetScrollY(target);
|
||||
}
|
||||
draw->AddRectFilled({minimum.x + scaled(1.0f, scale), viewport_y},
|
||||
{minimum.x + size.x - scaled(1.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(120, 146, 198, hovered || active ? 62 : 42), scaled(2.0f, scale));
|
||||
{minimum.x + viewport_size.x - scaled(1.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(109, 129, 170, hovered || active ? 44 : 28), scaled(2.0f, scale));
|
||||
draw->AddRect({minimum.x + scaled(1.0f, scale), viewport_y},
|
||||
{minimum.x + size.x - scaled(1.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(120, 146, 198, 185), scaled(2.0f, scale));
|
||||
{minimum.x + viewport_size.x - scaled(1.0f, scale), viewport_y + viewport_height},
|
||||
IM_COL32(120, 138, 170, 120), scaled(2.0f, scale));
|
||||
}
|
||||
|
||||
void drawCodeLine(const std::string& text, SyntaxLanguage language, SyntaxState& syntax,
|
||||
@@ -965,7 +986,8 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
}
|
||||
|
||||
ImGuiWindowFlags content_flags = line_wrap_ ? ImGuiWindowFlags_None : ImGuiWindowFlags_HorizontalScrollbar;
|
||||
if (scroll_to_top_) ImGui::SetNextWindowScroll({0.0f, 0.0f});
|
||||
const bool request_scroll_to_top = scroll_to_top_;
|
||||
if (request_scroll_to_top) ImGui::SetNextWindowScroll({0.0f, 0.0f});
|
||||
ImGui::BeginChild("diff_content_main", ImVec2{-1, -1}, ImGuiChildFlags_None, content_flags);
|
||||
scroll_to_top_ = false;
|
||||
const bool use_code_font = code_font && mode_ != Mode::history;
|
||||
@@ -1160,14 +1182,28 @@ void DiffViewer::draw(RepositoryView& repository, GitManager& manager, AvatarCac
|
||||
rendered_content_height = ImGui::GetCursorPosY();
|
||||
const float max_scroll_y = ImGui::GetScrollMaxY();
|
||||
if (show_minimap) {
|
||||
const float minimap_height = std::max(scaled(40.0f, scale), child_size.y - scaled(10.0f, scale));
|
||||
const float total_units = minimapTotalUnits(minimap_entries);
|
||||
const float min_unit_height = scaled(0.72f, scale);
|
||||
const float ideal_unit_height = scaled(2.05f, scale);
|
||||
const float max_minimap_height = std::max(scaled(24.0f, scale), child_size.y - scaled(8.0f, scale));
|
||||
const float fitted_unit_height = std::clamp(max_minimap_height / total_units, min_unit_height, ideal_unit_height);
|
||||
const float minimap_content_height = total_units * fitted_unit_height;
|
||||
const float minimap_height = std::min(max_minimap_height, minimap_content_height);
|
||||
const float minimap_x = child_minimum.x + child_size.x - scrollbar_width - minimap_width - scaled(4.0f, scale);
|
||||
const float minimap_y = child_minimum.y + scaled(4.0f, scale);
|
||||
ImGui::SetCursorScreenPos({minimap_x, minimap_y});
|
||||
ImGui::PushClipRect(child_minimum, {child_minimum.x + child_size.x, child_minimum.y + child_size.y}, true);
|
||||
drawMinimap(minimap_entries, {minimap_width, minimap_height}, scale,
|
||||
rendered_content_height, main_scroll_y, main_window_height, max_scroll_y);
|
||||
ImGui::PopClipRect();
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImVec4(0.28f, 0.31f, 0.37f, 0.55f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(0.36f, 0.40f, 0.47f, 0.72f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(0.44f, 0.49f, 0.57f, 0.86f));
|
||||
if (request_scroll_to_top) ImGui::SetNextWindowScroll({0.0f, 0.0f});
|
||||
ImGui::BeginChild("diff_content_minimap", {minimap_width, minimap_height},
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove);
|
||||
drawMinimap(minimap_entries, {minimap_width - scaled(1.0f, scale), minimap_height}, scale,
|
||||
rendered_content_height, main_scroll_y, main_window_height, max_scroll_y, minimap_content_height);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor(5);
|
||||
}
|
||||
if (use_code_font) ImGui::PopFont();
|
||||
ImGui::EndChild();
|
||||
|
||||
@@ -60,9 +60,12 @@ bool g_remote_add_popup = false;
|
||||
bool g_worktree_add_popup = false;
|
||||
bool g_submodule_add_popup = false;
|
||||
bool g_discard_all_popup = false;
|
||||
bool g_git_auth_popup = false;
|
||||
std::array<char, 256> g_git_name{};
|
||||
std::array<char, 1024> g_git_value{};
|
||||
std::array<char, 1024> g_git_path{};
|
||||
std::array<char, 256> g_git_auth_username{};
|
||||
std::array<char, 256> g_git_auth_password{};
|
||||
std::array<char, 256> g_repository_filter{};
|
||||
std::array<char, 128> g_create_repository_name{};
|
||||
std::array<char, 1024> g_create_repository_parent{};
|
||||
@@ -79,6 +82,9 @@ std::string g_merge_branch_source;
|
||||
std::string g_merge_branch_target;
|
||||
bool g_merge_branch_popup = false;
|
||||
std::string g_pending_commit_jump;
|
||||
std::string g_git_auth_remote_url;
|
||||
std::string g_git_auth_error;
|
||||
bool g_git_auth_remember = true;
|
||||
RepositoryView* g_expanded_refs_repository = nullptr;
|
||||
int g_expanded_refs_commit = -1;
|
||||
RepositoryView* g_inline_branch_repository = nullptr;
|
||||
@@ -140,6 +146,8 @@ struct GitAsyncRequest {
|
||||
std::string success_notice;
|
||||
std::string focus_commit;
|
||||
std::string preserve_selected_hash;
|
||||
std::optional<GitAuthOverride> auth_override;
|
||||
bool used_saved_app_credential = false;
|
||||
int pull_mode = 1;
|
||||
bool surface_errors = false;
|
||||
};
|
||||
@@ -152,10 +160,14 @@ struct GitAsyncResult {
|
||||
std::string notice;
|
||||
std::string focus_commit;
|
||||
std::string preserve_selected_hash;
|
||||
GitAsyncRequest retry_request;
|
||||
bool auth_required = false;
|
||||
bool surface_errors = false;
|
||||
std::unique_ptr<RepositoryView> snapshot;
|
||||
};
|
||||
|
||||
std::optional<GitAsyncRequest> g_git_auth_retry_request;
|
||||
|
||||
std::future<GitAsyncResult> g_git_async_future;
|
||||
|
||||
enum class ToastKind { info, success, warning, error };
|
||||
@@ -387,6 +399,37 @@ void mark_repository_refreshed(RepositoryView& repository) {
|
||||
repository.pending_background_refresh = false;
|
||||
}
|
||||
|
||||
std::string preferred_auth_remote_name(const RepositoryView& repository, const std::string& remote_hint = {}) {
|
||||
if (!remote_hint.empty()) return remote_hint;
|
||||
const auto origin = std::find(repository.remotes.begin(), repository.remotes.end(), "origin");
|
||||
if (origin != repository.remotes.end()) return *origin;
|
||||
return repository.remotes.empty() ? std::string{} : repository.remotes.front();
|
||||
}
|
||||
|
||||
void attach_saved_auth_if_available(RepositoryView& repository, GitAsyncRequest& request) {
|
||||
if (!g_user_data || !g_git_manager) return;
|
||||
const std::string remote_name = preferred_auth_remote_name(repository, request.remote);
|
||||
if (remote_name.empty()) return;
|
||||
std::string remote_error;
|
||||
const std::string remote_url = g_git_manager->remoteUrl(repository, remote_name, remote_error);
|
||||
if (remote_url.empty()) return;
|
||||
const auto saved = g_user_data->remoteCredential(remote_url);
|
||||
if (!saved) return;
|
||||
request.auth_override = GitAuthOverride{remote_url, saved->username, saved->password};
|
||||
request.used_saved_app_credential = true;
|
||||
}
|
||||
|
||||
void attach_auth_remote_metadata(RepositoryView& repository, GitAsyncRequest& request) {
|
||||
if (!g_git_manager) return;
|
||||
const std::string remote_name = preferred_auth_remote_name(repository, request.remote);
|
||||
if (remote_name.empty()) return;
|
||||
std::string remote_error;
|
||||
const std::string remote_url = g_git_manager->remoteUrl(repository, remote_name, remote_error);
|
||||
if (remote_url.empty()) return;
|
||||
if (request.auth_override) request.auth_override->remote_url = remote_url;
|
||||
else request.auth_override = GitAuthOverride{remote_url, {}, {}};
|
||||
}
|
||||
|
||||
GitAsyncResult execute_git_async_request(const GitAsyncRequest& request) {
|
||||
GitAsyncResult result;
|
||||
result.operation = request.operation;
|
||||
@@ -395,6 +438,7 @@ GitAsyncResult execute_git_async_request(const GitAsyncRequest& request) {
|
||||
result.focus_commit = request.focus_commit;
|
||||
result.preserve_selected_hash = request.preserve_selected_hash;
|
||||
result.surface_errors = request.surface_errors;
|
||||
result.retry_request = request;
|
||||
|
||||
GitManager manager;
|
||||
RepositoryView repository;
|
||||
@@ -413,19 +457,19 @@ GitAsyncResult execute_git_async_request(const GitAsyncRequest& request) {
|
||||
break;
|
||||
}
|
||||
case GitAsyncOperation::pull:
|
||||
action_ok = manager.pull(repository, request.pull_mode, result.notice);
|
||||
action_ok = manager.pull(repository, request.pull_mode, result.notice, request.auth_override);
|
||||
break;
|
||||
case GitAsyncOperation::push:
|
||||
action_ok = manager.push(repository, result.notice);
|
||||
action_ok = manager.push(repository, result.notice, request.auth_override);
|
||||
break;
|
||||
case GitAsyncOperation::checkout_branch:
|
||||
action_ok = manager.checkoutBranch(repository, request.branch, result.notice);
|
||||
break;
|
||||
case GitAsyncOperation::push_branch:
|
||||
action_ok = manager.pushBranch(repository, request.branch, result.notice);
|
||||
action_ok = manager.pushBranch(repository, request.branch, result.notice, request.auth_override);
|
||||
break;
|
||||
case GitAsyncOperation::fetch:
|
||||
action_ok = manager.fetch(repository, request.remote, result.notice);
|
||||
action_ok = manager.fetch(repository, request.remote, result.notice, request.auth_override);
|
||||
break;
|
||||
case GitAsyncOperation::stash:
|
||||
action_ok = manager.stash(repository, result.notice);
|
||||
@@ -434,7 +478,15 @@ GitAsyncResult execute_git_async_request(const GitAsyncRequest& request) {
|
||||
action_ok = manager.popStash(repository, result.notice);
|
||||
break;
|
||||
}
|
||||
if (!action_ok) return result;
|
||||
if (!action_ok) {
|
||||
const bool auth_operation = request.operation == GitAsyncOperation::fetch ||
|
||||
request.operation == GitAsyncOperation::pull ||
|
||||
request.operation == GitAsyncOperation::push ||
|
||||
request.operation == GitAsyncOperation::push_branch;
|
||||
if (auth_operation && GitManager::isAuthenticationFailure(result.notice))
|
||||
result.auth_required = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
auto snapshot = std::make_unique<RepositoryView>();
|
||||
transfer_repository_state(repository, *snapshot);
|
||||
@@ -448,6 +500,25 @@ void apply_git_async_result(GitAsyncResult result) {
|
||||
g_running_toolbar_action = ToolbarActionRequest::none;
|
||||
if (result.operation == GitAsyncOperation::checkout_branch)
|
||||
g_running_branch_checkout.clear();
|
||||
if (result.auth_required) {
|
||||
g_git_auth_retry_request = result.retry_request;
|
||||
g_git_auth_error = result.notice;
|
||||
g_git_auth_remote_url = result.retry_request.auth_override
|
||||
? result.retry_request.auth_override->remote_url
|
||||
: g_git_auth_remote_url;
|
||||
if (g_user_data && result.retry_request.used_saved_app_credential && !g_git_auth_remote_url.empty())
|
||||
g_user_data->clearRemoteCredential(g_git_auth_remote_url);
|
||||
if (result.retry_request.auth_override) {
|
||||
std::snprintf(g_git_auth_username.data(), g_git_auth_username.size(), "%s",
|
||||
result.retry_request.auth_override->username.c_str());
|
||||
std::fill(g_git_auth_password.begin(), g_git_auth_password.end(), '\0');
|
||||
} else {
|
||||
std::fill(g_git_auth_username.begin(), g_git_auth_username.end(), '\0');
|
||||
std::fill(g_git_auth_password.begin(), g_git_auth_password.end(), '\0');
|
||||
}
|
||||
g_git_auth_popup = true;
|
||||
return;
|
||||
}
|
||||
RepositoryView* target = find_repository_tab(result.tab);
|
||||
if (!target || target->path != result.repo_path) {
|
||||
if ((result.success || result.surface_errors) && !result.notice.empty()) g_notice = result.notice;
|
||||
@@ -642,6 +713,8 @@ bool start_push_branch_async(RepositoryView& repository, const std::string& bran
|
||||
request.repo_path = repository.path;
|
||||
request.branch = branch;
|
||||
request.preserve_selected_hash = selected_commit_hash(repository);
|
||||
attach_auth_remote_metadata(repository, request);
|
||||
attach_saved_auth_if_available(repository, request);
|
||||
return start_git_async_request(std::move(request));
|
||||
}
|
||||
|
||||
@@ -653,6 +726,8 @@ bool start_fetch_async(RepositoryView& repository, const std::string& remote, bo
|
||||
request.remote = remote;
|
||||
request.surface_errors = surface_errors;
|
||||
request.preserve_selected_hash = selected_commit_hash(repository);
|
||||
attach_auth_remote_metadata(repository, request);
|
||||
attach_saved_auth_if_available(repository, request);
|
||||
return start_git_async_request(std::move(request), "A Git operation is already running");
|
||||
}
|
||||
|
||||
@@ -669,6 +744,8 @@ bool start_toolbar_action_async(RepositoryView& repository, ToolbarActionRequest
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
attach_auth_remote_metadata(repository, request);
|
||||
attach_saved_auth_if_available(repository, request);
|
||||
return start_git_async_request(std::move(request));
|
||||
}
|
||||
|
||||
@@ -2856,8 +2933,21 @@ void draw_details(float width) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
void draw_toolbar_spinner(ImDrawList* draw, const ImVec2& center, float radius, ImU32 color) {
|
||||
const float start = static_cast<float>(ImGui::GetTime() * 6.0);
|
||||
const float span = 4.4f;
|
||||
constexpr int segments = 24;
|
||||
draw->PathClear();
|
||||
for (int index = 0; index < segments; ++index) {
|
||||
const float t = static_cast<float>(index) / static_cast<float>(segments - 1);
|
||||
const float angle = start + span * t;
|
||||
draw->PathLineTo({center.x + std::cos(angle) * radius, center.y + std::sin(angle) * radius});
|
||||
}
|
||||
draw->PathStroke(color, false, ui(2.0f));
|
||||
}
|
||||
|
||||
bool toolbar_action(const char* id, const char* label, const char* icon, const char* tooltip,
|
||||
bool enabled = true, bool dropdown = false, float logical_width = 58.0f,
|
||||
bool enabled = true, bool dropdown = false, float logical_width = 58.0f, bool spinning = false,
|
||||
bool* dropdown_clicked = nullptr) {
|
||||
ImGui::PushID(id);
|
||||
if (!enabled) ImGui::BeginDisabled();
|
||||
@@ -2884,7 +2974,15 @@ bool toolbar_action(const char* id, const char* label, const char* icon, const c
|
||||
{minimum.x + (maximum.x - minimum.x - label_size.x) * 0.5f, minimum.y + ui(4.0f)},
|
||||
text_color, label);
|
||||
const float icon_x = minimum.x + (maximum.x - minimum.x - icon_size.x) * 0.5f - (dropdown ? ui(4.0f) : 0.0f);
|
||||
draw->AddText(g_regular_font, icon_font_size, {icon_x, minimum.y + ui(19.0f)}, text_color, icon);
|
||||
if (spinning) {
|
||||
const ImVec2 center{
|
||||
minimum.x + (maximum.x - minimum.x) * 0.5f - (dropdown ? ui(4.0f) : 0.0f),
|
||||
minimum.y + ui(27.0f)
|
||||
};
|
||||
draw_toolbar_spinner(draw, center, ui(7.0f), text_color);
|
||||
} else {
|
||||
draw->AddText(g_regular_font, icon_font_size, {icon_x, minimum.y + ui(19.0f)}, text_color, icon);
|
||||
}
|
||||
if (dropdown)
|
||||
draw->AddText({icon_x + icon_size.x + ui(7.0f), minimum.y + ui(21.0f)}, text_color, ICON_TB_CARET_DOWN);
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
|
||||
@@ -3184,8 +3282,75 @@ void draw_licenses_popup() {
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void draw_git_credentials_popup() {
|
||||
if (g_git_auth_popup) {
|
||||
ImGui::OpenPopup("Git credentials required");
|
||||
g_git_auth_popup = false;
|
||||
}
|
||||
if (!ImGui::BeginPopupModal("Git credentials required", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return;
|
||||
|
||||
ImGui::TextWrapped("Authentication is required for this remote.");
|
||||
if (!g_git_auth_remote_url.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Remote");
|
||||
ImGui::TextWrapped("%s", g_git_auth_remote_url.c_str());
|
||||
}
|
||||
if (!g_git_auth_error.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.47f, 0.47f, 1.0f), "%s", g_git_auth_error.c_str());
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::SetNextItemWidth(ui(320.0f));
|
||||
ImGui::InputText("Username", g_git_auth_username.data(), g_git_auth_username.size());
|
||||
ImGui::SetNextItemWidth(ui(320.0f));
|
||||
ImGui::InputText("Password", g_git_auth_password.data(), g_git_auth_password.size(),
|
||||
ImGuiInputTextFlags_Password);
|
||||
ImGui::Checkbox("Remember securely", &g_git_auth_remember);
|
||||
|
||||
const bool can_retry = g_git_auth_retry_request.has_value() && g_git_auth_username[0] != '\0';
|
||||
if (!can_retry) ImGui::BeginDisabled();
|
||||
if (ImGui::Button("Retry", {ui(110.0f), 0.0f})) {
|
||||
GitAsyncRequest request = *g_git_auth_retry_request;
|
||||
request.auth_override = GitAuthOverride{
|
||||
g_git_auth_remote_url,
|
||||
g_git_auth_username.data(),
|
||||
g_git_auth_password.data(),
|
||||
};
|
||||
request.used_saved_app_credential = false;
|
||||
if (g_git_auth_remember && g_user_data && !g_git_auth_remote_url.empty()) {
|
||||
RepositoryView* target = find_repository_tab(request.tab);
|
||||
std::string store_error;
|
||||
bool stored_in_helper = false;
|
||||
if (target && g_git_manager)
|
||||
stored_in_helper = g_git_manager->storeCredential(*target, g_git_auth_remote_url,
|
||||
request.auth_override->username, request.auth_override->password, store_error);
|
||||
if (stored_in_helper) g_user_data->clearRemoteCredential(g_git_auth_remote_url);
|
||||
else g_user_data->storeRemoteCredential(g_git_auth_remote_url,
|
||||
request.auth_override->username, request.auth_override->password);
|
||||
} else if (g_user_data && !g_git_auth_remote_url.empty()) {
|
||||
g_user_data->clearRemoteCredential(g_git_auth_remote_url);
|
||||
}
|
||||
if (start_git_async_request(std::move(request))) {
|
||||
std::fill(g_git_auth_password.begin(), g_git_auth_password.end(), '\0');
|
||||
g_git_auth_retry_request.reset();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
if (!can_retry) ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", {ui(110.0f), 0.0f})) {
|
||||
g_git_auth_retry_request.reset();
|
||||
std::fill(g_git_auth_password.begin(), g_git_auth_password.end(), '\0');
|
||||
g_notice = "Authentication cancelled";
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void draw_git_action_popups() {
|
||||
static bool checkout_new_branch = true;
|
||||
draw_git_credentials_popup();
|
||||
if (g_merge_branch_popup) {
|
||||
g_merge_branch_popup = false;
|
||||
ImGui::OpenPopup("Merge branches");
|
||||
@@ -3963,14 +4128,14 @@ void draw_app() {
|
||||
const bool pull_running = g_running_toolbar_action == ToolbarActionRequest::pull;
|
||||
if (toolbar_action("pull", "Pull", pull_running ? ICON_TB_ROTATE_RIGHT : ICON_TB_DOWNLOAD,
|
||||
pull_running ? "Pull in progress..." : pull_mode_name(g_user_data->pullMode()),
|
||||
!toolbar_busy, true, 58, &pull_dropdown_clicked))
|
||||
!toolbar_busy, true, 58, pull_running, &pull_dropdown_clicked))
|
||||
g_pending_toolbar_action = ToolbarActionRequest::pull;
|
||||
if (pull_dropdown_clicked) ImGui::OpenPopup("pull_options");
|
||||
draw_pull_options();
|
||||
ImGui::SameLine(0, action_spacing);
|
||||
const bool push_running = g_running_toolbar_action == ToolbarActionRequest::push;
|
||||
if (toolbar_action("push", "Push", push_running ? ICON_TB_ROTATE_RIGHT : ICON_TB_UPLOAD,
|
||||
push_running ? "Push in progress..." : "Push to remote", !toolbar_busy, false, 58))
|
||||
push_running ? "Push in progress..." : "Push to remote", !toolbar_busy, false, 58, push_running))
|
||||
g_pending_toolbar_action = ToolbarActionRequest::push;
|
||||
ImGui::SameLine(0, action_spacing);
|
||||
if (toolbar_action("branch_action", "Branch", ICON_TB_CODE_BRANCH, "Create branch", true, false, 64)) {
|
||||
|
||||
Reference in New Issue
Block a user