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.

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