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();
let prefix_len = "https://gitlab.com".len() - 3;
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();
}
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();
}
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,
}
}
}
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(&""),
);
}
}
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
}