1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
use backtrace::Backtrace;
use colored::*;
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use serde::{Deserialize, Serialize};
use url::{form_urlencoded, Url};

pub fn link_to_issue_page() -> String {
    "https://gitlab.com/df_storyteller/df-storyteller/issues".to_owned()
}

pub fn link_to_issue_nr(issue_nr: u32) -> String {
    format!(
        "https://gitlab.com/df_storyteller/df-storyteller/issues/{}",
        issue_nr
    )
}

pub fn link_to_new_issue() -> String {
    "https://gitlab.com/df_storyteller/df-storyteller/issues/new".to_owned()
}

pub fn link_to_issue_template(template_name: &str) -> String {
    format!(
        "https://gitlab.com/df_storyteller/df-storyteller/issues/new?issuable_template={}",
        template_name
    )
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GitIssue<T> {
    pub title: String,
    pub message: String,
    pub labels: Vec<String>,
    pub debug_info_string: Option<String>,
    pub debug_info_json: Option<T>,
    pub add_steps: bool,
    pub ask_add_files: bool,
    pub include_backtrace: bool,
}

impl<T: Serialize + Clone> GitIssue<T> {
    pub fn new() -> Self {
        Self::default()
    }

    fn get_labels(&self) -> String {
        let mut labels_string = "/label ~\"Auto Generated Issue\"".to_owned();
        for label in &self.labels {
            labels_string = format!("{} ~\"{}\"", labels_string, label);
        }
        labels_string
    }

    fn get_debug_message(&self) -> String {
        let mut message = String::new();
        if let Some(debug_info) = &self.debug_info_string {
            message = debug_info.clone();
        }
        if let Some(debug_info) = &self.debug_info_json {
            let newline = if message.is_empty() {
                "".to_owned()
            } else {
                "\n".to_owned()
            };
            message = format!(
                "{}{}```json\n{}\n```",
                message,
                newline,
                serde_json::to_string_pretty(&debug_info).unwrap(),
            );
        }
        if message.is_empty() {
            message = "Please describe what happened and what you wanted to do.".to_owned();
        }
        message
    }

    pub fn create_message(&self) -> String {
        let mut url = self.new_issue_link();
        let mut url_string = url.as_str();
        // Add some margin
        let prefix_len = "https://gitlab.com".len() - 3;
        // Note: URLs longer then 2048 characters (not including the "https://gitlab.com").
        // Might/will not work on some browsers (Firefox and Gitlab itself gave errors.).
        // If this happens disable some parts of the issue te make it shorter.
        // For more info see: https://stackoverflow.com/a/417184/2037998
        let mut self_clone = (*self).clone();
        if url_string.len() > 2048 + prefix_len {
            warn!("URL shortened because of URL Limit.");
            if self_clone.add_steps {
                warn!("URL shortened: Please add Steps to get issue back in.");
            }
            self_clone.add_steps = false;
            if self_clone.ask_add_files {
                warn!("URL shortened: Please add a link to a zip with your legend files to the issue.");
            }
            self_clone.ask_add_files = false;
            url = self_clone.new_issue_link();
            url_string = url.as_str();
        }
        // Still to big?
        if url_string.len() > 2048 + prefix_len {
            warn!("URL shortened, removing backtrace.");
            self_clone.include_backtrace = false;
            url = self_clone.new_issue_link();
            url_string = url.as_str();
        }
        // This is really long...
        if url_string.len() > 2048 + prefix_len {
            warn!("URL shortened, removing debug info.");
            if self_clone.debug_info_json.is_some() {
                warn!("URL shortened: Please include warnings on screen to the issue.");
            }
            self_clone.debug_info_json = None;
            url = self_clone.new_issue_link();
            url_string = url.as_str();
        }

        let message = format!(
            "------------Report this issue------------\n  \
            Please report this issue. It take only a min. (GitLab account required)\n  \
            (Copy this link, CTRL+Click or Right Click the link to open)\n  \
            Check if already reported: {}\n  \
            You can review/add/remove data before submitting after opening the link.\n\
            Link🔗: {}\n\
            ------------------------------------------\n",
            self.search_if_issue_exists().dimmed().bright_cyan(),
            url_string.dimmed().bright_cyan()
        );

        message
    }

    fn search_if_issue_exists(&self) -> String {
        let encoded: String = form_urlencoded::Serializer::new(String::new())
            .append_pair("search", &self.title)
            .finish();
        format!(
            "https://gitlab.com/df_storyteller/df-storyteller/issues?scope=all&state=all&{}",
            encoded
        )
    }

    fn new_issue_link(&self) -> Url {
        let add_files = if self.ask_add_files {
            "<!-- Please include the legends files, you can upload a '.zip' archive somewhere.\n\
            For example Google Drive, Microsoft OneDrive, DropBox, pCloud, ... -->\n\
            * Link to legends: ..add link..\n\
            * DF Version: \n\
            * DFHack Version: \n\n"
        } else {
            ""
        };
        let reproduce = if self.add_steps {
            "## Reproduce:\n\
            Steps to recreate this issue:\n\
            1. ...\n\
            2. ...\n\n"
        } else {
            ""
        };
        let backtrace = if self.include_backtrace {
            format!(
                "### Backtrace:\n\
            <details><summary markdown=\"span\">Backtrace</summary>\n\n\
            ```\n\
            {}\n\
            ```\n\n\
            </details>\n\n",
                print_backtrace()
            )
        } else {
            "".to_owned()
        };
        let description = format!(
            "<!-- Please check above if there are issues with the same title.\n\
            Someone else might have already reported this.-->\n\
            ## Summary:\n\
            {message}\n\n\
            {add_files}\
            {reproduce}\
            ## Debug info:\n\
            {debug_message}\n\n\
            {backtrace}\
            ### System:\n\
            * DF Storyteller version: {version}\n\
            * System architecture: {arch}\n\
            * System OS: {os}\n\
            * Database: SQLite/Postgres\n\n\
            <!-- Leave the information below untouched! -->\n\
            {labels}",
            message = self.message,
            reproduce = reproduce,
            add_files = add_files,
            backtrace = backtrace,
            labels = self.get_labels(),
            debug_message = self.get_debug_message(),
            version = env!("CARGO_PKG_VERSION"),
            arch = std::env::consts::ARCH,
            os = std::env::consts::OS,
        );

        let encoded: String = form_urlencoded::Serializer::new(String::new())
            .append_pair("issue[title]", &self.title)
            .append_pair("issue[description]", &description)
            .finish();

        Url::parse(&format!(
            "https://gitlab.com/df_storyteller/df-storyteller/issues/new?{}",
            encoded
        ))
        .unwrap()
    }
}

impl<T: Serialize> Default for GitIssue<T> {
    fn default() -> Self {
        Self {
            title: String::new(),
            message: String::new(),
            labels: Vec::new(),
            debug_info_string: None,
            debug_info_json: None,
            add_steps: true,
            ask_add_files: false,
            include_backtrace: false,
        }
    }
}

/// Displays a shortened backtrace to see where the call came from.
pub fn print_backtrace() -> String {
    let bt = Backtrace::new();
    let frames = bt.frames();
    let mut backtrace_string = String::new();
    let max_line = 11;
    for (frame, line) in frames.iter().zip(0..max_line) {
        let symbol = frame.symbols().get(0).unwrap();
        let mut name = String::new();
        if let Some(name_value) = &symbol.name() {
            let full_name = name_value.to_string();
            let parts: Vec<&str> = full_name.split("::").collect();
            if parts.len() >= 3 {
                name = format!(
                    "{}::{}",
                    parts.get(parts.len() - 3).unwrap_or(&""),
                    parts.get(parts.len() - 2).unwrap_or(&""),
                );
            }
        }
        // Will not be set on some systems.
        // see: https://docs.rs/backtrace/latest/backtrace/struct.Symbol.html
        let mut filepath = String::new();
        if let Some(filename) = &symbol.filename() {
            let line_nr = &symbol.lineno().unwrap_or_default();
            let path = filename.to_str().unwrap();
            let (_, path) = path.split_at(path.find("df_st").unwrap_or(0));
            if !path.starts_with("/rustc/") {
                filepath = format!(" => {}:{}", path, line_nr);
            }
        }
        if line != 0 {
            backtrace_string = format!("{}{}: {}{}\n", backtrace_string, line, name, filepath);
        }
    }
    backtrace_string
}