Skip to content

Commit 18be325

Browse files
committed
gnd: Fix --from-example to clone from graph-tooling repo
The init_from_example function had two bugs: - Defaulted to "ethereum-gravatar" instead of requiring the example name - Used hardcoded URL to example-subgraph repo, ignoring the example variable Now: - Example name is mandatory (error with link to available examples) - Uses sparse checkout to fetch only the specific example from https://github.com/graphprotocol/graph-tooling/tree/main/examples - Legacy name "ethereum/gravatar" is converted to "ethereum-gravatar"
1 parent 25100ab commit 18be325

File tree

4 files changed

+201
-24
lines changed

4 files changed

+201
-24
lines changed

docs/specs/gnd-cli-expansion.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,13 @@ gnd init [directory]
303303
| `--allow-simple-name` | | `false` | Allow simple subgraph names |
304304
| `--help` | `-h` | | Show help |
305305

306+
**Example Subgraphs:**
307+
308+
The `--from-example <name>` flag clones from `https://github.com/graphprotocol/graph-tooling/tree/main/examples`.
309+
The example name is **required** - no default is provided. Available examples include:
310+
`ethereum-gravatar`, `aggregations`, `ethereum-basic-event-handlers`, `substreams-powered-subgraph`, etc.
311+
Legacy name `ethereum/gravatar` is automatically converted to `ethereum-gravatar`.
312+
306313
**Behavior:**
307314

308315
1. Prompt for missing information (protocol, network, contract, etc.)

gnd/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ which = "7"
6969
# Interactive prompts
7070
inquire = "0.7"
7171

72+
# Temp directories for init
73+
tempfile = "3"
74+
7275
# Browser opening for publish command
7376
open = "5"
7477

gnd/src/commands/init.rs

Lines changed: 98 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -385,24 +385,45 @@ async fn init_from_contract(opt: &InitOpt) -> Result<()> {
385385
Ok(())
386386
}
387387

388+
/// Recursively copy a directory and its contents.
389+
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
390+
fs::create_dir_all(dst)?;
391+
for entry in fs::read_dir(src)? {
392+
let entry = entry?;
393+
let src_path = entry.path();
394+
let dst_path = dst.join(entry.file_name());
395+
if entry.file_type()?.is_dir() {
396+
copy_dir_recursive(&src_path, &dst_path)?;
397+
} else {
398+
fs::copy(&src_path, &dst_path)?;
399+
}
400+
}
401+
Ok(())
402+
}
403+
388404
/// Initialize a subgraph from an example template.
389405
fn init_from_example(opt: &InitOpt) -> Result<()> {
390-
use std::fs;
391-
use std::process::Command;
406+
use std::process::{Command, Stdio};
392407

393-
let example = opt.from_example.as_deref().unwrap_or("ethereum-gravatar");
408+
let example = opt.from_example.as_deref().ok_or_else(|| {
409+
anyhow!(
410+
"Example name is required. See available examples at:\n\
411+
https://github.com/graphprotocol/graph-tooling/tree/main/examples"
412+
)
413+
})?;
414+
415+
// Handle legacy example name format
416+
let example = match example {
417+
"ethereum/gravatar" => "ethereum-gravatar",
418+
other => other,
419+
};
394420

395421
let subgraph_name = opt.subgraph_name.as_deref().unwrap_or("my-subgraph");
396422

397423
let directory = opt.directory.clone().unwrap_or_else(|| {
398424
PathBuf::from(subgraph_name.split('/').next_back().unwrap_or("subgraph"))
399425
});
400426

401-
step(
402-
Step::Generate,
403-
&format!("Creating subgraph from example: {}", example),
404-
);
405-
406427
// Check if directory already exists
407428
if directory.exists() {
408429
return Err(anyhow!(
@@ -411,37 +432,91 @@ fn init_from_example(opt: &InitOpt) -> Result<()> {
411432
));
412433
}
413434

414-
// Clone the example repository
415-
let repo_url = "https://github.com/graphprotocol/example-subgraph.git";
435+
// Clone only the specific example using git sparse-checkout
436+
let temp_dir = tempfile::tempdir()?;
437+
let repo_url = "https://github.com/graphprotocol/graph-tooling";
438+
let example_subpath = format!("examples/{}", example);
416439

417-
step(Step::Load, &format!("Cloning example from {}", repo_url));
440+
step(Step::Load, &format!("Fetching example: {}", example));
418441

442+
// Initialize empty repo with sparse checkout
419443
let status = Command::new("git")
420-
.args([
421-
"clone",
422-
"--depth",
423-
"1",
424-
repo_url,
425-
&directory.to_string_lossy(),
426-
])
444+
.current_dir(temp_dir.path())
445+
.args(["init"])
446+
.stdout(Stdio::null())
447+
.stderr(Stdio::null())
427448
.status()?;
428449

429450
if !status.success() {
430-
return Err(anyhow!("Failed to clone example repository"));
451+
return Err(anyhow!("Failed to initialize git repository"));
452+
}
453+
454+
// Configure sparse checkout for just the example directory
455+
Command::new("git")
456+
.current_dir(temp_dir.path())
457+
.args(["sparse-checkout", "init", "--cone"])
458+
.stdout(Stdio::null())
459+
.stderr(Stdio::null())
460+
.status()?;
461+
462+
Command::new("git")
463+
.current_dir(temp_dir.path())
464+
.args(["sparse-checkout", "set", &example_subpath])
465+
.stdout(Stdio::null())
466+
.stderr(Stdio::null())
467+
.status()?;
468+
469+
// Add remote and fetch only the needed content
470+
Command::new("git")
471+
.current_dir(temp_dir.path())
472+
.args(["remote", "add", "origin", repo_url])
473+
.stdout(Stdio::null())
474+
.stderr(Stdio::null())
475+
.status()?;
476+
477+
let fetch_status = Command::new("git")
478+
.current_dir(temp_dir.path())
479+
.args(["fetch", "--depth=1", "origin", "main"])
480+
.stdout(Stdio::null())
481+
.stderr(Stdio::null())
482+
.status()?;
483+
484+
if !fetch_status.success() {
485+
return Err(anyhow!("Failed to fetch from graph-tooling repository"));
431486
}
432487

433-
// Remove .git directory to start fresh
434-
let git_dir = directory.join(".git");
435-
if git_dir.exists() {
436-
fs::remove_dir_all(&git_dir)?;
488+
Command::new("git")
489+
.current_dir(temp_dir.path())
490+
.args(["checkout", "origin/main"])
491+
.stdout(Stdio::null())
492+
.stderr(Stdio::null())
493+
.status()?;
494+
495+
// Verify example exists
496+
let example_path = temp_dir.path().join("examples").join(example);
497+
if !example_path.exists() {
498+
return Err(anyhow!(
499+
"Example '{}' not found. See available examples at:\n\
500+
https://github.com/graphprotocol/graph-tooling/tree/main/examples",
501+
example
502+
));
437503
}
438504

505+
// Copy example to target directory
506+
step(
507+
Step::Generate,
508+
&format!("Creating subgraph from example: {}", example),
509+
);
510+
copy_dir_recursive(&example_path, &directory)?;
511+
439512
// Initialize fresh git repo unless skipped
440513
if !opt.skip_git {
441514
step(Step::Generate, "Initializing Git repository");
442515
let _ = Command::new("git")
443516
.current_dir(&directory)
444517
.arg("init")
518+
.stdout(Stdio::null())
519+
.stderr(Stdio::null())
445520
.status();
446521
}
447522

gnd/tests/cli_commands.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ fn test_init_from_example() {
108108
let stderr = String::from_utf8_lossy(&output.stderr);
109109
// Example downloads from GitHub, may fail in CI without network
110110
// Skip test if it fails due to network issues
111-
if stderr.contains("network") || stderr.contains("fetch") || stderr.contains("download") {
111+
if stderr.contains("network")
112+
|| stderr.contains("fetch")
113+
|| stderr.contains("download")
114+
|| stderr.contains("Failed to fetch")
115+
{
112116
eprintln!("Skipping test_init_from_example: network unavailable");
113117
return;
114118
}
@@ -132,6 +136,94 @@ fn test_init_from_example() {
132136
subgraph_dir.join("package.json").exists(),
133137
"package.json should exist"
134138
);
139+
140+
// Verify it's actually the ethereum-gravatar example (contains Gravatar entity)
141+
let schema = fs::read_to_string(subgraph_dir.join("schema.graphql")).unwrap();
142+
assert!(
143+
schema.contains("Gravatar"),
144+
"schema.graphql should contain Gravatar entity"
145+
);
146+
}
147+
148+
#[test]
149+
fn test_init_from_example_invalid_name() {
150+
let temp_dir = TempDir::new().unwrap();
151+
let output = run_gnd(
152+
&[
153+
"init",
154+
"--skip-install",
155+
"--from-example",
156+
"nonexistent-example-12345",
157+
"test",
158+
],
159+
temp_dir.path(),
160+
);
161+
162+
// Command should fail
163+
assert!(!output.status.success());
164+
165+
let stderr = String::from_utf8_lossy(&output.stderr);
166+
167+
// Skip test if it fails due to network issues (can't verify example doesn't exist)
168+
if stderr.contains("Failed to fetch from graph-tooling") {
169+
eprintln!("Skipping test_init_from_example_invalid_name: network unavailable");
170+
return;
171+
}
172+
173+
// Should show helpful error message
174+
assert!(
175+
stderr.contains("not found") || stderr.contains("graph-tooling"),
176+
"Error should mention example not found or link to graph-tooling. Got: {}",
177+
stderr
178+
);
179+
}
180+
181+
#[test]
182+
fn test_init_from_example_aggregations() {
183+
let temp_dir = TempDir::new().unwrap();
184+
let subgraph_dir = temp_dir.path().join("my-agg");
185+
186+
let output = run_gnd(
187+
&[
188+
"init",
189+
"--skip-install",
190+
"--from-example",
191+
"aggregations",
192+
"my-agg",
193+
],
194+
temp_dir.path(),
195+
);
196+
197+
if !output.status.success() {
198+
let stdout = String::from_utf8_lossy(&output.stdout);
199+
let stderr = String::from_utf8_lossy(&output.stderr);
200+
// Example downloads from GitHub, may fail in CI without network
201+
if stderr.contains("network")
202+
|| stderr.contains("fetch")
203+
|| stderr.contains("download")
204+
|| stderr.contains("Failed to fetch")
205+
{
206+
eprintln!("Skipping test_init_from_example_aggregations: network unavailable");
207+
return;
208+
}
209+
panic!(
210+
"gnd init --from-example aggregations failed:\nstdout: {}\nstderr: {}",
211+
stdout, stderr
212+
);
213+
}
214+
215+
// Verify scaffold was created
216+
assert!(subgraph_dir.exists(), "Subgraph directory should exist");
217+
218+
// Verify it's actually the aggregations example, not ethereum-gravatar
219+
let manifest = fs::read_to_string(subgraph_dir.join("subgraph.yaml")).unwrap();
220+
221+
// The aggregations example uses specVersion 1.1.0 and has blockHandlers,
222+
// whereas ethereum-gravatar uses 0.0.5 and has eventHandlers
223+
assert!(
224+
manifest.contains("specVersion: 1.1.0") || manifest.contains("blockHandlers"),
225+
"Manifest should be the aggregations example (expected specVersion 1.1.0 or blockHandlers)"
226+
);
135227
}
136228

137229
#[test]

0 commit comments

Comments
 (0)