diff options
Diffstat (limited to 'ticket')
-rw-r--r-- | ticket/Cargo.toml | 1 | ||||
-rw-r--r-- | ticket/src/actions.rs | 65 | ||||
-rw-r--r-- | ticket/src/main.rs | 193 | ||||
-rw-r--r-- | ticket/src/tui.rs | 20 |
4 files changed, 192 insertions, 87 deletions
diff --git a/ticket/Cargo.toml b/ticket/Cargo.toml index 6c752b5..a83f504 100644 --- a/ticket/Cargo.toml +++ b/ticket/Cargo.toml @@ -9,6 +9,7 @@ edition = "2018" [dependencies] anyhow = "1.0" colored = "1.9" +chrono = "0.4" paw = "1.0" rustyline = "5.0" serde = { version = "1.0", features = ["derive"] } diff --git a/ticket/src/actions.rs b/ticket/src/actions.rs index 381c9f7..3e237ff 100644 --- a/ticket/src/actions.rs +++ b/ticket/src/actions.rs @@ -1,5 +1,11 @@ -use crate::Ticket; -use anyhow::Result; +use crate::{ + Ticket, + TicketV0, +}; +use anyhow::{ + bail, + Result, +}; use log::*; use shared::find_root; use std::{ @@ -8,11 +14,11 @@ use std::{ }; pub fn get_open_tickets() -> Result<Vec<Ticket>> { - get_tickets(ticket_root()?.join("open")) + get_tickets(open_tickets()?) } pub fn get_closed_tickets() -> Result<Vec<Ticket>> { - get_tickets(ticket_root()?.join("closed")) + get_tickets(closed_tickets()?) } fn get_tickets(path: PathBuf) -> Result<Vec<Ticket>> { @@ -24,13 +30,60 @@ fn get_tickets(path: PathBuf) -> Result<Vec<Ticket>> { trace!("Looking at entry {}.", path.display()); if path.is_file() { trace!("Entry is a file."); - out.push(toml::from_slice::<Ticket>(&fs::read(&path)?)?); + match toml::from_slice::<Ticket>(&fs::read(&path)?) { + Ok(ticket) => out.push(ticket), + Err(e) => { + error!("Failed to parse ticket {}", path.canonicalize()?.display()); + error!("Is the file an old ticket format? You might need to run `ticket migrate`."); + bail!("Underlying error was {}", e); + } + } } } - out.sort_by(|a, b| a.number.cmp(&b.number)); + out.sort_by(|a, b| a.id.cmp(&b.id)); Ok(out) } pub fn ticket_root() -> Result<PathBuf> { Ok(find_root()?.join(".dev-suite").join("ticket")) } + +pub fn closed_tickets() -> Result<PathBuf> { + Ok(ticket_root()?.join("closed")) +} + +pub fn open_tickets() -> Result<PathBuf> { + Ok(ticket_root()?.join("open")) +} + +// Old version ticket code to handle grabbing code +pub fn get_all_ticketsv0() -> Result<Vec<TicketV0>> { + let mut tickets = get_open_ticketsv0()?; + tickets.extend(get_closed_ticketsv0()?); + Ok(tickets) +} +pub fn get_open_ticketsv0() -> Result<Vec<TicketV0>> { + get_ticketsv0(open_tickets()?) +} + +pub fn get_closed_ticketsv0() -> Result<Vec<TicketV0>> { + get_ticketsv0(closed_tickets()?) +} + +fn get_ticketsv0(path: PathBuf) -> Result<Vec<TicketV0>> { + let mut out = Vec::new(); + debug!("Looking for ticket."); + for entry in fs::read_dir(&path)? { + let entry = entry?; + let path = entry.path(); + trace!("Looking at entry {}.", path.display()); + if path.is_file() { + trace!("Entry is a file."); + if let Ok(ticket) = toml::from_slice::<TicketV0>(&fs::read(&path)?) { + out.push(ticket); + } + } + } + out.sort_by(|a, b| a.number.cmp(&b.number)); + Ok(out) +} diff --git a/ticket/src/main.rs b/ticket/src/main.rs index 4d5457d..11e27e8 100644 --- a/ticket/src/main.rs +++ b/ticket/src/main.rs @@ -6,6 +6,7 @@ use anyhow::{ bail, Result, }; +use chrono::prelude::*; use colored::*; use log::*; use rustyline::{ @@ -21,6 +22,15 @@ use std::{ fs, process, process::Command, + thread, + time, +}; +use uuid::{ + v1::{ + Context, + Timestamp, + }, + Uuid, }; #[derive(structopt::StructOpt)] @@ -33,13 +43,14 @@ struct Args { enum Cmd { /// Initialize the repo to use ticket Init, + /// Update tickets to newer formats + Migrate, + /// Create a new ticket New, - Show { - id: usize, - }, - Close { - id: usize, - }, + /// Show a ticket on the command line + Show { id: Uuid }, + /// Close a ticket on the command line + Close { id: Uuid }, } #[paw::main] @@ -53,6 +64,7 @@ fn main(args: Args) { if let Err(e) = match cmd { Cmd::Init => init(), Cmd::New => new(), + Cmd::Migrate => migrate(), Cmd::Show { id } => show(id), Cmd::Close { id } => close(id), } { @@ -81,22 +93,8 @@ fn new() -> Result<()> { debug!("Getting ticket root."); let ticket_root = ticket_root()?; trace!("Got ticket root: {}", ticket_root.display()); - let open = ticket_root.join("open"); - let closed = ticket_root.join("closed"); + let open = open_tickets()?; let description = ticket_root.join("description"); - let mut ticket_num = 1; - - // Fast enough for now but maybe not in the future - debug!("Getting number of tickets total."); - for entry in fs::read_dir(&open)?.chain(fs::read_dir(&closed)?) { - let entry = entry?; - let path = entry.path(); - if path.is_file() { - ticket_num += 1; - } - } - debug!("Ticket Total: {}", ticket_num - 1); - debug!("Next Ticket ID: {}", ticket_num); let mut rl = Editor::<()>::new(); let title = match rl.readline("Title: ") { @@ -132,16 +130,20 @@ fn new() -> Result<()> { let t = Ticket { title, status: Status::Open, - number: ticket_num, - assignee: None, + id: Uuid::new_v1( + Timestamp::from_unix(Context::new(1), Utc::now().timestamp() as u64, 0), + &[0, 5, 2, 4, 9, 3], + )?, + assignees: Vec::new(), description: description_contents, + comments: Vec::new(), + version: Version::V1, }; debug!("Converting ticket to toml and writing to disk."); fs::write( open.join(&format!( - "{}-{}.toml", - ticket_num, + "{}.toml", t.title .to_lowercase() .split_whitespace() @@ -155,7 +157,7 @@ fn new() -> Result<()> { Ok(()) } -fn show(id: usize) -> Result<()> { +fn show(id: Uuid) -> Result<()> { debug!("Getting ticket root."); let ticket_root = ticket_root()?; trace!("Ticket root at {}.", ticket_root.display()); @@ -170,33 +172,32 @@ fn show(id: usize) -> Result<()> { let path = entry.path(); trace!("Looking at entry {}.", path.display()); if path.is_file() { - if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) { - trace!("Entry is a file."); - if file_name.starts_with(&id.to_string()) { - trace!("This is the expected entry."); - let ticket = toml::from_slice::<Ticket>(&fs::read(&path)?)?; + let ticket = toml::from_slice::<Ticket>(&fs::read(&path)?)?; + if ticket.id == id { + trace!("This is the expected entry."); + println!( + "{}", + format!("{} - {}\n", ticket.id, ticket.title).bold().red() + ); + if !ticket.assignees.is_empty() { println!( - "{}", - format!("{} - {}\n", ticket.number, ticket.title) - .bold() - .red() + "{}{}", + "Assignees: ".bold().purple(), + ticket.assignees.join(", ") ); - if let Some(a) = ticket.assignee { - println!("{}{}", "Assignee: ".bold().purple(), a); - } - - print!( - "{}{}\n\n{}", - "Status: ".bold().purple(), - match ticket.status { - Status::Open => "Open".bold().green(), - Status::Closed => "Closed".bold().red(), - }, - ticket.description - ); - found = true; - break; } + + print!( + "{}{}\n\n{}", + "Status: ".bold().purple(), + match ticket.status { + Status::Open => "Open".bold().green(), + Status::Closed => "Closed".bold().red(), + }, + ticket.description + ); + found = true; + break; } } } @@ -207,7 +208,7 @@ fn show(id: usize) -> Result<()> { } } -fn close(id: usize) -> Result<()> { +fn close(id: Uuid) -> Result<()> { debug!("Getting ticket root."); let ticket_root = ticket_root()?; trace!("Ticket root at {}.", ticket_root.display()); @@ -221,20 +222,20 @@ fn close(id: usize) -> Result<()> { let path = entry.path(); trace!("Looking at entry {}.", path.display()); if path.is_file() { - if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) { - if file_name.starts_with(&id.to_string()) { - trace!("The ticket is open and exists."); - debug!("Reading in the ticket from disk and setting it to closed."); - let mut ticket = toml::from_slice::<Ticket>(&fs::read(&path)?)?; - ticket.status = Status::Closed; - debug!("Writing ticket to disk in the closed directory."); - fs::write(closed.join(file_name), toml::to_string_pretty(&ticket)?)?; - debug!("Removing old ticket."); - fs::remove_file(&path)?; - trace!("Removed the old ticket."); - found = true; - break; - } + let mut ticket = toml::from_slice::<Ticket>(&fs::read(&path)?)?; + if ticket.id == id { + debug!("Ticket found setting it to closed."); + ticket.status = Status::Closed; + trace!("Writing ticket to disk in the closed directory."); + fs::write( + closed.join(path.file_name().expect("Path should have a file name")), + toml::to_string_pretty(&ticket)?, + )?; + debug!("Removing old ticket."); + fs::remove_file(&path)?; + trace!("Removed the old ticket."); + found = true; + break; } } } @@ -245,10 +246,72 @@ fn close(id: usize) -> Result<()> { } } +/// Upgrade from V0 to V1 of the ticket +fn migrate() -> Result<()> { + let ctx = Context::new(1); + let tickets = get_all_ticketsv0()?; + + let open_tickets_path = open_tickets()?; + let closed_tickets_path = closed_tickets()?; + + for t in tickets.into_iter() { + let ticket = Ticket { + title: t.title, + status: t.status, + id: Uuid::new_v1( + Timestamp::from_unix(&ctx, Utc::now().timestamp() as u64, 0), + &[0, 5, 2, 4, 9, 3], + )?, + assignees: t.assignee.map(|a| vec![a]).unwrap_or_else(Vec::new), + description: t.description, + comments: Vec::new(), + version: Version::V1, + }; + + let path = match ticket.status { + Status::Open => &open_tickets_path, + Status::Closed => &closed_tickets_path, + }; + + let mut name = ticket + .title + .split_whitespace() + .collect::<Vec<&str>>() + .join("-"); + name.push_str(".toml"); + name = name.to_lowercase(); + fs::write(path.join(&name), toml::to_string_pretty(&ticket)?)?; + fs::remove_file(path.join(format!("{}-{}", t.number, name)))?; + // We need to make sure we get different times for each ticket + // Possible future migrations might not have this issue + thread::sleep(time::Duration::from_millis(1000)); + } + Ok(()) +} + #[derive(Serialize, Deserialize)] pub struct Ticket { title: String, status: Status, + id: Uuid, + assignees: Vec<String>, + description: String, + comments: Vec<(User, String)>, + version: Version, +} + +#[derive(Serialize, Deserialize)] +pub enum Version { + V1, +} + +#[derive(Serialize, Deserialize)] +pub struct User(String); + +#[derive(Serialize, Deserialize)] +pub struct TicketV0 { + title: String, + status: Status, number: usize, assignee: Option<String>, description: String, diff --git a/ticket/src/tui.rs b/ticket/src/tui.rs index 90dbeb6..e1aa666 100644 --- a/ticket/src/tui.rs +++ b/ticket/src/tui.rs @@ -216,7 +216,7 @@ pub fn run() -> Result<()> { .direction(Direction::Horizontal) .vertical_margin(3) .constraints( - [Constraint::Percentage(50), Constraint::Percentage(50)].as_ref(), + [Constraint::Percentage(30), Constraint::Percentage(70)].as_ref(), ) .split(size); @@ -283,7 +283,7 @@ pub fn run() -> Result<()> { impl<'a> App<'a> { fn table(&self, tab: &'a str) -> impl Widget + '_ { Table::new( - ["Id", "Title", "Assignee"].iter(), + ["Id", "Title"].iter(), self .tickets .tickets @@ -292,15 +292,7 @@ impl<'a> App<'a> { .iter() .enumerate() .map(move |(idx, i)| { - let data = vec![ - i.number.to_string(), - i.title.to_string(), - i.assignee - .as_ref() - .cloned() - .unwrap_or_else(|| "None".into()), - ] - .into_iter(); + let data = vec![i.id.to_string(), i.title.to_string()].into_iter(); let normal_style = Style::default().fg(Color::Yellow); let selected_style = Style::default().fg(Color::White).modifier(Modifier::BOLD); @@ -313,11 +305,7 @@ impl<'a> App<'a> { ) .block(Block::default().title(tab).borders(Borders::ALL)) .header_style(Style::default().fg(Color::Yellow)) - .widths(&[ - Constraint::Percentage(10), - Constraint::Percentage(70), - Constraint::Percentage(20), - ]) + .widths(&[Constraint::Percentage(30), Constraint::Percentage(70)]) .style(Style::default().fg(Color::White)) .column_spacing(1) } |