Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 186 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,20 @@ Promotorem pracy był **[mgr inż. Tomasz Herman](https://github.com/tomasz-herm
- [Architektura](#architektura)
- [Struktura projektu](#struktura-projektu)
- [Model rozproszony i architektura repozytoriów](#model-rozproszony-i-architektura-repozytoriów)
- [Warstwa sieciowa i protokoły synchronizacji](#warstwa-sieciowa-i-protokoły-synchronizacji)
- [Warstwa sieciowa i protokoły synchronizacji](#warstwa-sieciowa-i-protokoly-synchronizacji)
- [Skrypty użytkownika](#skrypty-uzytkownika)
- [Założenia systemu skryptów użytkownika](#zalozenia-systemu-skryptow-uzytkownika)
- [Koncepcja niezależności językowej](#koncepcja-niezaleznosci-jezykowej)
- [Fizyczna organizacja i konfiguracja skyptów użytkownika](#fizyczna-organizacja-i-konfiguracja-skyptow-uzytkownika)
- [Struktura obiektów kontekstu](#struktura-obiektow-kontekstu)
- [Przykład implementacji](#przyklad-implementacji)
- [Instalacja](#instalacja)
- [Wymagania wstępne i środowisko budowania](#wymagania-wstępne-i-środowisko-budowania)
- [Wymagania wstępne i środowisko budowania](#wymagania-wstepne-i-srodowisko-budowania)
- [Instalacja klienta CLI](#instalacja-klienta-cli)
- [Instalacja klienta GUI](#instalacja-klienta-gui)
- [Instalacja i konfiguracja serwera SSH](#instalacja-i-konfiguracja-serwera-ssh)
- [Weryfikacja instalacji](#weryfikacja-instalacji)
- [Instrukcja użytkownika](#instrukcja-użytkownika)
- [Instrukcja użytkownika](#instrukcja-uzytkownika)
- [Praca z interfejsem CLI](#praca-z-interfejsem-cli)
- [Praca z interfejsem GUI](#praca-z-interfejsem-gui)
- [Praca z serwerem SSH](#praca-z-serwerem-ssh)
Expand Down Expand Up @@ -398,6 +404,183 @@ Diagram sekwencji dla protokołu `meva-receive-pack`:
<img src="docs/images/receive-pack.png" width="60%"/>
</p>

## Skrypty użytkownika

System kontroli wersji MEVA został zaprojektowany jako rozwiązanie otwarte na rozszerzenia. Oprócz wbudowanego zestawu poleceń, udostępnia on mechanizm skryptów użytkownika (nazywanych również pluginami), który pozwala na automatyzację zadań, walidację danych oraz integrację zewnętrznych narzędzi bez konieczności ingerencji w kod źródłowy samej aplikacji.

### Założenia systemu skryptów użytkownika

Projektując moduł skryptów użytkownika, przyjęto szereg założeń definiujących jego zachowanie, zakres oraz sposób interakcji z otoczeniem:

- **Model zdarzeń**: System obsługuje dwa rodzaje zdarzeń:
- `pre-execute`: Występujące przed wykonaniem właściwej operacji (np. `commit`, `push`). Służy do walidacji oraz modyfikacji danych wejściowych. Błąd zwrócony przez plugin w tej fazie skutkuje natychmiastowym przerwaniem całej operacji.
- `post-execute`: Występujące po pomyślnym wykonaniu operacji. Służy do raportowania i analizowania wyników polecenia. Błąd w tej fazie nie wpływa na wynik operacji wykonanej już na repozytorium.

- **Zakresy**: Pluginy mogą być rejestrowane na dwóch poziomach widoczności:
- **Lokalny**: Ogranicza działanie skryptu do pojedynczego repozytorium. Konfiguracja i skrypty przechowywane są wewnątrz katalogu `.meva/plugins/`.
- **Globalny**: Rozszerza widoczność na wszystkie repozytoria danego użytkownika w systemie. Konfiguracja i skrypty przechowywane są w katalogu domowym użytkownika (`~/.meva/plugins/`).

- **Niezależna komunikacja**: Interfejs wymiany danych między systemem a pluginem jest zrealizowany poprzez system plików. W momencie wywołania, skrypt otrzymuje ścieżkę do tymczasowego pliku JSON (tzw. pliku kontekstu), co uniezależnia mechanizm od języka programowania, w którym napisano rozszerzenie.

- **Synchroniczne wykonanie**: Pluginy są uruchamiane synchronicznie, jeden po drugim. Kolejność ich wykonywania wynika z priorytetu (`order`) nadanego podczas rejestracji.

- **Kontrola czasu wykonania**: Istnieje możliwość zdefiniowania maksymalnego czasu wykonania osobno dla każdego zarejestrowanego skryptu. Jeżeli wykonanie logiki zapisanej w pluginie potrwa dłużej niż określony limit, system automatycznie przerywa wykonanie, informując o przekroczeniu czasu.

- **Przechwytywanie wejścia/wyjścia**: System w pełni zarządza standardowymi strumieniami (`stdin`, `stdout`, `stderr`) każdego skryptu. Strumienie wyjściowe są przechwytywane w czasie rzeczywistym, dzięki czemu komunikaty wypisywane na standardowe wyjście (`stdout`) i standardowe wyjście błędów (`stderr`) są jednocześnie prezentowane użytkownikowi w konsoli oraz zapisywane w logach wywołania. Skrypty mogą pobierać dane od użytkownika poprzez standardowe wejście (`stdin`).

- **Konfiguracja**: Działanie całego systemu rozszerzeń jest kontrolowane przez wpisy w głównych plikach konfiguracyjnych (sekcja `[plugins]`), co pozwala na ich szybkie włączenie lub wyłączenie bez konieczności odinstalowywania skryptów czy rekompilacji kodu.

### Koncepcja niezależności językowej

Istotną cechą systemu rozszerzeń jest brak wymogu tworzenia pluginów w konkretnym języku programowania. Zamiast dostarczać wewnętrzne API lub biblioteki dynamiczne (`.dll`, ang. _dynamic link libraries_), system **MEVA** wykorzystuje mechanizm procesów potomnych oraz komunikację opartą na plikach. Procesy potomne komunikują się poprzez ustandaryzowany plik kontekstu operacji w formacie [JSON](https://json.org/json-en.html).

Plugin został zdefiniowany jako dowolny plik wykonywalny lub skrypt interpretowany (na przykład w języku `Python`, `JavaScript`, `PowerShell` czy `Bash`).

Proces wymiany danych wygląda następująco:

1. System MEVA generuje plik tymczasowy w formacie JSON, zawierający metadane polecenia (kontekst wykonywanej operacji).
2. System uruchamia skrypt użytkownika jako osobny proces, przekazując ścieżkę do pliku kontekstu jako pierwszy argument wywołania.
3. Skrypt odczytuje dane, wykonuje swoją logikę i (opcjonalnie) modyfikuje plik kontekstu. Może również zapisać w nim informację o błędzie oraz zwrócić niezerowy kod wyjścia, przerywając wykonanie kolejnych skryptów lub samego polecenia.

Dzięki takiemu podejściu użytkownik ma pełną swobodę w doborze technologii do tworzenia rozszerzeń, o ile w danym środowisku systemowym dostępny jest odpowiedni interpreter.

### Fizyczna organizacja i konfiguracja skyptów użytkownika

System pluginów posiada własną, ustrukturyzowaną hierarchię plików, która umożliwia logiczną organizację skryptów i ich metadanych. Zdefiniowano dwa poziomy zasięgu pluginów: globalny (dla użytkownika systemu operacyjnego) oraz lokalny (dla konkretnego repozytorium).

```
.meva/plugins/
├── .invocations/
│ ├── commit/
│ │ ├── 20260112-202222/
│ │ │ ├── invocation.log
│ │ │ ├── pre-execute-context.json
│ │ │ ├── post-execute-context.json
│ │ │ ├── stdout.log
│ │ │ └── stderr.log
│ │ └── ...
│ └── ...
├── commit/
│ ├── plugins.json
│ ├── validate_message.py
│ └── ...
├── config/
│ ├── set/
│ │ ├── plugins.json
│ │ └── ...
│ └── unset/
│ ├── plugins.json
│ └── ...
└── ...
```

### Struktura obiektów kontekstu

Komunikacja między systemem MEVA a skryptami użytkownika odbywa się poprzez wymianę sformalizowanych obiektów danych w formacie JSON.

Każdy plik kontekstu operacji, do którego ścieżka jest przekazywana jako pierwszy argument pozycyjny podczas uruchamiania skryptu, zawiera cztery główne sekcje: metadane (`context`), dane wejściowe (`pre-payload`), dane wyjściowe (`post-payload`) oraz kanał błędów (`error`).

```json
{
"context": {
"command": "commit",
"event": "pre-execute",
"timestamp": "2026-01-12T12:34:56Z",
"working_dir": "/home/john/company_project"
},
"pre-payload": {
"message": "Implement user authentication.",
"author": {
"name": "John Doe",
"email": "john.doe@example.com"
},
"amend": false
},
"post-payload": {
"commit_hash": "a1b2c3d4e5f6...",
"changes": [
{
"added": {
"new_path": "src/auth.rs",
"insertions": 45
}
}
]
},
"error": null
}
```

Rola poszczególnych sekcji obiektu JSON jest następująca:

1. `context`: Sekcja zawierająca podstawowe informacje o kontekście wywołania skryptu użytkownika. Znajdują się tu informacje o typie polecenia (`command`), rodzaju zdarzenia (`event`), czasie wywołania (`timestamp`) oraz katalogu roboczym (`working_dir`).
2. `pre-payload`: Sekcja zawierająca dane wejściowe specyficzne dla danego polecenia. Są one dostępne zarówno w fazie `pre-execute`, jak i `post-execute`.
3. `post-payload`: Sekcja przeznaczona na wynik działania operacji. W fazie `pre-execute` wartość ta zawsze wynosi `null`. W fazie `post-execute` (jak na rysunku przedstawiającym kontekst polecenia `commit`) zawiera ona informacje dotyczące rezultatu polecenia.
4. `error`: Opcjonalna sekcja, która w przypadku poprawnego wykonania skryptu użytkownika przyjmuje wartość `null`. Plugin może ją zmodyfikować w celu zgłoszenia błędu wykonania.

W przypadku polecenia `commit` informacje zawarte w `pre-payload` obejmują między innymi treść wiadomości, dane autora oraz flagi sterujące (np. `amend`). Sekcja `post-payload` zawiera natomiast między innymi skrót zatwierdzenia, a także listę plików objętych zatwierdzeniem wraz z metadanymi (np. liczbę zmodyfikowanych linii).

#### Struktura obiektów błędu

Skrypt użytkownika ma możliwość przerwania łańcucha wywołań następujących po nim pluginów, a w fazie `pre-execute` — dodatkowo zablokowania wykonania głównego polecenia (np. uniemożliwienia utworzenia zatwierdzenia, którego format wiadomości nie spełnia określonych wymogów).

Sygnałem do przerwania operacji jest zakończenie procesu pluginu z kodem wyjścia (ang. _exit code_) różnym od zera. W takiej sytuacji system **MEVA** analizuje pole `error` w pliku kontekstu, aby wyświetlić użytkownikowi przyczynę blokady.

```json
{
"code": "VALIDATION_ERROR",
"message": "Commit message must reference an Azure DevOps work item (e.g. AB#1234)",
"details": "Regex pattern '^AB#\\d+' not matched"
}
```

Obiekt ten składa się z trzech pól:

1. `code`: Stały identyfikator typu błędu (np. `VALIDATION_ERROR`).
2. `message`: Komunikat przeznaczony bezpośrednio dla użytkownika końcowego, wyjaśniający, dlaczego operacja została przerwana.
3. `details`: Opcjonalne pole zawierające szczegóły techniczne, pomocne w diagnostyce.

### Przykład implementacji

W celu zaprezentowania praktycznego zastosowania omówionych mechanizmów, przedstawiona zostanie przykładowa implementacja pluginu walidacyjnego. Scenariusz ten zakłada integrację systemu MEVA z platformą Azure DevOps. Jedną z cech tej platformy jest identyfikowanie elementów roboczych (ang. _work items_) poprzez unikalny identyfikator (liczbę).

Wymaganiem biznesowym może być oznaczanie każdego zatwierdzenia identyfikatorem odpowiadającego mu elementu roboczego poprzez umieszczenie tego identyfikatora na początku wiadomości zatwierdzenia.

Poniżej przedstawiono kompletny kod skryptu napisanego w języku Python. Realizuje on logikę walidacji w fazie `pre-execute`, wykorzystując mechanizm pliku kontekstu do komunikacji z systemem MEVA.

```python
#!/usr/bin/env python3
import sys, json, re

filename = sys.argv[1]
with open(filename, "r", encoding="utf-8") as f:
data = json.load(f)

msg = data.get("pre-payload", {}).get("message", "")

pattern = r"^AB#[0-9]+"

if not re.match(pattern, msg):
data["error"] = {
"code": "VALIDATION_ERROR",
"message": "Commit message must reference an Azure DevOps work item (e.g. AB#1234)",
"details": f"Regex pattern '{pattern}' not matched"
}
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
sys.exit(1)

data["error"] = None
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)

sys.exit(0)
```

Wszystkie pliki związane z rozszerzeniami przechowywane są w katalogu `plugins/`. Dla zasięgu lokalnego katalog ten znajduje się wewnątrz folderu `.meva/`, natomiast dla zasięgu globalnego jego lokalizacja to katalog domowy użytkownika (ścieżka ``~/.meva/plugins/`).

Pluginy są grupowane w katalogach odpowiadających nazwom poleceń, na które mają one reagować (np. `commit/`, `config/set/`). Taka struktura umożliwa szybkie zlokalizowanie wszystkich skryptów zarejestrowanych na wywołanie konkretnego polecenia.

## Instalacja

Zarówno narzędzie wiersza poleceń (CLI), jak i interfejs graficzny (GUI) są dystrybuowane jako
Expand Down
3 changes: 3 additions & 0 deletions cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod init;
pub mod log;
pub mod ls_files;
pub mod ls_tree;
pub mod merge;
pub mod meva_command;
pub mod plugins;
pub mod pull;
Expand All @@ -33,6 +34,7 @@ pub use init::InitCommand;
pub use log::LogCommand;
pub use ls_files::LsFilesCommand;
pub use ls_tree::LsTreeCommand;
pub use merge::MergeCommand;
pub use plugins::PluginsCommand;
pub use pull::PullCommand;
pub use push::PushCommand;
Expand Down Expand Up @@ -91,5 +93,6 @@ pub fn collect_commands() -> CommandsCollection {
Box::new(PushCommand),
Box::new(PullCommand),
Box::new(CheckoutCommand),
Box::new(MergeCommand),
]
}
130 changes: 130 additions & 0 deletions cli/src/commands/merge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use async_trait::async_trait;
use clap::{Arg, ArgAction, ArgMatches, Command};
use miette::Result;

use engine::engine_container::MevaContainer;

use crate::{commands::MevaCommand, extensions::WithVerbose};

/// Implements the `merge` command for Meva DVCS.
#[derive(Default)]
pub struct MergeCommand;

impl MergeCommand {
/// The branch to merge into the current branch
const ARG_BRANCH: &'static str = "branch";

/// Merge changes but do not create a merge commit (squash)
const ARG_SQUASH: &'static str = "squash";

/// Abort the current merge process and restore the previous state
const ARG_ABORT: &'static str = "abort";

/// Use the given message as the merge commit message
const ARG_MESSAGE: &'static str = "message";

/// Allow fast-forward merge if possible
const ARG_FAST_FORWARD: &'static str = "ff";

/// Create a merge commit even when fast-forward is possible
const ARG_NO_FAST_FORWARD: &'static str = "no-ff";
}

#[async_trait]
impl MevaCommand for MergeCommand {
type Container = MevaContainer;

fn name(&self) -> &'static str {
"merge"
}

fn about(&self) -> &'static str {
"Join development histories together"
}

fn version(&self) -> &'static str {
"1.0.0"
}

/// Builds the CLI argument parser for the `merge` command.
fn build_command(&self) -> Command {
self.build_base_command()
.with_verbose_arg("Enable verbose output during the merge process")
.arg(
Arg::new(Self::ARG_BRANCH)
.help("The branch to merge into the current branch")
.value_name("BRANCH")
.required_unless_present(Self::ARG_ABORT),
)
.arg(
Arg::new(Self::ARG_ABORT)
.long(Self::ARG_ABORT)
.short('a')
.help("Abort the current merge process and restore the previous state")
.action(ArgAction::SetTrue)
.conflicts_with_all([
Self::ARG_BRANCH,
Self::ARG_SQUASH,
Self::ARG_MESSAGE,
Self::ARG_FAST_FORWARD,
Self::ARG_NO_FAST_FORWARD,
]),
)
.arg(
Arg::new(Self::ARG_SQUASH)
.long(Self::ARG_SQUASH)
.short('s')
.help("Merge changes but do not create a merge commit (squash)")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(Self::ARG_FAST_FORWARD)
.long(Self::ARG_FAST_FORWARD)
.help("Allow fast-forward merge if possible")
.action(ArgAction::SetTrue)
.conflicts_with(Self::ARG_NO_FAST_FORWARD),
)
.arg(
Arg::new(Self::ARG_NO_FAST_FORWARD)
.long(Self::ARG_NO_FAST_FORWARD)
.help("Create a merge commit even when fast-forward is possible")
.action(ArgAction::SetTrue)
.conflicts_with(Self::ARG_FAST_FORWARD),
)
.arg(
Arg::new(Self::ARG_MESSAGE)
.long(Self::ARG_MESSAGE)
.short('m')
.value_name("MESSAGE")
.help("Use the given message as the merge commit message")
.requires(Self::ARG_BRANCH),
)
}

/// Executes the `merge` command.
///
/// # Arguments
/// * `matches`: Parsed command-line arguments.
/// * `container`: Dependency injection container.
///
/// # Returns
/// * `Result<()>`: Success or error during execution.
async fn execute(&self, _matches: &ArgMatches, _container: &Self::Container) -> Result<()> {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;

#[rstest]
fn test_command_name_about_version() {
let cmd = MergeCommand;
assert_eq!(cmd.name(), "merge");
assert_eq!(cmd.about(), "Join development histories together");
assert_eq!(cmd.version(), "1.0.0");
}
}
Loading