├── 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 |
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 |
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 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/testData/local2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 | }
--------------------------------------------------------------------------------