diff --git a/.gitignore b/.gitignore
index b8c0c3c8b285..0c9f51c4cd66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,8 @@
taucetistation.dmb
Thumbs.db
data/
+cache/
cfg/
.vscode
**/MapFXDrawer/*.png
+test_merge.txt
diff --git a/code/__DEFINES/cache.dm b/code/__DEFINES/cache.dm
index 8d5acd9109ab..5abe88c7311e 100644
--- a/code/__DEFINES/cache.dm
+++ b/code/__DEFINES/cache.dm
@@ -1,3 +1,5 @@
+#define PERSISTENT_CACHE_FOLDER "cache/persistent"
+
#define try_access_persistent_cache(filename_key, arguments...) config.use_persistent_cache && _try_access_persistent_cache(filename_key, list(##arguments))
#define save_persistent_cache(file, filename_key, arguments...) config.use_persistent_cache && _save_persistent_cache(file, filename_key, list(##arguments))
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index 22cc320a36d7..2cc826d6c4ea 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -319,8 +319,11 @@
#define NOTIFY_ATTACK "attack"
#define NOTIFY_ORBIT "orbit"
+#define TEST_MERGE_DEFAULT_TEXT "Loading..."
+
#define TURF_DECALS_LIMIT 4 // max of /obj/effect/decal/turf_decal in one turf
+// todo: do something with this monster
#define CAN_SMOOTH_WITH_WALLS list( \
/turf/unsimulated/wall, \
/turf/simulated/wall, \
diff --git a/code/_globalvars/configuration.dm b/code/_globalvars/configuration.dm
index d214e2a4820c..d33f5148b0bf 100644
--- a/code/_globalvars/configuration.dm
+++ b/code/_globalvars/configuration.dm
@@ -3,8 +3,7 @@ var/global/datum/configuration/config = null
var/global/host = null
var/global/join_motd = null
var/global/host_announcements
-var/global/join_test_merge = null
-var/global/test_merges
+var/global/list/test_merges
var/global/station_name = "NSS Exodus"
var/global/station_name_ru = "КСН Исход"
var/global/system_name = "Tau Ceti"
diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm
index 5cb9eec85156..3e6402073f33 100644
--- a/code/controllers/configuration.dm
+++ b/code/controllers/configuration.dm
@@ -172,6 +172,7 @@ var/global/bridge_secret = null
var/ghost_interaction = 0
var/python_path = "" //Path to the python executable. Defaults to "python" on windows and "/usr/bin/env python2" on unix
+ var/github_token = "" // todo: move this to globals for security
var/use_overmap = 0
var/chat_bridge = 0
@@ -531,6 +532,9 @@ var/global/bridge_secret = null
else //probably windows, if not this should work anyway
config.python_path = "python"
+ if("github_token")
+ config.github_token = value
+
if("allow_cult_ghostwriter")
config.cult_ghostwriter = 1
@@ -630,8 +634,8 @@ var/global/bridge_secret = null
var/repo_path = replacetext(config.repository_link, "https://github.com/", "")
if(repo_path != config.repository_link)
var/split = splittext(repo_path, "/")
- github_repository_owner = split[1]
- github_repository_name = split[2]
+ config.github_repository_owner = split[1]
+ config.github_repository_name = split[2]
if("registration_panic_bunker_age")
config.registration_panic_bunker_age = value
diff --git a/code/game/verbs/ooc.dm b/code/game/verbs/ooc.dm
index 9140f1a0a4db..fc096a5a7cb7 100644
--- a/code/game/verbs/ooc.dm
+++ b/code/game/verbs/ooc.dm
@@ -209,3 +209,24 @@ var/global/bridge_ooc_colour = "#7b804f"
to_chat(src, "UI resource files resent successfully. If you are still having issues, please try manually clearing your BYOND cache.")
+/client/verb/show_test_merges()
+ set name = "Show Test Merges"
+ set desc = "Shows a list of all test merges that are currently active"
+ set category = "OOC"
+
+ if(!test_merges)
+ to_chat(src, "
No test merges are currently active
")
+ return
+
+ var/joined_text = "[EMBED_TIP("Test merged PRs", "Данные изменения временно залиты на сервер, для теста перед окончательным принятием изменений или сбора отзывов")]:
"
+ var/is_loading = FALSE
+ for(var/pr in test_merges)
+ if(test_merges[pr])
+ joined_text += "[ENTITY_TAB]#[pr]: [test_merges[pr]]
"
+ if(test_merges[pr] == TEST_MERGE_DEFAULT_TEXT)
+ is_loading = TRUE
+
+ if(is_loading)
+ joined_text += "
You can use OOC - Show Test Merges a bit later for more information about current test merges."
+
+ to_chat(src, "[joined_text]
")
diff --git a/code/game/world.dm b/code/game/world.dm
index 77eebc12bc69..fd9b558f6683 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -32,7 +32,7 @@ var/global/it_is_a_snow_day = FALSE
load_last_mode()
load_motd()
load_host_announcements()
- load_test_merge()
+ load_test_merges()
load_admins()
load_mentors()
load_supporters()
@@ -320,13 +320,47 @@ var/global/shutdown_processed = FALSE
host_announcements = "Important Admin Announcements:
[host_announcements]"
-/world/proc/load_test_merge()
- if(fexists("test_merge.txt"))
- join_test_merge = "Test merged PRs: "
- var/list/prs = splittext(trim(file2text("test_merge.txt")), " ")
- for(var/pr in prs)
- test_merges += "#[pr] "
- join_test_merge += "#[pr] "
+/world/proc/load_test_merges()
+ if(!fexists("test_merge.txt"))
+ return
+
+ test_merges = splittext(trim(file2text("test_merge.txt")), " ")
+
+ var/list/to_fetch = list()
+
+ for(var/pr in test_merges)
+ var/path = "[PERSISTENT_CACHE_FOLDER]/github/[pr]"
+ if(fexists(path))
+ test_merges[pr] = sanitize(file2text(path))
+ else
+ test_merges[pr] = TEST_MERGE_DEFAULT_TEXT
+ to_fetch += pr
+
+ if(length(to_fetch))
+ fetch_new_test_merges(to_fetch)
+
+/world/proc/fetch_new_test_merges(list/to_fetch)
+ set waitfor = FALSE
+
+ if(!to_fetch)
+ return
+
+ var/arguments = to_fetch.Join(" ")
+ if(config.github_token)
+ arguments += " -t '[config.github_token]'"
+ if(config.repository_link)
+ arguments += " -r '[config.github_repository_owner]/[config.github_repository_name]'"
+
+ var/json_content = world.ext_python("fetch_test_merges.py", arguments)
+ if(!json_content)
+ return
+
+ var/list/fetch = json_decode(json_content) // {"number": {"title": title, "success": TRUE|FALSE}}
+ for(var/pr in fetch)
+ test_merges[pr] = sanitize(fetch[pr]["title"])
+ if(fetch[pr]["success"])
+ var/path = "[PERSISTENT_CACHE_FOLDER]/github/[pr]"
+ text2file(fetch[pr]["title"], path)
/world/proc/load_regisration_panic_bunker()
if(config.registration_panic_bunker_age)
diff --git a/code/modules/mob/dead/new_player/login.dm b/code/modules/mob/dead/new_player/login.dm
index 80bbdf42be57..6405df9a5c3b 100644
--- a/code/modules/mob/dead/new_player/login.dm
+++ b/code/modules/mob/dead/new_player/login.dm
@@ -9,8 +9,8 @@
if(join_motd)
to_chat(src, "[join_motd]
")
- if(join_test_merge)
- to_chat(src, "[join_test_merge]
")
+ if(test_merges)
+ client.show_test_merges()
if(host_announcements)
to_chat(src, "[host_announcements]
")
diff --git a/code/modules/persistent_cache/persistent_cache.dm b/code/modules/persistent_cache/persistent_cache.dm
index c9728ed24ec8..a7eb3b73a851 100644
--- a/code/modules/persistent_cache/persistent_cache.dm
+++ b/code/modules/persistent_cache/persistent_cache.dm
@@ -8,8 +8,6 @@
// I = do_generation()
// save_persistent_cache(I, "filename.dmi", "icons/source.dmi", "icons/source2.dmi")
-#define PERSISTENT_CACHE_FOLDER "cache/persistent"
-
// use wrapper try_access_persistent_cache()
/proc/_try_access_persistent_cache(filename_key, list/key_files)
var/key_files_hash = key_files_hash(key_files)
@@ -53,5 +51,3 @@ var/global/list/md5_files_cache = list()
. += md5_files_cache[path]
. = md5(.) // just for short nice name
-
-#undef PERSISTENT_CACHE_FOLDER
diff --git a/code/modules/statistic/statistics.dm b/code/modules/statistic/statistics.dm
index 85ef4c9c611f..f3261b4b886e 100644
--- a/code/modules/statistic/statistics.dm
+++ b/code/modules/statistic/statistics.dm
@@ -118,7 +118,8 @@ var/global/datum/stat_collector/SSStatistics = new /datum/stat_collector
minimap_image = "nano/images/nanomap_[SSmapping.station_image]_1.png"
server_address = BYOND_SERVER_ADDRESS
base_commit_sha = global.base_commit_sha
- test_merges = global.test_merges
+ if(global.test_merges)
+ test_merges = "#" + jointext(global.test_merges, "# ")
completion_html = SSticker.mode.completition_text
save_manifest_entries()
diff --git a/config/example/config.txt b/config/example/config.txt
index a1783326911e..fb1067957768 100644
--- a/config/example/config.txt
+++ b/config/example/config.txt
@@ -162,10 +162,14 @@ ALIEN_AVAILABLE_BY_TIME DIONA 40000
## Required time to respawn after death
#DEATHTIME_REQUIRED 18000
-## Path to the python executable on the system. Leave blank for default.
+## Path to the python executable on the system. Leave blank for default.
## Default is "python" on Windows, "/usr/bin/env python2" on UNIX.
#PYTHON_PATH
+## GitHub PAT with no scopes to increase rate limits of GitHub requests.
+## Ignored when PYTHON_PATH is not set.
+#GITHUB_TOKEN
+
## Expected round length in minutes
EXPECTED_ROUND_LENGTH 90
diff --git a/interface/interface.dm b/interface/interface.dm
index 657f7237eb6a..8f3f5576f320 100644
--- a/interface/interface.dm
+++ b/interface/interface.dm
@@ -35,7 +35,7 @@
var/url_params = "[issue_template]"
if(global.round_id || config.server_name)
url_params += "Issue reported from [global.round_id ? " Round ID: [global.round_id][servername ? " ([servername])" : ""]" : servername]\n"
- url_params += "Testmerges: ```[test_merges ? test_merges : "No test merges"]```\n"
+ url_params += "Testmerges: ```[test_merges ? "#" + jointext(test_merges, "# ") : "No test merges"]```\n"
url_params += "Reporting client version: [byond_version].[byond_build]\n"
DIRECT_OUTPUT(src, link("[githuburl]/issues/new?body=[url_encode(url_params)]"))
diff --git a/scripts/fetch_test_merges.py b/scripts/fetch_test_merges.py
new file mode 100644
index 000000000000..768888be8c1c
--- /dev/null
+++ b/scripts/fetch_test_merges.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+
+# Fetches titles of test merged PRs from GitHub and saves them to cache
+# Args:
+# * PR IDs
+# * GitHub token (optional)
+# * Repository owner and name (optional e.g. 'TauCetiStation/TauCetiClassic'
+# * * If not set will try to get names from cache (in case you want to use something else to fetch and store test merges)
+# Returns:
+# * JSON-encoded object of test merges and their titles
+
+import argparse
+from dataclasses import dataclass
+import json
+import sys
+from pathlib import Path
+from concurrent.futures import ThreadPoolExecutor
+import requests
+
+
+@dataclass
+class FetchArgs:
+ pr_id: str
+ repo: str
+ headers: dict
+
+
+def read_arguments():
+ parser = argparse.ArgumentParser(description="get wrapper")
+
+ parser.add_argument("prs", nargs="+")
+
+ parser.add_argument("-t", "--token", default=None)
+
+ parser.add_argument("-r", "--repo_owner_and_name", default=None, dest="repo")
+
+ return parser.parse_args()
+
+
+def fetch_merge(args: FetchArgs):
+ try:
+ resp = requests.get(
+ f"https://api.github.com/repos/{args.repo}/pulls/{args.pr_id}",
+ timeout=30,
+ headers=args.headers,
+ )
+ if resp.status_code == 429:
+ # you look at your anonymous access and sigh
+ return args.pr_id, ("GITHUB API ERROR: RATE LIMITED", False)
+ if resp.status_code == 404:
+ # you look at your shithub and sigh
+ return args.pr_id, ("GITHUB API ERROR: PR NOT FOUND", False)
+ if resp.status_code == 401:
+ # you look at your token and sigh
+ return args.pr_id, ("GITHUB API ERROR: BAD CREDENTIALS", False)
+ if resp.status_code != 200:
+ error_msg = json.loads(resp.text)["message"]
+ print(error_msg, file=sys.stderr)
+ return args.pr_id, ("GITHUB API ERROR", False)
+ json_object = json.loads(resp.text)
+ return args.pr_id, (f'{json_object["title"]} ({json_object["user"]["login"]})', True)
+ except (requests.exceptions.RequestException, json.JSONDecodeError) as exc:
+ print(exc, file=sys.stderr)
+ return args.pr_id, ("FAILED TO GET PR TITLE", False)
+
+
+def main(options):
+ base_cache_path = Path("cache/github/pr")
+ base_cache_path.mkdir(parents=True, exist_ok=True)
+
+ test_merges = {}
+
+ to_fetch = set(options.prs)
+ headers = {
+ "Accept": "application/vnd.github+json",
+ "X-GitHub-Api-Version": "2022-11-28",
+ }
+ if options.token:
+ headers["Authorization"] = f"Bearer {options.token}"
+
+ with ThreadPoolExecutor(max_workers=10) as pool:
+ for merge_id, (title, success) in pool.map(
+ fetch_merge,
+ (FetchArgs(merge_id, options.repo, headers) for merge_id in to_fetch),
+ ):
+ test_merges[merge_id] = {'title': title, 'success': success}
+
+ sys.stdout.buffer.write(json.dumps(test_merges).encode("utf-8"))
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(read_arguments()))