From e8ad2202c7a97500980580faa8bc802dc2bd3ca2 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Thu, 8 Mar 2018 11:48:11 -0800 Subject: [PATCH] Add hostname to Repo model. - Ended up changing how we parse the json so we do the validation in the constructor of the Repo so it's the same everywhere. - This is a preliminary commit to finish the repo whitelisting feature --- server/events/event_parser.go | 102 ++++------------------ server/events/event_parser_test.go | 92 ++++++++----------- server/events/mocks/mock_event_parsing.go | 20 +++-- server/events/models/models.go | 51 +++++++++++ server/events/models/models_test.go | 76 ++++++++++++++++ 5 files changed, 194 insertions(+), 147 deletions(-) create mode 100644 server/events/models/models_test.go diff --git a/server/events/event_parser.go b/server/events/event_parser.go index fd3db3ae7..f0bfad61f 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -1,13 +1,11 @@ package events import ( - "errors" - "fmt" "regexp" - "strings" "github.com/google/go-github/github" "github.com/lkysow/go-gitlab" + "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events/models" ) @@ -35,8 +33,8 @@ type EventParsing interface { ParseGithubIssueCommentEvent(comment *github.IssueCommentEvent) (baseRepo models.Repo, user models.User, pullNum int, err error) ParseGithubPull(pull *github.PullRequest) (models.PullRequest, models.Repo, error) ParseGithubRepo(ghRepo *github.Repository) (models.Repo, error) - ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.PullRequest, models.Repo) - ParseGitlabMergeCommentEvent(event gitlab.MergeCommentEvent) (baseRepo models.Repo, headRepo models.Repo, user models.User) + ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.PullRequest, models.Repo, error) + ParseGitlabMergeCommentEvent(event gitlab.MergeCommentEvent) (baseRepo models.Repo, headRepo models.Repo, user models.User, err error) ParseGitlabMergeRequest(mr *gitlab.MergeRequest) models.PullRequest } @@ -114,37 +112,10 @@ func (e *EventParser) ParseGithubPull(pull *github.PullRequest) (models.PullRequ } func (e *EventParser) ParseGithubRepo(ghRepo *github.Repository) (models.Repo, error) { - var repo models.Repo - repoFullName := ghRepo.GetFullName() - if repoFullName == "" { - return repo, errors.New("repository.full_name is null") - } - repoOwner := ghRepo.Owner.GetLogin() - if repoOwner == "" { - return repo, errors.New("repository.owner.login is null") - } - repoName := ghRepo.GetName() - if repoName == "" { - return repo, errors.New("repository.name is null") - } - repoSanitizedCloneURL := ghRepo.GetCloneURL() - if repoSanitizedCloneURL == "" { - return repo, errors.New("repository.clone_url is null") - } - - // Construct HTTPS repo clone url string with username and password. - repoCloneURL := strings.Replace(repoSanitizedCloneURL, "https://", fmt.Sprintf("https://%s:%s@", e.GithubUser, e.GithubToken), -1) - - return models.Repo{ - Owner: repoOwner, - FullName: repoFullName, - CloneURL: repoCloneURL, - SanitizedCloneURL: repoSanitizedCloneURL, - Name: repoName, - }, nil + return models.NewRepo(ghRepo.GetFullName(), ghRepo.GetCloneURL(), e.GithubUser, e.GithubToken) } -func (e *EventParser) ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.PullRequest, models.Repo) { +func (e *EventParser) ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.PullRequest, models.Repo, error) { modelState := models.Closed if event.ObjectAttributes.State == gitlabPullOpened { modelState = models.Open @@ -161,62 +132,27 @@ func (e *EventParser) ParseGitlabMergeEvent(event gitlab.MergeEvent) (models.Pul State: modelState, } - cloneURL := e.addGitlabAuth(event.Project.GitHTTPURL) - // Get owner and name from PathWithNamespace because the fields - // event.Project.Name and event.Project.Owner can have capitals. - owner, name := e.getOwnerAndName(event.Project.PathWithNamespace) - repo := models.Repo{ - FullName: event.Project.PathWithNamespace, - Name: name, - SanitizedCloneURL: event.Project.GitHTTPURL, - Owner: owner, - CloneURL: cloneURL, - } - return pull, repo -} - -// addGitlabAuth adds gitlab username/password to the cloneURL. -// We support http and https URLs because GitLab's docs have http:// URLs whereas -// their API responses have https://. -// Ex. https://gitlab.com/owner/repo.git => https://uname:pass@gitlab.com/owner/repo.git -func (e *EventParser) addGitlabAuth(cloneURL string) string { - httpsReplaced := strings.Replace(cloneURL, "https://", fmt.Sprintf("https://%s:%s@", e.GitlabUser, e.GitlabToken), -1) - return strings.Replace(httpsReplaced, "http://", fmt.Sprintf("http://%s:%s@", e.GitlabUser, e.GitlabToken), -1) -} - -// getOwnerAndName takes pathWithNamespace that should look like "owner/repo" -// and returns "owner", "repo" -func (e *EventParser) getOwnerAndName(pathWithNamespace string) (string, string) { - pathSplit := strings.Split(pathWithNamespace, "/") - if len(pathSplit) > 1 { - return pathSplit[0], pathSplit[1] - } - return "", "" + repo, err := models.NewRepo(event.Project.PathWithNamespace, event.Project.GitHTTPURL, e.GitlabUser, e.GitlabToken) + return pull, repo, err } // ParseGitlabMergeCommentEvent creates Atlantis models out of a GitLab event. -func (e *EventParser) ParseGitlabMergeCommentEvent(event gitlab.MergeCommentEvent) (baseRepo models.Repo, headRepo models.Repo, user models.User) { - // Get owner and name from PathWithNamespace because the fields - // event.Project.Name and event.Project.Owner can have capitals. - owner, name := e.getOwnerAndName(event.Project.PathWithNamespace) - baseRepo = models.Repo{ - FullName: event.Project.PathWithNamespace, - Name: name, - SanitizedCloneURL: event.Project.GitHTTPURL, - Owner: owner, - CloneURL: e.addGitlabAuth(event.Project.GitHTTPURL), +func (e *EventParser) ParseGitlabMergeCommentEvent(event gitlab.MergeCommentEvent) (baseRepo models.Repo, headRepo models.Repo, user models.User, err error) { + // Parse the base repo first. + repoFullName := event.Project.PathWithNamespace + cloneURL := event.Project.GitHTTPURL + baseRepo, err = models.NewRepo(repoFullName, cloneURL, e.GitlabUser, e.GitlabToken) + if err != nil { + return } user = models.User{ Username: event.User.Username, } - owner, name = e.getOwnerAndName(event.MergeRequest.Source.PathWithNamespace) - headRepo = models.Repo{ - FullName: event.MergeRequest.Source.PathWithNamespace, - Name: name, - SanitizedCloneURL: event.MergeRequest.Source.GitHTTPURL, - Owner: owner, - CloneURL: e.addGitlabAuth(event.MergeRequest.Source.GitHTTPURL), - } + + // Now parse the head repo. + headRepoFullName := event.MergeRequest.Source.PathWithNamespace + headCloneURL := event.MergeRequest.Source.GitHTTPURL + headRepo, err = models.NewRepo(headRepoFullName, headCloneURL, e.GitlabUser, e.GitlabToken) return } diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index a13b9f3e2..8142e926d 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -2,7 +2,6 @@ package events_test import ( "encoding/json" - "errors" "testing" "github.com/google/go-github/github" @@ -22,38 +21,16 @@ var parser = events.EventParser{ } func TestParseGithubRepo(t *testing.T) { - testRepo := Repo - testRepo.FullName = nil - _, err := parser.ParseGithubRepo(&testRepo) - Equals(t, errors.New("repository.full_name is null"), err) - - testRepo = Repo - testRepo.Owner = nil - _, err = parser.ParseGithubRepo(&testRepo) - Equals(t, errors.New("repository.owner.login is null"), err) - - testRepo = Repo - testRepo.Name = nil - _, err = parser.ParseGithubRepo(&testRepo) - Equals(t, errors.New("repository.name is null"), err) - - testRepo = Repo - testRepo.CloneURL = nil - _, err = parser.ParseGithubRepo(&testRepo) - Equals(t, errors.New("repository.clone_url is null"), err) - - t.Log("should replace https clone with user/pass") - { - r, err := parser.ParseGithubRepo(&Repo) - Ok(t, err) - Equals(t, models.Repo{ - Owner: "owner", - FullName: "owner/repo", - CloneURL: "https://github-user:github-token@github.com/lkysow/atlantis-example.git", - SanitizedCloneURL: Repo.GetCloneURL(), - Name: "repo", - }, r) - } + r, err := parser.ParseGithubRepo(&Repo) + Ok(t, err) + Equals(t, models.Repo{ + Owner: "owner", + FullName: "owner/repo", + CloneURL: "https://github-user:github-token@github.com/owner/repo.git", + SanitizedCloneURL: Repo.GetCloneURL(), + Name: "repo", + Hostname: "github.com", + }, r) } func TestParseGithubIssueCommentEvent(t *testing.T) { @@ -68,30 +45,26 @@ func TestParseGithubIssueCommentEvent(t *testing.T) { User: &github.User{Login: github.String("comment_user")}, }, } - testComment := deepcopy.Copy(comment).(github.IssueCommentEvent) - testComment.Repo = nil - _, _, _, err := parser.ParseGithubIssueCommentEvent(&testComment) - Equals(t, errors.New("repository.full_name is null"), err) - testComment = deepcopy.Copy(comment).(github.IssueCommentEvent) + testComment := deepcopy.Copy(comment).(github.IssueCommentEvent) testComment.Comment = nil - _, _, _, err = parser.ParseGithubIssueCommentEvent(&testComment) - Equals(t, errors.New("comment.user.login is null"), err) + _, _, _, err := parser.ParseGithubIssueCommentEvent(&testComment) + ErrEquals(t, "comment.user.login is null", err) testComment = deepcopy.Copy(comment).(github.IssueCommentEvent) testComment.Comment.User = nil _, _, _, err = parser.ParseGithubIssueCommentEvent(&testComment) - Equals(t, errors.New("comment.user.login is null"), err) + ErrEquals(t, "comment.user.login is null", err) testComment = deepcopy.Copy(comment).(github.IssueCommentEvent) testComment.Comment.User.Login = nil _, _, _, err = parser.ParseGithubIssueCommentEvent(&testComment) - Equals(t, errors.New("comment.user.login is null"), err) + ErrEquals(t, "comment.user.login is null", err) testComment = deepcopy.Copy(comment).(github.IssueCommentEvent) testComment.Issue = nil _, _, _, err = parser.ParseGithubIssueCommentEvent(&testComment) - Equals(t, errors.New("issue.number is null"), err) + ErrEquals(t, "issue.number is null", err) // this should be successful repo, user, pullNum, err := parser.ParseGithubIssueCommentEvent(&comment) @@ -99,9 +72,10 @@ func TestParseGithubIssueCommentEvent(t *testing.T) { Equals(t, models.Repo{ Owner: *comment.Repo.Owner.Login, FullName: *comment.Repo.FullName, - CloneURL: "https://github-user:github-token@github.com/lkysow/atlantis-example.git", + CloneURL: "https://github-user:github-token@github.com/owner/repo.git", SanitizedCloneURL: *comment.Repo.CloneURL, Name: "repo", + Hostname: "github.com", }, repo) Equals(t, models.User{ Username: *comment.Comment.User.Login, @@ -113,32 +87,27 @@ func TestParseGithubPull(t *testing.T) { testPull := deepcopy.Copy(Pull).(github.PullRequest) testPull.Head.SHA = nil _, _, err := parser.ParseGithubPull(&testPull) - Equals(t, errors.New("head.sha is null"), err) + ErrEquals(t, "head.sha is null", err) testPull = deepcopy.Copy(Pull).(github.PullRequest) testPull.HTMLURL = nil _, _, err = parser.ParseGithubPull(&testPull) - Equals(t, errors.New("html_url is null"), err) + ErrEquals(t, "html_url is null", err) testPull = deepcopy.Copy(Pull).(github.PullRequest) testPull.Head.Ref = nil _, _, err = parser.ParseGithubPull(&testPull) - Equals(t, errors.New("head.ref is null"), err) + ErrEquals(t, "head.ref is null", err) testPull = deepcopy.Copy(Pull).(github.PullRequest) testPull.User.Login = nil _, _, err = parser.ParseGithubPull(&testPull) - Equals(t, errors.New("user.login is null"), err) + ErrEquals(t, "user.login is null", err) testPull = deepcopy.Copy(Pull).(github.PullRequest) testPull.Number = nil _, _, err = parser.ParseGithubPull(&testPull) - Equals(t, errors.New("number is null"), err) - - testPull = deepcopy.Copy(Pull).(github.PullRequest) - testPull.Head.Repo = nil - _, _, err = parser.ParseGithubPull(&testPull) - Equals(t, errors.New("repository.full_name is null"), err) + ErrEquals(t, "number is null", err) pullRes, repoRes, err := parser.ParseGithubPull(&Pull) Ok(t, err) @@ -154,9 +123,10 @@ func TestParseGithubPull(t *testing.T) { Equals(t, models.Repo{ Owner: "owner", FullName: "owner/repo", - CloneURL: "https://github-user:github-token@github.com/lkysow/atlantis-example.git", + CloneURL: "https://github-user:github-token@github.com/owner/repo.git", SanitizedCloneURL: Repo.GetCloneURL(), Name: "repo", + Hostname: "github.com", }, repoRes) } @@ -165,7 +135,8 @@ func TestParseGitlabMergeEvent(t *testing.T) { var event *gitlab.MergeEvent err := json.Unmarshal([]byte(mergeEventJSON), &event) Ok(t, err) - pull, repo := parser.ParseGitlabMergeEvent(*event) + pull, repo, err := parser.ParseGitlabMergeEvent(*event) + Ok(t, err) Equals(t, models.PullRequest{ URL: "http://example.com/diaspora/merge_requests/1", Author: "root", @@ -181,11 +152,13 @@ func TestParseGitlabMergeEvent(t *testing.T) { SanitizedCloneURL: "https://example.com/gitlabhq/gitlab-test.git", Owner: "gitlabhq", CloneURL: "https://gitlab-user:gitlab-token@example.com/gitlabhq/gitlab-test.git", + Hostname: "example.com", }, repo) t.Log("If the state is closed, should set field correctly.") event.ObjectAttributes.State = "closed" - pull, _ = parser.ParseGitlabMergeEvent(*event) + pull, _, err = parser.ParseGitlabMergeEvent(*event) + Ok(t, err) Equals(t, models.Closed, pull.State) } @@ -215,13 +188,15 @@ func TestParseGitlabMergeCommentEvent(t *testing.T) { var event *gitlab.MergeCommentEvent err := json.Unmarshal([]byte(mergeCommentEventJSON), &event) Ok(t, err) - baseRepo, headRepo, user := parser.ParseGitlabMergeCommentEvent(*event) + baseRepo, headRepo, user, err := parser.ParseGitlabMergeCommentEvent(*event) + Ok(t, err) Equals(t, models.Repo{ FullName: "gitlabhq/gitlab-test", Name: "gitlab-test", SanitizedCloneURL: "https://example.com/gitlabhq/gitlab-test.git", Owner: "gitlabhq", CloneURL: "https://gitlab-user:gitlab-token@example.com/gitlabhq/gitlab-test.git", + Hostname: "example.com", }, baseRepo) Equals(t, models.Repo{ FullName: "gitlab-org/gitlab-test", @@ -229,6 +204,7 @@ func TestParseGitlabMergeCommentEvent(t *testing.T) { SanitizedCloneURL: "https://example.com/gitlab-org/gitlab-test.git", Owner: "gitlab-org", CloneURL: "https://gitlab-user:gitlab-token@example.com/gitlab-org/gitlab-test.git", + Hostname: "example.com", }, headRepo) Equals(t, models.User{ Username: "root", diff --git a/server/events/mocks/mock_event_parsing.go b/server/events/mocks/mock_event_parsing.go index ad401861d..5e2d1746f 100644 --- a/server/events/mocks/mock_event_parsing.go +++ b/server/events/mocks/mock_event_parsing.go @@ -80,11 +80,12 @@ func (mock *MockEventParsing) ParseGithubRepo(ghRepo *github.Repository) (models return ret0, ret1 } -func (mock *MockEventParsing) ParseGitlabMergeEvent(event go_gitlab.MergeEvent) (models.PullRequest, models.Repo) { +func (mock *MockEventParsing) ParseGitlabMergeEvent(event go_gitlab.MergeEvent) (models.PullRequest, models.Repo, error) { params := []pegomock.Param{event} - result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGitlabMergeEvent", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem()}) + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGitlabMergeEvent", params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 models.PullRequest var ret1 models.Repo + var ret2 error if len(result) != 0 { if result[0] != nil { ret0 = result[0].(models.PullRequest) @@ -92,16 +93,20 @@ func (mock *MockEventParsing) ParseGitlabMergeEvent(event go_gitlab.MergeEvent) if result[1] != nil { ret1 = result[1].(models.Repo) } + if result[2] != nil { + ret2 = result[2].(error) + } } - return ret0, ret1 + return ret0, ret1, ret2 } -func (mock *MockEventParsing) ParseGitlabMergeCommentEvent(event go_gitlab.MergeCommentEvent) (models.Repo, models.Repo, models.User) { +func (mock *MockEventParsing) ParseGitlabMergeCommentEvent(event go_gitlab.MergeCommentEvent) (models.Repo, models.Repo, models.User, error) { params := []pegomock.Param{event} - result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGitlabMergeCommentEvent", params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem()}) + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseGitlabMergeCommentEvent", params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 models.Repo var ret1 models.Repo var ret2 models.User + var ret3 error if len(result) != 0 { if result[0] != nil { ret0 = result[0].(models.Repo) @@ -112,8 +117,11 @@ func (mock *MockEventParsing) ParseGitlabMergeCommentEvent(event go_gitlab.Merge if result[2] != nil { ret2 = result[2].(models.User) } + if result[3] != nil { + ret3 = result[3].(error) + } } - return ret0, ret1, ret2 + return ret0, ret1, ret2, ret3 } func (mock *MockEventParsing) ParseGitlabMergeRequest(mr *go_gitlab.MergeRequest) models.PullRequest { diff --git a/server/events/models/models.go b/server/events/models/models.go index e89fba0b9..4d6181144 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -4,8 +4,13 @@ package models import ( + "fmt" + "net/url" paths "path" + "strings" "time" + + "github.com/pkg/errors" ) // Repo is a VCS repository. @@ -23,6 +28,52 @@ type Repo struct { // SanitizedCloneURL is the full HTTPS url for cloning without the username and password. // ex. "https://github.com/atlantis/atlantis.git". SanitizedCloneURL string + // Hostname of the VCS provider this repo is hosted on. + Hostname string +} + +func NewRepo(repoFullName string, cloneURL string, vcsUser string, vcsToken string) (Repo, error) { + if repoFullName == "" { + return Repo{}, errors.New("repoFullName can't be empty") + } + if cloneURL == "" { + return Repo{}, errors.New("cloneURL can't be empty") + } + + // Ensure the Clone URL is for the same repo to avoid something malicious. + cloneURLParsed, err := url.Parse(cloneURL) + if err != nil { + return Repo{}, errors.Wrap(err, "invalid clone url") + } + expClonePath := fmt.Sprintf("/%s.git", repoFullName) + if expClonePath != cloneURLParsed.Path { + return Repo{}, fmt.Errorf("expected clone url to have path %q but had %q", expClonePath, cloneURLParsed.Path) + } + + // Construct clone urls with http auth. Need to do both https and http + // because in GitLab's docs they have some http urls. + auth := fmt.Sprintf("%s:%s@", vcsUser, vcsToken) + authedCloneURL := strings.Replace(cloneURL, "https://", "https://"+auth, -1) + authedCloneURL = strings.Replace(authedCloneURL, "http://", "http://"+auth, -1) + + // Get the owner and repo names from the full name. + var owner string + var repo string + pathSplit := strings.Split(repoFullName, "/") + if len(pathSplit) != 2 || pathSplit[0] == "" || pathSplit[1] == "" { + return Repo{}, fmt.Errorf("invalid repo format %q", repoFullName) + } + owner = pathSplit[0] + repo = pathSplit[1] + + return Repo{ + FullName: repoFullName, + Owner: owner, + Name: repo, + CloneURL: authedCloneURL, + SanitizedCloneURL: cloneURL, + Hostname: cloneURLParsed.Hostname(), + }, nil } // PullRequest is a VCS pull request. diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go new file mode 100644 index 000000000..c3f0e9f97 --- /dev/null +++ b/server/events/models/models_test.go @@ -0,0 +1,76 @@ +package models_test + +import ( + "testing" + + "fmt" + + "github.com/runatlantis/atlantis/server/events/models" + . "github.com/runatlantis/atlantis/testing" +) + +func TestNewRepo_EmptyRepoFullName(t *testing.T) { + _, err := models.NewRepo("", "https://github.com/notowner/repo.git", "u", "p") + ErrEquals(t, "repoFullName can't be empty", err) +} + +func TestNewRepo_EmptyCloneURL(t *testing.T) { + _, err := models.NewRepo("owner/repo", "", "u", "p") + ErrEquals(t, "cloneURL can't be empty", err) +} + +func TestNewRepo_InvalidCloneURL(t *testing.T) { + _, err := models.NewRepo("owner/repo", ":", "u", "p") + ErrEquals(t, "invalid clone url: parse :: missing protocol scheme", err) +} + +func TestNewRepo_CloneURLWrongRepo(t *testing.T) { + _, err := models.NewRepo("owner/repo", "https://github.com/notowner/repo.git", "u", "p") + ErrEquals(t, `expected clone url to have path "/owner/repo.git" but had "/notowner/repo.git"`, err) +} + +func TestNewRepo_FullNameWrongFormat(t *testing.T) { + cases := []string{ + "owner/repo/extra", + "/", + "//", + "///", + "a/", + "/b", + } + for _, c := range cases { + t.Run(c, func(t *testing.T) { + cloneURL := fmt.Sprintf("https://github.com/%s.git", c) + _, err := models.NewRepo(c, cloneURL, "u", "p") + ErrEquals(t, fmt.Sprintf(`invalid repo format "%s"`, c), err) + }) + } +} + +func TestNewRepo_HTTPAuth(t *testing.T) { + // When the url has http the auth should be added. + repo, err := models.NewRepo("owner/repo", "http://github.com/owner/repo.git", "u", "p") + Ok(t, err) + Equals(t, models.Repo{ + Hostname: "github.com", + SanitizedCloneURL: "http://github.com/owner/repo.git", + CloneURL: "http://u:p@github.com/owner/repo.git", + FullName: "owner/repo", + Owner: "owner", + Name: "repo", + }, repo) +} + +func TestNewRepo_HTTPSAuth(t *testing.T) { + // When the url has https the auth should be added. + repo, err := models.NewRepo("owner/repo", "https://github.com/owner/repo.git", "u", "p") + Ok(t, err) + Equals(t, models.Repo{ + Hostname: "github.com", + SanitizedCloneURL: "https://github.com/owner/repo.git", + CloneURL: "https://u:p@github.com/owner/repo.git", + FullName: "owner/repo", + Owner: "owner", + Name: "repo", + }, repo) +}