Skip to content

Commit 09a9004

Browse files
authored
Merge pull request #73 from github/jalafel/graceful-continue
Adds graceful continue
2 parents 899845a + 1b9afe3 commit 09a9004

File tree

5 files changed

+135
-23
lines changed

5 files changed

+135
-23
lines changed

cmd/gh-classroom/clone/student-repos/student-repos.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/github/gh-classroom/cmd/gh-classroom/shared"
1414
"github.com/github/gh-classroom/pkg/classroom"
1515
"github.com/spf13/cobra"
16+
"github.com/github/gh-classroom/cmd/gh-classroom/clone/utils"
1617
)
1718

1819
func NewCmdStudentRepo(f *cmdutil.Factory) *cobra.Command {
@@ -21,6 +22,7 @@ func NewCmdStudentRepo(f *cmdutil.Factory) *cobra.Command {
2122
var page int
2223
var perPage int
2324
var getAll bool
25+
var verbose bool
2426

2527
cmd := &cobra.Command{
2628
Use: "student-repos",
@@ -93,18 +95,26 @@ func NewCmdStudentRepo(f *cmdutil.Factory) *cobra.Command {
9395
}
9496

9597
totalCloned := 0
98+
cloneErrors := []string{}
9699
for _, acceptAssignment := range acceptedAssignmentList.AcceptedAssignments {
97-
clonePath := filepath.Join(fullPath, acceptAssignment.Repository.Name())
98-
if _, err := os.Stat(clonePath); os.IsNotExist(err) {
99-
fmt.Printf("Cloning into: %v\n", clonePath)
100-
_, _, err := gh.Exec("repo", "clone", acceptAssignment.Repository.FullName, "--", clonePath)
101-
totalCloned++
102-
if err != nil {
103-
log.Fatal(err)
104-
return
105-
}
100+
clonePath := filepath.Join(fullPath, acceptAssignment.Repository.Name)
101+
err := utils.CloneRepository(clonePath, acceptAssignment.Repository.FullName, gh.Exec)
102+
if err != nil {
103+
errMsg := fmt.Sprintf("Error cloning %s: %v", acceptAssignment.Repository.FullName, err)
104+
fmt.Println(errMsg)
105+
cloneErrors = append(cloneErrors, errMsg)
106+
continue // Continue with the next iteration
107+
}
108+
totalCloned++
109+
}
110+
if len(cloneErrors) > 0 {
111+
fmt.Println("Some repositories failed to clone.")
112+
if !verbose {
113+
fmt.Println("Run with --verbose flag to see more details")
106114
} else {
107-
fmt.Printf("Skip existing repo: %v use gh classroom pull to get new commits\n", clonePath)
115+
for _, errMsg := range cloneErrors {
116+
fmt.Println(errMsg)
117+
}
108118
}
109119
}
110120
if getAll {
@@ -121,6 +131,7 @@ func NewCmdStudentRepo(f *cmdutil.Factory) *cobra.Command {
121131
cmd.Flags().IntVar(&page, "page", 1, "Page number")
122132
cmd.Flags().IntVar(&perPage, "per-page", 15, "Number of accepted assignments per page")
123133
cmd.Flags().BoolVar(&getAll, "all", true, "Clone All assignments by default")
134+
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose error output")
124135

125136
return cmd
126137
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// cmd/gh-classroom/clone/utils/clone-repository.go
2+
3+
package utils
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"bytes"
9+
)
10+
11+
// This abstraction allows for easier testing and decoupling from the actual CLI.
12+
// Exec invokes a gh command in a subprocess and captures the output and error streams.
13+
type GitHubExec func(args ...string) (stdout, stderr bytes.Buffer, err error)
14+
15+
// CloneRepository attempts to clone a repository into the specified path.
16+
// It returns an error if the cloning process fails.
17+
func CloneRepository(clonePath string, repoFullName string, ghExec GitHubExec) error {
18+
if _, err := os.Stat(clonePath); os.IsNotExist(err) {
19+
fmt.Printf("Cloning into: %v\n", clonePath)
20+
_, _, err := ghExec("repo", "clone", repoFullName, "--", clonePath)
21+
if err != nil {
22+
fmt.Printf("error cloning %s: %v\n", repoFullName, err)
23+
return fmt.Errorf("error cloning %s: %v", repoFullName, err)
24+
}
25+
return nil // Success
26+
}
27+
fmt.Printf("Skip existing repo: %v use gh classroom pull to get new commits\n", clonePath)
28+
return fmt.Errorf("repository already exists: %s", clonePath)
29+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package utils
2+
3+
import (
4+
"os"
5+
"testing"
6+
"bytes"
7+
"fmt"
8+
"errors"
9+
"path/filepath"
10+
)
11+
12+
func TestCloneRepository(t *testing.T) {
13+
tmpDir, err := os.MkdirTemp("", "cloneTest")
14+
if err != nil {
15+
t.Fatalf("Failed to create temp directory: %v", err)
16+
}
17+
defer os.RemoveAll(tmpDir) // clean up
18+
// Test cases
19+
tests := []struct {
20+
name string
21+
execMock GitHubExec
22+
clonePath string
23+
repoFullName string
24+
wantErr bool
25+
errMsg string
26+
}{
27+
{
28+
name: "successful clone",
29+
execMock: func(args ...string) (stdout bytes.Buffer, stderr bytes.Buffer, err error) {
30+
var stdoutBuf, stderrBuf bytes.Buffer
31+
stdoutBuf.Write([]byte("your string here"))
32+
return stdoutBuf, stderrBuf, nil
33+
},
34+
clonePath: "", // Will be set to a temp directory in the test
35+
repoFullName: "example/repo",
36+
wantErr: false,
37+
},
38+
{
39+
name: "clone failure",
40+
execMock: func(args ...string) (stdout bytes.Buffer, stderr bytes.Buffer, err error) {
41+
var stdoutBuf, stderrBuf bytes.Buffer
42+
return stdoutBuf, stderrBuf, errors.New("clone error")
43+
},
44+
clonePath: filepath.Join(tmpDir, "repo"),
45+
repoFullName: "example/repo",
46+
wantErr: true,
47+
errMsg: "error cloning example/repo: clone error",
48+
},
49+
{
50+
name: "repository already exists",
51+
execMock: func(args ...string) (stdout bytes.Buffer, stderr bytes.Buffer, err error) {
52+
var stdoutBuf, stderrBuf bytes.Buffer
53+
return stdoutBuf, stderrBuf, nil
54+
},
55+
clonePath: "./repo", // Current directory always exists
56+
repoFullName: "example/repo",
57+
wantErr: true,
58+
errMsg: "repository already exists: .",
59+
},
60+
}
61+
62+
63+
for _, tt := range tests {
64+
t.Run(tt.name, func(t *testing.T) {
65+
if tt.name == "successful clone" {
66+
fmt.Println("Running successful clone test")
67+
tmpDir, err := os.MkdirTemp("", "cloneTest")
68+
if err != nil {
69+
t.Fatalf("Failed to create temp directory: %v", err)
70+
}
71+
defer os.RemoveAll(tmpDir) // clean up
72+
tt.clonePath = filepath.Join(tmpDir, "repo")
73+
}
74+
75+
76+
fmt.Println("Running test", tt.name, "with clonePath", tt.clonePath, "and repoFullName", tt.repoFullName)
77+
err := CloneRepository(tt.clonePath, tt.repoFullName, tt.execMock)
78+
if err != nil && err.Error() != tt.errMsg {
79+
t.Errorf("CloneRepository() error = %v, wantErr %v", err, tt.wantErr)
80+
}
81+
})
82+
}
83+
}

pkg/classroom/classroom.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package classroom
22

33
import (
44
"fmt"
5-
"strings"
65
)
76

87
type AssignmentList struct {
@@ -57,6 +56,7 @@ type Classroom struct {
5756

5857
type GithubRepository struct {
5958
Id int `json:"id"`
59+
Name string `json:"name"`
6060
FullName string `json:"full_name"`
6161
HtmlUrl string `json:"html_url"`
6262
NodeId string `json:"node_id"`
@@ -155,7 +155,3 @@ func (a Assignment) IsGroupAssignment() bool {
155155
func (a AcceptedAssignment) RepositoryUrl() string {
156156
return a.Repository.HtmlUrl
157157
}
158-
159-
func (gr GithubRepository) Name() string {
160-
return strings.Split(gr.FullName, "/")[1]
161-
}

pkg/classroom/classroom_test.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,4 @@ func TestAcceptedAssignments(t *testing.T) {
131131
acceptedAssignment := AcceptedAssignment{Repository: GithubRepository{HtmlUrl: "https://github.com/owner/repo"}}
132132
assert.Equal(t, acceptedAssignment.RepositoryUrl(), "https://github.com/owner/repo")
133133
})
134-
}
135-
136-
func TestGithubRepositories(t *testing.T) {
137-
t.Run("Returns repo name", func(t *testing.T) {
138-
repository := GithubRepository{FullName: "owner/repo"}
139-
assert.Equal(t, repository.Name(), "repo")
140-
})
141-
}
134+
}

0 commit comments

Comments
 (0)