You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
8.7 KiB

  1. 'use strict'
  2. /* global wiki */
  3. const Git = require('git-wrapper2-promise')
  4. const Promise = require('bluebird')
  5. const path = require('path')
  6. const fs = Promise.promisifyAll(require('fs-extra'))
  7. const _ = require('lodash')
  8. const URL = require('url')
  9. const moment = require('moment')
  10. const securityHelper = require('../helpers/security')
  11. /**
  12. * Git Model
  13. */
  14. module.exports = {
  15. _git: null,
  16. _url: '',
  17. _repo: {
  18. path: '',
  19. branch: 'master',
  20. exists: false
  21. },
  22. _signature: {
  23. email: 'wiki@example.com'
  24. },
  25. _opts: {
  26. clone: {},
  27. push: {}
  28. },
  29. onReady: null,
  30. /**
  31. * Initialize Git model
  32. *
  33. * @return {Object} Git model instance
  34. */
  35. init() {
  36. let self = this
  37. // -> Build repository path
  38. if (_.isEmpty(wiki.config.paths.repo)) {
  39. self._repo.path = path.join(wiki.ROOTPATH, 'repo')
  40. } else {
  41. self._repo.path = wiki.config.paths.repo
  42. }
  43. // -> Initialize repository
  44. self.onReady = (wiki.IS_MASTER) ? self._initRepo() : Promise.resolve()
  45. if (wiki.config.git) {
  46. self._repo.branch = wiki.config.git.branch || 'master'
  47. self._signature.email = wiki.config.git.serverEmail || 'wiki@example.com'
  48. }
  49. return self
  50. },
  51. /**
  52. * Initialize Git repository
  53. *
  54. * @param {Object} wiki.config The application config
  55. * @return {Object} Promise
  56. */
  57. _initRepo() {
  58. let self = this
  59. // -> Check if path is accessible
  60. return fs.mkdirAsync(self._repo.path).catch((err) => {
  61. if (err.code !== 'EEXIST') {
  62. wiki.logger.error('Invalid Git repository path or missing permissions.')
  63. }
  64. }).then(() => {
  65. self._git = new Git({ 'git-dir': self._repo.path })
  66. // -> Check if path already contains a git working folder
  67. return self._git.isRepo().then((isRepo) => {
  68. self._repo.exists = isRepo
  69. return (!isRepo) ? self._git.exec('init') : true
  70. }).catch((err) => { // eslint-disable-line handle-callback-err
  71. self._repo.exists = false
  72. })
  73. }).then(() => {
  74. if (wiki.config.git === false) {
  75. wiki.logger.warn('Remote Git syncing is disabled. Not recommended!')
  76. return Promise.resolve(true)
  77. }
  78. // Initialize remote
  79. let urlObj = URL.parse(wiki.config.git.url)
  80. if (wiki.config.git.auth.type !== 'ssh') {
  81. urlObj.auth = wiki.config.git.auth.username + ':' + wiki.config.git.auth.password
  82. }
  83. self._url = URL.format(urlObj)
  84. let gitConfigs = [
  85. () => { return self._git.exec('config', ['--local', 'user.name', 'Wiki']) },
  86. () => { return self._git.exec('config', ['--local', 'user.email', self._signature.email]) },
  87. () => { return self._git.exec('config', ['--local', '--bool', 'http.sslVerify', _.toString(wiki.config.git.auth.sslVerify)]) }
  88. ]
  89. if (wiki.config.git.auth.type === 'ssh') {
  90. gitConfigs.push(() => {
  91. return self._git.exec('config', ['--local', 'core.sshCommand', 'ssh -i "' + wiki.config.git.auth.privateKey + '" -o StrictHostKeyChecking=no'])
  92. })
  93. }
  94. return self._git.exec('remote', 'show').then((cProc) => {
  95. let out = cProc.stdout.toString()
  96. return Promise.each(gitConfigs, fn => { return fn() }).then(() => {
  97. if (!_.includes(out, 'origin')) {
  98. return self._git.exec('remote', ['add', 'origin', self._url])
  99. } else {
  100. return self._git.exec('remote', ['set-url', 'origin', self._url])
  101. }
  102. }).catch(err => {
  103. wiki.logger.error(err)
  104. })
  105. })
  106. }).catch((err) => {
  107. wiki.logger.error('Git remote error!')
  108. throw err
  109. }).then(() => {
  110. wiki.logger.info('Git Repository: OK')
  111. return true
  112. })
  113. },
  114. /**
  115. * Gets the repo path.
  116. *
  117. * @return {String} The repo path.
  118. */
  119. getRepoPath() {
  120. return this._repo.path || path.join(wiki.ROOTPATH, 'repo')
  121. },
  122. /**
  123. * Sync with the remote repository
  124. *
  125. * @return {Promise} Resolve on sync success
  126. */
  127. resync() {
  128. let self = this
  129. // Is git remote disabled?
  130. if (wiki.config.git === false) {
  131. return Promise.resolve(true)
  132. }
  133. // Fetch
  134. wiki.logger.info('Performing pull from remote Git repository...')
  135. return self._git.pull('origin', self._repo.branch).then((cProc) => {
  136. wiki.logger.info('Git Pull completed.')
  137. })
  138. .catch((err) => {
  139. wiki.logger.error('Unable to fetch from git origin!')
  140. throw err
  141. })
  142. .then(() => {
  143. // Check for changes
  144. return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
  145. let out = cProc.stdout.toString()
  146. if (_.includes(out, 'commit')) {
  147. wiki.logger.info('Performing push to remote Git repository...')
  148. return self._git.push('origin', self._repo.branch).then(() => {
  149. return wiki.logger.info('Git Push completed.')
  150. })
  151. } else {
  152. wiki.logger.info('Git Push skipped. Repository is already in sync.')
  153. }
  154. return true
  155. })
  156. })
  157. .catch((err) => {
  158. wiki.logger.error('Unable to push changes to remote Git repository!')
  159. throw err
  160. })
  161. },
  162. /**
  163. * Commits a document.
  164. *
  165. * @param {String} entryPath The entry path
  166. * @return {Promise} Resolve on commit success
  167. */
  168. commitDocument(entryPath, author) {
  169. let self = this
  170. let gitFilePath = entryPath + '.md'
  171. let commitMsg = ''
  172. return self._git.exec('ls-files', gitFilePath).then((cProc) => {
  173. let out = cProc.stdout.toString()
  174. return _.includes(out, gitFilePath)
  175. }).then((isTracked) => {
  176. commitMsg = (isTracked) ? wiki.lang.t('git:updated', { path: gitFilePath }) : wiki.lang.t('git:added', { path: gitFilePath })
  177. return self._git.add(gitFilePath)
  178. }).then(() => {
  179. let commitUsr = securityHelper.sanitizeCommitUser(author)
  180. return self._git.exec('commit', ['-m', commitMsg, '--author="' + commitUsr.name + ' <' + commitUsr.email + '>"']).catch((err) => {
  181. if (_.includes(err.stdout, 'nothing to commit')) { return true }
  182. })
  183. })
  184. },
  185. /**
  186. * Move a document.
  187. *
  188. * @param {String} entryPath The current entry path
  189. * @param {String} newEntryPath The new entry path
  190. * @return {Promise<Boolean>} Resolve on success
  191. */
  192. moveDocument(entryPath, newEntryPath) {
  193. let self = this
  194. let gitFilePath = entryPath + '.md'
  195. let gitNewFilePath = newEntryPath + '.md'
  196. let destPathObj = path.parse(this.getRepoPath() + '/' + gitNewFilePath)
  197. return fs.ensureDir(destPathObj.dir).then(() => {
  198. return self._git.exec('mv', [gitFilePath, gitNewFilePath]).then((cProc) => {
  199. let out = cProc.stdout.toString()
  200. if (_.includes(out, 'fatal')) {
  201. let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
  202. throw new Error(errorMsg)
  203. }
  204. return true
  205. })
  206. })
  207. },
  208. /**
  209. * Commits uploads changes.
  210. *
  211. * @param {String} msg The commit message
  212. * @return {Promise} Resolve on commit success
  213. */
  214. commitUploads(msg) {
  215. let self = this
  216. msg = msg || 'Uploads repository sync'
  217. return self._git.add('uploads').then(() => {
  218. return self._git.commit(msg).catch((err) => {
  219. if (_.includes(err.stdout, 'nothing to commit')) { return true }
  220. })
  221. })
  222. },
  223. getHistory(entryPath) {
  224. let self = this
  225. let gitFilePath = entryPath + '.md'
  226. return self._git.exec('log', ['--max-count=25', '--skip=1', '--format=format:%H %h %cI %cE %cN', '--', gitFilePath]).then((cProc) => {
  227. let out = cProc.stdout.toString()
  228. if (_.includes(out, 'fatal')) {
  229. let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
  230. throw new Error(errorMsg)
  231. }
  232. let hist = _.chain(out).split('\n').map(h => {
  233. let hParts = h.split(' ', 4)
  234. let hDate = moment(hParts[2])
  235. return {
  236. commit: hParts[0],
  237. commitAbbr: hParts[1],
  238. date: hParts[2],
  239. dateFull: hDate.format('LLLL'),
  240. dateCalendar: hDate.calendar(null, { sameElse: 'llll' }),
  241. email: hParts[3],
  242. name: hParts[4]
  243. }
  244. }).value()
  245. return hist
  246. })
  247. },
  248. getHistoryDiff(path, commit, comparewith) {
  249. let self = this
  250. if (!comparewith) {
  251. comparewith = 'HEAD'
  252. }
  253. return self._git.exec('diff', ['--no-color', `${commit}:${path}.md`, `${comparewith}:${path}.md`]).then((cProc) => {
  254. let out = cProc.stdout.toString()
  255. if (_.startsWith(out, 'fatal: ')) {
  256. throw new Error(out)
  257. } else if (!_.includes(out, 'diff')) {
  258. throw new Error('Unable to query diff data.')
  259. } else {
  260. return out
  261. }
  262. })
  263. }
  264. }