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