{"id":22305389,"url":"https://github.com/bvarnai/drs","last_synced_at":"2026-04-17T00:32:38.573Z","repository":{"id":227271674,"uuid":"768071110","full_name":"bvarnai/drs","owner":"bvarnai","description":"A utility to store directory revisions remotely using ssh/rsync and git","archived":false,"fork":false,"pushed_at":"2024-03-18T07:12:01.000Z","size":117,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-30T21:32:44.147Z","etag":null,"topics":["bash-script","command-line","devops","git","git-bash","git-for-windows","jq","rsync","shell-script","ssh","tooling"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bvarnai.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-06T12:19:03.000Z","updated_at":"2024-03-20T05:15:35.000Z","dependencies_parsed_at":"2025-01-30T21:29:46.860Z","dependency_job_id":"7d269f31-5edc-4ed3-b64a-c170486fba73","html_url":"https://github.com/bvarnai/drs","commit_stats":null,"previous_names":["bvarnai/drs"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bvarnai%2Fdrs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bvarnai%2Fdrs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bvarnai%2Fdrs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bvarnai%2Fdrs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bvarnai","download_url":"https://codeload.github.com/bvarnai/drs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245568581,"owners_count":20636803,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bash-script","command-line","devops","git","git-bash","git-for-windows","jq","rsync","shell-script","ssh","tooling"],"created_at":"2024-12-03T19:11:31.584Z","updated_at":"2026-04-17T00:32:38.557Z","avatar_url":"https://github.com/bvarnai.png","language":"Shell","readme":"# drs - an uncomplicated directory revision storage\n\n**drs** is a small set of shell scripts that allows you to store directory revisions (snapshots if you like) remotely. Revision metadata is stored in a *Git* repository while the directory contents are stored on a remote host using *SSH* and *rsync*. The metadata repository can be kept small since it's completely independent of the directory contents.\n\nIt's really easy to set up, depends only on standard tools, and is easy to extend.\n\n**Where does it fit?**\n\nI needed to store large builds (\\\u003e5GB) and distribute them efficiently to testers. The actual differences between builds were quite small, a few changed jars along with 100s of other jars that rarely change. In such cases, rsync does a spectacular job to speed things up. *Git* is great to keep track of everything else: branches, build information, etc.\n\n**Relation to Git**\n\n**drs** uses *Git* as a minimalistic database. Commands like `drs-put` and `drs-get` are integrated as *Git* aliases and organized around a producer/customer concept. The producer is usually a build job on CI; the consumer can be a human tester or a regression test job, for example. Most workflow tasks (except `git init`, `git tag`) are covered with `drs` commands, therefore users don't have to know *Git* much. For more details see [Differences to *Git*](#differences-to-git).\n\n## Table of contents\n\n- [drs - an uncomplicated directory revision storage](#drs---an-uncomplicated-directory-revision-storage)\n  - [Table of contents](#table-of-contents)\n  - [Demo](#demo)\n  - [Installation](#installation)\n    - [Using sources](#using-sources)\n    - [Using releases](#using-releases)\n    - [Install prerequisites](#install-prerequisites)\n      - [Install *client* prerequisites on Ubuntu](#install-client-prerequisites-on-ubuntu)\n      - [Install *client* prerequisites on Git for Windows (Git-Bash/MinGW/MSYS2)](#install-client-prerequisites-on-git-for-windows-git-bashmingwmsys2)\n      - [Final *client* check for the unbrave](#final-client-check-for-the-unbrave)\n      - [Install *server* prerequisites](#install-server-prerequisites)\n  - [Configuration](#configuration)\n    - [SSH configuration](#ssh-configuration)\n      - [SSH client setup](#ssh-client-setup)\n      - [SSH server setup](#ssh-server-setup)\n      - [How to set up SSH keys](#how-to-set-up-ssh-keys)\n    - [Metadata repository setup](#metadata-repository-setup)\n    - [Configuration file](#configuration-file)\n    - [Working directory explained](#working-directory-explained)\n    - [Hooks](#hooks)\n      - [Jenkins example](#jenkins-example)\n    - [Putting your initial directory revision](#putting-your-initial-directory-revision)\n  - [Usage](#usage)\n    - [A simple example](#a-simple-example)\n      - [Producer](#producer)\n      - [Consumer](#consumer)\n    - [Command reference](#command-reference)\n      - [info](#info)\n      - [name](#name)\n      - [select](#select)\n  - [](#)\n      - [update](#update)\n      - [get](#get)\n      - [create](#create)\n      - [put](#put)\n  - [Differences to Git](#differences-to-git)\n  - [Retention](#retention)\n  - [Development notes](#development-notes)\n    - [Shell vs. python, groovy etc.](#shell-vs-python-groovy-etc)\n\n-----\n\n## Demo\n\n![drs demo](docs/demo.gif)\n\n:tada: For a complete dockerized example see [drs demo](demo)\n\nIt's fully functional; you can play with `put` and `get` commands.\n\n-----\n\n## Installation\n\n### Using sources\n\n1.  Clone this repository to a suitable directory on your computer\n2.  Add this directory plus `src` to the `DRS_HOME` environment variable\n\n\u003c!-- end list --\u003e\n\n```bash\nexport DRS_HOME=~/drs/src\n```\n\n### Using releases\n\n1.  Download `drs.tar.gz` from the [latest release](https://github.com/bvarnai/drs/releases/latest)\n\n\u003c!-- end list --\u003e\n\n```bash\ncurl -o drs.tar.gz -L https://github.com/bvarnai/drs/releases/latest/download/drs.tar.gz\n```\n\n2.  Extract the archive (to a directory of your choosing)\n\n\u003c!-- end list --\u003e\n\n```bash\ntar -zxvf drs.tar.gz\n```\n\n3.  Add this directory to the `DRS_HOME` environment variable\n\n\u003c!-- end list --\u003e\n\n```bash\nexport DRS_HOME=~/drs\n```\n\n:memo: You can set `DRS_HOME` in `~/.profile` or `~/.bashrc` to make it permanent\n\n### Install prerequisites\n\n:memo: I omitted trivial dependencies like git, ssh etc.\n\n#### Install *client* prerequisites on Ubuntu\n\n```bash\nsudo apt install rsync jq\n```\n\n#### Install *client* prerequisites on [Git for Windows](https://gitforwindows.org/) (Git-Bash/MinGW/MSYS2)\n\nUnfortunately `Git-Bash` doesn't have a default package manager, so installing additional utils is not trivial.\n\nAfter trying out many approaches and tools, to my best knowledge the easiest way is to use [scoop](https://scoop.sh/) in Windows.\n\n```bash\nscoop bucket add main\nscoop install main/cwrsync main/jq\n```\n\nThis should work regardless of whether you used *scoop* to install *git* or not.\n\n#### Final *client* check for the unbrave\n\nThere is a script `check-client-prerequisites.sh` to check if your installation is ready:\n\n```bash\n$DRS_HOME/check-client-prerequisites.sh\n```\n\nIt should print all OK.\n\n#### Install *server* prerequisites\n\nYou will need an SSH server; pick your own favorite. For basic setup instructions see [SSH server setup](#ssh-server-setup) or check out the demo server [Dockerfile](demo/server/Dockerfile).\n\n:warning: No *rsync* daemon is needed, *SSH* only\n\n## Configuration\n\n### SSH configuration\n\n#### SSH client setup\n\n**drs** uses `ssh` to connect to the remote host. SSH configuration should be added to the `~/.ssh/config` file. This must be done on every client.\n\n```bash\nHost \u003cdrs-host-name\u003e\n    HostName \u003cdrs-real-host-name\u003e\n    User \u003cdrs-user\u003e\n    IdentityFile \u003cdrs-user-key\u003e\n    IdentitiesOnly yes\n    Port \u003cdrs-server-port\u003e\n    ForwardX11 no\n    Ciphers ^aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,chacha20-poly1305@openssh.com,aes128-ctr\n```\n\n  - `\u003cdrs-host-name\u003e`: a name that is used to identify the host. I recommend using something simple like `drs-server`; this allows you to change the real host name without changing the configuration in the repository\n  - `\u003cdrs-user-key\u003e`: ssh private key\n  - `\u003cdrs-real-host-name\u003e`: the real host name, for example drs.mycompany.com\n  - `\u003cdrs-user-name\u003e`: ssh user name to login\n  - `\u003cdrs-host-port\u003e`: ssh port of the host\n\n:bulb: Cipher list is optional, based on the post [Benchmark SSH Ciphers](https://gbe0.com/posts/linux/server/benchmark-ssh-ciphers/)\n\nAn example configuration:\n\n```\nHost drs-server\n    HostName drs.mycompany.com\n    IdentityFile id_rsa\n    IdentitiesOnly yes\n    User drs\n    Port 2222\n    ForwardX11 no\n    Ciphers ^aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,chacha20-poly1305@openssh.com,aes128-ctr\n```\n\n:memo: Note SSH configuration is an extensive topic with endless options to choose from. You can find out more about options here: [How to Use The SSH Config File](https://phoenixnap.com/kb/ssh-config)\n\n:bulb: If you are working in a secure, trusted environment, for example a company intranet, you can use a shared user for `drs`. It greatly simplifies client setup.\n\n#### SSH server setup\n\nIf you don't have an SSH server, please follow the guide [Initial Server Setup](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-22-04).\n\n#### How to set up SSH keys\n\nIf you don't have SSH keys, please follow the guide [How to Set Up SSH Keys](https://www.digitalocean.com/community/tutorials/how-to-set-up-ssh-keys-on-ubuntu-22-04).\n\n### Metadata repository setup\n\nThis section explains how to set up the **drs** metadata repository; it's nothing more than a normal *Git* repository.\n\n1.  Create an empty *Git* repository (or use an existing one):\n    ```bash\n    mkdir myrepo\n    git init\n    ```\n2.  Copy the configuration template file from `$DRS_HOME/drs.json`\n3.  Add your project directory (\"workDir\" property in `drs.json`) to your .gitignore file. It's `data` by default.\n4.  Install *Git* aliases:\n    ```bash\n    . $DRS_HOME/install.sh\n    ```\n5.  Add and commit configuration:\n    ```bash\n    git add .\n    git commit -m \"Add initial drs configuration\"\n    ```\n6.  Set remote:\n    ```bash\n     git remote add origin git@myrepo.git\n    ```\n7.  Push:\n    ```bash\n    git push -u origin master\n    ```\n\n### Configuration file\n\nThe configuration file is called `drs.json` and it's located in the root of the metadata repository.\n\n```json\n{\n    \"workDir\": \"\u003cworking-directory\u003e\",\n    \"defaultBranch\": \"\u003cdefault-branch\u003e\",\n    \"remote\": {\n        \"host\": \"\u003cdrs-host-name\u003e\",\n        \"path\": \"\u003cremote-path\u003e\",\n        \"rsyncOptions\": {\n            \"get\":\"\u003crsync-options\u003e\",\n            \"put\":\"\u003crsync-options\u003e\"\n        }\n    }\n}\n```\n\n  - `workDir` - (optional) working directory, defaults to `data`\n  - `defaultBranch` - (optional) commands will fall back to this branch if nothing is specified, defaults to `main`\n      - `remote` configuration section for remote:\n          - `host` - host name as specified in `~/.ssh/config` (see drs-host-name)\n          - `path` - path on the remote where revisions are stored\n          - `rsyncOptions` (optional) configuration options for rsync:\n              - `get` - (optional) passed to *rsync* for `get` command\n              - `put` - (optional) passed to *rsync* for `put` command\n\nFor all available *rsync* options see [rsync docs](https://download.samba.org/pub/rsync/rsync.html). The following *rsync* options are added implicitly:\n\n  - `-a` archive is the gold standard, it includes recursion (-r) plus preserves permissions, symlinks, and timestamps\n  - `--delete-during` delete during is more memory-efficient because rsync doesn't have to build a giant list of \"files to delete later\"; it just deletes them as it encounters them in the directory scan\n  - `-v` , `--info=progress2` and `--itemize-changes` if `-v|--verbose` is set\n  - `--quiet` if `-q|--quiet` is set\n\n:warning: When you provide multiple flags that contradict each other or offer different \"modes\" of the same function, the last flag specified on the command line wins. So you can always \"override\" those implicit options with `rsyncOptions`.\n\nExample minimum configuration:\n\n```json\n{\n    \"remote\": {\n        \"host\": \"drs-server\",\n        \"path\": \"/var/drs/myproject\",\n    }\n}\n```\n\nExample configuration:\n\n```json\n{\n    \"remote\": {\n        \"host\": \"drs-server\",\n        \"path\": \"/var/drs/myproject\",\n        \"rsyncOptions\": {\n            \"get\":\"-az --delete-during --stats\",\n            \"put\":\"-az --delete-during --whole-file --stats\"\n        }\n    }\n}\n```\n\nThis will store data on `drs-server` in the `/var/drs/myproject` directory.\n\n:memo: For my projects, the repository is called `myapp-builds` and the working directory is called `myapp`; this will give a `myapp-builds/myapp` local path. But there is nothing wrong with having a `myapp/myapp` structure.\n\n### Working directory explained\n\nThe actual contents/files are not stored in the **drs** metadata repository, but there is a dedicated directory called the working directory (a working copy if you please). For convenience, this is placed under a sub-directory in the **drs** repository and it's ignored by *Git*.\n\nExample structure:\n\n```bash\nmyrepo\n  data\n  .gitignore\n  drs.json\n```\n\n  - `data` is your working directory\n  - `.gitignore` contains the `data` entry\n    ```bash\n    data\n    ```\n\n:warning: The working directory is ignored; it's not visible to *Git*. This means you won't see any change/diff in *Git* when changing the working directory contents.\n\nOtherwise, there is no limitation on what you put in the metadata repository. For example, you can store build information, logs, anything really. I like to think of it as where you keep your complete build history. It should provide enough information to reproduce a specific build.\n\n### Hooks\n\nHooks are shell scripts to allow project-specific extensions. They are committed to the metadata repository with a predefined name and function to implement.\n\n  - `drs-info-hook.sh` is called by the `info` command. It can be used to print out user-friendly information such as links to Jenkins builds, source references, etc.\n    ```bash\n    function info_hook()\n    {\n        # your hook implementation\n        :\n    }\n    ```\n  - `drs-put-hook.sh` is called by the `put` command before commit. It can be used to collect all necessary information about a revision (a build). This can be used by the `info` command, for example.\n    ```bash\n    function put_hook()\n    {\n       # your hook implementation\n       :\n    }\n    ```\n\n#### Jenkins example\n\nGiven you have a Jenkins job which is producing your builds. `drs-put-hook.sh` will dump `env` to a file `env.json`. Then it will be committed and pushed to the metadata repository.\n\n`drs-put-hook.sh`:\n\n```bash\nfunction put_hook()\n{\n  jq -n env \u003e env.json\n}\n```\n\nClients consuming these builds will use `info` and can get valuable information.\n\n`drs-info-hook.sh`:\n\n```bash\nfunction info_hook()\n{\n  change_branch=$(jq -r '.CHANGE_BRANCH' env.json)\n  if [[ \"${change_branch}\" != \"null\" ]]; then\n    branch=\"${change_branch}\"\n    pr=\"true\"\n  else\n    branch=$(jq -r '.BRANCH_NAME' env.json)\n  fi\n\n  echo \"branch: ${branch}\"\n  if [[ -n \"${pr}\" ]]; then\n    echo \"PR: $(jq -r '.BRANCH_NAME' env.json)\"\n    echo \"PR link: $(jq -r '.CHANGE_URL' env.json)\"\n  fi\n\n  build_url=$(jq -r '.BUILD_URL' env.json)\n  echo \"build link: ${build_url}\"\n\n  job_url=$(jq -r '.JOB_URL' env.json)\n  echo \"job link: ${job_url}\"\n}\n```\n\n:memo: Jenkins adds many environment variables to builds implicitly. The actual availability depends on your job setup.\n\n### Putting your initial directory revision\n\n1.  Make sure you pushed your configuration files `drs.json` and `.gitignore`\n2.  Copy your initial content to the working directory\n3.  Put your directory to remote:\n    ```bash\n    git drs-put\n    ```\n\n## Usage\n\n### A simple example\n\n#### Producer\n\n```bash\n# create a new branch (based on the source branch)\ngit drs-create myFeature\n# put new build artifacts to remote\ngit drs-put\n```\n\n#### Consumer\n\n```bash\n# select the branch you need a build from\ngit drs-select myFeature\n# update to the latest available build\ngit drs-update\n# get the build\ngit drs-get\n```\n\n### Command reference\n\nCommand syntax is the following:\n\n```bash\ngit drs-\u003ccommand\u003e [options] [arguments]\n```\n\nOptional elements are shown in brackets [ ]. For example, many commands take a branch name as an argument.\n\nTo get some information about a command and a link to its reference documentation, use `command` with `help`:\n\n```bash\ngit drs-\u003ccommand\u003e help\n```\n\n:bulb: You can also use commands without a *Git* alias; this is recommended for scripts. Refer to the command name when calling:\n\n```bash\n$DRS_HOME/\u003ccommand\u003e.sh\n```\n\n-----\n\n#### info\n\nThe commit message is not very informative. To get more user-friendly information, use `info`:\n\n```bash\ngit drs-info\n```\n\nThe `info` command implementation is project-specific; see section [Hooks](#hooks).\n\n-----\n\n#### name\n\nTo get the current branch name, use `name`:\n\n```bash\ngit drs-name\n```\n\n-----\n\n#### select\n\nTo select and switch to an existing branch, use `select`:\n\n```bash\ngit drs-select [\u003cbranch\u003e|\u003ctag\u003e|\u003cuuid\u003e]\n```\n\nArguments:\n\n  - `branch, tag` - the branch or tag to select; if not specified, the `defaultBranch` property will be used (optional)\n  - `uuid` - the uuid to select; alternatively, this searches the log for a specific uuid (optional)\n\n:memo: `uuid` based selection is useful to identify builds; for example, Jenkins can post the `uuid` for each build and users can use this directly.\n\n![jenkins uuid](docs/jenkins-uuid.png)\n-----\n\n#### update\n\nTo get to the latest revision, use `update`:\n\n```bash\ngit drs-update\n```\n\n:memo: If you are in a detached HEAD state (not on any branch), `update` will fail. You need to select a branch then update it.\n\n-----\n\n#### get\n\nTo get the directory revision specified by the current commit. The working directory content will be synchronized with this revision.\n\n```bash\ngit drs-get [-v,--verbose] [-q,--quiet] [--stats] [--latest] [\u003ctarget_directory\u003e]\n```\n\nOptions:\n\n  - `verbose` - sets *rsync* verbose mode (optional)\n  - `quiet` - sets *rsync* quiet mode (optional)\n  - `stats` - enables *rsync* statistics (optional)\n  - `latest` - combines `update` and `get` to get the latest version\n\nArguments:\n\n  - `target_directory` – the directory to get content to; if not specified, the `name` property will be used (optional)\n\n:bulb: Usually you are only interested in the latest version; this can be done with a one-liner:\n\n```bash\ngit drs-get --latest\n```\n\n-----\n\n#### create\n\nTo create a new branch, use `create`:\n\n```bash\ngit drs-create [\u003cbranch\u003e]\n```\n\nArguments:\n\n  - `branch` - the branch to create (mandatory)\n\n-----\n\n#### put\n\nTo put a revision to the remote host, use `put`:\n\n```bash\ngit drs-put [-v,--verbose] [--no-sequence-check] [-s,--sequence \u003csequence_number\u003e] [\u003csource_directory\u003e]\n```\n\nOptions:\n\n  - `verbose` - sets *rsync* verbose mode (optional)\n  - `quiet` - sets *rsync* quiet mode (optional)\n  - `stats` - enables *rsync* statistics (optional)\n  - `no-sequence-check` - disables sequence number checking\n  - `sequence_number` - the sequence number; must be a comparable decimal (optional)\n\nArguments:\n\n  - `source_directory` – the directory to put content from (optional)\n\nSimple Jenkins example for using `--sequence`:\n\n```bash\n$DRS_HOME/create.sh $BRANCH_NAME\n$DRS_HOME/update.sh\n$DRS_HOME/put.sh --sequence $BUILD_ID my_build_dir\n```\n\n:memo: `BRANCH_NAME` and `BUILD_ID` are Jenkins job variables.\n\n`source_directory` allows you to use a source directory, eliminating the need to stage (copy) content to the working directory.\n\n## Differences to Git\n\nSince **drs** uses *Git* more like a database, not all *Git* concepts apply. Especially collaboration is completely different in a **drs** metadata repository.\n\n:warning: In case you want to work with *native* *Git* commands, the following notes are important to understand:\n\n  - **Origin has precedence**\n\n    To keep the workflow simple and robust, origin has precedence. Commands will force you to be up-to-date with origin and `drs-put` will implicitly try to push the new revision. This ensures whatever happens, users will fall back to a public *last known* version. Origin is the single source of truth, which is much less error-prone in a single-producer, multiple-consumer context.\n\n  - **No merging**\n\n    Revisions are not stored in *Git*; they are simple directories somewhere. As you cannot merge a directory on a filesystem, you cannot merge in **drs** either.\n\n  - **Commit message format**\n\n    The commit message has a strict format. You should not create them manually.\n\n:memo: **No merging** implies that branches are not merged. They are created then deleted if not needed. It's possible to keep all branches if you want to keep all history.\n\n## Retention\n\nDeleting revisions is done by deleting directories on the remote host. **drs** will try to locate a revision; if not found, it's assumed to be deleted. This is part of the normal workflow and will not be treated as an error. To implement a simple retention policy, you can set up a cron job or Jenkins job to delete directories older than 2 weeks, for example.\n\n## Development notes\n\n*Git* was a convenient choice to make something distributed and transactional. Directory metadata is published as a *Git* commit message in `json` format. :cold\\_sweat: ugh, you might say, and you are probably right. I abused the commit message, but in a good way, embracing the tremendous flexibility *Git* offers. I didn't use *Git* notes because I don't have anything to annotate; I just want to record something.\n\nSo a typical **drs** commit message looks like this:\n\n```json\n{\"uuid\":\"c1ca82b1-7f34-4f4c-9a76-05e3297b2a23\",\"seq\":\"1622824489\"}\n```\n\nThe `uuid` is used to identify the directory on the remote host. The sequence number helps to drop outdated builds.\n\n*rsync* is a great tool when you have small deltas to deal with. Initially, I wanted to use a \"trendy\" S3 ([minIO](https://min.io/) for example) based solution, but I realized not much is gained there. I think for a small development team, these just add unnecessary overhead.\n\n### Shell vs. python, groovy etc.\n\nObviously, this is a very subjective topic. I wanted to rely on external tools and keep it as simple as possible. No advanced logic and the seamless integration with *Git* aliases pushed me in the direction to use shell only.\n\nI used Google's [Shell Style Guide](https://google.github.io/styleguide/shellguide.html) with the help of [ShellCheck](https://www.shellcheck.net/).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbvarnai%2Fdrs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbvarnai%2Fdrs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbvarnai%2Fdrs/lists"}