Skip to main content

aptu_core/facade/
revert.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Revert facade functions for undoing aptu-authored changes.
4
5#[cfg(not(target_arch = "wasm32"))]
6use octocrab::Octocrab;
7use tracing::{debug, instrument};
8
9use crate::error::AptuError;
10#[cfg(not(target_arch = "wasm32"))]
11use crate::github::pulls::fetch_pr_details;
12
13/// Result from reverting issue comments and labels.
14#[derive(Debug, Clone)]
15pub struct RevertOutcome {
16    /// Whether this was a dry-run.
17    pub dry_run: bool,
18    /// Labels that were removed.
19    pub labels_removed: Vec<String>,
20    /// IDs of comments that were removed.
21    pub comment_ids: Vec<u64>,
22}
23
24/// Reverts all comments and labels posted by the authenticated aptu user on an issue.
25///
26/// Fetches the issue with comments, identifies comments authored by the authenticated user,
27/// and deletes them along with any labels (if not in dry-run mode).
28///
29/// # Arguments
30///
31/// * `client` - Authenticated Octocrab client
32/// * `owner` - Repository owner
33/// * `repo` - Repository name
34/// * `number` - Issue number
35/// * `dry_run` - If true, preview only without making deletions
36///
37/// # Returns
38///
39/// A `RevertOutcome` describing what was/would be removed.
40///
41/// # Errors
42///
43/// Returns an error if GitHub API calls fail or authentication fails.
44#[cfg(not(target_arch = "wasm32"))]
45#[instrument(skip(client), fields(owner = %owner, repo = %repo, number = number, dry_run))]
46pub async fn revert_issue(
47    client: &Octocrab,
48    owner: &str,
49    repo: &str,
50    number: u64,
51    dry_run: bool,
52) -> crate::Result<RevertOutcome> {
53    use crate::github::issues::{
54        delete_issue_comment, fetch_issue_with_comments, remove_issue_label,
55    };
56
57    debug!("Reverting issue comments and labels");
58
59    // Get authenticated user login
60    let authenticated_user = client
61        .current()
62        .user()
63        .await
64        .map_err(|e| AptuError::GitHub {
65            message: format!("Failed to get authenticated user: {e}"),
66        })?;
67    let auth_login = authenticated_user.login.clone();
68    debug!(auth_login = %auth_login, "Authenticated as user");
69
70    // Fetch issue with all comments
71    let issue_details = fetch_issue_with_comments(client, owner, repo, number)
72        .await
73        .map_err(|e| AptuError::GitHub {
74            message: format!("Failed to fetch issue: {e}"),
75        })?;
76
77    // Identify comments authored by the authenticated user
78    let mut comment_ids_to_delete = Vec::new();
79    for comment in &issue_details.comments {
80        if comment.author == auth_login {
81            comment_ids_to_delete.push(comment.id);
82            debug!(comment_id = comment.id, "Found aptu-authored comment");
83        }
84    }
85
86    // Collect all labels from the issue
87    let labels_to_remove: Vec<String> = issue_details.labels.clone();
88
89    if dry_run {
90        debug!(
91            comment_count = comment_ids_to_delete.len(),
92            label_count = labels_to_remove.len(),
93            "Dry-run mode: no deletions will be performed"
94        );
95        return Ok(RevertOutcome {
96            dry_run: true,
97            labels_removed: labels_to_remove,
98            comment_ids: comment_ids_to_delete,
99        });
100    }
101
102    // Delete comments
103    for comment_id in &comment_ids_to_delete {
104        if let Err(e) = delete_issue_comment(client, owner, repo, *comment_id).await {
105            return Err(AptuError::GitHub {
106                message: format!("Failed to delete comment #{comment_id}: {e}"),
107            });
108        }
109    }
110    debug!(count = comment_ids_to_delete.len(), "Comments deleted");
111
112    // Remove labels
113    for label in &labels_to_remove {
114        if let Err(e) = remove_issue_label(client, owner, repo, number, label).await {
115            return Err(AptuError::GitHub {
116                message: format!("Failed to remove label '{label}': {e}"),
117            });
118        }
119    }
120    debug!(count = labels_to_remove.len(), "Labels removed");
121
122    Ok(RevertOutcome {
123        dry_run: false,
124        labels_removed: labels_to_remove,
125        comment_ids: comment_ids_to_delete,
126    })
127}
128
129#[cfg(target_arch = "wasm32")]
130pub async fn revert_issue(
131    _owner: &str,
132    _repo: &str,
133    _number: u64,
134    _dry_run: bool,
135) -> crate::Result<()> {
136    Err(AptuError::GitHub {
137        message: "revert_issue is not supported on wasm32-unknown-unknown".into(),
138    })
139}
140
141/// Reverts all comments and labels posted by the authenticated aptu user on a PR.
142///
143/// Fetches the PR with review comments, identifies comments authored by the authenticated user,
144/// and deletes them along with any labels (if not in dry-run mode).
145///
146/// # Arguments
147///
148/// * `client` - Authenticated Octocrab client
149/// * `owner` - Repository owner
150/// * `repo` - Repository name
151/// * `number` - PR number
152/// * `dry_run` - If true, preview only without making deletions
153///
154/// # Returns
155///
156/// A `RevertOutcome` describing what was/would be removed.
157///
158/// # Errors
159///
160/// Returns an error if GitHub API calls fail or authentication fails.
161#[cfg(not(target_arch = "wasm32"))]
162#[instrument(skip(client), fields(owner = %owner, repo = %repo, number = number, dry_run))]
163pub async fn revert_pr(
164    client: &Octocrab,
165    owner: &str,
166    repo: &str,
167    number: u64,
168    dry_run: bool,
169) -> crate::Result<RevertOutcome> {
170    use crate::github::issues::remove_issue_label;
171    use crate::github::pulls::delete_pr_review_comment;
172
173    debug!("Reverting PR comments and labels");
174
175    // Get authenticated user login
176    let authenticated_user = client
177        .current()
178        .user()
179        .await
180        .map_err(|e| AptuError::GitHub {
181            message: format!("Failed to get authenticated user: {e}"),
182        })?;
183    let auth_login = authenticated_user.login.clone();
184    debug!(auth_login = %auth_login, "Authenticated as user");
185
186    // Fetch PR details with review comments
187    let pr_details = fetch_pr_details(
188        client,
189        owner,
190        repo,
191        number,
192        &crate::config::ReviewConfig::default(),
193    )
194    .await
195    .map_err(|e| AptuError::GitHub {
196        message: format!("Failed to fetch PR: {e}"),
197    })?;
198
199    // Identify review comments authored by the authenticated user
200    let mut comment_ids_to_delete = Vec::new();
201    for comment in &pr_details.review_comments {
202        if comment.author == auth_login {
203            comment_ids_to_delete.push(comment.id);
204            debug!(
205                comment_id = comment.id,
206                "Found aptu-authored review comment"
207            );
208        }
209    }
210
211    // Collect all labels from the PR
212    let labels_to_remove: Vec<String> = pr_details.labels.clone();
213
214    if dry_run {
215        debug!(
216            comment_count = comment_ids_to_delete.len(),
217            label_count = labels_to_remove.len(),
218            "Dry-run mode: no deletions will be performed"
219        );
220        return Ok(RevertOutcome {
221            dry_run: true,
222            labels_removed: labels_to_remove,
223            comment_ids: comment_ids_to_delete,
224        });
225    }
226
227    // Delete review comments
228    for comment_id in &comment_ids_to_delete {
229        if let Err(e) = delete_pr_review_comment(client, owner, repo, *comment_id).await {
230            return Err(AptuError::GitHub {
231                message: format!("Failed to delete PR review comment #{comment_id}: {e}"),
232            });
233        }
234    }
235    debug!(
236        count = comment_ids_to_delete.len(),
237        "PR review comments deleted"
238    );
239
240    // Remove labels
241    for label in &labels_to_remove {
242        if let Err(e) = remove_issue_label(client, owner, repo, number, label).await {
243            return Err(AptuError::GitHub {
244                message: format!("Failed to remove label '{label}': {e}"),
245            });
246        }
247    }
248    debug!(count = labels_to_remove.len(), "Labels removed from PR");
249
250    Ok(RevertOutcome {
251        dry_run: false,
252        labels_removed: labels_to_remove,
253        comment_ids: comment_ids_to_delete,
254    })
255}
256
257#[cfg(target_arch = "wasm32")]
258pub async fn revert_pr(
259    _owner: &str,
260    _repo: &str,
261    _number: u64,
262    _dry_run: bool,
263) -> crate::Result<()> {
264    Err(AptuError::GitHub {
265        message: "revert_pr is not supported on wasm32-unknown-unknown".into(),
266    })
267}