github_records_archiver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 20d21627161e4f5e187d555f3a2f12b925a633c6c8c3c2ca3bf28b6aee26ed18
4
+ data.tar.gz: '0805b746a6b6495fddf921a9638245286ec6d89da41dfcf6c30117f514ea8fb7'
5
+ SHA512:
6
+ metadata.gz: 448ef59368b08c0ffb7e221ecad0b0674ed5cbfb85a1dceec9f2ba3bbd5103d00694032decbf896f6febd34fb7ddae2e6324e539e4da70482e2a6df0170e6b8a
7
+ data.tar.gz: 2eaec591f98b3359ee9f3769c2797f71db0b94c85480be4f5eb29b80903fa7eccfb357c12b5616347f09fb335982aa5305ae83cd69cfe17a6fa96feef665220d
@@ -0,0 +1,3 @@
1
+ # Require @benbalter's :+1: for changes to the .github repo-config files
2
+ # mainly due to https://github.com/probot/settings privilege escalation
3
+ .github/* @benbalter
@@ -0,0 +1,20 @@
1
+ # Behaviorbot config. See https://github.com/behaviorbot/ for more information.
2
+ # Note: Please Don't edit this file directly.
3
+ # Edit https://github.com/benbalter/behaviorbot-config instead.
4
+
5
+ # Configuration for update-docs - https://github.com/behaviorbot/update-docs
6
+ updateDocsComment: "Thanks for the pull request! If you are making any changes to the user-facing functionality, please be sure to update the documentation in the `README` or `docs/` folder alongside your change. :heart:"
7
+
8
+ # Configuration for request-info - https://github.com/behaviorbot/request-info
9
+ requestInfoReplyComment: Thanks for this. Do you mind providing a bit more information about what problem you're trying to solve?
10
+ requestInfoLabelToAdd: more-information-needed
11
+
12
+ # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
13
+ #newIssueWelcomeComment: >
14
+ # Welcome!
15
+
16
+ # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
17
+ newPRWelcomeComment: Welcome! Congrats on your first pull request to GitHub Records Archiver. If you haven't already, please be sure to check out [the contributing guidelines](https://github.com/benbalter/github-records-archiver/blob/master/docs/CONTRIBUTING.md).
18
+
19
+ # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
20
+ firstPRMergeComment: "Congrats on getting your first pull request to GitHub Records Archiver merged! Without amazing humans like you submitting pull requests, we couldn’t run this project. You rock! :tada:<br /><br />If you're interested in tackling another bug or feature, take a look at [the open issues](https://github.com/benbalter/github-records-archiver/issues), especially those [labeled `help wanted`](https://github.com/benbalter/github-records-archiver/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)."
@@ -0,0 +1,13 @@
1
+ # Configuration for probot-no-response - https://github.com/probot/no-response
2
+
3
+ # Number of days of inactivity before an Issue is closed for lack of response
4
+ daysUntilClose: 14
5
+ # Label requiring a response
6
+ responseRequiredLabel: more-information-needed
7
+ # Comment to post when closing an Issue for lack of response. Set to `false` to disable
8
+ closeComment: >
9
+ This issue has been automatically closed because there has been no response
10
+ to our request for more information from the original author. With only the
11
+ information that is currently in the issue, we don't have enough information
12
+ to take action. Please reach out if you have or find the answers we need so
13
+ that we can investigate further.
@@ -0,0 +1,31 @@
1
+ # Repository settings set via https://github.com/probot/settings
2
+
3
+ repository:
4
+ has_issues: true
5
+ has_wiki: false
6
+ has_projects: false
7
+ has_downloads: false
8
+
9
+ labels:
10
+ - name: help wanted
11
+ oldname: help-wanted
12
+ color: 0e8a16
13
+ - name: more-information-needed
14
+ color: d93f0b
15
+ - name: bug
16
+ color: b60205
17
+ - name: feature
18
+ color: 1d76db
19
+ - name: good first issue
20
+ color: "5319e7"
21
+
22
+ # Not currently implemented by probot/settings, but manually implemented in script/deploy
23
+ branch_protection:
24
+ restrictions: null
25
+ enforce_admins: false
26
+ required_status_checks:
27
+ strict: true
28
+ contexts:
29
+ - "continuous-integration/travis-ci"
30
+ required_pull_request_reviews:
31
+ require_code_owner_reviews: true
data/.github/stale.yml ADDED
@@ -0,0 +1,27 @@
1
+ # Configuration for probot-stale - https://github.com/probot/stale
2
+
3
+ # Number of days of inactivity before an Issue or Pull Request becomes stale
4
+ daysUntilStale: 60
5
+
6
+ # Number of days of inactivity before a stale Issue or Pull Request is closed
7
+ daysUntilClose: 7
8
+
9
+ # Issues or Pull Requests with these labels will never be considered stale
10
+ exemptLabels:
11
+ - pinned
12
+ - security
13
+
14
+ # Label to use when marking as stale
15
+ staleLabel: wontfix
16
+
17
+ # Comment to post when marking as stale. Set to `false` to disable
18
+ markComment: >
19
+ This issue has been automatically marked as stale because it has not had
20
+ recent activity. It will be closed if no further activity occurs. Thank you
21
+ for your contributions.
22
+
23
+ # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable
24
+ closeComment: false
25
+
26
+ # Limit to only `issues` or `pulls`
27
+ # only: issues
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ /.env
2
+ /archive
3
+ Gemfile.lock
4
+ spec/examples.txt
5
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,19 @@
1
+ AllCops:
2
+ Exclude:
3
+ - archive/**/*
4
+ - vendor/**/*
5
+
6
+ Style/Documentation:
7
+ Enabled: false
8
+
9
+ Metrics/BlockLength:
10
+ Exclude:
11
+ - spec/**/*
12
+ - '*.gemspec'
13
+
14
+ Metrics/LineLength:
15
+ Exclude:
16
+ - spec/**/*
17
+
18
+ Layout/IndentHeredoc:
19
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ rvm:
2
+ - 2.4
3
+ before_install: gem install bundler
4
+ langauage: ruby
5
+ script: script/cibuild
6
+ sudo: false
7
+ cache: bundler
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Ben Balter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # GitHub Records Archiver
2
+
3
+ [![Build Status](https://travis-ci.org/benbalter/github-records-archiver.svg?branch=master)](https://travis-ci.org/benbalter/github-records-archiver) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
4
+
5
+ Backs up a GitHub organization's repositories and all their associated information for archival purposes.
6
+
7
+ ## What it archives
8
+
9
+ * Git data (change history, tags, branches, etc.)
10
+ * Wikis (including change history)
11
+ * Issues and pull request (including comments, current state, etc.)
12
+ * Teams (including members and repository permissions)
13
+
14
+ ## Requirements
15
+
16
+ 1. Ruby
17
+ 2. A GitHub [personal access token](https://github.com/settings/tokens/new) with `public_repo` and `repo` scope.
18
+
19
+ ## Setup
20
+
21
+ 1. `git clone https://github.com/benbalter/github-records-archiver`
22
+ 2. `cd github-records-archiver`
23
+ 3. `gem install bundler`
24
+ 4. `bundle install`
25
+
26
+ ## Usage
27
+
28
+ `bin/archive [ORGANIZATION]`
29
+
30
+ You'll want to set the following environmental variable:
31
+
32
+ * `GITHUB_TOKEN` - Your personal access token
33
+
34
+ You *may* set the following environmental variables:
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.
42
+
43
+ ## Output
44
+
45
+ The script will create an `archive` directory, with one folder for each repository.
46
+
47
+ Within each folder will be the repository content as a git repository.
48
+
49
+ If the repository has a Wiki, the wiki will be cloned as a `wiki` subfolder, as a Git repository.
50
+
51
+ 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).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/archive ADDED
@@ -0,0 +1,45 @@
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."
@@ -0,0 +1,46 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ ## Our Standards
8
+
9
+ Examples of behavior that contributes to creating a positive environment include:
10
+
11
+ * Using welcoming and inclusive language
12
+ * Being respectful of differing viewpoints and experiences
13
+ * Gracefully accepting constructive criticism
14
+ * Focusing on what is best for the community
15
+ * Showing empathy towards other community members
16
+
17
+ Examples of unacceptable behavior by participants include:
18
+
19
+ * The use of sexualized language or imagery and unwelcome sexual attention or advances
20
+ * Trolling, insulting/derogatory comments, and personal or political attacks
21
+ * Public or private harassment
22
+ * Publishing others' private information, such as a physical or electronic address, without explicit permission
23
+ * Other conduct which could reasonably be considered inappropriate in a professional setting
24
+
25
+ ## Our Responsibilities
26
+
27
+ Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28
+
29
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30
+
31
+ ## Scope
32
+
33
+ This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34
+
35
+ ## Enforcement
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ben@balter.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38
+
39
+ Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40
+
41
+ ## Attribution
42
+
43
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44
+
45
+ [homepage]: http://contributor-covenant.org
46
+ [version]: http://contributor-covenant.org/version/1/4/
@@ -0,0 +1,85 @@
1
+ # Contributing to GitHub Records Archiver
2
+
3
+ Hi there! We're thrilled that you'd like to contribute to GitHub Records Archiver. Your help is essential for keeping it great.
4
+
5
+ GitHub Records Archiver is an open source project supported by the efforts of an entire community and built one contribution at a time by users like you. We'd love for you to get involved. Whatever your level of skill or however much time you can give, your contribution is greatly appreciated. There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, submitting bug reports and feature requests, helping other users by commenting on issues, or writing code which can be incorporated into GitHub Records Archiver itself.
6
+
7
+ Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests.
8
+
9
+
10
+
11
+ ## How to report a bug
12
+
13
+ Think you found a bug? Please check [the list of open issues](https://github.com/benbalter/github-records-archiver/issues) to see if your bug has already been reported. If it hasn't please [submit a new issue](https://github.com/benbalter/github-records-archiver/issues/new).
14
+
15
+ Here are a few tips for writing *great* bug reports:
16
+
17
+ * Describe the specific problem (e.g., "widget doesn't turn clockwise" versus "getting an error")
18
+ * Include the steps to reproduce the bug, what you expected to happen, and what happened instead
19
+ * Check that you are using the latest version of the project and its dependencies
20
+ * Include what version of the project your using, as well as any relevant dependencies
21
+ * Only include one bug per issue. If you have discovered two bugs, please file two issues
22
+ * Include screenshots or screencasts whenever possible
23
+ * Even if you don't know how to fix the bug, including a failing test may help others track it down
24
+
25
+ **If you find a security vulnerability, do not open an issue. Please email ben@balter.com instead.**
26
+
27
+ ## How to suggest a feature or enhancement
28
+
29
+ If you find yourself wishing for a feature that doesn't exist in GitHub Records Archiver, you are probably not alone. There are bound to be others out there with similar needs. Many of the features that GitHub Records Archiver has today have been added because our users saw the need.
30
+
31
+ Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and goals of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible, including describing the problem you're trying to solve.
32
+
33
+ [Open an issue](https://github.com/benbalter/github-records-archiver/issues/new) which describes the feature you would like to see, why you want it, how it should work, etc.
34
+
35
+
36
+
37
+ ## Your first contribution
38
+
39
+ We'd love for you to contribute to the project. Unsure where to begin contributing to GitHub Records Archiver? You can start by looking through these "good first issue" and "help wanted" issues:
40
+
41
+ * [Good first issues](https://github.com/benbalter/github-records-archiver/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code and a test or two
42
+ * [Help wanted issues](https://github.com/benbalter/github-records-archiver/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) - issues which may be a bit more involved, but are specifically seeking community contributions
43
+
44
+ *p.s. Feel free to ask for help; everyone is a beginner at first* :smiley_cat:
45
+
46
+ ## How to propose changes
47
+
48
+ Here's a few general guidelines for proposing changes:
49
+
50
+ * If you are changing any user-facing functionality, please be sure to update the documentation
51
+ * If you are adding a new behavior or changing an existing behavior, please be sure to update the corresponding test(s)
52
+ * Each pull request should implement **one** feature or bug fix. If you want to add or fix more than one thing, submit more than one pull request
53
+ * Do not commit changes to files that are irrelevant to your feature or bug fix
54
+ * Don't bump the version number in your pull request (it will be bumped prior to release)
55
+ * Write [a good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
56
+
57
+ At a high level, [the process for proposing changes](https://guides.github.com/introduction/flow/) is:
58
+
59
+ 1. [Fork](https://github.com/benbalter/github-records-archiver/fork) and clone the project
60
+ 2. Configure and install the dependencies: `script/bootstrap`
61
+ 3. Make sure the tests pass on your machine: `script/cibuild`
62
+ 4. Create a descriptively named branch: `git checkout -b my-branch-name`
63
+ 5. Make your change, add tests and documentation, and make sure the tests still pass
64
+ 6. Push to your fork and [submit a pull request](https://github.com/benbalter/github-records-archiver/compare) describing your change
65
+ 7. Pat your self on the back and wait for your pull request to be reviewed and merged
66
+
67
+ **Interesting in submitting your first Pull Request?** It's easy! You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github)
68
+
69
+ ## Bootstrapping your local development environment
70
+
71
+ `script/bootstrap`
72
+
73
+ ## Running tests
74
+
75
+ `script/cibuild`
76
+
77
+ ## Code of conduct
78
+
79
+ This project is governed by [the Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.
80
+
81
+ ## Additional Resources
82
+
83
+ * [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/)
84
+ * [Using Pull Requests](https://help.github.com/articles/using-pull-requests/)
85
+ * [GitHub Help](https://help.github.com)
@@ -0,0 +1,38 @@
1
+
2
+ lib = File.expand_path('lib', __dir__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'github_records_archiver/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'github_records_archiver'
8
+ spec.version = GitHubRecordsArchiver::VERSION
9
+ spec.authors = ['Ben Balter']
10
+ spec.email = ['ben.balter@github.com']
11
+
12
+ spec.summary = <<-SUMMARY
13
+ Backs up a GitHub organization's repositories and all their associated
14
+ information for archival purposes
15
+ SUMMARY
16
+
17
+ spec.homepage = 'https://github.com/benbalter/github_records_archiver'
18
+ spec.license = 'MIT'
19
+
20
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
21
+ f.match(%r{^(test|spec|features)/})
22
+ end
23
+ spec.bindir = 'bin'
24
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_dependency 'dotenv', '~> 2.0'
28
+ spec.add_dependency 'octokit', '~> 4.0'
29
+ spec.add_dependency 'parallel', '~> 1.10'
30
+ spec.add_dependency 'ruby-progressbar', '~> 1.0'
31
+ spec.add_development_dependency 'addressable', '~> 2.5'
32
+ spec.add_development_dependency 'bundler', '~> 1.16'
33
+ spec.add_development_dependency 'pry', '~> 0.10'
34
+ spec.add_development_dependency 'rake', '~> 10.0'
35
+ spec.add_development_dependency 'rspec', '~> 3.0'
36
+ spec.add_development_dependency 'rubocop', '~> 0.50'
37
+ spec.add_development_dependency 'webmock', '~> 3.0'
38
+ end
@@ -0,0 +1,32 @@
1
+ module GitHubRecordsArchiver
2
+ class Comment
3
+ attr_reader :repository
4
+ attr_reader :id
5
+
6
+ include DataHelper
7
+
8
+ def initialize(repo, id)
9
+ repo = Repository.new(repo) if repo.is_a? String
10
+ @repository = repo
11
+ @id = id
12
+ end
13
+
14
+ def self.from_hash(repo, hash)
15
+ comment = Comment.new(repo, hash[:number])
16
+ comment.instance_variable_set '@data', hash.to_h
17
+ comment
18
+ end
19
+
20
+ def data
21
+ @data ||= begin
22
+ GitHubRecordsArchiver.client.issue_comment(repository.full_name, id)
23
+ end
24
+ end
25
+
26
+ def to_s
27
+ output = "@#{user[:login]} at #{created_at} wrote:\n\n"
28
+ output << body
29
+ output
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ module GitHubRecordsArchiver
2
+ module DataHelper
3
+ attr_writer :data
4
+
5
+ def method_missing(method_sym, *arguments, &block)
6
+ return data[method_sym] if data_key?(method_sym)
7
+ if method_sym.to_s.end_with? '?'
8
+ !send(non_predicate_method(method_sym)).to_s.empty?
9
+ else
10
+ super
11
+ end
12
+ end
13
+
14
+ def respond_to_missing?(method_sym, include_private = false)
15
+ if data_key? non_predicate_method(method_sym)
16
+ true
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ def to_h
23
+ data.to_h
24
+ end
25
+ alias as_json to_h
26
+
27
+ def to_json
28
+ as_json.to_json
29
+ end
30
+
31
+ def data
32
+ raise 'Not implemented'
33
+ end
34
+
35
+ private
36
+
37
+ def data_key?(key)
38
+ data && data.key?(key)
39
+ end
40
+
41
+ def non_predicate_method(method_sym)
42
+ method_sym.to_s.gsub(/\?\z/, '').to_sym
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ module GitHubRecordsArchiver
2
+ class GitError < StandardError; end
3
+
4
+ class GitRepository
5
+ def clone
6
+ if Dir.exist? repo_dir # Repo already exists, just pull new objects
7
+ Dir.chdir repo_dir do
8
+ git 'pull'
9
+ end
10
+ else # Clone Git content from scratch
11
+ git 'clone', clone_url, repo_dir
12
+ end
13
+ end
14
+
15
+ def repo_dir
16
+ raise 'Not implemented'
17
+ end
18
+
19
+ private
20
+
21
+ def clone_url
22
+ raise 'Not implemented'
23
+ end
24
+
25
+ # There's a bug, whereby if you attempt to clone a wiki that's enabled
26
+ # but has not yet been initialized, GitHub returns a remote error
27
+ # Rather than let this break the export, capture the error and continue
28
+ def wiki_does_not_exist?(output)
29
+ expected = '^fatal: remote error: access denied or repository not '
30
+ expected << "exported: .*?\.wiki\.git$"
31
+ output =~ /#{expected}/
32
+ end
33
+
34
+ # Attempting to clone an empty repo will rightfulyl fail at the Git level
35
+ # But we shouldn't let that fail the archive operation
36
+ def empty_repo?(output)
37
+ expected = 'Your configuration specifies to merge with the ref '
38
+ expected << "'refs/heads/master'\n"
39
+ expected << 'from the remote, but no such ref was fetched.'
40
+ output =~ Regexp.new(expected)
41
+ end
42
+
43
+ # Run a git command, piping output to stdout
44
+ def git(*args)
45
+ output, status = Open3.capture2e('git', *args)
46
+ return false if empty_repo?(output) || wiki_does_not_exist?(output)
47
+ raise GitError, output if status.exitstatus != 0
48
+ output
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,70 @@
1
+ module GitHubRecordsArchiver
2
+ class Issue
3
+ attr_reader :repository
4
+ attr_reader :number
5
+
6
+ include DataHelper
7
+
8
+ KEYS = %i[title number state html_url created_at closed_at].freeze
9
+
10
+ def initialize(repository: nil, number: nil)
11
+ repository = Repository.new(repository) if repository.is_a? String
12
+ @repository = repository
13
+ @number = number
14
+ end
15
+
16
+ def self.from_hash(repo, hash)
17
+ issue = Issue.new(repository: repo, number: hash[:number])
18
+ issue.instance_variable_set('@data', hash.to_h)
19
+ issue
20
+ end
21
+
22
+ def data
23
+ @data ||= GitHubRecordsArchiver.client.issue repository.name, number
24
+ end
25
+
26
+ def comments
27
+ @comments ||= begin
28
+ return [] if data[:comments].nil? || data[:comments].zero?
29
+ client = GitHubRecordsArchiver.client
30
+ comments = client.issue_comments repository.full_name, number
31
+ comments.map { |hash| Comment.from_hash(repository, hash) }
32
+ end
33
+ end
34
+
35
+ def to_s
36
+ md = meta_for_markdown.to_yaml + "---\n\n# #{title}\n\n"
37
+ md << body unless body.to_s.empty?
38
+ md << comments_string unless comments.nil?
39
+ md
40
+ end
41
+
42
+ def as_json
43
+ data.to_h.merge('comments' => comments.map(&:as_json))
44
+ end
45
+
46
+ def archive
47
+ File.write(path('md'), to_s)
48
+ File.write(path('json'), to_json)
49
+ end
50
+
51
+ private
52
+
53
+ def path(ext = 'md')
54
+ File.expand_path "#{number}.#{ext}", repository.issues_dir
55
+ end
56
+
57
+ def meta_for_markdown
58
+ meta = {}
59
+ KEYS.each { |key| meta[key.to_s] = data[key] }
60
+ meta['user'] = user[:login]
61
+ meta['assignee'] = assignee[:login] unless assignee.nil?
62
+ meta['tags'] = labels.map { |tag| tag[:name] }
63
+ meta
64
+ end
65
+
66
+ def comments_string
67
+ "\n\n---\n" + comments.map(&:to_s).join("\n\n---\n")
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,48 @@
1
+ module GitHubRecordsArchiver
2
+ class Organization
3
+ attr_reader :name
4
+
5
+ include DataHelper
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ end
10
+
11
+ def data
12
+ @data ||= GitHubRecordsArchiver.client.organization name
13
+ end
14
+
15
+ def repositories
16
+ @repositories ||= begin
17
+ repos = GitHubRecordsArchiver.client.organization_repositories(name)
18
+ repos.map { |hash| Repository.new(hash) }
19
+ end
20
+ end
21
+ alias repos repositories
22
+
23
+ def teams
24
+ @teams ||= begin
25
+ teams = GitHubRecordsArchiver.client.organization_teams(name)
26
+ teams.map { |h| Team.from_hash(self, h) }
27
+ rescue Octokit::Forbidden
28
+ []
29
+ end
30
+ end
31
+
32
+ def archive_dir
33
+ @archive_dir ||= begin
34
+ dir = File.expand_path name, GitHubRecordsArchiver.dest_dir
35
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
36
+ dir
37
+ end
38
+ end
39
+
40
+ def teams_dir
41
+ @teams_dir ||= begin
42
+ dir = File.expand_path 'teams', archive_dir
43
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
44
+ dir
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,54 @@
1
+ module GitHubRecordsArchiver
2
+ class Repository < GitRepository
3
+ attr_reader :name
4
+ include DataHelper
5
+
6
+ KEYS = %i[
7
+ name full_name description private fork homepage
8
+ forks_count stargazers_count watchers_count size
9
+ ].freeze
10
+
11
+ def initialize(name_or_hash)
12
+ if name_or_hash.is_a?(String)
13
+ @name = name_or_hash
14
+ else
15
+ @data = name_or_hash.to_h
16
+ @name = @data[:full_name]
17
+ end
18
+ end
19
+
20
+ def data
21
+ @data ||= GitHubRecordsArchiver.client.repository(name)
22
+ end
23
+
24
+ def wiki
25
+ @wiki ||= Wiki.new(self) if has_wiki?
26
+ end
27
+
28
+ def issues
29
+ @issues ||= begin
30
+ issues = GitHubRecordsArchiver.client.list_issues name, state: 'all'
31
+ issues.map { |i| Issue.from_hash(self, i) }
32
+ end
33
+ end
34
+
35
+ def issues_dir
36
+ @issues_dir ||= begin
37
+ dir = File.expand_path 'issues', repo_dir
38
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
39
+ dir
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def repo_dir
46
+ @repo_dir ||= File.expand_path data[:name], GitHubRecordsArchiver.dest_dir
47
+ end
48
+
49
+ def clone_url
50
+ replacement = "https://#{GitHubRecordsArchiver.token}:x-oauth-basic@"
51
+ data[:clone_url].gsub(%r{https?://}, replacement)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,66 @@
1
+ module GitHubRecordsArchiver
2
+ class Team
3
+ attr_reader :id
4
+ attr_reader :organization
5
+
6
+ include DataHelper
7
+
8
+ KEYS = %i[name slug description privacy permission].freeze
9
+
10
+ def initialize(org, id)
11
+ org = Organization.new(org) if org.is_a? String
12
+ @organization = org
13
+ @id = id
14
+ end
15
+
16
+ def self.from_hash(org, hash)
17
+ team = Team.new(org, hash[:id])
18
+ team.instance_variable_set '@data', hash.to_h
19
+ team
20
+ end
21
+
22
+ def data
23
+ @data ||= GitHubRecordsArchiver.client.team id
24
+ end
25
+
26
+ def repositories
27
+ @repositories ||= begin
28
+ repos = GitHubRecordsArchiver.client.team_repositories id
29
+ repos.map { |r| Repository.new(r) }
30
+ end
31
+ end
32
+
33
+ def members
34
+ @members ||= begin
35
+ members = GitHubRecordsArchiver.client.team_members id
36
+ members.map { |m| User.new(m) }
37
+ end
38
+ end
39
+
40
+ def archive
41
+ File.write(path, to_s)
42
+ end
43
+
44
+ def to_s
45
+ meta_for_markdown.to_yaml
46
+ end
47
+
48
+ def as_json
49
+ data.to_h.merge(repositories: repositories.map(&:name))
50
+ end
51
+
52
+ private
53
+
54
+ def path
55
+ File.expand_path "#{data[:slug]}.md", organization.teams_dir
56
+ end
57
+
58
+ def meta_for_markdown
59
+ meta = {}
60
+ KEYS.each { |key| meta[key.to_s] = data[key] }
61
+ meta['repositories'] = repositories.map(&:name)
62
+ meta['members'] = members.map(&:name)
63
+ meta
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,23 @@
1
+ module GitHubRecordsArchiver
2
+ class User
3
+ attr_reader :login
4
+ alias name login
5
+
6
+ include DataHelper
7
+
8
+ KEYS = %i[login site_admin type].freeze
9
+
10
+ def initialize(login_or_hash)
11
+ if login_or_hash.is_a? String
12
+ @login = login_or_hash
13
+ else
14
+ @login = login_or_hash[:login]
15
+ @data = login_or_hash.to_h
16
+ end
17
+ end
18
+
19
+ def data
20
+ @data ||= GitHubRecordsArchiver.client.user login
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module GitHubRecordsArchiver
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,26 @@
1
+ module GitHubRecordsArchiver
2
+ class Wiki < GitRepository
3
+ attr_accessor :repository
4
+
5
+ include DataHelper
6
+
7
+ def initialize(repository)
8
+ @repository = if repository.is_a?(String)
9
+ Repository.new(repository)
10
+ else
11
+ repository
12
+ end
13
+ end
14
+
15
+ def repo_dir
16
+ name = "#{repository.full_name}/wiki"
17
+ @repo_dir ||= File.expand_path name, GitHubRecordsArchiver.dest_dir
18
+ end
19
+
20
+ private
21
+
22
+ def clone_url
23
+ @clone_url ||= repository.clone_url.gsub(/\.git\z/, '.wiki.git')
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+
3
+ # stdlib
4
+ require 'yaml'
5
+ require 'json'
6
+ require 'logger'
7
+ require 'fileutils'
8
+ require 'open3'
9
+
10
+ # gems
11
+ require 'octokit'
12
+ require 'dotenv'
13
+
14
+ # Configuration
15
+ Dotenv.load
16
+ Octokit.auto_paginate = true
17
+
18
+ module GitHubRecordsArchiver
19
+ autoload :DataHelper, 'github_records_archiver/data_helper'
20
+ autoload :Comment, 'github_records_archiver/comment'
21
+ autoload :GitRepository, 'github_records_archiver/git_repository'
22
+ autoload :Issue, 'github_records_archiver/issue'
23
+ autoload :Organization, 'github_records_archiver/organization'
24
+ autoload :Repository, 'github_records_archiver/repository'
25
+ autoload :Team, 'github_records_archiver/team'
26
+ autoload :User, 'github_records_archiver/user'
27
+ autoload :VERSION, 'github_records_archiver/version'
28
+ autoload :Wiki, 'github_records_archiver/wiki'
29
+
30
+ class << self
31
+ def token
32
+ ENV['GITHUB_TOKEN']
33
+ end
34
+
35
+ def client
36
+ @client ||= Octokit::Client.new access_token: token
37
+ end
38
+
39
+ def dest_dir
40
+ ENV['GITHUB_ARCHIVE_DIR'] || File.expand_path('./archive', Dir.pwd)
41
+ end
42
+ end
43
+ end
data/script/bootstrap ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ set -ex
4
+
5
+ bundle install
data/script/cibuild ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ set -ex
4
+
5
+ bundle exec rspec
6
+ bundle exec rubocop
7
+ bundle exec gem build github_records_archiver.gemspec
data/script/console ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ set -ex
4
+
5
+ bundle exec pry -r './lib/github_records_archiver'
metadata ADDED
@@ -0,0 +1,231 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: github_records_archiver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ben Balter
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dotenv
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: octokit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: parallel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.10'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby-progressbar
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: addressable
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.5'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.16'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.16'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.10'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.10'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '10.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '10.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.50'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.50'
153
+ - !ruby/object:Gem::Dependency
154
+ name: webmock
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '3.0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '3.0'
167
+ description:
168
+ email:
169
+ - ben.balter@github.com
170
+ executables:
171
+ - archive
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - ".github/CODEOWNERS"
176
+ - ".github/config.yml"
177
+ - ".github/no-response.yml"
178
+ - ".github/settings.yml"
179
+ - ".github/stale.yml"
180
+ - ".gitignore"
181
+ - ".rspec"
182
+ - ".rubocop.yml"
183
+ - ".travis.yml"
184
+ - Gemfile
185
+ - LICENSE.md
186
+ - README.md
187
+ - Rakefile
188
+ - bin/archive
189
+ - docs/CODE_OF_CONDUCT.md
190
+ - docs/CONTRIBUTING.md
191
+ - github_records_archiver.gemspec
192
+ - lib/github_records_archiver.rb
193
+ - lib/github_records_archiver/comment.rb
194
+ - lib/github_records_archiver/data_helper.rb
195
+ - lib/github_records_archiver/git_repository.rb
196
+ - lib/github_records_archiver/issue.rb
197
+ - lib/github_records_archiver/organization.rb
198
+ - lib/github_records_archiver/repository.rb
199
+ - lib/github_records_archiver/team.rb
200
+ - lib/github_records_archiver/user.rb
201
+ - lib/github_records_archiver/version.rb
202
+ - lib/github_records_archiver/wiki.rb
203
+ - script/bootstrap
204
+ - script/cibuild
205
+ - script/console
206
+ homepage: https://github.com/benbalter/github_records_archiver
207
+ licenses:
208
+ - MIT
209
+ metadata: {}
210
+ post_install_message:
211
+ rdoc_options: []
212
+ require_paths:
213
+ - lib
214
+ required_ruby_version: !ruby/object:Gem::Requirement
215
+ requirements:
216
+ - - ">="
217
+ - !ruby/object:Gem::Version
218
+ version: '0'
219
+ required_rubygems_version: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - ">="
222
+ - !ruby/object:Gem::Version
223
+ version: '0'
224
+ requirements: []
225
+ rubyforge_project:
226
+ rubygems_version: 2.7.6
227
+ signing_key:
228
+ specification_version: 4
229
+ summary: Backs up a GitHub organization's repositories and all their associated information
230
+ for archival purposes
231
+ test_files: []