Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jneill committed Feb 24, 2016
0 parents commit b97c632
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
lib
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2016 Jordan Neill

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
110 changes: 110 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
## gh-migrations

Export all of an organization's GitHub data using the (in preview) [Migrations API](https://developer.github.com/v3/migration/migrations/)

Create a migration and then download the archive, containing each repository's:

* `.git` directory
* Wiki (as git repo)
* Issues
* Pull Requests
* Comments
* Releases
* Milestones
* Events (e.g. issue closed then reopened)
* Attachments (e.g. images in comments)

**Note** This is a temporary hack until the Migrations API comes out of preview and GitHub presumably provide a proper UI for it. It's an an extremely rough-and-ready tool: no tests, no error handling.

### Installation

Requires [node.js](https://nodejs.org/) (>= 4.0)

```
$ npm install -g gh-migrations
```

### Configuration

The tool can be configured using options on the command-line, or in `~/.gh.json` (as for [node-gh](https://github.com/node-gh/gh#config))

```json
{
"github_token": "e72e16c7e42f292c6912e7710c838347ae178b4a",
"default_org": "octokit"
}
```

##### OAuth Token

This is the token used to access the GitHub API. You'll need to [create a new personal access token](https://github.com/settings/tokens/new?scopes=repo&description=gh-migrations) with `repo` scope. You can set the token using the `--token` option on the command-line, or as `"github_token"` in `~/.gh.json`

##### Organization

Migrations are created in the scope of a GitHub Organization. You can set the organization name using the `--org` option on the command-line, or as `"default_org"` in `~/.gh.json`

### Usage

Create a new migration, specifying the list of repositories to include. Repository names need to be the full name (i.e. of the form `{user}/{repo}`)

```
$ gh-migrations create octokit/octokit.net octokit/go-octokit octokit/octokit.rb
Migration 4797
State: pending
Created: Mon Nov 8 12:25:35 2016 +0000
Updated: Mon Nov 8 12:25:36 2016 +0000
Repositories:
octokit/octokit.net
octokit/go-octokit
octokit/octokit.rb
```

View the list of existing migrations

```
$ gh-migrations list
┌──────────┬───────────┬─────────────────┬────────────────────────────────────┐
│ ID │ State │ Updated │ Repositories │
├──────────┼───────────┼─────────────────┼────────────────────────────────────┤
│ 4797 │ exporting │ 1 minute ago │ octokit/octokit.net │
│ │ │ │ octokit/go-octokit │
│ │ │ │ octokit/octokit.rb │
├──────────┼───────────┼─────────────────┼────────────────────────────────────┤
│ 4791 │ exported │ 9 hours ago │ octokit/octokit.py │
└──────────┴───────────┴─────────────────┴────────────────────────────────────┘
```

Once the migration state changes to `exported`, you can download the archive:

```
$ gh-migrations download 4797 > gh4797.tar.gz
```

### Library

This tool can also be used as a node module to access the Migrations API.

The library uses [ES6 generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) and [`co`](https://github.com/tj/co) to emulate [ES7 async/await](https://github.com/lukehoban/ecmascript-asyncawait).

```coffeescript
Api = require("gh-migrations")

api = new Api(token)
migrations = api.migrations("octokit")

co ->
migration = yield migrations.create([ "octokit/octokit.objc" ])
console.log migration.state # pending

# later
list = yield migrations.findAll()
migration = _.find(list, (m) -> m.id is migration.id)
console.log migration.state # exporting

# later still
migration = yield migrations.get(migration.id)
console.log migration.state # exported

migrations.download(migration.id)
.pipe(fs.createWriteStream("out.tar.gz"))
```
18 changes: 18 additions & 0 deletions bin/gh-migrations
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env node

var path = require('path')
var fs = require('fs')

var dir = path.dirname(fs.realpathSync(__filename))

var cli = null
try {
// use .js files if they exist (published module)
cli = require(path.join(dir, "../lib/cli"))
} catch (err) {
// otherwise use the .coffee versions (development)
require("coffee-script/register")
cli = require(path.join(dir, '../src/cli'))
}

cli.run()
40 changes: 40 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "gh-migrations",
"description": "GitHub Migrations CLI",
"version": "1.0.0",
"homepage": "https://github.com/jneill/gh-migrations",
"keyworks": [
"github",
"api",
"export",
"migration",
"migrations"
],
"repository": "jneill/gh-migrations",
"author": "Jordan Neill <[email protected]>",
"license": "MIT",
"main": "./lib/index.js",
"bin": {
"gh-migrations": "./bin/gh-migrations"
},
"preferGlobal": true,
"scripts": {
"prepublish": "./node_modules/coffee-script/bin/coffee --compile --output lib/ src/"
},
"engines": {
"node": ">=4.0.0"
},
"dependencies": {
"cli-table": "^0.3.1",
"co": "^4.6.0",
"co-fs": "^1.2.0",
"co-request": "^1.0.0",
"commander": "^2.9.0",
"lodash": "^4.5.1",
"moment": "^2.11.2",
"request": "^2.69.0"
},
"devDependencies": {
"coffee-script": "^1.10.0"
}
}
103 changes: 103 additions & 0 deletions src/cli.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@

fs = require("co-fs")
os = require("os")
path = require("path")

_ = require("lodash")
co = require("co")
moment = require("moment")
Table = require("cli-table")

Api = require("./index")

TIME_FORMAT = "ddd MMM D HH:mm:ss YYYY ZZ"

getConfig = ->
options = program.opts()
try json = yield fs.readFile(path.join(os.homedir(), ".gh.json"), "UTF8")
globalConfig = if json then JSON.parse(json) else {}
return {
token: options.token or globalConfig.github_token
org: options.org or globalConfig.default_org
}

migrations = ->
{ token, org } = yield getConfig()
new Api(token).migrations(org)

program = require("commander")

program
.version "1.0.0"
.description "GitHub Migrations CLI"
.option("-t, --token [token]", "a valid GitHub OAuth2 token")
.option("-o, --org [org]", "name of the organisation to migrate")

program
.command "create [repos...]"
.description "create new migration"
.action (repos) -> execAsync ->
migrations = yield migrations()
item = yield migrations.create(repos)
process.stdout.write formatItem(item) + "\n"

program
.command "list"
.description "list existing migrations"
.action -> execAsync ->
migrations = yield migrations()
list = yield migrations.find()
process.stdout.write formatList(list) + "\n"

program
.command "view [id]"
.description "view existing migration"
.action (id) -> execAsync ->
migrations = yield migrations()
item = yield migrations.get(id)
process.stdout.write formatItem(item) + "\n"

program
.command "download [id]"
.description "download migration archive"
.action (id) -> execAsync ->
migrations = yield migrations()
content = migrations.download(id)
content.pipe(process.stdout)

execAsync = (fn) ->
co(fn).catch (err) ->
process.stderr.write(err.stack) + "\n"
process.exit(1)

formatList = (migrations) ->
table = new Table
head: [ "ID", "State", "Updated", "Repositories" ]
colWidths: [ 10, 11, 17, 36 ]
style: { head: [ "bold", "cyan" ], border: [ "white" ] }
for migration in migrations
table.push [
migration.id
migration.state
moment(migration.updated_at).fromNow()
_.map(migration.repositories, "full_name").join("\n")
]
return table.toString()

formatItem = (migration) ->
"""
Migration #{migration.id}
State: #{migration.state}
Created: #{moment(migration.created_at).format(TIME_FORMAT)}
Updated: #{moment(migration.updated_at).format(TIME_FORMAT)}
Repositories:
#{_.map(migration.repositories, "full_name").join("\n ")}
"""

module.exports =
run: ({ argv } = process) ->
command = argv[2]
program.help() unless command in [
"list", "create", "view", "download"
]
program.parse(argv)
73 changes: 73 additions & 0 deletions src/index.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@

_ = require("lodash")
request = require("co-request")

USER_AGENT = "gh-migrations (https://github.com/jneill/gh-migrations)"

class Api

constructor: (token) ->
@_client = new Client(token)

migrations: (organization) ->
@migrations = new Migrations(@_client, organization)

class Migrations

constructor: (client, organization) ->
@org = organization
@_client = client

find: ->
yield @_client.get("/orgs/#{@org}/migrations")

get: (id) ->
yield @_client.get("/orgs/#{@org}/migrations/#{id}")

create: (repositories) ->
yield @_client.post("/orgs/#{@org}/migrations", { body: { repositories } })

download: (id) ->
@_client.stream "GET", "/orgs/#{@org}/migrations/#{id}/archive",
json: false
encoding: null

class Client

mergeOptions = (method, uri = "", options = {}) ->
if _.isObject(uri)
options = uri
options.uri or= ""
else if _.isString(uri)
options.uri = uri
options.method = method
options

constructor: (token) ->
@_requestDefaults =
baseUrl: "https://api.github.com"
json: true
headers:
"User-Agent": USER_AGENT
"Authorization": "token #{token}"
"Accept": "application/vnd.github.wyandotte-preview+json"
@_request = request.defaults(@_requestDefaults)

stream: (method, uri, options) ->
options = mergeOptions(method, uri, options)
_.defaults(options, { json: false, encoding: null }, @_requestDefaults)
# don't use co-request because we want the stream
require("request")(options)

request: (method, uri, options) ->
options = mergeOptions(method, uri, options)
res = yield @_request(options)
res.body

get: (uri, options) ->
yield @request("GET", uri, options)

post: (uri, options) ->
yield @request("POST", uri, options)

module.exports = Api

0 comments on commit b97c632

Please sign in to comment.