debian_workspace/action.rs
1use debian_control::relations::VersionConstraint;
2use debversion::Version;
3use std::path::{Path, PathBuf};
4
5/// One self-consistent set of actions that fixes a [`Diagnostic`].
6#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
7pub struct ActionPlan {
8 /// Imperative description of what this plan would do, shown to the
9 /// user (LSP code-action menu, `lintian-brush --interactive`). Every
10 /// plan must have one — a diagnostic with multiple plans needs each
11 /// titled distinctly so the user can pick.
12 pub label: String,
13 /// If true, this plan only applies when the user has opted into
14 /// opinionated fixes (`--opinionated` / `preferences.opinionated`).
15 /// The driver skips opinionated plans otherwise.
16 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
17 pub opinionated: bool,
18 /// Actions applied as a unit.
19 pub actions: Vec<Action>,
20}
21
22/// A change to apply to the working tree.
23///
24/// Dispatched on file kind: each per-file enum carries the actual operations.
25#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
26#[serde(tag = "kind", rename_all = "snake_case")]
27pub enum Action {
28 /// An edit to a deb822 file (debian/control, debian/copyright, …).
29 Deb822(Deb822Action),
30 /// An edit to a systemd unit file (.service, .socket, .target, …).
31 Systemd(SystemdAction),
32 /// An edit to a freedesktop .desktop entry file.
33 DesktopIni(DesktopIniAction),
34 /// An edit to a YAML file.
35 Yaml(YamlAction),
36 /// An edit to a `debian/changelog` file.
37 Changelog(ChangelogAction),
38 /// An edit to a `debian/watch` file.
39 Watch(WatchAction),
40 /// An edit to a Makefile (typically `debian/rules`).
41 Makefile(MakefileAction),
42 /// An edit to a DEP-3 patch header (a quilt patch under
43 /// `debian/patches/`).
44 Dep3(Dep3Action),
45 /// An edit to a lintian-overrides file (`debian/source/lintian-overrides`
46 /// or `debian/<pkg>.lintian-overrides`).
47 LintianOverrides(LintianOverridesAction),
48 /// An edit to a maintscript file (`debian/maintscript` or
49 /// `debian/<pkg>.maintscript`).
50 Maintscript(MaintscriptAction),
51 /// An edit to a `debian/debcargo.toml` file. Used for Rust crate
52 /// packages where the control file is generated.
53 Debcargo(DebcargoAction),
54 /// Invoke an external tool that mutates files in the working tree (e.g.
55 /// `debconf-updatepo`). Use this only when the operation can't be
56 /// expressed as one of the typed file actions above.
57 RunCommand(RunCommandAction),
58 /// A filesystem-level edit (chmod, write, delete, byte-range replace).
59 Filesystem(FilesystemAction),
60}
61
62/// Continuation-line indent pattern for multi-line deb822 field values.
63///
64/// Mirrors [`deb822_lossless::IndentPattern`] but is `serde`-serialisable
65/// so it can travel over the LSP wire alongside actions.
66#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
67#[serde(tag = "kind", rename_all = "snake_case")]
68pub enum IndentPattern {
69 /// All continuation lines use exactly `spaces` leading spaces.
70 Fixed {
71 /// Number of leading spaces (typically `1` for DEP-5 / Description).
72 spaces: usize,
73 },
74 /// Continuation lines align with the column after the field name and
75 /// `": "`, i.e. `field_name.len() + 2` spaces. The deb822 default for
76 /// most fields.
77 FieldNameLength,
78}
79
80impl IndentPattern {
81 /// Convert to the underlying `deb822_lossless` pattern for the
82 /// applier.
83 pub fn to_deb822(&self) -> deb822_lossless::IndentPattern {
84 match self {
85 IndentPattern::Fixed { spaces } => deb822_lossless::IndentPattern::Fixed(*spaces),
86 IndentPattern::FieldNameLength => deb822_lossless::IndentPattern::FieldNameLength,
87 }
88 }
89}
90
91/// The file an action targets, for grouping actions by file before they are
92/// applied.
93///
94/// `Rename` returns its *source* path here and `RunCommand` returns its
95/// monitored scope directory; neither is the full set of paths the action
96/// modifies. The authoritative modified set is what
97/// [`apply_actions`](crate::appliers::apply_actions) returns after running
98/// the appliers: the appliers observe what actually changed (a `Rename`
99/// touches both endpoints, a `RunCommand` touches whatever its command
100/// wrote).
101pub(crate) fn action_file(action: &Action) -> &Path {
102 match action {
103 Action::Deb822(a) => match a {
104 Deb822Action::SetField { file, .. }
105 | Deb822Action::SetFieldWithIndent { file, .. }
106 | Deb822Action::RemoveField { file, .. }
107 | Deb822Action::RenameField { file, .. }
108 | Deb822Action::RemoveParagraph { file, .. }
109 | Deb822Action::AppendParagraph { file, .. }
110 | Deb822Action::NormalizeFieldSpacing { file, .. }
111 | Deb822Action::DropRelation { file, .. }
112 | Deb822Action::ReplaceRelation { file, .. }
113 | Deb822Action::SetRelationVersionConstraint { file, .. }
114 | Deb822Action::EnsureSubstvar { file, .. }
115 | Deb822Action::DropSubstvar { file, .. }
116 | Deb822Action::EnsureRelation { file, .. }
117 | Deb822Action::MoveRelation { file, .. }
118 | Deb822Action::MakeAlternativePrimary { file, .. }
119 | Deb822Action::ReorderParagraphs { file, .. }
120 | Deb822Action::DropFieldComments { file, .. } => file,
121 },
122 Action::Systemd(a) => match a {
123 SystemdAction::SetField { file, .. }
124 | SystemdAction::RemoveField { file, .. }
125 | SystemdAction::RenameField { file, .. }
126 | SystemdAction::Add { file, .. }
127 | SystemdAction::RemoveValue { file, .. } => file,
128 },
129 Action::DesktopIni(a) => match a {
130 DesktopIniAction::SetField { file, .. }
131 | DesktopIniAction::RemoveField { file, .. }
132 | DesktopIniAction::RemoveAll { file, .. }
133 | DesktopIniAction::RenameField { file, .. } => file,
134 },
135 Action::Yaml(a) => match a {
136 YamlAction::SetField { file, .. }
137 | YamlAction::SetFieldOrdered { file, .. }
138 | YamlAction::RemoveField { file, .. }
139 | YamlAction::RenameField { file, .. } => file,
140 },
141 Action::Changelog(a) => match a {
142 ChangelogAction::ReplaceEntryChanges { file, .. }
143 | ChangelogAction::SetEntryDate { file, .. }
144 | ChangelogAction::RemoveBullet { file, .. }
145 | ChangelogAction::ReplaceBullet { file, .. }
146 | ChangelogAction::SetEntryVersion { file, .. } => file,
147 },
148 Action::Watch(a) => match a {
149 WatchAction::SetEntryMatchingPattern { file, .. }
150 | WatchAction::RemoveEntryOption { file, .. }
151 | WatchAction::SetEntryOption { file, .. }
152 | WatchAction::SetEntryUrl { file, .. }
153 | WatchAction::ConvertEntryToTemplate { file, .. } => file,
154 },
155 Action::Makefile(a) => match a {
156 MakefileAction::ReplaceRecipe { file, .. }
157 | MakefileAction::RemoveRecipe { file, .. }
158 | MakefileAction::SetVariable { file, .. }
159 | MakefileAction::SetVariableOperator { file, .. }
160 | MakefileAction::RemoveVariable { file, .. }
161 | MakefileAction::RemoveRule { file, .. }
162 | MakefileAction::RemovePhonyTarget { file, .. }
163 | MakefileAction::RenameRuleTarget { file, .. }
164 | MakefileAction::AddRule { file, .. }
165 | MakefileAction::AddPhonyTarget { file, .. }
166 | MakefileAction::AddInclude { file, .. }
167 | MakefileAction::ReplaceVariableWithInclude { file, .. }
168 | MakefileAction::InsertIncludeBeforeVariable { file, .. } => file,
169 },
170 Action::Dep3(a) => match a {
171 Dep3Action::SetField { file, .. }
172 | Dep3Action::RemoveField { file, .. }
173 | Dep3Action::RenameField { file, .. } => file,
174 },
175 Action::LintianOverrides(a) => match a {
176 LintianOverridesAction::AddLine { file, .. }
177 | LintianOverridesAction::DropLine { file, .. }
178 | LintianOverridesAction::RenameTag { file, .. }
179 | LintianOverridesAction::SetLineInfo { file, .. } => file,
180 },
181 Action::Maintscript(a) => match a {
182 MaintscriptAction::DropEntry { file, .. } => file,
183 },
184 Action::Debcargo(a) => match a {
185 DebcargoAction::SetSourceField { file, .. }
186 | DebcargoAction::SetTopLevelBool { file, .. } => file,
187 },
188 Action::RunCommand(a) => match a {
189 RunCommandAction::Run { scope, .. } => scope,
190 },
191 Action::Filesystem(a) => match a {
192 FilesystemAction::SetMode { file, .. }
193 | FilesystemAction::Delete { file }
194 | FilesystemAction::Rename { file, .. }
195 | FilesystemAction::RemoveDirIfEmpty { file }
196 | FilesystemAction::Write { file, .. }
197 | FilesystemAction::ReplaceText { file, .. }
198 | FilesystemAction::Substitute { file, .. }
199 | FilesystemAction::NormalizeLineEndings { file } => file,
200 },
201 }
202}
203
204/// Edits to a deb822 file.
205#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
206#[serde(tag = "op", rename_all = "snake_case")]
207pub enum Deb822Action {
208 /// Set a field value, inserting it if missing.
209 ///
210 /// Continuation-line indentation for multi-line values follows the
211 /// deb822 default: align continuations to the field-name column. Use
212 /// [`SetFieldWithIndent`](Self::SetFieldWithIndent) when a field
213 /// needs a specific indent (e.g. Description / DEP-5 mandate a
214 /// single-space indent).
215 SetField {
216 /// File to edit, relative to the package root.
217 file: PathBuf,
218 /// Which paragraph to edit.
219 paragraph: ParagraphSelector,
220 /// Field name.
221 field: String,
222 /// New value.
223 value: String,
224 },
225 /// Like [`SetField`](Self::SetField), but with an explicit
226 /// continuation-line indent pattern. Used for fields whose
227 /// formatting convention diverges from the deb822 default — most
228 /// notably binary-package `Description:` (single-space indent per
229 /// DEP-5) and debian/copyright bodies.
230 SetFieldWithIndent {
231 /// File to edit, relative to the package root.
232 file: PathBuf,
233 /// Which paragraph to edit.
234 paragraph: ParagraphSelector,
235 /// Field name.
236 field: String,
237 /// New value.
238 value: String,
239 /// Continuation-line indent pattern.
240 indent: IndentPattern,
241 },
242 /// Remove a field if present.
243 RemoveField {
244 /// File to edit, relative to the package root.
245 file: PathBuf,
246 /// Which paragraph to edit.
247 paragraph: ParagraphSelector,
248 /// Field name.
249 field: String,
250 },
251 /// Rename a field, preserving its value.
252 RenameField {
253 /// File to edit, relative to the package root.
254 file: PathBuf,
255 /// Which paragraph to edit.
256 paragraph: ParagraphSelector,
257 /// Current field name.
258 from: String,
259 /// New field name.
260 to: String,
261 },
262 /// Remove the paragraph identified by `paragraph`.
263 RemoveParagraph {
264 /// File to edit, relative to the package root.
265 file: PathBuf,
266 /// Which paragraph to drop.
267 paragraph: ParagraphSelector,
268 },
269 /// Append a new paragraph at the end of the file with the given
270 /// (field, value) pairs in order.
271 AppendParagraph {
272 /// File to edit, relative to the package root.
273 file: PathBuf,
274 /// Fields to populate the new paragraph with.
275 fields: Vec<(String, String)>,
276 /// Continuation-line indent for multi-line values, in spaces.
277 /// `None` lets the deb822 renderer auto-align to the field-name
278 /// column (the default for debian/control). Use `Some(1)` for
279 /// debian/copyright, where DEP-5 mandates a single-space indent.
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 indent: Option<usize>,
282 },
283 /// Normalize the whitespace around a field's separator (`:` and the
284 /// continuation indent). The deb822 spec allows arbitrary spacing
285 /// after the colon, but the convention is exactly one space; this
286 /// action collapses unusual spacing without otherwise touching the
287 /// value. A no-op if the field already has canonical spacing.
288 NormalizeFieldSpacing {
289 /// File to edit, relative to the package root.
290 file: PathBuf,
291 /// Which paragraph to edit.
292 paragraph: ParagraphSelector,
293 /// Field name.
294 field: String,
295 },
296 /// Drop every relation matching `package` from a relations field
297 /// (Depends, Build-Depends, etc.). Empty alternative groups are
298 /// removed; if the field becomes empty it is removed entirely. A
299 /// no-op if the package isn't named in the field.
300 DropRelation {
301 /// File to edit, relative to the package root.
302 file: PathBuf,
303 /// Which paragraph to edit.
304 paragraph: ParagraphSelector,
305 /// Relations field name (e.g. `Build-Depends`).
306 field: String,
307 /// Package name to drop.
308 package: String,
309 },
310 /// Replace the first relation that names `from_package` with the
311 /// `to_entry` text, keeping the entry's position in the field. A
312 /// no-op if `from_package` isn't named. If `to_entry` parses as a
313 /// relation whose package is already named elsewhere in the field,
314 /// the original `from_package` entry is dropped without inserting a
315 /// duplicate.
316 ReplaceRelation {
317 /// File to edit, relative to the package root.
318 file: PathBuf,
319 /// Which paragraph to edit.
320 paragraph: ParagraphSelector,
321 /// Relations field name (e.g. `Build-Depends`).
322 field: String,
323 /// Package name (matched exactly) of the relation to replace.
324 from_package: String,
325 /// New entry text (e.g. `perl`, `debhelper (>= 12)`).
326 to_entry: String,
327 },
328 /// Ensure a substvar (`${...}`) is present in a relations field. If
329 /// the field doesn't exist it's created with just the substvar; if
330 /// it exists and already mentions the substvar it's a no-op.
331 EnsureSubstvar {
332 /// File to edit, relative to the package root.
333 file: PathBuf,
334 /// Which paragraph to edit.
335 paragraph: ParagraphSelector,
336 /// Relations field name (e.g. `Depends`).
337 field: String,
338 /// Substvar to ensure, including the surrounding `${...}`.
339 substvar: String,
340 },
341 /// Drop a substvar (`${...}`) from a relations field. If the field
342 /// becomes empty it's removed entirely. A no-op if the substvar is
343 /// already absent.
344 DropSubstvar {
345 /// File to edit, relative to the package root.
346 file: PathBuf,
347 /// Which paragraph to edit.
348 paragraph: ParagraphSelector,
349 /// Relations field name.
350 field: String,
351 /// Substvar to drop, including the surrounding `${...}`.
352 substvar: String,
353 },
354 /// Ensure a relation entry is present in a relations field, creating
355 /// the field if necessary. `entry` is a literal relation entry string
356 /// (e.g. `python3-poetry-core` or `debhelper-compat (= 13)`).
357 ///
358 /// If `entry` carries no version constraint the action is a no-op
359 /// when any relation with the same package name is already present.
360 /// If `entry` has an exact version, the action upgrades any existing
361 /// relation to that exact version.
362 EnsureRelation {
363 /// File to edit, relative to the package root.
364 file: PathBuf,
365 /// Which paragraph to edit.
366 paragraph: ParagraphSelector,
367 /// Relations field name (e.g. `Build-Depends`).
368 field: String,
369 /// Literal relation entry to ensure.
370 entry: String,
371 },
372 /// Set the version constraint on every relation in `field` that names
373 /// `package`. Acts per-relation, so the constraint is replaced without
374 /// removing the package from the field or affecting any alternatives in
375 /// the same entry. Passing `None` drops the constraint entirely. A no-op
376 /// if the package isn't named in `field` or every matching relation
377 /// already has the requested constraint.
378 SetRelationVersionConstraint {
379 /// File to edit, relative to the package root.
380 file: PathBuf,
381 /// Which paragraph to edit.
382 paragraph: ParagraphSelector,
383 /// Relations field name (e.g. `Depends`).
384 field: String,
385 /// Package name to set the version constraint on.
386 package: String,
387 /// New constraint, or `None` to strip the constraint entirely.
388 constraint: Option<(VersionConstraint, Version)>,
389 },
390 /// Move a relation entry between two fields of the same paragraph,
391 /// preserving its version constraint and any alternatives. The entry
392 /// is identified by `package`. If `from_field` becomes empty after
393 /// the move it is removed entirely. A no-op if the package isn't
394 /// present in `from_field`.
395 MoveRelation {
396 /// File to edit, relative to the package root.
397 file: PathBuf,
398 /// Which paragraph to edit.
399 paragraph: ParagraphSelector,
400 /// Source relations field name.
401 from_field: String,
402 /// Destination relations field name.
403 to_field: String,
404 /// Package name identifying the entry to move.
405 package: String,
406 },
407 /// Reorder the alternatives in the relation entry that names
408 /// `package` so that `package` becomes the primary (first)
409 /// alternative. The other alternatives keep their relative order;
410 /// each alternative's version and architecture qualifiers are
411 /// preserved verbatim, and the `|` separators are normalised to the
412 /// conventional ` | `.
413 ///
414 /// Operates on the first entry of `field` that names `package`. A
415 /// no-op if `package` isn't named in `field`, or already heads its
416 /// entry.
417 MakeAlternativePrimary {
418 /// File to edit, relative to the package root.
419 file: PathBuf,
420 /// Which paragraph to edit.
421 paragraph: ParagraphSelector,
422 /// Relations field name (e.g. `Depends`).
423 field: String,
424 /// Package name whose alternative should become primary.
425 package: String,
426 },
427 /// Reorder a subset of paragraphs in a deb822 file. Paragraphs that
428 /// have `key_field` are pulled out and re-inserted in the order
429 /// given by `order` (which lists their `key_field` values). Other
430 /// paragraphs stay in place: the i-th slot occupied by a
431 /// participating paragraph in the original document is filled by
432 /// the i-th key from `order`. Keys in `order` that aren't present
433 /// in the document are skipped.
434 ReorderParagraphs {
435 /// File to edit, relative to the package root.
436 file: PathBuf,
437 /// Field whose presence marks a paragraph as participating in
438 /// the reorder, and whose value identifies it.
439 key_field: String,
440 /// Desired order of `key_field` values among the participating
441 /// paragraphs.
442 order: Vec<String>,
443 },
444 /// Drop the commented-out lines embedded in a field's value.
445 ///
446 /// A deb822 field's value can be followed by `#`-prefixed lines that
447 /// the parser keeps attached to that field — e.g. the commented-out
448 /// `Vcs-*` lines old `dh_make` versions append after `Homepage`.
449 /// This rewrites the field to its comment-free value, dropping those
450 /// lines. A no-op if the field carries no embedded comment lines.
451 DropFieldComments {
452 /// File to edit, relative to the package root.
453 file: PathBuf,
454 /// Which paragraph to edit.
455 paragraph: ParagraphSelector,
456 /// Field name.
457 field: String,
458 },
459}
460
461/// Edits to a systemd unit file.
462///
463/// Systemd unit files are sectioned ini-style files (`[Unit]`, `[Service]`,
464/// `[Install]`, …). Each variant identifies a single section by name and
465/// targets one entry within it.
466///
467/// Multi-valued fields (e.g. `Alias=`, `After=`) are handled by
468/// [`Add`](Self::Add) / [`RemoveValue`](Self::RemoveValue) — these append a
469/// new value or remove a specific one without touching siblings.
470/// [`SetField`](Self::SetField) replaces every occurrence of the key with a
471/// single value, which is the right thing for scalar fields like `PIDFile=`
472/// but the wrong thing for multi-valued ones.
473#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
474#[serde(tag = "op", rename_all = "snake_case")]
475pub enum SystemdAction {
476 /// Set a scalar field. Replaces every existing entry with the given key.
477 SetField {
478 /// File to edit, relative to the package root.
479 file: PathBuf,
480 /// Section name, e.g. "Service".
481 section: String,
482 /// Field name (no trailing `=`).
483 field: String,
484 /// New value.
485 value: String,
486 },
487 /// Remove every entry with the given key.
488 RemoveField {
489 /// File to edit, relative to the package root.
490 file: PathBuf,
491 /// Section name.
492 section: String,
493 /// Field name.
494 field: String,
495 },
496 /// Rename every entry with `from` to `to`, preserving values.
497 RenameField {
498 /// File to edit, relative to the package root.
499 file: PathBuf,
500 /// Section name.
501 section: String,
502 /// Current field name.
503 from: String,
504 /// New field name.
505 to: String,
506 },
507 /// Append a new entry. Use for multi-valued fields like `After=` or
508 /// `Alias=` to add another value without disturbing siblings.
509 Add {
510 /// File to edit, relative to the package root.
511 file: PathBuf,
512 /// Section name.
513 section: String,
514 /// Field name.
515 field: String,
516 /// Value to append.
517 value: String,
518 },
519 /// Remove a specific value from a multi-valued field. Other values for
520 /// the same key are preserved.
521 RemoveValue {
522 /// File to edit, relative to the package root.
523 file: PathBuf,
524 /// Section name.
525 section: String,
526 /// Field name.
527 field: String,
528 /// Value to drop.
529 value: String,
530 },
531}
532
533/// Edits to a freedesktop `.desktop` entry file.
534///
535/// Desktop entry files are sectioned ini-style files with `[Group]`
536/// headers and locale-tagged keys (e.g. `Name[de]=...`). Each variant
537/// identifies one group and one entry within it.
538#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
539#[serde(tag = "op", rename_all = "snake_case")]
540pub enum DesktopIniAction {
541 /// Set a key. If `locale` is `None`, sets the unlocalised entry;
542 /// otherwise sets the entry tagged with `locale` (e.g. `de`).
543 SetField {
544 /// File to edit, relative to the package root.
545 file: PathBuf,
546 /// Group name, e.g. "Desktop Entry".
547 group: String,
548 /// Key name.
549 field: String,
550 /// Locale tag, if any.
551 #[serde(skip_serializing_if = "Option::is_none", default)]
552 locale: Option<String>,
553 /// New value.
554 value: String,
555 },
556 /// Remove a key. If `locale` is `None`, removes the unlocalised entry
557 /// only; if a locale is given, removes only that locale variant. To
558 /// drop every locale variant of a key, use [`RemoveAll`](Self::RemoveAll).
559 RemoveField {
560 /// File to edit, relative to the package root.
561 file: PathBuf,
562 /// Group name.
563 group: String,
564 /// Key name.
565 field: String,
566 /// Locale tag, if any.
567 #[serde(skip_serializing_if = "Option::is_none", default)]
568 locale: Option<String>,
569 },
570 /// Remove a key together with every locale variant.
571 RemoveAll {
572 /// File to edit, relative to the package root.
573 file: PathBuf,
574 /// Group name.
575 group: String,
576 /// Key name.
577 field: String,
578 },
579 /// Rename a key, preserving its value (and every locale variant).
580 RenameField {
581 /// File to edit, relative to the package root.
582 file: PathBuf,
583 /// Group name.
584 group: String,
585 /// Current key name.
586 from: String,
587 /// New key name.
588 to: String,
589 },
590}
591
592/// Edits to a YAML file.
593///
594/// A YAML file is a tree of mappings, sequences and scalars; the
595/// `parent_path` field navigates from the top-level document down to the
596/// mapping that owns the key being edited. An empty `parent_path` means
597/// the top-level mapping (the common case for Debian's flat YAML files
598/// like `debian/upstream/metadata`).
599///
600/// Each path component is either a string (mapping key) or an index
601/// (sequence position).
602#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
603#[serde(tag = "op", rename_all = "snake_case")]
604pub enum YamlAction {
605 /// Set a scalar value at `parent_path`'s mapping under `key`. Inserts
606 /// the key if missing. New keys are appended at the end of the
607 /// mapping; use [`SetFieldOrdered`](Self::SetFieldOrdered) to
608 /// position the new key according to a canonical field order
609 /// (e.g. DEP-12).
610 SetField {
611 /// File to edit, relative to the package root.
612 file: PathBuf,
613 /// Path from the document root to the parent mapping.
614 #[serde(default, skip_serializing_if = "Vec::is_empty")]
615 parent_path: Vec<YamlPathComponent>,
616 /// Key to set (string scalar).
617 key: String,
618 /// New value (string scalar).
619 value: String,
620 },
621 /// Like [`SetField`](Self::SetField), but when inserting a new key,
622 /// position it according to `field_order`. Keys not listed in
623 /// `field_order` are placed at the end. A no-op if the key already
624 /// exists with the requested value.
625 SetFieldOrdered {
626 /// File to edit, relative to the package root.
627 file: PathBuf,
628 /// Path from the document root to the parent mapping.
629 #[serde(default, skip_serializing_if = "Vec::is_empty")]
630 parent_path: Vec<YamlPathComponent>,
631 /// Key to set (string scalar).
632 key: String,
633 /// New value (string scalar).
634 value: String,
635 /// Canonical field order. Keys appearing earlier in this list
636 /// are placed earlier in the mapping.
637 field_order: Vec<String>,
638 },
639 /// Remove a key from the mapping at `parent_path`.
640 RemoveField {
641 /// File to edit, relative to the package root.
642 file: PathBuf,
643 /// Path from the document root to the parent mapping.
644 #[serde(default, skip_serializing_if = "Vec::is_empty")]
645 parent_path: Vec<YamlPathComponent>,
646 /// Key to remove.
647 key: String,
648 },
649 /// Rename a key in the mapping at `parent_path`, preserving its
650 /// value and position.
651 RenameField {
652 /// File to edit, relative to the package root.
653 file: PathBuf,
654 /// Path from the document root to the parent mapping.
655 #[serde(default, skip_serializing_if = "Vec::is_empty")]
656 parent_path: Vec<YamlPathComponent>,
657 /// Current key name.
658 from: String,
659 /// New key name.
660 to: String,
661 },
662}
663
664/// One step in a [`YamlAction`] path.
665#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
666#[serde(tag = "kind", rename_all = "snake_case")]
667pub enum YamlPathComponent {
668 /// A mapping key (string).
669 Key {
670 /// Key name.
671 key: String,
672 },
673 /// A sequence index (0-based).
674 Index {
675 /// Position.
676 index: usize,
677 },
678}
679
680/// Edits to a `debian/changelog`.
681///
682/// Operations target entries by their version, which is stable across
683/// minor edits. Change-line content is supplied verbatim — the applier
684/// preserves the changelog's existing indentation rules when re-rendering.
685#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
686#[serde(tag = "op", rename_all = "snake_case")]
687pub enum ChangelogAction {
688 /// Replace the change lines of the entry with the given version. The
689 /// `lines` are stored verbatim with their leading ` *`/` `
690 /// continuation prefix; the applier writes them as-is into the entry.
691 ReplaceEntryChanges {
692 /// File to edit, relative to the package root. Almost always
693 /// `debian/changelog`, but kept explicit for symmetry.
694 file: PathBuf,
695 /// Version string of the target entry (e.g. `2.6.0-1`).
696 version: String,
697 /// Replacement change lines (one per line, no trailing newline).
698 lines: Vec<String>,
699 },
700 /// Set the trailer datetime of the entry with the given version.
701 ///
702 /// The datetime is stored as an RFC 2822 string (`"Sun, 22 Apr 2018
703 /// 00:58:14 +0000"`) — what `chrono::DateTime::to_rfc2822` produces
704 /// and what changelog trailers use natively.
705 SetEntryDate {
706 /// File to edit, relative to the package root.
707 file: PathBuf,
708 /// Version string of the target entry.
709 version: String,
710 /// New datetime as an RFC 2822 string.
711 rfc2822: String,
712 },
713 /// Remove a bullet from the entry with the given version.
714 ///
715 /// The bullet is identified by its author attribution (the `[ Name ]`
716 /// header that introduces multi-author groups, or `None` for an entry
717 /// without one) and its body text (the bullet's lines joined with
718 /// `\n`, exactly as `debian_changelog`'s `Bullet::lines()` returns
719 /// them).
720 ///
721 /// `occurrence` is a 0-based index that disambiguates when several
722 /// bullets share the same `(author, text)` key: `0` removes the first
723 /// match, `1` the second, etc. The applier walks bullets in
724 /// `iter_changes_by_author` order. Whitespace between surviving
725 /// bullets is preserved.
726 RemoveBullet {
727 /// File to edit, relative to the package root.
728 file: PathBuf,
729 /// Version string of the target entry.
730 version: String,
731 /// Author header above the bullet, if any.
732 author: Option<String>,
733 /// Body text of the bullet (lines joined by `\n`).
734 text: String,
735 /// 0-based index among bullets sharing the same `(author, text)`
736 /// key. Defaults to `0` over the wire when omitted.
737 #[serde(default)]
738 occurrence: usize,
739 },
740 /// Replace the body lines of a bullet, identified the same way as in
741 /// [`RemoveBullet`](Self::RemoveBullet). `new_lines` are stored
742 /// without their ` *`/` ` continuation prefix — the applier
743 /// passes them straight to `Bullet::replace_with`, which re-adds the
744 /// proper indentation.
745 ReplaceBullet {
746 /// File to edit, relative to the package root.
747 file: PathBuf,
748 /// Version string of the target entry.
749 version: String,
750 /// Author header above the bullet, if any.
751 author: Option<String>,
752 /// Current body text of the bullet (lines joined by `\n`).
753 text: String,
754 /// 0-based index among bullets sharing the same `(author, text)`
755 /// key.
756 #[serde(default)]
757 occurrence: usize,
758 /// Replacement body lines.
759 new_lines: Vec<String>,
760 },
761 /// Replace the version of the entry currently identified by `version`
762 /// with `new_version`. A no-op if no entry has that version.
763 SetEntryVersion {
764 /// File to edit, relative to the package root.
765 file: PathBuf,
766 /// Current version string of the target entry.
767 version: String,
768 /// New version string to write into the entry header.
769 new_version: String,
770 },
771}
772/// Edits to a `debian/watch` file.
773///
774/// Watch files are line-oriented, with each non-comment line describing a
775/// release-monitor entry: a URL, a matching regexp for the version, and
776/// optional `opts=...` flags. We address an entry by its current URL,
777/// which is unique across the watch files we routinely fix.
778#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
779#[serde(tag = "op", rename_all = "snake_case")]
780pub enum WatchAction {
781 /// Replace the matching pattern (the regexp following the URL) of the
782 /// entry whose current URL is `url`. A no-op if no entry matches.
783 SetEntryMatchingPattern {
784 /// File to edit, relative to the package root. Almost always
785 /// `debian/watch`.
786 file: PathBuf,
787 /// Current URL of the target entry.
788 url: String,
789 /// New matching pattern.
790 new_pattern: String,
791 },
792 /// Remove an `opts=...` option from the entry whose current URL is
793 /// `url`. A no-op if no entry matches or the option isn't set.
794 RemoveEntryOption {
795 /// File to edit, relative to the package root.
796 file: PathBuf,
797 /// Current URL of the target entry.
798 url: String,
799 /// Name of the option to remove (e.g. `filenamemangle`).
800 option: String,
801 },
802 /// Set (or insert) an `opts=...` option on the entry whose current URL
803 /// is `url`. A no-op if no entry matches or the option already has the
804 /// requested value.
805 SetEntryOption {
806 /// File to edit, relative to the package root.
807 file: PathBuf,
808 /// Current URL of the target entry.
809 url: String,
810 /// Name of the option to set (e.g. `dversionmangle`).
811 option: String,
812 /// New value for the option.
813 value: String,
814 },
815 /// Replace the URL of the entry whose current URL is `url`. A no-op if
816 /// no entry matches.
817 SetEntryUrl {
818 /// File to edit, relative to the package root.
819 file: PathBuf,
820 /// Current URL of the target entry.
821 url: String,
822 /// New URL.
823 new_url: String,
824 },
825 /// Convert the v5 entry whose current URL is `url` to its template
826 /// form (Template:/Owner:/Project: for GitHub, Template:/Dist: for
827 /// CPAN/PyPI, etc.). A no-op if the entry is already a template,
828 /// no template matches the URL/pattern, or no entry has that URL.
829 ConvertEntryToTemplate {
830 /// File to edit, relative to the package root.
831 file: PathBuf,
832 /// Current URL of the target entry.
833 url: String,
834 },
835}
836
837/// Edits to a Makefile (typically `debian/rules`).
838///
839/// Recipes are addressed by their exact current text (including leading
840/// indentation). This avoids index drift when multiple recipe edits target
841/// the same rule.
842#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
843#[serde(tag = "op", rename_all = "snake_case")]
844pub enum MakefileAction {
845 /// Replace the first recipe whose text exactly matches `recipe` in the
846 /// rule whose primary target is `target`. A no-op if no rule or recipe
847 /// matches.
848 ReplaceRecipe {
849 /// File to edit, relative to the package root.
850 file: PathBuf,
851 /// Primary target of the rule containing the recipe.
852 target: String,
853 /// Current recipe text, matched verbatim (including indentation).
854 recipe: String,
855 /// Replacement recipe text. The applier preserves the original
856 /// recipe's leading whitespace if `new_recipe` doesn't start with
857 /// whitespace itself.
858 new_recipe: String,
859 },
860 /// Remove the first recipe whose text exactly matches `recipe` from the
861 /// rule whose primary target is `target`. A no-op if no rule or recipe
862 /// matches.
863 RemoveRecipe {
864 /// File to edit, relative to the package root.
865 file: PathBuf,
866 /// Primary target of the rule containing the recipe.
867 target: String,
868 /// Recipe text, matched verbatim (including indentation).
869 recipe: String,
870 },
871 /// Replace the value of the first variable definition for `name`.
872 /// A no-op if no such variable exists.
873 SetVariable {
874 /// File to edit, relative to the package root.
875 file: PathBuf,
876 /// Variable name (matched exactly).
877 name: String,
878 /// New right-hand side, verbatim (no quoting applied).
879 value: String,
880 },
881 /// Change the assignment operator on the first variable definition
882 /// for `name` (e.g. `:=` to `?=`). A no-op if no such variable
883 /// exists or it already uses `operator`.
884 SetVariableOperator {
885 /// File to edit, relative to the package root.
886 file: PathBuf,
887 /// Variable name (matched exactly).
888 name: String,
889 /// New assignment operator (`=`, `:=`, `?=`, `+=`).
890 operator: String,
891 },
892 /// Remove the first variable definition for `name`. A no-op if no such
893 /// variable exists.
894 RemoveVariable {
895 /// File to edit, relative to the package root.
896 file: PathBuf,
897 /// Variable name (matched exactly).
898 name: String,
899 },
900 /// Remove the first rule whose primary target is `target`. A no-op if
901 /// no such rule exists.
902 RemoveRule {
903 /// File to edit, relative to the package root.
904 file: PathBuf,
905 /// Primary target of the rule to remove.
906 target: String,
907 },
908 /// Remove `target` from the prerequisites of the `.PHONY` rule. If
909 /// `.PHONY` becomes empty, the rule itself is removed. A no-op if
910 /// the target is not listed.
911 RemovePhonyTarget {
912 /// File to edit, relative to the package root.
913 file: PathBuf,
914 /// Target name to remove from `.PHONY`.
915 target: String,
916 },
917 /// Rename a target on the first rule that has it. A no-op if no rule
918 /// has the old target.
919 RenameRuleTarget {
920 /// File to edit, relative to the package root.
921 file: PathBuf,
922 /// Old target name (matched exactly after trimming).
923 from_target: String,
924 /// New target name.
925 to_target: String,
926 },
927 /// Append a new rule with `target` and the given (possibly empty)
928 /// prerequisites. The applier does not check for an existing rule —
929 /// detectors must guard against duplicates themselves.
930 AddRule {
931 /// File to edit, relative to the package root.
932 file: PathBuf,
933 /// Target name for the new rule.
934 target: String,
935 /// Prerequisite targets (in order).
936 prerequisites: Vec<String>,
937 },
938 /// Add `target` to the prerequisites of the `.PHONY` rule. A no-op if
939 /// `.PHONY` already lists `target`. If no `.PHONY` rule exists, the
940 /// applier creates one.
941 AddPhonyTarget {
942 /// File to edit, relative to the package root.
943 file: PathBuf,
944 /// Target name to add to `.PHONY`.
945 target: String,
946 },
947 /// Add an `include <path>` directive. A no-op if the file is already
948 /// included.
949 AddInclude {
950 /// File to edit, relative to the package root.
951 file: PathBuf,
952 /// Path to include (e.g. `/usr/share/dpkg/pkg-info.mk`).
953 path: String,
954 },
955 /// Replace the first variable definition for `name` with an
956 /// `include <path>` directive. A no-op if the variable doesn't
957 /// exist or `path` is already included. Used to migrate
958 /// `DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)` and
959 /// friends to a single `include /usr/share/dpkg/architecture.mk`,
960 /// keeping the include in the variable's old position.
961 ReplaceVariableWithInclude {
962 /// File to edit, relative to the package root.
963 file: PathBuf,
964 /// Variable name to replace (matched exactly).
965 name: String,
966 /// Path to include in place of the variable.
967 path: String,
968 },
969 /// Insert `include <path>` immediately before the first variable
970 /// definition whose name is `before_variable`. A no-op if the
971 /// variable doesn't exist or `path` is already included.
972 InsertIncludeBeforeVariable {
973 /// File to edit, relative to the package root.
974 file: PathBuf,
975 /// Path to include.
976 path: String,
977 /// Variable name to anchor the insertion against.
978 before_variable: String,
979 },
980}
981
982/// Edits to a DEP-3 patch header.
983///
984/// DEP-3 headers live at the top of a quilt patch (under
985/// `debian/patches/`) followed by a blank line and the unified diff. The
986/// applier parses just the header (everything before the first `---`,
987/// `diff `, or `Index:` line), edits it, and reassembles the file.
988#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
989#[serde(tag = "op", rename_all = "snake_case")]
990pub enum Dep3Action {
991 /// Set a field's value, inserting it if missing. The field is added in
992 /// the patch header's existing position when present, or appended.
993 SetField {
994 /// Patch file to edit, relative to the package root (e.g.
995 /// `debian/patches/foo.patch`).
996 file: PathBuf,
997 /// Field name (case-sensitive, e.g. `Author`).
998 field: String,
999 /// New value.
1000 value: String,
1001 },
1002 /// Remove a field. A no-op if the field isn't present.
1003 RemoveField {
1004 /// Patch file to edit, relative to the package root.
1005 file: PathBuf,
1006 /// Field name to remove.
1007 field: String,
1008 },
1009 /// Rename `from_field` to `to_field`, preserving its value. A no-op
1010 /// if `from_field` isn't present. If `to_field` already exists, it is
1011 /// overwritten.
1012 RenameField {
1013 /// Patch file to edit, relative to the package root.
1014 file: PathBuf,
1015 /// Current field name.
1016 from_field: String,
1017 /// New field name.
1018 to_field: String,
1019 },
1020}
1021
1022/// Identifies a specific override line for in-place edits.
1023///
1024/// We address lines by their visible content rather than by index because
1025/// other actions may shift the line numbering between detect and apply.
1026#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1027pub struct OverrideLineSelector {
1028 /// Tag name (matched exactly).
1029 pub tag: String,
1030 /// Optional info string the override carries (matched exactly, no
1031 /// wildcard expansion). `None` matches lines with no info.
1032 #[serde(default, skip_serializing_if = "Option::is_none")]
1033 pub info: Option<String>,
1034 /// Optional package name from the `package:` prefix. `None` matches
1035 /// lines without a package spec.
1036 #[serde(default, skip_serializing_if = "Option::is_none")]
1037 pub package: Option<String>,
1038}
1039
1040/// Edits to a `debian/source/lintian-overrides` or
1041/// `debian/<pkg>.lintian-overrides` file.
1042#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1043#[serde(tag = "op", rename_all = "snake_case")]
1044pub enum LintianOverridesAction {
1045 /// Append a new override line. If the file does not exist it is
1046 /// created (including any missing parent directories). The line is
1047 /// only added when no existing line already overrides the same tag
1048 /// (same tag + same optional info), so the action is idempotent.
1049 AddLine {
1050 /// File to edit, relative to the package root (e.g.
1051 /// `debian/source/lintian-overrides` or
1052 /// `debian/mypkg.lintian-overrides`).
1053 file: PathBuf,
1054 /// Optional package name prefix (e.g. `mypkg`).
1055 #[serde(default, skip_serializing_if = "Option::is_none")]
1056 package: Option<String>,
1057 /// Tag name.
1058 tag: String,
1059 /// Optional info string.
1060 #[serde(default, skip_serializing_if = "Option::is_none")]
1061 info: Option<String>,
1062 },
1063 /// Drop the first override line that matches `selector`. Each
1064 /// DropLine action consumes one line — to remove N copies of the
1065 /// same line, emit N actions. If the file becomes empty (no
1066 /// override lines remain), it is removed entirely.
1067 DropLine {
1068 /// File to edit, relative to the package root.
1069 file: PathBuf,
1070 /// Which override line to drop.
1071 selector: OverrideLineSelector,
1072 },
1073 /// Rename the tag on every line whose current tag is `from_tag`. The
1074 /// rest of each line (whitespace, comments, package spec, info) is
1075 /// preserved verbatim.
1076 RenameTag {
1077 /// File to edit, relative to the package root.
1078 file: PathBuf,
1079 /// Old tag name (matched exactly).
1080 from_tag: String,
1081 /// New tag name.
1082 to_tag: String,
1083 },
1084 /// Rewrite the info text on the first line that matches `selector`.
1085 /// Only the info portion changes — the package spec, tag, and
1086 /// surrounding whitespace are preserved.
1087 SetLineInfo {
1088 /// File to edit, relative to the package root.
1089 file: PathBuf,
1090 /// Which override line to update.
1091 selector: OverrideLineSelector,
1092 /// New info text. Empty string removes the info entirely.
1093 new_info: String,
1094 },
1095}
1096
1097/// Edits to a maintscript file.
1098///
1099/// Each line in a maintscript file is an independent dpkg-maintscript-helper
1100/// invocation. We address entries by their exact text, mirroring how
1101/// [`MakefileAction::ReplaceRecipe`] addresses recipe lines.
1102#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1103#[serde(tag = "op", rename_all = "snake_case")]
1104pub enum MaintscriptAction {
1105 /// Drop the first entry whose trimmed line text equals `entry`.
1106 /// Comments immediately preceding the dropped line are also removed.
1107 /// If the file ends up empty (no entries remain), it is removed
1108 /// entirely. Each `DropEntry` consumes one matching line — to remove
1109 /// N copies of the same entry, emit N actions.
1110 DropEntry {
1111 /// File to edit, relative to the package root.
1112 file: PathBuf,
1113 /// Entry text to drop, matched after trimming surrounding
1114 /// whitespace.
1115 entry: String,
1116 },
1117}
1118
1119/// Edits to a `debian/debcargo.toml` file.
1120///
1121/// Debcargo manages its own control file; we manipulate scalar fields under
1122/// the `[source]` table directly. Only a small set of operations is needed
1123/// in practice — the equivalent of typed setters on the generated control
1124/// fields (Vcs-Git, Vcs-Browser, Standards-Version, Section).
1125#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1126#[serde(tag = "op", rename_all = "snake_case")]
1127pub enum DebcargoAction {
1128 /// Set a string field on the `[source]` table. Creates the table and/or
1129 /// the field if absent. Overwrites any existing value.
1130 SetSourceField {
1131 /// File to edit, relative to the package root. Almost always
1132 /// `debian/debcargo.toml`.
1133 file: PathBuf,
1134 /// Key inside `[source]` (e.g. `vcs_git`, `vcs_browser`,
1135 /// `section`, `standards_version`).
1136 field: String,
1137 /// New string value.
1138 value: String,
1139 },
1140 /// Set a boolean field at the top level of the file. Creates the field if
1141 /// absent. Overwrites any existing value.
1142 SetTopLevelBool {
1143 /// File to edit, relative to the package root. Almost always
1144 /// `debian/debcargo.toml`.
1145 file: PathBuf,
1146 /// Top-level key (e.g. `collapse_features`).
1147 field: String,
1148 /// New boolean value.
1149 value: bool,
1150 },
1151}
1152
1153/// Run an external command that mutates the working tree.
1154///
1155/// Use sparingly: prefer typed actions whenever possible. The intended
1156/// use is tools like `debconf-updatepo` that produce changes we can't
1157/// describe declaratively.
1158#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1159#[serde(tag = "op", rename_all = "snake_case")]
1160pub enum RunCommandAction {
1161 /// Run `argv` in the package root. The applier snapshots `scope`
1162 /// before and after the run, considering the action a change iff any
1163 /// file under `scope` was added, removed, or had its bytes change.
1164 /// A non-zero exit code is a fixer error. ENOENT on `argv[0]` is
1165 /// reported as [`FixerError::MissingDependency`].
1166 Run {
1167 /// Argument vector. `argv[0]` is resolved via PATH.
1168 argv: Vec<String>,
1169 /// Subtree to monitor for changes, relative to the package root.
1170 /// Use `.` to monitor the entire tree.
1171 scope: PathBuf,
1172 /// Environment overrides applied on top of the inherited env.
1173 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1174 env: Vec<(String, String)>,
1175 },
1176}
1177
1178/// Filesystem-level edits.
1179#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1180#[serde(tag = "op", rename_all = "snake_case")]
1181pub enum FilesystemAction {
1182 /// Set the file mode (e.g. mark a script executable).
1183 SetMode {
1184 /// File to chmod, relative to the package root.
1185 file: PathBuf,
1186 /// New mode bits.
1187 mode: u32,
1188 },
1189 /// Delete a file.
1190 Delete {
1191 /// File to delete, relative to the package root.
1192 file: PathBuf,
1193 },
1194 /// Move a file from one path to another, atomically when possible.
1195 /// Creates the destination's parent directory if needed.
1196 Rename {
1197 /// Source path, relative to the package root.
1198 file: PathBuf,
1199 /// Destination path, relative to the package root.
1200 to: PathBuf,
1201 },
1202 /// Remove a directory if it is empty. A no-op if the directory has
1203 /// any remaining entries — useful as a follow-up to a `Delete` that
1204 /// might have been the last file in its parent directory.
1205 RemoveDirIfEmpty {
1206 /// Directory to remove, relative to the package root. The
1207 /// applier reuses the `file` field name for grouping purposes.
1208 file: PathBuf,
1209 },
1210 /// Overwrite (or create) a file with the given content.
1211 Write {
1212 /// File to write, relative to the package root.
1213 file: PathBuf,
1214 /// Bytes to write.
1215 content: Vec<u8>,
1216 },
1217 /// Replace a byte range in a file.
1218 ReplaceText {
1219 /// File to edit, relative to the package root.
1220 file: PathBuf,
1221 /// Range to replace.
1222 range: TextRange,
1223 /// Replacement text.
1224 replacement: String,
1225 },
1226 /// Replace every occurrence of a literal string with another. Operates
1227 /// on the file's textual content with no awareness of file structure.
1228 Substitute {
1229 /// File to edit, relative to the package root.
1230 file: PathBuf,
1231 /// String to find (literal, not a regex).
1232 from: String,
1233 /// Replacement string.
1234 to: String,
1235 },
1236 /// Normalise the file's line endings to LF (i.e. convert any CRLF
1237 /// sequences to LF). Carries no payload other than the path: each
1238 /// applier reads the current file, performs the conversion, and
1239 /// writes back. Modelling this as its own variant (rather than as a
1240 /// `Write` carrying the converted bytes) keeps the diagnostic stream
1241 /// declarative — anyone reading it sees the *intent* and not a byte
1242 /// blob — and lets an LSP host emit a structural `TextEdit` derived
1243 /// from the open buffer rather than from a possibly-stale snapshot.
1244 NormalizeLineEndings {
1245 /// File to convert, relative to the package root.
1246 file: PathBuf,
1247 },
1248}
1249
1250/// Identifies a paragraph in a deb822 file.
1251///
1252/// The variants are a union of file-format vocabularies. Each variant is
1253/// labelled with the family of files it applies to; the applier validates
1254/// that a selector matches the file it's targeting (e.g.
1255/// [`Binary`](Self::Binary) on `debian/copyright` is an error).
1256///
1257/// File-format-agnostic selectors ([`Index`](Self::Index),
1258/// [`ByKey`](Self::ByKey)) work on any deb822 file, including ones we
1259/// don't have a typed wrapper for.
1260#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1261#[serde(tag = "kind", rename_all = "snake_case")]
1262pub enum ParagraphSelector {
1263 /// debian/control: the source paragraph.
1264 Source,
1265 /// debian/control: a binary paragraph identified by its `Package:` field.
1266 Binary {
1267 /// Package name.
1268 package: String,
1269 },
1270 /// debian/copyright: the header paragraph (carrying `Format:`,
1271 /// `Upstream-Name:`, etc.).
1272 CopyrightHeader,
1273 /// debian/copyright: the paragraph whose `Files:` field matches the
1274 /// given glob string exactly.
1275 CopyrightFiles {
1276 /// Files-glob string, matched literally against the field value.
1277 glob: String,
1278 },
1279 /// debian/copyright: a standalone License paragraph (no `Files:` field)
1280 /// whose License synopsis equals `name`.
1281 CopyrightLicense {
1282 /// License short-name as it appears on the first line of the
1283 /// `License:` field (e.g. `GPL-2+`).
1284 name: String,
1285 },
1286 /// File-format-agnostic: the Nth paragraph (0-indexed). Use sparingly:
1287 /// indices shift as paragraphs are inserted or removed.
1288 Index {
1289 /// Zero-based paragraph index.
1290 index: usize,
1291 },
1292 /// File-format-agnostic: the first paragraph whose `field` has exactly
1293 /// the given `value`.
1294 ByKey {
1295 /// Field name to match (case-sensitive, as deb822 keys are).
1296 field: String,
1297 /// Required value.
1298 value: String,
1299 },
1300}
1301
1302/// A byte range in a file.
1303#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1304pub struct TextRange {
1305 /// Start byte offset (inclusive).
1306 pub start: usize,
1307 /// End byte offset (exclusive).
1308 pub end: usize,
1309}
1310
1311#[cfg(test)]
1312mod tests {
1313 use super::*;
1314
1315 #[test]
1316 fn action_serializes_with_kind_tag() {
1317 let action = Action::Deb822(Deb822Action::SetField {
1318 file: PathBuf::from("debian/control"),
1319 paragraph: ParagraphSelector::Binary {
1320 package: "foo".into(),
1321 },
1322 field: "Priority".into(),
1323 value: "optional".into(),
1324 });
1325 let json = serde_json::to_value(&action).unwrap();
1326 assert_eq!(json["kind"], "deb822");
1327 assert_eq!(json["op"], "set_field");
1328 assert_eq!(json["field"], "Priority");
1329 assert_eq!(json["value"], "optional");
1330 assert_eq!(json["paragraph"]["kind"], "binary");
1331 assert_eq!(json["paragraph"]["package"], "foo");
1332 }
1333
1334 #[test]
1335 fn action_roundtrips_through_json() {
1336 let original = Action::Filesystem(FilesystemAction::SetMode {
1337 file: PathBuf::from("debian/rules"),
1338 mode: 0o755,
1339 });
1340 let json = serde_json::to_string(&original).unwrap();
1341 let parsed: Action = serde_json::from_str(&json).unwrap();
1342 assert_eq!(original, parsed);
1343 }
1344}