使用 Golang 备份 GitHub 和 GitLab 仓库

作者:Amit Saha

想要学习 Golang 并构建一些有用的东西吗? 学习如何编写一个工具来备份您的 GitHub 和 GitLab 仓库。

GitHub 和 GitLab 是两个流行的 Git 仓库托管服务,用于托管和管理开源项目。 它们也已成为内容创作者邀请他人共享和协作的便捷方式,而无需搭建自己的基础设施。

然而,使用您自己不管理的托管服务也存在缺点。 系统会发生故障,服务会宕机,磁盘会崩溃。 托管在远程服务上的内容可能会凭空消失。 如果您能有一种简单的方法定期将您的 git 仓库备份到您控制的地方,那岂不是很好吗?

如果您按照本文进行操作,您将编写一个 Golang 程序,用于备份来自 GitHubGitLab(包括自定义 GitLab 安装)的 git 仓库。 熟悉 Golang 基础知识会有所帮助,但不是必需的。 让我们开始吧!

你好 Golang

在撰写本文时,Golang 的最新稳定版本是 1.8。 包名称通常是 golang,但如果您的 Linux 发行版没有此版本,您可以下载 Golang 编译器和其他 Linux 工具。 下载后,将其解压到 /usr/local


$ sudo tar -C /usr/local -xzf <filename-from-above>
$ export PATH=$PATH:/usr/local/go/bin

打开一个新的终端并输入 $ go version 应该会显示以下内容


$ go version
go version go1.8 linux/amd64

让我们编写您的第一个程序。 清单 1 展示了一个程序,该程序在运行时需要一个 -name 标志(或参数),并使用指定的名称打印问候语。 编译并运行该程序,如下所示


$ go build listing1.go
$ ./listing1 -name "Amit"
Hello Amit

$ ./listing1
./listing1
2017/02/18 22:48:25 Please specify your name using -name
$ echo $?
1

如果您不指定 -name 参数,它将退出并打印一条带有非零退出代码的消息。 您可以使用 go run 将编译和运行程序结合起来


$ go run listing1.go -name Amit
2017/03/04 23:08:11 Hello Amit

清单 1. 示例程序 listing1.go


package main

import (
    "flag"
    "log"
)

func main() {
    name := flag.String("name", "", "Your Name")
    flag.Parse()

    if len(*name) != 0 {
            log.Printf("Hello %s", *name)
    } else {
            log.Fatal("Please specify your name using -name")
    }
}

程序中的第一行声明了程序的包。 main 包是特殊的,任何可执行的 Go 程序都必须位于 main 包中。 接下来,程序使用 import 语句从 Golang 标准库导入了两个包


import (
        "flag"
        "log"
)

"flag" 包用于处理程序的命令行参数,而 "log" 包用于日志记录。

接下来,程序定义了程序执行开始的 main() 函数


func main() {
    name := flag.String("name", "", "Your Name")
    flag.Parse()

    if len(*name) != 0 {
        log.Printf("Hello %s", *name)
    } else {
        log.Fatal("Please specify your name using -name")
    }
}

与您将编写的其他函数不同,main 函数不返回任何内容,也不接受任何参数。 上面 main() 函数中的第一个语句定义了一个字符串标志 "name",默认值为空字符串,帮助消息为 "Your Name"。 该函数的返回值是一个字符串指针,存储在变量 name 中。 := 是声明变量的简写符号,其类型是从分配给它的值推断出来的。 在这种情况下,它的类型是 *string——对字符串值的引用或指针。

Parse() 函数解析标志,并通过返回的指针使指定的标志值可用。 如果在执行程序时为 "-name" 标志提供了值,则该值将存储在 "name" 中,并且可以通过 *name 访问(回想一下 name 是一个字符串指针)。 因此,您可以检查通过 name 引用的字符串的长度是否为非零,如果是,则通过 log 包的 Printf() 函数打印问候语。 但是,如果没有指定值,则使用 Fatal() 函数打印消息。 Fatal() 函数打印指定的消息并终止程序执行。

结构体、切片和映射

清单 2 中显示的程序演示了以下不同的内容

  • 定义结构体数据类型。
  • 创建映射。
  • 创建切片并对其进行迭代。
  • 定义用户自定义函数。

清单 2. 结构体、切片和映射示例


package main


import (
    "log"
)

type Repository struct {
    GitURL string
    Name   string
}

func getRepo(id int) Repository {
    repos := map[int]Repository{
        1: Repository{GitURL: "ssh://github.com/amitsaha/gitbackup",
         ↪Name: "gitbackup"},
        2: Repository{GitURL: "ssh://github.com/amitsaha/lj_gitbackup",
         ↪Name: "lj_gitbackup"},
    }

    return repos[id]
}

func backUp(r *Repository) {
    log.Printf("Backing up %s\n", r.Name)
}

func main() {
    var repositories []Repository
    repositories = append(repositories, getRepo(1))
    repositories = append(repositories, getRepo(2))
    repositories = append(repositories, getRepo(3))

    for _, r := range repositories {
            if (Repository{}) != r {
                    backUp(&r)
            }
    }
}

在开始时,您定义了一个新的结构体数据类型 Repository,如下所示


type Repository struct {
    GitURL string
    Name   string
}

结构体 Repository 有两个成员:GitURLName,类型均为 string。 您可以使用 r := Repository{"git+ssh://git.mydomain.com/myrepo", "myrepo"} 定义此结构体类型的变量。 您可以选择在定义结构体变量时忽略一个或两个成员。 例如,您可以使用 r := Repository{Name: "myrepo"} 忽略 GitURL,甚至可以同时忽略两者。 当您忽略一个成员时,该值默认为该类型的零值——int 类型为 0,string 类型为空字符串。

接下来,您定义一个函数 getRepo,它接受一个整数作为参数,并返回类型为 Repository 的值


func getRepo(id int) Repository {
    repos := map[int]Repository{
        1: Repository{GitURL: "git+ssh://github.com/amitsaha/gitbackup",
 ↪Name: "gitbackup"},
        2: Repository{GitURL:
 ↪"git+ssh://github.com/amitsaha/lj_gitbackup", Name: "lj_gitbackup"},
    }

    return repos[id]
}

getRepo() 函数中,您创建一个映射或哈希表,其中包含键值对——键是整数,值是 Repository 类型。 该映射使用两个键值对进行初始化。

该函数返回与指定整数对应的 Repository。 如果在映射中找不到指定的键,则返回该值类型的零值。 在这种情况下,如果提供了 1 或 2 以外的整数,则返回类型为 Repository 的值,并且两个成员都设置为空字符串。

接下来,您定义一个函数 backUp(),它接受指向 Repository 类型变量的指针作为参数,并打印仓库的 Name。 在最终程序中,此函数实际上将创建仓库的备份。

最后,是 main() 函数


func main() {
    var repositories []Repository
    repositories = append(repositories, getRepo(1))
    repositories = append(repositories, getRepo(2))
    repositories = append(repositories, getRepo(3))

    for _, r := range repositories {
        if (Repository{}) != r {
            backUp(&r)
        }
    }
}

在第一个语句中,您创建了一个切片 repositories,它将存储 Repository 类型的元素。 Golang 中的切片是动态大小的数组——类似于 Python 中的列表。 然后,您调用 getRepo() 函数以获取与键 1 对应的仓库,并使用 append() 函数将返回的值存储在 repositories 切片中。 在接下来的两个语句中,您也做了同样的事情。 当您使用键 3 调用 getRepo() 函数时,您将获得类型为 Repository 的空值。

然后,您使用带有 range 子句的 for 循环来迭代切片 repositories 的元素。 切片中元素的索引存储在 _ 变量中,元素本身通过 r 变量引用。 您检查元素是否不是空的 Repository 变量,如果不是,则调用 backUp() 函数,并传递元素的地址。 值得一提的是,没有理由传递元素的地址; 您可以传递元素的值本身。 但是,当结构体具有大量成员时,按地址传递是一种很好的实践。

当您构建并运行此程序时,您将看到以下输出


$ go run listing2.go
2017/02/19 19:44:32 Backing up gitbackup
2017/02/19 19:44:32 Backing up lj_gitbackup

Goroutine 和通道

考虑之前的程序(清单 2)。 您串行地使用仓库列表中的每个仓库调用 backUp() 函数。 当您实际创建大量仓库的备份时,串行执行可能会很慢。 由于每个仓库备份都独立于任何其他备份,因此它们可以并行运行。 Golang 使在一个程序中使用 goroutine 非常容易地拥有多个同时执行的单元。

goroutine 是其他编程语言所说的轻量级线程或绿色线程。 默认情况下,据说 Golang 程序在主 goroutine 中执行,主 goroutine 可以派生其他 goroutine。 主 goroutine 可以等待所有派生的 goroutine 完成,然后再使用 WaitGroup 类型的变量完成,您将在下面看到。

清单 3 修改了之前的程序,使得 backUp() 函数在 goroutine 中调用。 main() 函数声明了一个 wg 变量,其类型为在 sync 包中定义的 WaitGroup,然后设置了对该变量的 Wait() 函数的延迟调用。 defer 语句用于在当前函数返回之前执行任何函数。 因此,您可以确保在退出程序之前等待所有 goroutine 完成。

清单 3. Goroutine 示例


package main

import (
    "log"
    "sync"
)

type Repository struct {
    GitURL string
    Name   string
}

func getRepo(id int) Repository {

    repos := map[int]Repository{
            1: Repository{GitURL: "ssh://github.com/amitsaha/gitbackup",
             ↪Name: "gitbackup"},
            2: Repository{GitURL: "ssh://github.com/amitsaha/
            ↪lj_gitbackup", Name: "lj_gitbackup"},
    }

    return repos[id]
}

func backUp(r *Repository, wg *sync.WaitGroup) {
    defer wg.Done()
    log.Printf("Backing up %s\n", r.Name)
}

func main() {
    var wg sync.WaitGroup
    defer wg.Wait()

    var repositories []Repository
    repositories = append(repositories, getRepo(1))
    repositories = append(repositories, getRepo(2))
    repositories = append(repositories, getRepo(3))

    for _, r := range repositories {
            if (Repository{}) != r {
                    wg.Add(1)
                    go func(r Repository) {
                            backUp(&r, &wg)
                    }(r)
            }
    }
}

main() 函数中的另一个主要更改是您调用 backUp() 函数的方式。 您不是直接调用此函数,而是像下面这样在新 goroutine 中调用它


wg.Add(1)
go func(r Repository) {
        backUp(&r, &wg)
}(r)

您使用参数 1 调用 Add() 函数,以指示您将创建一个新的 goroutine,并且希望在退出之前等待该 goroutine 完成。 然后,您定义一个匿名函数,该函数接受一个类型为 Repository 的参数 r,该函数调用带有附加参数的函数 backUp(),即对变量 wg(之前声明的 WaitGroup 变量)的引用。

考虑这样一种情况,即您的仓库列表中有大量元素——对于此备份工具来说,这是一个非常现实的场景。 为仓库中的每个元素派生一个 goroutine 很容易导致并发运行的 goroutine 数量不受控制。 这可能导致程序达到操作系统施加的每个进程内存和文件描述符限制。

因此,您可能希望 регулировать 程序派生的 goroutine 的最大数量,并且仅当正在执行的 goroutine 完成时才派生新的 goroutine。 Golang 中的通道允许您实现这一点以及 goroutine 之间的其他同步操作。 清单 4 显示了如何 регулировать 派生的 goroutine 的最大数量。

清单 4. 通道示例


package main

import (
    "log"
    "sync"
)

type Repository struct {
    GitURL string
    Name   string
}

func getRepo(id int) Repository {

    repos := map[int]Repository{
            1:  Repository{GitURL: "ssh://github.com/amitsaha/gitbackup",
             ↪Name: "gitbackup"},
            2:  Repository{GitURL: "ssh://github.com/amitsaha/
            ↪lj_gitbackup", Name: "lj_gitbackup"},
            3:  Repository{GitURL: "ssh://github.com/amitsaha/gitbackup",
             ↪Name: "gitbackup"},
            4:  Repository{GitURL: "ssh://github.com/amitsaha/
            ↪lj_gitbackup", Name: "lj_gitbackup"},
            5:  Repository{GitURL: "ssh://github.com/amitsaha/gitbackup",
             ↪Name: "gitbackup"},
            6:  Repository{GitURL: "ssh://github.com/amitsaha/
            ↪lj_gitbackup", Name: "lj_gitbackup"},
            7:  Repository{GitURL: "ssh://github.com/amitsaha/gitbackup",
             ↪Name: "gitbackup"},
            8:  Repository{GitURL: "ssh://github.com/amitsaha/
            ↪lj_gitbackup", Name: "lj_gitbackup"},
            9:  Repository{GitURL: "ssh://github.com/amitsaha/gitbackup",
             ↪Name: "gitbackup"},
            10: Repository{GitURL: "ssh://github.com/amitsaha/
            ↪lj_gitbackup", Name: "lj_gitbackup"},
    }

    return repos[id]
}

func backUp(r *Repository, wg *sync.WaitGroup) {
    defer wg.Done()
    log.Printf("Backing up %s\n", r.Name)
}

func main() {
    var wg sync.WaitGroup
    defer wg.Wait()

    var repositories []Repository
    repositories = append(repositories, getRepo(1))
    repositories = append(repositories, getRepo(2))
    repositories = append(repositories, getRepo(3))
    repositories = append(repositories, getRepo(4))
    repositories = append(repositories, getRepo(5))
    repositories = append(repositories, getRepo(6))
    repositories = append(repositories, getRepo(7))
    repositories = append(repositories, getRepo(8))
    repositories = append(repositories, getRepo(9))
    repositories = append(repositories, getRepo(10))

    // Create a channel of capacity 5
    tokens := make(chan bool, 5)

    for _, r := range repositories {
            if (Repository{}) != r {
                    wg.Add(1)
                    // Get a token
                    tokens <- true
                    go func(r Repository) {
                            backUp(&r, &wg)
                            // release the token
                            <-tokens
                    }(r)
            }
    }
}

您创建一个容量为 5 的通道,并使用它来实现令牌系统。 该通道是使用 make 创建的


tokens := make(chan bool, 5)

上面的语句创建了一个“缓冲通道”——一个容量为 5 并且只能存储 “bool” 类型值的通道。 如果缓冲通道已满,则对其写入将阻塞;如果通道为空,则从中读取将阻塞。 此属性允许您实现令牌系统。

在您可以派生 goroutine 之前,您需要写入一个布尔值 true 到其中(“获取”令牌),然后在完成操作后将其取回(“释放”令牌)。 如果通道已满,则意味着最大数量的 goroutine 已经在运行,因此,您的写入尝试将阻塞,并且不会派生新的 goroutine。 写入操作通过以下方式执行


tokens <- true

在控制从 backUp() 函数返回后,您从通道读取一个值,从而释放令牌


<-tokens

上述机制确保您永远不会同时运行超过五个 goroutine,并且每个 goroutine 在退出之前都会释放其令牌,以便下一个 goroutine 可以运行。 本文末尾提到的 GitHub 仓库中的文件 listing5.go 使用 runtime 包来打印使用此机制运行的 goroutine 的数量,本质上允许您验证您的实现。

gitbackup——备份 GitHub 和 GitLab 仓库

到目前为止的示例程序中,我还没有探索使用任何第三方包。 尽管 Golang 的内置工具完全支持应用程序使用第三方仓库,但您将使用名为 gb 的工具来开发您的“gitbackup”项目。 我喜欢 gb 的一个主要原因是它通过其 “vendor” 插件非常容易地获取和更新第三方依赖项。 它还消除了将您的 go 应用程序放在 GOPATH 中的需要,这是内置 go 工具假设的要求。

接下来,您将获取并构建 gb


$ go get github.com/constabulary/gb/...

编译后的二进制文件 gb 放置在目录 $GOPATH/bin 中。 您将 $GOPATH/bin 添加到 $PATH 环境变量,并启动一个新的 shell 会话并输入 gb


$ gb
gb, a project based build tool for the Go programming language.

Usage:

     gb command [arguments]
     ..

接下来,安装 gb-vendor 插件


$ go get github.com/constabulary/gb/cmd/gb-vendor

gb 基于项目的概念工作。 一个项目在其内部有一个 “src” 子目录,其中包含一个或多个包,这些包位于它们自己的子目录中。 从 https://github.com/amitsaha/gitbackup 克隆 “gitbackup” 项目,您将注意到以下目录结构


$ tree -L 1 gitbackup
gitbackup
|--src
|  |--gitbackup
           |--main.go
        |--main_test.go
        |--remote.go
    ..

“gitbackup” 应用程序仅由一个包 “gitbackup” 组成,它有两个程序文件和单元测试。 让我们先看一下 remote.go 文件。 在一开始,除了标准库中的一些包之外,您还导入了第三方仓库

  • github.com/google/go-github/github:这是 GitHub API 的 Golang 接口。
  • golang.org/x/oauth2:用于向 GitHub API 发送经过身份验证的请求。
  • github.com/xanzy/go-gitlab:GitLab API 的 Golang 接口。

您定义了一个 Response 类型的结构体,它与上面 GitHub 和 GitLab 库实现的 Response 结构体匹配。 结构体 Repository 描述了您从 GitLab 或 GitHub 获取的每个仓库。 它有两个字符串字段:GitURL,表示仓库的 git 克隆 URL;以及 Name,仓库的名称。

NewClient() 函数接受服务名称(githubgitlab)作为参数,并返回相应的客户端,然后该客户端将用于与服务进行交互。 此函数的返回类型是 interface{},这是一种特殊的 Golang 类型,表示此函数可以返回任何类型的值。 根据指定的服务名称,它将是 *github.Client*gitlab.Client 类型。 如果指定了不同的服务名称,它将返回 nil。 为了能够在备份仓库之前获取仓库列表,您需要通过环境变量指定访问令牌。

GitLab 的令牌通过 GITLAB_TOKEN 环境变量指定,GitHub 的令牌通过 GITHUB_TOKEN 环境变量指定。 在此函数中,您使用 os 包中的 Getenv() 函数检查是否已指定正确的环境变量。 如果指定了环境变量,该函数将返回环境变量的值,如果未找到指定的环境变量,则返回空字符串。 如果未找到相应的环境变量,您将记录一条消息并使用 log 包中的 Fatal() 函数退出。

NewClient() 函数由 getRepositories() 函数使用,后者返回通过 API 调用服务获得的 Repository 对象切片。 该函数中有两个条件块,用于处理两个支持的服务。 第一个条件块通过 github.com/gooogle/go-github 包实现的 Repositories.List() 函数处理 GitHub 的仓库列表。 此函数的第一个参数是您要获取其仓库的 GitHub 用户名。 如果您将其留空字符串,它将返回当前经过身份验证的用户的仓库。 此选项的第二个参数是 github.RepositoryListOptions 类型的值,它允许您通过 Type 字段指定您要返回的仓库类型。 对 Repositories.List() 函数的调用如下


repos, resp, err := client.(*github.Client)
↪.Repositories.List("", &options)

回想一下,newClient() 函数返回类型为 interface{} 的值,这是一个空接口。 因此,如果您尝试将函数调用设为 client.Repositories.List(),编译器将报错


# gitbackup
remote.go:70: client.Repositories undefined (type interface {}
 ↪is interface with no methods)

因此,您需要执行“类型断言”,通过该断言,您可以访问客户端的底层值,该值是 *github.Client*gitlab.Client 类型之一。

您在由 for 循环指示的无限循环中从服务查询仓库列表


for {
        // This is an infinite loop
}

该函数返回三个值:第一个是仓库列表,第二个是 Response 类型的对象,第三个是错误值。 如果函数调用成功,则 err 的值为 nil。 然后,您迭代每个返回的对象,创建一个包含您关心的两个字段的 Repository 对象,并将其附加到切片仓库。 一旦您用尽了返回的仓库列表,您将检查 resp 对象的 NextPage 字段,以检查它是否等于 0。 如果它等于 0,您就知道没有什么可读的了; 您从循环中跳出,并从函数返回您到目前为止拥有的仓库列表。 如果您有一个非零值,则您有更多仓库,因此您将 ListOptions 结构体中的 Page 字段设置为此值


options.ListOptions.Page = resp.NextPage

“gitlab” 服务的处理程序与 “github” 服务几乎相同,但有一个额外的细节。“gitlab” 是一个开源项目,您可以在自己的主机上运行自定义安装。 您可以通过以下代码在此处处理它


if len(gitlabUrl) != 0 {
    gitlabUrlPath, err := url.Parse(gitlabUrl)
    if err != nil {
        log.Fatal("Invalid gitlab URL: %s", gitlabUrl)
    }
    gitlabUrlPath.Path = path.Join(gitlabUrlPath.Path, "api/v3")
    client.(*gitlab.Client).SetBaseURL(gitlabUrlPath.String())
}

如果 gitlabUrl 中的值是非空字符串,则您假设您需要查询托管在此 URL 的 GitLab。 您首先尝试使用 “url” 包中的 Parse() 函数对其进行解析,如果解析失败,则退出并显示错误消息。 GitLab API 位于 <gitlab 安装的 DNS>/api/v3,因此您更新解析后的 URL 的 Path 对象,然后调用 gitlab.Client 的函数 SetBaseURL() 将其设置为基本 URL。

接下来,让我们看一下 main.go 文件。 不过首先,您应该了解 “gitbackup” 在哪里创建 git 仓库的备份。 您可以通过 -backupdir 标志传递位置。 如果未指定,则默认为 $HOME/.gitbackup。 让我们将其称为 BACKUP_DIR。 仓库备份在 BACKUP_DIR/gitlab/ 或 BACKUP_DIR/github 中。 如果在 BACKUP_DIR/<service_name>/<repo> 中找不到仓库,您就知道您必须对仓库进行新的克隆(git clone)。 如果仓库存在,则更新它 (git pull)。 此操作在 main.go 的 backUp() 函数中执行


func backUp(backupDir string, repo *Repository, wg *sync.WaitGroup) {
    defer wg.Done()

    repoDir := path.Join(backupDir, repo.Name)
    _, err := os.Stat(repoDir)

    if err == nil {
        log.Printf("%s exists, updating. \n", repo.Name)
        cmd := exec.Command("git", "-C", repoDir, "pull")
        err = cmd.Run()
        if err != nil {
            log.Printf("Error pulling %s: %v\n", repo.GitURL, err)
        }
    } else {
        log.Printf("Cloning %s \n", repo.Name)
        cmd := exec.Command("git", "clone", repo.GitURL, repoDir)
        err := cmd.Run()
        if err != nil {
            log.Printf("Error cloning %s: %v", repo.Name, err)
        }
    }
}

该函数接受三个参数:第一个是指向备份目录位置的字符串,后跟对 Repository 对象的引用和对 WaitGroup 的引用。 您在 WaitGroup 上设置了对 Done() 的延迟调用。 接下来的两行然后使用 os 包中的 Stat() 函数检查仓库是否已存在于备份目录中。 如果目录存在,此函数将返回 nil 错误值,因此您可以使用 exec 包中的 Command() 函数执行 git pull 命令。 如果目录不存在,则改为执行 git clone 命令。

main() 函数为 “gitbackup” 程序设置标志

  • backupdir:备份目录。 如果未指定,则默认为 $HOME/.gitbackup。
  • github.repoType:要备份的 GitHub 仓库类型; all 将备份您的所有仓库。 其他选项是 ownermember
  • gitlab.projectVisibility:要克隆的 GitLab 项目的可见性级别。 它默认为 internal,指的是任何登录用户都可以克隆的项目。 其他选项是 publicprivate
  • gitlab.url GitLab 服务的 DNS。 如果您要创建自定义 GitLab 安装上的仓库备份,则只需指定此选项,而忽略指定 “service” 选项。
  • service:从中备份仓库的 Git 服务的服务名称。 目前,它识别 “gitlab” 和 “github”。

main() 函数中,如果未指定 backupdir,则默认使用 $HOME/.gitbackup/<service_name> 目录。 要查找主目录,请使用 github.com/mitchellh/go-homedir 包。 在任何一种情况下,如果目录树不存在,您都使用 MkdirAll() 函数创建它。

然后,您调用 remote.go 中定义的 getRepositories() 函数来获取要备份的仓库列表。 使用我之前描述的令牌系统将并发克隆的最大数量限制为 20。

现在,让我们从您之前创建的 “gitbackup” 仓库的克隆版本构建并运行该项目


$ pwd
/Users/amit/work/github.com/amitsaha/gitbackup
$ gb build
..
$ ./bin/gitbackup -help
Usage of ./bin/gitbackup:
  -backupdir string
        Backup directory
  -github.repoType string
        Repo types to backup (all, owner, member) (default "all")
  -gitlab.projectVisibility string
        Visibility level of Projects to clone (default "internal")
  -gitlab.url string
        DNS of the GitLab service
  -service string
        Git Hosted Service Name (github/gitlab)

在您可以备份 GitHub 或 GitLab 的仓库之前,您需要为每个仓库获取访问令牌。 为了能够备份 GitHub 仓库,请从 此处 获取仅具有 “repo” 作用域的 GitHub 个人访问令牌。 对于 GitLab,您可以从 https://<gitlab 位置>/profile/personal_access_tokens 获取具有 “api” 作用域的访问令牌。

以下命令将备份 github 中的所有仓库


$ GITHUB_TOKEN=my$token ./bin/gitbackup -service github

同样,要将 GitLab 安装中的仓库备份到自定义位置,请执行以下操作


$ GITLAB_TOKEN=my$token ./bin/gitbackup -gitlab.url
 ↪git.mydomain.com -backupdir /mnt/disk/gitbackup

请参阅 此处 的 README 了解更多信息,我欢迎通过 pull 请求对其进行改进。 在撰写本文和发布本文之间的时间里,gitbackup 发生了一些变化。 本文讨论的代码可在标签 https://github.com/amitsaha/gitbackup/releases/tag/lj-0.1 中找到。 要了解自此标签以来仓库当前版本中的更改,请参阅我的 博客文章

结论

在本文中,我介绍了一些关键的 Golang 功能,并将它们应用于编写一个工具来备份 GitHub 和 GitLab 的仓库。 在此过程中,我探索了接口、goroutine 和通道,通过标志传递命令行参数以及使用第三方包。

本文中讨论的代码清单可在 此处 获得。 请参阅“资源”部分,以了解有关 Golang、GitHub 和 GitLab API 的更多信息。

资源

Golang 和 gb 入门:http://bit.ly/2lKEJJm

Golang 示例:https://gobyexample.golang.ac.cn

Golang 类型断言:https://golang.ac.cn/doc/effective_go.html#interface_conversions

GitHub 仓库 API:https://developer.github.com/v3/repos

GitLab 项目 API:https://docs.gitlab.com/ce/api/projects.html

GitHub 的 Golang 接口:https://github.com/google/go-github

GitLab 的 Golang 接口:https://github.com/xanzy/go-gitlab

gb:https://getgb.io

加载 Disqus 评论