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.

282 lines
7.7 KiB

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