initial commit

This commit is contained in:
Cadey Ratio 2020-05-30 10:57:18 -04:00
commit 94a0431ed5
12 changed files with 2009 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
eval "$(lorri direnv)"

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.env

1501
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

19
Cargo.toml Normal file
View File

@ -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"] }

19
LICENSE Normal file
View File

@ -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.

4
README.md Normal file
View File

@ -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.

16
TODO.org Normal file
View File

@ -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

14
nix/sources.json Normal file
View File

@ -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"
}
}

134
nix/sources.nix Normal file
View File

@ -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); }

10
shell.nix Normal file
View File

@ -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
];
}

57
src/gitea.rs Normal file
View File

@ -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,
}

232
src/main.rs Normal file
View File

@ -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(())
}