initial commit
This commit is contained in:
commit
94a0431ed5
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
.env
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "gitea-release"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Christine Dodrill <me@christine.website>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
cli-table = "0.3"
|
||||||
|
comrak = "0.7"
|
||||||
|
git2 = "0.13"
|
||||||
|
kankyo = "0.3"
|
||||||
|
reqwest = { version = "0.10", features = ["json"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
structopt = { version = "0.3", default-features = false }
|
||||||
|
tokio = { version = "0.2", features = ["macros"] }
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2020 Christine Dodrill <me@christine.website>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,4 @@
|
||||||
|
# gitea-release
|
||||||
|
|
||||||
|
A small command line tool to automate releases for [Gitea](https://gitea.io)
|
||||||
|
repositories that reads from CHANGELOG and VERSION files.
|
|
@ -0,0 +1,16 @@
|
||||||
|
#+TITLE: TODO
|
||||||
|
|
||||||
|
* Commands
|
||||||
|
** TODO delete
|
||||||
|
** TODO download
|
||||||
|
** TODO edit
|
||||||
|
** DONE info
|
||||||
|
CLOSED: [2020-05-30 Sat 10:52]
|
||||||
|
** TODO release
|
||||||
|
** TODO upload
|
||||||
|
|
||||||
|
* Core Features
|
||||||
|
** DONE Gitea API client
|
||||||
|
CLOSED: [2020-05-30 Sat 10:52]
|
||||||
|
** TODO CHANGELOG.md parsing
|
||||||
|
** TODO VERSION parsing
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"nixpkgs-mozilla": {
|
||||||
|
"branch": "master",
|
||||||
|
"description": "mozilla related nixpkgs (extends nixos/nixpkgs repo)",
|
||||||
|
"homepage": null,
|
||||||
|
"owner": "mozilla",
|
||||||
|
"repo": "nixpkgs-mozilla",
|
||||||
|
"rev": "e912ed483e980dfb4666ae0ed17845c4220e5e7c",
|
||||||
|
"sha256": "08fvzb8w80bkkabc1iyhzd15f4sm7ra10jn32kfch5klgl0gj3j3",
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://github.com/mozilla/nixpkgs-mozilla/archive/e912ed483e980dfb4666ae0ed17845c4220e5e7c.tar.gz",
|
||||||
|
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
# This file has been generated by Niv.
|
||||||
|
|
||||||
|
let
|
||||||
|
|
||||||
|
#
|
||||||
|
# The fetchers. fetch_<type> fetches specs of type <type>.
|
||||||
|
#
|
||||||
|
|
||||||
|
fetch_file = pkgs: spec:
|
||||||
|
if spec.builtin or true then
|
||||||
|
builtins_fetchurl { inherit (spec) url sha256; }
|
||||||
|
else
|
||||||
|
pkgs.fetchurl { inherit (spec) url sha256; };
|
||||||
|
|
||||||
|
fetch_tarball = pkgs: spec:
|
||||||
|
if spec.builtin or true then
|
||||||
|
builtins_fetchTarball { inherit (spec) url sha256; }
|
||||||
|
else
|
||||||
|
pkgs.fetchzip { inherit (spec) url sha256; };
|
||||||
|
|
||||||
|
fetch_git = spec:
|
||||||
|
builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; };
|
||||||
|
|
||||||
|
fetch_builtin-tarball = spec:
|
||||||
|
builtins.trace
|
||||||
|
''
|
||||||
|
WARNING:
|
||||||
|
The niv type "builtin-tarball" will soon be deprecated. You should
|
||||||
|
instead use `builtin = true`.
|
||||||
|
|
||||||
|
$ niv modify <package> -a type=tarball -a builtin=true
|
||||||
|
''
|
||||||
|
builtins_fetchTarball { inherit (spec) url sha256; };
|
||||||
|
|
||||||
|
fetch_builtin-url = spec:
|
||||||
|
builtins.trace
|
||||||
|
''
|
||||||
|
WARNING:
|
||||||
|
The niv type "builtin-url" will soon be deprecated. You should
|
||||||
|
instead use `builtin = true`.
|
||||||
|
|
||||||
|
$ niv modify <package> -a type=file -a builtin=true
|
||||||
|
''
|
||||||
|
(builtins_fetchurl { inherit (spec) url sha256; });
|
||||||
|
|
||||||
|
#
|
||||||
|
# Various helpers
|
||||||
|
#
|
||||||
|
|
||||||
|
# The set of packages used when specs are fetched using non-builtins.
|
||||||
|
mkPkgs = sources:
|
||||||
|
let
|
||||||
|
sourcesNixpkgs =
|
||||||
|
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) {};
|
||||||
|
hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
|
||||||
|
hasThisAsNixpkgsPath = <nixpkgs> == ./.;
|
||||||
|
in
|
||||||
|
if builtins.hasAttr "nixpkgs" sources
|
||||||
|
then sourcesNixpkgs
|
||||||
|
else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
|
||||||
|
import <nixpkgs> {}
|
||||||
|
else
|
||||||
|
abort
|
||||||
|
''
|
||||||
|
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
|
||||||
|
add a package called "nixpkgs" to your sources.json.
|
||||||
|
'';
|
||||||
|
|
||||||
|
# The actual fetching function.
|
||||||
|
fetch = pkgs: name: spec:
|
||||||
|
|
||||||
|
if ! builtins.hasAttr "type" spec then
|
||||||
|
abort "ERROR: niv spec ${name} does not have a 'type' attribute"
|
||||||
|
else if spec.type == "file" then fetch_file pkgs spec
|
||||||
|
else if spec.type == "tarball" then fetch_tarball pkgs spec
|
||||||
|
else if spec.type == "git" then fetch_git spec
|
||||||
|
else if spec.type == "builtin-tarball" then fetch_builtin-tarball spec
|
||||||
|
else if spec.type == "builtin-url" then fetch_builtin-url spec
|
||||||
|
else
|
||||||
|
abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
|
||||||
|
|
||||||
|
# Ports of functions for older nix versions
|
||||||
|
|
||||||
|
# a Nix version of mapAttrs if the built-in doesn't exist
|
||||||
|
mapAttrs = builtins.mapAttrs or (
|
||||||
|
f: set: with builtins;
|
||||||
|
listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
|
||||||
|
);
|
||||||
|
|
||||||
|
# fetchTarball version that is compatible between all the versions of Nix
|
||||||
|
builtins_fetchTarball = { url, sha256 }@attrs:
|
||||||
|
let
|
||||||
|
inherit (builtins) lessThan nixVersion fetchTarball;
|
||||||
|
in
|
||||||
|
if lessThan nixVersion "1.12" then
|
||||||
|
fetchTarball { inherit url; }
|
||||||
|
else
|
||||||
|
fetchTarball attrs;
|
||||||
|
|
||||||
|
# fetchurl version that is compatible between all the versions of Nix
|
||||||
|
builtins_fetchurl = { url, sha256 }@attrs:
|
||||||
|
let
|
||||||
|
inherit (builtins) lessThan nixVersion fetchurl;
|
||||||
|
in
|
||||||
|
if lessThan nixVersion "1.12" then
|
||||||
|
fetchurl { inherit url; }
|
||||||
|
else
|
||||||
|
fetchurl attrs;
|
||||||
|
|
||||||
|
# Create the final "sources" from the config
|
||||||
|
mkSources = config:
|
||||||
|
mapAttrs (
|
||||||
|
name: spec:
|
||||||
|
if builtins.hasAttr "outPath" spec
|
||||||
|
then abort
|
||||||
|
"The values in sources.json should not have an 'outPath' attribute"
|
||||||
|
else
|
||||||
|
spec // { outPath = fetch config.pkgs name spec; }
|
||||||
|
) config.sources;
|
||||||
|
|
||||||
|
# The "config" used by the fetchers
|
||||||
|
mkConfig =
|
||||||
|
{ sourcesFile ? ./sources.json
|
||||||
|
, sources ? builtins.fromJSON (builtins.readFile sourcesFile)
|
||||||
|
, pkgs ? mkPkgs sources
|
||||||
|
}: rec {
|
||||||
|
# The sources, i.e. the attribute set of spec name to spec
|
||||||
|
inherit sources;
|
||||||
|
|
||||||
|
# The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
|
||||||
|
inherit pkgs;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }
|
|
@ -0,0 +1,10 @@
|
||||||
|
let
|
||||||
|
sources = import ./nix/sources.nix;
|
||||||
|
pkgs = import <nixpkgs> { overlays = [ (import sources.nixpkgs-mozilla) ]; };
|
||||||
|
in pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
latest.rustChannels.stable.rust
|
||||||
|
openssl
|
||||||
|
pkg-config
|
||||||
|
];
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Author {
|
||||||
|
pub id: i64,
|
||||||
|
pub login: String,
|
||||||
|
pub full_name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub avatar_url: String,
|
||||||
|
pub language: String,
|
||||||
|
pub is_admin: bool,
|
||||||
|
pub last_login: String,
|
||||||
|
pub created: String,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Release {
|
||||||
|
pub id: i64,
|
||||||
|
pub tag_name: String,
|
||||||
|
pub target_commitish: String,
|
||||||
|
pub name: String,
|
||||||
|
pub body: String,
|
||||||
|
pub url: String,
|
||||||
|
pub tarball_url: String,
|
||||||
|
pub zipball_url: String,
|
||||||
|
pub draft: bool,
|
||||||
|
pub prerelease: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
pub published_at: String,
|
||||||
|
pub author: Author,
|
||||||
|
pub assets: Vec<::serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
use cli_table::{Cell, Row};
|
||||||
|
impl Release {
|
||||||
|
pub fn row(&self) -> Row {
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::new(&format!("{}", self.id), Default::default()),
|
||||||
|
Cell::new(&self.tag_name, Default::default()),
|
||||||
|
Cell::new(&self.created_at, Default::default()),
|
||||||
|
Cell::new(&self.target_commitish, Default::default()),
|
||||||
|
Cell::new(&self.author.username, Default::default()),
|
||||||
|
Cell::new(&self.name, Default::default()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct CreateRelease {
|
||||||
|
pub body: String,
|
||||||
|
pub draft: bool,
|
||||||
|
pub name: String,
|
||||||
|
pub prerelease: bool,
|
||||||
|
pub tag_name: String,
|
||||||
|
pub target_commitish: String,
|
||||||
|
}
|
|
@ -0,0 +1,232 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use reqwest::{header, Client};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use structopt::StructOpt;
|
||||||
|
|
||||||
|
mod gitea;
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
struct Common {
|
||||||
|
/// The gitea server to connect to
|
||||||
|
#[structopt(short, long, env = "GITEA_SERVER")]
|
||||||
|
server: String,
|
||||||
|
/// The gitea token to authenticate with
|
||||||
|
#[structopt(long, env = "GITEA_TOKEN")]
|
||||||
|
token: String,
|
||||||
|
/// The gitea user to authenticate as
|
||||||
|
#[structopt(short, long, env = "GITEA_AUTH_USER")]
|
||||||
|
auth_user: String,
|
||||||
|
/// The owner of the gitea repo
|
||||||
|
#[structopt(short, long, env = "GITEA_OWNER")]
|
||||||
|
owner: String,
|
||||||
|
/// The gitea repo to operate on
|
||||||
|
#[structopt(short, long, env = "GITEA_REPO")]
|
||||||
|
repo: String,
|
||||||
|
/// The version tag to operate on
|
||||||
|
#[structopt(short, long)]
|
||||||
|
tag: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name your user agent after your app?
|
||||||
|
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
|
||||||
|
|
||||||
|
fn client(c: &Common) -> Result<Client> {
|
||||||
|
let mut headers = header::HeaderMap::new();
|
||||||
|
let auth = format!("token {}", &c.token);
|
||||||
|
let auth = auth.as_str();
|
||||||
|
headers.insert(header::AUTHORIZATION, header::HeaderValue::from_str(auth)?);
|
||||||
|
Ok(Client::builder()
|
||||||
|
.user_agent(APP_USER_AGENT)
|
||||||
|
.default_headers(headers)
|
||||||
|
.build()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
struct ReleaseMeta {
|
||||||
|
/// Release name
|
||||||
|
#[structopt(short, long, default_value = "")]
|
||||||
|
name: String,
|
||||||
|
/// Draft release
|
||||||
|
#[structopt(long)]
|
||||||
|
draft: bool,
|
||||||
|
/// Pre-release (not suitable for production)
|
||||||
|
#[structopt(short, long)]
|
||||||
|
pre_release: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
#[structopt(about = "Gitea release assistant")]
|
||||||
|
enum Cmd {
|
||||||
|
/// Delete a given release from Gitea
|
||||||
|
Delete {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
common: Common,
|
||||||
|
},
|
||||||
|
/// Downloads release artifacts
|
||||||
|
Download {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
common: Common,
|
||||||
|
/// Folder to download release artifacts to
|
||||||
|
#[structopt(short, long)]
|
||||||
|
fname: PathBuf,
|
||||||
|
},
|
||||||
|
/// Edits a release's description, name and other flags
|
||||||
|
Edit {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
common: Common,
|
||||||
|
/// Release description
|
||||||
|
#[structopt(short, long, default_value = "")]
|
||||||
|
description: String,
|
||||||
|
#[structopt(flatten)]
|
||||||
|
release_meta: ReleaseMeta,
|
||||||
|
},
|
||||||
|
/// Gets release info
|
||||||
|
Info {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
common: Common,
|
||||||
|
#[structopt(long, short)]
|
||||||
|
json: bool,
|
||||||
|
},
|
||||||
|
/// Create a new tag and release on Gitea
|
||||||
|
Release {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
common: Common,
|
||||||
|
/// Changelog file to read from to create the release description
|
||||||
|
#[structopt(short, long, default_value = "./CHANGELOG.md")]
|
||||||
|
changelog: PathBuf,
|
||||||
|
#[structopt(flatten)]
|
||||||
|
release_meta: ReleaseMeta,
|
||||||
|
},
|
||||||
|
/// Uploads release artifacts to Gitea
|
||||||
|
Upload {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
common: Common,
|
||||||
|
/// The name of the file
|
||||||
|
#[structopt(short, long, default_value = "")]
|
||||||
|
name: String,
|
||||||
|
/// The description of the file
|
||||||
|
#[structopt(short, long, default_value = "")]
|
||||||
|
label: String,
|
||||||
|
/// The location of the file on the disk
|
||||||
|
#[structopt(short, long)]
|
||||||
|
fname: PathBuf,
|
||||||
|
/// Replace existing release artifacts?
|
||||||
|
#[structopt(long)]
|
||||||
|
replace: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let _ = kankyo::init();
|
||||||
|
let cmd = Cmd::from_args();
|
||||||
|
|
||||||
|
match cmd {
|
||||||
|
Cmd::Delete { common } => {
|
||||||
|
let cli = client(&common);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cmd::Info { common, json } => {
|
||||||
|
use cli_table::{Cell, Row, Table};
|
||||||
|
let cli = client(&common)?;
|
||||||
|
|
||||||
|
let releases: Vec<gitea::Release> = cli
|
||||||
|
.get(
|
||||||
|
format!(
|
||||||
|
"{}/api/v1/repos/{}/{}/releases",
|
||||||
|
&common.server, &common.owner, &common.repo
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match common.tag {
|
||||||
|
Some(tag) => {
|
||||||
|
let mut release: Option<gitea::Release> = None;
|
||||||
|
|
||||||
|
for rls in releases {
|
||||||
|
if tag == rls.tag_name {
|
||||||
|
release = Some(rls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if release.is_none() {
|
||||||
|
return Err(anyhow!("tag {} not found", tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&release)?);
|
||||||
|
} else {
|
||||||
|
let rls = release.unwrap();
|
||||||
|
let table = Table::new(
|
||||||
|
vec![
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::new(&"id", Default::default()),
|
||||||
|
Cell::new(&rls.id, Default::default()),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::new(&"author", Default::default()),
|
||||||
|
Cell::new(
|
||||||
|
&format!(
|
||||||
|
"{} - {}",
|
||||||
|
rls.author.full_name, rls.author.username
|
||||||
|
),
|
||||||
|
Default::default(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::new(&"tag", Default::default()),
|
||||||
|
Cell::new(&rls.tag_name, Default::default()),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::new(&"created at", Default::default()),
|
||||||
|
Cell::new(&rls.created_at, Default::default()),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::new(&"name", Default::default()),
|
||||||
|
Cell::new(&rls.name, Default::default()),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::new(&"body", Default::default()),
|
||||||
|
Cell::new(&rls.body, Default::default()),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
Default::default(),
|
||||||
|
)?;
|
||||||
|
table.print_stdout()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&releases)?);
|
||||||
|
} else {
|
||||||
|
let mut rows: Vec<Row> = vec![Row::new(vec![
|
||||||
|
Cell::new(&"id", Default::default()),
|
||||||
|
Cell::new(&"tag", Default::default()),
|
||||||
|
Cell::new(&"created at", Default::default()),
|
||||||
|
Cell::new(&"commit", Default::default()),
|
||||||
|
Cell::new(&"author", Default::default()),
|
||||||
|
Cell::new(&"name", Default::default()),
|
||||||
|
])];
|
||||||
|
for release in releases {
|
||||||
|
rows.push(release.row())
|
||||||
|
}
|
||||||
|
|
||||||
|
let table = Table::new(rows, Default::default())?;
|
||||||
|
table.print_stdout()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
println!("{:?}", cmd);
|
||||||
|
println!("not implemented yet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue