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

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

10 11
const securityHelper = require('../helpers/security')

NGPixel's avatar
NGPixel committed
12 13 14 15 16
/**
 * Git Model
 */
module.exports = {

17 18 19 20 21 22 23 24
  _git: null,
  _url: '',
  _repo: {
    path: '',
    branch: 'master',
    exists: false
  },
  _signature: {
25
    email: 'wiki@example.com'
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
  },
  _opts: {
    clone: {},
    push: {}
  },
  onReady: null,

  /**
   * Initialize Git model
   *
   * @return     {Object}  Git model instance
   */
  init () {
    let self = this

    // -> Build repository path

    if (_.isEmpty(appconfig.paths.repo)) {
      self._repo.path = path.join(ROOTPATH, 'repo')
    } else {
      self._repo.path = appconfig.paths.repo
    }

    // -> Initialize repository

    self.onReady = self._initRepo(appconfig)

    // Define signature

55
    if (appconfig.git) {
56
      self._signature.email = appconfig.git.serverEmail || 'wiki@example.com'
57
    }
58 59 60 61 62 63 64 65 66 67 68 69 70

    return self
  },

  /**
   * Initialize Git repository
   *
   * @param      {Object}  appconfig  The application config
   * @return     {Object}  Promise
   */
  _initRepo (appconfig) {
    let self = this

NGPixel's avatar
NGPixel committed
71
    winston.info('Checking Git repository...')
72 73 74 75 76

    // -> Check if path is accessible

    return fs.mkdirAsync(self._repo.path).catch((err) => {
      if (err.code !== 'EEXIST') {
NGPixel's avatar
NGPixel committed
77
        winston.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 (appconfig.git === false) {
NGPixel's avatar
NGPixel committed
92
        winston.info('Remote Git syncing is disabled. Not recommended!')
93 94 95
        return Promise.resolve(true)
      }

96 97 98
      // Initialize remote

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

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

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

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 125 126
          } else {
            return self._git.exec('remote', ['set-url', 'origin', self._url])
          }
        }).catch(err => {
          winston.error(err)
        })
127 128
      })
    }).catch((err) => {
NGPixel's avatar
NGPixel committed
129
      winston.error('Git remote error!')
130 131
      throw err
    }).then(() => {
NGPixel's avatar
NGPixel committed
132
      winston.info('Git repository is OK.')
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
      return true
    })
  },

  /**
   * Gets the repo path.
   *
   * @return     {String}  The repo path.
   */
  getRepoPath () {
    return this._repo.path || path.join(ROOTPATH, 'repo')
  },

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

154 155 156 157 158 159
    // Is git remote disabled?

    if (appconfig.git === false) {
      return Promise.resolve(true)
    }

160 161
    // Fetch

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

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

        if (_.includes(out, 'commit')) {
NGPixel's avatar
NGPixel committed
177
          winston.info('Performing push to remote Git repository...')
178
          return self._git.push('origin', self._repo.branch).then(() => {
NGPixel's avatar
NGPixel committed
179
            return winston.info('Git Push completed.')
180 181
          })
        } else {
NGPixel's avatar
NGPixel committed
182
          winston.info('Git Push skipped. Repository is already in sync.')
183 184 185 186 187 188
        }

        return true
      })
    })
    .catch((err) => {
NGPixel's avatar
NGPixel committed
189
      winston.error('Unable to push changes to remote Git repository!')
190 191 192 193 194 195 196 197 198 199
      throw err
    })
  },

  /**
   * 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) => {
NGPixel's avatar
NGPixel committed
209
      commitMsg = (isTracked) ? lang.t('git:updated', { path: gitFilePath }) : 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 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
        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
   */
  moveDocument (entryPath, newEntryPath) {
    let self = this
    let gitFilePath = entryPath + '.md'
    let gitNewFilePath = newEntryPath + '.md'

    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
    })
  },

  /**
   * Commits uploads changes.
   *
   * @param      {String}   msg     The commit message
   * @return     {Promise}  Resolve on commit success
   */
  commitUploads (msg) {
    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 }
      })
    })
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
  },

  getHistory (entryPath) {
    let self = this
    let gitFilePath = entryPath + '.md'

    return self._git.exec('log', ['-n', '25', '--format=format:%H %h %cI %cE %cN', '--', gitFilePath]).then((cProc) => {
      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)
        return {
          commit: hParts[0],
          commitAbbr: hParts[1],
          date: hParts[2],
          email: hParts[3],
          name: hParts[4]
        }
      }).value()
      return hist
    })
280 281 282
  }

}