├── README.md ├── lib ├── com │ ├── fasterxml │ │ └── jackson │ │ │ ├── core │ │ │ └── util │ │ │ │ └── annotations.xml │ │ │ └── databind │ │ │ └── annotations.xml │ └── mcdermottroe │ │ └── apple │ │ └── annotations.xml ├── jackson-annotations-2.5.1-sources.jar ├── jackson-annotations-2.5.1.jar ├── jackson-core-2.5.1-sources.jar ├── jackson-core-2.5.1.jar ├── jackson-databind-2.5.1-sources.jar ├── jackson-databind-2.5.1.jar ├── java │ ├── lang │ │ └── annotations.xml │ └── util │ │ └── annotations.xml ├── org.eclipse.jgit-4.0.2-SNAPSHOT-sources.jar ├── org.eclipse.jgit-4.0.2-SNAPSHOT.jar └── org │ └── eclipse │ └── jgit │ ├── api │ └── annotations.xml │ ├── diff │ └── annotations.xml │ ├── dircache │ └── annotations.xml │ ├── errors │ └── annotations.xml │ ├── internal │ └── annotations.xml │ ├── lib │ └── annotations.xml │ ├── merge │ └── annotations.xml │ ├── revwalk │ └── annotations.xml │ ├── storage │ └── file │ │ └── annotations.xml │ ├── transport │ └── annotations.xml │ └── treewalk │ ├── annotations.xml │ └── filter │ └── annotations.xml ├── resources ├── META-INF │ └── plugin.xml └── messages │ └── IcsBundle.properties ├── settings-repository.iml ├── settings_repository.xml ├── src ├── BaseRepositoryManager.kt ├── CredentialsStore.kt ├── IcsBundle.kt ├── IcsManager.kt ├── IcsSettingsEditor.kt ├── IcsUrlBuilder.kt ├── ProjectId.kt ├── ReadOnlySourcesManager.kt ├── RepositoryManager.kt ├── RepositoryService.kt ├── actions │ ├── CommitToIcsAction.kt │ └── SyncAction.kt ├── autoSync.kt ├── copyAppSettingsToRepository.kt ├── git │ ├── GitBareRepositoryManager.kt │ ├── GitEx.kt │ ├── GitRepositoryManager.kt │ ├── JGitCredentialsProvider.kt │ ├── JGitMergeProvider.kt │ ├── JGitProgressMonitor.kt │ ├── commit.kt │ ├── dirCacheEditor.kt │ ├── gitCredential.kt │ ├── pull.kt │ └── reset.kt ├── keychain │ ├── CredentialsStore.kt │ ├── FileCredentialsStore.kt │ ├── OSXKeychainLibrary.kt │ └── OsXCredentialsStore.kt ├── org │ └── jetbrains │ │ └── settingsRepository │ │ ├── CommitToIcsDialog.java │ │ ├── IcsSettingsPanel.form │ │ ├── IcsSettingsPanel.java │ │ ├── RepositoryAuthenticationForm.form │ │ └── RepositoryAuthenticationForm.java ├── settings │ ├── IcsSettings.kt │ ├── readOnlySourcesEditor.kt │ └── upstreamEditor.kt └── util.kt ├── testData ├── local.xml ├── local2.xml └── remote.xml └── testSrc ├── BareGitTest.kt ├── CredentialsTest.kt ├── GitTest.kt ├── LoadTest.kt ├── RespositoryHelper.kt ├── SettingsRepositoryTestSuite.kt └── TestCase.kt /README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Available for all IntelliJ Platform based products (build number greater than 139.69/140.2). Settings | Plugins | Browse repositories -> type in Settings Repository. 4 | 5 | Don't try to install plugin from disk — otherwise you have to be aware of compatibility. 6 | 7 | # Configuration 8 | 9 | Use File -> Settings Repository… to configure. 10 | 11 | Specify URL of upstream Git repository. File URL is supported, you will be prompted to init repository if specified path is not exists or repository is not created. 12 | [GitHub](https://www.github.com) could be used to store settings. 13 | 14 | Synchronization is performed automatically: 15 | * after successful completion of "Update Project" or "Push" actions, 16 | * on application exit or project close. 17 | 18 | Also you can do sync using "VCS -> Sync Settings". The idea is do not disturb you. If you invoke such actions, so, you are ready to solve possible problems. 19 | 20 | ## Authentication 21 | On first sync you will be prompted to specify username/password. In case of GitHub strongly recommended to use a [personal access token](https://help.github.com/articles/creating-an-access-token-for-command-line-use) (leave password empty if you use token instead of username). Remember when generating the token to check `repo` scope permission. Bitbucket — [app password](https://bitbucket.org/account/admin/app-passwords). 22 | 23 | If you still want to use username/password instead of access token or your Git hosting provider doesn't support it, recommended to configure [git credentials helper](https://help.github.com/articles/caching-your-github-password-in-git). 24 | 25 | OS X Keychain is supported. It means that your credentials could be shared between all IntelliJ Platform based products (you will be prompted to grant access if origin application differs from requestor application (credentials were saved in IntelliJ IDEA, but requested from WebStorm). 26 | 27 | ## How to report issues 28 | Use [JetBrains YouTrack](https://youtrack.jetbrains.com/issues?q=%23%7BSettings+Repository%7D) — project IntelliJ IDEA, subsystem Settings Repository ([issue template](https://youtrack.jetbrains.com/newIssue?project=IDEA&clearDraft=true&c=Subsystem+Settings+Repository)). 29 | 30 | ## Sources 31 | **Plugin is part of IntelliJ IDEA Community Edition and bundled with IDEA 15, WebStorm 11 and PhpStorm 9.5. Please see https://github.com/JetBrains/intellij-community/tree/master/plugins/settings-repository This repository is archived.** 32 | -------------------------------------------------------------------------------- /lib/com/fasterxml/jackson/core/util/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/com/fasterxml/jackson/databind/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/com/mcdermottroe/apple/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/jackson-annotations-2.5.1-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develar/settings-repository/fc19dda8427bc2a266aa2d65aaff87527e949fbe/lib/jackson-annotations-2.5.1-sources.jar -------------------------------------------------------------------------------- /lib/jackson-annotations-2.5.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develar/settings-repository/fc19dda8427bc2a266aa2d65aaff87527e949fbe/lib/jackson-annotations-2.5.1.jar -------------------------------------------------------------------------------- /lib/jackson-core-2.5.1-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develar/settings-repository/fc19dda8427bc2a266aa2d65aaff87527e949fbe/lib/jackson-core-2.5.1-sources.jar -------------------------------------------------------------------------------- /lib/jackson-core-2.5.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develar/settings-repository/fc19dda8427bc2a266aa2d65aaff87527e949fbe/lib/jackson-core-2.5.1.jar -------------------------------------------------------------------------------- /lib/jackson-databind-2.5.1-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develar/settings-repository/fc19dda8427bc2a266aa2d65aaff87527e949fbe/lib/jackson-databind-2.5.1-sources.jar -------------------------------------------------------------------------------- /lib/jackson-databind-2.5.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develar/settings-repository/fc19dda8427bc2a266aa2d65aaff87527e949fbe/lib/jackson-databind-2.5.1.jar -------------------------------------------------------------------------------- /lib/java/lang/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/java/util/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lib/org.eclipse.jgit-4.0.2-SNAPSHOT-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develar/settings-repository/fc19dda8427bc2a266aa2d65aaff87527e949fbe/lib/org.eclipse.jgit-4.0.2-SNAPSHOT-sources.jar -------------------------------------------------------------------------------- /lib/org.eclipse.jgit-4.0.2-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develar/settings-repository/fc19dda8427bc2a266aa2d65aaff87527e949fbe/lib/org.eclipse.jgit-4.0.2-SNAPSHOT.jar -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/api/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | 55 | 56 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/diff/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/dircache/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/errors/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/internal/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/lib/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/merge/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/revwalk/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/storage/file/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/transport/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/treewalk/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /lib/org/eclipse/jgit/treewalk/filter/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | Settings Repository 3 | org.jetbrains.settingsRepository 4 | Supports sharing settings between installations of IntelliJ Platform based products used by the same developer (or team) on different computers.

6 |

Synchronization is performed automatically after successful completion of "Update Project" or "Push" actions. Also you can do sync using VCS -> Sync Settings.

7 |

See project page for more info.

]]>
8 | 999.999 9 | 10 | JetBrains 11 | 12 | 13 | com.intellij.modules.xml 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | messages.IcsBundle 41 |
-------------------------------------------------------------------------------- /resources/messages/IcsBundle.properties: -------------------------------------------------------------------------------- 1 | group.SyncSettings.text=Sync Settings 2 | action.MergeSettings.text=Merge 3 | action.ResetToTheirsSettings.text=Overwrite Local 4 | action.ResetToMySettings.text=Overwrite Remote 5 | 6 | sync.done.title=Settings Synced 7 | sync.done.message=Settings successfully synced 8 | sync.rejected.title=Failed to Sync Settings 9 | sync.not.authorized.title=Authorization Failed 10 | 11 | action.ConfigureIcs.text=Settings Repository\u2026 12 | 13 | action.CommitToIcs.text=Commit to Settings Repository 14 | 15 | init.dialog.message=Repository not exists, would you like to init it here? 16 | init.dialog.title=Init Repository 17 | 18 | specify.absolute.path.dialog.message=Please specify absolute path 19 | 20 | init.failed.title=Cannot Init Repository 21 | init.failed.message=Failed to init repository: {0} 22 | set.upstream.failed.title=Cannot Set Upstream Repository 23 | set.upstream.failed.message=Failed to set upstream repository: {0} 24 | 25 | task.commit.title=Committing Settings 26 | task.sync.title=Syncing Settings 27 | task.set.upstream.title=Setting upstream repository 28 | 29 | settings.panel.title=Settings Repository 30 | settings.update.on.start=Update repository from upstream on start 31 | 32 | sync.repositories.panel.title=Sync Repositories 33 | 34 | login.github.note=Strongly recommended to use an access token. 35 | login.other.git.provider.note=Consider to configure git credentials helper. 36 | settings.upstream.url=Upstream URL\: 37 | 38 | log.in.to=Log in to {0}. 39 | enter.your.password.for.ssh.key=Enter your password for the SSH key \"{0}\". 40 | -------------------------------------------------------------------------------- /settings-repository.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /settings_repository.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/out/artifacts 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/BaseRepositoryManager.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.application.invokeAndWaitIfNeed 5 | import com.intellij.openapi.fileTypes.StdFileTypes 6 | import com.intellij.openapi.util.io.FileUtil 7 | import com.intellij.openapi.vcs.merge.MergeDialogCustomizer 8 | import com.intellij.openapi.vcs.merge.MergeProvider2 9 | import com.intellij.openapi.vcs.merge.MergeSession 10 | import com.intellij.openapi.vcs.merge.MultipleFileMergeDialog 11 | import com.intellij.openapi.vfs.CharsetToolkit 12 | import com.intellij.openapi.vfs.VirtualFile 13 | import com.intellij.testFramework.LightVirtualFile 14 | import com.intellij.util.PathUtilRt 15 | import org.jetbrains.annotations.TestOnly 16 | import java.io.* 17 | import java.util.Arrays 18 | 19 | public abstract class BaseRepositoryManager(protected val dir: File) : RepositoryManager { 20 | protected val lock: Any = Object(); 21 | 22 | override fun listSubFileNames(path: String): Collection { 23 | val files = File(dir, path).list() 24 | if (files == null || files.size() == 0) { 25 | return listOf() 26 | } 27 | return listOf(*files) 28 | } 29 | 30 | override fun processChildren(path: String, filter: (name: String) -> Boolean, processor: (name: String, inputStream: InputStream) -> Boolean) { 31 | var files: Array? = null 32 | synchronized (lock) { 33 | files = File(dir, path).listFiles(object: FilenameFilter { 34 | override fun accept(dir: File, name: String) = filter(name) 35 | }) 36 | } 37 | 38 | if (files == null || files!!.isEmpty()) { 39 | return 40 | } 41 | 42 | for (file in files!!) { 43 | if (file.isDirectory() || file.isHidden()) { 44 | continue; 45 | } 46 | 47 | // we ignore empty files as well - delete if corrupted 48 | if (file.length() == 0L) { 49 | if (file.exists()) { 50 | try { 51 | LOG.warn("File $path is empty (length 0), will be removed") 52 | delete(file, path) 53 | } 54 | catch (e: Exception) { 55 | LOG.error(e) 56 | } 57 | } 58 | continue; 59 | } 60 | 61 | if (!processor(file.name, file.inputStream())) { 62 | break; 63 | } 64 | } 65 | } 66 | 67 | override fun deleteRepository() { 68 | FileUtil.delete(dir) 69 | } 70 | 71 | override fun read(path: String): InputStream? { 72 | synchronized (lock) { 73 | val file = File(dir, path) 74 | // we ignore empty files as well - delete if corrupted 75 | if (file.length() == 0L) { 76 | if (file.exists()) { 77 | try { 78 | LOG.warn("File $path is empty (length 0), will be removed") 79 | delete(file, path) 80 | } 81 | catch (e: Exception) { 82 | LOG.error(e) 83 | } 84 | } 85 | return null 86 | } 87 | return FileInputStream(file) 88 | } 89 | } 90 | 91 | override fun write(path: String, content: ByteArray, size: Int) { 92 | if (LOG.isDebugEnabled()) { 93 | LOG.debug("Write $path") 94 | } 95 | 96 | try { 97 | synchronized (lock) { 98 | val file = File(dir, path) 99 | FileUtil.writeToFile(file, content, 0, size) 100 | 101 | addToIndex(file, path, content, size) 102 | } 103 | } 104 | catch (e: Exception) { 105 | LOG.error(e) 106 | } 107 | } 108 | 109 | /** 110 | * path relative to repository root 111 | */ 112 | protected abstract fun addToIndex(file: File, path: String, content: ByteArray, size: Int) 113 | 114 | override fun delete(path: String) { 115 | if (LOG.isDebugEnabled()) { 116 | LOG.debug("Remove $path") 117 | } 118 | 119 | synchronized (lock) { 120 | val file = File(dir, path) 121 | // delete could be called for non-existent file 122 | if (file.exists()) { 123 | delete(file, path) 124 | } 125 | } 126 | } 127 | 128 | private fun delete(file: File, path: String) { 129 | val isFile = file.isFile() 130 | file.removeWithParentsIfEmpty(dir, isFile) 131 | deleteFromIndex(path, isFile) 132 | } 133 | 134 | protected abstract fun deleteFromIndex(path: String, isFile: Boolean) 135 | 136 | override fun has(path: String): Boolean { 137 | synchronized (lock) { 138 | return File(dir, path).exists() 139 | } 140 | } 141 | } 142 | 143 | fun File.removeWithParentsIfEmpty(root: File, isFile: Boolean = true) { 144 | FileUtil.delete(this) 145 | 146 | if (isFile) { 147 | // remove empty directories 148 | var parent = this.getParentFile() 149 | while (parent != null && parent != root && parent.delete()) { 150 | parent = parent.getParentFile() 151 | } 152 | } 153 | } 154 | // kotlin bug, cannot be val (.NoSuchMethodError: org.jetbrains.settingsRepository.SettingsRepositoryPackage.getMARKER_ACCEPT_MY()[B) 155 | TestOnly object AM { 156 | val MARKER_ACCEPT_MY: ByteArray = "__accept my__".toByteArray() 157 | val MARKER_ACCEPT_THEIRS: ByteArray = "__accept theirs__".toByteArray() 158 | } 159 | 160 | fun resolveConflicts(files: List, mergeProvider: MergeProvider2): List { 161 | if (ApplicationManager.getApplication()!!.isUnitTestMode()) { 162 | val mergeSession = mergeProvider.createMergeSession(files) 163 | for (file in files) { 164 | val mergeData = mergeProvider.loadRevisions(file) 165 | if (Arrays.equals(mergeData.CURRENT, AM.MARKER_ACCEPT_MY) || Arrays.equals(mergeData.LAST, AM.MARKER_ACCEPT_THEIRS)) { 166 | mergeSession.conflictResolvedForFile(file, MergeSession.Resolution.AcceptedYours) 167 | } 168 | else if (Arrays.equals(mergeData.CURRENT, AM.MARKER_ACCEPT_THEIRS) || Arrays.equals(mergeData.LAST, AM.MARKER_ACCEPT_MY)) { 169 | mergeSession.conflictResolvedForFile(file, MergeSession.Resolution.AcceptedTheirs) 170 | } 171 | else if (Arrays.equals(mergeData.LAST, AM.MARKER_ACCEPT_MY)) { 172 | file.setBinaryContent(mergeData.LAST!!) 173 | mergeProvider.conflictResolvedForFile(file) 174 | } 175 | else { 176 | throw UnsupportedOperationException() 177 | } 178 | } 179 | 180 | return files 181 | } 182 | 183 | var processedFiles: List? = null 184 | invokeAndWaitIfNeed { 185 | val fileMergeDialog = MultipleFileMergeDialog(null, files, mergeProvider, MergeDialogCustomizer()) 186 | fileMergeDialog.show() 187 | processedFiles = fileMergeDialog.getProcessedFiles() 188 | } 189 | return processedFiles!! 190 | } 191 | 192 | class RepositoryVirtualFile(private val path: String) : LightVirtualFile(PathUtilRt.getFileName(path), StdFileTypes.XML, "", CharsetToolkit.UTF8_CHARSET, 1L) { 193 | var content: ByteArray? = null 194 | private set 195 | 196 | override fun getPath() = path 197 | 198 | override fun setBinaryContent(content: ByteArray, newModificationStamp: Long, newTimeStamp: Long, requestor: Any?) { 199 | $content = content 200 | } 201 | 202 | override fun getOutputStream(requestor: Any?, newModificationStamp: Long, newTimeStamp: Long): OutputStream { 203 | throw IllegalStateException("You must use setBinaryContent") 204 | } 205 | 206 | override fun setContent(requestor: Any?, content: CharSequence?, fireEvent: Boolean) { 207 | throw IllegalStateException("You must use setBinaryContent") 208 | } 209 | } -------------------------------------------------------------------------------- /src/CredentialsStore.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.util.Computable 5 | import com.intellij.openapi.util.text.StringUtil 6 | import com.intellij.util.PathUtilRt 7 | import com.intellij.util.ui.UIUtil 8 | import org.jetbrains.keychain.Credentials 9 | 10 | public fun showAuthenticationForm(credentials: Credentials?, uri: String, host: String?, path: String?, sshKeyFile: String?): Credentials? { 11 | if (ApplicationManager.getApplication()?.isUnitTestMode() === true) { 12 | throw AssertionError("showAuthenticationForm called from tests") 13 | } 14 | 15 | return UIUtil.invokeAndWaitIfNeeded(object : Computable { 16 | override fun compute(): Credentials? { 17 | val note = if (sshKeyFile == null) IcsBundle.message(if (host == "github.com") "login.github.note" else "login.other.git.provider.note") else null 18 | var username = credentials?.id 19 | if (username == null && host == "github.com" && path != null && sshKeyFile == null) { 20 | val firstSlashIndex = path.indexOf('/', 1) 21 | username = path.substring(1, if (firstSlashIndex == -1) path.length() else firstSlashIndex) 22 | } 23 | 24 | val authenticationForm = RepositoryAuthenticationForm(if (sshKeyFile == null) { 25 | IcsBundle.message("log.in.to", StringUtil.trimMiddle(uri, 50)) 26 | } 27 | else { 28 | IcsBundle.message("enter.your.password.for.ssh.key", PathUtilRt.getFileName(sshKeyFile)) 29 | }, username, credentials?.token, note, sshKeyFile != null) 30 | if (authenticationForm.showAndGet()) { 31 | username = sshKeyFile ?: authenticationForm.getUsername() 32 | val passwordChars = authenticationForm.getPassword() 33 | return Credentials(username, if (passwordChars == null) (if (username == null) null else "x-oauth-basic") else String(passwordChars)) 34 | } 35 | else { 36 | return null 37 | } 38 | } 39 | }) 40 | } -------------------------------------------------------------------------------- /src/IcsBundle.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.CommonBundle 4 | import org.jetbrains.annotations.PropertyKey 5 | import java.lang.ref.Reference 6 | import java.lang.ref.SoftReference 7 | import java.util.ResourceBundle 8 | import kotlin.platform.platformStatic 9 | 10 | class IcsBundle { 11 | companion object { 12 | private var ourBundle: Reference? = null 13 | 14 | val BUNDLE: String = "messages.IcsBundle" 15 | 16 | platformStatic 17 | fun message(PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any?): String { 18 | return CommonBundle.message(getBundle(), key, *params) 19 | } 20 | 21 | private fun getBundle(): ResourceBundle { 22 | var bundle: ResourceBundle? = null 23 | if (ourBundle != null) { 24 | bundle = ourBundle!!.get() 25 | } 26 | if (bundle == null) { 27 | bundle = ResourceBundle.getBundle(BUNDLE) 28 | ourBundle = SoftReference(bundle) 29 | } 30 | return bundle!! 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/IcsSettingsEditor.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.ui.DialogWrapper 5 | import com.intellij.openapi.util.SystemInfo 6 | import com.intellij.ui.IdeBorderFactory 7 | import com.intellij.ui.JBTabsPaneImpl 8 | import com.intellij.ui.tabs.TabInfo 9 | import com.intellij.ui.tabs.TabsListener 10 | import com.intellij.ui.tabs.impl.JBTabsImpl 11 | import com.intellij.util.ArrayUtil 12 | import com.intellij.util.ui.UIUtil 13 | import java.awt.BorderLayout 14 | import java.awt.Insets 15 | import javax.swing.* 16 | import javax.swing.border.Border 17 | import javax.swing.border.EmptyBorder 18 | 19 | class IcsSettingsEditor(private val project: Project?) : DialogWrapper(project, true) { 20 | val upstreamEditor = IcsSettingsPanel(project, getContentPane()!!, { doOKAction() }) 21 | 22 | init { 23 | setTitle(IcsBundle.message("settings.panel.title")) 24 | init() 25 | } 26 | 27 | private var currentConfigurable: Configurable? = null 28 | 29 | override fun createCenterPanel(): JComponent? { 30 | upstreamEditor.panel.setBorder(DialogWrapper.ourDefaultBorder) 31 | 32 | val tabbedPane = JBTabsPaneImpl(null, SwingConstants.TOP, myDisposable) 33 | val tabs = tabbedPane.getTabs() as JBTabsImpl 34 | tabs.setSizeBySelected(true) 35 | 36 | var actions = arrayOf(getOKAction(), getCancelAction()) 37 | if (SystemInfo.isMac) { 38 | actions = ArrayUtil.reverseArray(actions) 39 | } 40 | 41 | val readOnlySourcesEditor = createReadOnlySourcesEditor(getContentPane(), project) 42 | 43 | tabs.addTab(TabInfo(wrap(upstreamEditor.panel, upstreamEditor.createActions())).setText("Upstream")) 44 | tabs.addTab(TabInfo(wrap(readOnlySourcesEditor.getComponent(), actions)).setText("Read-only Sources").setObject(readOnlySourcesEditor)) 45 | 46 | tabs.addListener(object : TabsListener.Adapter() { 47 | override fun selectionChanged(oldSelection: TabInfo?, newSelection: TabInfo?) { 48 | pack() 49 | currentConfigurable = newSelection?.getObject() as Configurable? 50 | } 51 | }) 52 | return tabbedPane.getComponent() 53 | } 54 | 55 | override fun getPreferredFocusedComponent(): JComponent? { 56 | return upstreamEditor.urlTextField 57 | } 58 | 59 | override fun doOKAction() { 60 | currentConfigurable?.apply() 61 | saveSettings(icsManager.settings) 62 | 63 | super.doOKAction() 64 | } 65 | 66 | private fun wrap(component: JComponent, actions: Array): JComponent { 67 | val panel = JPanel(BorderLayout()) 68 | panel.add(component, BorderLayout.CENTER) 69 | 70 | val buttonsPanel = Box.createHorizontalBox() 71 | buttonsPanel.setBorder(IdeBorderFactory.createEmptyBorder(Insets(8, 0, 0, 0))) 72 | buttonsPanel.add(Box.createHorizontalGlue()) 73 | for (action in actions) { 74 | buttonsPanel.add(createJButtonForAction(action)) 75 | } 76 | panel.add(buttonsPanel, BorderLayout.SOUTH) 77 | return panel 78 | } 79 | 80 | override fun createSouthPanel() = null 81 | 82 | override protected fun createContentPaneBorder(): Border { 83 | val insets = UIUtil.PANEL_REGULAR_INSETS 84 | return EmptyBorder(insets.top, 0, insets.bottom, 0) 85 | } 86 | } 87 | 88 | interface Configurable { 89 | fun isModified(): Boolean 90 | 91 | fun apply() 92 | 93 | fun reset() 94 | 95 | fun getComponent(): JComponent 96 | } -------------------------------------------------------------------------------- /src/IcsUrlBuilder.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.openapi.components.RoamingType 4 | import com.intellij.openapi.util.SystemInfo 5 | 6 | val PROJECTS_DIR_NAME: String = "_projects/" 7 | 8 | private fun getOsFolderName() = when { 9 | SystemInfo.isWindows -> "_windows" 10 | SystemInfo.isMac -> "_mac" 11 | SystemInfo.isLinux -> "_linux" 12 | SystemInfo.isFreeBSD -> "_freebsd" 13 | SystemInfo.isUnix -> "_unix" 14 | else -> "_unknown" 15 | } 16 | 17 | fun buildPath(path: String, roamingType: RoamingType, projectKey: String? = null) = when { 18 | projectKey != null -> "$PROJECTS_DIR_NAME$projectKey/$path" 19 | roamingType == RoamingType.PER_PLATFORM -> "${getOsFolderName()}/$path" 20 | else -> path 21 | } -------------------------------------------------------------------------------- /src/ProjectId.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.openapi.components.PersistentStateComponent 4 | import com.intellij.openapi.components.State 5 | import com.intellij.openapi.components.Storage 6 | import com.intellij.openapi.components.StoragePathMacros 7 | import com.intellij.util.xmlb.XmlSerializerUtil 8 | 9 | State(name = "IcsProjectId", storages = arrayOf(Storage(file = StoragePathMacros.WORKSPACE_FILE))) 10 | class ProjectId : PersistentStateComponent { 11 | var uid: String? = null 12 | var path: String? = null 13 | 14 | override fun getState(): ProjectId? { 15 | return this 16 | } 17 | 18 | override fun loadState(state: ProjectId?) { 19 | XmlSerializerUtil.copyBean(state!!, this) 20 | } 21 | } -------------------------------------------------------------------------------- /src/ReadOnlySourcesManager.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.util.SmartList 4 | import org.eclipse.jgit.lib.Repository 5 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder 6 | import java.io.File 7 | 8 | class ReadOnlySourcesManager(private val settings: IcsSettings) { 9 | private var _repositories: List? = null 10 | 11 | val repositories: List 12 | get() { 13 | var r = _repositories 14 | if (r == null) { 15 | if (settings.readOnlySources.isEmpty()) { 16 | r = emptyList() 17 | } 18 | else { 19 | r = SmartList() 20 | for (source in settings.readOnlySources) { 21 | try { 22 | val path = source.path ?: continue 23 | val dir = File(getPluginSystemDir(), path) 24 | if (dir.exists()) { 25 | r.add(FileRepositoryBuilder().setBare().setGitDir(dir).build()) 26 | } 27 | else { 28 | LOG.warn("Skip read-only source ${source.url} because dir doesn't exists") 29 | } 30 | } 31 | catch (e: Exception) { 32 | LOG.error(e) 33 | } 34 | } 35 | } 36 | _repositories = r 37 | } 38 | return r 39 | } 40 | 41 | fun setSources(sources: List) { 42 | settings.readOnlySources = sources 43 | _repositories = null 44 | } 45 | } -------------------------------------------------------------------------------- /src/RepositoryManager.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.openapi.progress.ProgressIndicator 4 | import gnu.trove.THashSet 5 | import java.io.InputStream 6 | import java.util.Collections 7 | 8 | public interface RepositoryManager { 9 | public fun createRepositoryIfNeed(): Boolean 10 | 11 | /** 12 | * Think twice before use 13 | */ 14 | public fun deleteRepository() 15 | 16 | public fun isRepositoryExists(): Boolean 17 | 18 | public fun getUpstream(): String? 19 | 20 | public fun hasUpstream(): Boolean 21 | 22 | /** 23 | * Return error message if failed 24 | */ 25 | public fun setUpstream(url: String?, branch: String?) 26 | 27 | public fun read(path: String): InputStream? 28 | 29 | public fun write(path: String, content: ByteArray, size: Int) 30 | 31 | public fun delete(path: String) 32 | 33 | public fun listSubFileNames(path: String): Collection 34 | 35 | public fun processChildren(path: String, filter: (name: String) -> Boolean, processor: (name: String, inputStream: InputStream) -> Boolean) 36 | 37 | /** 38 | * Not all implementations support progress indicator (will not be updated on progress) 39 | */ 40 | public fun commit(indicator: ProgressIndicator? = null): Boolean 41 | 42 | public fun getAheadCommitsCount(): Int 43 | 44 | public fun commit(paths: List) 45 | 46 | public fun push(indicator: ProgressIndicator? = null) 47 | 48 | public fun fetch(indicator: ProgressIndicator? = null): Updater 49 | 50 | public fun pull(indicator: ProgressIndicator): UpdateResult? 51 | 52 | public fun has(path: String): Boolean 53 | 54 | public fun resetToTheirs(indicator: ProgressIndicator): UpdateResult? 55 | 56 | public fun resetToMy(indicator: ProgressIndicator, localRepositoryInitializer: (() -> Unit)?): UpdateResult? 57 | 58 | public fun canCommit(): Boolean 59 | 60 | public interface Updater { 61 | fun merge(): UpdateResult? 62 | 63 | // valid only if merge was called before 64 | val definitelySkipPush: Boolean 65 | } 66 | } 67 | 68 | fun RepositoryManager.commitIfCan(indicator: ProgressIndicator? = null) { 69 | if (canCommit()) { 70 | commit(indicator) 71 | } 72 | } 73 | 74 | public interface UpdateResult { 75 | val changed: Collection 76 | val deleted: Collection 77 | } 78 | 79 | val EMPTY_UPDATE_RESULT = ImmutableUpdateResult(Collections.emptySet(), Collections.emptySet()) 80 | 81 | public data class ImmutableUpdateResult(override val changed: Collection, override val deleted: Collection) : UpdateResult { 82 | public fun toMutable(): MutableUpdateResult = MutableUpdateResult(changed, deleted) 83 | } 84 | 85 | public data class MutableUpdateResult(changed: Collection, deleted: Collection) : UpdateResult { 86 | override val changed = THashSet(changed) 87 | override val deleted = THashSet(deleted) 88 | 89 | fun add(result: UpdateResult?): MutableUpdateResult { 90 | if (result != null) { 91 | add(result.changed, result.deleted) 92 | } 93 | return this 94 | } 95 | 96 | fun add(newChanged: Collection, newDeleted: Collection): MutableUpdateResult { 97 | changed.removeAll(newDeleted) 98 | deleted.removeAll(newChanged) 99 | 100 | changed.addAll(newChanged) 101 | deleted.addAll(newDeleted) 102 | return this 103 | } 104 | 105 | fun addChanged(newChanged: Collection): MutableUpdateResult { 106 | deleted.removeAll(newChanged) 107 | changed.addAll(newChanged) 108 | return this 109 | } 110 | } 111 | 112 | public fun UpdateResult?.isEmpty(): Boolean = this == null || (changed.isEmpty() && deleted.isEmpty()) 113 | 114 | public fun UpdateResult?.concat(result: UpdateResult?): UpdateResult? { 115 | if (result.isEmpty()) { 116 | return this 117 | } 118 | else if (isEmpty()) { 119 | return result 120 | } 121 | else { 122 | this!! 123 | return MutableUpdateResult(changed, deleted).add(result!!) 124 | } 125 | } 126 | 127 | public class AuthenticationException(cause: Throwable) : RuntimeException(cause.getMessage(), cause) -------------------------------------------------------------------------------- /src/RepositoryService.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.openapi.ui.Messages 4 | import com.intellij.util.io.URLUtil 5 | import org.eclipse.jgit.lib.Constants 6 | import org.eclipse.jgit.transport.URIish 7 | import org.jetbrains.settingsRepository.git.createBareRepository 8 | import java.awt.Container 9 | import java.io.File 10 | import java.io.IOException 11 | 12 | public interface RepositoryService { 13 | public fun checkUrl(uriString: String, messageParent: Container? = null): Boolean { 14 | val uri = URIish(uriString) 15 | val isFile: Boolean 16 | if (uri.getScheme() == URLUtil.FILE_PROTOCOL) { 17 | isFile = true 18 | } 19 | else { 20 | isFile = uri.getScheme() == null && !uriString.startsWith("git@") 21 | } 22 | 23 | if (messageParent != null && isFile && !checkFileRepo(uriString, messageParent)) { 24 | return false 25 | } 26 | return true 27 | } 28 | 29 | public fun checkFileRepo(url: String, messageParent: Container): Boolean { 30 | val suffix = '/' + Constants.DOT_GIT 31 | val file = File(if (url.endsWith(suffix)) url.substring(0, url.length() - suffix.length()) else url) 32 | if (file.exists()) { 33 | if (!file.isDirectory()) { 34 | //noinspection DialogTitleCapitalization 35 | Messages.showErrorDialog(messageParent, "Specified path is not a directory", "Specified Path is Invalid") 36 | return false 37 | } 38 | else if (isValidRepository(file)) { 39 | return true 40 | } 41 | } 42 | else if (!file.isAbsolute()) { 43 | Messages.showErrorDialog(messageParent, IcsBundle.message("specify.absolute.path.dialog.message"), "") 44 | return false 45 | } 46 | 47 | if (Messages.showYesNoDialog(messageParent, IcsBundle.message("init.dialog.message"), IcsBundle.message("init.dialog.title"), Messages.getQuestionIcon()) == Messages.YES) { 48 | try { 49 | createBareRepository(file) 50 | return true 51 | } 52 | catch (e: IOException) { 53 | Messages.showErrorDialog(messageParent, IcsBundle.message("init.failed.message", e.getMessage()), IcsBundle.message("init.failed.title")) 54 | return false 55 | } 56 | } 57 | else { 58 | return false 59 | } 60 | } 61 | 62 | // must be protected, kotlin bug 63 | public fun isValidRepository(file: File): Boolean 64 | } -------------------------------------------------------------------------------- /src/actions/CommitToIcsAction.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.actions 2 | 3 | import com.intellij.openapi.components.ServiceManager 4 | import com.intellij.openapi.components.StorageScheme 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.project.ex.ProjectEx 7 | import com.intellij.openapi.ui.DialogWrapper 8 | import com.intellij.openapi.ui.MessageDialogBuilder 9 | import com.intellij.openapi.ui.Messages 10 | import com.intellij.openapi.vcs.CheckinProjectPanel 11 | import com.intellij.openapi.vcs.FilePath 12 | import com.intellij.openapi.vcs.FileStatus 13 | import com.intellij.openapi.vcs.actions.CommonCheckinFilesAction 14 | import com.intellij.openapi.vcs.actions.VcsContext 15 | import com.intellij.openapi.vcs.changes.Change 16 | import com.intellij.openapi.vcs.changes.ChangeListManager 17 | import com.intellij.openapi.vcs.changes.CommitContext 18 | import com.intellij.openapi.vcs.changes.CommitExecutor 19 | import com.intellij.openapi.vcs.checkin.BeforeCheckinDialogHandler 20 | import com.intellij.openapi.vcs.checkin.CheckinHandler 21 | import com.intellij.openapi.vcs.checkin.CheckinHandlerFactory 22 | import com.intellij.openapi.vfs.VfsUtilCore 23 | import com.intellij.openapi.vfs.VirtualFile 24 | import com.intellij.util.SmartList 25 | import org.jetbrains.settingsRepository.CommitToIcsDialog 26 | import org.jetbrains.settingsRepository.IcsBundle 27 | import org.jetbrains.settingsRepository.ProjectId 28 | import org.jetbrains.settingsRepository.icsManager 29 | import java.util.UUID 30 | 31 | class CommitToIcsAction : CommonCheckinFilesAction() { 32 | class IcsBeforeCommitDialogHandler : CheckinHandlerFactory() { 33 | override fun createHandler(panel: CheckinProjectPanel, commitContext: CommitContext): CheckinHandler { 34 | return CheckinHandler.DUMMY 35 | } 36 | 37 | override fun createSystemReadyHandler(project: Project): BeforeCheckinDialogHandler? { 38 | return BEFORE_CHECKIN_DIALOG_HANDLER 39 | } 40 | 41 | companion object { 42 | private val BEFORE_CHECKIN_DIALOG_HANDLER = object : BeforeCheckinDialogHandler() { 43 | override fun beforeCommitDialogShown(project: Project, changes: List, executors: Iterable, showVcsCommit: Boolean): Boolean { 44 | val collectConsumer = ProjectChangeCollectConsumer(project) 45 | collectProjectChanges(changes, collectConsumer) 46 | showDialog(project, collectConsumer, null) 47 | return true 48 | } 49 | } 50 | } 51 | } 52 | 53 | override fun getActionName(dataContext: VcsContext) = IcsBundle.message("action.CommitToIcs.text") 54 | 55 | override fun isApplicableRoot(file: VirtualFile, status: FileStatus, dataContext: VcsContext): Boolean { 56 | val project = dataContext.getProject() 57 | return project is ProjectEx && project.getStateStore().getStorageScheme() == StorageScheme.DIRECTORY_BASED && super.isApplicableRoot(file, status, dataContext) && !file.isDirectory() && isProjectConfigFile(file, dataContext.getProject()!!) 58 | } 59 | 60 | override fun prepareRootsForCommit(roots: Array, project: Project) = roots as Array 61 | 62 | override fun performCheckIn(context: VcsContext, project: Project, roots: Array) { 63 | val projectId = getProjectId(project) 64 | if (projectId == null) { 65 | return 66 | } 67 | 68 | val changes = context.getSelectedChanges() 69 | val collectConsumer = ProjectChangeCollectConsumer(project) 70 | if (changes != null && changes.isNotEmpty()) { 71 | for (change in changes) { 72 | collectConsumer.consume(change) 73 | } 74 | } 75 | else { 76 | val manager = ChangeListManager.getInstance(project) 77 | for (path in getRoots(context)) { 78 | collectProjectChanges(manager.getChangesIn(path), collectConsumer) 79 | } 80 | } 81 | 82 | showDialog(project, collectConsumer, projectId) 83 | } 84 | } 85 | 86 | private class ProjectChangeCollectConsumer(private val project: Project) { 87 | private var projectChanges: MutableList? = null 88 | 89 | fun consume(change: Change) { 90 | if (isProjectConfigFile(change.getVirtualFile(), project)) { 91 | if (projectChanges == null) { 92 | projectChanges = SmartList() 93 | } 94 | projectChanges!!.add(change) 95 | } 96 | } 97 | 98 | fun getResult() = if (projectChanges == null) listOf() else projectChanges!! 99 | 100 | fun hasResult() = projectChanges != null 101 | } 102 | 103 | private fun getProjectId(project: Project): String? { 104 | val projectId = ServiceManager.getService(project, javaClass())!! 105 | if (projectId.uid == null) { 106 | if (MessageDialogBuilder.yesNo("Settings Server Project Mapping", "Project is not mapped on Settings Server. Would you like to map?").project(project).doNotAsk(object : DialogWrapper.PropertyDoNotAskOption("") { 107 | override fun setToBeShown(value: Boolean, exitCode: Int) { 108 | icsManager.settings.doNoAskMapProject = !value 109 | } 110 | 111 | override fun isToBeShown(): Boolean { 112 | return !icsManager.settings.doNoAskMapProject 113 | } 114 | 115 | override fun canBeHidden(): Boolean { 116 | return true 117 | } 118 | }).show() == Messages.YES) { 119 | projectId.uid = UUID.randomUUID().toString() 120 | } 121 | } 122 | 123 | return projectId.uid 124 | } 125 | 126 | private fun showDialog(project: Project, collectConsumer: ProjectChangeCollectConsumer, projectId: String?) { 127 | if (!collectConsumer.hasResult()) { 128 | return 129 | } 130 | 131 | var effectiveProjectId = projectId 132 | if (effectiveProjectId == null) { 133 | effectiveProjectId = getProjectId(project) 134 | if (effectiveProjectId == null) { 135 | return 136 | } 137 | } 138 | 139 | CommitToIcsDialog(project, effectiveProjectId, collectConsumer.getResult()).show() 140 | } 141 | 142 | private fun collectProjectChanges(changes: Collection, collectConsumer: ProjectChangeCollectConsumer) { 143 | for (change in changes) { 144 | collectConsumer.consume(change) 145 | } 146 | } 147 | 148 | private fun isProjectConfigFile(file: VirtualFile?, project: Project): Boolean { 149 | if (file == null) { 150 | return false 151 | } 152 | 153 | val projectFile = project.getProjectFile() 154 | val projectConfigDir = projectFile?.getParent() 155 | return projectConfigDir != null && VfsUtilCore.isAncestor(projectConfigDir, file, true) 156 | } 157 | -------------------------------------------------------------------------------- /src/actions/SyncAction.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.actions 2 | 3 | import com.intellij.notification.NotificationGroup 4 | import com.intellij.notification.NotificationType 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.project.DumbAwareAction 7 | import com.intellij.openapi.project.Project 8 | import org.jetbrains.settingsRepository.* 9 | 10 | val NOTIFICATION_GROUP = NotificationGroup.balloonGroup(PLUGIN_NAME) 11 | 12 | abstract class SyncAction(private val syncType: SyncType) : DumbAwareAction() { 13 | override fun update(e: AnActionEvent) { 14 | e.getPresentation().setEnabledAndVisible(icsManager.repositoryManager.hasUpstream()) 15 | } 16 | 17 | override fun actionPerformed(event: AnActionEvent) { 18 | syncAndNotify(syncType, event.getProject()) 19 | } 20 | } 21 | 22 | fun syncAndNotify(syncType: SyncType, project: Project?, notifyIfUpToDate: Boolean = true) { 23 | try { 24 | if (icsManager.sync(syncType, project) == null && !notifyIfUpToDate) { 25 | return 26 | } 27 | } 28 | catch (e: Exception) { 29 | LOG.warn(e) 30 | NOTIFICATION_GROUP.createNotification(IcsBundle.message("sync.rejected.title"), e.getMessage() ?: "Internal error", NotificationType.ERROR, null).notify(project) 31 | } 32 | NOTIFICATION_GROUP.createNotification(IcsBundle.message("sync.done.message"), NotificationType.INFORMATION).notify(project) 33 | } 34 | 35 | // we don't 36 | class MergeAction : SyncAction(SyncType.MERGE) 37 | class ResetToTheirsAction : SyncAction(SyncType.OVERWRITE_LOCAL) 38 | class ResetToMyAction : SyncAction(SyncType.OVERWRITE_REMOTE) 39 | 40 | class ConfigureIcsAction : DumbAwareAction() { 41 | override fun actionPerformed(e: AnActionEvent) { 42 | icsManager.runInAutoCommitDisabledMode { 43 | IcsSettingsEditor(e.getProject()).show() 44 | } 45 | } 46 | 47 | override fun update(e: AnActionEvent) { 48 | e.getPresentation().setIcon(null) 49 | } 50 | } -------------------------------------------------------------------------------- /src/autoSync.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.notification.Notification 4 | import com.intellij.notification.Notifications 5 | import com.intellij.notification.NotificationsAdapter 6 | import com.intellij.openapi.application.Application 7 | import com.intellij.openapi.application.ApplicationAdapter 8 | import com.intellij.openapi.application.ModalityState 9 | import com.intellij.openapi.application.ex.ApplicationManagerEx 10 | import com.intellij.openapi.application.impl.ApplicationImpl 11 | import com.intellij.openapi.progress.ProcessCanceledException 12 | import com.intellij.openapi.progress.ProgressIndicator 13 | import com.intellij.openapi.project.Project 14 | import com.intellij.openapi.util.ShutDownTracker 15 | import com.intellij.openapi.vcs.VcsBundle 16 | import com.intellij.openapi.vcs.VcsNotifier 17 | import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier 18 | import java.util.concurrent.Future 19 | 20 | class AutoSync(private val icsManager: IcsManager) { 21 | private volatile var autoSyncFuture: Future<*>? = null 22 | 23 | fun waitAutoSync(indicator: ProgressIndicator) { 24 | val autoFuture = autoSyncFuture 25 | if (autoFuture != null) { 26 | if (autoFuture.isDone()) { 27 | autoSyncFuture = null 28 | } 29 | else if (autoSyncFuture != null) { 30 | LOG.info("Wait for auto sync future") 31 | indicator.setText("Wait for auto sync completion") 32 | while (!autoFuture.isDone()) { 33 | if (indicator.isCanceled()) { 34 | return 35 | } 36 | Thread.sleep(5) 37 | } 38 | } 39 | } 40 | } 41 | 42 | fun registerListeners(application: Application) { 43 | application.addApplicationListener(object : ApplicationAdapter() { 44 | override fun applicationExiting() { 45 | autoSync(true) 46 | } 47 | }) 48 | } 49 | 50 | fun registerListeners(project: Project) { 51 | project.getMessageBus().connect().subscribe(Notifications.TOPIC, object : NotificationsAdapter() { 52 | override fun notify(notification: Notification) { 53 | if (!icsManager.repositoryActive || project.isDisposed()) { 54 | return 55 | } 56 | 57 | if (when { 58 | notification.getGroupId() == VcsBalloonProblemNotifier.NOTIFICATION_GROUP.getDisplayId() -> { 59 | val message = notification.getContent() 60 | message.startsWith("VCS Update Finished") || 61 | message == VcsBundle.message("message.text.file.is.up.to.date") || 62 | message == VcsBundle.message("message.text.all.files.are.up.to.date") 63 | } 64 | 65 | notification.getGroupId() == VcsNotifier.NOTIFICATION_GROUP_ID.getDisplayId() && notification.getTitle() == "Push successful" -> true 66 | 67 | else -> false 68 | }) { 69 | autoSync() 70 | } 71 | } 72 | }) 73 | } 74 | 75 | fun autoSync(onAppExit: Boolean = false) { 76 | if (!icsManager.repositoryActive) { 77 | return 78 | } 79 | 80 | var future = autoSyncFuture 81 | if (future != null && !future.isDone()) { 82 | return 83 | } 84 | 85 | val app = ApplicationManagerEx.getApplicationEx() as ApplicationImpl 86 | 87 | if (onAppExit) { 88 | sync(app, onAppExit) 89 | return 90 | } 91 | else if (app.isDisposeInProgress()) { 92 | // will be handled by applicationExiting listener 93 | return 94 | } 95 | 96 | future = app.executeOnPooledThread { 97 | if (autoSyncFuture == future) { 98 | // to ensure that repository will not be in uncompleted state and changes will be pushed 99 | ShutDownTracker.getInstance().registerStopperThread(Thread.currentThread()) 100 | try { 101 | sync(app, onAppExit) 102 | } 103 | finally { 104 | autoSyncFuture = null 105 | ShutDownTracker.getInstance().unregisterStopperThread(Thread.currentThread()) 106 | } 107 | } 108 | } 109 | autoSyncFuture = future 110 | } 111 | 112 | private fun sync(app: ApplicationImpl, onAppExit: Boolean) { 113 | catchAndLog { 114 | icsManager.runInAutoCommitDisabledMode { 115 | val repositoryManager = icsManager.repositoryManager 116 | if (!repositoryManager.canCommit()) { 117 | LOG.warn("Auto sync skipped: repository is not committable") 118 | return@runInAutoCommitDisabledMode 119 | } 120 | 121 | // on app exit fetch and push only if there are commits to push 122 | if (onAppExit && !repositoryManager.commit() && repositoryManager.getAheadCommitsCount() == 0) { 123 | return@runInAutoCommitDisabledMode 124 | } 125 | 126 | val updater = repositoryManager.fetch() 127 | // we merge in EDT non-modal to ensure that new settings will be properly applied 128 | app.invokeAndWait({ 129 | catchAndLog { 130 | val updateResult = updater.merge() 131 | if (!onAppExit && !app.isDisposeInProgress() && updateResult != null && updateStoragesFromStreamProvider(app.getStateStore(), updateResult)) { 132 | // force to avoid saveAll & confirmation 133 | app.exit(true, true, true, true) 134 | } 135 | } 136 | }, ModalityState.NON_MODAL) 137 | 138 | if (!updater.definitelySkipPush) { 139 | repositoryManager.push() 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | inline fun catchAndLog(runnable: () -> Unit) { 147 | try { 148 | runnable() 149 | } 150 | catch (e: ProcessCanceledException) { 151 | } 152 | catch (e: Throwable) { 153 | if (e is AuthenticationException || e is NoRemoteRepositoryException) { 154 | LOG.warn(e) 155 | } 156 | else { 157 | LOG.error(e) 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /src/copyAppSettingsToRepository.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.ide.actions.ExportSettingsAction 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.application.impl.ApplicationImpl 6 | import com.intellij.openapi.components.ExportableComponent 7 | import com.intellij.openapi.components.PersistentStateComponent 8 | import com.intellij.openapi.components.RoamingType 9 | import com.intellij.openapi.components.State 10 | import com.intellij.openapi.util.io.FileUtil 11 | import java.io.File 12 | 13 | fun copyLocalConfig() { 14 | val stateStorageManager = (ApplicationManager.getApplication()!! as ApplicationImpl).getStateStore().getStateStorageManager() 15 | val streamProvider = stateStorageManager.getStreamProvider()!! as IcsManager.IcsStreamProvider 16 | 17 | val fileToComponents = ExportSettingsAction.getExportableComponentsMap(true, false) 18 | for (file in fileToComponents.keySet()) { 19 | val absolutePath = file.getAbsolutePath() 20 | var fileSpec = stateStorageManager.collapseMacros(absolutePath) 21 | if (fileSpec.equals(absolutePath)) { 22 | // we have not experienced such problem yet, but we are just aware 23 | val canonicalPath = file.getCanonicalPath() 24 | if (!canonicalPath.equals(absolutePath)) { 25 | fileSpec = stateStorageManager.collapseMacros(canonicalPath) 26 | } 27 | } 28 | 29 | val roamingType = getRoamingType(fileToComponents.get(file)) 30 | if (file.isFile()) { 31 | val fileBytes = FileUtil.loadFileBytes(file) 32 | streamProvider.doSave(fileSpec, fileBytes, fileBytes.size(), roamingType) 33 | } 34 | else { 35 | saveDirectory(file, fileSpec, roamingType, streamProvider) 36 | } 37 | } 38 | } 39 | 40 | private fun saveDirectory(parent: File, parentFileSpec: String, roamingType: RoamingType, streamProvider: IcsManager.IcsStreamProvider) { 41 | val files = parent.listFiles() 42 | if (files != null) { 43 | for (file in files) { 44 | val childFileSpec = parentFileSpec + '/' + file.getName() 45 | if (file.isFile()) { 46 | val fileBytes = FileUtil.loadFileBytes(file) 47 | streamProvider.doSave(childFileSpec, fileBytes, fileBytes.size(), roamingType) 48 | } 49 | else { 50 | saveDirectory(file, childFileSpec, roamingType, streamProvider) 51 | } 52 | } 53 | } 54 | } 55 | 56 | private fun getRoamingType(components: Collection): RoamingType { 57 | for (component in components) { 58 | if (component is ExportSettingsAction.ExportableComponentItem) { 59 | return component.getRoamingType() 60 | } 61 | else if (component is PersistentStateComponent<*>) { 62 | val stateAnnotation = component.javaClass.getAnnotation(javaClass()) 63 | if (stateAnnotation != null) { 64 | val storages = stateAnnotation.storages 65 | if (!storages.isEmpty()) { 66 | return storages[0].roamingType 67 | } 68 | } 69 | } 70 | } 71 | return RoamingType.PER_USER 72 | } -------------------------------------------------------------------------------- /src/git/GitBareRepositoryManager.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.git 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.util.ShutDownTracker 5 | import org.eclipse.jgit.lib.Repository 6 | 7 | class GitBareRepositoryManager(private val repository: Repository) { 8 | init { 9 | if (ApplicationManager.getApplication()?.isUnitTestMode() != true) { 10 | ShutDownTracker.getInstance().registerShutdownTask(object: Runnable { 11 | override fun run() { 12 | repository.close() 13 | } 14 | }) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/git/GitEx.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.git 2 | 3 | import com.intellij.openapi.progress.ProcessCanceledException 4 | import com.intellij.openapi.util.NotNullLazyValue 5 | import com.intellij.openapi.util.text.StringUtil 6 | import org.eclipse.jgit.api.CommitCommand 7 | import org.eclipse.jgit.api.ResetCommand 8 | import org.eclipse.jgit.dircache.DirCacheCheckout 9 | import org.eclipse.jgit.dircache.DirCacheEntry 10 | import org.eclipse.jgit.errors.TransportException 11 | import org.eclipse.jgit.internal.JGitText 12 | import org.eclipse.jgit.lib.* 13 | import org.eclipse.jgit.revwalk.RevCommit 14 | import org.eclipse.jgit.revwalk.RevWalk 15 | import org.eclipse.jgit.revwalk.RevWalkUtils 16 | import org.eclipse.jgit.revwalk.filter.RevFilter 17 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder 18 | import org.eclipse.jgit.transport.CredentialsProvider 19 | import org.eclipse.jgit.transport.FetchResult 20 | import org.eclipse.jgit.transport.RemoteConfig 21 | import org.eclipse.jgit.transport.Transport 22 | import org.eclipse.jgit.treewalk.FileTreeIterator 23 | import org.eclipse.jgit.treewalk.TreeWalk 24 | import org.eclipse.jgit.treewalk.filter.TreeFilter 25 | import org.jetbrains.keychain.CredentialsStore 26 | import org.jetbrains.settingsRepository.AuthenticationException 27 | import org.jetbrains.settingsRepository.LOG 28 | import java.io.File 29 | import java.io.InputStream 30 | 31 | fun wrapIfNeedAndReThrow(e: TransportException) { 32 | if (e.getStatus() == TransportException.Status.CANNOT_RESOLVE_REPO) { 33 | throw org.jetbrains.settingsRepository.NoRemoteRepositoryException(e) 34 | } 35 | 36 | val message = e.getMessage()!! 37 | if (e.getStatus() == TransportException.Status.NOT_AUTHORIZED || e.getStatus() == TransportException.Status.NOT_PERMITTED || 38 | message.contains(JGitText.get().notAuthorized) || message.contains("Auth cancel") || message.contains("Auth fail") || message.contains(": reject HostKey:") /* JSch */) { 39 | throw AuthenticationException(e) 40 | } 41 | else if (e.getStatus() == TransportException.Status.CANCELLED || message == "Download cancelled") { 42 | throw ProcessCanceledException() 43 | } 44 | else { 45 | throw e 46 | } 47 | } 48 | 49 | fun Repository.fetch(remoteConfig: RemoteConfig, credentialsProvider: CredentialsProvider? = null, progressMonitor: ProgressMonitor? = null): FetchResult? { 50 | val transport = Transport.open(this, remoteConfig) 51 | try { 52 | transport.setCredentialsProvider(credentialsProvider) 53 | return transport.fetch(progressMonitor ?: NullProgressMonitor.INSTANCE, null) 54 | } 55 | catch (e: TransportException) { 56 | val message = e.getMessage()!! 57 | if (message.startsWith("Remote does not have ")) { 58 | LOG.info(message) 59 | // "Remote does not have refs/heads/master available for fetch." - remote repository is not initialized 60 | return null 61 | } 62 | 63 | wrapIfNeedAndReThrow(e) 64 | return null 65 | } 66 | finally { 67 | transport.close() 68 | } 69 | } 70 | 71 | fun Repository.disableAutoCrLf(): Repository { 72 | val config = getConfig() 73 | config.setString(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_AUTOCRLF, ConfigConstants.CONFIG_KEY_FALSE) 74 | config.save() 75 | return this 76 | } 77 | 78 | fun createBareRepository(dir: File): Repository { 79 | val repository = FileRepositoryBuilder().setBare().setGitDir(dir).build() 80 | repository.create(true) 81 | return repository 82 | } 83 | 84 | fun createRepository(dir: File): Repository { 85 | val repository = FileRepositoryBuilder().setWorkTree(dir).build() 86 | repository.create() 87 | return repository 88 | } 89 | 90 | fun Repository.commit(message: String? = null, reflogComment: String? = null, author: PersonIdent? = null, committer: PersonIdent? = null): RevCommit { 91 | val commitCommand = CommitCommand(this).setAuthor(author).setCommitter(committer) 92 | if (message != null) { 93 | commitCommand.setMessage(message) 94 | } 95 | if (reflogComment != null) { 96 | commitCommand.setReflogComment(reflogComment) 97 | } 98 | return commitCommand.call() 99 | } 100 | 101 | fun Repository.resetHard(): DirCacheCheckout { 102 | val resetCommand = ResetCommand(this).setMode(ResetCommand.ResetType.HARD) 103 | resetCommand.call() 104 | return resetCommand.getDirCacheCheckout()!! 105 | } 106 | 107 | fun Config.getRemoteBranchFullName(): String { 108 | val name = getString(ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER, ConfigConstants.CONFIG_KEY_MERGE) 109 | if (StringUtil.isEmpty(name)) { 110 | throw IllegalStateException("branch.master.merge refspec must be specified") 111 | } 112 | return name!! 113 | } 114 | 115 | public fun Repository.setUpstream(url: String?, branchName: String = Constants.MASTER): StoredConfig { 116 | // our local branch named 'master' in any case 117 | val localBranchName = Constants.MASTER 118 | 119 | val config = getConfig() 120 | val remoteName = Constants.DEFAULT_REMOTE_NAME 121 | if (StringUtil.isEmptyOrSpaces(url)) { 122 | LOG.debug("Unset remote") 123 | config.unsetSection(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName) 124 | config.unsetSection(ConfigConstants.CONFIG_BRANCH_SECTION, localBranchName) 125 | } 126 | else { 127 | LOG.debug("Set remote $url") 128 | config.setString(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName, ConfigConstants.CONFIG_KEY_URL, url) 129 | // http://git-scm.com/book/en/Git-Internals-The-Refspec 130 | config.setString(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName, ConfigConstants.CONFIG_FETCH_SECTION, '+' + Constants.R_HEADS + branchName + ':' + Constants.R_REMOTES + remoteName + '/' + branchName) 131 | // todo should we set it if fetch specified (kirill.likhodedov suggestion) 132 | //config.setString(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName, "push", Constants.R_HEADS + localBranchName + ':' + Constants.R_HEADS + branchName); 133 | 134 | config.setString(ConfigConstants.CONFIG_BRANCH_SECTION, localBranchName, ConfigConstants.CONFIG_KEY_REMOTE, remoteName) 135 | config.setString(ConfigConstants.CONFIG_BRANCH_SECTION, localBranchName, ConfigConstants.CONFIG_KEY_MERGE, Constants.R_HEADS + branchName) 136 | } 137 | config.save() 138 | return config 139 | } 140 | 141 | public fun Repository.computeIndexDiff(): IndexDiff { 142 | val workingTreeIterator = FileTreeIterator(this) 143 | try { 144 | return IndexDiff(this, Constants.HEAD, workingTreeIterator) 145 | } 146 | finally { 147 | workingTreeIterator.reset() 148 | } 149 | } 150 | 151 | public fun cloneBare(uri: String, dir: File, credentialsStore: NotNullLazyValue? = null, progressMonitor: ProgressMonitor = NullProgressMonitor.INSTANCE): Repository { 152 | val repository = createBareRepository(dir) 153 | val config = repository.setUpstream(uri) 154 | val remoteConfig = RemoteConfig(config, Constants.DEFAULT_REMOTE_NAME) 155 | 156 | val result = repository.fetch(remoteConfig, if (credentialsStore == null) null else JGitCredentialsProvider(credentialsStore, repository), progressMonitor) ?: return repository 157 | var head = findBranchToCheckout(result) 158 | if (head == null) { 159 | val branch = Constants.HEAD 160 | head = result.getAdvertisedRef(branch) ?: result.getAdvertisedRef(Constants.R_HEADS + branch) ?: result.getAdvertisedRef(Constants.R_TAGS + branch) 161 | } 162 | 163 | if (head == null || head.getObjectId() == null) { 164 | return repository 165 | } 166 | 167 | if (head.getName().startsWith(Constants.R_HEADS)) { 168 | val newHead = repository.updateRef(Constants.HEAD) 169 | newHead.disableRefLog() 170 | newHead.link(head.getName()) 171 | val branchName = Repository.shortenRefName(head.getName()) 172 | config.setString(ConfigConstants.CONFIG_BRANCH_SECTION, branchName, ConfigConstants.CONFIG_KEY_REMOTE, Constants.DEFAULT_REMOTE_NAME) 173 | config.setString(ConfigConstants.CONFIG_BRANCH_SECTION, branchName, ConfigConstants.CONFIG_KEY_MERGE, head.getName()) 174 | val autoSetupRebase = config.getString(ConfigConstants.CONFIG_BRANCH_SECTION, null, ConfigConstants.CONFIG_KEY_AUTOSETUPREBASE) 175 | if (ConfigConstants.CONFIG_KEY_ALWAYS == autoSetupRebase || ConfigConstants.CONFIG_KEY_REMOTE == autoSetupRebase) { 176 | config.setBoolean(ConfigConstants.CONFIG_BRANCH_SECTION, branchName, ConfigConstants.CONFIG_KEY_REBASE, true) 177 | } 178 | config.save() 179 | } 180 | 181 | val rewWalk = RevWalk(repository) 182 | val commit: RevCommit 183 | try { 184 | commit = rewWalk.parseCommit(head.getObjectId()) 185 | } 186 | finally { 187 | rewWalk.close() 188 | } 189 | 190 | val u = repository.updateRef(Constants.HEAD, !head.getName().startsWith(Constants.R_HEADS)) 191 | u.setNewObjectId(commit.getId()) 192 | u.forceUpdate() 193 | return repository 194 | } 195 | 196 | private fun findBranchToCheckout(result: FetchResult): Ref? { 197 | val idHead = result.getAdvertisedRef(Constants.HEAD) ?: return null 198 | 199 | val master = result.getAdvertisedRef(Constants.R_HEADS + Constants.MASTER) 200 | if (master != null && master.getObjectId().equals(idHead.getObjectId())) { 201 | return master 202 | } 203 | 204 | for (r in result.getAdvertisedRefs()) { 205 | if (!r.getName().startsWith(Constants.R_HEADS)) { 206 | continue 207 | } 208 | if (r.getObjectId().equals(idHead.getObjectId())) { 209 | return r 210 | } 211 | } 212 | return null 213 | } 214 | 215 | public fun Repository.processChildren(path: String, filter: ((name: String) -> Boolean)? = null, processor: (name: String, inputStream: InputStream) -> Boolean) { 216 | val lastCommitId = resolve(Constants.HEAD) ?: return 217 | val reader = newObjectReader() 218 | try { 219 | val treeWalk = TreeWalk.forPath(reader, path, RevWalk(reader).parseCommit(lastCommitId).getTree()) ?: return 220 | if (!treeWalk.isSubtree()) { 221 | // not a directory 222 | LOG.warn("File $path is not a directory") 223 | return 224 | } 225 | 226 | treeWalk.setFilter(TreeFilter.ALL) 227 | treeWalk.enterSubtree() 228 | 229 | while (treeWalk.next()) { 230 | val fileMode = treeWalk.getFileMode(0) 231 | if (fileMode == FileMode.REGULAR_FILE || fileMode == FileMode.SYMLINK || fileMode == FileMode.EXECUTABLE_FILE) { 232 | val fileName = treeWalk.getNameString() 233 | if (filter != null && !filter(fileName)) { 234 | continue 235 | } 236 | 237 | val objectLoader = reader.open(treeWalk.getObjectId(0), Constants.OBJ_BLOB) 238 | // we ignore empty files 239 | if (objectLoader.getSize() == 0L) { 240 | LOG.warn("File $path skipped because empty (length 0)") 241 | continue 242 | } 243 | 244 | if (!processor(fileName, objectLoader.openStream())) { 245 | break; 246 | } 247 | } 248 | } 249 | } 250 | finally { 251 | reader.close() 252 | } 253 | } 254 | 255 | public fun Repository.read(path: String): InputStream? { 256 | val lastCommitId = resolve(Constants.HEAD) 257 | if (lastCommitId == null) { 258 | LOG.warn("Repository ${getDirectory().getName()} doesn't have HEAD") 259 | return null 260 | } 261 | 262 | val reader = newObjectReader() 263 | var releaseReader = true 264 | try { 265 | val treeWalk = TreeWalk.forPath(reader, path, RevWalk(reader).parseCommit(lastCommitId).getTree()) ?: return null 266 | val objectLoader = reader.open(treeWalk.getObjectId(0), Constants.OBJ_BLOB) 267 | val input = objectLoader.openStream() 268 | if (objectLoader.isLarge()) { 269 | // we cannot release reader because input uses it internally (window cursor -> inflater) 270 | releaseReader = false 271 | return InputStreamWrapper(input, reader) 272 | } 273 | else { 274 | return input 275 | } 276 | } 277 | finally { 278 | if (releaseReader) { 279 | reader.close() 280 | } 281 | } 282 | } 283 | 284 | private class InputStreamWrapper(private val delegate: InputStream, private val reader: ObjectReader) : InputStream() { 285 | override fun read() = delegate.read() 286 | 287 | override fun read(b: ByteArray) = delegate.read(b) 288 | 289 | override fun read(b: ByteArray, off: Int, len: Int) = delegate.read(b, off, len) 290 | 291 | override fun hashCode() = delegate.hashCode() 292 | 293 | override fun toString() = delegate.toString() 294 | 295 | override fun reset() = delegate.reset() 296 | 297 | override fun mark(limit: Int) = delegate.mark(limit) 298 | 299 | override fun skip(n: Long): Long { 300 | return super.skip(n) 301 | } 302 | 303 | override fun markSupported() = delegate.markSupported() 304 | 305 | override fun equals(other: Any?) = delegate.equals(other) 306 | 307 | override fun available() = delegate.available() 308 | 309 | override fun close() { 310 | try { 311 | delegate.close() 312 | } 313 | finally { 314 | reader.close(); 315 | } 316 | } 317 | } 318 | 319 | fun ObjectReader.getCachedBytes(dirCacheEntry: DirCacheEntry) = open(dirCacheEntry.getObjectId(), Constants.OBJ_BLOB).getCachedBytes() 320 | 321 | public fun Repository.getAheadCommitsCount(): Int { 322 | val config = getConfig() 323 | val shortBranchName = Repository.shortenRefName(config.getRemoteBranchFullName()) 324 | val trackingBranch = BranchConfig(config, shortBranchName).getTrackingBranch() ?: return -1 325 | val tracking = getRef(trackingBranch) ?: return -1 326 | val local = getRef("${Constants.R_HEADS}$shortBranchName") ?: return -1 327 | val walk = RevWalk(this) 328 | val localCommit = walk.parseCommit(local.getObjectId()) 329 | val trackingCommit = walk.parseCommit(tracking.getObjectId()) 330 | 331 | walk.setRevFilter(RevFilter.MERGE_BASE) 332 | walk.markStart(localCommit) 333 | walk.markStart(trackingCommit) 334 | val mergeBase = walk.next() 335 | 336 | walk.reset() 337 | walk.setRevFilter(RevFilter.ALL) 338 | return RevWalkUtils.count(walk, localCommit, mergeBase) 339 | } -------------------------------------------------------------------------------- /src/git/GitRepositoryManager.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.git 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.progress.EmptyProgressIndicator 5 | import com.intellij.openapi.progress.ProgressIndicator 6 | import com.intellij.openapi.util.NotNullLazyValue 7 | import com.intellij.openapi.util.ShutDownTracker 8 | import com.intellij.openapi.util.text.StringUtil 9 | import com.intellij.util.SmartList 10 | import org.eclipse.jgit.errors.TransportException 11 | import org.eclipse.jgit.lib.ConfigConstants 12 | import org.eclipse.jgit.lib.Constants 13 | import org.eclipse.jgit.lib.Repository 14 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder 15 | import org.eclipse.jgit.transport.* 16 | import org.jetbrains.jgit.dirCache.AddLoadedFile 17 | import org.jetbrains.jgit.dirCache.deletePath 18 | import org.jetbrains.jgit.dirCache.edit 19 | import org.jetbrains.keychain.CredentialsStore 20 | import org.jetbrains.settingsRepository.* 21 | import org.jetbrains.settingsRepository.RepositoryManager.Updater 22 | import java.io.File 23 | import java.io.IOException 24 | import kotlin.properties.Delegates 25 | 26 | class GitRepositoryManager(private val credentialsStore: NotNullLazyValue, dir: File = File(getPluginSystemDir(), "repository")) : BaseRepositoryManager(dir) { 27 | val repository: Repository 28 | get() { 29 | var r = _repository 30 | if (r == null) { 31 | r = FileRepositoryBuilder().setWorkTree(dir).build() 32 | _repository = r 33 | } 34 | return r!! 35 | } 36 | 37 | // we must recreate repository if dir changed because repository stores old state and cannot be reinitialized (so, old instance cannot be reused and we must instantiate new one) 38 | var _repository: Repository? = null 39 | 40 | val credentialsProvider: CredentialsProvider by Delegates.lazy { 41 | JGitCredentialsProvider(credentialsStore, repository) 42 | } 43 | 44 | init { 45 | if (ApplicationManager.getApplication()?.isUnitTestMode() != true) { 46 | ShutDownTracker.getInstance().registerShutdownTask(object: Runnable { 47 | override fun run() { 48 | _repository?.close() 49 | } 50 | }) 51 | } 52 | } 53 | 54 | override fun createRepositoryIfNeed(): Boolean { 55 | if (isRepositoryExists()) { 56 | return false 57 | } 58 | 59 | repository.create() 60 | repository.disableAutoCrLf() 61 | return true 62 | } 63 | 64 | override fun deleteRepository() { 65 | super.deleteRepository() 66 | 67 | val r = _repository 68 | if (r != null) { 69 | _repository = null 70 | r.close() 71 | } 72 | } 73 | 74 | override fun getUpstream(): String? { 75 | return StringUtil.nullize(repository.getConfig().getString(ConfigConstants.CONFIG_REMOTE_SECTION, Constants.DEFAULT_REMOTE_NAME, ConfigConstants.CONFIG_KEY_URL)) 76 | } 77 | 78 | override fun setUpstream(url: String?, branch: String?) { 79 | repository.setUpstream(url, branch ?: Constants.MASTER) 80 | } 81 | 82 | override fun isRepositoryExists() = repository.getObjectDatabase().exists() 83 | 84 | override fun hasUpstream() = getUpstream() != null 85 | 86 | override fun addToIndex(file: File, path: String, content: ByteArray, size: Int) { 87 | repository.edit(AddLoadedFile(path, content, size, file.lastModified())) 88 | } 89 | 90 | override fun deleteFromIndex(path: String, isFile: Boolean) { 91 | repository.deletePath(path, isFile, false) 92 | } 93 | 94 | override fun commit(indicator: ProgressIndicator?): Boolean { 95 | synchronized (lock) { 96 | return commit(this, indicator) 97 | } 98 | } 99 | 100 | override fun getAheadCommitsCount() = repository.getAheadCommitsCount() 101 | 102 | override fun commit(paths: List) { 103 | } 104 | 105 | override fun push(indicator: ProgressIndicator?) { 106 | LOG.debug("Push") 107 | 108 | val refSpecs = SmartList(RemoteConfig(repository.getConfig(), Constants.DEFAULT_REMOTE_NAME).getPushRefSpecs()) 109 | if (refSpecs.isEmpty()) { 110 | val head = repository.getRef(Constants.HEAD) 111 | if (head != null && head.isSymbolic()) { 112 | refSpecs.add(RefSpec(head.getLeaf().getName())) 113 | } 114 | } 115 | 116 | val monitor = indicator.asProgressMonitor() 117 | for (transport in Transport.openAll(repository, Constants.DEFAULT_REMOTE_NAME, Transport.Operation.PUSH)) { 118 | for (attempt in 0..1) { 119 | transport.setCredentialsProvider(credentialsProvider) 120 | try { 121 | val result = transport.push(monitor, transport.findRemoteRefUpdatesFor(refSpecs)) 122 | if (LOG.isDebugEnabled()) { 123 | printMessages(result) 124 | 125 | for (refUpdate in result.getRemoteUpdates()) { 126 | LOG.debug(refUpdate.toString()) 127 | } 128 | } 129 | break; 130 | } 131 | catch (e: TransportException) { 132 | if (e.getStatus() == TransportException.Status.NOT_PERMITTED) { 133 | if (attempt == 0) { 134 | credentialsProvider.reset(transport.getURI()) 135 | } 136 | else { 137 | throw AuthenticationException(e) 138 | } 139 | } 140 | else { 141 | wrapIfNeedAndReThrow(e) 142 | } 143 | } 144 | finally { 145 | transport.close() 146 | } 147 | } 148 | } 149 | } 150 | 151 | override fun fetch(indicator: ProgressIndicator?): Updater { 152 | val pullTask = Pull(this, indicator ?: EmptyProgressIndicator()) 153 | val refToMerge = pullTask.fetch() 154 | return object : Updater { 155 | override var definitelySkipPush = false 156 | 157 | override fun merge(): UpdateResult? { 158 | synchronized (lock) { 159 | val committed = commit(pullTask.indicator) 160 | if (refToMerge == null && !committed && getAheadCommitsCount() == 0) { 161 | definitelySkipPush = true 162 | return null 163 | } 164 | else { 165 | return pullTask.pull(prefetchedRefToMerge = refToMerge) 166 | } 167 | } 168 | } 169 | } 170 | } 171 | 172 | override fun pull(indicator: ProgressIndicator) = Pull(this, indicator).pull() 173 | 174 | override fun resetToTheirs(indicator: ProgressIndicator) = Reset(this, indicator).reset(true) 175 | 176 | override fun resetToMy(indicator: ProgressIndicator, localRepositoryInitializer: (() -> Unit)?) = Reset(this, indicator).reset(false, localRepositoryInitializer) 177 | 178 | override fun canCommit() = repository.getRepositoryState().canCommit() 179 | } 180 | 181 | fun printMessages(fetchResult: OperationResult) { 182 | if (LOG.isDebugEnabled()) { 183 | val messages = fetchResult.getMessages() 184 | if (!StringUtil.isEmptyOrSpaces(messages)) { 185 | LOG.debug(messages) 186 | } 187 | } 188 | } 189 | 190 | class GitRepositoryService : RepositoryService { 191 | override fun isValidRepository(file: File): Boolean { 192 | if (File(file, Constants.DOT_GIT).exists()) { 193 | return true 194 | } 195 | 196 | // existing bare repository 197 | try { 198 | FileRepositoryBuilder().setGitDir(file).setMustExist(true).build() 199 | } 200 | catch (e: IOException) { 201 | return false 202 | } 203 | 204 | return true 205 | } 206 | } -------------------------------------------------------------------------------- /src/git/JGitCredentialsProvider.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.git 2 | 3 | import com.intellij.openapi.ui.MessageDialogBuilder 4 | import com.intellij.openapi.ui.Messages 5 | import com.intellij.openapi.util.NotNullLazyValue 6 | import com.intellij.util.ui.UIUtil 7 | import org.eclipse.jgit.lib.Repository 8 | import org.eclipse.jgit.transport.CredentialItem 9 | import org.eclipse.jgit.transport.CredentialsProvider 10 | import org.eclipse.jgit.transport.URIish 11 | import org.jetbrains.keychain.Credentials 12 | import org.jetbrains.keychain.CredentialsStore 13 | import org.jetbrains.keychain.isFulfilled 14 | import org.jetbrains.keychain.isOSXCredentialsStoreSupported 15 | import org.jetbrains.settingsRepository.LOG 16 | import org.jetbrains.settingsRepository.nullize 17 | import org.jetbrains.settingsRepository.showAuthenticationForm 18 | 19 | class JGitCredentialsProvider(private val credentialsStore: NotNullLazyValue, private val repository: Repository) : CredentialsProvider() { 20 | private var credentialsFromGit: Credentials? = null 21 | 22 | override fun isInteractive() = true 23 | 24 | override fun supports(vararg items: CredentialItem): Boolean { 25 | for (item in items) { 26 | if (item is CredentialItem.Password || item is CredentialItem.Username || item is CredentialItem.StringType || item is CredentialItem.YesNoType) { 27 | continue 28 | } 29 | return false 30 | } 31 | return true 32 | } 33 | 34 | override fun get(uri: URIish, vararg items: CredentialItem): Boolean { 35 | var userNameItem: CredentialItem.Username? = null 36 | var passwordItem: CredentialItem? = null 37 | var sshKeyFile: String? = null 38 | for (item in items) { 39 | if (item is CredentialItem.Username) { 40 | userNameItem = item 41 | } 42 | else if (item is CredentialItem.Password) { 43 | passwordItem = item 44 | } 45 | else if (item is CredentialItem.StringType) { 46 | val promptText = item.getPromptText() 47 | if (promptText != null) { 48 | val marker = "Passphrase for " 49 | if (promptText.startsWith(marker) /* JSch prompt */) { 50 | sshKeyFile = promptText.substring(marker.length()) 51 | passwordItem = item 52 | continue 53 | } 54 | } 55 | } 56 | else if (item is CredentialItem.YesNoType) { 57 | UIUtil.invokeAndWaitIfNeeded(Runnable { 58 | item.setValue(MessageDialogBuilder.yesNo("", item.getPromptText()!!).show() == Messages.YES) 59 | }) 60 | return true 61 | } 62 | } 63 | 64 | if (userNameItem == null && passwordItem == null) { 65 | return false 66 | } 67 | return doGet(uri, userNameItem, passwordItem, sshKeyFile) 68 | } 69 | 70 | private fun doGet(uri: URIish, userNameItem: CredentialItem.Username?, passwordItem: CredentialItem?, sshKeyFile: String?): Boolean { 71 | var credentials: Credentials? 72 | 73 | // SSH URL git@github.com:develar/_idea_settings.git, so, username will be "git", we ignore it because in case of SSH credentials account name equals to key filename, but not to username 74 | val userFromUri: String? = if (sshKeyFile == null) uri.getUser().nullize() else null 75 | val passwordFromUri: String? = uri.getPass().nullize() 76 | var saveCredentialsToStore = false 77 | if (userFromUri != null && passwordFromUri != null) { 78 | credentials = Credentials(userFromUri, passwordFromUri) 79 | } 80 | else { 81 | // we open password protected SSH key file using OS X keychain - "git credentials" is pointless in this case 82 | if (sshKeyFile == null || !isOSXCredentialsStoreSupported) { 83 | if (credentialsFromGit == null) { 84 | credentialsFromGit = getCredentialsUsingGit(uri, repository) 85 | } 86 | credentials = credentialsFromGit 87 | } 88 | else { 89 | credentials = null 90 | } 91 | 92 | if (credentials == null) { 93 | try { 94 | credentials = credentialsStore.getValue().get(uri.getHost(), sshKeyFile) 95 | } 96 | catch (e: Throwable) { 97 | LOG.error(e) 98 | } 99 | 100 | saveCredentialsToStore = true 101 | 102 | if (userFromUri != null) { 103 | // username is in url - read password only if it is for the same user 104 | if (userFromUri != credentials?.id) { 105 | credentials = Credentials(userFromUri, passwordFromUri) 106 | } 107 | else if (passwordFromUri != null && passwordFromUri != credentials?.token) { 108 | credentials = Credentials(userFromUri, passwordFromUri) 109 | } 110 | } 111 | } 112 | } 113 | 114 | if (!credentials.isFulfilled()) { 115 | credentials = showAuthenticationForm(credentials, uri.toStringWithoutCredentials(), uri.getHost(), uri.getPath(), sshKeyFile) 116 | } 117 | 118 | if (saveCredentialsToStore && credentials.isFulfilled()) { 119 | credentialsStore.getValue().save(uri.getHost(), credentials!!, sshKeyFile) 120 | } 121 | 122 | userNameItem?.setValue(credentials?.id) 123 | if (passwordItem != null) { 124 | if (passwordItem is CredentialItem.Password) { 125 | passwordItem.setValue(credentials?.token?.toCharArray()) 126 | } 127 | else { 128 | (passwordItem as CredentialItem.StringType).setValue(credentials?.token) 129 | } 130 | } 131 | 132 | return credentials != null 133 | } 134 | 135 | override fun reset(uri: URIish) { 136 | credentialsFromGit = null 137 | credentialsStore.getValue().reset(uri.getHost()!!) 138 | } 139 | } 140 | 141 | fun URIish.toStringWithoutCredentials(): String { 142 | val r = StringBuilder() 143 | if (getScheme() != null) { 144 | r.append(getScheme()) 145 | r.append("://") 146 | } 147 | 148 | if (getHost() != null) { 149 | r.append(getHost()) 150 | if (getScheme() != null && getPort() > 0) { 151 | r.append(':') 152 | r.append(getPort()) 153 | } 154 | } 155 | 156 | if (getPath() != null) { 157 | if (getScheme() != null) { 158 | if (!getPath()!!.startsWith("/")) { 159 | r.append('/') 160 | } 161 | } 162 | else if (getHost() != null) { 163 | r.append(':') 164 | } 165 | 166 | r.append(if (getScheme() != null) getRawPath() else getPath()) 167 | } 168 | return r.toString() 169 | } 170 | -------------------------------------------------------------------------------- /src/git/JGitMergeProvider.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.git 2 | 3 | import com.intellij.openapi.fileEditor.FileDocumentManager 4 | import com.intellij.openapi.vcs.merge.MergeData 5 | import com.intellij.openapi.vcs.merge.MergeProvider2 6 | import com.intellij.openapi.vcs.merge.MergeSession 7 | import com.intellij.openapi.vfs.CharsetToolkit 8 | import com.intellij.openapi.vfs.VirtualFile 9 | import com.intellij.util.ui.ColumnInfo 10 | import org.eclipse.jgit.diff.RawText 11 | import org.eclipse.jgit.lib.ObjectId 12 | import org.eclipse.jgit.lib.Repository 13 | import org.jetbrains.jgit.dirCache.deletePath 14 | import org.jetbrains.jgit.dirCache.writePath 15 | import org.jetbrains.settingsRepository.RepositoryVirtualFile 16 | import java.nio.CharBuffer 17 | import java.util.ArrayList 18 | 19 | private fun conflictsToVirtualFiles(map: Map>): MutableList { 20 | val result = ArrayList(map.size()) 21 | for (path in map.keySet()) { 22 | result.add(RepositoryVirtualFile(path)) 23 | } 24 | return result 25 | } 26 | 27 | class JGitMergeProvider(private val repository: Repository, private val myCommit: ObjectId, private val theirsCommit: ObjectId, private val conflicts: Map>) : MergeProvider2 { 28 | override fun createMergeSession(files: List) = JGitMergeSession() 29 | 30 | override fun conflictResolvedForFile(file: VirtualFile) { 31 | // we can postpone dir cache update (on merge dialog close) to reduce number of flush, but it can leads to data loss (if app crashed during merge - nothing will be saved) 32 | // update dir cache 33 | val bytes = (file as RepositoryVirtualFile).content 34 | // not null if user accepts some revision (virtual file will be directly modified), otherwise document will be modified 35 | if (bytes == null) { 36 | val chars = FileDocumentManager.getInstance().getCachedDocument(file)!!.getImmutableCharSequence() 37 | val byteBuffer = CharsetToolkit.UTF8_CHARSET.encode(CharBuffer.wrap(chars)) 38 | addFile(byteBuffer.array(), file, byteBuffer.remaining()) 39 | } 40 | else { 41 | addFile(bytes, file) 42 | } 43 | } 44 | 45 | // cannot be private due to Kotlin bug 46 | fun addFile(bytes: ByteArray, file: VirtualFile, size: Int = bytes.size()) { 47 | repository.writePath(file.getPath(), bytes, size) 48 | } 49 | 50 | override fun isBinary(file: VirtualFile) = file.getFileType().isBinary() 51 | 52 | override fun loadRevisions(file: VirtualFile): MergeData { 53 | val sequences = conflicts[file.getPath()]!!.getSequences() 54 | val mergeData = MergeData() 55 | mergeData.ORIGINAL = (sequences[0] as RawText).getContent() 56 | mergeData.CURRENT = (sequences[1] as RawText).getContent() 57 | mergeData.LAST = (sequences[2] as RawText).getContent() 58 | return mergeData 59 | } 60 | 61 | private inner class JGitMergeSession : MergeSession { 62 | override fun getMergeInfoColumns(): Array> { 63 | return arrayOf(StatusColumn(false), StatusColumn(true)) 64 | } 65 | 66 | override fun canMerge(file: VirtualFile) = conflicts.contains(file.getPath()) 67 | 68 | override fun conflictResolvedForFile(file: VirtualFile, resolution: MergeSession.Resolution) { 69 | if (resolution == MergeSession.Resolution.Merged) { 70 | conflictResolvedForFile(file) 71 | } 72 | else { 73 | val content = getContent(file, resolution == MergeSession.Resolution.AcceptedTheirs) 74 | if (content == RawText.EMPTY_TEXT) { 75 | repository.deletePath(file.getPath()) 76 | } 77 | else { 78 | addFile(content.getContent(), file) 79 | } 80 | } 81 | } 82 | 83 | private fun getContent(file: VirtualFile, isTheirs: Boolean) = conflicts[file.getPath()]!!.getSequences()[if (isTheirs) 2 else 1] as RawText 84 | 85 | inner class StatusColumn(private val isTheirs: Boolean) : ColumnInfo(if (isTheirs) "Theirs" else "Yours") { 86 | override fun valueOf(file: VirtualFile?) = if (getContent(file!!, isTheirs) == RawText.EMPTY_TEXT) "Deleted" else "Modified" 87 | 88 | override fun getMaxStringValue() = "Modified" 89 | 90 | override fun getAdditionalWidth() = 10 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/git/JGitProgressMonitor.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.git 2 | 3 | import com.intellij.openapi.progress.EmptyProgressIndicator 4 | import com.intellij.openapi.progress.ProgressIndicator 5 | import org.eclipse.jgit.lib.NullProgressMonitor 6 | import org.eclipse.jgit.lib.ProgressMonitor 7 | 8 | fun ProgressIndicator?.asProgressMonitor() = if (this == null || this is EmptyProgressIndicator) NullProgressMonitor.INSTANCE else JGitProgressMonitor(this) 9 | 10 | private class JGitProgressMonitor(private val indicator: ProgressIndicator) : ProgressMonitor { 11 | override fun start(totalTasks: Int) { 12 | } 13 | 14 | override fun beginTask(title: String, totalWork: Int) { 15 | indicator.setText2(title) 16 | } 17 | 18 | override fun update(completed: Int) { 19 | // todo 20 | } 21 | 22 | override fun endTask() { 23 | indicator.setText2("") 24 | } 25 | 26 | override fun isCancelled() = indicator.isCanceled() 27 | } -------------------------------------------------------------------------------- /src/git/commit.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.git 2 | 3 | import com.intellij.openapi.application.ex.ApplicationInfoEx 4 | import com.intellij.openapi.progress.ProgressIndicator 5 | import com.intellij.util.PathUtilRt 6 | import com.intellij.util.SmartList 7 | import org.eclipse.jgit.lib.IndexDiff 8 | import org.eclipse.jgit.lib.ProgressMonitor 9 | import org.jetbrains.jgit.dirCache.AddFile 10 | import org.jetbrains.jgit.dirCache.PathEdit 11 | import org.jetbrains.jgit.dirCache.edit 12 | import org.jetbrains.settingsRepository.LOG 13 | import org.jetbrains.settingsRepository.PROJECTS_DIR_NAME 14 | import java.net.InetAddress 15 | 16 | fun commit(manager: GitRepositoryManager, indicator: ProgressIndicator?): Boolean { 17 | indicator?.checkCanceled() 18 | 19 | val diff = manager.repository.computeIndexDiff() 20 | val changed = diff.diff(indicator?.asProgressMonitor(), ProgressMonitor.UNKNOWN, ProgressMonitor.UNKNOWN, "Commit") 21 | 22 | // don't worry about untracked/modified only in the FS files 23 | if (!changed || (diff.getAdded().isEmpty() && diff.getChanged().isEmpty() && diff.getRemoved().isEmpty())) { 24 | if (diff.getModified().isEmpty()) { 25 | LOG.debug("Nothing to commit") 26 | return false 27 | } 28 | 29 | var edits: MutableList? = null 30 | for (path in diff.getModified()) { 31 | if (!path.startsWith(PROJECTS_DIR_NAME)) { 32 | if (edits == null) { 33 | edits = SmartList() 34 | } 35 | edits.add(AddFile(path)) 36 | } 37 | } 38 | if (edits != null) { 39 | manager.repository.edit(edits) 40 | } 41 | } 42 | 43 | if (LOG.isDebugEnabled()) { 44 | LOG.debug(indexDiffToString(diff)) 45 | } 46 | 47 | indicator?.checkCanceled() 48 | 49 | val builder = StringBuilder() 50 | builder.append(ApplicationInfoEx.getInstanceEx()!!.getFullApplicationName()) 51 | builder.append(' ' ).append('<').append(System.getProperty("user.name", "unknown-user")).append('@').append(InetAddress.getLocalHost().getHostName()) 52 | builder.append(' ') 53 | 54 | // we use Github (edit via web UI) terms here 55 | builder.appendCompactList("Update", diff.getChanged()) 56 | builder.appendCompactList("Create", diff.getAdded()) 57 | builder.appendCompactList("Delete", diff.getRemoved()) 58 | 59 | manager.repository.commit(builder.toString()) 60 | return true 61 | } 62 | 63 | private fun indexDiffToString(diff: IndexDiff): String { 64 | val builder = StringBuilder() 65 | builder.append("To commit:") 66 | builder.addList("Added", diff.getAdded()) 67 | builder.addList("Changed", diff.getChanged()) 68 | builder.addList("Deleted", diff.getRemoved()) 69 | builder.addList("Modified on disk relative to the index", diff.getModified()) 70 | builder.addList("Untracked files", diff.getUntracked()) 71 | builder.addList("Untracked folders", diff.getUntrackedFolders()) 72 | builder.addList("Missing", diff.getMissing()) 73 | return builder.toString() 74 | } 75 | 76 | private fun StringBuilder.appendCompactList(name: String, list: Collection) { 77 | addList(name, list, true) 78 | } 79 | 80 | private fun StringBuilder.addList(name: String, list: Collection, compact: Boolean = false) { 81 | if (list.isEmpty()) { 82 | return 83 | } 84 | 85 | if (compact) { 86 | if (length() != 0 && charAt(length() - 1) != ' ') { 87 | append('\t') 88 | } 89 | append(name) 90 | } 91 | else { 92 | append('\t').append(name).append(':') 93 | } 94 | append(' ') 95 | 96 | var isNotFirst = false 97 | for (path in list) { 98 | if (isNotFirst) { 99 | append(',').append(' ') 100 | } 101 | else { 102 | isNotFirst = true 103 | } 104 | append(if (compact) PathUtilRt.getFileName(path) else path) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/git/dirCacheEditor.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.jgit.dirCache 2 | 3 | import com.intellij.openapi.util.SystemInfo 4 | import com.intellij.openapi.util.io.FileUtil 5 | import org.eclipse.jgit.dircache.BaseDirCacheEditor 6 | import org.eclipse.jgit.dircache.DirCache 7 | import org.eclipse.jgit.dircache.DirCacheEntry 8 | import org.eclipse.jgit.internal.JGitText 9 | import org.eclipse.jgit.lib.Constants 10 | import org.eclipse.jgit.lib.FileMode 11 | import org.eclipse.jgit.lib.Repository 12 | import org.jetbrains.settingsRepository.byteBufferToBytes 13 | import org.jetbrains.settingsRepository.removeWithParentsIfEmpty 14 | import java.io.File 15 | import java.io.FileInputStream 16 | import java.text.MessageFormat 17 | import java.util.Collections 18 | import java.util.Comparator 19 | 20 | private val EDIT_CMP = object : Comparator { 21 | override fun compare(o1: PathEdit, o2: PathEdit): Int { 22 | val a = o1.path 23 | val b = o2.path 24 | return DirCache.cmp(a, a.size(), b, b.size()) 25 | } 26 | } 27 | 28 | /** 29 | * Don't copy edits, 30 | * DeletePath (renamed to DeleteFile) accepts raw path 31 | * Pass repository to apply 32 | */ 33 | public class DirCacheEditor(edits: List, private val repository: Repository, dirCache: DirCache, estimatedNumberOfEntries: Int) : BaseDirCacheEditor(dirCache, estimatedNumberOfEntries) { 34 | private val edits = edits.sortBy(EDIT_CMP) 35 | 36 | override fun commit(): Boolean { 37 | if (edits.isEmpty()) { 38 | // No changes? Don't rewrite the index. 39 | // 40 | cache.unlock() 41 | return true 42 | } 43 | return super.commit() 44 | } 45 | 46 | override fun finish() { 47 | if (!edits.isEmpty()) { 48 | applyEdits() 49 | replace() 50 | } 51 | } 52 | 53 | private fun applyEdits() { 54 | val maxIndex = cache.getEntryCount() 55 | var lastIndex = 0 56 | for (edit in edits) { 57 | var entryIndex = cache.findEntry(edit.path, edit.path.size()) 58 | val missing = entryIndex < 0 59 | if (entryIndex < 0) { 60 | entryIndex = -(entryIndex + 1) 61 | } 62 | val count = Math.min(entryIndex, maxIndex) - lastIndex 63 | if (count > 0) { 64 | fastKeep(lastIndex, count) 65 | } 66 | lastIndex = if (missing) entryIndex else cache.nextEntry(entryIndex) 67 | 68 | if (edit is DeleteFile) { 69 | continue 70 | } 71 | if (edit is DeleteDirectory) { 72 | lastIndex = cache.nextEntry(edit.path, edit.path.size(), entryIndex) 73 | continue 74 | } 75 | 76 | if (missing) { 77 | val entry = DirCacheEntry(edit.path) 78 | edit.apply(entry, repository) 79 | if (entry.getRawMode() == 0) { 80 | throw IllegalArgumentException(MessageFormat.format(JGitText.get().fileModeNotSetForPath, entry.getPathString())) 81 | } 82 | fastAdd(entry) 83 | } 84 | else if (edit is AddFile || edit is AddLoadedFile) { 85 | // apply to first entry and remove others 86 | var firstEntry = cache.getEntry(entryIndex) 87 | val entry: DirCacheEntry 88 | if (firstEntry.isMerged()) { 89 | entry = firstEntry 90 | } 91 | else { 92 | entry = DirCacheEntry(edit.path) 93 | entry.setCreationTime(firstEntry.getCreationTime()) 94 | } 95 | edit.apply(entry, repository) 96 | fastAdd(entry) 97 | } 98 | else { 99 | // apply to all entries of the current path (different stages) 100 | for (i in entryIndex..lastIndex - 1) { 101 | val entry = cache.getEntry(i) 102 | edit.apply(entry, repository) 103 | fastAdd(entry) 104 | } 105 | } 106 | } 107 | 108 | val count = maxIndex - lastIndex 109 | if (count > 0) { 110 | fastKeep(lastIndex, count) 111 | } 112 | } 113 | } 114 | 115 | public abstract class PathEdit(val path: ByteArray) { 116 | public abstract fun apply(entry: DirCacheEntry, repository: Repository) 117 | } 118 | 119 | private fun encodePath(path: String): ByteArray { 120 | val bytes = byteBufferToBytes(Constants.CHARSET.encode(path)) 121 | if (SystemInfo.isWindows) { 122 | for (i in 0..bytes.size() - 1) { 123 | if (bytes[i].toChar() == '\\') { 124 | bytes[i] = '/'.toByte() 125 | } 126 | } 127 | } 128 | return bytes 129 | } 130 | 131 | class AddFile(private val pathString: String) : PathEdit(encodePath(pathString)) { 132 | override fun apply(entry: DirCacheEntry, repository: Repository) { 133 | val file = File(repository.getWorkTree(), pathString) 134 | entry.setFileMode(FileMode.REGULAR_FILE) 135 | val length = file.length() 136 | entry.setLength(length) 137 | entry.setLastModified(file.lastModified()) 138 | 139 | val input = FileInputStream(file) 140 | val inserter = repository.newObjectInserter() 141 | try { 142 | entry.setObjectId(inserter.insert(Constants.OBJ_BLOB, length, input)) 143 | inserter.flush() 144 | } 145 | finally { 146 | inserter.close() 147 | input.close() 148 | } 149 | } 150 | } 151 | 152 | class AddLoadedFile(path: String, private val content: ByteArray, private val size: Int = content.size(), private val lastModified: Long = System.currentTimeMillis()) : PathEdit(encodePath(path)) { 153 | override fun apply(entry: DirCacheEntry, repository: Repository) { 154 | entry.setFileMode(FileMode.REGULAR_FILE) 155 | entry.setLength(size) 156 | entry.setLastModified(lastModified) 157 | 158 | val inserter = repository.newObjectInserter() 159 | try { 160 | entry.setObjectId(inserter.insert(Constants.OBJ_BLOB, content, 0, size)) 161 | inserter.flush() 162 | } 163 | finally { 164 | inserter.close() 165 | } 166 | } 167 | } 168 | 169 | fun DeleteFile(path: String) = DeleteFile(encodePath(path)) 170 | 171 | public class DeleteFile(path: ByteArray) : PathEdit(path) { 172 | override fun apply(entry: DirCacheEntry, repository: Repository) = throw UnsupportedOperationException(JGitText.get().noApplyInDelete) 173 | } 174 | 175 | public class DeleteDirectory(entryPath: String) : PathEdit(encodePath(if (entryPath.endsWith('/') || entryPath.length() == 0) entryPath else "$entryPath/")) { 176 | override fun apply(entry: DirCacheEntry, repository: Repository) = throw UnsupportedOperationException(JGitText.get().noApplyInDelete) 177 | } 178 | 179 | public fun Repository.edit(edit: PathEdit) { 180 | edit(Collections.singletonList(edit)) 181 | } 182 | 183 | public fun Repository.edit(edits: List) { 184 | if (edits.isEmpty()) { 185 | return 186 | } 187 | 188 | val dirCache = lockDirCache() 189 | try { 190 | DirCacheEditor(edits, this, dirCache, dirCache.getEntryCount() + 4).commit() 191 | } 192 | finally { 193 | dirCache.unlock() 194 | } 195 | } 196 | 197 | private class DirCacheTerminator(dirCache: DirCache) : BaseDirCacheEditor(dirCache, 0) { 198 | override fun finish() { 199 | replace() 200 | } 201 | } 202 | 203 | public fun Repository.deleteAllFiles(deletedSet: MutableSet? = null, fromWorkingTree: Boolean = true) { 204 | val dirCache = lockDirCache() 205 | try { 206 | if (deletedSet != null) { 207 | for (i in 0..dirCache.getEntryCount() - 1) { 208 | val entry = dirCache.getEntry(i) 209 | if (entry.getFileMode() == FileMode.REGULAR_FILE) { 210 | deletedSet.add(entry.getPathString()) 211 | } 212 | } 213 | } 214 | DirCacheTerminator(dirCache).commit() 215 | } 216 | finally { 217 | dirCache.unlock() 218 | } 219 | 220 | if (fromWorkingTree) { 221 | val files = getWorkTree().listFiles { it.getName() != Constants.DOT_GIT } 222 | if (files != null) { 223 | for (file in files) { 224 | FileUtil.delete(file) 225 | } 226 | } 227 | } 228 | } 229 | 230 | public fun Repository.writePath(path: String, bytes: ByteArray, size: Int = bytes.size()) { 231 | edit(AddLoadedFile(path, bytes, size)) 232 | FileUtil.writeToFile(File(getWorkTree(), path), bytes, 0, size) 233 | } 234 | 235 | public fun Repository.deletePath(path: String, isFile: Boolean = true, fromWorkingTree: Boolean = true) { 236 | edit((if (isFile) DeleteFile(path) else DeleteDirectory(path))) 237 | 238 | if (fromWorkingTree) { 239 | val workTree = getWorkTree() 240 | val ioFile = File(workTree, path) 241 | if (ioFile.exists()) { 242 | ioFile.removeWithParentsIfEmpty(workTree, isFile) 243 | } 244 | } 245 | } -------------------------------------------------------------------------------- /src/git/gitCredential.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.git 2 | 3 | import com.intellij.execution.configurations.GeneralCommandLine 4 | import com.intellij.execution.process.ProcessNotCreatedException 5 | import com.intellij.openapi.util.text.StringUtil 6 | import org.eclipse.jgit.lib.Repository 7 | import org.eclipse.jgit.transport.URIish 8 | import org.jetbrains.keychain.Credentials 9 | import org.jetbrains.settingsRepository.LOG 10 | 11 | private var canUseGitExe = true 12 | 13 | // https://www.kernel.org/pub/software/scm/git/docs/git-credential.html 14 | fun getCredentialsUsingGit(uri: URIish, repository: Repository): Credentials? { 15 | if (!canUseGitExe || repository.getConfig().getSubsections("credential").isEmpty()) { 16 | return null 17 | } 18 | 19 | val commandLine = GeneralCommandLine() 20 | commandLine.setExePath("git") 21 | commandLine.addParameter("credential") 22 | commandLine.addParameter("fill") 23 | commandLine.setPassParentEnvironment(true) 24 | val process: Process 25 | try { 26 | process = commandLine.createProcess() 27 | } 28 | catch (e: ProcessNotCreatedException) { 29 | canUseGitExe = false 30 | return null 31 | } 32 | 33 | val writer = process.getOutputStream().writer() 34 | writer.write("url=") 35 | writer.write(uri.toPrivateString()) 36 | writer.write("\n\n") 37 | writer.close(); 38 | 39 | val reader = process.getInputStream().reader().buffered() 40 | var username: String? = null 41 | var password: String? = null 42 | while (true) { 43 | val line = reader.readLine()?.trim() 44 | if (line == null || line.isEmpty()) { 45 | break 46 | } 47 | 48 | fun readValue() = line.substring(line.indexOf('=') + 1).trim() 49 | 50 | if (line.startsWith("username=")) { 51 | username = readValue() 52 | } 53 | else if (line.startsWith("password=")) { 54 | password = readValue() 55 | } 56 | } 57 | reader.close() 58 | 59 | val errorText = process.getErrorStream().reader().readText() 60 | if (!StringUtil.isEmpty(errorText)) { 61 | LOG.warn(errorText) 62 | } 63 | return if (username == null && password == null) null else Credentials(username, password) 64 | } 65 | -------------------------------------------------------------------------------- /src/git/pull.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.git 2 | 3 | import com.intellij.openapi.progress.ProgressIndicator 4 | import com.intellij.openapi.vfs.VirtualFile 5 | import org.eclipse.jgit.api.MergeCommand.FastForwardMode 6 | import org.eclipse.jgit.api.MergeResult 7 | import org.eclipse.jgit.api.MergeResult.MergeStatus 8 | import org.eclipse.jgit.api.errors.CheckoutConflictException 9 | import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException 10 | import org.eclipse.jgit.api.errors.JGitInternalException 11 | import org.eclipse.jgit.api.errors.NoHeadException 12 | import org.eclipse.jgit.dircache.DirCacheCheckout 13 | import org.eclipse.jgit.internal.JGitText 14 | import org.eclipse.jgit.lib.* 15 | import org.eclipse.jgit.merge.MergeMessageFormatter 16 | import org.eclipse.jgit.merge.MergeStrategy 17 | import org.eclipse.jgit.merge.ResolveMerger 18 | import org.eclipse.jgit.merge.SquashMessageFormatter 19 | import org.eclipse.jgit.revwalk.RevWalk 20 | import org.eclipse.jgit.revwalk.RevWalkUtils 21 | import org.eclipse.jgit.transport.RemoteConfig 22 | import org.eclipse.jgit.treewalk.FileTreeIterator 23 | import org.jetbrains.settingsRepository.* 24 | import java.io.IOException 25 | import java.text.MessageFormat 26 | import java.util.ArrayList 27 | 28 | open class Pull(val manager: GitRepositoryManager, val indicator: ProgressIndicator) { 29 | val repository = manager.repository 30 | 31 | // we must use the same StoredConfig instance during the operation 32 | val config = repository.getConfig() 33 | val remoteConfig = RemoteConfig(config, Constants.DEFAULT_REMOTE_NAME) 34 | 35 | fun pull(mergeStrategy: MergeStrategy = MergeStrategy.RECURSIVE, commitMessage: String? = null, prefetchedRefToMerge: Ref? = null): UpdateResult? { 36 | indicator.checkCanceled() 37 | 38 | LOG.debug("Pull") 39 | 40 | val repository = manager.repository 41 | val repositoryState = repository.getRepositoryState() 42 | if (repositoryState != RepositoryState.SAFE) { 43 | LOG.warn(MessageFormat.format(JGitText.get().cannotPullOnARepoWithState, repositoryState.name())) 44 | } 45 | 46 | val refToMerge = prefetchedRefToMerge ?: fetch() ?: return null 47 | 48 | val mergeResult = merge(refToMerge, mergeStrategy, commitMessage = commitMessage) 49 | val mergeStatus = mergeResult.mergeStatus 50 | if (LOG.isDebugEnabled()) { 51 | LOG.debug(mergeStatus.toString()) 52 | } 53 | 54 | if (mergeStatus == MergeStatus.CONFLICTING) { 55 | val mergedCommits = mergeResult.mergedCommits 56 | assert(mergedCommits.size() == 2) 57 | val conflicts = mergeResult.conflicts!! 58 | var unresolvedFiles = conflictsToVirtualFiles(conflicts) 59 | val mergeProvider = JGitMergeProvider(repository, mergedCommits[0]!!, mergedCommits[1]!!, conflicts) 60 | val resolvedFiles: List 61 | val mergedFiles = ArrayList() 62 | while (true) { 63 | resolvedFiles = resolveConflicts(unresolvedFiles, mergeProvider) 64 | 65 | for (file in resolvedFiles) { 66 | mergedFiles.add(file.getPath()) 67 | } 68 | 69 | if (resolvedFiles.size() == unresolvedFiles.size()) { 70 | break 71 | } 72 | else { 73 | unresolvedFiles.removeAll(mergedFiles) 74 | } 75 | } 76 | 77 | repository.commit() 78 | 79 | return mergeResult.result.toMutable().addChanged(mergedFiles) 80 | } 81 | else if (!mergeStatus.isSuccessful()) { 82 | throw IllegalStateException(mergeResult.toString()) 83 | } 84 | else { 85 | return mergeResult.result 86 | } 87 | } 88 | 89 | fun fetch(prevRefUpdateResult: RefUpdate.Result? = null): Ref? { 90 | indicator.checkCanceled() 91 | 92 | val repository = manager.repository 93 | 94 | val fetchResult = repository.fetch(remoteConfig, manager.credentialsProvider, indicator.asProgressMonitor()) ?: return null 95 | 96 | if (LOG.isDebugEnabled()) { 97 | printMessages(fetchResult) 98 | for (refUpdate in fetchResult.getTrackingRefUpdates()) { 99 | LOG.debug(refUpdate.toString()) 100 | } 101 | } 102 | 103 | indicator.checkCanceled() 104 | 105 | var hasChanges = false 106 | for (fetchRefSpec in remoteConfig.getFetchRefSpecs()) { 107 | val refUpdate = fetchResult.getTrackingRefUpdate(fetchRefSpec.getDestination()) 108 | if (refUpdate == null) { 109 | LOG.debug("No ref update for $fetchRefSpec") 110 | continue 111 | } 112 | 113 | val refUpdateResult = refUpdate.getResult() 114 | // we can have more than one fetch ref spec, but currently we don't worry about it 115 | if (refUpdateResult == RefUpdate.Result.LOCK_FAILURE || refUpdateResult == RefUpdate.Result.IO_FAILURE) { 116 | if (prevRefUpdateResult == refUpdateResult) { 117 | throw IOException("Ref update result " + refUpdateResult.name() + ", we have already tried to fetch again, but no luck") 118 | } 119 | 120 | LOG.warn("Ref update result " + refUpdateResult.name() + ", trying again after 500 ms") 121 | Thread.sleep(500) 122 | return fetch(refUpdateResult) 123 | } 124 | 125 | if (!(refUpdateResult == RefUpdate.Result.FAST_FORWARD || refUpdateResult == RefUpdate.Result.NEW || refUpdateResult == RefUpdate.Result.FORCED)) { 126 | throw UnsupportedOperationException("Unsupported ref update result") 127 | } 128 | 129 | if (!hasChanges) { 130 | hasChanges = refUpdateResult != RefUpdate.Result.NO_CHANGE 131 | } 132 | } 133 | 134 | if (!hasChanges) { 135 | LOG.debug("No remote changes") 136 | return null 137 | } 138 | 139 | return fetchResult.getAdvertisedRef(config.getRemoteBranchFullName()) ?: throw IllegalStateException("Could not get advertised ref") 140 | } 141 | 142 | fun merge(unpeeledRef: Ref, 143 | mergeStrategy: MergeStrategy = MergeStrategy.RECURSIVE, 144 | commit: Boolean = true, 145 | fastForwardMode: FastForwardMode = FastForwardMode.FF, 146 | squash: Boolean = false, 147 | forceMerge: Boolean = false, 148 | commitMessage: String? = null): MergeResultEx { 149 | indicator.checkCanceled() 150 | 151 | val head = repository.getRef(Constants.HEAD) ?: throw NoHeadException(JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported) 152 | 153 | // Check for FAST_FORWARD, ALREADY_UP_TO_DATE 154 | val revWalk = RevWalk(repository) 155 | var dirCacheCheckout: DirCacheCheckout? = null 156 | try { 157 | // handle annotated tags 158 | val ref = repository.peel(unpeeledRef) 159 | var objectId = ref.getPeeledObjectId() 160 | if (objectId == null) { 161 | objectId = ref.getObjectId() 162 | } 163 | 164 | val srcCommit = revWalk.lookupCommit(objectId) 165 | val headId = head.getObjectId() 166 | if (headId == null) { 167 | revWalk.parseHeaders(srcCommit) 168 | dirCacheCheckout = DirCacheCheckout(repository, repository.lockDirCache(), srcCommit.getTree()) 169 | dirCacheCheckout.setFailOnConflict(true) 170 | dirCacheCheckout.checkout() 171 | val refUpdate = repository.updateRef(head.getTarget().getName()) 172 | refUpdate.setNewObjectId(objectId) 173 | refUpdate.setExpectedOldObjectId(null) 174 | refUpdate.setRefLogMessage("initial pull", false) 175 | if (refUpdate.update() != RefUpdate.Result.NEW) { 176 | throw NoHeadException(JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported) 177 | } 178 | return MergeResultEx(srcCommit, MergeStatus.FAST_FORWARD, arrayOf(null, srcCommit), ImmutableUpdateResult(dirCacheCheckout.getUpdated().keySet(), dirCacheCheckout.getRemoved())) 179 | //return MergeResult(srcCommit, srcCommit, array(null, srcCommit), MergeStatus.FAST_FORWARD, mergeStrategy, null) 180 | } 181 | 182 | val refLogMessage = StringBuilder("merge ") 183 | refLogMessage.append(ref.getName()) 184 | 185 | val headCommit = revWalk.lookupCommit(headId) 186 | if (!forceMerge && revWalk.isMergedInto(srcCommit, headCommit)) { 187 | return MergeResultEx(headCommit, MergeStatus.ALREADY_UP_TO_DATE, arrayOf(headCommit, srcCommit), EMPTY_UPDATE_RESULT) 188 | //return MergeResult(headCommit, srcCommit, array(headCommit, srcCommit), MergeStatus.ALREADY_UP_TO_DATE, mergeStrategy, null) 189 | } 190 | else if (!forceMerge && fastForwardMode != FastForwardMode.NO_FF && revWalk.isMergedInto(headCommit, srcCommit)) { 191 | // FAST_FORWARD detected: skip doing a real merge but only update HEAD 192 | refLogMessage.append(": ").append(MergeStatus.FAST_FORWARD) 193 | dirCacheCheckout = DirCacheCheckout(repository, headCommit.getTree(), repository.lockDirCache(), srcCommit.getTree()) 194 | dirCacheCheckout.setFailOnConflict(true) 195 | dirCacheCheckout.checkout() 196 | // var msg: String? = null 197 | val newHead: ObjectId 198 | // val base: ObjectId 199 | val mergeStatus: MergeStatus 200 | if (squash) { 201 | // msg = JGitText.get().squashCommitNotUpdatingHEAD 202 | // base = headId 203 | newHead = headId 204 | mergeStatus = MergeStatus.FAST_FORWARD_SQUASHED 205 | val squashedCommits = RevWalkUtils.find(revWalk, srcCommit, headCommit) 206 | repository.writeSquashCommitMsg(SquashMessageFormatter().format(squashedCommits, head)) 207 | } 208 | else { 209 | updateHead(refLogMessage, srcCommit, headId, repository) 210 | // base = srcCommit 211 | newHead = srcCommit 212 | mergeStatus = MergeStatus.FAST_FORWARD 213 | } 214 | return MergeResultEx(newHead, mergeStatus, arrayOf(headCommit, srcCommit), ImmutableUpdateResult(dirCacheCheckout.getUpdated().keySet(), dirCacheCheckout.getRemoved())) 215 | //return MergeResult(newHead, base, array(headCommit, srcCommit), mergeStatus, mergeStrategy, null, msg) 216 | } 217 | else { 218 | if (fastForwardMode == FastForwardMode.FF_ONLY) { 219 | return MergeResultEx(headCommit, MergeStatus.ABORTED, arrayOf(headCommit, srcCommit), EMPTY_UPDATE_RESULT) 220 | // return MergeResult(headCommit, srcCommit, array(headCommit, srcCommit), MergeStatus.ABORTED, mergeStrategy, null) 221 | } 222 | 223 | val mergeMessage: String 224 | if (squash) { 225 | mergeMessage = "" 226 | repository.writeSquashCommitMsg(SquashMessageFormatter().format(RevWalkUtils.find(revWalk, srcCommit, headCommit), head)) 227 | } 228 | else { 229 | mergeMessage = MergeMessageFormatter().format(listOf(ref), head) 230 | repository.writeMergeCommitMsg(mergeMessage) 231 | repository.writeMergeHeads(arrayListOf(ref.getObjectId())) 232 | } 233 | val merger = mergeStrategy.newMerger(repository) 234 | val noProblems: Boolean 235 | var lowLevelResults: Map>? = null 236 | var failingPaths: Map? = null 237 | var unmergedPaths: List? = null 238 | if (merger is ResolveMerger) { 239 | merger.setCommitNames(arrayOf("BASE", "HEAD", ref.getName())) 240 | merger.setWorkingTreeIterator(FileTreeIterator(repository)) 241 | noProblems = merger.merge(headCommit, srcCommit) 242 | lowLevelResults = merger.getMergeResults() 243 | failingPaths = merger.getFailingPaths() 244 | unmergedPaths = merger.getUnmergedPaths() 245 | } 246 | else { 247 | noProblems = merger.merge(headCommit, srcCommit) 248 | } 249 | refLogMessage.append(": Merge made by ") 250 | if (revWalk.isMergedInto(headCommit, srcCommit)) { 251 | refLogMessage.append("recursive") 252 | } 253 | else { 254 | refLogMessage.append(mergeStrategy.getName()) 255 | } 256 | refLogMessage.append('.') 257 | 258 | var result: ImmutableUpdateResult? = null 259 | if (merger is ResolveMerger) { 260 | result = ImmutableUpdateResult(merger.getToBeCheckedOut().keySet(), merger.getToBeDeleted()) 261 | } 262 | 263 | if (noProblems) { 264 | // ResolveMerger does checkout 265 | if (merger !is ResolveMerger) { 266 | dirCacheCheckout = DirCacheCheckout(repository, headCommit.getTree(), repository.lockDirCache(), merger.getResultTreeId()) 267 | dirCacheCheckout.setFailOnConflict(true) 268 | dirCacheCheckout.checkout() 269 | result = ImmutableUpdateResult(dirCacheCheckout.getUpdated().keySet(), dirCacheCheckout.getRemoved()) 270 | } 271 | 272 | // var msg: String? = null 273 | var newHeadId: ObjectId? = null 274 | var mergeStatus: MergeResult.MergeStatus? = null 275 | if (!commit && squash) { 276 | mergeStatus = MergeResult.MergeStatus.MERGED_SQUASHED_NOT_COMMITTED 277 | } 278 | if (!commit && !squash) { 279 | mergeStatus = MergeResult.MergeStatus.MERGED_NOT_COMMITTED 280 | } 281 | if (commit && !squash) { 282 | newHeadId = repository.commit(commitMessage, refLogMessage.toString()).getId() 283 | mergeStatus = MergeResult.MergeStatus.MERGED 284 | } 285 | if (commit && squash) { 286 | // msg = JGitText.get().squashCommitNotUpdatingHEAD 287 | newHeadId = headCommit.getId() 288 | mergeStatus = MergeResult.MergeStatus.MERGED_SQUASHED 289 | } 290 | return MergeResultEx(newHeadId, mergeStatus!!, arrayOf(headCommit.getId(), srcCommit.getId()), result!!) 291 | // return MergeResult(newHeadId, null, array(headCommit.getId(), srcCommit.getId()), mergeStatus, mergeStrategy, null, msg) 292 | } 293 | else { 294 | if (failingPaths == null) { 295 | val mergeMessageWithConflicts = MergeMessageFormatter().formatWithConflicts(mergeMessage, unmergedPaths) 296 | repository.writeMergeCommitMsg(mergeMessageWithConflicts) 297 | return MergeResultEx(null, MergeResult.MergeStatus.CONFLICTING, arrayOf(headCommit.getId(), srcCommit.getId()), result!!, lowLevelResults) 298 | //return MergeResult(null, merger.getBaseCommitId(), array(headCommit.getId(), srcCommit.getId()), MergeResult.MergeStatus.CONFLICTING, mergeStrategy, lowLevelResults) 299 | } 300 | else { 301 | repository.writeMergeCommitMsg(null) 302 | repository.writeMergeHeads(null) 303 | return MergeResultEx(null, MergeResult.MergeStatus.FAILED, arrayOf(headCommit.getId(), srcCommit.getId()), result!!, lowLevelResults) 304 | //return MergeResult(null, merger.getBaseCommitId(), array(headCommit.getId(), srcCommit.getId()), MergeResult.MergeStatus.FAILED, mergeStrategy, lowLevelResults, failingPaths, null) 305 | } 306 | } 307 | } 308 | } 309 | catch (e: org.eclipse.jgit.errors.CheckoutConflictException) { 310 | throw CheckoutConflictException(if (dirCacheCheckout == null) listOf() else dirCacheCheckout.getConflicts(), e) 311 | } 312 | finally { 313 | revWalk.close() 314 | } 315 | } 316 | } 317 | 318 | class MergeResultEx(val newHead: ObjectId?, val mergeStatus: MergeStatus, val mergedCommits: Array, val result: ImmutableUpdateResult, val conflicts: Map>? = null) 319 | 320 | private fun updateHead(refLogMessage: StringBuilder, newHeadId: ObjectId, oldHeadID: ObjectId, repository: Repository) { 321 | val refUpdate = repository.updateRef(Constants.HEAD) 322 | refUpdate.setNewObjectId(newHeadId) 323 | refUpdate.setRefLogMessage(refLogMessage.toString(), false) 324 | refUpdate.setExpectedOldObjectId(oldHeadID) 325 | val rc = refUpdate.update() 326 | when (rc) { 327 | RefUpdate.Result.NEW, RefUpdate.Result.FAST_FORWARD -> return 328 | RefUpdate.Result.REJECTED, RefUpdate.Result.LOCK_FAILURE -> throw ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD, refUpdate.getRef(), rc) 329 | else -> throw JGitInternalException(MessageFormat.format(JGitText.get().updatingRefFailed, Constants.HEAD, newHeadId.toString(), rc)) 330 | } 331 | } 332 | 333 | -------------------------------------------------------------------------------- /src/git/reset.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.git 2 | 3 | import com.intellij.openapi.progress.ProgressIndicator 4 | import org.eclipse.jgit.merge.MergeStrategy 5 | import org.jetbrains.jgit.dirCache.deleteAllFiles 6 | import org.jetbrains.settingsRepository.LOG 7 | import org.jetbrains.settingsRepository.MutableUpdateResult 8 | import org.jetbrains.settingsRepository.UpdateResult 9 | 10 | class Reset(manager: GitRepositoryManager, indicator: ProgressIndicator) : Pull(manager, indicator) { 11 | fun reset(toTheirs: Boolean, localRepositoryInitializer: (() -> Unit)? = null): UpdateResult { 12 | if (LOG.isDebugEnabled()) { 13 | LOG.debug("Reset to ${if (toTheirs) "theirs" else "my"}") 14 | } 15 | 16 | val resetResult = repository.resetHard() 17 | val result = MutableUpdateResult(resetResult.getUpdated().keySet(), resetResult.getRemoved()) 18 | 19 | indicator.checkCanceled() 20 | 21 | val commitMessage = "Reset to ${if (toTheirs) manager.getUpstream() else "my"}" 22 | // grab added/deleted/renamed/modified files 23 | val mergeStrategy = if (toTheirs) MergeStrategy.THEIRS else MergeStrategy.OURS 24 | val firstMergeResult = pull(mergeStrategy, commitMessage) 25 | 26 | if (localRepositoryInitializer == null) { 27 | if (firstMergeResult == null) { 28 | // nothing to merge, so, we merge latest origin commit 29 | val fetchRefSpecs = remoteConfig.getFetchRefSpecs() 30 | assert(fetchRefSpecs.size() == 1) 31 | 32 | val latestUpstreamCommit = repository.getRef(fetchRefSpecs[0].getDestination()!!) 33 | if (latestUpstreamCommit == null) { 34 | if (toTheirs) { 35 | repository.deleteAllFiles(result.deleted) 36 | result.changed.removeAll(result.deleted) 37 | repository.commit(commitMessage) 38 | } 39 | else { 40 | LOG.debug("uninitialized remote (empty) - we don't need to merge") 41 | } 42 | return result 43 | } 44 | 45 | val mergeResult = merge(latestUpstreamCommit, mergeStrategy, true, forceMerge = true, commitMessage = commitMessage) 46 | if (!mergeResult.mergeStatus.isSuccessful()) { 47 | throw IllegalStateException(mergeResult.toString()) 48 | } 49 | result.add(mergeResult.result) 50 | } 51 | else { 52 | result.add(firstMergeResult) 53 | } 54 | } 55 | else { 56 | assert(!toTheirs) 57 | result.add(firstMergeResult) 58 | 59 | // must be performed only after initial pull, so, local changes will be relative to remote files 60 | localRepositoryInitializer() 61 | manager.commit(indicator) 62 | result.add(pull(mergeStrategy, commitMessage)) 63 | } 64 | return result 65 | } 66 | } -------------------------------------------------------------------------------- /src/keychain/CredentialsStore.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.keychain 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | 5 | val LOG: Logger = Logger.getInstance(javaClass()) 6 | 7 | public data class Credentials(id: String?, token: String?) { 8 | public val id: String? = if (id.isNullOrEmpty()) null else id 9 | public val token: String? = if (token.isNullOrEmpty()) null else token 10 | } 11 | 12 | public fun Credentials?.isFulfilled(): Boolean = this != null && id != null && token != null 13 | 14 | public interface CredentialsStore { 15 | public fun get(host: String?, sshKeyFile: String? = null): Credentials? 16 | 17 | public fun save(host: String?, credentials: Credentials, sshKeyFile: String? = null) 18 | 19 | public fun reset(host: String) 20 | } 21 | -------------------------------------------------------------------------------- /src/keychain/FileCredentialsStore.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.keychain 2 | 3 | import com.intellij.openapi.util.PasswordUtil 4 | import com.intellij.openapi.util.io.FileUtil 5 | import com.intellij.util.io.IOUtil 6 | import java.io.* 7 | 8 | class FileCredentialsStore(private val storeFile: File) : CredentialsStore { 9 | // we store only one for any URL, don't want to add complexity, OS keychain should be used 10 | private var credentials: Credentials? = null 11 | 12 | private var dataLoaded = !storeFile.exists() 13 | 14 | private fun ensureLoaded() { 15 | if (dataLoaded) { 16 | return 17 | } 18 | 19 | dataLoaded = true 20 | if (storeFile.exists()) { 21 | try { 22 | var hasErrors = true 23 | val `in` = DataInputStream(FileInputStream(storeFile).buffered()) 24 | try { 25 | credentials = Credentials(PasswordUtil.decodePassword(IOUtil.readString(`in`)), PasswordUtil.decodePassword(IOUtil.readString(`in`))) 26 | hasErrors = false 27 | } 28 | finally { 29 | if (hasErrors) { 30 | //noinspection ResultOfMethodCallIgnored 31 | storeFile.delete() 32 | } 33 | `in`.close() 34 | } 35 | } 36 | catch (e: IOException) { 37 | LOG.error(e) 38 | } 39 | } 40 | } 41 | 42 | override fun get(host: String?, sshKeyFile: String?): Credentials? { 43 | ensureLoaded() 44 | return credentials 45 | } 46 | 47 | override fun reset(host: String) { 48 | if (credentials != null) { 49 | dataLoaded = true 50 | storeFile.delete() 51 | 52 | credentials = Credentials(credentials!!.id, null) 53 | } 54 | } 55 | 56 | override fun save(host: String?, credentials: Credentials, sshKeyFile: String?) { 57 | if (credentials.equals(this.credentials)) { 58 | return 59 | } 60 | 61 | this.credentials = credentials 62 | 63 | try { 64 | FileUtil.createParentDirs(storeFile) 65 | val out = DataOutputStream(FileOutputStream(storeFile).buffered()) 66 | try { 67 | IOUtil.writeString(PasswordUtil.encodePassword(credentials.id), out) 68 | IOUtil.writeString(PasswordUtil.encodePassword(credentials.token), out) 69 | } 70 | finally { 71 | out.close() 72 | } 73 | } 74 | catch (e: IOException) { 75 | LOG.error(e) 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/keychain/OSXKeychainLibrary.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.keychain 2 | 3 | import com.intellij.openapi.util.SystemInfo 4 | import com.sun.jna.Pointer 5 | import java.nio.ByteBuffer 6 | import java.nio.CharBuffer 7 | 8 | val isOSXCredentialsStoreSupported: Boolean 9 | get() = SystemInfo.isMacIntel64 && SystemInfo.isMacOSLeopard 10 | 11 | // http://developer.apple.com/mac/library/DOCUMENTATION/Security/Reference/keychainservices/Reference/reference.html 12 | // It is very, very important to use CFRelease/SecKeychainItemFreeContent You must do it, otherwise you can get "An invalid record was encountered." 13 | public interface OSXKeychainLibrary : com.sun.jna.Library { 14 | companion object { 15 | private val LIBRARY = com.sun.jna.Native.loadLibrary("Security", javaClass()) as OSXKeychainLibrary 16 | 17 | fun saveGenericPassword(serviceName: ByteArray, accountName: String, password: CharArray) { 18 | saveGenericPassword(serviceName, accountName, Charsets.UTF_8.encode(CharBuffer.wrap(password))) 19 | } 20 | 21 | fun saveGenericPassword(serviceName: ByteArray, accountName: String, password: String) { 22 | saveGenericPassword(serviceName, accountName, Charsets.UTF_8.encode(password)) 23 | } 24 | 25 | private fun saveGenericPassword(serviceName: ByteArray, accountName: String, passwordBuffer: ByteBuffer) { 26 | val passwordData: ByteArray 27 | val passwordDataSize = passwordBuffer.limit() 28 | if (passwordBuffer.hasArray() && passwordBuffer.arrayOffset() == 0) { 29 | passwordData = passwordBuffer.array() 30 | } 31 | else { 32 | passwordData = ByteArray(passwordDataSize) 33 | passwordBuffer.get(passwordData) 34 | } 35 | saveGenericPassword(serviceName, accountName, passwordData, passwordDataSize) 36 | } 37 | 38 | fun findGenericPassword(serviceName: ByteArray, accountName: String): String? { 39 | val accountNameBytes = accountName.toByteArray() 40 | val passwordSize = IntArray(1); 41 | val passwordData = arrayOf(null); 42 | checkForError("find", LIBRARY.SecKeychainFindGenericPassword(null, serviceName.size(), serviceName, accountNameBytes.size(), accountNameBytes, passwordSize, passwordData)) 43 | val pointer = passwordData[0] ?: return null 44 | 45 | val result = String(pointer.getByteArray(0, passwordSize[0])) 46 | LIBRARY.SecKeychainItemFreeContent(null, pointer) 47 | return result 48 | } 49 | 50 | private fun saveGenericPassword(serviceName: ByteArray, accountName: String, password: ByteArray, passwordSize: Int) { 51 | val accountNameBytes = accountName.toByteArray() 52 | val itemRef = arrayOf(null) 53 | checkForError("find (for save)", LIBRARY.SecKeychainFindGenericPassword(null, serviceName.size(), serviceName, accountNameBytes.size(), accountNameBytes, null, null, itemRef)) 54 | val pointer = itemRef[0] 55 | if (pointer == null) { 56 | checkForError("save (new)", LIBRARY.SecKeychainAddGenericPassword(null, serviceName.size(), serviceName, accountNameBytes.size(), accountNameBytes, passwordSize, password)) 57 | } 58 | else { 59 | checkForError("save (update)", LIBRARY.SecKeychainItemModifyContent(pointer, null, passwordSize, password)) 60 | LIBRARY.CFRelease(pointer) 61 | } 62 | } 63 | 64 | fun deleteGenericPassword(serviceName: ByteArray, accountName: String) { 65 | val itemRef = arrayOf(null) 66 | val accountNameBytes = accountName.toByteArray() 67 | checkForError("find (for delete)", LIBRARY.SecKeychainFindGenericPassword(null, serviceName.size(), serviceName, accountNameBytes.size(), accountNameBytes, null, null, itemRef)) 68 | val pointer = itemRef[0] 69 | if (pointer != null) { 70 | checkForError("delete", LIBRARY.SecKeychainItemDelete(pointer)) 71 | LIBRARY.CFRelease(pointer) 72 | } 73 | } 74 | 75 | fun checkForError(message: String, code: Int) { 76 | if (code != 0 && code != /* errSecItemNotFound, always returned from find it seems */-25300) { 77 | val translated = LIBRARY.SecCopyErrorMessageString(code, null); 78 | val builder = StringBuilder(message).append(": ") 79 | if (translated == null) { 80 | builder.append(code); 81 | } 82 | else { 83 | val buf = CharArray(LIBRARY.CFStringGetLength(translated).toInt()) 84 | for (i in 0..buf.size() - 1) { 85 | buf[i] = LIBRARY.CFStringGetCharacterAtIndex(translated, i.toLong()) 86 | } 87 | LIBRARY.CFRelease(translated) 88 | builder.append(buf).append(" (").append(code).append(')') 89 | } 90 | LOG.error(builder.toString()) 91 | } 92 | } 93 | } 94 | 95 | public fun SecKeychainAddGenericPassword(keychain: Pointer?, serviceNameLength: Int, serviceName: ByteArray, accountNameLength: Int, accountName: ByteArray, passwordLength: Int, passwordData: ByteArray, itemRef: Pointer? = null): Int 96 | 97 | public fun SecKeychainItemModifyContent(/*SecKeychainItemRef*/ itemRef: Pointer, /*SecKeychainAttributeList**/ attrList: Pointer?, length: Int, data: ByteArray): Int 98 | 99 | public fun SecKeychainFindGenericPassword(keychainOrArray: Pointer?, 100 | serviceNameLength: Int, 101 | serviceName: ByteArray, 102 | accountNameLength: Int, 103 | accountName: ByteArray, 104 | passwordLength: IntArray? = null, 105 | passwordData: Array? = null, 106 | itemRef: Array? = null): Int 107 | 108 | public fun SecKeychainItemDelete(itemRef: Pointer): Int 109 | 110 | public fun /*CFString*/ SecCopyErrorMessageString(status: Int, reserved: Pointer?): Pointer? 111 | 112 | // http://developer.apple.com/library/mac/#documentation/CoreFoundation/Reference/CFStringRef/Reference/reference.html 113 | 114 | public fun /*CFIndex*/ CFStringGetLength(/*CFStringRef*/ theString: Pointer): Long 115 | 116 | public fun /*UniChar*/ CFStringGetCharacterAtIndex(/*CFStringRef*/ theString: Pointer, /*CFIndex*/ idx: Long): Char 117 | 118 | public fun CFRelease(/*CFTypeRef*/ cf: Pointer) 119 | 120 | public fun SecKeychainItemFreeContent(/*SecKeychainAttributeList*/attrList: Pointer?, data: Pointer?) 121 | } 122 | -------------------------------------------------------------------------------- /src/keychain/OsXCredentialsStore.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.keychain 2 | 3 | import com.intellij.openapi.util.PasswordUtil 4 | import gnu.trove.THashMap 5 | 6 | class OsXCredentialsStore(serviceName: String) : CredentialsStore { 7 | private val serviceName = serviceName.toByteArray() 8 | 9 | companion object { 10 | val SSH = "SSH".toByteArray() 11 | } 12 | 13 | private val accountToCredentials = THashMap() 14 | 15 | override fun get(host: String?, sshKeyFile: String?): Credentials? { 16 | if (host == null) { 17 | return null 18 | } 19 | 20 | val accountName: String = sshKeyFile ?: host 21 | var credentials = accountToCredentials[accountName] 22 | if (credentials != null) { 23 | return credentials 24 | } 25 | 26 | val data = OSXKeychainLibrary.findGenericPassword(getServiceName(sshKeyFile), accountName) ?: return null 27 | if (sshKeyFile == null) { 28 | val separatorIndex = data.indexOf('@') 29 | if (separatorIndex > 0) { 30 | val username = PasswordUtil.decodePassword(data.substring(0, separatorIndex)) 31 | val password = PasswordUtil.decodePassword(data.substring(separatorIndex + 1)) 32 | credentials = Credentials(username, password) 33 | } 34 | else { 35 | return null 36 | } 37 | } 38 | else { 39 | credentials = Credentials(sshKeyFile, data) 40 | } 41 | 42 | accountToCredentials[accountName] = credentials 43 | return credentials 44 | } 45 | 46 | private fun getServiceName(sshKeyFile: String?) = if (sshKeyFile == null) serviceName else SSH 47 | 48 | /** 49 | * Note - in case of SSH, our added password will not be used until ssh-agent will not be restarted (simply execute "killall ssh-agent"). 50 | * Also, if you remove password from keychain, ssh-agent will continue to use cached password. 51 | */ 52 | override fun save(host: String?, credentials: Credentials, sshKeyFile: String?) { 53 | val accountName: String = sshKeyFile ?: host!! 54 | var oldCredentials = accountToCredentials.put(accountName, credentials) 55 | if (credentials.equals(oldCredentials)) { 56 | return 57 | } 58 | 59 | val data = if (sshKeyFile == null) "${PasswordUtil.encodePassword(credentials.id)}@${PasswordUtil.encodePassword(credentials.token)}" else credentials.token!! 60 | OSXKeychainLibrary.saveGenericPassword(getServiceName(sshKeyFile), accountName, data) 61 | } 62 | 63 | override fun reset(host: String) { 64 | if (accountToCredentials.remove(host) != null) { 65 | OSXKeychainLibrary.deleteGenericPassword(serviceName, host) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/org/jetbrains/settingsRepository/CommitToIcsDialog.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository; 2 | 3 | import com.intellij.openapi.components.RoamingType; 4 | import com.intellij.openapi.components.TrackingPathMacroSubstitutor; 5 | import com.intellij.openapi.components.impl.stores.StateStorageManager; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.openapi.project.ex.ProjectEx; 8 | import com.intellij.openapi.ui.DialogWrapper; 9 | import com.intellij.openapi.vcs.changes.Change; 10 | import com.intellij.openapi.vcs.changes.ChangeList; 11 | import com.intellij.openapi.vcs.changes.ui.ChangesBrowser; 12 | import com.intellij.openapi.vfs.VirtualFile; 13 | import com.intellij.util.SmartList; 14 | import org.jetbrains.annotations.Nullable; 15 | 16 | import javax.swing.*; 17 | import java.util.Collections; 18 | import java.util.List; 19 | 20 | public class CommitToIcsDialog extends DialogWrapper { 21 | private final ChangesBrowser browser; 22 | private final Project project; 23 | private final String projectId; 24 | 25 | public CommitToIcsDialog(Project project, String projectId, List projectFileChanges) { 26 | super(project, true); 27 | 28 | this.project = project; 29 | this.projectId = projectId; 30 | 31 | browser = new ChangesBrowser(project, Collections.emptyList(), projectFileChanges, null, true, false, null, ChangesBrowser.MyUseCase.LOCAL_CHANGES, null); 32 | browser.setChangesToDisplay(projectFileChanges); 33 | 34 | setTitle(IcsBundle.message("action.CommitToIcs.text")); 35 | setOKButtonText(IcsBundle.message("action.CommitToIcs.text")); 36 | init(); 37 | } 38 | 39 | @Override 40 | protected void doOKAction() { 41 | List selectedChanges = browser.getSelectedChanges(); 42 | if (!selectedChanges.isEmpty()) { 43 | commitChanges(selectedChanges); 44 | } 45 | 46 | super.doOKAction(); 47 | } 48 | 49 | private void commitChanges(List changes) { 50 | StateStorageManager storageManager = ((ProjectEx)project).getStateStore().getStateStorageManager(); 51 | TrackingPathMacroSubstitutor macroSubstitutor = storageManager.getMacroSubstitutor(); 52 | assert macroSubstitutor != null; 53 | IcsManager icsManager = SettingsRepositoryPackage.getIcsManager(); 54 | 55 | SmartList addToIcs = new SmartList(); 56 | for (Change change : changes) { 57 | VirtualFile file = change.getVirtualFile(); 58 | assert file != null; 59 | String fileSpec = macroSubstitutor.collapsePath(file.getPath()); 60 | String repoPath = SettingsRepositoryPackage.buildPath(fileSpec, RoamingType.PER_USER, projectId); 61 | addToIcs.add(repoPath); 62 | if (!icsManager.getRepositoryManager().has(repoPath)) { 63 | // new, revert local 64 | // todo 65 | } 66 | } 67 | icsManager.getRepositoryManager().commit(addToIcs); 68 | } 69 | 70 | @Nullable 71 | @Override 72 | protected JComponent createCenterPanel() { 73 | return browser; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/org/jetbrains/settingsRepository/IcsSettingsPanel.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | -------------------------------------------------------------------------------- /src/org/jetbrains/settingsRepository/IcsSettingsPanel.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository; 2 | 3 | import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.ui.TextBrowseFolderListener; 6 | import com.intellij.openapi.ui.TextFieldWithBrowseButton; 7 | import com.intellij.openapi.util.text.StringUtil; 8 | import com.intellij.ui.DocumentAdapter; 9 | import kotlin.Unit; 10 | import kotlin.jvm.functions.Function0; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import javax.swing.*; 15 | import javax.swing.event.DocumentEvent; 16 | import java.awt.*; 17 | 18 | public class IcsSettingsPanel { 19 | JPanel panel; 20 | TextFieldWithBrowseButton urlTextField; 21 | private final Action[] syncActions; 22 | 23 | public IcsSettingsPanel(@Nullable final Project project, @NotNull Container dialogParent, @NotNull Function0 okAction) { 24 | urlTextField.setText(SettingsRepositoryPackage.getIcsManager().getRepositoryManager().getUpstream()); 25 | urlTextField.addBrowseFolderListener(new TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFolderDescriptor())); 26 | 27 | syncActions = SettingsRepositoryPackage.createMergeActions(project, urlTextField, dialogParent, okAction); 28 | 29 | urlTextField.getTextField().getDocument().addDocumentListener(new DocumentAdapter() { 30 | @Override 31 | protected void textChanged(DocumentEvent e) { 32 | SettingsRepositoryPackage.updateSyncButtonState(StringUtil.nullize(urlTextField.getText()), syncActions); 33 | } 34 | }); 35 | 36 | urlTextField.requestFocusInWindow(); 37 | SettingsRepositoryPackage.updateSyncButtonState(StringUtil.nullize(urlTextField.getText()), syncActions); 38 | } 39 | 40 | @NotNull 41 | Action[] createActions() { 42 | return syncActions; 43 | } 44 | } -------------------------------------------------------------------------------- /src/org/jetbrains/settingsRepository/RepositoryAuthenticationForm.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/org/jetbrains/settingsRepository/RepositoryAuthenticationForm.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository; 2 | 3 | import com.intellij.openapi.ui.DialogWrapper; 4 | import com.intellij.openapi.util.text.StringUtil; 5 | import com.intellij.openapi.vcs.changes.issueLinks.LinkMouseListenerBase; 6 | import com.intellij.ui.DocumentAdapter; 7 | import com.intellij.ui.JBColor; 8 | import com.intellij.ui.SimpleColoredComponent; 9 | import com.intellij.ui.SimpleTextAttributes; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | import javax.swing.*; 14 | import javax.swing.border.EmptyBorder; 15 | import javax.swing.event.DocumentEvent; 16 | import java.util.regex.Matcher; 17 | import java.util.regex.Pattern; 18 | 19 | final class RepositoryAuthenticationForm extends DialogWrapper { 20 | private static final Pattern HREF_PATTERN = Pattern.compile("([^<]*)"); 21 | 22 | private static final SimpleTextAttributes LINK_TEXT_ATTRIBUTES = new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, JBColor.blue); 23 | private static final SimpleTextAttributes SMALL_TEXT_ATTRIBUTES = new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, null); 24 | 25 | private JTextField tokenField; 26 | private SimpleColoredComponent noteComponent; 27 | private JPasswordField passwordField; 28 | 29 | private JPanel panel; 30 | private JLabel tokenLabel; 31 | private JLabel messageLabel; 32 | 33 | private final JComponent initialFocusedComponent; 34 | 35 | public RepositoryAuthenticationForm(@NotNull String message, @Nullable String token, @Nullable String password, @Nullable String note, boolean onlyPassword) { 36 | super(false); 37 | 38 | setTitle("Settings Repository"); 39 | setResizable(false); 40 | 41 | messageLabel.setText(message); 42 | messageLabel.setBorder(new EmptyBorder(0, 0, 10, 0)); 43 | 44 | if (onlyPassword) { 45 | tokenLabel.setVisible(false); 46 | tokenField.setVisible(false); 47 | 48 | passwordField.getDocument().addDocumentListener(new DocumentAdapter() { 49 | @Override 50 | protected void textChanged(DocumentEvent e) { 51 | setOKActionEnabled(e.getDocument().getLength() != 0); 52 | } 53 | }); 54 | initialFocusedComponent = passwordField; 55 | setOKActionEnabled(false); 56 | } 57 | else { 58 | tokenField.setText(token); 59 | passwordField.setText(password); 60 | initialFocusedComponent = StringUtil.isEmpty(token) ? tokenField : passwordField; 61 | } 62 | 63 | if (note == null) { 64 | noteComponent.setVisible(false); 65 | } 66 | else { 67 | Matcher matcher = HREF_PATTERN.matcher(note); 68 | int prev = 0; 69 | if (matcher.find()) { 70 | do { 71 | if (matcher.start() != prev) { 72 | noteComponent.append(note.substring(prev, matcher.start()), SMALL_TEXT_ATTRIBUTES); 73 | } 74 | noteComponent.append(matcher.group(2), LINK_TEXT_ATTRIBUTES, new SimpleColoredComponent.BrowserLauncherTag(matcher.group(1))); 75 | prev = matcher.end(); 76 | } 77 | while (matcher.find()); 78 | 79 | LinkMouseListenerBase.installSingleTagOn(noteComponent); 80 | } 81 | 82 | if (prev < note.length()) { 83 | noteComponent.append(note.substring(prev), SMALL_TEXT_ATTRIBUTES); 84 | } 85 | } 86 | 87 | init(); 88 | } 89 | 90 | @Nullable 91 | @Override 92 | public JComponent getPreferredFocusedComponent() { 93 | return initialFocusedComponent; 94 | } 95 | 96 | @Override 97 | protected JComponent createCenterPanel() { 98 | return panel; 99 | } 100 | 101 | @Nullable 102 | public String getUsername() { 103 | return StringUtil.nullize(tokenField.getText(), true); 104 | } 105 | 106 | @Nullable 107 | public char[] getPassword() { 108 | char[] chars = passwordField.getPassword(); 109 | return chars == null || chars.length == 0 ? null : chars; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/settings/IcsSettings.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore 4 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 5 | import com.fasterxml.jackson.annotation.JsonInclude 6 | import com.fasterxml.jackson.core.JsonGenerator 7 | import com.fasterxml.jackson.core.util.DefaultPrettyPrinter 8 | import com.fasterxml.jackson.databind.ObjectMapper 9 | import com.fasterxml.jackson.databind.ObjectWriter 10 | import com.intellij.openapi.util.io.FileUtil 11 | import com.intellij.util.PathUtilRt 12 | import com.intellij.util.SmartList 13 | import com.intellij.util.Time 14 | import java.io.File 15 | 16 | private val settingsFile = File(getPluginSystemDir(), "config.json") 17 | private val DEFAULT_COMMIT_DELAY = 10 * Time.MINUTE 18 | 19 | class MyPrettyPrinter : DefaultPrettyPrinter() { 20 | init { 21 | _arrayIndenter = DefaultPrettyPrinter.NopIndenter.instance 22 | } 23 | 24 | override fun createInstance() = MyPrettyPrinter() 25 | 26 | override fun writeObjectFieldValueSeparator(jg: JsonGenerator) { 27 | jg.writeRaw(": ") 28 | } 29 | 30 | override fun writeEndObject(jg: JsonGenerator, nrOfEntries: Int) { 31 | if (!_objectIndenter.isInline()) { 32 | --_nesting 33 | } 34 | if (nrOfEntries > 0) { 35 | _objectIndenter.writeIndentation(jg, _nesting) 36 | } 37 | jg.writeRaw('}') 38 | } 39 | 40 | override fun writeEndArray(jg: JsonGenerator, nrOfValues: Int) { 41 | if (!_arrayIndenter.isInline()) { 42 | --_nesting 43 | } 44 | jg.writeRaw(']') 45 | } 46 | } 47 | 48 | fun saveSettings(settings: IcsSettings) { 49 | val serialized = ObjectMapper().writer(MyPrettyPrinter()).writeValueAsBytes(settings) 50 | if (serialized.size() <= 2) { 51 | FileUtil.delete(settingsFile) 52 | } 53 | else { 54 | FileUtil.writeToFile(settingsFile, serialized) 55 | } 56 | } 57 | 58 | fun loadSettings(): IcsSettings { 59 | if (!settingsFile.exists()) { 60 | return IcsSettings() 61 | } 62 | 63 | val settings = ObjectMapper().readValue(settingsFile, javaClass()) 64 | if (settings.commitDelay <= 0) { 65 | settings.commitDelay = DEFAULT_COMMIT_DELAY 66 | } 67 | return settings 68 | } 69 | 70 | JsonInclude(value = JsonInclude.Include.NON_DEFAULT) 71 | class IcsSettings { 72 | var shareProjectWorkspace = false 73 | var commitDelay = DEFAULT_COMMIT_DELAY 74 | var doNoAskMapProject = false 75 | var readOnlySources: List = SmartList() 76 | } 77 | 78 | JsonInclude(value = JsonInclude.Include.NON_DEFAULT) 79 | JsonIgnoreProperties(ignoreUnknown = true) 80 | class ReadonlySource(var url: String? = null, var active: Boolean = true) { 81 | JsonIgnore 82 | val path: String? 83 | get() { 84 | if (url == null) { 85 | return null 86 | } 87 | else { 88 | var fileName = PathUtilRt.getFileName(url!!) 89 | val suffix = ".git" 90 | if (fileName.endsWith(suffix)) { 91 | fileName = fileName.substring(0, fileName.length() - suffix.length()) 92 | } 93 | // the convention is that the .git extension should be used for bare repositories 94 | return "${FileUtil.sanitizeName(fileName)}.${Integer.toHexString(url!!.hashCode())}.git" 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/settings/readOnlySourcesEditor.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory 4 | import com.intellij.openapi.progress.ProgressIndicator 5 | import com.intellij.openapi.progress.ProgressManager 6 | import com.intellij.openapi.progress.Task 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.ui.DialogBuilder 9 | import com.intellij.openapi.ui.TextBrowseFolderListener 10 | import com.intellij.openapi.ui.TextFieldWithBrowseButton 11 | import com.intellij.openapi.util.io.FileUtil 12 | import com.intellij.openapi.util.text.StringUtil 13 | import com.intellij.ui.DocumentAdapter 14 | import com.intellij.util.Function 15 | import com.intellij.util.containers.ContainerUtil 16 | import com.intellij.util.ui.FormBuilder 17 | import com.intellij.util.ui.table.TableModelEditor 18 | import gnu.trove.THashSet 19 | import org.jetbrains.settingsRepository.git.asProgressMonitor 20 | import org.jetbrains.settingsRepository.git.cloneBare 21 | import java.awt.Component 22 | import java.io.File 23 | import javax.swing.JTextField 24 | import javax.swing.event.DocumentEvent 25 | 26 | private val COLUMNS = arrayOf(object : TableModelEditor.EditableColumnInfo() { 27 | override fun getColumnClass() = javaClass() 28 | 29 | override fun valueOf(item: ReadonlySource) = item.active 30 | 31 | override fun setValue(item: ReadonlySource, value: Boolean) { 32 | item.active = value 33 | } 34 | }, 35 | object : TableModelEditor.EditableColumnInfo() { 36 | override fun valueOf(item: ReadonlySource) = item.url 37 | 38 | override fun setValue(item: ReadonlySource, value: String) { 39 | item.url = value 40 | } 41 | }) 42 | 43 | private fun createReadOnlySourcesEditor(dialogParent: Component, project: Project?): Configurable { 44 | val itemEditor = object : TableModelEditor.DialogItemEditor() { 45 | override fun clone(item: ReadonlySource, forInPlaceEditing: Boolean) = ReadonlySource(item.url, item.active) 46 | 47 | override fun getItemClass() = javaClass() 48 | 49 | override fun edit(item: ReadonlySource, mutator: Function, isAdd: Boolean) { 50 | val dialogBuilder = DialogBuilder(dialogParent) 51 | val urlField = TextFieldWithBrowseButton(JTextField(20)) 52 | urlField.addBrowseFolderListener(TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFolderDescriptor())) 53 | urlField.getTextField().getDocument().addDocumentListener(object : DocumentAdapter() { 54 | override fun textChanged(event: DocumentEvent) { 55 | val url = StringUtil.nullize(urlField.getText()) 56 | val enabled: Boolean 57 | try { 58 | enabled = url != null && url.length() > 1 && icsManager.repositoryService.checkUrl(url, null) 59 | } 60 | catch (e: Exception) { 61 | enabled = false 62 | } 63 | 64 | dialogBuilder.setOkActionEnabled(enabled) 65 | } 66 | }) 67 | 68 | dialogBuilder.title("Add read-only source").resizable(false).centerPanel(FormBuilder.createFormBuilder().addLabeledComponent("URL:", urlField).getPanel()).setPreferredFocusComponent(urlField) 69 | if (dialogBuilder.showAndGet()) { 70 | mutator.`fun`(item).url = urlField.getText() 71 | } 72 | } 73 | 74 | override fun applyEdited(oldItem: ReadonlySource, newItem: ReadonlySource) { 75 | newItem.url = oldItem.url 76 | } 77 | 78 | override fun isUseDialogToAdd() = true 79 | } 80 | 81 | val editor = TableModelEditor(COLUMNS, itemEditor, "No sources configured") 82 | editor.reset(icsManager.settings.readOnlySources) 83 | return object : Configurable { 84 | override fun isModified() = editor.isModified() 85 | 86 | override fun apply() { 87 | val oldList = icsManager.settings.readOnlySources 88 | val toDelete = THashSet(oldList.size()) 89 | for (oldSource in oldList) { 90 | ContainerUtil.addIfNotNull(toDelete, oldSource.path) 91 | } 92 | 93 | val toCheckout = THashSet() 94 | 95 | val newList = editor.apply() 96 | for (newSource in newList) { 97 | val path = newSource.path 98 | if (path != null && !toDelete.remove(path)) { 99 | toCheckout.add(newSource) 100 | } 101 | } 102 | 103 | if (toDelete.isEmpty() && toCheckout.isEmpty()) { 104 | return 105 | } 106 | 107 | ProgressManager.getInstance().run(object : Task.Modal(project, IcsBundle.message("task.sync.title"), true) { 108 | override fun run(indicator: ProgressIndicator) { 109 | indicator.setIndeterminate(true) 110 | 111 | val root = getPluginSystemDir() 112 | 113 | if (toDelete.isNotEmpty()) { 114 | indicator.setText("Deleting old repositories") 115 | for (path in toDelete) { 116 | indicator.checkCanceled() 117 | try { 118 | indicator.setText2(path) 119 | FileUtil.delete(File(root, path)) 120 | } 121 | catch (e: Exception) { 122 | LOG.error(e) 123 | } 124 | } 125 | } 126 | 127 | if (toCheckout.isNotEmpty()) { 128 | for (source in toCheckout) { 129 | indicator.checkCanceled() 130 | try { 131 | indicator.setText("Cloning ${StringUtil.trimMiddle(source.url!!, 255)}") 132 | cloneBare(source.url!!, File(root, source.path!!), credentialsStore, indicator.asProgressMonitor()).close() 133 | } 134 | catch (e: Exception) { 135 | LOG.error(e) 136 | } 137 | } 138 | } 139 | 140 | icsManager.readOnlySourcesManager.setSources(newList) 141 | } 142 | }) 143 | } 144 | 145 | override fun reset() { 146 | editor.reset(icsManager.settings.readOnlySources) 147 | } 148 | 149 | override fun getComponent() = editor.createComponent() 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/settings/upstreamEditor.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.notification.NotificationType 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.ui.Messages 7 | import com.intellij.openapi.ui.TextFieldWithBrowseButton 8 | import com.intellij.openapi.util.SystemInfo 9 | import com.intellij.openapi.util.text.StringUtil 10 | import com.intellij.util.ArrayUtil 11 | import org.jetbrains.settingsRepository.actions.NOTIFICATION_GROUP 12 | import java.awt.Container 13 | import java.awt.event.ActionEvent 14 | import javax.swing.AbstractAction 15 | import javax.swing.Action 16 | 17 | fun updateSyncButtonState(url: String?, syncActions: Array) { 18 | val enabled: Boolean 19 | try { 20 | enabled = url != null && url.length() > 1 && icsManager.repositoryService.checkUrl(url, null); 21 | } 22 | catch (e: Exception) { 23 | enabled = false; 24 | } 25 | 26 | for (syncAction in syncActions) { 27 | syncAction.setEnabled(enabled); 28 | } 29 | } 30 | 31 | fun createMergeActions(project: Project?, urlTextField: TextFieldWithBrowseButton, dialogParent: Container, okAction: (() -> Unit)): Array { 32 | var syncTypes = SyncType.values() 33 | if (SystemInfo.isMac) { 34 | syncTypes = ArrayUtil.reverseArray(syncTypes) 35 | } 36 | 37 | val icsManager = icsManager 38 | 39 | return Array(3) { 40 | val syncType = syncTypes[it] 41 | object : AbstractAction(IcsBundle.message("action." + (if (syncType == SyncType.MERGE) "Merge" else (if (syncType == SyncType.OVERWRITE_LOCAL) "ResetToTheirs" else "ResetToMy")) + "Settings.text")) { 42 | fun saveRemoteRepositoryUrl(): Boolean { 43 | val url = StringUtil.nullize(urlTextField.getText()) 44 | if (url != null && !icsManager.repositoryService.checkUrl(url, dialogParent)) { 45 | return false 46 | } 47 | 48 | val repositoryManager = icsManager.repositoryManager 49 | repositoryManager.createRepositoryIfNeed() 50 | repositoryManager.setUpstream(url, null) 51 | return true 52 | } 53 | 54 | override fun actionPerformed(event: ActionEvent) { 55 | val repositoryWillBeCreated = !icsManager.repositoryManager.isRepositoryExists() 56 | var upstreamSet = false 57 | try { 58 | if (!saveRemoteRepositoryUrl()) { 59 | if (repositoryWillBeCreated) { 60 | // remove created repository 61 | icsManager.repositoryManager.deleteRepository() 62 | } 63 | return 64 | } 65 | upstreamSet = true 66 | 67 | if (repositoryWillBeCreated && syncType != SyncType.OVERWRITE_LOCAL) { 68 | ApplicationManager.getApplication().saveSettings() 69 | 70 | icsManager.sync(syncType, project, { copyLocalConfig() }) 71 | } 72 | else { 73 | icsManager.sync(syncType, project, null) 74 | } 75 | } 76 | catch (e: Throwable) { 77 | if (repositoryWillBeCreated) { 78 | // remove created repository 79 | icsManager.repositoryManager.deleteRepository() 80 | } 81 | 82 | LOG.warn(e) 83 | 84 | if (!upstreamSet || e is NoRemoteRepositoryException) { 85 | Messages.showErrorDialog(dialogParent, IcsBundle.message("set.upstream.failed.message", e.getMessage()), IcsBundle.message("set.upstream.failed.title")) 86 | } 87 | else { 88 | Messages.showErrorDialog(dialogParent, StringUtil.notNullize(e.getMessage(), "Internal error"), IcsBundle.message(if (e is AuthenticationException) "sync.not.authorized.title" else "sync.rejected.title")) 89 | } 90 | return 91 | } 92 | 93 | 94 | NOTIFICATION_GROUP.createNotification(IcsBundle.message("sync.done.message"), NotificationType.INFORMATION).notify(project) 95 | okAction() 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/util.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository 2 | 3 | import com.intellij.openapi.application.PathManager 4 | import com.intellij.openapi.util.io.FileUtil 5 | import com.intellij.openapi.util.io.FileUtilRt 6 | import com.intellij.openapi.util.text.StringUtil 7 | import java.io.File 8 | import java.nio.ByteBuffer 9 | 10 | public fun String?.nullize(): String? = StringUtil.nullize(this) 11 | 12 | public fun byteBufferToBytes(byteBuffer: ByteBuffer): ByteArray { 13 | if (byteBuffer.hasArray() && byteBuffer.arrayOffset() == 0) { 14 | val bytes = byteBuffer.array() 15 | if (bytes.size() == byteBuffer.limit()) { 16 | return bytes 17 | } 18 | } 19 | 20 | val bytes = ByteArray(byteBuffer.limit()) 21 | byteBuffer.get(bytes) 22 | return bytes 23 | } 24 | 25 | private fun getPathToBundledFile(filename: String): String { 26 | var pluginsDirectory: String 27 | var folder = "/settings-repository" 28 | if ("jar" == javaClass().getResource("")!!.getProtocol()) { 29 | // running from build 30 | pluginsDirectory = PathManager.getPluginsPath() 31 | if (!File("$pluginsDirectory$folder").exists()) { 32 | pluginsDirectory = PathManager.getHomePath() 33 | folder = "/plugins$folder" 34 | } 35 | } 36 | else { 37 | // running from sources 38 | pluginsDirectory = PathManager.getHomePath() 39 | } 40 | return FileUtilRt.toSystemDependentName("$pluginsDirectory$folder/lib/$filename") 41 | } 42 | 43 | fun getPluginSystemDir(): File { 44 | val customPath = System.getProperty("ics.settingsRepository") 45 | if (customPath == null) { 46 | return File(PathManager.getConfigPath(), "settingsRepository") 47 | } 48 | else { 49 | return File(FileUtil.expandUserHome(customPath)) 50 | } 51 | } -------------------------------------------------------------------------------- /testData/local.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /testData/local2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /testData/remote.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /testSrc/BareGitTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.test 2 | 3 | import com.intellij.openapi.util.io.FileUtil 4 | import gnu.trove.THashMap 5 | import org.hamcrest.CoreMatchers.equalTo 6 | import org.hamcrest.CoreMatchers.nullValue 7 | import org.jetbrains.jgit.dirCache.AddFile 8 | import org.jetbrains.jgit.dirCache.edit 9 | import org.jetbrains.settingsRepository.git.cloneBare 10 | import org.jetbrains.settingsRepository.git.commit 11 | import org.jetbrains.settingsRepository.git.processChildren 12 | import org.jetbrains.settingsRepository.git.read 13 | import org.junit.Assert.assertThat 14 | import org.junit.Test 15 | import java.io.File 16 | 17 | class BareGitTest : TestCase() { 18 | public Test fun `remote doesn't have commits`() { 19 | val repository = cloneBare(createRepository().getWorkTree().getAbsolutePath(), tempDirManager.newDirectory()) 20 | assertThat(repository.read("\$ROOT_CONFIG$/keymaps/Mac OS X from RubyMine.xml"), nullValue()) 21 | } 22 | 23 | public Test fun bare() { 24 | val remoteRepository = createRepository() 25 | val workTree: File = remoteRepository.getWorkTree() 26 | val filePath = "\$ROOT_CONFIG$/keymaps/Mac OS X from RubyMine.xml" 27 | val file = File(testDataPath, "remote.xml") 28 | FileUtil.copy(file, File(workTree, filePath)) 29 | remoteRepository.edit(AddFile(filePath)) 30 | remoteRepository.commit("") 31 | 32 | val repository = cloneBare(remoteRepository.getWorkTree().getAbsolutePath(), tempDirManager.newDirectory()) 33 | assertThat(FileUtil.loadTextAndClose(repository.read(filePath)!!), equalTo(FileUtil.loadFile(file))) 34 | } 35 | 36 | public Test fun processChildren() { 37 | val remoteRepository = createRepository() 38 | 39 | val workTree: File = remoteRepository.getWorkTree() 40 | val filePath = "\$ROOT_CONFIG$/keymaps/Mac OS X from RubyMine.xml" 41 | val file = File(testDataPath, "remote.xml") 42 | FileUtil.copy(file, File(workTree, filePath)) 43 | remoteRepository.edit(AddFile(filePath)) 44 | remoteRepository.commit("") 45 | 46 | val repository = cloneBare(remoteRepository.getWorkTree().getAbsolutePath(), tempDirManager.newDirectory()) 47 | 48 | val data = THashMap() 49 | repository.processChildren("\$ROOT_CONFIG$/keymaps") {name, input -> 50 | data.put(name, FileUtil.loadTextAndClose(input)) 51 | true 52 | } 53 | 54 | assertThat(data.size(), equalTo(1)) 55 | assertThat(data.get("Mac OS X from RubyMine.xml"), equalTo(FileUtil.loadFile(file))) 56 | } 57 | } -------------------------------------------------------------------------------- /testSrc/CredentialsTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.test 2 | 3 | import com.intellij.openapi.util.NotNullLazyValue 4 | import com.intellij.openapi.util.io.FileUtil 5 | import com.intellij.testFramework.UsefulTestCase 6 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder 7 | import org.eclipse.jgit.transport.CredentialItem 8 | import org.eclipse.jgit.transport.URIish 9 | import org.hamcrest.CoreMatchers.equalTo 10 | import org.hamcrest.CoreMatchers.not 11 | import org.hamcrest.CoreMatchers.nullValue 12 | import org.hamcrest.text.IsEmptyString.isEmptyString 13 | import org.jetbrains.keychain.CredentialsStore 14 | import org.jetbrains.keychain.FileCredentialsStore 15 | import org.jetbrains.settingsRepository.git.JGitCredentialsProvider 16 | import org.junit.After 17 | import org.junit.Assert.assertThat 18 | import org.junit.Test 19 | import java.io.File 20 | 21 | class CredentialsTest { 22 | private var storeFile: File? = null 23 | 24 | private fun createProvider(credentialsStore: CredentialsStore): JGitCredentialsProvider { 25 | return JGitCredentialsProvider(NotNullLazyValue.createConstantValue(credentialsStore), FileRepositoryBuilder().setBare().setGitDir(File("/tmp/fake")).build()) 26 | } 27 | 28 | private fun createFileStore(): FileCredentialsStore { 29 | storeFile = FileUtil.generateRandomTemporaryPath() 30 | return FileCredentialsStore(storeFile!!) 31 | } 32 | 33 | public After fun tearDown() { 34 | storeFile?.delete() 35 | storeFile = null 36 | } 37 | 38 | public Test fun explicitSpecifiedInURL() { 39 | val credentialsStore = createFileStore() 40 | val username = CredentialItem.Username() 41 | val password = CredentialItem.Password() 42 | val uri = URIish("https://develar:bike@github.com/develar/settings-repository.git") 43 | assertThat(createProvider(credentialsStore).get(uri, username, password), equalTo(true)) 44 | assertThat(username.getValue(), equalTo("develar")) 45 | assertThat(String(password.getValue()!!), equalTo("bike")) 46 | // ensure that credentials store was not used 47 | assertThat(credentialsStore.get(uri.getHost()), nullValue()) 48 | assertThat(storeFile?.exists(), equalTo(false)) 49 | } 50 | 51 | public Test fun gitCredentialHelper() { 52 | // we don't yet setup test environment for this test and use host environment 53 | if (UsefulTestCase.IS_UNDER_TEAMCITY) { 54 | return 55 | } 56 | 57 | val credentialsStore = createFileStore() 58 | val username = CredentialItem.Username() 59 | val password = CredentialItem.Password() 60 | val uri = URIish("https://develar@bitbucket.org/develar/test-ics.git") 61 | assertThat(createProvider(credentialsStore).get(uri, username, password), equalTo(true)) 62 | assertThat(username.getValue(), equalTo("develar")) 63 | assertThat(String(password.getValue()!!), not(isEmptyString())) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /testSrc/GitTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.test 2 | 3 | import com.intellij.mock.MockVirtualFileSystem 4 | import com.intellij.openapi.components.RoamingType 5 | import com.intellij.openapi.progress.EmptyProgressIndicator 6 | import com.intellij.openapi.util.io.FileUtil 7 | import com.intellij.openapi.vfs.CharsetToolkit 8 | import com.intellij.openapi.vfs.VirtualFile 9 | import com.intellij.util.PathUtilRt 10 | import org.hamcrest.CoreMatchers.equalTo 11 | import org.hamcrest.Matchers.contains 12 | import org.hamcrest.Matchers.empty 13 | import org.jetbrains.jgit.dirCache.deletePath 14 | import org.jetbrains.jgit.dirCache.writePath 15 | import org.jetbrains.settingsRepository.AM 16 | import org.jetbrains.settingsRepository.SyncType 17 | import org.jetbrains.settingsRepository.git.commit 18 | import org.jetbrains.settingsRepository.git.computeIndexDiff 19 | import org.jetbrains.settingsRepository.git.resetHard 20 | import org.jetbrains.settingsRepository.icsManager 21 | import org.junit.Assert.assertThat 22 | import org.junit.Test 23 | import java.io.File 24 | import javax.swing.SwingUtilities 25 | 26 | class GitTest : TestCase() { 27 | public Test fun add() { 28 | val data = FileUtil.loadFileBytes(File(testDataPath, "remote.xml")) 29 | val addedFile = "\$APP_CONFIG$/remote.xml" 30 | save(addedFile, data) 31 | 32 | val diff = repository.computeIndexDiff() 33 | assertThat(diff.diff(), equalTo(true)) 34 | assertThat(diff.getAdded(), contains(equalTo(addedFile))) 35 | assertThat(diff.getChanged(), empty()) 36 | assertThat(diff.getRemoved(), empty()) 37 | assertThat(diff.getModified(), empty()) 38 | assertThat(diff.getUntracked(), empty()) 39 | assertThat(diff.getUntrackedFolders(), empty()) 40 | } 41 | 42 | public Test fun addSeveral() { 43 | val data = FileUtil.loadFileBytes(File(testDataPath, "remote.xml")) 44 | val data2 = FileUtil.loadFileBytes(File(testDataPath, "local.xml")) 45 | val addedFile = "\$APP_CONFIG$/remote.xml" 46 | val addedFile2 = "\$APP_CONFIG$/local.xml" 47 | save(addedFile, data) 48 | save(addedFile2, data2) 49 | 50 | val diff = repository.computeIndexDiff() 51 | assertThat(diff.diff(), equalTo(true)) 52 | assertThat(diff.getAdded(), contains(equalTo(addedFile), equalTo(addedFile2))) 53 | assertThat(diff.getChanged(), empty()) 54 | assertThat(diff.getRemoved(), empty()) 55 | assertThat(diff.getModified(), empty()) 56 | assertThat(diff.getUntracked(), empty()) 57 | assertThat(diff.getUntrackedFolders(), empty()) 58 | } 59 | 60 | public Test fun delete() { 61 | val data = FileUtil.loadFileBytes(File(testDataPath, "remote.xml")) 62 | delete(data, false) 63 | delete(data, true) 64 | } 65 | 66 | public Test fun setUpstream() { 67 | val url = "https://github.com/user/repo.git" 68 | repositoryManager.setUpstream(url, null) 69 | assertThat(repositoryManager.getUpstream(), equalTo(url)) 70 | } 71 | 72 | Test 73 | public fun pullToRepositoryWithoutCommits() { 74 | doPullToRepositoryWithoutCommits(null) 75 | } 76 | 77 | public Test fun pullToRepositoryWithoutCommitsAndCustomRemoteBranchName() { 78 | doPullToRepositoryWithoutCommits("customRemoteBranchName") 79 | } 80 | 81 | private fun doPullToRepositoryWithoutCommits(remoteBranchName: String?) { 82 | createLocalRepository(remoteBranchName) 83 | repositoryManager.pull(EmptyProgressIndicator()) 84 | compareFiles(repository.getWorkTree(), remoteRepository.getWorkTree()) 85 | } 86 | 87 | public Test fun pullToRepositoryWithCommits() { 88 | doPullToRepositoryWithCommits(null) 89 | } 90 | 91 | public Test fun pullToRepositoryWithCommitsAndCustomRemoteBranchName() { 92 | doPullToRepositoryWithCommits("customRemoteBranchName") 93 | } 94 | 95 | private fun doPullToRepositoryWithCommits(remoteBranchName: String?) { 96 | val file = createLocalRepositoryAndCommit(remoteBranchName) 97 | 98 | val progressIndicator = EmptyProgressIndicator() 99 | repositoryManager.commit(progressIndicator) 100 | repositoryManager.pull(progressIndicator) 101 | assertThat(FileUtil.loadFile(File(repository.getWorkTree(), file.name)), equalTo(String(file.data, CharsetToolkit.UTF8_CHARSET))) 102 | compareFiles(repository.getWorkTree(), remoteRepository.getWorkTree(), null, PathUtilRt.getFileName(file.name)) 103 | } 104 | 105 | private fun createLocalRepository(remoteBranchName: String?) { 106 | createFileRemote(remoteBranchName) 107 | repositoryManager.setUpstream(remoteRepository.getWorkTree().getAbsolutePath(), remoteBranchName) 108 | } 109 | 110 | private fun createLocalRepositoryAndCommit(remoteBranchName: String?): FileInfo { 111 | createLocalRepository(remoteBranchName) 112 | return addAndCommit("\$APP_CONFIG$/local.xml") 113 | } 114 | 115 | private fun compareFiles(fs: MockVirtualFileSystem) { 116 | compareFiles(fs.getRoot()) 117 | } 118 | 119 | private fun compareFiles(expected: VirtualFile?) { 120 | compareFiles(repository.getWorkTree(), remoteRepository.getWorkTree(), expected) 121 | } 122 | 123 | // never was merged. we reset using "merge with strategy "theirs", so, we must test - what's happen if it is not first merge? - see next test 124 | public Test fun resetToTheirsIfFirstMerge() { 125 | createLocalRepositoryAndCommit(null) 126 | sync(SyncType.OVERWRITE_LOCAL) 127 | compareFiles(fs("\$APP_CONFIG$/remote.xml")) 128 | } 129 | 130 | public Test fun resetToTheirsISecondMergeIsNull() { 131 | createLocalRepositoryAndCommit(null) 132 | sync(SyncType.MERGE) 133 | 134 | restoreRemoteAfterPush() 135 | 136 | val fs = MockVirtualFileSystem() 137 | 138 | fun testRemote() { 139 | fs.findFileByPath("\$APP_CONFIG$/local.xml") 140 | fs.findFileByPath("\$APP_CONFIG$/remote.xml") 141 | compareFiles(fs.getRoot()) 142 | } 143 | testRemote() 144 | 145 | addAndCommit("_mac/local2.xml") 146 | sync(SyncType.OVERWRITE_LOCAL) 147 | 148 | compareFiles(fs.getRoot()) 149 | 150 | // test: merge and push to remote after such reset 151 | sync(SyncType.MERGE) 152 | 153 | restoreRemoteAfterPush() 154 | 155 | testRemote() 156 | } 157 | 158 | public Test fun resetToMyIfFirstMerge() { 159 | createLocalRepositoryAndCommit(null) 160 | sync(SyncType.OVERWRITE_REMOTE) 161 | restoreRemoteAfterPush() 162 | compareFiles(fs("\$APP_CONFIG$/local.xml")) 163 | } 164 | 165 | public Test fun `reset to my, second merge is null`() { 166 | createLocalRepositoryAndCommit(null) 167 | sync(SyncType.MERGE) 168 | 169 | restoreRemoteAfterPush() 170 | 171 | val fs = fs("\$APP_CONFIG$/local.xml", "\$APP_CONFIG$/remote.xml") 172 | compareFiles(fs) 173 | 174 | val local2FilePath = "_mac/local2.xml" 175 | addAndCommit(local2FilePath) 176 | sync(SyncType.OVERWRITE_REMOTE) 177 | restoreRemoteAfterPush() 178 | 179 | fs.findFileByPath(local2FilePath) 180 | compareFiles(fs) 181 | 182 | // test: merge to remote after such reset 183 | sync(SyncType.MERGE) 184 | 185 | restoreRemoteAfterPush() 186 | 187 | compareFiles(fs) 188 | } 189 | 190 | public Test fun `merge - resolve conflicts to my`() { 191 | createLocalRepository(null) 192 | 193 | val data = AM.MARKER_ACCEPT_MY 194 | save("\$APP_CONFIG$/remote.xml", data) 195 | 196 | sync(SyncType.MERGE) 197 | 198 | restoreRemoteAfterPush() 199 | compareFiles(fs("\$APP_CONFIG$/remote.xml")) 200 | } 201 | 202 | public Test fun `merge - theirs file deleted, my modified, accept theirs`() { 203 | createLocalRepository(null) 204 | 205 | sync(SyncType.MERGE) 206 | 207 | val data = AM.MARKER_ACCEPT_THEIRS 208 | save("\$APP_CONFIG$/remote.xml", data) 209 | repositoryManager.commit(EmptyProgressIndicator()) 210 | 211 | val remoteRepository = testHelper.repository!! 212 | remoteRepository.deletePath("\$APP_CONFIG$/remote.xml") 213 | remoteRepository.commit("delete remote.xml") 214 | 215 | sync(SyncType.MERGE) 216 | 217 | compareFiles(fs()) 218 | } 219 | 220 | public Test fun `merge - my file deleted, theirs modified, accept my`() { 221 | createLocalRepository(null) 222 | 223 | sync(SyncType.MERGE) 224 | 225 | getProvider().delete("\$APP_CONFIG$/remote.xml", RoamingType.PER_USER) 226 | repositoryManager.commit(EmptyProgressIndicator()) 227 | 228 | val remoteRepository = testHelper.repository!! 229 | remoteRepository.writePath("\$APP_CONFIG$/remote.xml", AM.MARKER_ACCEPT_THEIRS) 230 | remoteRepository.commit("") 231 | 232 | sync(SyncType.MERGE) 233 | restoreRemoteAfterPush() 234 | 235 | compareFiles(fs()) 236 | } 237 | 238 | // remote is uninitialized (empty - initial commit is not done) 239 | public Test fun `merge with uninitialized upstream`() { 240 | doSyncWithUninitializedUpstream(SyncType.MERGE) 241 | } 242 | 243 | public Test fun `reset to my, uninitialized upstream`() { 244 | doSyncWithUninitializedUpstream(SyncType.OVERWRITE_REMOTE) 245 | } 246 | 247 | public Test fun `reset to theirs, uninitialized upstream`() { 248 | doSyncWithUninitializedUpstream(SyncType.OVERWRITE_LOCAL) 249 | } 250 | 251 | private fun doSyncWithUninitializedUpstream(syncType: SyncType) { 252 | createFileRemote(null, false) 253 | repositoryManager.setUpstream(remoteRepository.getWorkTree().getAbsolutePath(), null) 254 | 255 | val path = "\$APP_CONFIG$/local.xml" 256 | val data = FileUtil.loadFileBytes(File(testDataPath, PathUtilRt.getFileName(path))) 257 | save(path, data) 258 | 259 | sync(syncType) 260 | 261 | val fs = MockVirtualFileSystem() 262 | if (syncType != SyncType.OVERWRITE_LOCAL) { 263 | fs.findFileByPath(path) 264 | } 265 | restoreRemoteAfterPush(); 266 | compareFiles(fs) 267 | } 268 | 269 | private fun restoreRemoteAfterPush() { 270 | /** we must not push to non-bare repository - but we do it in test (our sync merge equals to "pull&push"), 271 | " 272 | By default, updating the current branch in a non-bare repository 273 | is denied, because it will make the index and work tree inconsistent 274 | with what you pushed, and will require 'git reset --hard' to match the work tree to HEAD. 275 | " 276 | so, we do "git reset --hard" 277 | */ 278 | testHelper.repository!!.resetHard() 279 | } 280 | 281 | private fun sync(syncType: SyncType) { 282 | SwingUtilities.invokeAndWait { 283 | icsManager.sync(syncType, fixture!!.getProject()) 284 | } 285 | } 286 | } -------------------------------------------------------------------------------- /testSrc/LoadTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.test 2 | 3 | import com.intellij.openapi.components.RoamingType 4 | import com.intellij.openapi.options.SchemeManagerImpl 5 | import com.intellij.options.TestScheme 6 | import com.intellij.options.TestSchemesProcessor 7 | import com.intellij.options.serialize 8 | import com.intellij.options.toByteArray 9 | import org.eclipse.jgit.lib.Repository 10 | import org.hamcrest.CoreMatchers.equalTo 11 | import org.jetbrains.settingsRepository.ReadonlySource 12 | import org.jetbrains.settingsRepository.getPluginSystemDir 13 | import org.jetbrains.settingsRepository.git.cloneBare 14 | import org.jetbrains.settingsRepository.git.commit 15 | import org.jetbrains.settingsRepository.icsManager 16 | import org.junit.Assert.assertThat 17 | import org.junit.Test 18 | import java.io.File 19 | 20 | class LoadTest : TestCase() { 21 | private fun createSchemesManager(dirPath: String): SchemeManagerImpl { 22 | return SchemeManagerImpl(dirPath, TestSchemesProcessor(), RoamingType.PER_USER, getProvider(), tempDirManager.newDirectory()) 23 | } 24 | 25 | public Test fun `load scheme`() { 26 | val localScheme = TestScheme("local") 27 | val data = localScheme.serialize().toByteArray() 28 | val dirPath = "\$ROOT_CONFIG$/keymaps" 29 | save("$dirPath/local.xml", data) 30 | 31 | val schemesManager = createSchemesManager(dirPath) 32 | schemesManager.loadSchemes() 33 | assertThat(schemesManager.getAllSchemes(), equalTo(listOf(localScheme))) 34 | } 35 | 36 | public Test fun `load scheme with the same names`() { 37 | val localScheme = TestScheme("local") 38 | val data = localScheme.serialize().toByteArray() 39 | val dirPath = "\$ROOT_CONFIG$/keymaps" 40 | save("$dirPath/local.xml", data) 41 | save("$dirPath/local2.xml", data) 42 | 43 | val schemesManager = createSchemesManager(dirPath) 44 | schemesManager.loadSchemes() 45 | assertThat(schemesManager.getAllSchemes(), equalTo(listOf(localScheme))) 46 | } 47 | 48 | public Test fun `load scheme from repo and read-only repo`() { 49 | val localScheme = TestScheme("local") 50 | 51 | val dirPath = "\$ROOT_CONFIG$/keymaps" 52 | save("$dirPath/local.xml", localScheme.serialize().toByteArray()) 53 | 54 | val remoteScheme = TestScheme("remote") 55 | val remoteRepository = createRepository() 56 | remoteRepository 57 | .add(remoteScheme.serialize().toByteArray(), "$dirPath/Mac OS X from RubyMine.xml") 58 | .commit("") 59 | 60 | remoteRepository.useAsReadOnlySource { 61 | val schemesManager = createSchemesManager(dirPath) 62 | schemesManager.loadSchemes() 63 | assertThat(schemesManager.getAllSchemes(), equalTo(listOf(remoteScheme, localScheme))) 64 | assertThat(schemesManager.isMetadataEditable(localScheme), equalTo(true)) 65 | assertThat(schemesManager.isMetadataEditable(remoteScheme), equalTo(false)) 66 | } 67 | } 68 | 69 | public Test fun `scheme overrides read-only`() { 70 | val schemeName = "Emacs" 71 | val localScheme = TestScheme(schemeName, "local") 72 | 73 | val dirPath = "\$ROOT_CONFIG$/keymaps" 74 | save("$dirPath/$schemeName.xml", localScheme.serialize().toByteArray()) 75 | 76 | val remoteScheme = TestScheme(schemeName, "remote") 77 | val remoteRepository = createRepository() 78 | remoteRepository 79 | .add(remoteScheme.serialize().toByteArray(), "$dirPath/$schemeName.xml") 80 | .commit("") 81 | 82 | remoteRepository.useAsReadOnlySource { 83 | val schemesManager = createSchemesManager(dirPath) 84 | schemesManager.loadSchemes() 85 | assertThat(schemesManager.getAllSchemes(), equalTo(listOf(localScheme))) 86 | assertThat(schemesManager.isMetadataEditable(localScheme), equalTo(false)) 87 | } 88 | } 89 | } 90 | 91 | inline fun Repository.useAsReadOnlySource(runnable: () -> Unit) { 92 | createAndRegisterReadOnlySource() 93 | try { 94 | runnable() 95 | } 96 | finally { 97 | icsManager.readOnlySourcesManager.setSources(emptyList()) 98 | } 99 | } 100 | 101 | fun Repository.createAndRegisterReadOnlySource(): ReadonlySource { 102 | val source = ReadonlySource(getWorkTree().getAbsolutePath()) 103 | assertThat(cloneBare(source.url!!, File(getPluginSystemDir(), source.path!!)).getObjectDatabase().exists(), equalTo(true)) 104 | icsManager.readOnlySourcesManager.setSources(listOf(source)) 105 | return source 106 | } -------------------------------------------------------------------------------- /testSrc/RespositoryHelper.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.test 2 | 3 | import com.intellij.mock.MockVirtualFileSystem 4 | import com.intellij.openapi.util.io.FileUtil 5 | import com.intellij.openapi.vfs.VirtualFile 6 | import com.intellij.util.ArrayUtil 7 | import org.eclipse.jgit.lib.Constants 8 | import org.eclipse.jgit.lib.Repository 9 | import org.hamcrest.CoreMatchers.equalTo 10 | import org.jetbrains.settingsRepository.git.createRepository 11 | import org.junit.Assert.assertThat 12 | import org.junit.rules.TestName 13 | import org.junit.runner.Description 14 | import java.io.File 15 | import java.util.Arrays 16 | import java.util.Comparator 17 | 18 | class RespositoryHelper : TestName() { 19 | var repository: Repository? = null 20 | 21 | public fun getRepository(baseDir: File): Repository { 22 | if (repository == null) { 23 | repository = createRepository(File(baseDir, "upstream")) 24 | } 25 | return repository!! 26 | } 27 | 28 | override fun finished(description: Description) { 29 | super.finished(description) 30 | 31 | if (repository != null) { 32 | FileUtil.delete(repository!!.getWorkTree()) 33 | repository = null 34 | } 35 | } 36 | } 37 | 38 | data class FileInfo(val name: String, val data: ByteArray) 39 | 40 | fun fs(vararg paths: String): MockVirtualFileSystem { 41 | val fs = MockVirtualFileSystem() 42 | for (path in paths) { 43 | fs.findFileByPath(path) 44 | } 45 | return fs 46 | } 47 | 48 | private fun compareFiles(local: File, remote: File, expected: VirtualFile? = null, vararg localExcludes: String) { 49 | var localFiles = local.list()!! 50 | var remoteFiles = remote.list()!! 51 | 52 | localFiles = ArrayUtil.remove(localFiles, Constants.DOT_GIT) 53 | remoteFiles = ArrayUtil.remove(remoteFiles, Constants.DOT_GIT) 54 | 55 | Arrays.sort(localFiles) 56 | Arrays.sort(remoteFiles) 57 | 58 | if (localExcludes.size() != 0) { 59 | for (localExclude in localExcludes) { 60 | localFiles = ArrayUtil.remove(localFiles, localExclude) 61 | } 62 | } 63 | 64 | assertThat(localFiles, equalTo(remoteFiles)) 65 | 66 | val expectedFiles: Array? 67 | if (expected == null) { 68 | expectedFiles = null 69 | } 70 | else { 71 | //noinspection UnsafeVfsRecursion 72 | expectedFiles = expected.getChildren() 73 | Arrays.sort(expectedFiles!!, object : Comparator { 74 | override fun compare(o1: VirtualFile, o2: VirtualFile): Int { 75 | return o1.getName().compareTo(o2.getName()) 76 | } 77 | }) 78 | 79 | for (i in 0..expectedFiles.size() - 1) { 80 | assertThat(localFiles[i], equalTo(expectedFiles[i].getName())) 81 | } 82 | } 83 | 84 | for (i in 0..localFiles.size() - 1) { 85 | val localFile = File(local, localFiles[i]) 86 | val remoteFile = File(remote, remoteFiles[i]) 87 | val expectedFile: VirtualFile? 88 | if (expectedFiles == null) { 89 | expectedFile = null 90 | } 91 | else { 92 | expectedFile = expectedFiles[i] 93 | assertThat(expectedFile.isDirectory(), equalTo(localFile.isDirectory())) 94 | } 95 | 96 | if (localFile.isFile()) { 97 | assertThat(FileUtil.loadFile(localFile), equalTo(FileUtil.loadFile(remoteFile))) 98 | } 99 | else { 100 | compareFiles(localFile, remoteFile, expectedFile, *localExcludes) 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /testSrc/SettingsRepositoryTestSuite.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.test 2 | 3 | import org.junit.runner.RunWith 4 | import org.junit.runners.Suite 5 | 6 | RunWith(Suite::class) 7 | Suite.SuiteClasses(GitTest::class, BareGitTest::class, LoadTest::class) 8 | class SettingsRepositoryTestSuite -------------------------------------------------------------------------------- /testSrc/TestCase.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.settingsRepository.test 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.application.PathManager 5 | import com.intellij.openapi.application.impl.ApplicationImpl 6 | import com.intellij.openapi.components.RoamingType 7 | import com.intellij.openapi.components.impl.stores.StreamProvider 8 | import com.intellij.openapi.diagnostic.Logger 9 | import com.intellij.openapi.progress.EmptyProgressIndicator 10 | import com.intellij.openapi.util.io.FileUtil 11 | import com.intellij.openapi.util.io.FileUtilRt 12 | import com.intellij.testFramework.TemporaryDirectory 13 | import com.intellij.testFramework.TestLoggerFactory 14 | import com.intellij.testFramework.fixtures.IdeaProjectTestFixture 15 | import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory 16 | import com.intellij.util.PathUtilRt 17 | import org.eclipse.jgit.api.Git 18 | import org.eclipse.jgit.lib.Repository 19 | import org.hamcrest.CoreMatchers 20 | import org.hamcrest.Matchers 21 | import org.jetbrains.jgit.dirCache.AddFile 22 | import org.jetbrains.jgit.dirCache.edit 23 | import org.jetbrains.settingsRepository.git 24 | import org.jetbrains.settingsRepository.git.GitRepositoryManager 25 | import org.jetbrains.settingsRepository.git.commit 26 | import org.jetbrains.settingsRepository.git.computeIndexDiff 27 | import org.jetbrains.settingsRepository.icsManager 28 | import org.junit.After 29 | import org.junit.Assert 30 | import org.junit.Before 31 | import org.junit.Rule 32 | import java.io.File 33 | import javax.swing.SwingUtilities 34 | 35 | val testDataPath: String = "${PathManager.getHomePath()}/settings-repository/testData" 36 | 37 | fun getProvider(): StreamProvider { 38 | val provider = (ApplicationManager.getApplication() as ApplicationImpl).getStateStore().getStateStorageManager().getStreamProvider() 39 | Assert.assertThat(provider, CoreMatchers.notNullValue()) 40 | return provider!! 41 | } 42 | 43 | val repositoryManager: GitRepositoryManager 44 | get() = icsManager.repositoryManager as GitRepositoryManager 45 | 46 | val repository: Repository 47 | get() = repositoryManager.repository 48 | 49 | 50 | fun save(path: String, data: ByteArray) { 51 | getProvider().saveContent(path, data, data.size(), RoamingType.PER_USER) 52 | } 53 | 54 | fun addAndCommit(path: String): FileInfo { 55 | val data = FileUtil.loadFileBytes(File(testDataPath, PathUtilRt.getFileName(path))) 56 | save(path, data) 57 | repositoryManager.commit(EmptyProgressIndicator()) 58 | return FileInfo(path, data) 59 | } 60 | 61 | fun Repository.add(data: ByteArray, path: String): Repository { 62 | FileUtil.writeToFile(File(getWorkTree(), path), data) 63 | edit(AddFile(path)) 64 | return this 65 | } 66 | 67 | fun delete(data: ByteArray, directory: Boolean) { 68 | val addedFile = "\$APP_CONFIG$/remote.xml" 69 | save(addedFile, data) 70 | getProvider().delete(if (directory) "\$APP_CONFIG$" else addedFile, RoamingType.PER_USER) 71 | 72 | val diff = repository.computeIndexDiff() 73 | Assert.assertThat(diff.diff(), CoreMatchers.equalTo(false)) 74 | Assert.assertThat(diff.getAdded(), Matchers.empty()) 75 | Assert.assertThat(diff.getChanged(), Matchers.empty()) 76 | Assert.assertThat(diff.getRemoved(), Matchers.empty()) 77 | Assert.assertThat(diff.getModified(), Matchers.empty()) 78 | Assert.assertThat(diff.getUntracked(), Matchers.empty()) 79 | Assert.assertThat(diff.getUntrackedFolders(), Matchers.empty()) 80 | } 81 | 82 | abstract class TestCase { 83 | var fixture: IdeaProjectTestFixture? = null 84 | 85 | val tempDirManager = TemporaryDirectory() 86 | public Rule fun getTemporaryFolder(): TemporaryDirectory = tempDirManager 87 | 88 | val testHelper = RespositoryHelper() 89 | 90 | Rule 91 | public fun getTestWatcher(): RespositoryHelper = testHelper 92 | 93 | val remoteRepository: Repository 94 | get() = testHelper.repository!! 95 | 96 | companion object { 97 | private var ICS_DIR: File? = null 98 | 99 | init { 100 | Logger.setFactory(javaClass()) 101 | } 102 | 103 | // BeforeClass doesn't work in Kotlin 104 | public fun setIcsDir() { 105 | val icsDirPath = System.getProperty("ics.settingsRepository") 106 | if (icsDirPath == null) { 107 | // we must not create file (i.e. this file doesn't exist) 108 | ICS_DIR = FileUtilRt.generateRandomTemporaryPath() 109 | System.setProperty("ics.settingsRepository", ICS_DIR!!.getAbsolutePath()) 110 | } 111 | else { 112 | ICS_DIR = File(FileUtil.expandUserHome(icsDirPath)) 113 | FileUtil.delete(ICS_DIR!!) 114 | } 115 | } 116 | } 117 | 118 | Before 119 | public fun setUp() { 120 | if (ICS_DIR == null) { 121 | setIcsDir() 122 | } 123 | 124 | fixture = IdeaTestFixtureFactory.getFixtureFactory().createLightFixtureBuilder().getFixture() 125 | SwingUtilities.invokeAndWait(object : Runnable { 126 | override fun run() { 127 | fixture!!.setUp() 128 | } 129 | }) 130 | 131 | (icsManager.repositoryManager as GitRepositoryManager).createRepositoryIfNeed() 132 | icsManager.repositoryActive = true 133 | } 134 | 135 | After 136 | public fun tearDown() { 137 | icsManager.repositoryActive = false 138 | try { 139 | if (fixture != null) { 140 | SwingUtilities.invokeAndWait(object : Runnable { 141 | override fun run() { 142 | fixture!!.tearDown() 143 | } 144 | }) 145 | } 146 | } 147 | finally { 148 | repositoryManager.deleteRepository() 149 | } 150 | } 151 | 152 | fun createFileRemote(branchName: String? = null, initialCommit: Boolean = true): File { 153 | val repository = getRemoteRepository(branchName) 154 | 155 | val workTree: File = repository.getWorkTree() 156 | if (initialCommit) { 157 | val addedFile = "\$APP_CONFIG$/remote.xml" 158 | FileUtil.copy(File(testDataPath, "remote.xml"), File(workTree, addedFile)) 159 | repository.edit(AddFile(addedFile)) 160 | repository.commit("") 161 | } 162 | return workTree 163 | } 164 | 165 | fun getRemoteRepository(branchName: String? = null): Repository { 166 | val repository = testHelper.getRepository(ICS_DIR!!) 167 | if (branchName != null) { 168 | // jgit cannot checkout&create branch if no HEAD (no commits in our empty repository), so we create initial empty commit 169 | repository.commit("") 170 | Git(repository).checkout().setCreateBranch(true).setName(branchName).call() 171 | } 172 | return repository 173 | } 174 | 175 | fun createRepository() = git.createRepository(tempDirManager.newDirectory()) 176 | } --------------------------------------------------------------------------------