diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..06d8d40 --- /dev/null +++ b/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 32cc1cb..0000000 --- a/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM alpine - -COPY ./bin/gitlabfs /usr/bin/gitlabfs - -ENTRYPOINT ["gitlabfs"] - diff --git a/Makefile b/Makefile index ac29b4a..e41dcba 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PROGRAM := gitlabfs +PROGRAM := gitforgefs TARGET_DIR := ./bin VERSION := $(shell git describe --tags --always) diff --git a/README.md b/README.md index 9e1dc91..06c0995 100644 --- a/README.md +++ b/README.md @@ -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/`. diff --git a/config.example.yaml b/config.example.yaml index 9bd2843..9d4568b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -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) diff --git a/config/config.test.yaml b/config/config.test.yaml new file mode 100644 index 0000000..a4329c9 --- /dev/null +++ b/config/config.test.yaml @@ -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 \ No newline at end of file diff --git a/config/loader.go b/config/loader.go new file mode 100644 index 0000000..48043d8 --- /dev/null +++ b/config/loader.go @@ -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 +} diff --git a/config/loader_test.go b/config/loader_test.go new file mode 100644 index 0000000..4a33457 --- /dev/null +++ b/config/loader_test.go @@ -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) + } + }) + } +} diff --git a/contrib/systemd/README.md b/contrib/systemd/README.md index 5912950..30ace8e 100644 --- a/contrib/systemd/README.md +++ b/contrib/systemd/README.md @@ -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@.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@.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@.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@.service`. eg: `systemctl --user enable gitforgefs@gitlab.com.service` diff --git a/contrib/systemd/gitlabfs@.service b/contrib/systemd/gitforgefs@.service similarity index 63% rename from contrib/systemd/gitlabfs@.service rename to contrib/systemd/gitforgefs@.service index 6ede4ab..8e9c900 100644 --- a/contrib/systemd/gitlabfs@.service +++ b/contrib/systemd/gitforgefs@.service @@ -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 \ No newline at end of file diff --git a/forges/gitea/client.go b/forges/gitea/client.go new file mode 100644 index 0000000..d87c932 --- /dev/null +++ b/forges/gitea/client.go @@ -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, *¤tUser.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) +} diff --git a/forges/gitea/organization.go b/forges/gitea/organization.go new file mode 100644 index 0000000..267a66a --- /dev/null +++ b/forges/gitea/organization.go @@ -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 +} diff --git a/forges/gitea/repository.go b/forges/gitea/repository.go new file mode 100644 index 0000000..ec9088c --- /dev/null +++ b/forges/gitea/repository.go @@ -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 +} diff --git a/forges/gitea/user.go b/forges/gitea/user.go new file mode 100644 index 0000000..8e41e7e --- /dev/null +++ b/forges/gitea/user.go @@ -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 +} diff --git a/forges/github/client.go b/forges/github/client.go new file mode 100644 index 0000000..dda37a5 --- /dev/null +++ b/forges/github/client.go @@ -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) +} diff --git a/forges/github/organization.go b/forges/github/organization.go new file mode 100644 index 0000000..9d1b7e0 --- /dev/null +++ b/forges/github/organization.go @@ -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 +} diff --git a/forges/github/repository.go b/forges/github/repository.go new file mode 100644 index 0000000..f1e66ba --- /dev/null +++ b/forges/github/repository.go @@ -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 +} diff --git a/forges/github/user.go b/forges/github/user.go new file mode 100644 index 0000000..1d1d644 --- /dev/null +++ b/forges/github/user.go @@ -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 +} diff --git a/forges/gitlab/client.go b/forges/gitlab/client.go new file mode 100644 index 0000000..fdc3963 --- /dev/null +++ b/forges/gitlab/client.go @@ -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) + } +} diff --git a/forges/gitlab/group.go b/forges/gitlab/group.go new file mode 100644 index 0000000..0cdfb13 --- /dev/null +++ b/forges/gitlab/group.go @@ -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 +} diff --git a/forges/gitlab/project.go b/forges/gitlab/project.go new file mode 100644 index 0000000..c960d97 --- /dev/null +++ b/forges/gitlab/project.go @@ -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 +} diff --git a/forges/gitlab/user.go b/forges/gitlab/user.go new file mode 100644 index 0000000..cde05c9 --- /dev/null +++ b/forges/gitlab/user.go @@ -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 +} diff --git a/fs/group.go b/fs/group.go deleted file mode 100644 index 0485e5f..0000000 --- a/fs/group.go +++ /dev/null @@ -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 -} diff --git a/fs/groups.go b/fs/groups.go deleted file mode 100644 index 2635407..0000000 --- a/fs/groups.go +++ /dev/null @@ -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) - } -} diff --git a/fs/repository.go b/fs/repository.go deleted file mode 100644 index 5370473..0000000 --- a/fs/repository.go +++ /dev/null @@ -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 -} diff --git a/fs/root.go b/fs/root.go deleted file mode 100644 index 9bcfc86..0000000 --- a/fs/root.go +++ /dev/null @@ -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) - } - } -} diff --git a/fs/users.go b/fs/users.go deleted file mode 100644 index 6094c68..0000000 --- a/fs/users.go +++ /dev/null @@ -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 -} diff --git a/fstree/group.go b/fstree/group.go new file mode 100644 index 0000000..7be1bfa --- /dev/null +++ b/fstree/group.go @@ -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 +} diff --git a/fs/refresh.go b/fstree/refresh.go similarity index 74% rename from fs/refresh.go rename to fstree/refresh.go index 30f4974..25671e9 100644 --- a/fs/refresh.go +++ b/fstree/refresh.go @@ -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 } diff --git a/fstree/repository.go b/fstree/repository.go new file mode 100644 index 0000000..acf70bf --- /dev/null +++ b/fstree/repository.go @@ -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 +} diff --git a/fstree/root.go b/fstree/root.go new file mode 100644 index 0000000..d45e907 --- /dev/null +++ b/fstree/root.go @@ -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) + } + } +} diff --git a/git/client.go b/git/client.go index 868e255..cf25c08 100644 --- a/git/client.go +++ b/git/client.go @@ -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 diff --git a/git/clone.go b/git/clone.go index 8a92920..bc85ec5 100644 --- a/git/clone.go +++ b/git/clone.go @@ -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) } diff --git a/git/pull.go b/git/pull.go index 31096a4..5de6b83 100644 --- a/git/pull.go +++ b/git/pull.go @@ -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 diff --git a/gitlab/client.go b/gitlab/client.go deleted file mode 100644 index 3c73f44..0000000 --- a/gitlab/client.go +++ /dev/null @@ -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 -} diff --git a/gitlab/group.go b/gitlab/group.go deleted file mode 100644 index 86a4c90..0000000 --- a/gitlab/group.go +++ /dev/null @@ -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 -} diff --git a/gitlab/project.go b/gitlab/project.go deleted file mode 100644 index 6f789f5..0000000 --- a/gitlab/project.go +++ /dev/null @@ -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 -} diff --git a/gitlab/user.go b/gitlab/user.go deleted file mode 100644 index a54af3a..0000000 --- a/gitlab/user.go +++ /dev/null @@ -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 -} diff --git a/go.mod b/go.mod index e61a2d5..36e7fed 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 51a4346..5d2b0d8 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index cbe3585..d833972 100644 --- a/main.go +++ b/main.go @@ -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 { diff --git a/media/group_id.jpg b/media/group_id.jpg deleted file mode 100644 index a41e9bb..0000000 Binary files a/media/group_id.jpg and /dev/null differ diff --git a/utils/process.go b/utils/process.go index 49cfed0..d5f2fcc 100644 --- a/utils/process.go +++ b/utils/process.go @@ -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...) }