Not sure
Some checks failed
Build Releases / Build Linux x64 DEB (push) Failing after 1m14s
Build Releases / Build Windows x64 (push) Failing after 1m20s
Build Releases / Create Release (push) Has been skipped

This commit is contained in:
2026-06-19 15:12:11 -05:00
parent 76634c3fd3
commit 1114c05cb9
7 changed files with 677 additions and 97 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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);
};

View File

@@ -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();

View File

@@ -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_;
};

View File

@@ -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();

View File

@@ -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)) {