Builder.coffee 9.32 KB
Newer Older
1
2
################################################################################
#
3
4
#	Better
#
5
#	Builds the Better data for the site and the apps.
6
#
7
#	Updates the repositories, runs the Blockdown parser on the content, and
8
9
#	applies the respective theme.
#
10
11
12
#	This is Independent Technology.
#
#	▲❤ We practice Ethical Design (https://ind.ie/ethical-design)
13
#
14
15
#	© Aral Balkan. © Ind.ie. All Rights Reserved.
#	Released with love by Ind.ie under GNU AGPLv3 or later.
16
17
18
19
20
#	Free as in freedom. Please see the LICENSE file.
#
################################################################################

spawn = require('child-process-promise').spawn
21
path = require 'path'
22
23
24
25

winston = require 'winston'

app = require './App'
26

27
28
29
Blockdown = require './Blockdown'
NodeGit = require 'nodegit'

30
31
exec = (require 'child_process').exec

32
33
touch = require 'touch'

34
Cache = require './Cache2'
35

36
37
Promise = require 'thrush'

38
39
WebkitContentBlockerRulesToEasyList = require './WebkitContentBlockerRulesToEasyList'

40
41
42
class Builder
	instance = null

43
44
	log: null

45
46
47
	# This is the authorisation object we send
	authorisation: null

48
49
50
51
	sshPath: null
	privateSSHKeyPath: null
	publicSSHKeyPath: null

52
53
54
55
56
57
	constructor: ->
		# Singleton access.
		if instance
			return instance

		# Set up logging.
58
		logFile = path.join app.logsDirectory, 'Builder.log'
59
		@log = new winston.Logger
60
61
			transports:
				[
62
					new (winston.transports.Console)({ level: 'info' , formatter: (options) -> return options.message})
63
64
65
					new (winston.transports.File)({ filename: logFile, level: 'debug' })
				]

Aral Balkan's avatar
Aral Balkan committed
66
		# Convenience: important paths
67
		@sshPath = path.join app.privateDirectory, 'ssh'
68
69
70
		@privateSSHKeyPath = path.join @sshPath, 'id_rsa'
		@publicSSHKeyPath = path.join @sshPath, 'id_rsa.pub'

71
72
73
74
75
		# Create the authorisation object (that we use when making calls to the origin remotes
		# on source.ind.ie)
		@authorisation =
			callbacks:
				credentials: (url, userName) =>
76
					@log.info "Providing credentials: #{url}, #{userName}"
77
78
79
80
					NodeGit.Cred.sshKeyNew userName, @publicSSHKeyPath, @privateSSHKeyPath, ''
				# certificateCheck: () ->
				# 	return 1

81
82
83
84
85
86
87
88
89
		# Save reference to the singleton instance and return it.
		instance = @
		return instance


	#
	# Start a build.
	#
	build: =>
90
91
		@updateCache()
			.then @pullContentRepository
92
			.then @pullThemesRepository
93
94
			.then @renderBlockdown
			.then @updateRepositories
95
			.then =>
96
				@log.info "\t✓ Blockdown content ready."
97

98
99
100
101
				# If the app is running in development, fire off a
				# notification to let people know that rendering is done.
				exec "osascript -e 'display notification \"♥ Ready\" with title \"Better\" sound name \"Ping\"'"

102
103
104
105
106
				# Touch a file to signal that the build is complete.
				# (Used by the installer on the first run to know
				# when to continue with the installation process.)
				touch.sync (path.join app.homeDirectory, '.private', 'last-build-stamp')

107
		.catch (e) =>
108
109
110
111
			# If the app is running in development, fire off a
			# notification to let people know that rendering is done.
			exec "osascript -e 'display notification \"✗ Error\" with title \"Better\" sound name \"Basso\"'"

112
			@log.error "Build error.\n\n#{e}"
113
114
115
			console.trace()
			process.exit 1

116
117
118
119
	#
	# Update the cache.
	#
	updateCache: =>
120
121
		Promise.try =>
				(new Cache).build()
122
123

	#
124
	# Render content in Blockdown format.
125
126
	#
	renderBlockdown: =>
127
128
129

		startTime = new Date

130
		# @log.info 'Rendering content…'
131
		blockdown = new Blockdown
132
133
134

		easyListConverter = new WebkitContentBlockerRulesToEasyList

135
136
		blockdown.renderDataForSite()
		.then =>
137
			@log.info "\t✓ Rendered content for site."
Aral Balkan's avatar
Aral Balkan committed
138
			easyListConverter.convert()
139
			.then =>
Aral Balkan's avatar
Aral Balkan committed
140
141
142
143
144
145
146
				@log.info "\t✓ Rendered EasyList version."
				
				duration = ((new Date) - startTime)
				durationUnit = "ms"
				if duration > 1000
					duration = duration / 1000
					durationUnit = "seconds"
147

Aral Balkan's avatar
Aral Balkan committed
148
				@log.info "\t✓ Render complete. (#{duration} #{durationUnit})"
149
150
151
152
153
154
155


	#
	# Returns the latest content repository changes.
	# (Fetches and merges the latest changes from the content repository.)
	#
	pullContentRepository: =>
156
		@log.debug 'Pulling content repository.'
157
		return @pullRepository app.contentDirectory
158
159
			.then ->
				app.refreshContentIndex()
160
161
162
163
164


	#
	# Pulls the theme repository at the given path and then refreshes the themes.
	#
165
166
	pullThemesRepository: (repositoryDirectory) =>
		@pullRepository app.themesDirectory
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
			.then ->
				app.loadThemes()

	#
	# Returns the latest content repository changes for the repository at the given path.
	# (Fetches and merges the latest changes.)
	#
	pullRepository: (repositoryDirectory) =>
		repository = null

		# Perform a pull (fetch + merge), based on
		# https://github.com/nodegit/nodegit/blob/master/examples/pull.js
		NodeGit.Repository.open repositoryDirectory
			.then (repo) =>
				# Fetch
				repository = repo
				return repository.fetch 'origin', @authorisation
			.then =>
				# Merge
				repository.mergeBranches 'master', 'origin/master'
187
			.catch (e) =>
188
				@log.error "Build error.\n\n#{e}"
189
190
				console.trace()
				process.exit 1
191
192
193
194
195
196

	#
	# Updates the data for site and apps repositories.
	#
	updateRepositories: =>

197
		# @log.info 'Updating the data repositories for the app and the site…'
198
199
200

		@updateRepository app.dataForSiteDirectory
			.then =>
Aral Balkan's avatar
Aral Balkan committed
201
				@log.info "\t✓ Data repository update for site complete."
202
203
204
205
206
207
208
209
210


	#
	# Update the requested repository at the repository path and push to the remote.
	#
	updateRepository: (repositoryPath) =>

		repository = null
		index = null
211
		signature = NodeGit.Signature.now 'Better Builder', 'hello+better+builder@ind.ie'
212
213
214
215
216
217

		indexTreeOid = null
		siteURL = null

		NodeGit.Repository.open repositoryPath
			.then (repositoryResult) =>
218
				@log.debug "Update: got repository open result for #{repositoryPath}"
219
				repository = repositoryResult
220
				repository.index()
221
			.then (indexResult) =>
222
				@log.debug 'Update: got index result'
223
224
225
226
227
228
229
				index = indexResult
				index.read 1	# Force a read of the index into memory.
				# The ADD_CHECK_PATHSPEC should make addAll function like
				# git add --all (according to https://libgit2.github.com/libgit2/#HEAD/group/index/git_index_add_all)
				index.addAll '*', NodeGit.Index.ADD_OPTION.ADD_CHECK_PATHSPEC
				# DEBUG: Dry run
				# index.addAll '*', NodeGit.Index.ADD_OPTION.ADD_CHECK_PATHSPEC, (path, matchedPathSpec, payload) =>
230
231
232
233
234
235
				# 	@log.info "Path:"
				# 	@log.info path
				# 	@log.info "matchedPathSpec:"
				# 	@log.info matchedPathSpec
				# 	@log.info "payload:"
				# 	@log.info payload
236
237
238
239
240
241
242
243
				# 	return 1

			.then (result) =>
				writeResult = index.write() # sync
				index.writeTree()
			.then (writeTreeOidResult) =>
				indexTreeOid = writeTreeOidResult
				# Get the parent reference in the next two steps
Aral Balkan's avatar
Aral Balkan committed
244
				# (Courtesy Rafael: http://stackoverflow.com/a/28890806/253485)
245
246
247
248
249
250
251
252
253
				NodeGit.Reference.nameToId repository, 'HEAD'
			.then (head) =>
				repository.getCommit head
			.then (parent) =>
				repository.createCommit 'HEAD', signature, signature, 'Latest generated content.', indexTreeOid, [parent]
			.then (commitOidResult) =>
				NodeGit.Remote.lookup repository, 'origin'
			.then (origin) =>
				# NodeGit.Remote.initCallbacks @authorisation
254
255
256
257
258
259
260
261
262
				origin.push ['refs/heads/master:refs/heads/master'], 
				{ 
					credentials: @authorisation,
					pushUpdateReference: (ref, status, data) ->
						@log.debug "Push result status: #{status}"
				}
				# null,
				# repository.defaultSignature()
				# 'Push to master'
263
			.then (pushResult) =>
Aral Balkan's avatar
Aral Balkan committed
264
				@log.debug "site.origin push result: #{pushResult}"
265
266
267
				if repositoryPath == app.dataForSiteDirectory
					@pushToSite repository
			.then =>
268
				@log.debug "Repository update complete for: #{repositoryPath}"
269
			.catch ((e) =>
270
				@log.error "Git error: #{e}")
271

272
273
274
275
276
277
278
279
280
281
282

	#
	# Pushes to the site.
	#
	pushToSite: (repository) =>

		siteURL = null

		NodeGit.Remote.lookup repository, 'site'
			.then (site) =>
				siteURL = site.url()
283
				@log.debug "Got site: #{siteURL}"
284
285
286
287
288
289
290

				# NodeGit.Remote.initCallbacks @authorisation
				site.push ['refs/heads/master:refs/heads/master'], @authorisation
					# null,
					# repository.defaultSignature()
					# 'Push to master'
			.then (pushResult) =>
291
				@log.debug "Site push result: #{pushResult}"
292
293
294
295
296
297
298
299

				# If we pushed locally, we must manually trigger the post-receive hook.
				# (Nodegit — actually libgit2 — doesn’t trigger web hooks but, according to
				# the discussion below, it would not have triggered the post-receive hook
				# anyway as that’s the responsibility of the app.)
				#
				# (See https://github.com/libgit2/libgit2/issues/964#issuecomment-109998760)
				if siteURL[0] == '/'
300
					siteLocalPostReceiveHooksPath = path.join app.homeDirectory, '.private', 'site-local', 'data.git', 'hooks'
301
					siteLocalPostReceiveHookPath = path.join siteLocalPostReceiveHooksPath, 'post-receive'
302
					@log.debug "We pushed locally, about to manually trigger the post-receive hook at #{siteLocalPostReceiveHookPath}"
303
304
					spawn siteLocalPostReceiveHookPath, [], {cwd:siteLocalPostReceiveHooksPath}
						.progress (childProcess) =>
305
							@log.debug "Child process running with ID: #{childProcess.pid}"
306
							childProcess.stdout.on 'data', (data) =>
307
								@log.debug "Child stdout: #{data.toString()}"
308
							childProcess.stderr.on 'data', (data) =>
309
								@log.debug "Child stderr: #{data.toString()}"
310
						.then =>
311
							@log.debug "Local push complete."
312
						.fail (error) =>
313
							@log.error "Local push failed with error: #{error}"
314

315
				@log.debug 'Completed update.'
316

317
module.exports = Builder