cb8a650a
Add patch labels: full write path + --label on patch list
a73x 2026-08-10 05:57
Commit message
src/cli.rs
| Old | New | ||
|---|---|---|---|
| @@ -342,6 +342,9 @@ pub enum PatchCmd { | |||
| 342 | /// Sort order: recent (default), created, alpha | 342 | /// Sort order: recent (default), created, alpha |
| 343 | #[arg(long, default_value = "recent")] | 343 | #[arg(long, default_value = "recent")] |
| 344 | sort: SortMode, | 344 | sort: SortMode, |
| 345 | /// Filter by label (repeatable; matches patches carrying any of the given labels) | ||
| 346 | #[arg(long)] | ||
| 347 | label: Vec<String>, | ||
| 345 | }, | 348 | }, |
| 346 | /// Show patch details | 349 | /// Show patch details |
| 347 | Show { | 350 | Show { |
| @@ -415,6 +418,20 @@ pub enum PatchCmd { | |||
| 415 | #[arg(long)] | 418 | #[arg(long)] |
| 416 | json: bool, | 419 | json: bool, |
| 417 | }, | 420 | }, |
| 421 | /// Add a label to a patch | ||
| 422 | Label { | ||
| 423 | /// Patch ID (prefix match) | ||
| 424 | id: String, | ||
| 425 | /// Label to add | ||
| 426 | label: String, | ||
| 427 | }, | ||
| 428 | /// Remove a label from a patch | ||
| 429 | Unlabel { | ||
| 430 | /// Patch ID (prefix match) | ||
| 431 | id: String, | ||
| 432 | /// Label to remove | ||
| 433 | label: String, | ||
| 434 | }, | ||
| 418 | /// Close a patch | 435 | /// Close a patch |
| 419 | Close { | 436 | Close { |
| 420 | /// Patch ID (prefix match) | 437 | /// Patch ID (prefix match) |
| @@ -458,6 +475,8 @@ impl Commands { | |||
| 458 | | PatchCmd::Comment { .. } | 475 | | PatchCmd::Comment { .. } |
| 459 | | PatchCmd::Review { .. } | 476 | | PatchCmd::Review { .. } |
| 460 | | PatchCmd::Revise { .. } | 477 | | PatchCmd::Revise { .. } |
| 478 | | PatchCmd::Label { .. } | ||
| 479 | | PatchCmd::Unlabel { .. } | ||
| 461 | | PatchCmd::Close { .. } | 480 | | PatchCmd::Close { .. } |
| 462 | ), | 481 | ), |
| 463 | _ => false, | 482 | _ => false, |
src/dag.rs
| Old | New | ||
|---|---|---|---|
| @@ -328,6 +328,8 @@ fn commit_message(action: &Action) -> String { | |||
| 328 | Action::IssueReopen => "issue: reopen".to_string(), | 328 | Action::IssueReopen => "issue: reopen".to_string(), |
| 329 | Action::IssueCommitLink { commit } => format!("issue: commit link {}", &commit[..commit.len().min(7)]), | 329 | Action::IssueCommitLink { commit } => format!("issue: commit link {}", &commit[..commit.len().min(7)]), |
| 330 | Action::PatchCreate { title, .. } => format!("patch: create \"{}\"", title), | 330 | Action::PatchCreate { title, .. } => format!("patch: create \"{}\"", title), |
| 331 | Action::PatchLabel { ref label } => format!("patch: label \"{}\"", label), | ||
| 332 | Action::PatchUnlabel { ref label } => format!("patch: unlabel \"{}\"", label), | ||
| 331 | Action::PatchRevision { .. } => "patch: revision".to_string(), | 333 | Action::PatchRevision { .. } => "patch: revision".to_string(), |
| 332 | Action::PatchReview { verdict, .. } => format!("patch: review ({})", verdict), | 334 | Action::PatchReview { verdict, .. } => format!("patch: review ({})", verdict), |
| 333 | Action::PatchComment { .. } => "patch: comment".to_string(), | 335 | Action::PatchComment { .. } => "patch: comment".to_string(), |
src/event.rs
| Old | New | ||
|---|---|---|---|
| @@ -121,6 +121,10 @@ pub enum Action { | |||
| 121 | #[serde(default, skip_serializing_if = "Option::is_none")] | 121 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 122 | revision: Option<u32>, | 122 | revision: Option<u32>, |
| 123 | }, | 123 | }, |
| 124 | #[serde(rename = "patch.label")] | ||
| 125 | PatchLabel { label: String }, | ||
| 126 | #[serde(rename = "patch.unlabel")] | ||
| 127 | PatchUnlabel { label: String }, | ||
| 124 | #[serde(rename = "patch.comment")] | 128 | #[serde(rename = "patch.comment")] |
| 125 | PatchComment { body: String }, | 129 | PatchComment { body: String }, |
| 126 | #[serde(rename = "patch.inline_comment")] | 130 | #[serde(rename = "patch.inline_comment")] |
src/lib.rs
| Old | New | ||
|---|---|---|---|
| @@ -348,18 +348,24 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> { | |||
| 348 | offset, | 348 | offset, |
| 349 | json, | 349 | json, |
| 350 | sort, | 350 | sort, |
| 351 | label, | ||
| 351 | } => { | 352 | } => { |
| 352 | if json { | 353 | if json { |
| 353 | let output = patch::list_json(repo, all, archived, sort)?; | 354 | let output = patch::list_json(repo, all, archived, sort, &label)?; |
| 354 | println!("{}", output); | 355 | println!("{}", output); |
| 355 | return Ok(()); | 356 | return Ok(()); |
| 356 | } | 357 | } |
| 357 | let entries = patch::list(repo, all, archived, limit, offset, sort)?; | 358 | let entries = patch::list(repo, all, archived, limit, offset, sort, &label)?; |
| 358 | if entries.is_empty() { | 359 | if entries.is_empty() { |
| 359 | println!("No patches found."); | 360 | println!("No patches found."); |
| 360 | } else { | 361 | } else { |
| 361 | for e in &entries { | 362 | for e in &entries { |
| 362 | let p = &e.patch; | 363 | let p = &e.patch; |
| 364 | let labels = if p.labels.is_empty() { | ||
| 365 | String::new() | ||
| 366 | } else { | ||
| 367 | format!(" [{}]", p.labels.join(", ")) | ||
| 368 | }; | ||
| 363 | let stale = match p.staleness(repo) { | 369 | let stale = match p.staleness(repo) { |
| 364 | Ok((_, behind)) if behind > 0 => format!(" [behind {}]", behind), | 370 | Ok((_, behind)) if behind > 0 => format!(" [behind {}]", behind), |
| 365 | Ok(_) => String::new(), | 371 | Ok(_) => String::new(), |
| @@ -370,8 +376,8 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> { | |||
| 370 | _ => String::new(), | 376 | _ => String::new(), |
| 371 | }; | 377 | }; |
| 372 | println!( | 378 | println!( |
| 373 | "{:.8} {:6} {} (by {}){}{}", | 379 | "{:.8} {:6} {}{} (by {}){}{}", |
| 374 | p.id, p.status, p.title, p.author.name, stale, unread | 380 | p.id, p.status, p.title, labels, p.author.name, stale, unread |
| 375 | ); | 381 | ); |
| 376 | } | 382 | } |
| 377 | } | 383 | } |
| @@ -418,6 +424,9 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> { | |||
| 418 | } | 424 | } |
| 419 | } | 425 | } |
| 420 | println!("Created: {}", p.created_at); | 426 | println!("Created: {}", p.created_at); |
| 427 | if !p.labels.is_empty() { | ||
| 428 | println!("Labels: {}", p.labels.join(", ")); | ||
| 429 | } | ||
| 421 | if let Some(ref fixes) = p.fixes { | 430 | if let Some(ref fixes) = p.fixes { |
| 422 | println!("Fixes: {:.8}", fixes); | 431 | println!("Fixes: {:.8}", fixes); |
| 423 | } | 432 | } |
| @@ -560,6 +569,16 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> { | |||
| 560 | } | 569 | } |
| 561 | Ok(()) | 570 | Ok(()) |
| 562 | } | 571 | } |
| 572 | PatchCmd::Label { id, label } => { | ||
| 573 | patch::label(repo, &id, &label)?; | ||
| 574 | println!("Label '{}' added.", label); | ||
| 575 | Ok(()) | ||
| 576 | } | ||
| 577 | PatchCmd::Unlabel { id, label } => { | ||
| 578 | patch::unlabel(repo, &id, &label)?; | ||
| 579 | println!("Label '{}' removed.", label); | ||
| 580 | Ok(()) | ||
| 581 | } | ||
| 563 | PatchCmd::Close { id, reason } => { | 582 | PatchCmd::Close { id, reason } => { |
| 564 | patch::close(repo, &id, reason.as_deref())?; | 583 | patch::close(repo, &id, reason.as_deref())?; |
| 565 | println!("Patch closed."); | 584 | println!("Patch closed."); |
src/log.rs
| Old | New | ||
|---|---|---|---|
| @@ -121,6 +121,8 @@ fn action_type_name(action: &Action) -> String { | |||
| 121 | Action::IssueReopen => "IssueReopen".to_string(), | 121 | Action::IssueReopen => "IssueReopen".to_string(), |
| 122 | Action::IssueCommitLink { .. } => "IssueCommitLink".to_string(), | 122 | Action::IssueCommitLink { .. } => "IssueCommitLink".to_string(), |
| 123 | Action::PatchCreate { .. } => "PatchCreate".to_string(), | 123 | Action::PatchCreate { .. } => "PatchCreate".to_string(), |
| 124 | Action::PatchLabel { .. } => "PatchLabel".to_string(), | ||
| 125 | Action::PatchUnlabel { .. } => "PatchUnlabel".to_string(), | ||
| 124 | Action::PatchRevision { .. } => "PatchRevision".to_string(), | 126 | Action::PatchRevision { .. } => "PatchRevision".to_string(), |
| 125 | Action::PatchReview { .. } => "PatchReview".to_string(), | 127 | Action::PatchReview { .. } => "PatchReview".to_string(), |
| 126 | Action::PatchComment { .. } => "PatchComment".to_string(), | 128 | Action::PatchComment { .. } => "PatchComment".to_string(), |
| @@ -160,6 +162,8 @@ fn action_summary(action: &Action) -> String { | |||
| 160 | format!("commit link {}", &commit[..commit.len().min(7)]) | 162 | format!("commit link {}", &commit[..commit.len().min(7)]) |
| 161 | } | 163 | } |
| 162 | Action::PatchCreate { title, .. } => format!("create \"{}\"", title), | 164 | Action::PatchCreate { title, .. } => format!("create \"{}\"", title), |
| 165 | Action::PatchLabel { label } => format!("label \"{}\"", label), | ||
| 166 | Action::PatchUnlabel { label } => format!("unlabel \"{}\"", label), | ||
| 163 | Action::PatchRevision { body, .. } => match body { | 167 | Action::PatchRevision { body, .. } => match body { |
| 164 | Some(b) => format!("revision: {}", truncate(b, 50)), | 168 | Some(b) => format!("revision: {}", truncate(b, 50)), |
| 165 | None => "revision".to_string(), | 169 | None => "revision".to_string(), |
src/patch.rs
| Old | New | ||
|---|---|---|---|
| @@ -114,16 +114,14 @@ pub fn list( | |||
| 114 | limit: Option<usize>, | 114 | limit: Option<usize>, |
| 115 | offset: Option<usize>, | 115 | offset: Option<usize>, |
| 116 | sort: SortMode, | 116 | sort: SortMode, |
| 117 | labels: &[String], | ||
| 117 | ) -> Result<Vec<ListEntry>, crate::error::Error> { | 118 | ) -> Result<Vec<ListEntry>, crate::error::Error> { |
| 118 | let patches = if show_archived { | 119 | let patches = if show_archived { |
| 119 | state::list_patches_with_archived(repo)? | 120 | state::list_patches_with_archived(repo)? |
| 120 | } else { | 121 | } else { |
| 121 | state::list_patches(repo)? | 122 | state::list_patches(repo)? |
| 122 | }; | 123 | }; |
| 123 | // Patches have no labelling mechanism yet (no `patch label` command, no | 124 | let filtered = cli::filter_sort_paginate(patches, show_closed, labels, sort, offset, limit); |
| 124 | // `labels` field on PatchState), so there is no CLI flag to plumb a | ||
| 125 | // label filter through here. Pass an empty slice, which is a no-op. | ||
| 126 | let filtered = cli::filter_sort_paginate(patches, show_closed, &[], sort, offset, limit); | ||
| 127 | let entries = filtered | 125 | let entries = filtered |
| 128 | .into_iter() | 126 | .into_iter() |
| 129 | .map(|patch| { | 127 | .map(|patch| { |
| @@ -134,6 +132,7 @@ pub fn list( | |||
| 134 | Ok(entries) | 132 | Ok(entries) |
| 135 | } | 133 | } |
| 136 | 134 | ||
| 135 | #[allow(clippy::too_many_arguments)] | ||
| 137 | pub fn list_to_writer( | 136 | pub fn list_to_writer( |
| 138 | repo: &Repository, | 137 | repo: &Repository, |
| 139 | show_closed: bool, | 138 | show_closed: bool, |
| @@ -141,9 +140,10 @@ pub fn list_to_writer( | |||
| 141 | limit: Option<usize>, | 140 | limit: Option<usize>, |
| 142 | offset: Option<usize>, | 141 | offset: Option<usize>, |
| 143 | sort: SortMode, | 142 | sort: SortMode, |
| 143 | labels: &[String], | ||
| 144 | writer: &mut dyn std::io::Write, | 144 | writer: &mut dyn std::io::Write, |
| 145 | ) -> Result<(), crate::error::Error> { | 145 | ) -> Result<(), crate::error::Error> { |
| 146 | let entries = list(repo, show_closed, show_archived, limit, offset, sort)?; | 146 | let entries = list(repo, show_closed, show_archived, limit, offset, sort, labels)?; |
| 147 | if entries.is_empty() { | 147 | if entries.is_empty() { |
| 148 | writeln!(writer, "No patches found.").ok(); | 148 | writeln!(writer, "No patches found.").ok(); |
| 149 | return Ok(()); | 149 | return Ok(()); |
| @@ -174,8 +174,9 @@ pub fn list_json( | |||
| 174 | show_closed: bool, | 174 | show_closed: bool, |
| 175 | show_archived: bool, | 175 | show_archived: bool, |
| 176 | sort: SortMode, | 176 | sort: SortMode, |
| 177 | labels: &[String], | ||
| 177 | ) -> Result<String, crate::error::Error> { | 178 | ) -> Result<String, crate::error::Error> { |
| 178 | let entries = list(repo, show_closed, show_archived, None, None, sort)?; | 179 | let entries = list(repo, show_closed, show_archived, None, None, sort, labels)?; |
| 179 | let patches: Vec<&PatchState> = entries.iter().map(|e| &e.patch).collect(); | 180 | let patches: Vec<&PatchState> = entries.iter().map(|e| &e.patch).collect(); |
| 180 | Ok(serde_json::to_string_pretty(&patches)?) | 181 | Ok(serde_json::to_string_pretty(&patches)?) |
| 181 | } | 182 | } |
| @@ -667,6 +668,30 @@ pub fn delete(repo: &Repository, id_prefix: &str) -> Result<String, crate::error | |||
| 667 | Ok(id) | 668 | Ok(id) |
| 668 | } | 669 | } |
| 669 | 670 | ||
| 671 | pub fn label(repo: &Repository, id_prefix: &str, label: &str) -> Result<(), crate::error::Error> { | ||
| 672 | let (ref_name, _id) = state::resolve_patch_ref(repo, id_prefix)?; | ||
| 673 | dag::append_action( | ||
| 674 | repo, | ||
| 675 | &ref_name, | ||
| 676 | Action::PatchLabel { | ||
| 677 | label: label.to_string(), | ||
| 678 | }, | ||
| 679 | )?; | ||
| 680 | Ok(()) | ||
| 681 | } | ||
| 682 | |||
| 683 | pub fn unlabel(repo: &Repository, id_prefix: &str, label: &str) -> Result<(), crate::error::Error> { | ||
| 684 | let (ref_name, _id) = state::resolve_patch_ref(repo, id_prefix)?; | ||
| 685 | dag::append_action( | ||
| 686 | repo, | ||
| 687 | &ref_name, | ||
| 688 | Action::PatchUnlabel { | ||
| 689 | label: label.to_string(), | ||
| 690 | }, | ||
| 691 | )?; | ||
| 692 | Ok(()) | ||
| 693 | } | ||
| 694 | |||
| 670 | pub fn close( | 695 | pub fn close( |
| 671 | repo: &Repository, | 696 | repo: &Repository, |
| 672 | id_prefix: &str, | 697 | id_prefix: &str, |
src/server/http/repo/patches.rs
| Old | New | ||
|---|---|---|---|
| @@ -50,6 +50,7 @@ pub struct PatchListItem { | |||
| 50 | pub status: String, | 50 | pub status: String, |
| 51 | pub title: String, | 51 | pub title: String, |
| 52 | pub author: String, | 52 | pub author: String, |
| 53 | pub labels: String, | ||
| 53 | pub branch: String, | 54 | pub branch: String, |
| 54 | pub updated: String, | 55 | pub updated: String, |
| 55 | } | 56 | } |
| @@ -105,6 +106,7 @@ pub struct PatchDetailView { | |||
| 105 | pub body: String, | 106 | pub body: String, |
| 106 | pub status: String, | 107 | pub status: String, |
| 107 | pub author: String, | 108 | pub author: String, |
| 109 | pub labels: String, | ||
| 108 | pub branch: String, | 110 | pub branch: String, |
| 109 | pub base_ref: String, | 111 | pub base_ref: String, |
| 110 | pub revisions: Vec<RevisionView>, | 112 | pub revisions: Vec<RevisionView>, |
| @@ -174,6 +176,7 @@ pub async fn patches( | |||
| 174 | status: p.status.as_str().to_string(), | 176 | status: p.status.as_str().to_string(), |
| 175 | title: p.title, | 177 | title: p.title, |
| 176 | author: p.author.name, | 178 | author: p.author.name, |
| 179 | labels: p.labels.join(", "), | ||
| 177 | branch: p.branch, | 180 | branch: p.branch, |
| 178 | updated: p.last_updated, | 181 | updated: p.last_updated, |
| 179 | } | 182 | } |
| @@ -218,6 +221,7 @@ pub async fn patch_detail( | |||
| 218 | body: ps.body, | 221 | body: ps.body, |
| 219 | status: ps.status.as_str().to_string(), | 222 | status: ps.status.as_str().to_string(), |
| 220 | author: ps.author.name, | 223 | author: ps.author.name, |
| 224 | labels: ps.labels.join(", "), | ||
| 221 | branch: ps.branch, | 225 | branch: ps.branch, |
| 222 | base_ref: ps.base_ref, | 226 | base_ref: ps.base_ref, |
| 223 | revisions: ps | 227 | revisions: ps |
src/server/http/templates/patch_detail.html
| Old | New | ||
|---|---|---|---|
| @@ -9,6 +9,9 @@ | |||
| 9 | by <strong>{{ patch.author }}</strong> | 9 | by <strong>{{ patch.author }}</strong> |
| 10 | <span class="mono" style="color: #666;">{{ patch.branch }} → {{ patch.base_ref }}</span> | 10 | <span class="mono" style="color: #666;">{{ patch.branch }} → {{ patch.base_ref }}</span> |
| 11 | </p> | 11 | </p> |
| 12 | {% if !patch.labels.is_empty() %} | ||
| 13 | <p style="color: #666;">Labels: {{ patch.labels }}</p> | ||
| 14 | {% endif %} | ||
| 12 | {% if !patch.body.is_empty() %} | 15 | {% if !patch.body.is_empty() %} |
| 13 | <pre style="background: #f8f8f8; padding: 12px; border-radius: 4px; white-space: pre-wrap;">{{ patch.body }}</pre> | 16 | <pre style="background: #f8f8f8; padding: 12px; border-radius: 4px; white-space: pre-wrap;">{{ patch.body }}</pre> |
| 14 | {% endif %} | 17 | {% endif %} |
src/server/http/templates/patches.html
| Old | New | ||
|---|---|---|---|
| @@ -21,6 +21,7 @@ | |||
| 21 | <th>Status</th> | 21 | <th>Status</th> |
| 22 | <th>Title</th> | 22 | <th>Title</th> |
| 23 | <th>Author</th> | 23 | <th>Author</th> |
| 24 | <th>Labels</th> | ||
| 24 | <th>Branch</th> | 25 | <th>Branch</th> |
| 25 | <th>Updated</th> | 26 | <th>Updated</th> |
| 26 | </tr> | 27 | </tr> |
| @@ -32,6 +33,7 @@ | |||
| 32 | <td><span class="status-{{ p.status }}">{{ p.status }}</span></td> | 33 | <td><span class="status-{{ p.status }}">{{ p.status }}</span></td> |
| 33 | <td><a href="/{{ repo_name }}/patches/{{ p.id }}">{{ p.title }}</a></td> | 34 | <td><a href="/{{ repo_name }}/patches/{{ p.id }}">{{ p.title }}</a></td> |
| 34 | <td style="color: #666;">{{ p.author }}</td> | 35 | <td style="color: #666;">{{ p.author }}</td> |
| 36 | <td style="color: #666;">{{ p.labels }}</td> | ||
| 35 | <td class="mono" style="color: #666;">{{ p.branch }}</td> | 37 | <td class="mono" style="color: #666;">{{ p.branch }}</td> |
| 36 | <td class="mono" style="color: #666;">{{ p.updated }}</td> | 38 | <td class="mono" style="color: #666;">{{ p.updated }}</td> |
| 37 | </tr> | 39 | </tr> |
src/state.rs
| Old | New | ||
|---|---|---|---|
| @@ -237,6 +237,10 @@ pub struct PatchState { | |||
| 237 | /// Nothing resolves through it any more: a patch is addressed by its own | 237 | /// Nothing resolves through it any more: a patch is addressed by its own |
| 238 | /// revision refs, so an ephemeral or rewritten branch costs it nothing. | 238 | /// revision refs, so an ephemeral or rewritten branch costs it nothing. |
| 239 | pub branch: String, | 239 | pub branch: String, |
| 240 | /// Postdates patch labelling: absent on any `PatchState` serialized | ||
| 241 | /// before this field existed, which must load as an empty vec. | ||
| 242 | #[serde(default)] | ||
| 243 | pub labels: Vec<String>, | ||
| 240 | pub comments: Vec<Comment>, | 244 | pub comments: Vec<Comment>, |
| 241 | pub inline_comments: Vec<InlineComment>, | 245 | pub inline_comments: Vec<InlineComment>, |
| 242 | pub reviews: Vec<Review>, | 246 | pub reviews: Vec<Review>, |
| @@ -278,6 +282,9 @@ impl crate::cli::Listable for PatchState { | |||
| 278 | fn title(&self) -> &str { | 282 | fn title(&self) -> &str { |
| 279 | &self.title | 283 | &self.title |
| 280 | } | 284 | } |
| 285 | fn labels(&self) -> &[String] { | ||
| 286 | &self.labels | ||
| 287 | } | ||
| 281 | } | 288 | } |
| 282 | 289 | ||
| 283 | impl IssueState { | 290 | impl IssueState { |
| @@ -672,6 +679,7 @@ impl PatchState { | |||
| 672 | base_ref, | 679 | base_ref, |
| 673 | fixes, | 680 | fixes, |
| 674 | branch, | 681 | branch, |
| 682 | labels: Vec::new(), | ||
| 675 | comments: Vec::new(), | 683 | comments: Vec::new(), |
| 676 | inline_comments: Vec::new(), | 684 | inline_comments: Vec::new(), |
| 677 | reviews: Vec::new(), | 685 | reviews: Vec::new(), |
| @@ -681,6 +689,18 @@ impl PatchState { | |||
| 681 | author: event.author.clone(), | 689 | author: event.author.clone(), |
| 682 | }); | 690 | }); |
| 683 | } | 691 | } |
| 692 | Action::PatchLabel { label } => { | ||
| 693 | if let Some(ref mut s) = state { | ||
| 694 | if !s.labels.contains(&label) { | ||
| 695 | s.labels.push(label); | ||
| 696 | } | ||
| 697 | } | ||
| 698 | } | ||
| 699 | Action::PatchUnlabel { label } => { | ||
| 700 | if let Some(ref mut s) = state { | ||
| 701 | s.labels.retain(|l| l != &label); | ||
| 702 | } | ||
| 703 | } | ||
| 684 | Action::PatchRevision { | 704 | Action::PatchRevision { |
| 685 | commit, | 705 | commit, |
| 686 | tree, | 706 | tree, |
| @@ -1471,4 +1491,29 @@ mod tests { | |||
| 1471 | let issue: IssueState = serde_json::from_str(json).unwrap(); | 1491 | let issue: IssueState = serde_json::from_str(json).unwrap(); |
| 1472 | assert_eq!(issue.relates_to, Vec::<String>::new()); | 1492 | assert_eq!(issue.relates_to, Vec::<String>::new()); |
| 1473 | } | 1493 | } |
| 1494 | |||
| 1495 | // `labels` on `PatchState` postdates patch labelling: cached state (and | ||
| 1496 | // any other on-disk JSON) written before this feature existed has no | ||
| 1497 | // `labels` field at all. It must still deserialize, as an empty vec. | ||
| 1498 | #[test] | ||
| 1499 | fn deserializes_missing_labels_field_as_empty_vec_on_patch_state() { | ||
| 1500 | let json = r#"{ | ||
| 1501 | "id": "abc123", | ||
| 1502 | "title": "t", | ||
| 1503 | "body": "", | ||
| 1504 | "status": "open", | ||
| 1505 | "base_ref": "main", | ||
| 1506 | "fixes": null, | ||
| 1507 | "branch": "feature/x", | ||
| 1508 | "comments": [], | ||
| 1509 | "inline_comments": [], | ||
| 1510 | "reviews": [], | ||
| 1511 | "revisions": [], | ||
| 1512 | "created_at": "2026-01-01T00:00:00Z", | ||
| 1513 | "last_updated": "", | ||
| 1514 | "author": {"name": "A", "email": "a@example.com"} | ||
| 1515 | }"#; | ||
| 1516 | let patch: PatchState = serde_json::from_str(json).unwrap(); | ||
| 1517 | assert_eq!(patch.labels, Vec::<String>::new()); | ||
| 1518 | } | ||
| 1474 | } | 1519 | } |
src/tui/mod.rs
| Old | New | ||
|---|---|---|---|
| @@ -80,6 +80,7 @@ mod tests { | |||
| 80 | base_ref: "main".into(), | 80 | base_ref: "main".into(), |
| 81 | fixes: None, | 81 | fixes: None, |
| 82 | branch: format!("feature/{}", id), | 82 | branch: format!("feature/{}", id), |
| 83 | labels: vec![], | ||
| 83 | comments: vec![], | 84 | comments: vec![], |
| 84 | inline_comments: vec![], | 85 | inline_comments: vec![], |
| 85 | reviews: vec![], | 86 | reviews: vec![], |
| @@ -276,6 +277,7 @@ mod tests { | |||
| 276 | base_ref: "main".to_string(), | 277 | base_ref: "main".to_string(), |
| 277 | fixes: None, | 278 | fixes: None, |
| 278 | branch: format!("feature/p{:07x}", i), | 279 | branch: format!("feature/p{:07x}", i), |
| 280 | labels: vec![], | ||
| 279 | comments: Vec::new(), | 281 | comments: Vec::new(), |
| 280 | inline_comments: Vec::new(), | 282 | inline_comments: Vec::new(), |
| 281 | reviews: Vec::new(), | 283 | reviews: Vec::new(), |
| @@ -1032,6 +1034,7 @@ mod tests { | |||
| 1032 | base_ref: "main".into(), | 1034 | base_ref: "main".into(), |
| 1033 | fixes: Some("i1".into()), | 1035 | fixes: Some("i1".into()), |
| 1034 | branch: "feature/fix-thing".into(), | 1036 | branch: "feature/fix-thing".into(), |
| 1037 | labels: vec!["needs-review".into()], | ||
| 1035 | comments: vec![crate::state::Comment { | 1038 | comments: vec![crate::state::Comment { |
| 1036 | author: make_author(), | 1039 | author: make_author(), |
| 1037 | body: "Thread comment".into(), | 1040 | body: "Thread comment".into(), |
| @@ -1276,6 +1279,17 @@ mod tests { | |||
| 1276 | } | 1279 | } |
| 1277 | 1280 | ||
| 1278 | #[test] | 1281 | #[test] |
| 1282 | fn test_render_patch_detail_shows_labels() { | ||
| 1283 | let mut app = make_app(3, 0); | ||
| 1284 | app.mode = ViewMode::PatchDetail; | ||
| 1285 | app.current_patch = Some(make_patch_with_revisions()); | ||
| 1286 | app.patch_diff = String::new(); | ||
| 1287 | |||
| 1288 | let buf = render_app(&mut app); | ||
| 1289 | assert_buffer_contains(&buf, "needs-review"); | ||
| 1290 | } | ||
| 1291 | |||
| 1292 | #[test] | ||
| 1279 | fn test_render_patch_detail_shows_reviews() { | 1293 | fn test_render_patch_detail_shows_reviews() { |
| 1280 | let mut app = make_app(3, 0); | 1294 | let mut app = make_app(3, 0); |
| 1281 | app.mode = ViewMode::PatchDetail; | 1295 | app.mode = ViewMode::PatchDetail; |
src/tui/widgets.rs
| Old | New | ||
|---|---|---|---|
| @@ -14,6 +14,8 @@ pub(crate) fn action_type_label(action: &Action) -> &str { | |||
| 14 | Action::IssueClose { .. } => "Issue Close", | 14 | Action::IssueClose { .. } => "Issue Close", |
| 15 | Action::IssueReopen => "Issue Reopen", | 15 | Action::IssueReopen => "Issue Reopen", |
| 16 | Action::PatchCreate { .. } => "Patch Create", | 16 | Action::PatchCreate { .. } => "Patch Create", |
| 17 | Action::PatchLabel { .. } => "Patch Label", | ||
| 18 | Action::PatchUnlabel { .. } => "Patch Unlabel", | ||
| 17 | Action::PatchRevision { .. } => "Patch Revision", | 19 | Action::PatchRevision { .. } => "Patch Revision", |
| 18 | Action::PatchReview { .. } => "Patch Review", | 20 | Action::PatchReview { .. } => "Patch Review", |
| 19 | Action::PatchComment { .. } => "Patch Comment", | 21 | Action::PatchComment { .. } => "Patch Comment", |
| @@ -106,10 +108,10 @@ pub(crate) fn format_event_detail(oid: &Oid, event: &crate::event::Event) -> Str | |||
| 106 | } | 108 | } |
| 107 | } | 109 | } |
| 108 | } | 110 | } |
| 109 | Action::IssueLabel { label } => { | 111 | Action::IssueLabel { label } | Action::PatchLabel { label } => { |
| 110 | detail.push_str(&format!("\nLabel: {}\n", label)); | 112 | detail.push_str(&format!("\nLabel: {}\n", label)); |
| 111 | } | 113 | } |
| 112 | Action::IssueUnlabel { label } => { | 114 | Action::IssueUnlabel { label } | Action::PatchUnlabel { label } => { |
| 113 | detail.push_str(&format!("\nRemoved Label: {}\n", label)); | 115 | detail.push_str(&format!("\nRemoved Label: {}\n", label)); |
| 114 | } | 116 | } |
| 115 | Action::IssueRelate { relates_to } => { | 117 | Action::IssueRelate { relates_to } => { |
| @@ -627,6 +629,13 @@ fn build_patch_detail_text(app: &App) -> Text<'static> { | |||
| 627 | ]), | 629 | ]), |
| 628 | ]; | 630 | ]; |
| 629 | 631 | ||
| 632 | if !patch.labels.is_empty() { | ||
| 633 | lines.push(Line::from(vec![ | ||
| 634 | Span::styled("Labels: ", Style::default().fg(Color::DarkGray)), | ||
| 635 | Span::raw(patch.labels.join(", ")), | ||
| 636 | ])); | ||
| 637 | } | ||
| 638 | |||
| 630 | if let Some(ref fixes) = patch.fixes { | 639 | if let Some(ref fixes) = patch.fixes { |
| 631 | lines.push(Line::from(vec![ | 640 | lines.push(Line::from(vec![ |
| 632 | Span::styled("Fixes: ", Style::default().fg(Color::DarkGray)), | 641 | Span::styled("Fixes: ", Style::default().fg(Color::DarkGray)), |
tests/cli_test.rs
| Old | New | ||
|---|---|---|---|
| @@ -591,29 +591,173 @@ fn test_patch_list_filters_by_status() { | |||
| 591 | assert!(out.contains("Closed patch")); | 591 | assert!(out.contains("Closed patch")); |
| 592 | } | 592 | } |
| 593 | 593 | ||
| 594 | // =========================================================================== | ||
| 595 | // Patch labels | ||
| 596 | // =========================================================================== | ||
| 597 | |||
| 594 | #[test] | 598 | #[test] |
| 595 | fn test_patch_list_rejects_label_flag() { | 599 | fn test_patch_label_and_show() { |
| 596 | // Patches have no way to carry labels (there is no `patch label` | ||
| 597 | // command, unlike `issue label`, and no `labels` field on PatchState). | ||
| 598 | // A `--label` flag that always returns an empty list would be worse | ||
| 599 | // than no flag at all -- it looks like "no patches have this label" | ||
| 600 | // when really the concept doesn't exist. So `patch list` must reject | ||
| 601 | // `--label` outright as an unknown argument rather than silently | ||
| 602 | // accepting and no-op-filtering it. | ||
| 603 | let repo = TestRepo::new("Alice", "alice@example.com"); | 600 | let repo = TestRepo::new("Alice", "alice@example.com"); |
| 604 | repo.patch_create("Open patch"); | 601 | let id = repo.patch_create("Labeled patch"); |
| 605 | 602 | ||
| 606 | let output = repo.run(&["patch", "list", "--label", "bug"]); | 603 | let out = repo.run_ok(&["patch", "label", &id, "bug"]); |
| 607 | assert!( | 604 | assert!(out.contains("Label") && out.contains("added")); |
| 608 | !output.status.success(), | 605 | |
| 609 | "patch list --label should be rejected" | 606 | let out = repo.run_ok(&["patch", "show", &id]); |
| 610 | ); | 607 | assert!(out.contains("bug")); |
| 611 | let stderr = String::from_utf8(output.stderr).unwrap(); | 608 | } |
| 612 | assert!( | 609 | |
| 613 | stderr.contains("unexpected argument") || stderr.contains("unrecognized"), | 610 | #[test] |
| 614 | "expected an unknown-argument error, got: {}", | 611 | fn test_patch_multiple_labels() { |
| 615 | stderr | 612 | let repo = TestRepo::new("Alice", "alice@example.com"); |
| 616 | ); | 613 | let id = repo.patch_create("Multi-label patch"); |
| 614 | |||
| 615 | repo.run_ok(&["patch", "label", &id, "bug"]); | ||
| 616 | repo.run_ok(&["patch", "label", &id, "priority"]); | ||
| 617 | |||
| 618 | let out = repo.run_ok(&["patch", "show", &id]); | ||
| 619 | assert!(out.contains("bug")); | ||
| 620 | assert!(out.contains("priority")); | ||
| 621 | } | ||
| 622 | |||
| 623 | #[test] | ||
| 624 | fn test_patch_unlabel() { | ||
| 625 | let repo = TestRepo::new("Alice", "alice@example.com"); | ||
| 626 | let id = repo.patch_create("Remove label"); | ||
| 627 | |||
| 628 | repo.run_ok(&["patch", "label", &id, "bug"]); | ||
| 629 | repo.run_ok(&["patch", "label", &id, "wontfix"]); | ||
| 630 | repo.run_ok(&["patch", "unlabel", &id, "bug"]); | ||
| 631 | |||
| 632 | let out = repo.run_ok(&["patch", "show", &id]); | ||
| 633 | assert!(!out.contains("bug")); | ||
| 634 | assert!(out.contains("wontfix")); | ||
| 635 | } | ||
| 636 | |||
| 637 | #[test] | ||
| 638 | fn test_patch_label_is_idempotent() { | ||
| 639 | // Adding the same label twice does not duplicate it, matching | ||
| 640 | // `issue label`'s behavior. | ||
| 641 | let repo = TestRepo::new("Alice", "alice@example.com"); | ||
| 642 | let id = repo.patch_create("Idempotent label"); | ||
| 643 | |||
| 644 | repo.run_ok(&["patch", "label", &id, "bug"]); | ||
| 645 | repo.run_ok(&["patch", "label", &id, "bug"]); | ||
| 646 | |||
| 647 | let out = repo.run_ok(&["patch", "show", &id, "--json"]); | ||
| 648 | let parsed: serde_json::Value = serde_json::from_str(&out).unwrap(); | ||
| 649 | let labels = parsed["labels"].as_array().unwrap(); | ||
| 650 | assert_eq!(labels.len(), 1); | ||
| 651 | } | ||
| 652 | |||
| 653 | #[test] | ||
| 654 | fn test_patch_unlabel_absent_is_noop() { | ||
| 655 | let repo = TestRepo::new("Alice", "alice@example.com"); | ||
| 656 | let id = repo.patch_create("Unlabel noop"); | ||
| 657 | |||
| 658 | repo.run_ok(&["patch", "label", &id, "bug"]); | ||
| 659 | repo.run_ok(&["patch", "unlabel", &id, "nonexistent"]); | ||
| 660 | |||
| 661 | let out = repo.run_ok(&["patch", "show", &id]); | ||
| 662 | assert!(out.contains("bug")); | ||
| 663 | } | ||
| 664 | |||
| 665 | #[test] | ||
| 666 | fn test_patch_label_shown_in_list() { | ||
| 667 | let repo = TestRepo::new("Alice", "alice@example.com"); | ||
| 668 | let id = repo.patch_create("Listed with label"); | ||
| 669 | |||
| 670 | repo.run_ok(&["patch", "label", &id, "enhancement"]); | ||
| 671 | |||
| 672 | let out = repo.run_ok(&["patch", "list"]); | ||
| 673 | assert!(out.contains("enhancement")); | ||
| 674 | } | ||
| 675 | |||
| 676 | #[test] | ||
| 677 | fn test_patch_list_filters_by_label() { | ||
| 678 | let repo = TestRepo::new("Alice", "alice@example.com"); | ||
| 679 | let bug_id = repo.patch_create("A bug patch"); | ||
| 680 | repo.patch_create("No label patch"); | ||
| 681 | repo.run_ok(&["patch", "label", &bug_id, "bug"]); | ||
| 682 | |||
| 683 | let out = repo.run_ok(&["patch", "list", "--label", "bug"]); | ||
| 684 | assert!(out.contains("A bug patch")); | ||
| 685 | assert!(!out.contains("No label patch")); | ||
| 686 | } | ||
| 687 | |||
| 688 | #[test] | ||
| 689 | fn test_patch_list_label_or_semantics() { | ||
| 690 | let repo = TestRepo::new("Alice", "alice@example.com"); | ||
| 691 | let bug_id = repo.patch_create("A bug patch"); | ||
| 692 | let docs_id = repo.patch_create("Docs patch"); | ||
| 693 | repo.patch_create("Unrelated patch"); | ||
| 694 | repo.run_ok(&["patch", "label", &bug_id, "bug"]); | ||
| 695 | repo.run_ok(&["patch", "label", &docs_id, "docs"]); | ||
| 696 | |||
| 697 | let out = repo.run_ok(&["patch", "list", "--label", "bug", "--label", "docs"]); | ||
| 698 | assert!(out.contains("A bug patch")); | ||
| 699 | assert!(out.contains("Docs patch")); | ||
| 700 | assert!(!out.contains("Unrelated patch")); | ||
| 701 | } | ||
| 702 | |||
| 703 | #[test] | ||
| 704 | fn test_patch_list_label_no_match() { | ||
| 705 | let repo = TestRepo::new("Alice", "alice@example.com"); | ||
| 706 | repo.patch_create("Unlabeled patch"); | ||
| 707 | |||
| 708 | let out = repo.run_ok(&["patch", "list", "--label", "nonexistent"]); | ||
| 709 | assert!(out.contains("No patches found")); | ||
| 710 | } | ||
| 711 | |||
| 712 | #[test] | ||
| 713 | fn test_patch_list_label_composes_with_all_archived() { | ||
| 714 | let repo = TestRepo::new("Alice", "alice@example.com"); | ||
| 715 | let id = repo.patch_create("Closed labeled"); | ||
| 716 | repo.run_ok(&["patch", "label", &id, "bug"]); | ||
| 717 | repo.run_ok(&["patch", "close", &id]); | ||
| 718 | |||
| 719 | // Closed patches are archived, so --label alone (without --all --archived) | ||
| 720 | // must not show it, even though it carries a matching label. | ||
| 721 | let out = repo.run_ok(&["patch", "list", "--label", "bug"]); | ||
| 722 | assert!(!out.contains("Closed labeled")); | ||
| 723 | |||
| 724 | let out = repo.run_ok(&["patch", "list", "--all", "--archived", "--label", "bug"]); | ||
| 725 | assert!(out.contains("Closed labeled")); | ||
| 726 | } | ||
| 727 | |||
| 728 | #[test] | ||
| 729 | fn test_patch_list_label_limit_offset_applies_after_filter() { | ||
| 730 | let repo = TestRepo::new("Alice", "alice@example.com"); | ||
| 731 | let a = repo.patch_create("Alpha patch"); | ||
| 732 | let _b = repo.patch_create("Beta unlabeled patch"); | ||
| 733 | let c = repo.patch_create("Gamma patch"); | ||
| 734 | repo.run_ok(&["patch", "label", &a, "keep"]); | ||
| 735 | repo.run_ok(&["patch", "label", &c, "keep"]); | ||
| 736 | |||
| 737 | // Only "Alpha" and "Gamma" carry the "keep" label. Sorting alphabetically | ||
| 738 | // and requesting a single result with offset 1 should skip "Alpha" and | ||
| 739 | // land on "Gamma" -- proving limit/offset apply to the filtered set, not | ||
| 740 | // the unfiltered one (which would offset past "Beta unlabeled" instead). | ||
| 741 | let out = repo.run_ok(&[ | ||
| 742 | "patch", "list", "--label", "keep", "--sort", "alpha", "--offset", "1", "-n", "1", | ||
| 743 | ]); | ||
| 744 | assert!(out.contains("Gamma")); | ||
| 745 | assert!(!out.contains("Alpha")); | ||
| 746 | assert!(!out.contains("Beta")); | ||
| 747 | } | ||
| 748 | |||
| 749 | #[test] | ||
| 750 | fn test_patch_list_label_json() { | ||
| 751 | let repo = TestRepo::new("Alice", "alice@example.com"); | ||
| 752 | let bug_id = repo.patch_create("A bug patch"); | ||
| 753 | repo.patch_create("No label patch"); | ||
| 754 | repo.run_ok(&["patch", "label", &bug_id, "bug"]); | ||
| 755 | |||
| 756 | let out = repo.run_ok(&["patch", "list", "--json", "--label", "bug"]); | ||
| 757 | let parsed: serde_json::Value = serde_json::from_str(&out).unwrap(); | ||
| 758 | let arr = parsed.as_array().unwrap(); | ||
| 759 | assert_eq!(arr.len(), 1); | ||
| 760 | assert_eq!(arr[0]["title"], "A bug patch"); | ||
| 617 | } | 761 | } |
| 618 | 762 | ||
| 619 | #[test] | 763 | #[test] |
tests/collab_test.rs
| Old | New | ||
|---|---|---|---|
| @@ -1305,6 +1305,7 @@ fn capture_patch_list( | |||
| 1305 | limit, | 1305 | limit, |
| 1306 | offset, | 1306 | offset, |
| 1307 | git_collab::cli::SortMode::Recent, | 1307 | git_collab::cli::SortMode::Recent, |
| 1308 | &[], | ||
| 1308 | &mut buf, | 1309 | &mut buf, |
| 1309 | ) | 1310 | ) |
| 1310 | .unwrap(); | 1311 | .unwrap(); |
| @@ -1578,9 +1579,14 @@ fn test_patch_list_json_output() { | |||
| 1578 | create_patch(&repo, &alice(), "Patch one"); | 1579 | create_patch(&repo, &alice(), "Patch one"); |
| 1579 | create_patch(&repo, &bob(), "Patch two"); | 1580 | create_patch(&repo, &bob(), "Patch two"); |
| 1580 | 1581 | ||
| 1581 | let json_str = | 1582 | let json_str = git_collab::patch::list_json( |
| 1582 | git_collab::patch::list_json(&repo, false, false, git_collab::cli::SortMode::Recent) | 1583 | &repo, |
| 1583 | .unwrap(); | 1584 | false, |
| 1585 | false, | ||
| 1586 | git_collab::cli::SortMode::Recent, | ||
| 1587 | &[], | ||
| 1588 | ) | ||
| 1589 | .unwrap(); | ||
| 1584 | let value: serde_json::Value = serde_json::from_str(&json_str).unwrap(); | 1590 | let value: serde_json::Value = serde_json::from_str(&json_str).unwrap(); |
| 1585 | let arr = value.as_array().unwrap(); | 1591 | let arr = value.as_array().unwrap(); |
| 1586 | assert_eq!(arr.len(), 2); | 1592 | assert_eq!(arr.len(), 2); |
tests/server_behavior_test.rs
| Old | New | ||
|---|---|---|---|
| @@ -80,6 +80,25 @@ fn pushed_repository_content_renders_across_repo_http_pages() { | |||
| 80 | } | 80 | } |
| 81 | 81 | ||
| 82 | #[test] | 82 | #[test] |
| 83 | fn patch_label_renders_in_server_ui() { | ||
| 84 | let harness = ServerHarness::new("behavior-patch-labels"); | ||
| 85 | |||
| 86 | harness.push_head(); | ||
| 87 | |||
| 88 | let patch_id = harness.work_repo().patch_create("Labeled patch"); | ||
| 89 | harness | ||
| 90 | .work_repo() | ||
| 91 | .run_ok(&["patch", "label", &patch_id, "needs-review"]); | ||
| 92 | harness.push_collab_refs(); | ||
| 93 | |||
| 94 | let list_page = harness.get_ok(&format!("/{}/patches", harness.repo_name())); | ||
| 95 | assert!(list_page.body.contains("needs-review")); | ||
| 96 | |||
| 97 | let detail_page = harness.get_ok(&format!("/{}/patches/{}", harness.repo_name(), patch_id)); | ||
| 98 | assert!(detail_page.body.contains("needs-review")); | ||
| 99 | } | ||
| 100 | |||
| 101 | #[test] | ||
| 83 | fn private_repo_policy_hides_repo_from_ui_and_uses_configured_description() { | 102 | fn private_repo_policy_hides_repo_from_ui_and_uses_configured_description() { |
| 84 | let harness = ServerHarness::new("behavior-private"); | 103 | let harness = ServerHarness::new("behavior-private"); |
| 85 | 104 | ||
tests/sort_test.rs
| Old | New | ||
|---|---|---|---|
| @@ -231,7 +231,7 @@ fn test_patch_default_sort_by_recency() { | |||
| 231 | let (_, _) = create_patch_at(&repo, &alice(), "Beta patch", "2025-06-01T00:00:00Z"); | 231 | let (_, _) = create_patch_at(&repo, &alice(), "Beta patch", "2025-06-01T00:00:00Z"); |
| 232 | 232 | ||
| 233 | let entries = | 233 | let entries = |
| 234 | git_collab::patch::list(&repo, true, false, None, None, SortMode::Recent).unwrap(); | 234 | git_collab::patch::list(&repo, true, false, None, None, SortMode::Recent, &[]).unwrap(); |
| 235 | assert_eq!(entries.len(), 2); | 235 | assert_eq!(entries.len(), 2); |
| 236 | assert_eq!(entries[0].patch.title, "Alpha patch"); | 236 | assert_eq!(entries[0].patch.title, "Alpha patch"); |
| 237 | assert_eq!(entries[1].patch.title, "Beta patch"); | 237 | assert_eq!(entries[1].patch.title, "Beta patch"); |
| @@ -254,7 +254,7 @@ fn test_patch_sort_by_created() { | |||
| 254 | let (_, _) = create_patch_at(&repo, &alice(), "Beta patch", "2025-06-01T00:00:00Z"); | 254 | let (_, _) = create_patch_at(&repo, &alice(), "Beta patch", "2025-06-01T00:00:00Z"); |
| 255 | 255 | ||
| 256 | let entries = | 256 | let entries = |
| 257 | git_collab::patch::list(&repo, true, false, None, None, SortMode::Created).unwrap(); | 257 | git_collab::patch::list(&repo, true, false, None, None, SortMode::Created, &[]).unwrap(); |
| 258 | assert_eq!(entries.len(), 2); | 258 | assert_eq!(entries.len(), 2); |
| 259 | assert_eq!(entries[0].patch.title, "Beta patch"); | 259 | assert_eq!(entries[0].patch.title, "Beta patch"); |
| 260 | assert_eq!(entries[1].patch.title, "Alpha patch"); | 260 | assert_eq!(entries[1].patch.title, "Alpha patch"); |
| @@ -269,7 +269,7 @@ fn test_patch_sort_alpha() { | |||
| 269 | create_patch_at(&repo, &alice(), "Apple patch", "2025-06-01T00:00:00Z"); | 269 | create_patch_at(&repo, &alice(), "Apple patch", "2025-06-01T00:00:00Z"); |
| 270 | create_patch_at(&repo, &alice(), "Mango patch", "2025-03-01T00:00:00Z"); | 270 | create_patch_at(&repo, &alice(), "Mango patch", "2025-03-01T00:00:00Z"); |
| 271 | 271 | ||
| 272 | let entries = git_collab::patch::list(&repo, true, false, None, None, SortMode::Alpha).unwrap(); | 272 | let entries = git_collab::patch::list(&repo, true, false, None, None, SortMode::Alpha, &[]).unwrap(); |
| 273 | assert_eq!(entries.len(), 3); | 273 | assert_eq!(entries.len(), 3); |
| 274 | assert_eq!(entries[0].patch.title, "Apple patch"); | 274 | assert_eq!(entries[0].patch.title, "Apple patch"); |
| 275 | assert_eq!(entries[1].patch.title, "Mango patch"); | 275 | assert_eq!(entries[1].patch.title, "Mango patch"); |