git.js 8.68 KB
Newer Older
1
'use strict'
NGPixel's avatar
NGPixel committed
2

3
/* global wiki */
4

5 6 7
const Git = require('git-wrapper2-promise')
const Promise = require('bluebird')
const path = require('path')
8
const fs = Promise.promisifyAll(require('fs-extra'))
9 10
const _ = require('lodash')
const URL = require('url')
11
const moment = require('moment')
NGPixel's avatar
NGPixel committed
12

13 14
const securityHelper = require('../helpers/security')

NGPixel's avatar
NGPixel committed
15 16 17 18 19
/**
 * Git Model
 */
module.exports = {

20 21 22 23 24 25 26 27
  _git: null,
  _url: '',
  _repo: {
    path: '',
    branch: 'master',
    exists: false
  },
  _signature: {
28
    email: 'wiki@example.com'
29 30 31 32 33 34 35 36 37 38 39 40
  },
  _opts: {
    clone: {},
    push: {}
  },
  onReady: null,

  /**
   * Initialize Git model
   *
   * @return     {Object}  Git model instance
   */
41
  init() {
42 43 44 45
    let self = this

    // -> Build repository path

46 47
    if (_.isEmpty(wiki.config.paths.repo)) {
      self._repo.path = path.join(wiki.ROOTPATH, 'repo')
48
    } else {
49
      self._repo.path = wiki.config.paths.repo
50 51 52 53
    }

    // -> Initialize repository

54
    self.onReady = (wiki.IS_MASTER) ? self._initRepo() : Promise.resolve()
55

56
    if (wiki.config.git) {
NGPixel's avatar
NGPixel committed
57 58
      self._repo.branch = wiki.config.git.branch || 'master'
      self._signature.email = wiki.config.git.serverEmail || 'wiki@example.com'
59
    }
60 61 62 63 64 65 66

    return self
  },

  /**
   * Initialize Git repository
   *
67
   * @param      {Object}  wiki.config  The application config
68 69
   * @return     {Object}  Promise
   */
70
  _initRepo() {
71 72 73 74 75 76
    let self = this

    // -> Check if path is accessible

    return fs.mkdirAsync(self._repo.path).catch((err) => {
      if (err.code !== 'EEXIST') {
77
        wiki.logger.error('Invalid Git repository path or missing permissions.')
78 79 80 81 82 83 84 85 86 87 88 89 90
      }
    }).then(() => {
      self._git = new Git({ 'git-dir': self._repo.path })

      // -> Check if path already contains a git working folder

      return self._git.isRepo().then((isRepo) => {
        self._repo.exists = isRepo
        return (!isRepo) ? self._git.exec('init') : true
      }).catch((err) => { // eslint-disable-line handle-callback-err
        self._repo.exists = false
      })
    }).then(() => {
91
      if (wiki.config.git === false) {
92
        wiki.logger.warn('Remote Git syncing is disabled. Not recommended!')
93 94 95
        return Promise.resolve(true)
      }

96 97
      // Initialize remote

98 99 100
      let urlObj = URL.parse(wiki.config.git.url)
      if (wiki.config.git.auth.type !== 'ssh') {
        urlObj.auth = wiki.config.git.auth.username + ':' + wiki.config.git.auth.password
101
      }
102 103
      self._url = URL.format(urlObj)

104
      let gitConfigs = [
105
        () => { return self._git.exec('config', ['--local', 'user.name', 'Wiki']) },
106
        () => { return self._git.exec('config', ['--local', 'user.email', self._signature.email]) },
107
        () => { return self._git.exec('config', ['--local', '--bool', 'http.sslVerify', _.toString(wiki.config.git.auth.sslVerify)]) }
108 109
      ]

110
      if (wiki.config.git.auth.type === 'ssh') {
111
        gitConfigs.push(() => {
112
          return self._git.exec('config', ['--local', 'core.sshCommand', 'ssh -i "' + wiki.config.git.auth.privateKey + '" -o StrictHostKeyChecking=no'])
113 114 115
        })
      }

116 117
      return self._git.exec('remote', 'show').then((cProc) => {
        let out = cProc.stdout.toString()
NGPixel's avatar
NGPixel committed
118 119
        return Promise.each(gitConfigs, fn => { return fn() }).then(() => {
          if (!_.includes(out, 'origin')) {
120
            return self._git.exec('remote', ['add', 'origin', self._url])
NGPixel's avatar
NGPixel committed
121 122 123 124
          } else {
            return self._git.exec('remote', ['set-url', 'origin', self._url])
          }
        }).catch(err => {
125
          wiki.logger.error(err)
NGPixel's avatar
NGPixel committed
126
        })
127 128
      })
    }).catch((err) => {
129
      wiki.logger.error('Git remote error!')
130 131
      throw err
    }).then(() => {
132
      wiki.logger.info('Git Repository: OK')
133 134 135 136 137 138 139 140 141
      return true
    })
  },

  /**
   * Gets the repo path.
   *
   * @return     {String}  The repo path.
   */
142
  getRepoPath() {
143
    return this._repo.path || path.join(wiki.ROOTPATH, 'repo')
144 145 146 147 148 149 150
  },

  /**
   * Sync with the remote repository
   *
   * @return     {Promise}  Resolve on sync success
   */
151
  resync() {
152 153
    let self = this

154 155
    // Is git remote disabled?

156
    if (wiki.config.git === false) {
157 158 159
      return Promise.resolve(true)
    }

160 161
    // Fetch

162
    wiki.logger.info('Performing pull from remote Git repository...')
163
    return self._git.pull('origin', self._repo.branch).then((cProc) => {
164
      wiki.logger.info('Git Pull completed.')
165
    })
166
      .catch((err) => {
167
        wiki.logger.error('Unable to fetch from git origin!')
168 169 170 171
        throw err
      })
      .then(() => {
        // Check for changes
172

173 174
        return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
          let out = cProc.stdout.toString()
175

176
          if (_.includes(out, 'commit')) {
177
            wiki.logger.info('Performing push to remote Git repository...')
178
            return self._git.push('origin', self._repo.branch).then(() => {
179
              return wiki.logger.info('Git Push completed.')
180 181
            })
          } else {
182
            wiki.logger.info('Git Push skipped. Repository is already in sync.')
183
          }
184

185 186 187 188
          return true
        })
      })
      .catch((err) => {
189
        wiki.logger.error('Unable to push changes to remote Git repository!')
190
        throw err
191 192 193 194 195 196 197 198 199
      })
  },

  /**
   * Commits a document.
   *
   * @param      {String}   entryPath  The entry path
   * @return     {Promise}  Resolve on commit success
   */
200
  commitDocument(entryPath, author) {
201 202 203 204 205 206 207 208
    let self = this
    let gitFilePath = entryPath + '.md'
    let commitMsg = ''

    return self._git.exec('ls-files', gitFilePath).then((cProc) => {
      let out = cProc.stdout.toString()
      return _.includes(out, gitFilePath)
    }).then((isTracked) => {
209
      commitMsg = (isTracked) ? wiki.lang.t('git:updated', { path: gitFilePath }) : wiki.lang.t('git:added', { path: gitFilePath })
210 211
      return self._git.add(gitFilePath)
    }).then(() => {
212 213
      let commitUsr = securityHelper.sanitizeCommitUser(author)
      return self._git.exec('commit', ['-m', commitMsg, '--author="' + commitUsr.name + ' <' + commitUsr.email + '>"']).catch((err) => {
214 215 216 217 218 219 220 221 222 223 224 225
        if (_.includes(err.stdout, 'nothing to commit')) { return true }
      })
    })
  },

  /**
   * Move a document.
   *
   * @param      {String}            entryPath     The current entry path
   * @param      {String}            newEntryPath  The new entry path
   * @return     {Promise<Boolean>}  Resolve on success
   */
226
  moveDocument(entryPath, newEntryPath) {
227 228 229
    let self = this
    let gitFilePath = entryPath + '.md'
    let gitNewFilePath = newEntryPath + '.md'
230
    let destPathObj = path.parse(this.getRepoPath() + '/' + gitNewFilePath)
231

232 233 234 235 236 237 238 239 240
    return fs.ensureDir(destPathObj.dir).then(() => {
      return self._git.exec('mv', [gitFilePath, gitNewFilePath]).then((cProc) => {
        let out = cProc.stdout.toString()
        if (_.includes(out, 'fatal')) {
          let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
          throw new Error(errorMsg)
        }
        return true
      })
241 242 243 244 245 246 247 248 249
    })
  },

  /**
   * Commits uploads changes.
   *
   * @param      {String}   msg     The commit message
   * @return     {Promise}  Resolve on commit success
   */
250
  commitUploads(msg) {
251 252 253 254 255 256 257 258
    let self = this
    msg = msg || 'Uploads repository sync'

    return self._git.add('uploads').then(() => {
      return self._git.commit(msg).catch((err) => {
        if (_.includes(err.stdout, 'nothing to commit')) { return true }
      })
    })
259 260
  },

261
  getHistory(entryPath) {
262 263 264
    let self = this
    let gitFilePath = entryPath + '.md'

NGPixel's avatar
NGPixel committed
265
    return self._git.exec('log', ['--max-count=25', '--skip=1', '--format=format:%H %h %cI %cE %cN', '--', gitFilePath]).then((cProc) => {
266 267 268 269 270 271 272
      let out = cProc.stdout.toString()
      if (_.includes(out, 'fatal')) {
        let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
        throw new Error(errorMsg)
      }
      let hist = _.chain(out).split('\n').map(h => {
        let hParts = h.split(' ', 4)
273
        let hDate = moment(hParts[2])
274 275 276 277
        return {
          commit: hParts[0],
          commitAbbr: hParts[1],
          date: hParts[2],
278 279
          dateFull: hDate.format('LLLL'),
          dateCalendar: hDate.calendar(null, { sameElse: 'llll' }),
280 281 282 283 284 285
          email: hParts[3],
          name: hParts[4]
        }
      }).value()
      return hist
    })
NGPixel's avatar
NGPixel committed
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
  },

  getHistoryDiff(path, commit, comparewith) {
    let self = this
    if (!comparewith) {
      comparewith = 'HEAD'
    }

    return self._git.exec('diff', ['--no-color', `${commit}:${path}.md`, `${comparewith}:${path}.md`]).then((cProc) => {
      let out = cProc.stdout.toString()
      if (_.startsWith(out, 'fatal: ')) {
        throw new Error(out)
      } else if (!_.includes(out, 'diff')) {
        throw new Error('Unable to query diff data.')
      } else {
        return out
      }
    })
304 305 306
  }

}