Skip to content

Commit d116e1b

Browse files
authored
Support --json output for status command (#11)
2 parents 1582f97 + 1427abf commit d116e1b

4 files changed

Lines changed: 173 additions & 41 deletions

File tree

Cargo.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ git2 = { version = "^0.20", features = ["vendored-openssl", "vendored-libgit2"]
2424
anyhow = "^1.0"
2525
indoc = "^2.0"
2626
const_format = "^0.2"
27+
serde = { version = "^1.0", features = ["derive"] }
28+
serde_json = "^1.0"
2729

2830
[target.'cfg(windows)'.dependencies]
2931
windows-permissions = "0.2.4"

src/main.rs

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod filter;
1414
mod fs_helpers;
1515
mod key;
1616
mod repo;
17+
mod status;
1718

1819
use anyhow::{Context, Result};
1920
use clap::{Parser, Subcommand};
@@ -81,6 +82,9 @@ enum Commands {
8182
/// Files to check (if empty, shows repository status)
8283
#[arg(value_name = "FILE")]
8384
files: Vec<String>,
85+
/// Output status in JSON format
86+
#[arg(long)]
87+
json: bool,
8488
},
8589
/// Key management commands
8690
#[command(about = "Manage encryption key")]
@@ -143,7 +147,7 @@ fn main() -> Result<()> {
143147
Commands::Init => cmd_init(),
144148
Commands::Unlock { key_source } => cmd_unlock(key_source),
145149
Commands::Lock { force } => cmd_lock(force),
146-
Commands::Status { files } => cmd_status(files),
150+
Commands::Status { files, json } => cmd_status(files, json),
147151
Commands::Key { key_cmd } => cmd_key(key_cmd),
148152
Commands::Filter { filter_cmd } => cmd_filter(filter_cmd),
149153
}
@@ -240,54 +244,58 @@ fn cmd_lock(force: bool) -> Result<()> {
240244
Ok(())
241245
}
242246

243-
fn cmd_status(files: Vec<String>) -> Result<()> {
247+
fn cmd_status(files: Vec<String>, json: bool) -> Result<()> {
244248
let repo = repo::Repo::discover()?;
245249

246250
if files.is_empty() {
247251
// Show repository status
248-
println!("Repository: {}", repo.workdir().display());
249-
let is_unlocked = repo.is_unlocked()?;
250-
println!(
251-
"Status: {}",
252-
if is_unlocked { "unlocked" } else { "locked" }
253-
);
254-
252+
let repo_status = if repo.is_unlocked()? {
253+
status::LockStatus::Unlocked
254+
} else {
255+
status::LockStatus::Locked
256+
};
255257
let filters_configured = repo.filters_configured()?;
256-
println!(
257-
"Filters configured: {}",
258-
if filters_configured { "yes" } else { "no" }
259-
);
260-
261-
println!("\nTracked files configured for encryption by Git filter:");
262-
let mut has_files = false;
263-
for file_result in repo.find_filtered_files()? {
264-
let file = file_result?;
265-
println!(" 🔒 {}", file.display());
266-
has_files = true;
267-
}
268-
if !has_files {
269-
println!(" (none)");
270-
}
271-
272-
// Only show warning if there are actually untracked files
273-
if repo.has_untracked_files()? {
274-
println!(
275-
"\nNote: You have untracked files in your working copy. Even if some\n\
276-
of those new files match the filter patterns in `.gitattributes`,\n\
277-
they won't be listed here until you `git add` them to the staging area."
278-
);
258+
let has_untracked_files = repo.has_untracked_files()?;
259+
let encrypted_files: Vec<_> = repo
260+
.find_filtered_files()?
261+
.collect::<Result<Vec<_>>>()
262+
.context("Failed to get file path")?;
263+
264+
let status = status::RepositoryStatus {
265+
repository: repo.workdir().to_string_lossy().into_owned(),
266+
status: repo_status,
267+
filters_configured,
268+
encrypted_files,
269+
has_untracked_files,
270+
};
271+
272+
if json {
273+
println!("{}", serde_json::to_string_pretty(&status)?);
274+
} else {
275+
print!("{}", status);
279276
}
280277
} else {
281278
// Check status for specific files
282-
for file_str in &files {
283-
let file_path = std::path::Path::new(file_str);
284-
let is_filtered = repo.is_filtered_file(file_path)?;
285-
let status = if is_filtered {
286-
"🔒 Encrypted in the repository"
287-
} else {
288-
"👀 Not encrypted in the repository"
289-
};
290-
println!("{:20}: {}", file_str, status);
279+
let file_statuses: Vec<status::FileStatus> = files
280+
.iter()
281+
.map(|file_str| {
282+
let file_path = std::path::Path::new(file_str);
283+
let is_filtered = repo.is_filtered_file(file_path)?;
284+
Ok(status::FileStatus {
285+
file: file_path.to_path_buf(),
286+
encrypted: is_filtered,
287+
})
288+
})
289+
.collect::<Result<Vec<_>>>()?;
290+
291+
let status_list = status::FileStatusList {
292+
files: file_statuses,
293+
};
294+
295+
if json {
296+
println!("{}", serde_json::to_string_pretty(&status_list)?);
297+
} else {
298+
print!("{}", status_list);
291299
}
292300
}
293301

src/status.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use serde::Serialize;
2+
use std::fmt;
3+
use std::path::PathBuf;
4+
5+
#[derive(Serialize)]
6+
#[serde(rename_all = "lowercase")]
7+
pub enum LockStatus {
8+
Locked,
9+
Unlocked,
10+
}
11+
12+
impl fmt::Display for LockStatus {
13+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14+
match self {
15+
LockStatus::Locked => write!(f, "locked"),
16+
LockStatus::Unlocked => write!(f, "unlocked"),
17+
}
18+
}
19+
}
20+
21+
#[derive(Serialize)]
22+
pub struct RepositoryStatus {
23+
pub repository: String,
24+
pub status: LockStatus,
25+
pub filters_configured: bool,
26+
pub encrypted_files: Vec<PathBuf>,
27+
pub has_untracked_files: bool,
28+
}
29+
30+
impl fmt::Display for RepositoryStatus {
31+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32+
writeln!(f, "Repository: {}", self.repository)?;
33+
writeln!(f, "Status: {}", self.status)?;
34+
writeln!(
35+
f,
36+
"Filters configured: {}",
37+
if self.filters_configured { "yes" } else { "no" }
38+
)?;
39+
40+
writeln!(
41+
f,
42+
"\nTracked files configured for encryption by Git filter:"
43+
)?;
44+
if self.encrypted_files.is_empty() {
45+
writeln!(f, " (none)")?;
46+
} else {
47+
for file in &self.encrypted_files {
48+
writeln!(f, " 🔒 {}", file.to_string_lossy())?;
49+
}
50+
}
51+
52+
// Only show warning if there are actually untracked files
53+
if self.has_untracked_files {
54+
writeln!(
55+
f,
56+
"\nNote: You have untracked files in your working copy. Even if some\n\
57+
of those new files match the filter patterns in `.gitattributes`,\n\
58+
they won't be listed here until you `git add` them to the staging area."
59+
)?;
60+
}
61+
62+
Ok(())
63+
}
64+
}
65+
66+
#[derive(Serialize)]
67+
pub struct FileStatus {
68+
pub file: PathBuf,
69+
pub encrypted: bool,
70+
}
71+
72+
impl fmt::Display for FileStatus {
73+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74+
let status = if self.encrypted {
75+
"🔒 Encrypted in the repository"
76+
} else {
77+
"👀 Not encrypted in the repository"
78+
};
79+
write!(f, "{:20}: {}", self.file.to_string_lossy(), status)
80+
}
81+
}
82+
83+
#[derive(Serialize)]
84+
pub struct FileStatusList {
85+
pub files: Vec<FileStatus>,
86+
}
87+
88+
impl fmt::Display for FileStatusList {
89+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90+
for file_status in &self.files {
91+
writeln!(f, "{}", file_status)?;
92+
}
93+
Ok(())
94+
}
95+
}

0 commit comments

Comments
 (0)