use std::path::PathBuf;
use std::sync::mpsc::{self, Receiver};
use std::thread;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Clear, List, ListItem, Paragraph, Wrap};
use crate::collection::Collection;
use crate::environment::Environment;
use crate::git_remote::{self, RefKind, RemoteRefs};
use crate::i18n::Strings;
use super::draw::*;
use super::editor::*;
use super::remote::{WorkspaceGitFilter, WorkspaceGitOrigin};
use super::theme::*;
pub(crate) enum GitSaveSource {
Collection,
Workspace {
root: PathBuf,
filter: WorkspaceGitFilter,
},
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum GitSaveTarget {
Branch,
Tag,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum TargetIntent {
NewRef,
ExistingBranch,
}
pub(crate) enum GitSaveError {
TagExists,
RefExistsRace,
Other(String),
}
pub(crate) enum GitSaveMsg {
Refs(Result<RemoteRefs, String>),
Pushed(Result<String, GitSaveError>),
}
pub(crate) enum GitSaveStage {
Connect { field: u8 },
ChoosePaths { field: u8 },
ChooseTarget {
sel: Option<usize>,
refs: Option<RemoteRefs>,
},
CommitMessage,
Pushing,
Done,
Error(String),
}
pub(crate) struct GitSaveWizard {
pub(crate) ci: usize,
pub(crate) url: Editor,
pub(crate) token: Editor,
pub(crate) has_env: bool,
pub(crate) env: Option<Environment>,
pub(crate) include_env: bool,
pub(crate) collection_path: Editor,
pub(crate) env_path: Editor,
pub(crate) target_kind: GitSaveTarget,
pub(crate) target_name: Editor,
pub(crate) target_intent: TargetIntent,
pub(crate) commit_msg: Editor,
pub(crate) origin_gitref: String,
pub(crate) source: GitSaveSource,
pub(crate) stage: GitSaveStage,
pub(crate) rx: Option<Receiver<GitSaveMsg>>,
}
impl GitSaveWizard {
pub(crate) fn new(ci: usize, col: &Collection, env: Option<Environment>) -> Self {
let origin = col
.git_origin
.clone()
.expect("git-save requires a collection git_origin");
let has_env = env.is_some();
let default_env_path = env
.as_ref()
.and_then(|e| e.git_origin.as_ref())
.map(|o| o.path.clone())
.unwrap_or_else(|| {
format!(
"{}.vars",
env.as_ref().map(|e| e.name.as_str()).unwrap_or(&col.name)
)
});
let target_name = match origin.ref_kind {
RefKind::Branch => origin.ref_name.clone(),
RefKind::Tag => String::new(),
};
Self {
ci,
url: Editor::new(&origin.repo_url, false),
token: Editor::blank(),
has_env,
env,
include_env: has_env,
collection_path: Editor::new(&origin.path, false),
env_path: Editor::new(&default_env_path, false),
target_kind: GitSaveTarget::Branch,
target_name: Editor::new(&target_name, false),
target_intent: TargetIntent::ExistingBranch,
commit_msg: Editor::new(&format!("Update {} via PaperBoy", col.name), false),
origin_gitref: origin.gitref(),
source: GitSaveSource::Collection,
stage: GitSaveStage::Connect { field: 0 },
rx: None,
}
}
pub(crate) fn new_workspace(ci: usize, col: &Collection, origin: &WorkspaceGitOrigin) -> Self {
let root = col
.workspace_root
.clone()
.expect("workspace git-save requires a workspace_root");
let (target_kind, origin_gitref) = match origin.ref_kind {
RefKind::Branch => (
GitSaveTarget::Branch,
git_remote::branch_ref(&origin.ref_name),
),
RefKind::Tag => (GitSaveTarget::Tag, git_remote::tag_ref(&origin.ref_name)),
};
let target_name = match origin.ref_kind {
RefKind::Branch => origin.ref_name.clone(),
RefKind::Tag => String::new(),
};
let target_intent = match origin.ref_kind {
RefKind::Branch => TargetIntent::ExistingBranch,
RefKind::Tag => TargetIntent::NewRef,
};
Self {
ci,
url: Editor::new(&origin.repo_url, false),
token: Editor::blank(),
has_env: false,
env: None,
include_env: false,
collection_path: Editor::blank(),
env_path: Editor::blank(),
target_kind,
target_name: Editor::new(&target_name, false),
target_intent,
commit_msg: Editor::new(&format!("Update {} via PaperBoy", col.name), false),
origin_gitref,
source: GitSaveSource::Workspace {
root,
filter: origin.filter,
},
stage: GitSaveStage::Connect { field: 0 },
rx: None,
}
}
pub(crate) fn token_opt(&self) -> Option<String> {
let t = self.token.text();
if t.trim().is_empty() { None } else { Some(t) }
}
}
pub(crate) fn spawn_git_save_refs(url: String, token: Option<String>) -> Receiver<GitSaveMsg> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let _ = tx.send(GitSaveMsg::Refs(git_remote::refs_fresh(
&url,
token.as_deref(),
)));
});
rx
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn spawn_git_save_push(
url: String,
token: Option<String>,
origin_gitref: String,
target_kind: GitSaveTarget,
target_name: String,
intent: TargetIntent,
files: Vec<(String, String)>,
message: String,
) -> Receiver<GitSaveMsg> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let result = (|| -> Result<String, GitSaveError> {
let fresh =
git_remote::refs_fresh(&url, token.as_deref()).map_err(GitSaveError::Other)?;
if target_kind == GitSaveTarget::Tag && fresh.tags.iter().any(|t| t == &target_name) {
return Err(GitSaveError::TagExists);
}
let is_existing_branch = target_kind == GitSaveTarget::Branch
&& fresh.branches.iter().any(|b| b == &target_name);
if target_kind == GitSaveTarget::Branch
&& intent == TargetIntent::NewRef
&& is_existing_branch
{
return Err(GitSaveError::RefExistsRace);
}
let base_gitref = if is_existing_branch {
git_remote::branch_ref(&target_name)
} else {
origin_gitref.clone()
};
let (repo, base_sha) = git_remote::fetch_base(&url, token.as_deref(), &base_gitref)
.map_err(GitSaveError::Other)?;
let (author_name, author_email) = git_remote::author_identity();
let push_result = (|| -> Result<String, String> {
let commit_sha = git_remote::commit_files(
&repo,
&base_sha,
&files,
&message,
&author_name,
&author_email,
)?;
let target_ref = match target_kind {
GitSaveTarget::Branch => git_remote::branch_ref(&target_name),
GitSaveTarget::Tag => git_remote::tag_ref(&target_name),
};
git_remote::push_commit(&url, token.as_deref(), &repo, &commit_sha, &target_ref)?;
Ok(commit_sha)
})();
git_remote::cleanup(&repo);
push_result.map_err(GitSaveError::Other)
})();
let _ = tx.send(GitSaveMsg::Pushed(result));
});
rx
}
pub(crate) fn draw_git_save_wizard(f: &mut Frame, w: &GitSaveWizard, s: &Strings, th: &Theme) {
let title = s.git_save_title;
match &w.stage {
GitSaveStage::Connect { field } => {
let area = centered_rect(74, 8, f.area());
f.render_widget(Clear, area);
let block = panel(title.to_string(), true, th);
let inner = block.inner(area);
f.render_widget(block, area);
let rows = Layout::vertical([
Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(1), ])
.split(inner);
f.render_widget(
Paragraph::new(Span::styled(
s.git_url_label,
Style::default().fg(th.accent),
)),
rows[0],
);
render_line_field(f, rows[1], &w.url, *field == 0, false, th);
f.render_widget(
Paragraph::new(Span::styled(
s.git_token_label,
Style::default().fg(th.accent),
)),
rows[3],
);
render_line_field(f, rows[4], &w.token, *field == 1, true, th);
f.render_widget(
Paragraph::new(Line::styled(
s.git_connect_hint,
Style::default().fg(th.dim),
)),
rows[5],
);
}
GitSaveStage::ChoosePaths { field } => {
let rows_n: u16 = if w.has_env { 8 } else { 4 };
let area = centered_rect(76, rows_n + 4, f.area());
f.render_widget(Clear, area);
let block = panel(title.to_string(), true, th);
let inner = block.inner(area);
f.render_widget(block, area);
let mut constraints = vec![Constraint::Length(1), Constraint::Length(1)]; if w.has_env {
constraints.push(Constraint::Length(1)); constraints.push(Constraint::Length(1)); if w.include_env {
constraints.push(Constraint::Length(1)); constraints.push(Constraint::Length(1)); }
}
constraints.push(Constraint::Min(1)); let rows = Layout::vertical(constraints).split(inner);
f.render_widget(
Paragraph::new(Span::styled(
s.git_save_collection_path_label,
Style::default().fg(th.accent),
)),
rows[0],
);
render_line_field(f, rows[1], &w.collection_path, *field == 0, false, th);
if w.has_env {
let mark = if w.include_env { "[x]" } else { "[ ]" };
let cb_style = if *field == 1 {
Style::default()
.bg(th.accent)
.fg(th.bg)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(th.text)
};
f.render_widget(
Paragraph::new(Line::styled(
format!("{mark} {}", s.git_save_include_env_label),
cb_style,
)),
rows[3],
);
if w.include_env {
f.render_widget(
Paragraph::new(Span::styled(
s.git_save_env_path_label,
Style::default().fg(th.accent),
)),
rows[4],
);
render_line_field(f, rows[5], &w.env_path, *field == 2, false, th);
}
}
let hint_row = rows.len() - 1;
f.render_widget(
Paragraph::new(Line::styled(
s.git_save_step_hint,
Style::default().fg(th.dim),
)),
rows[hint_row],
);
}
GitSaveStage::ChooseTarget { sel, refs } => {
let branches = refs
.as_ref()
.map(|r| r.branches.clone())
.unwrap_or_default();
let dropdown_rows = if sel.is_some() {
branches.len().min(5) as u16
} else {
0
};
let area = centered_rect(74, 8 + dropdown_rows, f.area());
f.render_widget(Clear, area);
let block = panel(title.to_string(), true, th);
let inner = block.inner(area);
f.render_widget(block, area);
let rows = Layout::vertical([
Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(dropdown_rows), Constraint::Min(1), ])
.split(inner);
let (branch_style, tag_style) = if w.target_kind == GitSaveTarget::Branch {
(
Style::default()
.bg(th.accent)
.fg(th.bg)
.add_modifier(Modifier::BOLD),
Style::default().fg(th.dim),
)
} else {
(
Style::default().fg(th.dim),
Style::default()
.bg(th.accent)
.fg(th.bg)
.add_modifier(Modifier::BOLD),
)
};
f.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(format!(" {} ", s.git_save_branch_label), branch_style),
Span::raw(" "),
Span::styled(format!(" {} ", s.git_save_tag_label), tag_style),
])),
rows[0],
);
f.render_widget(Paragraph::new(Span::raw("")), rows[1]);
render_line_field(f, rows[2], &w.target_name, sel.is_none(), false, th);
if dropdown_rows > 0 {
let items: Vec<ListItem> = branches
.iter()
.take(5)
.enumerate()
.map(|(i, b)| {
let style = if *sel == Some(i) {
Style::default()
.bg(th.accent)
.fg(th.bg)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(th.dim)
};
ListItem::new(Line::styled(b.clone(), style))
})
.collect();
f.render_widget(List::new(items), rows[3]);
}
let hint_row = rows.len() - 1;
f.render_widget(
Paragraph::new(Line::styled(
s.git_save_target_hint,
Style::default().fg(th.dim),
)),
rows[hint_row],
);
}
GitSaveStage::CommitMessage => {
let area = centered_rect(74, 6, f.area());
f.render_widget(Clear, area);
let block = panel(title.to_string(), true, th);
let inner = block.inner(area);
f.render_widget(block, area);
let rows = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(1),
])
.split(inner);
f.render_widget(
Paragraph::new(Span::styled(
s.git_save_commit_msg_label,
Style::default().fg(th.accent),
)),
rows[0],
);
render_line_field(f, rows[1], &w.commit_msg, true, false, th);
f.render_widget(
Paragraph::new(Line::styled(
s.git_save_commit_msg_hint,
Style::default().fg(th.dim),
)),
rows[2],
);
}
GitSaveStage::Pushing => {
let width = (s.git_save_pushing.len() as u16 + 4).min(f.area().width);
let area = centered_rect(width, 3, f.area());
f.render_widget(Clear, area);
let block = panel(title.to_string(), true, th);
let inner = block.inner(area);
f.render_widget(block, area);
f.render_widget(
Paragraph::new(Span::styled(
s.git_save_pushing,
Style::default().fg(th.text).add_modifier(Modifier::BOLD),
)),
inner,
);
}
GitSaveStage::Done => {
let width = (s.git_save_success.len() as u16 + 4).min(f.area().width);
let area = centered_rect(width, 3, f.area());
f.render_widget(Clear, area);
let block = panel(title.to_string(), true, th);
let inner = block.inner(area);
f.render_widget(block, area);
f.render_widget(
Paragraph::new(Span::styled(
s.git_save_success,
Style::default().fg(th.text).add_modifier(Modifier::BOLD),
)),
inner,
);
}
GitSaveStage::Error(e) => {
let width = (f.area().width * 6 / 10).max(40);
let area = centered_rect(width, 8, f.area());
f.render_widget(Clear, area);
let block = panel(title.to_string(), true, th);
let inner = block.inner(area);
f.render_widget(block, area);
let rows = Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).split(inner);
f.render_widget(
Paragraph::new(e.clone())
.style(Style::default().fg(th.err))
.wrap(Wrap { trim: true }),
rows[0],
);
f.render_widget(
Paragraph::new(Line::styled(s.git_error_hint, Style::default().fg(th.dim))),
rows[1],
);
}
}
}