new-repository-format

This commit is contained in:
Dmitry Zuikov 2023-06-26 18:02:57 +03:00
parent 456cdd9e01
commit 1b93659ef4
25 changed files with 2307 additions and 605 deletions

View File

@ -1,3 +1,24 @@
## 2023-06-23
Странно, но всё еще новый формат репозитория.
Вроде бы сегодня можно попробовать его смержить
в мастер. Совместим со старым он не будет
## 2023-04-26
тестируем новый формат репозитория.
получится ли запустить его одновременно
со старым? вопрос.
Потенциально, можно делать инкрементальные добавления
в файл лога гита + так же нужно добавить запись
"последняя запись". Но тогда эту последнюю запись
невозможно будет добавлять, так как лог append-only.
Можно её добавить только при передаче через TCP стрим,
что бы как-то определять конец файла.
Тест.
## 2023-04-18 ## 2023-04-18

15
docs/papers/Makefile Normal file
View File

@ -0,0 +1,15 @@
all: hbs2-git-problem hbs2-git-new-repo
.PHONY: all clean
%.pdf: %.tex
xelatex $<
xelatex $<
hbs2-git-problem: hbs2-git-problem.pdf
hbs2-git-new-repo: hbs2-git-new-repo.pdf
clean:
rm -f *.aux *.log *.nav *.out *.snm *.vrb *.toc *.pdf

View File

@ -0,0 +1,537 @@
\documentclass[11pt,a4paper]{article}
\usepackage{polyglossia}
\usepackage{xltxtra}
\usepackage[margin=2cm,a4paper]{geometry}% http://ctan.org/pkg/geometry
\usepackage{pdfpages}
\usepackage{graphicx}
\usepackage[ddmmyyyy]{datetime}
\usepackage{booktabs}
\usepackage{enumitem}
\usepackage{amssymb}
\usepackage{amsmath}
\usepackage{bm}
\usepackage[nomessages]{fp}
\usepackage{caption}
\usepackage{url}
\usepackage{indentfirst}
\usepackage[parfill]{parskip}
\usepackage[ colorlinks=true
, linkcolor=black
, anchorcolor=black
, citecolor=black
, filecolor=black
, menucolor=black
, runcolor=black
, urlcolor=blue]{hyperref}
\usepackage{tikz}
\usetikzlibrary{arrows,snakes,shapes,backgrounds,positioning,calc}
\usepackage{marvosym}
\usepackage{pifont}
\usepackage{fontspec}
\usepackage{fontawesome5}
\usepackage{listings}
\usepackage{verbatim}
\usepackage{xcolor}
\usepackage{float} % Needed for the floating environment
\setmainlanguage{russian}
\defaultfontfeatures{Ligatures=TeX,Mapping=tex-text}
\setmainfont{Liberation Serif}
\newfontfamily\cyrillicfont{Liberation Serif}[Script=Cyrillic]
\newfontfamily{\cyrillicfonttt}{Liberation Mono}[Scale=0.8]
\setlist{noitemsep}
\setlength{\intextsep}{2cm}
\newcommand{\term}[2]{\textit{#2}}
\renewcommand{\dateseparator}{.}
\renewcommand*\contentsname{Содержание}
\lstset{
language=Haskell,
basicstyle=\ttfamily\small,
keywordstyle=\color{blue},
commentstyle=\color{green},
stringstyle=\color{red},
% numberstyle=\tiny\color{gray},
% numbers=left,
% stepnumber=1,
showstringspaces=false,
breaklines=true,
frame=single,
}
\newfloat{Code}{t}{myc}
\graphicspath{ {img/}}
\title{Репозиторий hbs2-git}
\begin{document}
\maketitle
\section{Идея}
hbs2-git это адаптер, позволяющий git работать с HBS2 в качестве бекенда для сохранения и получения
объектов. HBS2 это распределённый P2P CAS, позволяющий обмениваться данными, синхронизировать их,
подписываться на специфические источники при помощи механизма \term{reflog}{ссылок}.
Таким образом, hbs2-git позволяет производить децентрализованную синхронизацию репозиториев без
участия какого-то выделенного сервера/сервиса, используя вместо него множество пиров, которые
находят друг-друга при помощи механизма \term{pex}{PEX} (Peer Exchange) --- то есть находят друг
друга при помощи broadcast сообщений, DNS бутстрапа и списка известных пиров, то есть примерно теми
же способами, что используются в прочих децентрализованных сетях.
Авторизация и аутентификация осуществляются при помощи криптографических механизмов: криптоподписи
и шифрования.
Механизмы эти работают на уровне ссылок, блоков, протоколов и можно еще дополнительно шифровать
собственный контент, если будет такое желание и необходимость.
Вместо того, что бы делать $git~push$ в remote на каком-то сервере с доменным именем, доступом и
прочее, $git~push$ происходит в \term{reflog}{ссылку/рефлог}, который поддерживается всеми пирами,
которые согласны поддерживать этот рефлог.
Узел hbs2-peer может постить \term{transaction}{транзакции} обновления ссылок и получать их, а так же
получать журнал транзакций в виде дерева Меркля.
Как было сказано выше, hbs2-git использует механизм \term{reflog}{рефлога} или же просто
\term{reflog}{ссылки} для своей работы. Рефлог представляет собой некий уникальный идентификатор,
ассоциированный с публичным ключом шифрования (фактически: публичный ключ и есть), а обладание
приватным ключом от этого публичного ключа является доказательством права записи в эту ссылку.
Ссылки могут быть разных типов, рефлог --- это условное название ссылки определенного, конкретного
вида, который реализован, как примитив в HBS2 (hbs2-peer будет точнее).
Рефлог есть список транзакций, упорядоченный по хэшам их контента, представленный в виде хэш-ссылки
на \term{ann-merkle-tree}{дерево Меркля} журнала (лога) транзакций.
Предполагается, что у рефлога есть один писатель --- владелец приватного ключа.
Никакого консенсуса с рефлогом не ассоциировано, однако, он может возникать на уровне данных
транзакций и стейта.
Например, в случае git есть порядок объектов, определяемый DAG ссылки (в смысле git. т.е в основном
это бранч). Имея порядок объектов, можно вывести и порядок операций, т.к. в логе операции
упорядочены последовательно, а сами логи имеют ссылку на коммиты, для которых тоже есть порядок.
Таким образом, даже имея несколько писателей в одну ссылку, которые, вроде бы, могут <<портить>>
ссылки git друг другу, мы можем добавить в стейт идентификатор пользователя в ключ для таблицы
ссылок, и, таким образом, каждый будет видеть только <<своё>> значение ссылки, кроме того, можно
генерировать уникальные ссылки из объединения ключа пользователя и названия бранча.
На текущий момент это не реализовано (но возможно).
Содержимое транзакций может быть произвольным, но есть специальные виды транзакций,
которые поддерживаются hbs2-peer и содержат ссылку на какое-то \term{ann-merkle-tree}{дерево
Меркля}. Смысл поддержки заключается в том, что если hbs2-peer получает транзакцию такого
типа, он заодно автоматически скачивает и дерево, на которое она ссылается.
Транзакции содержат числовое поле, которое призвано их упорядочивать, однако оно может
использоваться или не использоваться, по усмотрению. Внутри списка они упорядочиваются по хэшам, а
приложение может их упорядочивать, как ему хочется или даже несколькими способами сразу.
В нашем конкретном случае, каждая транзакция ссылается на \term{log}{лог} объектов git,
соответственно, лог содержит коммиты, каждый коммит может быть упорядочен относительно прочих,
значит, и логи могут быть упорядочены, например, по максимальной высоте (глубине?) коммита,
который в нём содержится.
Теоретически, тут могут быть краевые случаи, но поскольку мы для создания этого лога
используем функциональность git, мы считаем, что содержимое лога разумно.
Можно сказать, что порядок транзакций задаётся порядком логов, на которые они ссылаются,
который определяется порядком следования коммитов в git.
При своей работе hbs2-git формирует логи git в виде файлов, содержащих заголовки секций
и секции, в которых содержатся объекты разного типа, и помещает их в HBS2 при помощи API hbs2-peer.
Далее, полученные хэш-ссылки на деревья помещаются в транзакции, после чего транзакции подписываются
и публикуются.
И обратно, что бы получить новые объекты при $git~fetch$ hbs2-git получает текущее значение
хэш-ссылки рефлога, скачивает данные рефлога, получает список транзакций, обрабатывает те, которые
до сих пор не обработаны, получает логи, сканирует логи и импортирует объекты в репозиторий git
и обновляет стейт.
\section{Компоненты}
\begin{figure}[h!]
\centering
\begin{tikzpicture}[ every label/.style={font=\scriptsize},
every node/.style={font=\scriptsize},
handle/.style={ draw=black
, circle
, inner sep=2pt
},
box/.style={ draw=black
, rounded corners,
, anchor=base
, font=\scriptsize
, minimum height=1.5cm
, text width=1.5cm
, align=center
},
db/.style={ cylinder
, draw
, fill=gray!10
, minimum height=1cm
, minimum width=1.5cm
, shape border rotate=90
, aspect=0.5
}
]
\node[box,minimum height=3cm] (peer) {hbs2-peer};
\node[ box
, minimum height=1cm
, text width=6em
, left=5cm of peer.north west
, anchor = north east
] (git-remote-hbs2) {git-remote-hbs2};
\node[ box
, minimum height=1cm
, text width=6em
, below=1cm of git-remote-hbs2.south east, anchor = north east
] (git-hbs2) {git-hbs2};
\node[ rectangle
, draw
, dashed
, above = 2mm of git-remote-hbs2.north west
, xshift = -2mm
, text width=2.8cm
, minimum height=3.6cm
, anchor=north west] (tools) {};
\node[box, minimum height=1cm, below=2cm of git-hbs2.south, anchor=north] (git) {git};
\node[db,below=1.6cm of git.north,anchor=north] (repo) {git repo};
\draw[->] (git.south) -- ($(repo.north) - (0,+2mm)$) ;
\draw[->] (git.north) -- ($(tools.south west)!(git.north)!(tools.south east)$)
node[midway,right] {CLI/PIPE};
\node[ db
, left=1cm of git-remote-hbs2.north west, anchor=north east
, yshift=-1cm
] (state) {State};
\draw[->] (git-remote-hbs2.west) -| ($(state.north) - (0,+2mm)$) ;
\draw[->] (git-hbs2.west) -| (state.south);
\node[handle,left=3cm of peer.130] (catAPI) {};
\node[handle,below=5mm of catAPI] (putAPI) {};
\node[handle,below=5mm of putAPI] (refLogGetAPI) {};
\node[handle,below=5mm of refLogGetAPI] (reflogSendRaw) {};
\draw[->] (catAPI) -- ($(peer.north west)!(catAPI)!(peer.south west)$) node[above,midway] {HTTP: Cat};
\draw[->] (putAPI) -- ($(peer.north west)!(putAPI)!(peer.south west)$) node[above,midway] {HTTP: Put};
\draw[->] (refLogGetAPI) -- ($(peer.north west)!(refLogGetAPI)!(peer.south west)$)
node[above, midway] {HTTP: RefLogGet};
\draw[->] (reflogSendRaw) -- ($(peer.north west)!(reflogSendRaw)!(peer.south west)$)
node[above, midway] {CLI: RefLogSendRaw};
\draw[dashed] (catAPI) -- (putAPI) -- (refLogGetAPI) -- (reflogSendRaw)
node[circle,midway,yshift=0.7cm,inner sep=2pt] (m1) {};
\draw[->] (git-remote-hbs2.east) -- (m1);
\draw[->] (git-hbs2.east) -- (m1);
\node[ db
, below=1cm of peer.south
] (store) {Store};
\draw[->] (peer.south) -- ($(store.north) - (0,+2mm)$) ;
\end{tikzpicture}
\end{figure}
\subsection*{git-remote-hbs2}
Исполняемый файл, git-remote-helper, хэлпер git для протокола hbs2://
\subsection*{git-hbs2}
Исполняемый файл, различные функции для работы с hbs2-git, например,
export для первоначальной инициализации \term{reflog}{рефлога}, или
scan для перестройки \term{state}{стейта}.
\subsection*{git}
Процесс git
\subsection*{hbs2-peer}
Процесс hbs2-peer
\subsection*{Store}
Хранилище объектов HBS2 (меркл-деревья, блоки, ссылки, ...)
\subsection*{State}
Вычисляемый \term{state}{стейт} для \term{reflog}{рефлога} репозитория.
\section{Отсортированный по времени/высоте сегментированный лог}
\begin{figure}[h!]
\centering
\begin{tikzpicture}[every node/.append style={font=\scriptsize}]
\node[ rectangle split
, rectangle split parts=3
, draw
, font=\scriptsize
, text width=3cm
, label={above:Reflog}
] (reflog)
{
Transaction~N
\nodepart{two}...
\nodepart{three}Transaction~1
};
\node[ draw
, right = 2cm of reflog.north east, anchor=north west
, rectangle split, rectangle split horizontal, rectangle split parts=3
, label={below:PUSH log}
] (pushlog)
{ \nodepart{one}$S_1$
\nodepart{two}...
\nodepart{three}$S_n$
};
\draw[-latex] (reflog.12) -- (pushlog) node[midway,above] {merkle~tree};
\end{tikzpicture}
\end{figure}
Данный формат использует механизм \term{reflog}{reflog} hbs2, который является Merkle-деревом
списка транзакций. Каждая транзакция имеет стандартный вид, и содержит ссылку на
\term{log}{лог} (хэш \term{ann-merkle-tree}{аннотированного merkle-дерева} лога).
Каждая секция $S_n$ \term{log}{лога} имеет вид:
\begin{figure}[h!]
\centering
\begin{tikzpicture}[every node/.append style={font=\scriptsize}]
\node[ rectangle split, rectangle split horizontal
, rectangle split parts=2
, draw
, font=\scriptsize
, text width=3cm
, label={above:$S_n$}
] (reflog)
{
HEAD(64)
\nodepart{two}DATA(LogEntrySize)
};
\node[ rectangle split
, rectangle split parts=3
, draw
, font=\scriptsize
, text width=3cm
, label={above:HEAD}
, below=1cm of reflog.south west, anchor=north west
] (head)
{
\nodepart{one}LogEntryType
\nodepart{two}LogEntryHash
\nodepart{three}LogEntrySize : W32
};
\draw[->] (reflog.186) -- ($(head.north west)!(reflog.186)!(head.north east)$);
\end{tikzpicture}
\end{figure}
Где HEAD это заголовок фиксированного размера 64~байта, DATA -- байтовая строка размера (LogEntrySize)~байт
произвольного формата, зависящего от LogEntryType.
Формат HEAD: сериализованное в CBOR значение вида $(Type, GitHash?, Size)$, где GitHash --
опционален (соответствует типу $Maybe GitHash$).
Формат DATA: сжатая GZip произвольная строка, размером LogEntrySize \textbf{в сжатом виде}.
Таким образом, данный формат может быть расширен добавлением новых конструкторов в тип
$LogEntryType$.
Текущие основные типы секций:
\begin{description}
\item[GitLogEntryCommit] объект git типа Commit
\item[GitLogEntryTree] объект git типа Tree
\item[GitLogEntryBlob] объект git типа Blob
\item[GitLogEntryHead] объект типа RepoHead, сериализованный в CBOR
\end{description}
\textbf{RepoHead}
\begin{lstlisting}
data RepoHead =
RepoHead
{ _repoHEAD :: Maybe GitRef
, _repoHeads :: HashMap GitRef GitHash
}
\end{lstlisting}
\section{Операции}
\subsection{EXPORT}
\begin{enumerate}
\item Перечислить все объекты git в порядке следования коммитов, так, что после каждого коммита
идут его непосредственные зависимости (tree, blob)
\item Писать данные объекты в \term{log}{лог}
\item Писать лог в hbs2 как \term{ann-merkle-tree}{аннотированное дерево}
\item Порождать \term{reflog-tran}{транзакцию} для \term{reflog}{рефлога} со ссылкой на дерево из
предыдущего пункта
\end{enumerate}
\subsection{PUSH}
\begin{enumerate}
\item Перечислить все объекты git в порядке следования коммитов начиная с последнего известного
для данной ветки, аналогично EXPORT.
\item Далее аналогично 2--4 EXPORT
\end{enumerate}
\subsection{FETCH}
\begin{enumerate}
\item Получить \term{hashref}{значение HashRef} для \term{reflog}{рефлога}
\item Обойти все необработанные \term{transaction}{транзакции} \term{reflog}{рефлога}
\item Для каждой такой транзакции извлечь ссылку на \term{merkle-tree}{дерево}
\item Получить \term{log}{лог} для этого \term{merkle-tree}{дерева}
\item Обойти секции лога, импортировать объекты в репозиторий git, устанавливать
ссылки (в \term{state}{стейте})
\item Обновлять \term{state}{стейт}
\end{enumerate}
Поскольку одна операция $git~push$ устанавливает одну ссылку в заданное значение коммита,
у нас есть соответствие $refname \leftrightarrow commit$ для каждого лога.
Поскольку для коммитов определен порядок, мы можем вычислить порядок логов в журнале транзакций
и порядок следования операций установления ссылки.
к
Таким образом, мы можем вычислить актуальные (последние) значения для каждой ссылки для
объединения логов.
\subsection{DELETE}
Удалять ссылку. Удаление ссылки производится путем установки её в значение
'0\{40\}' (хэш из сорока нулей) а не в пустую строку, как это происхоит в git,
что бы не рассматривать частный случай при разборе объектов типа RepoHead.
В \term{state}{стейте} происходит фильтрация и ссылки, установленные в
данное значение --- не выводятся.
\section{Стейт}
Каждому \term{reflog}{рефлогу} соответствует \term{state}{стейт}, который вычисляется и
синхронизируется при операциях FETCH и PUSH.
\term{state}{Стейт} представляет собой БД SQLite которая содержит необходимую для функционирования
hbs2-git информацию (объекты, отношения, логи, индексы).
\term{state}{Стейт} может быть в любой момент пересоздан из рефлога.
Операции из рефлога над \term{state}{стейтом} идемпотентны.
Порядок существенных операций не играет роли, однако стейт содержит временные метки,
которые будут меняться в зависимости от времени.
То есть, существенная часть стейта определяется только операциями из рефлога, причем,
порядок неважен, так как значимые сущности имеют порядок, определяемый репозиторием git
(топологический).
\section{Замечания}
\subsection*{Перечисление объектов репозитория}
Перечисление объектов репозитория осуществляется при помощи команды
\begin{verbatim}
git rev-list --objects --in-commit-order --reverse --date-order
\end{verbatim}
и введение явной сортировки приводит к тому, что на больших репозиториях
данная команда сначала перечисляет все объекты, и потом их сортирует, что
происходит очень долго.
Явно заданный порядок нужен для возможности повторного использования
частей логов для уменьшения оверхеда по данным, однако эффективность
данной меры пока не оценена, а замедление на больших репозиториях очень
значительное.
Цель частичного повторного использования сегментов лога может быть
достигнута, если одному \term{PUSH}{PUSH} или \term{EXPORT}{EXPORT} будет
соответствовать не один лог, а некоторое количество логов, с ограничением
каждого или по размеру, или по количеству объектов.
Это не создаёт никаких дополнительных расходов, следовательно, должно
быть реализовано.
Тогда существует вероятность, что некоторые последовательности коммитов
будут повторно использоваться в системе, и кажется, это лучше, чем текущая
ситуация с попыткой переиспользования частей лога.
Откуда берётся переиспользование вообще: так как лог сохраняется, как
\term{merkle}{дерево Меркля}, он разбивается на сегменты фиксированного
размера ($\approx{}256K$) и эти сегменты могут не скачиваться повторно, если они
уже есть, или же скачиваться у разных участников сети, если у них они
есть.
\subsection*{Использование fast-export/fast-import}
Хотя чтение и импорт объектов при помощи fast-export/fast-import
может быть быстрее, а сжатие такого лога полностью при помощи
gzip --- лучше, надо заметить следующее:
\begin{enumerate}
\item Отсутствие хэшей в этом формате,
которые заменены короткими уникальными идентификаторами
\item Очень сильное замедление сжатия с ростом объема лога fast-export
\item Быстрое чтение объектов при помощи fast-export
\item Вероятно, более быстрый импорт объектов при помощи fast-import
\end{enumerate}
\subsubsection*{Следствия}
На больших логах будет работать очень медленно, так же медленно, как и сейчас.
На маленьких логах всё работает достаточно быстро и сейчас, нет смысла утруждаться.
Хэши нужны, что бы контролировать целостность логов, то есть придется парсить формат fast-import
и их считать, что еще больше замедлит.
Если хэши объектов не считать, то есть опасность постинга по сути мусора в репозиторий git, т.к.
нет возможности контролировать и сопоставлять объекты.
Текущий формат лога позволяет добавить секции, где будут логи формата fast-import.
Это может привести к некоторому ускорению, если у нас не будет огромных логов, а логи будут нарезаны
по некоему разумному размеру.
Выглядит так, что можно попробовать сделать это в дальнейшем, когда другие способы оптимизации
скорости будут исчерпаны.
\end{document}

View File

@ -0,0 +1,627 @@
\documentclass[11pt,a4paper]{article}
\usepackage{polyglossia}
\usepackage{xltxtra}
\usepackage[margin=2cm,a4paper]{geometry}% http://ctan.org/pkg/geometry
\usepackage{pdfpages}
\usepackage{graphicx}
\usepackage[ddmmyyyy]{datetime}
\usepackage{booktabs}
\usepackage{enumitem}
\usepackage{amssymb}
\usepackage{amsmath}
\usepackage{bm}
\usepackage[nomessages]{fp}
\usepackage{caption}
\usepackage{url}
\usepackage{indentfirst}
\usepackage[parfill]{parskip}
\usepackage[ colorlinks=true
, linkcolor=black
, anchorcolor=black
, citecolor=black
, filecolor=black
, menucolor=black
, runcolor=black
, urlcolor=blue]{hyperref}
\usepackage{tikz}
\usetikzlibrary{arrows,snakes,shapes,backgrounds,positioning,calc}
\usepackage{marvosym}
\usepackage{pifont}
\usepackage{fontspec}
\usepackage{fontawesome5}
\setmainlanguage{russian}
\defaultfontfeatures{Ligatures=TeX,Mapping=tex-text}
\setmainfont{Liberation Serif}
\newfontfamily\cyrillicfont{Liberation Serif}[Script=Cyrillic]
\setlist{noitemsep}
\setlength{\intextsep}{2cm}
\renewcommand{\dateseparator}{.}
\renewcommand*\contentsname{Содержание}
\graphicspath{ {img/}}
\title{Проблема репозитория HBS2 GIT}
\begin{document}
\maketitle
\section{Определения}
\begin{description}[itemsep=10pt]
\item[Объект] Любой объект, сохраняемый и передаваемый в системе. Файл,
структура данных и т.п.
\item[Блок] Адресуемые (имеющие криптографический хэш) данные. Данные это последовательность байт.
Может быть любого размера, типичный размер: не более 256K. Зашифрованный блок может
быть больше, так как при шифровании в него добавятся данные алгоритма шифрования.
Блок может быть и меньше 256K, допустим блок это маленький объект. Тогда размер блока
будет равен размеру объекта.
\par \textbf{Note:} при шифровании данные можно предварительно сжимать.
\item[Чанк] Нумерованая часть \textit{блока}. Для каждого блока известного размера можно построить
единственное разбиение на чанки заданного размера. Размер чанка выбирается таким
образом, что бы его можно было поместить в UDP пакет безопасного размера.
Безопасного это такого, что можно не заботиться о фрагментации. Считается безопасным
размер UDP пакета 508 байт, на практике MTU всегда 1500 и либо фрагментацией
занимается сетевой стек и делает это хорошо, либо MTU других размеров уже не
встречаются, в любом случае практических проблем с размером чанка = 1400 не
встретилось. В системе размер чанка принят за 1200 --- 1400 байт. Чанк используется
в протоколе передачи блоков: так как по UDP мы не можем запросить блок целиком, мы
запрашиваем его по частям, каждая такая часть и есть чанк. В протоколе существует
возможность запрашивать не каждый чанк одельно, а последовательностями, именуемыми
burst.
\item[BlockSize] Размер блока. Может быть любой, не должен быть слишком
маленьким, но и слишком большим. Типично в районе 256K.
\item[ChunkSize] Максимальный рамер чанка. Должен иметь размер, не превышающий
максимального безопасного размера для UDP (1200 --- 1400
байт) (с учетом заголовка пакета).
\item[Burst] Последовательность чанков в протоколе. Клиент запрашивает не по каждому чанку
отдельно, а сериями по N чанков, начиная с k-го. Каждая такая последовательность
и есть burst.
\item[Merkle~Tree] Любой крупный объект, данные или последовательность объектов
разбивается на блоки при помощи построения его Merkle дерева
(в нашей интерпретации). Дерево имеет два параметра:
количество элементов в каждом узле, и размер (в каком-то виде) каждого
элемента. Построение дерева происходит в два этапа: сначала мы производим
разбиение, учитывая эти два параметра, затем --- обходим получившееся дерево,
сериализуем каждый узел (пишем блоки в \textit{хранилище}), и из полученных
при сериализации хэшей строим уже дерево Меркля (Merkle~Tree).
Дерево может состоять из узлов типа (Merkle~Tree) либо Leaf
- указание на блок (данных).
\item[Annotated~Merkle~Tree] Merkle~Tree с аннотацией в виде либо короткой
строки, либо ссылкой на блок с метаинформацией.
\item[Peer] Пир. Участник сети, авторизованный своим криптографическим ключом
подписи, и поддерживающий протоколы системы.
\item[RTT] Round trip time, время на посылку запроса и получение
ответа (в сети). Можно рассматривать на уровне прикладного протокола HBS2,
можно на уровне транспортного (TCP). Обычно имеется ввиду уровень протокола
HBS2.
\item[Reflog] Механизм реализации изменяемой ссылки. Каждая ссылка
определяется неким публичным ключом. Для каждой ссылки можно опубликовать
транзакцию, короткий (ChunkSize) блок данных, который может либо содержать
произвольные данные, либо \textit{ссылку} на блок или Merkle~Tree. В случае,
если внутри ссылка -- то данные ссылки будут скачаны пиром автоматически
рекурсивно. Все транзакции Reflog упорядочиваются по их хэшам в виде списка,
который сохраняется в виде Merkle~Tree. Таким образом, значение ссылки типа
Reflog это хэш Merkle~Tree упорядоченного по хэшам списка транзакций.
\end{description}
\section{Проблема}
Медленная передача большого количества маленьких объектов (блоков).
Приводит к очень медленному первоначальному скачиванию репозитория.
Обновления затем происходят с приемлемой скоростью.
Связано с тем, что в текущем протоколе для каждого передаваемого адресуемого
блока нам нужно:
\begin{enumerate}
\item Запросить размер (выяснить его наличие)
\item Запросить его чанки
\end{enumerate}
Таким образом, скачивание одного маленького объекта занимает минимум:
\mbox{$ 2 \times RTT + C + T$}, где C - время обработки запроса на обеих
сторонах, T - время передачи самих данных.
На практике это означает, что например при типичном $ RTT = 50ms $ мы доcтигаем
скорости 6 блоков (чанков) в секунду, что даём нам не более 9Kb/s, реально в
районе 6 --- 8. Каковая скорость и наблюдается экспериментально.
Текущее устройство репозитория hbs2-git:
\begin{figure}[h!]
\centering
\begin{tikzpicture}[every node/.append style={font=\scriptsize}]
\node[ rectangle split
, rectangle split parts=3
, draw
, font=\scriptsize
, text width=3cm
, label={above:Reflog}
] (reflog)
{
Transaction~N
\nodepart{two}...
\nodepart{three}Transaction~1
};
\node[ rectangle split
, rectangle split parts=4
, draw
, font=\scriptsize
, text width=3cm
, right=2cm of reflog.north east, anchor=north west
, label={above:Object~list}
] (objlist)
{
\nodepart{one}HEAD~Object
\nodepart{two}Git Object~1
\nodepart{three}...
\nodepart{four}Git object~N
};
\node[draw,rectangle,below=2cm of objlist,xshift=-3cm,minimum
height=1cm,label={below:Git Object}]
(merkle) {Annotated~Merkle~Tree};
\draw[-latex] (reflog.15) -- (objlist.155);
\draw[-latex] (objlist.205) to [bend right,looseness=1] (merkle.north);
\end{tikzpicture}
\end{figure}
\begin{description}[itemsep=10pt]
\item[Reflog] Это Merkle~Tree списка \textit{транзакций}.
\item[Транзакция] Это небольшой объект (размера не больше ChunkSize),
произвольного содержания, в случае HBS2 Git -- со ссылкой на Merkle~Tree
списка объектов.
\item[Object list] Список всех достижимых объектов репозитория (транзитивное
замыкание всех объектов для коммита) + HEAD (список ссылок в смысле git и их значений)
\item[Git Object] Сохраненный в виде Annotated~Merkle~Tree объект git. В
аннотации указан тип объекта (commit, blob, tree)
\end{description}
Данное устройство было выбрано как
\begin{enumerate}
\item Наиболее простое и очевидное
\item Минимизирующее оверхед по данным на уровне git объектов -- т.е уже
имеющиеся git объекты никогда не будут скачиваться повторно
\item Любая транзакция описывает полный, консистентный репозиторий, т.е можно
не иметь полного набора транзакций, но иметь, тем не менее, полный
репозиторий.
\item Каждый когда-либо созданный git object доступен в сети пиров по хэшу его
Merkle~Tree, что казалось довольно удобно (но практически бесполезно)
\end{enumerate}
Несмотря на стремление к минимизации оверхеда по данным, данное устройство его,
тем не менее, создаёт в другом месте:
Каждое Merkle~Tree это минимум один дополнительный блок -- т.е блок на
Merkle~Tree + блок на блок данных.
Как можно видеть, данное устройство располагает к образованию большого
количества маленьких объектов: сами объекты git как правило маленькие,
транзакции = маленькие объекты (ChunkSize).
Большое число: 10---20 тысяч для репозитория размером порядка 600 коммитов.
Что, если взять скорость 5 блоков (чанков) в секунду (из оценки выше), приводит
к нас с показателю 4000 секунд на 20'000 блоков, что примерно соответствует
наблюдаемой картине (в реальности чуть лучше).
Несмотря на то, что последующие скачивания будут выполняться быстро (объекты не
будут выкачиваться повторно, будут скачаны только недостающие объекты, их
немного), первоначальное развертывание репозитория происходит неприемлемо
медленно.
Это и есть проблема.
\section{Возможные решения}
Что мы в принципе хотим добиться:
\begin{enumerate}
\item Быстрое скачивание всего репозитория;
\item Минимизировать оверхед по данным, т.е что бы одни и те же объекты
(разбитые на блоки), по возможности, скачивались один раз.
\end{enumerate}
\subsection{Большой лог}
Основная идея --- не порождать большое число маленьких объектов, как в текущем
дизайне, вместо этого ввести \textit{лог}, куда будут упорядоченно
писаться объекты git, каждый объект только один раз.
Такой лог представляет собой просто большой файл, который может передаваться
обычными механизмами с максимально высокой скоростью ( десятки Mb/s ).
Данное решение имеет недостатки:
\begin{itemize}
\item[-] Более нет соответствия $ git object \leftrightarrow merkle tree $
\item[-] При отсутствии выравнивания, логи разных форков репозиториев
не сойдутся никогда, следовательно, всегда будет перевыкачиваться
различающаяся часть и эта часть будет расти со временем
\item[-] При наличии <<крупного>> (256K) выравнивания оверхед по данным возрастает
на порядки ( ~ 3Gb для числа объектов $ \approx 12000$ )
\item[-] При наличии <<мелкого>> выравнивания оверхед всё равно
существенный ( $ \approx 50\% $ для блока размером 1K ),
но растёт число мелких объектов и соответствующим падением скорости
передачи.
\end{itemize}
\subsubsection{Большой лог всех объектов}
В случае выровненной записи --- получаем описанные выше недостатки.
В случае невыровненной записи --- получаем расхождение логов у разных писателей
(форков), соответствовать будет лишь начальная часть.
Можно ввести специальную дисциплину работы с логом и мерж лога, тогда можно
будет их переупорядочивать у всех, и логи будут периодически сходиться. Высокая
сложность реализации и много неоднозначностей.
Можно писать лог каждый раз при push, тогда для одинаковых git репозиториев
будут одинаковые логи. Очень медленно и время будет расти с ростом репозитория.
\textbf{Плюсы:}
простая реализация, быстрое первоначальное развертывание.
\textbf{Минусы:}
всё остальное.
\subsubsection{Отсортированный по времени/высоте сегментированный лог}
Как можно видеть, ситуация с большим логом объектов, даже отсортированных по
высоте/времени может приводить к перевыкачиванию значительной части лога в
ситуации, когда кто-то отредактировал историю так, что новые объекты появились в
начале лога, что допустимо в git. В случае больших репозиториев это приведёт к
скачиванию большого количества данных и хранению этих данных. Что бы реже
сталкиваться с подобной проблемой, будем строить стейт следующим образом:
\begin{figure}[h!]
\centering
\begin{tikzpicture}[every node/.append style={font=\scriptsize}]
\node[ rectangle split
, rectangle split parts=3
, draw
, font=\scriptsize
, text width=3cm
, label={above:Reflog}
] (reflog)
{
Transaction~N
\nodepart{two}...
\nodepart{three}Transaction~1
};
\node[ draw
, right = 2cm of reflog.north east, anchor=north west
, rectangle split, rectangle split horizontal, rectangle split parts=4
, label={below:PUSH log}
] (pushlog)
{ \nodepart{one}$S_1$
\nodepart{two}...
\nodepart{three}$S_n$
\nodepart{four}HEAD
};
\draw[-latex] (reflog.12) -- (pushlog) node[midway,above] {merkle~tree};
\end{tikzpicture}
\end{figure}
\begin{itemize}
\item[-] Каждая транзация $T_x$ содержит
отсортированные по времени (высоте) и по хэшу объекты репозитория
\item[-] $S(R_x) = \bigcup$ объектов из всех транзакций Reflog
\item[-] Берем объекты, отсутствующие в $S(R_x)$, сортируем по времени/высоте и затем хэшам
и пишем в лог в сжатом виде можно сжать весь лог)
\item[-] Объект HEAD пишем в тот же лог, начало или конец (TBD)
\item[-] Публикуем транзацию $T_y$, которая содержит ссылку на Merkle~Tree лога из предыдущего
пункта
\end{itemize}
\textbf{Следствия:}
\begin{itemize}
\item[-] Стейт $S(R_x)$ каждой ссылки $R_x$ обновляется инкрементально
\item[-] Каждое обновление (транзакция $T_x$) соответствует push в соответствующий
\textit{git~remote}
\item[-] Каждое обновление содержит объекты, отсутствующие в $S(R_x)$, отсортированные
детерменированным образом, зависимости только состояния репозитория git
\item[-] Каждый сегмент лога, кроме первого, как правило, сравнительно небольшой -- содержит
только новые объекты, созданные пользователем (commit, tree, blob)
\item[-] В рамках одной ссылки логи перевыкачиваться не будут, как логи (содержимое
Merkle~Tree транзаций $T_x$ создаются детерменированным образом в завимости только
содержимого репозитория git
\item[-] Частичные логи могут иногда совпадать, так что они не всегда будут дублироваться между
ссылками. Различные кейсы рассмотрим далее отдельно
\end{itemize}
\textbf{Ситуация 1:}
\begin{itemize}
\item[-] Алиса делает \textit{EXPORT} существующего репозитория в свою ссылку $R_a$
\item[-] Боб делает \textit{git~clone} данного репозитория (ссылки $R_a$)
\item[-] Боб создаёт новую ссылку $R_b$
\item[-] Боб делает \textit{EXPORT} репозитория в эту ссылку
и сообщает ссылку $R_b$ Алисе
\item[-] Боб делает изменения и делает \textit{git~push} в ссылку $R_b$
\end{itemize}
В этом случае:
\begin{enumerate}
\item Создаётся лог, содержащий объекты $S(R_a)$ --- все объекты репозитория,
в порядке создания/высоты и далее в порядке хэшей
\item Публикуется транзация $T_x$ со ссылкой на Merkle~Tree лога объектов
\item Боб принимает данную транзацию, объектов $S(R_a)$ --- данных объектов
у него еще нет, следовательно дублирования нет
\item Объекты $S(R_a)$ распаковываются в репозиторий git
\item Создаётся новая ссылка $R_b$. При \textit{EXPORT} создаётся
первоначальная транзакция, которая ровно такая же, как и $T_x$,
так как содержимое лога определяется только объектами репозитория git,
а они все из $S(R_a)$
\item Боб делает свои изменения и делает \textit{git~push}
\item Создается $T_{b1}$, содержащая объекты $S(R_{b1})$, отсутствующие в
$S(R_a)$
\item Алиса получает $T_{b1}$, содержащая объекты $S(R_{b1})$ и импортирует
эти объекты в свой репозиторий git.
\end{enumerate}
На шаге (5) мог не выполниться \textit{EXPORT}, так как его выполнение зависит
от пользователя. В этом случае на шаге (7) будет создана $T_{b1}$ содержащая все
объекты репозитория, включая те, что были созданы Бобом на шаге (6). В этом
случае, какое-то количество объектов в зависимости от гранулярности разбиения
(BlockSize), попадут в те блоки, что уже есть у Алисы в $S(R_a)$. И чем больше
был размер лога $S(R_a)$, тем больше объектов Алиса не будет скачивать повторно,
так как порядок следования определяется только объектами в репозитории, Боб не
переписывал историю и его объекты будут следовать за объектами Алисы.
Если же Боб переписал историю, что, в целом, возможно, то Алиса не будет
повторно выкачивать только часть лога до изменений Боба.
Таким образом видно, что при создании ссылки $R_b$ нужно форсировать операцию
EXPORT.
Как можно её форсировать, ведь Боб может и не создавать ссылку, или создавать
её в любой момент времени.
Например, при операции PUSH нужно каким-то образом получить все объекты,
которые шли раньше изменений Боба и создать транзакцию $T_{b0}$ в которой
будут эти объекты.
Как система должна понять, что этот \textit{git~push} --- не первоначальная
инициация репозитория, а апдейт? Например, так: при \textit{FETCH}
в случае отсутствия транзакций создаётся (и публикуется) первая транзакция.
Тогда в большей части случаев она будет соответствовать инициации репозитория,
и при последующем \textit{PUSH} или \textit{EXPORT} в создаваемую транзакцию
попадут только те объекты, которые создал владелец новой ссылки (Боб в нашем
случае).
\subsubsection{Merkle~Tree+лог с переменным разбиением}
\begin{figure}[h!]
\centering
\begin{tikzpicture}[every node/.append style={font=\scriptsize}]
\node[ rectangle split
, rectangle split parts=3
, draw
, font=\scriptsize
, text width=3cm
, label={above:Reflog}
] (reflog)
{
Transaction~N
\nodepart{two}...
\nodepart{three}Transaction~1
};
\node[ rectangle split
, rectangle split parts=5
, draw
, font=\scriptsize
, text width=3cm
, right=2cm of reflog.north east, anchor=north west
, label={above:Object~list}
] (objlist)
{
\nodepart{one}HEAD~Object
\nodepart{two}SmallObjectLog
\nodepart{three}...
\nodepart{four}BigGitObject
\nodepart{five}...
};
\node[draw,rectangle,below=1cm of objlist.north east,xshift=2cm,minimum
height=1cm,label={[xshift=1.2cm]above:Small~Object~Log}]
(smallmerkle) {Merkle~Tree};
\node[right=0.1cm of smallmerkle,text width=4.2cm,font=\scriptsize]
(smallmerklenote) {С переменным шагом разбиения:\\один объект=один лист};
\node[ rectangle split
, rectangle split parts=4
, draw
, font=\scriptsize
, text width=3cm
, below=1cm of smallmerkle
, label={below:SmallObjectLog}
] (objlog)
{
\nodepart{one}SmallObject~1
\nodepart{two}...
\nodepart{three}SmallObject~N
\nodepart{four}...
};
\node[draw,rectangle,below=2cm of objlist,xshift=-3cm,minimum
height=1cm,label={below:Git Object}]
(merkle) {Annotated~Merkle~Tree};
\draw[-latex] (reflog.15) -- (objlist.155);
\draw[-latex] (objlist.200) to [bend right,looseness=1] (merkle.north);
\draw[-latex] (objlist.12) -| (smallmerkle.north);
\draw[-latex] (smallmerkle.south) -- (objlog.north);
\end{tikzpicture}
\end{figure}
Несмотря на то, что сейчас дерево разбивается на блоки фиксированного размера,
необходимости в этом нет. Это означает, что мы можем генерировать Merkle~Tree
таким образом, что каждому маленькому объекту будет соответствовать один объект
(лист) Merkle~Tree (т.е они не будут разбиваться: мы сначала генерируем лог
<<маленьких>> объектов, затем разбиваем его, принимая во внимание размер каждой
секции (заголовок+объект), так что каждая секция --- ровно один лист (хэш)
Merkle~Tree.
\textbf{Требование:} Каждый <<маленький>> объект должен быть достаточно
маленьким для того, что бы можно было его безопасно читать в память.
<<Большие>> объекты передаются ровно так, как сейчас, то есть каждый отдельно.
<<Большие>> это, допустим, от 1Mb.
\textbf{Плюсы:}
\begin{itemize}
\item[+] Выполняется требование отсутствия оверхеда по данным
\item[+] Нет множества <<мелких>> объектов: лог <<большой>>,
отдельные объекты тоже <<большие>>.
\end{itemize}
\textbf{Минусы:}
\begin{itemize}
\item[-] Передаваться будет дольше, чем <<большой>> лог
\item[-] Средняя сложность реализации
\item[-] <<Переменное разбиение>> $ => $ объекты git маленькие $ => $ много
маленьких объектов $ => $ \textbf{не работает}
\end{itemize}
\subsection{Ускорение скачивания мелких объектов}
Невредно сделать в любом случае, однако полезность данного механизма может
варьироваться, а сложность может быть велика. Мы рассматриваем в первую очередь
ограничения UDP, так как в настоящий момент по TCP ходят в точности такие же
протоколы, как и по UDP. Это может быть изменено, но потребуется поддержка
механизмов, которые будут динамически разрешать или запрещать протоколы в
зависимости от типов транспорта. Это возможно, сложность --- ниже средней.
\subsubsection{Пакетные запросы}
Поскольку мы можем передать по UDP 1200 --- 1400 байт в одной датаграмме,
мы можем запросить $ 1200 / 32 \approx 32 $ объекта за один раз.
Но если речь идёт о UDP, то ответ, очевидно, не может быть получен в одной
датаграмме, значит, нужен какой-то механизм стриминга/сборки ответа из кусков,
и уже имеющийся механизм не подходит, так как ответ создаётся динамически и
отсутствует в хранилище. Таким образом, его надо или создавать в хранилище (и
увеличивать число данных, причём, мусора), либо как-то создавать временный
ответ и скачивать его текущими механизмами.
Это возможно, но неизящно:
\begin{enumerate}
\item Получить запрос
\item Сформировать ответ (записать данные на /tmp допустим)
\item Посчитать хэш ответа ( как блока(!) )
\item Добавить хэш в специальную таблицу <<временные объекты>>
\item Сообщить хэш и размер запрашивающей стороне
\item Запрашивающая сторона получает блок обычным способом
\item При обработке запроса сервер смотрит, не является ли блок временным
объектом и читает его, а не хранилище
\end{enumerate}
Кроме того, ускорение может получиться не столь значительным, так как
мы можем не знать все объекты заранее, и что бы их узнать, надо сначала
скачать другие объекты.
Из наблюдаемых явлений видно, что в какой-то момент времени очередь запросов
становится весьма большой ( $ \approx 10K $ объектов ), следовательно, можно
запрашивать объекты пачками, следовательно, будет иметь место ускорение в $
\approx 32 $ раза, что приведёт к скорости отдачи мелких объектов в районе
160Kb/s. Что не бог весть что, на самом деле. Текущие скорости отдачи
"нормальных" объектов составляют около ширины канала, десятки мегабайт в
секунду.
\subsubsection{Стриминг Merkle~Tree}
Специальная команда, в ответ на которую передаются все блоки Merkle~Tree, не
дожидаясь запросов.
Проблематичная реализация на UDP, в виду возможного реордеринга и потери
пакетов, а имеющийся текущий механизм, устойчивый к реордерингу и потерям,
неприменим.
Вернее, он применим, если мы создадим механизм <<временного>> хранилища --
аналогичного обычному, но существующему лишь некоторое время. Тогда
любые промежуточные объекты могут быть созданы, существовать в
течение сессии или некоторого времени и впоследствии удалены.
Можно и не создавать временное хранилище, а помечать объекты, как временные
и рекурсивно удалять через некоторое время.
Можно создавать временный объект <<транзитивного замыкания>> для дерева,
сообщать его хэш, и давать клиенту скачивать его обычным образом.
Можно видеть, что любые варианты пакетного скачивания объектов ведут к
работе <<сервера>> и расходованию его ресурсов, т.е при незначительном
усилии со строны клиента (один запрос) сервер вынужден сканировать десятки
тысяч объектов и создавать временные объекты потенциально любых размеров,
что открывает путь для намеренных и ненамеренных атак на пира.
Если у нас есть надёжный транспортный протокол (TCP) то можно стримить данные,
не создавая временных структур, тогда затраты локальных ресурсов будут ниже,
но тем ме менее, возможность DoS остаётся: одна команда со стороны <<клиента>>
приводит к несопоставимо большой вычислительной работе <<сервера>> и передачи
большого объема данных (много больше исходного запроса).
\end{document}

View File

@ -0,0 +1,11 @@
FIXME: delete-refs-properly
Сейчас мы помечаем удалённые бранчи 0000000000000000000000000000000000000000
и если такой бранч был удалён однажды, то больше его мы не увидим. Тогда как
реально они могут быть удалены, созданы, опять удалены.
Для каждой операции в logrefval мы пишем хэш лога, в котором она определена.
Так же у нас есть logobject в котором есть связь объектов и логов, и коммиты,
для которых мы можем построить высоту (топологически).
Таким образом, у лога можно определить "высоту", а значит, и для всех операций
в логе можно определить высоту. Таким образом, у последовательности изменений
значения ссылки возникает порядок.

View File

@ -0,0 +1,33 @@
FIXME: filter-existed-objects-from-log
Сейчас если коммит ссылается на уже существующие в стейте
объекты, они всё попадут в лог. Нужно отфильтровывать их
оттуда
TODO: faster-git-clone
Медленное клонирование.
Можно попытаться оптимизировать, можно
попытаться сделать через fast-import -- т.е
дампить объекты в формате fast-import-stream,
должно быть намного быстрее
FIXME: faster-export
Сейчас обходятся вообще все коммиты в git rev-list,
а можно только те, которых нет в стейте - т.е начиная
с какого-то. Ну например, для данной ссылке брать коммит
с максимальной высотой (глубиной) и rev-list делать до него.
Пример:
```
[dmz@minipig:~/w/hbs2]$ git rev-list --objects 640c447e9dca6a32ecb80f85f2d3ab9ad45dc91e..
0e887a87e30005a8ebdb43aa5bf0ed78383cf52a
5509c970621a75c9f82b4d2743fd211c1165e61f
7f0c4c0659367ae10bd3eb84b7bc26f09dd26282 hbs2-git
6d38123f72101aa6a66c35ced40f5dd156a722c4 hbs2-git/lib
1aadc3441288d6d4f9fddb00342dd296242ded1a hbs2-git/lib/HBS2Git
bb270a54495fdf44e831b53c63b5814a44d354af hbs2-git/lib/HBS2Git/State.hs
```
TODO: git-tags-support
Поддержать теги, в т.ч. подписанные.
Неподписанные возможно и так будут работать.

View File

@ -0,0 +1,12 @@
FIXME: hbs2-slow-import-export-1
Сейчас полностью перечитывается весь лог при вызове importRefLogNew,
если хотя бы одна из ссылок отсутствует. Например, всё будет пересканировано
при добавлении новой ссылки (бранча).
Нужно: перечитывать только отсутствующие ссылки.
TODO: use-fast-import-for-import
Можно генерировать формат git fast-import при импорте, тогда
ускорится импорт раз в десять.

View File

@ -1,46 +1,44 @@
module Main where module Main where
import HBS2.Prelude import HBS2.Prelude.Plated
import HBS2.Data.Types.Refs import HBS2.Data.Types.Refs
import HBS2.Base58 import HBS2.Base58
import HBS2.OrDie import HBS2.OrDie
import HBS2.Git.Types import HBS2.Git.Types
import HBS2.Git.Local.CLI
import HBS2.Clock
import HBS2.System.Logger.Simple import HBS2.System.Logger.Simple
import HBS2Git.Types() import HBS2Git.Types(traceTime)
import HBS2Git.Types qualified as G
import HBS2Git.App import HBS2Git.App
import HBS2Git.State import HBS2Git.State
import HBS2Git.Update import HBS2Git.Import
import HBS2Git.Export import HBS2.Git.Local.CLI
import HBS2Git.Config as Config
import HBS2Git.Export (runExport)
import GitRemoteTypes import GitRemoteTypes
import GitRemotePush import GitRemotePush
import Control.Concurrent.Async
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad.Reader import Control.Monad.Reader
import Data.Attoparsec.Text import Data.Attoparsec.Text hiding (try)
import Data.Attoparsec.Text qualified as Atto import Data.Attoparsec.Text qualified as Atto
import Data.ByteString.Char8 qualified as BS import Data.ByteString.Char8 qualified as BS
import Data.ByteString.Lazy.Char8 qualified as LBS import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.Foldable import Data.Foldable
import Data.Functor import Data.Functor
import Data.HashSet qualified as HashSet import Data.Function ((&))
import Data.HashMap.Strict qualified as HashMap
import Data.Maybe import Data.Maybe
import Data.Text qualified as Text import Data.Text qualified as Text
import Data.List qualified as List
import System.Environment import System.Environment
import System.Exit qualified as Exit
import System.Posix.Signals import System.Posix.Signals
import System.ProgressBar
import Text.InterpolatedString.Perl6 (qc) import Text.InterpolatedString.Perl6 (qc)
import UnliftIO.IO as UIO import UnliftIO.IO as UIO
import Control.Monad.Trans.Maybe
import Control.Monad.Catch import Control.Monad.Catch
import Control.Monad.Trans.Resource
send :: MonadIO m => BS.ByteString -> m () send :: MonadIO m => BS.ByteString -> m ()
send = liftIO . BS.hPutStr stdout send = liftIO . BS.hPutStr stdout
@ -73,14 +71,16 @@ parseRepoURL url' = either (const Nothing) Just (parseOnly p url)
capabilities :: BS.ByteString capabilities :: BS.ByteString
capabilities = BS.unlines ["push","fetch"] capabilities = BS.unlines ["push","fetch"]
readHeadDef :: HasCatAPI m => DBEnv -> m LBS.ByteString
readHeadDef db = guessHead :: GitRef -> Integer
withDB db stateGetHead >>= guessHead = \case
\r' -> maybe1 r' (pure "\n") \r -> do "refs/heads/master" -> 0
readObject r <&> fromMaybe "\n" "refs/heads/main" -> 0
_ -> 1
loop :: forall m . ( MonadIO m loop :: forall m . ( MonadIO m
, MonadCatch m , MonadCatch m
, MonadUnliftIO m
, HasProgress (RunWithConfig (GitRemoteApp m)) , HasProgress (RunWithConfig (GitRemoteApp m))
) => [String] -> GitRemoteApp m () ) => [String] -> GitRemoteApp m ()
loop args = do loop args = do
@ -104,8 +104,7 @@ loop args = do
db <- dbEnv dbPath db <- dbEnv dbPath
--FIXME: git-fetch-second-time -- TODO: hbs2-peer-fetch-reference-and-wait
-- Разобраться, почему git fetch срабатывает со второго раза
checkRef <- readRef ref <&> isJust checkRef <- readRef ref <&> isJust
@ -114,30 +113,27 @@ loop args = do
warn "trying to init reference --- may be it's ours" warn "trying to init reference --- may be it's ours"
liftIO $ runApp NoLog (runExport Nothing ref) liftIO $ runApp NoLog (runExport Nothing ref)
hdRefOld <- readHeadDef db refs <- withDB db stateGetActualRefs
updateLocalState ref let heads = [ h | h@GitHash{} <- universeBi refs ]
hd <- readHeadDef db missed <- try (mapM (gitReadObject Nothing) heads) <&> either (\(_::SomeException) -> True) (const False)
hashes <- withDB db stateGetAllObjects let force = missed || List.null heads
-- FIXME: asap-get-all-existing-objects-or-all-if-clone debug $ "THIS MIGHT BE CLONE!" <+> pretty force
-- если clone - доставать всё
-- если fetch - брать список объектов и импортировать
-- только те, которых нет в репо
existed <- gitListAllObjects <&> HashSet.fromList -- sync state first
traceTime "TIMING: importRefLogNew" $ importRefLogNew force ref
jobz <- liftIO newTQueueIO refsNew <- withDB db stateGetActualRefs
let possibleHead = listToMaybe $ List.take 1 $ List.sortOn guessHead (fmap fst refsNew)
jobNumT <- liftIO $ newTVarIO 0 let hd = refsNew & LBS.pack . show
liftIO $ atomically $ for_ hashes $ \o@(_,gh,_) -> do . pretty
unless (HashSet.member gh existed) do . AsGitRefsFile
modifyTVar' jobNumT succ . RepoHead possibleHead
writeTQueue jobz o . HashMap.fromList
env <- ask
batch <- liftIO $ newTVarIO False batch <- liftIO $ newTVarIO False
@ -153,10 +149,6 @@ loop args = do
let str = BS.unwords (BS.words s) let str = BS.unwords (BS.words s)
let cmd = BS.words str let cmd = BS.words str
-- trace $ pretty (fmap BS.unpack cmd)
-- hPrint stderr $ show $ pretty (fmap BS.unpack cmd)
--
isBatch <- liftIO $ readTVarIO batch isBatch <- liftIO $ readTVarIO batch
case cmd of case cmd of
@ -172,26 +164,6 @@ loop args = do
next next
["list"] -> do ["list"] -> do
hl <- liftIO $ readTVarIO jobNumT
pb <- newProgressMonitor "storing git objects" hl
-- FIXME: thread-num-hardcoded
liftIO $ replicateConcurrently_ 4 $ fix \nl -> do
atomically (tryReadTQueue jobz) >>= \case
Nothing -> pure ()
Just (h,_,t) -> do
runRemoteM env do
-- FIXME: proper-error-handling
o <- readObject h `orDie` [qc|unable to fetch object {pretty t} {pretty h}|]
r <- gitStoreObject (GitObject t o)
when (isNothing r) do
err $ "can't write object to git" <+> pretty h
G.updateProgress pb 1
nl
for_ (LBS.lines hd) (sendLn . LBS.toStrict) for_ (LBS.lines hd) (sendLn . LBS.toStrict)
sendEol sendEol
next next
@ -211,16 +183,16 @@ loop args = do
let bra = BS.split ':' rr let bra = BS.split ':' rr
let pu = fmap (fromString' . BS.unpack) bra let pu = fmap (fromString' . BS.unpack) bra
liftIO $ atomically $ writeTVar batch True liftIO $ atomically $ writeTVar batch True
-- debug $ "FUCKING PUSH" <> viaShow rr <+> pretty pu
-- shutUp
pushed <- push ref pu pushed <- push ref pu
case pushed of case pushed of
Nothing -> hPrint stderr "fucked!" >> sendEol Nothing -> hPrint stderr "oopsie!" >> sendEol >> shutUp
Just re -> sendLn [qc|ok {pretty re}|] Just re -> sendLn [qc|ok {pretty re}|]
next next
other -> die $ show other other -> die $ show other
-- updateLocalState ref
where where
fromString' "" = Nothing fromString' "" = Nothing
fromString' x = Just $ fromString x fromString' x = Just $ fromString x

View File

@ -6,8 +6,6 @@ import HBS2.Data.Types.Refs
import HBS2.OrDie import HBS2.OrDie
import HBS2.System.Logger.Simple import HBS2.System.Logger.Simple
import HBS2.Net.Auth.Credentials hiding (getCredentials) import HBS2.Net.Auth.Credentials hiding (getCredentials)
-- import HBS2.Merkle
-- import HBS2.Hash
import HBS2.Git.Local import HBS2.Git.Local
import HBS2.Git.Local.CLI import HBS2.Git.Local.CLI
@ -16,22 +14,17 @@ import HBS2Git.Config as Config
import HBS2Git.Types import HBS2Git.Types
import HBS2Git.State import HBS2Git.State
import HBS2Git.App import HBS2Git.App
import HBS2Git.Export (export) import HBS2Git.Export (exportRefOnly,exportRefDeleted)
import HBS2Git.Import (importRefLogNew)
import GitRemoteTypes import GitRemoteTypes
import Data.Maybe
import Control.Monad.Reader import Control.Monad.Reader
import Data.Functor import Data.Functor
import Data.Set (Set) import Data.Set (Set)
import Data.Set qualified as Set
import Lens.Micro.Platform
import Data.HashMap.Strict qualified as HashMap
import Text.InterpolatedString.Perl6 (qc) import Text.InterpolatedString.Perl6 (qc)
import Data.ByteString qualified as BS
import Control.Concurrent.STM.TVar
import Control.Concurrent.STM
import Control.Monad.Catch import Control.Monad.Catch
import Control.Monad.Trans.Resource
newtype RunWithConfig m a = newtype RunWithConfig m a =
WithConfig { fromWithConf :: ReaderT [Syntax C] m a } WithConfig { fromWithConf :: ReaderT [Syntax C] m a }
@ -43,6 +36,8 @@ newtype RunWithConfig m a =
, MonadTrans , MonadTrans
, MonadThrow , MonadThrow
, MonadCatch , MonadCatch
-- , MonadMask
, MonadUnliftIO
) )
@ -65,43 +60,37 @@ instance MonadIO m => HasRefCredentials (RunWithConfig (GitRemoteApp m)) where
push :: forall m . ( MonadIO m push :: forall m . ( MonadIO m
, MonadCatch m , MonadCatch m
, HasProgress (RunWithConfig (GitRemoteApp m)) , HasProgress (RunWithConfig (GitRemoteApp m))
, MonadUnliftIO m
) )
=> RepoRef -> [Maybe GitRef] -> GitRemoteApp m (Maybe GitRef) => RepoRef -> [Maybe GitRef] -> GitRemoteApp m (Maybe GitRef)
push remote [bFrom , Just br] = do
push remote what@[Just bFrom , Just br] = do
(_, syn) <- Config.configInit (_, syn) <- Config.configInit
dbPath <- makeDbPath remote dbPath <- makeDbPath remote
db <- dbEnv dbPath db <- dbEnv dbPath
runWithConfig syn do runWithConfig syn do
_ <- cfgValue @ConfBranch @(Set GitRef) <&> transformBi normalizeRef
brCfg <- cfgValue @ConfBranch @(Set GitRef) <&> transformBi normalizeRef
loadCredentials mempty loadCredentials mempty
trace $ "PUSH PARAMS" <+> pretty what
gh <- gitGetHash (normalizeRef bFrom) `orDie` [qc|can't read hash for ref {pretty br}|]
_ <- traceTime "TIME: exportRefOnly" $ exportRefOnly () remote (Just bFrom) br gh
importRefLogNew False remote
pure (Just br)
oldHead <- readHead db <&> fromMaybe mempty push remote [Nothing, Just br] = do
(_, syn) <- Config.configInit
newHead <- case bFrom of runWithConfig syn do
Just newBr -> do _ <- cfgValue @ConfBranch @(Set GitRef) <&> transformBi normalizeRef
gh <- gitGetHash (normalizeRef newBr) `orDie` [qc|can't read hash for ref {pretty newBr}|] loadCredentials mempty
pure $ over repoHeads (HashMap.insert br gh) oldHead trace $ "deleting remote reference" <+> pretty br
exportRefDeleted () remote br
Nothing -> do importRefLogNew False remote
warn $ "about to delete branch" <+> pretty br <+> pretty "in" <+> pretty remote
when ( br `Set.member` brCfg ) do
err $ "remove" <+> pretty br <+> "from config first"
exitFailure
pure $ over repoHeads (HashMap.delete br) oldHead
(root, hh) <- export remote newHead
info $ "head:" <+> pretty hh
info $ "merkle:" <+> pretty root
pure (Just br) pure (Just br)
push r w = do push r w = do

View File

@ -14,6 +14,7 @@ import Data.HashMap.Strict qualified as HashMap
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad.Catch import Control.Monad.Catch
import Control.Monad.Trans.Resource
data RemoteEnv = data RemoteEnv =
RemoteEnv RemoteEnv
@ -35,6 +36,7 @@ newtype GitRemoteApp m a =
, MonadReader RemoteEnv , MonadReader RemoteEnv
, MonadThrow , MonadThrow
, MonadCatch , MonadCatch
, MonadUnliftIO
) )
runRemoteM :: MonadIO m => RemoteEnv -> GitRemoteApp m a -> m a runRemoteM :: MonadIO m => RemoteEnv -> GitRemoteApp m a -> m a

View File

@ -23,6 +23,7 @@ main = join . customExecParser (prefs showHelpOnError) $
parser = hsubparser ( command "export" (info pExport (progDesc "export repo")) parser = hsubparser ( command "export" (info pExport (progDesc "export repo"))
<> command "list-refs" (info pListRefs (progDesc "list refs")) <> command "list-refs" (info pListRefs (progDesc "list refs"))
<> command "show" (info pShow (progDesc "show various types of objects")) <> command "show" (info pShow (progDesc "show various types of objects"))
<> command "tools" (info pTools (progDesc "misc tools"))
) )
pExport = do pExport = do
@ -40,4 +41,19 @@ main = join . customExecParser (prefs showHelpOnError) $
pShow = do pShow = do
object <- optional $ object <- optional $
argument (maybeReader showReader) (metavar "object" <> help "<HASH-REF> | config") argument (maybeReader showReader) (metavar "object" <> help "<HASH-REF> | config")
pure $ runApp NoLog (runShow object) pure $ runApp NoLog (runShow object)
pTools = hsubparser ( command "scan" (info pToolsScan (progDesc "scan reference"))
<> command "refs" (info pToolsGetRefs (progDesc "list references"))
)
pToolsScan = do
ref <- strArgument (metavar "HASH-REF")
pure $ runApp WithLog (runToolsScan ref)
pToolsGetRefs = do
ref <- strArgument (metavar "HASH-REF")
pure $ runApp WithLog (runToolsGetRefs ref)

View File

@ -16,14 +16,16 @@ data ShowObject = ShowRef RepoRef | ShowConfig
showRef :: MonadIO m => RepoRef -> App m () showRef :: MonadIO m => RepoRef -> App m ()
showRef h = do showRef h = do
db <- makeDbPath h >>= dbEnv db <- makeDbPath h >>= dbEnv
withDB db do -- FIXME: re-implement-showRef
hd <- stateGetHead pure ()
imported <- stateGetLastImported 10 -- withDB db do
liftIO $ do -- hd <- stateGetHead
print $ "current state for" <+> pretty (AsBase58 h) -- imported <- stateGetLastImported 10
print $ "head:" <+> pretty hd -- liftIO $ do
print $ pretty "last operations:" -- print $ "current state for" <+> pretty (AsBase58 h)
for_ imported (\(t,h1,h2) -> print $ pretty t <+> pretty h1 <+> pretty h2) -- print $ "head:" <+> pretty hd
-- print $ pretty "last operations:"
-- for_ imported (\(t,h1,h2) -> print $ pretty t <+> pretty h1 <+> pretty h2)
showRefs :: MonadIO m => App m () showRefs :: MonadIO m => App m ()
showRefs = do showRefs = do

View File

@ -63,27 +63,33 @@ common shared-properties
, containers , containers
, cryptonite , cryptonite
, directory , directory
, exceptions
, filelock
, filepath , filepath
, hashable
, http-conduit
, interpolatedstring-perl6 , interpolatedstring-perl6
, memory , memory
, microlens-platform , microlens-platform
, mtl , mtl
, prettyprinter , prettyprinter
, prettyprinter-ansi-terminal , prettyprinter-ansi-terminal
, resourcet
, safe , safe
, serialise , serialise
, sqlite-simple
, stm
, suckless-conf , suckless-conf
, temporary
, text , text
, time
, timeit
, transformers , transformers
, typed-process , typed-process
, uniplate , uniplate
, hashable , unliftio
, sqlite-simple , unliftio-core
, stm
, unordered-containers , unordered-containers
, filelock
, http-conduit
, exceptions
library library
import: shared-properties import: shared-properties
@ -99,7 +105,7 @@ library
HBS2Git.Config HBS2Git.Config
HBS2Git.App HBS2Git.App
HBS2Git.State HBS2Git.State
HBS2Git.Update HBS2Git.GitRepoLog
-- other-modules: -- other-modules:
-- other-extensions: -- other-extensions:
@ -108,6 +114,7 @@ library
, terminal-progress-bar , terminal-progress-bar
, http-types , http-types
, uuid , uuid
, zlib
hs-source-dirs: lib hs-source-dirs: lib
default-language: Haskell2010 default-language: Haskell2010

View File

@ -1,13 +1,17 @@
{-# Language AllowAmbiguousTypes #-} {-# Language AllowAmbiguousTypes #-}
module HBS2.Git.Local.CLI where module HBS2.Git.Local.CLI
( module HBS2.Git.Local.CLI
, getStdin
, stopProcess
) where
import HBS2.Prelude.Plated
import HBS2.Git.Types import HBS2.Git.Types
import HBS2.System.Logger.Simple import HBS2.System.Logger.Simple
import Control.Concurrent.Async import Control.Concurrent.Async
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad.IO.Class
import Control.Monad.Writer import Control.Monad.Writer
import Data.HashSet (HashSet) import Data.HashSet (HashSet)
import Data.HashSet qualified as HashSet import Data.HashSet qualified as HashSet
@ -21,15 +25,13 @@ import Data.Function
import Data.Maybe import Data.Maybe
import Data.Set qualified as Set import Data.Set qualified as Set
import Data.Set (Set) import Data.Set (Set)
import Data.String import Data.List qualified as List
import Data.Text.Encoding qualified as Text import Data.Text.Encoding qualified as Text
import Data.Text.Encoding (decodeLatin1) import Data.Text.Encoding (decodeLatin1)
import Data.Text qualified as Text import Data.Text qualified as Text
import Data.Text (Text)
import Prettyprinter
import Safe
import System.Process.Typed import System.Process.Typed
import Text.InterpolatedString.Perl6 (qc) import Text.InterpolatedString.Perl6 (qc)
import System.IO
-- FIXME: specify-git-dir -- FIXME: specify-git-dir
@ -68,9 +70,31 @@ gitGetDepsPure (GitObject Commit bs) = Set.fromList (recurse ls)
gitGetDepsPure _ = mempty gitGetDepsPure _ = mempty
gitCommitGetParentsPure :: LBS.ByteString -> [GitHash]
gitCommitGetParentsPure bs = foldMap seek pairs
where
pairs = take 2 . LBS.words <$> LBS.lines bs
seek = \case
["parent", x] -> [fromString (LBS.unpack x)]
_ -> mempty
data GitParsedRef = GitCommitRef GitHash
| GitTreeRef GitHash
deriving stock (Data,Eq,Ord)
gitGetParsedCommit :: MonadIO m => GitObject -> m [GitParsedRef]
gitGetParsedCommit (GitObject Commit bs) = do
let ws = fmap LBS.words (LBS.lines bs)
oo <- forM ws $ \case
["tree", s] -> pure [GitTreeRef (fromString (LBS.unpack s))]
["commit", s] -> pure [GitCommitRef (fromString (LBS.unpack s))]
_ -> pure mempty
pure $ mconcat oo
gitGetParsedCommit _ = pure mempty
-- FIXME: use-fromStringMay
gitGetObjectType :: MonadIO m => GitHash -> m (Maybe GitObjectType) gitGetObjectType :: MonadIO m => GitHash -> m (Maybe GitObjectType)
gitGetObjectType hash = do gitGetObjectType hash = do
(_, out, _) <- readProcess (shell [qc|git cat-file -t {pretty hash}|]) (_, out, _) <- readProcess (shell [qc|git cat-file -t {pretty hash}|])
@ -105,11 +129,13 @@ gitGetDependencies hash = do
_ -> pure mempty _ -> pure mempty
-- | calculates all dependencies of given list
-- of git objects
gitGetAllDependencies :: MonadIO m gitGetAllDependencies :: MonadIO m
=> Int => Int -- ^ number of threads
-> [ GitHash ] -> [ GitHash ] -- ^ initial list of objects to calculate deps
-> ( GitHash -> IO [GitHash] ) -> ( GitHash -> IO [GitHash] ) -- ^ lookup function
-> ( GitHash -> IO () ) -> ( GitHash -> IO () ) -- ^ progress update function
-> m [(GitHash, GitHash)] -> m [(GitHash, GitHash)]
gitGetAllDependencies n objects lookup progress = liftIO do gitGetAllDependencies n objects lookup progress = liftIO do
@ -182,6 +208,24 @@ gitGetTransitiveClosure cache exclude hash = do
pure res pure res
-- gitGetAllDepsByCommit :: GitHash -> IO [GitHash]
-- gitGetAllDepsByCommit h = do
-- -- FIXME: error-handling
-- (_, out, _) <- liftIO $ readProcess (shell [qc|git rev-list {pretty h}|])
-- let ls = LBS.lines out & fmap ( fromString . LBS.unpack )
-- forM ls $ \l -> do
-- o <- liftIO $ gitReadObject (Just Commit) l
-- let tree = gitGetDepsPure (GitObject Commit o)
-- (_, out, _) <- liftIO $ readProcess (shell [qc|git rev-list {pretty h}|])
-- print tree
-- -- mapM_ (print.pretty) ls
-- pure []
-- deps <- mapM gitGetDependencies ls <&> mconcat
-- pure $ List.nub $ ls <> deps
-- FIXME: inject-git-working-dir-via-typeclass -- FIXME: inject-git-working-dir-via-typeclass
gitConfigGet :: MonadIO m => Text -> m (Maybe Text) gitConfigGet :: MonadIO m => Text -> m (Maybe Text)
@ -277,8 +321,16 @@ gitStoreObject (GitObject t s) = do
ExitSuccess -> pure $ Just (parseHashLazy out) ExitSuccess -> pure $ Just (parseHashLazy out)
ExitFailure{} -> pure Nothing ExitFailure{} -> pure Nothing
gitCheckObject :: MonadIO m => GitHash -> m Bool
gitCheckObject gh = do
let cmd = [qc|git cat-file -e {pretty gh}|]
let procCfg = setStderr closed (shell cmd)
(code, _, _) <- readProcess procCfg
case code of
ExitSuccess -> pure True
ExitFailure{} -> pure False
gitListAllObjects :: MonadIO m => m [GitHash] gitListAllObjects :: MonadIO m => m [(GitObjectType, GitHash)]
gitListAllObjects = do gitListAllObjects = do
let cmd = [qc|git cat-file --batch-check --batch-all-objects|] let cmd = [qc|git cat-file --batch-check --batch-all-objects|]
let procCfg = setStdin closed $ setStderr closed (shell cmd) let procCfg = setStdin closed $ setStderr closed (shell cmd)
@ -288,7 +340,7 @@ gitListAllObjects = do
where where
fromLine = \case fromLine = \case
[ha, _, _] -> [fromString (LBS.unpack ha)] [ha, tp, _] -> [(fromString (LBS.unpack tp), fromString (LBS.unpack ha))]
_ -> [] _ -> []
-- FIXME: better error handling -- FIXME: better error handling
@ -330,3 +382,82 @@ gitListLocalBranches = do
_ -> [] _ -> []
gitListAllCommits :: MonadIO m => m [GitHash]
gitListAllCommits = do
let cmd = [qc|git log --all --pretty=format:'%H'|]
let procCfg = setStdin closed $ setStderr closed (shell cmd)
(_, out, _) <- readProcess procCfg
pure $ fmap (fromString . LBS.unpack) (LBS.lines out)
gitRunCommand :: MonadIO m => String -> m (Either ExitCode ByteString)
gitRunCommand cmd = do
let procCfg = setStdin closed $ setStderr closed (shell cmd)
(code, out, _) <- readProcess procCfg
case code of
ExitSuccess -> pure (Right out)
e -> pure (Left e)
-- | list all commits from the given one in order of date
gitListAllCommitsExceptBy :: MonadIO m => Set GitHash -> Maybe GitHash -> GitHash -> m [GitHash]
gitListAllCommitsExceptBy excl l h = do
let from = maybe mempty (\r -> [qc|{pretty r}..|] ) l
let cmd = [qc|git rev-list --reverse --date-order {from}{pretty h}|]
let procCfg = setStdin closed $ setStderr closed (shell cmd)
(_, out, _) <- readProcess procCfg
let res = fmap (fromString . LBS.unpack) (LBS.lines out)
pure $ List.reverse $ filter ( not . flip Set.member excl) res
-- | list all objects for the given commit range in order of date
gitRevList :: MonadIO m => Maybe GitHash -> GitHash -> m [GitHash]
gitRevList l h = do
let from = maybe mempty (\r -> [qc|{pretty r}..|] ) l
-- let cmd = [qc|git rev-list --objects --in-commit-order --reverse --date-order {from}{pretty h}|]
let cmd = [qc|git rev-list --objects --reverse --in-commit-order {from}{pretty h}|]
let procCfg = setStdin closed $ setStderr closed (shell cmd)
(_, out, _) <- readProcess procCfg
pure $ mapMaybe (fmap (fromString . LBS.unpack) . headMay . LBS.words) (LBS.lines out)
-- TODO: handle-invalid-input-somehow
gitGetObjectTypeMany :: MonadIO m => [GitHash] -> m [(GitHash, GitObjectType)]
gitGetObjectTypeMany hashes = do
let hss = LBS.unlines $ fmap (LBS.pack.show.pretty) hashes
let cmd = [qc|git cat-file --batch-check='%(objectname) %(objecttype)'|]
let procCfg = setStdin (byteStringInput hss) $ setStderr closed (shell cmd)
(_, out, _) <- readProcess procCfg
pure $ mapMaybe (parse . fmap LBS.unpack . LBS.words) (LBS.lines out)
where
parse [h,tp] = (,) <$> fromStringMay h <*> fromStringMay tp
parse _ = Nothing
gitGetCommitImmediateDeps :: MonadIO m => GitHash -> m [GitHash]
gitGetCommitImmediateDeps h = do
o <- gitReadObject (Just Commit) h
let lws = LBS.lines o & fmap LBS.words
t <- forM lws $ \case
["tree", hs] -> pure (Just ( fromString @GitHash (LBS.unpack hs) ))
_ -> pure Nothing
let tree = take 1 $ catMaybes t
deps <- gitRunCommand [qc|git rev-list --objects {pretty (headMay tree)}|]
>>= either (const $ pure mempty)
(pure . mapMaybe withLine . LBS.lines)
pure $ List.nub $ tree <> deps
where
withLine :: LBS.ByteString -> Maybe GitHash
withLine l = do
let wordsInLine = LBS.words l
firstWord <- listToMaybe wordsInLine
pure $ fromString @GitHash $ LBS.unpack firstWord
startGitHashObject :: GitObjectType -> IO (Process Handle () ())
startGitHashObject objType = do
let cmd = "git"
let args = ["hash-object", "-w", "-t", show (pretty objType), "--stdin-paths"]
let config = setStdin createPipe $ setStdout closed $ setStderr inherit $ proc cmd args
startProcess config

View File

@ -41,6 +41,11 @@ instance Serialise GitHash
instance IsString GitHash where instance IsString GitHash where
fromString s = GitHash (B16.decodeLenient (BS.pack s)) fromString s = GitHash (B16.decodeLenient (BS.pack s))
instance FromStringMaybe GitHash where
fromStringMay s = either (const Nothing) pure (GitHash <$> B16.decode bs)
where
bs = BS.pack s
instance Pretty GitHash where instance Pretty GitHash where
pretty (GitHash s) = pretty @String [qc|{B16.encode s}|] pretty (GitHash s) = pretty @String [qc|{B16.encode s}|]
@ -58,6 +63,13 @@ instance IsString GitObjectType where
"blob" -> Blob "blob" -> Blob
x -> error [qc|invalid git object type {x}|] x -> error [qc|invalid git object type {x}|]
instance FromStringMaybe GitObjectType where
fromStringMay = \case
"commit" -> Just Commit
"tree" -> Just Tree
"blob" -> Just Blob
_ -> Nothing
instance Pretty GitObjectType where instance Pretty GitObjectType where
pretty = \case pretty = \case
Commit -> pretty @String "commit" Commit -> pretty @String "commit"

View File

@ -432,26 +432,6 @@ makeDbPath h = do
liftIO $ createDirectoryIfMissing True state liftIO $ createDirectoryIfMissing True state
pure $ state </> show (pretty (AsBase58 h)) pure $ state </> show (pretty (AsBase58 h))
readHead :: (MonadIO m, HasCatAPI m) => DBEnv -> m (Maybe RepoHead)
readHead db = runMaybeT do
href <- MaybeT $ withDB db stateGetHead
trace $ "repoHead" <+> pretty href
bs <- MaybeT $ readObject href
let toParse = fmap LBS.words ( LBS.lines bs )
let fromSymb = Just . fromString . LBS.unpack . LBS.dropWhile (=='@')
let fromBS :: forall a . IsString a => LBS.ByteString -> a
fromBS = fromString . LBS.unpack
let parsed = flip foldMap toParse $ \case
[a,"HEAD"] -> [RepoHead (fromSymb a) mempty]
[h,r] -> [RepoHead Nothing (HashMap.singleton (fromBS r) (fromBS h))]
_ -> mempty
pure $ mconcat parsed
loadCredentials :: ( MonadIO m loadCredentials :: ( MonadIO m
, HasConf m , HasConf m
, HasRefCredentials m , HasRefCredentials m

View File

@ -2,11 +2,9 @@
module HBS2Git.Export where module HBS2Git.Export where
import HBS2.Prelude.Plated import HBS2.Prelude.Plated
import HBS2.Clock
import HBS2.Data.Types.Refs import HBS2.Data.Types.Refs
import HBS2.OrDie import HBS2.OrDie
import HBS2.System.Logger.Simple import HBS2.System.Logger.Simple
import HBS2.Merkle
import HBS2.Net.Proto.Definition() import HBS2.Net.Proto.Definition()
import HBS2.Base58 import HBS2.Base58
@ -15,203 +13,258 @@ import HBS2.Git.Local.CLI
import HBS2Git.App import HBS2Git.App
import HBS2Git.State import HBS2Git.State
import HBS2Git.Update
import HBS2Git.Config import HBS2Git.Config
import HBS2Git.GitRepoLog
import Data.Functor
import Data.List (sortBy)
import Control.Applicative import Control.Applicative
import Control.Monad.Catch
import Control.Monad.Reader import Control.Monad.Reader
import UnliftIO.Async
import Control.Concurrent.STM
import Data.ByteString.Lazy.Char8 qualified as LBS import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.Cache as Cache
import Data.Foldable (for_) import Data.Foldable (for_)
import Data.Functor
import Data.HashMap.Strict qualified as HashMap import Data.HashMap.Strict qualified as HashMap
import Data.HashSet qualified as HashSet import Data.HashSet qualified as HashSet
import Data.HashSet (HashSet)
import Data.Maybe import Data.Maybe
import Data.Set qualified as Set import Data.Set qualified as Set
import Data.Set (Set) import Data.Map qualified as Map
import Data.List qualified as List
import Lens.Micro.Platform import Lens.Micro.Platform
import Control.Concurrent.STM import Prettyprinter.Render.Terminal
import Control.Concurrent.Async
import Control.Monad.Catch
import Text.InterpolatedString.Perl6 (qc)
import System.Directory import System.Directory
import System.FilePath import System.FilePath
import Prettyprinter.Render.Terminal import Text.InterpolatedString.Perl6 (qc)
import UnliftIO.IO
import System.IO hiding (hClose,hPrint)
import System.IO.Temp
import Control.Monad.Trans.Resource
data HashCache = class ExportRepoOps a where
HashCache
{ hCache :: Cache GitHash (Set GitHash)
, hDb :: DBEnv
}
instance Hashable GitHash => HasCache HashCache GitHash (Set GitHash) IO where instance ExportRepoOps ()
cacheInsert (HashCache cache _) = Cache.insert cache
cacheLookup (HashCache cache db) k = do exportRefDeleted :: forall o m . ( MonadIO m
refs <- withDB db (stateGetDeps k)
case refs of
[] -> Cache.lookup' cache k
xs -> pure $ Just $ Set.fromList xs
newHashCache :: MonadIO m => DBEnv -> m HashCache
newHashCache db = do
ca <- liftIO $ Cache.newCache Nothing
pure $ HashCache ca db
export :: forall m . ( MonadIO m
, MonadCatch m , MonadCatch m
-- , MonadMask m
, MonadUnliftIO m
, HasCatAPI m , HasCatAPI m
, HasConf m , HasConf m
, HasRefCredentials m , HasRefCredentials m
, HasProgress m , HasProgress m
) => RepoRef -> RepoHead -> m (HashRef, HashRef) , ExportRepoOps o
export h repoHead = do )
=> o
-> RepoRef
-> GitRef
-> m HashRef
exportRefDeleted _ repo ref = do
trace $ "exportRefDeleted" <+> pretty repo <+> pretty ref
let refs = HashMap.toList (view repoHeads repoHead) dbPath <- makeDbPath repo
db <- dbEnv dbPath
-- это "ненормальный" лог, т.е удаление ссылки в текущем контексте
-- мы удаляем ссылку "там", то есть нам нужно "то" значение ссылки
-- удалить её локально мы можем и так, просто гитом.
-- NOTE: empty-log-post
-- мы тут постим пустой лог (не содержащий коммитов)
-- нам нужно будет найти его позицию относитеьлно прочих логов.
-- его контекст = текущее значение ссылки, которое мы удаляем
-- но вот если мы удаляем уже удаленную ссылку, то для ссылки 00..0
-- будет ошибка где-то.
vals <- withDB db $ stateGetActualRefs <&> List.nub . fmap snd
let (ctxHead, ctxBs) = makeContextEntry vals
trace $ "DELETING REF CONTEXT" <+> pretty vals
let repoHead = RepoHead Nothing (HashMap.fromList [(ref,"0000000000000000000000000000000000000000")])
let repoHeadStr = (LBS.pack . show . pretty . AsGitRefsFile) repoHead
let ha = gitHashObject (GitObject Blob repoHeadStr)
let headEntry = GitLogEntry GitLogEntryHead (Just ha) ( fromIntegral $ LBS.length repoHeadStr )
let content = gitRepoLogMakeEntry ctxHead ctxBs
<> gitRepoLogMakeEntry headEntry repoHeadStr
-- FIXME: remove-code-dup
let meta = fromString $ show
$ "hbs2-git" <> line
<> "type:" <+> "hbs2-git-push-log"
<> line
logMerkle <- storeObject meta content `orDie` [qc|Can't store push log|]
postRefUpdate repo 0 logMerkle
pure logMerkle
makeContextEntry :: [GitHash] -> (GitLogEntry, LBS.ByteString)
makeContextEntry hashes = (entryHead, payload)
where
ha = Nothing
payload = GitLogContextCommits (HashSet.fromList hashes) & serialise
entryHead = GitLogEntry GitLogContext ha undefined
-- | Exports only one ref to the repo.
-- Corresponds to a single ```git push``` operation
exportRefOnly :: forall o m . ( MonadIO m
, MonadCatch m
-- , MonadMask m
, MonadUnliftIO m
, HasCatAPI m
, HasConf m
, HasRefCredentials m
, HasProgress m
, ExportRepoOps o
)
=> o
-> RepoRef
-> Maybe GitRef
-> GitRef
-> GitHash
-> m HashRef
exportRefOnly _ remote rfrom ref val = do
let repoHead = RepoHead Nothing (HashMap.fromList [(ref,val)])
let repoHeadStr = (LBS.pack . show . pretty . AsGitRefsFile) repoHead let repoHeadStr = (LBS.pack . show . pretty . AsGitRefsFile) repoHead
dbPath <- makeDbPath h dbPath <- makeDbPath remote
trace $ "dbPath" <+> pretty dbPath
db <- dbEnv dbPath db <- dbEnv dbPath
sp <- withDB db savepointNew trace $ "exportRefOnly" <+> pretty remote <+> pretty ref <+> pretty val
withDB db $ savepointBegin sp -- 1. get max ref value for known REMOTE branch
-- 2. if unkwnown - get max branch ref value for known LOCAL branch (known from the state)
-- 3. if unkwnown - then Nothing
-- therefore, we export only the delta for the objects for push between known state and current
-- git repot state
-- if it's a new branch push without any objects commited -- then empty log
-- only with HEAD section should be created
lastKnownRev <- withDB db do
rThat <- stateGetActualRefValue ref
rThis <- maybe1 rfrom (pure Nothing) stateGetActualRefValue
pure $ rThat <|> rThis
rr <- try $ do trace $ "LAST_KNOWN_REV" <+> braces (pretty rfrom) <+> braces (pretty ref) <+> braces (pretty lastKnownRev)
skip <- withDB db stateGetExported <&> HashSet.fromList entries <- gitRevList lastKnownRev val
-- TODO: process-only-commits-to-make-first-run-faster -- NOTE: just-for-test-new-non-empty-push-to-another-branch-112
ooo <- gitListAllObjects <&> filter (not . (`HashSet.member` skip))
cached0 <- withDB db stateGetAllDeps -- FIXME: may-blow-on-huge-repo-export
let cached = HashMap.fromListWith (<>) [ (k, [v]) | (k,v) <- cached0 ] types <- gitGetObjectTypeMany entries <&> Map.fromList
let lookup h = pure $ HashMap.lookup h cached & fromMaybe mempty
monDep <- newProgressMonitor "calculate dependencies" (length ooo) let lookupType t = Map.lookup t types
let justOrDie msg x = pure x `orDie` msg
allDeps <- gitGetAllDependencies 4 ooo lookup (const $ updateProgress monDep 1) trace $ "ENTRIES:" <+> pretty (length entries)
let sz = length allDeps trace "MAKING OBJECTS LOG"
mon1 <- newProgressMonitor "storing dependencies" sz
withDB db $ transactional do let fname = [qc|{pretty val}.data|]
for_ allDeps $ \(obj,dep) -> do
updateProgress mon1 1
stateAddDep dep obj
deps <- withDB db $ do runResourceT $ do
x <- forM refs $ stateGetDepsRec . snd
pure $ mconcat x
withDB db $ transactional do -- to speedup inserts written <- liftIO $ newTVarIO (HashSet.empty :: HashSet GitHash)
let metaApp = "application:" <+> "hbs2-git" <> line let myTempDir = "hbs-git"
temp <- liftIO getCanonicalTemporaryDirectory
(_,dir) <- allocate (createTempDirectory temp myTempDir) removeDirectoryRecursive
let metaHead = fromString $ show let fpath = dir </> fname
$ metaApp <> "type:" <+> "head" <> line fh <- liftIO $ openBinaryFile fpath AppendMode
-- let gha = gitHashObject (GitObject Blob repoHead) expMon <- newProgressMonitor "export objects" (length entries)
hh <- lift $ storeObject metaHead repoHeadStr `orDie` "cant save repo head"
mon3 <- newProgressMonitor "export objects from repo" (length deps) enq <- liftIO newTQueueIO
for_ deps $ \d -> do -- FIXME: export-wtf?
here <- stateGetHash d <&> isJust
-- FIXME: asap-check-if-objects-is-in-hbs2
unless here do
lbs <- gitReadObject Nothing d
-- TODO: why-not-default-blob aread <- async $ do
-- anything is blob for_ entries $ \d -> do
tp <- gitGetObjectType d <&> fromMaybe Blob -- here <- liftIO $ readTVarIO written <&> HashSet.member d
inState <- withDB db (stateIsLogObjectExists d)
updateProgress expMon 1
unless (here || inState) do
tp <- lookupType d & justOrDie [qc|no object type for {pretty d}|]
o <- gitReadObject (Just tp) d
let entry = GitLogEntry ( gitLogEntryTypeOf tp ) (Just d) ( fromIntegral $ LBS.length o )
liftIO $ atomically $ writeTQueue enq (Just (d,tp,entry,o))
let metaO = fromString $ show liftIO $ atomically $ writeTQueue enq Nothing
$ metaApp
<> "type:" <+> pretty tp <+> pretty d fix \next -> do
mbEntry <- liftIO $ atomically $ readTQueue enq
case mbEntry of
Nothing -> pure ()
Just (d,tp,entry,o) -> do
gitRepoLogWriteEntry fh entry o
liftIO $ atomically $ modifyTVar written (HashSet.insert d)
trace $ "writing" <+> pretty tp <+> pretty d
-- TODO: here-split-log-to-parts
next
mapM_ wait [aread]
-- FIXME: problem-log-is-not-assotiated-with-commit
-- Если так получилось, что в журнале подъехала только ссылка,
-- и больше нет никакой информации -- мы не можем определить
-- глубину(высоту?) этой ссылки, и, соответственно, вычислить
-- её depth в стейте.
-- Решение: в этом (или иных) случаях добавлять информацию о контексте,
-- например, состояние других известных ссылок в моменте. Список ссылок
-- берём из state, полагая, что раз ссылка в стейте, значит, она является
-- важной. Имея эту информацию, мы можем хоть как-то вычислять depth
-- этого лога. Похоже на векторные часы, кстати.
-- это "нормальный" лог. даже если хвост его приедет пустым (не будет коммитов)
-- тут мы запомним, что его контекст = коммит, на который он устанавливает ссылку
-- и этот коммит должен быть в секциях лога, которые приехали перед ним.
-- следствие: у предыдущего лога будет такая же глубина, как и у этого.
vals <- withDB db $ stateGetActualRefs <&> List.nub . fmap snd
let (e, bs) = makeContextEntry (val:vals)
trace $ "writing context entry" <+> pretty [val]
gitRepoLogWriteEntry fh e bs
let ha = gitHashObject (GitObject Blob repoHeadStr)
let headEntry = GitLogEntry GitLogEntryHead (Just ha) ( fromIntegral $ LBS.length repoHeadStr )
gitRepoLogWriteEntry fh headEntry repoHeadStr
-- TODO: find-prev-push-log-and-make-ref
gitRepoLogWriteHead fh (GitLogHeadEntry Nothing)
hClose fh
trace "STORING PUSH LOG"
let meta = fromString $ show
$ "hbs2-git" <> line
<> "type:" <+> "hbs2-git-push-log"
<> line <> line
hr' <- lift $ storeObject metaO lbs content <- liftIO $ LBS.readFile fpath
logMerkle <- lift $ storeObject meta content `orDie` [qc|Can't store push log|]
maybe1 hr' (pure ()) $ \hr -> do trace $ "PUSH LOG HASH: " <+> pretty logMerkle
statePutHash tp d hr trace $ "POSTING REFERENCE UPDATE TRANSACTION" <+> pretty remote <+> pretty logMerkle
updateProgress mon3 1 -- FIXME: calculate-seqno-as-topsort-order
lift $ postRefUpdate remote 0 logMerkle
hashes <- (hh : ) <$> stateGetAllHashes pure logMerkle
let pt = toPTree (MaxSize 512) (MaxNum 512) hashes -- FIXME: settings runExport :: forall m . (MonadIO m, MonadUnliftIO m, MonadCatch m, HasProgress (App m))
tobj <- liftIO newTQueueIO
-- FIXME: progress-indicator
root <- makeMerkle 0 pt $ \(ha,_,bss) -> do
liftIO $ atomically $ writeTQueue tobj (ha,bss)
objs <- liftIO $ atomically $ flushTQueue tobj
mon2 <- newProgressMonitor "store objects" (length objs)
for_ objs $ \(ha,bss) -> do
updateProgress mon2 1
here <- lift $ getBlockSize (HashRef ha) <&> isJust
unless here do
void $ lift $ storeObject (fromString (show metaApp)) bss
trace "generate update transaction"
trace $ "objects:" <+> pretty (length hashes)
seqno <- stateGetSequence <&> succ
-- FIXME: same-transaction-different-seqno
postRefUpdate h seqno (HashRef root)
let noRef = do
pause @'Seconds 20
shutUp
die $ show $ pretty "No reference appeared for" <+> pretty h
wmon <- newProgressMonitor "waiting for ref" 20
void $ liftIO $ race noRef $ do
runApp NoLog do
fix \next -> do
v <- readRefHttp h
updateProgress wmon 1
case v of
Nothing -> pause @'Seconds 1 >> next
Just{} -> pure ()
withDB db $ transactional $ mapM_ statePutExported ooo
pure (HashRef root, hh)
case rr of
Left ( e :: SomeException ) -> do
withDB db (savepointRollback sp)
err $ viaShow e
shutUp
die "aborted"
Right r -> do
withDB db (savepointRelease sp)
pure r
runExport :: forall m . (MonadIO m, MonadCatch m, HasProgress (App m))
=> Maybe FilePath -> RepoRef -> App m () => Maybe FilePath -> RepoRef -> App m ()
runExport fp h = do runExport fp repo = do
liftIO $ putDoc $ liftIO $ putDoc $
line line
<> green "Exporting to reflog" <+> pretty (AsBase58 h) <> green "Exporting to reflog" <+> pretty (AsBase58 repo)
<> section <> section
<> "it may take some time on the first run" <> "it may take some time on the first run"
<> section <> section
@ -234,16 +287,20 @@ runExport fp h = do
fullHead <- gitHeadFullName headBranch fullHead <- gitHeadFullName headBranch
debug $ "HEAD" <+> pretty fullHead -- debug $ "HEAD" <+> pretty fullHead
let repoHead = RepoHead (Just fullHead) -- let repoHead = RepoHead (Just fullHead)
(HashMap.fromList refs) -- (HashMap.fromList refs)
trace $ "NEW REPO HEAD" <+> pretty (AsGitRefsFile repoHead) -- trace $ "NEW REPO HEAD" <+> pretty (AsGitRefsFile repoHead)
(root, hhh) <- export h repoHead val <- gitGetHash fullHead `orDie` [qc|Can't resolve ref {pretty fullHead}|]
updateLocalState h -- _ <- exportRefOnly () remote br gh
hhh <- exportRefOnly () repo Nothing fullHead val
-- NOTE: ???
-- traceTime "importRefLogNew (export)" $ importRefLogNew False repo
shutUp shutUp
@ -251,7 +308,6 @@ runExport fp h = do
cfgPath <- configPath cwd cfgPath <- configPath cwd
let krf = fromMaybe "keyring-file" fp & takeFileName let krf = fromMaybe "keyring-file" fp & takeFileName
liftIO $ putStrLn "" liftIO $ putStrLn ""
liftIO $ putDoc $ liftIO $ putDoc $
"exported" <+> pretty hhh "exported" <+> pretty hhh
@ -269,7 +325,7 @@ runExport fp h = do
<> section <> section
<> green "Add git remote:" <> green "Add git remote:"
<> section <> section
<> pretty [qc|git remote add remotename hbs2://{pretty h}|] <> pretty [qc|git remote add remotename hbs2://{pretty repo}|]
<> section <> section
<> green "Work with git as usual:" <> green "Work with git as usual:"
<> section <> section

View File

@ -0,0 +1,180 @@
{-# Language TemplateHaskell #-}
module HBS2Git.GitRepoLog where
import HBS2.Prelude.Plated
import HBS2.Git.Types
import HBS2.Data.Types.Refs
import HBS2.System.Logger.Simple
import Data.Word
import Data.Function
import Lens.Micro.Platform
import Codec.Serialise
import Data.ByteString.Lazy qualified as LBS
import Data.ByteString.Lazy (ByteString)
-- import System.IO
import UnliftIO.IO
import Control.Monad.IO.Unlift
import Codec.Compression.GZip
import System.Directory
import Data.HashSet (HashSet)
import Data.HashSet qualified as HashSet
import Control.Concurrent.STM
import Data.Maybe
data GitLogEntryType = GitLogEntryCommit
| GitLogEntryBlob
| GitLogEntryTree
| GitLogEntryHead
| GitLogHead
| GitLogDeps
| GitLogHeadDel
| GitLogContext
deriving stock (Eq,Ord,Enum,Generic,Show)
newtype GitLogTimeStamp = GitLogTimeStamp Int
deriving stock (Eq,Ord,Show,Data,Generic)
instance Serialise GitLogTimeStamp
newtype GitLogHeadEntry =
GitLogHeadEntry
{ _gitLogHeadAfter :: Maybe HashRef
}
deriving stock (Eq,Generic)
instance Serialise GitLogHeadEntry
makeLenses ''GitLogHeadEntry
newtype GitLogDepsEntry =
GitLogDepsEntry
{ _gitLogDeps :: [HashRef]
}
deriving stock (Eq,Generic)
makeLenses ''GitLogDepsEntry
instance Serialise GitLogDepsEntry
-- deletion is handled by special way.
-- we need a context WHEN the reference is deleted
-- because it may be deleted, created again, deleted again, etc.
-- Having current repository context via collecting all reference states
-- we may calculate an actual current state of the reference.
-- Or, we may use a special code to mark object as deleted
data GitLogHeadDelEntry =
GitLogHeadDelEntry
{ _gitHeadContext :: [(GitRef, GitHash)] -- this gives us context to order this delete operation
, _gitHeadDeleted :: GitRef -- this is a reference to delete
}
deriving stock (Eq,Generic)
makeLenses ''GitLogHeadDelEntry
instance Serialise GitLogHeadDelEntry
data GitLogContextEntry =
GitLogNoContext
| GitLogContextCommits (HashSet GitHash)
deriving stock (Eq,Data,Generic)
commitsOfGitLogContextEntry :: GitLogContextEntry -> [GitHash]
commitsOfGitLogContextEntry = \case
GitLogNoContext -> mempty
GitLogContextCommits co -> HashSet.toList co
instance Serialise GitLogContextEntry
data GitLogEntry =
GitLogEntry
{ _gitLogEntryType :: GitLogEntryType
, _gitLogEntryHash :: Maybe GitHash
, _gitLogEntrySize :: Word32
}
deriving stock (Eq,Ord,Generic,Show)
makeLenses 'GitLogEntry
entryHeadSize :: Integral a => a
entryHeadSize = 64
instance Serialise GitLogEntryType
instance Serialise GitLogEntry
gitLogEntryTypeOf :: GitObjectType -> GitLogEntryType
gitLogEntryTypeOf = \case
Commit -> GitLogEntryCommit
Tree -> GitLogEntryTree
Blob -> GitLogEntryBlob
-- | scans hbs2-git repo log
gitRepoLogScan :: forall m . MonadUnliftIO m
=> Bool -- ^ do read log section content
-> FilePath -- ^ log file path
-> (GitLogEntry -> Maybe ByteString -> m ()) -- ^ log section callback
-> m ()
gitRepoLogScan r fn cb = do
trace $ "gitRepoLogScan" <+> pretty fn
withBinaryFile fn ReadMode $ \h -> do
sz <- liftIO $ getFileSize fn
go h sz
where
go _ 0 = pure ()
go h size = do
es <- liftIO $ LBS.hGet h entryHeadSize <&> deserialise @GitLogEntry
let esize = es ^. gitLogEntrySize
let consumed = entryHeadSize + fromIntegral esize
if r then do
o <- liftIO $ LBS.hGet h (fromIntegral esize) <&> decompress
cb es (Just o)
else do
liftIO $ hSeek h RelativeSeek (fromIntegral esize)
cb es Nothing
go h ( max 0 (size - consumed) )
gitRepoLogWriteHead :: forall m . MonadIO m => Handle -> GitLogHeadEntry -> m ()
gitRepoLogWriteHead fh e = do
let s = serialise e
let entry = GitLogEntry GitLogHead Nothing (fromIntegral $ LBS.length s)
gitRepoLogWriteEntry fh entry s
gitRepoLogMakeEntry :: GitLogEntry -> ByteString -> ByteString
gitRepoLogMakeEntry entry' o = bs <> ss
where
bs = LBS.take entryHeadSize $ serialise entry <> LBS.replicate entryHeadSize 0
ss = compressWith co o
entry = entry' & set gitLogEntrySize (fromIntegral $ LBS.length ss)
co = defaultCompressParams { compressLevel = bestSpeed }
-- TODO: use-gitRepoLogMakeEntry-in-body
gitRepoLogWriteEntry :: forall m . MonadIO m => Handle -> GitLogEntry -> ByteString -> m ()
gitRepoLogWriteEntry fh entry' o = do
let ss = compressWith co o
let entry = entry' & set gitLogEntrySize (fromIntegral $ LBS.length ss)
let bs = LBS.take entryHeadSize $ serialise entry <> LBS.replicate entryHeadSize 0
liftIO $ LBS.hPutStr fh bs
liftIO $ LBS.hPutStr fh ss
where
co = defaultCompressParams { compressLevel = bestSpeed }
gitRepoMakeIndex :: FilePath -> IO (HashSet GitHash)
gitRepoMakeIndex fp = do
here <- doesFileExist fp
if not here then do
pure mempty
else do
out <- newTQueueIO
gitRepoLogScan False fp $ \e _ -> do
atomically $ writeTQueue out ( e ^. gitLogEntryHash )
atomically $ flushTQueue out <&> HashSet.fromList . catMaybes

View File

@ -11,25 +11,30 @@ import HBS2.Net.Proto.RefLog
import Text.InterpolatedString.Perl6 (qc) import Text.InterpolatedString.Perl6 (qc)
import HBS2.Data.Detect hiding (Blob) import HBS2.Data.Detect hiding (Blob)
import Data.Config.Suckless
import HBS2.Git.Local import HBS2.Git.Local
import HBS2Git.GitRepoLog
import HBS2Git.App import HBS2Git.App
import HBS2Git.State import HBS2Git.State
import HBS2.Git.Local.CLI
import Data.Fixed
import Control.Monad.Trans.Maybe import Control.Monad.Trans.Maybe
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Concurrent.STM.TQueue qualified as Q import Control.Concurrent.STM.TQueue qualified as Q
import Control.Monad.Reader import Control.Monad.Reader
import Data.Foldable (for_)
import Data.Maybe import Data.Maybe
import Data.Text qualified as Text import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.ByteString.Lazy qualified as LBS
import Lens.Micro.Platform import Lens.Micro.Platform
-- import System.Exit import Data.Set qualified as Set
import Codec.Serialise import Codec.Serialise
import Control.Monad.Catch import Control.Monad.Catch
import Control.Monad.Trans.Resource
import System.Directory
import System.IO.Temp
import UnliftIO.IO
import System.IO (openBinaryFile)
import System.FilePath.Posix
import Data.HashMap.Strict qualified as HashMap
data RunImportOpts = data RunImportOpts =
RunImportOpts RunImportOpts
@ -42,123 +47,177 @@ makeLenses 'RunImportOpts
isRunImportDry :: RunImportOpts -> Bool isRunImportDry :: RunImportOpts -> Bool
isRunImportDry o = view runImportDry o == Just True isRunImportDry o = view runImportDry o == Just True
walkHashes :: HasCatAPI m => TQueue HashRef -> Hash HbSync -> m ()
walkHashes q h = walkMerkle h (readBlock . HashRef) $ \(hr :: Either (Hash HbSync) [HashRef]) -> do walkHashes q h = walkMerkle h (readBlock . HashRef) $ \(hr :: Either (Hash HbSync) [HashRef]) -> do
case hr of case hr of
Left hx -> die $ show $ pretty "missed block:" <+> pretty hx Left hx -> die $ show $ pretty "missed block:" <+> pretty hx
Right (hrr :: [HashRef]) -> do Right (hrr :: [HashRef]) -> do
forM_ hrr $ liftIO . atomically . Q.writeTQueue q forM_ hrr $ liftIO . atomically . Q.writeTQueue q
importRefLog :: (MonadIO m, HasCatAPI m) => DBEnv -> RepoRef -> m ()
importRefLog db ref = do
logRoot <- readRef ref `orDie` [qc|can't read ref {pretty ref}|] data ImportCmd = ImportCmd GitObjectType FilePath
| ImportStop
deriving (Show)
trace $ pretty logRoot importRefLogNew :: ( MonadIO m
, MonadUnliftIO m
, MonadCatch m
, HasCatAPI m
)
=> Bool -> RepoRef -> m ()
importRefLogNew force ref = runResourceT do
let myTempDir = "hbs-git"
temp <- liftIO getCanonicalTemporaryDirectory
(_,dir) <- allocate (createTempDirectory temp myTempDir) removeDirectoryRecursive
db <- makeDbPath ref >>= dbEnv
do
trace $ "importRefLogNew" <+> pretty ref
logRoot <- lift $ readRef ref `orDie` [qc|can't read ref {pretty ref}|]
trace $ "ROOT" <+> pretty logRoot
trans <- withDB db $ stateGetAllTranImported <&> Set.fromList
done <- withDB db $ stateGetRefImported logRoot
when (not done || force) do
logQ <- liftIO newTQueueIO logQ <- liftIO newTQueueIO
walkHashes logQ (fromHashRef logRoot)
entries <- liftIO $ atomically $ flushTQueue logQ lift $ walkHashes logQ (fromHashRef logRoot)
let notSkip n = force || not (Set.member n trans)
entries <- liftIO $ atomically $ flushTQueue logQ <&> filter notSkip
pCommit <- liftIO $ startGitHashObject Commit
pTree <- liftIO $ startGitHashObject Tree
pBlob <- liftIO $ startGitHashObject Blob
let hCommits = getStdin pCommit
let hTrees = getStdin pTree
let hBlobs = getStdin pBlob
let handles = [hCommits, hTrees, hBlobs]
sp0 <- withDB db savepointNew
withDB db $ savepointBegin sp0
forM_ entries $ \e -> do forM_ entries $ \e -> do
missed <- readBlock e <&> isNothing missed <- lift $ readBlock e <&> isNothing
when missed do when missed do
debug $ "MISSED BLOCK" <+> pretty e debug $ "MISSED BLOCK" <+> pretty e
let fname = show (pretty e)
let fpath = dir </> fname
(keyFh, fh) <- allocate (openBinaryFile fpath AppendMode) hClose
runMaybeT $ do runMaybeT $ do
bs <- MaybeT $ readBlock e bs <- MaybeT $ lift $ readBlock e
refupd <- MaybeT $ pure $ deserialiseOrFail @(RefLogUpdate HBS2L4Proto) bs & either (const Nothing) Just refupd <- MaybeT $ pure $ deserialiseOrFail @(RefLogUpdate HBS2L4Proto) bs & either (const Nothing) Just
e <- MaybeT $ pure $ deserialiseOrFail (LBS.fromStrict $ view refLogUpdData refupd) & either (const Nothing) Just payload <- MaybeT $ pure $ deserialiseOrFail (LBS.fromStrict $ view refLogUpdData refupd) & either (const Nothing) Just
let (SequentialRef n (AnnotatedHashRef _ h)) = e let (SequentialRef _ (AnnotatedHashRef _ h)) = payload
withDB db $ stateUpdateRefLog n h trace $ "PUSH LOG HASH" <+> pretty h
new <- withDB db stateGetHead <&> isNothing here <- withDB db $ stateGetLogImported h
when new do unless (here && not force) do
pure ()
importObjects :: (MonadIO m, MonadCatch m, HasCatAPI m) => DBEnv -> HashRef -> m () lift $ deepScan ScanDeep (const none) (fromHashRef h) (lift . readBlock . HashRef) $ \ha -> do
importObjects db root = do sec <- lift $ readBlock (HashRef ha) `orDie` [qc|missed block {pretty ha}|]
-- skip merkle tree head block, write only the data
when (h /= HashRef ha) do
liftIO $ LBS.hPutStr fh sec
q <- liftIO newTQueueIO release keyFh
walkHashes q (fromHashRef root) tnum <- liftIO $ newTVarIO 0
liftIO $ gitRepoLogScan True fpath $ \_ _ -> do
liftIO $ atomically $ modifyTVar tnum succ
entries <- liftIO $ atomically $ Q.flushTQueue q num <- liftIO $ readTVarIO tnum
trace $ "LOG ENTRY COUNT" <+> pretty num
hd <- pure (headMay entries) `orDie` "no head block found" let pref = take 16 (show (pretty e))
sz <- liftIO $ getFileSize fpath <&> realToFrac
let name = [qc|import {pref}... {sz / (1024*1024) :: Fixed E3}|]
-- TODO: what-if-metadata-is-really-big? oMon <- newProgressMonitor name num
hdData <- readBlock hd `orDie` "empty head block"
let hdBlk = tryDetect (fromHashRef hd) hdData lift $ gitRepoLogScan True fpath $ \entry s -> do
updateProgress oMon 1
let meta = headDef "" [ Text.unpack s | ShortMetadata s <- universeBi hdBlk ] lbs <- pure s `orDie` [qc|git object not read from log|]
syn <- liftIO $ parseTop meta & either (const $ die "invalid head block meta") pure withDB db do
let app sy = headDef False case view gitLogEntryType entry of
[ True GitLogEntryCommit -> do
| ListVal @C (Key "application:" [SymbolVal "hbs2-git"]) <- sy bss <- lift (pure s) `orDie` [qc|git object not read from log|]
] let co = view gitLogEntryHash entry
hx <- pure (view gitLogEntryHash entry) `orDie` [qc|empty git hash|]
let hdd = headDef False trace $ "logobject" <+> pretty h <+> "commit" <+> pretty (view gitLogEntryHash entry)
[ True
| ListVal @C (Key "type:" [SymbolVal "head"]) <- syn
]
unless ( app syn && hdd ) do writeIfNew hCommits dir hx (GitObject Commit lbs)
liftIO $ die "invalid head block meta" statePutLogObject (h, Commit, hx)
let rest = drop 1 entries let parents = gitCommitGetParentsPure bss
forM_ parents $ \p -> do
trace $ "fact" <+> "commit-parent" <+> pretty co <+> pretty p
statePutLogCommitParent (hx,p)
withDB db $ transactional $ do GitLogEntryBlob -> do
trace $ "logobject" <+> pretty h <+> "blob" <+> pretty (view gitLogEntryHash entry)
hx <- pure (view gitLogEntryHash entry) `orDie` [qc|empty git hash|]
writeIfNew hBlobs dir hx (GitObject Blob lbs)
statePutLogObject (h, Blob, hx)
trace "ABOUT TO UPDATE HEAD" GitLogEntryTree -> do
trace $ "logobject" <+> pretty h <+> "tree" <+> pretty (view gitLogEntryHash entry)
hx <- pure (view gitLogEntryHash entry) `orDie` [qc|empty git hash|]
writeIfNew hTrees dir hx (GitObject Tree lbs)
statePutLogObject (h, Tree, hx)
statePutHead hd GitLogContext -> do
statePutImported root hd trace $ "logobject" <+> pretty h <+> "context" <+> pretty (view gitLogEntryHash entry)
mon <- newProgressMonitor "importing objects" (length rest) let co = fromMaybe mempty $ deserialiseOrFail @GitLogContextEntry
<$> s >>= either (const Nothing) Just <&> commitsOfGitLogContextEntry
for_ rest $ \r -> do forM_ co (statePutLogContextCommit h)
updateProgress mon 1 GitLogEntryHead -> do
trace $ "HEAD ENTRY" <+> viaShow s
let mbrh = fromStringMay @RepoHead (maybe mempty LBS.unpack s)
rh <- pure mbrh `orDie` [qc|invalid log header in {pretty h} {s}|]
gh <- stateGetGitHash r <&> isJust forM_ (HashMap.toList $ view repoHeads rh) $ \(re,ha) -> do
trace $ "logrefval" <+> pretty h <+> pretty re <+> pretty ha
statePutLogRefVal (h,re,ha)
unless gh do _ -> pure ()
blk <- lift $ readBlock r `orDie` "empty data block" statePutLogImported h
statePutTranImported e
let what = tryDetect (fromHashRef r) blk withDB db $ do
statePutRefImported logRoot
stateUpdateCommitDepths
savepointRelease sp0
let short = headDef "" [ s | ShortMetadata s <- universeBi what ] mapM_ hClose handles
let fields = Text.lines short & fmap Text.words where
let fromTxt = fromString . Text.unpack
let fromRec t = Just . (t,) . fromTxt
hm <- forM fields $ \case
["type:", "blob", x] -> pure $ fromRec Blob x
["type:", "commit", x] -> pure $ fromRec Commit x
["type:", "tree", x] -> pure $ fromRec Tree x
_ -> pure Nothing
case catMaybes hm of
[(t,sha1)] -> do
trace $ "statePutHash" <+> pretty t <+> pretty sha1
-- FIXME: return-dry?
statePutHash t sha1 r
_ -> err $ "skipping bad object" <+> pretty r
pure ()
writeIfNew gitHandle dir h (GitObject tp s) = do
let nf = dir </> show (pretty h)
liftIO $ LBS.writeFile nf s
hPutStrLn gitHandle nf
hFlush gitHandle
trace $ "WRITTEN OBJECT" <+> pretty tp <+> pretty h <+> pretty nf

View File

@ -5,12 +5,20 @@ import HBS2.Prelude
import HBS2Git.App import HBS2Git.App
import HBS2.Data.Types.Refs (HashRef) import HBS2.Data.Types.Refs (HashRef)
import HBS2.System.Logger.Simple
import HBS2.Git.Local.CLI import HBS2.Git.Local.CLI
import HBS2.Git.Types
import HBS2Git.Import (importRefLogNew)
import HBS2Git.State
import Data.HashMap.Strict qualified as HashMap
import Data.Functor import Data.Functor
import Data.Text qualified as Text import Data.Text qualified as Text
import Data.Traversable import Data.Traversable
import Prettyprinter.Render.Terminal import Prettyprinter.Render.Terminal
import Control.Monad.IO.Unlift
import Control.Monad.Catch
import System.IO (stdout)
data AsRemoteEntry = AsRemoteEntry data AsRemoteEntry = AsRemoteEntry
{ remoteName :: Text, { remoteName :: Text,
@ -65,6 +73,21 @@ runListRefs = do
where where
isHbs2 (_, b) = Text.isPrefixOf hbs2Prefix b isHbs2 (_, b) = Text.isPrefixOf hbs2Prefix b
runToolsScan :: (MonadUnliftIO m,MonadCatch m) => RepoRef -> App m ()
runToolsScan ref = do
trace $ "runToolsScan" <+> pretty ref
importRefLogNew False ref
shutUp
pure ()
runToolsGetRefs :: (MonadUnliftIO m,MonadCatch m) => RepoRef -> App m ()
runToolsGetRefs ref = do
db <- makeDbPath ref >>= dbEnv
refs <- withDB db stateGetActualRefs
let rh = RepoHead Nothing (HashMap.fromList refs)
hPrint stdout $ pretty (AsGitRefsFile rh)
shutUp
getRefVal :: (MonadIO m, HasCatAPI m) => Text -> m (Maybe HashRef) getRefVal :: (MonadIO m, HasCatAPI m) => Text -> m (Maybe HashRef)
getRefVal url = getRefVal url =
case Text.stripPrefix hbs2Prefix url of case Text.stripPrefix hbs2Prefix url of

View File

@ -27,9 +27,17 @@ import Control.Monad.Catch
import Control.Concurrent.STM import Control.Concurrent.STM
import System.IO.Unsafe import System.IO.Unsafe
-- FIXME: move-orphans-to-separate-module
instance ToField GitHash where instance ToField GitHash where
toField h = toField (show $ pretty h) toField h = toField (show $ pretty h)
instance ToField GitRef where
toField h = toField (show $ pretty h)
instance FromField GitRef where
fromField = fmap fromString . fromField @String
instance FromField GitHash where instance FromField GitHash where
fromField = fmap fromString . fromField @String fromField = fmap fromString . fromField @String
@ -39,7 +47,6 @@ instance FromField GitObjectType where
instance ToField HashRef where instance ToField HashRef where
toField h = toField (show $ pretty h) toField h = toField (show $ pretty h)
instance ToField GitObjectType where instance ToField GitObjectType where
toField h = toField (show $ pretty h) toField h = toField (show $ pretty h)
@ -90,54 +97,97 @@ stateInit :: MonadIO m => DB m ()
stateInit = do stateInit = do
conn <- ask conn <- ask
liftIO $ execute_ conn [qc| liftIO $ execute_ conn [qc|
create table if not exists dep create table if not exists logrefval
( object text not null ( loghash text not null
, parent text not null , refname text not null
, primary key (object, parent) , refval text not null
, primary key (loghash, refname)
) )
|] |]
liftIO $ execute_ conn [qc| liftIO $ execute_ conn [qc|
create table if not exists object create table if not exists logobject
( githash text not null ( loghash text not null
, hash text not null unique
, type text not null , type text not null
, primary key (githash,hash) , githash text not null
, primary key (loghash, githash)
) )
|] |]
liftIO $ execute_ conn [qc| liftIO $ execute_ conn [qc|
create table if not exists head create table if not exists logcommitparent
( key text not null primary key ( kommit text not null
, hash text not null unique , parent text not null
, primary key (kommit,parent)
) )
|] |]
liftIO $ execute_ conn [qc| liftIO $ execute_ conn [qc|
create table if not exists imported create table if not exists logimported
( seq integer primary key autoincrement ( hash text not null
, ts DATE DEFAULT (datetime('now','localtime')) , primary key (hash)
, merkle text not null
, head text not null
, unique (merkle,head)
) )
|] |]
liftIO $ execute_ conn [qc| liftIO $ execute_ conn [qc|
create table if not exists reflog create table if not exists refimported
( seq integer primary key ( hash text not null
, ts DATE DEFAULT (datetime('now','localtime')) , timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
, merkle text not null , primary key (hash)
, unique (merkle)
) )
|] |]
liftIO $ execute_ conn [qc| liftIO $ execute_ conn [qc|
create table if not exists exported create table if not exists tranimported
( githash text not null primary key ( hash text not null
, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
, primary key (hash)
) )
|] |]
liftIO $ execute_ conn [qc|
DROP VIEW IF EXISTS v_refval_actual;
|]
liftIO $ execute_ conn [qc|
CREATE view v_refval_actual AS
WITH a1 as (
SELECT
l.refname
, l.refval
, vd.depth
FROM logrefval l
JOIN v_log_depth vd on vd.loghash = l.loghash )
SELECT a1.refname, a1.refval, MAX(a1.depth) from a1
GROUP by a1.refname
HAVING a1.refval <> '0000000000000000000000000000000000000000' ;
|]
liftIO $ execute_ conn [qc|
CREATE TABLE IF NOT EXISTS logcommitdepth
( kommit text not null
, depth integer not null
, primary key (kommit)
);
|]
liftIO $ execute_ conn [qc|
DROP VIEW IF EXISTS v_log_depth;
|]
liftIO $ execute_ conn [qc|
CREATE VIEW v_log_depth AS
SELECT
lo.loghash,
MAX(ld.depth) AS depth
FROM logobject lo
JOIN logcommitdepth ld ON lo.githash = ld.kommit
WHERE lo.type in ( 'commit', 'context' )
GROUP BY lo.loghash;
|]
newtype Savepoint = newtype Savepoint =
Savepoint String Savepoint String
@ -189,179 +239,157 @@ transactional action = do
-- состояние репозитория -- состояние репозитория
statePutExported :: MonadIO m => GitHash -> DB m () statePutLogRefVal :: MonadIO m => (HashRef, GitRef, GitHash) -> DB m ()
statePutExported h = do statePutLogRefVal row = do
conn <- ask conn <- ask
liftIO $ execute conn [qc| liftIO $ execute conn [qc|
insert into exported (githash) values(?) insert into logrefval (loghash,refname,refval) values(?,?,?)
on conflict (githash) do nothing on conflict (loghash,refname) do nothing
|] row
statePutLogObject :: MonadIO m => (HashRef, GitObjectType, GitHash) -> DB m ()
statePutLogObject row = do
conn <- ask
liftIO $ execute conn [qc|
insert into logobject (loghash,type,githash) values(?,?,?)
on conflict (loghash,githash) do nothing
|] row
stateIsLogObjectExists :: MonadIO m => GitHash -> DB m Bool
stateIsLogObjectExists h = do
conn <- ask
liftIO $ query conn [qc|
SELECT NULL FROM logobject WHERE githash = ? LIMIT 1
|] (Only h) <&> isJust . listToMaybe . fmap (fromOnly @(Maybe Int))
statePutLogContextCommit :: MonadIO m => HashRef -> GitHash -> DB m ()
statePutLogContextCommit loghash ctx = do
conn <- ask
liftIO $ execute conn [qc|
insert into logobject (loghash,type,githash) values(?,'context',?)
on conflict (loghash,githash) do nothing
|] (loghash,ctx)
statePutLogCommitParent :: MonadIO m => (GitHash, GitHash) -> DB m ()
statePutLogCommitParent row = do
conn <- ask
liftIO $ execute conn [qc|
insert into logcommitparent (kommit,parent) values(?,?)
on conflict (kommit,parent) do nothing
|] row
statePutLogImported :: MonadIO m => HashRef -> DB m ()
statePutLogImported h = do
conn <- ask
liftIO $ execute conn [qc|
insert into logimported (hash) values(?)
on conflict (hash) do nothing
|] (Only h) |] (Only h)
stateGetExported :: MonadIO m => DB m [GitHash]
stateGetExported = do stateGetLogImported :: MonadIO m => HashRef -> DB m Bool
stateGetLogImported h = do
conn <- ask
r <- liftIO $ query @_ @(Only Int) conn [qc|
select 1 from logimported where hash = ? limit 1
|] (Only h)
pure $ not $ null r
statePutRefImported :: MonadIO m => HashRef -> DB m ()
statePutRefImported h = do
conn <- ask
liftIO $ execute conn [qc|
insert into refimported (hash) values(?)
on conflict (hash) do nothing
|] (Only h)
stateGetRefImported :: MonadIO m => HashRef -> DB m Bool
stateGetRefImported h = do
conn <- ask
r <- liftIO $ query @_ @(Only Int) conn [qc|
select 1 from refimported where hash = ? limit 1
|] (Only h)
pure $ not $ null r
statePutTranImported :: MonadIO m => HashRef -> DB m ()
statePutTranImported h = do
conn <- ask
liftIO $ execute conn [qc|
insert into tranimported (hash) values(?)
on conflict (hash) do nothing
|] (Only h)
stateGetTranImported :: MonadIO m => HashRef -> DB m Bool
stateGetTranImported h = do
conn <- ask
r <- liftIO $ query @_ @(Only Int) conn [qc|
select 1 from tranimported where hash = ? limit 1
|] (Only h)
pure $ not $ null r
stateGetAllTranImported :: MonadIO m => DB m [HashRef]
stateGetAllTranImported = do
conn <- ask
results <- liftIO $ query_ conn [qc|
select hash from tranimported
|]
pure $ map fromOnly results
stateGetImportedCommits :: MonadIO m => DB m [GitHash]
stateGetImportedCommits = do
conn <- ask conn <- ask
liftIO $ query_ conn [qc| liftIO $ query_ conn [qc|
select githash from exported select distinct(githash) from logobject where type = 'commit'
|] <&> fmap fromOnly |] <&> fmap fromOnly
statePutImported :: MonadIO m => HashRef -> HashRef -> DB m () stateGetActualRefs :: MonadIO m => DB m [(GitRef, GitHash)]
statePutImported merkle hd = do stateGetActualRefs = do
conn <- ask
liftIO $ execute conn [qc|
insert into imported (merkle,head) values(?,?)
on conflict (merkle,head) do nothing
|] (merkle,hd)
stateUpdateRefLog :: MonadIO m => Integer -> HashRef -> DB m ()
stateUpdateRefLog seqno merkle = do
conn <- ask
liftIO $ execute conn [qc|
insert into reflog (seq,merkle) values(?,?)
on conflict (merkle) do nothing
on conflict (seq) do nothing
|] (seqno,merkle)
stateGetRefLogLast :: MonadIO m => DB m (Maybe (Integer, HashRef))
stateGetRefLogLast = do
conn <- ask conn <- ask
liftIO $ query_ conn [qc| liftIO $ query_ conn [qc|
select seq, merkle from reflog select refname,refval from v_refval_actual
order by seq desc |]
limit 1
|] <&> listToMaybe
statePutHead :: MonadIO m => HashRef -> DB m () stateGetActualRefValue :: MonadIO m => GitRef -> DB m (Maybe GitHash)
statePutHead h = do stateGetActualRefValue ref = do
conn <- ask
liftIO $ execute conn [qc|
insert into head (key,hash) values('head',?)
on conflict (key) do update set hash = ?
|] (h,h)
stateGetHead :: MonadIO m => DB m (Maybe HashRef)
stateGetHead = do
conn <- ask
liftIO $ query_ conn [qc|
select hash from head where key = 'head'
limit 1
|] <&> listToMaybe . fmap fromOnly
stateAddDep :: MonadIO m => GitHash -> GitHash -> DB m ()
stateAddDep h1 h2 = do
conn <- ask
void $ liftIO $ execute conn [qc|
insert into dep (object,parent) values(?,?)
on conflict (object,parent) do nothing
|] (h1,h2)
stateGetDepsRec :: MonadIO m => GitHash -> DB m [GitHash]
stateGetDepsRec h = do
conn <- ask conn <- ask
liftIO $ query conn [qc| liftIO $ query conn [qc|
select refval from v_refval_actual
where refname = ?
|] (Only ref) <&> fmap fromOnly . listToMaybe
WITH RECURSIVE find_children(object, parent) AS ( stateUpdateCommitDepths :: MonadIO m => DB m ()
SELECT object, parent FROM dep WHERE parent = ? stateUpdateCommitDepths = do
UNION conn <- ask
SELECT d.object, d.parent FROM dep d INNER JOIN find_children fc sp <- savepointNew
ON d.parent = fc.object savepointBegin sp
-- TODO: check-if-delete-logcommitdepth-is-needed
liftIO $ execute_ conn [qc|DELETE FROM logcommitdepth|]
liftIO $ execute_ conn [qc|
INSERT INTO logcommitdepth (kommit, depth)
WITH RECURSIVE depths(kommit, level) AS (
SELECT
kommit,
0
FROM logcommitparent
UNION ALL
SELECT
p.kommit,
d.level + 1
FROM logcommitparent p
INNER JOIN depths d ON p.parent = d.kommit
) )
SELECT object FROM find_children group by object; SELECT
kommit,
|] (Only h) <&> mappend [h] . fmap fromOnly MAX(level)
FROM depths
stateGetAllDeps :: MonadIO m => DB m [(GitHash,GitHash)] WHERE kommit NOT IN (SELECT kommit FROM logcommitdepth)
stateGetAllDeps = do GROUP BY kommit;
conn <- ask
liftIO $ query_ conn [qc|
select parent, object from dep where parent = ?
|] |]
savepointRelease sp
stateDepFilterAll :: MonadIO m => DB m [GitHash]
stateDepFilterAll = do
conn <- ask
liftIO $ query_ conn [qc|
select distinct(parent) from dep
union
select githash from object o where o.type = 'blob'
|] <&> fmap fromOnly
stateDepFilter :: MonadIO m => GitHash -> DB m Bool
stateDepFilter h = do
conn <- ask
liftIO $ query @_ @[Int] conn [qc|
select 1 from dep
where parent = ?
or exists (select null from object where githash = ? and type = 'blob')
limit 1
|] (h,h) <&> isJust . listToMaybe
stateGetDeps :: MonadIO m => GitHash -> DB m [GitHash]
stateGetDeps h = do
conn <- ask
liftIO $ query conn [qc|
select object from dep where parent = ?
|] (Only h) <&> fmap fromOnly
statePutHash :: MonadIO m => GitObjectType -> GitHash -> HashRef -> DB m ()
statePutHash t g h = do
conn <- ask
liftIO $ execute conn [qc|
insert into object (githash,hash,type) values(?,?,?)
on conflict (githash,hash) do nothing
|] (g,h,t)
stateGetHash :: MonadIO m => GitHash -> DB m (Maybe HashRef)
stateGetHash h = do
conn <- ask
liftIO $ query conn [qc|
select hash from object where githash = ?
limit 1
|] (Only h) <&> fmap fromOnly <&> listToMaybe
stateGetGitHash :: MonadIO m => HashRef -> DB m (Maybe GitHash)
stateGetGitHash h = do
conn <- ask
liftIO $ query conn [qc|
select githash from object where hash = ?
limit 1
|] (Only h) <&> fmap fromOnly <&> listToMaybe
stateGetAllHashes :: MonadIO m => DB m [HashRef]
stateGetAllHashes = do
conn <- ask
liftIO $ query_ conn [qc|
select distinct(hash) from object
|] <&> fmap fromOnly
stateGetAllObjects:: MonadIO m => DB m [(HashRef,GitHash,GitObjectType)]
stateGetAllObjects = do
conn <- ask
liftIO $ query_ conn [qc|
select hash, githash, type from object
|]
stateGetLastImported :: MonadIO m => Int -> DB m [(Text,HashRef,HashRef)]
stateGetLastImported n = do
conn <- ask
liftIO $ query conn [qc|
select ts, merkle, head from imported
order by seq desc
limit ?
|] (Only n)
stateGetSequence :: MonadIO m => DB m Integer
stateGetSequence = do
conn <- ask
liftIO $ query_ conn [qc|
select coalesce(max(seq),0) from reflog;
|] <&> fmap fromOnly
<&> listToMaybe
<&> fromMaybe 0

View File

@ -15,6 +15,8 @@ import HBS2.Data.Types.Refs
import HBS2.Net.Proto.Types import HBS2.Net.Proto.Types
import HBS2.Net.Auth.Credentials import HBS2.Net.Auth.Credentials
import HBS2.System.Logger.Simple
import Data.Config.Suckless import Data.Config.Suckless
import System.ProgressBar import System.ProgressBar
@ -24,8 +26,11 @@ import Control.Applicative
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Reader import Control.Monad.Reader
import Database.SQLite.Simple (Connection) import Database.SQLite.Simple (Connection)
import Data.Char (isSpace)
import Data.Set qualified as Set import Data.Set qualified as Set
import Data.Set (Set) import Data.Set (Set)
import Data.List qualified as List
import Data.Maybe
import Lens.Micro.Platform import Lens.Micro.Platform
import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap import Data.HashMap.Strict qualified as HashMap
@ -35,6 +40,9 @@ import System.IO qualified as IO
import System.IO (Handle) import System.IO (Handle)
import Data.Kind import Data.Kind
import Control.Monad.Catch import Control.Monad.Catch
import Control.Monad.IO.Unlift
import System.TimeIt
-- FIXME: remove-udp-hardcode-asap -- FIXME: remove-udp-hardcode-asap
type Schema = HBS2Basic type Schema = HBS2Basic
@ -77,7 +85,7 @@ data RepoHead =
{ _repoHEAD :: Maybe GitRef { _repoHEAD :: Maybe GitRef
, _repoHeads :: HashMap GitRef GitHash , _repoHeads :: HashMap GitRef GitHash
} }
deriving stock (Generic) deriving stock (Generic,Show)
makeLenses 'RepoHead makeLenses 'RepoHead
@ -90,17 +98,40 @@ instance Semigroup RepoHead where
& set repoHeads ( view repoHeads a <> view repoHeads b ) & set repoHeads ( view repoHeads a <> view repoHeads b )
instance Pretty (AsGitRefsFile RepoHead) where instance Pretty (AsGitRefsFile RepoHead) where
pretty (AsGitRefsFile h) = vcat (hhead : fmap fmt els) pretty (AsGitRefsFile h) = hhead <> vcat (fmap fmt els)
where where
hhead = case view repoHEAD h of hhead = case view repoHEAD h of
Nothing -> mempty Nothing -> mempty
Just r -> "@" <> pretty r <+> "HEAD" Just r -> "@" <> pretty r <+> "HEAD" <> line
els = HashMap.toList (view repoHeads h) els = HashMap.toList (view repoHeads h)
fmt (r,hx) = pretty hx <+> pretty (normalizeRef r) fmt (r,hx) = pretty hx <+> pretty (normalizeRef r)
instance Serialise RepoHead instance Serialise RepoHead
-- FIXME: test-for-from-string-maybe-repohead
-- Нужно написать или сгенерировать тест
instance FromStringMaybe RepoHead where
fromStringMay "" = Nothing
fromStringMay s =
case traverse decodePair (take 2 . words <$> lines trimmed) of
Right xs -> Just $ mconcat xs
_ -> Nothing
where
trimmed = dropWhile isSpace s
hbranch x = fromString <$> List.stripPrefix "@" x
decodePair :: [String] -> Either [String] RepoHead
decodePair [x, "HEAD"] | "@" `List.isPrefixOf` x = Right $ RepoHead (hbranch x) mempty
-- special case: deleted branch. should be handled somehow
decodePair [_] = Right $ RepoHead Nothing mempty
decodePair [x,r] = case fromStringMay x of
Just h -> Right $ RepoHead Nothing (HashMap.singleton (fromString r) h)
Nothing -> Left [r,x]
decodePair other = Left other
pattern Key :: forall {c}. Id -> [Syntax c] -> [Syntax c] pattern Key :: forall {c}. Id -> [Syntax c] -> [Syntax c]
pattern Key n ns <- SymbolVal n : ns pattern Key n ns <- SymbolVal n : ns
@ -136,6 +167,12 @@ instance (HasCatAPI m, MonadIO m) => HasCatAPI (MaybeT m) where
getHttpPutAPI = lift getHttpPutAPI getHttpPutAPI = lift getHttpPutAPI
getHttpRefLogGetAPI = lift getHttpRefLogGetAPI getHttpRefLogGetAPI = lift getHttpRefLogGetAPI
-- instance (HasCatAPI (App m), MonadIO m) => HasCatAPI (ResourceT (App m)) where
-- getHttpCatAPI = lift getHttpCatAPI
-- getHttpSizeAPI = lift getHttpSizeAPI
-- getHttpPutAPI = lift getHttpPutAPI
-- getHttpRefLogGetAPI = lift getHttpRefLogGetAPI
class Monad m => HasCfgKey a b m where class Monad m => HasCfgKey a b m where
-- type family CfgValue a :: Type -- type family CfgValue a :: Type
key :: Id key :: Id
@ -155,6 +192,8 @@ newtype App m a =
, MonadReader AppEnv , MonadReader AppEnv
, MonadThrow , MonadThrow
, MonadCatch , MonadCatch
, MonadUnliftIO
, MonadTrans
) )
instance MonadIO m => HasConf (App m) where instance MonadIO m => HasConf (App m) where
@ -194,3 +233,9 @@ die :: MonadIO m => String -> m a
die s = do die s = do
liftIO $ Exit.die s liftIO $ Exit.die s
traceTime :: MonadIO m => String -> m a -> m a
traceTime s action = do
(t, x) <- timeItT action
trace $ "time" <+> pretty s <+> pretty t
pure x

View File

@ -1,56 +0,0 @@
module HBS2Git.Update where
import HBS2.Prelude.Plated
import HBS2.OrDie
import HBS2.System.Logger.Simple
import HBS2.Git.Types
import HBS2Git.Types
import HBS2Git.App
import HBS2Git.State
import HBS2Git.Import
import Control.Monad.Catch
updateLocalState :: (MonadIO m, HasCatAPI m, MonadCatch m) => RepoRef -> m ()
updateLocalState ref = do
dbPath <- makeDbPath ref
trace $ "dbPath:" <+> pretty dbPath
db <- dbEnv dbPath
withDB db stateInit
trace $ "updateLocalState" <+> pretty ref
sp <- withDB db savepointNew
withDB db $ savepointBegin sp
r <- try $ do
-- TODO: read-reflog
-- TODO: update-reflog
importRefLog db ref
(n,hash) <- withDB db $ stateGetRefLogLast `orDie` "empty reflog"
trace $ "got reflog" <+> pretty (n,hash)
importObjects db hash
withDB db (savepointRelease sp)
case r of
Left (e :: SomeException) -> do
withDB db $ savepointRollback sp
err (viaShow e)
err "error happened. state rolled back"
Right{} -> pure ()