~aw/patches

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
7 3

[PATCH mygit 1/5] percent decode repository name

Johann Galle
Details
Message ID
<20210315175744.6493-1-johann@qwertqwefsday.eu>
DKIM signature
pass
Download raw message
Patch: +5 -0
From: Johann150 <johann@qwertqwefsday.eu>

---
 Cargo.lock  | 1 +
 Cargo.toml  | 1 +
 src/main.rs | 3 +++
 3 files changed, 5 insertions(+)

diff --git a/Cargo.lock b/Cargo.lock
index 677d1e4..6da49e6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1127,6 +1127,7 @@ dependencies = [
 "chrono",
 "git2",
 "once_cell",
 "percent-encoding",
 "pico-args",
 "pulldown-cmark",
 "serde",
diff --git a/Cargo.toml b/Cargo.toml
index 49c992f..9515e55 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
chrono = "0.4"
git2 = {version="0.13", default-features = false}
once_cell = "1.7.2"
percent-encoding = "2.1"
pico-args = "0.4"
pulldown-cmark = "0.8"
serde = { version = "1.0", features = ["derive"] }
diff --git a/src/main.rs b/src/main.rs
index 6ad1499..9f264ab 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -84,6 +84,9 @@ struct RepoHomeTemplate {
}

fn repo_from_request(repo_name: &str) -> Result<Repository> {
    let repo_name = percent_encoding::percent_decode_str(repo_name)
        .decode_utf8_lossy()
        .into_owned();
    let repo_path = Path::new(&CONFIG.repo_directory).join(repo_name);
    // TODO CLEAN PATH! VERY IMPORTANT! DONT FORGET!
    let r = Repository::open(repo_path)?;
-- 
2.20.1

[PATCH mygit 2/5] handle empty repo correctly

Johann Galle
Details
Message ID
<20210315175744.6493-2-johann@qwertqwefsday.eu>
In-Reply-To
<20210315175744.6493-1-johann@qwertqwefsday.eu> (view parent)
DKIM signature
pass
Download raw message
Patch: +37 -0
From: Johann150 <johann@qwertqwefsday.eu>

---
 src/main.rs               | 31 +++++++++++++++++++++++++++++++
 templates/repo-empty.html |  6 ++++++
 2 files changed, 37 insertions(+)
 create mode 100644 templates/repo-empty.html

diff --git a/src/main.rs b/src/main.rs
index 9f264ab..f727880 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -83,6 +83,12 @@ struct RepoHomeTemplate {
    readme_text: String,
}

#[derive(Template)]
#[template(path = "repo-empty.html")]
struct RepoEmptyTemplate {
    repo: Repository,
}

fn repo_from_request(repo_name: &str) -> Result<Repository> {
    let repo_name = percent_encoding::percent_decode_str(repo_name)
        .decode_utf8_lossy()
@@ -103,6 +109,10 @@ async fn repo_home(req: Request<()>) -> tide::Result {
    }

    let repo = repo_from_request(&req.param("repo_name")?)?;
    if repo.is_empty().unwrap() {
        // we would not be able to find HEAD
        return Ok(RepoEmptyTemplate { repo }.into())
    }

    let readme_text = {
        let mut format = ReadmeFormat::Plaintext;
@@ -155,6 +165,13 @@ struct RepoLogTemplate<'a> {

async fn repo_log(req: Request<()>) -> tide::Result {
    let repo = repo_from_request(&req.param("repo_name")?)?;
    if repo.is_empty().unwrap() {
        // redirect to start page of repo
        let mut url = req.url().clone();
        url.path_segments_mut().unwrap().pop();
        return Ok(tide::Redirect::temporary(url.to_string()).into());
    }

    let mut revwalk = repo.revwalk()?;
    match req.param("ref") {
        Ok(r) => revwalk.push_ref(&format!("refs/heads/{}", r))?,
@@ -180,6 +197,13 @@ struct RepoRefTemplate<'a> {
}
async fn repo_refs(req: Request<()>) -> tide::Result {
    let repo = repo_from_request(&req.param("repo_name")?)?;
    if repo.is_empty().unwrap() {
        // redirect to start page of repo
        let mut url = req.url().clone();
        url.path_segments_mut().unwrap().pop();
        return Ok(tide::Redirect::temporary(url.to_string()).into());
    }

    let branches = repo
        .references()?
        .filter_map(|x| x.ok())
@@ -207,6 +231,13 @@ struct RepoTreeTemplate<'a> {
async fn repo_tree(req: Request<()>) -> tide::Result {
    // TODO handle subtrees
    let repo = repo_from_request(&req.param("repo_name")?)?;
    if repo.is_empty().unwrap() {
        // redirect to start page of repo
        let mut url = req.url().clone();
        url.path_segments_mut().unwrap().pop();
        return Ok(tide::Redirect::temporary(url.to_string()).into());
    }

    // TODO accept reference or commit id
    let spec = req.param("ref").unwrap_or("HEAD");
    let commit = repo.revparse_single(spec)?.peel_to_commit()?;
diff --git a/templates/repo-empty.html b/templates/repo-empty.html
new file mode 100644
index 0000000..70760c3
--- /dev/null
+++ b/templates/repo-empty.html
@@ -0,0 +1,6 @@
{% extends "base.html" %}

{% block content %}
  {% include "repo-navbar.html" %}
  <em>(This repository is empty.)</em>
{% endblock %}
-- 
2.20.1

[PATCH mygit 3/5] use repo_name filter everywhere

Johann Galle
Details
Message ID
<20210315175744.6493-3-johann@qwertqwefsday.eu>
In-Reply-To
<20210315175744.6493-1-johann@qwertqwefsday.eu> (view parent)
DKIM signature
pass
Download raw message
Patch: +7 -12
From: Johann150 <johann@qwertqwefsday.eu>

---
 templates/commit.html      | 5 ++---
 templates/log.html         | 3 +--
 templates/refs.html        | 5 ++---
 templates/repo-navbar.html | 4 ++--
 templates/repo.html        | 1 -
 templates/tree.html        | 1 -
 6 files changed, 7 insertions(+), 12 deletions(-)

diff --git a/templates/commit.html b/templates/commit.html
index 5137b92..1e140c9 100644
--- a/templates/commit.html
+++ b/templates/commit.html
@@ -1,11 +1,10 @@
{% extends "base.html" %}

{% block content %}
  {% let name = repo.workdir().unwrap().file_name().unwrap().to_str().unwrap() %}
  {% include "repo-navbar.html" %}
  <b>Commit:</b> <div class="commit-hash">{{commit.id()}}</div> (<a href="/{{name}}/tree/{{parent.id()}}">tree</a>)
  <b>Commit:</b> <div class="commit-hash">{{commit.id()}}</div> (<a href="/{{repo|repo_name|urlencode_strict}}/tree/{{parent.id()}}">tree</a>)
  <br>
  <b>Parent:</b> <a href="/{{name}}/commit/{{parent.id()}}"><div class="commit-hash">{{parent.id()}}</div></a> (<a href="/{{name}}/tree/{{parent.id()}}">tree</a>)
  <b>Parent:</b> <a href="/{{repo|repo_name|urlencode_strict}}/commit/{{parent.id()}}"><div class="commit-hash">{{parent.id()}}</div></a> (<a href="/{{repo|repo_name|urlencode_strict}}/tree/{{parent.id()}}">tree</a>)
  <br>
  <b>Author:</b> {{commit.author().name().unwrap()}} &lt;<a href="mailto:{{commit.author().email().unwrap()}}">{{commit.author().email().unwrap()}}</a>&gt;
  <br>
diff --git a/templates/log.html b/templates/log.html
index 730d245..382196a 100644
--- a/templates/log.html
+++ b/templates/log.html
@@ -1,12 +1,11 @@
{% extends "base.html" %}

{% block content %}
  {% let name = repo.workdir().unwrap().file_name().unwrap().to_str().unwrap() %}
  {% include "repo-navbar.html" %}
  <table>
  {% for commit in commits %} 
  <tr>
    <td class="commit-hash"><a href="/{{name}}/commit/{{commit.id()}}">{{commit.id().to_string()[..7]}}</a></td>
    <td class="commit-hash"><a href="/{{repo|repo_name|urlencode_strict}}/commit/{{commit.id()}}">{{commit.id().to_string()[..7]}}</a></td>
    {% let summary = commit.summary().unwrap_or("")|truncate(72) %}
    <td class="commit-summary">{{summary}}</td>
    <td class="commit-author-email"><a href="mailto:{{commit.author().email().unwrap_or("")}}">{{commit.author().name().unwrap_or("")}}</a></td>
diff --git a/templates/refs.html b/templates/refs.html
index c048961..15b7bfc 100644
--- a/templates/refs.html
+++ b/templates/refs.html
@@ -1,14 +1,13 @@
{% extends "base.html" %}

{% block content %}
  {% let name = repo.workdir().unwrap().file_name().unwrap().to_str().unwrap() %}
  {% include "repo-navbar.html" %}
  <h2>Branches</h2>
  <table>
  {% for branch in branches %}
  <tr>
    <td class="git-reference">
    <a href="/{{name}}/log/{{branch.shorthand().unwrap()}}">{{ branch.shorthand().unwrap() }}</a>
    <a href="/{{repo|repo_name|urlencode_strict}}/log/{{branch.shorthand().unwrap()}}">{{ branch.shorthand().unwrap() }}</a>
    </td>
  </tr>
  {% endfor %}
@@ -16,7 +15,7 @@
  <h2>Tags</h2>
  <table>
  {% for tag in tags %}
  <tr><td class="git-reference"><a href="/{{name}}/commit/{{tag.shorthand().unwrap()}}">{{ tag.shorthand().unwrap() }}</a></td></tr>
  <tr><td class="git-reference"><a href="/{{repo|repo_name|urlencode_strict}}/commit/{{tag.shorthand().unwrap()}}">{{ tag.shorthand().unwrap() }}</a></td></tr>
  {% endfor %}
  </table>
{% endblock %}
diff --git a/templates/repo-navbar.html b/templates/repo-navbar.html
index effeb08..e902449 100644
--- a/templates/repo-navbar.html
+++ b/templates/repo-navbar.html
@@ -1,5 +1,5 @@
<h1><a href="/">index</a>/{{name}}</h1>
<h1><a href="/">index</a>/{{repo|repo_name}}</h1>
<div>My cool repo</div>
<div class="clone-url">git clone git.alexwennerberg.com/repo </div>
<div class="navbar"><a href="/{{name}}">README</a> |  <a href="/{{name}}/tree">tree</a> |  <a href="/{{name}}/log">log</a> |  <a href="/{{name}}/refs">refs</a></div>
<div class="navbar"><a href="/{{repo|repo_name|urlencode_strict}}">README</a> |  <a href="/{{repo|repo_name|urlencode_strict}}/tree">tree</a> |  <a href="/{{repo|repo_name|urlencode_strict}}/log">log</a> |  <a href="/{{repo|repo_name|urlencode_strict}}/refs">refs</a></div>
<hr class='thin'>
diff --git a/templates/repo.html b/templates/repo.html
index 85733e4..e2fcd0e 100644
--- a/templates/repo.html
+++ b/templates/repo.html
@@ -1,7 +1,6 @@
{% extends "base.html" %}

{% block content %}
  {% let name = repo.workdir().unwrap().file_name().unwrap().to_str().unwrap() %}
  {% include "repo-navbar.html" %}
  {{ readme_text|safe }}
{% endblock %}
diff --git a/templates/tree.html b/templates/tree.html
index 06a38c3..8f39c30 100644
--- a/templates/tree.html
+++ b/templates/tree.html
@@ -1,7 +1,6 @@
{% extends "base.html" %}

{% block content %}
  {% let name = repo.workdir().unwrap().file_name().unwrap().to_str().unwrap() %}
  {% include "repo-navbar.html" %}
  <table>
    {% for entry in tree %}
-- 
2.20.1

[PATCH mygit 4/5] handle shallow clones

Johann Galle
Details
Message ID
<20210315175744.6493-4-johann@qwertqwefsday.eu>
In-Reply-To
<20210315175744.6493-1-johann@qwertqwefsday.eu> (view parent)
DKIM signature
pass
Download raw message
Patch: +20 -9
From: Johann150 <johann@qwertqwefsday.eu>

Although unusual for a git server, encountering a shallow clone should
be handled correctly.
---
 src/main.rs | 29 ++++++++++++++++++++---------
 1 file changed, 20 insertions(+), 9 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index f727880..342248f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -111,7 +111,7 @@ async fn repo_home(req: Request<()>) -> tide::Result {
    let repo = repo_from_request(&req.param("repo_name")?)?;
    if repo.is_empty().unwrap() {
        // we would not be able to find HEAD
        return Ok(RepoEmptyTemplate { repo }.into())
        return Ok(RepoEmptyTemplate { repo }.into());
    }

    let readme_text = {
@@ -172,15 +172,26 @@ async fn repo_log(req: Request<()>) -> tide::Result {
        return Ok(tide::Redirect::temporary(url.to_string()).into());
    }

    let mut revwalk = repo.revwalk()?;
    match req.param("ref") {
        Ok(r) => revwalk.push_ref(&format!("refs/heads/{}", r))?,
        _ => revwalk.push_head()?,
    let commits = if repo.is_shallow() {
        tide::log::warn!("repository {:?} is only a shallow clone", repo.path());
        vec![repo.head()?.peel_to_commit().unwrap()]
    } else {
        let mut revwalk = repo.revwalk()?;
        match req.param("ref") {
            Ok(r) => revwalk.push_ref(&format!("refs/heads/{}", r))?,
            _ => revwalk.push_head()?,
        };

        // show newest commits first
        revwalk
            .set_sorting(git2::Sort::TIME | git2::Sort::REVERSE)
            .unwrap();

        revwalk
            .filter_map(|oid| repo.find_commit(oid.unwrap()).ok().clone()) // TODO error handling
            .take(100) // Only get first 100 commits
            .collect()
    };
    let commits = revwalk
        .filter_map(|oid| repo.find_commit(oid.unwrap()).ok().clone()) // TODO error handling
        .take(100) // Only get first 100 commits
        .collect();
    let tmpl = RepoLogTemplate {
        repo: &repo,
        commits,
-- 
2.20.1

[PATCH mygit 5/5] change name in some more places

Johann Galle
Details
Message ID
<20210315175744.6493-5-johann@qwertqwefsday.eu>
In-Reply-To
<20210315175744.6493-1-johann@qwertqwefsday.eu> (view parent)
DKIM signature
pass
Download raw message
Patch: +24 -24
From: Johann150 <johann@qwertqwefsday.eu>

---
 Cargo.lock                 | 40 +++++++++++++++++++-------------------
 mygit.toml => grifter.toml |  0
 src/main.rs                |  4 ++--
 templates/base.html        |  4 ++--
 4 files changed, 24 insertions(+), 24 deletions(-)
 rename mygit.toml => grifter.toml (100%)

diff --git a/Cargo.lock b/Cargo.lock
index 6da49e6..1decbdc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -863,6 +863,26 @@ dependencies = [
 "web-sys",
]

[[package]]
name = "grifter"
version = "0.1.0"
dependencies = [
 "anyhow",
 "askama",
 "askama_tide",
 "async-std",
 "chrono",
 "git2",
 "once_cell",
 "percent-encoding",
 "pico-args",
 "pulldown-cmark",
 "serde",
 "syntect",
 "tide",
 "toml",
]

[[package]]
name = "hashbrown"
version = "0.9.1"
@@ -1116,26 +1136,6 @@ version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"

[[package]]
name = "mygit"
version = "0.1.0"
dependencies = [
 "anyhow",
 "askama",
 "askama_tide",
 "async-std",
 "chrono",
 "git2",
 "once_cell",
 "percent-encoding",
 "pico-args",
 "pulldown-cmark",
 "serde",
 "syntect",
 "tide",
 "toml",
]

[[package]]
name = "nb-connect"
version = "1.0.3"
diff --git a/mygit.toml b/grifter.toml
similarity index 100%
rename from mygit.toml
rename to grifter.toml
diff --git a/src/main.rs b/src/main.rs
index 342248f..bd1242d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,7 +18,7 @@ pub struct Config {
}

const HELP: &str = "\
mygit
grifter

FLAGS:
  -h, --help            Prints help information
@@ -39,7 +39,7 @@ fn args() -> Config {
    }

    let toml_text =
        fs::read_to_string("mygit.toml").expect("expected configuration file mygit.toml");
        fs::read_to_string("grifter.toml").expect("expected configuration file grifter.toml");
    match toml::from_str(&toml_text) {
        Ok(config) => config,
        Err(e) => {
diff --git a/templates/base.html b/templates/base.html
index e47495c..10db5d2 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -13,9 +13,9 @@
    <div id="content">
      {% block content %}{% endblock %}
    </div>
  <hr class = "thin">
  <hr class="thin">
  <div class="footer">
  Server running <a href="git.alexwennerberg.com/mygit">mygit</a>
  Server running <a href="git.alexwennerberg.com/grifter">grifter</a>
  </div>
  </body>
</html>
-- 
2.20.1
Details
Message ID
<C9Y5O3EFDVMX.38PU2L9KTC47S@debian-alex>
In-Reply-To
<20210315175744.6493-1-johann@qwertqwefsday.eu> (view parent)
DKIM signature
fail
Download raw message
DKIM signature: fail
Hi these look good! I'm on lunch break so I don't have time to look into
this in detail -- but my only question is about the first patch -- the
other 4 are ready to apply. Is there a way to do this that uses the tide
crate itself rather than another crate? I havent checked but it seems
like something I would expect the crate to be able to do

Alex

On Mon Mar 15, 2021 at 10:57 AM PDT, Johann Galle wrote:
> From: Johann150 <johann@qwertqwefsday.eu>
>
> ---
> Cargo.lock | 1 +
> Cargo.toml | 1 +
> src/main.rs | 3 +++
> 3 files changed, 5 insertions(+)
>
> diff --git a/Cargo.lock b/Cargo.lock
> index 677d1e4..6da49e6 100644
> --- a/Cargo.lock
> +++ b/Cargo.lock
> @@ -1127,6 +1127,7 @@ dependencies = [
> "chrono",
> "git2",
> "once_cell",
> + "percent-encoding",
> "pico-args",
> "pulldown-cmark",
> "serde",
> diff --git a/Cargo.toml b/Cargo.toml
> index 49c992f..9515e55 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -15,6 +15,7 @@ async-std = { version = "1.8.0", features =
> ["attributes"] }
> chrono = "0.4"
> git2 = {version="0.13", default-features = false}
> once_cell = "1.7.2"
> +percent-encoding = "2.1"
> pico-args = "0.4"
> pulldown-cmark = "0.8"
> serde = { version = "1.0", features = ["derive"] }
> diff --git a/src/main.rs b/src/main.rs
> index 6ad1499..9f264ab 100644
> --- a/src/main.rs
> +++ b/src/main.rs
> @@ -84,6 +84,9 @@ struct RepoHomeTemplate {
> }
>  
> fn repo_from_request(repo_name: &str) -> Result<Repository> {
> + let repo_name = percent_encoding::percent_decode_str(repo_name)
> + .decode_utf8_lossy()
> + .into_owned();
> let repo_path = Path::new(&CONFIG.repo_directory).join(repo_name);
> // TODO CLEAN PATH! VERY IMPORTANT! DONT FORGET!
> let r = Repository::open(repo_path)?;
> --
> 2.20.1
Johann Galle
Details
Message ID
<5d3834d5-8da1-c5d6-a7dc-a5ee16820eaf@qwertqwefsday.eu>
In-Reply-To
<C9Y5O3EFDVMX.38PU2L9KTC47S@debian-alex> (view parent)
DKIM signature
pass
Download raw message
On 2021-03-15T20:01+01:00, Alex Wennerberg wrote:
 > Hi these look good! I'm on lunch break so I don't have time to look into this in detail -- but my only question is about the first patch -- the other 4 are ready to apply. Is there a way to do this that uses the tide crate itself rather than another crate? I havent checked but it seems like something I would expect the crate to be able to do

That is something I also wondered and tried to find on <https://docs.rs/tide>. Searching for "percent", "url", "encode" or "decode" did not bring anything up. Same results for the re-exportet http-types crate. But anyway we were already indirectly dependent on the percent-encoding crate [1], so the meaning of adding this to our Crates.toml only means that we can now also access it.

[1] more completely, we get this inverse dependency tree for it:

$ cargo tree -i percent-encoding
percent-encoding v2.1.0
├── askama_shared v0.11.1
│   ├── askama v0.10.5
│   │   ├── askama_tide v0.13.0
│   │   │   └── grifter v0.1.0
│   │   └── grifter v0.1.0
│   └── askama_derive v0.10.5 (proc-macro)
│       └── askama v0.10.5 (*)
├── cookie v0.14.4
│   └── http-types v2.10.0
│       ├── async-h1 v2.3.2
│       │   └── tide v0.16.0
│       │       ├── askama_tide v0.13.0 (*)
│       │       └── grifter v0.1.0
│       ├── async-sse v4.1.0
│       │   └── tide v0.16.0 (*)
│       ├── http-client v6.3.4
│       │   └── tide v0.16.0 (*)
│       └── tide v0.16.0 (*)
├── form_urlencoded v1.0.1
│   ├── serde_urlencoded v0.7.0
│   │   └── http-types v2.10.0 (*)
│   └── url v2.2.1
│       ├── git2 v0.13.17
│       │   └── grifter v0.1.0
│       └── http-types v2.10.00 (*)
├── grifter v0.1.0
├── serde_qs v0.7.2
│   └── http-types v2.10.0 (*)
└── url v2.2.1 (*)
Details
Message ID
<A43A6913-A1B7-4B7A-959D-E9CEBA269AF2@alexwennerberg.com>
In-Reply-To
<5d3834d5-8da1-c5d6-a7dc-a5ee16820eaf@qwertqwefsday.eu> (view parent)
DKIM signature
pass
Download raw message
Makes sense to me. I’ll merge these as soon as I get home

Alex

> On Mar 15, 2021, at 1:54 PM, Johann Galle <johann@qwertqwefsday.eu> wrote:
> 
> On 2021-03-15T20:01+01:00, Alex Wennerberg wrote:
> > Hi these look good! I'm on lunch break so I don't have time to look into this in detail -- but my only question is about the first patch -- the other 4 are ready to apply. Is there a way to do this that uses the tide crate itself rather than another crate? I havent checked but it seems like something I would expect the crate to be able to do
> 
> That is something I also wondered and tried to find on <https://docs.rs/tide>. Searching for "percent", "url", "encode" or "decode" did not bring anything up. Same results for the re-exportet http-types crate. But anyway we were already indirectly dependent on the percent-encoding crate [1], so the meaning of adding this to our Crates.toml only means that we can now also access it.
> 
> [1] more completely, we get this inverse dependency tree for it:
> 
> $ cargo tree -i percent-encoding
> percent-encoding v2.1.0
> ├── askama_shared v0.11.1
> │   ├── askama v0.10.5
> │   │   ├── askama_tide v0.13.0
> │   │   │   └── grifter v0.1.0
> │   │   └── grifter v0.1.0
> │   └── askama_derive v0.10.5 (proc-macro)
> │       └── askama v0.10.5 (*)
> ├── cookie v0.14.4
> │   └── http-types v2.10.0
> │       ├── async-h1 v2.3.2
> │       │   └── tide v0.16.0
> │       │       ├── askama_tide v0.13.0 (*)
> │       │       └── grifter v0.1.0
> │       ├── async-sse v4.1.0
> │       │   └── tide v0.16.0 (*)
> │       ├── http-client v6.3.4
> │       │   └── tide v0.16.0 (*)
> │       └── tide v0.16.0 (*)
> ├── form_urlencoded v1.0.1
> │   ├── serde_urlencoded v0.7.0
> │   │   └── http-types v2.10.0 (*)
> │   └── url v2.2.1
> │       ├── git2 v0.13.17
> │       │   └── grifter v0.1.0
> │       └── http-types v2.10.00 (*)
> ├── grifter v0.1.0
> ├── serde_qs v0.7.2
> │   └── http-types v2.10.0 (*)
> └── url v2.2.1 (*)
Reply to thread Export thread (mbox)