3e2bdec197ef1fe7d035868f91bced58acdc72c2
[idea/community.git] / plugins / settings-repository / src / git / GitEx.kt
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.settingsRepository.git
17
18 import com.intellij.openapi.progress.ProcessCanceledException
19 import com.intellij.openapi.util.text.StringUtil
20 import org.eclipse.jgit.api.CommitCommand
21 import org.eclipse.jgit.api.ResetCommand
22 import org.eclipse.jgit.dircache.DirCacheCheckout
23 import org.eclipse.jgit.errors.TransportException
24 import org.eclipse.jgit.internal.JGitText
25 import org.eclipse.jgit.lib.*
26 import org.eclipse.jgit.revwalk.RevCommit
27 import org.eclipse.jgit.revwalk.RevSort
28 import org.eclipse.jgit.revwalk.RevWalk
29 import org.eclipse.jgit.revwalk.filter.RevFilter
30 import org.eclipse.jgit.storage.file.FileRepositoryBuilder
31 import org.eclipse.jgit.transport.CredentialsProvider
32 import org.eclipse.jgit.transport.FetchResult
33 import org.eclipse.jgit.transport.RemoteConfig
34 import org.eclipse.jgit.transport.Transport
35 import org.eclipse.jgit.treewalk.FileTreeIterator
36 import org.eclipse.jgit.treewalk.TreeWalk
37 import org.eclipse.jgit.treewalk.filter.TreeFilter
38 import org.jetbrains.settingsRepository.AuthenticationException
39 import org.jetbrains.settingsRepository.IcsCredentialsStore
40 import org.jetbrains.settingsRepository.LOG
41 import java.io.InputStream
42 import java.nio.file.Path
43
44 fun wrapIfNeedAndReThrow(e: TransportException) {
45   if (e is org.eclipse.jgit.errors.NoRemoteRepositoryException || e.status == TransportException.Status.CANNOT_RESOLVE_REPO) {
46     throw org.jetbrains.settingsRepository.NoRemoteRepositoryException(e)
47   }
48
49   val message = e.message!!
50   if (e.status == TransportException.Status.NOT_AUTHORIZED || e.status == TransportException.Status.NOT_PERMITTED ||
51       message.contains(JGitText.get().notAuthorized) || message.contains("Auth cancel") || message.contains("Auth fail") || message.contains(": reject HostKey:") /* JSch */) {
52     throw AuthenticationException(e)
53   }
54   else if (e.status == TransportException.Status.CANCELLED || message == "Download cancelled") {
55     throw ProcessCanceledException()
56   }
57   else {
58     throw e
59   }
60 }
61
62 fun Repository.fetch(remoteConfig: RemoteConfig, credentialsProvider: CredentialsProvider? = null, progressMonitor: ProgressMonitor? = null): FetchResult? {
63   try {
64     val transport = Transport.open(this, remoteConfig)
65     try {
66       transport.credentialsProvider = credentialsProvider
67       return transport.fetch(progressMonitor ?: NullProgressMonitor.INSTANCE, null)
68     }
69     finally {
70       transport.close()
71     }
72   }
73   catch (e: TransportException) {
74     val message = e.message!!
75     if (message.startsWith("Remote does not have ")) {
76       LOG.info(message)
77       // "Remote does not have refs/heads/master available for fetch." - remote repository is not initialized
78       return null
79     }
80
81     wrapIfNeedAndReThrow(e)
82     return null
83   }
84 }
85
86 fun Repository.disableAutoCrLf(): Repository {
87   val config = config
88   config.setString(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_AUTOCRLF, ConfigConstants.CONFIG_KEY_FALSE)
89   config.save()
90   return this
91 }
92
93 fun createBareRepository(dir: Path): Repository {
94   val repository = FileRepositoryBuilder().setBare().setGitDir(dir.toFile()).build()
95   repository.create(true)
96   return repository
97 }
98
99 fun createGitRepository(dir: Path): Repository {
100   val repository = FileRepositoryBuilder().setWorkTree(dir.toFile()).build()
101   repository.create()
102   return repository
103 }
104
105 fun Repository.commit(message: String? = null, reflogComment: String? = null, author: PersonIdent? = null, committer: PersonIdent? = null): RevCommit {
106   val commitCommand = CommitCommand(this).setAuthor(author).setCommitter(committer)
107   if (message != null) {
108     @Suppress("UsePropertyAccessSyntax")
109     commitCommand.setMessage(message)
110   }
111   if (reflogComment != null) {
112     commitCommand.setReflogComment(reflogComment)
113   }
114   return commitCommand.call()
115 }
116
117 fun Repository.resetHard(): DirCacheCheckout {
118   val resetCommand = ResetCommand(this).setMode(ResetCommand.ResetType.HARD)
119   resetCommand.call()
120   return resetCommand.dirCacheCheckout!!
121 }
122
123 fun Config.getRemoteBranchFullName(): String {
124   val name = getString(ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER, ConfigConstants.CONFIG_KEY_MERGE)
125   if (StringUtil.isEmpty(name)) {
126     throw IllegalStateException("branch.master.merge refspec must be specified")
127   }
128   return name!!
129 }
130
131 fun Repository.setUpstream(url: String?, branchName: String = Constants.MASTER): StoredConfig {
132   // our local branch named 'master' in any case
133   val localBranchName = Constants.MASTER
134
135   val config = config
136   val remoteName = Constants.DEFAULT_REMOTE_NAME
137   if (StringUtil.isEmptyOrSpaces(url)) {
138     LOG.debug("Unset remote")
139     config.unsetSection(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName)
140     config.unsetSection(ConfigConstants.CONFIG_BRANCH_SECTION, localBranchName)
141   }
142   else {
143     LOG.debug("Set remote $url")
144     config.setString(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName, ConfigConstants.CONFIG_KEY_URL, url)
145     // http://git-scm.com/book/en/Git-Internals-The-Refspec
146     config.setString(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName, ConfigConstants.CONFIG_FETCH_SECTION, '+' + Constants.R_HEADS + branchName + ':' + Constants.R_REMOTES + remoteName + '/' + branchName)
147     // todo should we set it if fetch specified (kirill.likhodedov suggestion)
148     //config.setString(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName, "push", Constants.R_HEADS + localBranchName + ':' + Constants.R_HEADS + branchName);
149
150     config.setString(ConfigConstants.CONFIG_BRANCH_SECTION, localBranchName, ConfigConstants.CONFIG_KEY_REMOTE, remoteName)
151     config.setString(ConfigConstants.CONFIG_BRANCH_SECTION, localBranchName, ConfigConstants.CONFIG_KEY_MERGE, Constants.R_HEADS + branchName)
152   }
153   config.save()
154   return config
155 }
156
157 fun Repository.computeIndexDiff(): IndexDiff {
158   val workingTreeIterator = FileTreeIterator(this)
159   try {
160     return IndexDiff(this, Constants.HEAD, workingTreeIterator)
161   }
162   finally {
163     workingTreeIterator.reset()
164   }
165 }
166
167 fun cloneBare(uri: String, dir: Path, credentialsStore: Lazy<IcsCredentialsStore>? = null, progressMonitor: ProgressMonitor = NullProgressMonitor.INSTANCE): Repository {
168   val repository = createBareRepository(dir)
169   val config = repository.setUpstream(uri)
170   val remoteConfig = RemoteConfig(config, Constants.DEFAULT_REMOTE_NAME)
171
172   val result = repository.fetch(remoteConfig, if (credentialsStore == null) null else JGitCredentialsProvider(credentialsStore, repository), progressMonitor) ?: return repository
173   var head = findBranchToCheckout(result)
174   if (head == null) {
175     val branch = Constants.HEAD
176     head = result.getAdvertisedRef(branch) ?: result.getAdvertisedRef(Constants.R_HEADS + branch) ?: result.getAdvertisedRef(Constants.R_TAGS + branch)
177   }
178
179   if (head == null || head.objectId == null) {
180     return repository
181   }
182
183   if (head.name.startsWith(Constants.R_HEADS)) {
184     val newHead = repository.updateRef(Constants.HEAD)
185     newHead.disableRefLog()
186     newHead.link(head.name)
187     val branchName = Repository.shortenRefName(head.name)
188     config.setString(ConfigConstants.CONFIG_BRANCH_SECTION, branchName, ConfigConstants.CONFIG_KEY_REMOTE, Constants.DEFAULT_REMOTE_NAME)
189     config.setString(ConfigConstants.CONFIG_BRANCH_SECTION, branchName, ConfigConstants.CONFIG_KEY_MERGE, head.name)
190     val autoSetupRebase = config.getString(ConfigConstants.CONFIG_BRANCH_SECTION, null, ConfigConstants.CONFIG_KEY_AUTOSETUPREBASE)
191     if (ConfigConstants.CONFIG_KEY_ALWAYS == autoSetupRebase || ConfigConstants.CONFIG_KEY_REMOTE == autoSetupRebase) {
192       config.setBoolean(ConfigConstants.CONFIG_BRANCH_SECTION, branchName, ConfigConstants.CONFIG_KEY_REBASE, true)
193     }
194     config.save()
195   }
196
197   val commit = RevWalk(repository).use { it.parseCommit(head!!.objectId) }
198   val u = repository.updateRef(Constants.HEAD, !head.name.startsWith(Constants.R_HEADS))
199   u.setNewObjectId(commit.id)
200   u.forceUpdate()
201   return repository
202 }
203
204 private fun findBranchToCheckout(result: FetchResult): Ref? {
205   val idHead = result.getAdvertisedRef(Constants.HEAD) ?: return null
206
207   val master = result.getAdvertisedRef(Constants.R_HEADS + Constants.MASTER)
208   if (master != null && master.objectId == idHead.objectId) {
209     return master
210   }
211
212   for (r in result.advertisedRefs) {
213     if (!r.name.startsWith(Constants.R_HEADS)) {
214       continue
215     }
216     if (r.objectId == idHead.objectId) {
217       return r
218     }
219   }
220   return null
221 }
222
223 fun Repository.processChildren(path: String, filter: ((name: String) -> Boolean)? = null, processor: (name: String, inputStream: InputStream) -> Boolean) {
224   val lastCommitId = resolve(Constants.HEAD) ?: return
225   val reader = newObjectReader()
226   reader.use {
227     val treeWalk = TreeWalk.forPath(reader, path, RevWalk(reader).parseCommit(lastCommitId).tree) ?: return
228     if (!treeWalk.isSubtree) {
229       // not a directory
230       LOG.warn("File $path is not a directory")
231       return
232     }
233
234     treeWalk.filter = TreeFilter.ALL
235     treeWalk.enterSubtree()
236
237     while (treeWalk.next()) {
238       val fileMode = treeWalk.getFileMode(0)
239       if (fileMode == FileMode.REGULAR_FILE || fileMode == FileMode.SYMLINK || fileMode == FileMode.EXECUTABLE_FILE) {
240         val fileName = treeWalk.nameString
241         if (filter != null && !filter(fileName)) {
242           continue
243         }
244
245         val objectLoader = reader.open(treeWalk.getObjectId(0), Constants.OBJ_BLOB)
246         // we ignore empty files
247         if (objectLoader.size == 0L) {
248           LOG.warn("File $path skipped because empty (length 0)")
249           continue
250         }
251
252         if (!objectLoader.openStream().use { processor(fileName, it) }) {
253           break
254         }
255       }
256     }
257   }
258 }
259
260 fun Repository.read(path: String): InputStream? {
261   val lastCommitId = resolve(Constants.HEAD)
262   if (lastCommitId == null) {
263     LOG.warn("Repository ${directory.name} doesn't have HEAD")
264     return null
265   }
266
267   val reader = newObjectReader()
268   var releaseReader = true
269   try {
270     val treeWalk = TreeWalk.forPath(reader, path, RevWalk(reader).parseCommit(lastCommitId).tree) ?: return null
271     val objectLoader = reader.open(treeWalk.getObjectId(0), Constants.OBJ_BLOB)
272     val input = objectLoader.openStream()
273     if (objectLoader.isLarge) {
274       // we cannot release reader because input uses it internally (window cursor -> inflater)
275       releaseReader = false
276       return InputStreamWrapper(input, reader)
277     }
278     else {
279       return input
280     }
281   }
282   finally {
283     if (releaseReader) {
284       reader.close()
285     }
286   }
287 }
288
289 private class InputStreamWrapper(private val delegate: InputStream, private val reader: ObjectReader) : InputStream() {
290   override fun read() = delegate.read()
291
292   override fun read(b: ByteArray) = delegate.read(b)
293
294   override fun read(b: ByteArray, off: Int, len: Int) = delegate.read(b, off, len)
295
296   override fun hashCode() = delegate.hashCode()
297
298   override fun toString() = delegate.toString()
299
300   override fun reset() = delegate.reset()
301
302   override fun mark(limit: Int) = delegate.mark(limit)
303
304   override fun skip(n: Long): Long {
305     return super.skip(n)
306   }
307
308   override fun markSupported() = delegate.markSupported()
309
310   override fun equals(other: Any?) = delegate == other
311
312   override fun available() = delegate.available()
313
314   override fun close() {
315     try {
316       delegate.close()
317     }
318     finally {
319       reader.close()
320     }
321   }
322 }
323
324 fun Repository.getAheadCommitsCount(): Int {
325   val config = config
326   val shortBranchName = Repository.shortenRefName(config.getRemoteBranchFullName())
327   val trackingBranch = BranchConfig(config, shortBranchName).trackingBranch ?: return -1
328   val local = exactRef("${Constants.R_HEADS}$shortBranchName") ?: return -1
329   val walk = RevWalk(this)
330   val localCommit = walk.parseCommit(local.objectId)
331
332   val trackingCommit = findRef(trackingBranch)?.let { walk.parseCommit(it.objectId) }
333
334   walk.revFilter = RevFilter.MERGE_BASE
335   if (trackingCommit == null) {
336     walk.markStart(localCommit)
337     walk.sort(RevSort.REVERSE)
338   }
339   else {
340     walk.markStart(localCommit)
341     walk.markStart(trackingCommit)
342     val mergeBase = walk.next()
343     walk.reset()
344
345     walk.markStart(localCommit)
346     walk.markUninteresting(mergeBase)
347   }
348
349   walk.revFilter = RevFilter.ALL
350
351   var num = 0
352   for (c in walk) {
353     num++
354   }
355   return num
356 }
357
358 inline fun <T : AutoCloseable, R> T.use(block: (T) -> R): R {
359   var closed = false
360   try {
361     return block(this)
362   }
363   catch (e: Exception) {
364     closed = true
365     try {
366       close()
367     }
368     catch (closeException: Exception) {
369       @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
370       (e as java.lang.Throwable).addSuppressed(closeException)
371     }
372     throw e
373   }
374   finally {
375     if (!closed) {
376       close()
377     }
378   }
379 }