Skip to content

Commit

Permalink
Pull repositories automatically (#8)
Browse files Browse the repository at this point in the history
* Big refactoring, replace `fetch` by `pull` about repositories since it
was confusing

* Reduce pause delay

* Minor adjustments

* Fix ugly and unreliable hardcoded delay in the e2e tests

* Add database migration to rename fetch -> pull
  • Loading branch information
motet-a authored Jul 21, 2017
1 parent 42ad25d commit b529129
Show file tree
Hide file tree
Showing 22 changed files with 406 additions and 251 deletions.
21 changes: 0 additions & 21 deletions client/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,23 +89,6 @@ const receiveDeleteRepoError = error => ({



const requestPullRepo = name => dispatch => {
dispatch({type: 'wsRequestPullRepo', name})
wsSend({type: 'pullRepo', name})
}

const receivePullRepo = name => ({
type: 'wsReceivePullRepo',
name,
})

const receivePullRepoError = error => ({
type: 'wsReceivePullRepoError',
error,
})



const requestSearch = ({repoName, query}) => dispatch => {
if (query) {
dispatch({type: 'wsRequestSearch', repoName, query})
Expand Down Expand Up @@ -160,10 +143,6 @@ export default {
receiveDeleteRepo,
receiveDeleteRepoError,

requestPullRepo,
receivePullRepo,
receivePullRepoError,

requestLogout,

requestSearch,
Expand Down
2 changes: 1 addition & 1 deletion client/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ html, body, #react-root, main {
color: $gray !important;
}

&--fetchFailed {
&--pullFailed {
@include error;
}
}
Expand Down
14 changes: 2 additions & 12 deletions client/src/repo-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export const NotFound = f(({name}) =>

export const getRepoStatus = repo => [
!repo.cloned && 'not cloned yet',
repo.beingFetched && 'being fetched',
repo.fetchFailed && 'last fetch failed',
repo.beingPulled && 'being pulled',
repo.pullFailed && 'last pull failed',
].filter(v => v).join(', ')

class RepoInfoPageV extends React.Component {
Expand All @@ -37,11 +37,6 @@ class RepoInfoPageV extends React.Component {
return dispatch(actions.requestDeleteRepo(repoName))
}

pull = () => {
const {dispatch, repoName} = this.props
return dispatch(actions.requestPullRepo(repoName))
}

componentWillReceiveProps(nextProps) {
if (nextProps.deleteRepo.name == nextProps.repoName) {
nextProps.router.navigate('root')
Expand Down Expand Up @@ -80,11 +75,6 @@ class RepoInfoPageV extends React.Component {
'search',
),

loggedIn && !repo.beingFetched && Button(
{onClick: this.pull},
repo.cloned ? 'pull' : 'clone again',
),

loggedIn && !deleteRepo.loading && Button(
{onClick: this.remove},
'delete',
Expand Down
2 changes: 1 addition & 1 deletion client/src/repos.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const RepoLink = f(({repo}) =>
className: classnames(
'RepoLink',
!repo.cloned && 'RepoLink--notCloned',
repo.fetchFailed && 'RepoLink--fetchFailed',
repo.pullFailed && 'RepoLink--pullFailed',
),
routeName: repo.cloned ? 'repositorySearch' : 'repositoryInfo',
routeParams: {name: repo.name},
Expand Down
54 changes: 36 additions & 18 deletions server/src/clones.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Functions to manage cloned Git repositories

const EventEmitter = require('events')
const assert = require('assert')
const {promisify} = require('util')
const fs = require('fs')
Expand Down Expand Up @@ -57,14 +58,37 @@ const execGit = async (args, options) => {
}
}

class Clones {
// Events:
// - `pullStart` is emitted at the moment when a repo begins to
// be pulled (or cloned)
//
// - `pullEnd` is emitted at the moment when a pull (or clone)
// ends (successfully or not).
//
// Given a repository A, `pullEnd` and `pullStart` are emitted just
// after the return value of `clones.isBeingPulled(A)` has changed.
class Clones extends EventEmitter {
constructor(baseDirName) {
super()

this._basePath = path.join(__dirname, '..', 'var', baseDirName)
this._beingFetched = new Set()
this._beingPulled = new Set()
}

isBeingPulled(name) {
return this._beingPulled.has(name)
}

isBeingFetched(name) {
return this._beingFetched.has(name)
_emitPullStart(name) {
assert(!this.isBeingPulled(name))
this._beingPulled.add(name)
this.emit('pullStart', {repoName: name})
}

_emitPullEnd(name) {
assert(this.isBeingPulled(name))
this._beingPulled.delete(name)
this.emit('pullEnd', {repoName: name})
}

path(name) {
Expand All @@ -87,16 +111,11 @@ class Clones {
return stdout.trim()
}

// `onFetchStart` is called just after
// `clones.isBeingFetched(name)` becomes `true`
async clone(url, name, options = {}) {
const onFetchStart = options.onFetchStart || (() => {})

assert(!this.isBeingFetched(name))
async clone(url, name, options) {
assert(!options, 'deprecated')

try {
this._beingFetched.add(name)
onFetchStart()
this._emitPullStart(name)

await this.remove(name)

Expand All @@ -116,18 +135,17 @@ class Clones {
{timeout: 60 * 1000},
)
} finally {
this._beingFetched.delete(name)
this._emitPullEnd(name)
}
}

async pull(name, options = {}) {
const onFetchStart = options.onFetchStart || (() => {})
async pull(name, options) {
assert(!options, 'deprecated')

const dir = this.path(name)

try {
this._beingFetched.add(name)
onFetchStart()
this._emitPullStart(name)

const branch = await this._getCurrentBranch(name)

Expand Down Expand Up @@ -155,7 +173,7 @@ class Clones {
]
)
} finally {
this._beingFetched.delete(name)
this._emitPullEnd(name)
}
}

Expand Down
4 changes: 2 additions & 2 deletions server/src/clones.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('clones', () => {
await clones.remove('r')
assert(!await clones.exists('r'))
assert(!await fileExists(path.join(basePath, 'r', 'package.json')))
}).timeout(10 * 1000)
}).timeout(20 * 1000)

it('fails with a nonexistent repo', async () => {
try {
Expand All @@ -41,5 +41,5 @@ describe('clones', () => {
return
}
assert(false)
}).timeout(10 * 1000)
}).timeout(20 * 1000)
})
4 changes: 3 additions & 1 deletion server/src/create-test-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const createSearches = require('./searches')
const {dummyLogger} = require('./log')
const createDb = require('./db')
const createClones = require('./clones')
const createPullCron = require('./pull-cron')
const serve = require('./serve')

module.exports = async () => {
Expand All @@ -16,9 +17,10 @@ module.exports = async () => {
const clones = createClones('test-clones')
const searches = createSearches()
const log = dummyLogger
const pullCron = createPullCron({db, clones, log})

const server = await serve({
db, clones, searches, log,
db, clones, searches, log, pullCron,
address, port,
})

Expand Down
54 changes: 39 additions & 15 deletions server/src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const {promisify} = require('util')

const migrate = require('./migrate')
const {randomString} = require('./util')
const sqlite = require('./sqlite-promise')
const createSqliteDriver = require('./sqlite-promise')

const V = require('@motet_a/validate')

Expand All @@ -17,14 +17,19 @@ const repoSpec = V.shape({
webUrl: V.string.max(256),
gitUrl: V.string.min(1).max(256),
cloned: V.boolean,
fetchFailed: V.boolean,
pullFailed: V.boolean,
})

const repoFromSQLite = dbRepo =>
dbRepo && Object.assign({}, dbRepo, {
const repoFromSQLite = dbRepo => {
if (!dbRepo) {
return
}

return Object.assign({}, dbRepo, {
cloned: !!dbRepo.cloned,
fetchFailed: !!dbRepo.fetchFailed,
pullFailed: !!dbRepo.pullFailed,
})
}

const errors = {
ALREADY_EXISTS: 'ALREADY_EXISTS',
Expand All @@ -43,6 +48,9 @@ const newDbError = code => {
const isDbError = error =>
error && error[dbErrorSymbol] === dbErrorSymbol

const nowTimestamp = () =>
new Date().getTime()

class Db {
constructor(db) {
assert(db)
Expand All @@ -63,6 +71,20 @@ class Db {
return repos.map(repoFromSQLite)
}

async getRepoNamesToPull(defaultPullDelay) {
const repos = await this._db.all(
`SELECT name, pulledAt, cloned, pullDelay
FROM repos
WHERE cloned AND (
coalesce(pulledAt, 0) + coalesce(pullDelay, ?)
) < ?;`,
defaultPullDelay,
nowTimestamp()
)

return repos.map(r => r.name)
}

async getRepo(name) {
const repo = await this._db.get(
`SELECT *
Expand All @@ -80,13 +102,13 @@ class Db {
try {
return await this._db.run(
`INSERT INTO repos (
name, webUrl, gitUrl, cloned, fetchFailed
name, webUrl, gitUrl, cloned, pullFailed
) VALUES (?, ?, ?, ?, ?);`,
repo.name,
repo.webUrl,
repo.gitUrl,
repo.cloned,
repo.fetchFailed
repo.pullFailed
)
} catch (error) {
if (error.code === 'SQLITE_CONSTRAINT' &&
Expand All @@ -99,12 +121,13 @@ class Db {
}

// Returns a Promise
setRepoFetchFailed(name, fetchFailed) {
setRepoPullFailed(name, pullFailed) {
return this._db.run(
`UPDATE repos
SET fetchFailed = ?
SET pullFailed = ?, pulledAt = ?
WHERE name = ?;`,
fetchFailed,
pullFailed,
nowTimestamp(),
name
)
}
Expand All @@ -113,9 +136,10 @@ class Db {
setRepoCloned(name, cloned) {
return this._db.run(
`UPDATE repos
SET cloned = ?
SET cloned = ?, pulledAt = ?
WHERE name = ?;`,
cloned,
nowTimestamp(),
name
)
}
Expand Down Expand Up @@ -206,12 +230,12 @@ const createDb = async (fileName, options = {}) => {
__dirname, '..', 'var', fileName + '.sqlitedb'
)

const rawDB = await sqlite(filePath)
const sqliteDriver = await createSqliteDriver(filePath)
if (dropTables) {
await rawDB.dropTables()
await sqliteDriver.dropTables()
}
await migrate(rawDB)
const db = new Db(rawDB)
await migrate(sqliteDriver)
const db = new Db(sqliteDriver)
return db
}

Expand Down
Loading

0 comments on commit b529129

Please sign in to comment.