Skip to content
This repository has been archived by the owner on Jul 14, 2023. It is now read-only.

Commit

Permalink
Merge pull request #18 from education/feature/cloning
Browse files Browse the repository at this point in the history
Repository cloning implementation
  • Loading branch information
Nick Tikhonov committed Jul 25, 2016
2 parents 254382e + 50e3cc8 commit 03ab28b
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 10 deletions.
5 changes: 4 additions & 1 deletion .jestconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"<rootDir>/node_modules/reselect/",
"<rootDir>/node_modules/enzyme",
"<rootDir>/node_modules/sinon/",
"<rootDir>/node_modules/classnames/"
"<rootDir>/node_modules/classnames/",
"<rootDir>/node_modules/nodegit/",
"<rootDir>/node_modules/rimraf/",
"<rootDir>/node_modules/promisify-node/"
],
"moduleFileExtensions": [
"js",
Expand Down
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ notifications:

sudo: false

addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- libstdc++-4.9-dev

node_js:
- "6"

Expand Down
93 changes: 93 additions & 0 deletions app/lib/__tests__/cloneutils-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
jest.unmock("../cloneutils.js")

import { clone } from "../cloneutils.js"
import rmdir from "rimraf"
import sinon from "sinon"

const fs = require("fs")

const TEST_REPO = "https://github.com/education/classroom-desktop"
const TEST_FAKE_REPO = "https://github.com/education/a-repo-that-will-never-exist"

const DESTINATION = "/tmp/" + Math.random().toString(36).substring(7)

describe("Clone Utilities", () => {
describe("clone", () => {
const removeTestDir = (done) => {
rmdir(DESTINATION, () => {
done()
})
}

beforeEach(removeTestDir)
afterEach(removeTestDir)

it("clones the repository to the correct destination", (done) => {
clone(
TEST_REPO,
DESTINATION,
(percentage) => {}
).then(() => {
const stats = fs.lstatSync(DESTINATION)
expect(stats.isDirectory()).toBe(true)
done()
})
})

it("notifies when the repo is 0% downloaded", (done) => {
const statusCallback = sinon.spy()

clone(
TEST_REPO,
DESTINATION,
statusCallback
).then(() => {
expect(statusCallback.calledWith(0)).toBe(true)
done()
})
})

it("notifies when the repo is 100% downloaded", (done) => {
const statusCallback = sinon.spy()

clone(
TEST_REPO,
DESTINATION,
statusCallback
).then(() => {
expect(statusCallback.calledWith(100)).toBe(true)
done()
})
})

it("throws an error when cloning a repo that doesn't exist", (done) => {
clone(
TEST_FAKE_REPO,
DESTINATION,
() => {}
).then(() => {
fail("Clone should not succeed")
}).catch(() => {
done()
})
})

it("throws an error when cloning a repo to an existing non-empty directory", (done) => {
clone(
TEST_REPO,
DESTINATION,
() => {}
).then(() => {
return clone(
TEST_REPO,
DESTINATION,
() => {}
)
}).then(() => {
fail("Clone should not succeed")
}).catch(() => {
done()
})
})
})
})
64 changes: 64 additions & 0 deletions app/lib/cloneutils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const NodeGit = require("nodegit")

// Internal: Helper function that constructs the options object used by NodeGit.
const buildOptions = (progressCallback) => {
const callbacks = new NodeGit.RemoteCallbacks()
callbacks.transferProgress = progressCallback

const fetchOptions = new NodeGit.FetchOptions()
fetchOptions.callbacks = callbacks

const options = new NodeGit.CloneOptions()
options.fetchOpts = fetchOptions

return options
}

// Public: Clones a public git repository to the specified destination directory,
// and notifies the caller of clone progress via callback.
//
// repoURL - String containing the URL of the public Git repository. It
// should be possible to clone the repository with `git clone`
// destination - String containing Absolute path where the repository files will
// be cloned.
// progressCallback - Callback Function used for notifying the caller of clone
// progress. The function is called with the percentage of object
// fetched.
//
// Examples
//
// clone(
// "https://github.com/education/classroom-desktop",
// "/tmp/desktop-classroom",
// (percentage) => {
// console.log("Percentage complete: " + percentage + "%")
// }
// ).then(() => {
// console.log("Finished cloning")
// })
//
// Returns a Promise
export const clone = (repoURL, destination, progressCallback) => {
return new Promise((resolve, reject) => {
let progressOnCompletion = false

const options = buildOptions((progressInfo) => {
const percentage = 100 * progressInfo.receivedObjects() / progressInfo.totalObjects()
if (percentage === 100) progressOnCompletion = true
progressCallback(percentage)
})

NodeGit.Clone(
repoURL,
destination,
options
).then(() => {
if (!progressOnCompletion) {
progressCallback(100)
}
resolve()
}).catch((err) => {
reject(err)
})
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ jest.unmock("../ItemArchivePanel.jsx")
jest.unmock("../../../shared/components/ItemPanel.jsx")

import React from "react"
import { mount } from "enzyme"
import { shallow } from "enzyme"

import ItemArchivePanelList from "../ItemArchivePanelList.jsx"
import ItemArchivePanel from "../ItemArchivePanel.jsx"

let testProps = {
submissions: [{
Expand All @@ -28,7 +27,7 @@ let testProps = {

describe("ItemArchivePanelList", () => {
it("renders ItemArchivePanel components as children", () => {
let wrapper = mount(<ItemArchivePanelList {...testProps} />)
expect(wrapper.find(ItemArchivePanel).length).toEqual(2)
let wrapper = shallow(<ItemArchivePanelList {...testProps} />)
expect(wrapper.find("ItemArchivePanel").length).toEqual(2)
})
})
4 changes: 2 additions & 2 deletions app/routes/select/components/__tests__/SubmissionList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ jest.unmock("../SubmissionList.jsx")
jest.unmock("../../../shared/components/ItemPanel.jsx")

import React from "react"
import { mount } from "enzyme"
import { shallow } from "enzyme"

import SubmissionList from "../SubmissionList.jsx"
import SelectableSubmission from "../../containers/SelectableSubmission.jsx"
Expand All @@ -27,7 +27,7 @@ let testProps = {

describe("SubmissionList", () => {
it("renders SelectableSubmission components as children", () => {
let wrapper = mount(<SubmissionList {...testProps} />)
let wrapper = shallow(<SubmissionList {...testProps} />)
expect(wrapper.find(SelectableSubmission).length).toEqual(2)
})
})
6 changes: 3 additions & 3 deletions app/routes/shared/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ const Header = ({
<img src={imagePath} className="media-object" alt={title} />
</a>
<div className="media-body">
<div classNam="row">
<div classNam="col-sm-6">
<div className="row">
<div className="col-sm-6">
<h4 className="media-heading">{title}</h4>
{subtitle}
</div>
<div classNam="col-sm-6">
<div className="col-sm-6">
{children}
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"homepage": "https://github.com/education/classroom-desktop#readme",
"devDependencies": {
"babel-jest": "^13.2.2",
"babel-polyfill": "^6.9.1",
"electron-packager": "^7.0.4",
"electron-prebuilt": "^1.2.0",
"eslint": "^2.13.0",
Expand All @@ -48,6 +49,9 @@
"classnames": "^2.2.5",
"electron-is-dev": "^0.1.1",
"font-awesome": "^4.6.3",
"jest-cli": "^13.2.3",
"mocha": "^2.5.3",
"nodegit": "^0.14.1",
"react": "^15.1.0",
"react-addons-test-utils": "15.1.0",
"react-dom": "^15.1.0",
Expand All @@ -57,6 +61,7 @@
"recompose": "^0.20.2",
"redux": "^3.5.2",
"reselect": "^2.5.3",
"rimraf": "^2.5.3",
"sinon": "^1.17.4",
"winston": "^2.2.0"
}
Expand Down

0 comments on commit 03ab28b

Please sign in to comment.