Skip to content

Commit 0005256

Browse files
authored
Fix private repository backup (#28)
1 parent bc6af8e commit 0005256

File tree

8 files changed

+117
-17
lines changed

8 files changed

+117
-17
lines changed

README.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ GitLab (including custom GitLab installations).
99
pull requests or other data associated with a git repository. This may or may not be in the future
1010
scope of this tool.
1111

12-
If you are following along my Linux Journal article, please obtain the version of the source tagged
13-
with [lj-0.1](https://github.com/amitsaha/gitbackup/releases/tag/lj-0.1).
12+
If you are following along my Linux Journal article (published in 2017), please obtain the version of the
13+
source tagged with [lj-0.1](https://github.com/amitsaha/gitbackup/releases/tag/lj-0.1).
1414

1515
## Installling `gitbackup`
1616

@@ -23,6 +23,28 @@ and architecture and copy the binary somewhere in your ``$PATH``. It is recommen
2323
backing up GitHub repositories and [GitLab personal access token](https://gitlab.com/profile/personal_access_tokens)
2424
for GitLab. You can supply the token to ``gitbackup`` using ``GITHUB_TOKEN`` and ``GITLAB_TOKEN`` environment variables respectively.
2525

26+
### OAuth Scopes required
27+
28+
#### GitHub
29+
30+
- `repo`: Reading repositories, including private repositories
31+
- `user - read:user`: Reading the authenticated user details. This is needed for retrieving username which is needed for retrieving private repositories.
32+
33+
#### GitLab
34+
35+
- `api`: Grants complete read/write access to the API, including all groups and projects.
36+
For some reason, `read_user` and `read_repository` is not sufficient.
37+
38+
### Security and credentials
39+
40+
When you provide the tokens via environment variables, they remain accessible in your shell history
41+
and via the processes' environment for the lifetime of the process. By default, SSH authentication
42+
is used to clone your repositories. If `use-https-clone` is specified, private repositories
43+
are cloned via `https` basic auth and the token provided will be stored in the repositories'
44+
`.git/config`.
45+
46+
### Examples
47+
2648
Typing ``-help`` will display the command line options that `gitbackup` recognizes:
2749

2850
```
@@ -38,8 +60,12 @@ Usage of ./bin/gitbackup:
3860
Project type to clone (all, owner, member) (default "all")
3961
-gitlab.projectVisibility string
4062
Visibility level of Projects to clone (internal, public, private) (default "internal")
63+
-ignore-private
64+
Ignore private repositories/projects
4165
-service string
42-
Git Hosted Service Name (github/gitlab)
66+
Git Hosted Service Name (github/gitlab)
67+
-use-https-clone
68+
Use HTTPS for cloning instead of SSH
4369
```
4470
### Backing up your GitHub repositories
4571

@@ -123,7 +149,6 @@ Similarly, it will create a ``gitlab.com`` directory, if you are backing up repo
123149
If you have specified a Git Host URL, it will create a directory structure ``data/host-url/``.
124150

125151

126-
127152
## Building
128153

129154
If you have Golang 1.12.x+ installed, you can clone the repository and:

backup.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,20 @@ func backUp(backupDir string, repo *Repository, wg *sync.WaitGroup) ([]byte, err
3030
cmd := execCommand(gitCommand, "-C", repoDir, "pull")
3131
stdoutStderr, err = cmd.CombinedOutput()
3232
} else {
33-
log.Printf("Cloning %s \n", repo.Name)
34-
cmd := execCommand(gitCommand, "clone", repo.GitURL, repoDir)
33+
log.Printf("Cloning %s\n", repo.Name)
34+
log.Printf("%#v\n", repo)
35+
36+
if repo.Private && useHTTPSClone != nil && *useHTTPSClone && ignorePrivate != nil && !*ignorePrivate {
37+
// Add username and token to the clone URL
38+
// https://gitlab.com/amitsaha/testproject1 => https://amitsaha:token@gitlab.com/amitsaha/testproject1
39+
u, err := url.Parse(repo.CloneURL)
40+
if err != nil {
41+
log.Fatalf("Invalid clone URL: %v\n", err)
42+
}
43+
repo.CloneURL = u.Scheme + "://" + gitHostUsername + ":" + gitHostToken + "@" + u.Host + u.Path
44+
}
45+
46+
cmd := execCommand(gitCommand, "clone", repo.CloneURL, repoDir)
3547
stdoutStderr, err = cmd.CombinedOutput()
3648
}
3749

backup_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ package main
22

33
import (
44
"fmt"
5-
"github.com/spf13/afero"
65
"os"
76
"os/exec"
87
"path"
98
"sync"
109
"testing"
10+
11+
"github.com/spf13/afero"
1112
)
1213

1314
func fakePullCommand(command string, args ...string) (cmd *exec.Cmd) {
@@ -28,7 +29,7 @@ func fakeCloneCommand(command string, args ...string) (cmd *exec.Cmd) {
2829

2930
func TestBackup(t *testing.T) {
3031
var wg sync.WaitGroup
31-
repo := Repository{Name: "testrepo", GitURL: "git://foo.com/foo"}
32+
repo := Repository{Name: "testrepo", CloneURL: "git://foo.com/foo"}
3233
backupDir := "/tmp/backupdir"
3334

3435
// Memory FS

client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func newClient(service string, gitHostURL string) interface{} {
3030
if githubToken == "" {
3131
log.Fatal("GITHUB_TOKEN environment variable not set")
3232
}
33+
gitHostToken = githubToken
3334
ts := oauth2.StaticTokenSource(
3435
&oauth2.Token{AccessToken: githubToken},
3536
)
@@ -46,6 +47,7 @@ func newClient(service string, gitHostURL string) interface{} {
4647
if gitlabToken == "" {
4748
log.Fatal("GITLAB_TOKEN environment variable not set")
4849
}
50+
gitHostToken = gitlabToken
4951
client := gitlab.NewClient(nil, gitlabToken)
5052
if gitHostURLParsed != nil {
5153
client.SetBaseURL(gitHostURLParsed.String())

helpers.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
7+
"github.com/google/go-github/github"
8+
gitlab "github.com/xanzy/go-gitlab"
9+
)
10+
11+
func getUsername(client interface{}, service string) string {
12+
13+
if client == nil {
14+
log.Fatalf("Couldn't acquire a client to talk to %s", service)
15+
}
16+
17+
if service == "github" {
18+
ctx := context.Background()
19+
user, _, err := client.(*github.Client).Users.Get(ctx, "")
20+
if err != nil {
21+
log.Fatal("Error retrieving username", err.Error())
22+
}
23+
return *user.Name
24+
}
25+
26+
if service == "gitlab" {
27+
user, _, err := client.(*gitlab.Client).Users.CurrentUser()
28+
if err != nil {
29+
log.Fatal("Error retrieving username", err.Error())
30+
}
31+
return user.Username
32+
}
33+
34+
return ""
35+
}

main.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import (
1010
// concurrent git clones
1111
var MaxConcurrentClones = 20
1212

13+
var gitHostToken string
14+
var useHTTPSClone *bool
15+
var ignorePrivate *bool
16+
var gitHostUsername string
17+
1318
func main() {
1419

1520
// Used for waiting for all the goroutines to finish before exiting
@@ -26,6 +31,8 @@ func main() {
2631
service := flag.String("service", "", "Git Hosted Service Name (github/gitlab)")
2732
githostURL := flag.String("githost.url", "", "DNS of the custom Git host")
2833
backupDir := flag.String("backupdir", "", "Backup directory")
34+
ignorePrivate = flag.Bool("ignore-private", false, "Ignore private repositories/projects")
35+
useHTTPSClone = flag.Bool("use-https-clone", false, "Use HTTPS for cloning instead of SSH")
2936

3037
// GitHub specific flags
3138
githubRepoType := flag.String("github.repoType", "all", "Repo types to backup (all, owner, member)")
@@ -42,6 +49,12 @@ func main() {
4249
*backupDir = setupBackupDir(*backupDir, *service, *githostURL)
4350
tokens := make(chan bool, MaxConcurrentClones)
4451
client := newClient(*service, *githostURL)
52+
53+
gitHostUsername = getUsername(client, *service)
54+
55+
if len(gitHostUsername) == 0 && !*ignorePrivate && *useHTTPSClone {
56+
log.Fatal("Your Git host's username is needed for backing up private repositories via HTTPS")
57+
}
4558
repos, err := getRepositories(client, *service, *githubRepoType, *gitlabRepoVisibility, *gitlabProjectMembership)
4659
if err != nil {
4760
log.Fatal(err)

repositories.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ type Response struct {
3030
// Repository is a container for the details for a repository
3131
// we will backup
3232
type Repository struct {
33-
GitURL string
33+
CloneURL string
3434
Name string
3535
Namespace string
36+
Private bool
3637
}
3738

3839
func getRepositories(client interface{}, service string, githubRepoType string, gitlabRepoVisibility string, gitlabProjectType string) ([]*Repository, error) {
@@ -42,6 +43,7 @@ func getRepositories(client interface{}, service string, githubRepoType string,
4243
}
4344

4445
var repositories []*Repository
46+
var cloneURL string
4547

4648
if service == "github" {
4749
ctx := context.Background()
@@ -51,7 +53,12 @@ func getRepositories(client interface{}, service string, githubRepoType string,
5153
if err == nil {
5254
for _, repo := range repos {
5355
namespace := strings.Split(*repo.FullName, "/")[0]
54-
repositories = append(repositories, &Repository{GitURL: *repo.GitURL, Name: *repo.Name, Namespace: namespace})
56+
if useHTTPSClone != nil && *useHTTPSClone {
57+
cloneURL = *repo.CloneURL
58+
} else {
59+
cloneURL = *repo.SSHURL
60+
}
61+
repositories = append(repositories, &Repository{CloneURL: cloneURL, Name: *repo.Name, Namespace: namespace, Private: *repo.Private})
5562
}
5663
} else {
5764
return nil, err
@@ -103,7 +110,12 @@ func getRepositories(client interface{}, service string, githubRepoType string,
103110
if err == nil {
104111
for _, repo := range repos {
105112
namespace := strings.Split(repo.PathWithNamespace, "/")[0]
106-
repositories = append(repositories, &Repository{GitURL: repo.SSHURLToRepo, Name: repo.Name, Namespace: namespace})
113+
if useHTTPSClone != nil && *useHTTPSClone {
114+
cloneURL = repo.WebURL
115+
} else {
116+
cloneURL = repo.SSHURLToRepo
117+
}
118+
repositories = append(repositories, &Repository{CloneURL: cloneURL, Name: repo.Name, Namespace: namespace, Private: repo.Public})
107119
}
108120
} else {
109121
return nil, err

repositories_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"testing"
1212

1313
"github.com/google/go-github/github"
14-
"github.com/xanzy/go-gitlab"
14+
gitlab "github.com/xanzy/go-gitlab"
1515
)
1616

1717
var (
@@ -57,15 +57,15 @@ func TestGetGitHubRepositories(t *testing.T) {
5757
defer teardown()
5858

5959
mux.HandleFunc("/user/repos", func(w http.ResponseWriter, r *http.Request) {
60-
fmt.Fprint(w, `[{"full_name": "test/r1", "id":1, "git_url": "git://github.com/u/r1", "name": "r1"}]`)
60+
fmt.Fprint(w, `[{"full_name": "test/r1", "id":1, "ssh_url": "https://github.com/u/r1", "name": "r1", "private": false}]`)
6161
})
6262

6363
repos, err := getRepositories(GitHubClient, "github", "all", "", "")
6464
if err != nil {
6565
t.Fatalf("%v", err)
6666
}
6767
var expected []*Repository
68-
expected = append(expected, &Repository{Namespace: "test", GitURL: "git://github.com/u/r1", Name: "r1"})
68+
expected = append(expected, &Repository{Namespace: "test", CloneURL: "https://github.com/u/r1", Name: "r1", Private: false})
6969
if !reflect.DeepEqual(repos, expected) {
7070
t.Errorf("Expected %+v, Got %+v", expected, repos)
7171
}
@@ -76,15 +76,15 @@ func TestGetGitLabRepositories(t *testing.T) {
7676
defer teardown()
7777

7878
mux.HandleFunc("/api/v4/projects", func(w http.ResponseWriter, r *http.Request) {
79-
fmt.Fprint(w, `[{"path_with_namespace": "test/r1", "id":1, "ssh_url_to_repo": "git://gitlab.com/u/r1", "name": "r1"}]`)
79+
fmt.Fprint(w, `[{"path_with_namespace": "test/r1", "id":1, "ssh_url_to_repo": "https://gitlab.com/u/r1", "name": "r1"}]`)
8080
})
8181

82-
repos, err := getRepositories(GitLabClient, "gitlab", "internal", "","")
82+
repos, err := getRepositories(GitLabClient, "gitlab", "internal", "", "")
8383
if err != nil {
8484
t.Fatalf("%v", err)
8585
}
8686
var expected []*Repository
87-
expected = append(expected, &Repository{Namespace: "test", GitURL: "git://gitlab.com/u/r1", Name: "r1"})
87+
expected = append(expected, &Repository{Namespace: "test", CloneURL: "https://gitlab.com/u/r1", Name: "r1"})
8888
if !reflect.DeepEqual(repos, expected) {
8989
for i := 0; i < len(repos); i++ {
9090
t.Errorf("Expected %+v, Got %+v", expected[i], repos[i])

0 commit comments

Comments
 (0)