Repository: goreleaser/goreleaser
Pull request #2548 (2021-10-03 15:04:59)

feat: changelog from github
seems like that cool "contributors" section on the release only works if for people mentioned in the release.

therefore, adding a new `changelog implementation`: `github`.

Previous `git` one still works and is the default.

I've manually edited the previous release so you can see how it looks like: https://github.com/goreleaser/goreleaser/releases/tag/v0.180.3

TODO:

- [x] more tests

codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2548?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2548](https://codecov.io/gh/goreleaser/goreleaser/pull/2548?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (94293eb) into [mocks](https://codecov.io/gh/goreleaser/goreleaser/commit/754e4e639e63910ae0a25f37065140ae763cf6ac?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (754e4e6) will **decrease** coverage by `0.25%`.
> The diff coverage is `50.94%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2548/graphs/tree.svg?width=650&height=150&src=pr&token=3bJvv6b7hN&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)](https://codecov.io/gh/goreleaser/goreleaser/pull/2548?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)

```diff
@@            Coverage Diff             @@
##            mocks    #2548      +/-   ##
==========================================
- Coverage   82.80%   82.55%   -0.26%     
==========================================
  Files          96       96              
  Lines        6864     6906      +42     
==========================================
+ Hits         5684     5701      +17     
- Misses       1002     1026      +24     
- Partials      178      179       +1     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2548?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [internal/client/client.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2548/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvY2xpZW50L2NsaWVudC5nbw==) | `79.41% <ø> (ø)` | |
| [internal/client/mock.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2548/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvY2xpZW50L21vY2suZ28=) | `78.57% <0.00%> (-3.93%)` | :arrow_down: |
| [pkg/config/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2548/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-cGtnL2NvbmZpZy9jb25maWcuZ28=) | `90.72% <ø> (ø)` | |
| [internal/pipe/changelog/changelog.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2548/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9jaGFuZ2Vsb2cvY2hhbmdlbG9nLmdv) | `84.79% <42.85%> (-11.10%)` | :arrow_down: |
| [internal/client/github.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2548/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvY2xpZW50L2dpdGh1Yi5nbw==) | `35.08% <72.72%> (+1.74%)` | :arrow_up: |
| [internal/client/gitea.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2548/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvY2xpZW50L2dpdGVhLmdv) | `88.05% <100.00%> (+0.10%)` | :arrow_up: |
| [internal/client/gitlab.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2548/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvY2xpZW50L2dpdGxhYi5nbw==) | `39.60% <100.00%> (+0.34%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2548?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2548?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [754e4e6...94293eb](https://codecov.io/gh/goreleaser/goreleaser/pull/2548?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser).


Patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index b7419561..5fb32704 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -35,6 +35,7 @@ checksum:
 
 changelog:
   sort: asc
+  use: github
   filters:
     exclude:
     - '^docs:'
diff --git a/internal/client/client.go b/internal/client/client.go
index cbb62907..ac591743 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -12,6 +12,9 @@ import (
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
+// ErrNotImplemented is returned when a client does not implement certain feature.
+var ErrNotImplemented = fmt.Errorf("not implemented")
+
 // Info of the repository.
 type Info struct {
 	Description string
@@ -40,6 +43,7 @@ type Client interface {
 	CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo Repo, content []byte, path, message string) (err error)
 	Upload(ctx *context.Context, releaseID string, artifact *artifact.Artifact, file *os.File) (err error)
 	GetDefaultBranch(ctx *context.Context, repo Repo) (string, error)
+	Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error)
 }
 
 // New creates a new client depending on the token type.
@@ -56,8 +60,6 @@ func newWithToken(ctx *context.Context, token string) (Client, error) {
 		return NewGitLab(ctx, token)
 	case context.TokenTypeGitea:
 		return NewGitea(ctx, token)
-	case context.TokenTypeMock:
-		return NewMock(), nil
 	default:
 		return nil, fmt.Errorf("invalid client token type: %q", ctx.TokenType)
 	}
diff --git a/internal/client/gitea.go b/internal/client/gitea.go
index 4019cba5..12e941f8 100644
--- a/internal/client/gitea.go
+++ b/internal/client/gitea.go
@@ -66,6 +66,10 @@ func NewGitea(ctx *context.Context, token string) (Client, error) {
 	return &giteaClient{client: client}, nil
 }
 
+func (c *giteaClient) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
+	return "", ErrNotImplemented
+}
+
 // CloseMilestone closes a given milestone.
 func (c *giteaClient) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
 	closedState := gitea.StateClosed
diff --git a/internal/client/github.go b/internal/client/github.go
index d38ea564..7effc525 100644
--- a/internal/client/github.go
+++ b/internal/client/github.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"reflect"
 	"strconv"
+	"strings"
 
 	"github.com/apex/log"
 	"github.com/google/go-github/v35/github"
@@ -24,6 +25,12 @@ type githubClient struct {
 	client *github.Client
 }
 
+// NewUnauthenticatedGitHub returns a github client that is not authenticated.
+// Used in tests only.
+func NewUnauthenticatedGitHub() Client {
+	return &githubClient{client: github.NewClient(nil)}
+}
+
 // NewGitHub returns a github client implementation.
 func NewGitHub(ctx *context.Context, token string) (Client, error) {
 	ts := oauth2.StaticTokenSource(
@@ -51,6 +58,23 @@ func NewGitHub(ctx *context.Context, token string) (Client, error) {
 	return &githubClient{client: client}, nil
 }
 
+func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
+	result, _, err := c.client.Repositories.CompareCommits(ctx, repo.Owner, repo.Name, prev, current)
+	if err != nil {
+		return "", err
+	}
+	var log []string
+	for _, commit := range result.Commits {
+		log = append(log, fmt.Sprintf(
+			"- %s: %s (@%s)",
+			commit.GetSHA(),
+			strings.Split(commit.Commit.GetMessage(), "\n")[0],
+			commit.GetAuthor().GetLogin(),
+		))
+	}
+	return strings.Join(log, "\n"), nil
+}
+
 // GetDefaultBranch returns the default branch of a github repo
 func (c *githubClient) GetDefaultBranch(ctx *context.Context, repo Repo) (string, error) {
 	p, res, err := c.client.Repositories.Get(ctx, repo.Owner, repo.Name)
diff --git a/internal/client/gitlab.go b/internal/client/gitlab.go
index bf98f400..9146392e 100644
--- a/internal/client/gitlab.go
+++ b/internal/client/gitlab.go
@@ -49,6 +49,10 @@ func NewGitLab(ctx *context.Context, token string) (Client, error) {
 	return &gitlabClient{client: client}, nil
 }
 
+func (c *gitlabClient) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
+	return "", ErrNotImplemented
+}
+
 // GetDefaultBranch get the default branch
 func (c *gitlabClient) GetDefaultBranch(ctx *context.Context, repo Repo) (string, error) {
 	projectID := repo.String()
diff --git a/internal/client/mock.go b/internal/client/mock.go
index 891fd058..718236e2 100644
--- a/internal/client/mock.go
+++ b/internal/client/mock.go
@@ -30,12 +30,16 @@ type Mock struct {
 	Lock                sync.Mutex
 }
 
+func (c *Mock) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
+	return "", ErrNotImplemented
+}
+
 func (c *Mock) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
 	return nil
 }
 
 func (c *Mock) GetDefaultBranch(ctx *context.Context, repo Repo) (string, error) {
-	return "", errors.New("Mock does not yet implement GetDefaultBranch")
+	return "", ErrNotImplemented
 }
 
 func (c *Mock) CreateRelease(ctx *context.Context, body string) (string, error) {
diff --git a/internal/pipe/changelog/changelog.go b/internal/pipe/changelog/changelog.go
index add01f25..ed91cb04 100644
--- a/internal/pipe/changelog/changelog.go
+++ b/internal/pipe/changelog/changelog.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 
 	"github.com/apex/log"
+	"github.com/goreleaser/goreleaser/internal/client"
 	"github.com/goreleaser/goreleaser/internal/git"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/context"
@@ -106,7 +107,7 @@ func checkSortDirection(mode string) error {
 }
 
 func buildChangelog(ctx *context.Context) ([]string, error) {
-	log, err := getChangelog(ctx.Git.CurrentTag)
+	log, err := getChangelog(ctx, ctx.Git.CurrentTag)
 	if err != nil {
 		return nil, err
 	}
@@ -161,21 +162,37 @@ func extractCommitInfo(line string) string {
 	return strings.Join(strings.Split(line, " ")[1:], " ")
 }
 
-func getChangelog(tag string) (string, error) {
+func getChangelog(ctx *context.Context, tag string) (string, error) {
 	prev, err := previous(tag)
 	if err != nil {
 		return "", err
 	}
-	if isSHA1(prev) {
-		return gitLog(prev, tag)
+	return doGetChangelog(ctx, prev, tag)
+}
+
+func doGetChangelog(ctx *context.Context, prev, tag string) (string, error) {
+	l, err := getChangeloger(ctx)
+	if err != nil {
+		return "", err
 	}
-	return gitLog(fmt.Sprintf("tags/%s..tags/%s", prev, tag))
+	return l.Log(ctx, prev, tag)
 }
 
-func gitLog(refs ...string) (string, error) {
-	args := []string{"log", "--pretty=oneline", "--abbrev-commit", "--no-decorate", "--no-color"}
-	args = append(args, refs...)
-	return git.Run(args...)
+func getChangeloger(ctx *context.Context) (changeloger, error) {
+	switch ctx.Config.Changelog.Use {
+	case "git":
+		fallthrough
+	case "":
+		return gitChangeloger{}, nil
+	case "github":
+		client, err := client.New(ctx)
+		if err != nil {
+			return nil, err
+		}
+		return &scmChangeloger{client: client}, nil
+	default:
+		return nil, fmt.Errorf("invalid changelog.use: %q", ctx.Config.Changelog.Use)
+	}
 }
 
 func previous(tag string) (result string, err error) {
@@ -190,13 +207,6 @@ func previous(tag string) (result string, err error) {
 	return
 }
 
-var validSHA1 = regexp.MustCompile(`^[a-fA-F0-9]{40}$`)
-
-// isSHA1 te lets us know if the ref is a SHA1 or not.
-func isSHA1(ref string) bool {
-	return validSHA1.MatchString(ref)
-}
-
 func loadContent(ctx *context.Context, fileName, tmplName string) (string, error) {
 	if tmplName != "" {
 		log.Debugf("loading template %s", tmplName)
@@ -214,3 +224,36 @@ func loadContent(ctx *context.Context, fileName, tmplName string) (string, error
 
 	return "", nil
 }
+
+type changeloger interface {
+	Log(ctx *context.Context, prev, current string) (string, error)
+}
+
+type gitChangeloger struct{}
+
+var validSHA1 = regexp.MustCompile(`^[a-fA-F0-9]{40}$`)
+
+func (g gitChangeloger) Log(ctx *context.Context, prev, current string) (string, error) {
+	args := []string{"log", "--pretty=oneline", "--abbrev-commit", "--no-decorate", "--no-color"}
+	if validSHA1.MatchString(prev) {
+		args = append(args, prev, current)
+	} else {
+		args = append(args, fmt.Sprintf("tags/%s..tags/%s", prev, current))
+	}
+	return git.Run(args...)
+}
+
+type scmChangeloger struct {
+	client client.Client
+}
+
+func (c *scmChangeloger) Log(ctx *context.Context, prev, current string) (string, error) {
+	repo, err := git.ExtractRepoFromConfig()
+	if err != nil {
+		return "", err
+	}
+	return c.client.Changelog(ctx, client.Repo{
+		Owner: repo.Owner,
+		Name:  repo.Name,
+	}, prev, current)
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 4a02a575..42799db9 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -595,6 +595,7 @@ type Changelog struct {
 	Filters Filters `yaml:",omitempty"`
 	Sort    string  `yaml:",omitempty"`
 	Skip    bool    `yaml:",omitempty"` // TODO(caarlos0): rename to Disable to match other pipes
+	Use     string  `yaml:",omitempty"`
 }
 
 // EnvFiles holds paths to files that contains environment variables
diff --git a/pkg/context/context.go b/pkg/context/context.go
index 7de7fac7..cc76b7a4 100644
--- a/pkg/context/context.go
+++ b/pkg/context/context.go
@@ -59,8 +59,6 @@ const (
 	TokenTypeGitLab TokenType = "gitlab"
 	// TokenTypeGitea defines gitea as type of the token.
 	TokenTypeGitea TokenType = "gitea"
-	// TokenTypeMock is a mock token type used in tests.
-	TokenTypeMock = "mock"
 )
 
 // Context carries along some data through the pipes.
diff --git a/www/docs/customization/changelog.md b/www/docs/customization/changelog.md
new file mode 100644
index 00000000..7226dedb
--- /dev/null
+++ b/www/docs/customization/changelog.md
@@ -0,0 +1,34 @@
+# Changelog
+
+You can customize how the changelog is generated using the `changelog` section in the config file:
+
+```yaml
+# .goreleaser.yml
+changelog:
+  # Set it to true if you wish to skip the changelog generation.
+  # This may result in an empty release notes on GitHub/GitLab/Gitea.
+  skip: true
+
+  # Changelog generation implementation to use.
+  #
+  # Valid options are:
+  # - `git`: uses `git log`;
+  # - `github`: uses the compare GitHub API, appending the author login to the changelog.
+  #
+  # Defaults to `git`.
+  use: github
+
+  # Sorts the changelog by the commit's messages.
+  # Could either be asc, desc or empty
+  # Default is empty
+  sort: asc
+
+  filters:
+    # Commit messages matching the regexp listed here will be removed from
+    # the changelog
+    # Default is empty
+    exclude:
+      - '^docs:'
+      - typo
+      - (?i)foo
+```
diff --git a/www/docs/customization/release.md b/www/docs/customization/release.md
index 3ee39822..2aac3969 100644
--- a/www/docs/customization/release.md
+++ b/www/docs/customization/release.md
@@ -182,34 +182,6 @@ ALLOWED_TYPES = application/gzip|application/x-gzip|application/x-gtar|applicati
 !!! warning
     `draft` and `prerelease` are only supported by GitHub and Gitea.
 
-## Customize the changelog
-
-You can customize how the changelog is generated using the
-`changelog` section in the config file:
-
-```yaml
-# .goreleaser.yml
-changelog:
-  # Set it to true if you wish to skip the changelog generation.
-  # This may result in an empty release notes on GitHub/GitLab/Gitea.
-  skip: true
-
-  # Sorts the changelog by the commit's messages.
-  # Could either be asc, desc or empty
-  # Default is empty
-  sort: asc
-
-  filters:
-
-    # Commit messages matching the regexp listed here will be removed from
-    # the changelog
-    # Default is empty
-    exclude:
-      - '^docs:'
-      - typo
-      - (?i)foo
-```
-
 ### Define Previous Tag
 
 GoReleaser uses `git describe` to get the previous tag used for generating the Changelog.
diff --git a/www/mkdocs.yml b/www/mkdocs.yml
index e688a311..41d48221 100644
--- a/www/mkdocs.yml
+++ b/www/mkdocs.yml
@@ -96,6 +96,7 @@ nav:
     - customization/homebrew.md
     - customization/gofish.md
     - customization/scoop.md
+    - customization/changelog.md
     - customization/release.md
     - customization/artifactory.md
     - customization/publishers.md


Test patch:
diff --git a/internal/client/gitea_test.go b/internal/client/gitea_test.go
index a2c74c18..84d18057 100644
--- a/internal/client/gitea_test.go
+++ b/internal/client/gitea_test.go
@@ -601,3 +601,30 @@ func TestGiteaGetDefaultBranchErr(t *testing.T) {
 	_, err = client.GetDefaultBranch(ctx, repo)
 	require.Error(t, err)
 }
+
+func TestGiteaChangelog(t *testing.T) {
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+		if strings.HasSuffix(r.URL.Path, "api/v1/version") {
+			w.WriteHeader(http.StatusOK)
+			fmt.Fprint(w, "{\"version\":\"1.12.0\"}")
+		}
+	}))
+	defer srv.Close()
+
+	ctx := context.New(config.Project{
+		GiteaURLs: config.GiteaURLs{
+			API: srv.URL,
+		},
+	})
+	client, err := NewGitea(ctx, "test-token")
+	require.NoError(t, err)
+	repo := Repo{
+		Owner:  "someone",
+		Name:   "something",
+		Branch: "somebranch",
+	}
+
+	_, err = client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
+	require.EqualError(t, err, ErrNotImplemented.Error())
+}
diff --git a/internal/client/github_test.go b/internal/client/github_test.go
index 531d864d..b79b8c3e 100644
--- a/internal/client/github_test.go
+++ b/internal/client/github_test.go
@@ -2,8 +2,10 @@ package client
 
 import (
 	"fmt"
+	"io"
 	"net/http"
 	"net/http/httptest"
+	"os"
 	"testing"
 	"text/template"
 
@@ -14,6 +16,17 @@ import (
 )
 
 func TestNewGitHubClient(t *testing.T) {
+	t.Run("unauthenticated", func(t *testing.T) {
+		ctx := context.New(config.Project{})
+		repo := Repo{
+			Owner: "goreleaser",
+			Name:  "goreleaser",
+		}
+		b, err := NewUnauthenticatedGitHub().GetDefaultBranch(ctx, repo)
+		require.NoError(t, err)
+		require.Equal(t, "master", b)
+	})
+
 	t.Run("good urls", func(t *testing.T) {
 		githubURL := "https://github.mycompany.com"
 		ctx := context.New(config.Project{
@@ -207,7 +220,7 @@ func TestGithubGetDefaultBranch(t *testing.T) {
 
 		// Assume the request to create a branch was good
 		w.WriteHeader(http.StatusOK)
-		fmt.Fprint(w, "{}")
+		fmt.Fprint(w, `{"default_branch": "main"}`)
 	}))
 	defer srv.Close()
 
@@ -216,6 +229,7 @@ func TestGithubGetDefaultBranch(t *testing.T) {
 			API: srv.URL + "/",
 		},
 	})
+
 	client, err := NewGitHub(ctx, "test-token")
 	require.NoError(t, err)
 	repo := Repo{
@@ -224,8 +238,9 @@ func TestGithubGetDefaultBranch(t *testing.T) {
 		Branch: "somebranch",
 	}
 
-	_, err = client.GetDefaultBranch(ctx, repo)
+	b, err := client.GetDefaultBranch(ctx, repo)
 	require.NoError(t, err)
+	require.Equal(t, "main", b)
 	require.Equal(t, 1, totalRequests)
 }
 
@@ -255,3 +270,35 @@ func TestGithubGetDefaultBranchErr(t *testing.T) {
 	_, err = client.GetDefaultBranch(ctx, repo)
 	require.Error(t, err)
 }
+
+func TestChangelog(t *testing.T) {
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+
+		if r.URL.Path == "/repos/someone/something/compare/v1.0.0...v1.1.0" {
+			r, err := os.Open("testdata/github/compare.json")
+			require.NoError(t, err)
+			_, err = io.Copy(w, r)
+			require.NoError(t, err)
+			return
+		}
+	}))
+	defer srv.Close()
+
+	ctx := context.New(config.Project{
+		GitHubURLs: config.GitHubURLs{
+			API: srv.URL + "/",
+		},
+	})
+	client, err := NewGitHub(ctx, "test-token")
+	require.NoError(t, err)
+	repo := Repo{
+		Owner:  "someone",
+		Name:   "something",
+		Branch: "somebranch",
+	}
+
+	log, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
+	require.NoError(t, err)
+	require.Equal(t, "- 6dcb09b5b57875f334f61aebed695e2e4193db5e: Fix all the bugs (@octocat)", log)
+}
diff --git a/internal/client/gitlab_test.go b/internal/client/gitlab_test.go
index 0b48c372..77f2fe36 100644
--- a/internal/client/gitlab_test.go
+++ b/internal/client/gitlab_test.go
@@ -380,3 +380,26 @@ func TestGitlabGetDefaultBranchErr(t *testing.T) {
 	_, err = client.GetDefaultBranch(ctx, repo)
 	require.Error(t, err)
 }
+
+func TestGitlabChangelog(t *testing.T) {
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+	}))
+	defer srv.Close()
+
+	ctx := context.New(config.Project{
+		GitLabURLs: config.GitLabURLs{
+			API: srv.URL,
+		},
+	})
+	client, err := NewGitLab(ctx, "test-token")
+	require.NoError(t, err)
+	repo := Repo{
+		Owner:  "someone",
+		Name:   "something",
+		Branch: "somebranch",
+	}
+
+	_, err = client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
+	require.EqualError(t, err, ErrNotImplemented.Error())
+}
diff --git a/internal/client/testdata/github/compare.json b/internal/client/testdata/github/compare.json
new file mode 100644
index 00000000..d0747881
--- /dev/null
+++ b/internal/client/testdata/github/compare.json
@@ -0,0 +1,13 @@
+{
+  "commits": [
+    {
+      "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
+      "commit": {
+        "message": "Fix all the bugs\nlalalal"
+      },
+      "author": {
+        "login": "octocat"
+      }
+    }
+  ]
+}
diff --git a/internal/pipe/changelog/changelog_test.go b/internal/pipe/changelog/changelog_test.go
index 996ce15e..de605924 100644
--- a/internal/pipe/changelog/changelog_test.go
+++ b/internal/pipe/changelog/changelog_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/stretchr/testify/require"
 
+	"github.com/goreleaser/goreleaser/internal/client"
 	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
@@ -103,8 +104,11 @@ func TestChangelogPreviousTagEnv(t *testing.T) {
 	testlib.GitCommit(t, "third")
 	testlib.GitTag(t, "v0.0.3")
 	ctx := context.New(config.Project{
-		Dist:      folder,
-		Changelog: config.Changelog{Filters: config.Filters{}},
+		Dist: folder,
+		Changelog: config.Changelog{
+			Use:     "git",
+			Filters: config.Filters{},
+		},
 	})
 	ctx.Git.CurrentTag = "v0.0.3"
 	require.NoError(t, os.Setenv("GORELEASER_PREVIOUS_TAG", "v0.0.1"))
@@ -426,6 +430,59 @@ func TestChangeLogWithoutReleaseFooter(t *testing.T) {
 	require.Equal(t, rune(ctx.ReleaseNotes[len(ctx.ReleaseNotes)-1]), '\n')
 }
 
+func TestGetChangelogGitHub(t *testing.T) {
+	ctx := context.New(config.Project{
+		Changelog: config.Changelog{
+			Use: "github",
+		},
+	})
+
+	l := scmChangeloger{client: client.NewUnauthenticatedGitHub()}
+	log, err := l.Log(ctx, "v0.180.1", "v0.180.2")
+	require.NoError(t, err)
+	require.Equal(t, "- c90f1085f255d0af0b055160bfff5ee40f47af79: fix: do not skip any defaults (#2521) (@caarlos0)", log)
+}
+
+func TestGetChangeloger(t *testing.T) {
+	t.Run("default", func(t *testing.T) {
+		c, err := getChangeloger(context.New(config.Project{}))
+		require.NoError(t, err)
+		require.IsType(t, c, gitChangeloger{})
+	})
+
+	t.Run("git", func(t *testing.T) {
+		c, err := getChangeloger(context.New(config.Project{
+			Changelog: config.Changelog{
+				Use: "git",
+			},
+		}))
+		require.NoError(t, err)
+		require.IsType(t, c, gitChangeloger{})
+	})
+
+	t.Run("gituhb", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			Changelog: config.Changelog{
+				Use: "github",
+			},
+		})
+		ctx.TokenType = context.TokenTypeGitHub
+		c, err := getChangeloger(ctx)
+		require.NoError(t, err)
+		require.IsType(t, c, &scmChangeloger{})
+	})
+
+	t.Run("invalid", func(t *testing.T) {
+		c, err := getChangeloger(context.New(config.Project{
+			Changelog: config.Changelog{
+				Use: "nope",
+			},
+		}))
+		require.EqualError(t, err, `invalid changelog.use: "nope"`)
+		require.Nil(t, c)
+	})
+}
+
 func TestSkip(t *testing.T) {
 	t.Run("skip on snapshot", func(t *testing.T) {
 		ctx := context.New(config.Project{})
diff --git a/internal/pipe/milestone/milestone_test.go b/internal/pipe/milestone/milestone_test.go
index c7acc077..67356fb5 100644
--- a/internal/pipe/milestone/milestone_test.go
+++ b/internal/pipe/milestone/milestone_test.go
@@ -242,3 +242,7 @@ func (c *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.Commi
 func (c *DummyClient) Upload(ctx *context.Context, releaseID string, artifact *artifact.Artifact, file *os.File) error {
 	return nil
 }
+
+func (c *DummyClient) Changelog(ctx *context.Context, repo client.Repo, prev, current string) (string, error) {
+	return "", errors.New("not implemented")
+}