Compare commits

..

14 commits
sqlite ... main

Author SHA1 Message Date
8b648f422e
nix: remove naersk 2022-09-06 00:06:54 +03:00
7f53459116
add nix 2022-08-21 14:40:22 +03:00
9a16557016
repo: remove fs implementation
Closes #34
2022-08-21 12:48:47 +03:00
45672c2964
fixup: insert task without finished at 2022-08-21 00:08:29 +03:00
2702cfd049
Merge pull request 'Get list of finished tasks' (#32) from finished into main
Reviewed-on: #32
2022-08-20 21:02:17 +00:00
8bd588070e
bin: add migration of finished fs tasks 2022-08-21 00:01:28 +03:00
73ab922c77
cli/list: don't show current task on finished 2022-08-21 00:01:00 +03:00
a8f8e23cf4
repo/sqlite: returns finished tasks
- repo/fs: returns finished tasks
- cli/list: add --finished flag to get finished tasks

Closes #4
2022-08-20 23:38:45 +03:00
1224eb2d39
add a makefile with frequently used commands 2022-08-20 22:56:09 +03:00
e2ac408c35
db: add migration 2022-08-20 16:47:55 +03:00
11557e1de2
shell/zsh: use first param to get task idx 2022-08-20 16:17:55 +03:00
6a31732d1b
shell/zsh: add remove cmd
Closes #31
2022-08-20 16:14:17 +03:00
862b3218e5
repo/sqlite: don't update tas info without changes 2022-08-20 15:54:17 +03:00
eb78dad91a
Replace FS with Sqlite (#20)
- [x] Deps: add rusqlite
- [x] Deps: add time
- [x] Add created_at to the domain Task
- [x] Add schema and first migration
- [x] Check current version and run migration
- [x] Implement Repository trait
- [x] Add script to migrate from FS implementation

Closes #5
Closes #19

Reviewed-on: #20
Co-authored-by: Dmitriy Pleshevskiy <dmitriy@ideascup.me>
Co-committed-by: Dmitriy Pleshevskiy <dmitriy@ideascup.me>
2022-08-20 12:42:20 +00:00
17 changed files with 174 additions and 354 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

2
.gitignore vendored
View file

@ -1 +1,3 @@
/target /target
.direnv/

39
Cargo.lock generated
View file

@ -286,43 +286,6 @@ dependencies = [
"time", "time",
] ]
[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]]
name = "serde"
version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b5b8d809babe02f538c2cfec6f2c1ed10804c0e5a6a041a049a4f5588ccc2e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.9.0" version = "1.9.0"
@ -347,8 +310,6 @@ dependencies = [
"clap", "clap",
"log", "log",
"rusqlite", "rusqlite",
"serde",
"serde_json",
"time", "time",
"xdg", "xdg",
] ]

View file

@ -14,8 +14,6 @@ maintenance = { status = "experimental" }
clap = { version = "3.2.16", default-features = false, features = ["derive", "std"] } clap = { version = "3.2.16", default-features = false, features = ["derive", "std"] }
log = "0.4.17" log = "0.4.17"
rusqlite = { version = "0.28.0", features = ["bundled", "time"] } rusqlite = { version = "0.28.0", features = ["bundled", "time"] }
serde = { version = "1.0.142", features = ["derive"] }
serde_json = "1.0.83"
time = "0.3" time = "0.3"
xdg = "2.4.1" xdg = "2.4.1"

View file

@ -0,0 +1,9 @@
CREATE VIEW finished_tasks
AS
SELECT
t.*,
row_number() OVER (ORDER BY t.finished_at DESC) AS idx
FROM tasks AS t
WHERE t.finished_at IS NOT NULL
ORDER BY t.finished_at DESC
;

View file

@ -24,3 +24,13 @@ FROM tasks AS t
WHERE t.finished_at IS NULL WHERE t.finished_at IS NULL
ORDER BY t.created_at ORDER BY t.created_at
; ;
CREATE VIEW finished_tasks
AS
SELECT
t.*,
row_number() OVER (ORDER BY t.finished_at DESC) AS idx
FROM tasks AS t
WHERE t.finished_at IS NOT NULL
ORDER BY t.finished_at DESC
;

7
default.nix Normal file
View file

@ -0,0 +1,7 @@
(import (
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
) {
src = ./.;
}).defaultNix

43
flake.lock Normal file
View file

@ -0,0 +1,43 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1661008273,
"narHash": "sha256-UpDqsGzUswIHG7FwzeIewjWlElF17UVLNbI2pwlbcBY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0cc6444e74cd21e8da8d81ef4cd778492e10f843",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

31
flake.nix Normal file
View file

@ -0,0 +1,31 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, utils }:
utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
cargoToml = with builtins; (fromTOML (readFile ./Cargo.toml));
in
rec {
packages.default = pkgs.rustPlatform.buildRustPackage {
inherit (cargoToml.package) name version;
src = nixpkgs.lib.cleanSource ./.;
doCheck = true;
cargoLock.lockFile = ./Cargo.lock;
};
apps.default = utils.lib.mkApp {
inherit (cargoToml.package) name;
drv = packages.default;
};
devShell = with pkgs; mkShell {
packages = [ cargo rustc rustfmt clippy rust-analyzer ];
RUST_SRC_PATH = rustPlatform.rustLibSrc;
};
});
}

8
makefile Normal file
View file

@ -0,0 +1,8 @@
install:
cargo install --path .
new-migration:
touch ./database/migrations/$$(date +%Y%m%d%H%M).sql

7
shell.nix Normal file
View file

@ -0,0 +1,7 @@
(import (
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
) {
src = ./.;
}).shellNix

View file

@ -3,11 +3,19 @@ function __tas_task_idx() {
} }
function __tas_show() { function __tas_show() {
tas show $(__tas_task_idx "$@") tas show $(__tas_task_idx "$1")
} }
function __tas_start() { function __tas_start() {
tas start $(__tas_task_idx "$@") if [[ "$1" != "" ]]; then
tas start $(__tas_task_idx "$1")
fi
}
function __tas_remove() {
if [[ "$1" != "" ]]; then
tas remove $(__tas_task_idx "$1")
fi
} }
function __tas_list() { function __tas_list() {
@ -28,6 +36,11 @@ function taz() {
"start" | "st") "start" | "st")
__tas_start "$(__tas_list)" __tas_start "$(__tas_list)"
;;
"remove" | "rm")
__tas_remove "$(__tas_list)"
;;
esac esac
} }

View file

@ -1,26 +0,0 @@
use tas::repo::{self, Repository};
use xdg::BaseDirectories;
fn main() {
let xdg_dirs = BaseDirectories::with_prefix(env!("CARGO_PKG_NAME")).unwrap();
let fs_repo = repo::fs::FsRepo::new(xdg_dirs.clone());
let tasks = fs_repo.get_tasks().unwrap();
let sqlite_repo = repo::sqlite::SqliteRepo::new(xdg_dirs).unwrap();
for task in tasks {
log::info!("task: {}", task.name);
log::info!(" inserting...");
sqlite_repo
.insert_task(repo::InsertTaskData {
name: task.name,
project: task.project,
link: task.link,
dir_path: task.dir_path,
index: None,
})
.unwrap();
log::info!(" inserted");
}
}

View file

@ -18,20 +18,27 @@ use crate::repo::Repository;
#[derive(clap::Args)] #[derive(clap::Args)]
pub struct Args { pub struct Args {
#[clap(short, long)]
finished: bool,
projects: Vec<String>, projects: Vec<String>,
} }
pub fn execute(repo: impl Repository, args: Args) { pub fn execute(repo: impl Repository, args: Args) {
let tasks = match repo.get_tasks() { let tasks = match repo.get_tasks(args.finished) {
Ok(tasks) => tasks, Ok(tasks) => tasks,
Err(err) => return eprintln!("Cannot read tasks: {}", err), Err(err) => return eprintln!("Cannot read tasks: {}", err),
}; };
let cur_task = match repo.get_current_task_opt() { let cur_task = if args.finished {
None
} else {
match repo.get_current_task_opt() {
Ok(cur_task) => cur_task, Ok(cur_task) => cur_task,
Err(err) => { Err(err) => {
return eprintln!("Cannot read current task: {}", err); return eprintln!("Cannot read current task: {}", err);
} }
}
}; };
let projects = args let projects = args

View file

@ -13,7 +13,6 @@
//! You should have received a copy of the GNU General Public License //! You should have received a copy of the GNU General Public License
//! along with tas. If not, see <https://www.gnu.org/licenses/>. //! along with tas. If not, see <https://www.gnu.org/licenses/>.
//! //!
pub mod fs;
pub mod sqlite; pub mod sqlite;
use std::path::PathBuf; use std::path::PathBuf;
@ -69,7 +68,7 @@ pub trait Repository {
fn get_task_opt(&self, id: domain::TaskIdx) -> Result<Option<domain::Task>, Error>; fn get_task_opt(&self, id: domain::TaskIdx) -> Result<Option<domain::Task>, Error>;
fn get_tasks(&self) -> Result<Vec<domain::Task>, Error>; fn get_tasks(&self, finished: bool) -> Result<Vec<domain::Task>, Error>;
fn remove_task(&self, id: domain::TaskIdx) -> Result<domain::Task, Error>; fn remove_task(&self, id: domain::TaskIdx) -> Result<domain::Task, Error>;

View file

@ -1,263 +0,0 @@
//! Copyright (C) 2022, Dmitriy Pleshevskiy <dmitriy@ideascup.me>
//!
//! tas is free software: you can redistribute it and/or modify
//! it under the terms of the GNU General Public License as published by
//! the Free Software Foundation, either version 3 of the License, or
//! (at your option) any later version.
//!
//! tas is distributed in the hope that it will be useful,
//! but WITHOUT ANY WARRANTY; without even the implied warranty of
//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//! GNU General Public License for more details.
//!
//! You should have received a copy of the GNU General Public License
//! along with tas. If not, see <https://www.gnu.org/licenses/>.
//!
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use crate::domain;
use crate::repo::{Error, InsertTaskData, Repository, UpdateTaskData};
use serde::{Deserialize, Serialize};
use xdg::BaseDirectories;
#[derive(Deserialize, Serialize, Clone)]
pub struct Task {
name: String,
group: Option<String>,
link: Option<String>,
path: Option<String>,
// created_at
}
impl From<Task> for domain::Task {
fn from(repo: Task) -> Self {
domain::Task {
name: repo.name,
project: repo.group,
link: repo.link,
dir_path: repo.path.map(PathBuf::from),
created_at: time::OffsetDateTime::now_utc(),
}
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct CurrentTaskInfo {
task_idx: usize,
task: Task,
// started_at
}
impl From<CurrentTaskInfo> for domain::CurrentTaskInfo {
fn from(repo: CurrentTaskInfo) -> Self {
domain::CurrentTaskInfo {
task_idx: repo.task_idx,
task: repo.task.into(),
}
}
}
const CURRENT_TASK_FILE: &str = "current.json";
const DATA_FILE: &str = "data.json";
const FINISHED_DATA_FILE: &str = "finished_data.json";
pub struct FsRepo {
xdg_dirs: BaseDirectories,
}
impl FsRepo {
pub fn new(xdg_dirs: BaseDirectories) -> Self {
Self { xdg_dirs }
}
}
impl Repository for FsRepo {
fn get_current_task_opt(&self) -> Result<Option<domain::CurrentTaskInfo>, Error> {
self.get_current_task_impl()
.map(|cur_task| cur_task.map(From::from))
}
fn get_task_opt(&self, id: domain::TaskIdx) -> Result<Option<domain::Task>, Error> {
let tasks = self.get_tasks_impl()?;
if id == 0 || id > tasks.len() {
return Err(Error::NotFound);
}
Ok(Some(tasks[id - 1].clone().into()))
}
fn get_tasks(&self) -> Result<Vec<domain::Task>, Error> {
self.get_tasks_impl()
.map(|tasks| tasks.into_iter().map(Task::into).collect())
}
fn remove_task(&self, id: domain::TaskIdx) -> Result<domain::Task, Error> {
let mut tasks = self.get_tasks_impl()?;
if id == 0 || id > tasks.len() {
return Err(Error::NotFound);
}
let removed = tasks.remove(id - 1);
self.save_tasks_impl(tasks)?;
Ok(removed.into())
}
fn update_task(
&self,
id: domain::TaskIdx,
update_data: UpdateTaskData,
) -> Result<domain::Task, Error> {
let mut tasks = self.get_tasks_impl()?;
if id == 0 || id > tasks.len() {
return Err(Error::NotFound);
}
let mut task = &mut tasks[id - 1];
if let Some(name) = update_data.name {
task.name = name;
}
if let Some(group) = update_data.project {
task.group = group;
}
if let Some(link) = update_data.link {
task.link = link;
}
if let Some(path) = update_data.dir_path {
task.path = path.and_then(|p| p.into_os_string().into_string().ok());
}
let new_task = task.clone();
self.save_tasks_impl(tasks)?;
Ok(new_task.into())
}
fn insert_task(&self, insert_data: InsertTaskData) -> Result<domain::Task, Error> {
let new_task = Task {
name: insert_data.name,
group: insert_data.project,
link: insert_data.link,
path: insert_data
.dir_path
.and_then(|p| p.into_os_string().into_string().ok()),
};
let mut tasks = self.get_tasks_impl()?;
match insert_data.index {
None => tasks.push(new_task.clone()),
Some(idx) => tasks.insert(idx, new_task.clone()),
}
self.save_tasks_impl(tasks)?;
Ok(new_task.into())
}
fn start_task(&self, id: domain::TaskIdx) -> Result<domain::Task, Error> {
let tasks = self.get_tasks_impl()?;
if id == 0 || id > tasks.len() {
return Err(Error::NotFound);
}
let task = &tasks[id - 1];
let mut cur_task = self.get_current_task_impl()?;
cur_task.replace(CurrentTaskInfo {
task_idx: id,
task: task.clone(),
});
self.save_current_task_impl(cur_task)?;
Ok(task.clone().into())
}
fn stop_task(&self) -> Result<domain::Task, Error> {
let mut cur_task = self.get_current_task_impl()?;
let old = cur_task.take().ok_or(Error::NotFound)?;
self.save_current_task_impl(cur_task)?;
Ok(old.task.into())
}
fn finish_task(&self) -> Result<domain::Task, Error> {
let mut cur_task = self.get_current_task_impl()?;
let old = cur_task.take().ok_or(Error::NotFound)?;
let mut finished_tasks = self.get_finished_tasks_impl()?;
let mut tasks = self.get_tasks_impl()?;
let task = tasks.remove(old.task_idx - 1);
finished_tasks.push(task.clone());
self.save_current_task_impl(cur_task)?;
self.save_tasks_impl(tasks)?;
self.save_finished_tasks_impl(finished_tasks)?;
Ok(task.into())
}
}
impl FsRepo {
fn get_current_task_impl(&self) -> Result<Option<CurrentTaskInfo>, Error> {
let file_path = self.xdg_dirs.get_data_file(CURRENT_TASK_FILE);
Ok(File::open(&file_path)
.ok()
.and_then(|file| serde_json::from_reader(file).ok()))
}
fn save_current_task_impl(&self, cur_task: Option<CurrentTaskInfo>) -> Result<(), Error> {
let file_path = self
.xdg_dirs
.place_data_file(CURRENT_TASK_FILE)
.map_err(|_| Error::InsertData)?;
let mut file = File::create(&file_path).map_err(|_| Error::InsertData)?;
let new_data = serde_json::to_vec(&cur_task).map_err(|_| Error::InvalidData)?;
file.write_all(&new_data).map_err(|_| Error::InsertData)
}
fn get_tasks_impl(&self) -> Result<Vec<Task>, Error> {
let file_path = self.xdg_dirs.get_data_file(DATA_FILE);
File::open(&file_path)
.ok()
.map(|file| serde_json::from_reader(file).map_err(|_| Error::InvalidData))
.transpose()
.map(|tasks| tasks.unwrap_or_default())
}
fn save_tasks_impl(&self, tasks: Vec<Task>) -> Result<(), Error> {
let file_path = self
.xdg_dirs
.place_data_file(DATA_FILE)
.map_err(|_| Error::InsertData)?;
let mut file = File::create(&file_path).map_err(|_| Error::InsertData)?;
let new_data = serde_json::to_vec(&tasks).map_err(|_| Error::InvalidData)?;
file.write_all(&new_data).map_err(|_| Error::InsertData)
}
fn get_finished_tasks_impl(&self) -> Result<Vec<Task>, Error> {
let file_path = self.xdg_dirs.get_data_file(FINISHED_DATA_FILE);
File::open(&file_path)
.ok()
.map(|file| serde_json::from_reader(file).map_err(|_| Error::InvalidData))
.transpose()
.map(|tasks| tasks.unwrap_or_default())
}
fn save_finished_tasks_impl(&self, tasks: Vec<Task>) -> Result<(), Error> {
let file_path = self
.xdg_dirs
.place_data_file(FINISHED_DATA_FILE)
.map_err(|_| Error::InsertData)?;
let mut file = File::create(&file_path).map_err(|_| Error::InsertData)?;
let new_data = serde_json::to_vec(&tasks).map_err(|_| Error::InvalidData)?;
file.write_all(&new_data).map_err(|_| Error::InsertData)
}
}

View file

@ -53,7 +53,7 @@ impl<'r> TryFrom<&'r rusqlite::Row<'_>> for Task {
} }
} }
const SCHEMA_FILE: &str = "schema.sql"; const SCHEMA_FILE: &str = "tas.db";
pub struct SqliteRepo { pub struct SqliteRepo {
conn: Connection, conn: Connection,
@ -82,11 +82,16 @@ impl Repository for SqliteRepo {
self.get_task_opt_impl(id).map(|t| t.map(From::from)) self.get_task_opt_impl(id).map(|t| t.map(From::from))
} }
fn get_tasks(&self) -> Result<Vec<domain::Task>, Error> { fn get_tasks(&self, finished: bool) -> Result<Vec<domain::Task>, Error> {
let mut stmt = self let mut stmt = if finished {
.conn self.conn
.prepare("SELECT * FROM finished_tasks")
.map_err(|_| Error::PrepareQuery)?
} else {
self.conn
.prepare("SELECT * FROM active_tasks") .prepare("SELECT * FROM active_tasks")
.map_err(|_| Error::PrepareQuery)?; .map_err(|_| Error::PrepareQuery)?
};
let rows = stmt let rows = stmt
.query_map([], |row| Task::try_from(row)) .query_map([], |row| Task::try_from(row))
@ -251,12 +256,16 @@ impl SqliteRepo {
} }
macro_rules! run_migration { macro_rules! run_migration {
($this:ident, $ver:ident = $version:literal) => { ($this:ident, $ver:ident = $version:expr) => {
run_migration!($this, $ver = $version => concat!("migrations/", $version));
};
($this:ident, $ver:ident = $version:expr => $sql_name:expr) => {
$this $this
.conn .conn
.execute_batch(&format!( .execute_batch(&format!(
"BEGIN; {} COMMIT;", "BEGIN; {} COMMIT;",
include_str!(concat!("../../database/migrations/", $version, ".sql")) include_str!(concat!("../../database/", $sql_name, ".sql"))
)) ))
.map_err(|_| MigrationError::Upgrade)?; .map_err(|_| MigrationError::Upgrade)?;
@ -288,17 +297,21 @@ impl std::fmt::Display for MigrationError {
impl std::error::Error for MigrationError {} impl std::error::Error for MigrationError {}
const LATEST_VERSION: i64 = 202208162308; const LATEST_VERSION: i64 = 202208201623;
impl SqliteRepo { impl SqliteRepo {
pub fn upgrade(&self) -> Result<(), MigrationError> { pub fn upgrade(&self) -> Result<(), MigrationError> {
let mut version = self.version(); let mut version = self.version();
if version == Some(LATEST_VERSION) { match version {
return Ok(()); Some(LATEST_VERSION) => return Ok(()),
None => {
run_migration!(self, version = LATEST_VERSION => "schema");
}
Some(v) => {
if v == 202208162308 {
run_migration!(self, version = 202208201623);
}
} }
if version.is_none() {
run_migration!(self, version = 202208162308);
} }
self.conn self.conn