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.

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