Skip to content

Commit 9635339

Browse files
committed
Merge branch 'feat/config-file-init'
2 parents 6d38b44 + 8993dd7 commit 9635339

10 files changed

Lines changed: 650 additions & 21 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,7 @@ dist/
4343

4444
testdata/**expected
4545

46+
# Config file (generated by gitbackup init)
47+
gitbackup.yml
4648
# Integration test secrets
4749
test/.env

‎README.md‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Contact us for any custom on-prem or cloud deployment, new feature requests or e
2323
- [GitLab](#gitlab)
2424
- [Forgejo](#forgejo)
2525
- [Security and credentials](#security-and-credentials)
26+
- [Configuration file](#configuration-file)
2627
- [Examples](#examples)
2728
- [Backing up your GitHub repositories](#backing-up-your-github-repositories)
2829
- [Backing up your GitLab repositories](#backing-up-your-gitlab-repositories)
@@ -123,6 +124,77 @@ is used to clone your repositories. If `use-https-clone` is specified, private r
123124
are cloned via `https` basic auth and the token provided will be stored in the repositories'
124125
`.git/config`.
125126

127+
### Configuration file
128+
129+
**Note:** Migration-related flags (``-github.createUserMigration``, ``-github.listUserMigrations``, etc.) are CLI-only and not supported in the config file.
130+
131+
Instead of passing all options as CLI flags, you can use a ``gitbackup.yml`` configuration file.
132+
133+
The config file is stored in the OS-specific configuration directory:
134+
135+
- **Linux:** ``$XDG_CONFIG_HOME/gitbackup/gitbackup.yml`` or ``~/.config/gitbackup/gitbackup.yml``
136+
- **macOS:** ``~/Library/Application Support/gitbackup/gitbackup.yml``
137+
- **Windows:** ``%AppData%/gitbackup/gitbackup.yml``
138+
139+
To create a default configuration file:
140+
141+
```lang=bash
142+
$ gitbackup init
143+
```
144+
145+
To create it at a custom location:
146+
147+
```lang=bash
148+
$ gitbackup init --config /path/to/gitbackup.yml
149+
```
150+
151+
This creates a ``gitbackup.yml`` with default values that you can edit:
152+
153+
```yaml
154+
service: github
155+
githost_url: ""
156+
backup_dir: ""
157+
ignore_private: false
158+
ignore_fork: false
159+
use_https_clone: false
160+
bare: false
161+
github:
162+
repo_type: all
163+
namespace_whitelist: []
164+
gitlab:
165+
project_visibility: internal
166+
project_membership_type: all
167+
forgejo:
168+
repo_type: user
169+
```
170+
171+
To validate your configuration file (checks field values and required environment variables):
172+
173+
```lang=bash
174+
$ gitbackup validate
175+
```
176+
177+
To see available options for a subcommand:
178+
179+
```lang=bash
180+
$ gitbackup init --help
181+
$ gitbackup validate --help
182+
```
183+
184+
The config file is automatically loaded at runtime from the default location. CLI flags override config file values, so you can use the config file for your base settings and override individual options as needed:
185+
186+
```lang=bash
187+
$ GITHUB_TOKEN=secret$token gitbackup -ignore-fork
188+
```
189+
190+
To use a config file at a custom location:
191+
192+
```lang=bash
193+
$ GITHUB_TOKEN=secret$token gitbackup -config /path/to/gitbackup.yml
194+
```
195+
196+
Secrets (tokens, passwords) are not stored in the config file — they are always provided via environment variables.
197+
126198
### Examples
127199

128200
Typing ``-help`` will display the command line options that `gitbackup` recognizes:

‎config_file.go‎

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"gopkg.in/yaml.v3"
9+
)
10+
11+
const defaultConfigFile = "gitbackup.yml"
12+
13+
// defaultConfigPath returns the OS-specific default path for the config file.
14+
// On Linux: ~/.config/gitbackup/gitbackup.yml
15+
// On macOS: ~/Library/Application Support/gitbackup/gitbackup.yml
16+
// On Windows: %AppData%/gitbackup/gitbackup.yml
17+
func defaultConfigPath() (string, error) {
18+
configDir, err := os.UserConfigDir()
19+
if err != nil {
20+
return "", fmt.Errorf("unable to determine config directory: %v", err)
21+
}
22+
return filepath.Join(configDir, "gitbackup", defaultConfigFile), nil
23+
}
24+
25+
// resolveConfigPath returns the config path to use.
26+
// If configPath is non-empty, it is returned as-is.
27+
// Otherwise, the OS-specific default path is returned.
28+
func resolveConfigPath(configPath string) (string, error) {
29+
if configPath != "" {
30+
return configPath, nil
31+
}
32+
return defaultConfigPath()
33+
}
34+
35+
// fileConfig represents the YAML configuration file structure.
36+
// Migration-related flags are intentionally excluded as they
37+
// are one-off operations better suited to CLI flags.
38+
type fileConfig struct {
39+
Service string `yaml:"service"`
40+
GitHostURL string `yaml:"githost_url"`
41+
BackupDir string `yaml:"backup_dir"`
42+
IgnorePrivate bool `yaml:"ignore_private"`
43+
IgnoreFork bool `yaml:"ignore_fork"`
44+
UseHTTPSClone bool `yaml:"use_https_clone"`
45+
Bare bool `yaml:"bare"`
46+
GitHub githubConfig `yaml:"github"`
47+
GitLab gitlabConfig `yaml:"gitlab"`
48+
Forgejo forgejoConfig `yaml:"forgejo"`
49+
}
50+
51+
type githubConfig struct {
52+
RepoType string `yaml:"repo_type"`
53+
NamespaceWhitelist []string `yaml:"namespace_whitelist"`
54+
}
55+
56+
type gitlabConfig struct {
57+
ProjectVisibility string `yaml:"project_visibility"`
58+
ProjectMembershipType string `yaml:"project_membership_type"`
59+
}
60+
61+
type forgejoConfig struct {
62+
RepoType string `yaml:"repo_type"`
63+
}
64+
65+
// defaultFileConfig returns a fileConfig with the same defaults as the CLI flags
66+
func defaultFileConfig() fileConfig {
67+
return fileConfig{
68+
Service: "github",
69+
GitHostURL: "",
70+
BackupDir: "",
71+
IgnorePrivate: false,
72+
IgnoreFork: false,
73+
UseHTTPSClone: false,
74+
Bare: false,
75+
GitHub: githubConfig{
76+
RepoType: "all",
77+
NamespaceWhitelist: []string{},
78+
},
79+
GitLab: gitlabConfig{
80+
ProjectVisibility: "internal",
81+
ProjectMembershipType: "all",
82+
},
83+
Forgejo: forgejoConfig{
84+
RepoType: "user",
85+
},
86+
}
87+
}
88+
89+
// handleInitConfig creates a default gitbackup.yml at the given path,
90+
// or at the OS-specific default location if configPath is empty.
91+
func handleInitConfig(configPath string) error {
92+
path, err := resolveConfigPath(configPath)
93+
if err != nil {
94+
return err
95+
}
96+
97+
if _, err := os.Stat(path); err == nil {
98+
return fmt.Errorf("%s already exists", path)
99+
}
100+
101+
// Create parent directory if it doesn't exist
102+
dir := filepath.Dir(path)
103+
if err := os.MkdirAll(dir, 0755); err != nil {
104+
return fmt.Errorf("error creating directory %s: %v", dir, err)
105+
}
106+
107+
fmt.Println("Creating a default config with service: github")
108+
109+
cfg := defaultFileConfig()
110+
data, err := yaml.Marshal(&cfg)
111+
if err != nil {
112+
return fmt.Errorf("error generating config: %v", err)
113+
}
114+
115+
err = os.WriteFile(path, data, 0644)
116+
if err != nil {
117+
return fmt.Errorf("error writing %s: %v", path, err)
118+
}
119+
120+
fmt.Printf("Created %s\n", path)
121+
return nil
122+
}
123+
124+
// fileConfigToAppConfig converts a fileConfig into an appConfig.
125+
// Migration-related fields are left at their zero values since they
126+
// are CLI-only flags.
127+
func fileConfigToAppConfig(fc *fileConfig) *appConfig {
128+
return &appConfig{
129+
service: fc.Service,
130+
gitHostURL: fc.GitHostURL,
131+
backupDir: fc.BackupDir,
132+
ignorePrivate: fc.IgnorePrivate,
133+
ignoreFork: fc.IgnoreFork,
134+
useHTTPSClone: fc.UseHTTPSClone,
135+
bare: fc.Bare,
136+
githubRepoType: fc.GitHub.RepoType,
137+
githubNamespaceWhitelist: fc.GitHub.NamespaceWhitelist,
138+
gitlabProjectVisibility: fc.GitLab.ProjectVisibility,
139+
gitlabProjectMembershipType: fc.GitLab.ProjectMembershipType,
140+
forgejoRepoType: fc.Forgejo.RepoType,
141+
}
142+
}
143+
144+
// loadConfigFile reads and parses the config file at the given path,
145+
// or at the OS-specific default location if configPath is empty.
146+
func loadConfigFile(configPath string) (*fileConfig, error) {
147+
path, err := resolveConfigPath(configPath)
148+
if err != nil {
149+
return nil, err
150+
}
151+
152+
data, err := os.ReadFile(path)
153+
if err != nil {
154+
return nil, fmt.Errorf("error reading %s: %v", path, err)
155+
}
156+
157+
var cfg fileConfig
158+
err = yaml.Unmarshal(data, &cfg)
159+
if err != nil {
160+
return nil, fmt.Errorf("error parsing %s: %v", path, err)
161+
}
162+
return &cfg, nil
163+
}
164+
165+
// handleValidateConfig reads the config file and validates its contents.
166+
// If configPath is empty, the OS-specific default location is used.
167+
func handleValidateConfig(configPath string) error {
168+
path, err := resolveConfigPath(configPath)
169+
if err != nil {
170+
return err
171+
}
172+
173+
cfg, err := loadConfigFile(configPath)
174+
if err != nil {
175+
return err
176+
}
177+
178+
var errors []string
179+
180+
// Validate service
181+
if _, ok := knownServices[cfg.Service]; !ok {
182+
errors = append(errors, fmt.Sprintf("invalid service: %q (must be github, gitlab, bitbucket, or forgejo)", cfg.Service))
183+
}
184+
185+
// Validate service-specific field values
186+
switch cfg.Service {
187+
case "github":
188+
if !contains([]string{"all", "owner", "member", "starred"}, cfg.GitHub.RepoType) {
189+
errors = append(errors, fmt.Sprintf("invalid github.repo_type: %q (must be all, owner, member, or starred)", cfg.GitHub.RepoType))
190+
}
191+
case "gitlab":
192+
if !contains([]string{"internal", "public", "private"}, cfg.GitLab.ProjectVisibility) {
193+
errors = append(errors, fmt.Sprintf("invalid gitlab.project_visibility: %q (must be internal, public, or private)", cfg.GitLab.ProjectVisibility))
194+
}
195+
if !validGitlabProjectMembership(cfg.GitLab.ProjectMembershipType) {
196+
errors = append(errors, fmt.Sprintf("invalid gitlab.project_membership_type: %q (must be all, owner, member, or starred)", cfg.GitLab.ProjectMembershipType))
197+
}
198+
case "forgejo":
199+
if !contains([]string{"user", "starred"}, cfg.Forgejo.RepoType) {
200+
errors = append(errors, fmt.Sprintf("invalid forgejo.repo_type: %q (must be user or starred)", cfg.Forgejo.RepoType))
201+
}
202+
}
203+
204+
// Validate required environment variables
205+
switch cfg.Service {
206+
case "github":
207+
if os.Getenv("GITHUB_TOKEN") == "" {
208+
errors = append(errors, "GITHUB_TOKEN environment variable not set")
209+
}
210+
case "gitlab":
211+
if os.Getenv("GITLAB_TOKEN") == "" {
212+
errors = append(errors, "GITLAB_TOKEN environment variable not set")
213+
}
214+
case "bitbucket":
215+
if os.Getenv("BITBUCKET_USERNAME") == "" {
216+
errors = append(errors, "BITBUCKET_USERNAME environment variable not set")
217+
}
218+
if os.Getenv("BITBUCKET_TOKEN") == "" && os.Getenv("BITBUCKET_PASSWORD") == "" {
219+
errors = append(errors, "BITBUCKET_TOKEN or BITBUCKET_PASSWORD environment variable must be set")
220+
}
221+
case "forgejo":
222+
if os.Getenv("FORGEJO_TOKEN") == "" {
223+
errors = append(errors, "FORGEJO_TOKEN environment variable not set")
224+
}
225+
}
226+
227+
if len(errors) > 0 {
228+
fmt.Println("Validation errors:")
229+
for _, e := range errors {
230+
fmt.Printf(" - %s\n", e)
231+
}
232+
return fmt.Errorf("config validation failed")
233+
}
234+
235+
fmt.Printf("%s is valid\n", path)
236+
return nil
237+
}

0 commit comments

Comments
 (0)