Фаза 1: мастер первого запуска — SettingsStore, GitManager (auto-init+коммиты), SyncManager (проверка соединений+ErrorCatalog), WelcomeWizard/ProviderSetupGuide; тесты smoke/git/vault (12/12)
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
#include "git/GitManager.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
namespace {
|
||||
|
||||
QString gitErr()
|
||||
{
|
||||
const git_error *e = git_error_last();
|
||||
return e && e->message ? QString::fromUtf8(e->message) : QStringLiteral("неизвестная ошибка git");
|
||||
}
|
||||
|
||||
struct CommitInfo {
|
||||
git_signature *author = nullptr;
|
||||
git_index *index = nullptr;
|
||||
git_repository *repo = nullptr;
|
||||
git_oid treeId{};
|
||||
git_oid commitId{};
|
||||
~CommitInfo()
|
||||
{
|
||||
if (author) git_signature_free(author);
|
||||
if (index) git_index_free(index);
|
||||
if (repo) git_repository_free(repo);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
GitManager::GitManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
if (!m_gitInitialized) {
|
||||
git_libgit2_init();
|
||||
m_gitInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
QString GitManager::repoExists(const QString &path)
|
||||
{
|
||||
git_repository *repo = nullptr;
|
||||
if (git_repository_open(&repo, path.toUtf8().constData()) < 0)
|
||||
return QString();
|
||||
git_repository_free(repo);
|
||||
return path;
|
||||
}
|
||||
|
||||
void GitManager::setRepoIdentity(git_repository *repo)
|
||||
{
|
||||
git_config *cfg = nullptr;
|
||||
if (git_repository_config(&cfg, repo) < 0)
|
||||
return;
|
||||
|
||||
auto setIfMissing = [cfg](const char *key, const char *value) {
|
||||
git_buf buf{};
|
||||
if (git_config_get_string_buf(&buf, cfg, key) < 0)
|
||||
git_config_set_string(cfg, key, value);
|
||||
git_buf_dispose(&buf);
|
||||
};
|
||||
setIfMissing("user.name", "Flora");
|
||||
setIfMissing("user.email", "flora@localhost");
|
||||
git_config_free(cfg);
|
||||
}
|
||||
|
||||
void GitManager::setLastError(const QString &msg)
|
||||
{
|
||||
if (m_lastError == msg)
|
||||
return;
|
||||
m_lastError = msg;
|
||||
emit lastErrorChanged();
|
||||
}
|
||||
|
||||
bool GitManager::initRepo(const QString &path)
|
||||
{
|
||||
const QByteArray p = path.toUtf8();
|
||||
git_repository *repo = nullptr;
|
||||
|
||||
if (git_repository_open(&repo, p.constData()) == 0) {
|
||||
git_repository_free(repo);
|
||||
return true; // .git уже есть
|
||||
}
|
||||
|
||||
git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
|
||||
opts.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_MKDIR;
|
||||
opts.initial_head = "main";
|
||||
|
||||
const int rc = git_repository_init_ext(&repo, p.constData(), &opts);
|
||||
if (rc < 0) {
|
||||
setLastError(QStringLiteral("Не удалось создать git-репозиторий: %1")
|
||||
.arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
setRepoIdentity(repo);
|
||||
git_repository_free(repo);
|
||||
setLastError(QString());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GitManager::hasCommits(const QString &path) const
|
||||
{
|
||||
git_repository *repo = nullptr;
|
||||
if (git_repository_open(&repo, path.toUtf8().constData()) < 0)
|
||||
return false;
|
||||
const bool has = git_repository_head_unborn(repo) == 0;
|
||||
git_repository_free(repo);
|
||||
return has;
|
||||
}
|
||||
|
||||
bool GitManager::commitAll(const QString &path, const QString &message)
|
||||
{
|
||||
CommitInfo info;
|
||||
if (git_repository_open(&info.repo, path.toUtf8().constData()) < 0) {
|
||||
setLastError(QStringLiteral("Хранилище не является git-репозиторием"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (git_repository_index(&info.index, info.repo) < 0) {
|
||||
setLastError(QStringLiteral("Не удалось открыть индекс: %1").arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Добавляем все файлы, включая новые (с учётом .gitignore)
|
||||
git_index_add_all(info.index, nullptr, 0, nullptr, nullptr);
|
||||
const int wrc = git_index_write(info.index);
|
||||
if (wrc < 0) {
|
||||
setLastError(QStringLiteral("Не удалось записать индекс: %1").arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (git_index_entrycount(info.index) == 0) {
|
||||
setLastError(QStringLiteral("Нет изменений для коммита"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Создаём дерево
|
||||
git_tree *tree = nullptr;
|
||||
if (git_index_write_tree(&info.treeId, info.index) < 0
|
||||
|| git_tree_lookup(&tree, info.repo, &info.treeId) < 0) {
|
||||
setLastError(QStringLiteral("Не удалось создать дерево: %1").arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Родитель: head, если коммиты уже есть
|
||||
git_reference *headRef = nullptr;
|
||||
git_commit *parentCommit = nullptr;
|
||||
int nParents = 0;
|
||||
if (git_repository_head(&headRef, info.repo) == 0) {
|
||||
git_object *obj = nullptr;
|
||||
if (git_reference_peel(&obj, headRef, GIT_OBJECT_COMMIT) == 0) {
|
||||
parentCommit = reinterpret_cast<git_commit *>(obj);
|
||||
nParents = 1;
|
||||
}
|
||||
if (headRef) git_reference_free(headRef);
|
||||
}
|
||||
|
||||
// Если дерево не изменилось относительно родителя — коммитить нечего.
|
||||
if (parentCommit) {
|
||||
const git_oid *prevTree = git_commit_tree_id(parentCommit);
|
||||
if (prevTree && git_oid_equal(prevTree, &info.treeId)) {
|
||||
git_commit_free(parentCommit);
|
||||
git_tree_free(tree);
|
||||
setLastError(QStringLiteral("Нет изменений для коммита"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const git_commit *parents[1] = {parentCommit};
|
||||
|
||||
// Подпись из конфига репозитория
|
||||
git_signature *author = nullptr;
|
||||
if (git_signature_default(&author, info.repo) < 0) {
|
||||
git_commit_free(parentCommit);
|
||||
git_tree_free(tree);
|
||||
setLastError(QStringLiteral("Не настроена git-личность: %1").arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
info.author = author;
|
||||
|
||||
const int crc = git_commit_create(&info.commitId, info.repo, "HEAD",
|
||||
author, author, "UTF-8",
|
||||
message.toUtf8().constData(),
|
||||
tree, nParents, parents);
|
||||
|
||||
if (parentCommit) git_commit_free(parentCommit);
|
||||
git_tree_free(tree);
|
||||
|
||||
if (crc < 0) {
|
||||
setLastError(QStringLiteral("Не удалось создать коммит: %1").arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
|
||||
setLastError(QString());
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <git2.h>
|
||||
|
||||
// Обёртка над libgit2: инициализация репозитория хранилища и авто-коммиты.
|
||||
class GitManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GitManager(QObject *parent = nullptr);
|
||||
|
||||
// Инициализирует git-репозиторий внутри хранилища (папка path).
|
||||
// Если .git уже есть — просто открывает его. Возвращает true при успехе.
|
||||
Q_INVOKABLE bool initRepo(const QString &path);
|
||||
|
||||
// Коммитит все изменения в хранилище (рабочее дерево). Возвращает true/false.
|
||||
Q_INVOKABLE bool commitAll(const QString &path, const QString &message);
|
||||
|
||||
// Есть ли в хранилище хотя бы один коммит.
|
||||
Q_INVOKABLE bool hasCommits(const QString &path) const;
|
||||
|
||||
// Последнее сообщение об ошибке (русскоязычное, из каталога ошибок).
|
||||
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
||||
|
||||
QString lastError() const { return m_lastError; }
|
||||
|
||||
static QString repoExists(const QString &path);
|
||||
|
||||
signals:
|
||||
void lastErrorChanged();
|
||||
|
||||
private:
|
||||
void setRepoIdentity(git_repository *repo);
|
||||
void setLastError(const QString &msg);
|
||||
|
||||
QString m_lastError;
|
||||
bool m_gitInitialized = false;
|
||||
};
|
||||
Reference in New Issue
Block a user