Skip to content

Commit

Permalink
Update to use a regexp instead.
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickdappollonio committed Aug 23, 2024
1 parent edb05d0 commit a3e5050
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 27 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Rust

on:
release:
types: [created]

jobs:
release:
name: release ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-gnu
archive: zip
- target: x86_64-unknown-linux-musl
archive: tar.gz tar.xz tar.zst
- target: x86_64-apple-darwin
archive: zip

steps:
- name: Checkout
uses: actions/checkout@master

- name: Compile and release
uses: rust-build/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
RUSTTARGET: ${{ matrix.target }}
ARCHIVE_TYPES: ${{ matrix.archive }}
61 changes: 61 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2021"

[dependencies]
getopts = "0.2.21"
regex = "1.10.6"
subprocess = "0.2.9"

[profile.release]
Expand Down
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# `gc-rust` a GitHub clone helper

`gc-rust` is a tiny Rust application that allows you to clone GitHub repositories with ease to a predetermined location.

As an original Go developer, I liked the idea that my code was organized in the form of:

```
~/go/src/github.com/<username>/<repository>
```

So I kept maintaining even non-Go projects in the same way.

This is where `gc-rust` comes in handy. Given a GitHub repository URL, it will perform the `git clone` operation by finding the appropriate location for the resulting folder.

For example, given the repository:

```
github.com/example/application
```

It will correctly create the folder structure so the repository is cloned to:

```
~/go/src/github.com/example/application
```

If there was a preexistent folder, it will ask you if you want to overwrite it. **This will destroy any prior content in the destination folder!**

### Usage

To clone the repository, you can run any of the following:

```bash
gc-rust [email protected]:example/application.git
gc-rust github.com/example/application
gc-rust https://github.com/example/application
gc-rust https://github.com/example/application/issues
gc-rust https://github.com/example/application/security/dependabot
```

All of them will detect the repository being `github.com/example/app` and clone it to the correct location.

The output of `gc-rust` will all be printed to `stderr` with one exception: the folder location where it was cloned. This is useful if you want to create a function that both clones a repository and then `cd` into it:

```bash
function gc() {
if ! type "gc-rust" > /dev/null; then
echo -e "Install gc-rust first from github.com/patrickdappollonio/gc-rust"
exit 1
fi

cd "$(gc-rust "$1")" || return
}
```

With this in your `bashrc` or `bash_profile`, you can now simply run `gc` and it will clone the repository and `cd` into it:

```bash
$ pwd
/home/patrick/go/src/github.com/patrickdappollonio/gc-rust

$ gc https://github.com/patrickdappollonio/http-server
 Cloning patrickdappollonio/http-server...
Cloning into '/home/patrick/Golang/src/github.com/patrickdappollonio/http-server'...
remote: Enumerating objects: 848, done.
remote: Counting objects: 100% (228/228), done.
remote: Compressing objects: 100% (156/156), done.
remote: Total 848 (delta 183), reused 72 (delta 72), pack-reused 620 (from 1)
Receiving objects: 100% (848/848), 4.11 MiB | 17.99 MiB/s, done.
Resolving deltas: 100% (469/469), done.
 Successfully cloned patrickdappollonio/http-server into /home/patrick/Golang/src/github.com/patrickdappollonio/http-server

$ pwd
/home/patrick/go/src/github.com/patrickdappollonio/http-server
```

### Defining a location for the repositories

By default, `gc-rust` will clone the repositories to the path defined in the environment variable `$GC_DOWNLOAD_PATH`. If this variable is not set, it will use the `$GOPATH` environment variable since the original idea came from Go project management. If neither are defined you'll see an error.

### Specifying a branch

Contrary to what you might think, `gc-rust` will not deduce a branch name from the URL. Instead, it will clone using whatever branch is currently set as the default in the repository. If you want to clone a specific branch, you can do so by specifying the `-b` or `--branch` flag:

```bash
# this will clone `patrickdappollonio/http-server` into the `feature-branch` branch,
# and not the branch called `example` (as seen by the URL)
gc-rust https://github.com/patrickdappollonio/http-server/tree/example -b feature-branch
```
60 changes: 54 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use getopts::Options;
use std::fmt::{Display, Formatter};
use std::path::Path;
use std::{env, fmt};
Expand All @@ -12,15 +13,19 @@ enum ApplicationError {
CantCreateTargetDir(std::io::Error),
CantDeleteTargetDir(std::io::Error),
FailedCloneCommand(subprocess::PopenError),
FailedCheckoutCommand(subprocess::PopenError),
FailedGitOperation(),
FailedParsingRepo(parser::ParseRepoError),
FailedCaptureInput(std::io::Error),
ArgumentParsingError(getopts::Fail),
}

impl Display for ApplicationError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
ApplicationError::BaseDirNotFound => write!(f, "Base directory not found"),
ApplicationError::BaseDirNotFound => {
write!(f, "The base directory on which to download the repositories was not found. Ensure you have set the $GC_DOWNLOAD_PATH or $GOPATH environment variable.")
}
ApplicationError::BaseDirCannotBeOpened(err) => {
write!(f, "Base directory cannot be opened: {}", err)
}
Expand All @@ -33,6 +38,9 @@ impl Display for ApplicationError {
ApplicationError::FailedCloneCommand(err) => {
write!(f, "Failed to run the git clone command: {}", err)
}
ApplicationError::FailedCheckoutCommand(err) => {
write!(f, "Failed to run the git checkout command: {}", err)
}
ApplicationError::FailedGitOperation() => {
write!(f, "Failed to clone the repo.")
}
Expand All @@ -42,6 +50,9 @@ impl Display for ApplicationError {
ApplicationError::FailedParsingRepo(err) => {
write!(f, "Failed to parse the repository URL: {}", err)
}
ApplicationError::ArgumentParsingError(err) => {
write!(f, "Failed to parse arguments: {}", err)
}
}
}
}
Expand All @@ -64,19 +75,37 @@ fn main() {

fn run() -> Result<(), ApplicationError> {
// Get the base directory
let base_dir = env::var("GOPATH").map_err(|_| ApplicationError::BaseDirNotFound)?;
let base_dir = env::var("GC_DOWNLOAD_PATH")
.or_else(|_| env::var("GOPATH"))
.map_err(|_| ApplicationError::BaseDirNotFound)?;
let base_dir = format!("{}/src", base_dir);

// Try opening the base directory
fs::read_dir(&base_dir).map_err(ApplicationError::BaseDirCannotBeOpened)?;

// Get the repository URL from the command line arguments
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: gc <repository-url>");
let mut opts = Options::new();
opts.optopt(
"b",
"branch",
"set the branch to checkout after cloning",
"BRANCH",
);

let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => return Err(ApplicationError::ArgumentParsingError(f)),
};

let repo_url = if !matches.free.is_empty() {
matches.free[0].clone()
} else {
eprintln!("Usage: gc <repository-url> [-b <branch>]");
return Ok(());
}
let repo_url = &args[1];
};

let branch = matches.opt_str("b");

// Parse the repository URL
let (host, team, project) = parser::repository(repo_url.to_string())?;
Expand Down Expand Up @@ -116,6 +145,25 @@ fn run() -> Result<(), ApplicationError> {
"\u{f058} Successfully cloned {}/{} into {}",
team, project, project_path
);

if let Some(branch) = branch {
eprintln!("\u{f5c4} Checking out branch {}...", branch);

let exec = Exec::cmd("git")
.args(&["checkout", &branch])
.cwd(&project_path)
.stdout(Redirection::None)
.stderr(Redirection::None)
.capture()
.map_err(ApplicationError::FailedCheckoutCommand)?;

if !exec.success() {
return Err(ApplicationError::FailedGitOperation());
}

eprintln!("\u{f5c4} Successfully checked out branch {}", branch);
}

println!("{}", project_path);
Ok(())
}
Loading

0 comments on commit a3e5050

Please sign in to comment.