Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Restored dependency and parent relationship lines from cached Markdown issue
bodies without making additional GitHub requests.
- Upgraded all bundle artifact uploads to `actions/upload-artifact@v7` and
downloads to `actions/download-artifact@v8`, removing the obsolete
action-runtime warnings without suppressing them.
Expand Down
47 changes: 46 additions & 1 deletion crates/github/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ impl Cache {

pub fn load(&self, repo: &RepoRef) -> Option<Snapshot> {
let bytes = fs::read(self.path_for(repo)).ok()?;
serde_json::from_slice(&bytes).ok()
let mut snapshot: Snapshot = serde_json::from_slice(&bytes).ok()?;
crate::textref::enrich_relationships(&mut snapshot.issues);
Some(snapshot)
}

/// Writes a snapshot through a same-directory temporary file.
Expand Down Expand Up @@ -320,6 +322,49 @@ mod tests {
assert_eq!(snapshot.issues[0].parent_issue, None);
}

#[test]
fn load_enriches_relationships_from_cached_bodies() {
let dir = tempfile::tempdir().unwrap();
let cache = Cache::new(dir.path().to_path_buf());
let repo = repo();
let mut source = snapshot("Root", 1_753_000_000);
source.issues.push(RawIssue {
number: 2,
parent_issue: None,
title: "Dependent".into(),
body: "## Parent\n\n#1\n## Blocked by\n\n- #1".into(),
state: IssueState::Open,
assignees: vec![],
milestone: None,
labels: vec![],
blocked_by: vec![],
url: "u2".into(),
});
source.issues.push(RawIssue {
number: 3,
parent_issue: None,
title: "Blocker by inversion".into(),
body: "## Blocks\n\n- #2".into(),
state: IssueState::Open,
assignees: vec![],
milestone: None,
labels: vec![],
blocked_by: vec![],
url: "u3".into(),
});

cache.store(&repo, &source).unwrap();
let loaded = cache.load(&repo).unwrap();

assert_eq!(loaded.issues[1].parent_issue, Some(1));
assert_eq!(loaded.issues[1].blocked_by, vec![1, 3]);

cache.store(&repo, &loaded).unwrap();
let loaded_again = cache.load(&repo).unwrap();

assert_eq!(loaded_again, loaded);
}

#[test]
fn store_replaces_an_existing_snapshot() {
let dir = tempfile::tempdir().unwrap();
Expand Down
101 changes: 41 additions & 60 deletions crates/github/src/sync.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::collections::HashMap;

use octocrab::{FromResponse, Octocrab};
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand Down Expand Up @@ -190,67 +188,50 @@ fn map_octocrab_error(error: octocrab::Error) -> ProviderError {
}

fn map_issues(nodes: Vec<IssueNode>) -> Vec<RawIssue> {
let mut issues = Vec::with_capacity(nodes.len());
let mut inversions = Vec::new();

for node in nodes {
let body = node.body.unwrap_or_default();
let refs = textref::scan(&body);
let mut blocked_by = node
.blocked_by
.nodes
.into_iter()
.map(|issue| issue.number)
.chain(refs.blocked_by)
.collect::<Vec<_>>();
blocked_by.sort_unstable();
blocked_by.dedup();

inversions.extend(refs.blocks.into_iter().map(|target| (node.number, target)));
issues.push(RawIssue {
number: node.number,
parent_issue: node.parent.map(|parent| parent.number),
title: node.title,
body,
state: match node.state {
GithubIssueState::Open => IssueState::Open,
GithubIssueState::Closed if node.state_reason.as_deref() == Some("NOT_PLANNED") => {
IssueState::ClosedNotPlanned
}
GithubIssueState::Closed => IssueState::Closed,
},
assignees: node
.assignees
.nodes
.into_iter()
.map(|assignee| assignee.login)
.collect(),
milestone: node.milestone.map(|milestone| milestone.title),
labels: node
.labels
let mut issues = nodes
.into_iter()
.map(|node| {
let body = node.body.unwrap_or_default();
let blocked_by = node
.blocked_by
.nodes
.into_iter()
.map(|label| label.name)
.collect(),
blocked_by,
url: node.url,
});
}
.map(|issue| issue.number)
.collect::<Vec<_>>();
RawIssue {
number: node.number,
parent_issue: node.parent.map(|parent| parent.number),
title: node.title,
body,
state: match node.state {
GithubIssueState::Open => IssueState::Open,
GithubIssueState::Closed
if node.state_reason.as_deref() == Some("NOT_PLANNED") =>
{
IssueState::ClosedNotPlanned
}
GithubIssueState::Closed => IssueState::Closed,
},
assignees: node
.assignees
.nodes
.into_iter()
.map(|assignee| assignee.login)
.collect(),
milestone: node.milestone.map(|milestone| milestone.title),
labels: node
.labels
.nodes
.into_iter()
.map(|label| label.name)
.collect(),
blocked_by,
url: node.url,
}
})
.collect::<Vec<_>>();

let positions = issues
.iter()
.enumerate()
.map(|(index, issue)| (issue.number, index))
.collect::<HashMap<_, _>>();
for (blocker, target) in inversions {
if let Some(&index) = positions.get(&target) {
issues[index].blocked_by.push(blocker);
}
}
for issue in &mut issues {
issue.blocked_by.sort_unstable();
issue.blocked_by.dedup();
}
textref::enrich_relationships(&mut issues);

issues
}
Expand Down
Loading