Skip to content

Commit a36184d

Browse files
authored
Merge pull request #13 from 7a6163/fix/preserve-ignore-comments
2 parents f4543d8 + e6997cc commit a36184d

5 files changed

Lines changed: 130 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
### 2.8.1 / 2026-03-19
2+
3+
#### Fixed
4+
5+
* **`--write-ignore` now preserves existing inline comments.** Previously,
6+
running `--write-ignore` a second time would strip `# comments` from
7+
already-ignored advisory entries because the YAML parser discards comments
8+
and the scanner skips already-ignored advisories (so `build_ignore_comments`
9+
could not regenerate them). The configuration loader now extracts inline
10+
comments from the raw YAML text before parsing and merges them with newly
11+
generated comments on save, with new comments taking precedence.
12+
13+
---
14+
115
### 2.4.0 / 2026-03-11
216

317
#### Fixed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "gem-audit"
3-
version = "2.8.0"
3+
version = "2.8.1"
44
edition = "2024"
55
description = "Ultra-fast, standalone security auditor for Gemfile.lock"
66
license = "MIT"

src/configuration.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::HashSet;
1+
use std::collections::{HashMap, HashSet};
22
use std::path::Path;
33
use thiserror::Error;
44

@@ -9,6 +9,8 @@ pub struct Configuration {
99
pub ignore: HashSet<String>,
1010
/// Maximum database age in days before warning.
1111
pub max_db_age_days: Option<u64>,
12+
/// Inline comments parsed from the YAML file (advisory ID → comment text).
13+
pub ignore_comments: HashMap<String, String>,
1214
}
1315

1416
/// Errors that can occur when loading a configuration file.
@@ -115,8 +117,24 @@ impl Configuration {
115117
})
116118
}
117119

120+
/// Extract inline `# comments` from YAML ignore entries.
121+
///
122+
/// Matches lines in the format produced by [`save()`]: ` - ID # comment`.
123+
fn parse_ignore_comments(yaml: &str) -> HashMap<String, String> {
124+
yaml.lines()
125+
.filter_map(|line| {
126+
let trimmed = line.trim();
127+
let entry = trimmed.strip_prefix("- ")?;
128+
let (id, comment) = entry.split_once(" # ")?;
129+
Some((id.trim().to_string(), comment.to_string()))
130+
})
131+
.collect()
132+
}
133+
118134
/// Parse configuration from a YAML string.
119135
pub fn from_yaml(yaml: &str) -> Result<Self, ConfigError> {
136+
let ignore_comments = Self::parse_ignore_comments(yaml);
137+
120138
let value: serde_yml::Value =
121139
serde_yml::from_str(yaml).map_err(|e| ConfigError::InvalidYaml(e.to_string()))?;
122140

@@ -163,6 +181,7 @@ impl Configuration {
163181
Ok(Configuration {
164182
ignore,
165183
max_db_age_days,
184+
ignore_comments,
166185
})
167186
}
168187
}
@@ -290,6 +309,7 @@ mod tests {
290309
let config = Configuration {
291310
ignore,
292311
max_db_age_days: Some(7),
312+
..Configuration::default()
293313
};
294314
config.save(&path, None).unwrap();
295315

@@ -326,6 +346,7 @@ mod tests {
326346
let config = Configuration {
327347
ignore,
328348
max_db_age_days: None,
349+
..Configuration::default()
329350
};
330351
config.save(&path, None).unwrap();
331352

@@ -349,6 +370,7 @@ mod tests {
349370
let config = Configuration {
350371
ignore,
351372
max_db_age_days: None,
373+
..Configuration::default()
352374
};
353375

354376
let mut comments = std::collections::HashMap::new();
@@ -436,4 +458,86 @@ mod tests {
436458
other => panic!("expected InvalidConfiguration, got: {:?}", other),
437459
}
438460
}
461+
462+
// ========== Comment Parsing ==========
463+
464+
#[test]
465+
fn parse_ignore_comments_extracts_comments() {
466+
let yaml = "---\nignore:\n - CVE-2020-1234 # gem 1.0 (Critical) - Title\n - GHSA-aaaa-bbbb-cccc # rack 2.0 (Medium) - Other\n";
467+
let comments = Configuration::parse_ignore_comments(yaml);
468+
assert_eq!(comments.len(), 2);
469+
assert_eq!(
470+
comments.get("CVE-2020-1234").unwrap(),
471+
"gem 1.0 (Critical) - Title"
472+
);
473+
assert_eq!(
474+
comments.get("GHSA-aaaa-bbbb-cccc").unwrap(),
475+
"rack 2.0 (Medium) - Other"
476+
);
477+
}
478+
479+
#[test]
480+
fn parse_ignore_comments_skips_uncommented_entries() {
481+
let yaml = "---\nignore:\n - CVE-2020-1234\n - GHSA-aaaa-bbbb-cccc # has comment\n";
482+
let comments = Configuration::parse_ignore_comments(yaml);
483+
assert_eq!(comments.len(), 1);
484+
assert!(comments.contains_key("GHSA-aaaa-bbbb-cccc"));
485+
assert!(!comments.contains_key("CVE-2020-1234"));
486+
}
487+
488+
#[test]
489+
fn parse_ignore_comments_empty_yaml() {
490+
let comments = Configuration::parse_ignore_comments("---\nignore: []\n");
491+
assert!(comments.is_empty());
492+
}
493+
494+
#[test]
495+
fn from_yaml_preserves_comments() {
496+
let yaml = "---\nignore:\n - CVE-2020-1234 # gem 1.0 (Critical) - Title\n - GHSA-aaaa-bbbb-cccc\n";
497+
let config = Configuration::from_yaml(yaml).unwrap();
498+
assert_eq!(config.ignore.len(), 2);
499+
assert_eq!(config.ignore_comments.len(), 1);
500+
assert_eq!(
501+
config.ignore_comments.get("CVE-2020-1234").unwrap(),
502+
"gem 1.0 (Critical) - Title"
503+
);
504+
}
505+
506+
#[test]
507+
fn save_and_reload_preserves_comments() {
508+
let tmp = tempfile::tempdir().unwrap();
509+
let path = tmp.path().join(".gem-audit.yml");
510+
511+
let mut ignore = HashSet::new();
512+
ignore.insert("CVE-2020-1234".to_string());
513+
ignore.insert("GHSA-aaaa-bbbb-cccc".to_string());
514+
515+
let mut comments = HashMap::new();
516+
comments.insert(
517+
"CVE-2020-1234".to_string(),
518+
"gem 1.0 (Critical) - Title".to_string(),
519+
);
520+
comments.insert(
521+
"GHSA-aaaa-bbbb-cccc".to_string(),
522+
"rack 2.0 (Medium) - Other".to_string(),
523+
);
524+
525+
let config = Configuration {
526+
ignore,
527+
max_db_age_days: None,
528+
..Configuration::default()
529+
};
530+
config.save(&path, Some(&comments)).unwrap();
531+
532+
let reloaded = Configuration::load(&path).unwrap();
533+
assert_eq!(reloaded.ignore_comments.len(), 2);
534+
assert_eq!(
535+
reloaded.ignore_comments.get("CVE-2020-1234").unwrap(),
536+
"gem 1.0 (Critical) - Title"
537+
);
538+
assert_eq!(
539+
reloaded.ignore_comments.get("GHSA-aaaa-bbbb-cccc").unwrap(),
540+
"rack 2.0 (Medium) - Other"
541+
);
542+
}
439543
}

src/main.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::HashSet;
1+
use std::collections::{HashMap, HashSet};
22
use std::io::{self, IsTerminal, Write};
33
use std::path::{Path, PathBuf};
44
use std::process;
@@ -285,15 +285,21 @@ fn write_ignore_list(
285285
config: &Configuration,
286286
config_path: &Path,
287287
) -> i32 {
288-
let (new_ids, comments) = check::build_ignore_comments(report);
288+
let (new_ids, new_comments) = check::build_ignore_comments(report);
289289
let merged_ignore: HashSet<String> = config.ignore.union(&new_ids).cloned().collect();
290290

291+
let mut merged_comments = config.ignore_comments.clone();
292+
for (id, comment) in new_comments {
293+
merged_comments.insert(id, comment);
294+
}
295+
291296
let updated_config = Configuration {
292297
ignore: merged_ignore,
293298
max_db_age_days: config.max_db_age_days,
299+
ignore_comments: HashMap::new(),
294300
};
295301

296-
match updated_config.save(config_path, Some(&comments)) {
302+
match updated_config.save(config_path, Some(&merged_comments)) {
297303
Ok(()) => {
298304
let count = updated_config.ignore.len() - config.ignore.len();
299305
eprintln!(

0 commit comments

Comments
 (0)