Initial commit.
This commit is contained in:
commit
5ad403bf33
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
build
|
||||||
|
|
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[submodule "external/inja"]
|
||||||
|
path = external/inja
|
||||||
|
url = https://github.com/pantor/inja.git
|
||||||
|
[submodule "external/json"]
|
||||||
|
path = external/json
|
||||||
|
url = https://github.com/nlohmann/json.git
|
16
CMakeLists.txt
Normal file
16
CMakeLists.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(sinja)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -march=native -flto")
|
||||||
|
|
||||||
|
add_executable(sinja sinja.cpp) # Assuming your file is sinja.cpp
|
||||||
|
|
||||||
|
# Add this line to tell the compiler where to find Inja and JSON headers
|
||||||
|
target_include_directories(sinja PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/external")
|
||||||
|
|
||||||
|
target_link_libraries(sinja PRIVATE pthread)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
27
README.md
Normal file
27
README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Sinja: A High-Performance JSON Templating ServerSinja is a blazing-fast, stable, and production-grade HTTP server written in C++. Its sole purpose is to render templates using JSON data with maximum performance and concurrency. It is built on a modern, multi-threaded architecture designed to saturate CPU cores and handle thousands of simultaneous requests with predictable, low latency.This server is not a web framework; it is a specialized tool designed to be a highly efficient microservice in a larger system.Core FeaturesMassively Concurrent: Built on a SO_REUSEPORT architecture that allows multiple threads to accept connections in parallel, eliminating common server bottlenecks.Blazing Fast: Written in modern C++ and leverages the high-performance Inja templating library for efficient rendering.Stable & Robust: Designed for production workloads with graceful shutdown, robust error handling, and a focus on reliability over premature optimization.Simple, Focused API: A single POST /render endpoint that accepts a JSON payload, making it easy to integrate with any language or service.Modern C++: Uses C++17 for clean, efficient, and maintainable code.Performance ProfileBenchmarks against the final, stable version of the server (sinja/5.0-stable) demonstrate its key characteristics. Under a high-concurrency load, the server achieves:Zero I/O or HTTP Errors: 100% of requests are handled successfully without dropping connections.Consistent Low Latency: Mean and median client-observed latency are nearly identical (~10ms), indicating a lack of "long-tail" delays and highly predictable performance.High Throughput: Capable of handling a high volume of requests per second, limited primarily by the complexity of the templates being rendered.Efficient CPU-Bound Work: Server-side render times are consistently low (~9ms for the benchmark template), showcasing the efficiency of the Inja library and the C++ implementation.The server's strength lies in its ability to maintain this performance profile across thousands of concurrent connections, a scenario where single-threaded or GIL-bound application servers would falter.Architecture: The SO_REUSEPORT ModelThe core of Sinja's performance comes from its networking architecture. Unlike traditional servers that use a single main thread to accept() all connections and dispatch them to a worker queue (a classic bottleneck), Sinja uses the SO_REUSEPORT socket option.This allows every single worker thread to create its own listening socket on the exact same port. The Linux kernel then becomes the load balancer, distributing incoming connections directly and efficiently across the worker threads.Benefits of this Architecture:True Parallelism: Each thread runs an independent accept loop. There is no contention for a shared queue.No Head-of-Line Blocking: A slow or complex request being handled by Thread 1 has zero impact on Thread 2, which can continue to accept and process new requests at full speed.Perfect Scalability: The server's capacity scales linearly with the number of available CPU cores.Resilience: It is inherently more resilient to connection bursts, as multiple threads are available to immediately handle the load.API UsageThe server exposes a single, simple endpoint.Endpoint: POST /renderContent-Type Header: Must be application/json.Body: A JSON object with two keys:template (string): The path to the template file, relative to the --templates directory.context (object): A JSON object containing the data to be used for rendering.Example with curlcurl -X POST http://localhost:8080/render \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data '{
|
||||||
|
"template": "welcome_email.txt",
|
||||||
|
"context": {
|
||||||
|
"username": "Alex",
|
||||||
|
"items": [
|
||||||
|
{"name": "Apples", "price": 1.5},
|
||||||
|
{"name": "Oranges", "price": 2.0}
|
||||||
|
],
|
||||||
|
"is_premium": true
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
Building and RunningDependenciesA C++17 compliant compiler (GCC 8+, Clang 6+)CMake (3.10+)Inja & nlohmann/json (handled via Git submodules or manual download)Build Instructions# 1. Clone the repository and submodules
|
||||||
|
git clone <repository_url>
|
||||||
|
cd sinja
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
|
# 2. Build the project
|
||||||
|
mkdir build && cd build
|
||||||
|
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||||
|
make -j$(nproc)
|
||||||
|
|
||||||
|
# 3. The executable will be in the build directory
|
||||||
|
./sinja --help
|
||||||
|
Running the Server./sinja --templates /path/to/your/templates --threads 8 --port 8080
|
||||||
|
Command-line arguments:--templates, -t: (Required) The directory where your template files are stored.--address, -a: The IP address to bind to (default: 0.0.0.0).--port, -p: The port to listen on (default: 8080).--threads, -w: The number of worker threads to spawn (default: hardware concurrency).Critical Advice: When to Use SinjaSinja is a specialized tool, not a general-purpose solution.Ideal Use Cases:High-Traffic Email/Notification Service: Rendering thousands of unique emails or push notifications per minute from a user database.Dynamic HTML Snippet Generation: Acting as a microservice for a front-end framework, rendering server-side components that are too complex for client-side logic.Report Generation: Quickly creating text-based reports (CSV, XML, formatted text) from large JSON data sources.Offloading CPU Work: Augmenting an application server written in a GIL-bound language (like Python or Ruby). The application server can handle I/O, while Sinja handles the heavy, CPU-bound template rendering.When NOT to Use Sinja:As a Full Web Framework: It has no routing, no database layer, no session management, and no authentication.For Simple, Low-Traffic Sites: The complexity of a C++ service is overkill if a simple PHP or Node.js script would suffice.If Your Bottleneck is I/O: If your service spends most of its time waiting for a slow database, making the rendering part faster with Sinja will have minimal impact.Design RationaleWhy C++? For direct memory control, system-level API access (socket, epoll), and the ability to achieve true, lock-free parallelism without a Global Interpreter Lock.Why Inja? It's a modern, header-only C++ library with a simple API, excellent performance, and native support for nlohmann::json, the de-facto standard for JSON in C++. Its internal caching of parsed templates is highly efficient.Why was the external render cache removed? During development, a complex LRU cache for rendered output was implemented. However, rigorous benchmarking revealed that it made the server less stable and slower on cache misses. The stability, predictability, and simplicity of the core SO_REUSEPORT architecture with Inja's built-in template cache proved to be the superior production-ready solution. This reflects a philosophy of prioritizing robustness over complex, marginal optimizations.
|
193
bench_render.py
Normal file
193
bench_render.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import statistics
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
DEFAULT_URL = "http://127.0.0.1:8083/render"
|
||||||
|
DEFAULT_BODY = {
|
||||||
|
"template": "hello.txt",
|
||||||
|
"context": {
|
||||||
|
"name": "Retoor",
|
||||||
|
"stats": {"notifications": 3},
|
||||||
|
"repos": [{"name": "snek"}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def percentile(values, p):
|
||||||
|
if not values:
|
||||||
|
return float("nan")
|
||||||
|
s = sorted(values)
|
||||||
|
k = (len(s) - 1) * (p / 100.0)
|
||||||
|
f = int(k)
|
||||||
|
c = min(f + 1, len(s) - 1)
|
||||||
|
if f == c:
|
||||||
|
return s[f]
|
||||||
|
d0 = s[f] * (c - k)
|
||||||
|
d1 = s[c] * (k - f)
|
||||||
|
return d0 + d1
|
||||||
|
|
||||||
|
async def worker(name, session, url, payload_bytes, headers, queue, results, failures):
|
||||||
|
while True:
|
||||||
|
idx = await queue.get()
|
||||||
|
if idx is None:
|
||||||
|
queue.task_done()
|
||||||
|
return
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
try:
|
||||||
|
headers['Content-Length'] = str(len(payload_bytes))
|
||||||
|
async with session.post(url, data=payload_bytes, headers=headers) as resp:
|
||||||
|
body = await resp.read()
|
||||||
|
dt = time.perf_counter() - t0
|
||||||
|
rt_us = resp.headers.get("X-Render-Time-Us")
|
||||||
|
results.append((
|
||||||
|
dt,
|
||||||
|
resp.status,
|
||||||
|
len(body),
|
||||||
|
int(rt_us) if rt_us and rt_us.isdigit() else None
|
||||||
|
))
|
||||||
|
except Exception as e:
|
||||||
|
failures.append(repr(e))
|
||||||
|
finally:
|
||||||
|
queue.task_done()
|
||||||
|
|
||||||
|
async def run_fixed_requests(url, total, concurrency, timeout, body):
|
||||||
|
payload_bytes = json.dumps(body, separators=(",", ":")).encode("utf-8")
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
conn = aiohttp.TCPConnector(limit=concurrency, limit_per_host=concurrency, ttl_dns_cache=300)
|
||||||
|
to = aiohttp.ClientTimeout(total=timeout)
|
||||||
|
results = []
|
||||||
|
failures = []
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(connector=conn, timeout=to) as session:
|
||||||
|
# enqueue all requests
|
||||||
|
for i in range(total):
|
||||||
|
queue.put_nowait(i)
|
||||||
|
# sentinels to stop workers
|
||||||
|
for _ in range(concurrency):
|
||||||
|
queue.put_nowait(None)
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
asyncio.create_task(worker(f"w{i}", session, url, payload_bytes, headers, queue, results, failures))
|
||||||
|
for i in range(concurrency)
|
||||||
|
]
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
await queue.join()
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
for t in tasks:
|
||||||
|
await t
|
||||||
|
|
||||||
|
return results, failures, t1 - t0
|
||||||
|
|
||||||
|
async def run_duration(url, seconds, concurrency, timeout, body):
|
||||||
|
payload_bytes = json.dumps(body, separators=(",", ":")).encode("utf-8")
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
conn = aiohttp.TCPConnector(limit=concurrency, limit_per_host=concurrency, ttl_dns_cache=300)
|
||||||
|
to = aiohttp.ClientTimeout(total=timeout)
|
||||||
|
results = []
|
||||||
|
failures = []
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(connector=conn, timeout=to) as session:
|
||||||
|
async def feeder():
|
||||||
|
end = time.perf_counter() + seconds
|
||||||
|
i = 0
|
||||||
|
while time.perf_counter() < end:
|
||||||
|
queue.put_nowait(i)
|
||||||
|
i += 1
|
||||||
|
# yield control
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
for _ in range(concurrency):
|
||||||
|
queue.put_nowait(None)
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
asyncio.create_task(worker(f"w{i}", session, url, payload_bytes, headers, queue, results, failures))
|
||||||
|
for i in range(concurrency)
|
||||||
|
]
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
await feeder()
|
||||||
|
await queue.join()
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
for t in tasks:
|
||||||
|
await t
|
||||||
|
|
||||||
|
return results, failures, t1 - t0
|
||||||
|
|
||||||
|
def summarize(results, failures, wall):
|
||||||
|
lat = [dt for (dt, status, _, _) in results if status == 200]
|
||||||
|
codes = Counter(status for (_, status, _, _) in results)
|
||||||
|
bytes_total = sum(sz for (_, _, sz, _) in results)
|
||||||
|
rt_us_vals = [rt for (_, status, _, rt) in results if status == 200 and rt is not None]
|
||||||
|
|
||||||
|
ok = sum(1 for (_, status, _, _) in results if status == 200)
|
||||||
|
tot = ok + len([1 for (_, status, _, _) in results if status != 200]) + len(failures)
|
||||||
|
rps = (len(results) + len(failures)) / wall if wall > 0 else 0.0
|
||||||
|
|
||||||
|
print("\n=== Benchmark Summary ===")
|
||||||
|
print(f"Total time : {wall:.3f}s")
|
||||||
|
print(f"Requests sent : {tot}")
|
||||||
|
print(f" 2xx OK : {ok}")
|
||||||
|
print(f" Errors (HTTP) : {sum(v for c,v in codes.items() if c != 200)}")
|
||||||
|
print(f" Failures (I/O) : {len(failures)}")
|
||||||
|
print(f"Throughput : {rps:.1f} req/s")
|
||||||
|
print(f"Transferred : {bytes_total/1024:.1f} KiB")
|
||||||
|
if lat:
|
||||||
|
print("\nLatency (client observed):")
|
||||||
|
print(f" mean : {statistics.mean(lat)*1000:.3f} ms")
|
||||||
|
print(f" median : {statistics.median(lat)*1000:.3f} ms")
|
||||||
|
print(f" p95 : {percentile(lat,95)*1000:.3f} ms")
|
||||||
|
print(f" p99 : {percentile(lat,99)*1000:.3f} ms")
|
||||||
|
if rt_us_vals:
|
||||||
|
print("\nServer render time (X-Render-Time-Us header):")
|
||||||
|
print(f" mean : {statistics.mean(rt_us_vals):.0f} µs")
|
||||||
|
print(f" median : {statistics.median(rt_us_vals):.0f} µs")
|
||||||
|
print(f" p95 : {percentile(rt_us_vals,95):.0f} µs")
|
||||||
|
print(f" p99 : {percentile(rt_us_vals,99):.0f} µs")
|
||||||
|
|
||||||
|
if codes:
|
||||||
|
print("\nHTTP status codes:")
|
||||||
|
for code, cnt in sorted(codes.items()):
|
||||||
|
print(f" {code}: {cnt}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="aiohttp benchmark for /render")
|
||||||
|
ap.add_argument("--url", default=DEFAULT_URL, help="Target URL")
|
||||||
|
ap.add_argument("-c", "--concurrency", type=int, default=32, help="Concurrent workers")
|
||||||
|
group = ap.add_mutually_exclusive_group()
|
||||||
|
group.add_argument("-n", "--requests", type=int, default=1000, help="Total requests (fixed)")
|
||||||
|
group.add_argument("-d", "--duration", type=float, help="Duration in seconds (open loop)")
|
||||||
|
ap.add_argument("-t", "--timeout", type=float, default=10.0, help="Total request timeout (seconds)")
|
||||||
|
ap.add_argument("--print-one", action="store_true", help="Print one sample response body for sanity")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
body = DEFAULT_BODY
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.duration:
|
||||||
|
results, failures, wall = asyncio.run(run_duration(args.url, args.duration, args.concurrency, args.timeout, body))
|
||||||
|
else:
|
||||||
|
results, failures, wall = asyncio.run(run_fixed_requests(args.url, args.requests, args.concurrency, args.timeout, body))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nInterrupted.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Optional: print one successful response body (sanity)
|
||||||
|
if args.print_one:
|
||||||
|
# do one extra request
|
||||||
|
async def fetch_one():
|
||||||
|
to = aiohttp.ClientTimeout(total=args.timeout)
|
||||||
|
async with aiohttp.ClientSession(timeout=to) as s:
|
||||||
|
async with s.post(args.url, json=body) as r:
|
||||||
|
print("\n--- Sample response body ---")
|
||||||
|
print(await r.text())
|
||||||
|
asyncio.run(fetch_one())
|
||||||
|
|
||||||
|
summarize(results, failures, wall)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
10
build.sh
Executable file
10
build.sh
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#sudo apt-get update
|
||||||
|
#sudo apt-get install -y build-essential cmake
|
||||||
|
|
||||||
|
mkdir -p build && cd build
|
||||||
|
cmake -DCMAKE_BUILD_TYPE=Release ..
|
||||||
|
cmake --build . -j
|
||||||
|
ulimit -n 65536
|
||||||
|
# $(nproc)
|
||||||
|
./sinja --templates /home/retoor/projects/sinja/templates --address 0.0.0.0 --port 8083 --threads 1
|
||||||
|
|
1
external/inja
vendored
Submodule
1
external/inja
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 2d6b382e2a239f4cbac2d3108e72d486302ee80c
|
1
external/json
vendored
Submodule
1
external/json
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 867127c2f49ba238c6f6345c4b3cff72fdd96aec
|
624
main.cpp.mini
Normal file
624
main.cpp.mini
Normal file
@ -0,0 +1,624 @@
|
|||||||
|
// sinja: ultra-fast JSON templating REST server
|
||||||
|
// Only dependency: nlohmann::json
|
||||||
|
// Build: see CMakeLists.txt
|
||||||
|
// Run : ./sinja --templates /path/to/templates --address 0.0.0.0 --port 8083 --threads $(nproc)
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cassert>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <csignal>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
// ---- Tunables ----------------------------------------------------------------
|
||||||
|
static constexpr const char* kServerName = "sinja/1.0";
|
||||||
|
static constexpr size_t kMaxBodyBytes = 8 * 1024 * 1024; // 8 MiB
|
||||||
|
static constexpr size_t kMaxHeaderBytes = 64 * 1024; // 64 KiB
|
||||||
|
static constexpr int kRecvTimeoutSec = 5; // per recv()
|
||||||
|
static constexpr int kSendTimeoutSec = 5;
|
||||||
|
static constexpr int kBacklog = 1024;
|
||||||
|
|
||||||
|
// ---- Config ------------------------------------------------------------------
|
||||||
|
struct Settings {
|
||||||
|
std::string address = "0.0.0.0";
|
||||||
|
int port = 8080;
|
||||||
|
size_t threads = std::thread::hardware_concurrency() ? std::thread::hardware_concurrency() : 4;
|
||||||
|
fs::path template_root;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Small utils -------------------------------------------------------------
|
||||||
|
static inline std::string ltrim(std::string s){
|
||||||
|
size_t i=0; while(i<s.size() && std::isspace((unsigned char)s[i])) ++i; return s.substr(i);
|
||||||
|
}
|
||||||
|
static inline std::string rtrim(std::string s){
|
||||||
|
if(s.empty()) return s;
|
||||||
|
size_t i=s.size()-1;
|
||||||
|
while (i<s.size() && std::isspace((unsigned char)s[i])) { if(i==0) return ""; --i; }
|
||||||
|
return s.substr(0, i+1);
|
||||||
|
}
|
||||||
|
static inline std::string trim(std::string s){ return rtrim(ltrim(std::move(s))); }
|
||||||
|
static inline bool starts_with(const std::string& s, const std::string& p){
|
||||||
|
return s.size()>=p.size() && std::equal(p.begin(), p.end(), s.begin());
|
||||||
|
}
|
||||||
|
static std::string to_lower(std::string s){
|
||||||
|
for (auto& c: s) c = (char)std::tolower((unsigned char)c);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Secure template resolution ----------------------------------------------
|
||||||
|
static fs::path secure_resolve_template(const fs::path& root, const std::string& req, std::string& reason) {
|
||||||
|
if (req.empty()) { reason = "empty template name"; return {}; }
|
||||||
|
fs::path rel(req);
|
||||||
|
if (rel.is_absolute()) { reason = "absolute path not allowed"; return {}; }
|
||||||
|
fs::path cand = fs::weakly_canonical(root / rel);
|
||||||
|
fs::path canon_root = fs::weakly_canonical(root);
|
||||||
|
std::string c1 = cand.string(), c0 = canon_root.string();
|
||||||
|
if (!starts_with(c1, c0)) { reason = "path traversal rejected"; return {}; }
|
||||||
|
if (!fs::exists(cand) || !fs::is_regular_file(cand)) { reason = "template file not found"; return {}; }
|
||||||
|
return cand;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Mini Inja (subset): {{ a.b[0].c }} and trims {{- ... -}} ----------------
|
||||||
|
struct MiniInja {
|
||||||
|
enum class TokType { Text, Var };
|
||||||
|
struct Tok {
|
||||||
|
TokType type;
|
||||||
|
std::string payload; // Var: expression; Text: literal
|
||||||
|
bool trim_left = false;
|
||||||
|
bool trim_right = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::vector<Tok> compile(const std::string& src) {
|
||||||
|
std::vector<Tok> out;
|
||||||
|
size_t i = 0, n = src.size();
|
||||||
|
while (i < n) {
|
||||||
|
size_t open = src.find("{{", i);
|
||||||
|
if (open == std::string::npos) {
|
||||||
|
out.push_back({TokType::Text, src.substr(i), false, false});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (open > i)
|
||||||
|
out.push_back({TokType::Text, src.substr(i, open - i), false, false});
|
||||||
|
|
||||||
|
bool trimL = (open + 2 < n && src[open+2] == '-');
|
||||||
|
size_t expr_start = open + 2 + (trimL ? 1 : 0);
|
||||||
|
size_t close = src.find("}}", expr_start);
|
||||||
|
if (close == std::string::npos) throw std::runtime_error("Unclosed {{ tag");
|
||||||
|
bool trimR = (close>0 && src[close-1]=='-');
|
||||||
|
size_t expr_end = close - (trimR ? 1 : 0);
|
||||||
|
|
||||||
|
std::string expr = trim(src.substr(expr_start, expr_end - expr_start));
|
||||||
|
out.push_back({TokType::Var, expr, trimL, trimR});
|
||||||
|
i = close + 2;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const json* resolve_ptr(const json& ctx, const std::string& path) {
|
||||||
|
// supports a.b.c[2].d
|
||||||
|
const json* cur = &ctx;
|
||||||
|
size_t i = 0, n = path.size();
|
||||||
|
auto error = [&]{ throw std::runtime_error("Missing key in path: " + path); };
|
||||||
|
while (i < n) {
|
||||||
|
if (path[i] == '[') { // index starts
|
||||||
|
size_t r = path.find(']', i+1); if (r == std::string::npos) error();
|
||||||
|
int idx = std::stoi(path.substr(i+1, r-i-1));
|
||||||
|
if (!cur->is_array() || idx < 0 || (size_t)idx >= cur->size()) error();
|
||||||
|
cur = &(*cur)[(size_t)idx];
|
||||||
|
i = r + 1; if (i<n && path[i]=='.') ++i; continue;
|
||||||
|
}
|
||||||
|
size_t dot = path.find_first_of(".[", i);
|
||||||
|
std::string key = (dot==std::string::npos) ? path.substr(i) : path.substr(i, dot-i);
|
||||||
|
if (!cur->contains(key)) error();
|
||||||
|
cur = &(*cur)[key];
|
||||||
|
i = (dot==std::string::npos)? n : dot;
|
||||||
|
while (i<n && path[i]=='[') {
|
||||||
|
size_t r = path.find(']', i+1); if (r==std::string::npos) error();
|
||||||
|
int idx = std::stoi(path.substr(i+1, r-i-1));
|
||||||
|
if (!cur->is_array() || idx < 0 || (size_t)idx >= cur->size()) error();
|
||||||
|
cur = &(*cur)[(size_t)idx];
|
||||||
|
i = r + 1;
|
||||||
|
}
|
||||||
|
if (i<n && path[i]=='.') ++i;
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string val_to_string(const json& v) {
|
||||||
|
if (v.is_string()) return v.get<std::string>();
|
||||||
|
if (v.is_boolean()) return v.get<bool>() ? "true" : "false";
|
||||||
|
if (v.is_number_integer()) return std::to_string(v.get<long long>());
|
||||||
|
if (v.is_number_unsigned()) return std::to_string(v.get<unsigned long long>());
|
||||||
|
if (v.is_number_float()) return std::to_string(v.get<double>());
|
||||||
|
return v.dump(); // arrays/objects
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string render(const std::vector<Tok>& prog, const json& ctx) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(1024);
|
||||||
|
for (size_t k = 0; k < prog.size(); ++k) {
|
||||||
|
const auto& t = prog[k];
|
||||||
|
if (t.type == TokType::Text) {
|
||||||
|
out.append(t.payload);
|
||||||
|
} else {
|
||||||
|
if (t.trim_left) {
|
||||||
|
while (!out.empty() && (out.back()==' '||out.back()=='\t'||out.back()=='\r'||out.back()=='\n'))
|
||||||
|
out.pop_back();
|
||||||
|
}
|
||||||
|
const json* pv = resolve_ptr(ctx, t.payload);
|
||||||
|
out.append(val_to_string(*pv));
|
||||||
|
if (t.trim_right && k+1 < prog.size() && prog[k+1].type == TokType::Text) {
|
||||||
|
auto &nxt = const_cast<Tok&>(prog[k+1]);
|
||||||
|
size_t j=0; while (j<nxt.payload.size() && (nxt.payload[j]==' '||nxt.payload[j]=='\t'||nxt.payload[j]=='\r'||nxt.payload[j]=='\n')) ++j;
|
||||||
|
if (j>0) nxt.payload.erase(0, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per-thread template cache
|
||||||
|
struct ThreadLocalTemplates {
|
||||||
|
struct Entry { fs::file_time_type mtime; std::vector<MiniInja::Tok> program; };
|
||||||
|
std::unordered_map<std::string, Entry> cache;
|
||||||
|
fs::path root;
|
||||||
|
explicit ThreadLocalTemplates(fs::path root_) : root(std::move(root_)) {}
|
||||||
|
const std::vector<MiniInja::Tok>& get(const fs::path& full) {
|
||||||
|
std::string key = full.string();
|
||||||
|
auto mtime = fs::last_write_time(full);
|
||||||
|
auto it = cache.find(key);
|
||||||
|
if (it != cache.end() && it->second.mtime == mtime) return it->second.program;
|
||||||
|
std::ifstream f(full, std::ios::binary);
|
||||||
|
if (!f) throw std::runtime_error("cannot open template");
|
||||||
|
std::string src((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||||
|
auto prog = MiniInja::compile(src);
|
||||||
|
cache[key] = Entry{mtime, std::move(prog)};
|
||||||
|
return cache[key].program;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static thread_local std::unique_ptr<ThreadLocalTemplates> tltpl;
|
||||||
|
|
||||||
|
// ---- HTTP primitives ---------------------------------------------------------
|
||||||
|
struct Request {
|
||||||
|
std::string method, target, version;
|
||||||
|
std::map<std::string,std::string> headers; // lower-cased keys
|
||||||
|
std::string body;
|
||||||
|
bool keep_alive = true;
|
||||||
|
};
|
||||||
|
struct Response {
|
||||||
|
int status = 200;
|
||||||
|
std::string reason = "OK";
|
||||||
|
std::vector<std::pair<std::string,std::string>> headers;
|
||||||
|
std::string body;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string status_reason(int code){
|
||||||
|
switch(code){
|
||||||
|
case 200: return "OK";
|
||||||
|
case 400: return "Bad Request";
|
||||||
|
case 404: return "Not Found";
|
||||||
|
case 411: return "Length Required";
|
||||||
|
case 413: return "Payload Too Large";
|
||||||
|
case 415: return "Unsupported Media Type";
|
||||||
|
case 431: return "Request Header Fields Too Large";
|
||||||
|
case 500: return "Internal Server Error";
|
||||||
|
default: return "Error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_socket_timeouts(int fd){
|
||||||
|
timeval tv{.tv_sec = kRecvTimeoutSec, .tv_usec = 0};
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||||
|
tv = timeval{.tv_sec = kSendTimeoutSec, .tv_usec = 0};
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||||
|
int flag = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool recv_append(int fd, std::string& buf){
|
||||||
|
char tmp[8192];
|
||||||
|
ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0);
|
||||||
|
if (n <= 0) return false;
|
||||||
|
buf.append(tmp, (size_t)n);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static bool send_all(int fd, const char* data, size_t len){
|
||||||
|
size_t off = 0;
|
||||||
|
while (off < len) {
|
||||||
|
ssize_t n = ::send(fd, data + off, len - off, 0);
|
||||||
|
if (n < 0) { if (errno==EINTR) continue; return false; }
|
||||||
|
off += (size_t)n;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static std::string build_response(const Response& res, bool keep_alive){
|
||||||
|
std::string out;
|
||||||
|
out.reserve(128 + res.body.size() + 128);
|
||||||
|
out += "HTTP/1.1 " + std::to_string(res.status) + " " + (res.reason.empty()?status_reason(res.status):res.reason) + "\r\n";
|
||||||
|
out += "Server: "; out += kServerName; out += "\r\n";
|
||||||
|
out += "Content-Length: " + std::to_string(res.body.size()) + "\r\n";
|
||||||
|
out += std::string("Connection: ") + (keep_alive ? "keep-alive" : "close") + "\r\n";
|
||||||
|
for (auto& h : res.headers) { out += h.first; out += ": "; out += h.second; out += "\r\n"; }
|
||||||
|
out += "\r\n";
|
||||||
|
out += res.body;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_json_ct(const std::map<std::string,std::string>& h){
|
||||||
|
auto it = h.find("content-type");
|
||||||
|
if (it==h.end()) return false;
|
||||||
|
auto v = to_lower(it->second);
|
||||||
|
return v.find("application/json") != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse chunked body; returns <bytes_consumed, body>
|
||||||
|
static std::optional<std::pair<size_t,std::string>>
|
||||||
|
parse_chunked_body(const std::string& buf, size_t start_off) {
|
||||||
|
size_t p = start_off;
|
||||||
|
std::string out;
|
||||||
|
out.reserve(1024);
|
||||||
|
while (true) {
|
||||||
|
size_t eol = buf.find("\r\n", p);
|
||||||
|
if (eol == std::string::npos) return std::nullopt;
|
||||||
|
std::string size_line = buf.substr(p, eol - p);
|
||||||
|
size_t sc = size_line.find(';'); if (sc != std::string::npos) size_line.resize(sc);
|
||||||
|
size_line = trim(size_line);
|
||||||
|
if (size_line.empty()) throw std::runtime_error("bad chunk size");
|
||||||
|
size_t chunk_size = 0;
|
||||||
|
try { chunk_size = std::stoul(size_line, nullptr, 16); } catch (...) { throw std::runtime_error("bad chunk size"); }
|
||||||
|
p = eol + 2;
|
||||||
|
if (chunk_size == 0) {
|
||||||
|
// last-chunk, then optional trailers terminated by CRLFCRLF
|
||||||
|
size_t trailer_end = buf.find("\r\n\r\n", p);
|
||||||
|
if (trailer_end == std::string::npos) return std::nullopt;
|
||||||
|
size_t consumed = (trailer_end + 4) - start_off;
|
||||||
|
return std::make_pair(consumed, std::move(out));
|
||||||
|
}
|
||||||
|
if (buf.size() < p + chunk_size + 2) return std::nullopt;
|
||||||
|
if (out.size() + chunk_size > kMaxBodyBytes) throw std::runtime_error("payload too large");
|
||||||
|
out.append(buf.data() + p, chunk_size);
|
||||||
|
p += chunk_size;
|
||||||
|
if (!(buf[p] == '\r' && buf[p+1] == '\n')) throw std::runtime_error("bad chunk CRLF");
|
||||||
|
p += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a full HTTP request if available in inbuf
|
||||||
|
static std::optional<Request> parse_request(std::string& inbuf) {
|
||||||
|
size_t hdr_end = inbuf.find("\r\n\r\n");
|
||||||
|
if (hdr_end == std::string::npos) return std::nullopt;
|
||||||
|
if (hdr_end > kMaxHeaderBytes) throw std::runtime_error("headers too large");
|
||||||
|
|
||||||
|
std::string head = inbuf.substr(0, hdr_end);
|
||||||
|
size_t line_end = head.find("\r\n");
|
||||||
|
if (line_end == std::string::npos) return std::nullopt;
|
||||||
|
|
||||||
|
Request r;
|
||||||
|
{ // Request line
|
||||||
|
std::string reqline = head.substr(0, line_end);
|
||||||
|
size_t sp1 = reqline.find(' ');
|
||||||
|
size_t sp2 = reqline.find(' ', sp1+1);
|
||||||
|
if (sp1==std::string::npos||sp2==std::string::npos) throw std::runtime_error("bad request line");
|
||||||
|
r.method = reqline.substr(0, sp1);
|
||||||
|
r.target = reqline.substr(sp1+1, sp2 - sp1 - 1);
|
||||||
|
r.version = reqline.substr(sp2+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
size_t off = line_end + 2;
|
||||||
|
while (off < head.size()) {
|
||||||
|
size_t nxt = head.find("\r\n", off);
|
||||||
|
if (nxt == std::string::npos) break;
|
||||||
|
std::string line = head.substr(off, nxt - off);
|
||||||
|
off = nxt + 2;
|
||||||
|
if (line.empty()) break;
|
||||||
|
size_t colon = line.find(':'); if (colon == std::string::npos) continue;
|
||||||
|
std::string k = to_lower(trim(line.substr(0, colon)));
|
||||||
|
std::string v = trim(line.substr(colon+1));
|
||||||
|
r.headers[k] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t body_start = hdr_end + 4;
|
||||||
|
|
||||||
|
const bool has_te = r.headers.count("transfer-encoding") &&
|
||||||
|
to_lower(r.headers.at("transfer-encoding")).find("chunked") != std::string::npos;
|
||||||
|
|
||||||
|
if (has_te) {
|
||||||
|
auto parsed = parse_chunked_body(inbuf, body_start);
|
||||||
|
if (!parsed) return std::nullopt; // need more data
|
||||||
|
const auto& [consumed_body, body] = *parsed;
|
||||||
|
r.body = body;
|
||||||
|
inbuf.erase(0, body_start + consumed_body);
|
||||||
|
} else {
|
||||||
|
size_t content_len = 0;
|
||||||
|
if (r.headers.count("content-length")) {
|
||||||
|
content_len = (size_t)std::stoull(r.headers["content-length"]);
|
||||||
|
if (content_len > kMaxBodyBytes) throw std::runtime_error("payload too large");
|
||||||
|
}
|
||||||
|
if (inbuf.size() < body_start + content_len) return std::nullopt;
|
||||||
|
r.body.assign(inbuf.data() + body_start, content_len);
|
||||||
|
inbuf.erase(0, body_start + content_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.keep_alive = true;
|
||||||
|
if (r.version == "HTTP/1.0") {
|
||||||
|
r.keep_alive = (to_lower(r.headers["connection"]) == "keep-alive");
|
||||||
|
} else {
|
||||||
|
if (r.headers.count("connection") && to_lower(r.headers["connection"]) == "close") r.keep_alive = false;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool send_100_continue_if_needed(int fd, const std::string& head_block){
|
||||||
|
// Search for "Expect: 100-continue" case-insensitive in headers block
|
||||||
|
size_t off = 0;
|
||||||
|
while (true) {
|
||||||
|
size_t nxt = head_block.find("\r\n", off);
|
||||||
|
if (nxt == std::string::npos) break;
|
||||||
|
std::string line = head_block.substr(off, nxt - off);
|
||||||
|
off = nxt + 2;
|
||||||
|
if (line.empty()) break;
|
||||||
|
size_t colon = line.find(':'); if (colon == std::string::npos) continue;
|
||||||
|
std::string k = to_lower(trim(line.substr(0, colon)));
|
||||||
|
if (k == "expect") {
|
||||||
|
std::string v = to_lower(trim(line.substr(colon+1)));
|
||||||
|
if (v.find("100-continue") != std::string::npos) {
|
||||||
|
static const char kCont[] = "HTTP/1.1 100 Continue\r\n\r\n";
|
||||||
|
return send_all(fd, kCont, sizeof(kCont)-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Blocking queue & server -------------------------------------------------
|
||||||
|
template<typename T>
|
||||||
|
class BlockingQueue {
|
||||||
|
std::mutex m;
|
||||||
|
std::condition_variable cv;
|
||||||
|
std::vector<T> q;
|
||||||
|
public:
|
||||||
|
void push(T v){ {std::lock_guard<std::mutex> lk(m); q.push_back(std::move(v)); } cv.notify_one(); }
|
||||||
|
bool pop(T& out){ std::unique_lock<std::mutex> lk(m); cv.wait(lk,[&]{ return !q.empty(); }); out = std::move(q.back()); q.pop_back(); return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Server {
|
||||||
|
Settings cfg;
|
||||||
|
int listen_fd = -1;
|
||||||
|
std::atomic<bool> running{true};
|
||||||
|
BlockingQueue<int> queue;
|
||||||
|
std::vector<std::thread> workers;
|
||||||
|
|
||||||
|
explicit Server(Settings s): cfg(std::move(s)) {}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (listen_fd < 0) { perror("socket"); std::exit(1); }
|
||||||
|
int yes=1;
|
||||||
|
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||||
|
#ifdef SO_REUSEPORT
|
||||||
|
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));
|
||||||
|
#endif
|
||||||
|
sockaddr_in addr{};
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons(cfg.port);
|
||||||
|
if (cfg.address == "0.0.0.0") {
|
||||||
|
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||||
|
} else {
|
||||||
|
in_addr a{}; if (::inet_pton(AF_INET, cfg.address.c_str(), &a) != 1) { std::cerr << "Invalid address\n"; std::exit(1); }
|
||||||
|
addr.sin_addr = a;
|
||||||
|
}
|
||||||
|
if (::bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); std::exit(1); }
|
||||||
|
if (::listen(listen_fd, kBacklog) < 0) { perror("listen"); std::exit(1); }
|
||||||
|
|
||||||
|
// workers
|
||||||
|
for (size_t i=0;i<cfg.threads;i++) workers.emplace_back([this]{ worker_loop(); });
|
||||||
|
|
||||||
|
std::cout << "Listening on http://" << cfg.address << ":" << cfg.port << " with " << cfg.threads << " workers\n";
|
||||||
|
accept_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void accept_loop(){
|
||||||
|
while (running.load(std::memory_order_relaxed)) {
|
||||||
|
int fd = ::accept(listen_fd, nullptr, nullptr);
|
||||||
|
if (fd < 0) {
|
||||||
|
if (errno==EINTR) continue;
|
||||||
|
perror("accept"); continue;
|
||||||
|
}
|
||||||
|
set_socket_timeouts(fd);
|
||||||
|
queue.push(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_json_error(int fd, int code, const std::string& msg, bool keep_alive) {
|
||||||
|
Response res;
|
||||||
|
res.status = code; res.reason = status_reason(code);
|
||||||
|
res.headers.push_back({"Content-Type","application/json; charset=utf-8"});
|
||||||
|
res.headers.push_back({"Cache-Control","no-store"});
|
||||||
|
res.headers.push_back({"X-Error-Detail", msg});
|
||||||
|
res.body = json({{"error", msg}}).dump();
|
||||||
|
auto raw = build_response(res, keep_alive);
|
||||||
|
send_all(fd, raw.data(), raw.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void worker_loop() {
|
||||||
|
if (!tltpl) tltpl = std::make_unique<ThreadLocalTemplates>(cfg.template_root);
|
||||||
|
while (true) {
|
||||||
|
int fd; queue.pop(fd);
|
||||||
|
handle_connection(fd);
|
||||||
|
::shutdown(fd, SHUT_WR);
|
||||||
|
::close(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_connection(int fd) {
|
||||||
|
std::string inbuf; inbuf.reserve(16*1024);
|
||||||
|
bool keep = true;
|
||||||
|
while (keep) {
|
||||||
|
// Ensure we have headers; also guard header size growth
|
||||||
|
while (true) {
|
||||||
|
size_t pos = inbuf.find("\r\n\r\n");
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
// Send 100-continue if asked
|
||||||
|
if (!send_100_continue_if_needed(fd, inbuf.substr(0, pos))) return;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (inbuf.size() > kMaxHeaderBytes) { send_json_error(fd, 431, "headers too large", false); return; }
|
||||||
|
if (!recv_append(fd, inbuf)) return; // closed or timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Request> reqOpt;
|
||||||
|
try {
|
||||||
|
reqOpt = parse_request(inbuf);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
int code = 400;
|
||||||
|
std::string what = e.what();
|
||||||
|
if (what == "payload too large") code = 413;
|
||||||
|
else if (what == "length required") code = 411;
|
||||||
|
else if (what == "headers too large") code = 431;
|
||||||
|
send_json_error(fd, code, what, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!reqOpt) {
|
||||||
|
// Need more body bytes
|
||||||
|
if (!recv_append(fd, inbuf)) return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Request req = std::move(*reqOpt);
|
||||||
|
keep = req.keep_alive;
|
||||||
|
|
||||||
|
if (!(req.method=="POST" && req.target=="/render")) {
|
||||||
|
send_json_error(fd, 404, "Not Found", keep);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!is_json_ct(req.headers)) {
|
||||||
|
send_json_error(fd, 415, "Content-Type must be application/json", keep);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (req.body.size() > kMaxBodyBytes) {
|
||||||
|
send_json_error(fd, 413, "Body too large", keep);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON request
|
||||||
|
json jreq;
|
||||||
|
try { jreq = json::parse(req.body); }
|
||||||
|
catch (const std::exception& e) { send_json_error(fd, 400, std::string("Invalid JSON: ") + e.what(), keep); continue; }
|
||||||
|
|
||||||
|
if (!jreq.contains("template") || !jreq["template"].is_string()) {
|
||||||
|
send_json_error(fd, 400, R"(Missing "template" (string))", keep); continue;
|
||||||
|
}
|
||||||
|
if (jreq.contains("context") && !jreq["context"].is_object()) {
|
||||||
|
send_json_error(fd, 400, R"("context" must be an object)", keep); continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string reason;
|
||||||
|
fs::path full = secure_resolve_template(cfg.template_root, jreq["template"].get<std::string>(), reason);
|
||||||
|
if (full.empty()) { send_json_error(fd, 400, "Template error: " + reason, keep); continue; }
|
||||||
|
|
||||||
|
json ctx = jreq.value("context", json::object());
|
||||||
|
|
||||||
|
// Render
|
||||||
|
Response res;
|
||||||
|
auto t0 = std::chrono::steady_clock::now();
|
||||||
|
try {
|
||||||
|
const auto& prog = tltpl->get(full);
|
||||||
|
std::string rendered = MiniInja::render(prog, ctx);
|
||||||
|
auto t1 = std::chrono::steady_clock::now();
|
||||||
|
auto us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||||
|
|
||||||
|
res.status = 200; res.reason = "OK";
|
||||||
|
res.headers.push_back({"Content-Type","text/plain; charset=utf-8"});
|
||||||
|
res.headers.push_back({"Cache-Control","no-store"});
|
||||||
|
res.headers.push_back({"X-Render-Time-Us", std::to_string(us)});
|
||||||
|
res.body = std::move(rendered);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
send_json_error(fd, 400, std::string("Render error: ") + e.what(), keep);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto raw = build_response(res, keep);
|
||||||
|
if (!send_all(fd, raw.data(), raw.size())) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Args & main -------------------------------------------------------------
|
||||||
|
static Settings parse_args(int argc, char** argv){
|
||||||
|
Settings s;
|
||||||
|
for (int i=1;i<argc;i++){
|
||||||
|
std::string a = argv[i];
|
||||||
|
auto next = [&]()->std::string{ if (i+1>=argc) throw std::runtime_error("Missing value for "+a); return std::string(argv[++i]); };
|
||||||
|
if (a=="--templates"||a=="-t") s.template_root = next();
|
||||||
|
else if (a=="--address"||a=="-a") s.address = next();
|
||||||
|
else if (a=="--port"||a=="-p") s.port = std::stoi(next());
|
||||||
|
else if (a=="--threads"||a=="-w") s.threads = (size_t)std::stoul(next());
|
||||||
|
else if (a=="--help"||a=="-h") {
|
||||||
|
std::cout <<
|
||||||
|
"Usage: sinja --templates DIR [--address ADDR] [--port PORT] [--threads N]\n"
|
||||||
|
" --templates, -t Template directory (required)\n"
|
||||||
|
" --address, -a Bind address (default 0.0.0.0)\n"
|
||||||
|
" --port, -p Port (default 8080)\n"
|
||||||
|
" --threads, -w Worker threads (default HW concurrency)\n";
|
||||||
|
std::exit(0);
|
||||||
|
} else throw std::runtime_error("Unknown arg: " + a);
|
||||||
|
}
|
||||||
|
if (s.template_root.empty()) throw std::runtime_error("Missing required --templates DIR");
|
||||||
|
if (!fs::exists(s.template_root) || !fs::is_directory(s.template_root))
|
||||||
|
throw std::runtime_error("Template directory invalid: " + s.template_root.string());
|
||||||
|
s.template_root = fs::weakly_canonical(s.template_root);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::atomic<bool> g_shutdown{false};
|
||||||
|
static void on_signal(int){ g_shutdown.store(true); }
|
||||||
|
|
||||||
|
int main(int argc, char** argv){
|
||||||
|
try {
|
||||||
|
auto cfg = parse_args(argc, argv);
|
||||||
|
std::signal(SIGPIPE, SIG_IGN);
|
||||||
|
std::signal(SIGINT, on_signal);
|
||||||
|
std::signal(SIGTERM, on_signal);
|
||||||
|
|
||||||
|
std::cout << "Starting " << kServerName << "\n"
|
||||||
|
<< " Address : " << cfg.address << "\n"
|
||||||
|
<< " Port : " << cfg.port << "\n"
|
||||||
|
<< " Threads : " << cfg.threads << "\n"
|
||||||
|
<< " Templates : " << cfg.template_root << "\n";
|
||||||
|
|
||||||
|
Server srv(std::move(cfg));
|
||||||
|
// simple foreground run; Ctrl+C to stop
|
||||||
|
srv.start();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Fatal: " << e.what() << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
31
render_template.py
Normal file
31
render_template.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Set up Jinja2 environment to load templates from the 'templates' directory
|
||||||
|
env = Environment(loader=FileSystemLoader('templates'))
|
||||||
|
|
||||||
|
def render_template(template_name, times=1000):
|
||||||
|
template = env.get_template(template_name)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
for _ in range(times):
|
||||||
|
rendered_content = template.render({
|
||||||
|
"name": "Retoor",
|
||||||
|
"stats": {"notifications": 3},
|
||||||
|
"repos": [{"name": "snek"}],
|
||||||
|
})
|
||||||
|
end_time = time.time()
|
||||||
|
|
||||||
|
total_time = end_time - start_time
|
||||||
|
avg_time_per_render = total_time / times if times > 0 else 0
|
||||||
|
print(f"Total time for {times} renders: {total_time:.6f} seconds")
|
||||||
|
print(f"Average time per render: {avg_time_per_render:.6f} seconds")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print('Usage: python render_template.py <template_name>')
|
||||||
|
sys.exit(1)
|
||||||
|
template_name = sys.argv[1]
|
||||||
|
render_template(template_name)
|
||||||
|
|
455
sinja.cpp
Normal file
455
sinja.cpp
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
// sinja: blazing-fast, stable, production-grade JSON templating REST server
|
||||||
|
// Architecture: Multi-threaded SO_REUSEPORT with robust, high-performance request handling.
|
||||||
|
// Dependency: inja (which includes nlohmann::json)
|
||||||
|
// Build: see CMakeLists.txt (ensure you link with -lpthread)
|
||||||
|
// Run : ./sinja --templates /path/to/templates
|
||||||
|
|
||||||
|
#include <inja/inja.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <csignal>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
// ---- Tunables ----------------------------------------------------------------
|
||||||
|
static constexpr const char* kServerName = "sinja/5.0-stable";
|
||||||
|
static constexpr size_t kMaxBodyBytes = 8 * 1024 * 1024;
|
||||||
|
static constexpr size_t kMaxHeaderBytes = 64 * 1024;
|
||||||
|
static constexpr int kRecvTimeoutSec = 10;
|
||||||
|
static constexpr int kSendTimeoutSec = 10;
|
||||||
|
static constexpr int kBacklog = 8192; // Increased for modern kernels
|
||||||
|
|
||||||
|
// ---- Global state for graceful shutdown --------------------------------------
|
||||||
|
static std::atomic<bool> g_running{true};
|
||||||
|
static void on_signal(int){ g_running.store(false); }
|
||||||
|
|
||||||
|
// ---- Config ------------------------------------------------------------------
|
||||||
|
struct Settings {
|
||||||
|
std::string address = "0.0.0.0";
|
||||||
|
int port = 8080;
|
||||||
|
size_t threads = std::thread::hardware_concurrency() ? std::thread::hardware_concurrency() : 4;
|
||||||
|
fs::path template_root;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Small utils -------------------------------------------------------------
|
||||||
|
static inline std::string ltrim(std::string s){
|
||||||
|
size_t i=0; while(i<s.size() && std::isspace((unsigned char)s[i])) ++i; return s.substr(i);
|
||||||
|
}
|
||||||
|
static inline std::string rtrim(std::string s){
|
||||||
|
if(s.empty()) return s;
|
||||||
|
size_t i=s.size()-1;
|
||||||
|
while (i<s.size() && std::isspace((unsigned char)s[i])) { if(i==0) return ""; --i; }
|
||||||
|
return s.substr(0, i+1);
|
||||||
|
}
|
||||||
|
static inline std::string trim(std::string s){ return rtrim(ltrim(std::move(s))); }
|
||||||
|
static std::string to_lower(std::string s){
|
||||||
|
for (auto& c: s) c = (char)std::tolower((unsigned char)c);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Secure template validation ----------------------------------------------
|
||||||
|
static bool validate_template_path(const std::string& req) {
|
||||||
|
return !(req.empty() || req[0] == '/' || req.find("..") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- HTTP primitives ---------------------------------------------------------
|
||||||
|
struct Request {
|
||||||
|
std::string method, target, version;
|
||||||
|
std::map<std::string, std::string> headers;
|
||||||
|
std::string body;
|
||||||
|
bool keep_alive = true;
|
||||||
|
};
|
||||||
|
struct Response {
|
||||||
|
int status = 200;
|
||||||
|
std::string reason = "OK";
|
||||||
|
std::vector<std::pair<std::string,std::string>> headers;
|
||||||
|
std::string body;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string status_reason(int code){
|
||||||
|
switch(code){
|
||||||
|
case 200: return "OK"; case 400: return "Bad Request"; case 404: return "Not Found";
|
||||||
|
case 411: return "Length Required"; case 413: return "Payload Too Large";
|
||||||
|
case 415: return "Unsupported Media Type"; case 431: return "Request Header Fields Too Large";
|
||||||
|
case 500: return "Internal Server Error"; default: return "Error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_socket_timeouts(int fd, int recv_sec, int send_sec){
|
||||||
|
timeval tv{.tv_sec = recv_sec, .tv_usec = 0};
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||||
|
tv = timeval{.tv_sec = send_sec, .tv_usec = 0};
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||||
|
int flag = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool recv_append(int fd, std::string& buf){
|
||||||
|
char tmp[8192];
|
||||||
|
ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0);
|
||||||
|
if (n <= 0) return false;
|
||||||
|
buf.append(tmp, (size_t)n);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool send_all(int fd, const char* data, size_t len){
|
||||||
|
size_t off = 0;
|
||||||
|
while (off < len) {
|
||||||
|
ssize_t n = ::send(fd, data + off, len - off, MSG_NOSIGNAL);
|
||||||
|
if (n < 0) { if (errno == EINTR) continue; return false; }
|
||||||
|
off += (size_t)n;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string build_response(const Response& res, bool keep_alive){
|
||||||
|
std::string out;
|
||||||
|
out.reserve(256 + res.body.size());
|
||||||
|
out += "HTTP/1.1 " + std::to_string(res.status) + " " + (res.reason.empty()?status_reason(res.status):res.reason) + "\r\n";
|
||||||
|
out += "Server: "; out += kServerName; out += "\r\n";
|
||||||
|
out += "Content-Length: " + std::to_string(res.body.size()) + "\r\n";
|
||||||
|
out += std::string("Connection: ") + (keep_alive ? "keep-alive" : "close") + "\r\n";
|
||||||
|
for (const auto& h : res.headers) { out += h.first; out += ": "; out += h.second; out += "\r\n"; }
|
||||||
|
out += "\r\n";
|
||||||
|
out += res.body;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_json_ct(const std::map<std::string, std::string>& h){
|
||||||
|
auto it = h.find("content-type");
|
||||||
|
if (it == h.end()) return false;
|
||||||
|
return to_lower(it->second).find("application/json") != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::optional<std::pair<size_t, std::string>>
|
||||||
|
parse_chunked_body(const std::string& buf, size_t start_off) {
|
||||||
|
size_t p = start_off;
|
||||||
|
std::string out;
|
||||||
|
while (true) {
|
||||||
|
size_t eol = buf.find("\r\n", p);
|
||||||
|
if (eol == std::string::npos) return std::nullopt;
|
||||||
|
std::string size_line = buf.substr(p, eol - p);
|
||||||
|
size_t sc = size_line.find(';'); if (sc != std::string::npos) size_line.resize(sc);
|
||||||
|
size_line = trim(size_line);
|
||||||
|
if (size_line.empty()) throw std::runtime_error("bad chunk size");
|
||||||
|
size_t chunk_size = 0;
|
||||||
|
try { chunk_size = std::stoul(size_line, nullptr, 16); } catch (...) { throw std::runtime_error("bad chunk size"); }
|
||||||
|
p = eol + 2;
|
||||||
|
if (chunk_size == 0) {
|
||||||
|
size_t trailer_end = buf.find("\r\n\r\n", p);
|
||||||
|
if (trailer_end == std::string::npos) return std::nullopt;
|
||||||
|
return std::make_pair((trailer_end + 4) - start_off, std::move(out));
|
||||||
|
}
|
||||||
|
if (buf.size() < p + chunk_size + 2) return std::nullopt;
|
||||||
|
if (out.size() + chunk_size > kMaxBodyBytes) throw std::runtime_error("payload too large");
|
||||||
|
out.append(buf.data() + p, chunk_size);
|
||||||
|
p += chunk_size;
|
||||||
|
if (!(buf[p] == '\r' && buf[p+1] == '\n')) throw std::runtime_error("bad chunk CRLF");
|
||||||
|
p += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::optional<Request> parse_request(std::string& inbuf) {
|
||||||
|
size_t hdr_end = inbuf.find("\r\n\r\n");
|
||||||
|
if (hdr_end == std::string::npos) return std::nullopt;
|
||||||
|
if (hdr_end > kMaxHeaderBytes) throw std::runtime_error("headers too large");
|
||||||
|
|
||||||
|
std::string head = inbuf.substr(0, hdr_end);
|
||||||
|
size_t line_end = head.find("\r\n");
|
||||||
|
if (line_end == std::string::npos) return std::nullopt;
|
||||||
|
|
||||||
|
Request r;
|
||||||
|
std::string req_line = head.substr(0, line_end);
|
||||||
|
size_t sp1 = req_line.find(' ');
|
||||||
|
size_t sp2 = req_line.find(' ', sp1 + 1);
|
||||||
|
if (sp1 == std::string::npos || sp2 == std::string::npos) throw std::runtime_error("bad request line");
|
||||||
|
r.method = req_line.substr(0, sp1);
|
||||||
|
r.target = req_line.substr(sp1 + 1, sp2 - sp1 - 1);
|
||||||
|
r.version = req_line.substr(sp2 + 1);
|
||||||
|
|
||||||
|
size_t off = line_end + 2;
|
||||||
|
while (off < head.size()) {
|
||||||
|
size_t next_line = head.find("\r\n", off);
|
||||||
|
std::string line = head.substr(off, next_line - off);
|
||||||
|
if (next_line == std::string::npos) {
|
||||||
|
off = head.size();
|
||||||
|
} else {
|
||||||
|
off = next_line + 2;
|
||||||
|
}
|
||||||
|
if (line.empty()) break;
|
||||||
|
size_t colon_pos = line.find(':');
|
||||||
|
if (colon_pos != std::string::npos) {
|
||||||
|
r.headers[to_lower(trim(line.substr(0, colon_pos)))] = trim(line.substr(colon_pos + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t body_start = hdr_end + 4;
|
||||||
|
size_t consumed_len = 0;
|
||||||
|
|
||||||
|
auto te_it = r.headers.find("transfer-encoding");
|
||||||
|
if (te_it != r.headers.end() && to_lower(te_it->second).find("chunked") != std::string::npos) {
|
||||||
|
auto parsed = parse_chunked_body(inbuf, body_start);
|
||||||
|
if (!parsed) return std::nullopt;
|
||||||
|
r.body = std::move(parsed->second);
|
||||||
|
consumed_len = body_start + parsed->first;
|
||||||
|
} else {
|
||||||
|
size_t content_len = 0;
|
||||||
|
auto cl_it = r.headers.find("content-length");
|
||||||
|
if (cl_it != r.headers.end()) {
|
||||||
|
try { content_len = std::stoull(cl_it->second); } catch (...) { throw std::runtime_error("invalid content-length"); }
|
||||||
|
if (content_len > kMaxBodyBytes) throw std::runtime_error("payload too large");
|
||||||
|
}
|
||||||
|
if (inbuf.size() < body_start + content_len) return std::nullopt;
|
||||||
|
r.body = inbuf.substr(body_start, content_len);
|
||||||
|
consumed_len = body_start + content_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
inbuf.erase(0, consumed_len);
|
||||||
|
|
||||||
|
auto conn_it = r.headers.find("connection");
|
||||||
|
if (r.version == "HTTP/1.0") {
|
||||||
|
r.keep_alive = (conn_it != r.headers.end() && to_lower(conn_it->second) == "keep-alive");
|
||||||
|
} else {
|
||||||
|
r.keep_alive = (conn_it == r.headers.end() || to_lower(conn_it->second) != "close");
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Core Server Logic -------------------------------------------------------
|
||||||
|
class Server {
|
||||||
|
Settings cfg;
|
||||||
|
std::vector<std::thread> workers;
|
||||||
|
std::vector<int> listen_fds;
|
||||||
|
std::mutex fds_mutex;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Server(Settings s): cfg(std::move(s)) {}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
std::cout << "Starting " << kServerName << "...\n";
|
||||||
|
for (size_t i = 0; i < cfg.threads; ++i) {
|
||||||
|
workers.emplace_back([this]{ worker_loop(); });
|
||||||
|
}
|
||||||
|
std::cout << " Listening on http://" << cfg.address << ":" << cfg.port << " with " << cfg.threads << " workers.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
g_running.store(false);
|
||||||
|
std::cout << "\nShutting down... finishing active connections.\n";
|
||||||
|
std::lock_guard<std::mutex> lock(fds_mutex);
|
||||||
|
for (int fd : listen_fds) {
|
||||||
|
::shutdown(fd, SHUT_RDWR);
|
||||||
|
::close(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void join() {
|
||||||
|
for (auto& t : workers) {
|
||||||
|
if (t.joinable()) t.join();
|
||||||
|
}
|
||||||
|
std::cout << "All workers stopped. Shutdown complete.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void send_json_error(int fd, int code, const std::string& msg) {
|
||||||
|
Response res;
|
||||||
|
res.status = code; res.reason = status_reason(code);
|
||||||
|
res.headers.push_back({"Content-Type","application/json; charset=utf-8"});
|
||||||
|
res.headers.push_back({"Cache-Control","no-store"});
|
||||||
|
res.body = json({{"error", msg}}).dump();
|
||||||
|
auto raw = build_response(res, false);
|
||||||
|
send_all(fd, raw.data(), raw.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_connection(int fd, inja::Environment& env) {
|
||||||
|
std::string inbuf;
|
||||||
|
inbuf.reserve(16*1024);
|
||||||
|
bool keep = true;
|
||||||
|
|
||||||
|
while (keep && g_running.load(std::memory_order_relaxed)) {
|
||||||
|
std::optional<Request> reqOpt;
|
||||||
|
while (g_running.load(std::memory_order_relaxed)) {
|
||||||
|
try {
|
||||||
|
reqOpt = parse_request(inbuf);
|
||||||
|
if (reqOpt) break;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
int code = 400; std::string what = e.what();
|
||||||
|
if (what == "payload too large") code = 413;
|
||||||
|
else if (what == "headers too large") code = 431;
|
||||||
|
send_json_error(fd, code, what);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!recv_append(fd, inbuf)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reqOpt) return;
|
||||||
|
Request req = std::move(*reqOpt);
|
||||||
|
keep = req.keep_alive;
|
||||||
|
|
||||||
|
if (!(req.method == "POST" && req.target == "/render")) {
|
||||||
|
send_json_error(fd, 404, "Not Found");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!is_json_ct(req.headers)) {
|
||||||
|
send_json_error(fd, 415, "Content-Type must be application/json");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
json jreq;
|
||||||
|
try {
|
||||||
|
if (req.body.empty()) throw std::runtime_error("Request body is empty");
|
||||||
|
jreq = json::parse(req.body);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
send_json_error(fd, 400, std::string("Invalid JSON: ") + e.what());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jreq.contains("template") || !jreq["template"].is_string()) {
|
||||||
|
send_json_error(fd, 400, R"(Missing "template" (string))"); continue;
|
||||||
|
}
|
||||||
|
if (jreq.contains("context") && !jreq["context"].is_object()) {
|
||||||
|
send_json_error(fd, 400, R"("context" must be an object)"); continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string template_name = jreq["template"].get<std::string>();
|
||||||
|
if (!validate_template_path(template_name)) {
|
||||||
|
send_json_error(fd, 400, "Template error: invalid path"); continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
json ctx = jreq.value("context", json::object());
|
||||||
|
Response res;
|
||||||
|
|
||||||
|
auto t0 = std::chrono::steady_clock::now();
|
||||||
|
try {
|
||||||
|
res.body = env.render_file(template_name, ctx);
|
||||||
|
auto t1 = std::chrono::steady_clock::now();
|
||||||
|
auto us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||||
|
res.headers.push_back({"Content-Type","text/plain; charset=utf-8"});
|
||||||
|
res.headers.push_back({"X-Render-Time-Us", std::to_string(us)});
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
send_json_error(fd, 500, std::string("Render error: ") + e.what());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto raw = build_response(res, keep);
|
||||||
|
if (!send_all(fd, raw.data(), raw.size())) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void worker_loop() {
|
||||||
|
inja::Environment env(cfg.template_root.string());
|
||||||
|
env.set_trim_blocks(true);
|
||||||
|
env.set_lstrip_blocks(true);
|
||||||
|
|
||||||
|
int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (listen_fd < 0) { perror("socket"); return; }
|
||||||
|
|
||||||
|
int yes=1;
|
||||||
|
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||||
|
#ifdef SO_REUSEPORT
|
||||||
|
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
sockaddr_in addr{};
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons(cfg.port);
|
||||||
|
if (::inet_pton(AF_INET, cfg.address.c_str(), &addr.sin_addr) != 1) {
|
||||||
|
std::cerr << "Invalid address\n"; ::close(listen_fd); return;
|
||||||
|
}
|
||||||
|
if (::bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||||
|
perror("bind"); ::close(listen_fd); return;
|
||||||
|
}
|
||||||
|
if (::listen(listen_fd, kBacklog) < 0) {
|
||||||
|
perror("listen"); ::close(listen_fd); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(fds_mutex);
|
||||||
|
listen_fds.push_back(listen_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (g_running.load(std::memory_order_relaxed)) {
|
||||||
|
int client_fd = ::accept(listen_fd, nullptr, nullptr);
|
||||||
|
if (client_fd < 0) {
|
||||||
|
if (g_running.load(std::memory_order_relaxed)) perror("accept");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
set_socket_timeouts(client_fd, kRecvTimeoutSec, kSendTimeoutSec);
|
||||||
|
handle_connection(client_fd, env);
|
||||||
|
::shutdown(client_fd, SHUT_WR);
|
||||||
|
::close(client_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
::close(listen_fd);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Args & main -------------------------------------------------------------
|
||||||
|
static Settings parse_args(int argc, char** argv){
|
||||||
|
Settings s;
|
||||||
|
for (int i=1;i<argc;i++){
|
||||||
|
std::string a = argv[i];
|
||||||
|
auto next = [&]()->std::string{ if (i+1>=argc) throw std::runtime_error("Missing value for "+a); return std::string(argv[++i]); };
|
||||||
|
if (a=="--templates"||a=="-t") s.template_root = next();
|
||||||
|
else if (a=="--address"||a=="-a") s.address = next();
|
||||||
|
else if (a=="--port"||a=="-p") s.port = std::stoi(next());
|
||||||
|
else if (a=="--threads"||a=="-w") s.threads = (size_t)std::stoul(next());
|
||||||
|
else if (a=="--help"||a=="-h") {
|
||||||
|
std::cout << "Usage: sinja --templates DIR [options]\n"
|
||||||
|
" --address, -a Bind address (default 0.0.0.0)\n"
|
||||||
|
" --port, -p Port (default 8080)\n"
|
||||||
|
" --threads, -w Worker threads (default HW concurrency)\n";
|
||||||
|
std::exit(0);
|
||||||
|
} else throw std::runtime_error("Unknown arg: " + a);
|
||||||
|
}
|
||||||
|
if (s.template_root.empty()) throw std::runtime_error("Missing required --templates DIR");
|
||||||
|
if (!fs::is_directory(s.template_root)) throw std::runtime_error("Template directory invalid: " + s.template_root.string());
|
||||||
|
(s.template_root) = fs::weakly_canonical(s.template_root);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv){
|
||||||
|
try {
|
||||||
|
auto cfg = parse_args(argc, argv);
|
||||||
|
std::signal(SIGPIPE, SIG_IGN);
|
||||||
|
std::signal(SIGINT, on_signal);
|
||||||
|
std::signal(SIGTERM, on_signal);
|
||||||
|
|
||||||
|
Server srv(std::move(cfg));
|
||||||
|
srv.start();
|
||||||
|
|
||||||
|
while (g_running.load()) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.stop();
|
||||||
|
srv.join();
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Fatal: " << e.what() << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
997
templates/hello.txt
Normal file
997
templates/hello.txt
Normal file
@ -0,0 +1,997 @@
|
|||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
Hello, {{ name }}! Your thing is: {% for repo in repos %}{{ repo.name }},{% endfor %} {{ repos }}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user