Repository: cargo-generate/cargo-generate (rust)

Issue:
Git SSH URL Support
The [Github Docs](https://docs.github.com/en/enterprise-server@3.0/github/getting-started-with-github/about-remote-repositories#about-remote-repositories) specify that a remote repository URL must have one of the following forms

- https://[hostname]/user/repo.git
- git@[hostname]:user/repo.git

As a result, if cargo-generate handles the former, I would expect it to handle the latter.

```
cameron@chowder ~> cargo generate --version
cargo-generate 0.6.1
cameron@chowder ~> cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
🔧   Creating project called `my-project`...
     Running `git fetch --force --update-head-ok 'https://github.com/rustwasm/wasm-pack-template.git' 'refs/heads/master:refs/remotes/origin/master'`
✨   Done! New project created /home/cameron/my-project
cameron@chowder ~> cargo generate --git git@github.com/rustwasm/wasm-pack-template.git --name my-project-2
🔧   Creating project called `my-project-2`...
Error: ⛔   Git Error: unexpected http status code: 404; class=Http (34)
cameron@chowder ~ [1]> cargo generate --git ssh://git@github.com/rustwasm/wasm-pack-template.git --name my-project-2
🔧   Creating project called `my-project-2`...
Error: ⛔   Git Error: authentication required but no callback set; class=Ssh (23)
```

If this is a features maintainers would like to have, then I would be happy to implement it.

Pull request description:
 feat(remote:ssh): support for ssh remote urls
this PR adds support for ssh remote urls

 - support public ssh remote repository urls start with `git@` and `ssh://`
 - support private ssh remote repositories
 - add a public and an ignored private repo test (ignored because of ci/cd)
 - todo: documentation
 - use dir dependency to get home direction in a cross-platform manner
 - refactor git checkout, to use now close and repository builder
 - make clippy happy
 - based on @SkamDart's PR [#328](https://github.com/cargo-generate/cargo-generate/pull/328)
 - Resolves #325

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f98eb45..4f3644c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -79,6 +79,12 @@ jobs:
         run: |
           git config --global user.email "you@example.com"
           git config --global user.name "Your Name"
+      - name: Install SSH key
+        uses: shimataro/ssh-key-action@v2
+        with:
+          key: ${{ secrets.CI_SSH_KEY }}
+          known_hosts: ${{ secrets.KNOWN_HOSTS }}
+          if_key_exists: replace # replace / ignore / fail; optional (defaults to fail)
       - name: cargo test
         run: cargo test --all --locked -- -Z unstable-options
 
diff --git a/Cargo.lock b/Cargo.lock
index b021e9f..1b7a0fc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -199,6 +199,7 @@ dependencies = [
  "cargo",
  "console",
  "dialoguer",
+ "dirs",
  "git2",
  "heck",
  "home",
@@ -482,6 +483,26 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "dirs"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
 [[package]]
 name = "doc-comment"
 version = "0.3.3"
@@ -1331,6 +1352,16 @@ dependencies = [
  "bitflags",
 ]
 
+[[package]]
+name = "redox_users"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+]
+
 [[package]]
 name = "regex"
 version = "1.5.4"
diff --git a/Cargo.toml b/Cargo.toml
index 7e3c01a..f0b225b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,7 @@ include = ["src/**/*", "LICENSE-*", "*.md"]
 cargo = "0.54.0"
 console = "0.14.1"
 dialoguer = "0.8.0"
+dirs = "3.0.2"
 indicatif = "0.16.2"
 git2 = "0.13.20"
 tempfile = "3.2.0"
diff --git a/README.md b/README.md
index 2363469..99e2ce5 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,29 @@ You can also pass the name of your project to the tool using the `--name` or `-n
 cargo generate --git https://github.com/githubusername/mytemplate.git --name myproject
 ```
 
+## git over ssh
+
+New in version [0.7.0] is the support for both public and private and ssh git remote urls.
+For example:
+```sh
+cargo generate --git git@github.com:rustwasm/wasm-pack-template.git --name mywasm
+```
+leads to the same result as:
+```sh
+cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name mywasm
+```
+as well as:
+```sh
+cargo generate --git rustwasm/wasm-pack-template --name mywasm
+```
+
+> NOTE: you can pass a custom ssh identity file with via `-i | --identity` like `-i ~/.ssh/id_rsa_other`
+
+## http(s) proxy
+
+New in version [0.7.0] is automatic proxy usage. So, if http(s)_PROXY env variables are provided, they 
+will be used for cloning a http(s) template repository. 
+
 ## Favorites
 
 Favorite templates can be defined in a config file, that by default is placed at `$CARGO_HOME/cargo-generate`. 
@@ -97,13 +120,23 @@ cargo generate demo --branch master --name expanded_demo
 Templates are git repositories whose files contain placeholders. The current
 supported placeholders are:
 
-- `{{authors}}`: this will be filled in by a function borrowed from Cargo's source code, that determines your information from Cargo's configuration.
-- `{{project-name}}`: this is supplied by either passing the `--name` flag to the command or working with the interactive CLI to supply a name.
-- `{{crate_name}}`: the snake_case_version of `project-name`
-- `{{crate_type}}`: this is supplied by either passing the `--bin` or `--lib` flag to the command line, contains either `bin` or `lib`, `--bin` is the default
-- `{{os-arch}}`: contains the current operating system and architecture ex: `linux-x86_64`
-
-Additionally all filters and tags of the liquid template language are supported. 
+- `{{authors}}`
+  
+  this will be filled in by a function borrowed from Cargo's source code, that determines your information from Cargo's configuration.
+- `{{project-name}}`
+  
+  this is supplied by either passing the `--name` flag to the command or working with the interactive CLI to supply a name.
+- `{{crate_name}}`
+  
+  the snake_case_version of `project-name`
+- `{{crate_type}}`
+  
+  this is supplied by either passing the `--bin` or `--lib` flag to the command line, contains either `bin` or `lib`, `--bin` is the default
+- `{{os-arch}}`
+  
+  contains the current operating system and architecture ex: `linux-x86_64`
+
+Additionally, **all filters and tags** of the liquid template language are supported. 
 For more information, check out the [Liquid Documentation on `Tags` and `Filters`][liquid].
 
 [liquid]: https://shopify.github.io/liquid
@@ -145,7 +178,7 @@ or use only the library version by passing `--lib` as a command line argument.
 
 Sometimes templates need to make decisions. For example one might want to conditionally include some code or not. 
 Another use case might be that the user of a template should be able to choose out of provided options in an interactive way.
-Also it might be helpful to offer a reasonable default value that the user just simply can use.
+Also, it might be helpful to offer a reasonable default value that the user just simply can use.
 
 Since version [0.6.0](https://github.com/cargo-generate/cargo-generate/releases/tag/v0.6.0) it is possible to use placeholders in a `cargo-generate.toml` that is in the root folder of a template.  
 Here [an example](https://github.com/sassman/hermit-template-rs):
@@ -290,3 +323,4 @@ conditions.
 If you want to contribute to `cargo-generate`, please read our [CONTRIBUTING notes].
 
 [CONTRIBUTING notes]: CONTRIBUTING.md
+[0.7.0]: https://github.com/cargo-generate/cargo-generate/releases/tag/v0.7.0
\ No newline at end of file
diff --git a/src/emoji.rs b/src/emoji.rs
index 021bc4c..eca8dd8 100644
--- a/src/emoji.rs
+++ b/src/emoji.rs
@@ -5,3 +5,4 @@ pub(crate) static SPARKLE: Emoji<'_, '_> = Emoji("✨  ", "");
 pub(crate) static WARN: Emoji<'_, '_> = Emoji("⚠️  ", "");
 pub(crate) static WRENCH: Emoji<'_, '_> = Emoji("🔧  ", "");
 pub(crate) static SHRUG: Emoji<'_, '_> = Emoji("🤷  ", "");
+pub(crate) static INFO: Emoji<'_, '_> = Emoji("💡  ", "");
diff --git a/src/git.rs b/src/git.rs
index 9983eb0..d4a7a81 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -1,53 +1,57 @@
-use anyhow::{Context, Result};
+use crate::emoji;
+use anyhow::Context;
+use anyhow::Result;
 use cargo::core::GitReference;
-use cargo::sources::git::GitRemote;
-use cargo::util::config::Config;
-use git2::{ProxyOptions, Repository as GitRepository, RepositoryInitOptions};
+use console::style;
+use git2::build::RepoBuilder;
+use git2::{Cred, FetchOptions, ProxyOptions, RemoteCallbacks, Repository, RepositoryInitOptions};
 use remove_dir_all::remove_dir_all;
-use std::env::current_dir;
-use std::path::Path;
-use std::path::PathBuf;
-use tempfile::Builder;
-use url::{ParseError, Url};
-
-pub(crate) struct GitConfig {
-    remote: Url,
+use std::borrow::Cow;
+use std::ops::Deref;
+use std::path::{Path, PathBuf};
+
+#[derive(Debug, PartialEq)]
+enum RepoKind {
+    LocalFolder,
+    RemoteHttp,
+    RemoteHttps,
+    RemoteSsh,
+    Invalid,
+}
+
+pub(crate) struct GitConfig<'a> {
+    remote: Cow<'a, str>,
     branch: GitReference,
+    kind: RepoKind,
+    identity: Option<PathBuf>,
 }
 
-impl GitConfig {
+impl<'a> GitConfig<'a> {
     /// Creates a new `GitConfig` by parsing `git` as a URL or a local path.
-    pub fn new(git: &str, branch: Option<String>) -> Result<Self> {
-        let remote = match Url::parse(git) {
-            Ok(u) => u,
-            Err(ParseError::RelativeUrlWithoutBase) => {
-                let given_path = Path::new(git);
-                let mut git_path = PathBuf::new();
-                if given_path.is_relative() {
-                    git_path.push(current_dir()?);
-                    git_path.push(given_path);
-                    if !git_path.exists() {
-                        anyhow::bail!(
-                            "Failed parsing git remote {:?}: path {:?} doesn't exist",
-                            git,
-                            &git_path
-                        );
-                    }
-                } else {
-                    git_path.push(git)
+    pub fn new(
+        git: Cow<'a, str>,
+        branch: Option<String>,
+        identity: Option<PathBuf>,
+    ) -> Result<Self> {
+        let (remote, kind) = match determine_repo_kind(git.as_ref()) {
+            RepoKind::Invalid => anyhow::bail!("Invalid git remote '{}'", &git),
+            RepoKind::LocalFolder => {
+                let full_path = canonicalize_path(git.deref().as_ref())?;
+                if !full_path.exists() {
+                    anyhow::bail!("The given git remote {:?} does not exist.", &git,);
                 }
-                Url::from_file_path(&git_path).map_err(|()| {
-                    anyhow::format_err!(
-                        "Failed parsing git remote (also tried as a file path): {}",
-                        &git
-                    )
-                })?
+                (
+                    full_path.display().to_string().into(),
+                    RepoKind::LocalFolder,
+                )
             }
-            Err(err) => anyhow::bail!("Failed parsing git remote {:?}: {}", git, err),
+            k => (git, k),
         };
 
         Ok(GitConfig {
             remote,
+            kind,
+            identity,
             branch: branch
                 .map(GitReference::Branch)
                 .unwrap_or(GitReference::DefaultBranch),
@@ -58,149 +62,294 @@ impl GitConfig {
     /// [hub].
     ///
     /// [hub]: https://github.com/github/hub
-    pub fn new_abbr(git: &str, branch: Option<String>) -> Result<Self> {
-        Self::new(git, branch.clone()).or_else(|e| {
-            Self::new(&format!("https://github.com/{}.git", git), branch).map_err(|_| e)
+    pub fn new_abbr(
+        git: Cow<'a, str>,
+        branch: Option<String>,
+        identity: Option<PathBuf>,
+    ) -> Result<Self> {
+        Self::new(git.clone(), branch.clone(), identity.clone()).or_else(|_| {
+            let full_remote = format!("https://github.com/{}.git", &git);
+            Self::new(full_remote.into(), branch, identity)
         })
     }
 }
 
+fn canonicalize_path(p: &Path) -> Result<PathBuf> {
+    let p = if p.to_str().unwrap().starts_with("~/") {
+        home()?.join(p.strip_prefix("~/").unwrap())
+    } else {
+        p.to_path_buf()
+    };
+
+    p.canonicalize().context("path does not exist")
+}
+
+#[test]
+fn should_canonicalize() {
+    #[cfg(target_os = "macos")]
+    assert!(canonicalize_path(&PathBuf::from("../"))
+        .unwrap()
+        .starts_with("/Users/"));
+    #[cfg(target_os = "linux")]
+    assert!(canonicalize_path(&PathBuf::from("../"))
+        .unwrap()
+        .starts_with("/home/"));
+    #[cfg(windows)]
+    assert!(canonicalize_path(&PathBuf::from("../"))
+        .unwrap()
+        // not a bug, a feature:
+        // https://stackoverflow.com/questions/41233684/why-does-my-canonicalized-path-get-prefixed-with
+        .to_str()
+        .unwrap()
+        .starts_with("\\\\?\\"));
+}
+
+/// takes care of `~/` paths, defaults to `$HOME/.ssh/id_rsa` and resolves symlinks.
+fn get_private_key_path(identity: Option<PathBuf>) -> Result<PathBuf> {
+    let private_key = identity.unwrap_or(home()?.join(".ssh/id_rsa"));
+
+    canonicalize_path(&private_key).context("private key path was not was incorrect")
+}
+
+fn git_ssh_credentials_callback<'a>(identity: Option<PathBuf>) -> Result<RemoteCallbacks<'a>> {
+    let private_key = get_private_key_path(identity)?;
+    println!(
+        "{} {} `{}` {}",
+        emoji::INFO,
+        style("Using private key:").bold(),
+        style(pretty_path(&private_key)?).bold().yellow(),
+        style("for git-ssh checkout").bold()
+    );
+    let mut cb = RemoteCallbacks::new();
+    cb.credentials(
+        move |_url, username_from_url: Option<&str>, _allowed_types| {
+            Cred::ssh_key(username_from_url.unwrap_or("git"), None, &private_key, None)
+        },
+    );
+    Ok(cb)
+}
+
+/// home path wrapper
+fn home() -> Result<PathBuf> {
+    canonicalize_path(&dirs::home_dir().context("$HOME was not set")?)
+}
+
+#[test]
+fn should_pretty_path() {
+    let p = pretty_path(home().unwrap().as_path().join(".cargo").as_path()).unwrap();
+    #[cfg(unix)]
+    assert_eq!(p, "$HOME/.cargo");
+    #[cfg(windows)]
+    assert_eq!(p, "%userprofile%\\.cargo");
+}
+
+/// prevents from long stupid paths, and replace the home path by the literal `$HOME`
+fn pretty_path(a: &Path) -> Result<String> {
+    #[cfg(unix)]
+    let home_var = "$HOME";
+    #[cfg(windows)]
+    let home_var = "%userprofile%";
+    Ok(a.display()
+        .to_string()
+        .replace(&home()?.display().to_string(), home_var))
+}
+
+/// thanks to @extrawurst for pointing this out
+/// https://github.com/extrawurst/gitui/blob/master/asyncgit/src/sync/branch/mod.rs#L38
+fn get_branch_name_repo(repo: &Repository) -> Result<String> {
+    let iter = repo.branches(None)?;
+
+    for b in iter {
+        let b = b?;
+
+        if b.0.is_head() {
+            let name = b.0.name()?.unwrap_or("");
+            return Ok(name.into());
+        }
+    }
+
+    anyhow::bail!("A repo has no Head")
+}
+
+fn init_all_submodules(repo: &Repository) -> Result<()> {
+    for mut sub in repo.submodules().unwrap() {
+        sub.update(true, None)?;
+    }
+
+    Ok(())
+}
+
 pub(crate) fn create(project_dir: &Path, args: GitConfig) -> Result<String> {
-    let temp = Builder::new().prefix(project_dir).tempdir()?;
-    let config = Config::default()?;
-    let remote = GitRemote::new(&args.remote);
-
-    let ((db, rev), branch_name) = match &args.branch {
-        GitReference::Branch(branch_name) => (
-            remote.checkout(&temp.path(), None, &args.branch, None, &config)?,
-            branch_name.clone(),
-        ),
-        GitReference::DefaultBranch => {
-            // Cargo has a specific behavior for now for handling the "default" branch. It forces
-            // it to the branch named "master" even if the actual default branch of the repository
-            // is something else. They intent to change this behavior in the future but they don't
-            // want to break the compatibility.
-            //
-            // See issues:
-            //  - https://github.com/rust-lang/cargo/issues/8364
-            //  - https://github.com/rust-lang/cargo/issues/8468
-            let repo = git2::Repository::init(&temp.path())?;
-            let mut origin = repo.remote_anonymous(remote.url().as_str())?;
+    let mut builder = RepoBuilder::new();
+    if let GitReference::Branch(branch_name) = &args.branch {
+        builder.branch(branch_name.as_str());
+    }
+
+    let mut fo = FetchOptions::new();
+    match args.kind {
+        RepoKind::LocalFolder => {}
+        RepoKind::RemoteHttp | RepoKind::RemoteHttps => {
             let mut proxy = ProxyOptions::new();
             proxy.auto();
-            origin.connect_auth(git2::Direction::Fetch, None, Some(proxy))?;
-            let default_branch = origin.default_branch()?;
-            let branch_name = default_branch
-                .as_str()
-                .unwrap_or("refs/heads/master")
-                .replace("refs/heads/", "");
-            (
-                remote.checkout(
-                    &temp.path(),
-                    None,
-                    &GitReference::Branch(branch_name.clone()),
-                    None,
-                    &config,
-                )?,
-                branch_name,
-            )
+            fo.proxy_options(proxy);
         }
-        _ => unreachable!(),
-    };
+        RepoKind::RemoteSsh => {
+            let callbacks = git_ssh_credentials_callback(args.identity)?;
+            fo.remote_callbacks(callbacks);
+        }
+        RepoKind::Invalid => {
+            unreachable!()
+        }
+    }
+    builder.fetch_options(fo);
+
+    let repo = builder.clone(args.remote.as_ref(), project_dir)?;
+    let branch = get_branch_name_repo(&repo)?;
+    init_all_submodules(&repo)?;
+    remove_history(project_dir)?;
 
-    // This clones the remote and handles all the submodules
-    db.copy_to(rev, project_dir, &config)?;
-    Ok(branch_name)
+    Ok(branch)
 }
 
-pub(crate) fn remove_history(project_dir: &Path) -> Result<()> {
-    remove_dir_all(project_dir.join(".git")).context("Error cleaning up cloned template")?;
+fn remove_history(project_dir: &Path) -> Result<()> {
+    let git_dir = project_dir.join(".git");
+    if git_dir.exists() {
+        remove_dir_all(git_dir).context("Error cleaning up cloned template")?;
+    }
     Ok(())
 }
 
-pub fn init(project_dir: &Path, branch: &str) -> Result<GitRepository> {
-    GitRepository::discover(project_dir).or_else(|_| {
+pub fn init(project_dir: &Path, branch: &str) -> Result<Repository> {
+    Repository::discover(project_dir).or_else(|_| {
         let mut opts = RepositoryInitOptions::new();
         opts.bare(false);
         opts.initial_head(branch);
-        GitRepository::init_opts(project_dir, &opts).context("Couldn't init new repository")
+        Repository::init_opts(project_dir, &opts).context("Couldn't init new repository")
     })
 }
 
+/// determines what kind of repository we got
+fn determine_repo_kind(remote_url: &str) -> RepoKind {
+    if remote_url.starts_with("git@") {
+        RepoKind::RemoteSsh
+    } else if remote_url.starts_with("http://") {
+        RepoKind::RemoteHttp
+    } else if remote_url.starts_with("https://") {
+        RepoKind::RemoteHttps
+    } else if Path::new(remote_url).exists() {
+        RepoKind::LocalFolder
+    } else {
+        RepoKind::Invalid
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
+    use std::env::current_dir;
+    use url::Url;
 
     const REPO_URL: &str = "https://github.com/cargo-generate/cargo-generate.git";
+    const REPO_URL_SSH: &str = "git@github.com:cargo-generate/cargo-generate.git";
 
     #[test]
-    #[should_panic(expected = "invalid port number")]
-    fn should_fail_for_ssh_remote_urls() {
-        GitConfig::new(
-            REPO_URL
-                .replace("https://github.com/", "ssh://git@github.com:")
-                .as_str(),
-            None,
-        )
-        .unwrap();
+    fn should_determine_repo_kind() {
+        for (u, k) in &[
+            (REPO_URL, RepoKind::RemoteHttps),
+            (
+                "http://github.com/cargo-generate/cargo-generate.git",
+                RepoKind::RemoteHttp,
+            ),
+            (REPO_URL_SSH, RepoKind::RemoteSsh),
+            ("./", RepoKind::LocalFolder),
+            ("ftp://foobar.bak", RepoKind::Invalid),
+        ] {
+            let kind = determine_repo_kind(u);
+            assert_eq!(&kind, k, "{} is not a {:?}", u, k);
+        }
+    }
+
+    #[test]
+    fn should_not_fail_for_ssh_remote_urls() {
+        let config = GitConfig::new(REPO_URL_SSH.into(), None, None).unwrap();
+        assert_eq!(config.kind, RepoKind::RemoteSsh);
     }
 
     #[test]
-    #[should_panic(expected = "aslkdgjlaskjdglskj\" doesn't exist")]
+    #[should_panic(expected = "Invalid git remote 'aslkdgjlaskjdglskj'")]
     fn should_fail_for_non_existing_local_path() {
-        GitConfig::new("aslkdgjlaskjdglskj", None).unwrap();
+        GitConfig::new("aslkdgjlaskjdglskj".into(), None, None).unwrap();
     }
 
     #[test]
     fn should_support_a_local_relative_path() {
-        let remote: String = GitConfig::new("src", None).unwrap().remote.into();
+        let remote: String = GitConfig::new("src".into(), None, None)
+            .unwrap()
+            .remote
+            .into();
+        #[cfg(unix)]
         assert!(
             remote.ends_with("/src"),
             "remote {} ends with /src",
             &remote
         );
+        #[cfg(windows)]
+        assert!(
+            remote.ends_with("\\src"),
+            "remote {} ends with \\src",
+            &remote
+        );
 
         #[cfg(unix)]
+        assert!(remote.starts_with('/'), "remote {} starts with /", &remote);
+        #[cfg(windows)]
         assert!(
-            remote.starts_with("file:///"),
-            "remote {} starts with file:///",
+            remote.starts_with("\\\\?\\"),
+            "remote {} starts with \\\\?\\",
             &remote
         );
     }
 
     #[test]
-    #[cfg(unix)]
     fn should_support_a_local_absolute_path() {
         // Absolute path.
         // If this fails because you cloned this repository into a non-UTF-8 directory... all
         // I can say is you probably had it comin'.
-        let remote: String = GitConfig::new(current_dir().unwrap().to_str().unwrap(), None)
-            .unwrap()
-            .remote
-            .into();
+        let remote: String = GitConfig::new(
+            current_dir().unwrap().display().to_string().into(),
+            None,
+            None,
+        )
+        .unwrap()
+        .remote
+        .into();
+        #[cfg(unix)]
+        assert!(remote.starts_with('/'), "remote {} starts with /", &remote);
+        #[cfg(windows)]
         assert!(
-            remote.starts_with("file:///"),
-            "remote {} starts with file:///",
-            remote
+            remote.starts_with("\\\\?\\"),
+            "remote {} starts with \\\\?\\ then the drive letter",
+            &remote
         );
     }
 
     #[test]
     fn should_test_happy_path() {
         // Remote HTTPS URL.
-        let cfg = GitConfig::new(REPO_URL, Some("main".to_owned())).unwrap();
+        let cfg = GitConfig::new(REPO_URL.into(), Some("main".to_owned()), None).unwrap();
 
-        assert_eq!(cfg.remote, Url::parse(REPO_URL).unwrap());
+        assert_eq!(cfg.remote.as_ref(), Url::parse(REPO_URL).unwrap().as_str());
         assert_eq!(cfg.branch, GitReference::Branch("main".to_owned()));
     }
 
     #[test]
     fn should_support_abbreviated_repository_short_urls_like() {
         assert_eq!(
-            GitConfig::new_abbr("cargo-generate/cargo-generate", None)
+            GitConfig::new_abbr("cargo-generate/cargo-generate".into(), None, None)
                 .unwrap()
-                .remote,
-            Url::parse(REPO_URL).unwrap()
+                .remote
+                .as_ref(),
+            Url::parse(REPO_URL).unwrap().as_str()
         );
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index d1e9246..72614e0 100755
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -175,6 +175,10 @@ pub struct Args {
     /// Populates a template variable `crate_type` with value `"bin"`
     #[structopt(long, conflicts_with = "lib")]
     pub bin: bool,
+
+    /// Use a different ssh identity
+    #[structopt(short = "i", long = "identity", parse(from_os_str))]
+    pub ssh_identity: Option<PathBuf>,
 }
 
 //
@@ -226,18 +230,19 @@ fn create_git(args: Args, name: &ProjectName) -> Result<()> {
         .as_ref()
         .map(|p| Path::new(p))
         .map_or(Ok(Default::default()), |path| get_config_file_values(path))?;
+    let remote = args
+        .git
+        .clone()
+        .with_context(|| "Missing option git, or a favorite")?;
     let git_config = GitConfig::new_abbr(
-        &args
-            .git
-            .clone()
-            .with_context(|| "Missing option git, or a favorite")?,
+        remote.into(),
         args.branch.to_owned(),
+        args.ssh_identity.clone(),
     )?;
 
     if let Some(dir) = &create_project_dir(&name, force) {
         match git::create(dir, git_config) {
             Ok(branch) => {
-                git::remove_history(dir)?;
                 progress(name, &template_values, dir, &branch, &args)?;
             }
             Err(e) => anyhow::bail!(


Test patch:
diff --git a/tests/integration/basics.rs b/tests/integration/basics.rs
index 15dde11..39b369f 100644
--- a/tests/integration/basics.rs
+++ b/tests/integration/basics.rs
@@ -563,7 +563,7 @@ version = "0.1.0"
             .path()
             .join("dangerous.todelete.cargogeneratetests")
     )
-    .expect("should exist")
+    .unwrap()
     .is_file());
 }
 
@@ -1055,7 +1055,12 @@ _This README was generated with [cargo-readme](https://github.com/livioribeiro/c
         .success()
         .stdout(predicates::str::contains("Done!").from_utf8());
 
-    assert!(dir.read("foobar-project/README.tpl").contains(raw_body));
+    let template = dir.read("foobar-project/README.tpl");
+    assert!(template.contains("{{badges}}"));
+    assert!(template.contains("{{crate}}"));
+    assert!(template.contains("{{project-name}}"));
+    assert!(template.contains("{{readme}}"));
+    assert!(template.contains("{{license}}"));
 }
 
 #[test]
@@ -1158,3 +1163,76 @@ version = "0.1.0"
     let cargo_toml = dir.read("foobar-project/Cargo.toml");
     assert!(cargo_toml.contains("this is a bin"));
 }
+
+#[cfg(test)]
+#[cfg(unix)]
+mod ssh_remote {
+    use super::*;
+
+    #[test]
+    fn it_should_support_a_public_repo() {
+        let dir = tmp_dir().build();
+
+        binary()
+            .arg("generate")
+            .arg("--git")
+            .arg("git@github.com:ashleygwilliams/wasm-pack-template.git")
+            .arg("--name")
+            .arg("foobar-project")
+            .current_dir(&dir.path())
+            .assert()
+            .success()
+            .stdout(predicates::str::contains("Done!").from_utf8());
+
+        let cargo_toml = dir.read("foobar-project/Cargo.toml");
+        assert!(cargo_toml.contains("foobar-project"));
+    }
+
+    #[test]
+    fn it_should_support_a_private_repo() {
+        let dir = tmp_dir().build();
+
+        binary()
+            .arg("generate")
+            .arg("--git")
+            .arg("git@github.com:cargo-generate/wasm-pack-template.git")
+            .arg("--name")
+            .arg("foobar-project")
+            .current_dir(&dir.path())
+            .assert()
+            .success()
+            .stdout(predicates::str::contains("Done!").from_utf8());
+
+        let cargo_toml = dir.read("foobar-project/Cargo.toml");
+        assert!(cargo_toml.contains("foobar-project"));
+    }
+
+    #[test]
+    #[ignore]
+    // for now only locally working
+    fn it_should_support_a_custom_ssh_key() {
+        let dir = tmp_dir().build();
+
+        binary()
+            .arg("generate")
+            .arg("-i")
+            .arg("~/workspaces/rust/cargo-generate-org/.env/id_rsa_ci")
+            .arg("--git")
+            .arg("git@github.com:cargo-generate/wasm-pack-template.git")
+            .arg("--name")
+            .arg("foobar-project")
+            .current_dir(&dir.path())
+            .assert()
+            .success()
+            .stdout(
+                predicates::str::contains("Using private key:")
+                    .and(predicates::str::contains(
+                        "cargo-generate-org/.env/id_rsa_ci",
+                    ))
+                    .from_utf8(),
+            );
+
+        let cargo_toml = dir.read("foobar-project/Cargo.toml");
+        assert!(cargo_toml.contains("foobar-project"));
+    }
+}
diff --git a/tests/integration/library.rs b/tests/integration/library.rs
index b824144..7938c40 100644
--- a/tests/integration/library.rs
+++ b/tests/integration/library.rs
@@ -31,6 +31,7 @@ version = "0.1.0"
         favorite: None,
         bin: true,
         lib: false,
+        ssh_identity: None,
     };
     // need to cd to the dir as we aren't running in the cargo shell.
     assert!(std::env::set_current_dir(&dir.root).is_ok());