├── crates
├── git-remote-helper
│ ├── src
│ │ ├── git
│ │ │ ├── service
│ │ │ │ ├── mod.rs
│ │ │ │ └── receive_pack
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ └── response
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ └── report_status_v2
│ │ │ │ │ ├── tests
│ │ │ │ │ ├── fixture
│ │ │ │ │ │ ├── mod.rs
│ │ │ │ │ │ ├── blocking_io.rs
│ │ │ │ │ │ └── async_io.rs
│ │ │ │ │ └── mod.rs
│ │ │ │ │ └── mod.rs
│ │ │ ├── mod.rs
│ │ │ └── config
│ │ │ │ └── mod.rs
│ │ ├── cli
│ │ │ └── mod.rs
│ │ ├── commands
│ │ │ ├── mod.rs
│ │ │ ├── fetch.rs
│ │ │ ├── list.rs
│ │ │ └── push.rs
│ │ └── lib.rs
│ └── Cargo.toml
├── git-remote-icp
│ ├── src
│ │ ├── http
│ │ │ ├── mod.rs
│ │ │ └── reqwest
│ │ │ │ ├── mod.rs
│ │ │ │ └── remote.rs
│ │ ├── config.rs
│ │ ├── main.rs
│ │ └── connect.rs
│ └── Cargo.toml
├── git-remote-http-reqwest
│ ├── src
│ │ ├── main.rs
│ │ └── connect.rs
│ └── Cargo.toml
└── git-remote-tcp
│ ├── src
│ ├── main.rs
│ └── connect.rs
│ └── Cargo.toml
├── identity.pub
├── identity.pem
├── Cargo.toml
├── .gitignore
├── .github
└── workflows
│ └── ci.yml
├── .gitconfig
├── README.md
├── flake.lock
├── nix
└── git-remote-helper.nix
├── flake.nix
└── LICENSE
/crates/git-remote-helper/src/git/service/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod receive_pack;
2 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/git/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod config;
2 | pub mod service;
3 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/git/service/receive_pack/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod response;
2 |
--------------------------------------------------------------------------------
/crates/git-remote-icp/src/http/mod.rs:
--------------------------------------------------------------------------------
1 | mod reqwest;
2 |
3 | pub use self::reqwest::Remote;
4 |
5 | use git_repository as git;
6 | pub use git::protocol::transport::client::http::*;
7 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/git/service/receive_pack/response/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod report_status_v2;
2 |
3 | pub use report_status_v2::read_and_parse;
4 | pub use report_status_v2::CommandStatusV2;
5 |
--------------------------------------------------------------------------------
/identity.pub:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAERzzyM2WgleyVhRLy9UpXBg8UZkvRNCTY
3 | E7X4b0wxP8XED9VZpqKi0n+CBhh7Wgf1o/qCE64kxfzZSs9yOY95xg==
4 | -----END PUBLIC KEY-----
5 |
--------------------------------------------------------------------------------
/crates/git-remote-http-reqwest/src/main.rs:
--------------------------------------------------------------------------------
1 | mod connect;
2 |
3 | use connect::connect;
4 | use git_remote_helper;
5 |
6 | pub fn main() -> anyhow::Result<()> {
7 | env_logger::init();
8 | git_remote_helper::main(connect)
9 | }
10 |
--------------------------------------------------------------------------------
/identity.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN EC PRIVATE KEY-----
2 | MHQCAQEEIOH9PLPiXw7Hj9w1SlxRw3U9rYZE40DAgpKJNZcoGm2toAcGBSuBBAAK
3 | oUQDQgAERzzyM2WgleyVhRLy9UpXBg8UZkvRNCTYE7X4b0wxP8XED9VZpqKi0n+C
4 | Bhh7Wgf1o/qCE64kxfzZSs9yOY95xg==
5 | -----END EC PRIVATE KEY-----
6 |
--------------------------------------------------------------------------------
/crates/git-remote-tcp/src/main.rs:
--------------------------------------------------------------------------------
1 | mod connect;
2 |
3 | use git_remote_helper;
4 | use connect::connect;
5 |
6 | #[tokio::main]
7 | pub async fn main() -> anyhow::Result<()> {
8 | env_logger::init();
9 | git_remote_helper::main(connect).await
10 | }
11 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/git/service/receive_pack/response/report_status_v2/tests/fixture/mod.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "async-network-client")]
2 | mod async_io;
3 |
4 | #[cfg(feature = "blocking-network-client")]
5 | mod blocking_io;
6 |
7 | pub struct Fixture<'a>(pub &'a [u8]);
8 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/cli/mod.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 |
3 | #[derive(Debug, Parser)]
4 | #[command(about, version)]
5 | pub struct Args {
6 | /// A remote repository; either the name of a configured remote or a URL
7 | pub repository: String,
8 |
9 | /// A URL of the form icp://
or icp::://
10 | pub url: String,
11 | }
12 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "crates/git-remote-helper",
4 | "crates/git-remote-http-reqwest",
5 | "crates/git-remote-icp",
6 | "crates/git-remote-tcp",
7 | ]
8 |
9 | [workspace.dependencies]
10 | anyhow = "1.0"
11 | async-trait = "0.1"
12 | env_logger = "0.9"
13 | log = "0.4"
14 | git-remote-helper = { path = "crates/git-remote-helper" }
15 | git-features = { version = "0.26" }
16 | git-repository = { version = "0.33" }
17 | git-validate = { version = "0.7" }
18 | tokio = { version = "1.12", features = ["full"] }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Rust
2 | # https://github.com/github/gitignore/blob/master/Rust.gitignore
3 |
4 | # Generated by Cargo
5 | # will have compiled files and executables
6 | debug/
7 | target/
8 |
9 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
10 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
11 | # Cargo.lock
12 |
13 | # These are backup files generated by rustfmt
14 | **/*.rs.bk
15 |
16 | # MSVC Windows builds of rustc generate these, which store debugging information
17 | *.pdb
18 |
19 |
20 | # Nix
21 | result
22 | result-*
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/commands/mod.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 | use strum::EnumVariantNames;
3 |
4 | pub mod fetch;
5 | pub mod list;
6 | pub mod push;
7 |
8 | use list::ListVariant;
9 |
10 | #[derive(Debug, EnumVariantNames, Eq, Ord, PartialEq, PartialOrd, Parser)]
11 | #[strum(serialize_all = "kebab_case")]
12 | pub enum Commands {
13 | Capabilities,
14 | Fetch {
15 | hash: String, // TODO: gitoxide::hash::ObjectId?
16 |
17 | name: String,
18 | },
19 | List {
20 | variant: Option,
21 | },
22 | Push {
23 | src_dst: String,
24 | },
25 | }
26 |
--------------------------------------------------------------------------------
/crates/git-remote-tcp/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "git-remote-tcp"
3 | description = "A Git remote helper for the git:// protocol."
4 | version = "0.1.0"
5 | edition = "2021"
6 | authors = ["Paul Young <84700+paulyoung@users.noreply.github.com>"]
7 |
8 | [[bin]]
9 | name = "git-remote-tcp"
10 |
11 | [dependencies]
12 | anyhow = { workspace = true }
13 | env_logger = { workspace = true }
14 | git-remote-helper = { workspace = true, features = ["async-network-client"] }
15 | git-repository = { workspace = true, features = ["async-network-client-async-std"] }
16 | log = { workspace = true }
17 | tokio = { workspace = true }
18 |
--------------------------------------------------------------------------------
/crates/git-remote-http-reqwest/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "git-remote-http-reqwest"
3 | description = "A Git remote helper for http:// and https:// protocols."
4 | version = "0.1.0"
5 | edition = "2021"
6 | authors = ["Paul Young <84700+paulyoung@users.noreply.github.com>"]
7 |
8 | [[bin]]
9 | name = "git-remote-http-reqwest"
10 |
11 | [dependencies]
12 | anyhow = { workspace = true }
13 | env_logger = { workspace = true }
14 | git-remote-helper = { workspace = true, features = ["blocking-network-client"] }
15 | git-repository = { workspace = true, features = ["blocking-http-transport-reqwest"] }
16 | log = { workspace = true }
17 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: "ci"
2 | on:
3 | pull_request:
4 | push:
5 | jobs:
6 | build:
7 | name: ${{ matrix.package }} on ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | package:
11 | - git-remote-helper-async
12 | - git-remote-helper-blocking
13 | - git-remote-http-reqwest
14 | - git-remote-icp
15 | - git-remote-tcp
16 | os:
17 | - macos-latest
18 | - ubuntu-latest
19 | runs-on: ${{ matrix.os }}
20 | steps:
21 | - uses: actions/checkout@v3
22 | - uses: cachix/install-nix-action@v18
23 | - run: nix build .#${{ matrix.package }} --show-trace
24 |
--------------------------------------------------------------------------------
/crates/git-remote-icp/src/http/reqwest/mod.rs:
--------------------------------------------------------------------------------
1 | use ic_agent::export::Principal;
2 | use ic_agent::Agent;
3 |
4 | /// An implementation for HTTP requests via `reqwest`.
5 | pub struct Remote {
6 | agent: Agent,
7 | canister_id: Principal,
8 | /// A worker thread which performs the actual request.
9 | handle: Option>>,
10 | /// A channel to send requests (work) to the worker thread.
11 | request: std::sync::mpsc::SyncSender,
12 | /// A channel to receive the result of the prior request.
13 | response: std::sync::mpsc::Receiver,
14 | }
15 |
16 | ///
17 | mod remote;
18 |
--------------------------------------------------------------------------------
/.gitconfig:
--------------------------------------------------------------------------------
1 | [icp]
2 | # Optional. See "Generating a public/private key pair"
3 | privateKey = ./identity.pem
4 |
5 | # Optional. Defaults to w7uni-tiaaa-aaaam-qaydq-cai
6 | canisterId = rwlgt-iiaaa-aaaaa-aaaaa-cai
7 |
8 | # Optional. Defaults to https://ic0.app
9 | replicaUrl = http://localhost:8000
10 |
11 | # Optional. Defaults to false.
12 | #
13 | # By default, the helper is configured to talk to the main Internet Computer,
14 | # and verifies responses using a hard-coded public key.
15 | #
16 | # This function will instruct the helper to ask the replica for its public
17 | # key, and use that instead. This is required when talking to a local test
18 | # instance, for example.
19 | #
20 | # Only enable this when you are not talking to the main Internet Computer,
21 | # otherwise you are prone to man-in-the-middle attacks.
22 | fetchRootKey = true
23 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "git-remote-helper"
3 | description = "A library for implementing Git remote helpers."
4 | version = "0.1.0"
5 | edition = "2021"
6 | authors = ["Paul Young <84700+paulyoung@users.noreply.github.com>"]
7 |
8 | [features]
9 | async-network-client = ["git-repository/async-network-client"]
10 | blocking-network-client = ["git-repository/blocking-network-client", "maybe-async/is_sync"]
11 |
12 | [dependencies]
13 | anyhow = { workspace = true }
14 | clap = { version = "4.0", features = ["derive"] }
15 | derive_more = "0.99"
16 | git-repository = { workspace = true }
17 | git-validate = { workspace = true }
18 | log = { workspace = true }
19 | maybe-async = "0.2"
20 | nom = "7.0"
21 | strum = { version = "0.24", features = ["derive"] }
22 | tokio = { workspace = true }
23 |
24 | [dev-dependencies]
25 | async-trait = { workspace = true }
26 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/git/config/mod.rs:
--------------------------------------------------------------------------------
1 | use anyhow::anyhow;
2 |
3 | // TODO: figure out why this doesn't find the setting when used with `git -c`
4 | // let private_key_path = config.string("icp.privateKey").ok_or_else(|| {
5 | // anyhow!("failed to read icp.privateKey from git config. Set with `git config --global icp.privateKey `")
6 | // })?;
7 |
8 | pub fn get(key: &str) -> anyhow::Result {
9 | let config_value = std::process::Command::new("git")
10 | .arg("config")
11 | .arg(key)
12 | .output()?;
13 |
14 | let config_value = config_value.stdout;
15 | let config_value = String::from_utf8(config_value)?;
16 | let config_value = config_value.trim().to_string();
17 |
18 | if config_value.is_empty() {
19 | Err(anyhow!("{} is empty", key))
20 | } else {
21 | Ok(config_value)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/crates/git-remote-tcp/src/connect.rs:
--------------------------------------------------------------------------------
1 | use git::protocol::transport;
2 | use git::url::Scheme;
3 | use git_repository as git;
4 | use log::trace;
5 | use std::convert::{Infallible, TryInto};
6 | use transport::client::connect::Error;
7 |
8 | pub async fn connect(
9 | url: Url,
10 | options: transport::connect::Options,
11 | ) -> Result, Error>
12 | where
13 | Url: TryInto,
14 | git::url::parse::Error: From,
15 | {
16 | let mut url: git::Url = url.try_into().map_err(git::url::parse::Error::from)?;
17 | trace!("Provided URL scheme: {:#?}", url.scheme);
18 |
19 | url.scheme = match url.scheme {
20 | Scheme::Ext(scheme) if &scheme == "tcp" => Ok(Scheme::Git),
21 | scheme @ Scheme::Git => Ok(scheme),
22 | _ => Err(Error::UnsupportedScheme(url.scheme)),
23 | }?;
24 | trace!("Resolved URL scheme: {:#?}", url.scheme);
25 |
26 | transport::connect::<_, Infallible>(url, options).await
27 | }
28 |
--------------------------------------------------------------------------------
/crates/git-remote-http-reqwest/src/connect.rs:
--------------------------------------------------------------------------------
1 | use git::protocol::transport;
2 | use git::url::Scheme;
3 | use git_repository as git;
4 | use log::trace;
5 | use std::convert::{Infallible, TryInto};
6 | use transport::client::connect::Error;
7 |
8 | pub fn connect(
9 | url: Url,
10 | options: transport::connect::Options,
11 | ) -> Result, Error>
12 | where
13 | Url: TryInto,
14 | git::url::parse::Error: From,
15 | {
16 | let mut url: git::Url = url.try_into().map_err(git::url::parse::Error::from)?;
17 | trace!("Provided URL scheme: {:#?}", url.scheme);
18 |
19 | url.scheme = match url.scheme {
20 | Scheme::Ext(scheme) if &scheme == "http-reqwest" => Ok(Scheme::Http),
21 | Scheme::Ext(scheme) if &scheme == "https-reqwest" => Ok(Scheme::Https),
22 | scheme @ (Scheme::Http | Scheme::Https) => Ok(scheme),
23 | _ => Err(Error::UnsupportedScheme(url.scheme)),
24 | }?;
25 | trace!("Resolved URL scheme: {:#?}", url.scheme);
26 |
27 | transport::connect::<_, Infallible>(url, options)
28 | }
29 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/git/service/receive_pack/response/report_status_v2/tests/fixture/blocking_io.rs:
--------------------------------------------------------------------------------
1 | use super::Fixture;
2 | use git_repository as git;
3 | use git::bstr::{BStr, ByteSlice};
4 | use git::protocol::transport::packetline;
5 |
6 | impl<'a> std::io::Read for Fixture<'a> {
7 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
8 | self.0.read(buf)
9 | }
10 | }
11 |
12 | impl<'a> std::io::BufRead for Fixture<'a> {
13 | fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
14 | self.0.fill_buf()
15 | }
16 |
17 | fn consume(&mut self, amt: usize) {
18 | self.0.consume(amt)
19 | }
20 | }
21 |
22 | impl<'a> git::protocol::transport::client::ReadlineBufRead for Fixture<'a> {
23 | fn readline(
24 | &mut self,
25 | ) -> Option<
26 | std::io::Result, packetline::decode::Error>>,
27 | > {
28 | let bytes: &BStr = self.0.into();
29 | let mut lines = bytes.lines();
30 | let res = lines.next()?;
31 | self.0 = lines.as_bytes();
32 | Some(Ok(Ok(packetline::PacketLineRef::Data(res))))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/crates/git-remote-icp/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "git-remote-icp"
3 | description = "A Git remote helper for the Internet Computer Protocol."
4 | version = "0.1.0"
5 | edition = "2021"
6 | authors = ["Paul Young <84700+paulyoung@users.noreply.github.com>"]
7 |
8 | [[bin]]
9 | name = "git-remote-icp"
10 |
11 | [dependencies]
12 | anyhow = { workspace = true }
13 | candid = "0.8"
14 | env_logger = { workspace = true }
15 | # Needed because git-repository doesn't seem to expose this feature
16 | git-features = { workspace = true, features = ["io-pipe"] }
17 | git-remote-helper = { workspace = true, features = ["blocking-network-client"] }
18 | git-repository = { workspace = true, features = ["blocking-http-transport-reqwest"] }
19 | ic-agent = "0.23"
20 | # When using ic-certified-assets = "0.2" we get an error that CandidType isn't
21 | # implemented for HttpRequest even though it is. This appears to be because it
22 | # uses candid 0.7 when we are using candid 0.8. This commit isn't on crates.io
23 | # but depends on candid 0.8.
24 | ic-certified-assets = { git = "https://github.com/dfinity/sdk", rev = "763c2bb35bcba5cee34ecc08a991252f474e631e" }
25 | log = { workspace = true }
26 | reqwest = "0.11"
27 | serde_bytes = "0.11"
28 | thiserror = "1.0"
29 | tokio = { workspace = true }
--------------------------------------------------------------------------------
/crates/git-remote-icp/src/config.rs:
--------------------------------------------------------------------------------
1 | use anyhow::anyhow;
2 | use git_remote_helper::git;
3 | use ic_agent::export::Principal;
4 |
5 | const CANISTER_ID_KEY: &str = "icp.canisterId";
6 | const DEFAULT_CANISTER_ID: &str = "w7uni-tiaaa-aaaam-qaydq-cai";
7 |
8 | pub fn canister_id() -> anyhow::Result {
9 | let canister_id =
10 | git::config::get(CANISTER_ID_KEY).unwrap_or_else(|_| DEFAULT_CANISTER_ID.to_string());
11 | let principal = Principal::from_text(canister_id)?;
12 | Ok(principal)
13 | }
14 |
15 | const FETCH_ROOT_KEY_KEY: &str = "icp.fetchRootKey";
16 | const DEFAULT_FETCH_ROOT_KEY: bool = false;
17 |
18 | pub fn fetch_root_key() -> bool {
19 | git::config::get(FETCH_ROOT_KEY_KEY)
20 | .map(|config_value| matches!(config_value.as_str(), "true"))
21 | .unwrap_or(DEFAULT_FETCH_ROOT_KEY)
22 | }
23 |
24 | const PRIVATE_KEY_KEY: &str = "icp.privateKey";
25 |
26 | pub fn private_key() -> anyhow::Result {
27 | git::config::get(PRIVATE_KEY_KEY).map_err(|_| {
28 | anyhow!("failed to read icp.privateKey from git config. Set `icp.privateKey = `")
29 | })
30 | }
31 |
32 | const REPLICA_URL_KEY: &str = "icp.replicaUrl";
33 | const DEFAULT_REPLICA_URL: &str = "https://ic0.app";
34 |
35 | pub fn replica_url() -> String {
36 | git::config::get(REPLICA_URL_KEY).unwrap_or_else(|_| DEFAULT_REPLICA_URL.to_string())
37 | }
38 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/commands/fetch.rs:
--------------------------------------------------------------------------------
1 | use git_repository as git;
2 | use log::trace;
3 | use std::collections::BTreeSet;
4 | use maybe_async::maybe_async;
5 |
6 | pub type Batch = BTreeSet<(String, String)>;
7 |
8 | #[maybe_async]
9 | pub async fn process(
10 | transport: T,
11 | repo: &git::Repository,
12 | url: &str,
13 | batch: &mut Batch,
14 | ) -> anyhow::Result<()>
15 | where
16 | T: git::protocol::transport::client::Transport,
17 | {
18 | if !batch.is_empty() {
19 | trace!("process fetch: {:#?}", batch);
20 |
21 | let mut remote = repo.remote_at(url)?;
22 |
23 | for (hash, _name) in batch.iter() {
24 | remote = remote.with_refspecs(Some(hash.as_bytes()), git::remote::Direction::Fetch)?;
25 | }
26 |
27 | // Implement once option capability is supported
28 | let progress = git::progress::Discard;
29 |
30 | let outcome = remote
31 | .to_connection_with_transport(transport, progress)
32 | .prepare_fetch(git::remote::ref_map::Options {
33 | prefix_from_spec_as_filter_on_remote: true,
34 | handshake_parameters: vec![],
35 | extra_refspecs: vec![],
36 | })
37 | .await?
38 | .receive(&git::interrupt::IS_INTERRUPTED)
39 | .await?;
40 |
41 | trace!("outcome: {:#?}", outcome);
42 |
43 | // TODO: delete .keep files by outputting: lock
44 | // TODO: determine if gitoxide handles this for us yet
45 |
46 | batch.clear();
47 | println!();
48 | }
49 |
50 | Ok(())
51 | }
52 |
--------------------------------------------------------------------------------
/crates/git-remote-icp/src/main.rs:
--------------------------------------------------------------------------------
1 | mod config;
2 | mod connect;
3 | mod http;
4 |
5 | use anyhow::anyhow;
6 | use ic_agent::identity::{AnonymousIdentity, Identity, Secp256k1Identity};
7 | use log::trace;
8 | use std::sync::Arc;
9 |
10 | pub fn main() -> anyhow::Result<()> {
11 | env_logger::init();
12 |
13 | let private_key_path = config::private_key();
14 | trace!("private key path: {:#?}", private_key_path);
15 |
16 | let identity = get_identity(private_key_path)?;
17 |
18 | let principal = identity.sender().map_err(|err| anyhow!(err))?;
19 | trace!("principal: {}", principal);
20 | eprintln!("Principal for caller: {}", principal);
21 |
22 | let fetch_root_key = config::fetch_root_key();
23 | trace!("fetch root key: {}", fetch_root_key);
24 |
25 | let replica_url = config::replica_url();
26 | trace!("replica url: {}", replica_url);
27 |
28 | let canister_id = config::canister_id()?;
29 | trace!("canister id: {}", canister_id);
30 |
31 | git_remote_helper::main(connect::connect(
32 | identity,
33 | fetch_root_key,
34 | replica_url,
35 | canister_id,
36 | ))
37 | }
38 |
39 | fn get_identity(private_key_path: anyhow::Result) -> anyhow::Result> {
40 | match private_key_path {
41 | Ok(path) => {
42 | eprintln!("Using identity for private key found in git config");
43 | let identity = Secp256k1Identity::from_pem_file(path)?;
44 | Ok(Arc::new(identity))
45 | }
46 | Err(_) => {
47 | eprintln!("No private key found git config, using anonymous identity");
48 | Ok(Arc::new(AnonymousIdentity {}))
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/git/service/receive_pack/response/report_status_v2/tests/fixture/async_io.rs:
--------------------------------------------------------------------------------
1 | use super::Fixture;
2 | use async_trait::async_trait;
3 | use core::pin::Pin;
4 | use git::bstr::{BStr, ByteSlice};
5 | use git::protocol::transport::packetline;
6 | use git_repository as git;
7 |
8 | impl<'a> Fixture<'a> {
9 | fn project(self: Pin<&mut Self>) -> Pin<&mut &'a [u8]> {
10 | unsafe { Pin::new(&mut self.get_unchecked_mut().0) }
11 | }
12 | }
13 |
14 | impl<'a> git::protocol::futures_io::AsyncRead for Fixture<'a> {
15 | fn poll_read(
16 | self: std::pin::Pin<&mut Self>,
17 | cx: &mut std::task::Context<'_>,
18 | buf: &mut [u8],
19 | ) -> std::task::Poll> {
20 | self.project().poll_read(cx, buf)
21 | }
22 | }
23 |
24 | impl<'a> git::protocol::futures_io::AsyncBufRead for Fixture<'a> {
25 | fn poll_fill_buf(
26 | self: std::pin::Pin<&mut Self>,
27 | cx: &mut std::task::Context<'_>,
28 | ) -> std::task::Poll> {
29 | self.project().poll_fill_buf(cx)
30 | }
31 |
32 | fn consume(self: std::pin::Pin<&mut Self>, amt: usize) {
33 | self.project().consume(amt)
34 | }
35 | }
36 |
37 | #[async_trait(?Send)]
38 | impl<'a> git::protocol::transport::client::ReadlineBufRead for Fixture<'a> {
39 | async fn readline(
40 | &mut self,
41 | ) -> Option, packetline::decode::Error>>>
42 | {
43 | let bytes: &BStr = self.0.into();
44 | let mut lines = bytes.lines();
45 | let res = lines.next()?;
46 | self.0 = lines.as_bytes();
47 | Some(Ok(Ok(packetline::PacketLineRef::Data(res))))
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/crates/git-remote-icp/src/connect.rs:
--------------------------------------------------------------------------------
1 | use crate::http::Remote;
2 |
3 | use git::protocol::transport;
4 | use git::url::Scheme;
5 | use git_repository as git;
6 | use ic_agent::agent::http_transport::ReqwestHttpReplicaV2Transport;
7 | use ic_agent::export::Principal;
8 | use ic_agent::{Agent, Identity};
9 | use log::trace;
10 | use std::sync::Arc;
11 | use tokio::runtime::Runtime;
12 | use transport::client::connect::Error;
13 |
14 | pub fn connect<'a, Url, E>(
15 | identity: Arc,
16 | fetch_root_key: bool,
17 | replica_url: String,
18 | canister_id: Principal,
19 | ) -> impl Fn(Url, transport::connect::Options) -> Result, Error>
20 | where
21 | Url: TryInto,
22 | git::url::parse::Error: From,
23 | {
24 | trace!("identity: {:#?}", identity);
25 | trace!("fetch_root_key: {:#?}", fetch_root_key);
26 | trace!("replica_url: {}", replica_url);
27 | trace!("canister_id: {}", canister_id);
28 |
29 | move |url: Url, options| {
30 | let mut url = url.try_into().map_err(git::url::parse::Error::from)?;
31 |
32 | if url.user().is_some() {
33 | return Err(Error::UnsupportedUrlTokens {
34 | url: url.to_bstring(),
35 | scheme: url.scheme,
36 | });
37 | }
38 |
39 | trace!("Provided URL scheme: {:#?}", url.scheme);
40 |
41 | url.scheme = match url.scheme {
42 | Scheme::Ext(scheme) if &scheme == "icp" => Ok(Scheme::Https),
43 | scheme @ (Scheme::Https | Scheme::Http) => Ok(scheme),
44 | _ => Err(Error::UnsupportedScheme(url.scheme)),
45 | }?;
46 |
47 | trace!("Resolved URL scheme: {:#?}", url.scheme);
48 |
49 | let replica_transport = ReqwestHttpReplicaV2Transport::create(&replica_url)
50 | .map_err(|err| Error::Connection(Box::new(err)))?;
51 |
52 | let agent = Agent::builder()
53 | .with_transport(replica_transport)
54 | .with_arc_identity(identity.clone())
55 | .build()
56 | .map_err(|err| Error::Connection(Box::new(err)))?;
57 |
58 | if fetch_root_key {
59 | let runtime = Runtime::new().map_err(|err| Error::Connection(Box::new(err)))?;
60 |
61 | runtime
62 | .block_on(agent.fetch_root_key())
63 | .map_err(|err| Error::Connection(Box::new(err)))?;
64 | }
65 |
66 | let remote = Remote::new(agent, canister_id);
67 |
68 | let transport = transport::client::http::connect_http(
69 | remote,
70 | &url.to_bstring().to_string(),
71 | options.version,
72 | );
73 |
74 | Ok(Box::new(transport))
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/commands/list.rs:
--------------------------------------------------------------------------------
1 | use clap::ValueEnum;
2 | use git_repository as git;
3 | use log::trace;
4 | use maybe_async::maybe_async;
5 |
6 | #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, ValueEnum)]
7 | pub enum ListVariant {
8 | ForPush,
9 | }
10 |
11 | #[maybe_async]
12 | pub async fn execute(
13 | mut transport: T,
14 | authenticate: AuthFn,
15 | variant: &Option,
16 | ) -> anyhow::Result<()>
17 | where
18 | AuthFn: FnMut(git::credentials::helper::Action) -> git::credentials::protocol::Result,
19 | T: git::protocol::transport::client::Transport,
20 | {
21 | match variant {
22 | Some(x) => match x {
23 | ListVariant::ForPush => trace!("list for-push"),
24 | },
25 | None => {
26 | trace!("list");
27 | }
28 | }
29 |
30 | // Implement once option capability is supported
31 | let mut progress = git::progress::Discard;
32 | let extra_parameters = vec![];
33 |
34 | let outcome = git::protocol::fetch::handshake(
35 | &mut transport,
36 | authenticate,
37 | extra_parameters,
38 | &mut progress,
39 | )
40 | .await?;
41 |
42 | let refs = git::protocol::ls_refs(
43 | &mut transport,
44 | &outcome.capabilities,
45 | // TODO: gain a better understanding of
46 | // https://github.com/Byron/gitoxide/blob/da5f63cbc7506990f46d310f8064678decb86928/git-repository/src/remote/connection/ref_map.rs#L153-L168
47 | |_capabilities, _arguments, _features| Ok(git::protocol::ls_refs::Action::Continue),
48 | &mut progress,
49 | )
50 | .await?;
51 |
52 | trace!("refs: {:#?}", refs);
53 |
54 | // TODO: buffer and flush
55 | refs.iter().for_each(|r| println!("{}", ref_to_string(r)));
56 | println!();
57 |
58 | Ok(())
59 | }
60 |
61 | fn ref_to_string(r: &git::protocol::handshake::Ref) -> String {
62 | use git::protocol::handshake::Ref;
63 |
64 | match r {
65 | Ref::Peeled {
66 | full_ref_name,
67 | tag: _,
68 | object: _,
69 | } => {
70 | // FIXME: not sure how to handle peeled refs yet
71 | format!("? {}", full_ref_name)
72 | }
73 | Ref::Direct {
74 | full_ref_name,
75 | object,
76 | } => {
77 | // 91536083cdb16ef3c29638054642b50a34ea8c25 refs/heads/main
78 | format!("{} {}", object, full_ref_name)
79 | }
80 | Ref::Symbolic {
81 | full_ref_name,
82 | target,
83 | object: _,
84 | } => {
85 | // @refs/heads/main HEAD
86 | format!("@{} {}", target, full_ref_name)
87 | }
88 | // TODO: determine if this is the correct way to handle unborn symrefs
89 | Ref::Unborn {
90 | full_ref_name,
91 | target,
92 | } => {
93 | // @refs/heads/main HEAD
94 | format!("@{} {}", target, full_ref_name)
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # git-remote-icp
2 |
3 |
4 | [](https://github.com/codebase-labs/git-remote-icp/actions/workflows/ci.yml)
5 |
6 | A [Git remote helper](https://git-scm.com/docs/gitremote-helpers) for the [Internet Computer](https://internetcomputer.org) Protocol.
7 |
8 | ## Demos
9 |
10 | * Cloning a repo from [codebase.org](https://codebase.org), hosted on the Internet Computer, using the IC’s native auth:
11 | * [with the Git CLI](https://twitter.com/py/status/1608749309427879936)
12 | * [with GitHub Desktop](https://twitter.com/py/status/1608749699980464129)
13 |
14 | ## Usage
15 |
16 | 1. Install to a location that is in your `PATH`.
17 | 2. Use `git` as you normally would, but use `icp://` instead of `https://` in URLs.
18 |
19 |
20 | ## Generating a public/private key pair
21 |
22 | ```
23 | openssl ecparam -name secp256k1 -genkey -noout -out identity.pem
24 | openssl ec -in identity.pem -pubout -out identity.pub
25 | ```
26 |
27 | ## Configuring Git
28 |
29 | See the example `.gitconfig`
30 |
31 | ## Crates
32 |
33 | This repository contains the following other crates:
34 |
35 | * `git-remote-helper`
36 |
37 | A library for implementing Git remote helpers.
38 |
39 | Provides core functionality for remote helpers in a protocol-agnostic way for both blocking and async implementations.
40 |
41 | * `git-remote-tcp`
42 |
43 | A Git remote helper for the `git://` protocol.
44 |
45 | Primarily used to test that the async implementation in `git-remote-helper` behaves the same as `git`.
46 |
47 | * `git-remote-http-reqwest`
48 |
49 | A Git remote helper for `http://` and `https://` protocols.
50 |
51 | Primarily used to test that the blocking implementation in `git-remote-helper` behaves the same as `git` (`git-remote-http` and `git-remote-https`).
52 |
53 | ## Development
54 |
55 | * Set `HOME=.` when run from the root of this repository to use the provided `.gitconfig`.
56 | * The `icp://` scheme requires HTTPS. Use `icp::http://` for local development.
57 |
58 | ### Against a local repository
59 |
60 | ```
61 | cargo build --package git-remote-icp && PATH=./target/debug:$PATH RUST_LOG=trace HOME=. git clone icp::http://rwlgt-iiaaa-aaaaa-aaaaa-cai.raw.ic0.localhost:8453/@paul/hello-world.git
62 | ```
63 |
64 | or
65 |
66 | ```
67 | cargo build --package git-remote-icp && PATH=./target/debug:$PATH RUST_LOG=trace HOME=. git clone icp::http://git.codebase.ic0.localhost:8453/@paul/hello-world.git
68 | ```
69 |
70 | ### Against a remote repository
71 |
72 | ```
73 | cargo build --package git-remote-icp && PATH=./target/debug:$PATH RUST_LOG=trace HOME=. git clone icp://w7uni-tiaaa-aaaam-qaydq-cai.raw.ic0.app/@paul/hello-world.git
74 | ```
75 |
76 | ### By manually invoking the remote helper
77 |
78 | ```
79 | cargo build --package git-remote-icp && PATH=./target/debug:$PATH RUST_LOG=trace HOME=. GIT_DIR=~/temp/hello-world git-remote-icp origin http://rwlgt-iiaaa-aaaaa-aaaaa-cai.raw.ic0.localhost:8453/@paul/hello-world.git
80 | ```
81 |
82 | or, without rebuilding:
83 |
84 | ```
85 | RUST_LOG=trace HOME=. GIT_DIR=~/temp/hello-world cargo run origin http://rwlgt-iiaaa-aaaaa-aaaaa-cai.raw.ic0.localhost:8453/@paul/hello-world.git
86 | ```
87 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "crane": {
4 | "inputs": {
5 | "flake-compat": "flake-compat",
6 | "flake-utils": "flake-utils",
7 | "nixpkgs": [
8 | "nixpkgs"
9 | ],
10 | "rust-overlay": "rust-overlay"
11 | },
12 | "locked": {
13 | "lastModified": 1668047118,
14 | "narHash": "sha256-F4xP7dAU6ca+hYa3qF0CtnwfQJT3YH4qEh/IxO+p9t0=",
15 | "owner": "ipetkov",
16 | "repo": "crane",
17 | "rev": "074825a9e8d6446564e2ae6949ac3feb79aa7397",
18 | "type": "github"
19 | },
20 | "original": {
21 | "owner": "ipetkov",
22 | "repo": "crane",
23 | "type": "github"
24 | }
25 | },
26 | "flake-compat": {
27 | "flake": false,
28 | "locked": {
29 | "lastModified": 1650374568,
30 | "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
31 | "owner": "edolstra",
32 | "repo": "flake-compat",
33 | "rev": "b4a34015c698c7793d592d66adbab377907a2be8",
34 | "type": "github"
35 | },
36 | "original": {
37 | "owner": "edolstra",
38 | "repo": "flake-compat",
39 | "type": "github"
40 | }
41 | },
42 | "flake-utils": {
43 | "locked": {
44 | "lastModified": 1667395993,
45 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
46 | "owner": "numtide",
47 | "repo": "flake-utils",
48 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
49 | "type": "github"
50 | },
51 | "original": {
52 | "owner": "numtide",
53 | "repo": "flake-utils",
54 | "type": "github"
55 | }
56 | },
57 | "flake-utils_2": {
58 | "locked": {
59 | "lastModified": 1667395993,
60 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
61 | "owner": "numtide",
62 | "repo": "flake-utils",
63 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
64 | "type": "github"
65 | },
66 | "original": {
67 | "owner": "numtide",
68 | "repo": "flake-utils",
69 | "type": "github"
70 | }
71 | },
72 | "flake-utils_3": {
73 | "locked": {
74 | "lastModified": 1659877975,
75 | "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
76 | "owner": "numtide",
77 | "repo": "flake-utils",
78 | "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
79 | "type": "github"
80 | },
81 | "original": {
82 | "owner": "numtide",
83 | "repo": "flake-utils",
84 | "type": "github"
85 | }
86 | },
87 | "nixpkgs": {
88 | "locked": {
89 | "lastModified": 1668086072,
90 | "narHash": "sha256-msFoXI5ThCmhTTqgl27hpCXWhaeqxphBaleJAgD8JYM=",
91 | "owner": "NixOS",
92 | "repo": "nixpkgs",
93 | "rev": "72d8853228c9758820c39b8659415b6d89279493",
94 | "type": "github"
95 | },
96 | "original": {
97 | "owner": "NixOS",
98 | "ref": "nixpkgs-unstable",
99 | "repo": "nixpkgs",
100 | "type": "github"
101 | }
102 | },
103 | "nixpkgs_2": {
104 | "locked": {
105 | "lastModified": 1663819708,
106 | "narHash": "sha256-nKyJpwzGoV+5BNLHlrEMawsDxR1i6WOXqTcHWrWE8y4=",
107 | "owner": "NixOS",
108 | "repo": "nixpkgs",
109 | "rev": "f586d35a11ec07ced41d9d62c125c6ca0006141f",
110 | "type": "github"
111 | },
112 | "original": {
113 | "owner": "NixOS",
114 | "ref": "nixpkgs-unstable",
115 | "repo": "nixpkgs",
116 | "type": "github"
117 | }
118 | },
119 | "root": {
120 | "inputs": {
121 | "crane": "crane",
122 | "flake-utils": "flake-utils_3",
123 | "nixpkgs": "nixpkgs_2",
124 | "rust-overlay": "rust-overlay_2"
125 | }
126 | },
127 | "rust-overlay": {
128 | "inputs": {
129 | "flake-utils": "flake-utils_2",
130 | "nixpkgs": "nixpkgs"
131 | },
132 | "locked": {
133 | "lastModified": 1667487142,
134 | "narHash": "sha256-bVuzLs1ZVggJAbJmEDVO9G6p8BH3HRaolK70KXvnWnU=",
135 | "owner": "oxalica",
136 | "repo": "rust-overlay",
137 | "rev": "cf668f737ac986c0a89e83b6b2e3c5ddbd8cf33b",
138 | "type": "github"
139 | },
140 | "original": {
141 | "owner": "oxalica",
142 | "repo": "rust-overlay",
143 | "type": "github"
144 | }
145 | },
146 | "rust-overlay_2": {
147 | "inputs": {
148 | "flake-utils": [
149 | "flake-utils"
150 | ],
151 | "nixpkgs": [
152 | "nixpkgs"
153 | ]
154 | },
155 | "locked": {
156 | "lastModified": 1670552927,
157 | "narHash": "sha256-lCE51eAGrAFS4k9W5aDGFpVtOAwQQ/rFMN80PCDh0vo=",
158 | "owner": "oxalica",
159 | "repo": "rust-overlay",
160 | "rev": "a0fdafd18c9cf599fde17fbaf07dbb20fa57eecb",
161 | "type": "github"
162 | },
163 | "original": {
164 | "owner": "oxalica",
165 | "repo": "rust-overlay",
166 | "type": "github"
167 | }
168 | }
169 | },
170 | "root": "root",
171 | "version": 7
172 | }
173 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![deny(rust_2018_idioms)]
2 |
3 | pub mod cli;
4 | pub mod commands;
5 | pub mod git;
6 |
7 | use anyhow::{anyhow, Context};
8 | use clap::{Command, FromArgMatches as _, Parser as _, Subcommand as _};
9 | use cli::Args;
10 | use commands::Commands;
11 | use git_repository as gitoxide;
12 | use gitoxide::protocol::transport;
13 | use log::trace;
14 | use maybe_async::maybe_async;
15 | use std::collections::BTreeSet;
16 | use std::env;
17 | use std::path::Path;
18 | use strum::VariantNames as _;
19 |
20 | #[cfg(all(feature = "async-network-client", feature = "blocking-network-client"))]
21 | compile_error!("Cannot set both 'async-network-client' and 'blocking-network-client' features as they are mutually exclusive");
22 |
23 | const GIT_DIR: &str = "GIT_DIR";
24 |
25 | #[maybe_async]
26 | pub async fn main(
27 | connect: impl Fn(String, transport::client::connect::Options) -> C,
28 | ) -> anyhow::Result<()>
29 | where
30 | C: std::future::Future<
31 | Output = Result<
32 | Box<(dyn transport::client::Transport + Send)>,
33 | transport::client::connect::Error,
34 | >,
35 | >,
36 | {
37 | let args = Args::parse();
38 | trace!("args.repository: {:?}", args.repository);
39 | trace!("args.url: {:?}", args.url);
40 |
41 | gitoxide::interrupt::init_handler(move || {})?;
42 |
43 | let git_dir = env::var(GIT_DIR).context("failed to get GIT_DIR")?;
44 | trace!("GIT_DIR: {}", git_dir);
45 |
46 | let repo_dir = Path::new(&git_dir)
47 | .parent()
48 | .ok_or_else(|| anyhow!("failed to get repository directory"))?;
49 |
50 | let repo = gitoxide::open(repo_dir)?;
51 |
52 | // TODO: implementer provides this
53 | let authenticate =
54 | |action| panic!("unexpected call to authenticate with action: {:#?}", action);
55 |
56 | let mut fetch: commands::fetch::Batch = BTreeSet::new();
57 | let mut push: commands::push::Batch = BTreeSet::new();
58 |
59 | loop {
60 | trace!("loop");
61 |
62 | // TODO: BString?
63 | let mut input = String::new();
64 |
65 | std::io::stdin()
66 | .read_line(&mut input)
67 | .context("failed to read from stdin")?;
68 |
69 | let input = input.trim();
70 |
71 | if input.is_empty() {
72 | trace!("terminated with a blank line");
73 |
74 | let fetch_transport = connect(
75 | args.url.clone(),
76 | transport::client::connect::Options {
77 | version: transport::Protocol::V2,
78 | #[cfg(feature = "blocking-network-client")]
79 | ssh: Default::default(),
80 | },
81 | )
82 | .await?;
83 |
84 | commands::fetch::process(fetch_transport, &repo, &args.url, &mut fetch).await?;
85 |
86 | // NOTE: push still uses the v1 protocol so we use that here.
87 | let mut push_transport = connect(
88 | args.url.clone(),
89 | transport::client::connect::Options {
90 | version: transport::Protocol::V1,
91 | #[cfg(feature = "blocking-network-client")]
92 | ssh: Default::default(),
93 | },
94 | )
95 | .await?;
96 |
97 | commands::push::process(&mut push_transport, &repo, authenticate, &mut push).await?;
98 |
99 | // continue; // Useful to inspect .git directory before it disappears
100 | break Ok(());
101 | }
102 |
103 | let input = input.split(' ').collect::>();
104 |
105 | trace!("input: {:#?}", input);
106 |
107 | let input_command = Command::new("git-remote-icp")
108 | .multicall(true)
109 | .subcommand_required(true);
110 |
111 | let input_command = Commands::augment_subcommands(input_command);
112 | let matches = input_command.try_get_matches_from(input)?;
113 | let command = Commands::from_arg_matches(&matches)?;
114 |
115 | match command {
116 | Commands::Capabilities => {
117 | // TODO: buffer and flush
118 | Commands::VARIANTS
119 | .iter()
120 | .filter(|command| **command != "capabilities" && **command != "list")
121 | .for_each(|command| println!("{}", command));
122 | println!();
123 | }
124 | Commands::Fetch { hash, name } => {
125 | trace!("batch fetch {} {}", hash, name);
126 | let _ = fetch.insert((hash, name));
127 | }
128 | Commands::List { variant } => {
129 | let mut transport = connect(
130 | args.url.clone(),
131 | transport::client::connect::Options {
132 | version: transport::Protocol::V2,
133 | #[cfg(feature = "blocking-network-client")]
134 | ssh: Default::default(),
135 | },
136 | )
137 | .await?;
138 |
139 | commands::list::execute(&mut transport, authenticate, &variant).await?
140 | }
141 | Commands::Push { src_dst } => {
142 | trace!("batch push {}", src_dst);
143 | let _ = push.insert(src_dst);
144 | }
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/nix/git-remote-helper.nix:
--------------------------------------------------------------------------------
1 | { pkgs
2 | , craneLib
3 | , src
4 | , scheme
5 | , port
6 | , path_ ? "/"
7 | , nativeBuildInputs ? []
8 | , installCheckInputs ? []
9 | , doInstallCheck ? true
10 | , configure ? ""
11 | , setup
12 | , teardown
13 | }:
14 |
15 | let
16 | schemeToEnvVar = scheme:
17 | builtins.replaceStrings ["-"] ["_"] (pkgs.lib.toUpper scheme);
18 |
19 | SCHEME = {
20 | INTERNAL = schemeToEnvVar scheme.internal;
21 | EXTERNAL = schemeToEnvVar scheme.external;
22 | };
23 |
24 | pname = "git-remote-${scheme.external}";
25 | cargoExtraArgs = "--package ${pname}";
26 | in
27 | craneLib.buildPackage {
28 | inherit cargoExtraArgs pname src nativeBuildInputs doInstallCheck;
29 | cargoArtifacts = craneLib.buildDepsOnly {
30 | inherit cargoExtraArgs pname src nativeBuildInputs;
31 | };
32 | installCheckInputs = installCheckInputs ++ [
33 | pkgs.git
34 | pkgs.netcat
35 | ];
36 | installCheckPhase = ''
37 | set -e
38 |
39 | export HOME=$TMP
40 | export PATH=$out/bin:$PATH
41 |
42 | export RUST_BACKTRACE=full
43 | export RUST_LOG=trace
44 |
45 | export GIT_TRACE=true
46 | export GIT_CURL_VERBOSE=true
47 | export GIT_TRACE_PACK_ACCESS=true
48 | export GIT_TRACE_PACKET=true
49 | export GIT_TRACE_PACKFILE=true
50 | export GIT_TRACE_PERFORMANCE=true
51 | export GIT_TRACE_SETUP=true
52 | export GIT_TRACE_SHALLOW=true
53 |
54 | export GIT_AUTHOR_DATE="2022-11-14 21:26:57 -0800"
55 | export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
56 |
57 | git config --global init.defaultBranch main
58 | git config --global user.name "Test"
59 | git config --global user.email 0+test.users.noreply@codebase.org
60 | git config --global receive.denyCurrentBranch updateInstead
61 | git config --global protocol.version 2
62 |
63 | echo "---------------"
64 | echo "configure start"
65 | echo "---------------"
66 |
67 | ${configure}
68 |
69 | echo "------------------"
70 | echo "configure complete"
71 | echo "------------------"
72 |
73 | # Set up test repo
74 |
75 | mkdir test-repo
76 | git -C test-repo init
77 | echo '# Hello, World!' > test-repo/README.md
78 | git -C test-repo add .
79 | git -C test-repo commit -m "Initial commit"
80 |
81 | GIT_LOG_INIT=$(git -C test-repo log)
82 |
83 | git clone --bare test-repo test-repo-bare
84 | git -C test-repo-bare update-server-info
85 |
86 | echo "-----------"
87 | echo "setup start"
88 | echo "-----------"
89 |
90 | ${setup}
91 |
92 | echo "--------------"
93 | echo "setup complete"
94 | echo "--------------"
95 |
96 | while ! nc -z localhost ${toString port}; do
97 | sleep 0.1
98 | done
99 |
100 |
101 | # Test clone
102 |
103 | echo "------------------"
104 | echo "native clone start"
105 | echo "------------------"
106 |
107 | git clone ${scheme.internal}://localhost:${toString port}${path_} test-repo-${scheme.internal}
108 |
109 | echo "---------------------"
110 | echo "native clone complete"
111 | echo "---------------------"
112 |
113 | echo "-------------------------"
114 | echo "remote helper clone start"
115 | echo "-------------------------"
116 |
117 | git clone ${scheme.external}://localhost:${toString port}${path_} test-repo-${scheme.external}
118 |
119 | echo "----------------------------"
120 | echo "remote helper clone complete"
121 | echo "----------------------------"
122 |
123 | GIT_LOG_${SCHEME.INTERNAL}=$(git -C test-repo-${scheme.internal} log)
124 | GIT_LOG_${SCHEME.EXTERNAL}=$(git -C test-repo-${scheme.external} log)
125 |
126 | if [ "$GIT_LOG_INIT" == "$GIT_LOG_${SCHEME.INTERNAL}" ]; then
127 | echo "GIT_LOG_INIT == GIT_LOG_${SCHEME.INTERNAL}"
128 | else
129 | echo "GIT_LOG_INIT != GIT_LOG_${SCHEME.INTERNAL}"
130 | exit 1
131 | fi
132 |
133 | if [ "$GIT_LOG_${SCHEME.INTERNAL}" == "$GIT_LOG_${SCHEME.EXTERNAL}" ]; then
134 | echo "GIT_LOG_${SCHEME.INTERNAL} == GIT_LOG_${SCHEME.EXTERNAL}"
135 | else
136 | echo "GIT_LOG_${SCHEME.INTERNAL} != GIT_LOG_${SCHEME.EXTERNAL}"
137 | exit 1
138 | fi
139 |
140 | GIT_DIFF_${SCHEME.INTERNAL}=$(git -C test-repo-${scheme.internal} diff)
141 |
142 | git -C test-repo-${scheme.external} remote add -f test-repo-${scheme.internal} "$PWD/test-repo-${scheme.internal}"
143 | git -C test-repo-${scheme.external} remote update
144 | GIT_DIFF_${SCHEME.EXTERNAL}=$(git -C test-repo-${scheme.external} diff main remotes/test-repo-${scheme.internal}/main)
145 |
146 | if [ "$GIT_DIFF_${SCHEME.INTERNAL}" == "$GIT_DIFF_${SCHEME.EXTERNAL}" ]; then
147 | echo "GIT_DIFF_${SCHEME.INTERNAL} == GIT_DIFF_${SCHEME.EXTERNAL}"
148 | else
149 | echo "GIT_DIFF_${SCHEME.INTERNAL} != GIT_DIFF_${SCHEME.EXTERNAL}"
150 | exit 1
151 | fi
152 |
153 |
154 | # Test push
155 |
156 | echo "-----------------"
157 | echo "native push start"
158 | echo "-----------------"
159 |
160 | echo "" >> test-repo-${scheme.internal}/README.md
161 | git -C test-repo-${scheme.internal} add .
162 | git -C test-repo-${scheme.internal} commit -m "Add trailing newline"
163 | git -C test-repo-${scheme.internal} push origin main
164 |
165 | echo "--------------------"
166 | echo "native push complete"
167 | echo "--------------------"
168 |
169 | echo "------------------------"
170 | echo "remote helper push start"
171 | echo "------------------------"
172 |
173 | echo "" >> test-repo-${scheme.external}/README.md
174 | git -C test-repo-${scheme.external} add .
175 | git -C test-repo-${scheme.external} commit -m "Add trailing newline"
176 | git -C test-repo-${scheme.external} push origin main
177 |
178 | echo "---------------------------"
179 | echo "remote helper push complete"
180 | echo "---------------------------"
181 |
182 | GIT_LOG_${SCHEME.INTERNAL}_REMOTE=$(git -C test-repo-${scheme.internal} log origin/main)
183 | GIT_LOG_${SCHEME.EXTERNAL}_REMOTE=$(git -C test-repo-${scheme.external} log origin/main)
184 |
185 | if [ "$GIT_LOG_${SCHEME.INTERNAL}_REMOTE" == "$GIT_LOG_${SCHEME.EXTERNAL}_REMOTE" ]; then
186 | echo "GIT_LOG_${SCHEME.INTERNAL}_REMOTE == GIT_LOG_${SCHEME.EXTERNAL}_REMOTE"
187 | else
188 | echo "GIT_LOG_${SCHEME.INTERNAL}_REMOTE != GIT_LOG_${SCHEME.EXTERNAL}_REMOTE"
189 | echo "<<<<<<< GIT_LOG_${SCHEME.INTERNAL}_REMOTE"
190 | echo "$GIT_LOG_${SCHEME.INTERNAL}_REMOTE"
191 | echo "======="
192 | echo "$GIT_LOG_${SCHEME.EXTERNAL}_REMOTE"
193 | echo ">>>>>>> GIT_LOG_${SCHEME.EXTERNAL}_REMOTE"
194 |
195 | exit 1
196 | fi
197 |
198 | git -C test-repo-${scheme.external} remote update
199 | GIT_DIFF_${SCHEME.INTERNAL}_REMOTE=$(git -C test-repo-${scheme.internal} diff origin/main origin/main)
200 | GIT_DIFF_${SCHEME.EXTERNAL}_REMOTE=$(git -C test-repo-${scheme.external} diff origin/main remotes/test-repo-${scheme.internal}/main)
201 |
202 | if [ "$GIT_DIFF_${SCHEME.INTERNAL}_REMOTE" == "$GIT_DIFF_${SCHEME.EXTERNAL}_REMOTE" ]; then
203 | echo "GIT_DIFF_${SCHEME.INTERNAL}_REMOTE == GIT_DIFF_${SCHEME.EXTERNAL}_REMOTE"
204 | else
205 | echo "GIT_DIFF_${SCHEME.INTERNAL}_REMOTE != GIT_DIFF_${SCHEME.EXTERNAL}_REMOTE"
206 | echo "<<<<<<< GIT_DIFF_${SCHEME.INTERNAL}_REMOTE"
207 | echo "$GIT_DIFF_${SCHEME.INTERNAL}_REMOTE"
208 | echo "======="
209 | echo "$GIT_DIFF_${SCHEME.EXTERNAL}_REMOTE"
210 | echo ">>>>>>> GIT_DIFF_${SCHEME.EXTERNAL}_REMOTE"
211 |
212 | exit 1
213 | fi
214 |
215 | echo "--------------"
216 | echo "teardown start"
217 | echo "--------------"
218 |
219 | ${teardown}
220 |
221 | echo "-----------------"
222 | echo "teardown complete"
223 | echo "-----------------"
224 | '';
225 | }
226 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | inputs = {
3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
4 |
5 | crane = {
6 | url = "github:ipetkov/crane";
7 | inputs.nixpkgs.follows = "nixpkgs";
8 | };
9 |
10 | flake-utils.url = "github:numtide/flake-utils";
11 |
12 | rust-overlay = {
13 | url = "github:oxalica/rust-overlay";
14 | inputs = {
15 | nixpkgs.follows = "nixpkgs";
16 | flake-utils.follows = "flake-utils";
17 | };
18 | };
19 | };
20 |
21 | outputs = {
22 | self,
23 | nixpkgs,
24 | crane,
25 | flake-utils,
26 | rust-overlay
27 | }:
28 | let
29 | supportedSystems = [
30 | flake-utils.lib.system.aarch64-darwin
31 | flake-utils.lib.system.x86_64-darwin
32 | flake-utils.lib.system.x86_64-linux
33 | ];
34 | in
35 | flake-utils.lib.eachSystem supportedSystems (system:
36 | let
37 | pkgs = import nixpkgs {
38 | inherit system;
39 | overlays = [
40 | (import rust-overlay)
41 | ];
42 | };
43 |
44 | rust = pkgs.rust-bin.stable.latest.default;
45 | # rust = pkgs.rust-bin.nightly."2022-10-31".default;
46 |
47 | # NB: we don't need to overlay our custom toolchain for the *entire*
48 | # pkgs (which would require rebuilding anything else which uses rust).
49 | # Instead, we just want to update the scope that crane will use by appending
50 | # our specific toolchain there.
51 | craneLib = (crane.mkLib pkgs).overrideToolchain rust;
52 | # craneLib = crane.lib."${system}";
53 |
54 | src = ./.;
55 |
56 | lighttpd-conf = port: pkgs.writeText "lighthttpd.conf" ''
57 | server.document-root = var.CWD
58 | server.port = ${toString port}
59 |
60 | server.modules = (
61 | "mod_alias",
62 | "mod_setenv",
63 | "mod_cgi",
64 | )
65 |
66 | debug.log-request-header = "enable"
67 | debug.log-response-header = "enable"
68 | debug.log-file-not-found = "enable"
69 | debug.log-request-handling = "enable"
70 | debug.log-condition-handling = "enable"
71 | debug.log-condition-cache-handling = "enable"
72 | debug.log-ssl-noise = "enable"
73 | debug.log-timeouts = "enable"
74 |
75 | alias.url += (
76 | "/git" => "${pkgs.git}/libexec/git-core/git-http-backend"
77 | )
78 |
79 | $HTTP["url"] =~ "^/git" {
80 | cgi.assign = ("" => "")
81 |
82 | setenv.set-environment = (
83 | "GIT_PROJECT_ROOT" => var.CWD,
84 | "GIT_HTTP_EXPORT_ALL" => "",
85 | "REMOTE_USER" => "$REDIRECT_REMOTE_USER",
86 | )
87 |
88 | $REQUEST_HEADER["Git-Protocol"] =~ "(.+)" {
89 | setenv.add-environment += (
90 | "HTTP_GIT_PROTOCOL" => "%1",
91 | )
92 | }
93 | }
94 | '';
95 |
96 | git-remote-helper = features: craneLib.buildPackage rec {
97 | inherit src;
98 | pname = "git-remote-helper";
99 | cargoExtraArgs = "--package ${pname} --features ${features}";
100 | cargoArtifacts = craneLib.buildDepsOnly {
101 | inherit cargoExtraArgs pname src;
102 | };
103 | nativeBuildInputs = [
104 | pkgs.cmake
105 | ];
106 | };
107 |
108 | git-remote-helper-async = git-remote-helper "async-network-client";
109 | git-remote-helper-blocking = git-remote-helper "blocking-network-client";
110 |
111 | git-remote-http-reqwest = pkgs.callPackage ./nix/git-remote-helper.nix rec {
112 | inherit craneLib src;
113 | scheme = { internal = "http"; external = "http-reqwest"; };
114 | path_ = "/git/test-repo-bare";
115 | port = 8888;
116 | installCheckInputs = [
117 | pkgs.lighttpd
118 | ];
119 | configure = ''
120 | git config --global --type bool http.receivePack true
121 | '';
122 | setup = ''
123 | lighttpd -f ${lighttpd-conf port} -D 2>&1 &
124 | HTTP_SERVER_PID=$!
125 | trap "EXIT_CODE=\$? && kill \$HTTP_SERVER_PID && exit \$EXIT_CODE" EXIT
126 | '';
127 | teardown = ''
128 | kill "$HTTP_SERVER_PID"
129 | '';
130 | };
131 |
132 | git-remote-icp = pkgs.callPackage ./nix/git-remote-helper.nix {
133 | inherit craneLib src;
134 | scheme = { internal = "http"; external = "icp"; };
135 | port = 1234;
136 | nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [
137 | # https://nixos.wiki/wiki/Rust#Building_the_openssl-sys_crate
138 | pkgs.openssl_1_1
139 | pkgs.pkgconfig
140 | ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
141 | pkgs.darwin.apple_sdk.frameworks.Security
142 | ];
143 | # This package is currently not tested _in this repository_
144 | doInstallCheck = false;
145 | configure = ''
146 | HOME=.
147 | '';
148 | setup = ''
149 | '';
150 | teardown = ''
151 | '';
152 | };
153 |
154 | git-remote-tcp = pkgs.callPackage ./nix/git-remote-helper.nix rec {
155 | inherit craneLib src;
156 | scheme = { internal = "git"; external = "tcp"; };
157 | # DEFAULT_GIT_PORT is 9418
158 | path_ = "/test-repo-bare";
159 | port = 9418;
160 | setup = ''
161 | # Based on https://github.com/Byron/gitoxide/blob/0c9c48b3b91a1396eb1796f288a2cb10380d1f14/tests/helpers.sh#L59
162 | git daemon --verbose --base-path=. --enable=receive-pack --export-all &
163 | GIT_DAEMON_PID=$!
164 | trap "EXIT_CODE=\$? && kill \$GIT_DAEMON_PID && exit \$EXIT_CODE" EXIT
165 | '';
166 | teardown = ''
167 | kill "$GIT_DAEMON_PID"
168 | '';
169 | };
170 |
171 | apps = {
172 | git-remote-http-reqwest = flake-utils.lib.mkApp {
173 | drv = git-remote-http-reqwest;
174 | };
175 |
176 | git-remote-icp = flake-utils.lib.mkApp {
177 | drv = git-remote-icp;
178 | };
179 |
180 | git-remote-tcp = flake-utils.lib.mkApp {
181 | drv = git-remote-tcp;
182 | };
183 | };
184 | in
185 | rec {
186 | checks = {
187 | inherit
188 | git-remote-helper-blocking
189 | git-remote-helper-async
190 | git-remote-http-reqwest
191 | git-remote-icp
192 | git-remote-tcp
193 | ;
194 | };
195 |
196 | packages = {
197 | inherit
198 | git-remote-helper-blocking
199 | git-remote-helper-async
200 | git-remote-http-reqwest
201 | git-remote-icp
202 | git-remote-tcp
203 | ;
204 | lighttpd-conf = lighttpd-conf 8888;
205 | };
206 |
207 | inherit apps;
208 |
209 | defaultPackage = packages.git-remote-icp;
210 | defaultApp = apps.git-remote-icp;
211 |
212 | devShell = pkgs.mkShell {
213 | # RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
214 | RUST_SRC_PATH = pkgs.rust.packages.stable.rustPlatform.rustLibSrc;
215 | inputsFrom = builtins.attrValues checks;
216 | nativeBuildInputs = pkgs.lib.foldl
217 | (state: drv: builtins.concatLists [state drv.nativeBuildInputs])
218 | [pkgs.lighttpd]
219 | (pkgs.lib.attrValues packages)
220 | ;
221 | };
222 | }
223 | );
224 | }
225 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/commands/push.rs:
--------------------------------------------------------------------------------
1 | use crate::git::service::receive_pack;
2 | use anyhow::anyhow;
3 | use git::bstr::ByteSlice as _;
4 | use git::odb::pack::data::output::count::objects::ObjectExpansion;
5 | use git_repository as git;
6 | use log::trace;
7 | use maybe_async::maybe_async;
8 | use std::collections::BTreeSet;
9 |
10 | #[cfg(feature = "blocking-network-client")]
11 | use std::io::Write as _;
12 |
13 | #[cfg(feature = "async-network-client")]
14 | use git::protocol::futures_lite::io::AsyncWriteExt as _;
15 |
16 | pub type Batch = BTreeSet;
17 |
18 | #[maybe_async]
19 | pub async fn process(
20 | mut transport: T,
21 | repo: &git::Repository,
22 | authenticate: AuthFn,
23 | batch: &mut Batch,
24 | ) -> anyhow::Result<()>
25 | where
26 | AuthFn: FnMut(git::credentials::helper::Action) -> git::credentials::protocol::Result,
27 | T: git::protocol::transport::client::Transport,
28 | {
29 | if !batch.is_empty() {
30 | trace!("process push: {:#?}", batch);
31 |
32 | use git::refspec::parse::Operation;
33 | use git::refspec::{instruction, Instruction};
34 |
35 | // Implement once option capability is supported
36 | let mut progress = git::progress::Discard;
37 | let extra_parameters = vec![];
38 |
39 | let mut outcome = git::protocol::handshake(
40 | &mut transport,
41 | git::protocol::transport::Service::ReceivePack,
42 | authenticate,
43 | extra_parameters,
44 | &mut progress,
45 | )
46 | .await?;
47 |
48 | let remote_refs = outcome
49 | .refs
50 | .take()
51 | .ok_or_else(|| anyhow!("failed to take remote refs"))?;
52 |
53 | trace!("remote_refs: {:#?}", remote_refs);
54 |
55 | let mut request_writer = transport.request(
56 | git::protocol::transport::client::WriteMode::Binary,
57 | // This is currently redundant because we use `.into_parts()`
58 | git::protocol::transport::client::MessageKind::Flush,
59 | )?;
60 |
61 | let instructions = batch
62 | .iter()
63 | .map(|unparse_ref_spec| {
64 | let ref_spec_ref =
65 | git::refspec::parse(unparse_ref_spec.as_bytes().as_bstr(), Operation::Push)?;
66 | Ok(ref_spec_ref.instruction())
67 | })
68 | .collect::, anyhow::Error>>()?;
69 |
70 | trace!("instructions: {:#?}", instructions);
71 |
72 | let push_instructions = instructions
73 | .iter()
74 | .filter_map(|instruction| match instruction {
75 | Instruction::Push(instruction::Push::Matching {
76 | src,
77 | dst,
78 | allow_non_fast_forward,
79 | }) => Some((src, dst, allow_non_fast_forward)),
80 | _ => None,
81 | });
82 |
83 | trace!("push instructions: {:#?}", push_instructions);
84 |
85 | // TODO: use Traverse for initial push
86 | let input_object_expansion = ObjectExpansion::TreeAdditionsComparedToAncestor;
87 |
88 | let mut entries = vec![];
89 |
90 | for (src, dst, _allow_non_fast_forward) in push_instructions {
91 | // local
92 | let mut src_reference = repo.find_reference(*src)?;
93 | let src_id = src_reference.peel_to_id_in_place()?;
94 |
95 | // remote
96 | let dst_id = remote_refs
97 | .iter()
98 | .find_map(|r| {
99 | let (name, target, peeled) = r.unpack();
100 | (name == *dst).then(|| peeled.or(target)).flatten()
101 | })
102 | .map(|x| x.to_owned())
103 | .unwrap_or_else(|| git::hash::Kind::Sha1.null());
104 |
105 | trace!("dst_id: {:#?}", dst_id);
106 |
107 | let dst_object = repo.find_object(dst_id)?;
108 | let dst_commit = dst_object.try_into_commit()?;
109 | let dst_commit_time = dst_commit
110 | .committer()
111 | .map(|committer| committer.time.seconds_since_unix_epoch)?;
112 |
113 | let ancestors = src_id
114 | .ancestors()
115 | .sorting(
116 | git::traverse::commit::Sorting::ByCommitTimeNewestFirstCutoffOlderThan {
117 | time_in_seconds_since_epoch: dst_commit_time,
118 | },
119 | )
120 | // TODO: repo object cache?
121 | .all()
122 | // NOTE: this is suboptimal but makes debugging easier
123 | .map(|ancestor_commits| ancestor_commits.collect::>());
124 |
125 | trace!("ancestors: {:#?}", ancestors);
126 |
127 | // FIXME: We need to handle fast-forwards and force pushes.
128 | // Ideally we'd fail fast but we can't because figuring out
129 | // if a fast-forward is possible consumes the
130 | // `ancestor_commits` iterator which can't be cloned.
131 | //
132 | // TODO: Investigate if we can do this after we're otherwise
133 | // done with `ancestor_commits`.
134 | /*
135 | let is_fast_forward = match ancestor_commits {
136 | Ok(mut commits) => commits.any(|commit_id| {
137 | commit_id.map_or(false, |commit_id| commit_id == dst_id)
138 | }),
139 | Err(_) => false,
140 | };
141 |
142 | trace!("is_fast_forward: {:#?}", is_fast_forward);
143 | trace!("allow_non_fast_forward: {:#?}", allow_non_fast_forward);
144 |
145 | if !is_fast_forward && !allow_non_fast_forward {
146 | return Err(anyhow!("attempted non fast-forward push without force"));
147 | }
148 | */
149 |
150 | // TODO: set_pack_cache?
151 | // TODO: ignore_replacements?
152 | let mut db = repo.objects.clone();
153 | db.prevent_pack_unload();
154 |
155 | // NOTE: we don't want to short circuit on this Result
156 | // until after we've determined if we can fast-forward.
157 | let commits = ancestors?;
158 |
159 | let (mut counts, _count_stats) =
160 | git::odb::pack::data::output::count::objects_unthreaded(
161 | db.clone(),
162 | commits.into_iter(),
163 | // Implement once option capability is supported
164 | git::progress::Discard,
165 | &git::interrupt::IS_INTERRUPTED,
166 | input_object_expansion,
167 | )?;
168 |
169 | counts.shrink_to_fit();
170 |
171 | trace!("counts: {:#?}", counts);
172 |
173 | // TODO: in order iter
174 | let mut entries_iter = git::odb::pack::data::output::entry::iter_from_counts(
175 | counts,
176 | db,
177 | git::progress::Discard,
178 | git::odb::pack::data::output::entry::iter_from_counts::Options {
179 | allow_thin_pack: false,
180 | ..Default::default()
181 | },
182 | );
183 |
184 | entries.push(
185 | git::parallel::InOrderIter::from(entries_iter.by_ref())
186 | .collect::, _>>()?
187 | .into_iter()
188 | .flatten()
189 | .collect::>(),
190 | );
191 |
192 | // NOTE: We request `report-status` and `report-status-v2` so that
193 | // we receive a response that includes a status report.
194 | //
195 | // We request both in case a server only advertises one or the
196 | // other.
197 | //
198 | // We parse the status report from the response and write our own
199 | // status report to stdout in the format that remote helpers are
200 | // expected to produce.
201 | let chunk = format!(
202 | "{} {} {}\0 report-status report-status-v2 \n",
203 | dst_id.to_hex(),
204 | src_id.to_hex(),
205 | dst
206 | );
207 |
208 | request_writer.write_all(chunk.as_bytes().as_bstr()).await?;
209 | }
210 |
211 | request_writer
212 | .write_message(git::protocol::transport::client::MessageKind::Flush)
213 | .await?;
214 |
215 | let entries = entries.into_iter().flatten().collect::>();
216 | trace!("entries: {:#?}", entries);
217 |
218 | let num_entries: u32 = entries.len().try_into()?;
219 | trace!("num entries: {:#?}", num_entries);
220 |
221 | let (mut writer, reader) = request_writer.into_parts();
222 |
223 | #[cfg(feature = "async-network-client")]
224 | let mut writer = git::protocol::futures_lite::io::BlockOn::new(&mut writer);
225 |
226 | let pack_writer = git::odb::pack::data::output::bytes::FromEntriesIter::new(
227 | std::iter::once(Ok::<
228 | _,
229 | git::odb::pack::data::output::entry::iter_from_counts::Error<
230 | git::odb::store::find::Error,
231 | >,
232 | >(entries)),
233 | &mut writer,
234 | num_entries,
235 | git::odb::pack::data::Version::V2,
236 | git::hash::Kind::Sha1,
237 | );
238 |
239 | // The pack writer is lazy, so we need to consume it
240 | for write_result in pack_writer {
241 | let bytes_written = write_result?;
242 | trace!("bytes written: {:#?}", bytes_written);
243 | }
244 |
245 | // Signal that we are done writing
246 | drop(writer);
247 |
248 | trace!("finished writing pack");
249 |
250 |
251 | let (_unpack_result, command_statuses) =
252 | receive_pack::response::read_and_parse(reader).await?;
253 |
254 | command_statuses.iter().for_each(|command_status| {
255 | trace!("{:#?}", command_status);
256 | match command_status {
257 | receive_pack::response::CommandStatusV2::Ok(ref_name, _option_lines) => {
258 | let output = format!("ok {}", ref_name);
259 | trace!("output: {}", output);
260 | println!("{}", output);
261 | }
262 | receive_pack::response::CommandStatusV2::Fail(ref_name, error_msg) => {
263 | let output = format!("error {} {}\0", ref_name, error_msg);
264 | trace!("output: {}", output);
265 | println!("{}", output);
266 | }
267 | }
268 | });
269 |
270 | batch.clear();
271 |
272 | // Terminate the status report output
273 | println!();
274 | }
275 |
276 | Ok(())
277 | }
278 |
--------------------------------------------------------------------------------
/crates/git-remote-icp/src/http/reqwest/remote.rs:
--------------------------------------------------------------------------------
1 | // Based on
2 | // https://github.com/Byron/gitoxide/blob/e6b9906c486b11057936da16ed6e0ec450a0fb83/git-transport/src/client/blocking_io/http/reqwest/remote.rs
3 |
4 | use crate::{http, http::reqwest::Remote};
5 |
6 | use candid::{Decode, Encode};
7 | use git_features::io::pipe;
8 | use git_repository as git;
9 | use git::protocol::transport::client::http::PostBodyDataKind;
10 | use ic_agent::export::Principal;
11 | use ic_agent::Agent;
12 | use ic_certified_assets::types::{HeaderField, HttpRequest, HttpResponse};
13 | use log::trace;
14 | use serde_bytes::ByteBuf;
15 | use std::any::Any;
16 | use std::io::{Read, Write};
17 | use std::ops::Deref;
18 | use tokio::runtime::Runtime;
19 |
20 | /// The error returned by the 'remote' helper, a purely internal construct to perform http requests.
21 | #[derive(Debug, thiserror::Error)]
22 | #[allow(missing_docs)]
23 | pub enum Error {
24 | #[error(transparent)]
25 | Reqwest(#[from] reqwest::Error),
26 | }
27 |
28 | impl git::protocol::transport::IsSpuriousError for Error {
29 | fn is_spurious(&self) -> bool {
30 | match self {
31 | Error::Reqwest(err) => {
32 | err.is_timeout()
33 | || err.is_connect()
34 | || err
35 | .status()
36 | .map_or(false, |status| status.is_server_error())
37 | }
38 | }
39 | }
40 | }
41 |
42 | impl Remote {
43 | pub fn new(agent: Agent, canister_id: Principal) -> Self {
44 | let (req_send, req_recv) = std::sync::mpsc::sync_channel(0);
45 | let (res_send, res_recv) = std::sync::mpsc::sync_channel(0);
46 | let runtime = Runtime::new().expect("failed to create runtime");
47 | let moved_agent = agent.clone();
48 | let handle = std::thread::spawn(move || -> Result<(), Error> {
49 | // We may error while configuring, which is expected as part of the internal protocol. The error will be
50 | // received and the sender of the request might restart us.
51 | for Request {
52 | url,
53 | headers,
54 | upload_body_kind,
55 | } in req_recv
56 | {
57 | let (post_body_tx, mut post_body_rx) = pipe::unidirectional(0);
58 | let (mut response_body_tx, response_body_rx) = pipe::unidirectional(0);
59 | let (mut headers_tx, headers_rx) = pipe::unidirectional(0);
60 |
61 | if res_send
62 | .send(Response {
63 | headers: headers_rx,
64 | body: response_body_rx,
65 | upload_body: post_body_tx,
66 | })
67 | .is_err()
68 | {
69 | // This means our internal protocol is violated as the one who sent the request isn't listening anymore.
70 | // Shut down as something is off.
71 | break;
72 | }
73 |
74 | let mut body = ByteBuf::new();
75 |
76 | if let Some(_) = upload_body_kind {
77 | if let Err(err) = post_body_rx.read_to_end(&mut body) {
78 | let kind = std::io::ErrorKind::Other;
79 | let err = Err(std::io::Error::new(kind, err));
80 | response_body_tx.channel.send(err).ok();
81 | continue;
82 | }
83 | }
84 |
85 | let method = if let Some(_) = upload_body_kind {
86 | "POST"
87 | } else {
88 | "GET"
89 | }
90 | .to_string();
91 |
92 | let http_request = HttpRequest {
93 | method,
94 | url,
95 | headers,
96 | body,
97 | };
98 |
99 | trace!("http_request: {:#?}", http_request);
100 |
101 | let arg = match candid::Encode!(&http_request) {
102 | Ok(arg) => arg,
103 | Err(err) => {
104 | let kind = std::io::ErrorKind::Other;
105 | let err = Err(std::io::Error::new(kind, err));
106 | headers_tx.channel.send(err).ok();
107 | continue;
108 | }
109 | };
110 |
111 | let res = if let Some(_) = upload_body_kind {
112 | runtime.block_on(
113 | moved_agent
114 | .update(&canister_id, "http_request_update")
115 | .with_arg(&arg)
116 | .call_and_wait(),
117 | )
118 | } else {
119 | runtime.block_on(
120 | moved_agent
121 | .query(&canister_id, "http_request")
122 | .with_arg(&arg)
123 | .call(),
124 | )
125 | };
126 |
127 | let res = res
128 | .map_err(|agent_error| {
129 | std::io::Error::new(std::io::ErrorKind::Other, agent_error)
130 | })
131 | .and_then(|res| {
132 | Decode!(res.as_slice(), HttpResponse).map_err(|candid_error| {
133 | std::io::Error::new(std::io::ErrorKind::Other, candid_error)
134 | })
135 | })
136 | .and_then(|res| match res.status_code {
137 | 400..=499 | 500..=599 => reqwest::StatusCode::from_u16(res.status_code)
138 | .map_err(|invalid_status_code_error| {
139 | std::io::Error::new(
140 | std::io::ErrorKind::Other,
141 | invalid_status_code_error,
142 | )
143 | })
144 | .and_then(|status| {
145 | let kind = if status == reqwest::StatusCode::UNAUTHORIZED {
146 | std::io::ErrorKind::PermissionDenied
147 | } else if status.is_server_error() {
148 | std::io::ErrorKind::ConnectionAborted
149 | } else {
150 | std::io::ErrorKind::Other
151 | };
152 | let err = format!("Received HTTP status {}", status.as_str());
153 | Err(std::io::Error::new(kind, err))
154 | }),
155 | _ => Ok(res),
156 | });
157 |
158 | let res = match res {
159 | Ok(res) => res,
160 | Err(err) => {
161 | headers_tx.channel.send(Err(err)).ok();
162 | continue;
163 | }
164 | };
165 |
166 | let send_headers = {
167 | move || -> std::io::Result<()> {
168 | for (name, value) in res.headers {
169 | headers_tx.write_all(name.as_str().as_bytes())?;
170 | headers_tx.write_all(b":")?;
171 | headers_tx.write_all(value.as_bytes())?;
172 | headers_tx.write_all(b"\n")?;
173 | }
174 | // Make sure this is an FnOnce closure to signal the remote reader we are done.
175 | drop(headers_tx);
176 | Ok(())
177 | }
178 | };
179 |
180 | // We don't have to care if anybody is receiving the header, as
181 | // a matter of fact we cannot fail sending them. Thus an error
182 | // means the receiver failed somehow, but might also have
183 | // decided not to read headers at all. Fine with us.
184 | send_headers().ok();
185 |
186 | // Reading the response body is streaming and may fail for many
187 | // reasons. If so, we send the error over the response body
188 | // channel and that's all we can do.
189 | if let Err(err) = std::io::copy(&mut res.body.deref(), &mut response_body_tx) {
190 | response_body_tx.channel.send(Err(err)).ok();
191 | }
192 | }
193 | Ok(())
194 | });
195 |
196 | Remote {
197 | agent,
198 | canister_id,
199 | handle: Some(handle),
200 | request: req_send,
201 | response: res_recv,
202 | }
203 | }
204 | }
205 |
206 | /// utilities
207 | impl Remote {
208 | fn make_request(
209 | &mut self,
210 | url: &str,
211 | _base_url: &str,
212 | headers: impl IntoIterator- >,
213 | upload_body_kind: Option,
214 | ) -> Result, http::Error> {
215 | let mut header_values = Vec::new();
216 | for header_line in headers {
217 | let header_line = header_line.as_ref();
218 | let colon_pos = header_line
219 | .find(':')
220 | .expect("header line must contain a colon to separate key and value");
221 | let header_name = &header_line[..colon_pos];
222 | let value = &header_line[colon_pos + 1..];
223 | header_values.push((header_name.trim().to_string(), value.trim().to_string()));
224 | }
225 | self.request
226 | .send(Request {
227 | url: url.to_owned(),
228 | headers: header_values,
229 | upload_body_kind,
230 | })
231 | .expect("the remote cannot be down at this point");
232 |
233 | let Response {
234 | headers,
235 | body,
236 | upload_body,
237 | } = match self.response.recv() {
238 | Ok(res) => res,
239 | Err(_) => {
240 | let err = self
241 | .handle
242 | .take()
243 | .expect("always present")
244 | .join()
245 | .expect("no panic")
246 | .expect_err("no receiver means thread is down with init error");
247 | *self = Self::new(self.agent.clone(), self.canister_id);
248 | return Err(http::Error::InitHttpClient {
249 | source: Box::new(err),
250 | });
251 | }
252 | };
253 |
254 | Ok(http::PostResponse {
255 | post_body: upload_body,
256 | headers,
257 | body,
258 | })
259 | }
260 | }
261 |
262 | impl http::Http for Remote {
263 | type Headers = pipe::Reader;
264 | type ResponseBody = pipe::Reader;
265 | type PostBody = pipe::Writer;
266 |
267 | fn get(
268 | &mut self,
269 | url: &str,
270 | base_url: &str,
271 | headers: impl IntoIterator
- >,
272 | ) -> Result, http::Error> {
273 | self.make_request(url, base_url, headers, None)
274 | .map(Into::into)
275 | }
276 |
277 | fn post(
278 | &mut self,
279 | url: &str,
280 | base_url: &str,
281 | headers: impl IntoIterator
- >,
282 | post_body_kind: PostBodyDataKind,
283 | ) -> Result, http::Error>
284 | {
285 | self.make_request(url, base_url, headers, Some(post_body_kind))
286 | }
287 |
288 | fn configure(
289 | &mut self,
290 | _config: &dyn Any,
291 | ) -> Result<(), Box> {
292 | Ok(())
293 | }
294 | }
295 |
296 | pub(crate) struct Request {
297 | pub url: String,
298 | pub headers: Vec,
299 | pub upload_body_kind: Option,
300 | }
301 |
302 | /// A link to a thread who provides data for the contained readers.
303 | /// The expected order is:
304 | /// - write `upload_body`
305 | /// - read `headers` to end
306 | /// - read `body` to end
307 | pub(crate) struct Response {
308 | pub headers: pipe::Reader,
309 | pub body: pipe::Reader,
310 | pub upload_body: pipe::Writer,
311 | }
312 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/git/service/receive_pack/response/report_status_v2/tests/mod.rs:
--------------------------------------------------------------------------------
1 | mod fixture;
2 |
3 | use super::*;
4 | use fixture::Fixture;
5 | use git::bstr::ByteSlice;
6 | use git_repository as git;
7 | use maybe_async::maybe_async;
8 |
9 | #[maybe_async::test(
10 | feature = "blocking-network-client",
11 | async(feature = "async-network-client", tokio::test)
12 | )]
13 | async fn test_read_and_parse_ok_0_command_status_v2() {
14 | let mut input = vec!["000eunpack ok", "0000"].join("\n").into_bytes();
15 | let reader = Fixture(&mut input);
16 | let result = read_and_parse(reader).await;
17 | assert_eq!(
18 | result,
19 | Err(ParseError::ExpectedOneOrMoreCommandStatusV2),
20 | "report-status-v2"
21 | )
22 | }
23 |
24 | #[maybe_async::test(
25 | feature = "blocking-network-client",
26 | async(feature = "async-network-client", tokio::test)
27 | )]
28 | async fn test_read_and_parse_ok_1_command_status_v2_ok() {
29 | let mut input = vec!["000eunpack ok", "0017ok refs/heads/main", "0000"]
30 | .join("\n")
31 | .into_bytes();
32 | let reader = Fixture(&mut input);
33 | let result = read_and_parse(reader).await;
34 | assert_eq!(
35 | result,
36 | Ok((
37 | UnpackResult::Ok,
38 | vec![CommandStatusV2::Ok(
39 | RefName(BString::new(b"refs/heads/main".to_vec())),
40 | Vec::new(),
41 | ),]
42 | )),
43 | "report-status-v2"
44 | )
45 | }
46 |
47 | #[maybe_async::test(
48 | feature = "blocking-network-client",
49 | async(feature = "async-network-client", tokio::test)
50 | )]
51 | async fn test_read_and_parse_ok_1_command_status_v2_fail() {
52 | let mut input = vec![
53 | "000eunpack ok",
54 | "002ang refs/heads/main some error message",
55 | "0000",
56 | ]
57 | .join("\n")
58 | .into_bytes();
59 | let reader = Fixture(&mut input);
60 | let result = read_and_parse(reader).await;
61 | assert_eq!(
62 | result,
63 | Ok((
64 | UnpackResult::Ok,
65 | vec![CommandStatusV2::Fail(
66 | RefName(BString::new(b"refs/heads/main".to_vec())),
67 | ErrorMsg(BString::new(b"some error message\n".to_vec()))
68 | ),]
69 | )),
70 | "report-status-v2"
71 | )
72 | }
73 |
74 | #[maybe_async::test(
75 | feature = "blocking-network-client",
76 | async(feature = "async-network-client", tokio::test)
77 | )]
78 | async fn test_read_and_parse_ok_2_command_statuses_v2_ok_fail() {
79 | let mut input = vec![
80 | "000eunpack ok",
81 | "0018ok refs/heads/debug",
82 | "0028ng refs/heads/main non-fast-forward",
83 | "0000",
84 | ]
85 | .join("\n")
86 | .into_bytes();
87 | let reader = Fixture(&mut input);
88 | let result = read_and_parse(reader).await;
89 | assert_eq!(
90 | result,
91 | Ok((
92 | UnpackResult::Ok,
93 | vec![
94 | CommandStatusV2::Ok(
95 | RefName(BString::new(b"refs/heads/debug".to_vec())),
96 | Vec::new(),
97 | ),
98 | CommandStatusV2::Fail(
99 | RefName(BString::new(b"refs/heads/main".to_vec())),
100 | ErrorMsg(BString::new(b"non-fast-forward\n".to_vec()))
101 | ),
102 | ]
103 | )),
104 | "report-status-v2"
105 | )
106 | }
107 |
108 | #[maybe_async::test(
109 | feature = "blocking-network-client",
110 | async(feature = "async-network-client", tokio::test)
111 | )]
112 | async fn test_read_and_parse_ok_2_command_statuses_v2_fail_ok() {
113 | let mut input = vec![
114 | "000eunpack ok",
115 | "0028ng refs/heads/main non-fast-forward",
116 | "0018ok refs/heads/debug",
117 | "0000",
118 | ]
119 | .join("\n")
120 | .into_bytes();
121 | let reader = Fixture(&mut input);
122 | let result = read_and_parse(reader).await;
123 | assert_eq!(
124 | result,
125 | Ok((
126 | UnpackResult::Ok,
127 | vec![
128 | CommandStatusV2::Fail(
129 | RefName(BString::new(b"refs/heads/main".to_vec())),
130 | ErrorMsg(BString::new(b"non-fast-forward\n".to_vec()))
131 | ),
132 | CommandStatusV2::Ok(
133 | RefName(BString::new(b"refs/heads/debug".to_vec())),
134 | Vec::new(),
135 | ),
136 | ]
137 | )),
138 | "report-status-v2"
139 | )
140 | }
141 |
142 | #[maybe_async]
143 | #[test]
144 | fn test_parse_unpack_status_ok() {
145 | let input = b"unpack ok";
146 | let result = parse_unpack_status::>(input);
147 | assert_eq!(result.map(|x| x.1), Ok(UnpackResult::Ok), "ok")
148 | }
149 |
150 | #[maybe_async]
151 | #[test]
152 | fn test_parse_unpack_status_ok_newline() {
153 | let input = b"unpack ok\n";
154 | let result = parse_unpack_status::>(input);
155 | assert_eq!(result.map(|x| x.1), Ok(UnpackResult::Ok), "ok")
156 | }
157 |
158 | #[maybe_async]
159 | #[test]
160 | fn test_parse_unpack_status_error_msg() {
161 | let input = b"unpack some error message";
162 | let result = parse_unpack_status::>(input);
163 | assert_eq!(
164 | result.map(|x| x.1),
165 | Ok(UnpackResult::ErrorMsg(ErrorMsg(BString::new(
166 | b"some error message".to_vec()
167 | )))),
168 | "error msg"
169 | )
170 | }
171 |
172 | #[maybe_async]
173 | #[test]
174 | fn test_parse_unpack_status_error_msg_newline() {
175 | let input = b"unpack some error message\n";
176 | let result = parse_unpack_status::>(input);
177 | assert_eq!(
178 | result.map(|x| x.1),
179 | Ok(UnpackResult::ErrorMsg(ErrorMsg(BString::new(
180 | b"some error message\n".to_vec()
181 | )))),
182 | "error msg"
183 | )
184 | }
185 |
186 | #[maybe_async]
187 | #[test]
188 | fn test_parse_unpack_result_ok() {
189 | let input = b"ok";
190 | let result = parse_unpack_result::>(input);
191 | assert_eq!(result.map(|x| x.1), Ok(UnpackResult::Ok), "ok");
192 | }
193 |
194 | #[maybe_async]
195 | #[test]
196 | fn test_parse_unpack_result_error_msg() {
197 | let input = b"some error message";
198 | let result = parse_unpack_result::>(input);
199 | assert_eq!(
200 | result.map(|x| x.1),
201 | Ok(UnpackResult::ErrorMsg(ErrorMsg(BString::new(
202 | input.to_vec()
203 | )))),
204 | "error msg"
205 | )
206 | }
207 |
208 | #[maybe_async::test(
209 | feature = "blocking-network-client",
210 | async(feature = "async-network-client", tokio::test)
211 | )]
212 | async fn test_read_and_parse_command_status_v2_command_ok_v2_0_option_lines() {
213 | let input = b"ok refs/heads/main";
214 | let mut reader = Fixture(input);
215 | let result = read_and_parse_command_statuses_v2::>(&mut reader).await;
216 | assert_eq!(
217 | result,
218 | Ok(vec![CommandStatusV2::Ok(
219 | RefName(BString::new(b"refs/heads/main".to_vec())),
220 | Vec::new(),
221 | )]),
222 | "command-status-v2"
223 | )
224 | }
225 |
226 | #[maybe_async::test(
227 | feature = "blocking-network-client",
228 | async(feature = "async-network-client", tokio::test)
229 | )]
230 | async fn test_read_and_parse_command_status_v2_command_ok_v2_0_option_lines_newline() {
231 | let input = b"ok refs/heads/main\n";
232 | let mut reader = Fixture(input);
233 | let result = read_and_parse_command_statuses_v2::>(&mut reader).await;
234 | assert_eq!(
235 | result,
236 | Ok(vec![CommandStatusV2::Ok(
237 | RefName(BString::new(b"refs/heads/main".to_vec())),
238 | Vec::new(),
239 | )]),
240 | "command-status-v2"
241 | )
242 | }
243 |
244 | #[ignore]
245 | #[maybe_async::test(
246 | feature = "blocking-network-client",
247 | async(feature = "async-network-client", tokio::test)
248 | )]
249 | async fn test_read_and_parse_command_status_v2_command_ok_v2_1_option_lines() {
250 | todo!()
251 | }
252 |
253 | #[ignore]
254 | #[maybe_async::test(
255 | feature = "blocking-network-client",
256 | async(feature = "async-network-client", tokio::test)
257 | )]
258 | async fn test_read_and_parse_command_status_v2_command_ok_v2_1_option_lines_newline() {
259 | todo!()
260 | }
261 |
262 | #[ignore]
263 | #[maybe_async::test(
264 | feature = "blocking-network-client",
265 | async(feature = "async-network-client", tokio::test)
266 | )]
267 | async fn test_read_and_parse_command_status_v2_command_ok_v2_2_option_lines() {
268 | todo!()
269 | }
270 |
271 | #[ignore]
272 | #[maybe_async::test(
273 | feature = "blocking-network-client",
274 | async(feature = "async-network-client", tokio::test)
275 | )]
276 | async fn test_read_and_parse_command_status_v2_command_ok_v2_2_option_lines_newline() {
277 | todo!()
278 | }
279 |
280 | #[ignore]
281 | #[maybe_async::test(
282 | feature = "blocking-network-client",
283 | async(feature = "async-network-client", tokio::test)
284 | )]
285 | async fn test_read_and_parse_command_status_v2_command_ok_v2_3_option_lines() {
286 | todo!()
287 | }
288 |
289 | #[ignore]
290 | #[maybe_async::test(
291 | feature = "blocking-network-client",
292 | async(feature = "async-network-client", tokio::test)
293 | )]
294 | async fn test_read_and_parse_command_status_v2_command_ok_v2_3_option_lines_newline() {
295 | todo!()
296 | }
297 |
298 | #[ignore]
299 | #[maybe_async::test(
300 | feature = "blocking-network-client",
301 | async(feature = "async-network-client", tokio::test)
302 | )]
303 | async fn test_read_and_parse_command_status_v2_command_ok_v2_4_option_lines() {
304 | todo!()
305 | }
306 |
307 | #[ignore]
308 | #[maybe_async::test(
309 | feature = "blocking-network-client",
310 | async(feature = "async-network-client", tokio::test)
311 | )]
312 | async fn test_read_and_parse_command_status_v2_command_ok_v2_4_option_lines_newline() {
313 | todo!()
314 | }
315 |
316 | #[maybe_async::test(
317 | feature = "blocking-network-client",
318 | async(feature = "async-network-client", tokio::test)
319 | )]
320 | async fn test_read_and_parse_command_status_v2_command_fail() {
321 | let input = b"ng refs/heads/main some error message";
322 | let mut reader = Fixture(input);
323 | let result = read_and_parse_command_statuses_v2::>(&mut reader).await;
324 | assert_eq!(
325 | result,
326 | Ok(vec![CommandStatusV2::Fail(
327 | RefName(BString::new(b"refs/heads/main".to_vec())),
328 | ErrorMsg(BString::new(b"some error message".to_vec())),
329 | )]),
330 | "command-status-v2"
331 | )
332 | }
333 |
334 | #[maybe_async::test(
335 | feature = "blocking-network-client",
336 | async(feature = "async-network-client", tokio::test)
337 | )]
338 | async fn test_read_and_parse_command_status_v2_command_fail_newline() {
339 | let input = b"ng refs/heads/main some error message\n";
340 | let mut reader = Fixture(input);
341 | let result = read_and_parse_command_statuses_v2::>(&mut reader).await;
342 | assert_eq!(
343 | result,
344 | Ok(vec![CommandStatusV2::Fail(
345 | RefName(BString::new(b"refs/heads/main".to_vec())),
346 | ErrorMsg(BString::new(b"some error message".to_vec())),
347 | )]),
348 | "command-status-v2"
349 | )
350 | }
351 |
352 | #[maybe_async]
353 | #[test]
354 | fn test_parse_command_ok() {
355 | let input = b"ok refs/heads/main";
356 | let result = parse_command_ok::>(input);
357 | assert_eq!(
358 | result.map(|x| x.1),
359 | Ok(RefName(BString::new(b"refs/heads/main".to_vec()))),
360 | "command-ok"
361 | )
362 | }
363 |
364 | #[maybe_async]
365 | #[test]
366 | fn test_parse_command_ok_newline() {
367 | let input = b"ok refs/heads/main\n";
368 | let result = parse_command_ok::>(input);
369 | assert_eq!(
370 | result.map(|x| x.1),
371 | Ok(RefName(BString::new(b"refs/heads/main".to_vec()))),
372 | "command-ok"
373 | )
374 | }
375 |
376 | #[maybe_async]
377 | #[test]
378 | fn test_parse_command_fail() {
379 | let input = b"ng refs/heads/main some error message";
380 | let result = parse_command_fail::>(input);
381 | assert_eq!(
382 | result.map(|x| x.1),
383 | Ok((
384 | RefName(BString::new(b"refs/heads/main".to_vec())),
385 | ErrorMsg(BString::new(b"some error message".to_vec())),
386 | )),
387 | "command-fail"
388 | )
389 | }
390 |
391 | #[maybe_async]
392 | #[test]
393 | fn test_parse_command_fail_newline() {
394 | let input = b"ng refs/heads/main some error message\n";
395 | let result = parse_command_fail::>(input);
396 | assert_eq!(
397 | result.map(|x| x.1),
398 | Ok((
399 | RefName(BString::new(b"refs/heads/main".to_vec())),
400 | ErrorMsg(BString::new(b"some error message\n".to_vec())),
401 | )),
402 | "command-fail"
403 | )
404 | }
405 |
406 | #[maybe_async]
407 | #[test]
408 | fn test_parse_error_msg_not_ok() {
409 | let input = b"some error message";
410 | let result = parse_error_msg::>(input);
411 | assert_eq!(
412 | result.map(|x| x.1),
413 | Ok(ErrorMsg(BString::new(input.to_vec()))),
414 | "error msg not ok"
415 | )
416 | }
417 |
418 | #[maybe_async]
419 | #[test]
420 | fn test_parse_error_msg_ok() {
421 | let input = b"ok";
422 | let result = parse_error_msg::>(input);
423 | assert_eq!(
424 | result.map(|x| x.1),
425 | Err(nom::Err::Error(nom::error::Error {
426 | input: input.as_bytes(),
427 | code: nom::error::ErrorKind::Verify
428 | })),
429 | "error msg is ok"
430 | )
431 | }
432 |
433 | #[maybe_async]
434 | #[test]
435 | fn test_parse_error_msg_empty() {
436 | let input = b"";
437 | let result = parse_error_msg::>(input);
438 | assert_eq!(
439 | result.map(|x| x.1),
440 | Err(nom::Err::Error(nom::error::Error {
441 | input: input.as_bytes(),
442 | code: nom::error::ErrorKind::Verify
443 | })),
444 | "error msg is empty"
445 | )
446 | }
447 |
--------------------------------------------------------------------------------
/crates/git-remote-helper/src/git/service/receive_pack/response/report_status_v2/mod.rs:
--------------------------------------------------------------------------------
1 | // https://git-scm.com/docs/pack-protocol/2.29.0#_report_status
2 |
3 | use derive_more::Display;
4 | use git::bstr::BString;
5 | use git::protocol::transport::client::ReadlineBufRead;
6 | use git::protocol::transport::packetline;
7 | use git_repository as git;
8 | use maybe_async::maybe_async;
9 | use nom::branch::alt;
10 | use nom::bytes::complete::{tag, take_while1};
11 | use nom::character::complete::char;
12 | use nom::combinator::{eof, opt};
13 | use nom::error::context;
14 | use nom::IResult;
15 | use std::cell::Cell;
16 |
17 | #[cfg(test)]
18 | mod tests;
19 |
20 | pub type ReportStatusV2 = (UnpackResult, Vec);
21 |
22 | #[derive(Clone, Debug, Eq, PartialEq)]
23 | pub enum UnpackResult {
24 | Ok,
25 | ErrorMsg(ErrorMsg),
26 | }
27 |
28 | #[derive(Clone, Debug, Eq, PartialEq)]
29 | pub enum CommandStatusV2 {
30 | Ok(RefName, Vec),
31 | Fail(RefName, ErrorMsg),
32 | }
33 |
34 | #[derive(Clone, Debug, Eq, PartialEq)]
35 | pub enum CommandStatusV2Line {
36 | Ok(RefName),
37 | Fail(RefName, ErrorMsg),
38 | OptionLine(OptionLine),
39 | }
40 |
41 | #[derive(Clone, Debug, Eq, PartialEq)]
42 | pub enum OptionLine {
43 | OptionRefName(RefName),
44 | OptionOldOid(git::hash::ObjectId),
45 | OptionNewOid(git::hash::ObjectId),
46 | OptionForce,
47 | }
48 |
49 | #[derive(Clone, Debug, Display, Eq, PartialEq)]
50 | pub struct ErrorMsg(BString);
51 |
52 | #[derive(Clone, Debug, Display, Eq, PartialEq)]
53 | pub struct RefName(BString);
54 |
55 | #[maybe_async]
56 | pub async fn read_and_parse<'a, T>(reader: T) -> Result
57 | where
58 | T: ReadlineBufRead + Unpin + 'a,
59 | {
60 | let mut streaming_peekable_iter =
61 | git::protocol::transport::packetline::StreamingPeekableIter::new(
62 | reader,
63 | &[git::protocol::transport::packetline::PacketLineRef::Flush],
64 | );
65 |
66 | streaming_peekable_iter.fail_on_err_lines(true);
67 | let mut reader = streaming_peekable_iter.as_read();
68 |
69 | let unpack_result = read_data_line_and_parse_with::<_, nom::error::Error<_>>(
70 | &mut reader,
71 | parse_unpack_status,
72 | ParseError::FailedToReadUnpackStatus,
73 | )
74 | .await?;
75 |
76 | let command_statuses_v2 =
77 | read_and_parse_command_statuses_v2::>(&mut reader).await?;
78 |
79 | Ok((unpack_result, command_statuses_v2))
80 | }
81 |
82 | fn parse_unpack_status<'a, E>(input: &'a [u8]) -> IResult<&'a [u8], UnpackResult, E>
83 | where
84 | E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]>,
85 | {
86 | context("unpack-status", |input| {
87 | let (next_input, _unpack) = tag(b"unpack")(input)?;
88 | let (next_input, _space) = char(' ')(next_input)?;
89 | let (next_input, unpack_result) = parse_unpack_result(next_input)?;
90 | let (next_input, _newline) = opt(char('\n'))(next_input)?;
91 | let (next_input, _) = eof(next_input)?;
92 | Ok((next_input, unpack_result))
93 | })(input)
94 | }
95 |
96 | fn parse_unpack_result<'a, E>(input: &'a [u8]) -> IResult<&'a [u8], UnpackResult, E>
97 | where
98 | E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]>,
99 | {
100 | context(
101 | "unpack-result",
102 | alt((
103 | nom::combinator::map(tag(b"ok"), |_| UnpackResult::Ok),
104 | nom::combinator::map(parse_error_msg, UnpackResult::ErrorMsg),
105 | )),
106 | )(input)
107 | }
108 |
109 | // TODO: send commit without tree to trigger error for test case
110 | fn parse_error_msg<'a, E>(input: &'a [u8]) -> IResult<&'a [u8], ErrorMsg, E>
111 | where
112 | E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]>,
113 | {
114 | context("error-msg", |input| {
115 | let (next_input, error_msg) =
116 | // The core rules for the ABNF standard define OCTET as %x00-FF.
117 | //
118 | // However, representing this accurately with `take_while1(|chr|
119 | // 0x00 <= chr && chr <= 0xFF)` exceeds the limits of the u8 type,
120 | // so we use `rest` instead.
121 | nom::combinator::verify(nom::combinator::rest, |bytes: &[u8]| {
122 | !bytes.is_empty() && bytes != b"ok"
123 | })(input)?;
124 |
125 | Ok((next_input, ErrorMsg(BString::from(error_msg))))
126 | })(input)
127 | }
128 |
129 | #[maybe_async]
130 | async fn read_and_parse_command_statuses_v2<'a, E>(
131 | reader: &'a mut (dyn ReadlineBufRead + 'a),
132 | ) -> Result, ParseError>
133 | where
134 | E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]> + std::fmt::Debug,
135 | {
136 | let candidate: Cell