This commit is contained in:
Massaki Archambault 2024-08-14 16:18:16 +00:00 committed by GitHub
commit 3d8ad75476
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 2304 additions and 1477 deletions

11
CHANGELOG.md Normal file
View File

@ -0,0 +1,11 @@
# v1.0.0
* Added support for Github forge
* Added support for Gitea/Forgejo forge
* **BREAKING** Renamed project from `gitlabfs` to `gitforgefs`
* **BREAKING** Added mandatory configuration *fs.forge* (no default)
* **BREAKING** Changed Gitlab user configuration to use user names instead of user ids
* Handle archived repo as hidden files by default
* Improved support for old version of git
* Fixed various race conditions
* Fixed inode collision issue

View File

@ -1,6 +0,0 @@
FROM alpine
COPY ./bin/gitlabfs /usr/bin/gitlabfs
ENTRYPOINT ["gitlabfs"]

View File

@ -1,4 +1,4 @@
PROGRAM := gitlabfs
PROGRAM := gitforgefs
TARGET_DIR := ./bin
VERSION := $(shell git describe --tags --always)

145
README.md
View File

@ -1,119 +1,94 @@
# gitlabfs
# gitforgefs
`gitlabfs` allows you to mount and navigate Gitlab groups and user personal projects as a FUSE filesystem with every groups represented as a folder and every projects represented as a symlink pointing on a local clone of the project.
*Formerly gitlabfs*
Partial output of `tree`, truncated and with a max of 4 levels:
`gitforgefs` allows you to mount and navigate git forges (Github, Gitlab, Gitea, etc.) as a [FUSE](https://github.com/libfuse/libfuse) filesystem with every groups, organization, and users represented as a folder and every repositories represented as a symlink pointing on a local clone of the project. This is helpful to automate the organization of your local clones.
To help illustrate, this is the output of `tree` in a filesystem exposing all the repositories of a github user.
```
$ tree -L 4
$ tree
.
├── projects
│   └── gitlab-org
│   ├── 5-minute-production-app
│   │   ├── deploy-template -> /home/marchambault/.local/share/gitlabfs/gitlab.com/22487050
│   │   ├── examples
│   │   ├── hipio -> /home/marchambault/.local/share/gitlabfs/gitlab.com/23344605
│   │   ├── sandbox
│   │   └── static-template -> /home/marchambault/.local/share/gitlabfs/gitlab.com/23203100
│   ├── allocations -> /home/marchambault/.local/share/gitlabfs/gitlab.com/684698
│   ├── apilab -> /home/marchambault/.local/share/gitlabfs/gitlab.com/2383700
│   ├── architecture
│   │   └── tasks -> /home/marchambault/.local/share/gitlabfs/gitlab.com/22351703
│   ├── async-retrospectives -> /home/marchambault/.local/share/gitlabfs/gitlab.com/7937396
│   ├── auto-deploy-app -> /home/marchambault/.local/share/gitlabfs/gitlab.com/6329546
│   ├── auto-deploy-helm -> /home/marchambault/.local/share/gitlabfs/gitlab.com/3651684
│   ├── auto-devops-v12-10 -> /home/marchambault/.local/share/gitlabfs/gitlab.com/18629149
│   ├── backstage-changelog -> /home/marchambault/.local/share/gitlabfs/gitlab.com/7602162
│   ├── blob-examples -> /home/marchambault/.local/share/gitlabfs/gitlab.com/3094319
│   ├── build
│   │   ├── CNG -> /home/marchambault/.local/share/gitlabfs/gitlab.com/4359271
│   │   ├── CNG-mirror -> /home/marchambault/.local/share/gitlabfs/gitlab.com/7682093
│   │   ├── dsop-scripts -> /home/marchambault/.local/share/gitlabfs/gitlab.com/19310217
│   │   ├── omnibus-mirror
│   │   └── tr-test-dependency-proxy -> /home/marchambault/.local/share/gitlabfs/gitlab.com/20085049
│   ├── charts
│   │   ├── apparmor -> /home/marchambault/.local/share/gitlabfs/gitlab.com/18991900
│   │   ├── auto-deploy-app -> /home/marchambault/.local/share/gitlabfs/gitlab.com/11915984
│   │   ├── components
│   │   ├── consul -> /home/marchambault/.local/share/gitlabfs/gitlab.com/18663049
│   │   ├── deploy-image-helm-base -> /home/marchambault/.local/share/gitlabfs/gitlab.com/7453181
│   │   ├── elastic-stack -> /home/marchambault/.local/share/gitlabfs/gitlab.com/18439881
│   │   ├── fluentd-elasticsearch -> /home/marchambault/.local/share/gitlabfs/gitlab.com/17253921
│   │   ├── gitlab -> /home/marchambault/.local/share/gitlabfs/gitlab.com/3828396
│   │   ├── gitlab-runner -> /home/marchambault/.local/share/gitlabfs/gitlab.com/6329679
│   │   ├── knative -> /home/marchambault/.local/share/gitlabfs/gitlab.com/16590122
│   │   └── plantuml -> /home/marchambault/.local/share/gitlabfs/gitlab.com/14372596
│   [...]
└── users
└── badjware
└── test_project -> /home/marchambault/.local/share/gitlabfs/gitlab.com/23370783
└── badjware
├── aws-cloud-gaming -> /home/marchambault/.local/share/gitforgefs/github.com/257091317
├── certbot -> /home/marchambault/.local/share/gitforgefs/github.com/122014287
├── certbot-dns-cpanel -> /home/marchambault/.local/share/gitforgefs/github.com/131224547
├── certbot-dns-ispconfig -> /home/marchambault/.local/share/gitforgefs/github.com/227005814
├── CommonLibVR -> /home/marchambault/.local/share/gitforgefs/github.com/832968971
├── community -> /home/marchambault/.local/share/gitforgefs/github.com/424689724
├── docker-postal -> /home/marchambault/.local/share/gitforgefs/github.com/132605640
├── dotfiles -> /home/marchambault/.local/share/gitforgefs/github.com/192993195
├── ecommerce-exporter -> /home/marchambault/.local/share/gitforgefs/github.com/562583906
├── FightClub5eXML -> /home/marchambault/.local/share/gitforgefs/github.com/246177579
├── gitforgefs -> /home/marchambault/.local/share/gitforgefs/github.com/324617595
├── kustomize-plugins -> /home/marchambault/.local/share/gitforgefs/github.com/263480122
├── librechat-mistral -> /home/marchambault/.local/share/gitforgefs/github.com/753193720
├── PapyrusExtenderSSE -> /home/marchambault/.local/share/gitforgefs/github.com/832969611
├── Parsec-Cloud-Preparation-Tool -> /home/marchambault/.local/share/gitforgefs/github.com/258052650
├── po3-Tweaks -> /home/marchambault/.local/share/gitforgefs/github.com/832969112
├── prometheus-ecs-discovery -> /home/marchambault/.local/share/gitforgefs/github.com/187891900
├── simplefuse -> /home/marchambault/.local/share/gitforgefs/github.com/111226611
├── tmux-continuum -> /home/marchambault/.local/share/gitforgefs/github.com/160746043
├── ttyd -> /home/marchambault/.local/share/gitforgefs/github.com/132514236
├── usb-libvirt-hotplug -> /home/marchambault/.local/share/gitforgefs/github.com/128696299
└── vfio-win10 -> /home/marchambault/.local/share/gitforgefs/github.com/388475049
696 directories, 0 files
24 directories, 0 files
```
## Supported forges
Currently, the following forges are supported:
| Forge | Name in configuration | API token permissions, if using an API key |
| ------------------------------- | --------------------- | ------------------------------------------------------ |
| [Gitlab](https://gitlab.com) | `gitlab` | `read_user`, `read_api` |
| [Github](https://github.com) | `github` | `repo` |
| [Gitea](https://gitea.com) | `gitea` | organization: `read`, repository: `read`, user: `read` |
| [Forgejo](https://forgejo.org/) | `gitea` | organization: `read`, repository: `read`, user: `read` |
Merge requests to add support to other forges are welcome.
## Install
Install [go](https://golang.org/) and run
``` sh
go install github.com/badjware/gitlabfs@latest
go install github.com/badjware/gitforgefs
```
The executable will be in `$GOPATH/bin/gitlabfs` or `~/go/bin/gitlabfs` by default. For convenience, copy `gitlabfs` somewhere suitable or add `~/go/bin` in your `PATH`.
The executable will be in `$GOPATH/bin/gitforgefs` or `~/go/bin/gitforgefs` by default. For convenience, add `~/go/bin` in your `$PATH` if not done already.
## Usage
Download the [example configuration file](./config.example.yaml) and edit the default configuration to suit your needs.
### Getting an API token
To generate an api token, log into your Gitlab instance, and go in your user settings > Access Token. Create a personal access token with the following permissions at the minimum:
* `read_user`
* `read_api`
### Getting the group ids
The group id can be seen just under the name of the group in Gitlab.
![group-id](media/group_id.jpg)
### Getting the user ids
Log into gitlab and go to https://gitlab.com/api/v4/users?username=USERNAME where `USERNAME` is the username of the user you wish to know the id of. The json response will contain the user id.
See https://forum.gitlab.com/t/where-is-my-user-id-in-gitlab-com/7912
### Mounting the filesystem
You can mount the filesystem with the following command:
Then, you can run gitforgefs as follows:
``` sh
~/go/bin/gitlabfs -config /path/to/your/config.yaml /path/to/mountpoint
gitforgefs -config config.yaml /path/to/mountpoint
```
Once the filesystem is mounted, you can `cd` into it and navigate it like any other filesystem. The first time `ls` is run the list of groups and projects is fetched from Gitlab. This operation can take a few seconds and the command will appear frozen until it's completed. Subsequent `ls` will fetch from the cache and should be much faster.
If `on_clone` is set to `init` or `no-checkout`, the locally cloned project will appear empty. Simply running `git pull` manually in the project folder will sync it up with Gitlab.
### Unmounting the filesystem
To stop the filesystem, use the command `umount /path/to/mountpoint` to cleanly unmount the filesystem.
If `gitlabfs` is not cleanly stopped, you might start seeing the error "transport endpoint is not connected" when trying to access the mountpoint, even preventing from mounting back the filesystem on the same mountpoint. To fix this, use `umount` as root user, eg: `sudo umount /path/to/mountpoint`.
Stopping gitforgefs will unmount the filesystem. In the event the mountpoint is stuck in a bad state (eg: due to receiving a SIGKILL), you may need to manually cleanup using `umount`:
``` sh
sudo umount /path/to/mountpoint
```
### Running automatically on user login
See [./contrib/systemd](contrib/systemd) for instructions on how to configure a systemd service to automatically run gitlabfs on user login.
See [./contrib/systemd](contrib/systemd) for instructions on how to configure a systemd service to automatically run gitforgefs on user login.
## Caching
To reduce the number of calls to the Gitlab api and improve the responsiveness of the filesystem, `gitlabfs` will cache the content of the group in memory. If a group or project is renamed, created or deleted from Gitlab, these change will not appear in the filesystem. To force `gitlabfs` to refresh its cache, use `touch .refresh` in the folder to refresh to force `gitlabfs` to query Gitlab for the list of groups and projects again.
### Filesystem cache
While the filesystem lives in memory, the git repositories that are cloned are saved on disk. By default, they are saved in `$XDG_DATA_HOME/gitlabfs` or `$HOME/.local/share/gitlabfs`, if `$XDG_DATA_HOME` is unset. `gitlabfs` symlink to the local clone of that repo. The local clone is unaffected by project rename or archive/unarchive in Gitlab and a given project will always point to the correct local folder.
To reduce the number of calls to the APIs and improve the responsiveness of the filesystem, gitforgefs will cache the content of the forge in memory. If a group or project is renamed, created or deleted from the forge, these change will not appear in the filesystem immediately. To force gitforgefs to refresh its cache, use `touch .refresh` in the folder to signal gitforgefs to refresh this folder.
## Known issues / Future improvements
* Cache persists forever until a manual refresh is requested. Some way to automatically refresh would be nice.
* The filesystem is currently read-only. Implementing `mkdir` to create groups, `ln` or `touch` to create projects, etc. would be nice.
* Code need some cleanup and could maybe be optimized here and there.
### Local repository cache
## Building
While the filesystem lives in memory, the git repositories that are cloned are saved on disk. By default, they are saved in `$XDG_DATA_HOME/gitforgefs` or `$HOME/.local/share/gitforgefs`, if `$XDG_DATA_HOME` is unset. `gitforgefs` symlink to the local clone of that repo. The local clone is unaffected by project rename or archive/unarchive in Gitlab and a given project will always point to the correct local folder.
## Future improvements
* Cache persists forever until a manual refresh is requested. Some way to automatically refresh after a timeout would be nice.
## Building from the repo
Simply use `make` to create the executable. The executable will be in `bin/`.

View File

@ -6,6 +6,10 @@ fs:
# See mount.fuse(8) for the full list of options.
#mountoptions: nodev,nosuid
# The git forge to use as the backend.
# Must be one of "gitlab", "github", or "gitea"
forge: gitlab
gitlab:
# The gitlab url.
url: https://gitlab.com
@ -14,30 +18,96 @@ gitlab:
# Default to anonymous (only public projects will be visible).
#token:
# Must be set to either "http" or "ssh".
# The protocol to configure the git remote on.
# "http" may not work on private projects unless a credential manager is configured
# If possible, prefer "ssh" over "http"
pull_method: http
# A list of the group ids to expose their projects in the filesystem.
group_ids:
- 9970 # gitlab-org
# A list of the user ids to expose their personal projects in the filesystem.
user_ids: []
# A list of the name of the user to expose their repositories un the filesystem
user_names: []
# Set how archived projects are handled.
# If set to "show", it will add them to the filesystem and treat them like any other project
# If set to "hide", it will add them to the filesystem, but prefix the symlink with a "."
# If set to "ignore", it will make them absent from the filesystem
# Default to "hide"
archived_project_handling: hide
# If set to true, the user the api token belongs to will automatically be added to the list of users exposed by the filesystem.
include_current_user: true
github:
# The github api token
# Default to anonymous (only public repositories will be visible)
#token:
# Must be set to either "http" or "ssh".
# The protocol to configure the git remote on.
# "http" may not work on private repositories unless a credential manager is configured
# If possible, prefer "ssh" over "http"
pull_method: http
# A list of the name of the organizations to expose in the filesystem
org_names: []
# A list of the name of the user to expose their repositories un the filesystem
user_names: []
# Set how archived repositories are handled.
# If set to "show", it will add them to the filesystem and treat them like any other repository
# If set to "hide", it will add them to the filesystem, but prefix the symlink with a "."
# If set to "ignore", it will make them absent from the filesystem
# Default to "hide"
archived_repo_handling: hide
# If set to true, the personal repositories and the repositories of the organizations the user the api token belongs to
# will be automatically be added to the list of users exposed by the filesystem.
include_current_user: true
gitea:
# The gitea url.
url: https://gitea.com
# The gitlab api token
# Default to anonymous (only public repositories will be visible)
#token:
# Must be set to either "http" or "ssh".
# The protocol to configure the git remote on.
# "http" may not work on private repositories unless a credential manager is configured
# If possible, prefer "ssh" over "http"
pull_method: http
# A list of the name of the organizations to expose in the filesystem
org_names: []
# A list of the name of the user to expose their repositories un the filesystem
user_names: []
# Set how archived repositories are handled.
# If set to "show", it will add them to the filesystem and treat them like any other repository
# If set to "hide", it will add them to the filesystem, but prefix the symlink with a "."
# If set to "ignore", it will make them absent from the filesystem
# Default to "hide"
archived_repo_handling: hide
# If set to true, the personal repositories and the repositories of the organizations the user the api token belongs to
# will be automatically be added to the list of users exposed by the filesystem.
include_current_user: true
git:
# Path to the local repository cache. Repositories in the filesystem will symlink to a folder in this path.
# Default to $XDG_DATA_HOME/gitlabfs, or $HOME/.local/share/gitlabfs if the environment variable $XDG_DATA_HOME is unset.
# Default to $XDG_DATA_HOME/gitforgefs, or $HOME/.local/share/gitforgefs if the environment variable $XDG_DATA_HOME is unset.
#clone_location:
# The name of the remote in the local clone.
remote: origin
# Must be set to either "http" or "ssh".
# The protocol to configure the git remote on.
# "http" may not work on private repos unless a credential manager is configured
# If possible, prefer "ssh" over "http"
pull_method: http
# Must be set to either "init", or "clone".
# If set to "init", the local copy will be initialized with `git init` and the remote is configured manually. The git server is nerver queried. (fast)
# If set to "clone", the local copy will be initialized with `git clone`. (slow)

45
config/config.test.yaml Normal file
View File

@ -0,0 +1,45 @@
fs:
mountpoint: /tmp/gitforgefs/test/mnt/gitlab
mountoptions: nodev
forge: gitlab
gitlab:
url: https://example.com
token: "12345"
pull_method: ssh
group_ids:
- 123
user_names:
- test-user
archived_project_handling: hide
include_current_user: true
github:
token: "12345"
pull_method: http
org_names:
- test-org
user_names:
- test-user
archived_repo_handling: hide
include_current_user: true
gitea:
url: https://example.com
token: "12345"
pull_method: http
org_names:
- test-org
user_names:
- test-user
archived_repo_handling: hide
include_current_user: true
git:
clone_location: /tmp/gitforgefs/test/cache/gitlab
remote: origin
on_clone: clone
auto_pull: false
depth: 0
queue_size: 100
worker_count: 1

190
config/loader.go Normal file
View File

@ -0,0 +1,190 @@
package config
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v2"
)
const (
ForgeGitlab = "gitlab"
ForgeGithub = "github"
ForgeGitea = "gitea"
PullMethodHTTP = "http"
PullMethodSSH = "ssh"
ArchivedProjectShow = "show"
ArchivedProjectHide = "hide"
ArchivedProjectIgnore = "ignore"
)
type (
Config struct {
FS FSConfig `yaml:"fs,omitempty"`
Gitlab GitlabClientConfig `yaml:"gitlab,omitempty"`
Github GithubClientConfig `yaml:"github,omitempty"`
Gitea GiteaClientConfig `yaml:"gitea,omitempty"`
Git GitClientConfig `yaml:"git,omitempty"`
}
FSConfig struct {
Mountpoint string `yaml:"mountpoint,omitempty"`
MountOptions string `yaml:"mountoptions,omitempty"`
Forge string `yaml:"forge,omitempty"`
}
GitlabClientConfig struct {
URL string `yaml:"url,omitempty"`
Token string `yaml:"token,omitempty"`
GroupIDs []int `yaml:"group_ids,omitempty"`
UserNames []string `yaml:"user_names,omitempty"`
ArchivedProjectHandling string `yaml:"archived_project_handling,omitempty"`
IncludeCurrentUser bool `yaml:"include_current_user,omitempty"`
PullMethod string `yaml:"pull_method,omitempty"`
}
GithubClientConfig struct {
Token string `yaml:"token,omitempty"`
OrgNames []string `yaml:"org_names,omitempty"`
UserNames []string `yaml:"user_names,omitempty"`
ArchivedRepoHandling string `yaml:"archived_repo_handling,omitempty"`
IncludeCurrentUser bool `yaml:"include_current_user,omitempty"`
PullMethod string `yaml:"pull_method,omitempty"`
}
GiteaClientConfig struct {
URL string `yaml:"url,omitempty"`
Token string `yaml:"token,omitempty"`
OrgNames []string `yaml:"org_names,omitempty"`
UserNames []string `yaml:"user_names,omitempty"`
ArchivedRepoHandling string `yaml:"archived_repo_handling,omitempty"`
IncludeCurrentUser bool `yaml:"include_current_user,omitempty"`
PullMethod string `yaml:"pull_method,omitempty"`
}
GitClientConfig struct {
CloneLocation string `yaml:"clone_location,omitempty"`
Remote string `yaml:"remote,omitempty"`
OnClone string `yaml:"on_clone,omitempty"`
AutoPull bool `yaml:"auto_pull,omitempty"`
Depth int `yaml:"depth,omitempty"`
QueueSize int `yaml:"queue_size,omitempty"`
QueueWorkerCount int `yaml:"worker_count,omitempty"`
}
)
func LoadConfig(configPath string) (*Config, error) {
// defaults
dataHome := os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
dataHome = filepath.Join(os.Getenv("HOME"), ".local/share")
}
defaultCloneLocation := filepath.Join(dataHome, "gitforgefs")
config := &Config{
FS: FSConfig{
Mountpoint: "",
MountOptions: "nodev,nosuid",
Forge: "",
},
Gitlab: GitlabClientConfig{
URL: "https://gitlab.com",
Token: "",
PullMethod: "http",
GroupIDs: []int{9970},
UserNames: []string{},
ArchivedProjectHandling: "hide",
IncludeCurrentUser: true,
},
Github: GithubClientConfig{
Token: "",
PullMethod: "http",
OrgNames: []string{},
UserNames: []string{},
ArchivedRepoHandling: "hide",
IncludeCurrentUser: true,
},
Git: GitClientConfig{
CloneLocation: defaultCloneLocation,
Remote: "origin",
OnClone: "init",
AutoPull: false,
Depth: 0,
QueueSize: 200,
QueueWorkerCount: 5,
},
}
f, err := os.Open(configPath)
if err != nil {
return nil, fmt.Errorf("failed to open config file: %v", err)
}
defer f.Close()
d := yaml.NewDecoder(f)
if err := d.Decode(config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %v", err)
}
// validate forge is set
if config.FS.Forge != ForgeGithub && config.FS.Forge != ForgeGitlab && config.FS.Forge != ForgeGitea {
return nil, fmt.Errorf("fs.forge must be either \"%v\", \"%v\", or \"%v\"", ForgeGitlab, ForgeGithub, ForgeGitea)
}
return config, nil
}
func MakeGitlabConfig(config *Config) (*GitlabClientConfig, error) {
// parse pull_method
if config.Gitlab.PullMethod != PullMethodHTTP && config.Gitlab.PullMethod != PullMethodSSH {
return nil, fmt.Errorf("gitlab.pull_method must be either \"%v\" or \"%v\"", PullMethodHTTP, PullMethodSSH)
}
// parse archive_handing
if config.Gitlab.ArchivedProjectHandling != ArchivedProjectShow && config.Gitlab.ArchivedProjectHandling != ArchivedProjectHide && config.Gitlab.ArchivedProjectHandling != ArchivedProjectIgnore {
return nil, fmt.Errorf("gitlab.archived_project_handling must be either \"%v\", \"%v\" or \"%v\"", ArchivedProjectShow, ArchivedProjectHide, ArchivedProjectIgnore)
}
return &config.Gitlab, nil
}
func MakeGithubConfig(config *Config) (*GithubClientConfig, error) {
// parse pull_method
if config.Github.PullMethod != PullMethodHTTP && config.Github.PullMethod != PullMethodSSH {
return nil, fmt.Errorf("github.pull_method must be either \"%v\" or \"%v\"", PullMethodHTTP, PullMethodSSH)
}
// parse archive_handing
if config.Github.ArchivedRepoHandling != ArchivedProjectShow && config.Github.ArchivedRepoHandling != ArchivedProjectHide && config.Github.ArchivedRepoHandling != ArchivedProjectIgnore {
return nil, fmt.Errorf("github.archived_repo_handling must be either \"%v\", \"%v\" or \"%v\"", ArchivedProjectShow, ArchivedProjectHide, ArchivedProjectIgnore)
}
return &config.Github, nil
}
func MakeGiteaConfig(config *Config) (*GiteaClientConfig, error) {
// parse pull_method
if config.Gitea.PullMethod != PullMethodHTTP && config.Gitea.PullMethod != PullMethodSSH {
return nil, fmt.Errorf("gitea.pull_method must be either \"%v\" or \"%v\"", PullMethodHTTP, PullMethodSSH)
}
// parse archive_handing
if config.Gitea.ArchivedRepoHandling != ArchivedProjectShow && config.Gitea.ArchivedRepoHandling != ArchivedProjectHide && config.Gitea.ArchivedRepoHandling != ArchivedProjectIgnore {
return nil, fmt.Errorf("gitea.archived_repo_handling must be either \"%v\", \"%v\" or \"%v\"", ArchivedProjectShow, ArchivedProjectHide, ArchivedProjectIgnore)
}
return &config.Gitea, nil
}
func MakeGitConfig(config *Config) (*GitClientConfig, error) {
// parse on_clone
if config.Git.OnClone != "init" && config.Git.OnClone != "clone" {
return nil, fmt.Errorf("git.on_clone must be either \"init\" or \"clone\"")
}
return &config.Git, nil
}

210
config/loader_test.go Normal file
View File

@ -0,0 +1,210 @@
package config_test
import (
"reflect"
"testing"
"github.com/badjware/gitforgefs/config"
)
func TestLoadConfig(t *testing.T) {
tests := map[string]struct {
input string
expected *config.Config
}{
"LoadConfig": {
input: "config.test.yaml",
expected: &config.Config{
FS: config.FSConfig{
Mountpoint: "/tmp/gitforgefs/test/mnt/gitlab",
MountOptions: "nodev",
Forge: "gitlab",
},
Gitlab: config.GitlabClientConfig{
URL: "https://example.com",
Token: "12345",
PullMethod: "ssh",
GroupIDs: []int{123},
UserNames: []int{456},
ArchivedProjectHandling: "hide",
IncludeCurrentUser: true,
},
Github: config.GithubClientConfig{
Token: "12345",
PullMethod: "http",
OrgNames: []string{"test-org"},
UserNames: []string{"test-user"},
ArchivedRepoHandling: "hide",
IncludeCurrentUser: true,
},
Gitea: config.GiteaClientConfig{
URL: "https://example.com",
Token: "12345",
PullMethod: "http",
OrgNames: []string{"test-org"},
UserNames: []string{"test-user"},
ArchivedRepoHandling: "hide",
IncludeCurrentUser: true,
},
Git: config.GitClientConfig{
CloneLocation: "/tmp/gitforgefs/test/cache/gitlab",
Remote: "origin",
OnClone: "clone",
AutoPull: false,
Depth: 0,
QueueSize: 100,
QueueWorkerCount: 1,
}},
},
}
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
t.Parallel()
got, err := config.LoadConfig(test.input)
expected := test.expected
if !reflect.DeepEqual(got, expected) {
t.Fatalf("LoadConfig(%v) returned %v; expected %v; error: %v", test.input, got, expected, err)
}
})
}
}
func TestMakeGitConfig(t *testing.T) {
tests := map[string]struct {
input *config.Config
expected *config.GitClientConfig
}{
"ValidConfig": {
input: &config.Config{
FS: config.FSConfig{
Forge: "gitlab",
},
Git: config.GitClientConfig{
CloneLocation: "/tmp",
Remote: "origin",
OnClone: "init",
AutoPull: false,
Depth: 0,
QueueSize: 200,
QueueWorkerCount: 5,
},
},
expected: &config.GitClientConfig{
CloneLocation: "/tmp",
Remote: "origin",
OnClone: "init",
AutoPull: false,
Depth: 0,
QueueSize: 200,
QueueWorkerCount: 5,
},
},
"InvalidOnClone": {
input: &config.Config{
FS: config.FSConfig{
Forge: "gitlab",
},
Git: config.GitClientConfig{
CloneLocation: "/tmp",
Remote: "origin",
OnClone: "invalid",
AutoPull: false,
Depth: 0,
QueueSize: 200,
QueueWorkerCount: 5,
},
},
expected: nil,
},
}
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
t.Parallel()
got, err := config.MakeGitConfig(test.input)
expected := test.expected
if !reflect.DeepEqual(got, expected) {
t.Fatalf("MakeGitConfig(%v) returned %v; expected %v; error %v", test.input, got, expected, err)
}
})
}
}
func TestMakeGitlabConfig(t *testing.T) {
tests := map[string]struct {
input *config.Config
expected *config.GitlabClientConfig
}{
"ValidConfig": {
input: &config.Config{
FS: config.FSConfig{
Forge: "gitlab",
},
Gitlab: config.GitlabClientConfig{
URL: "https://gitlab.com",
PullMethod: "http",
Token: "",
GroupIDs: []int{9970},
UserNames: []int{},
ArchivedProjectHandling: "hide",
IncludeCurrentUser: true,
},
},
expected: &config.GitlabClientConfig{
URL: "https://gitlab.com",
PullMethod: "http",
Token: "",
GroupIDs: []int{9970},
UserNames: []int{},
ArchivedProjectHandling: "hide",
IncludeCurrentUser: true,
},
},
"InvalidPullMethod": {
input: &config.Config{
FS: config.FSConfig{
Forge: "gitlab",
},
Gitlab: config.GitlabClientConfig{
URL: "https://gitlab.com",
PullMethod: "invalid",
Token: "",
GroupIDs: []int{9970},
UserNames: []int{},
ArchivedProjectHandling: "hide",
IncludeCurrentUser: true,
},
},
expected: nil,
},
"InvalidArchiveHandling": {
input: &config.Config{
Gitlab: config.GitlabClientConfig{
URL: "https://gitlab.com",
PullMethod: "http",
Token: "",
GroupIDs: []int{9970},
UserNames: []int{},
IncludeCurrentUser: true,
ArchivedProjectHandling: "invalid",
},
},
expected: nil,
},
}
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
t.Parallel()
got, err := config.MakeGitlabConfig(test.input)
expected := test.expected
if !reflect.DeepEqual(got, expected) {
t.Fatalf("MakeGitlabConfig(%v) returned %v; expected %v; error: %v", test.input, got, expected, err)
}
})
}
}

View File

@ -1,12 +1,15 @@
This unit file allows you to automatically start gitlabfs as a systemd unit.
This unit file allows you to automatically start gitforgefs as a systemd unit.
## Install
1. Install gitlabfs using `go get`
2. Run `which gitlabfs` to verify that gitlabfs is present in your PATH. if the command fail, you may need to add **$HOME/go/bin** to your PATH.
3. Copy **gitlabfs@.service** into **~/.config/systemd/user**. Create the folder if it does not exists.
4. Reload systemd: `systemctl --user daemon-reload`
## Setup
1. Install gitforgefs
2. Copy **gitforgefs@.service** into **$HOME/.config/systemd/user**. Create the folder if it does not exists.
``` sh
mkdir -p $HOME/.config/systemd/user
curl -o $HOME/.config/systemd/user/gitforgefs@.service https://raw.githubusercontent.com/badjware/gitlabfs/dev/contrib/systemd/gitforgefs%40.service
```
3. Reload systemd: `systemctl --user daemon-reload`
## Usage
1. Create your gitlabfs config file in **~/.config/gitlabfs** eg: **~/.config/gitlabfs/gitlab.com.yaml**. Make sure the config file name ends with **.yaml** and a mountpoint is configured in the file.
2. Start your service with `systemctl --user start gitlabfs@<name of your config>.service`. eg: `systemctl --user start gitlabfs@gitlab.com.service`. Omit the **.yaml** in the name of the service.
3. Enable your service start on login with `systemctl --user enable gitlabfs@<name of your config>.service`. eg: `systemctl --user enable gitlabfs@gitlab.com.service`
1. Create your gitforgefs config file in **$HOME/.config/gitforgefs** eg: **$HOME/.config/gitforgefs/gitlab.com.yaml**. Make sure the config file name ends with **.yaml** and a mountpoint is configured in the file.
2. Start your service with `systemctl --user start gitforgefs@<name of your config>.service`. eg: `systemctl --user start gitforgefs@gitlab.com.service`. Omit the **.yaml** extension.
3. Enable your service to start on login with `systemctl --user enable gitforgefs@<name of your config>.service`. eg: `systemctl --user enable gitforgefs@gitlab.com.service`

View File

@ -4,7 +4,7 @@ Wants=network-online.target
After=network-online.target
[Service]
ExecStart=%h/go/bin/gitlabfs -config %E/gitlabfs/%i.yaml
ExecStart=%h/go/bin/gitforgefs -config %E/gitforgefs/%i.yaml
[Install]
WantedBy=default.target

96
forges/gitea/client.go Normal file
View File

@ -0,0 +1,96 @@
package gitea
import (
"fmt"
"log/slog"
"sync"
"code.gitea.io/sdk/gitea"
"github.com/badjware/gitforgefs/config"
"github.com/badjware/gitforgefs/fstree"
)
type giteaClient struct {
config.GiteaClientConfig
client *gitea.Client
logger *slog.Logger
rootContent map[string]fstree.GroupSource
// API response cache
organizationCacheMux sync.RWMutex
organizationNameToIDMap map[string]int64
organizationCache map[int64]*Organization
userCacheMux sync.RWMutex
userNameToIDMap map[string]int64
userCache map[int64]*User
}
func NewClient(logger *slog.Logger, config config.GiteaClientConfig) (*giteaClient, error) {
client, err := gitea.NewClient(config.URL, gitea.SetToken(config.Token))
if err != nil {
return nil, fmt.Errorf("failed to create the gitea client: %v", err)
}
giteaClient := &giteaClient{
GiteaClientConfig: config,
client: client,
logger: logger,
rootContent: nil,
organizationNameToIDMap: map[string]int64{},
organizationCache: map[int64]*Organization{},
userNameToIDMap: map[string]int64{},
userCache: map[int64]*User{},
}
// Fetch current user and add it to the list
currentUser, _, err := client.GetMyUserInfo()
if err != nil {
logger.Warn("failed to fetch the current user:", "error", err.Error())
} else {
giteaClient.UserNames = append(giteaClient.UserNames, *&currentUser.UserName)
}
return giteaClient, nil
}
func (c *giteaClient) FetchRootGroupContent() (map[string]fstree.GroupSource, error) {
if c.rootContent == nil {
rootContent := make(map[string]fstree.GroupSource)
for _, orgName := range c.GiteaClientConfig.OrgNames {
org, err := c.fetchOrganization(orgName)
if err != nil {
c.logger.Warn(err.Error())
} else {
rootContent[org.Name] = org
}
}
for _, userName := range c.GiteaClientConfig.UserNames {
user, err := c.fetchUser(userName)
if err != nil {
c.logger.Warn(err.Error())
} else {
rootContent[user.Name] = user
}
}
c.rootContent = rootContent
}
return c.rootContent, nil
}
func (c *giteaClient) FetchGroupContent(gid uint64) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) {
if org, found := c.organizationCache[int64(gid)]; found {
return c.fetchOrganizationContent(org)
}
if user, found := c.userCache[int64(gid)]; found {
return c.fetchUserContent(user)
}
return nil, nil, fmt.Errorf("invalid gid: %v", gid)
}

View File

@ -0,0 +1,104 @@
package gitea
import (
"fmt"
"sync"
"code.gitea.io/sdk/gitea"
"github.com/badjware/gitforgefs/fstree"
)
type Organization struct {
ID int64
Name string
mux sync.Mutex
// hold org content
childRepositories map[string]fstree.RepositorySource
}
func (o *Organization) GetGroupID() uint64 {
return uint64(o.ID)
}
func (o *Organization) InvalidateContentCache() {
o.mux.Lock()
defer o.mux.Unlock()
// clear child repositories from cache
o.childRepositories = nil
}
func (c *giteaClient) fetchOrganization(orgName string) (*Organization, error) {
c.organizationCacheMux.RLock()
cachedId, found := c.organizationNameToIDMap[orgName]
if found {
cachedOrg := c.organizationCache[cachedId]
c.organizationCacheMux.RUnlock()
// if found in cache, return the cached reference
c.logger.Debug("Organization cache hit", "org_name", orgName)
return cachedOrg, nil
} else {
c.organizationCacheMux.RUnlock()
c.logger.Debug("Organization cache miss", "org_name", orgName)
}
// If not found in cache, fetch organization infos from API
giteaOrg, _, err := c.client.GetOrg(orgName)
if err != nil {
return nil, fmt.Errorf("failed to fetch organization with name %v: %v", orgName, err)
}
newOrg := Organization{
ID: giteaOrg.ID,
Name: giteaOrg.UserName,
childRepositories: nil,
}
// save in cache
c.organizationCacheMux.Lock()
c.organizationCache[newOrg.ID] = &newOrg
c.organizationNameToIDMap[newOrg.Name] = newOrg.ID
c.organizationCacheMux.Unlock()
return &newOrg, nil
}
func (c *giteaClient) fetchOrganizationContent(org *Organization) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) {
org.mux.Lock()
defer org.mux.Unlock()
// Get cached data if available
// TODO: cache cache invalidation?
if org.childRepositories == nil {
childRepositories := make(map[string]fstree.RepositorySource)
// Fetch the organization repositories
listReposOptions := gitea.ListReposOptions{
ListOptions: gitea.ListOptions{PageSize: 100},
}
for {
giteaRepositories, response, err := c.client.ListOrgRepos(org.Name, gitea.ListOrgReposOptions(listReposOptions))
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch repository in gitea: %v", err)
}
for _, giteaRepository := range giteaRepositories {
repository := c.newRepositoryFromGiteaRepository(giteaRepository)
if repository != nil {
childRepositories[repository.Path] = repository
}
}
if response.NextPage == 0 {
break
}
// Get the next page
listReposOptions.Page = response.NextPage
}
org.childRepositories = childRepositories
}
return make(map[string]fstree.GroupSource), org.childRepositories, nil
}

View File

@ -0,0 +1,50 @@
package gitea
import (
"path"
"code.gitea.io/sdk/gitea"
"github.com/badjware/gitforgefs/config"
)
type Repository struct {
ID int64
Path string
CloneURL string
DefaultBranch string
}
func (r *Repository) GetRepositoryID() uint64 {
return uint64(r.ID)
}
func (r *Repository) GetCloneURL() string {
return r.CloneURL
}
func (r *Repository) GetDefaultBranch() string {
return r.DefaultBranch
}
func (c *giteaClient) newRepositoryFromGiteaRepository(repository *gitea.Repository) *Repository {
if c.ArchivedRepoHandling == config.ArchivedProjectIgnore && repository.Archived {
return nil
}
r := Repository{
ID: repository.ID,
Path: repository.Name,
DefaultBranch: repository.DefaultBranch,
}
if r.DefaultBranch == "" {
r.DefaultBranch = "master"
}
if c.PullMethod == config.PullMethodSSH {
r.CloneURL = repository.SSHURL
} else {
r.CloneURL = repository.CloneURL
}
if c.ArchivedRepoHandling == config.ArchivedProjectHide && repository.Archived {
r.Path = path.Join(path.Dir(r.Path), "."+path.Base(r.Path))
}
return &r
}

104
forges/gitea/user.go Normal file
View File

@ -0,0 +1,104 @@
package gitea
import (
"fmt"
"sync"
"code.gitea.io/sdk/gitea"
"github.com/badjware/gitforgefs/fstree"
)
type User struct {
ID int64
Name string
mux sync.Mutex
// hold user content
childRepositories map[string]fstree.RepositorySource
}
func (u *User) GetGroupID() uint64 {
return uint64(u.ID)
}
func (u *User) InvalidateContentCache() {
u.mux.Lock()
defer u.mux.Unlock()
// clear child repositories from cache
u.childRepositories = nil
}
func (c *giteaClient) fetchUser(userName string) (*User, error) {
c.userCacheMux.RLock()
cachedId, found := c.userNameToIDMap[userName]
if found {
cachedUser := c.userCache[cachedId]
c.userCacheMux.RUnlock()
// if found in cache, return the cached reference
c.logger.Debug("User cache hit", "user_name", userName)
return cachedUser, nil
} else {
c.userCacheMux.RUnlock()
c.logger.Debug("User cache miss", "user_name", userName)
}
// If not found in cache, fetch user infos from API
giteaUser, _, err := c.client.GetUserInfo(userName)
if err != nil {
return nil, fmt.Errorf("failed to fetch user with name %v: %v", userName, err)
}
newUser := User{
ID: giteaUser.ID,
Name: giteaUser.UserName,
childRepositories: nil,
}
// save in cache
c.userCacheMux.Lock()
c.userCache[newUser.ID] = &newUser
c.userNameToIDMap[newUser.Name] = newUser.ID
c.userCacheMux.Unlock()
return &newUser, nil
}
func (c *giteaClient) fetchUserContent(user *User) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) {
user.mux.Lock()
defer user.mux.Unlock()
// Get cached data if available
// TODO: cache cache invalidation?
if user.childRepositories == nil {
childRepositories := make(map[string]fstree.RepositorySource)
// Fetch the user repositories
listReposOptions := gitea.ListReposOptions{
ListOptions: gitea.ListOptions{PageSize: 100},
}
for {
giteaRepositories, response, err := c.client.ListUserRepos(user.Name, listReposOptions)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch repository in gitea: %v", err)
}
for _, giteaRepository := range giteaRepositories {
repository := c.newRepositoryFromGiteaRepository(giteaRepository)
if repository != nil {
childRepositories[repository.Path] = repository
}
}
if response.NextPage == 0 {
break
}
// Get the next page
listReposOptions.Page = response.NextPage
}
user.childRepositories = childRepositories
}
return make(map[string]fstree.GroupSource), user.childRepositories, nil
}

97
forges/github/client.go Normal file
View File

@ -0,0 +1,97 @@
package github
import (
"context"
"fmt"
"log/slog"
"sync"
"github.com/badjware/gitforgefs/config"
"github.com/badjware/gitforgefs/fstree"
"github.com/google/go-github/v63/github"
)
type githubClient struct {
config.GithubClientConfig
client *github.Client
logger *slog.Logger
rootContent map[string]fstree.GroupSource
// API response cache
organizationCacheMux sync.RWMutex
organizationNameToIDMap map[string]int64
organizationCache map[int64]*Organization
userCacheMux sync.RWMutex
userNameToIDMap map[string]int64
userCache map[int64]*User
}
func NewClient(logger *slog.Logger, config config.GithubClientConfig) (*githubClient, error) {
client := github.NewClient(nil)
if config.Token != "" {
client = client.WithAuthToken(config.Token)
}
gitHubClient := &githubClient{
GithubClientConfig: config,
client: client,
logger: logger,
rootContent: nil,
organizationNameToIDMap: map[string]int64{},
organizationCache: map[int64]*Organization{},
userNameToIDMap: map[string]int64{},
userCache: map[int64]*User{},
}
// Fetch current user and add it to the list
currentUser, _, err := client.Users.Get(context.Background(), "")
if err != nil {
logger.Warn("failed to fetch the current user:", "error", err.Error())
} else {
gitHubClient.UserNames = append(gitHubClient.UserNames, *currentUser.Login)
}
return gitHubClient, nil
}
func (c *githubClient) FetchRootGroupContent() (map[string]fstree.GroupSource, error) {
if c.rootContent == nil {
rootContent := make(map[string]fstree.GroupSource)
for _, orgName := range c.GithubClientConfig.OrgNames {
org, err := c.fetchOrganization(orgName)
if err != nil {
c.logger.Warn(err.Error())
} else {
rootContent[org.Name] = org
}
}
for _, userName := range c.GithubClientConfig.UserNames {
user, err := c.fetchUser(userName)
if err != nil {
c.logger.Warn(err.Error())
} else {
rootContent[user.Name] = user
}
}
c.rootContent = rootContent
}
return c.rootContent, nil
}
func (c *githubClient) FetchGroupContent(gid uint64) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) {
if org, found := c.organizationCache[int64(gid)]; found {
return c.fetchOrganizationContent(org)
}
if user, found := c.userCache[int64(gid)]; found {
return c.fetchUserContent(user)
}
return nil, nil, fmt.Errorf("invalid gid: %v", gid)
}

View File

@ -0,0 +1,105 @@
package github
import (
"context"
"fmt"
"sync"
"github.com/badjware/gitforgefs/fstree"
"github.com/google/go-github/v63/github"
)
type Organization struct {
ID int64
Name string
mux sync.Mutex
// hold org content
childRepositories map[string]fstree.RepositorySource
}
func (o *Organization) GetGroupID() uint64 {
return uint64(o.ID)
}
func (o *Organization) InvalidateContentCache() {
o.mux.Lock()
defer o.mux.Unlock()
// clear child repositories from cache
o.childRepositories = nil
}
func (c *githubClient) fetchOrganization(orgName string) (*Organization, error) {
c.organizationCacheMux.RLock()
cachedId, found := c.organizationNameToIDMap[orgName]
if found {
cachedOrg := c.organizationCache[cachedId]
c.organizationCacheMux.RUnlock()
// if found in cache, return the cached reference
c.logger.Debug("Organization cache hit", "org_name", orgName)
return cachedOrg, nil
} else {
c.organizationCacheMux.RUnlock()
c.logger.Debug("Organization cache miss", "org_name", orgName)
}
// If not found in cache, fetch organization infos from API
githubOrg, _, err := c.client.Organizations.Get(context.Background(), orgName)
if err != nil {
return nil, fmt.Errorf("failed to fetch organization with name %v: %v", orgName, err)
}
newOrg := Organization{
ID: *githubOrg.ID,
Name: *githubOrg.Login,
childRepositories: nil,
}
// save in cache
c.organizationCacheMux.Lock()
c.organizationCache[newOrg.ID] = &newOrg
c.organizationNameToIDMap[newOrg.Name] = newOrg.ID
c.organizationCacheMux.Unlock()
return &newOrg, nil
}
func (c *githubClient) fetchOrganizationContent(org *Organization) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) {
org.mux.Lock()
defer org.mux.Unlock()
// Get cached data if available
// TODO: cache cache invalidation?
if org.childRepositories == nil {
childRepositories := make(map[string]fstree.RepositorySource)
// Fetch the organization repositories
repositoryListOpt := &github.RepositoryListByOrgOptions{
ListOptions: github.ListOptions{PerPage: 100},
}
for {
githubRepositories, response, err := c.client.Repositories.ListByOrg(context.Background(), org.Name, repositoryListOpt)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch repository in github: %v", err)
}
for _, githubRepository := range githubRepositories {
repository := c.newRepositoryFromGithubRepository(githubRepository)
if repository != nil {
childRepositories[repository.Path] = repository
}
}
if response.NextPage == 0 {
break
}
// Get the next page
repositoryListOpt.Page = response.NextPage
}
org.childRepositories = childRepositories
}
return make(map[string]fstree.GroupSource), org.childRepositories, nil
}

View File

@ -0,0 +1,50 @@
package github
import (
"path"
"github.com/badjware/gitforgefs/config"
"github.com/google/go-github/v63/github"
)
type Repository struct {
ID int64
Path string
CloneURL string
DefaultBranch string
}
func (r *Repository) GetRepositoryID() uint64 {
return uint64(r.ID)
}
func (r *Repository) GetCloneURL() string {
return r.CloneURL
}
func (r *Repository) GetDefaultBranch() string {
return r.DefaultBranch
}
func (c *githubClient) newRepositoryFromGithubRepository(repository *github.Repository) *Repository {
if c.ArchivedRepoHandling == config.ArchivedProjectIgnore && *repository.Archived {
return nil
}
r := Repository{
ID: *repository.ID,
Path: *repository.Name,
DefaultBranch: *repository.DefaultBranch,
}
if r.DefaultBranch == "" {
r.DefaultBranch = "master"
}
if c.PullMethod == config.PullMethodSSH {
r.CloneURL = *repository.SSHURL
} else {
r.CloneURL = *repository.CloneURL
}
if c.ArchivedRepoHandling == config.ArchivedProjectHide && *repository.Archived {
r.Path = path.Join(path.Dir(r.Path), "."+path.Base(r.Path))
}
return &r
}

105
forges/github/user.go Normal file
View File

@ -0,0 +1,105 @@
package github
import (
"context"
"fmt"
"sync"
"github.com/badjware/gitforgefs/fstree"
"github.com/google/go-github/v63/github"
)
type User struct {
ID int64
Name string
mux sync.Mutex
// hold user content
childRepositories map[string]fstree.RepositorySource
}
func (u *User) GetGroupID() uint64 {
return uint64(u.ID)
}
func (u *User) InvalidateContentCache() {
u.mux.Lock()
defer u.mux.Unlock()
// clear child repositories from cache
u.childRepositories = nil
}
func (c *githubClient) fetchUser(userName string) (*User, error) {
c.userCacheMux.RLock()
cachedId, found := c.userNameToIDMap[userName]
if found {
cachedUser := c.userCache[cachedId]
c.userCacheMux.RUnlock()
// if found in cache, return the cached reference
c.logger.Debug("User cache hit", "user_name", userName)
return cachedUser, nil
} else {
c.userCacheMux.RUnlock()
c.logger.Debug("User cache miss", "user_name", userName)
}
// If not found in cache, fetch user infos from API
githubUser, _, err := c.client.Users.Get(context.Background(), userName)
if err != nil {
return nil, fmt.Errorf("failed to fetch user with name %v: %v", userName, err)
}
newUser := User{
ID: *githubUser.ID,
Name: *githubUser.Login,
childRepositories: nil,
}
// save in cache
c.userCacheMux.Lock()
c.userCache[newUser.ID] = &newUser
c.userNameToIDMap[newUser.Name] = newUser.ID
c.userCacheMux.Unlock()
return &newUser, nil
}
func (c *githubClient) fetchUserContent(user *User) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) {
user.mux.Lock()
defer user.mux.Unlock()
// Get cached data if available
// TODO: cache cache invalidation?
if user.childRepositories == nil {
childRepositories := make(map[string]fstree.RepositorySource)
// Fetch the user repositories
repositoryListOpt := &github.RepositoryListByUserOptions{
ListOptions: github.ListOptions{PerPage: 100},
}
for {
githubRepositories, response, err := c.client.Repositories.ListByUser(context.Background(), user.Name, repositoryListOpt)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch repository in github: %v", err)
}
for _, githubRepository := range githubRepositories {
repository := c.newRepositoryFromGithubRepository(githubRepository)
if repository != nil {
childRepositories[repository.Path] = repository
}
}
if response.NextPage == 0 {
break
}
// Get the next page
repositoryListOpt.Page = response.NextPage
}
user.childRepositories = childRepositories
}
return make(map[string]fstree.GroupSource), user.childRepositories, nil
}

118
forges/gitlab/client.go Normal file
View File

@ -0,0 +1,118 @@
package gitlab
import (
"fmt"
"log/slog"
"slices"
"sync"
"github.com/badjware/gitforgefs/config"
"github.com/badjware/gitforgefs/fstree"
"github.com/xanzy/go-gitlab"
)
type gitlabClient struct {
config.GitlabClientConfig
client *gitlab.Client
logger *slog.Logger
rootContent map[string]fstree.GroupSource
userIDs []int
// API response cache
groupCacheMux sync.RWMutex
groupCache map[int]*Group
userCacheMux sync.RWMutex
userCache map[int]*User
}
func NewClient(logger *slog.Logger, config config.GitlabClientConfig) (*gitlabClient, error) {
client, err := gitlab.NewClient(
config.Token,
gitlab.WithBaseURL(config.URL),
)
if err != nil {
return nil, fmt.Errorf("failed to create gitlab client: %v", err)
}
gitlabClient := &gitlabClient{
GitlabClientConfig: config,
client: client,
logger: logger,
rootContent: nil,
userIDs: []int{},
groupCache: map[int]*Group{},
userCache: map[int]*User{},
}
// Fetch current user and add it to the list
currentUser, _, err := client.Users.CurrentUser()
if err != nil {
logger.Warn("failed to fetch the current user:", "error", err.Error())
} else {
gitlabClient.userIDs = append(gitlabClient.userIDs, currentUser.ID)
}
// Fetch the configured users and add them to the list
for _, userName := range config.UserNames {
user, _, err := client.Users.ListUsers(&gitlab.ListUsersOptions{Username: &userName})
if err != nil || len(user) != 1 {
logger.Warn("failed to fetch the user", "userName", userName, "error", err.Error())
} else {
gitlabClient.userIDs = append(gitlabClient.userIDs, user[0].ID)
}
}
return gitlabClient, nil
}
func (c *gitlabClient) FetchRootGroupContent() (map[string]fstree.GroupSource, error) {
// use cached values if available
if c.rootContent == nil {
rootGroupCache := make(map[string]fstree.GroupSource)
// fetch root groups
for _, gid := range c.GroupIDs {
group, err := c.fetchGroup(gid)
if err != nil {
return nil, err
}
rootGroupCache[group.Name] = group
}
// fetch users
for _, uid := range c.userIDs {
user, err := c.fetchUser(uid)
if err != nil {
return nil, err
}
rootGroupCache[user.Name] = user
}
c.rootContent = rootGroupCache
}
return c.rootContent, nil
}
func (c *gitlabClient) FetchGroupContent(gid uint64) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) {
if slices.Contains[[]int, int](c.userIDs, int(gid)) {
// gid is a user
user, err := c.fetchUser(int(gid))
if err != nil {
return nil, nil, err
}
return c.fetchUserContent(user)
} else {
// gid is a group
group, err := c.fetchGroup(int(gid))
if err != nil {
return nil, nil, err
}
return c.fetchGroupContent(group)
}
}

181
forges/gitlab/group.go Normal file
View File

@ -0,0 +1,181 @@
package gitlab
import (
"fmt"
"sync"
"github.com/badjware/gitforgefs/fstree"
"github.com/xanzy/go-gitlab"
)
type Group struct {
ID int
Name string
gitlabClient *gitlabClient
mux sync.Mutex
// hold group content
childGroups map[string]fstree.GroupSource
childProjects map[string]fstree.RepositorySource
}
func (g *Group) GetGroupID() uint64 {
return uint64(g.ID)
}
func (g *Group) InvalidateContentCache() {
g.mux.Lock()
defer g.mux.Unlock()
// clear child group from cache
g.gitlabClient.groupCacheMux.Lock()
for _, childGroup := range g.childGroups {
gid := int(childGroup.GetGroupID())
delete(g.gitlabClient.groupCache, gid)
}
g.gitlabClient.groupCacheMux.Unlock()
g.childGroups = nil
// clear child repositories from cache
g.childGroups = nil
}
func (c *gitlabClient) fetchGroup(gid int) (*Group, error) {
// start by searching the cache
// TODO: cache invalidation?
c.groupCacheMux.RLock()
group, found := c.groupCache[gid]
c.groupCacheMux.RUnlock()
if found {
c.logger.Debug("Group cache hit", "gid", gid)
return group, nil
} else {
c.logger.Debug("Group cache miss; fetching group", "gid", gid)
}
// If not in cache, fetch group infos from API
gitlabGroup, _, err := c.client.Groups.GetGroup(gid, &gitlab.GetGroupOptions{})
if err != nil {
return nil, fmt.Errorf("failed to fetch group with id %v: %v", gid, err)
}
c.logger.Debug("Fetched group", "gid", gid)
newGroup := Group{
ID: gitlabGroup.ID,
Name: gitlabGroup.Path,
gitlabClient: c,
childGroups: nil,
childProjects: nil,
}
// save in cache
c.groupCacheMux.Lock()
c.groupCache[gid] = &newGroup
c.groupCacheMux.Unlock()
return &newGroup, nil
}
func (c *gitlabClient) newGroupFromGitlabGroup(gitlabGroup *gitlab.Group) (*Group, error) {
gid := gitlabGroup.ID
// start by searching the cache
c.groupCacheMux.RLock()
group, found := c.groupCache[gid]
c.groupCacheMux.RUnlock()
if found {
// if found in cache, return the cached reference
c.logger.Debug("Group cache hit", "gid", gid)
return group, nil
} else {
c.logger.Debug("Group cache miss; registering group", "gid", gid)
}
// if not found in cache, convert and save to cache now
newGroup := Group{
ID: gitlabGroup.ID,
Name: gitlabGroup.Path,
gitlabClient: c,
childGroups: nil,
childProjects: nil,
}
// save in cache
c.groupCacheMux.Lock()
c.groupCache[gid] = &newGroup
c.groupCacheMux.Unlock()
return &newGroup, nil
}
func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) {
// Only a single routine can fetch the group content at the time.
// We lock for the whole duration of the function to avoid fetching the same data from the API
// multiple times if concurrent calls where to occur.
group.mux.Lock()
defer group.mux.Unlock()
// Get cached data if available
// TODO: cache cache invalidation?
if group.childGroups == nil || group.childProjects == nil {
childGroups := make(map[string]fstree.GroupSource)
childProjects := make(map[string]fstree.RepositorySource)
// List subgroups in path
listGroupsOpt := &gitlab.ListSubGroupsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 100,
},
AllAvailable: gitlab.Ptr(true),
}
for {
gitlabGroups, response, err := c.client.Groups.ListSubGroups(group.ID, listGroupsOpt)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch groups in gitlab: %v", err)
}
for _, gitlabGroup := range gitlabGroups {
group, _ := c.newGroupFromGitlabGroup(gitlabGroup)
childGroups[group.Name] = group
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
listGroupsOpt.Page = response.NextPage
}
// List projects in path
listProjectOpt := &gitlab.ListGroupProjectsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 100,
}}
for {
gitlabProjects, response, err := c.client.Groups.ListGroupProjects(group.ID, listProjectOpt)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err)
}
for _, gitlabProject := range gitlabProjects {
project := c.newProjectFromGitlabProject(gitlabProject)
if project != nil {
childProjects[project.Path] = project
}
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
listProjectOpt.Page = response.NextPage
}
group.childGroups = childGroups
group.childProjects = childProjects
}
return group.childGroups, group.childProjects, nil
}

51
forges/gitlab/project.go Normal file
View File

@ -0,0 +1,51 @@
package gitlab
import (
"path"
"github.com/badjware/gitforgefs/config"
"github.com/xanzy/go-gitlab"
)
type Project struct {
ID int
Path string
CloneURL string
DefaultBranch string
}
func (p *Project) GetRepositoryID() uint64 {
return uint64(p.ID)
}
func (p *Project) GetCloneURL() string {
return p.CloneURL
}
func (p *Project) GetDefaultBranch() string {
return p.DefaultBranch
}
func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) *Project {
// https://godoc.org/github.com/xanzy/go-gitlab#Project
if c.ArchivedProjectHandling == config.ArchivedProjectIgnore && project.Archived {
return nil
}
p := Project{
ID: project.ID,
Path: project.Path,
DefaultBranch: project.DefaultBranch,
}
if p.DefaultBranch == "" {
p.DefaultBranch = "master"
}
if c.PullMethod == config.PullMethodSSH {
p.CloneURL = project.SSHURLToRepo
} else {
p.CloneURL = project.HTTPURLToRepo
}
if c.ArchivedProjectHandling == config.ArchivedProjectHide && project.Archived {
p.Path = path.Join(path.Dir(p.Path), "."+path.Base(p.Path))
}
return &p
}

106
forges/gitlab/user.go Normal file
View File

@ -0,0 +1,106 @@
package gitlab
import (
"fmt"
"sync"
"github.com/badjware/gitforgefs/fstree"
"github.com/xanzy/go-gitlab"
)
type User struct {
ID int
Name string
mux sync.Mutex
// hold user content
childProjects map[string]fstree.RepositorySource
}
func (u *User) GetGroupID() uint64 {
return uint64(u.ID)
}
func (u *User) InvalidateContentCache() {
u.mux.Lock()
defer u.mux.Unlock()
// clear child repositories from cache
u.childProjects = nil
}
func (c *gitlabClient) fetchUser(uid int) (*User, error) {
// start by searching the cache
// TODO: cache invalidation?
c.userCacheMux.RLock()
user, found := c.userCache[uid]
c.userCacheMux.RUnlock()
if found {
// if found in cache, return the cached reference
c.logger.Debug("User cache hit", "uid", uid)
return user, nil
} else {
c.logger.Debug("User cache miss", "uid", uid)
}
// If not found in cache, fetch group infos from API
gitlabUser, _, err := c.client.Users.GetUser(uid, gitlab.GetUsersOptions{})
if err != nil {
return nil, fmt.Errorf("failed to fetch user with id %v: %v", uid, err)
}
newUser := User{
ID: gitlabUser.ID,
Name: gitlabUser.Username,
childProjects: nil,
}
// save in cache
c.userCacheMux.Lock()
c.userCache[uid] = &newUser
c.userCacheMux.Unlock()
return &newUser, nil
}
func (c *gitlabClient) fetchUserContent(user *User) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) {
// Only a single routine can fetch the user content at the time.
// We lock for the whole duration of the function to avoid fetching the same data from the API
// multiple times if concurrent calls where to occur.
user.mux.Lock()
defer user.mux.Unlock()
// Get cached data if available
// TODO: cache cache invalidation?
if user.childProjects == nil {
childProjects := make(map[string]fstree.RepositorySource)
// Fetch the user repositories
listProjectOpt := &gitlab.ListProjectsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 100,
}}
for {
gitlabProjects, response, err := c.client.Projects.ListUserProjects(user.ID, listProjectOpt)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err)
}
for _, gitlabProject := range gitlabProjects {
project := c.newProjectFromGitlabProject(gitlabProject)
if project != nil {
childProjects[project.Path] = project
}
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
listProjectOpt.Page = response.NextPage
}
user.childProjects = childProjects
}
return make(map[string]fstree.GroupSource), user.childProjects, nil
}

View File

@ -1,115 +0,0 @@
package fs
import (
"context"
"syscall"
"github.com/badjware/gitlabfs/gitlab"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type groupNode struct {
fs.Inode
param *FSParam
group *gitlab.Group
staticNodes map[string]staticNode
}
// Ensure we are implementing the NodeReaddirer interface
var _ = (fs.NodeReaddirer)((*groupNode)(nil))
// Ensure we are implementing the NodeLookuper interface
var _ = (fs.NodeLookuper)((*groupNode)(nil))
func newGroupNodeByID(gid int, param *FSParam) (*groupNode, error) {
group, err := param.Gitlab.FetchGroup(gid)
if err != nil {
return nil, err
}
node := &groupNode{
param: param,
group: group,
staticNodes: map[string]staticNode{
".refresh": newRefreshNode(group, param),
},
}
return node, nil
}
func newGroupNode(group *gitlab.Group, param *FSParam) (*groupNode, error) {
node := &groupNode{
param: param,
group: group,
staticNodes: map[string]staticNode{
".refresh": newRefreshNode(group, param),
},
}
return node, nil
}
func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
groupContent, _ := n.param.Gitlab.FetchGroupContent(n.group)
entries := make([]fuse.DirEntry, 0, len(groupContent.Groups)+len(groupContent.Projects)+len(n.staticNodes))
for _, group := range groupContent.Groups {
entries = append(entries, fuse.DirEntry{
Name: group.Name,
Ino: uint64(group.ID),
Mode: fuse.S_IFDIR,
})
}
for _, project := range groupContent.Projects {
entries = append(entries, fuse.DirEntry{
Name: project.Name,
Ino: uint64(project.ID),
Mode: fuse.S_IFLNK,
})
}
for name, staticNode := range n.staticNodes {
entries = append(entries, fuse.DirEntry{
Name: name,
Ino: staticNode.Ino(),
Mode: staticNode.Mode(),
})
}
return fs.NewListDirStream(entries), 0
}
func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
groupContent, _ := n.param.Gitlab.FetchGroupContent(n.group)
// Check if the map of groups contains it
group, ok := groupContent.Groups[name]
if ok {
attrs := fs.StableAttr{
Ino: uint64(group.ID),
Mode: fuse.S_IFDIR,
}
groupNode, _ := newGroupNode(group, n.param)
return n.NewInode(ctx, groupNode, attrs), 0
}
// Check if the map of projects contains it
project, ok := groupContent.Projects[name]
if ok {
attrs := fs.StableAttr{
Ino: uint64(project.ID),
Mode: fuse.S_IFLNK,
}
repositoryNode, _ := newRepositoryNode(project, n.param)
return n.NewInode(ctx, repositoryNode, attrs), 0
}
// Check if the map of static nodes contains it
staticNode, ok := n.staticNodes[name]
if ok {
attrs := fs.StableAttr{
Ino: staticNode.Ino(),
Mode: staticNode.Mode(),
}
return n.NewInode(ctx, staticNode, attrs), 0
}
return nil, syscall.ENOENT
}

View File

@ -1,47 +0,0 @@
package fs
import (
"context"
"fmt"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type groupsNode struct {
fs.Inode
param *FSParam
rootGroupIds []int
}
// Ensure we are implementing the NodeOnAdder interface
var _ = (fs.NodeOnAdder)((*groupsNode)(nil))
func newGroupsNode(rootGroupIds []int, param *FSParam) *groupsNode {
return &groupsNode{
param: param,
rootGroupIds: rootGroupIds,
}
}
func (n *groupsNode) OnAdd(ctx context.Context) {
for _, groupID := range n.rootGroupIds {
groupNode, err := newGroupNodeByID(groupID, n.param)
if err != nil {
fmt.Printf("root group fetch fail: %v\n", err)
fmt.Printf("Please verify the group exists, is public or a token with sufficient permissions is set in the config files.\n")
fmt.Printf("Skipping group %v\n", groupID)
return
}
inode := n.NewPersistentInode(
ctx,
groupNode,
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild(groupNode.group.Name, inode, false)
}
}

View File

@ -1,35 +0,0 @@
package fs
import (
"context"
"syscall"
"github.com/badjware/gitlabfs/gitlab"
"github.com/hanwen/go-fuse/v2/fs"
)
type RepositoryNode struct {
fs.Inode
param *FSParam
project *gitlab.Project
}
// Ensure we are implementing the NodeReaddirer interface
var _ = (fs.NodeReadlinker)((*RepositoryNode)(nil))
func newRepositoryNode(project *gitlab.Project, param *FSParam) (*RepositoryNode, error) {
node := &RepositoryNode{
param: param,
project: project,
}
// Passthrough the error if there is one, nothing to add here
// Errors on clone/pull are non-fatal
return node, nil
}
func (n *RepositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
// Create the local copy of the repo
localRepoLoc, _ := n.param.Git.CloneOrPull(n.project.CloneURL, n.project.ID, n.project.DefaultBranch)
return []byte(localRepoLoc), 0
}

View File

@ -1,129 +0,0 @@
package fs
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/badjware/gitlabfs/git"
"github.com/badjware/gitlabfs/gitlab"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
const (
staticInodeStart = uint64(int(^(uint(0))>>1)) + 1
)
type staticNode interface {
fs.InodeEmbedder
Ino() uint64
Mode() uint32
}
type FSParam struct {
Git git.GitClonerPuller
Gitlab gitlab.GitlabFetcher
RootGroupIds []int
UserIds []int
staticInoChan chan uint64
}
type rootNode struct {
fs.Inode
param *FSParam
rootGroupIds []int
userIds []int
}
var _ = (fs.NodeOnAdder)((*rootNode)(nil))
func (n *rootNode) OnAdd(ctx context.Context) {
groupsInode := n.NewPersistentInode(
ctx,
newGroupsNode(
n.rootGroupIds,
n.param,
),
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild("groups", groupsInode, false)
usersInode := n.NewPersistentInode(
ctx,
newUsersNode(
n.userIds,
n.param,
),
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild("users", usersInode, false)
fmt.Println("Mounted and ready to use")
}
func Start(mountpoint string, mountoptions []string, param *FSParam, debug bool) error {
fmt.Printf("Mounting in %v\n", mountpoint)
opts := &fs.Options{}
opts.MountOptions.Options = mountoptions
opts.Debug = debug
param.staticInoChan = make(chan uint64)
root := &rootNode{
param: param,
rootGroupIds: param.RootGroupIds,
userIds: param.UserIds,
}
go staticInoGenerator(root.param.staticInoChan)
server, err := fs.Mount(mountpoint, root, opts)
if err != nil {
return fmt.Errorf("mount failed: %v", err)
}
signalChan := make(chan os.Signal)
go signalHandler(signalChan, server)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
// server.Serve() is already called in fs.Mount() so we shouldn't call it ourself. We wait for the server to terminate.
server.Wait()
return nil
}
func staticInoGenerator(staticInoChan chan<- uint64) {
i := staticInodeStart
for {
staticInoChan <- i
i++
}
}
func signalHandler(signalChan <-chan os.Signal, server *fuse.Server) {
err := server.WaitMount()
if err != nil {
fmt.Printf("failed to start exit signal handler: %v\n", err)
return
}
for {
s := <-signalChan
fmt.Printf("Caught %v: stopping\n", s)
err := server.Unmount()
if err != nil {
fmt.Printf("Failed to unmount: %v\n", err)
}
}
}

View File

@ -1,159 +0,0 @@
package fs
import (
"context"
"fmt"
"syscall"
"github.com/badjware/gitlabfs/gitlab"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type usersNode struct {
fs.Inode
param *FSParam
userIds []int
}
// Ensure we are implementing the NodeOnAdder interface
var _ = (fs.NodeOnAdder)((*usersNode)(nil))
func newUsersNode(userIds []int, param *FSParam) *usersNode {
return &usersNode{
param: param,
userIds: userIds,
}
}
func (n *usersNode) OnAdd(ctx context.Context) {
// Fetch the current logged user
currentUser, err := n.param.Gitlab.FetchCurrentUser()
// Skip if we are anonymous (or the call fails for some reason...)
if err != nil {
fmt.Println(err)
} else {
currentUserNode, _ := newUserNode(currentUser, n.param)
inode := n.NewPersistentInode(
ctx,
currentUserNode,
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild(currentUserNode.user.Name, inode, false)
}
for _, userID := range n.userIds {
if currentUser != nil && currentUser.ID == userID {
// We already added the current user, we can skip it
continue
}
userNode, err := newUserNodeByID(userID, n.param)
if err != nil {
fmt.Printf("user fetch fail: %v\n", err)
fmt.Printf("Please verify the user exists and token with sufficient permissions is set in the config files.\n")
fmt.Printf("Skipping user %v\n", userID)
return
}
inode := n.NewPersistentInode(
ctx,
userNode,
fs.StableAttr{
Ino: <-n.param.staticInoChan,
Mode: fuse.S_IFDIR,
},
)
n.AddChild(userNode.user.Name, inode, false)
}
}
type userNode struct {
fs.Inode
param *FSParam
user *gitlab.User
staticNodes map[string]staticNode
}
// Ensure we are implementing the NodeReaddirer interface
var _ = (fs.NodeReaddirer)((*userNode)(nil))
// Ensure we are implementing the NodeLookuper interface
var _ = (fs.NodeLookuper)((*userNode)(nil))
func newUserNodeByID(uid int, param *FSParam) (*userNode, error) {
user, err := param.Gitlab.FetchUser(uid)
if err != nil {
return nil, err
}
node := &userNode{
param: param,
user: user,
staticNodes: map[string]staticNode{
".refresh": newRefreshNode(user, param),
},
}
return node, nil
}
func newUserNode(user *gitlab.User, param *FSParam) (*userNode, error) {
node := &userNode{
param: param,
user: user,
staticNodes: map[string]staticNode{
".refresh": newRefreshNode(user, param),
},
}
return node, nil
}
func (n *userNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
userContent, _ := n.param.Gitlab.FetchUserContent(n.user)
entries := make([]fuse.DirEntry, 0, len(userContent.Projects)+len(n.staticNodes))
for _, project := range userContent.Projects {
entries = append(entries, fuse.DirEntry{
Name: project.Name,
Ino: uint64(project.ID),
Mode: fuse.S_IFLNK,
})
}
for name, staticNode := range n.staticNodes {
entries = append(entries, fuse.DirEntry{
Name: name,
Ino: staticNode.Ino(),
Mode: staticNode.Mode(),
})
}
return fs.NewListDirStream(entries), 0
}
func (n *userNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
userContent, _ := n.param.Gitlab.FetchUserContent(n.user)
// Check if the map of projects contains it
project, ok := userContent.Projects[name]
if ok {
attrs := fs.StableAttr{
Ino: uint64(project.ID),
Mode: fuse.S_IFLNK,
}
repositoryNode, _ := newRepositoryNode(project, n.param)
return n.NewInode(ctx, repositoryNode, attrs), 0
}
// Check if the map of static nodes contains it
staticNode, ok := n.staticNodes[name]
if ok {
attrs := fs.StableAttr{
Ino: staticNode.Ino(),
Mode: staticNode.Mode(),
}
return n.NewInode(ctx, staticNode, attrs), 0
}
return nil, syscall.ENOENT
}

115
fstree/group.go Normal file
View File

@ -0,0 +1,115 @@
package fstree
import (
"context"
"syscall"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
const (
groupBaseInode = 1_000_000_000
)
type groupNode struct {
fs.Inode
param *FSParam
source GroupSource
staticNodes map[string]staticNode
}
type GroupSource interface {
GetGroupID() uint64
InvalidateContentCache()
}
// Ensure we are implementing the NodeReaddirer interface
var _ = (fs.NodeReaddirer)((*groupNode)(nil))
// Ensure we are implementing the NodeLookuper interface
var _ = (fs.NodeLookuper)((*groupNode)(nil))
func newGroupNodeFromSource(source GroupSource, param *FSParam) (*groupNode, error) {
node := &groupNode{
param: param,
source: source,
staticNodes: map[string]staticNode{
".refresh": newRefreshNode(source, param),
},
}
return node, nil
}
func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
groups, repositories, err := n.param.GitForge.FetchGroupContent(n.source.GetGroupID())
if err != nil {
n.param.logger.Error(err.Error())
}
entries := make([]fuse.DirEntry, 0, len(groups)+len(repositories)+len(n.staticNodes))
for groupName, group := range groups {
entries = append(entries, fuse.DirEntry{
Name: groupName,
Ino: group.GetGroupID() + groupBaseInode,
Mode: fuse.S_IFDIR,
})
}
for repositoryName, repository := range repositories {
entries = append(entries, fuse.DirEntry{
Name: repositoryName,
Ino: repository.GetRepositoryID() + repositoryBaseInode,
Mode: fuse.S_IFLNK,
})
}
for name, staticNode := range n.staticNodes {
entries = append(entries, fuse.DirEntry{
Name: name,
Ino: staticNode.Ino(),
Mode: staticNode.Mode(),
})
}
return fs.NewListDirStream(entries), 0
}
func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
groups, repositories, err := n.param.GitForge.FetchGroupContent(n.source.GetGroupID())
if err != nil {
n.param.logger.Error(err.Error())
} else {
// Check if the map of groups contains it
group, found := groups[name]
if found {
attrs := fs.StableAttr{
Ino: group.GetGroupID() + groupBaseInode,
Mode: fuse.S_IFDIR,
}
groupNode, _ := newGroupNodeFromSource(group, n.param)
return n.NewInode(ctx, groupNode, attrs), 0
}
// Check if the map of projects contains it
repository, found := repositories[name]
if found {
attrs := fs.StableAttr{
Ino: repository.GetRepositoryID() + repositoryBaseInode,
Mode: fuse.S_IFLNK,
}
repositoryNode, _ := newRepositoryNodeFromSource(repository, n.param)
return n.NewInode(ctx, repositoryNode, attrs), 0
}
// Check if the map of static nodes contains it
staticNode, ok := n.staticNodes[name]
if ok {
attrs := fs.StableAttr{
Ino: staticNode.Ino(),
Mode: staticNode.Mode(),
}
return n.NewInode(ctx, staticNode, attrs), 0
}
}
return nil, syscall.ENOENT
}

View File

@ -1,18 +1,18 @@
package fs
package fstree
import (
"context"
"syscall"
"github.com/badjware/gitlabfs/gitlab"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type refreshNode struct {
fs.Inode
ino uint64
refresher gitlab.Refresher
ino uint64
source GroupSource
}
// Ensure we are implementing the NodeSetattrer interface
@ -21,10 +21,10 @@ var _ = (fs.NodeSetattrer)((*refreshNode)(nil))
// Ensure we are implementing the NodeOpener interface
var _ = (fs.NodeOpener)((*refreshNode)(nil))
func newRefreshNode(refresher gitlab.Refresher, param *FSParam) *refreshNode {
func newRefreshNode(source GroupSource, param *FSParam) *refreshNode {
return &refreshNode{
ino: <-param.staticInoChan,
refresher: refresher,
ino: 0,
source: source,
}
}
@ -41,6 +41,6 @@ func (n *refreshNode) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.Se
}
func (n *refreshNode) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
n.refresher.InvalidateCache()
n.source.InvalidateContentCache()
return nil, 0, 0
}

49
fstree/repository.go Normal file
View File

@ -0,0 +1,49 @@
package fstree
import (
"context"
"syscall"
"github.com/hanwen/go-fuse/v2/fs"
)
const (
repositoryBaseInode = 2_000_000_000
)
type repositoryNode struct {
fs.Inode
param *FSParam
source RepositorySource
}
type RepositorySource interface {
// GetName() string
GetRepositoryID() uint64
GetCloneURL() string
GetDefaultBranch() string
}
// Ensure we are implementing the NodeReaddirer interface
var _ = (fs.NodeReadlinker)((*repositoryNode)(nil))
func newRepositoryNodeFromSource(source RepositorySource, param *FSParam) (*repositoryNode, error) {
node := &repositoryNode{
param: param,
source: source,
}
// Passthrough the error if there is one, nothing to add here
// Errors on clone/pull are non-fatal
return node, nil
}
func (n *repositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
// Create the local copy of the repo
// TODO: cleanup
localRepositoryPath, err := n.param.GitClient.FetchLocalRepositoryPath(n.source)
if err != nil {
n.param.logger.Error(err.Error())
}
return []byte(localRepositoryPath), 0
}

107
fstree/root.go Normal file
View File

@ -0,0 +1,107 @@
package fstree
import (
"context"
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type staticNode interface {
fs.InodeEmbedder
Ino() uint64
Mode() uint32
}
type GitClient interface {
FetchLocalRepositoryPath(source RepositorySource) (string, error)
}
type GitForge interface {
FetchRootGroupContent() (map[string]GroupSource, error)
FetchGroupContent(gid uint64) (map[string]GroupSource, map[string]RepositorySource, error)
}
type FSParam struct {
GitClient GitClient
GitForge GitForge
logger *slog.Logger
}
type rootNode struct {
fs.Inode
param *FSParam
}
var _ = (fs.NodeOnAdder)((*rootNode)(nil))
func Start(logger *slog.Logger, mountpoint string, mountoptions []string, param *FSParam, debug bool) error {
logger.Info("Mounting", "mountpoint", mountpoint)
opts := &fs.Options{}
opts.MountOptions.Options = mountoptions
opts.Debug = debug
param.logger = logger
root := &rootNode{
param: param,
}
server, err := fs.Mount(mountpoint, root, opts)
if err != nil {
return fmt.Errorf("mount failed: %v", err)
}
signalChan := make(chan os.Signal)
go signalHandler(logger, signalChan, server)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
// server.Serve() is already called in fs.Mount() so we shouldn't call it ourself. We wait for the server to terminate.
server.Wait()
return nil
}
func (n *rootNode) OnAdd(ctx context.Context) {
rootGroups, err := n.param.GitForge.FetchRootGroupContent()
if err != nil {
panic(err)
}
for groupName, group := range rootGroups {
groupNode, _ := newGroupNodeFromSource(group, n.param)
persistentInode := n.NewPersistentInode(
ctx,
groupNode,
fs.StableAttr{
Ino: group.GetGroupID() + groupBaseInode,
Mode: fuse.S_IFDIR,
},
)
n.AddChild(groupName, persistentInode, false)
}
n.param.logger.Info("Mounted and ready to use")
}
func signalHandler(logger *slog.Logger, signalChan <-chan os.Signal, server *fuse.Server) {
err := server.WaitMount()
if err != nil {
logger.Error("failed to start exit signal handler", "error", err)
return
}
for {
s := <-signalChan
logger.Info("Caught signal", "signal", s)
err := server.Unmount()
if err != nil {
logger.Error("Failed to unmount", "error", err)
}
}
}

View File

@ -2,49 +2,46 @@ package git
import (
"context"
"net/url"
"fmt"
"log/slog"
"os"
"path/filepath"
"regexp"
"strconv"
"time"
"github.com/badjware/gitforgefs/config"
"github.com/badjware/gitforgefs/fstree"
"github.com/badjware/gitforgefs/utils"
"github.com/vmihailenco/taskq/v3"
"github.com/vmihailenco/taskq/v3/memqueue"
)
const (
CloneInit = iota
CloneClone = iota
)
type GitClonerPuller interface {
CloneOrPull(url string, pid int, defaultBranch string) (localRepoLoc string, err error)
}
type GitClientParam struct {
CloneLocation string
RemoteName string
RemoteURL *url.URL
CloneMethod int
PullDepth int
AutoPull bool
QueueSize int
QueueWorkerCount int
}
type gitClient struct {
GitClientParam
config.GitClientConfig
logger *slog.Logger
hostnameProg *regexp.Regexp
majorVersion int
minorVersion int
patchVersion string
queue taskq.Queue
cloneTask *taskq.Task
pullTask *taskq.Task
}
func NewClient(p GitClientParam) (*gitClient, error) {
func NewClient(logger *slog.Logger, p config.GitClientConfig) (*gitClient, error) {
queueFactory := memqueue.NewFactory()
// Create the client
c := &gitClient{
GitClientParam: p,
GitClientConfig: p,
logger: logger,
hostnameProg: regexp.MustCompile(`([a-z0-1\-]+\.)+[a-z0-1\-]+`),
queue: queueFactory.RegisterQueue(&taskq.QueueOptions{
Name: "git-queue",
@ -54,6 +51,25 @@ func NewClient(p GitClientParam) (*gitClient, error) {
}),
}
// Parse git version
gitVersionOutput, err := utils.ExecProcess(logger, "git", "--version")
if err != nil {
return nil, fmt.Errorf("failed to run \"git --version\": %v", err)
}
prog := regexp.MustCompile(`([0-9]+)\.([0-9]+)\.(.+)`)
gitVersionMatches := prog.FindStringSubmatch(gitVersionOutput)
c.majorVersion, err = strconv.Atoi(gitVersionMatches[1])
if err != nil {
return nil, fmt.Errorf("failed to parse git major version \"%v\": %v", gitVersionOutput, err)
}
c.minorVersion, err = strconv.Atoi(gitVersionMatches[2])
if err != nil {
return nil, fmt.Errorf("failed to parse git minor version \"%v\": %v", gitVersionOutput, err)
}
c.patchVersion = gitVersionMatches[3]
logger.Info("Detected git version", "major", c.majorVersion, "minor", c.minorVersion, "patch", c.patchVersion)
// Register tasks
c.cloneTask = taskq.RegisterTask(&taskq.TaskOptions{
Name: "git-clone",
Handler: c.clone,
@ -68,21 +84,27 @@ func NewClient(p GitClientParam) (*gitClient, error) {
return c, nil
}
func (c *gitClient) getLocalRepoLoc(pid int) string {
return filepath.Join(c.CloneLocation, c.RemoteURL.Hostname(), strconv.Itoa(pid))
}
func (c *gitClient) FetchLocalRepositoryPath(source fstree.RepositorySource) (localRepoLoc string, err error) {
rid := source.GetRepositoryID()
cloneUrl := source.GetCloneURL()
defaultBranch := source.GetDefaultBranch()
func (c *gitClient) CloneOrPull(url string, pid int, defaultBranch string) (localRepoLoc string, err error) {
localRepoLoc = c.getLocalRepoLoc(pid)
// Parse the url
hostname := c.hostnameProg.FindString(cloneUrl)
if hostname == "" {
return "", fmt.Errorf("failed to match a valid hostname from \"%v\"", cloneUrl)
}
localRepoLoc = filepath.Join(c.CloneLocation, hostname, strconv.Itoa(int(rid)))
if _, err := os.Stat(localRepoLoc); os.IsNotExist(err) {
// Dispatch clone msg
msg := c.cloneTask.WithArgs(context.Background(), url, defaultBranch, localRepoLoc)
msg.OnceInPeriod(time.Second, pid)
msg := c.cloneTask.WithArgs(context.Background(), cloneUrl, defaultBranch, localRepoLoc)
msg.OnceInPeriod(time.Second, rid)
c.queue.Add(msg)
} else if c.AutoPull {
// Dispatch pull msg
msg := c.pullTask.WithArgs(context.Background(), localRepoLoc, defaultBranch)
msg.OnceInPeriod(time.Second, pid)
msg.OnceInPeriod(time.Second, rid)
c.queue.Add(msg)
}
return localRepoLoc, nil

View File

@ -4,36 +4,44 @@ import (
"fmt"
"strconv"
"github.com/badjware/gitlabfs/utils"
"github.com/badjware/gitforgefs/utils"
)
func (c *gitClient) clone(url string, defaultBranch string, dst string) error {
if c.CloneMethod == CloneInit {
if c.GitClientConfig.OnClone == "init" {
// "Fake" cloning the repo by never actually talking to the git server
// This skip a fetch operation that we would do if we where to do a proper clone
// We can save a lot of time and network i/o doing it this way, at the cost of
// resulting in a very barebone local copy
// Init the local repo
fmt.Printf("Initializing %v into %v\n", url, dst)
_, err := utils.ExecProcess(
"git", "init",
"--initial-branch", defaultBranch,
c.logger.Info("Initializing git repository", "directory", dst, "repository", url)
args := []string{
"init",
}
if c.majorVersion > 2 || c.majorVersion == 2 && c.minorVersion >= 28 {
args = append(args, "--initial-branch", defaultBranch)
} else {
c.logger.Warn("Version of git is too old to support --initial-branch. Consider upgrading git to version >= 2.28.0")
}
args = append(args,
"--",
dst, // directory
)
_, err := utils.ExecProcess(c.logger, "git", args...)
if err != nil {
return fmt.Errorf("failed to init git repo %v to %v: %v", url, dst, err)
}
// Configure the remote
_, err = utils.ExecProcessInDir(
c.logger,
dst, // workdir
"git", "remote", "add",
"-m", defaultBranch,
"--",
c.RemoteName, // name
url, // url
c.GitClientConfig.Remote, // name
url, // url
)
if err != nil {
return fmt.Errorf("failed to setup remote %v in git repo %v: %v", url, dst, err)
@ -41,17 +49,19 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error {
// Configure the default branch
_, err = utils.ExecProcessInDir(
c.logger,
dst, // workdir
"git", "config", "--local",
"--",
fmt.Sprintf("branch.%s.remote", defaultBranch), // key
c.RemoteName, // value
c.GitClientConfig.Remote, // value
)
if err != nil {
return fmt.Errorf("failed to setup default branch remote in git repo %v: %v", dst, err)
}
_, err = utils.ExecProcessInDir(
c.logger,
dst, // workdir
"git", "config", "--local",
"--",
@ -64,14 +74,21 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error {
}
} else {
// Clone the repo
_, err := utils.ExecProcess(
"git", "clone",
"--origin", c.RemoteName,
"--depth", strconv.Itoa(c.PullDepth),
c.logger.Info("Cloning git repository", "directory", dst, "repository", url)
args := []string{
"clone",
"--origin", c.GitClientConfig.Remote,
}
if c.GitClientConfig.Depth != 0 {
args = append(args, "--depth", strconv.Itoa(c.GitClientConfig.Depth))
}
args = append(args,
"--",
url, // repository
dst, // directory
)
_, err := utils.ExecProcess(c.logger, "git", args...)
if err != nil {
return fmt.Errorf("failed to clone git repo %v to %v: %v", url, dst, err)
}

View File

@ -4,12 +4,13 @@ import (
"fmt"
"strconv"
"github.com/badjware/gitlabfs/utils"
"github.com/badjware/gitforgefs/utils"
)
func (c *gitClient) pull(repoPath string, defaultBranch string) error {
// Check if the local repo is on default branch
branchName, err := utils.ExecProcessInDir(
c.logger,
repoPath, // workdir
"git", "branch",
"--show-current",
@ -20,19 +21,24 @@ func (c *gitClient) pull(repoPath string, defaultBranch string) error {
if branchName == defaultBranch {
// Pull the repo
_, err = utils.ExecProcessInDir(
repoPath, // workdir
"git", "pull",
"--depth", strconv.Itoa(c.PullDepth),
args := []string{
"pull",
}
if c.GitClientConfig.Depth != 0 {
args = append(args, "--depth", strconv.Itoa(c.GitClientConfig.Depth))
}
args = append(args,
"--",
c.RemoteName, // repository
defaultBranch, // refspec
c.GitClientConfig.Remote, // repository
defaultBranch, // refspec
)
_, err = utils.ExecProcessInDir(c.logger, repoPath, "git", args...)
if err != nil {
return fmt.Errorf("failed to pull git repo %v: %v", repoPath, err)
}
} else {
fmt.Printf("%v != %v, skipping pull", branchName, defaultBranch)
c.logger.Info("Skipping pull because local is not on default branch", "currentBranch", branchName, "defaultBranch", defaultBranch)
}
return nil

View File

@ -1,47 +0,0 @@
package gitlab
import (
"fmt"
"github.com/xanzy/go-gitlab"
)
const (
PullMethodHTTP = "http"
PullMethodSSH = "ssh"
)
type GitlabFetcher interface {
GroupFetcher
UserFetcher
}
type Refresher interface {
InvalidateCache()
}
type GitlabClientParam struct {
PullMethod string
IncludeCurrentUser bool
}
type gitlabClient struct {
GitlabClientParam
client *gitlab.Client
}
func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientParam) (*gitlabClient, error) {
client, err := gitlab.NewClient(
gitlabToken,
gitlab.WithBaseURL(gitlabUrl),
)
if err != nil {
return nil, fmt.Errorf("failed to create gitlab client: %v", err)
}
gitlabClient := &gitlabClient{
GitlabClientParam: p,
client: client,
}
return gitlabClient, nil
}

View File

@ -1,114 +0,0 @@
package gitlab
import (
"fmt"
"sync"
"github.com/xanzy/go-gitlab"
)
type GroupFetcher interface {
FetchGroup(gid int) (*Group, error)
FetchGroupContent(group *Group) (*GroupContent, error)
}
type GroupContent struct {
Groups map[string]*Group
Projects map[string]*Project
}
type Group struct {
ID int
Name string
mux sync.Mutex
content *GroupContent
}
func NewGroupFromGitlabGroup(group *gitlab.Group) Group {
// https://godoc.org/github.com/xanzy/go-gitlab#Group
return Group{
ID: group.ID,
Name: group.Path,
}
}
func (g *Group) InvalidateCache() {
g.mux.Lock()
defer g.mux.Unlock()
g.content = nil
}
func (c *gitlabClient) FetchGroup(gid int) (*Group, error) {
gitlabGroup, _, err := c.client.Groups.GetGroup(gid)
if err != nil {
return nil, fmt.Errorf("failed to fetch group with id %v: %v", gid, err)
}
group := NewGroupFromGitlabGroup(gitlabGroup)
return &group, nil
}
func (c *gitlabClient) FetchGroupContent(group *Group) (*GroupContent, error) {
group.mux.Lock()
defer group.mux.Unlock()
// Get cached data if available
if group.content != nil {
return group.content, nil
}
content := &GroupContent{
Groups: map[string]*Group{},
Projects: map[string]*Project{},
}
// List subgroups in path
ListGroupsOpt := &gitlab.ListSubgroupsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 100,
},
AllAvailable: gitlab.Bool(true),
}
for {
gitlabGroups, response, err := c.client.Groups.ListSubgroups(group.ID, ListGroupsOpt)
if err != nil {
return nil, fmt.Errorf("failed to fetch groups in gitlab: %v", err)
}
for _, gitlabGroup := range gitlabGroups {
group := NewGroupFromGitlabGroup(gitlabGroup)
content.Groups[group.Name] = &group
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
ListGroupsOpt.Page = response.NextPage
}
// List projects in path
listProjectOpt := &gitlab.ListGroupProjectsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 100,
}}
for {
gitlabProjects, response, err := c.client.Groups.ListGroupProjects(group.ID, listProjectOpt)
if err != nil {
return nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err)
}
for _, gitlabProject := range gitlabProjects {
project := c.newProjectFromGitlabProject(gitlabProject)
content.Projects[project.Name] = &project
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
listProjectOpt.Page = response.NextPage
}
group.content = content
return content, nil
}

View File

@ -1,30 +0,0 @@
package gitlab
import (
"github.com/xanzy/go-gitlab"
)
type Project struct {
ID int
Name string
CloneURL string
DefaultBranch string
}
func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) Project {
// https://godoc.org/github.com/xanzy/go-gitlab#Project
p := Project{
ID: project.ID,
Name: project.Path,
DefaultBranch: project.DefaultBranch,
}
if p.DefaultBranch == "" {
p.DefaultBranch = "master"
}
if c.PullMethod == PullMethodSSH {
p.CloneURL = project.SSHURLToRepo
} else {
p.CloneURL = project.HTTPURLToRepo
}
return p
}

View File

@ -1,103 +0,0 @@
package gitlab
import (
"errors"
"fmt"
"sync"
"github.com/xanzy/go-gitlab"
)
type UserFetcher interface {
FetchUser(uid int) (*User, error)
FetchCurrentUser() (*User, error)
FetchUserContent(user *User) (*UserContent, error)
}
type UserContent struct {
Projects map[string]*Project
}
type User struct {
ID int
Name string
mux sync.Mutex
content *UserContent
}
func NewUserFromGitlabUser(user *gitlab.User) User {
// https://godoc.org/github.com/xanzy/go-gitlab#User
return User{
ID: user.ID,
Name: user.Username,
}
}
func (u *User) InvalidateCache() {
u.mux.Lock()
defer u.mux.Unlock()
u.content = nil
}
func (c *gitlabClient) FetchUser(uid int) (*User, error) {
gitlabUser, _, err := c.client.Users.GetUser(uid)
if err != nil {
return nil, fmt.Errorf("failed to fetch user with id %v: %v", uid, err)
}
user := NewUserFromGitlabUser(gitlabUser)
return &user, nil
}
func (c *gitlabClient) FetchCurrentUser() (*User, error) {
if c.IncludeCurrentUser {
gitlabUser, _, err := c.client.Users.CurrentUser()
if err != nil {
return nil, fmt.Errorf("failed to fetch current user: %v", err)
}
user := NewUserFromGitlabUser(gitlabUser)
return &user, nil
}
// no current user to fetch, return nil
return nil, errors.New("current user fetch is disabled")
}
func (c *gitlabClient) FetchUserContent(user *User) (*UserContent, error) {
user.mux.Lock()
defer user.mux.Unlock()
// Get cached data if available
if user.content != nil {
return user.content, nil
}
content := &UserContent{
Projects: map[string]*Project{},
}
// Fetch the user repositories
listProjectOpt := &gitlab.ListProjectsOptions{
ListOptions: gitlab.ListOptions{
Page: 1,
PerPage: 100,
}}
for {
gitlabProjects, response, err := c.client.Projects.ListUserProjects(user.ID, listProjectOpt)
if err != nil {
return nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err)
}
for _, gitlabProject := range gitlabProjects {
project := c.newProjectFromGitlabProject(gitlabProject)
content.Projects[project.Name] = &project
}
if response.CurrentPage >= response.TotalPages {
break
}
// Get the next page
listProjectOpt.Page = response.NextPage
}
user.content = content
return content, nil
}

43
go.mod
View File

@ -1,18 +1,41 @@
module github.com/badjware/gitlabfs
module github.com/badjware/gitforgefs
go 1.15
go 1.21
require (
code.gitea.io/sdk/gitea v0.19.0
github.com/google/go-github/v63 v63.0.0
github.com/hanwen/go-fuse/v2 v2.5.1
github.com/vmihailenco/taskq/v3 v3.2.9
github.com/xanzy/go-gitlab v0.107.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/bsm/redislock v0.7.2 // indirect
github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-redis/redis_rate/v9 v9.1.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hanwen/go-fuse/v2 v2.1.0
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.6.8 // indirect
github.com/klauspost/compress v1.14.4 // indirect
github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d
github.com/xanzy/go-gitlab v0.47.0
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/klauspost/compress v1.15.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/yaml.v2 v2.4.0
google.golang.org/protobuf v1.29.1 // indirect
)

440
go.sum
View File

@ -1,506 +1,202 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/aws/aws-sdk-go v1.42.7 h1:Ee7QC4Y/eGebVGO/5IGN3fSXXSrheesZYYj2pYJG7Zk=
github.com/aws/aws-sdk-go v1.42.7/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/bsm/ginkgo v1.16.4/go.mod h1:RabIZLzOCPghgHJKUqHZpqrQETA5AnF4aCSIYy5C1bk=
github.com/bsm/gomega v1.13.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk=
github.com/bsm/redislock v0.7.1/go.mod h1:TSF3xUotaocycoHjVAp535/bET+ZmvrtcyNrXc0Whm8=
code.gitea.io/sdk/gitea v0.19.0 h1:8I6s1s4RHgzxiPHhOQdgim1RWIRcr0LVMbHBjBFXq4Y=
code.gitea.io/sdk/gitea v0.19.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI=
github.com/aws/aws-sdk-go v1.43.45 h1:2708Bj4uV+ym62MOtBnErm/CDX61C4mFe9V2gXy1caE=
github.com/aws/aws-sdk-go v1.43.45/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/bsm/redislock v0.7.2 h1:jggqOio8JyX9FJBKIfjF3fTxAu/v7zC5mAID9LveqG4=
github.com/bsm/redislock v0.7.2/go.mod h1:kS2g0Yvlymc9Dz8V3iVYAtLAaSVruYbAFdYBDrmC5WU=
github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3 h1:IHZ1Le1ejzkmS7Si7dIzJvYDWe+BIoNmqMnfWHBZSVw=
github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3/go.mod h1:M5XHQLu90v2JNm/bW2tdsYar+5vhV0gEcBcmDBNAN1Y=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-redis/redis/v8 v8.1.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo=
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-redis/redis_rate/v9 v9.1.2 h1:H0l5VzoAtOE6ydd38j8MCq3ABlGLnvvbA1xDSVVCHgQ=
github.com/go-redis/redis_rate/v9 v9.1.2/go.mod h1:oam2de2apSgRG8aJzwJddXbNu91Iyz1m8IKJE2vpvlQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v63 v63.0.0 h1:13xwK/wk9alSokujB9lJkuzdmQuVn2QCPeck76wR3nE=
github.com/google/go-github/v63 v63.0.0/go.mod h1:IqbcrgUmIcEaioWrGYei/09o+ge5vhffGOcxrO0AfmA=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc=
github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek=
github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs=
github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/iron-io/iron_go3 v0.0.0-20190916120531-a4a7f74b73ac h1:w5wltlINIIqRTqQ64dASrCo0fM7k9nosPbKCZnkL0W0=
github.com/iron-io/iron_go3 v0.0.0-20190916120531-a4a7f74b73ac/go.mod h1:gyMTRVO+ZkEy7wQDyD++okPsBN2q127EpuShhHMWG54=
github.com/jeffh/go.bdd v0.0.0-20120717032931-88f798ee0c74 h1:gyfyP8SEIZHs1u2ivTdIbWRtfaKbg5K79d06vnqroJo=
github.com/jeffh/go.bdd v0.0.0-20120717032931-88f798ee0c74/go.mod h1:qNa9FlAfO0U/qNkzYBMH1JKYRMzC+sP9IcyV4U18l98=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4=
github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A=
github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d h1:dhLHiNAeaWqhaRbetihHq5M7vBrQjrutblYqL4w4ro8=
github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d/go.mod h1:IFuypxi7Y0h+PcactlQOPf92Ssxg0FWxQZ8ptxYW/Zk=
github.com/xanzy/go-gitlab v0.47.0 h1:nC35CNaGr9skHkJq1HMYZ58R7gZsy7SO37SkA2RIHbM=
github.com/xanzy/go-gitlab v0.47.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/vmihailenco/taskq/v3 v3.2.9 h1:QE1O8IJlh4xvSB9MJsnEBzNzmJc61y320xAyBeQZ/40=
github.com/vmihailenco/taskq/v3 v3.2.9/go.mod h1:ZoRbkYMZWEUKtKvYlLGKiaRQKUjdvwWAIs/WiW1Nwtg=
github.com/xanzy/go-gitlab v0.107.0 h1:P2CT9Uy9yN9lJo3FLxpMZ4xj6uWcpnigXsjvqJ6nd2Y=
github.com/xanzy/go-gitlab v0.107.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY=
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

182
main.go
View File

@ -3,137 +3,20 @@ package main
import (
"flag"
"fmt"
"net/url"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/badjware/gitlabfs/fs"
"github.com/badjware/gitlabfs/git"
"github.com/badjware/gitlabfs/gitlab"
"gopkg.in/yaml.v2"
"github.com/badjware/gitforgefs/config"
"github.com/badjware/gitforgefs/forges/gitea"
"github.com/badjware/gitforgefs/forges/github"
"github.com/badjware/gitforgefs/forges/gitlab"
"github.com/badjware/gitforgefs/fstree"
"github.com/badjware/gitforgefs/git"
)
type (
Config struct {
FS FSConfig `yaml:"fs,omitempty"`
Gitlab GitlabConfig `yaml:"gitlab,omitempty"`
Git GitConfig `yaml:"git,omitempty"`
}
FSConfig struct {
Mountpoint string `yaml:"mountpoint,omitempty"`
MountOptions string `yaml:"mountoptions,omitempty"`
}
GitlabConfig struct {
URL string `yaml:"url,omitempty"`
Token string `yaml:"token,omitempty"`
GroupIDs []int `yaml:"group_ids,omitempty"`
UserIDs []int `yaml:"user_ids,omitempty"`
IncludeCurrentUser bool `yaml:"include_current_user,omitempty"`
}
GitConfig struct {
CloneLocation string `yaml:"clone_location,omitempty"`
Remote string `yaml:"remote,omitempty"`
PullMethod string `yaml:"pull_method,omitempty"`
OnClone string `yaml:"on_clone,omitempty"`
AutoPull bool `yaml:"auto_pull,omitempty"`
Depth int `yaml:"depth,omitempty"`
QueueSize int `yaml:"queue_size,omitempty"`
QueueWorkerCount int `yaml:"worker_count,omitempty"`
}
)
func loadConfig(configPath string) (*Config, error) {
// defaults
dataHome := os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
dataHome = filepath.Join(os.Getenv("HOME"), ".local/share")
}
defaultCloneLocation := filepath.Join(dataHome, "gitlabfs")
config := &Config{
FS: FSConfig{
Mountpoint: "",
MountOptions: "nodev,nosuid",
},
Gitlab: GitlabConfig{
URL: "https://gitlab.com",
Token: "",
GroupIDs: []int{9970},
UserIDs: []int{},
IncludeCurrentUser: true,
},
Git: GitConfig{
CloneLocation: defaultCloneLocation,
Remote: "origin",
PullMethod: "http",
OnClone: "init",
AutoPull: false,
Depth: 0,
QueueSize: 200,
QueueWorkerCount: 5,
},
}
if configPath != "" {
f, err := os.Open(configPath)
if err != nil {
return nil, fmt.Errorf("failed to open config file: %v", err)
}
defer f.Close()
d := yaml.NewDecoder(f)
if err := d.Decode(config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %v", err)
}
}
return config, nil
}
func makeGitlabConfig(config *Config) (*gitlab.GitlabClientParam, error) {
// parse pull_method
if config.Git.PullMethod != gitlab.PullMethodHTTP && config.Git.PullMethod != gitlab.PullMethodSSH {
return nil, fmt.Errorf("pull_method must be either \"%v\" or \"%v\"", gitlab.PullMethodHTTP, gitlab.PullMethodSSH)
}
return &gitlab.GitlabClientParam{
PullMethod: config.Git.PullMethod,
IncludeCurrentUser: config.Gitlab.IncludeCurrentUser && config.Gitlab.Token != "",
}, nil
}
func makeGitConfig(config *Config) (*git.GitClientParam, error) {
// Parse the gilab url
parsedGitlabURL, err := url.Parse(config.Gitlab.URL)
if err != nil {
return nil, err
}
// parse on_clone
cloneMethod := 0
if config.Git.OnClone == "init" {
cloneMethod = git.CloneInit
} else if config.Git.OnClone == "clone" {
cloneMethod = git.CloneClone
} else {
return nil, fmt.Errorf("on_clone must be either \"init\" or \"clone\"")
}
return &git.GitClientParam{
CloneLocation: config.Git.CloneLocation,
RemoteName: config.Git.Remote,
RemoteURL: parsedGitlabURL,
CloneMethod: cloneMethod,
AutoPull: config.Git.AutoPull,
PullDepth: config.Git.Depth,
QueueSize: config.Git.QueueSize,
QueueWorkerCount: config.Git.QueueWorkerCount,
}, nil
}
func main() {
configPath := flag.String("config", "", "The config file")
configPath := flag.String("config", "config.yaml", "The config file")
mountoptionsFlag := flag.String("o", "", "Filesystem mount options. See mount.fuse(8)")
debug := flag.Bool("debug", false, "Enable debug logging")
@ -145,14 +28,17 @@ func main() {
}
flag.Parse()
config, err := loadConfig(*configPath)
loadedConfig, err := config.LoadConfig(*configPath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Get logger
logger := slog.Default()
// Configure mountpoint
mountpoint := config.FS.Mountpoint
mountpoint := loadedConfig.FS.Mountpoint
if flag.NArg() == 1 {
mountpoint = flag.Arg(0)
}
@ -163,7 +49,7 @@ func main() {
}
// Configure mountoptions
mountoptions := config.FS.MountOptions
mountoptions := loadedConfig.FS.MountOptions
if *mountoptionsFlag != "" {
mountoptions = *mountoptionsFlag
}
@ -173,26 +59,46 @@ func main() {
}
// Create the git client
gitClientParam, err := makeGitConfig(config)
gitClientParam, err := config.MakeGitConfig(loadedConfig)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
gitClient, _ := git.NewClient(*gitClientParam)
gitClient, _ := git.NewClient(logger, *gitClientParam)
// Create the gitlab client
gitlabClientParam, err := makeGitlabConfig(config)
if err != nil {
fmt.Println(err)
os.Exit(1)
var gitForgeClient fstree.GitForge
if loadedConfig.FS.Forge == config.ForgeGitlab {
// Create the gitlab client
gitlabClientConfig, err := config.MakeGitlabConfig(loadedConfig)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
gitForgeClient, _ = gitlab.NewClient(logger, *gitlabClientConfig)
} else if loadedConfig.FS.Forge == config.ForgeGithub {
// Create the github client
githubClientConfig, err := config.MakeGithubConfig(loadedConfig)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
gitForgeClient, _ = github.NewClient(logger, *githubClientConfig)
} else if loadedConfig.FS.Forge == config.ForgeGitea {
// Create the gitea client
giteaClientConfig, err := config.MakeGiteaConfig(loadedConfig)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
gitForgeClient, _ = gitea.NewClient(logger, *giteaClientConfig)
}
gitlabClient, _ := gitlab.NewClient(config.Gitlab.URL, config.Gitlab.Token, *gitlabClientParam)
// Start the filesystem
err = fs.Start(
err = fstree.Start(
logger,
mountpoint,
parsedMountoptions,
&fs.FSParam{Git: gitClient, Gitlab: gitlabClient, RootGroupIds: config.Gitlab.GroupIDs, UserIds: config.Gitlab.UserIDs},
&fstree.FSParam{GitClient: gitClient, GitForge: gitForgeClient},
*debug,
)
if err != nil {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,7 +1,7 @@
package utils
import (
"fmt"
"log/slog"
"os/exec"
"strings"
)
@ -11,19 +11,19 @@ const (
stderr = "stderr"
)
func ExecProcessInDir(workdir string, command string, args ...string) (string, error) {
func ExecProcessInDir(logger *slog.Logger, workdir string, command string, args ...string) (string, error) {
cmd := exec.Command(command, args...)
if workdir != "" {
cmd.Dir = workdir
}
// Run the command
fmt.Printf("%v %v\n", command, strings.Join(args, " "))
logger.Debug("Running command", "cmd", command, "args", args)
output, err := cmd.Output()
return strings.TrimSpace(string(output)), err
}
func ExecProcess(command string, args ...string) (string, error) {
return ExecProcessInDir("", command, args...)
func ExecProcess(logger *slog.Logger, command string, args ...string) (string, error) {
return ExecProcessInDir(logger, "", command, args...)
}