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.

284 lines
7.7 KiB

  1. 'use strict'
  2. /* global lang, winston */
  3. const Git = require('git-wrapper2-promise')
  4. const Promise = require('bluebird')
  5. const path = require('path')
  6. const fs = Promise.promisifyAll(require('fs'))
  7. const _ = require('lodash')
  8. const URL = require('url')
  9. const securityHelper = require('../helpers/security')
  10. /**
  11. * Git Model
  12. */
  13. module.exports = {
  14. _git: null,
  15. _url: '',
  16. _repo: {
  17. path: '',
  18. branch: 'master',
  19. exists: false
  20. },
  21. _signature: {
  22. email: 'wiki@example.com'
  23. },
  24. _opts: {
  25. clone: {},
  26. push: {}
  27. },
  28. onReady: null,
  29. /**
  30. * Initialize Git model
  31. *
  32. * @return {Object} Git model instance
  33. */
  34. init () {
  35. let self = this
  36. // -> Build repository path
  37. if (_.isEmpty(appconfig.paths.repo)) {
  38. self._repo.path = path.join(ROOTPATH, 'repo')
  39. } else {
  40. self._repo.path = appconfig.paths.repo
  41. }
  42. // -> Initialize repository
  43. self.onReady = self._initRepo(appconfig)
  44. // Define signature
  45. if (appconfig.git) {
  46. self._signature.email = appconfig.git.serverEmail || 'wiki@example.com'
  47. }
  48. return self
  49. },
  50. /**
  51. * Initialize Git repository
  52. *
  53. * @param {Object} appconfig The application config
  54. * @return {Object} Promise
  55. */
  56. _initRepo (appconfig) {
  57. let self = this
  58. winston.info('Checking Git repository...')
  59. // -> Check if path is accessible
  60. return fs.mkdirAsync(self._repo.path).catch((err) => {
  61. if (err.code !== 'EEXIST') {
  62. winston.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 (appconfig.git === false) {
  75. winston.info('Remote Git syncing is disabled. Not recommended!')
  76. return Promise.resolve(true)
  77. }
  78. // Initialize remote
  79. let urlObj = URL.parse(appconfig.git.url)
  80. if (appconfig.git.auth.type !== 'ssh') {
  81. urlObj.auth = appconfig.git.auth.username + ':' + appconfig.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(appconfig.git.auth.sslVerify)]) }
  88. ]
  89. if (appconfig.git.auth.type === 'ssh') {
  90. gitConfigs.push(() => {
  91. return self._git.exec('config', ['--local', 'core.sshCommand', 'ssh -i "' + appconfig.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. winston.error(err)
  104. })
  105. })
  106. }).catch((err) => {
  107. winston.error('Git remote error!')
  108. throw err
  109. }).then(() => {
  110. winston.info('Git repository is 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(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 (appconfig.git === false) {
  131. return Promise.resolve(true)
  132. }
  133. // Fetch
  134. winston.info('Performing pull from remote Git repository...')
  135. return self._git.pull('origin', self._repo.branch).then((cProc) => {
  136. winston.info('Git Pull completed.')
  137. })
  138. .catch((err) => {
  139. winston.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. winston.info('Performing push to remote Git repository...')
  148. return self._git.push('origin', self._repo.branch).then(() => {
  149. return winston.info('Git Push completed.')
  150. })
  151. } else {
  152. winston.info('Git Push skipped. Repository is already in sync.')
  153. }
  154. return true
  155. })
  156. })
  157. .catch((err) => {
  158. winston.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) ? lang.t('git:updated', { path: gitFilePath }) : 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. return self._git.exec('mv', [gitFilePath, gitNewFilePath]).then((cProc) => {
  197. let out = cProc.stdout.toString()
  198. if (_.includes(out, 'fatal')) {
  199. let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
  200. throw new Error(errorMsg)
  201. }
  202. return true
  203. })
  204. },
  205. /**
  206. * Commits uploads changes.
  207. *
  208. * @param {String} msg The commit message
  209. * @return {Promise} Resolve on commit success
  210. */
  211. commitUploads (msg) {
  212. let self = this
  213. msg = msg || 'Uploads repository sync'
  214. return self._git.add('uploads').then(() => {
  215. return self._git.commit(msg).catch((err) => {
  216. if (_.includes(err.stdout, 'nothing to commit')) { return true }
  217. })
  218. })
  219. },
  220. getHistory (entryPath) {
  221. let self = this
  222. let gitFilePath = entryPath + '.md'
  223. return self._git.exec('log', ['-n', '25', '--format=format:%H %h %cI %cE %cN', '--', gitFilePath]).then((cProc) => {
  224. let out = cProc.stdout.toString()
  225. if (_.includes(out, 'fatal')) {
  226. let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
  227. throw new Error(errorMsg)
  228. }
  229. let hist = _.chain(out).split('\n').map(h => {
  230. let hParts = h.split(' ', 4)
  231. return {
  232. commit: hParts[0],
  233. commitAbbr: hParts[1],
  234. date: hParts[2],
  235. email: hParts[3],
  236. name: hParts[4]
  237. }
  238. }).value()
  239. return hist
  240. })
  241. }
  242. }