├── docker ├── .gitignore ├── Dockerfile └── build.sh ├── Cargo.toml ├── .gitmodules ├── .github └── dependabot.yml ├── .gitignore ├── codecov.yml ├── handlers ├── Cargo.toml └── src │ └── lib.rs ├── git ├── Cargo.toml └── src │ └── lib.rs ├── CONTRIBUTING.md ├── server ├── tests │ └── integration_tests.rs ├── Cargo.toml └── src │ └── main.rs ├── LICENSE ├── .devcontainer ├── LICENCE ├── devcontainer.json └── Dockerfile ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── README.md └── .travis.yml /docker/.gitignore: -------------------------------------------------------------------------------- 1 | gitkv 2 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ADD gitkv /gitkv 3 | ENTRYPOINT ["/gitkv"] 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "git", 5 | "handlers", 6 | "server", 7 | ] 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "server/test/fixtures"] 2 | path = server/test/fixtures 3 | url = https://github.com/intenthq/gitkv-test-fixtures.git 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70...100" 9 | 10 | status: 11 | project: yes 12 | patch: yes 13 | changes: no 14 | 15 | parsers: 16 | gcov: 17 | branch_detection: 18 | conditional: yes 19 | loop: yes 20 | method: no 21 | macro: no 22 | 23 | comment: 24 | layout: "header, diff" 25 | behavior: default 26 | require_changes: no 27 | -------------------------------------------------------------------------------- /handlers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "handlers" 3 | version = "0.1.0" 4 | authors = ["Intent HQ "] 5 | edition = "2018" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | git = { path = "../git" } 10 | actix = "0.10.0" 11 | 12 | # When building for musl (ie. a static binary), we opt into the "vendored" 13 | # feature flag of openssl-sys which compiles libopenssl statically for us. 14 | [target.'cfg(target_env="musl")'.dependencies.openssl-sys] 15 | features = ["vendored"] 16 | version = "0.9.58" 17 | -------------------------------------------------------------------------------- /git/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "git" 3 | version = "0.1.0" 4 | authors = ["Intent HQ "] 5 | edition = "2018" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | git2 = "0.13.10" 10 | 11 | # When building for musl (ie. a static binary), we opt into the "vendored" 12 | # feature flag of openssl-sys which compiles libopenssl statically for us. 13 | [target.'cfg(target_env="musl")'.dependencies.openssl-sys] 14 | features = ["vendored"] 15 | version = "0.9.58" 16 | 17 | [dev-dependencies] 18 | tempfile = "3.1.0" 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Gitkv 2 | 3 | ## Code of Conduct 4 | 5 | This project and everyone participating in it is governed by the [Gitkv Code of Conduct](CODE_OF_CONDUCT.md). 6 | By participating, you are expected to uphold this code. 7 | 8 | ## How Can I Contribute 9 | 10 | Any contribution is welcome, raise a bug (and fix it! 😄) request or add a new feature, add some documentation... 11 | 12 | You can also take a look at the [issues](https://github.com/intenthq/gitkv/issues) and pick the one you like better. 13 | 14 | If you are going to contribute, we ask you to do the following: 15 | - Use `rustfmt` to format your code 16 | - Cover the logic with enough tests 17 | - Write decent commit messages 18 | -------------------------------------------------------------------------------- /server/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | 3 | #[test] 4 | fn fails_to_start_with_invalid_host() { 5 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); 6 | let assert = cmd.arg("--host=").assert(); 7 | 8 | assert.failure(); 9 | } 10 | 11 | #[test] 12 | fn fails_to_start_with_invalid_port() { 13 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); 14 | let assert = cmd.arg("--port=").assert(); 15 | 16 | assert.failure(); 17 | } 18 | 19 | #[test] 20 | fn fails_to_start_with_invalid_repo_root() { 21 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); 22 | let assert = cmd.arg("--repo-root=").assert(); 23 | 24 | assert.failure(); 25 | } 26 | 27 | // FIXME: How to test with a process that never ends unless terminated? 28 | // #[test] 29 | // fn can_cat_file() { 30 | // let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); 31 | 32 | // let assert = cmd 33 | // .arg("--repo-root=test") 34 | // .assert(); 35 | 36 | // assert.success(); 37 | // } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 IntentHQ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gitkv" 3 | version = "0.1.0" 4 | description = "gitkv is a server for using git as a key value store for text files" 5 | authors = ["Intent HQ "] 6 | edition = "2018" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | git = { path = "../git" } 11 | handlers = { path = "../handlers" } 12 | actix = "0.10.0" # Actor communication between handlers and Git 13 | actix-rt = "1.1.1" # Actix macros 14 | actix-web = "3.0.2" # Web framework 15 | clap = "4.1.6" # CLI argument parsing 16 | env_logger = "0.7.1" # Configure logging level with env variables 17 | log = "0.4.11" # Logging facade 18 | serde = "1.0.114" # Serialisation of results 19 | serde_derive = "1.0.114" # Macros for deriving Serde converstions 20 | serde_json = "1.0.57" # JSON support for Serde 21 | 22 | [dev-dependencies] 23 | assert_cmd = "1.0.1" # Run our binaries from the integration tests 24 | predicates = "1.0.5" # Assert on binaries being run in the integration tests 25 | 26 | # When building for musl (ie. a static binary), we opt into the "vendored" 27 | # feature flag of openssl-sys which compiles libopenssl statically for us. 28 | [target.'cfg(target_env="musl")'.dependencies.openssl-sys] 29 | features = ["vendored"] 30 | version = "0.9.58" 31 | -------------------------------------------------------------------------------- /.devcontainer/LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Source: https://github.com/microsoft/vscode-dev-containers/tree/5f08ac062ef11a3675f4373622c4ed4f67170cb7/containers/rust 3 | 4 | Copyright (c) Microsoft Corporation. All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE 23 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust", 3 | "dockerFile": "Dockerfile", 4 | "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], 5 | 6 | // Set *default* container specific settings.json values on container create. 7 | "settings": { 8 | "terminal.integrated.shell.linux": "/bin/bash", 9 | "lldb.executable": "/usr/bin/lldb", 10 | // VS Code don't watch files under ./target 11 | "files.watcherExclude": { 12 | "**/target/**": true 13 | } 14 | }, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": [ 18 | "rust-lang.rust", 19 | "bungcip.better-toml", 20 | "vadimcn.vscode-lldb", 21 | // (Optional) Displays the current CPU stats, memory/disk consumption, clock freq. etc. of the container host in the VS Code status bar. 22 | "mutantdino.resourcemonitor" 23 | ] 24 | 25 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 26 | // "forwardPorts": [], 27 | 28 | // Use 'postCreateCommand' to run commands after the container is created. 29 | // "postCreateCommand": "rustc --version", 30 | 31 | // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. 32 | // "remoteUser": "vscode" 33 | } 34 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Make all paths relative to the dir of this script. 6 | cd "$(dirname "$0")" > /dev/null 7 | 8 | if [[ $# -ne 1 ]] && [[ $# -ne 2 ]]; then 9 | echo "Expected 1 or 2 parameters, but got $#." 10 | 11 | cat < [] 13 | 14 | Builds the Gitkv docker image for the given version and tags the container as 15 | 'intenthq/gitkv:'. 16 | 17 | The binary is copied from the Cargo release build for the given target. 18 | 19 | Parameters: 20 | VERSION: The version to build, in the format 'vX.Y.Z' — note the 'v' prefix 21 | is needed. 22 | TARGET: The binary target triple to build into the image, 23 | eg. 'x86_64-unknown-linux-musl'. If not given, will assume this 24 | machine's target triple. 25 | USAGE_DOC 26 | exit 1 27 | fi 28 | 29 | VERSION=$1 30 | 31 | if [ -z $2 ]; then 32 | BINARY_PATH=../target/release/gitkv 33 | else 34 | TARGET=$2 35 | BINARY_PATH=../target/$TARGET/release/gitkv 36 | fi 37 | 38 | if [ ! -f $BINARY_PATH ]; then 39 | echo 2>&1 "Could not find binary at expected path '$BINARY_PATH'. Have you run 'cargo build --release'?" 40 | exit 1 41 | fi 42 | 43 | echo "Building 'gitkv:$VERSION' Docker container..." 44 | cp $BINARY_PATH ./gitkv 45 | docker build -t intenthq/gitkv:$VERSION . 46 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1 2 | 3 | # This Dockerfile adds a non-root user with sudo access. Update the “remoteUser” property in 4 | # devcontainer.json to use it. More info: https://aka.ms/vscode-remote/containers/non-root-user. 5 | ARG USERNAME=vscode 6 | ARG USER_UID=1000 7 | ARG USER_GID=$USER_UID 8 | 9 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 10 | RUN apt-get update \ 11 | && export DEBIAN_FRONTEND=noninteractive \ 12 | && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \ 13 | # 14 | # Verify git, needed tools installed 15 | && apt-get -y install git openssh-client cmake less iproute2 procps lsb-release \ 16 | # 17 | # Install lldb, vadimcn.vscode-lldb VSCode extension dependencies 18 | && apt-get install -y lldb python3-minimal libpython3.7 \ 19 | # 20 | # Install Rust components 21 | && rustup update 2>&1 \ 22 | && rustup component add rls rust-analysis rust-src rustfmt clippy 2>&1 \ 23 | # 24 | # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user. 25 | && groupadd --gid $USER_GID $USERNAME \ 26 | && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \ 27 | # [Optional] Add sudo support for the non-root user 28 | && apt-get install -y sudo \ 29 | && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\ 30 | && chmod 0440 /etc/sudoers.d/$USERNAME \ 31 | # 32 | # Clean up 33 | && apt-get autoremove -y \ 34 | && apt-get clean -y \ 35 | && rm -rf /var/lib/apt/lists/* 36 | 37 | # [Optional] Uncomment this section to install additional OS packages. 38 | # RUN apt-get update \ 39 | # && export DEBIAN_FRONTEND=noninteractive \ 40 | # && apt-get -y install --no-install-recommends 41 | 42 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug unit tests in library 'git'", 11 | "cargo": { 12 | "args": [ 13 | "test", 14 | "--no-run", 15 | "--lib", 16 | "--package=git" 17 | ], 18 | "filter": { 19 | "name": "git", 20 | "kind": "lib" 21 | } 22 | }, 23 | "args": [], 24 | "cwd": "${workspaceFolder}" 25 | }, 26 | { 27 | "type": "lldb", 28 | "request": "launch", 29 | "name": "Debug unit tests in library 'handlers'", 30 | "cargo": { 31 | "args": [ 32 | "test", 33 | "--no-run", 34 | "--lib", 35 | "--package=handlers" 36 | ], 37 | "filter": { 38 | "name": "handlers", 39 | "kind": "lib" 40 | } 41 | }, 42 | "args": [], 43 | "cwd": "${workspaceFolder}" 44 | }, 45 | { 46 | "type": "lldb", 47 | "request": "launch", 48 | "name": "Debug executable 'gitkv'", 49 | "cargo": { 50 | "args": [ 51 | "build", 52 | "--bin=gitkv", 53 | "--package=gitkv" 54 | ], 55 | "filter": { 56 | "name": "gitkv", 57 | "kind": "bin" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}" 62 | }, 63 | { 64 | "type": "lldb", 65 | "request": "launch", 66 | "name": "Debug unit tests in executable 'gitkv'", 67 | "cargo": { 68 | "args": [ 69 | "test", 70 | "--no-run", 71 | "--bin=gitkv", 72 | "--package=gitkv" 73 | ], 74 | "filter": { 75 | "name": "gitkv", 76 | "kind": "bin" 77 | } 78 | }, 79 | "args": [], 80 | "cwd": "${workspaceFolder}" 81 | } 82 | ] 83 | } -------------------------------------------------------------------------------- /handlers/src/lib.rs: -------------------------------------------------------------------------------- 1 | use actix::dev::MessageResponse; 2 | use actix::{Actor, Context, Handler, Message}; 3 | use git::{git2::Repository, GitOps, LibGitOps}; 4 | use std::collections::HashMap; 5 | use std::path::PathBuf; 6 | 7 | #[derive(Message)] 8 | #[rtype(result = "CatFileResponse")] 9 | pub struct CatFile { 10 | pub repo_key: String, 11 | pub reference: String, 12 | pub path: PathBuf, 13 | } 14 | 15 | #[derive(MessageResponse)] 16 | pub struct CatFileResponse(pub Result, String>); 17 | 18 | #[derive(Message)] 19 | #[rtype(result = "LsDirResponse")] 20 | pub struct LsDir { 21 | pub repo_key: String, 22 | pub reference: String, 23 | pub path: PathBuf, 24 | } 25 | 26 | #[derive(MessageResponse)] 27 | pub struct LsDirResponse(pub Result, String>); 28 | 29 | #[derive(Message)] 30 | #[rtype(result = "ResolveRefResponse")] 31 | pub struct ResolveRef { 32 | pub repo_key: String, 33 | pub reference: String, 34 | } 35 | 36 | #[derive(MessageResponse)] 37 | pub struct ResolveRefResponse(pub Result); 38 | 39 | pub struct GitRepos { 40 | repos: HashMap, 41 | ops: Box, 42 | } 43 | 44 | impl Actor for GitRepos { 45 | type Context = Context; 46 | } 47 | 48 | impl GitRepos { 49 | pub fn new(repos: HashMap) -> GitRepos { 50 | GitRepos { 51 | repos, 52 | ops: Box::new(LibGitOps {}), 53 | } 54 | } 55 | } 56 | 57 | impl Handler for GitRepos { 58 | type Result = CatFileResponse; 59 | 60 | fn handle(&mut self, req: CatFile, _: &mut Self::Context) -> Self::Result { 61 | CatFileResponse(match self.repos.get(&req.repo_key) { 62 | Some(repo) => self 63 | .ops 64 | .cat_file(repo, &req.reference, &req.path) 65 | .map_err(|x| x.to_string()), 66 | None => Err(format!("No repo found with name '{}'", &req.repo_key)), 67 | }) 68 | } 69 | } 70 | 71 | impl Handler for GitRepos { 72 | type Result = LsDirResponse; 73 | 74 | fn handle(&mut self, req: LsDir, _: &mut Self::Context) -> Self::Result { 75 | LsDirResponse(match self.repos.get(&req.repo_key) { 76 | Some(repo) => self 77 | .ops 78 | .ls_dir(repo, &req.reference, &req.path) 79 | .map_err(|x| x.to_string()), 80 | None => Err(format!("No repo found with name '{}'", &req.repo_key)), 81 | }) 82 | } 83 | } 84 | 85 | impl Handler for GitRepos { 86 | type Result = ResolveRefResponse; 87 | 88 | fn handle(&mut self, req: ResolveRef, _: &mut Self::Context) -> Self::Result { 89 | ResolveRefResponse(match self.repos.get(&req.repo_key) { 90 | Some(repo) => self 91 | .ops 92 | .resolve_ref(repo, &req.reference) 93 | .map_err(|x| x.to_string()), 94 | None => Err(format!("No repo found with name '{}'", &req.repo_key)), 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at albert.pastrana@intenthq.com or 59 | nathan.kleyn@intenthq.com. All complaints will be reviewed and investigated and 60 | will result in a response that is deemed necessary and appropriate to the 61 | circumstances. The project team is obligated to maintain confidentiality with 62 | regard to the reporter of an incident. Further details of specific enforcement 63 | policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitkv [![Build Status](https://travis-ci.org/intenthq/gitkv.svg?branch=master)](https://travis-ci.org/intenthq/gitkv)[![codecov](https://codecov.io/gh/intenthq/gitkv/branch/master/graph/badge.svg)](https://codecov.io/gh/intenthq/gitkv) 2 | 3 | Gitkv is a server for using git as a key value store for text files 4 | 5 | ## Installation 6 | 7 | ### Binary 8 | 9 | Releases of Gitkv are available as pre-compiled static binaries on the corresponding GitHub release. Simply download the appropriate build for your machine and make sure it's in your PATH (or use it directly). 10 | 11 | ### Docker 12 | 13 | Gitkv is also distributed as a Docker image that can be pulled from [Docker Hub](https://hub.docker.com/r/intenthq/gitkv): 14 | 15 | ```sh 16 | docker run intenthq/gitkv 17 | ``` 18 | 19 | ### Source 20 | 21 | To run Gitkv from source first [install Rust](https://www.rust-lang.org/tools/install). 22 | 23 | This is a standard Cargo project — [here is a link to the Rust documentation on how to use Cargo](https://doc.rust-lang.org/cargo/), but some common tasks you may wish to use are as follows: 24 | 25 | * `cargo build` — build the Gitkv binary, but in debug mode (unoptimised). 26 | * `cargo build --release` — same as the above, but in release mode (optimised). 27 | * `cargo run` — build and run the binary in one step. 28 | * `cargo test` — run the tests. 29 | 30 | ## Usage 31 | 32 | ``` 33 | gitkv is a server for using git as a key value store for text files 34 | 35 | USAGE: 36 | gitkv [OPTIONS] 37 | 38 | FLAGS: 39 | --help Prints help information 40 | -V, --version Prints version information 41 | 42 | OPTIONS: 43 | -h, --host host to listen to [default: localhost] 44 | -p, --port port to listen to [default: 7791] 45 | -r, --repo-root path where the different repositories are located [default: ./] 46 | ``` 47 | 48 | You can modify the amount of logging with the `RUST_LOG` parameter: 49 | 50 | For basic application info (default): `RUST_LOG=gitkv=info ./gitkv` 51 | Including incoming HTTP requests: `RUST_LOG=info ./gitkv` 52 | For more information check [env_logger](https://docs.rs/env_logger/*/env_logger/index.html)'s documentation. 53 | 54 | ## Security 55 | 56 | Note that git stores all the content plain so that it's not a good place to store secrets and sensitive information. 57 | 58 | There are solutions that offer [encrypted git](https://keybase.io/blog/encrypted-git-for-everyone), but we do recommend to store the secrets using a different solution like [Vault](https://www.vaultproject.io/). 59 | 60 | ## When is it useful? 61 | 62 | This server can be used when you need a data store that can easily support: 63 | - Small to medium text based data 64 | - Versioning of this data 65 | - Data follows some kind of hierarchy 66 | - Access using HTTP + Ability to use the configuration without a central server (just the git repo itself) 67 | - And you can't use GitHub/GitLab api directly 68 | 69 | ### Why git? 70 | 71 | [Git](https://git-scm.com/) is an excellent version control system with lots of tooling around to compare files, have approval mechanisms for the data you store (i.e. pull requests) or to have different flows for editing your files. 72 | 73 | Although it's not designed with performance in mind, for some use cases like pulling configuration files with a specific version. 74 | 75 | Some people has previously mentioned the idea to use [git as a database](https://www.kenneth-truyers.net/2016/10/13/git-nosql-database/) with some pretty interesting thoughts. 76 | 77 | ### Alternatives/Similar projects 78 | 79 | - GitHub/GitLab/Bitbucket APIs 80 | - https://github.com/gitpython-developers/gitdb 81 | - https://github.com/attic-labs/noms 82 | - https://github.com/mirage/irmin 83 | - http://orpheus-db.github.io/ 84 | - https://www.klonio.com/ 85 | 86 | ## How to contribute 87 | 88 | Any contribution will be welcome, please refer to our [contributing guidelines](CONTRIBUTING.md) for more information. 89 | 90 | # License 91 | 92 | This project is [licensed under the MIT license](LICENSE). 93 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | os: linux 3 | dist: xenial 4 | osx_image: xcode10.2 5 | 6 | # Caching strategy from https://levans.fr/rust_travis_cache.html 7 | # We avoid using cache: cargo which blows up the cache. 8 | cache: 9 | - directories: 10 | - "${HOME}/.cargo" 11 | - "${HOME}/kcov/" 12 | 13 | before_cache: 14 | - "rm -rf ${HOME}/.cargo/registry" 15 | 16 | addons: 17 | apt: 18 | packages: 19 | # For building MUSL static builds on Linux. 20 | - musl-tools 21 | # packages for kcov 22 | - cmake 23 | - libcurl4-openssl-dev 24 | - libelf-dev 25 | - libdw-dev 26 | - binutils-dev 27 | sources: 28 | - kalakris-cmake 29 | homebrew: 30 | packages: 31 | - cmake 32 | 33 | jobs: 34 | fast_finish: true 35 | allow_failures: 36 | - rust: beta 37 | - rust: nightly 38 | include: 39 | - os: linux 40 | rust: nightly 41 | env: TARGET=x86_64-unknown-linux-musl 42 | - os: linux 43 | rust: beta 44 | env: TARGET=x86_64-unknown-linux-musl 45 | - os: linux 46 | rust: stable 47 | env: TARGET=x86_64-unknown-linux-musl 48 | - os: osx 49 | rust: nightly 50 | env: TARGET=x86_64-apple-darwin 51 | - os: osx 52 | rust: beta 53 | env: TARGET=x86_64-apple-darwin 54 | - os: osx 55 | rust: stable 56 | env: TARGET=x86_64-apple-darwin 57 | 58 | before_script: 59 | - rustup target add $TARGET 60 | - export PATH=$HOME/.cargo/bin:$PATH 61 | # Cargo install fails if a crate is already installed — which it will be if it was cached. 62 | # It recommends to use `--force`, but that _always_ recompiles from scratch which takes ~5 minutes. 63 | # FIXME: Follow https://github.com/rust-lang/cargo/issues/6667 for a WIP fix on Cargo for this. 64 | - cargo install cargo-audit || echo "cargo-update already installed" 65 | - cargo install cargo-travis || echo "cargo-travis already installed" 66 | - rustup component add clippy 67 | - rustup component add rustfmt 68 | 69 | script: 70 | - cargo build --target $TARGET --release 71 | - cargo clippy --all-targets --all-features -- -D warnings 72 | - cargo test --target $TARGET --release 73 | - cargo fmt --all -- --check 74 | - cargo audit 75 | # Make a copy of the binary named nicely for the GitHub release, if needed. 76 | - cp target/$TARGET/release/gitkv target/$TARGET/release/gitkv-$TARGET && chmod +x target/$TARGET/release/gitkv-$TARGET 77 | 78 | after_success: 79 | - test $TRAVIS_OS_NAME = "linux" && cargo coverage --target $TARGET --kcov-build-location "${HOME}/kcov/" 80 | - test $TRAVIS_OS_NAME = "linux" && bash <(curl -s https://codecov.io/bash) 81 | 82 | deploy: 83 | # Push binaries to the GitHub release for this version. 84 | - provider: releases 85 | edge: true 86 | token: 87 | secure: YP0Ieo3OYZaBkvZ/kXIUzY3RW+1u3m7/HXuB89kKT7Bi+DZrmYrbFngLG/sypeDaQudflyqWEKdszuu1OKFhRFBeSHlpivKPphfn8ghRvfbCoABn5gcegNSLyx2k+zWSjPTo6H2dR+dOtg0d64pxTXD3GJhkIotoxR3ODYlWqtojuBGMpBtN8fxF3ofWGAUTe2Ix5VTnrfghSQwUHM86ERNOLivhOJoxTHNUGtU421q9TgS7rrE1xssydJ+Iemcti8zH3G+s0QHEytqJnxsSbWotiixx4JcY+qnZ8PtGbA2DVfoWokfCRPRh6XOHB27Jt8graiG/wnl/RI30FgbBWv2Y25rXXX0V6Ql/773EGzayV8oP9WkYSYrwfFov0/zzM+AaQdbLnjTXJ7JbfMJPSKT6j5bbgY8jTAubnIDzwhaUfy4Hq4gkofOOksTJ9ef09lklQctEb1ISx+V/+KGAXveGIsQq6g3ap7EjvlF+M1XD6VHotlVlA4Xd1Z+TR5+Ae2AXZwpjT53TmNT3BPFAAl4CLqbah/poo8/ML9t2X+SyA2o6TaW3sqSBA5l3sFLCQjN805sVM/tUMk8OBRHwBoW18zg9jfDj9vbuIO7oq0CP8AfcN/25/QX+7QfZ9lEvOCQSaXzKIN5ToOix35/wmJzL5af05cYjz1rYW1Un1B4= 88 | file: target/$TARGET/release/gitkv-$TARGET 89 | on: 90 | rust: stable 91 | tags: true 92 | branch: /^v\d+\.\d+\.\d+.*$/ 93 | # Push Docker container to Docker Hub tagger for this version. 94 | - provider: script 95 | edge: true 96 | script: ./docker/build.sh $TRAVIS_BRANCH $TARGET && docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" && docker push intenthq/gitkv:$TRAVIS_BRANCH 97 | on: 98 | condition: $TRAVIS_OS_NAME = "linux" 99 | rust: stable 100 | tags: true 101 | branch: /^v\d+\.\d+\.\d+.*$/ 102 | -------------------------------------------------------------------------------- /git/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub extern crate git2; 2 | 3 | use git2::{Error, Repository}; 4 | use std::{ 5 | collections::HashMap, 6 | fs, 7 | path::{Path, PathBuf}, 8 | }; 9 | 10 | pub trait GitOps { 11 | fn cat_file(&self, repo: &Repository, reference: &str, path: &Path) -> Result, Error>; 12 | 13 | fn ls_dir( 14 | &self, 15 | repo: &Repository, 16 | reference: &str, 17 | path: &Path, 18 | ) -> Result, Error>; 19 | 20 | fn resolve_ref(&self, repo: &Repository, reference: &str) -> Result; 21 | } 22 | 23 | pub struct LibGitOps; 24 | 25 | impl GitOps for LibGitOps { 26 | /// Given an existing git repository, it will read the blob that the reference and the filename 27 | /// point to and return it as a String. 28 | fn cat_file(&self, repo: &Repository, reference: &str, path: &Path) -> Result, Error> { 29 | let git_ref = repo.revparse_single(reference)?; 30 | let tree = git_ref.peel_to_tree()?; 31 | let te = tree.get_path(path)?; 32 | 33 | repo.find_blob(te.id()).map(|x| x.content().to_owned()) 34 | } 35 | 36 | fn ls_dir( 37 | &self, 38 | repo: &Repository, 39 | reference: &str, 40 | directory: &Path, 41 | ) -> Result, Error> { 42 | let git_ref = repo.revparse_single(reference)?; 43 | let tree = git_ref.peel_to_tree()?; 44 | let path = std::path::Path::new(directory); 45 | let te = tree.get_path(path)?; 46 | 47 | repo.find_tree(te.id()).map({ 48 | |tree| { 49 | tree.iter() 50 | .flat_map(|tree_entry| tree_entry.name().map(|name| name.into())) 51 | .collect() 52 | } 53 | }) 54 | } 55 | 56 | fn resolve_ref(&self, repo: &Repository, reference: &str) -> Result { 57 | let git_ref = repo.revparse_single(reference)?; 58 | git_ref.peel_to_commit().map(|c| c.id().to_string()) 59 | } 60 | } 61 | 62 | pub fn load_repos(root_path: &Path) -> HashMap { 63 | fs::read_dir(root_path) 64 | .expect("Failed to read repos directory") 65 | .filter_map(|entry| { 66 | entry.ok().and_then(|e| { 67 | let path = e.path(); 68 | if path.is_dir() { 69 | let local_path = path.clone(); 70 | let repo_name = local_path 71 | .file_stem() 72 | .and_then(|name| name.to_os_string().into_string().ok()); 73 | 74 | repo_name.and_then(|name| Repository::open(path).ok().map(|repo| (name, repo))) 75 | } else { 76 | None 77 | } 78 | }) 79 | }) 80 | .collect() 81 | } 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | 86 | extern crate tempfile; 87 | 88 | use super::{GitOps, LibGitOps}; 89 | 90 | use git2::{Repository, Signature, Time}; 91 | use std::fs; 92 | use std::io::Write; 93 | use std::path::{Path, PathBuf}; 94 | use std::str; 95 | 96 | // cat tests 97 | 98 | fn git_cat_file( 99 | repo_path: &Repository, 100 | reference: &str, 101 | path: &str, 102 | ) -> Result, git2::Error> { 103 | let gh = LibGitOps {}; 104 | gh.cat_file(repo_path, reference, &PathBuf::from(path)) 105 | } 106 | 107 | fn git_cat_file_err(repo_path: &Repository, reference: &str, path: &str) -> git2::Error { 108 | git_cat_file(repo_path, reference, path).expect_err("should be an error") 109 | } 110 | 111 | #[test] 112 | fn test_cat_file_with_valid_branch_ref_and_file() { 113 | with_repo("file content", "dir/existing.file", |repo, _| { 114 | let res = git_cat_file(repo, "master", "dir/existing.file").expect("should be ok"); 115 | assert_eq!( 116 | std::str::from_utf8(&res).expect("valid utf8"), 117 | "file content" 118 | ); 119 | }) 120 | } 121 | 122 | #[test] 123 | fn test_cat_file_with_valid_sha_ref_and_file() { 124 | with_repo("file content", "dir/existing.file", |repo, commit_sha| { 125 | let res = git_cat_file(repo, commit_sha, "dir/existing.file").expect("should be ok"); 126 | assert_eq!( 127 | std::str::from_utf8(&res).expect("valid utf8"), 128 | "file content" 129 | ); 130 | }) 131 | } 132 | 133 | #[test] 134 | fn test_cat_file_with_valid_tag_ref_and_file() { 135 | with_repo("file content", "dir/existing.file", |repo, _| { 136 | let res = 137 | git_cat_file(repo, "this-is-a-tag", "dir/existing.file").expect("should be ok"); 138 | assert_eq!( 139 | std::str::from_utf8(&res).expect("valid utf8"), 140 | "file content" 141 | ); 142 | }) 143 | } 144 | 145 | #[test] 146 | fn test_cat_file_with_non_existing_ref() { 147 | with_repo("file content", "dir/existing.file", |repo, _| { 148 | let res = git_cat_file_err(repo, "idonot/exist", "dir/existing.file"); 149 | assert_eq!(res.code(), git2::ErrorCode::NotFound); 150 | assert_eq!(res.class(), git2::ErrorClass::Reference); 151 | }) 152 | } 153 | 154 | #[test] 155 | fn test_cat_file_with_non_existing_file() { 156 | with_repo("file content", "dir/existing.file", |repo, _| { 157 | let res = git_cat_file_err(repo, "master", "non-existing.file"); 158 | assert_eq!(res.code(), git2::ErrorCode::NotFound); 159 | assert_eq!(res.class(), git2::ErrorClass::Tree); 160 | }) 161 | } 162 | 163 | #[test] 164 | fn test_cat_file_with_dir() { 165 | with_repo("content", "dir/existing.file", |repo, _| { 166 | let res = git_cat_file_err(repo, "master", "dir"); 167 | assert_eq!(res.code(), git2::ErrorCode::NotFound); 168 | assert_eq!(res.class(), git2::ErrorClass::Invalid); 169 | }) 170 | } 171 | 172 | // ls tests 173 | 174 | // Converts a vec of string like things into a vec of owned paths. 175 | macro_rules! as_path_bufs { 176 | ($vec: expr) => {{ 177 | $vec.iter().map(PathBuf::from).collect::>() 178 | }}; 179 | } 180 | 181 | fn git_ls_dir( 182 | repo_path: &Repository, 183 | reference: &str, 184 | path: &str, 185 | ) -> Result, git2::Error> { 186 | let gh = LibGitOps {}; 187 | gh.ls_dir(repo_path, reference, &PathBuf::from(path)) 188 | } 189 | 190 | fn git_ls_dir_err(repo_path: &Repository, reference: &str, directory: &str) -> git2::Error { 191 | git_ls_dir(repo_path, reference, directory).expect_err("should be an error") 192 | } 193 | 194 | #[test] 195 | fn test_ls_dir_with_valid_branch_ref_and_dir() { 196 | with_repo("file content", "dir/existing.file", |repo, _| { 197 | let res = git_ls_dir(repo, "master", "dir").expect("should be ok"); 198 | assert_eq!(res, as_path_bufs!(vec!["existing.file"])); 199 | }) 200 | } 201 | 202 | #[test] 203 | fn test_ls_dir_with_valid_sha_ref_and_file() { 204 | with_repo("file content", "dir/existing.file", |repo, commit_sha| { 205 | let res = git_ls_dir(repo, commit_sha, "dir").expect("should be ok"); 206 | assert_eq!(res, as_path_bufs!(vec!["existing.file"])); 207 | }) 208 | } 209 | 210 | #[test] 211 | fn test_ls_dir_with_valid_tag_ref_and_file() { 212 | with_repo("file content", "dir/existing.file", |repo, _| { 213 | let res = git_ls_dir(repo, "this-is-a-tag", "dir").expect("should be ok"); 214 | assert_eq!(res, as_path_bufs!(vec!["existing.file"])); 215 | }) 216 | } 217 | 218 | #[test] 219 | fn test_ls_dir_with_non_existing_ref() { 220 | with_repo("file content", "dir/existing.file", |repo, _| { 221 | let res = git_ls_dir_err(repo, "idonot/exist", "dir"); 222 | assert_eq!(res.code(), git2::ErrorCode::NotFound); 223 | assert_eq!(res.class(), git2::ErrorClass::Reference); 224 | }) 225 | } 226 | 227 | #[test] 228 | fn test_ls_dir_with_non_existing_dir() { 229 | with_repo("file content", "dir/existing.file", |repo, _| { 230 | let res = git_ls_dir_err(repo, "master", "non-existing"); 231 | assert_eq!(res.code(), git2::ErrorCode::NotFound); 232 | assert_eq!(res.class(), git2::ErrorClass::Tree); 233 | }) 234 | } 235 | 236 | #[test] 237 | fn test_ls_dir_with_file() { 238 | with_repo("content", "dir/existing.file", |repo, _| { 239 | let res = git_ls_dir_err(repo, "master", "dir/existing.file"); 240 | assert_eq!(res.code(), git2::ErrorCode::NotFound); 241 | assert_eq!(res.class(), git2::ErrorClass::Invalid); 242 | }) 243 | } 244 | 245 | // resolve tests 246 | 247 | fn git_resolve(repo_path: &Repository, reference: &str) -> Result { 248 | let gh = LibGitOps {}; 249 | gh.resolve_ref(repo_path, reference) 250 | } 251 | 252 | #[test] 253 | fn test_resolve_ref_with_valid_branch_ref() { 254 | with_repo("file content", "dir/existing.file", |repo, commit_sha| { 255 | let res = git_resolve(repo, "master").expect("should be ok"); 256 | assert_eq!(res, commit_sha); 257 | }) 258 | } 259 | 260 | #[test] 261 | fn test_resolve_ref_with_valid_sha() { 262 | with_repo("file content", "dir/existing.file", |repo, commit_sha| { 263 | let res = git_resolve(repo, commit_sha).expect("should be ok"); 264 | assert_eq!(res, commit_sha); 265 | }) 266 | } 267 | 268 | #[test] 269 | fn test_resolve_ref_with_valid_tag_ref() { 270 | with_repo("file content", "dir/existing.file", |repo, commit_sha| { 271 | let res = git_resolve(repo, "this-is-a-tag").expect("should be ok"); 272 | assert_eq!(res, commit_sha); 273 | }) 274 | } 275 | 276 | #[test] 277 | fn test_resolve_ref_with_non_existing_ref() { 278 | with_repo("file content", "dir/existing.file", |repo, _| { 279 | let res = git_resolve(repo, "idonot/exist").expect_err("should be an error"); 280 | assert_eq!(res.code(), git2::ErrorCode::NotFound); 281 | assert_eq!(res.class(), git2::ErrorClass::Reference); 282 | }) 283 | } 284 | 285 | pub fn with_repo(file_contents: &str, file: &str, callback: F) 286 | where 287 | F: Fn(&Repository, &str), 288 | { 289 | let dir = tempfile::Builder::new() 290 | .prefix("testgitrepo") 291 | .tempdir() 292 | .expect("can't create tmp dir"); 293 | 294 | let repo = Repository::init(&dir).expect("can't initialise repository"); 295 | 296 | let path = dir.path().join(file); 297 | path.parent().map(|parent| fs::create_dir_all(&parent)); 298 | fs::File::create(path) 299 | .and_then(|mut file| file.write_all(file_contents.as_bytes())) 300 | .expect("can't write file contents"); 301 | 302 | let time = Time::new(123_456_789, 0); 303 | let sig = Signature::new("Foo McBarson", "foo.mcbarson@iamarealboy.net", &time) 304 | .expect("couldn't create signature for commit"); 305 | 306 | let commit_oid = repo 307 | .index() 308 | .and_then(|mut index| { 309 | index 310 | .add_path(Path::new(file)) 311 | .expect("can't add file to index"); 312 | 313 | index 314 | .write_tree() 315 | .and_then(|tid| repo.find_tree(tid)) 316 | .and_then(|tree| { 317 | repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[]) 318 | }) 319 | }) 320 | .expect("can't do first commit"); 321 | 322 | let commit = repo 323 | .find_object(commit_oid, None) 324 | .expect("Could not find first commit."); 325 | repo.tag("this-is-a-tag", &commit, &sig, "This is a tag.", false) 326 | .expect("Could not create tag."); 327 | 328 | let commit_sha = format!("{}", commit_oid); 329 | 330 | callback(&repo, &commit_sha); 331 | dir.close().expect("couldn't close the dir"); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /server/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | #[macro_use] 4 | extern crate serde_derive; 5 | extern crate actix_web; 6 | #[macro_use] 7 | extern crate log; 8 | extern crate env_logger; 9 | 10 | use actix::{Actor, Addr}; 11 | use actix_web::{error, get, http, middleware, web, App, HttpServer}; 12 | use env_logger::Env; 13 | use handlers::{ 14 | CatFile, CatFileResponse, GitRepos, LsDir, LsDirResponse, ResolveRef, ResolveRefResponse, 15 | }; 16 | use std::path::{Path, PathBuf}; 17 | 18 | const DEFAULT_PORT: &str = "7791"; 19 | const DEFAULT_HOST: &str = "localhost"; 20 | const DEFAULT_REPO_ROOT: &str = "./"; 21 | const DEFAULT_REFERENCE: &str = "origin/master"; 22 | 23 | #[derive(Deserialize)] 24 | pub struct PathParams { 25 | pub repo: String, 26 | pub path: PathBuf, 27 | } 28 | 29 | #[derive(Deserialize)] 30 | pub struct RepoPathParams { 31 | pub repo: String, 32 | } 33 | 34 | #[derive(Deserialize)] 35 | pub struct QueryParams { 36 | pub reference: Option, 37 | } 38 | 39 | pub struct AppState { 40 | pub git_repos: Addr, 41 | } 42 | 43 | #[actix_rt::main] 44 | async fn main() -> std::io::Result<()> { 45 | env_logger::from_env(Env::default().default_filter_or("gitkv=info")).init(); 46 | 47 | let args = parse_args().get_matches(); 48 | 49 | let host = args.value_of("host").unwrap_or(DEFAULT_HOST); 50 | let port = args.value_of("port").unwrap_or(DEFAULT_PORT); 51 | let repo_root = Path::new(args.value_of("repo-root").unwrap_or(DEFAULT_REPO_ROOT)); 52 | 53 | run_server(host, port, repo_root).await 54 | } 55 | 56 | async fn run_server(host: &str, port: &str, repo_root: &Path) -> std::io::Result<()> { 57 | let repos = git::load_repos(&repo_root); 58 | 59 | info!("Loaded Git repos: {:?}", repos.keys()); 60 | 61 | let addr = GitRepos::new(repos).start(); 62 | let listen_address = format!("{}:{}", host, port); 63 | 64 | info!("Listening on {}", listen_address); 65 | 66 | HttpServer::new(move || { 67 | App::new() 68 | .data(AppState { 69 | git_repos: addr.clone(), 70 | }) 71 | .wrap(middleware::Logger::default()) 72 | .service(cat_file) 73 | .service(ls_dir) 74 | .service(resolve_ref) 75 | }) 76 | .bind(listen_address)? 77 | .run() 78 | .await 79 | } 80 | 81 | macro_rules! not_found { 82 | () => { 83 | |err| error::InternalError::new(err, http::StatusCode::NOT_FOUND).into() 84 | }; 85 | } 86 | 87 | #[get("/repos/{repo}/cat/{path:.+}")] 88 | async fn cat_file( 89 | (app_state, path_params, query_params): ( 90 | web::Data, 91 | web::Path, 92 | web::Query, 93 | ), 94 | ) -> Result { 95 | let addr: Addr = app_state.git_repos.clone(); 96 | let repo_key = path_params.repo.clone(); 97 | let path = path_params.path.clone(); 98 | let reference = query_params 99 | .reference 100 | .as_deref() 101 | .unwrap_or(DEFAULT_REFERENCE) 102 | .to_string(); 103 | 104 | // TODO return proper content type depending on the content of the blob 105 | addr.send(CatFile { 106 | repo_key, 107 | reference, 108 | path, 109 | }) 110 | .await 111 | .map_err(not_found!()) 112 | .and_then(|CatFileResponse(resp)| resp.map(web::Bytes::from).map_err(not_found!())) 113 | } 114 | 115 | #[get("/repos/{repo}/ls/{path:.+}")] 116 | async fn ls_dir( 117 | (app_state, path_params, query_params): ( 118 | web::Data, 119 | web::Path, 120 | web::Query, 121 | ), 122 | ) -> Result { 123 | let addr: Addr = app_state.git_repos.clone(); 124 | let repo_key = path_params.repo.clone(); 125 | let path = path_params.path.clone(); 126 | let reference = query_params 127 | .reference 128 | .as_deref() 129 | .unwrap_or(DEFAULT_REFERENCE) 130 | .to_string(); 131 | 132 | addr.send(LsDir { 133 | repo_key, 134 | reference, 135 | path, 136 | }) 137 | .await 138 | .map_err(not_found!()) 139 | .and_then(|LsDirResponse(resp)| { 140 | resp.map_err(not_found!()) 141 | .and_then(|children| serde_json::to_string(&children).map_err(not_found!())) 142 | }) 143 | } 144 | 145 | #[get("/repos/{repo}/resolve")] 146 | async fn resolve_ref( 147 | (app_state, repo_path_params, query_params): ( 148 | web::Data, 149 | web::Path, 150 | web::Query, 151 | ), 152 | ) -> Result { 153 | let addr: Addr = app_state.git_repos.clone(); 154 | let repo_key = repo_path_params.repo.clone(); 155 | let reference = query_params 156 | .reference 157 | .as_deref() 158 | .unwrap_or(DEFAULT_REFERENCE) 159 | .to_string(); 160 | 161 | addr.send(ResolveRef { 162 | repo_key, 163 | reference, 164 | }) 165 | .await 166 | .map_err(not_found!()) 167 | .and_then(|ResolveRefResponse(resp)| resp.map_err(not_found!())) 168 | } 169 | 170 | fn parse_args<'a, 'b>() -> clap::App<'a, 'b> { 171 | clap::App::new(crate_name!()) 172 | .version(crate_version!()) 173 | // FIXME: Switch back to `crate_authors` macro once deprecation warnings are fixed in 174 | // stable warnings. 175 | // 176 | // .author(crate_authors!("\n")) 177 | .author("Intent HQ") 178 | .about(crate_description!()) 179 | .arg( 180 | clap::Arg::with_name("port") 181 | .short("p") 182 | .long("port") 183 | .takes_value(true) 184 | .value_name("PORT") 185 | .default_value(DEFAULT_PORT) 186 | .help("port to listen to"), 187 | ) 188 | .arg( 189 | clap::Arg::with_name("host") 190 | .short("h") 191 | .long("host") 192 | .takes_value(true) 193 | .value_name("HOST") 194 | .default_value(DEFAULT_HOST) 195 | .help("host to listen to"), 196 | ) 197 | .arg( 198 | clap::Arg::with_name("repo-root") 199 | .short("r") 200 | .long("repo-root") 201 | .takes_value(true) 202 | .value_name("PATH") 203 | .default_value(DEFAULT_REPO_ROOT) 204 | .help("path where the different repositories are located"), 205 | ) 206 | } 207 | 208 | #[cfg(test)] 209 | mod tests { 210 | use super::*; 211 | use actix_web::{test, App}; 212 | use std::str; 213 | 214 | fn start_test_server() -> test::TestServer { 215 | test::start_with(test::config().h1(), || { 216 | let addr = GitRepos::new(git::load_repos(Path::new("test"))).start(); 217 | 218 | App::new() 219 | .data(AppState { git_repos: addr }) 220 | .service(cat_file) 221 | .service(ls_dir) 222 | .service(resolve_ref) 223 | }) 224 | } 225 | 226 | macro_rules! assert_test_server_responds_with { 227 | ($path:expr, $expected_status:expr, $expected_body:expr) => {{ 228 | let srv = start_test_server(); 229 | 230 | let req = srv.get(&$path); 231 | let mut resp = req.send().await.unwrap(); 232 | let bytes = resp.body().await.unwrap(); 233 | let body = str::from_utf8(&bytes).unwrap(); 234 | 235 | assert_eq!(resp.status(), $expected_status); 236 | assert_eq!(body, $expected_body); 237 | }}; 238 | } 239 | 240 | // cat tests 241 | 242 | #[actix_rt::test] 243 | async fn cat_file_with_empty_repo() { 244 | assert_test_server_responds_with!("/repos//cat/README.md?reference=origin/master", 404, "") 245 | } 246 | 247 | #[actix_rt::test] 248 | async fn cat_file_with_empty_path() { 249 | assert_test_server_responds_with!("/repos/fixtures/cat/?reference=origin/master", 404, "") 250 | } 251 | 252 | #[actix_rt::test] 253 | async fn cat_file_with_invalid_repo() { 254 | assert_test_server_responds_with!( 255 | "/repos/idontexist/cat/README.md?reference=origin/master", 256 | 404, 257 | "No repo found with name 'idontexist'" 258 | ) 259 | } 260 | 261 | #[actix_rt::test] 262 | async fn cat_file_with_invalid_path() { 263 | assert_test_server_responds_with!( 264 | "/repos/fixtures/cat/not-a-file?reference=origin/master", 265 | 404, 266 | "the path 'not-a-file' does not exist in the given tree; class=Tree (14); code=NotFound (-3)" 267 | ) 268 | } 269 | 270 | #[actix_rt::test] 271 | async fn cat_file_with_invalid_reference_parameter() { 272 | assert_test_server_responds_with!( 273 | "/repos/fixtures/cat/example.txt?reference=idonot/exist", 274 | 404, 275 | "revspec 'idonot/exist' not found; class=Reference (4); code=NotFound (-3)" 276 | ) 277 | } 278 | 279 | #[actix_rt::test] 280 | async fn cat_file_with_valid_file() { 281 | assert_test_server_responds_with!( 282 | "/repos/fixtures/cat/example.txt", 283 | 200, 284 | "Bux poi — updated!\n" 285 | ); 286 | } 287 | 288 | #[actix_rt::test] 289 | async fn cat_file_with_valid_sha_reference_parameter() { 290 | assert_test_server_responds_with!( 291 | "/repos/fixtures/cat/example.txt?reference=467e981f94686d7a1db395f8acfd3cf7e7adfcd3", 292 | 200, 293 | "Bux poi\n" 294 | ); 295 | } 296 | 297 | #[actix_rt::test] 298 | async fn cat_file_with_valid_tag_reference_parameter() { 299 | assert_test_server_responds_with!( 300 | "/repos/fixtures/cat/example.txt?reference=v0.1.0", 301 | 200, 302 | "Bux poi\n" 303 | ); 304 | } 305 | 306 | // ls tests 307 | 308 | #[actix_rt::test] 309 | async fn ls_dir_with_empty_repo() { 310 | assert_test_server_responds_with!("/repos//ls/a-dir?reference=origin/master", 404, "") 311 | } 312 | 313 | #[actix_rt::test] 314 | async fn ls_dir_with_empty_path() { 315 | assert_test_server_responds_with!("/repos/fixtures/ls/?reference=origin/master", 404, "") 316 | } 317 | 318 | #[actix_rt::test] 319 | async fn ls_dir_with_invalid_repo() { 320 | assert_test_server_responds_with!( 321 | "/repos/idontexist/ls/a-dir?reference=origin/master", 322 | 404, 323 | "No repo found with name 'idontexist'" 324 | ) 325 | } 326 | 327 | #[actix_rt::test] 328 | async fn ls_dir_with_invalid_path() { 329 | assert_test_server_responds_with!( 330 | "/repos/fixtures/ls/not-a-dir?reference=origin/master", 331 | 404, 332 | "the path 'not-a-dir' does not exist in the given tree; class=Tree (14); code=NotFound (-3)" 333 | ) 334 | } 335 | 336 | #[actix_rt::test] 337 | async fn ls_dir_with_invalid_reference_parameter() { 338 | assert_test_server_responds_with!( 339 | "/repos/fixtures/ls/example.txt?reference=idonot/exist", 340 | 404, 341 | "revspec 'idonot/exist' not found; class=Reference (4); code=NotFound (-3)" 342 | ) 343 | } 344 | 345 | #[actix_rt::test] 346 | async fn ls_dir_with_valid_dir() { 347 | // Note that we do not expect recursive results — so `a-dir/nested-dir` 348 | // and its children are expected to be absent! 349 | assert_test_server_responds_with!( 350 | "/repos/fixtures/ls/a-dir", 351 | 200, 352 | "[\"file-a\",\"file-b\",\"file-c\",\"file-d\",\"nested-dir\"]" 353 | ); 354 | } 355 | 356 | #[actix_rt::test] 357 | async fn ls_dir_with_valid_sha_reference_parameter() { 358 | assert_test_server_responds_with!( 359 | "/repos/fixtures/ls/a-dir?reference=467e981f94686d7a1db395f8acfd3cf7e7adfcd3", 360 | 200, 361 | "[\"file-a\",\"file-b\",\"file-c\",\"nested-dir\"]" 362 | ); 363 | } 364 | 365 | #[actix_rt::test] 366 | async fn ls_dir_with_valid_tag_reference_parameter() { 367 | assert_test_server_responds_with!( 368 | "/repos/fixtures/ls/a-dir?reference=v0.1.0", 369 | 200, 370 | "[\"file-a\",\"file-b\",\"file-c\",\"nested-dir\"]" 371 | ); 372 | } 373 | 374 | // resolve tests 375 | 376 | fn origin_master_sha() -> String { 377 | String::from("e6134971608eb6ba7eb29047d5884c3377bc1fd2") 378 | } 379 | 380 | #[actix_rt::test] 381 | async fn resolve_ref_with_empty_reference() { 382 | assert_test_server_responds_with!("/repos/fixtures/resolve", 200, origin_master_sha()) 383 | } 384 | 385 | #[actix_rt::test] 386 | async fn resolve_ref_with_branch_name() { 387 | assert_test_server_responds_with!( 388 | "/repos/fixtures/resolve?reference=origin/master", 389 | 200, 390 | origin_master_sha() 391 | ) 392 | } 393 | 394 | #[actix_rt::test] 395 | async fn resolve_ref_with_commit_sha() { 396 | assert_test_server_responds_with!( 397 | "/repos/fixtures/resolve?reference=467e981f94686d7a1db395f8acfd3cf7e7adfcd3", 398 | 200, 399 | "467e981f94686d7a1db395f8acfd3cf7e7adfcd3" 400 | ) 401 | } 402 | 403 | #[actix_rt::test] 404 | async fn resolve_ref_with_tag() { 405 | assert_test_server_responds_with!( 406 | "/repos/fixtures/resolve?reference=v0.1.0", 407 | 200, 408 | "467e981f94686d7a1db395f8acfd3cf7e7adfcd3" 409 | ) 410 | } 411 | 412 | #[actix_rt::test] 413 | async fn resolve_ref_with_invalid_ref() { 414 | assert_test_server_responds_with!( 415 | "/repos/fixtures/resolve?reference=idonot/exist", 416 | 404, 417 | "revspec 'idonot/exist' not found; class=Reference (4); code=NotFound (-3)" 418 | ) 419 | } 420 | } 421 | --------------------------------------------------------------------------------