Compare commits
No commits in common. "bee2d265d79d7481e33d0942e5e4d39471bb5741" and "21a549b19f69f3188203c169c312a88f7d32a44f" have entirely different histories.
bee2d265d7
...
21a549b19f
24 changed files with 46 additions and 441 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,3 @@
|
||||||
/target
|
/target
|
||||||
/dist
|
/dist
|
||||||
.env
|
.env
|
||||||
.vscode
|
|
|
@ -1,131 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use git2::DiffLine;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::path::Path;
|
|
||||||
use time::{
|
|
||||||
format_description::well_known::{Iso8601, Rfc2822},
|
|
||||||
OffsetDateTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::ThreadSafeRepository;
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
|
||||||
pub struct LineChange {
|
|
||||||
pub op: char,
|
|
||||||
pub old_line: Option<u32>,
|
|
||||||
pub new_line: Option<u32>,
|
|
||||||
pub content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
|
||||||
pub struct CommitInfo {
|
|
||||||
pub oid: String,
|
|
||||||
pub message: String,
|
|
||||||
pub author: String,
|
|
||||||
pub date: String,
|
|
||||||
pub changes: Vec<LineChange>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ThreadSafeRepository {
|
|
||||||
pub fn get_history(&self, path: &Path) -> Result<Vec<CommitInfo>> {
|
|
||||||
let repository = self.repository()?;
|
|
||||||
|
|
||||||
let mut revwalk = repository.revwalk()?;
|
|
||||||
revwalk.push_head()?;
|
|
||||||
revwalk.set_sorting(git2::Sort::TIME | git2::Sort::REVERSE)?;
|
|
||||||
|
|
||||||
let mut history = Vec::new();
|
|
||||||
let mut last_blob = None;
|
|
||||||
for oid in revwalk {
|
|
||||||
// Get commit
|
|
||||||
let oid = oid?;
|
|
||||||
let commit = repository.find_commit(oid)?;
|
|
||||||
let tree = commit.tree()?;
|
|
||||||
|
|
||||||
// Check if file changed in this commit
|
|
||||||
let entry = tree.get_path(path)?;
|
|
||||||
if last_blob == Some(entry.id()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get changes
|
|
||||||
let current = repository.find_blob(entry.id())?;
|
|
||||||
let old = last_blob.and_then(|id| repository.find_blob(id).ok());
|
|
||||||
|
|
||||||
let mut changes = vec![];
|
|
||||||
repository.diff_blobs(
|
|
||||||
old.as_ref(),
|
|
||||||
None,
|
|
||||||
Some(¤t),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
Some(&mut |_, _, line| {
|
|
||||||
changes.push(line.into());
|
|
||||||
true
|
|
||||||
}),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Write new blob id to compare against
|
|
||||||
last_blob = Some(entry.id());
|
|
||||||
|
|
||||||
history.push(CommitInfo {
|
|
||||||
oid: commit.id().to_string(),
|
|
||||||
message: commit.message().unwrap_or_default().to_string(),
|
|
||||||
author: commit.author().to_string(),
|
|
||||||
date: OffsetDateTime::from_unix_timestamp(commit.time().seconds())
|
|
||||||
.unwrap_or_else(|_| OffsetDateTime::now_utc())
|
|
||||||
.format(&Iso8601::DEFAULT)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
changes,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(history)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_file_at_commit(&self, path: &Path, oid: &str) -> Result<String> {
|
|
||||||
// lol codewhisperer autocompleted all this, have fun
|
|
||||||
let repository = self.repository()?;
|
|
||||||
let commit = repository.find_commit(oid.parse()?)?;
|
|
||||||
let tree = commit.tree()?;
|
|
||||||
let entry = tree.get_path(path)?;
|
|
||||||
let blob = repository.find_blob(entry.id())?;
|
|
||||||
Ok(String::from_utf8_lossy(blob.content()).into_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DiffLine<'_>> for LineChange {
|
|
||||||
fn from(value: DiffLine<'_>) -> Self {
|
|
||||||
Self {
|
|
||||||
op: value.origin(),
|
|
||||||
old_line: value.old_lineno(),
|
|
||||||
new_line: value.new_lineno(),
|
|
||||||
content: String::from_utf8_lossy(value.content()).into_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommitInfo {
|
|
||||||
pub fn diff(&self) -> Vec<String> {
|
|
||||||
let mut ordered = self.changes.clone();
|
|
||||||
ordered.sort_by(|a, b| {
|
|
||||||
let line_a = a.old_line.or_else(|| a.new_line).unwrap_or_default();
|
|
||||||
let line_b = b.old_line.or_else(|| b.new_line).unwrap_or_default();
|
|
||||||
line_a.cmp(&line_b)
|
|
||||||
});
|
|
||||||
ordered
|
|
||||||
.iter()
|
|
||||||
.map(|change| format!("{} {}", change.op, change.content))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn date_human(&self) -> String {
|
|
||||||
OffsetDateTime::parse(&self.date, &Iso8601::DEFAULT)
|
|
||||||
.ok()
|
|
||||||
.and_then(|date| date.format(&Rfc2822).ok())
|
|
||||||
.unwrap_or_else(|| self.date.clone())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::{
|
use super::{
|
||||||
accept::{parse_accept, ResponseType},
|
accept::{parse_accept, ResponseType},
|
||||||
error::{ErrorInfo, Result},
|
error::{AppError, ErrorInfo},
|
||||||
};
|
};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_axum::IntoResponse;
|
use askama_axum::IntoResponse;
|
||||||
|
@ -12,7 +12,7 @@ use axum::{
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
pub type HandlerResponse = Result<askama_axum::Response>;
|
pub type HandlerResponse = Result<askama_axum::Response, AppError>;
|
||||||
|
|
||||||
struct Response {
|
struct Response {
|
||||||
html: String,
|
html: String,
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
use crate::git::{GitConfig, ThreadSafeRepository};
|
|
||||||
use crate::http::response::response_interceptor;
|
use crate::http::response::response_interceptor;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use axum::middleware::from_fn;
|
use axum::middleware::from_fn;
|
||||||
use bollard::Docker;
|
use bollard::Docker;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use node::git::{GitConfig, ThreadSafeRepository};
|
||||||
use std::{net::SocketAddr, path::PathBuf};
|
use std::{net::SocketAddr, path::PathBuf};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
mod git;
|
|
||||||
mod http;
|
mod http;
|
||||||
mod nix;
|
mod nix;
|
||||||
mod node;
|
mod node;
|
||||||
mod route;
|
mod route;
|
||||||
mod stack;
|
|
||||||
|
|
||||||
/// GitOps+WebUI for arion-based stacks
|
/// GitOps+WebUI for arion-based stacks
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
|
|
|
@ -3,8 +3,6 @@ use git2::{ErrorCode, Index, IndexAddOption, Oid, Repository, Signature};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
pub mod history;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ThreadSafeRepository {
|
pub struct ThreadSafeRepository {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
|
@ -1,4 +1,6 @@
|
||||||
pub mod container;
|
pub mod container;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod git;
|
||||||
pub mod nix;
|
pub mod nix;
|
||||||
|
pub mod stack;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
|
|
|
@ -6,9 +6,8 @@ use tempfile::tempdir;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
git::ThreadSafeRepository,
|
|
||||||
http::error::AppError,
|
http::error::AppError,
|
||||||
node::{error::StackError, nix::parse_arion_compose},
|
node::{error::StackError, git::ThreadSafeRepository, nix::parse_arion_compose},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{arion, utils, COMPOSE_FILE, PACKAGE_CONTENTS, PACKAGE_FILE};
|
use super::{arion, utils, COMPOSE_FILE, PACKAGE_CONTENTS, PACKAGE_FILE};
|
||||||
|
@ -33,8 +32,8 @@ pub async fn write(base_dir: &Path, stack_name: &str, contents: &str) -> Result<
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit(repository: ThreadSafeRepository, stack_name: &str, message: &str) -> Result<()> {
|
pub fn commit(repository: ThreadSafeRepository, stack_name: &str, message: &str) -> Result<()> {
|
||||||
let compose_path = path(stack_name);
|
let compose_path = format!("{}/{}", stack_name, COMPOSE_FILE);
|
||||||
repository.commit_files(&[&compose_path], message)?;
|
repository.commit_files(&[&PathBuf::from(compose_path)], message)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,8 +70,3 @@ pub async fn check(arion_bin: &Path, source: &str) -> crate::http::error::Result
|
||||||
Ok(info.project)
|
Ok(info.project)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn path(stack_name: &str) -> PathBuf {
|
|
||||||
format!("{}/{}", stack_name, COMPOSE_FILE).into()
|
|
||||||
}
|
|
|
@ -93,10 +93,3 @@ pub async fn containers(docker: &Docker, stack_name: &str) -> Result<Vec<Contain
|
||||||
}))
|
}))
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shortcut function to get a stack name from directory
|
|
||||||
pub async fn stack_name(base_dir: &Path, folder: &str) -> Result<String> {
|
|
||||||
let compose_file = compose::get(base_dir, folder).await?;
|
|
||||||
let info = parse_arion_compose(&compose_file)?;
|
|
||||||
Ok(info.project)
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::node::container::ContainerInfo;
|
use super::container::ContainerInfo;
|
||||||
|
|
||||||
pub mod arion;
|
pub mod arion;
|
||||||
pub mod compose;
|
pub mod compose;
|
|
@ -3,7 +3,7 @@ use futures_util::future::try_join;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
use crate::git::ThreadSafeRepository;
|
use crate::node::git::ThreadSafeRepository;
|
||||||
|
|
||||||
use super::{arion, COMPOSE_FILE, PACKAGE_CONTENTS, PACKAGE_FILE};
|
use super::{arion, COMPOSE_FILE, PACKAGE_CONTENTS, PACKAGE_FILE};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
http::error::Result,
|
|
||||||
node::container::{kill, remove, restart, start, stop},
|
node::container::{kill, remove, restart, start, stop},
|
||||||
|
route::AppError,
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
|
@ -17,7 +17,7 @@ macro_rules! container_command {
|
||||||
($cmd: ident) => {
|
($cmd: ident) => {
|
||||||
move |Path(cont_name): Path<String>, State(state): State<AppState>| async move {
|
move |Path(cont_name): Path<String>, State(state): State<AppState>| async move {
|
||||||
$cmd(&state.docker, &cont_name).await?;
|
$cmd(&state.docker, &cont_name).await?;
|
||||||
Ok(Redirect::to("./")) as Result<Redirect>
|
Ok(Redirect::to("./")) as Result<Redirect, AppError>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ pub(super) fn router() -> Router<AppState> {
|
||||||
post(
|
post(
|
||||||
move |Path(cont_name): Path<String>, State(state): State<AppState>| async move {
|
move |Path(cont_name): Path<String>, State(state): State<AppState>| async move {
|
||||||
remove(&state.docker, &cont_name).await?;
|
remove(&state.docker, &cont_name).await?;
|
||||||
Ok(Redirect::to("/")) as Result<Redirect>
|
Ok(Redirect::to("/")) as Result<Redirect, AppError>
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
http::error::AppError,
|
http::{
|
||||||
http::response::{reply, HandlerResponse},
|
error::AppError,
|
||||||
node::system::{system_info, SystemInfo},
|
response::{reply, HandlerResponse},
|
||||||
|
},
|
||||||
|
node::{
|
||||||
stack::{list, NodeInfo},
|
stack::{list, NodeInfo},
|
||||||
|
system::{system_info, SystemInfo},
|
||||||
|
},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use axum::{extract::State, http::StatusCode};
|
use axum::{extract::State, http::StatusCode};
|
||||||
|
|
||||||
use crate::{http::error::Result, stack::compose, AppState};
|
use crate::{http::error::AppError, node::stack::compose, AppState};
|
||||||
|
|
||||||
pub(super) async fn check_stack_file(
|
pub(super) async fn check_stack_file(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
body: String,
|
body: String,
|
||||||
) -> Result<StatusCode> {
|
) -> Result<StatusCode, AppError> {
|
||||||
compose::check(&state.arion_bin, &body).await?;
|
compose::check(&state.arion_bin, &body).await?;
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ use axum::{extract::State, response::Redirect, Form};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
http::error::Result,
|
http::error::AppError,
|
||||||
stack::{compose, operation},
|
node::stack::{compose, operation},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ pub(super) struct CreateStackForm {
|
||||||
pub(super) async fn create_stack(
|
pub(super) async fn create_stack(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(form): Form<CreateStackForm>,
|
Form(form): Form<CreateStackForm>,
|
||||||
) -> Result<Redirect> {
|
) -> Result<Redirect, AppError> {
|
||||||
// Make sure body is is ok
|
// Make sure body is is ok
|
||||||
let name = compose::check(&state.arion_bin, &form.source).await?;
|
let name = compose::check(&state.arion_bin, &form.source).await?;
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,7 @@ use axum::{
|
||||||
response::Redirect,
|
response::Redirect,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{http::error::AppError, node::stack::operation, AppState};
|
||||||
http::error::Result,
|
|
||||||
stack::{self, operation},
|
|
||||||
AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "stack/delete-one.html")]
|
#[template(path = "stack/delete-one.html")]
|
||||||
|
@ -17,21 +13,14 @@ struct ConfirmDeleteTemplate {
|
||||||
stack_name: String,
|
stack_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn confirm_deletion_page(
|
pub(super) async fn confirm_deletion_page(Path(stack_name): Path<String>) -> impl IntoResponse {
|
||||||
Path(stack_name): Path<String>,
|
ConfirmDeleteTemplate { stack_name }
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> Result<impl IntoResponse> {
|
|
||||||
let project_id = stack::list::stack_name(&state.stack_dir, &stack_name).await?;
|
|
||||||
|
|
||||||
Ok(ConfirmDeleteTemplate {
|
|
||||||
stack_name: project_id,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn delete_stack(
|
pub(super) async fn delete_stack(
|
||||||
Path(stack_name): Path<String>,
|
Path(stack_name): Path<String>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<Redirect> {
|
) -> Result<Redirect, AppError> {
|
||||||
operation::remove(
|
operation::remove(
|
||||||
&state.stack_dir,
|
&state.stack_dir,
|
||||||
&state.arion_bin,
|
&state.arion_bin,
|
||||||
|
|
|
@ -6,11 +6,7 @@ use axum::{
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{http::error::AppError, node::stack::arion, node::stack::compose, AppState};
|
||||||
http::error::{AppError, Result},
|
|
||||||
stack::{arion, compose},
|
|
||||||
AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub(super) struct EditStackForm {
|
pub(super) struct EditStackForm {
|
||||||
|
@ -22,7 +18,7 @@ pub(super) async fn edit_stack(
|
||||||
Path(stack_name): Path<String>,
|
Path(stack_name): Path<String>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(form): Form<EditStackForm>,
|
Form(form): Form<EditStackForm>,
|
||||||
) -> Result<Redirect> {
|
) -> Result<Redirect, AppError> {
|
||||||
let commit_message = if form.commit_message.trim().is_empty() {
|
let commit_message = if form.commit_message.trim().is_empty() {
|
||||||
format!("Update {}", stack_name)
|
format!("Update {}", stack_name)
|
||||||
} else {
|
} else {
|
||||||
|
@ -43,34 +39,20 @@ pub(super) async fn edit_stack(
|
||||||
// Make sure file is ok
|
// Make sure file is ok
|
||||||
compose::check(&state.arion_bin, &source).await?;
|
compose::check(&state.arion_bin, &source).await?;
|
||||||
|
|
||||||
edit_stack_int(state, &stack_name, &source, &commit_message).await?;
|
|
||||||
|
|
||||||
Ok(Redirect::to("./"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn edit_stack_int(
|
|
||||||
state: AppState,
|
|
||||||
stack_name: &str,
|
|
||||||
source: &str,
|
|
||||||
commit_message: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Make sure file is ok
|
|
||||||
compose::check(&state.arion_bin, &source).await?;
|
|
||||||
|
|
||||||
// Write compose file
|
// Write compose file
|
||||||
compose::write(&state.stack_dir, stack_name, source).await?;
|
compose::write(&state.stack_dir, &stack_name, &source).await?;
|
||||||
|
|
||||||
// Git commit
|
// Git commit
|
||||||
compose::commit(state.repository, stack_name, commit_message)?;
|
compose::commit(state.repository, &stack_name, &commit_message)?;
|
||||||
|
|
||||||
// Update stack
|
// Update stack
|
||||||
arion::command(
|
arion::command(
|
||||||
&state.stack_dir,
|
&state.stack_dir,
|
||||||
stack_name,
|
&stack_name,
|
||||||
&state.arion_bin,
|
&state.arion_bin,
|
||||||
arion::StackCommand::Start,
|
arion::StackCommand::Start,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(Redirect::to("./"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
use askama::Template;
|
|
||||||
use axum::{
|
|
||||||
extract::{Path, State},
|
|
||||||
response::Redirect,
|
|
||||||
Form,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
git::history::CommitInfo,
|
|
||||||
http::error::Result,
|
|
||||||
http::response::{reply, HandlerResponse},
|
|
||||||
node::nix::parse_arion_compose,
|
|
||||||
stack::compose,
|
|
||||||
AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::edit::edit_stack_int;
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "stack/history.html")]
|
|
||||||
struct HistoryTemplate {
|
|
||||||
stack_name: String,
|
|
||||||
commits: Vec<CommitInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub(super) struct RestoreForm {
|
|
||||||
oid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn list(
|
|
||||||
Path(stack_name): Path<String>,
|
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> HandlerResponse {
|
|
||||||
let compose_file = compose::get(&state.stack_dir, &stack_name).await?;
|
|
||||||
let info = parse_arion_compose(&compose_file)?;
|
|
||||||
|
|
||||||
let git_compose_path = compose::path(&stack_name);
|
|
||||||
let commits = state.repository.get_history(&git_compose_path)?;
|
|
||||||
|
|
||||||
let mut newest_first = commits.clone();
|
|
||||||
newest_first.reverse();
|
|
||||||
|
|
||||||
reply(
|
|
||||||
json!({
|
|
||||||
"commits": commits,
|
|
||||||
}),
|
|
||||||
HistoryTemplate {
|
|
||||||
stack_name: info.project,
|
|
||||||
commits: newest_first,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn restore(
|
|
||||||
Path(stack_name): Path<String>,
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Form(form): Form<RestoreForm>,
|
|
||||||
) -> Result<Redirect> {
|
|
||||||
let path = compose::path(&stack_name);
|
|
||||||
let source = state.repository.get_file_at_commit(&path, &form.oid)?;
|
|
||||||
let short_id = form.oid[..6].to_string();
|
|
||||||
|
|
||||||
edit_stack_int(
|
|
||||||
state,
|
|
||||||
&stack_name,
|
|
||||||
&source,
|
|
||||||
format!("Revert to {}", short_id).as_str(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Redirect::to(".."))
|
|
||||||
}
|
|
|
@ -6,8 +6,8 @@ use axum::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
http::error::Result,
|
http::error::AppError,
|
||||||
stack::arion::{command, StackCommand},
|
node::stack::arion::{command, StackCommand},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,14 +17,13 @@ mod check;
|
||||||
mod create;
|
mod create;
|
||||||
mod delete;
|
mod delete;
|
||||||
mod edit;
|
mod edit;
|
||||||
mod history;
|
|
||||||
mod read;
|
mod read;
|
||||||
|
|
||||||
macro_rules! stack_command {
|
macro_rules! stack_command {
|
||||||
($cmd: expr) => {
|
($cmd: expr) => {
|
||||||
move |Path(stack_name): Path<String>, State(state): State<AppState>| async move {
|
move |Path(stack_name): Path<String>, State(state): State<AppState>| async move {
|
||||||
command(&state.stack_dir, &stack_name, &state.arion_bin, $cmd).await?;
|
command(&state.stack_dir, &stack_name, &state.arion_bin, $cmd).await?;
|
||||||
Ok(Redirect::to("./")) as Result<Redirect>
|
Ok(Redirect::to("./")) as Result<Redirect, AppError>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -55,6 +54,5 @@ pub(super) fn router() -> Router<AppState> {
|
||||||
"/:stack/delete",
|
"/:stack/delete",
|
||||||
get(delete::confirm_deletion_page).post(delete::delete_stack),
|
get(delete::confirm_deletion_page).post(delete::delete_stack),
|
||||||
)
|
)
|
||||||
.route("/:stack/history", get(history::list))
|
//.route("/:stack/history", get(get_stack_history))
|
||||||
.route("/:stack/history/revert", post(history::restore))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,14 @@ use serde_json::json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
http::response::{reply, HandlerResponse},
|
http::response::{reply, HandlerResponse},
|
||||||
node::container::ContainerInfo,
|
node::{container::ContainerInfo, nix::parse_arion_compose, stack},
|
||||||
node::nix::parse_arion_compose,
|
AppState,
|
||||||
stack, AppState,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "stack/get-one.html")]
|
#[template(path = "stack/get-one.html")]
|
||||||
struct GetOneTemplate {
|
struct GetOneTemplate {
|
||||||
|
stack_folder: String,
|
||||||
stack_name: String,
|
stack_name: String,
|
||||||
file_contents: String,
|
file_contents: String,
|
||||||
containers: Vec<ContainerInfo>,
|
containers: Vec<ContainerInfo>,
|
||||||
|
@ -33,6 +33,7 @@ pub(super) async fn get_one(
|
||||||
"containers": containers,
|
"containers": containers,
|
||||||
}),
|
}),
|
||||||
GetOneTemplate {
|
GetOneTemplate {
|
||||||
|
stack_folder: stack_name,
|
||||||
stack_name: info.project,
|
stack_name: info.project,
|
||||||
file_contents,
|
file_contents,
|
||||||
containers: containers.iter().map(|c| c.clone().into()).collect(),
|
containers: containers.iter().map(|c| c.clone().into()).collect(),
|
||||||
|
|
|
@ -100,13 +100,6 @@ button {
|
||||||
color: var(--link-hover);
|
color: var(--link-hover);
|
||||||
border-color: var(--link-hover);
|
border-color: var(--link-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: #323035;
|
|
||||||
border-color: #3C393F;
|
|
||||||
color: #6F6D78;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.table {
|
table.table {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{% block title %}{% endblock %} - staxman</title>
|
<title>{% block title %}{{ title }} - staxman{% endblock %}</title>
|
||||||
<link rel="preconnect" href="https://rsms.me/">
|
<link rel="preconnect" href="https://rsms.me/">
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||||
<link rel="stylesheet" href="https://iosevka-webfonts.github.io/iosevka/iosevka.css">
|
<link rel="stylesheet" href="https://iosevka-webfonts.github.io/iosevka/iosevka.css">
|
||||||
|
|
|
@ -1,140 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Stack history for {{stack_name}}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<main>
|
|
||||||
<header>
|
|
||||||
<h1>Stack history for <span class="stack-name">{{stack_name}}</span></h1>
|
|
||||||
<div id="page-actions">
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<section class="versions">
|
|
||||||
{% for (i, commit) in commits.iter().enumerate() %}
|
|
||||||
<article>
|
|
||||||
<p>
|
|
||||||
<time datetime="{{commit.date}}">{{commit.date_human()}}</time>
|
|
||||||
<span class="commit_id">
|
|
||||||
{{commit.oid[..7]}}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<details open>
|
|
||||||
<summary title="{{commit.message}} ({{commit.author}})">
|
|
||||||
{{commit.message}}
|
|
||||||
</summary>
|
|
||||||
<code>
|
|
||||||
{% for line in commit.diff() %}
|
|
||||||
{% match line.chars().next() %}
|
|
||||||
{% when Some with ('+') %}
|
|
||||||
<pre class="add">{{line}}</pre>
|
|
||||||
{% when Some with ('-') %}
|
|
||||||
<pre class="del">{{line}}</pre>
|
|
||||||
{% when _ %}
|
|
||||||
<pre class="context">{{line}}</pre>
|
|
||||||
{% endmatch %}
|
|
||||||
{% endfor %}
|
|
||||||
</code>
|
|
||||||
</details>
|
|
||||||
<div class="actions">
|
|
||||||
{% if i == 0 %}
|
|
||||||
<button disabled>CURRENT</button>
|
|
||||||
{% else %}
|
|
||||||
<form method="POST" action="./history/revert">
|
|
||||||
<input type="hidden" name="oid" value="{{commit.oid}}" />
|
|
||||||
<button type="submit">Revert to this revision</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.stack-name {
|
|
||||||
color: var(--text-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
summary {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.versions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1ch;
|
|
||||||
|
|
||||||
& article {
|
|
||||||
border: 1px solid var(--table-border-color);
|
|
||||||
background-color: var(--table-bg);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 8px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5ch;
|
|
||||||
|
|
||||||
& p {
|
|
||||||
font-size: 9pt;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
& .commit_id::before {
|
|
||||||
content: " - ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& code {
|
|
||||||
font-size: 10pt;
|
|
||||||
background-color: var(--bg-raised);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
margin-top: 0.5ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
& pre {
|
|
||||||
&.context {
|
|
||||||
color: var(--button-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.add {
|
|
||||||
color: var(--success-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.del {
|
|
||||||
color: var(--danger-text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
& button {
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-size: 10pt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
const actions = document.getElementById("page-actions");
|
|
||||||
const toggle_text = ["Collapse all", "Expand all"];
|
|
||||||
|
|
||||||
/** @type {HTMLDetailsElement[]} */
|
|
||||||
const versions = [...document.querySelectorAll(".versions details")];
|
|
||||||
|
|
||||||
let toggle = document.createElement("button");
|
|
||||||
toggle.type = "button";
|
|
||||||
toggle.appendChild(document.createTextNode(toggle_text[0]));
|
|
||||||
toggle.addEventListener("click", () => {
|
|
||||||
const index = toggle_text.indexOf(toggle.textContent);
|
|
||||||
versions.forEach(version => { version.open = index == 1 });
|
|
||||||
toggle.textContent = toggle_text[(index + 1) % 2];
|
|
||||||
});
|
|
||||||
actions.appendChild(toggle);
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
Reference in a new issue