github_records_archiver 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.gitignore +4 -3
- data/.rubocop.yml +2 -1
- data/Gemfile +2 -0
- data/README.md +55 -21
- data/bin/github-records-archiver +77 -0
- data/github_records_archiver.gemspec +1 -0
- data/lib/github_records_archiver.rb +24 -8
- data/lib/github_records_archiver/git_repository.rb +11 -3
- data/lib/github_records_archiver/repository.rb +4 -6
- data/lib/github_records_archiver/version.rb +1 -1
- data/lib/github_records_archiver/wiki.rb +1 -2
- metadata +18 -3
- data/bin/archive +0 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04613c74d230d25723b56ff88a3e34c840d8a8ae56bc7543a386f0bb91fc7ca4
|
4
|
+
data.tar.gz: 0eff9d4f24718e4db5107ccfb6ffd8c45f5783f31de16b51edea0324cda20803
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0704f880ce05d5f0990f36a02bd7962452e072255d88f53d00f5878b035908a7171cf155bdb4284b6cdc8ca5d94ff89f8fbd98fd55bbe6fffb9f6b6226ff860a
|
7
|
+
data.tar.gz: 328a9351d1161903ffe67f0b3bbcacb8f1830baf87bd72a157c5306d7ab8af75c84d7df49d689d34c9745fe58c843b479573ba038ab726cd9febd4d89d824867
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# GitHub Records Archiver
|
2
2
|
|
3
|
-
[![Build Status](https://travis-ci.org/benbalter/
|
3
|
+
[![Build Status](https://travis-ci.org/benbalter/github_records_archiver.svg?branch=master)](https://travis-ci.org/benbalter/github_records_archiver) [![Gem Version](https://badge.fury.io/rb/github_records_archiver.svg)](http://badge.fury.io/rb/github_records_archiver) [![Coverage Status](https://coveralls.io/repos/github/benbalter/github_records_archiver/badge.svg)](https://coveralls.io/github/benbalter/github_records_archiver) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
|
4
4
|
|
5
5
|
Backs up a GitHub organization's repositories and all their associated information for archival purposes.
|
6
6
|
|
@@ -14,38 +14,72 @@ Backs up a GitHub organization's repositories and all their associated informati
|
|
14
14
|
## Requirements
|
15
15
|
|
16
16
|
1. Ruby
|
17
|
-
2. A GitHub [personal access token](https://github.com/settings/tokens/new) with `
|
17
|
+
2. A GitHub [personal access token](https://github.com/settings/tokens/new) with `repo` scope.
|
18
18
|
|
19
19
|
## Setup
|
20
20
|
|
21
|
-
|
22
|
-
2. `cd github-records-archiver`
|
23
|
-
3. `gem install bundler`
|
24
|
-
4. `bundle install`
|
21
|
+
If you have Ruby installed, simply run:
|
25
22
|
|
26
|
-
|
23
|
+
```shell
|
24
|
+
gem install github_records_archiver
|
25
|
+
```
|
27
26
|
|
28
|
-
|
27
|
+
## Basic usage
|
29
28
|
|
30
|
-
|
29
|
+
```shell
|
30
|
+
$ github_records_archiver archive ORGANIZATION --token PERSONAL_ACCESS_TOKEN`
|
31
|
+
```
|
32
|
+
Alternatively, you could pass the personal access token as the `GITHUB_TOKEN` environmental variable:
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
* `GITHUB_ARCHIVE_DIR` to specify the output directory. It will default to `./archive`.
|
37
|
-
* `GITHUB_ORGANIZATION` - The organization to archive if none is passed as an argument.
|
38
|
-
|
39
|
-
These can be passed as `GITHUB_TOKEN=123ABC GITHUB_ORGANIZATION=whitehouse bin/archive`.
|
40
|
-
|
41
|
-
You can also add the values to a `.env` file in the project's root directory, which will be automatically set as environmental variables.
|
34
|
+
```shell
|
35
|
+
$ GITHUB_TOKEN=1234 github_records_archiver archive ORGANIZATION`
|
36
|
+
```
|
42
37
|
|
43
38
|
## Output
|
44
39
|
|
45
|
-
The script will create an `archive` directory, with one folder for each
|
40
|
+
The script will create an `archive` directory, with one folder for each organization.
|
41
|
+
|
42
|
+
Within each organization folder, there will be one folder per repository.
|
46
43
|
|
47
|
-
Within each folder will be the repository content as a git repository.
|
44
|
+
Within each repository folder will be the repository content as a git repository.
|
48
45
|
|
49
46
|
If the repository has a Wiki, the wiki will be cloned as a `wiki` subfolder, as a Git repository.
|
50
47
|
|
51
48
|
If the repository has issues or pull requests, it will create an `issues` sub-folder with each issue and its associated comments stored as both markdown (human readable) and JSON (machine readable).
|
49
|
+
|
50
|
+
Example output:
|
51
|
+
|
52
|
+
```
|
53
|
+
├─ archive
|
54
|
+
├─── organization
|
55
|
+
├──── repository
|
56
|
+
├────── README.md
|
57
|
+
├────── LICENSE.txt
|
58
|
+
├──── wiki
|
59
|
+
├────── wiki-page.md
|
60
|
+
├──── issues
|
61
|
+
├────── 1.md
|
62
|
+
├────── 1.json
|
63
|
+
├─── another organization
|
64
|
+
├──── another-repository
|
65
|
+
├────── README.md
|
66
|
+
├────── LICENSE.txt
|
67
|
+
├──── wiki
|
68
|
+
├────── wiki-page.md
|
69
|
+
├──── issues
|
70
|
+
├────── 1.md
|
71
|
+
├────── 1.json
|
72
|
+
```
|
73
|
+
|
74
|
+
## Advanced usage
|
75
|
+
|
76
|
+
You may set the following flags:
|
77
|
+
|
78
|
+
* `--dest-dir` - the destination archive directory, defaults to `./archive`
|
79
|
+
* `--verbose` - verbose output while archiving
|
80
|
+
|
81
|
+
Additionally, the following commands are also available:
|
82
|
+
|
83
|
+
* `delete [ORGANIZATION]` - delete the entire archive directory or an organization's archive
|
84
|
+
* `help` - display help information
|
85
|
+
* `version` - display the GitHub Record Archiver version
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'parallel'
|
5
|
+
require_relative '../lib/github_records_archiver'
|
6
|
+
|
7
|
+
class GitHubRecordsArchiverCLI < Thor
|
8
|
+
package_name 'GitHub Records Archiver'
|
9
|
+
class_option :dest_dir, type: :string, required: false,
|
10
|
+
desc: "The destination directory for the archive", default: GitHubRecordsArchiver.dest_dir
|
11
|
+
class_option :token, type: :string, required: false, desc: "A GitHub personal access token with repo scope"
|
12
|
+
class_option :verbose, type: :boolean, desc: 'Display verbose output while archiving', default: false
|
13
|
+
|
14
|
+
desc 'version', 'Outputs the GitHubRecordsArchiver version'
|
15
|
+
def version
|
16
|
+
say "GitHub Records Archiver v#{GitHubRecordsArchiver::VERSION}"
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "delete [ORGANIZATION]", "Deletes all archives, or the archive for an organization"
|
20
|
+
def delete(org_name = nil)
|
21
|
+
path = GitHubRecordsArchiver.dest_dir
|
22
|
+
path = File.join path, org_name if org_name
|
23
|
+
FileUtils.rm_rf(path) if yes? "Are you sure? Remove #{path}?"
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'archive ORGANIZATION', 'Create or update archive for the given organization'
|
27
|
+
def archive(org_name)
|
28
|
+
start_time # Memoize start time for comparison
|
29
|
+
@org_name = org_name
|
30
|
+
|
31
|
+
GitHubRecordsArchiver.shell = shell
|
32
|
+
%i(token dest_dir verbose).each do |option|
|
33
|
+
next unless options[option]
|
34
|
+
GitHubRecordsArchiver.public_send "#{option}=".to_sym, options[option]
|
35
|
+
end
|
36
|
+
|
37
|
+
say "Starting archive for @#{org.name} in #{org.archive_dir}"
|
38
|
+
shell.indent(2) do
|
39
|
+
archive_teams
|
40
|
+
archive_repos
|
41
|
+
end
|
42
|
+
say "Done in #{Time.now - start_time} seconds.", :green
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def start_time
|
48
|
+
@start_time ||= Time.now
|
49
|
+
end
|
50
|
+
|
51
|
+
def organization
|
52
|
+
@organization ||= GitHubRecordsArchiver::Organization.new @org_name
|
53
|
+
end
|
54
|
+
alias_method :org, :organization
|
55
|
+
|
56
|
+
def archive_teams
|
57
|
+
say_status "Teams found:", org.teams.count, :white
|
58
|
+
Parallel.each(org.teams, progress: 'Archiving teams', &:archive)
|
59
|
+
end
|
60
|
+
|
61
|
+
def archive_repos
|
62
|
+
say_status "Repositories found:", org.repos.count, :white
|
63
|
+
|
64
|
+
Parallel.each(org.repos, progress: 'Archiving repos') do |repo|
|
65
|
+
begin
|
66
|
+
repo.clone
|
67
|
+
repo.wiki.clone if repo.has_wiki?
|
68
|
+
Parallel.each(repo.issues, &:archive)
|
69
|
+
rescue GitHubRecordsArchiver::GitError => e
|
70
|
+
say "Failed to archive #{repo.name}", :red
|
71
|
+
say e.message, :red
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
GitHubRecordsArchiverCLI.start(ARGV)
|
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.add_dependency 'octokit', '~> 4.0'
|
29
29
|
spec.add_dependency 'parallel', '~> 1.10'
|
30
30
|
spec.add_dependency 'ruby-progressbar', '~> 1.0'
|
31
|
+
spec.add_dependency 'thor', '~> 0.19'
|
31
32
|
spec.add_development_dependency 'addressable', '~> 2.5'
|
32
33
|
spec.add_development_dependency 'bundler', '~> 1.16'
|
33
34
|
spec.add_development_dependency 'pry', '~> 0.10'
|
@@ -1,18 +1,14 @@
|
|
1
1
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
2
|
|
3
|
-
# stdlib
|
4
3
|
require 'yaml'
|
5
4
|
require 'json'
|
6
5
|
require 'logger'
|
7
6
|
require 'fileutils'
|
8
7
|
require 'open3'
|
9
|
-
|
10
|
-
# gems
|
8
|
+
require 'thor'
|
11
9
|
require 'octokit'
|
12
|
-
require 'dotenv'
|
10
|
+
require 'dotenv/load'
|
13
11
|
|
14
|
-
# Configuration
|
15
|
-
Dotenv.load
|
16
12
|
Octokit.auto_paginate = true
|
17
13
|
|
18
14
|
module GitHubRecordsArchiver
|
@@ -28,8 +24,10 @@ module GitHubRecordsArchiver
|
|
28
24
|
autoload :Wiki, 'github_records_archiver/wiki'
|
29
25
|
|
30
26
|
class << self
|
27
|
+
attr_writer :token, :dest_dir, :verbose, :shell
|
28
|
+
|
31
29
|
def token
|
32
|
-
ENV['GITHUB_TOKEN']
|
30
|
+
@token ||= ENV['GITHUB_TOKEN']
|
33
31
|
end
|
34
32
|
|
35
33
|
def client
|
@@ -37,7 +35,25 @@ module GitHubRecordsArchiver
|
|
37
35
|
end
|
38
36
|
|
39
37
|
def dest_dir
|
40
|
-
|
38
|
+
@dest_dir ||= File.expand_path('./archive', Dir.pwd)
|
39
|
+
end
|
40
|
+
|
41
|
+
def verbose
|
42
|
+
@verbose ||= false
|
43
|
+
end
|
44
|
+
alias verbose? verbose
|
45
|
+
|
46
|
+
def shell
|
47
|
+
@shell ||= Thor::Base.shell.new
|
48
|
+
end
|
49
|
+
|
50
|
+
def verbose_status(status, message, color = :white)
|
51
|
+
return unless verbose?
|
52
|
+
shell.say_status status, remove_token(message), color
|
53
|
+
end
|
54
|
+
|
55
|
+
def remove_token(string)
|
56
|
+
string.gsub(GitHubRecordsArchiver.token, '<GITHUB_TOKEN>')
|
41
57
|
end
|
42
58
|
end
|
43
59
|
end
|
@@ -3,11 +3,13 @@ module GitHubRecordsArchiver
|
|
3
3
|
|
4
4
|
class GitRepository
|
5
5
|
def clone
|
6
|
-
|
6
|
+
# Repo already exists, just pull new objects
|
7
|
+
if Dir.exist? File.join(repo_dir, '.git')
|
7
8
|
Dir.chdir repo_dir do
|
8
9
|
git 'pull'
|
9
10
|
end
|
10
|
-
else
|
11
|
+
else
|
12
|
+
# Clone Git content from scratch
|
11
13
|
git 'clone', clone_url, repo_dir
|
12
14
|
end
|
13
15
|
end
|
@@ -43,8 +45,14 @@ module GitHubRecordsArchiver
|
|
43
45
|
# Run a git command, piping output to stdout
|
44
46
|
def git(*args)
|
45
47
|
output, status = Open3.capture2e('git', *args)
|
48
|
+
cmd = "git #{args.join(' ')}"
|
49
|
+
cmd << " in #{Dir.pwd}" if args == ['pull']
|
50
|
+
GitHubRecordsArchiver.verbose_status 'Git command:', cmd
|
46
51
|
return false if empty_repo?(output) || wiki_does_not_exist?(output)
|
47
|
-
|
52
|
+
if status.exitstatus != 0
|
53
|
+
output = GitHubRecordsArchiver.remove_token(output)
|
54
|
+
GitHubRecordsArchiver.shell.say_status 'Git Error', output, :red
|
55
|
+
end
|
48
56
|
output
|
49
57
|
end
|
50
58
|
end
|
@@ -40,15 +40,13 @@ module GitHubRecordsArchiver
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
private
|
44
|
-
|
45
|
-
def repo_dir
|
46
|
-
@repo_dir ||= File.expand_path data[:name], GitHubRecordsArchiver.dest_dir
|
47
|
-
end
|
48
|
-
|
49
43
|
def clone_url
|
50
44
|
replacement = "https://#{GitHubRecordsArchiver.token}:x-oauth-basic@"
|
51
45
|
data[:clone_url].gsub(%r{https?://}, replacement)
|
52
46
|
end
|
47
|
+
|
48
|
+
def repo_dir
|
49
|
+
@repo_dir ||= File.expand_path full_name, GitHubRecordsArchiver.dest_dir
|
50
|
+
end
|
53
51
|
end
|
54
52
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: github_records_archiver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Balter
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '1.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: thor
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.19'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.19'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: addressable
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -168,10 +182,11 @@ description:
|
|
168
182
|
email:
|
169
183
|
- ben.balter@github.com
|
170
184
|
executables:
|
171
|
-
-
|
185
|
+
- github-records-archiver
|
172
186
|
extensions: []
|
173
187
|
extra_rdoc_files: []
|
174
188
|
files:
|
189
|
+
- ".coveralls.yml"
|
175
190
|
- ".github/CODEOWNERS"
|
176
191
|
- ".github/config.yml"
|
177
192
|
- ".github/no-response.yml"
|
@@ -185,7 +200,7 @@ files:
|
|
185
200
|
- LICENSE.md
|
186
201
|
- README.md
|
187
202
|
- Rakefile
|
188
|
-
- bin/
|
203
|
+
- bin/github-records-archiver
|
189
204
|
- docs/CODE_OF_CONDUCT.md
|
190
205
|
- docs/CONTRIBUTING.md
|
191
206
|
- github_records_archiver.gemspec
|
data/bin/archive
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# Backs up a GitHub organization's repositories and
|
3
|
-
# all their associated information for archival purposes.
|
4
|
-
# Usage: ruby archive.rb
|
5
|
-
|
6
|
-
require './lib/github_records_archiver'
|
7
|
-
require 'parallel'
|
8
|
-
|
9
|
-
def logger
|
10
|
-
@logger ||= Logger.new(STDOUT)
|
11
|
-
end
|
12
|
-
|
13
|
-
def log(msg)
|
14
|
-
logger.info(msg)
|
15
|
-
end
|
16
|
-
|
17
|
-
def error(msg)
|
18
|
-
logger.error(msg)
|
19
|
-
end
|
20
|
-
|
21
|
-
archiver = GitHubRecordsArchiver
|
22
|
-
pwd = Dir.pwd
|
23
|
-
start = Time.now
|
24
|
-
org_name = ARGV[0] || ENV['GITHUB_ORGANIZATION']
|
25
|
-
org = archiver::Organization.new org_name
|
26
|
-
|
27
|
-
log "Starting archive for @#{org.name} in #{org.archive_dir}"
|
28
|
-
|
29
|
-
log "Found #{org.teams.count} teams"
|
30
|
-
Parallel.each(org.teams, progress: 'Archiving teams', &:archive)
|
31
|
-
|
32
|
-
log "Found #{org.repos.count} repos"
|
33
|
-
Parallel.each(org.repos, progress: 'Archiving repos') do |repo|
|
34
|
-
begin
|
35
|
-
repo.clone
|
36
|
-
repo.wiki.clone if repo.has_wiki?
|
37
|
-
Parallel.each(repo.issues, &:archive)
|
38
|
-
rescue GitHubRecordsArchiver::GitError => e
|
39
|
-
error "Failed to archive #{repo.name}"
|
40
|
-
error e.message
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
Dir.chdir pwd
|
45
|
-
log "Done in #{Time.now - start} seconds."
|