Skip to content

Commit 72a750b

Browse files
Merge pull request #1442 from SierraSoftworks/feature/support_for_forking
feat(fork): implement repository forking functionality in `gt new`
2 parents e46c2c6 + 8a34799 commit 72a750b

File tree

15 files changed

+490
-35
lines changed

15 files changed

+490
-35
lines changed

.github/workflows/changelog.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ on:
55
branches:
66
- main
77
pull_request:
8-
types: [opened, reopened, synchronize]
8+
types: [ opened, reopened, synchronize ]
99

1010
jobs:
1111
update:
1212
name: Prepare
13-
runs-on: ubuntu-20.04
13+
runs-on: ubuntu-latest
1414
steps:
1515
- uses: release-drafter/release-drafter@v6.1.0
1616
env:

.github/workflows/test.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
jobs:
88
code-quality:
99
name: Code Quality
10-
runs-on: ubuntu-20.04
10+
runs-on: ubuntu-latest
1111
steps:
1212
- uses: actions/checkout@v4
1313

@@ -21,6 +21,11 @@ jobs:
2121
components: clippy, rustfmt
2222
override: true
2323

24+
- name: install dependencies
25+
run: |
26+
sudo apt-get update
27+
sudo apt-get install -y libdbus-1-3 libdbus-1-dev
28+
2429
- name: install protoc
2530
run: |
2631
Invoke-WebRequest -OutFile /tmp/protoc.zip -Uri https://github.com/protocolbuffers/protobuf/releases/download/v3.20.2/protoc-3.20.2-linux-x86_64.zip
@@ -218,6 +223,6 @@ jobs:
218223
needs:
219224
- test-platforms
220225
- code-quality
221-
runs-on: ubuntu-20.04
226+
runs-on: ubuntu-latest
222227
steps:
223228
- run: echo "Tests Complete for All Platforms"

docs/commands/repos.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ helping you quickly figure out where your repo should be created.
9898
- `-o`/`--open` <Badge text="v2.1+"/> will open this repository in your default application once it has been created.
9999
You can make this behaviour the default with the [
100100
`open_new_repo_in_default_app`](../config/features.md#open-new-repo-in-default-app) feature flag.
101+
- `-f`/`--fork`/`--from` <Badge text="v3.9+"/> create a fork of an existing remote (on supported services) or a copy of
102+
an existing remote repository (on unsupported services)
101103

102104
#### Example
103105

@@ -110,6 +112,9 @@ gt n --open gh:notheotherben/demo
110112
111113
# Create a new repository but don't create it remotely
112114
gt n --no-create-remote gh:notheotherben/demo
115+
116+
# Fork a repository
117+
gt n gh:notheotherben/demo --fork gh:git-fixtures/basic
113118
```
114119

115120
## list <Badge text="v1.0+"/>

flake.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@
6565
# we can block the CI if there are issues here, but not
6666
# prevent downstream consumers from building our crate by itself.
6767
git-tool-clippy = craneLib.cargoClippy {
68-
inherit cargoArtifacts src buildInputs;
68+
inherit cargoArtifacts src nativeBuildInputs buildInputs;
6969
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
7070
};
7171

7272
git-tool-doc = craneLib.cargoDoc {
73-
inherit cargoArtifacts src;
73+
inherit cargoArtifacts src nativeBuildInputs buildInputs;
7474
};
7575

7676
# Check formatting

src/commands/clone.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ impl CommandRunnable for CloneCommand {
5050
)
5151
})?;
5252

53-
let operation = sequence![GitClone {}];
53+
let operation = sequence![GitClone::default()];
5454

5555
for line in file.lines() {
5656
if line.trim_start().is_empty() || line.trim_start().starts_with('#') {
@@ -69,7 +69,7 @@ impl CommandRunnable for CloneCommand {
6969
let repo = core.resolver().get_best_repo(&identifier)?;
7070

7171
if !repo.exists() {
72-
match sequence![GitClone {}].apply_repo(core, &repo).await {
72+
match sequence![GitClone::default()].apply_repo(core, &repo).await {
7373
Ok(()) => {}
7474
Err(e) => return Err(e),
7575
}

src/commands/new.rs

Lines changed: 139 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use crate::engine::Identifier;
23
use crate::{engine::features, tasks::*};
34
use clap::Arg;
45
use tracing_batteries::prelude::*;
@@ -44,6 +45,21 @@ impl CommandRunnable for NewCommand {
4445
.help("don't check whether the repository already exists on the remote service before creating a new local repository")
4546
.action(clap::ArgAction::SetTrue),
4647
)
48+
.arg(
49+
Arg::new("from")
50+
.long("from")
51+
.alias("fork")
52+
.short('f')
53+
.help("create a fork of an existing remote (on supported services) or a copy of an existing remote repository (on unsupported services)"),
54+
)
55+
56+
.arg(
57+
Arg::new("fork-all-branches")
58+
.long("fork-all-branches")
59+
.short('A')
60+
.help("when forking from an existing repository, fork all branches (Default: False - default branch only).")
61+
.action(clap::ArgAction::SetTrue),
62+
)
4763
}
4864

4965
#[tracing::instrument(name = "gt new", err, skip(self, core, matches))]
@@ -64,17 +80,43 @@ impl CommandRunnable for NewCommand {
6480
return Ok(0);
6581
}
6682

67-
let tasks = sequence![
68-
EnsureNoRemote {
69-
enabled: !matches.get_flag("no-check-exists")
70-
},
71-
GitInit {},
72-
GitRemote { name: "origin" },
73-
GitCheckout { branch: "main" },
74-
CreateRemote {
75-
enabled: !matches.get_flag("no-create-remote")
76-
}
77-
];
83+
let tasks = if let Some(from_repo) = matches.get_one::<String>("from") {
84+
let from_repo_id: Identifier = from_repo.as_str().parse()?;
85+
let from_repo = core.resolver().get_best_repo(&from_repo_id)?;
86+
let from_service = core.config().get_service(&from_repo.service)?;
87+
let from_url = from_service.get_git_url(&from_repo)?;
88+
89+
let target_service = core.config().get_service(&from_repo.service)?;
90+
let target_url = target_service.get_git_url(&repo)?;
91+
92+
sequence![
93+
ForkRemote {
94+
from_repo: from_repo.clone(),
95+
default_branch_only: !matches.get_flag("fork-all-branches"),
96+
},
97+
GitClone::with_url(&from_url),
98+
GitAddRemote {
99+
name: "origin".into(),
100+
url: target_url,
101+
},
102+
GitAddRemote {
103+
name: "upstream".into(),
104+
url: from_url,
105+
}
106+
]
107+
} else {
108+
sequence![
109+
EnsureNoRemote {
110+
enabled: !matches.get_flag("no-check-exists")
111+
},
112+
GitInit {},
113+
GitRemote { name: "origin" },
114+
GitCheckout { branch: "main" },
115+
CreateRemote {
116+
enabled: !matches.get_flag("no-create-remote")
117+
}
118+
]
119+
};
78120

79121
tasks.apply_repo(core, &repo).await?;
80122

@@ -94,6 +136,8 @@ impl CommandRunnable for NewCommand {
94136
async fn complete(&self, core: &Core, completer: &Completer, _matches: &ArgMatches) {
95137
completer.offer("--open");
96138
completer.offer("--no-create-remote");
139+
completer.offer("--from");
140+
97141
if let Ok(repos) = core.resolver().get_repos() {
98142
let mut namespaces = std::collections::HashSet::new();
99143
let default_svc = core
@@ -117,9 +161,11 @@ impl CommandRunnable for NewCommand {
117161

118162
#[cfg(test)]
119163
mod tests {
120-
use mockall::predicate::eq;
121-
122164
use super::*;
165+
use crate::engine::Repo;
166+
use mockall::predicate::eq;
167+
use rstest::rstest;
168+
use tempfile::tempdir;
123169

124170
#[tokio::test]
125171
async fn run_partial() {
@@ -189,4 +235,84 @@ mod tests {
189235

190236
assert!(repo.valid());
191237
}
238+
239+
#[rstest]
240+
#[cfg(feature = "auth")]
241+
#[case(
242+
"gh:git-fixtures/basic",
243+
"git-fixtures/basic",
244+
"gh:cedi/basic",
245+
"cedi/basic"
246+
)]
247+
#[case(
248+
"gh:git-fixtures/basic",
249+
"git-fixtures/basic",
250+
"gh:SierraSoftworks/basic",
251+
"SierraSoftworks/basic"
252+
)]
253+
#[tokio::test]
254+
#[cfg_attr(feature = "pure-tests", ignore)]
255+
async fn fork_repo(
256+
#[case] source_repo: &str,
257+
#[case] source: &str,
258+
#[case] target_repo: &str,
259+
#[case] target: &str,
260+
) {
261+
let cmd = NewCommand {};
262+
263+
let args = cmd
264+
.app()
265+
.get_matches_from(vec!["new", target_repo, "--fork", source_repo]);
266+
267+
let temp = tempdir().unwrap();
268+
let temp_path = temp.path().to_path_buf();
269+
270+
let core = Core::builder()
271+
.with_config_for_dev_directory(temp.path())
272+
.with_mock_http_client(crate::online::service::github::mocks::repo_fork(source))
273+
.with_mock_keychain(|mock| {
274+
mock.expect_get_token()
275+
.with(eq("gh"))
276+
.returning(|_| Ok("test_token".into()));
277+
})
278+
.with_mock_resolver(|mock| {
279+
let source_temp_path = temp_path.clone();
280+
let source = source.to_owned();
281+
let source_segments = source.split('/');
282+
let full_source_path = source_segments
283+
.fold(source_temp_path.clone(), |path, segment| path.join(segment));
284+
let source_identifier: Identifier = source_repo.parse().unwrap();
285+
mock.expect_get_best_repo()
286+
.with(eq(source_identifier))
287+
.times(1)
288+
.returning(move |_| {
289+
Ok(Repo::new("gh:git-fixtures/basic", full_source_path.clone()))
290+
});
291+
292+
let target_temp_path = temp_path.clone();
293+
let target = target.to_owned();
294+
let target_segments = target.split('/');
295+
let full_target_path = target_segments
296+
.fold(target_temp_path.clone(), |path, segment| path.join(segment));
297+
let target_identifier: Identifier = target_repo.parse().unwrap();
298+
mock.expect_get_best_repo()
299+
.with(eq(target_identifier))
300+
.times(2)
301+
.returning(move |_| {
302+
Ok(Repo::new("gh:git-fixtures/empty", full_target_path.clone()))
303+
});
304+
})
305+
.build();
306+
307+
let repo = core
308+
.resolver()
309+
.get_best_repo(&target_repo.parse().unwrap())
310+
.unwrap();
311+
312+
assert!(!repo.valid());
313+
314+
cmd.assert_run_successful(&core, &args).await;
315+
316+
assert!(repo.valid());
317+
}
192318
}

src/commands/open.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ New applications can be configured either by making changes to your configuratio
7070
};
7171

7272
if !repo.exists() {
73-
match sequence![GitClone {}].apply_repo(core, &repo).await {
73+
match sequence![GitClone::default()].apply_repo(core, &repo).await {
7474
Ok(()) => {}
7575
Err(_) if matches.get_flag("create") => {
7676
sequence![

src/completion/completer.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ mod tests {
167167
fn test_offer_apps() {
168168
let console = console::mock();
169169
let core = Core::builder()
170-
.with_config_for_dev_directory(&get_dev_dir())
170+
.with_config_for_dev_directory(get_dev_dir())
171171
.with_console(console.clone())
172172
.build();
173173

@@ -188,7 +188,7 @@ mod tests {
188188
fn test_offer_namespaces() {
189189
let console = console::mock();
190190
let core = Core::builder()
191-
.with_config_for_dev_directory(&get_dev_dir())
191+
.with_config_for_dev_directory(get_dev_dir())
192192
.with_console(console.clone())
193193
.build();
194194

@@ -209,7 +209,7 @@ mod tests {
209209
fn test_offer_repos() {
210210
let console = console::mock();
211211
let core = Core::builder()
212-
.with_config_for_dev_directory(&get_dev_dir())
212+
.with_config_for_dev_directory(get_dev_dir())
213213
.with_console(console.clone())
214214
.build();
215215

src/engine/features.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub const CREATE_REMOTE: &str = "create_remote";
55
pub const CREATE_REMOTE_PRIVATE: &str = "create_remote_private";
66
pub const CHECK_EXISTS: &str = "check_exists";
77
pub const MOVE_REMOTE: &str = "move_remote";
8+
pub const FORK_REMOTE: &str = "fork_remote";
89

910
pub const OPEN_NEW_REPO: &str = "open_new_repo_in_default_app";
1011
pub const ALWAYS_OPEN_BEST_MATCH: &str = "always_open_best_match";
@@ -18,6 +19,7 @@ lazy_static! {
1819
CREATE_REMOTE_PRIVATE,
1920
CHECK_EXISTS,
2021
MOVE_REMOTE,
22+
FORK_REMOTE,
2123
OPEN_NEW_REPO,
2224
ALWAYS_OPEN_BEST_MATCH,
2325
TELEMETRY,
@@ -71,6 +73,7 @@ impl FeaturesBuilder {
7173
self.with(CREATE_REMOTE, true)
7274
.with(CREATE_REMOTE_PRIVATE, true)
7375
.with(MOVE_REMOTE, true)
76+
.with(FORK_REMOTE, true)
7477
.with(TELEMETRY, false)
7578
.with(CHECK_FOR_UPDATES, true)
7679
.with(CHECK_EXISTS, true)

0 commit comments

Comments
 (0)