├── 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/badge.svg?event=push)](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> = Cell::new(None); 137 | let mut command_statuses_v2: Vec = Vec::new(); 138 | 139 | while let Some(outcome) = reader.readline().await { 140 | let line = as_slice(outcome)?; 141 | let command_status_v2_line = parse_with(parse_command_status_v2_line, line)?; 142 | 143 | match (candidate.take(), command_status_v2_line) { 144 | // No `command-ok` candidate for adding `option-line`s to, followed 145 | // by a `command-ok` status line. For well-behaved input, this is 146 | // either the first line or a the line after a `command-fail` line. 147 | // 148 | // Set the line as a candidate for adding `option-lines` to. 149 | (None, CommandStatusV2Line::Ok(ref_name)) => { 150 | candidate.set(Some(CommandStatusV2::Ok(ref_name, Vec::new()))); 151 | } 152 | // No `command-ok` candidate for adding `option-line`s to, followed 153 | // by a `command-fail` status line. For well-behaved input, this is 154 | // either the first line or a the line after a `command-fail` line. 155 | // 156 | // Immediately promote the line to `command-status-v2` since 157 | // `option-line` doesn't apply to `command-fail`. 158 | (None, CommandStatusV2Line::Fail(ref_name, error_msg)) => { 159 | command_statuses_v2.push(CommandStatusV2::Fail(ref_name, error_msg)); 160 | } 161 | // A `command-ok` status line followed by a `command-ok` status 162 | // line. 163 | // 164 | // Promote the previous candidate to `command-status-v2` and set the 165 | // current line as the new candidate. 166 | (Some(command_status_v2), CommandStatusV2Line::Ok(ref_name)) => { 167 | command_statuses_v2.push(command_status_v2.clone()); 168 | let new_candidate = CommandStatusV2::Ok(ref_name, Vec::new()); 169 | candidate.set(Some(new_candidate)); 170 | } 171 | // A `command-ok` status line followed by a `command-fail` status line. 172 | // 173 | // Promote both the previous candidate and the current line to 174 | // `command-status-v2`, and reset the candidate since `option-line` 175 | // doesn't apply to `command-fail`. 176 | (Some(command_status_v2), CommandStatusV2Line::Fail(ref_name, error_msg)) => { 177 | command_statuses_v2.push(command_status_v2.clone()); 178 | command_statuses_v2.push(CommandStatusV2::Fail(ref_name, error_msg)); 179 | // This should be redundant because `std::cell::Cell::take()` 180 | // should leave `Default::default()`. 181 | candidate.set(None); 182 | } 183 | // No `command-ok` candidate for adding `option-line`s to, followed 184 | // by an `option-line`. 185 | // 186 | // This is invalid since we don't have a canidate `command-ok` line 187 | // to add `option-line`s to. 188 | (None, CommandStatusV2Line::OptionLine(_)) => { 189 | return Err(ParseError::UnexpectedOptionLine) 190 | } 191 | // A `command-ok` line followed by an `option-line`. 192 | // 193 | // Add the `option-line` to the `command-ok` and set it as the new 194 | // candidate in case the next line is also an `option-line`. 195 | ( 196 | Some(CommandStatusV2::Ok(ref_name, mut option_lines)), 197 | CommandStatusV2Line::OptionLine(option_line), 198 | ) => { 199 | option_lines.push(option_line); 200 | let new_candidate = CommandStatusV2::Ok(ref_name, option_lines); 201 | candidate.set(Some(new_candidate)); 202 | } 203 | // A `command-fail` line followed by an `option-line`. 204 | // 205 | // This is invalid since we don't have a canidate `command-ok` line 206 | // to add `option-line`s to. 207 | (Some(CommandStatusV2::Fail(_, _)), CommandStatusV2Line::OptionLine(_)) => { 208 | return Err(ParseError::UnexpectedOptionLine) 209 | } 210 | } 211 | } 212 | 213 | // The last line of the input produced a candidate which we need to 214 | // promote to a `command-status-v2`. 215 | match candidate.take() { 216 | // A `command-ok` line. This is the only valid candidate at this stage. 217 | // 218 | // Promote the candidate to `command-status-v2`. 219 | Some(CommandStatusV2::Ok(ref_name, option_lines)) => { 220 | command_statuses_v2.push(CommandStatusV2::Ok(ref_name, option_lines)); 221 | } 222 | // A `command-fail` line. This is an invalid candidate. 223 | Some(CommandStatusV2::Fail(_, _)) => return Err(ParseError::UnexpectedCommandFailLine), 224 | None => (), 225 | } 226 | 227 | if command_statuses_v2.is_empty() { 228 | Err(ParseError::ExpectedOneOrMoreCommandStatusV2) 229 | } else { 230 | Ok(command_statuses_v2) 231 | } 232 | } 233 | 234 | fn parse_command_status_v2_line<'a, E>(input: &'a [u8]) -> IResult<&'a [u8], CommandStatusV2Line, E> 235 | where 236 | E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]>, 237 | { 238 | context( 239 | "command-status-v2 line", 240 | alt(( 241 | nom::combinator::map(parse_command_ok, CommandStatusV2Line::Ok), 242 | nom::combinator::map(parse_command_fail, |(ref_name, error_msg)| { 243 | CommandStatusV2Line::Fail(ref_name, error_msg) 244 | }), 245 | nom::combinator::map(parse_option_line, CommandStatusV2Line::OptionLine), 246 | )), 247 | )(input) 248 | } 249 | 250 | fn parse_command_ok<'a, E>(input: &'a [u8]) -> IResult<&'a [u8], RefName, E> 251 | where 252 | E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]>, 253 | { 254 | context("command-ok", |input| { 255 | let (next_input, _unpack) = tag(b"ok")(input)?; 256 | let (next_input, _space) = char(' ')(next_input)?; 257 | let (next_input, refname) = parse_refname(next_input)?; 258 | let (next_input, _newline) = opt(char('\n'))(next_input)?; 259 | let (next_input, _) = eof(next_input)?; 260 | Ok((next_input, refname)) 261 | })(input) 262 | } 263 | 264 | fn parse_command_fail<'a, E>(input: &'a [u8]) -> IResult<&'a [u8], (RefName, ErrorMsg), E> 265 | where 266 | E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]>, 267 | { 268 | context("command-fail", |input| { 269 | let (next_input, _unpack) = tag(b"ng")(input)?; 270 | let (next_input, _space) = char(' ')(next_input)?; 271 | let (next_input, refname) = parse_refname(next_input)?; 272 | let (next_input, _space) = char(' ')(next_input)?; 273 | let (next_input, error_msg) = parse_error_msg(next_input)?; 274 | let (next_input, _newline) = opt(char('\n'))(next_input)?; 275 | let (next_input, _) = eof(next_input)?; 276 | Ok((next_input, (refname, error_msg))) 277 | })(input) 278 | } 279 | 280 | // NOTE 281 | // * This parser is intentionally overly-permissive for now since we treat 282 | // refnames as opaque values anyway. 283 | // * `git_validate::refname` doesn't cover all of the validation cases 284 | // described in documentation. 285 | fn parse_refname<'a, E>(input: &'a [u8]) -> IResult<&'a [u8], RefName, E> 286 | where 287 | E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]>, 288 | { 289 | context("refname", |input| { 290 | let parser = nom::combinator::verify( 291 | take_while1(|chr| { 292 | 0o040 <= chr 293 | && !vec![0o177, b' ', b'~', b'^', b':', b'?', b'*', b'['].contains(&chr) 294 | }), 295 | |refname: &[u8]| git_validate::refname(refname.into()).is_ok(), 296 | ); 297 | nom::combinator::map(parser, |refname: &[u8]| { 298 | RefName(BString::new(refname.to_vec())) 299 | })(input) 300 | })(input) 301 | } 302 | 303 | fn parse_option_line<'a, E>(input: &'a [u8]) -> IResult<&'a [u8], OptionLine, E> 304 | where 305 | E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]>, 306 | { 307 | context("option-line", |_input| { 308 | // TODO 309 | todo!("option-line") 310 | })(input) 311 | } 312 | 313 | #[derive(Clone, Debug, Eq, PartialEq)] 314 | pub enum ParseError { 315 | FailedToReadUnpackStatus, 316 | Io(String), 317 | ExpectedOneOrMoreCommandStatusV2, 318 | Nom(String), 319 | PacketLineDecode(String), 320 | UnexpectedCommandFailLine, 321 | UnexpectedFlush, 322 | UnexpectedDelimiter, 323 | UnexpectedOptionLine, 324 | UnexpectedResponseEnd, 325 | } 326 | 327 | impl std::fmt::Display for ParseError { 328 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 329 | let msg = match self { 330 | Self::FailedToReadUnpackStatus => "failed to read unpack status".to_string(), 331 | Self::Io(err) => format!("IO error: {}", err), 332 | Self::ExpectedOneOrMoreCommandStatusV2 => { 333 | "expected one or more command status v2".to_string() 334 | } 335 | Self::Nom(err) => format!("nom error: {}", err), 336 | Self::PacketLineDecode(err) => err.to_string(), 337 | Self::UnexpectedCommandFailLine => "unexpected command fail line".to_string(), 338 | Self::UnexpectedFlush => "unexpected flush packet".to_string(), 339 | Self::UnexpectedDelimiter => "unexpected delimiter".to_string(), 340 | Self::UnexpectedOptionLine => "unexpected option line".to_string(), 341 | Self::UnexpectedResponseEnd => "unexpected response end".to_string(), 342 | }; 343 | write!(f, "{}", msg) 344 | } 345 | } 346 | 347 | impl std::error::Error for ParseError {} 348 | 349 | #[maybe_async] 350 | async fn read_data_line_and_parse_with<'a, Ok, E>( 351 | input: &'a mut (dyn ReadlineBufRead + 'a), 352 | parser: impl FnMut(&'a [u8]) -> IResult<&'a [u8], Ok>, 353 | read_err: ParseError, 354 | ) -> Result 355 | where 356 | E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]>, 357 | { 358 | let line = read_data_line(input, read_err).await?; 359 | parse_with(parser, line) 360 | } 361 | 362 | fn parse_with<'a, Ok>( 363 | mut parser: impl FnMut(&'a [u8]) -> IResult<&'a [u8], Ok>, 364 | input: &'a [u8], 365 | ) -> Result { 366 | parser(input) 367 | .map(|x| x.1) 368 | .map_err(|err| ParseError::Nom(err.to_string())) 369 | } 370 | 371 | #[maybe_async] 372 | async fn read_data_line<'a>( 373 | input: &'a mut (dyn ReadlineBufRead + 'a), 374 | err: ParseError, 375 | ) -> Result<&'a [u8], ParseError> { 376 | match input.readline().await { 377 | Some(line) => as_slice(line), 378 | None => Err(err), 379 | } 380 | } 381 | 382 | // Similar to `as_slice()` on `packetline::PacketLineRef` 383 | fn as_slice( 384 | readline_outcome: std::io::Result< 385 | Result, packetline::decode::Error>, 386 | >, 387 | ) -> Result<&[u8], ParseError> { 388 | let packet_line_ref = readline_outcome 389 | .map_err(|err| ParseError::Io(err.to_string()))? 390 | .map_err(|err| ParseError::PacketLineDecode(err.to_string()))?; 391 | 392 | match packet_line_ref { 393 | packetline::PacketLineRef::Data(data) => Ok(data), 394 | packetline::PacketLineRef::Flush => Err(ParseError::UnexpectedFlush), 395 | packetline::PacketLineRef::Delimiter => Err(ParseError::UnexpectedDelimiter), 396 | packetline::PacketLineRef::ResponseEnd => Err(ParseError::UnexpectedResponseEnd), 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------