Skip to content

Commit

Permalink
feat(gh): allow multiple hosts (#236)
Browse files Browse the repository at this point in the history
  • Loading branch information
nobe4 authored Jan 8, 2025
1 parent 755572e commit ed5aec2
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 47 deletions.
74 changes: 73 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,79 @@ section](#automatic-fetching).
The other commands are used to interact with the local cache. It uses `gh
api`-equivalent to modify the notifications on GitHub's side.

# Configure
# Authentication

`gh-not` uses `gh`'s built-in authentication, meaning that if `gh` works,
`gh-not` should work.
If you want to use a specific PAT, you can do so with the environment variable
`GH_TOKEN`. The PAT requires the scopes: `notifications`, and `repo`.
E.g.:
```bash
# gh's authentication for github.com
gh-not ...

# Using a PAT for github.com.
GH_TOKEN=XXX gh-not ...
```

`gh-not` also respects `GH_HOST` and `GH_ENTERPRISE_TOKEN` if you need to use a
non-`github.com` host.

E.g.:

```bash
# gh's authentication for ghe.io
GH_HOST=ghe.io gh-not ...

# Using a PAT for ghe.io
GH_HOST=ghe.io GH_ENTERPRISE_TOKEN=XXX gh-not ...
```

See the [`gh` environment documentation](https://cli.github.com/manual/gh_help_environment).

> [!IMPORTANT]
> If you plan on using `gh-not` with more than one host, you might want to
> create a separate cache for it. See [cache](#cache).
# Configuration

## Cache

The cache is where the notifications are locally stored.

It contains 2 fields:

- `path`: the path to the JSON file.

- `TTLInHours`: how long before the cache needs to be refreshed.

If you use multiple hosts, you might want to have separate configurations and
caches to prevent overrides. Create one config file per host you want to use and
point the cache's path to a _different file_.

E.g.

- `config.github.yaml`
```yaml
cache:
path: cache.github.json
...
```

Use it with `gh-not --config config.github.yaml`.

- `config.gheio.yaml `
```yaml
cache:
path: cache.gheio.json
...
```
Use it with `gh-not --config config.gheio.yaml`.

## Rules

The configuration file contains the rules to apply to the notifications. Each
rule contains three fields:
Expand Down
2 changes: 1 addition & 1 deletion internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import (
)

type Requestor interface {
Request(method string, url string, body io.Reader) (*http.Response, error)
Request(method string, path string, body io.Reader) (*http.Response, error)
}
4 changes: 1 addition & 3 deletions internal/api/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"io"
"net/http"
"os"

"github.com/nobe4/gh-not/internal/gh"
)

type API struct {
Expand All @@ -19,7 +17,7 @@ func New(path string) *API {
}

func (a *API) Request(verb string, url string, _ io.Reader) (*http.Response, error) {
if verb == "GET" && url == gh.DefaultURL.String() {
if verb == "GET" {
return a.readFile()
}

Expand Down
2 changes: 2 additions & 0 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ func NewFileCache(path string) *FileCache {
}

func (c *FileCache) Read(out any) error {
slog.Debug("Reading cache", "path", c.path)

content, err := os.ReadFile(c.path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
Expand Down
25 changes: 9 additions & 16 deletions internal/gh/gh.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,20 @@ import (
"github.com/nobe4/gh-not/internal/notifications"
)

var (
linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`)

//nolint:gochecknoglobals // This is used as a default in a couple of places.
DefaultURL = url.URL{
Scheme: "https",
Host: "api.github.com",
Path: "/notifications",
}
)
var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`)

type Client struct {
API api.Requestor
cache cache.RefreshReadWriter
maxRetry int
maxPage int
url string
path string
}

func NewClient(api api.Requestor, cache cache.RefreshReadWriter, config config.Endpoint) *Client {
url := DefaultURL
path := url.URL{Path: "notifications"}

query := url.Query()
query := path.Query()
if config.All {
query.Set("all", "true")
}
Expand All @@ -50,14 +41,14 @@ func NewClient(api api.Requestor, cache cache.RefreshReadWriter, config config.E
query.Set("per_page", strconv.Itoa(config.PerPage))
}

url.RawQuery = query.Encode()
path.RawQuery = query.Encode()

return &Client{
API: api,
cache: cache,
maxRetry: config.MaxRetry,
maxPage: config.MaxPage,
url: url.String(),
path: path.String(),
}
}

Expand Down Expand Up @@ -104,6 +95,8 @@ func parse(r *http.Response) ([]*notifications.Notification, string, error) {
return n, nextPageLink(&r.Header), nil
}

// TODO: this should only return the path, as the full URL is not expected in
// the Request.
func nextPageLink(h *http.Header) string {
for _, m := range linkRE.FindAllStringSubmatch(h.Get("Link"), -1) {
if len(m) > 2 && m[2] == "next" {
Expand Down Expand Up @@ -152,7 +145,7 @@ func (c *Client) paginate() (notifications.Notifications, error) {
var err error

pageLeft := c.maxPage
endpoint := c.url
endpoint := c.path

for endpoint != "" && pageLeft > 0 {
slog.Info("API REST request", "endpoint", endpoint, "page_left", pageLeft)
Expand Down
46 changes: 23 additions & 23 deletions internal/gh/gh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func mockClient(c []mock.Call) (*Client, *mock.Mock) {

return &Client{
API: mock,
url: endpoint,
path: endpoint,
maxRetry: 100,
maxPage: 100,
}, mock
Expand Down Expand Up @@ -160,34 +160,34 @@ func TestIsRetryable(t *testing.T) {

func TestNewClient(t *testing.T) {
tests := []struct {
name string
all bool
perPage int
wantURL string
name string
all bool
perPage int
wantPath string
}{
{
name: "default",
all: false,
perPage: 0,
wantURL: "https://api.github.com/notifications",
name: "default",
all: false,
perPage: 0,
wantPath: "notifications",
},
{
name: "all",
all: true,
perPage: 0,
wantURL: "https://api.github.com/notifications?all=true",
name: "all",
all: true,
perPage: 0,
wantPath: "notifications?all=true",
},
{
name: "10 per page",
all: false,
perPage: 10,
wantURL: "https://api.github.com/notifications?per_page=10",
name: "10 per page",
all: false,
perPage: 10,
wantPath: "notifications?per_page=10",
},
{
name: "all and 10 per page",
all: true,
perPage: 10,
wantURL: "https://api.github.com/notifications?all=true&per_page=10",
name: "all and 10 per page",
all: true,
perPage: 10,
wantPath: "notifications?all=true&per_page=10",
},
}

Expand All @@ -202,8 +202,8 @@ func TestNewClient(t *testing.T) {
client := NewClient(nil, nil, *config)

// only testing the result URL, the rest is stored verbatim.
if client.url != test.wantURL {
t.Errorf("expected %s, got %s", test.wantURL, client.url)
if client.path != test.wantPath {
t.Errorf("expected %s, got %s", test.wantPath, client.path)
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion test/integration/000/calls.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"verb": "GET",
"endpoint": "https://api.github.com/notifications?all=true",
"endpoint": "notifications?all=true",
"response": {
"status_code": 200,
"body": []
Expand Down
2 changes: 1 addition & 1 deletion test/integration/001/calls.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"verb": "GET",
"endpoint": "https://api.github.com/notifications?all=true",
"endpoint": "notifications?all=true",
"response": {
"status_code": 200,
"headers": {
Expand Down
2 changes: 1 addition & 1 deletion test/integration/002/calls.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"verb": "GET",
"endpoint": "https://api.github.com/notifications?all=true",
"endpoint": "notifications?all=true",
"response": {
"status_code": 200,
"headers": {},
Expand Down

0 comments on commit ed5aec2

Please sign in to comment.