Skip to content

Commit 6c7d4a7

Browse files
Merge pull request #1355 from SierraSoftworks/feat/clone-batch
feat: Add support for importing a file list of repos
2 parents 32f20d9 + 5cb4639 commit 6c7d4a7

File tree

3 files changed

+115
-11
lines changed

3 files changed

+115
-11
lines changed

docs/commands/repos.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ gt o vs
5959
# Open a repository in VS Code
6060
gt o gh:SierraSoftworks/git-tool code
6161
```
62-
62+
6363
::: tip
6464
If you are already inside a repository, you can specify only an app and it will launch in the
6565
context of the current repo, like `gt o vs` in the example above. *This can be very useful if
@@ -161,8 +161,23 @@ new dev-box, this is the command for you.
161161
```powershell
162162
# Clone a repository into the appropriate folder
163163
gt clone gh:SierraSoftworks/git-tool
164+
165+
# Clone a series of repositories into the appropriate folders
166+
# The repositories.txt file should contain a list of repositories, one per line
167+
# e.g.
168+
# gh:SierraSoftworks/git-tool
169+
# gh:SierraSoftworks/tailscale-udm
170+
# gh:SierraSoftworks/vue-template
171+
gt clone @repositories.txt
164172
```
165173

174+
::: tip
175+
As of <Badge text="v3.7.0+"/>, you can use the `@` symbol to specify a file
176+
containing a list of repositories to clone. This can be paired with
177+
[`gt list -q`](#list) to quickly backup and restore your list of local repositories,
178+
or setup a new machine.
179+
:::
180+
166181
## fix <Badge text="v2.1.4+"/>
167182
Git-Tool usually takes care of setting up your git `origin` remote, however sometimes you
168183
want to rename projects or even entire organizations. To make your life a little bit easier,
@@ -195,4 +210,4 @@ in the directory you are attempting to delete.
195210
```powershell
196211
# Remove a repository
197212
gt remove gh:SierraSoftworks/git-tool
198-
```
213+
```

docs/package-lock.json

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/clone.rs

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::*;
22
use crate::core::Target;
33
use crate::tasks::*;
44
use clap::Arg;
5+
use std::path::PathBuf;
56
use tracing_batteries::prelude::*;
67

78
pub struct CloneCommand;
@@ -31,12 +32,46 @@ impl CommandRunnable for CloneCommand {
3132
"You didn't specify the repository you wanted to clone.",
3233
"Remember to specify a repository name like this: 'git-tool clone gh:sierrasoftworks/git-tool'."))?;
3334

34-
let repo = core.resolver().get_best_repo(repo_name)?;
35-
36-
if !repo.exists() {
37-
match sequence![GitClone {}].apply_repo(core, &repo).await {
38-
Ok(()) => {}
39-
Err(e) => return Err(e),
35+
if let Some(file_path) = repo_name.strip_prefix('@') {
36+
// Load the list of repos to clone from a file
37+
let file_path: PathBuf = file_path.parse().map_err(|e| {
38+
errors::user_with_internal(
39+
"The specified file path is not valid.",
40+
"Please make sure you are specifying a valid file path for your import file.",
41+
e,
42+
)
43+
})?;
44+
45+
let file = std::fs::read_to_string(&file_path).map_err(|e| {
46+
errors::user_with_internal(
47+
"Could not read the specified clone file.",
48+
"Please make sure the file exists and is readable.",
49+
e,
50+
)
51+
})?;
52+
53+
let operation = sequence![GitClone {}];
54+
55+
for line in file.lines() {
56+
if line.trim_start().is_empty() || line.trim_start().starts_with('#') {
57+
continue;
58+
}
59+
60+
let repo = core.resolver().get_best_repo(line.trim())?;
61+
writeln!(core.output(), "{}", repo)?;
62+
match operation.apply_repo(core, &repo).await {
63+
Ok(()) => {}
64+
Err(e) => return Err(e),
65+
}
66+
}
67+
} else {
68+
let repo = core.resolver().get_best_repo(repo_name)?;
69+
70+
if !repo.exists() {
71+
match sequence![GitClone {}].apply_repo(core, &repo).await {
72+
Ok(()) => {}
73+
Err(e) => return Err(e),
74+
}
4075
}
4176
}
4277

@@ -127,4 +162,61 @@ features:
127162
Err(err) => panic!("{}", err.message()),
128163
}
129164
}
165+
166+
#[tokio::test]
167+
#[cfg_attr(feature = "pure-tests", ignore)]
168+
async fn run_batch() {
169+
let cmd = CloneCommand {};
170+
171+
let temp = tempdir().unwrap();
172+
173+
let args = cmd.app().get_matches_from(vec![
174+
"clone",
175+
format!("@{}", temp.path().join("import.txt").display()).as_str(),
176+
]);
177+
178+
let cfg = Config::from_str(
179+
"
180+
directory: /dev
181+
182+
apps:
183+
- name: test-app
184+
command: test
185+
args:
186+
- '{{ .Target.Name }}'
187+
188+
features:
189+
http_transport: true
190+
",
191+
)
192+
.unwrap();
193+
194+
let temp_path = temp.path().to_path_buf();
195+
196+
std::fs::write(temp.path().join("import.txt"), "gh:git-fixtures/basic")
197+
.expect("writing should succeed");
198+
199+
let core = Core::builder()
200+
.with_config(cfg)
201+
.with_mock_launcher(|mock| {
202+
mock.expect_run().never();
203+
})
204+
.with_mock_resolver(|mock| {
205+
let temp_path = temp_path.clone();
206+
mock.expect_get_best_repo()
207+
.once()
208+
.with(mockall::predicate::eq("gh:git-fixtures/basic"))
209+
.returning(move |_| {
210+
Ok(Repo::new("gh:git-fixtures/basic", temp_path.join("repo")))
211+
});
212+
})
213+
.build();
214+
215+
match cmd.run(&core, &args).await {
216+
Ok(status) => {
217+
assert_eq!(status, 0);
218+
}
219+
Err(err) => panic!("{}", err.message()),
220+
}
221+
}
130222
}

0 commit comments

Comments
 (0)