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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- [cmd/list] added `--csv` option (GitHub Issue #57)
- [cmd/list] added `--sort` option (GitHub Issue #7)
- [cmd/list] added `--filter` option (GitHub Issue #6)
- [cmd] added `link` command to change relationships between issues (GitHub Issue #29)
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Issues live alongside your code inside `.gitissues/`, making them platform-indep
- ✅ Highly configurable: default columns for `list`, available options for `state` and `type`, relation ship categories, commit message template, external editor
- ✅ External editor renders issue information as markdown
- ✅ Git-integration: auto-commit of changes
- ✅ Possibility to export issue list into CSV file
- ✅ Automated integration tests
- 🚧 Add `search` command across all issue titles and descriptions
- 🚧 Comments / discussions
Expand Down Expand Up @@ -43,6 +44,7 @@ git issue list --columns id,assignee,title
git issue list --columns "*"
git issue list --filter priority=p2 title=*driver*
git issue list --sort assignee=asc priority=desc
git issue list --csv

# Show all issue information (markdown) -- launches external text editor
git issue show 1234
Expand Down Expand Up @@ -126,6 +128,9 @@ relationships:
link: child
child:
link: parent

# Separator used when exporting to CSV
export_csv_separator: ','
```

#### Options
Expand All @@ -140,6 +145,7 @@ relationships:
- `states` (list of strings): Available issue states. Default is the first element.
- `types` (list of strings): Available issue types. Default is empty.
- `relationships` (object): Available relationships between issues
- `export_csv_separator` (char): Separator for CSV file exports

#### users.yaml

Expand Down Expand Up @@ -233,6 +239,7 @@ Issues live in `.gitissues/issues/{ID}/`:
├── .tmp/ # Temporary files: Put in `.gitignore`
├── config.yaml # Configuration
├── description.md # Description template
├── exports/ # Location of CSV exports: Put in `.gitignore`
└── issues/
└── 0000000001/
├── meta.yaml # Structured metadata
Expand Down
3 changes: 3 additions & 0 deletions config/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ relationships:
link: child
child:
link: parent

# Separator used when exporting to CSV
export_csv_separator: ','
103 changes: 62 additions & 41 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use std::path::Path;

use regex::Regex;

use crate::model::{Filter, Meta, Sorting, dash_if_empty, gitissues_base, load_config, load_meta};
use crate::model::{Filter, Meta, Sorting, current_timestamp, dash_if_empty, gitissues_base, issue_exports_dir, load_config, load_meta};

pub fn run(columns: Option<Vec<String>>, filter: Option<Vec<Filter>>, sort: Option<Vec<Sorting>>) -> Result<(), String> {
pub fn run(columns: Option<Vec<String>>, filter: Option<Vec<Filter>>, sort: Option<Vec<Sorting>>, print_csv: bool) -> Result<(), String> {
let mut issues = get_issues_metadata()?;

sort_issues(&mut issues, sort)?;
Expand All @@ -15,10 +15,10 @@ pub fn run(columns: Option<Vec<String>>, filter: Option<Vec<Filter>>, sort: Opti
// Print
match columns {
None => {
print_default_list(&issues)?;
print_list(&issues, None, print_csv)?;
}
Some(cols) => {
print_custom_list(&issues, cols)?;
print_list(&issues, Some(cols), print_csv)?;
}
}

Expand Down Expand Up @@ -77,60 +77,81 @@ fn validate_column_names(columns: &mut [String], context: &str) -> Result<(), St
Ok(())
}

fn print_default_list(issues: &Vec<Meta>) -> Result<(), String> {
let config = load_config()?;

let mut columns = config.list_columns;

validate_column_names(&mut columns, "config.yaml:list_columns")?;
fn print_ln(print_csv: bool, csv_content: &mut String) {
if print_csv {
csv_content.push('\n');
} else {
println!();
}
}

wildcard_expansion(&mut columns);
fn to_csv_field(value: &str, separator: char) -> String {
format!("\"{value}\"{separator}")
}

let column_widths = calculate_column_widths(issues, &columns)?;
/// print list
/// - issues: list of issue metadata
/// - columns: list of columns to print (None means default from config)
/// - print_csv: whether to print as CSV
fn print_list(issues: &Vec<Meta>, columns: Option<Vec<String>>, print_csv: bool) -> Result<(), String> {
let config = load_config()?;

// Header
for col in &columns {
let width = *column_widths.get(col).unwrap_or(&22);
print!("{:<width$}", col, width = width);
}
println!();
let mut cols = match &columns {
Some(value) => value.clone(),
None => config.list_columns,
};

// Rows
for meta in issues {
for col in &columns {
let value = get_column_value(col, meta)?;
let width = *column_widths.get(col).unwrap_or(&22);
print!("{:<width$}", value, width = width);
}
println!();
}
let context = if columns.is_some() {
"--columns"
} else {
"config.yaml:list_columns"
};

Ok(())
}
validate_column_names(&mut cols, context)?;

fn print_custom_list(issues: &Vec<Meta>, mut columns: Vec<String>) -> Result<(), String> {
validate_column_names(&mut columns, "--columns")?;
wildcard_expansion(&mut cols);

wildcard_expansion(&mut columns);
let column_widths = calculate_column_widths(issues, &cols)?;

let column_widths = calculate_column_widths(issues, &columns)?;
let mut csv_content = String::new();
let csv_separator = config.export_csv_separator;

// Print header
for col in &columns {
let width = *column_widths.get(col).unwrap_or(&22);
print!("{:<width$}", col, width = width);
for col in &cols {
if print_csv {
csv_content.push_str(&to_csv_field(col, csv_separator));
} else {
let width = *column_widths.get(col).unwrap_or(&22);
print!("{:<width$}", col, width = width);
}
}

println!();
print_ln(print_csv, &mut csv_content);

// Print rows
for meta in issues {
for col in &columns {
for col in &cols {
let value = get_column_value(col, meta)?;
let width = *column_widths.get(col).unwrap_or(&22);
print!("{:<width$}", value, width = width);

if print_csv {
csv_content.push_str(&to_csv_field(&value.to_string(), csv_separator));
} else {
let width = *column_widths.get(col).unwrap_or(&22);
print!("{:<width$}", value, width = width);
}
}
println!();

print_ln(print_csv, &mut csv_content);
}

if print_csv {
// Create exports directory
let export_dir = issue_exports_dir();
fs::create_dir_all(&export_dir).map_err(|e| format!("Failed to create {}: {e}", export_dir.display()))?;

// Write CSV file
let export_file = export_dir.join(format!("{}.csv", current_timestamp().replace(":", "-")));
fs::write(&export_file, csv_content).map_err(|e| format!("Failed to write {}: {e}", export_file.display()))?;
}

Ok(())
Expand Down
11 changes: 10 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ enum Commands {
/// Sort issues by meta fields [<field>=asc|desc]
#[arg(long, num_args = 1..)]
sort: Option<Vec<Sorting>>,

/// Print output to CSV file
#[arg(long, default_value_t = false)]
csv: bool,
},

/// Show issue details
Expand Down Expand Up @@ -155,7 +159,12 @@ fn main() {
labels,
} => new::run(title, type_, assignee, priority, due_date, labels),

Commands::List { columns, filter, sort } => list::run(columns, filter, sort),
Commands::List {
columns,
filter,
sort,
csv,
} => list::run(columns, filter, sort, csv),

Commands::Show { id } => show::run(id),

Expand Down
5 changes: 5 additions & 0 deletions src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ pub struct Config {
pub states: Vec<String>,
pub types: Vec<String>,
pub relationships: IndexMap<String, Relationship>,
pub export_csv_separator: char,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -278,6 +279,10 @@ pub fn issue_tmp_show_dir(id: u32) -> std::path::PathBuf {
Path::new(gitissues_base()).join(".tmp").join(format!("show-{id}"))
}

pub fn issue_exports_dir() -> std::path::PathBuf {
Path::new(gitissues_base()).join("exports")
}

/// git commit based on template from config
pub fn git_commit(id: u32, title: String, action: &str) -> Result<(), String> {
use std::process::Command;
Expand Down