summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Documentation/access-control.txt13
-rw-r--r--Documentation/cmd-stream-events.txt4
-rw-r--r--Documentation/config-gerrit.txt60
-rw-r--r--Documentation/config-labels.txt8
-rw-r--r--Documentation/config-themes.txt (renamed from Documentation/config-headerfooter.txt)20
-rw-r--r--Documentation/dev-eclipse.txt15
-rw-r--r--Documentation/dev-release-deploy-config.txt3
-rw-r--r--Documentation/dev-release.txt9
-rw-r--r--Documentation/index.txt2
-rw-r--r--Documentation/install.txt2
-rw-r--r--Documentation/licenses.txt1
-rw-r--r--Documentation/rest-api-accounts.txt202
-rw-r--r--Documentation/rest-api-changes.txt161
-rw-r--r--Documentation/rest-api-projects.txt106
-rw-r--r--Documentation/user-upload.txt34
-rw-r--r--ReleaseNotes/ReleaseNotes-2.7.txt278
-rw-r--r--ReleaseNotes/index.txt5
-rwxr-xr-xcontrib/trivial_rebase.py19
-rw-r--r--gerrit-acceptance-tests/pom.xml2
-rw-r--r--gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java13
-rw-r--r--gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java297
-rw-r--r--gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java179
-rw-r--r--gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java300
-rw-r--r--gerrit-antlr/pom.xml2
-rw-r--r--gerrit-cache-h2/pom.xml2
-rw-r--r--gerrit-common/pom.xml7
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java5
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java10
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java4
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java11
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java16
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java4
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java9
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java10
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/SingleListChangeInfo.java52
-rw-r--r--gerrit-extension-api/pom.xml2
-rw-r--r--gerrit-gwtdebug/pom.xml2
-rw-r--r--gerrit-gwtexpui/.gitignore6
-rw-r--r--gerrit-gwtexpui/.settings/org.eclipse.core.resources.prefs4
-rw-r--r--gerrit-gwtexpui/.settings/org.eclipse.core.runtime.prefs3
-rw-r--r--gerrit-gwtexpui/.settings/org.eclipse.jdt.core.prefs285
-rw-r--r--gerrit-gwtexpui/.settings/org.eclipse.jdt.ui.prefs3
-rw-r--r--gerrit-gwtexpui/COPYING202
-rw-r--r--gerrit-gwtexpui/pom.xml75
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml (renamed from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml)10
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java22
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java25
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java230
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css25
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swfbin0 -> 5380 bytes
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml (renamed from gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml)9
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java130
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml20
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java40
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java51
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java183
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java33
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java94
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java19
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java136
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java37
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties14
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java29
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java228
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java25
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java34
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java29
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java61
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css99
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml19
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java67
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java36
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java160
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java205
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java40
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java93
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml19
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java77
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java23
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java25
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css43
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml19
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java137
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java33
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java56
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java56
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java40
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java96
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java84
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java55
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java302
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java411
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java22
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java22
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java28
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css23
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java106
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java118
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml27
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java76
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java65
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java20
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java86
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java65
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java81
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java57
-rw-r--r--gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java87
-rw-r--r--gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java77
-rw-r--r--gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java28
-rw-r--r--gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java265
-rw-r--r--gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java58
-rw-r--r--gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java119
-rw-r--r--gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java133
-rw-r--r--gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java82
-rw-r--r--gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java93
-rw-r--r--gerrit-gwtui/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-gwtui/pom.xml65
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml9
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java157
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java7
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java23
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java10
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java109
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java84
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java (renamed from gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java)25
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml (renamed from gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml)0
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java3
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties4
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java46
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java38
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java4
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java4
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java8
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties8
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java8
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java4
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java11
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties10
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java62
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java13
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java12
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java17
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java3
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java47
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java39
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.pngbin0 -> 4822 bytes
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.pngbin0 -> 371 bytes
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css27
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java37
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java22
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java55
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java55
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java211
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java83
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java90
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java5
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java26
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java (renamed from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java)37
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java35
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java39
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java23
-rw-r--r--gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java98
-rw-r--r--gerrit-httpd/pom.xml8
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java30
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java1
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java2
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java6
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java3
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java5
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java5
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java14
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java9
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java5
-rw-r--r--gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html2
-rw-r--r--gerrit-launcher/pom.xml2
-rw-r--r--gerrit-main/pom.xml2
-rw-r--r--gerrit-openid/pom.xml2
-rw-r--r--gerrit-patch-commonsnet/pom.xml2
-rw-r--r--gerrit-patch-jgit/pom.xml2
-rw-r--r--gerrit-pgm/pom.xml2
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java1
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java1
-rw-r--r--gerrit-plugin-api/pom.xml4
-rw-r--r--gerrit-plugin-archetype/pom.xml2
-rw-r--r--gerrit-plugin-gwt-archetype/pom.xml2
-rw-r--r--gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css2
-rw-r--r--gerrit-plugin-gwtui/pom.xml2
-rw-r--r--gerrit-plugin-js-archetype/pom.xml2
-rw-r--r--gerrit-prettify/pom.xml7
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java2
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java (renamed from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java)2
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.properties (renamed from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties)0
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java (renamed from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFactory.java)2
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java (renamed from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java)3
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java (renamed from gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseHtmlFile.java)2
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java1
-rw-r--r--gerrit-reviewdb/pom.xml2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java34
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java10
-rw-r--r--gerrit-server/pom.xml2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java43
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java54
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java37
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java34
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java41
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java27
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java47
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java14
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java78
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java5
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java43
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java7
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java3
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java130
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java5
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java3
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java19
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java45
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java71
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java73
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java55
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java51
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java64
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java38
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java3
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java36
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java3
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java40
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java46
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java5
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java15
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java49
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java11
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java11
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java12
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/AccountAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/ApprovalAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/MessageAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/PatchAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetCommentAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java)4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/RefUpdateAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitLabelAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitRecordAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/data/TrackingIdAttribute.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java5
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java12
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java3
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java9
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java80
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java303
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java112
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java5
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java92
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java53
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java94
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java96
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java29
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java72
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java8
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java126
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java224
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java176
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java26
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_79.java26
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java2
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java233
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java224
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java8
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java50
-rw-r--r--gerrit-sshd/pom.xml2
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java52
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java68
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java48
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java2
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java49
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java7
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java3
-rw-r--r--gerrit-util-cli/pom.xml2
-rw-r--r--gerrit-util-ssl/pom.xml2
-rw-r--r--gerrit-war/pom.xml2
m---------plugins/commit-message-length-validator0
m---------plugins/replication0
m---------plugins/reviewnotes0
-rw-r--r--pom.xml31
-rw-r--r--tools/gwtui_dbg.launch2
-rwxr-xr-xtools/release.sh2
315 files changed, 11833 insertions, 1574 deletions
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 100e47312f..ab32d78a77 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -775,6 +775,10 @@ In order to submit, all labels (such as `Verified` and `Code-Review`,
above) must enable submit, and also must not block it. See above for
details on each label.
+To link:user-upload.html#auto_merge[immediately submit a change on push]
+the caller needs to have the Submit permission on `refs/for/<ref>`
+(e.g. on `refs/for/refs/heads/master`).
+
[[category_view_drafts]]
View Drafts
@@ -1312,6 +1316,15 @@ Allow access to execute `replication start` command, if the
replication plugin is installed on the server.
+[[capability_streamEvents]]
+Stream Events
+~~~~~~~~~~~~~
+
+Allow performing streaming of Gerrit events. This capability
+allows the granted group to
+link:cmd-stream-events.html[stream Gerrit events via ssh].
+
+
[[capability_viewCaches]]
View Caches
~~~~~~~~~~~
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index ce23da67f0..6da0ef0242 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -23,7 +23,9 @@ Event output is in JSON, one event per line.
ACCESS
------
-Any user who has configured an SSH key.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_streamEvents[the 'Stream Events' global capability].
SCRIPTING
---------
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 27a0937f63..daafcf167b 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -744,6 +744,11 @@ how the replacement is displayed to the user.
html = $1<a href=\"http://trak.example.com/$2\">$2</a>
----
+Comment links can also be specified in `project.config` and sections in
+children override those in parents. The only restriction is that to
+avoid injecting arbitrary user-supplied HTML in the page, comment links
+defined in `project.config` may only supply `link`, not `html`.
+
[[commentlink.name.match]]commentlink.<name>.match::
+
A JavaScript regular expression to match positions to be replaced
@@ -779,6 +784,21 @@ match expression may be accessed as `$'n'`.
The configuration file eats double quotes, so escaping them as
`\"` is necessary to protect them from the parser.
+[[commentlink.name.enabled]]commentlink.<name>.enabled::
++
+Whether the comment link is enabled. A child project may override a
+section in a parent or the site-wide config that is disabled by
+specifying `enabled = true`.
++
+Disabling sections in `gerrit.config` can be used by site administrators
+to create a library of comment links with `html` set that are not
+user-supplied and thus can be verified to be XSS-free, but are only
+enabled for a subset of projects.
++
+Note that the names and contents of disabled sections are visible even
+to anonymous users via the
+link:rest-api-projects.html#get-config[REST API].
+
[[contactstore]]Section contactstore
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1322,6 +1342,16 @@ using the property 'gitweb.pathSeparator'.
+
Valid values are the characters '*', '(' and ')'.
+[[gitweb.linkDrafts]]gitweb.linkDrafts::
++
+Whether or not Gerrit should provide links to gitweb on draft patch sets.
++
+By default, Gerrit will show links to gitweb on all patch sets. If gitweb
+only allows publicly viewable references, set this to false to remove
+the links to draft patch sets from the change review screen.
++
+Valid values are "true" and "false," default is "true."
+
[[groups]]Section groups
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2439,6 +2469,34 @@ Supported MACs: hmac-md5, hmac-md5-96, hmac-sha1, hmac-sha1-96.
+
By default, all supported MACs are available.
+[[sshd.kerberosKeytab]]sshd.kerberosKeytab::
++
+Enable kerberos authentication for SSH connections. To permit
+kerberos authentication, the server must have a host principal
+(see `sshd.kerberosPrincipal`) which is acquired from a keytab.
+This must be provisioned by the kerberos administrators, and is
+typically installed into `/etc/krb5.keytab` on host machines.
++
+The keytab must contain at least one `host/` principal, typically
+using the host's canonical name. If it does not use the
+canonical name, the `sshd.kerberosPrincipal` should be configured
+with the correct name.
++
+By default, not set and so kerberos authentication is not enabled.
+
+[[sshd.kerberosPrincipal]]sshd.kerberosPrincipal::
++
+If kerberos authentication is enabled with `sshd.kerberosKeytab`,
+instead use the given principal name instead of the default.
+If the principal does not begin with `host/` a warning message is
+printed and may prevent successful authentication.
++
+This may be useful if the host is behind an IP load balancer or
+other SSH forwarding systems, since the principal name is constructed
+by the client and must match for kerberos authentication to work.
++
+By default, `host/canonical.host.name`
+
[[suggest]] Section suggest
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2734,7 +2792,7 @@ Files in this directory provide additional configuration.
+
Other files support site customization.
+
-* link:config-headerfooter.html[Site Header/Footer]
+* link:config-themes.html[Themes]
GERRIT
------
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 2fd7a95194..1ff7e24e6e 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -221,6 +221,14 @@ determining whether a change is submittable.
If true, the lowest possible negative value for the label is copied
forward when a new patch set is uploaded.
+[[label_copyMaxScore]]
+`label.Label-Name.copyMaxScore`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If true, the highest possible positive value for the label is copied
+forward when a new patch set is uploaded. This can be used to enable
+sticky approvals, reducing turn-around for trivial cleanups prior to
+submitting a change.
[[label_canOverride]]
`label.Label-Name.canOverride`
diff --git a/Documentation/config-headerfooter.txt b/Documentation/config-themes.txt
index ae5d8f7eb5..c102381860 100644
--- a/Documentation/config-headerfooter.txt
+++ b/Documentation/config-themes.txt
@@ -1,29 +1,39 @@
-Gerrit Code Review - Site Customization
-=======================================
+Gerrit Code Review - Themes
+===========================
Gerrit supports some customization of the HTML it sends to
the browser, allowing organizations to alter the look and
feel of the application to fit with their general scheme.
+Configuration can either be sitewide or per-project. Projects without a
+specified theme inherit from their parents, or from the sitewide theme
+for `All-Projects`.
+
+Sitewide themes are stored in `'$site_path'/etc`, and per-project
+themes are stored in `'$site_path'/themes/{project-name}`. Files are
+only served from a single theme directory; if you want to modify or
+extend an inherited theme, you must copy it into the appropriate
+per-project directory.
+
HTML Header/Footer
------------------
At startup Gerrit reads the following files (if they exist) and
uses them to customize the HTML page it sends to clients:
-* `'$site_path'/etc/GerritSiteHeader.html`
+* `<theme-dir>/GerritSiteHeader.html`
+
HTML is inserted below the menu bar, but above any page content.
This is a good location for an organizational logo, or links to
other systems like bug tracking.
-* `'$site_path'/etc/GerritSiteFooter.html`
+* `<theme-dir>/GerritSiteFooter.html`
+
HTML is inserted at the bottom of the page, below all other content,
but just above the footer rule and the "Powered by Gerrit Code
Review (v....)" message shown at the extreme bottom.
-* `'$site_path'/etc/GerritSite.css`
+* `<theme-dir>/GerritSite.css`
+
The CSS rules are inlined into the top of the HTML page, inside
of a `<style>` tag. These rules can be used to support styling
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 64e5935689..019c78f0d7 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -79,10 +79,9 @@ Duplicate the existing `pgm_daemon` launch configuration:
Running Hosted Mode
~~~~~~~~~~~~~~~~~~~
-To debug the GWT code executing in the web browser, three additional Git
+To debug the GWT code executing in the web browser, two additional Git
repositories need to be cloned.
-* https://gerrit.googlesource.com/gwtexpui
* https://gerrit.googlesource.com/gwtjsonrpc
* https://gerrit.googlesource.com/gwtorm
@@ -121,6 +120,18 @@ the link:config-gerrit.html#auth.type[auth.type] configuration parameter
to `DEVELOPMENT_BECOME_ANY_ACCOUNT` to disable OpenID and allow you to
impersonate whatever account you otherwise would've used.
+* Gerrit site doesn't appear, only directory listing is shown. Web toolkit
+developer browser plugin is missing. If there is no warning, that browser
+plugin is missing with the suggestion to install it, you can install the
+right extension for your browser from the following locations:
++
+https://dl.google.com/dl/gwt/plugins/chrome/gwt-dev-plugin.crx[Chrome]
++
+link:https://dl.google.com/dl/gwt/plugins/firefox/gwt-dev-plugin.xpi[Firefox]
++
+link:http://dl.google.com/dl/gwt/plugins/ie/1.0.7263.20091208111100/gwt-dev-plugin.msi[IE]
++
+https://dl.google.com/dl/gwt/plugins/safari/gwt-dev-plugin.dmg[Safari]
GERRIT
------
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index bc52d5009b..ffdb6ea775 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -14,8 +14,7 @@ Jar.
* `gerrit-maven`:
+
-Bucket to store Gerrit Subproject Artifacts (e.g. `gwtexpui`,
-`gwtjsonrpc` etc.).
+Bucket to store Gerrit Subproject Artifacts (e.g. `gwtjsonrpc` etc.).
* `gerrit-plugins`:
+
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index ae2aed689e..cd7cd34ea7 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -111,7 +111,6 @@ Release Subprojects
The subprojects to be released are:
-* `gwtexpui`
* `gwtjsonrpc`
* `gwtorm`
* `prolog-cafe`
@@ -139,16 +138,8 @@ Build Gerrit
* Build the Gerrit WAR
+
====
- rm -f ~/.m2/settings.xml
./tools/release.sh
====
-+
-[WARNING]
-========================================================================
-Make sure you are compiling the release for all browsers. Check in your
-Maven `~/.m2/settings.xml` file that no Maven profile is active that
-limits the compilation to a certain browser.
-========================================================================
* Sanity check WAR
* Test the new Gerrit version
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 9b2d8ebbaa..88b50fac54 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -48,7 +48,7 @@ Configuration
* link:config-gerrit.html[System Settings]
* link:config-contact.html[User Contact Information]
* link:config-gitweb.html[Gitweb Integration]
-* link:config-headerfooter.html[Site Header/Footer]
+* link:config-themes.html[Themes]
* link:config-sso.html[Single Sign-On Systems]
* link:config-reverseproxy.html[Reverse Proxy]
* link:config-hooks.html[Hooks]
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 4b14ff0bba..a18a506d29 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -143,7 +143,7 @@ For more information, see the related topics in this manual:
* link:config-reverseproxy.html[Reverse Proxy]
* link:config-sso.html[Single Sign-On Systems]
-* link:config-headerfooter.html[Site Header/Footer]
+* link:config-themes.html[Themes]
* link:config-gitweb.html[Gitweb Integration]
* link:config-gerrit.html[Other System Settings]
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 69a12c3e12..fbdd9cd33f 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -12,7 +12,6 @@ Included Components
|======================================================================
|Included Package | License
|Gerrit Code Review | <<apache2,Apache License 2.0>>
-|gwtexpui | <<apache2,Apache License 2.0>>
|gwtjsonrpc | <<apache2,Apache License 2.0>>
|gwtorm | <<apache2,Apache License 2.0>>
|Google Gson | <<apache2,Apache License 2.0>>
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 2ccd39a465..f17a741b58 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -257,6 +257,115 @@ The response redirects to the URL of the avatar image.
Location: https://profiles/avatar/john_doe.jpeg?s=20x20
----
+[[get-avatar-change-url]]
+Get Avatar Change URL
+~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/avatar.change.url'
+
+Retrieves the URL where the user can change the avatar image.
+
+.Request
+----
+ GET /a/accounts/self/avatar.change.url HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: text/plain;charset=UTF-8
+
+ https://profiles/pictures/john.doe
+----
+
+[[get-diff-preferences]]
+Get Diff Preferences
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/preferences.diff'
+
+Retrieves the diff preferences of a user.
+
+.Request
+----
+ GET /a/accounts/self/preferences.diff HTTP/1.0
+----
+
+As result the diff preferences of the user are returned as a
+link:#diff-preferences-info[DiffPreferencesInfo] entity.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "context": 10,
+ "ignore_whitespace": "IGNORE_ALL_SPACE",
+ "intraline_difference": true,
+ "line_length": 100,
+ "show_tabs": true,
+ "show_whitespace_errors": true,
+ "syntax_highlighting": true,
+ "tab_size": 8
+ }
+----
+
+[[set-diff-preferences]]
+Set Diff Preferences
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/preferences.diff'
+
+Sets the diff preferences of a user.
+
+The new diff preferences must be provided in the request body as a
+link:#diff-preferences-input[DiffPreferencesInput] entity.
+
+.Request
+----
+ GET /a/accounts/self/preferences.diff HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "context": 10,
+ "ignore_whitespace": "IGNORE_ALL_SPACE",
+ "intraline_difference": true,
+ "line_length": 100,
+ "show_line_endings": true,
+ "show_tabs": true,
+ "show_whitespace_errors": true,
+ "syntax_highlighting": true,
+ "tab_size": 8
+ }
+----
+
+As result the new diff preferences of the user are returned as a
+link:#diff-preferences-info[DiffPreferencesInfo] entity.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "context": 10,
+ "ignore_whitespace": "IGNORE_ALL_SPACE",
+ "intraline_difference": true,
+ "line_length": 100,
+ "show_line_endings": true,
+ "show_tabs": true,
+ "show_whitespace_errors": true,
+ "syntax_highlighting": true,
+ "tab_size": 8
+ }
+----
+
[[ids]]
IDs
@@ -349,6 +458,99 @@ link:access-control.html#capability_startReplication[Start Replication]
capability.
|=================================
+[[diff-preferences-info]]
+DiffPreferencesInfo
+~~~~~~~~~~~~~~~~~~~
+The `DiffPreferencesInfo` entity contains information about the diff
+preferences of a user.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=====================================
+|Field Name ||Description
+|`context` ||
+The number of lines of context when viewing a patch.
+|`expand_all_comments` |not set if `false`|
+Whether all inline comments should be automatically expanded.
+|`ignore_whitespace` ||
+Whether whitespace changes should be ignored and if yes, which
+whitespace changes should be ignored. +
+Allowed values are `IGNORE_NONE`, `IGNORE_SPACE_AT_EOL`,
+`IGNORE_SPACE_CHANGE`, `IGNORE_ALL_SPACE`.
+|`intraline_difference` |not set if `false`|
+Whether intraline differences should be highlighted.
+|`line_length` ||
+Number of characters that should be displayed in one line.
+|`manual_review` |not set if `false`|
+Whether the 'Reviewed' flag should not be set automatically on a patch
+when it is viewed.
+|`retain_header` |not set if `false`|
+Whether the header that is displayed above the patch (that either shows
+the commit message, the diff preferences, the patch sets or the files)
+should be retained on file switch.
+|`show_line_endings` |not set if `false`|
+Whether Windows EOL/Cr-Lf should be displayed as '\r' in a dotted-line
+box.
+|`show_tabs` |not set if `false`|
+Whether tabs should be shown.
+|`show_whitespace_errors`|not set if `false`|
+Whether whitespace errors should be shown.
+|`skip_deleted` |not set if `false`|
+Whether deleted files should be skipped on file switch.
+|`skip_uncommented` |not set if `false`|
+Whether uncommented files should be skipped on file switch.
+|`syntax_highlighting` |not set if `false`|
+Whether syntax highlighting should be enabled.
+|`tab_size` ||
+Number of spaces that should be used to display one tab.
+|=====================================
+
+[[diff-preferences-input]]
+DiffPreferencesInput
+~~~~~~~~~~~~~~~~~~~~
+The `DiffPreferencesInput` entity contains information for setting the
+diff preferences of a user. Fields which are not set will not be
+updated.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=====================================
+|Field Name ||Description
+|`context` |optional|
+The number of lines of context when viewing a patch.
+|`expand_all_comments` |optional|
+Whether all inline comments should be automatically expanded.
+|`ignore_whitespace` |optional|
+Whether whitespace changes should be ignored and if yes, which
+whitespace changes should be ignored. +
+Allowed values are `IGNORE_NONE`, `IGNORE_SPACE_AT_EOL`,
+`IGNORE_SPACE_CHANGE`, `IGNORE_ALL_SPACE`.
+|`intraline_difference` |optional|
+Whether intraline differences should be highlighted.
+|`line_length` |optional|
+Number of characters that should be displayed in one line.
+|`manual_review` |optional|
+Whether the 'Reviewed' flag should not be set automatically on a patch
+when it is viewed.
+|`retain_header` |optional|
+Whether the header that is displayed above the patch (that either shows
+the commit message, the diff preferences, the patch sets or the files)
+should be retained on file switch.
+|`show_line_endings` |optional|
+Whether Windows EOL/Cr-Lf should be displayed as '\r' in a dotted-line
+box.
+|`show_tabs` |optional|
+Whether tabs should be shown.
+|`show_whitespace_errors`|optional|
+Whether whitespace errors should be shown.
+|`skip_deleted` |optional|
+Whether deleted files should be skipped on file switch.
+|`skip_uncommented` |optional|
+Whether uncommented files should be skipped on file switch.
+|`syntax_highlighting` |optional|
+Whether syntax highlighting should be enabled.
+|`tab_size` |optional|
+Number of spaces that should be used to display one tab.
+|=====================================
+
[[query-limit-info]]
QueryLimitInfo
~~~~~~~~~~~~~~
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 696a1746d5..09c7e115e0 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -208,6 +208,11 @@ default. Optional fields are:
referencing accounts.
--
+[[messages]]
+--
+* `MESSAGES`: include messages associated with the change.
+--
+
.Request
----
GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES HTTP/1.0
@@ -356,7 +361,8 @@ Get Change Detail
'GET /changes/link:#change-id[\{change-id\}]/detail'
Retrieves a change with link:#labels[labels], link:#detailed-labels[
-detailed labels] and link:#detailed-accounts[detailed accounts].
+detailed labels], link:#detailed-accounts[detailed accounts], and
+link:#messages[messages].
.Request
----
@@ -473,6 +479,30 @@ describes the change.
"name": "Jane Roe",
"email": "jane.roe@example.com"
}
+ ],
+ "messages": [
+ {
+ "id": "YH-egE",
+ "author": {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
+ },
+ "updated": "2013-03-23 21:34:02.419000000",
+ "message": "Patch Set 1:\n\nThis is the first message.",
+ "revision_number": 1
+ },
+ {
+ "id": "WEEdhU",
+ "author": {
+ "_account_id": 1000097,
+ "name": "Jane Roe",
+ "email": "jane.roe@example.com"
+ },
+ "updated": "2013-03-23 21:36:52.332000000",
+ "message": "Patch Set 1:\n\nThis is the second message.\n\nWith a line break.",
+ "revision_number": 1
+ }
]
}
----
@@ -1518,6 +1548,99 @@ Deletes a draft comment from a revision.
HTTP/1.1 204 No Content
----
+[[list-comments]]
+List Comments
+~~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/'
+
+Lists the published comments of a revision.
+
+As result a map is returned that maps the file path to a list of
+link:#comment-info[CommentInfo] entries. The entries in the map are
+sorted by file path.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/comments/ HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+ {
+ "kind": "gerritcodereview#comment",
+ "id": "TvcXrmjM",
+ "line": 23,
+ "message": "[nit] trailing whitespace",
+ "updated": "2013-02-26 15:40:43.986000000",
+ "author": {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
+ }
+ },
+ {
+ "kind": "gerritcodereview#comment",
+ "id": "TveXwFiA",
+ "line": 49,
+ "in_reply_to": "TfYX-Iuo",
+ "message": "Done",
+ "updated": "2013-02-26 15:40:45.328000000",
+ "author": {
+ "_account_id": 1000097,
+ "name": "Jane Roe",
+ "email": "jane.roe@example.com"
+ }
+ }
+ ]
+ }
+----
+
+[[get-comment]]
+Get Comment
+~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/link:#comment-id[\{comment-id\}]'
+
+Retrieves a published comment of a revision.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/comments/TvcXrmjM HTTP/1.0
+----
+
+As response a link:#comment-info[CommentInfo] entity is returned that
+describes the published comment.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#comment",
+ "id": "TvcXrmjM",
+ "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "line": 23,
+ "message": "[nit] trailing whitespace",
+ "updated": "2013-02-26 15:40:43.986000000",
+ "author": {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
+ }
+ }
+----
+
[[set-reviewed]]
Set Reviewed
~~~~~~~~~~~~
@@ -1582,6 +1705,11 @@ This can be:
("I8473b95934b5732ac55d26311a706c9c2bde9940")
* a legacy numeric change ID ("4247")
+[[comment-id]]
+\{comment-id\}
+~~~~~~~~~~~~~~
+UUID of a published comment.
+
[[draft-id]]
\{draft-id\}
~~~~~~~~~~~~
@@ -1717,6 +1845,10 @@ Only set if link:#detailed-labels[detailed labels] are requested.
The reviewers that can be removed by the calling user as a list of
link:rest-api-accounts.html#account-info[AccountInfo] entities. +
Only set if link:#detailed-labels[detailed labels] are requested.
+|`messages`|optional|
+Messages associated with the change as a list of
+link:#change-message-info[ChangeMessageInfo] entities. +
+Only set if link:#messages[messages] are requested.
|`current_revision` |optional|
The commit ID of the current patch set of this change. +
Only set if link:#current-revision[the current revision] is requested
@@ -1730,6 +1862,27 @@ Whether the query would deliver more results if not limited. +
Only set on either the last or the first change that is returned.
|==================================
+[[change-message-info]]
+ChangeMessageInfo
+~~~~~~~~~~~~~~~~~
+The `ChangeMessageInfo` entity contains information about a message
+attached to a change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name ||Description
+|`id` ||The ID of the message.
+|`author` |optional|
+Author of the message as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity. +
+Unset if written by the Gerrit system.
+|`date` ||
+The link:rest-api.html#timestamp[timestamp] this message was posted.
+|`message` ||The text left by the user.
+|`_revision_number` |optional|
+Which patchset (if any) generated this message.
+|==================================
+
[[comment-info]]
CommentInfo
~~~~~~~~~~~
@@ -1739,7 +1892,7 @@ The `CommentInfo` entity contains information about an inline comment.
|===========================
|Field Name ||Description
|`kind` ||`gerritcodereview#comment`
-|`id` ||The URL encoded UUID of the draft comment.
+|`id` ||The URL encoded UUID of the comment.
|`path` |optional|
The path of the file for which the inline comment was done. +
Not set if returned in a map where the key is the file path.
@@ -1756,6 +1909,10 @@ The URL encoded UUID of the comment to which this comment is a reply.
|`updated` ||
The link:rest-api.html#timestamp[timestamp] of when this comment was
written.
+|`author` |optional|
+The author of the message as an +
+link:rest-api-accounts.html#account-info[AccountInfo] entity. +
+Unset for draft comments, assumed to be the calling user.
|===========================
[[comment-input]]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 9aba0e9133..35caac4114 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -408,6 +408,58 @@ link:#repository-statistics-info[RepositoryStatisticsInfo] entity.
}
----
+[[get-config]]
+Get Config
+~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/config'
+
+Gets some configuration information about a project. Note that this
+config info is not simply the contents of `project.config`; it generally
+contains fields that may have been inherited from parent projects.
+
+.Request
+----
+ GET /projects/myproject/config
+----
+
+A link:#config-info[ConfigInfo] entity is returned that describes the
+project configuration. Some fields are only visible to users that have
+read access to `refs/meta/config`.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#project_config",
+ "use_contributor_agreements": {
+ "value": true,
+ "configured_value": "TRUE",
+ "inherited_value": false
+ },
+ "use_content_merge": {
+ "value": true,
+ "configured_value": "INHERIT",
+ "inherited_value": true
+ },
+ "use_signed_off_by": {
+ "value": false,
+ "configured_value": "INHERIT",
+ "inherited_value": false
+ },
+ "require_change_id": {
+ "value": false,
+ "configured_value": "FALSE",
+ "inherited_value": true
+ }
+ "commentlinks": {}
+ }
+----
+
[[run-gc]]
Run GC
~~~~~~
@@ -693,6 +745,43 @@ The name of the project.
JSON Entities
-------------
+[[config-info]]
+ConfigInfo
+~~~~~~~~~~
+The `ConfigInfo` entity contains information about the effective project
+configuration.
+
+Fields marked with * are only visible to users who have read access to
+`refs/meta/config`.
+
+[options="header",width="50%",cols="1,6"]
+|======================================
+|Field Name |Description
+|`use_contributor_agreements*`|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+authors must complete a contributor agreement on the site before
+pushing any commits or changes to this project.
+|`use_content_merge*`|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+Gerrit will try to perform a 3-way merge of text file content when a
+file has been modified by both the destination branch and the change
+being submitted. This option only takes effect if submit type is not
+FAST_FORWARD_ONLY.
+|`use_signed_off_by*`|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+each change must contain a Signed-off-by line from either the author or
+the uploader in the commit message.
+|`require_change_id*`|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether a
+valid link:user-changeid.html[Change-Id] footer in any commit uploaded
+for review is required. This does not apply to commits pushed directly
+to a branch or tag.
+|`commentlinks`|
+Comment link configuration for the project. Has the same format as the
+link:config-gerrit.html#_a_id_commentlink_a_section_commentlink[commentlink section]
+of `gerrit.config`.
+|======================================
+
[[dashboard-info]]
DashboardInfo
~~~~~~~~~~~~~
@@ -775,6 +864,23 @@ The ref to which `HEAD` should be set, the `refs/heads` prefix can be
omitted.
|============================
+[[inherited-boolean-info]]
+InheritedBooleanInfo
+~~~~~~~~~~~~~~~~~~~~
+A boolean value that can also be inherited.
+
+[options="header",width="50%",cols="1,^2,4"]
+|================================
+|Field Name ||Description
+|`value` ||
+The effective boolean value.
+|`configured_value` ||
+The configured value, can be `TRUE`, `FALSE` or `INHERITED`.
+|`inherited_value` |optional|
+The boolean value inherited from the parent. +
+Not set if there is no parent.
+|================================
+
[[project-description-input]]
ProjectDescriptionInput
~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 55ab89521a..cbb152c374 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -333,6 +333,40 @@ grant nothing at all. This ensures that accidental pushes don't
make undesired changes to the public repository.
+[[auto_merge]]
+Auto-Merge during Push
+~~~~~~~~~~~~~~~~~~~~~~
+
+Changes can be directly submitted on push. This is primarily useful
+for teams that don't want to do code review but want to use Gerrit's
+submit strategies to handle contention on busy branches. Using
+`%submit` creates a change and submits it immediately, if the caller
+has link:access-control.html#category_submit[Submit] permission on
+`refs/for/<ref>` (e.g. on `refs/for/refs/heads/master`).
+
+====
+ git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%submit
+====
+
+On auto-merge of a change neither labels nor submit rules are checked.
+If the merge fails the change stays open, but when pushing a new patch
+set the merge can be reattempted by using `%submit` again.
+
+
+[[base]]
+Selecting Merge Base
+~~~~~~~~~~~~~~~~~~~~
+
+By default new changes are opened only for new unique commits
+that have never before been seen by the Gerrit server. Clients
+may override that behavior and force new changes to be created
+by setting the merge base SHA-1 using the '%base' argument:
+
+====
+ git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%base=$(git rev-parse origin/master)
+====
+
+
repo upload
-----------
diff --git a/ReleaseNotes/ReleaseNotes-2.7.txt b/ReleaseNotes/ReleaseNotes-2.7.txt
new file mode 100644
index 0000000000..dae075d3a2
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.7.txt
@@ -0,0 +1,278 @@
+Release notes for Gerrit 2.7
+============================
+
+
+Gerrit 2.7 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.7.war[
+http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.7.war]
+
+Gerrit 2.7 includes the bug fixes done with
+link:ReleaseNotes-2.6.1.html[Gerrit 2.6.1] and
+link:ReleaseNotes-2.6.2.html[Gerrit 2.6.2]. These bug fixes are *not*
+listed in these release notes.
+
+Schema Change
+-------------
+
+
+*WARNING:* This release contains schema changes. To upgrade:
+----
+ java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.7.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.7.x. If you are upgrading from 2.2.x.x or
+newer, you may ignore this warning and upgrade directly to 2.7.x.
+
+
+
+Release Highlights
+------------------
+
+
+* New `copyMaxScore` setting for labels.
+* Comment links configurable per project.
+* Themes configurable per project.
+* Better support for binary files and images in diff screens.
+* User avatars in more places.
+* Several new REST APIs.
+
+
+New Features
+------------
+
+
+General
+~~~~~~~
+
+* New `copyMaxScore` setting for labels.
++
+Labels can be link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-labels.html#label_copyMaxScore[
+configured] to copy approvals forward to the next patch set.
+
+* Comment links can be link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-gerrit.html#commentlink[
+defined per project in the project configuration].
+
+* Gerrit administrators can define project-specific themes.
++
+Themes can be link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-themes.html[
+configured site-wide or per project].
+
+* New '/a/tools' URL.
++
+This allows users to download the `commit-msg` hook via the command line if the
+Gerrit server requires authentication globally.
+
+* New 'Stream Events' global capability.
++
+The link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/access-control.txt#capability_streamEvents[
+Stream Events capability] controls access to the `stream-events` ssh command.
++
+Only administrators and users having this capability are allowed to use `stream-events`.
+
+* Allow opening new changes on existing commits.
++
+The `%base` argument can be used with `refs/for/` to identify a specific revision the server should
+start to look for new commits at. Any commits in the range `$base..$tip` will be opened as a new
+change, even if the commit already has another change on a different branch.
+
+* New setting `gitweb.linkDrafts` to control if gitweb links are shown on drafts.
++
+By default, Gerrit will show links to gitweb on all patch sets. If the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-gerrit.html#gitweb.linkDrafts[
+gitweb.linkDrafts setting] is set to 'false', links will not be shown on
+draft patch sets.
+
+* Allow changes to be automatically submitted on push.
++
+Teams that want to use Gerrit's submit strategies to handle contention on busy
+branches can use `%submit` to create a change and have it
+link:link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/user-upload.html#auto_merge[
+immediately submitted], if the caller has Submit permission on `refs/for/<ref>`.
+
+* Allow administrators to see all groups.
+
+
+Web UI
+~~~~~~
+
+
+Global
+^^^^^^
+
+* User avatars are displayed in more places in the Web UI.
+
+* 'Diffy' is used as avatar for the Gerrit server itself.
+
+* A popup with user profile information is shown when hovering the
+mouse over avatar images.
+
+
+Change Screens
+^^^^^^^^^^^^^^
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=667[Issue 667]:
+Highlight patch sets that have drafts.
++
+Patch sets having unpublished draft comments are highlighted with an icon.
+
+* Option to show relative times in change tables.
++
+A new preference setting allows the user to decide if absolute or relative dates
+should be shown in change tables.
+
+* Option to set default visibility of change comments.
++
+A new preference setting allows the user to set the default visibility of
+change comments.
+
+
+Diff Screens
+^^^^^^^^^^^^
+
+* Show images in side-by-side and unified diffs.
+
+* Show diffed images above/below each other in unified diffs.
+
+* Harmonize unified diff's styling of images with that of text.
+
+
+REST API
+~~~~~~~~
+
+
+Several new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api.html[
+REST API endpoints] are added.
+
+Accounts
+^^^^^^^^
+
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-accounts.html#get-diff-preferences[
+Get account diff preferences]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-accounts.html#set-diff-preferences[
+Set account diff preferences]
+
+
+Changes
+^^^^^^^
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1820[Issue 1820]:
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-changes.html#list-comments[
+List comments]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-changes.html#get-comment[
+Get comment]
+
+
+
+Projects
+^^^^^^^^
+
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-projects.html#get-config[
+Get project configuration]
+
+
+ssh
+~~~
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1088[Issue 1088]:
+Support link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-gerrit.html#sshd.kerberosKeytab[
+Kerberos authentication for ssh interaction].
+
+
+Bug Fixes
+---------
+
+General
+~~~~~~~
+
+* Postpone check for first account until adding an account.
+
+* Mark `ALREADY_MERGED` changes as merged in the database.
++
+If a change was marked `ALREADY_MERGED`, likely due to a bug in
+merge code, it does not end up in the list of changes to be submitted
+and never gets marked as merged despite the branch head already
+having advanced.
++
+Works around (but does not fix)
+link:https://code.google.com/p/gerrit/issues/detail?id=1985[Issue 1985] and
+link:https://code.google.com/p/gerrit/issues/detail?id=600[Issue 600]
+to allow recovery from certain kind of bad state.
+
+
+Web UI
+~~~~~~
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1848[Issue 1848]:
+Don't discard inline comments when escape key is pressed.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1863[Issue 1863]:
+Drop Arial Unicode MS font and request only sans-serif.
++
+Arial Unicode MS does not have a bold version. Selecting this font prevents
+correct display of bold text on Mac OS X. Simplify the selector to sans-serif
+and allow the browser to use the user's preferred font in this family.
+
+
+REST API
+~~~~~~~~
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1819[Issue 1819]:
+Include change-level messages to the payload returned from
+the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/rest-api-changes#get-change-detail[
+Get Change Detail REST API endpoint].
+
+* Correct URL encoding in 'GroupInfo'.
+
+
+Email
+~~~~~
+
+* Log failure to access reviewer list for notification emails.
+
+* Log when appropriate if email delivery is skipped.
+
+
+ssh
+~~~
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2016[Issue 2016]:
+Flush caches after adding or deleting ssh keys via the `set-account` ssh command.
+
+Tools
+~~~~~
+
+
+* The release build now builds for all browser configurations.
+
+
+Upgrades
+--------
+
+* `gwtexpui` is now built in the gerrit tree rather than linking a separate module.
+
+
+
+Documentation
+-------------
+
+
+* Update the access control documentation to clarify how to set
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/access-control.html#global_capabilities[
+global capabilities].
+
+* Clarify the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.7/config-gerrit.html#cache_names[
+change cache configuration].
+
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 3f9ed7c1c6..974d4db904 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
Gerrit Code Review - Release Notes
==================================
+[[2_7]]
+Version 2.7.x
+-------------
+* link:ReleaseNotes-2.7.html[2.7]
+
[[2_6]]
Version 2.6.x
-------------
diff --git a/contrib/trivial_rebase.py b/contrib/trivial_rebase.py
index 7764470430..30e60afea4 100755
--- a/contrib/trivial_rebase.py
+++ b/contrib/trivial_rebase.py
@@ -131,7 +131,7 @@ class TrivialRebase:
Returns a list of approval dicts.
"""
- sql_query = ("\"SELECT value,account_id,category_id FROM patch_set_approvals "
+ sql_query = ("\"SELECT value,account_id,category_id AS label FROM patch_set_approvals "
"WHERE change_id = %s AND patch_set_id = %s AND value != 0\""
% (self.changeId, (self.patchset - 1)))
gsql_out = self.GsqlQuery(sql_query)
@@ -221,19 +221,20 @@ class TrivialRebase:
# Note: Sites with different 'copy_min_score' values in the
# approval_categories DB table might want different behavior here.
# Additional categories should also be added if desired.
- if approval["category_id"] == "Code-Review" and approval['value'] != '-2':
- self.AppendAcctApproval(approval['account_id'], '--code-review %s' % approval['value'])
- elif approval["category_id"] == "Verified":
+ if approval["label"] == "Code-Review":
+ if approval['value'] != '-2':
+ self.AppendAcctApproval(approval['account_id'],
+ '--label Code-Review=%s' % approval['value'])
+ elif approval["label"] == "Verified":
# Don't re-add verifies
- # self.AppendAcctApproval(approval['account_id'], '--verified %s' % approval['value'])
+ # self.AppendAcctApproval(approval['account_id'], '--label Verified=%s' % approval['value'])
continue
- elif approval["category_id"] == "SUBM":
+ elif approval["label"] == "SUBM":
# We don't care about previous submit attempts
continue
else:
- self.AppendAcctApproval(approval['account_id'], '--%s %s' %
- (approval['category_id'].lower().replace(' ', '-'),
- approval['value']))
+ self.AppendAcctApproval(approval['account_id'], '--label %s=%s' %
+ (approval['label'], approval['value']))
gerrit_review_msg = ("\'Automatically re-added by Gerrit trivial rebase "
"detection script.\'")
diff --git a/gerrit-acceptance-tests/pom.xml b/gerrit-acceptance-tests/pom.xml
index eb27301b13..b71546dc1d 100644
--- a/gerrit-acceptance-tests/pom.xml
+++ b/gerrit-acceptance-tests/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-acceptance-tests</artifactId>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
index 9faf32a8c1..c8158c07e4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
@@ -107,11 +107,17 @@ public class GitUtil {
public static String createCommit(Git git, PersonIdent i, String msg)
throws GitAPIException, IOException {
- return createCommit(git, i, msg, true);
+ return createCommit(git, i, msg, true, false);
}
- public static String createCommit(Git git, PersonIdent i, String msg,
- boolean insertChangeId) throws GitAPIException, IOException {
+ public static void amendCommit(Git git, PersonIdent i, String msg, String changeId)
+ throws GitAPIException, IOException {
+ msg = ChangeIdUtil.insertId(msg, ObjectId.fromString(changeId.substring(1)));
+ createCommit(git, i, msg, false, true);
+ }
+
+ private static String createCommit(Git git, PersonIdent i, String msg,
+ boolean insertChangeId, boolean amend) throws GitAPIException, IOException {
ObjectId changeId = null;
if (insertChangeId) {
changeId = computeChangeId(git, i, msg);
@@ -119,6 +125,7 @@ public class GitUtil {
}
final CommitCommand commitCmd = git.commit();
+ commitCmd.setAmend(amend);
commitCmd.setAuthor(i);
commitCmd.setCommitter(i);
commitCmd.setMessage(msg);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
index f90535078f..9799cc16b5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
@@ -14,27 +14,15 @@
package com.google.gerrit.acceptance.git;
-import static com.google.gerrit.acceptance.git.GitUtil.add;
import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.git.GitUtil.createCommit;
import static com.google.gerrit.acceptance.git.GitUtil.createProject;
import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
-import static com.google.gerrit.acceptance.git.GitUtil.pushHead;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import com.google.common.base.Function;
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
+
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AccountCreator;
import com.google.gerrit.acceptance.SshSession;
import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.server.OrmException;
@@ -45,37 +33,17 @@ import com.jcraft.jsch.JSchException;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.transport.PushResult;
-import org.eclipse.jgit.transport.RemoteRefUpdate;
-import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
-@RunWith(Parameterized.class)
public class PushForReviewIT extends AbstractDaemonTest {
-
private enum Protocol {
SSH, HTTP
}
- @Parameters(name="{0}")
- public static List<Object[]> getParam() {
- List<Object[]> params = Lists.newArrayList();
- for(Protocol p : Protocol.values()) {
- params.add(new Object[] {p});
- }
- return params;
- }
-
@Inject
private AccountCreator accounts;
@@ -86,11 +54,7 @@ public class PushForReviewIT extends AbstractDaemonTest {
private Project.NameKey project;
private Git git;
private ReviewDb db;
- private Protocol protocol;
-
- public PushForReviewIT(Protocol p) {
- this.protocol = p;
- }
+ private String sshUrl;
@Before
public void setUp() throws Exception {
@@ -102,21 +66,25 @@ public class PushForReviewIT extends AbstractDaemonTest {
initSsh(admin);
SshSession sshSession = new SshSession(admin);
createProject(sshSession, project.get());
+ sshUrl = sshSession.getUrl();
+ sshSession.close();
+
+ db = reviewDbProvider.open();
+ }
+
+ private void selectProtocol(Protocol p) throws GitAPIException, IOException {
String url;
- switch (protocol) {
+ switch (p) {
case SSH:
- url = sshSession.getUrl();
+ url = sshUrl;
break;
case HTTP:
url = admin.getHttpUrl();
break;
default:
- throw new IllegalStateException("unexpected protocol: " + protocol);
+ throw new IllegalArgumentException("unexpected protocol: " + p);
}
git = cloneProject(url + "/" + project.get());
- sshSession.close();
-
- db = reviewDbProvider.open();
}
@After
@@ -125,185 +93,176 @@ public class PushForReviewIT extends AbstractDaemonTest {
}
@Test
- public void testPushForMaster() throws GitAPIException, OrmException,
+ public void testPushForMaster_HTTP() throws GitAPIException, OrmException,
IOException {
- PushOneCommit push = new PushOneCommit();
- String ref = "refs/for/master";
- PushResult r = push.to(ref);
- assertOkStatus(r, ref);
- assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, null);
+ testPushForMaster(Protocol.HTTP);
}
@Test
- public void testPushForMasterWithTopic() throws GitAPIException,
+ public void testPushForMaster_SSH() throws GitAPIException, OrmException,
+ IOException {
+ testPushForMaster(Protocol.SSH);
+ }
+
+ private void testPushForMaster(Protocol p) throws GitAPIException,
OrmException, IOException {
+ selectProtocol(p);
+ PushOneCommit.Result r = pushTo("refs/for/master");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, null);
+ }
+
+ @Test
+ public void testPushForMasterWithTopic_HTTP()
+ throws GitAPIException, OrmException, IOException {
+ testPushForMasterWithTopic(Protocol.HTTP);
+ }
+
+ @Test
+ public void testPushForMasterWithTopic_SSH()
+ throws GitAPIException, OrmException, IOException {
+ testPushForMasterWithTopic(Protocol.SSH);
+ }
+
+ private void testPushForMasterWithTopic(Protocol p) throws GitAPIException,
+ OrmException, IOException {
+ selectProtocol(p);
// specify topic in ref
- PushOneCommit push = new PushOneCommit();
String topic = "my/topic";
- String ref = "refs/for/master/" + topic;
- PushResult r = push.to(ref);
- assertOkStatus(r, ref);
- assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+ PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, topic);
// specify topic as option
- push = new PushOneCommit();
- ref = "refs/for/master%topic=" + topic;
- r = push.to(ref);
- assertOkStatus(r, ref);
- assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+ r = pushTo("refs/for/master%topic=" + topic);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, topic);
+ }
+
+ @Test
+ public void testPushForMasterWithCc_HTTP() throws GitAPIException,
+ OrmException, IOException, JSchException {
+ testPushForMasterWithCc(Protocol.HTTP);
}
@Test
- public void testPushForMasterWithCc() throws GitAPIException, OrmException,
- IOException, JSchException {
+ public void testPushForMasterWithCc_SSH() throws GitAPIException,
+ OrmException, IOException, JSchException {
+ testPushForMasterWithCc(Protocol.SSH);
+ }
+
+ private void testPushForMasterWithCc(Protocol p) throws GitAPIException,
+ OrmException, IOException, JSchException {
+ selectProtocol(p);
// cc one user
TestAccount user = accounts.create("user", "user@example.com", "User");
- PushOneCommit push = new PushOneCommit();
String topic = "my/topic";
- String ref = "refs/for/master/" + topic + "%cc=" + user.email;
- PushResult r = push.to(ref);
- assertOkStatus(r, ref);
- assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+ PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, topic);
// cc several users
TestAccount user2 =
accounts.create("another-user", "another.user@example.com", "Another User");
- push = new PushOneCommit();
- ref = "refs/for/master/" + topic + "%cc=" + admin.email + ",cc=" + user.email
- + ",cc=" + user2.email;
- r = push.to(ref);
- assertOkStatus(r, ref);
- assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+ r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+ + user.email + ",cc=" + user2.email);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, topic);
// cc non-existing user
String nonExistingEmail = "non.existing@example.com";
- push = new PushOneCommit();
- ref = "refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
- + nonExistingEmail + ",cc=" + user.email;
- r = push.to(ref);
- assertErrorStatus(r, "user \"" + nonExistingEmail + "\" not found", ref);
+ r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+ + nonExistingEmail + ",cc=" + user.email);
+ r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
+ }
+
+ @Test
+ public void testPushForMasterWithReviewer_HTTP() throws GitAPIException,
+ OrmException, IOException, JSchException {
+ testPushForMasterWithReviewer(Protocol.HTTP);
}
@Test
- public void testPushForMasterWithReviewer() throws GitAPIException,
+ public void testPushForMasterWithReviewer_SSH() throws GitAPIException,
OrmException, IOException, JSchException {
+ testPushForMasterWithReviewer(Protocol.SSH);
+ }
+
+ private void testPushForMasterWithReviewer(Protocol p)
+ throws GitAPIException, OrmException, IOException, JSchException {
+ selectProtocol(p);
// add one reviewer
TestAccount user = accounts.create("user", "user@example.com", "User");
- PushOneCommit push = new PushOneCommit();
String topic = "my/topic";
- String ref = "refs/for/master/" + topic + "%r=" + user.email;
- PushResult r = push.to(ref);
- assertOkStatus(r, ref);
- assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT,
- topic, user);
+ PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, topic, user);
// add several reviewers
TestAccount user2 =
accounts.create("another-user", "another.user@example.com", "Another User");
- push = new PushOneCommit();
- ref = "refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
- + ",r=" + user2.email;
- r = push.to(ref);
- assertOkStatus(r, ref);
+ r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
+ + ",r=" + user2.email);
+ r.assertOkStatus();
// admin is the owner of the change and should not appear as reviewer
- assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT,
- topic, user, user2);
+ r.assertChange(Change.Status.NEW, topic, user, user2);
// add non-existing user as reviewer
String nonExistingEmail = "non.existing@example.com";
- push = new PushOneCommit();
- ref = "refs/for/master/" + topic + "%r=" + admin.email + ",r="
- + nonExistingEmail + ",r=" + user.email;
- r = push.to(ref);
- assertErrorStatus(r, "user \"" + nonExistingEmail + "\" not found", ref);
+ r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r="
+ + nonExistingEmail + ",r=" + user.email);
+ r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
}
@Test
- public void testPushForMasterAsDraft() throws GitAPIException, OrmException,
- IOException {
- // create draft by pushing to 'refs/drafts/'
- PushOneCommit push = new PushOneCommit();
- String ref = "refs/drafts/master";
- PushResult r = push.to(ref);
- assertOkStatus(r, ref);
- assertChange(push.changeId, Change.Status.DRAFT, PushOneCommit.SUBJECT, null);
-
- // create draft by using 'draft' option
- push = new PushOneCommit();
- ref = "refs/for/master%draft";
- r = push.to(ref);
- assertOkStatus(r, ref);
- assertChange(push.changeId, Change.Status.DRAFT, PushOneCommit.SUBJECT, null);
+ public void testPushForMasterAsDraft_HTTP() throws GitAPIException,
+ OrmException, IOException {
+ testPushForMasterAsDraft(Protocol.HTTP);
}
@Test
- public void testPushForNonExistingBranch() throws GitAPIException,
+ public void testPushForMasterAsDraft_SSH() throws GitAPIException,
OrmException, IOException {
- PushOneCommit push = new PushOneCommit();
- String branchName = "non-existing";
- String ref = "refs/for/" + branchName;
- PushResult r = push.to(ref);
- assertErrorStatus(r, "branch " + branchName + " not found", ref);
+ testPushForMasterAsDraft(Protocol.SSH);
}
- private void assertChange(String changeId, Change.Status expectedStatus,
- String expectedSubject, String expectedTopic,
- TestAccount... expectedReviewers) throws OrmException {
- Change c =
- Iterables.getOnlyElement(db.changes().byKey(new Change.Key(changeId)).toList());
- assertEquals(expectedSubject, c.getSubject());
- assertEquals(expectedStatus, c.getStatus());
- assertEquals(expectedTopic, Strings.emptyToNull(c.getTopic()));
- assertReviewers(c, expectedReviewers);
- }
+ private void testPushForMasterAsDraft(Protocol p) throws GitAPIException,
+ OrmException, IOException {
+ selectProtocol(p);
+ // create draft by pushing to 'refs/drafts/'
+ PushOneCommit.Result r = pushTo("refs/drafts/master");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.DRAFT, null);
- private void assertReviewers(Change c, TestAccount... expectedReviewers)
- throws OrmException {
- Set<Account.Id> expectedReviewerIds =
- Sets.newHashSet(Lists.transform(Arrays.asList(expectedReviewers),
- new Function<TestAccount, Account.Id>() {
- @Override
- public Account.Id apply(TestAccount a) {
- return a.id;
- }
- }));
-
- for (PatchSetApproval psa : db.patchSetApprovals().byPatchSet(
- c.currentPatchSetId())) {
- assertTrue("unexpected reviewer " + psa.getAccountId(),
- expectedReviewerIds.remove(psa.getAccountId()));
- }
- assertTrue("missing reviewers: " + expectedReviewerIds,
- expectedReviewerIds.isEmpty());
+ // create draft by using 'draft' option
+ r = pushTo("refs/for/master%draft");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.DRAFT, null);
}
- private static void assertOkStatus(PushResult result, String ref) {
- assertStatus(Status.OK, null, result, ref);
+ @Test
+ public void testPushForNonExistingBranch_HTTP() throws GitAPIException,
+ OrmException, IOException {
+ testPushForNonExistingBranch(Protocol.HTTP);
}
- private static void assertErrorStatus(PushResult result,
- String expectedMessage, String ref) {
- assertStatus(Status.REJECTED_OTHER_REASON, expectedMessage, result, ref);
+ @Test
+ public void testPushForNonExistingBranch_SSH() throws GitAPIException,
+ OrmException, IOException {
+ testPushForNonExistingBranch(Protocol.SSH);
}
- private static void assertStatus(Status expectedStatus,
- String expectedMessage, PushResult result, String ref) {
- RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
- assertEquals(refUpdate.getMessage() + "\n" + result.getMessages(),
- expectedStatus, refUpdate.getStatus());
- assertEquals(expectedMessage, refUpdate.getMessage());
+ private void testPushForNonExistingBranch(Protocol p) throws GitAPIException,
+ OrmException, IOException {
+ selectProtocol(p);
+ String branchName = "non-existing";
+ PushOneCommit.Result r = pushTo("refs/for/" + branchName);
+ r.assertErrorStatus("branch " + branchName + " not found");
}
- private class PushOneCommit {
- final static String FILE_NAME = "a.txt";
- final static String FILE_CONTENT = "some content";
- final static String SUBJECT = "test commit";
- String changeId;
-
- public PushResult to(String ref) throws GitAPIException, IOException {
- add(git, FILE_NAME, FILE_CONTENT);
- changeId = createCommit(git, admin.getIdent(), SUBJECT);
- return pushHead(git, ref);
- }
+ private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+ IOException {
+ PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+ return push.to(git, ref);
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
new file mode 100644
index 0000000000..fb1b59206d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
@@ -0,0 +1,179 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.gerrit.acceptance.git.GitUtil.add;
+import static com.google.gerrit.acceptance.git.GitUtil.amendCommit;
+import static com.google.gerrit.acceptance.git.GitUtil.createCommit;
+import static com.google.gerrit.acceptance.git.GitUtil.pushHead;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+
+public class PushOneCommit {
+ public final static String SUBJECT = "test commit";
+
+ private final static String FILE_NAME = "a.txt";
+ private final static String FILE_CONTENT = "some content";
+
+ private final ReviewDb db;
+ private final PersonIdent i;
+
+ private final String subject;
+ private final String fileName;
+ private final String content;
+ private String changeId;
+
+ public PushOneCommit(ReviewDb db, PersonIdent i) {
+ this(db, i, SUBJECT, FILE_NAME, FILE_CONTENT);
+ }
+
+ public PushOneCommit(ReviewDb db, PersonIdent i, String subject,
+ String fileName, String content) {
+ this(db, i, subject, fileName, content, null);
+ }
+
+ public PushOneCommit(ReviewDb db, PersonIdent i, String subject,
+ String fileName, String content, String changeId) {
+ this.db = db;
+ this.i = i;
+ this.subject = subject;
+ this.fileName = fileName;
+ this.content = content;
+ this.changeId = changeId;
+ }
+
+ public Result to(Git git, String ref)
+ throws GitAPIException, IOException {
+ add(git, fileName, content);
+ if (changeId != null) {
+ amendCommit(git, i, subject, changeId);
+ } else {
+ changeId = createCommit(git, i, subject);
+ }
+ return new Result(db, ref, pushHead(git, ref), changeId, subject);
+ }
+
+ public static class Result {
+ private final ReviewDb db;
+ private final String ref;
+ private final PushResult result;
+ private final String changeId;
+ private final String subject;
+
+ private Result(ReviewDb db, String ref, PushResult result, String changeId,
+ String subject) {
+ this.db = db;
+ this.ref = ref;
+ this.result = result;
+ this.changeId = changeId;
+ this.subject = subject;
+ }
+
+ public PatchSet.Id getPatchSetId() throws OrmException {
+ return Iterables.getOnlyElement(
+ db.changes().byKey(new Change.Key(changeId))).currentPatchSetId();
+ }
+
+ public String getChangeId() {
+ return changeId;
+ }
+
+ public void assertChange(Change.Status expectedStatus,
+ String expectedTopic, TestAccount... expectedReviewers)
+ throws OrmException {
+ Change c =
+ Iterables.getOnlyElement(db.changes().byKey(new Change.Key(changeId)).toList());
+ assertEquals(subject, c.getSubject());
+ assertEquals(expectedStatus, c.getStatus());
+ assertEquals(expectedTopic, Strings.emptyToNull(c.getTopic()));
+ assertReviewers(c, expectedReviewers);
+ }
+
+ private void assertReviewers(Change c, TestAccount... expectedReviewers)
+ throws OrmException {
+ Set<Account.Id> expectedReviewerIds =
+ Sets.newHashSet(Lists.transform(Arrays.asList(expectedReviewers),
+ new Function<TestAccount, Account.Id>() {
+ @Override
+ public Account.Id apply(TestAccount a) {
+ return a.id;
+ }
+ }));
+
+ for (PatchSetApproval psa : db.patchSetApprovals().byPatchSet(
+ c.currentPatchSetId())) {
+ assertTrue("unexpected reviewer " + psa.getAccountId(),
+ expectedReviewerIds.remove(psa.getAccountId()));
+ }
+ assertTrue("missing reviewers: " + expectedReviewerIds,
+ expectedReviewerIds.isEmpty());
+ }
+
+ public void assertOkStatus() {
+ assertStatus(Status.OK, null);
+ }
+
+ public void assertErrorStatus(String expectedMessage) {
+ assertStatus(Status.REJECTED_OTHER_REASON, expectedMessage);
+ }
+
+ private void assertStatus(Status expectedStatus, String expectedMessage) {
+ RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
+ assertEquals(message(refUpdate),
+ expectedStatus, refUpdate.getStatus());
+ assertEquals(expectedMessage, refUpdate.getMessage());
+ }
+
+ public void assertMessage(String expectedMessage) {
+ RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
+ assertTrue(message(refUpdate), message(refUpdate).toLowerCase().contains(
+ expectedMessage.toLowerCase()));
+ }
+
+ private String message(RemoteRefUpdate refUpdate) {
+ StringBuilder b = new StringBuilder();
+ if (refUpdate.getMessage() != null) {
+ b.append(refUpdate.getMessage());
+ b.append("\n");
+ }
+ b.append(result.getMessages());
+ return b.toString();
+ }
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
new file mode 100644
index 0000000000..fa85927d8e
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -0,0 +1,300 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.git.CommitMergeStatus;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefSpec;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SubmitOnPushIT extends AbstractDaemonTest {
+
+ @Inject
+ private AccountCreator accounts;
+
+ @Inject
+ private SchemaFactory<ReviewDb> reviewDbProvider;
+
+ @Inject
+ private GitRepositoryManager repoManager;
+
+ @Inject
+ private MetaDataUpdate.Server metaDataUpdateFactory;
+
+ @Inject
+ private ProjectCache projectCache;
+
+ @Inject
+ private GroupCache groupCache;
+
+ @Inject
+ private @GerritPersonIdent PersonIdent serverIdent;
+
+ private TestAccount admin;
+ private Project.NameKey project;
+ private Git git;
+ private ReviewDb db;
+
+ @Before
+ public void setUp() throws Exception {
+ admin =
+ accounts.create("admin", "admin@example.com", "Administrator",
+ "Administrators");
+
+ project = new Project.NameKey("p");
+ initSsh(admin);
+ SshSession sshSession = new SshSession(admin);
+ createProject(sshSession, project.get());
+ git = cloneProject(sshSession.getUrl() + "/" + project.get());
+ sshSession.close();
+
+ db = reviewDbProvider.open();
+ }
+
+ @After
+ public void cleanup() {
+ db.close();
+ }
+
+ @Test
+ public void submitOnPush() throws GitAPIException, OrmException,
+ IOException, ConfigInvalidException {
+ grantSubmit(project, "refs/for/refs/heads/master");
+ PushOneCommit.Result r = pushTo("refs/for/master%submit");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.MERGED, null, admin);
+ assertSubmitApproval(r.getPatchSetId());
+ assertCommit(project, "refs/heads/master");
+ }
+
+ @Test
+ public void submitOnPushToRefsMetaConfig() throws GitAPIException,
+ OrmException, IOException, ConfigInvalidException {
+ grantSubmit(project, "refs/for/refs/meta/config");
+
+ git.fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
+ ObjectId objectId = git.getRepository().getRef("refs/meta/config").getObjectId();
+ git.checkout().setName(objectId.getName()).call();
+
+ PushOneCommit.Result r = pushTo("refs/for/refs/meta/config%submit");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.MERGED, null, admin);
+ assertSubmitApproval(r.getPatchSetId());
+ assertCommit(project, "refs/meta/config");
+ }
+
+ @Test
+ public void submitOnPushMergeConflict() throws GitAPIException, OrmException,
+ IOException, ConfigInvalidException {
+ String master = "refs/heads/master";
+ ObjectId objectId = git.getRepository().getRef(master).getObjectId();
+ push(master, "one change", "a.txt", "some content");
+ git.checkout().setName(objectId.getName()).call();
+
+ grantSubmit(project, "refs/for/refs/heads/master");
+ PushOneCommit.Result r =
+ push("refs/for/master%submit", "other change", "a.txt", "other content");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, null, admin);
+ r.assertMessage(CommitMergeStatus.PATH_CONFLICT.getMessage());
+ }
+
+ @Test
+ public void submitOnPushSuccessfulMerge() throws GitAPIException, OrmException,
+ IOException, ConfigInvalidException {
+ String master = "refs/heads/master";
+ ObjectId objectId = git.getRepository().getRef(master).getObjectId();
+ push(master, "one change", "a.txt", "some content");
+ git.checkout().setName(objectId.getName()).call();
+
+ grantSubmit(project, "refs/for/refs/heads/master");
+ PushOneCommit.Result r =
+ push("refs/for/master%submit", "other change", "b.txt", "other content");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.MERGED, null, admin);
+ assertMergeCommit(master, "other change");
+ }
+
+ @Test
+ public void submitOnPushNewPatchSet() throws GitAPIException,
+ OrmException, IOException, ConfigInvalidException {
+ PushOneCommit.Result r =
+ push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
+
+ grantSubmit(project, "refs/for/refs/heads/master");
+ r = push("refs/for/master%submit", PushOneCommit.SUBJECT, "a.txt",
+ "other content", r.getChangeId());
+ r.assertOkStatus();
+ r.assertChange(Change.Status.MERGED, null, admin);
+ Change c = Iterables.getOnlyElement(db.changes().byKey(
+ new Change.Key(r.getChangeId())).toList());
+ assertEquals(2, db.patchSets().byChange(c.getId()).toList().size());
+ assertSubmitApproval(r.getPatchSetId());
+ assertCommit(project, "refs/heads/master");
+ }
+
+ @Test
+ public void submitOnPushNotAllowed_Error() throws GitAPIException,
+ OrmException, IOException {
+ PushOneCommit.Result r = pushTo("refs/for/master%submit");
+ r.assertErrorStatus("submit not allowed");
+ }
+
+ @Test
+ public void submitOnPushNewPatchSetNotAllowed_Error() throws GitAPIException,
+ OrmException, IOException, ConfigInvalidException {
+ PushOneCommit.Result r =
+ push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
+
+ r = push("refs/for/master%submit", PushOneCommit.SUBJECT, "a.txt",
+ "other content", r.getChangeId());
+ r.assertErrorStatus("submit not allowed");
+ }
+
+ @Test
+ public void submitOnPushingDraft_Error() throws GitAPIException,
+ OrmException, IOException {
+ PushOneCommit.Result r = pushTo("refs/for/master%draft,submit");
+ r.assertErrorStatus("cannot submit draft");
+ }
+
+ @Test
+ public void submitOnPushToNonExistingBranch_Error() throws GitAPIException,
+ OrmException, IOException {
+ String branchName = "non-existing";
+ PushOneCommit.Result r = pushTo("refs/for/" + branchName + "%submit");
+ r.assertErrorStatus("branch " + branchName + " not found");
+ }
+
+ private void grantSubmit(Project.NameKey project, String ref)
+ throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+ MetaDataUpdate md = metaDataUpdateFactory.create(project);
+ md.setMessage("Grant submit on " + ref);
+ ProjectConfig config = ProjectConfig.read(md);
+ AccessSection s = config.getAccessSection(ref, true);
+ Permission p = s.getPermission(Permission.SUBMIT, true);
+ AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+ p.add(new PermissionRule(config.resolve(adminGroup)));
+ config.commit(md);
+ projectCache.evict(config.getProject());
+ }
+
+ private void assertSubmitApproval(PatchSet.Id patchSetId) throws OrmException {
+ List<PatchSetApproval> approvals = db.patchSetApprovals().byPatchSet(patchSetId).toList();
+ assertEquals(1, approvals.size());
+ PatchSetApproval a = approvals.get(0);
+ assertEquals(PatchSetApproval.LabelId.SUBMIT.get(), a.getLabel());
+ assertEquals(1, a.getValue());
+ assertEquals(admin.id, a.getAccountId());
+ }
+
+ private void assertCommit(Project.NameKey project, String branch) throws IOException {
+ Repository r = repoManager.openRepository(project);
+ try {
+ RevWalk rw = new RevWalk(r);
+ try {
+ RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
+ assertEquals(PushOneCommit.SUBJECT, c.getShortMessage());
+ assertEquals(admin.email, c.getAuthorIdent().getEmailAddress());
+ assertEquals(admin.email, c.getCommitterIdent().getEmailAddress());
+ } finally {
+ rw.release();
+ }
+ } finally {
+ r.close();
+ }
+ }
+
+ private void assertMergeCommit(String branch, String subject) throws IOException {
+ Repository r = repoManager.openRepository(project);
+ try {
+ RevWalk rw = new RevWalk(r);
+ try {
+ RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
+ assertEquals(2, c.getParentCount());
+ assertEquals("Merge \"" + subject + "\"", c.getShortMessage());
+ assertEquals(admin.email, c.getAuthorIdent().getEmailAddress());
+ assertEquals(serverIdent.getEmailAddress(), c.getCommitterIdent().getEmailAddress());
+ } finally {
+ rw.release();
+ }
+ } finally {
+ r.close();
+ }
+ }
+
+ private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+ IOException {
+ PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+ return push.to(git, ref);
+ }
+
+ private PushOneCommit.Result push(String ref, String subject,
+ String fileName, String content) throws GitAPIException, IOException {
+ PushOneCommit push =
+ new PushOneCommit(db, admin.getIdent(), subject, fileName, content);
+ return push.to(git, ref);
+ }
+
+ private PushOneCommit.Result push(String ref, String subject,
+ String fileName, String content, String changeId) throws GitAPIException,
+ IOException {
+ PushOneCommit push = new PushOneCommit(db, admin.getIdent(), subject,
+ fileName, content, changeId);
+ return push.to(git, ref);
+ }
+}
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
index d88ed425cd..e1eba403b4 100644
--- a/gerrit-antlr/pom.xml
+++ b/gerrit-antlr/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-antlr</artifactId>
diff --git a/gerrit-cache-h2/pom.xml b/gerrit-cache-h2/pom.xml
index a848aec17a..4abd77c25c 100644
--- a/gerrit-cache-h2/pom.xml
+++ b/gerrit-cache-h2/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-cache-h2</artifactId>
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index 09e1d4efa1..8f133e92ac 100644
--- a/gerrit-common/pom.xml
+++ b/gerrit-common/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-common</artifactId>
@@ -40,8 +40,9 @@ limitations under the License.
</dependency>
<dependency>
- <groupId>gwtexpui</groupId>
- <artifactId>gwtexpui</artifactId>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-gwtexpui</artifactId>
+ <version>${project.version}</version>
</dependency>
<dependency>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
index 3d08d06bb9..f5ef9c5f97 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
@@ -34,7 +34,10 @@ public enum ListChangesOption {
ALL_FILES(6),
/** If accounts are included, include detailed account info. */
- DETAILED_ACCOUNTS(7);
+ DETAILED_ACCOUNTS(7),
+
+ /** Include messages associated with the change. */
+ MESSAGES(9);
private final int value;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index 7ac21db51c..f6d5ea331c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -20,6 +20,7 @@ import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import java.util.List;
+import java.util.Set;
/** Detail necessary to display a change. */
public class ChangeDetail {
@@ -37,6 +38,7 @@ public class ChangeDetail {
protected List<ChangeInfo> dependsOn;
protected List<ChangeInfo> neededBy;
protected List<PatchSet> patchSets;
+ protected Set<PatchSet.Id> patchSetsWithDraftComments;
protected List<SubmitRecord> submitRecords;
protected Project.SubmitType submitType;
protected SubmitTypeRecord submitTypeRecord;
@@ -187,6 +189,14 @@ public class ChangeDetail {
patchSets = s;
}
+ public void setPatchSetsWithDraftComments(Set<PatchSet.Id> pwdc) {
+ this.patchSetsWithDraftComments = pwdc;
+ }
+
+ public boolean hasDraftComments(PatchSet.Id id) {
+ return patchSetsWithDraftComments.contains(id);
+ }
+
public void setSubmitRecords(List<SubmitRecord> all) {
submitRecords = all;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
index 93f283bebe..e4d7b8008b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
@@ -14,16 +14,16 @@
package com.google.gerrit.common.data;
-import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.Project;
+import java.util.ArrayList;
import java.util.List;
public class GarbageCollectionResult {
protected List<Error> errors;
public GarbageCollectionResult() {
- errors = Lists.newArrayList();
+ errors = new ArrayList<Error>();
}
public void addError(Error e) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
index 16b99dc15f..7660a80c84 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
@@ -20,9 +20,7 @@ import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadComma
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwtexpui.safehtml.client.RegexFindReplace;
-import java.util.List;
import java.util.Set;
public class GerritConfig implements Cloneable {
@@ -44,7 +42,6 @@ public class GerritConfig implements Cloneable {
protected String editFullNameUrl;
protected Project.NameKey wildProject;
protected Set<Account.FieldName> editableAccountFields;
- protected List<RegexFindReplace> commentLinks;
protected boolean documentationAvailable;
protected boolean testChangeMerge;
protected String anonymousCowardName;
@@ -188,14 +185,6 @@ public class GerritConfig implements Cloneable {
editableAccountFields = af;
}
- public List<RegexFindReplace> getCommentLinks() {
- return commentLinks;
- }
-
- public void setCommentLinks(final List<RegexFindReplace> cl) {
- commentLinks = cl;
- }
-
public boolean isDocumentationAvailable() {
return documentationAvailable;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
index 3580774f2e..8528c0fdbe 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
@@ -75,6 +75,9 @@ public class GitWebType {
* project names */
private char pathSeparator = '/';
+ /** Whether to include links to draft patch sets */
+ private boolean linkDrafts;
+
/** Private default constructor for gson. */
protected GitWebType() {
}
@@ -125,6 +128,15 @@ public class GitWebType {
}
/**
+ * Get whether to link to draft patch sets
+ *
+ * @return True to link
+ */
+ public boolean getLinkDrafts() {
+ return linkDrafts;
+ }
+
+ /**
* Set the pattern for branch view.
*
* @param pattern The pattern for branch view
@@ -201,4 +213,8 @@ public class GitWebType {
public void setPathSeparator(char separator) {
this.pathSeparator = separator;
}
+
+ public void setLinkDrafts(boolean linkDrafts) {
+ this.linkDrafts = linkDrafts;
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index 7db691d2d6..8c08feb67e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -73,6 +73,9 @@ public class GlobalCapability {
/** Forcefully restart replication to any configured destination. */
public static final String START_REPLICATION = "startReplication";
+ /** Can perform streaming of Gerrit events. */
+ public static final String STREAM_EVENTS = "streamEvents";
+
/** Can view the server's current cache states. */
public static final String VIEW_CACHES = "viewCaches";
@@ -99,6 +102,7 @@ public class GlobalCapability {
NAMES_ALL.add(QUERY_LIMIT);
NAMES_ALL.add(RUN_GC);
NAMES_ALL.add(START_REPLICATION);
+ NAMES_ALL.add(STREAM_EVENTS);
NAMES_ALL.add(VIEW_CACHES);
NAMES_ALL.add(VIEW_CONNECTIONS);
NAMES_ALL.add(VIEW_QUEUE);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
index 37ae19e71e..db8bde9524 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -96,6 +96,7 @@ public class LabelType {
protected String abbreviation;
protected String functionName;
protected boolean copyMinScore;
+ protected boolean copyMaxScore;
protected List<LabelValue> values;
protected short maxNegative;
@@ -187,6 +188,14 @@ public class LabelType {
this.copyMinScore = copyMinScore;
}
+ public boolean isCopyMaxScore() {
+ return copyMaxScore;
+ }
+
+ public void setCopyMaxScore(boolean copyMaxScore) {
+ this.copyMaxScore = copyMaxScore;
+ }
+
public boolean isMaxNegative(PatchSetApproval ca) {
return maxNegative == ca.getValue();
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
index a2debf2a4a..39f5cb0aed 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
@@ -17,6 +17,7 @@ package com.google.gerrit.common.data;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
import java.util.List;
@@ -24,6 +25,7 @@ public class PatchSetDetail {
protected PatchSet patchSet;
protected PatchSetInfo info;
protected List<Patch> patches;
+ protected Project.NameKey project;
public PatchSetDetail() {
}
@@ -51,4 +53,12 @@ public class PatchSetDetail {
public void setPatches(final List<Patch> p) {
patches = p;
}
+
+ public Project.NameKey getProject() {
+ return project;
+ }
+
+ public void setProject(final Project.NameKey p) {
+ project = p;
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SingleListChangeInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SingleListChangeInfo.java
deleted file mode 100644
index e55375bdbe..0000000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SingleListChangeInfo.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import java.util.List;
-
-/** Summary information needed for screens showing a single list of changes}. */
-public class SingleListChangeInfo {
- protected AccountInfoCache accounts;
- protected List<ChangeInfo> changes;
- protected boolean atEnd;
-
- public SingleListChangeInfo() {
- }
-
- public AccountInfoCache getAccounts() {
- return accounts;
- }
-
- public void setAccounts(final AccountInfoCache ac) {
- accounts = ac;
- }
-
- public List<ChangeInfo> getChanges() {
- return changes;
- }
-
- public boolean isAtEnd() {
- return atEnd;
- }
-
- public void setChanges(List<ChangeInfo> c) {
- setChanges(c, true);
- }
-
- public void setChanges(List<ChangeInfo> c, boolean end) {
- changes = c;
- atEnd = end;
- }
-}
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 956db5a621..4f572097dd 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-extension-api</artifactId>
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index da16eda198..0622e1b4cd 100644
--- a/gerrit-gwtdebug/pom.xml
+++ b/gerrit-gwtdebug/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-gwtdebug</artifactId>
diff --git a/gerrit-gwtexpui/.gitignore b/gerrit-gwtexpui/.gitignore
new file mode 100644
index 0000000000..406c4d5907
--- /dev/null
+++ b/gerrit-gwtexpui/.gitignore
@@ -0,0 +1,6 @@
+/target
+/generated_classes
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtexpui/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..f9fe34593f
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding/<project>=UTF-8
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.core.runtime.prefs b/gerrit-gwtexpui/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000000..8667cfd4a3
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.jdt.core.prefs b/gerrit-gwtexpui/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000000..21aa7e756b
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,285 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=16
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=0
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=2
+org.eclipse.jdt.core.formatter.use_on_off_tags=false
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
diff --git a/gerrit-gwtexpui/.settings/org.eclipse.jdt.ui.prefs b/gerrit-gwtexpui/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000000..7d663fda5c
--- /dev/null
+++ b/gerrit-gwtexpui/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+formatter_profile=_Google Format
+formatter_settings_version=12
diff --git a/gerrit-gwtexpui/COPYING b/gerrit-gwtexpui/COPYING
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/gerrit-gwtexpui/COPYING
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/gerrit-gwtexpui/pom.xml b/gerrit-gwtexpui/pom.xml
new file mode 100644
index 0000000000..86845d387d
--- /dev/null
+++ b/gerrit-gwtexpui/pom.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2009 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-parent</artifactId>
+ <version>2.7-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>gerrit-gwtexpui</artifactId>
+
+ <name>Gerrit Code Review - GWT expui</name>
+ <description>Extended UI tools for GWT</description>
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-source-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+
+ <extensions>
+ <extension>
+ <groupId>com.googlesource.gerrit</groupId>
+ <artifactId>gs-maven-wagon</artifactId>
+ <version>3.3</version>
+ </extension>
+ </extensions>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.gwt</groupId>
+ <artifactId>gwt-user</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.gwt</groupId>
+ <artifactId>gwt-dev</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
index 88bea842c4..0e9b0727f5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2011 The Android Open Source Project
+ Copyright (C) 2009 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,8 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<module rename-to="gerrit_ui">
- <inherits name='com.google.gerrit.GerritGwtUI'/>
- <set-property name="user.agent" value="safari" />
- <set-property name="locale" value="default" />
+<module>
+ <inherits name='com.google.gwt.resources.Resources'/>
+ <inherits name="com.google.gwtexpui.safehtml.SafeHtml"/>
+ <inherits name="com.google.gwtexpui.user.User"/>
</module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java
new file mode 100644
index 0000000000..68495e8e45
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.clippy.client;
+
+import com.google.gwt.resources.client.CssResource;
+
+public interface ClippyCss extends CssResource {
+ String label();
+ String control();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
new file mode 100644
index 0000000000..4c2b8981d4
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.clippy.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+
+public interface ClippyResources extends ClientBundle {
+ public static final ClippyResources I = GWT.create(ClippyResources.class);
+
+ @Source("clippy.css")
+ ClippyCss css();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
new file mode 100644
index 0000000000..273318be51
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
@@ -0,0 +1,230 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.clippy.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HasText;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtexpui.user.client.UserAgent;
+
+/**
+ * Label which permits the user to easily copy the complete content.
+ * <p>
+ * If the Flash plugin is available a "movie" is embedded that provides
+ * one-click copying of the content onto the system clipboard. The label (if
+ * visible) can also be clicked, switching from a label to an input box,
+ * allowing the user to copy the text with a keyboard shortcut.
+ */
+public class CopyableLabel extends Composite implements HasText {
+ private static final int SWF_WIDTH = 110;
+ private static final int SWF_HEIGHT = 14;
+ private static String swfUrl;
+ private static boolean flashEnabled = true;
+
+ static {
+ ClippyResources.I.css().ensureInjected();
+ }
+
+ public static boolean isFlashEnabled() {
+ return flashEnabled;
+ }
+
+ public static void setFlashEnabled(final boolean on) {
+ flashEnabled = on;
+ }
+
+ private static String swfUrl() {
+ if (swfUrl == null) {
+ swfUrl = GWT.getModuleBaseURL() + "gwtexpui_clippy1.cache.swf";
+ }
+ return swfUrl;
+ }
+
+ private final FlowPanel content;
+ private String text;
+ private int visibleLen;
+ private Label textLabel;
+ private TextBox textBox;
+ private Element swf;
+
+ /**
+ * Create a new label
+ *
+ * @param str initial content
+ */
+ public CopyableLabel(final String str) {
+ this(str, true);
+ }
+
+ /**
+ * Create a new label
+ *
+ * @param str initial content
+ * @param showLabel if true, the content is shown, if false it is hidden from
+ * view and only the copy icon is displayed.
+ */
+ public CopyableLabel(final String str, final boolean showLabel) {
+ content = new FlowPanel();
+ initWidget(content);
+
+ text = str;
+ visibleLen = text.length();
+
+ if (showLabel) {
+ textLabel = new InlineLabel(getText());
+ textLabel.setStyleName(ClippyResources.I.css().label());
+ textLabel.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ showTextBox();
+ }
+ });
+ content.add(textLabel);
+ }
+ embedMovie();
+ }
+
+ /**
+ * Change the text which is displayed in the clickable label.
+ *
+ * @param text the new preview text, should be shorter than the original text
+ * which would be copied to the clipboard.
+ */
+ public void setPreviewText(final String text) {
+ if (textLabel != null) {
+ textLabel.setText(text);
+ visibleLen = text.length();
+ }
+ }
+
+ private void embedMovie() {
+ if (flashEnabled && UserAgent.hasFlash) {
+ final String flashVars = "text=" + URL.encodeQueryString(getText());
+ final SafeHtmlBuilder h = new SafeHtmlBuilder();
+
+ h.openElement("div");
+ h.setStyleName(ClippyResources.I.css().control());
+
+ h.openElement("object");
+ h.setWidth(SWF_WIDTH);
+ h.setHeight(SWF_HEIGHT);
+ h.setAttribute("classid", "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000");
+ h.paramElement("movie", swfUrl());
+ h.paramElement("FlashVars", flashVars);
+
+ h.openElement("embed");
+ h.setWidth(SWF_WIDTH);
+ h.setHeight(SWF_HEIGHT);
+ h.setAttribute("wmode", "transparent");
+ h.setAttribute("type", "application/x-shockwave-flash");
+ h.setAttribute("src", swfUrl());
+ h.setAttribute("FlashVars", flashVars);
+ h.closeSelf();
+
+ h.closeElement("object");
+ h.closeElement("div");
+
+ if (swf != null) {
+ DOM.removeChild(getElement(), swf);
+ }
+ DOM.appendChild(getElement(), swf = SafeHtml.parse(h));
+ }
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(final String newText) {
+ text = newText;
+ visibleLen = newText.length();
+
+ if (textLabel != null) {
+ textLabel.setText(getText());
+ }
+ if (textBox != null) {
+ textBox.setText(getText());
+ textBox.selectAll();
+ }
+ embedMovie();
+ }
+
+ private void showTextBox() {
+ if (textBox == null) {
+ textBox = new TextBox();
+ textBox.setText(getText());
+ textBox.setVisibleLength(visibleLen);
+ textBox.addKeyPressHandler(new KeyPressHandler() {
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ if (event.isControlKeyDown() || event.isMetaKeyDown()) {
+ switch (event.getCharCode()) {
+ case 'c':
+ case 'x':
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ hideTextBox();
+ }
+ });
+ break;
+ }
+ }
+ }
+ });
+ textBox.addBlurHandler(new BlurHandler() {
+ @Override
+ public void onBlur(final BlurEvent event) {
+ hideTextBox();
+ }
+ });
+ content.insert(textBox, 1);
+ }
+
+ textLabel.setVisible(false);
+ textBox.setVisible(true);
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ textBox.selectAll();
+ textBox.setFocus(true);
+ }
+ });
+ }
+
+ private void hideTextBox() {
+ if (textBox != null) {
+ textBox.removeFromParent();
+ textBox = null;
+ }
+ textLabel.setVisible(true);
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css
new file mode 100644
index 0000000000..b962df304d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css
@@ -0,0 +1,25 @@
+/* Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.label {
+ vertical-align: top;
+}
+.control {
+ margin-left: 5px;
+ display: inline-block !important;
+ height: 14px;
+ width: 14px;
+ overflow: hidden;
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf
new file mode 100644
index 0000000000..e46886cd11
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml
index d7e835f77c..b3859873a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/CSS.gwt.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2011 The Android Open Source Project
+ Copyright (C) 2009 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,8 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<module rename-to="gerrit_ui">
- <inherits name='com.google.gerrit.GerritGwtUI'/>
- <set-property name="user.agent" value="gecko1_8" />
- <set-property name="locale" value="default" />
+<module>
+ <define-linker name='cachecss' class='com.google.gwtexpui.css.rebind.CssLinker'/>
+ <add-linker name='cachecss'/>
</module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java
new file mode 100644
index 0000000000..0f6992d5f9
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.css.rebind;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.AbstractLinker;
+import com.google.gwt.core.ext.linker.Artifact;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.PublicResource;
+import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
+import com.google.gwt.core.ext.linker.impl.StandardStylesheetReference;
+import com.google.gwt.dev.util.Util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+
+@LinkerOrder(LinkerOrder.Order.PRE)
+public class CssLinker extends AbstractLinker {
+ @Override
+ public String getDescription() {
+ return "CssLinker";
+ }
+
+ @Override
+ public ArtifactSet link(final TreeLogger logger, final LinkerContext context,
+ final ArtifactSet artifacts) throws UnableToCompleteException {
+ final ArtifactSet returnTo = new ArtifactSet();
+ int index = 0;
+
+ final HashMap<String, PublicResource> css =
+ new HashMap<String, PublicResource>();
+
+ for (final StandardStylesheetReference ssr : artifacts
+ .<StandardStylesheetReference> find(StandardStylesheetReference.class)) {
+ css.put(ssr.getSrc(), null);
+ }
+ for (final PublicResource pr : artifacts
+ .<PublicResource> find(PublicResource.class)) {
+ if (css.containsKey(pr.getPartialPath())) {
+ css.put(pr.getPartialPath(), new CssPubRsrc(name(logger, pr), pr));
+ }
+ }
+
+ for (Artifact<?> a : artifacts) {
+ if (a instanceof PublicResource) {
+ final PublicResource r = (PublicResource) a;
+ if (css.containsKey(r.getPartialPath())) {
+ a = css.get(r.getPartialPath());
+ }
+ } else if (a instanceof StandardStylesheetReference) {
+ final StandardStylesheetReference r = (StandardStylesheetReference) a;
+ final PublicResource p = css.get(r.getSrc());
+ a = new StandardStylesheetReference(p.getPartialPath(), index);
+ }
+
+ returnTo.add(a);
+ index++;
+ }
+ return returnTo;
+ }
+
+ private String name(final TreeLogger logger, final PublicResource r)
+ throws UnableToCompleteException {
+ final InputStream in = r.getContents(logger);
+ final ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+ try {
+ try {
+ final byte[] buf = new byte[2048];
+ int n;
+ while ((n = in.read(buf)) >= 0) {
+ tmp.write(buf, 0, n);
+ }
+ tmp.close();
+ } finally {
+ in.close();
+ }
+ } catch (IOException e) {
+ final UnableToCompleteException ute = new UnableToCompleteException();
+ ute.initCause(e);
+ throw ute;
+ }
+
+ String base = r.getPartialPath();
+ final int s = base.lastIndexOf('/');
+ if (0 < s) {
+ base = base.substring(0, s + 1);
+ } else {
+ base = "";
+ }
+ return base + Util.computeStrongName(tmp.toByteArray()) + ".cache.css";
+ }
+
+ private static class CssPubRsrc extends PublicResource {
+ private static final long serialVersionUID = 1L;
+ private final PublicResource src;
+
+ CssPubRsrc(final String partialPath, final PublicResource r) {
+ super(StandardLinkerContext.class, partialPath);
+ src = r;
+ }
+
+ @Override
+ public InputStream getContents(final TreeLogger logger)
+ throws UnableToCompleteException {
+ return src.getContents(logger);
+ }
+
+ @Override
+ public long getLastModified() {
+ return src.getLastModified();
+ }
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
new file mode 100644
index 0000000000..771050f203
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<module>
+ <inherits name='com.google.gwt.resources.Resources'/>
+ <inherits name='com.google.gwtexpui.user.User'/>
+ <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
+</module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java
new file mode 100644
index 0000000000..304d56ea39
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+
+public final class CompoundKeyCommand extends KeyCommand {
+ final KeyCommandSet set;
+
+ public CompoundKeyCommand(int mask, char key, String help, KeyCommandSet s) {
+ super(mask, key, help);
+ set = s;
+ }
+
+ public CompoundKeyCommand(int mask, int key, String help, KeyCommandSet s) {
+ super(mask, key, help);
+ set = s;
+ }
+
+ public KeyCommandSet getSet() {
+ return set;
+ }
+
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ GlobalKey.temporaryWithTimeout(set);
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
new file mode 100644
index 0000000000..d680a72130
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+class DocWidget extends Widget implements HasKeyPressHandlers {
+ private static DocWidget me;
+
+ static DocWidget get() {
+ if (me == null) {
+ me = new DocWidget();
+ }
+ return me;
+ }
+
+ private DocWidget() {
+ setElement((Element) docnode());
+ onAttach();
+ RootPanel.detachOnWindowClose(this);
+ }
+
+ @Override
+ public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+ return addDomHandler(handler, KeyPressEvent.getType());
+ }
+
+ private static Node docnode() {
+ return Document.get();
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
new file mode 100644
index 0000000000..1eaaa3cfdc
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
@@ -0,0 +1,183 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+
+public class GlobalKey {
+ public static final KeyPressHandler STOP_PROPAGATION = new KeyPressHandler() {
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ event.stopPropagation();
+ }
+ };
+
+ private static State global;
+ static State active;
+ private static CloseHandler<PopupPanel> restoreGlobal;
+ private static Timer restoreTimer;
+
+ static {
+ KeyResources.I.css().ensureInjected();
+ }
+
+ private static void initEvents() {
+ if (active == null) {
+ DocWidget.get().addKeyPressHandler(new KeyPressHandler() {
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ final KeyCommandSet s = active.live;
+ if (s != active.all) {
+ active.live = active.all;
+ restoreTimer.cancel();
+ }
+ s.onKeyPress(event);
+ }
+ });
+
+ restoreTimer = new Timer() {
+ @Override
+ public void run() {
+ active.live = active.all;
+ }
+ };
+
+ global = new State(null);
+ active = global;
+ }
+ }
+
+ private static void initDialog() {
+ if (restoreGlobal == null) {
+ restoreGlobal = new CloseHandler<PopupPanel>() {
+ @Override
+ public void onClose(final CloseEvent<PopupPanel> event) {
+ active = global;
+ }
+ };
+ }
+ }
+
+ static void temporaryWithTimeout(final KeyCommandSet s) {
+ active.live = s;
+ restoreTimer.schedule(250);
+ }
+
+ public static void dialog(final PopupPanel panel) {
+ initEvents();
+ initDialog();
+ assert panel.isShowing();
+ assert active == global;
+ active = new State(panel);
+ active.add(new HidePopupPanelCommand(0, KeyCodes.KEY_ESCAPE, panel));
+ panel.addCloseHandler(restoreGlobal);
+ }
+
+ public static HandlerRegistration addApplication(final Widget widget,
+ final KeyCommand appKey) {
+ initEvents();
+ final State state = stateFor(widget);
+ state.add(appKey);
+ return new HandlerRegistration() {
+ @Override
+ public void removeHandler() {
+ state.remove(appKey);
+ }
+ };
+ }
+
+ public static HandlerRegistration add(final Widget widget,
+ final KeyCommandSet cmdSet) {
+ initEvents();
+ final State state = stateFor(widget);
+ state.add(cmdSet);
+ return new HandlerRegistration() {
+ @Override
+ public void removeHandler() {
+ state.remove(cmdSet);
+ }
+ };
+ }
+
+ private static State stateFor(Widget w) {
+ while (w != null) {
+ if (w == active.root) {
+ return active;
+ }
+ w = w.getParent();
+ }
+ return global;
+ }
+
+ public static void filter(final KeyCommandFilter filter) {
+ active.filter(filter);
+ if (active != global) {
+ global.filter(filter);
+ }
+ }
+
+ private GlobalKey() {
+ }
+
+ static class State {
+ final Widget root;
+ final KeyCommandSet app;
+ final KeyCommandSet all;
+ KeyCommandSet live;
+
+ State(final Widget r) {
+ root = r;
+
+ app = new KeyCommandSet(KeyConstants.I.applicationSection());
+ app.add(ShowHelpCommand.INSTANCE);
+
+ all = new KeyCommandSet();
+ all.add(app);
+
+ live = all;
+ }
+
+ void add(final KeyCommand k) {
+ app.add(k);
+ all.add(k);
+ }
+
+ void remove(final KeyCommand k) {
+ app.remove(k);
+ all.remove(k);
+ }
+
+ void add(final KeyCommandSet s) {
+ all.add(s);
+ }
+
+ void remove(final KeyCommandSet s) {
+ all.remove(s);
+ }
+
+ void filter(final KeyCommandFilter f) {
+ all.filter(f);
+ }
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java
new file mode 100644
index 0000000000..0274b9d2f6
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+/** Hides the given popup panel when invoked. */
+public class HidePopupPanelCommand extends KeyCommand {
+ private final PopupPanel panel;
+
+ public HidePopupPanelCommand(int mask, int key, PopupPanel panel) {
+ super(mask, key, KeyConstants.I.closeCurrentDialog());
+ this.panel = panel;
+ }
+
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ panel.hide();
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
new file mode 100644
index 0000000000..ba4f62649d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+
+public abstract class KeyCommand implements KeyPressHandler {
+ public static final int M_CTRL = 1 << 16;
+ public static final int M_ALT = 2 << 16;
+ public static final int M_META = 4 << 16;
+
+ public static boolean same(final KeyCommand a, final KeyCommand b) {
+ return a.getClass() == b.getClass() && a.helpText.equals(b.helpText);
+ }
+
+ final int keyMask;
+ private final String helpText;
+
+ public KeyCommand(final int mask, final int key, final String help) {
+ this(mask, (char) key, help);
+ }
+
+ public KeyCommand(final int mask, final char key, final String help) {
+ assert help != null;
+ keyMask = mask | key;
+ helpText = help;
+ }
+
+ public String getHelpText() {
+ return helpText;
+ }
+
+ SafeHtml describeKeyStroke() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+
+ if ((keyMask & M_CTRL) == M_CTRL) {
+ modifier(b, KeyConstants.I.keyCtrl());
+ }
+ if ((keyMask & M_ALT) == M_ALT) {
+ modifier(b, KeyConstants.I.keyAlt());
+ }
+ if ((keyMask & M_META) == M_META) {
+ modifier(b, KeyConstants.I.keyMeta());
+ }
+
+ final char c = (char) (keyMask & 0xffff);
+ switch (c) {
+ case KeyCodes.KEY_ENTER:
+ namedKey(b, KeyConstants.I.keyEnter());
+ break;
+ case KeyCodes.KEY_ESCAPE:
+ namedKey(b, KeyConstants.I.keyEsc());
+ break;
+ default:
+ b.openSpan();
+ b.setStyleName(KeyResources.I.css().helpKey());
+ b.append(String.valueOf(c));
+ b.closeSpan();
+ break;
+ }
+
+ return b;
+ }
+
+ private void modifier(final SafeHtmlBuilder b, final String name) {
+ namedKey(b, name);
+ b.append(" + ");
+ }
+
+ private void namedKey(final SafeHtmlBuilder b, final String name) {
+ b.append('<');
+ b.openSpan();
+ b.setStyleName(KeyResources.I.css().helpKey());
+ b.append(name);
+ b.closeSpan();
+ b.append(">");
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
new file mode 100644
index 0000000000..05f41d4b63
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+public interface KeyCommandFilter {
+ public boolean include(KeyCommand key);
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
new file mode 100644
index 0000000000..4f3205abff
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class KeyCommandSet implements KeyPressHandler {
+ private final Map<Integer, KeyCommand> map;
+ private List<KeyCommandSet> sets;
+ private String name;
+
+ public KeyCommandSet() {
+ this("");
+ }
+
+ public KeyCommandSet(final String setName) {
+ map = new HashMap<Integer, KeyCommand>();
+ name = setName;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(final String setName) {
+ assert setName != null;
+ name = setName;
+ }
+
+ public boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ public void add(final KeyCommand k) {
+ assert !map.containsKey(k.keyMask)
+ : "Key " + k.describeKeyStroke().asString()
+ + " already registered";
+ if (!map.containsKey(k.keyMask)) {
+ map.put(k.keyMask, k);
+ }
+ }
+
+ public void remove(final KeyCommand k) {
+ assert map.get(k.keyMask) == k;
+ map.remove(k.keyMask);
+ }
+
+ public void add(final KeyCommandSet set) {
+ if (sets == null) {
+ sets = new ArrayList<KeyCommandSet>();
+ }
+ assert !sets.contains(set);
+ sets.add(set);
+ for (final KeyCommand k : set.map.values()) {
+ add(k);
+ }
+ }
+
+ public void remove(final KeyCommandSet set) {
+ assert sets != null;
+ assert sets.contains(set);
+ sets.remove(set);
+ for (final KeyCommand k : set.map.values()) {
+ remove(k);
+ }
+ }
+
+ public void filter(final KeyCommandFilter filter) {
+ if (sets != null) {
+ for (final KeyCommandSet s : sets) {
+ s.filter(filter);
+ }
+ }
+ for (final Iterator<KeyCommand> i = map.values().iterator(); i.hasNext();) {
+ final KeyCommand kc = i.next();
+ if (!filter.include(kc)) {
+ i.remove();
+ } else if (kc instanceof CompoundKeyCommand) {
+ ((CompoundKeyCommand) kc).set.filter(filter);
+ }
+ }
+ }
+
+ public Collection<KeyCommand> getKeys() {
+ return map.values();
+ }
+
+ public Collection<KeyCommandSet> getSets() {
+ return sets != null ? sets : Collections.<KeyCommandSet> emptyList();
+ }
+
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ final KeyCommand k = map.get(toMask(event));
+ if (k != null) {
+ event.preventDefault();
+ event.stopPropagation();
+ k.onKeyPress(event);
+ }
+ }
+
+ static int toMask(final KeyPressEvent event) {
+ int mask = event.getCharCode();
+ if (event.isAltKeyDown()) {
+ mask |= KeyCommand.M_ALT;
+ }
+ if (event.isControlKeyDown()) {
+ mask |= KeyCommand.M_CTRL;
+ }
+ if (event.isMetaKeyDown()) {
+ mask |= KeyCommand.M_META;
+ }
+ return mask;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
new file mode 100644
index 0000000000..56fb85c71e
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.i18n.client.Constants;
+
+public interface KeyConstants extends Constants {
+ public static final KeyConstants I = GWT.create(KeyConstants.class);
+
+ String applicationSection();
+ String showHelp();
+ String closeCurrentDialog();
+
+ String keyboardShortcuts();
+ String closeButton();
+ String orOtherKey();
+ String thenOtherKey();
+
+ String keyCtrl();
+ String keyAlt();
+ String keyMeta();
+ String keyEnter();
+ String keyEsc();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
new file mode 100644
index 0000000000..e21daf502e
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
@@ -0,0 +1,14 @@
+applicationSection = Application
+showHelp = Open shortcut help
+closeCurrentDialog = Close current dialog
+
+keyboardShortcuts = Keyboard Shortcuts
+closeButton = Close
+orOtherKey = or
+thenOtherKey = then
+
+keyCtrl = Ctrl
+keyAlt = Alt
+keyMeta = Meta
+keyEnter = Enter
+keyEsc = Esc
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java
new file mode 100644
index 0000000000..d19018de8f
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCss.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.resources.client.CssResource;
+
+public interface KeyCss extends CssResource {
+ String helpPopup();
+ String helpHeader();
+ String helpHeaderGlue();
+ String helpTable();
+ String helpTableGlue();
+ String helpGroup();
+ String helpKeyStroke();
+ String helpSeparator();
+ String helpKey();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
new file mode 100644
index 0000000000..7bd023396d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
@@ -0,0 +1,228 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusPanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+
+public class KeyHelpPopup extends PluginSafePopupPanel implements
+ KeyPressHandler {
+ private final FocusPanel focus;
+
+ public KeyHelpPopup() {
+ super(true/* autohide */, true/* modal */);
+ setStyleName(KeyResources.I.css().helpPopup());
+
+ final Anchor closer = new Anchor(KeyConstants.I.closeButton());
+ closer.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ hide();
+ }
+ });
+
+ final Grid header = new Grid(1, 3);
+ header.setStyleName(KeyResources.I.css().helpHeader());
+ header.setText(0, 0, KeyConstants.I.keyboardShortcuts());
+ header.setWidget(0, 2, closer);
+
+ final CellFormatter fmt = header.getCellFormatter();
+ fmt.addStyleName(0, 1, KeyResources.I.css().helpHeaderGlue());
+ fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
+
+ final Grid lists = new Grid(0, 7);
+ lists.setStyleName(KeyResources.I.css().helpTable());
+ populate(lists);
+ lists.getCellFormatter().addStyleName(0, 3,
+ KeyResources.I.css().helpTableGlue());
+
+ final FlowPanel body = new FlowPanel();
+ body.add(header);
+ DOM.appendChild(body.getElement(), DOM.createElement("hr"));
+ body.add(lists);
+
+ focus = new FocusPanel(body);
+ DOM.setStyleAttribute(focus.getElement(), "outline", "0px");
+ DOM.setElementAttribute(focus.getElement(), "hideFocus", "true");
+ focus.addKeyPressHandler(this);
+ add(focus);
+ }
+
+ @Override
+ public void setVisible(final boolean show) {
+ super.setVisible(show);
+ if (show) {
+ focus.setFocus(true);
+ }
+ }
+
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ if (KeyCommandSet.toMask(event) == ShowHelpCommand.INSTANCE.keyMask) {
+ // Block the '?' key from triggering us to show right after
+ // we just hide ourselves.
+ //
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ hide();
+ }
+
+ private void populate(final Grid lists) {
+ int end[] = new int[5];
+ int column = 0;
+ for (final KeyCommandSet set : combinedSetsByName()) {
+ int row = end[column];
+ row = formatGroup(lists, row, column, set);
+ end[column] = row;
+ if (column == 0) {
+ column = 4;
+ } else {
+ column = 0;
+ }
+ }
+ }
+
+ /**
+ * @return an ordered collection of KeyCommandSet, combining sets which share
+ * the same name, so that each set name appears at most once.
+ */
+ private static Collection<KeyCommandSet> combinedSetsByName() {
+ final LinkedHashMap<String, KeyCommandSet> byName =
+ new LinkedHashMap<String, KeyCommandSet>();
+ for (final KeyCommandSet set : GlobalKey.active.all.getSets()) {
+ KeyCommandSet v = byName.get(set.getName());
+ if (v == null) {
+ v = new KeyCommandSet(set.getName());
+ byName.put(v.getName(), v);
+ }
+ v.add(set);
+ }
+ return byName.values();
+ }
+
+ private int formatGroup(final Grid lists, int row, final int col,
+ final KeyCommandSet set) {
+ if (set.isEmpty()) {
+ return row;
+ }
+
+ if (lists.getRowCount() < row + 1) {
+ lists.resizeRows(row + 1);
+ }
+ lists.setText(row, col + 2, set.getName());
+ lists.getCellFormatter().addStyleName(row, col + 2,
+ KeyResources.I.css().helpGroup());
+ row++;
+
+ return formatKeys(lists, row, col, set, null);
+ }
+
+ private int formatKeys(final Grid lists, int row, final int col,
+ final KeyCommandSet set, final SafeHtml prefix) {
+ final CellFormatter fmt = lists.getCellFormatter();
+ final int initialRow = row;
+ final List<KeyCommand> keys = sort(set);
+ if (lists.getRowCount() < row + keys.size()) {
+ lists.resizeRows(row + keys.size());
+ }
+ FORMAT_KEYS: for (int i = 0; i < keys.size(); i++) {
+ final KeyCommand k = keys.get(i);
+
+ if (k instanceof CompoundKeyCommand) {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ b.append(k.describeKeyStroke());
+ row = formatKeys(lists, row, col, ((CompoundKeyCommand) k).getSet(), b);
+ continue;
+ }
+
+ for (int prior = 0; prior < i; prior++) {
+ if (KeyCommand.same(keys.get(prior), k)) {
+ final int r = initialRow + prior;
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ b.append(SafeHtml.get(lists, r, col + 0));
+ b.append(" ");
+ b.append(KeyConstants.I.orOtherKey());
+ b.append(" ");
+ if (prefix != null) {
+ b.append(prefix);
+ b.append(" ");
+ b.append(KeyConstants.I.thenOtherKey());
+ b.append(" ");
+ }
+ b.append(k.describeKeyStroke());
+ SafeHtml.set(lists, r, col + 0, b);
+ continue FORMAT_KEYS;
+ }
+ }
+
+ if (prefix != null) {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ b.append(prefix);
+ b.append(" ");
+ b.append(KeyConstants.I.thenOtherKey());
+ b.append(" ");
+ b.append(k.describeKeyStroke());
+ SafeHtml.set(lists, row, col + 0, b);
+ } else {
+ SafeHtml.set(lists, row, col + 0, k.describeKeyStroke());
+ }
+ lists.setText(row, col + 1, ":");
+ lists.setText(row, col + 2, k.getHelpText());
+
+ fmt.addStyleName(row, col + 0, KeyResources.I.css().helpKeyStroke());
+ fmt.addStyleName(row, col + 1, KeyResources.I.css().helpSeparator());
+ row++;
+ }
+
+ return row;
+ }
+
+ private List<KeyCommand> sort(final KeyCommandSet set) {
+ final List<KeyCommand> keys = new ArrayList<KeyCommand>(set.getKeys());
+ Collections.sort(keys, new Comparator<KeyCommand>() {
+ @Override
+ public int compare(KeyCommand arg0, KeyCommand arg1) {
+ if (arg0.keyMask < arg1.keyMask) {
+ return -1;
+ } else if (arg0.keyMask > arg1.keyMask) {
+ return 1;
+ }
+ return 0;
+ }
+ });
+ return keys;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
new file mode 100644
index 0000000000..a52ca2a5e9
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyResources.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+
+public interface KeyResources extends ClientBundle {
+ public static final KeyResources I = GWT.create(KeyResources.class);
+
+ @Source("key.css")
+ KeyCss css();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
new file mode 100644
index 0000000000..c06d2c427f
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextArea.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.TextArea;
+
+public class NpTextArea extends TextArea {
+ public NpTextArea() {
+ addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+ }
+
+ public NpTextArea(final Element element) {
+ super(element);
+ addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+ }
+
+ public void setSpellCheck(boolean spell) {
+ DOM.setElementPropertyBoolean(getElement(), "spellcheck", spell);
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java
new file mode 100644
index 0000000000..86402e1771
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.TextBox;
+
+public class NpTextBox extends TextBox {
+ public NpTextBox() {
+ addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+ }
+
+ public NpTextBox(final Element element) {
+ super(element);
+ addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
new file mode 100644
index 0000000000..50a4a86d60
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+
+
+public class ShowHelpCommand extends KeyCommand {
+ public static final ShowHelpCommand INSTANCE = new ShowHelpCommand();
+ private static KeyHelpPopup current;
+
+ public ShowHelpCommand() {
+ super(0, '?', KeyConstants.I.showHelp());
+ }
+
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ if (current != null) {
+ // Already open? Close the dialog.
+ //
+ current.hide();
+ current = null;
+ return;
+ }
+
+ final KeyHelpPopup help = new KeyHelpPopup();
+ help.addCloseHandler(new CloseHandler<PopupPanel>() {
+ @Override
+ public void onClose(final CloseEvent<PopupPanel> event) {
+ current = null;
+ }
+ });
+ current = help;
+ help.setPopupPositionAndShow(new PositionCallback() {
+ @Override
+ public void setPosition(final int pWidth, final int pHeight) {
+ final int left = (Window.getClientWidth() - pWidth) >> 1;
+ final int wLeft = Window.getScrollLeft();
+ final int wTop = Window.getScrollTop();
+ help.setPopupPosition(wLeft + left, wTop + 50);
+ }
+ });
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css
new file mode 100644
index 0000000000..9372e45a6e
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/key.css
@@ -0,0 +1,99 @@
+/* Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@external .popupContent;
+
+.helpPopup {
+ background: #000000 none repeat scroll 0 50%;
+ color: #ffffff;
+ font-family: arial,sans-serif;
+ font-weight: bold;
+ overflow: hidden;
+ text-align: left;
+ text-shadow: 1px 1px 7px #000000;
+ width: 92%;
+ z-index: 1002;
+ opacity: 0.85;
+ }
+
+@if user.agent safari {
+ .helpPopup {
+ \-webkit-border-radius: 10px;
+ }
+}
+@if user.agent gecko1_8 {
+ .helpPopup {
+ \-moz-border-radius: 10px;
+ }
+}
+
+.helpPopup .popupContent {
+ margin: 10px;
+}
+
+.helpPopup hr {
+ width: 100%;
+}
+
+.helpHeader {
+ width: 100%;
+}
+
+.helpHeader td {
+ white-space: nowrap;
+ color: #ffffff;
+}
+
+.helpHeader a,
+.helpHeader a:visited,
+.helpHeader a:hover {
+ color: #dddd00;
+}
+
+.helpHeaderGlue {
+ width: 100%;
+}
+
+.helpTable {
+ width: 90%;
+}
+.helpTable td {
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+.helpTableGlue {
+ width: 25px;
+}
+
+.helpGroup {
+ color: #dddd00;
+ padding-top: 0.8em;
+ text-align: left;
+}
+
+.helpKeyStroke {
+ text-align: right;
+}
+
+.helpSeparator {
+ width: 0.5em;
+ text-align: center;
+ font-weight: bold;
+}
+
+.helpKey {
+ color: #dddd00;
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml
new file mode 100644
index 0000000000..a6978ab1da
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/ServerPlannedIFrameLinker.gwt.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http:..www.apache.org.licenses.LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<module>
+ <define-linker name='serverplanned' class='com.google.gwtexpui.linker.rebind.ServerPlannedIFrameLinker'/>
+ <add-linker name='serverplanned'/>
+</module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java
new file mode 100644
index 0000000000..3e2361c26e
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/rebind/ServerPlannedIFrameLinker.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.linker.rebind;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.AbstractLinker;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.CompilationResult;
+import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.SelectionProperty;
+import com.google.gwt.core.ext.linker.StylesheetReference;
+
+import java.util.Map;
+import java.util.SortedMap;
+
+/** Saves data normally used by the {@code nocache.js} file. */
+@LinkerOrder(LinkerOrder.Order.POST)
+public class ServerPlannedIFrameLinker extends AbstractLinker {
+ @Override
+ public String getDescription() {
+ return "ServerPlannedIFrameLinker";
+ }
+
+ @Override
+ public ArtifactSet link(final TreeLogger logger, final LinkerContext context,
+ final ArtifactSet artifacts) throws UnableToCompleteException {
+ ArtifactSet toReturn = new ArtifactSet(artifacts);
+
+ StringBuilder table = new StringBuilder();
+ for (StylesheetReference r : artifacts.find(StylesheetReference.class)) {
+ table.append("css ");
+ table.append(r.getSrc());
+ table.append("\n");
+ }
+
+ for (CompilationResult r : artifacts.find(CompilationResult.class)) {
+ table.append(r.getStrongName() + "\n");
+ for (SortedMap<SelectionProperty, String> p : r.getPropertyMap()) {
+ for (Map.Entry<SelectionProperty, String> e : p.entrySet()) {
+ table.append(" ");
+ table.append(e.getKey().getName());
+ table.append("=");
+ table.append(e.getValue());
+ table.append('\n');
+ }
+ }
+ table.append("\n");
+ }
+
+ toReturn.add(emitString(logger, table.toString(), "permutations"));
+ return toReturn;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java
new file mode 100644
index 0000000000..89da5292bf
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/ClientSideRule.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.linker.server;
+
+import javax.servlet.http.HttpServletRequest;
+
+/** A rule that must execute on the client, as we don't know how to compute it. */
+final class ClientSideRule implements Rule {
+ private final String name;
+
+ ClientSideRule(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String select(HttpServletRequest req) {
+ return null;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java
new file mode 100644
index 0000000000..b319db1458
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Permutation.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.linker.server;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+
+/** A single permutation of the compiled GWT application. */
+public class Permutation {
+ private final PermutationSelector selector;
+ private final String cacheHTML;
+ private final String[] values;
+
+ Permutation(PermutationSelector sel, String cacheHTML, String[] values) {
+ this.selector = sel;
+ this.cacheHTML = cacheHTML;
+ this.values = values;
+ }
+
+ boolean matches(String[] r) {
+ return Arrays.equals(values, r);
+ }
+
+ /**
+ * Append GWT bootstrap for this permutation onto the end of the body.
+ * <p>
+ * The GWT bootstrap for this particular permutation is appended onto the end
+ * of the {@code body} element of the passed host page.
+ * <p>
+ * To keep the bootstrap code small and simple, not all GWT features are
+ * actually supported. The {@code gwt:property}, {@code gwt:onPropertyErrorFn}
+ * and {@code gwt:onLoadErrorFn} meta tags are ignored and not handled.
+ * <p>
+ * Load order may differ from the standard GWT {@code nocache.js}. The browser
+ * is asked to load the iframe immediately, rather than after the body has
+ * finished loading.
+ *
+ * @param dom host page HTML document.
+ */
+ public void inject(Document dom) {
+ String moduleName = selector.getModuleName();
+ String moduleFunc = moduleName;
+
+ StringBuilder s = new StringBuilder();
+ s.append("\n");
+ s.append("function " + moduleFunc + "(){");
+ s.append("var s,l,t");
+ s.append(",w=window");
+ s.append(",d=document");
+ s.append(",n='" + moduleName + "'");
+ s.append(",f=d.createElement('iframe')");
+ s.append(";");
+
+ // Callback to execute the module once both s and l are true.
+ //
+ s.append("function m(){");
+ s.append("if(s&&l){");
+ // Base path needs to be absolute. There isn't an easy way to do this
+ // other than forcing an image to load and then pulling the URL back.
+ //
+ s.append("var b,i=d.createElement('img');");
+ s.append("i.src=n+'/clear.cache.gif';");
+ s.append("b=i.src;");
+ s.append("b=b.substring(0,b.lastIndexOf('/')+1);");
+ s.append(moduleFunc + "=null;"); // allow us to GC
+ s.append("f.contentWindow.gwtOnLoad(undefined,n,b);");
+ s.append("}");
+ s.append("}");
+
+ // Set s true when the module script has finished loading. The
+ // exact name here is known to the IFrameLinker and is called by
+ // the code in the iframe.
+ //
+ s.append(moduleFunc + ".onScriptLoad=function(){");
+ s.append("s=1;m();");
+ s.append("};");
+
+ // Set l true when the browser has finished processing the iframe
+ // tag, and everything else on the page.
+ //
+ s.append(moduleFunc + ".r=function(){");
+ s.append("l=1;m();");
+ s.append("};");
+
+ // Prevents mixed mode security in IE6/7.
+ s.append("f.src=\"javascript:''\";");
+ s.append("f.id=n;");
+ s.append("f.style.cssText"
+ + "='position:absolute;width:0;height:0;border:none';");
+ s.append("f.tabIndex=-1;");
+ s.append("d.body.appendChild(f);");
+
+ // The src has to be set after the iframe is attached to the DOM to avoid
+ // refresh quirks in Safari. We have to use the location.replace trick to
+ // avoid FF2 refresh quirks.
+ //
+ s.append("f.contentWindow.location.replace(n+'/" + cacheHTML + "');");
+
+ // defer attribute here is to workaround IE running immediately.
+ //
+ s.append("d.write('<script defer=\"defer\">" //
+ + moduleFunc + ".r()</'+'script>');");
+ s.append("}");
+ s.append(moduleFunc + "();");
+ s.append("\n//");
+
+ final Element html = dom.getDocumentElement();
+ final Element head = (Element) html.getElementsByTagName("head").item(0);
+ final Element body = (Element) html.getElementsByTagName("body").item(0);
+
+ for (String css : selector.getCSS()) {
+ if (isRelativeURL(css)) {
+ css = moduleName + '/' + css;
+ }
+
+ final Element link = dom.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.setAttribute("href", css);
+ head.appendChild(link);
+ }
+
+ final Element script = dom.createElement("script");
+ script.setAttribute("type", "text/javascript");
+ script.setAttribute("language", "javascript");
+ script.appendChild(dom.createComment(s.toString()));
+ body.appendChild(script);
+ }
+
+ private static boolean isRelativeURL(String src) {
+ if (src.startsWith("/")) {
+ return false;
+ }
+
+ try {
+ // If it parses as a URL, assume it is not relative.
+ //
+ new URL(src);
+ return false;
+ } catch (MalformedURLException e) {
+ }
+
+ return true;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java
new file mode 100644
index 0000000000..d3e5ae35e4
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/PermutationSelector.java
@@ -0,0 +1,205 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.linker.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Selects a permutation based on the HTTP request.
+ * <p>
+ * To use this class the application's GWT module must include our linker by
+ * inheriting our module:
+ *
+ * <pre>
+ * &lt;inherits name='com.google.gwtexpui.linker.ServerPlannedIFrameLinker'/&gt;
+ * </pre>
+ */
+public class PermutationSelector {
+ private final String moduleName;
+ private final Map<String, Rule> rulesByName;
+ private final List<Rule> ruleOrder;
+ private final List<Permutation> permutations;
+ private final List<String> css;
+
+ /**
+ * Create an empty selector for a module.
+ * <p>
+ * {@link UserAgentRule} rule is automatically registered. Additional custom
+ * selector rules may be registered before {@link #init(ServletContext)} is
+ * called to finish the selector setup.
+ *
+ * @param moduleName the name of the module within the context.
+ */
+ public PermutationSelector(final String moduleName) {
+ this.moduleName = moduleName;
+
+ this.rulesByName = new HashMap<String, Rule>();
+ this.ruleOrder = new ArrayList<Rule>();
+ this.permutations = new ArrayList<Permutation>();
+ this.css = new ArrayList<String>();
+
+ register(new UserAgentRule());
+ }
+
+ private void notInitialized() {
+ if (!ruleOrder.isEmpty()) {
+ throw new IllegalStateException("Already initialized");
+ }
+ }
+
+ /**
+ * Register a property selection rule.
+ *
+ * @param r the rule implementation.
+ */
+ public void register(Rule r) {
+ notInitialized();
+ rulesByName.put(r.getName(), r);
+ }
+
+ /**
+ * Initialize the selector by reading the module's {@code permutations} file.
+ *
+ * @param ctx context to load the module data from.
+ * @throws ServletException
+ * @throws IOException
+ */
+ public void init(ServletContext ctx) throws ServletException, IOException {
+ notInitialized();
+
+ final String tableName = "/" + moduleName + "/permutations";
+ final InputStream in = ctx.getResourceAsStream(tableName);
+ if (in == null) {
+ throw new ServletException("No " + tableName + " in context");
+ }
+ try {
+ BufferedReader r = new BufferedReader(new InputStreamReader(in, "UTF-8"));
+ for (;;) {
+ final String strongName = r.readLine();
+ if (strongName == null) {
+ break;
+ }
+
+ if (strongName.startsWith("css ")) {
+ css.add(strongName.substring("css ".length()));
+ continue;
+ }
+
+ Map<String, String> selections = new LinkedHashMap<String, String>();
+ for (;;) {
+ String permutation = r.readLine();
+ if (permutation == null || permutation.isEmpty()) {
+ break;
+ }
+
+ int eq = permutation.indexOf('=');
+ if (eq < 0) {
+ throw new ServletException(tableName + " has malformed content");
+ }
+
+ String k = permutation.substring(0, eq).trim();
+ String v = permutation.substring(eq + 1);
+
+ Rule rule = get(k);
+ if (!ruleOrder.contains(rule)) {
+ ruleOrder.add(rule);
+ }
+
+ if (selections.put(k, v) != null) {
+ throw new ServletException("Table " + tableName
+ + " has multiple values for " + k + " within permutation "
+ + strongName);
+ }
+ }
+
+ String cacheHtml = strongName + ".cache.html";
+ String[] values = new String[ruleOrder.size()];
+ for (int i = 0; i < values.length; i++) {
+ values[i] = selections.get(ruleOrder.get(i).getName());
+ }
+ permutations.add(new Permutation(this, cacheHtml, values));
+ }
+ } finally {
+ in.close();
+ }
+ }
+
+ private Rule get(final String name) {
+ Rule r = rulesByName.get(name);
+ if (r == null) {
+ r = new ClientSideRule(name);
+ register(r);
+ }
+ return r;
+ }
+
+ /** @return name of the module (within the application context). */
+ public String getModuleName() {
+ return moduleName;
+ }
+
+ /** @return all possible permutations */
+ public List<Permutation> getPermutations() {
+ return Collections.unmodifiableList(permutations);
+ }
+
+ /**
+ * Select the permutation that best matches the browser request.
+ *
+ * @param req current request.
+ * @return the selected permutation; null if no permutation can be fit to the
+ * request and the standard {@code nocache.js} loader must be used.
+ */
+ public Permutation select(HttpServletRequest req) {
+ final String[] values = new String[ruleOrder.size()];
+ for (int i = 0; i < values.length; i++) {
+ final String value = ruleOrder.get(i).select(req);
+ if (value == null) {
+ // If the rule returned null it doesn't know how to compute
+ // the value for this HTTP request. Since we can't do that
+ // defer to JavaScript by not picking a permutation.
+ //
+ return null;
+ }
+ values[i] = value;
+ }
+
+ for (Permutation p : permutations) {
+ if (p.matches(values)) {
+ return p;
+ }
+ }
+
+ return null;
+ }
+
+ Collection<String> getCSS() {
+ return css;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java
new file mode 100644
index 0000000000..76b9b5165a
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/Rule.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.linker.server;
+
+import javax.servlet.http.HttpServletRequest;
+
+/** A selection rule for a permutation property. */
+public interface Rule {
+ /** @return the property name, for example {@code "user.agent"}. */
+ public String getName();
+
+ /**
+ * Compute the value for this property, given the current request.
+ * <p>
+ * This rule method must compute the proper permutation value, matching what
+ * the GWT module XML files use for this property. The rule may use any state
+ * available in the current servlet request.
+ * <p>
+ * If this method returns {@code null} server side selection will be aborted
+ * and selection for all properties will be handled on the client side by the
+ * {@code nocache.js} file.
+ *
+ * @param req the request
+ * @return the value for the property; null if the value cannot be determined
+ * on the server side.
+ */
+ public String select(HttpServletRequest req);
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
new file mode 100644
index 0000000000..366b6c57a3
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.linker.server;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import static java.util.regex.Pattern.compile;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Selects the value for the {@code user.agent} property.
+ * <p>
+ * Examines the {@code User-Agent} HTTP request header, and tries to match it to
+ * known {@code user.agent} values.
+ * <p>
+ * Ported from JavaScript in {@code com.google.gwt.user.UserAgent.gwt.xml}.
+ */
+public class UserAgentRule implements Rule {
+ private static final Pattern msie = compile(".*msie ([0-9]+)\\.([0-9]+).*");
+ private static final Pattern gecko = compile(".*rv:([0-9]+)\\.([0-9]+).*");
+
+ public String getName() {
+ return "user.agent";
+ }
+
+ @Override
+ public String select(HttpServletRequest req) {
+ String ua = req.getHeader("User-Agent");
+ if (ua == null) {
+ return null;
+ }
+
+ ua = ua.toLowerCase();
+
+ if (ua.indexOf("opera") != -1) {
+ return "opera";
+
+ } else if (ua.indexOf("webkit") != -1) {
+ return "safari";
+
+ } else if (ua.indexOf("msie") != -1) {
+ // GWT 2.0 uses document.documentMode here, which we can't do
+ // on the server side.
+
+ Matcher m = msie.matcher(ua);
+ if (m.matches() && m.groupCount() == 2) {
+ int v = makeVersion(m);
+ if (v >= 10000) {
+ return "ie10";
+ }
+ if (v >= 9000) {
+ return "ie9";
+ }
+ if (v >= 8000) {
+ return "ie8";
+ }
+ if (v >= 6000) {
+ return "ie6";
+ }
+ }
+ return null;
+
+ } else if (ua.indexOf("gecko") != -1) {
+ Matcher m = gecko.matcher(ua);
+ if (m.matches() && m.groupCount() == 2) {
+ if (makeVersion(m) >= 1008) {
+ return "gecko1_8";
+ }
+ }
+ return "gecko";
+ }
+
+ return null;
+ }
+
+ private int makeVersion(Matcher result) {
+ return (Integer.parseInt(result.group(1)) * 1000)
+ + Integer.parseInt(result.group(2));
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml
new file mode 100644
index 0000000000..0df89283a3
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/Progress.gwt.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<module>
+ <inherits name='com.google.gwt.resources.Resources'/>
+ <inherits name="com.google.gwt.user.User"/>
+</module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java
new file mode 100644
index 0000000000..5e13f55b76
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.progress.client;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Label;
+
+/**
+ * A simple progress bar with a text label.
+ * <p>
+ * The bar is 200 pixels wide and 20 pixels high. To keep the implementation
+ * simple and lightweight this dimensions are fixed and shouldn't be modified by
+ * style overrides in client code or CSS.
+ */
+public class ProgressBar extends Composite {
+ static {
+ ProgressResources.I.css().ensureInjected();
+ }
+
+ private final String callerText;
+ private final Label bar;
+ private final Label msg;
+ private int value;
+
+ /** Create a bar with no message text. */
+ public ProgressBar() {
+ this("");
+ }
+
+ /** Create a bar displaying the specified message. */
+ public ProgressBar(final String text) {
+ if (text == null || text.length() == 0) {
+ callerText = "";
+ } else {
+ callerText = text + " ";
+ }
+
+ final FlowPanel body = new FlowPanel();
+ body.setStyleName(ProgressResources.I.css().container());
+
+ msg = new Label(callerText);
+ msg.setStyleName(ProgressResources.I.css().text());
+ body.add(msg);
+
+ bar = new Label("");
+ bar.setStyleName(ProgressResources.I.css().bar());
+ body.add(bar);
+
+ initWidget(body);
+ }
+
+ /** @return the current value of the progress meter. */
+ public int getValue() {
+ return value;
+ }
+
+ /** Update the bar's percent completion. */
+ public void setValue(final int pComplete) {
+ assert 0 <= pComplete && pComplete <= 100;
+ value = pComplete;
+ bar.setWidth("" + (2 * pComplete) + "px");
+ msg.setText(callerText + pComplete + "%");
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java
new file mode 100644
index 0000000000..9de2748f90
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressCss.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.progress.client;
+
+import com.google.gwt.resources.client.CssResource;
+
+public interface ProgressCss extends CssResource {
+ String container();
+ String text();
+ String bar();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
new file mode 100644
index 0000000000..0276e9a608
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressResources.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.progress.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+
+public interface ProgressResources extends ClientBundle {
+ public static final ProgressResources I = GWT.create(ProgressResources.class);
+
+ @Source("progress.css")
+ ProgressCss css();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css
new file mode 100644
index 0000000000..683396e6a1
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/progress.css
@@ -0,0 +1,43 @@
+/* Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.container {
+ position: relative;
+ border: 1px solid #6B90DA;
+ height: 20px;
+ width: 200px;
+}
+
+.text {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ z-index: 2;
+ width: 200px;
+ padding-bottom: 3px;
+ text-align: center;
+ font-weight: bold;
+ font-style: italic;
+ font-size: smaller;
+}
+
+.bar {
+ background: #F0F7F9;
+ border-right: 1px solid #D0D7D9;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 20px;
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
new file mode 100644
index 0000000000..0df89283a3
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/SafeHtml.gwt.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<module>
+ <inherits name='com.google.gwt.resources.Resources'/>
+ <inherits name="com.google.gwt.user.User"/>
+</module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
new file mode 100644
index 0000000000..46d7f51ead
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/** Lightweight map of names/values for element attribute construction. */
+class AttMap {
+ private static final Tag ANY = new AnyTag();
+ private static final HashMap<String, Tag> TAGS;
+ static {
+ final Tag src = new SrcTag();
+ TAGS = new HashMap<String, Tag>();
+ TAGS.put("a", new AnchorTag());
+ TAGS.put("form", new FormTag());
+ TAGS.put("img", src);
+ TAGS.put("script", src);
+ TAGS.put("frame", src);
+ }
+
+ private final ArrayList<String> names = new ArrayList<String>();
+ private final ArrayList<String> values = new ArrayList<String>();
+
+ private Tag tag = ANY;
+ private int live;
+
+ void reset(final String tagName) {
+ tag = TAGS.get(tagName.toLowerCase());
+ if (tag == null) {
+ tag = ANY;
+ }
+ live = 0;
+ }
+
+ void onto(final Buffer raw, final SafeHtmlBuilder esc) {
+ for (int i = 0; i < live; i++) {
+ final String v = values.get(i);
+ if (v.length() > 0) {
+ raw.append(" ");
+ raw.append(names.get(i));
+ raw.append("=\"");
+ esc.append(v);
+ raw.append("\"");
+ }
+ }
+ }
+
+ String get(String name) {
+ name = name.toLowerCase();
+
+ for (int i = 0; i < live; i++) {
+ if (name.equals(names.get(i))) {
+ return values.get(i);
+ }
+ }
+ return "";
+ }
+
+ void set(String name, final String value) {
+ name = name.toLowerCase();
+ tag.assertSafe(name, value);
+
+ for (int i = 0; i < live; i++) {
+ if (name.equals(names.get(i))) {
+ values.set(i, value);
+ return;
+ }
+ }
+
+ final int i = live++;
+ if (names.size() < live) {
+ names.add(name);
+ values.add(value);
+ } else {
+ names.set(i, name);
+ values.set(i, value);
+ }
+ }
+
+ private static void assertNotJavascriptUrl(final String value) {
+ if (value.startsWith("#")) {
+ // common in GWT, and safe, so bypass further checks
+
+ } else if (value.trim().toLowerCase().startsWith("javascript:")) {
+ // possibly unsafe, we could have random user code here
+ // we can't tell if its safe or not so we refuse to accept
+ //
+ throw new RuntimeException("javascript unsafe in href: " + value);
+ }
+ }
+
+ private static interface Tag {
+ void assertSafe(String name, String value);
+ }
+
+ private static class AnyTag implements Tag {
+ public void assertSafe(String name, String value) {
+ }
+ }
+
+ private static class AnchorTag implements Tag {
+ public void assertSafe(String name, String value) {
+ if ("href".equals(name)) {
+ assertNotJavascriptUrl(value);
+ }
+ }
+ }
+
+ private static class FormTag implements Tag {
+ public void assertSafe(String name, String value) {
+ if ("action".equals(name)) {
+ assertNotJavascriptUrl(value);
+ }
+ }
+ }
+
+ private static class SrcTag implements Tag {
+ public void assertSafe(String name, String value) {
+ if ("src".equals(name)) {
+ assertNotJavascriptUrl(value);
+ }
+ }
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
new file mode 100644
index 0000000000..d79c580623
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+interface Buffer {
+ void append(boolean v);
+
+ void append(char v);
+
+ void append(int v);
+
+ void append(long v);
+
+ void append(float v);
+
+ void append(double v);
+
+ void append(String v);
+
+ String toString();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
new file mode 100644
index 0000000000..a1801ad038
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+final class BufferDirect implements Buffer {
+ private final StringBuilder strbuf = new StringBuilder();
+
+ boolean isEmpty() {
+ return strbuf.length() == 0;
+ }
+
+ public void append(final boolean v) {
+ strbuf.append(v);
+ }
+
+ public void append(final char v) {
+ strbuf.append(v);
+ }
+
+ public void append(final int v) {
+ strbuf.append(v);
+ }
+
+ public void append(final long v) {
+ strbuf.append(v);
+ }
+
+ public void append(final float v) {
+ strbuf.append(v);
+ }
+
+ public void append(final double v) {
+ strbuf.append(v);
+ }
+
+ public void append(final String v) {
+ strbuf.append(v);
+ }
+
+ @Override
+ public String toString() {
+ return strbuf.toString();
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
new file mode 100644
index 0000000000..6b5346d8d2
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+final class BufferSealElement implements Buffer {
+ private final SafeHtmlBuilder shb;
+
+ BufferSealElement(final SafeHtmlBuilder safeHtmlBuilder) {
+ shb = safeHtmlBuilder;
+ }
+
+ public void append(final boolean v) {
+ shb.sealElement().append(v);
+ }
+
+ public void append(final char v) {
+ shb.sealElement().append(v);
+ }
+
+ public void append(final double v) {
+ shb.sealElement().append(v);
+ }
+
+ public void append(final float v) {
+ shb.sealElement().append(v);
+ }
+
+ public void append(final int v) {
+ shb.sealElement().append(v);
+ }
+
+ public void append(final long v) {
+ shb.sealElement().append(v);
+ }
+
+ public void append(final String v) {
+ shb.sealElement().append(v);
+ }
+
+ @Override
+ public String toString() {
+ return shb.sealElement().toString();
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
new file mode 100644
index 0000000000..f7bc907bf6
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import com.google.gwt.regexp.shared.RegExp;
+
+/** A Find/Replace pair used against the {@link SafeHtml} block of text. */
+public interface FindReplace {
+ /**
+ * @return regular expression to match substrings with; should be treated as
+ * immutable.
+ */
+ public RegExp pattern();
+
+ /**
+ * Find and replace a single instance of this pattern in an input.
+ * <p>
+ * <b>WARNING:</b> No XSS sanitization is done on the return value of this
+ * method, e.g. this value may be passed directly to
+ * {@link SafeHtml#replaceAll(String, String)}. Implementations must sanitize output
+ * appropriately.
+ *
+ * @param input input string.
+ * @return result of regular expression replacement.
+ * @throws IllegalArgumentException if the input could not be safely sanitized.
+ */
+ public String replace(String input);
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
new file mode 100644
index 0000000000..e2c576bce3
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+import java.util.ArrayList;
+
+/**
+ * A suggestion oracle that tries to highlight the matched text.
+ * <p>
+ * Suggestions supplied by the implementation of
+ * {@link #onRequestSuggestions(Request, Callback)} are modified to wrap all
+ * occurrences of the {@link SuggestOracle.Request#getQuery()} substring in HTML
+ * <code>&lt;strong&gt;</code> tags, so they can be emphasized to the user.
+ */
+public abstract class HighlightSuggestOracle extends SuggestOracle {
+ private static String escape(String ds) {
+ return new SafeHtmlBuilder().append(ds).asString();
+ }
+
+ @Override
+ public final boolean isDisplayStringHTML() {
+ return true;
+ }
+
+ @Override
+ public final void requestSuggestions(final Request request, final Callback cb) {
+ onRequestSuggestions(request, new Callback() {
+ public void onSuggestionsReady(final Request request,
+ final Response response) {
+ final String qpat = getQueryPattern(request.getQuery());
+ final boolean html = isHTML();
+ final ArrayList<Suggestion> r = new ArrayList<Suggestion>();
+ for (final Suggestion s : response.getSuggestions()) {
+ r.add(new BoldSuggestion(qpat, s, html));
+ }
+ cb.onSuggestionsReady(request, new Response(r));
+ }
+ });
+ }
+
+ protected String getQueryPattern(final String query) {
+ return "(" + escape(query) + ")";
+ }
+
+ /**
+ * @return true if {@link SuggestOracle.Suggestion#getDisplayString()} returns
+ * HTML; false if the text must be escaped before evaluating in an
+ * HTML like context.
+ */
+ protected boolean isHTML() {
+ return false;
+ }
+
+ /** Compute the suggestions and return them for display. */
+ protected abstract void onRequestSuggestions(Request request, Callback done);
+
+ private static class BoldSuggestion implements Suggestion {
+ private final Suggestion suggestion;
+ private final String displayString;
+
+ BoldSuggestion(final String qstr, final Suggestion s, final boolean html) {
+ suggestion = s;
+
+ String ds = s.getDisplayString();
+ if (!html) {
+ ds = escape(ds);
+ }
+ displayString = sgi(ds, qstr, "<strong>$1</strong>");
+ }
+
+ private static native String sgi(String inString, String pat, String newHtml)
+ /*-{ return inString.replace(RegExp(pat, 'gi'), newHtml); }-*/;
+
+ public String getDisplayString() {
+ return displayString;
+ }
+
+ public String getReplacementString() {
+ return suggestion.getReplacementString();
+ }
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java
new file mode 100644
index 0000000000..eaa4f23030
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/LinkFindReplace.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import com.google.gwt.regexp.shared.RegExp;
+
+/**
+ * A Find/Replace pair whose replacement string is a link.
+ * <p>
+ * It is safe to pass arbitrary user-provided links to this class. Links are
+ * sanitized as follows:
+ * <ul>
+ * <li>Only http(s) and mailto links are supported; any other scheme results in
+ * an {@link IllegalArgumentException} from {@link #replace(String)}.
+ * <li>Special characters in the link after regex replacement are escaped with
+ * {@link SafeHtmlBuilder}.</li>
+ * </ul>
+ */
+public class LinkFindReplace implements FindReplace {
+ public static boolean hasValidScheme(String link) {
+ int colon = link.indexOf(':');
+ if (colon < 0) {
+ return true;
+ }
+ String scheme = link.substring(0, colon);
+ return "http".equalsIgnoreCase(scheme)
+ || "https".equalsIgnoreCase(scheme)
+ || "mailto".equalsIgnoreCase(scheme);
+ }
+
+ private RegExp pat;
+ private String link;
+
+ protected LinkFindReplace() {
+ }
+
+ /**
+ * @param regex regular expression pattern to match substrings with.
+ * @param repl replacement link href. Capture groups within
+ * <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+ */
+ public LinkFindReplace(String find, String link) {
+ this.pat = RegExp.compile(find);
+ this.link = link;
+ }
+
+ @Override
+ public RegExp pattern() {
+ return pat;
+ }
+
+ @Override
+ public String replace(String input) {
+ String href = pat.replace(input, link);
+ if (!hasValidScheme(href)) {
+ throw new IllegalArgumentException(
+ "Invalid scheme (" + toString() + "): " + href);
+ }
+ String result = new SafeHtmlBuilder()
+ .openAnchor()
+ .setAttribute("href", href)
+ .append(SafeHtml.asis(input))
+ .closeAnchor()
+ .asString();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "find = " + pat.getSource() + ", link = " + link;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java
new file mode 100644
index 0000000000..d22fef6e76
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/RawFindReplace.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import com.google.gwt.regexp.shared.RegExp;
+
+/**
+ * A Find/Replace pair whose replacement string is arbitrary HTML.
+ * <p>
+ * <b>WARNING:</b> This class is not safe used with user-provided patterns.
+ */
+public class RawFindReplace implements FindReplace {
+ private RegExp pat;
+ private String replace;
+
+ protected RawFindReplace() {
+ }
+
+ /**
+ * @param regex regular expression pattern to match substrings with.
+ * @param repl replacement expression. Capture groups within
+ * <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+ */
+ public RawFindReplace(String find, String replace) {
+ this.pat = RegExp.compile(find);
+ this.replace = replace;
+ }
+
+ @Override
+ public RegExp pattern() {
+ return pat;
+ }
+
+ @Override
+ public String replace(String input) {
+ return pat.replace(input, replace);
+ }
+
+ @Override
+ public String toString() {
+ return "find = " + pat.getSource() + ", replace = " + replace;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
new file mode 100644
index 0000000000..0a9f7a2ed9
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
@@ -0,0 +1,302 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.regexp.shared.MatchResult;
+import com.google.gwt.regexp.shared.RegExp;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HTMLTable;
+import com.google.gwt.user.client.ui.HasHTML;
+import com.google.gwt.user.client.ui.InlineHTML;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.Iterator;
+import java.util.List;
+
+/** Immutable string safely placed as HTML without further escaping. */
+public abstract class SafeHtml {
+ public static final SafeHtmlResources RESOURCES;
+
+ static {
+ if (GWT.isClient()) {
+ RESOURCES = GWT.create(SafeHtmlResources.class);
+ RESOURCES.css().ensureInjected();
+
+ } else {
+ RESOURCES = new SafeHtmlResources() {
+ @Override
+ public SafeHtmlCss css() {
+ return new SafeHtmlCss() {
+ public String wikiList() {
+ return "wikiList";
+ }
+
+ public String wikiPreFormat() {
+ return "wikiPreFormat";
+ }
+
+ public boolean ensureInjected() {
+ return false;
+ }
+
+ public String getName() {
+ return null;
+ }
+
+ public String getText() {
+ return null;
+ }
+ };
+ }
+ };
+ }
+ }
+
+ /** @return the existing HTML property of a widget. */
+ public static SafeHtml get(final HasHTML t) {
+ return new SafeHtmlString(t.getHTML());
+ }
+
+ /** @return the existing HTML text, wrapped in a safe buffer. */
+ public static SafeHtml asis(final String htmlText) {
+ return new SafeHtmlString(htmlText);
+ }
+
+ /** Set the HTML property of a widget. */
+ public static <T extends HasHTML> T set(final T e, final SafeHtml str) {
+ e.setHTML(str.asString());
+ return e;
+ }
+
+ /** @return the existing inner HTML of any element. */
+ public static SafeHtml get(final Element e) {
+ return new SafeHtmlString(DOM.getInnerHTML(e));
+ }
+
+ /** Set the inner HTML of any element. */
+ public static Element set(final Element e, final SafeHtml str) {
+ DOM.setInnerHTML(e, str.asString());
+ return e;
+ }
+
+ /** @return the existing inner HTML of a table cell. */
+ public static SafeHtml get(final HTMLTable t, final int row, final int col) {
+ return new SafeHtmlString(t.getHTML(row, col));
+ }
+
+ /** Set the inner HTML of a table cell. */
+ public static <T extends HTMLTable> T set(final T t, final int row,
+ final int col, final SafeHtml str) {
+ t.setHTML(row, col, str.asString());
+ return t;
+ }
+
+ /** Parse an HTML block and return the first (typically root) element. */
+ public static Element parse(final SafeHtml str) {
+ return DOM.getFirstChild(set(DOM.createDiv(), str));
+ }
+
+ /** Convert bare http:// and https:// URLs into &lt;a href&gt; tags. */
+ public SafeHtml linkify() {
+ final String part = "(?:" +
+ "[a-zA-Z0-9$_.+!*',%;:@=?#/~-]" +
+ "|&(?!lt;|gt;)" +
+ ")";
+ return replaceAll(
+ "(https?://" +
+ part + "{2,}" +
+ "(?:[(]" + part + "*" + "[)])*" +
+ part + "*" +
+ ")",
+ "<a href=\"$1\" target=\"_blank\">$1</a>");
+ }
+
+ /**
+ * Apply {@link #linkify()}, and "\n\n" to &lt;p&gt;.
+ * <p>
+ * Lines that start with whitespace are assumed to be preformatted, and are
+ * formatted by the {@link SafeHtmlCss#wikiPreFormat()} CSS class.
+ */
+ public SafeHtml wikify() {
+ final SafeHtmlBuilder r = new SafeHtmlBuilder();
+ for (final String p : linkify().asString().split("\n\n")) {
+ if (isPreFormat(p)) {
+ r.openElement("p");
+ for (final String line : p.split("\n")) {
+ r.openSpan();
+ r.setStyleName(RESOURCES.css().wikiPreFormat());
+ r.append(asis(line));
+ r.closeSpan();
+ r.br();
+ }
+ r.closeElement("p");
+
+ } else if (isList(p)) {
+ wikifyList(r, p);
+
+ } else {
+ r.openElement("p");
+ r.append(asis(p));
+ r.closeElement("p");
+ }
+ }
+ return r.toSafeHtml();
+ }
+
+ private void wikifyList(final SafeHtmlBuilder r, final String p) {
+ boolean in_ul = false;
+ boolean in_p = false;
+ for (String line : p.split("\n")) {
+ if (line.startsWith("-") || line.startsWith("*")) {
+ if (!in_ul) {
+ if (in_p) {
+ in_p = false;
+ r.closeElement("p");
+ }
+
+ in_ul = true;
+ r.openElement("ul");
+ r.setStyleName(RESOURCES.css().wikiList());
+ }
+ line = line.substring(1).trim();
+
+ } else if (!in_ul) {
+ if (!in_p) {
+ in_p = true;
+ r.openElement("p");
+ } else {
+ r.append(' ');
+ }
+ r.append(asis(line));
+ continue;
+ }
+
+ r.openElement("li");
+ r.append(asis(line));
+ r.closeElement("li");
+ }
+
+ if (in_ul) {
+ r.closeElement("ul");
+ } else if (in_p) {
+ r.closeElement("p");
+ }
+ }
+
+ private static boolean isPreFormat(final String p) {
+ return p.contains("\n ") || p.contains("\n\t") || p.startsWith(" ")
+ || p.startsWith("\t");
+ }
+
+ private static boolean isList(final String p) {
+ return p.contains("\n- ") || p.contains("\n* ") || p.startsWith("- ")
+ || p.startsWith("* ");
+ }
+
+ /**
+ * Replace first occurrence of <code>regex</code> with <code>repl</code> .
+ * <p>
+ * <b>WARNING:</b> This replacement is being performed against an otherwise
+ * safe HTML string. The caller must ensure that the replacement does not
+ * introduce cross-site scripting attack entry points.
+ *
+ * @param regex regular expression pattern to match the substring with.
+ * @param repl replacement expression. Capture groups within
+ * <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+ * @return a new string, after the replacement has been made.
+ */
+ public SafeHtml replaceFirst(final String regex, final String repl) {
+ return new SafeHtmlString(asString().replaceFirst(regex, repl));
+ }
+
+ /**
+ * Replace each occurrence of <code>regex</code> with <code>repl</code> .
+ * <p>
+ * <b>WARNING:</b> This replacement is being performed against an otherwise
+ * safe HTML string. The caller must ensure that the replacement does not
+ * introduce cross-site scripting attack entry points.
+ *
+ * @param regex regular expression pattern to match substrings with.
+ * @param repl replacement expression. Capture groups within
+ * <code>regex</code> can be referenced with <code>$<i>n</i></code>.
+ * @return a new string, after the replacements have been made.
+ */
+ public SafeHtml replaceAll(final String regex, final String repl) {
+ return new SafeHtmlString(asString().replaceAll(regex, repl));
+ }
+
+ /**
+ * Replace all find/replace pairs in the list in a single pass.
+ *
+ * @param findReplaceList find/replace pairs to use.
+ * @return a new string, after the replacements have been made.
+ */
+ public <T> SafeHtml replaceAll(List<? extends FindReplace> findReplaceList) {
+ if (findReplaceList == null || findReplaceList.isEmpty()) {
+ return this;
+ }
+
+ StringBuilder pat = new StringBuilder();
+ Iterator<? extends FindReplace> it = findReplaceList.iterator();
+ while (it.hasNext()) {
+ FindReplace fr = it.next();
+ pat.append(fr.pattern().getSource());
+ if (it.hasNext()) {
+ pat.append('|');
+ }
+ }
+
+ StringBuilder result = new StringBuilder();
+ RegExp re = RegExp.compile(pat.toString(), "g");
+ String orig = asString();
+ int index = 0;
+ MatchResult mat;
+ while ((mat = re.exec(orig)) != null) {
+ String g = mat.getGroup(0);
+ // Re-run each candidate to find which one matched.
+ for (FindReplace fr : findReplaceList) {
+ if (fr.pattern().test(g)) {
+ try {
+ String repl = fr.replace(g);
+ result.append(orig.substring(index, mat.getIndex()));
+ result.append(repl);
+ } catch (IllegalArgumentException e) {
+ continue;
+ }
+ index = mat.getIndex() + g.length();
+ break;
+ }
+ }
+ }
+ result.append(orig.substring(index, orig.length()));
+ return asis(result.toString());
+ }
+
+ /** @return a GWT block display widget displaying this HTML. */
+ public Widget toBlockWidget() {
+ return new HTML(asString());
+ }
+
+ /** @return a GWT inline display widget displaying this HTML. */
+ public Widget toInlineWidget() {
+ return new InlineHTML(asString());
+ }
+
+ /** @return a clean HTML string safe for inclusion in any context. */
+ public abstract String asString();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
new file mode 100644
index 0000000000..9fe3267e7b
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
@@ -0,0 +1,411 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import com.google.gwt.core.client.GWT;
+
+/**
+ * Safely constructs a {@link SafeHtml}, escaping user provided content.
+ */
+public class SafeHtmlBuilder extends SafeHtml {
+ private static final Impl impl;
+
+ static {
+ if (GWT.isClient()) {
+ impl = new ClientImpl();
+ } else {
+ impl = new ServerImpl();
+ }
+ }
+
+ private final BufferDirect dBuf;
+ private Buffer cb;
+
+ private BufferSealElement sBuf;
+ private AttMap att;
+
+ public SafeHtmlBuilder() {
+ cb = dBuf = new BufferDirect();
+ }
+
+ /** @return true if this builder has not had an append occur yet. */
+ public boolean isEmpty() {
+ return dBuf.isEmpty();
+ }
+
+ /** @return true if this builder has content appended into it. */
+ public boolean hasContent() {
+ return !isEmpty();
+ }
+
+ public SafeHtmlBuilder append(final boolean in) {
+ cb.append(in);
+ return this;
+ }
+
+ public SafeHtmlBuilder append(final char in) {
+ switch (in) {
+ case '&':
+ cb.append("&amp;");
+ break;
+
+ case '>':
+ cb.append("&gt;");
+ break;
+
+ case '<':
+ cb.append("&lt;");
+ break;
+
+ case '"':
+ cb.append("&quot;");
+ break;
+
+ case '\'':
+ cb.append("&#39;");
+ break;
+
+ default:
+ cb.append(in);
+ break;
+ }
+ return this;
+ }
+
+ public SafeHtmlBuilder append(final int in) {
+ cb.append(in);
+ return this;
+ }
+
+ public SafeHtmlBuilder append(final long in) {
+ cb.append(in);
+ return this;
+ }
+
+ public SafeHtmlBuilder append(final float in) {
+ cb.append(in);
+ return this;
+ }
+
+ public SafeHtmlBuilder append(final double in) {
+ cb.append(in);
+ return this;
+ }
+
+ /** Append already safe HTML as-is, avoiding double escaping. */
+ public SafeHtmlBuilder append(final SafeHtml in) {
+ if (in != null) {
+ cb.append(in.asString());
+ }
+ return this;
+ }
+
+ /** Append the string, escaping unsafe characters. */
+ public SafeHtmlBuilder append(final String in) {
+ if (in != null) {
+ impl.escapeStr(this, in);
+ }
+ return this;
+ }
+
+ /** Append the string, escaping unsafe characters. */
+ public SafeHtmlBuilder append(final StringBuilder in) {
+ if (in != null) {
+ append(in.toString());
+ }
+ return this;
+ }
+
+ /** Append the string, escaping unsafe characters. */
+ public SafeHtmlBuilder append(final StringBuffer in) {
+ if (in != null) {
+ append(in.toString());
+ }
+ return this;
+ }
+
+ /** Append the result of toString(), escaping unsafe characters. */
+ public SafeHtmlBuilder append(final Object in) {
+ if (in != null) {
+ append(in.toString());
+ }
+ return this;
+ }
+
+ /** Append the string, escaping unsafe characters. */
+ public SafeHtmlBuilder append(final CharSequence in) {
+ if (in != null) {
+ escapeCS(this, in);
+ }
+ return this;
+ }
+
+ /**
+ * Open an element, appending "<tagName>" to the buffer.
+ * <p>
+ * After the element is open the attributes may be manipulated until the next
+ * <code>append</code>, <code>openElement</code>, <code>closeSelf</code> or
+ * <code>closeElement</code> call.
+ *
+ * @param tagName name of the HTML element to open.
+ */
+ public SafeHtmlBuilder openElement(final String tagName) {
+ assert isElementName(tagName);
+ cb.append("<");
+ cb.append(tagName);
+ if (sBuf == null) {
+ att = new AttMap();
+ sBuf = new BufferSealElement(this);
+ }
+ att.reset(tagName);
+ cb = sBuf;
+ return this;
+ }
+
+ /**
+ * Get an attribute of the last opened element.
+ *
+ * @param name name of the attribute to read.
+ * @return the attribute value, as a string. The empty string if the attribute
+ * has not been assigned a value. The returned string is the raw
+ * (unescaped) value.
+ */
+ public String getAttribute(final String name) {
+ assert isAttributeName(name);
+ assert cb == sBuf;
+ return att.get(name);
+ }
+
+ /**
+ * Set an attribute of the last opened element.
+ *
+ * @param name name of the attribute to set.
+ * @param value value to assign; any existing value is replaced. The value is
+ * escaped (if necessary) during the assignment.
+ */
+ public SafeHtmlBuilder setAttribute(final String name, final String value) {
+ assert isAttributeName(name);
+ assert cb == sBuf;
+ att.set(name, value != null ? value : "");
+ return this;
+ }
+
+ /**
+ * Set an attribute of the last opened element.
+ *
+ * @param name name of the attribute to set.
+ * @param value value to assign, any existing value is replaced.
+ */
+ public SafeHtmlBuilder setAttribute(final String name, final int value) {
+ return setAttribute(name, String.valueOf(value));
+ }
+
+ /**
+ * Append a new value into a whitespace delimited attribute.
+ * <p>
+ * If the attribute is not yet assigned, this method sets the attribute. If
+ * the attribute is already assigned, the new value is appended onto the end,
+ * after appending a single space to delimit the values.
+ *
+ * @param name name of the attribute to append onto.
+ * @param value additional value to append.
+ */
+ public SafeHtmlBuilder appendAttribute(final String name, String value) {
+ if (value != null && value.length() > 0) {
+ final String e = getAttribute(name);
+ return setAttribute(name, e.length() > 0 ? e + " " + value : value);
+ }
+ return this;
+ }
+
+ /** Set the height attribute of the current element. */
+ public SafeHtmlBuilder setHeight(final int height) {
+ return setAttribute("height", height);
+ }
+
+ /** Set the width attribute of the current element. */
+ public SafeHtmlBuilder setWidth(final int width) {
+ return setAttribute("width", width);
+ }
+
+ /** Set the CSS class name for this element. */
+ public SafeHtmlBuilder setStyleName(final String style) {
+ assert isCssName(style);
+ return setAttribute("class", style);
+ }
+
+ /**
+ * Add an additional CSS class name to this element.
+ *<p>
+ * If no CSS class name has been specified yet, this method initializes it to
+ * the single name.
+ */
+ public SafeHtmlBuilder addStyleName(final String style) {
+ assert isCssName(style);
+ return appendAttribute("class", style);
+ }
+
+ private void sealElement0() {
+ assert cb == sBuf;
+ cb = dBuf;
+ att.onto(cb, this);
+ }
+
+ Buffer sealElement() {
+ sealElement0();
+ cb.append(">");
+ return cb;
+ }
+
+ /** Close the current element with a self closing suffix ("/ &gt;"). */
+ public SafeHtmlBuilder closeSelf() {
+ sealElement0();
+ cb.append(" />");
+ return this;
+ }
+
+ /** Append a closing tag for the named element. */
+ public SafeHtmlBuilder closeElement(final String name) {
+ assert isElementName(name);
+ cb.append("</");
+ cb.append(name);
+ cb.append(">");
+ return this;
+ }
+
+ /** Append "&amp;nbsp;" - a non-breaking space, useful in empty table cells. */
+ public SafeHtmlBuilder nbsp() {
+ cb.append("&nbsp;");
+ return this;
+ }
+
+ /** Append "&lt;br /&gt;" - a line break with no attributes */
+ public SafeHtmlBuilder br() {
+ cb.append("<br />");
+ return this;
+ }
+
+ /** Append "&lt;tr&gt;"; attributes may be set if needed */
+ public SafeHtmlBuilder openTr() {
+ return openElement("tr");
+ }
+
+ /** Append "&lt;/tr&gt;" */
+ public SafeHtmlBuilder closeTr() {
+ return closeElement("tr");
+ }
+
+ /** Append "&lt;td&gt;"; attributes may be set if needed */
+ public SafeHtmlBuilder openTd() {
+ return openElement("td");
+ }
+
+ /** Append "&lt;/td&gt;" */
+ public SafeHtmlBuilder closeTd() {
+ return closeElement("td");
+ }
+
+ /** Append "&lt;div&gt;"; attributes may be set if needed */
+ public SafeHtmlBuilder openDiv() {
+ return openElement("div");
+ }
+
+ /** Append "&lt;/div&gt;" */
+ public SafeHtmlBuilder closeDiv() {
+ return closeElement("div");
+ }
+
+ /** Append "&lt;span&gt;"; attributes may be set if needed */
+ public SafeHtmlBuilder openSpan() {
+ return openElement("span");
+ }
+
+ /** Append "&lt;/span&gt;" */
+ public SafeHtmlBuilder closeSpan() {
+ return closeElement("span");
+ }
+
+ /** Append "&lt;a&gt;"; attributes may be set if needed */
+ public SafeHtmlBuilder openAnchor() {
+ return openElement("a");
+ }
+
+ /** Append "&lt;/a&gt;" */
+ public SafeHtmlBuilder closeAnchor() {
+ return closeElement("a");
+ }
+
+ /** Append "&lt;param name=... value=... /&gt;". */
+ public SafeHtmlBuilder paramElement(final String name, final String value) {
+ openElement("param");
+ setAttribute("name", name);
+ setAttribute("value", value);
+ return closeSelf();
+ }
+
+ /** @return an immutable {@link SafeHtml} representation of the buffer. */
+ public SafeHtml toSafeHtml() {
+ return new SafeHtmlString(asString());
+ }
+
+ @Override
+ public String asString() {
+ return cb.toString();
+ }
+
+ private static void escapeCS(final SafeHtmlBuilder b, final CharSequence in) {
+ for (int i = 0; i < in.length(); i++) {
+ b.append(in.charAt(i));
+ }
+ }
+
+ private static boolean isElementName(final String name) {
+ return name.matches("^[a-zA-Z][a-zA-Z0-9_-]*$");
+ }
+
+ private static boolean isAttributeName(final String name) {
+ return isElementName(name);
+ }
+
+ private static boolean isCssName(final String name) {
+ return isElementName(name);
+ }
+
+ private static abstract class Impl {
+ abstract void escapeStr(SafeHtmlBuilder b, String in);
+ }
+
+ private static class ServerImpl extends Impl {
+ @Override
+ void escapeStr(final SafeHtmlBuilder b, final String in) {
+ SafeHtmlBuilder.escapeCS(b, in);
+ }
+ }
+
+ private static class ClientImpl extends Impl {
+ @Override
+ void escapeStr(final SafeHtmlBuilder b, final String in) {
+ b.cb.append(escape(in));
+ }
+
+ private static native String escape(String src)
+ /*-{ return src.replace(/&/g,'&amp;')
+ .replace(/>/g,'&gt;')
+ .replace(/</g,'&lt;')
+ .replace(/"/g,'&quot;')
+ .replace(/'/g,'&#39;');
+ }-*/;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
new file mode 100644
index 0000000000..f6836a088d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlCss.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import com.google.gwt.resources.client.CssResource;
+
+public interface SafeHtmlCss extends CssResource {
+ String wikiPreFormat();
+ String wikiList();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
new file mode 100644
index 0000000000..e3f5724d61
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlResources.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import com.google.gwt.resources.client.ClientBundle;
+
+public interface SafeHtmlResources extends ClientBundle {
+ @Source("safehtml.css")
+ SafeHtmlCss css();
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
new file mode 100644
index 0000000000..a229421d9b
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+class SafeHtmlString extends SafeHtml {
+ private final String html;
+
+ SafeHtmlString(final String h) {
+ html = h;
+ }
+
+ @Override
+ public String asString() {
+ return html;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css
new file mode 100644
index 0000000000..fcad92c47a
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/safehtml.css
@@ -0,0 +1,23 @@
+/* Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.wikiPreFormat {
+ white-space: pre;
+ font-family: 'Lucida Console', 'Lucida Sans Typewriter', Monaco, monospace;
+ font-size: small;
+}
+
+.wikiList {
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
new file mode 100644
index 0000000000..c4d681f900
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.server;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Forces GWT resources to cache for a very long time.
+ * <p>
+ * GWT compiled JavaScript and ImageBundles can be cached indefinitely by a
+ * browser and/or an edge proxy, as they never contain user-specific data and
+ * are named by a unique checksum. If their content is ever modified then the
+ * URL changes, so user agents would request a different resource. We force
+ * these resources to have very long expiration times.
+ * <p>
+ * To use, add the following block to your <code>web.xml</code>:
+ *
+ * <pre>
+ * &lt;filter&gt;
+ * &lt;filter-name&gt;CacheControl&lt;/filter-name&gt;
+ * &lt;filter-class&gt;com.google.gwtexpui.server.CacheControlFilter&lt;/filter-class&gt;
+ * &lt;/filter&gt;
+ * &lt;filter-mapping&gt;
+ * &lt;filter-name&gt;CacheControl&lt;/filter-name&gt;
+ * &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
+ * &lt;/filter-mapping&gt;
+ * </pre>
+ */
+public class CacheControlFilter implements Filter {
+ public void init(final FilterConfig config) {
+ }
+
+ public void destroy() {
+ }
+
+ public void doFilter(final ServletRequest sreq, final ServletResponse srsp,
+ final FilterChain chain) throws IOException, ServletException {
+ final HttpServletRequest req = (HttpServletRequest) sreq;
+ final HttpServletResponse rsp = (HttpServletResponse) srsp;
+ final String pathInfo = pathInfo(req);
+
+ if (cacheForever(pathInfo, req)) {
+ CacheHeaders.setCacheable(req, rsp, 365, TimeUnit.DAYS);
+ } else if (nocache(pathInfo)) {
+ CacheHeaders.setNotCacheable(rsp);
+ }
+
+ chain.doFilter(req, rsp);
+ }
+
+ private static boolean cacheForever(final String pathInfo,
+ final HttpServletRequest req) {
+ if (pathInfo.endsWith(".cache.html")) {
+ return true;
+ } else if (pathInfo.endsWith(".cache.gif")) {
+ return true;
+ } else if (pathInfo.endsWith(".cache.png")) {
+ return true;
+ } else if (pathInfo.endsWith(".cache.css")) {
+ return true;
+ } else if (pathInfo.endsWith(".cache.jar")) {
+ return true;
+ } else if (pathInfo.endsWith(".cache.swf")) {
+ return true;
+ } else if (pathInfo.endsWith(".nocache.js")) {
+ final String v = req.getParameter("content");
+ return v != null && v.length() > 20;
+ }
+ return false;
+ }
+
+ private static boolean nocache(final String pathInfo) {
+ if (pathInfo.endsWith(".nocache.js")) {
+ return true;
+ }
+ return false;
+ }
+
+ private static String pathInfo(final HttpServletRequest req) {
+ final String uri = req.getRequestURI();
+ final String ctx = req.getContextPath();
+ return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
new file mode 100644
index 0000000000..11409e8ff3
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.server;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Utilities to manage HTTP caching directives in responses. */
+public class CacheHeaders {
+ private static final long MAX_CACHE_DURATION = DAYS.toSeconds(365);
+
+ /**
+ * Do not cache the response, anywhere.
+ *
+ * @param res response being returned.
+ */
+ public static void setNotCacheable(HttpServletResponse res) {
+ String cc = "no-cache, no-store, max-age=0, must-revalidate";
+ res.setHeader("Cache-Control", cc);
+ res.setHeader("Pragma", "no-cache");
+ res.setHeader("Expires", "Fri, 01 Jan 1990 00:00:00 GMT");
+ res.setDateHeader("Date", System.currentTimeMillis());
+ }
+
+ /**
+ * Permit caching the response for up to the age specified.
+ * <p>
+ * If the request is on a secure connection (e.g. SSL) private caching is
+ * used. This allows the user-agent to cache the response, but requests
+ * intermediate proxies to not cache. This may offer better protection for
+ * Set-Cookie headers.
+ * <p>
+ * If the request is on plaintext (insecure), public caching is used. This may
+ * allow an intermediate proxy to cache the response, including any Set-Cookie
+ * header that may have also been included.
+ *
+ * @param req current request.
+ * @param res response being returned.
+ * @param age how long the response can be cached.
+ * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+ */
+ public static void setCacheable(
+ HttpServletRequest req, HttpServletResponse res,
+ long age, TimeUnit unit) {
+ if (req.isSecure()) {
+ setCacheablePrivate(res, age, unit);
+ } else {
+ setCacheablePublic(res, age, unit);
+ }
+ }
+
+ /**
+ * Allow the response to be cached by proxies and user-agents.
+ * <p>
+ * If the response includes a Set-Cookie header the cookie may be cached by a
+ * proxy and returned to multiple browsers behind the same proxy. This is
+ * insecure for authenticated connections.
+ *
+ * @param res response being returned.
+ * @param age how long the response can be cached.
+ * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+ */
+ public static void setCacheablePublic(HttpServletResponse res,
+ long age, TimeUnit unit) {
+ long now = System.currentTimeMillis();
+ long sec = maxAgeSeconds(age, unit);
+
+ res.setDateHeader("Expires", now + SECONDS.toMillis(sec));
+ res.setDateHeader("Date", now);
+ cache(res, "public", age, unit);
+ }
+
+ /**
+ * Allow the response to be cached only by the user-agent.
+ *
+ * @param res response being returned.
+ * @param age how long the response can be cached.
+ * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+ */
+ public static void setCacheablePrivate(HttpServletResponse res,
+ long age, TimeUnit unit) {
+ long now = System.currentTimeMillis();
+ res.setDateHeader("Expires", now);
+ res.setDateHeader("Date", now);
+ cache(res, "private", age, unit);
+ }
+
+ private static void cache(HttpServletResponse res,
+ String type, long age, TimeUnit unit) {
+ res.setHeader("Cache-Control", String.format(
+ "%s, max-age=%d",
+ type, maxAgeSeconds(age, unit)));
+ }
+
+ private static long maxAgeSeconds(long age, TimeUnit unit) {
+ return Math.min(unit.toSeconds(age), MAX_CACHE_DURATION);
+ }
+
+ private CacheHeaders() {
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
new file mode 100644
index 0000000000..c681d893bf
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
@@ -0,0 +1,27 @@
+<!--
+ Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<module>
+ <inherits name="com.google.gwt.user.User"/>
+
+ <replace-with class="com.google.gwtexpui.user.client.PluginSafeDialogBoxImplAutoHide">
+ <when-type-is class="com.google.gwtexpui.user.client.PluginSafeDialogBoxImpl" />
+ <any>
+ <when-property-is name="user.agent" value="safari"/>
+ <when-property-is name="user.agent" value="gecko"/>
+ <when-property-is name="user.agent" value="gecko1_8"/>
+ </any>
+ </replace-with>
+</module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java
new file mode 100644
index 0000000000..78ea8d607e
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Window;
+
+/** A DialogBox that automatically re-centers itself if the window changes */
+public class AutoCenterDialogBox extends PluginSafeDialogBox {
+ private HandlerRegistration recenter;
+
+ public AutoCenterDialogBox() {
+ this(false);
+ }
+
+ public AutoCenterDialogBox(final boolean autoHide) {
+ this(autoHide, true);
+ }
+
+ public AutoCenterDialogBox(final boolean autoHide, final boolean modal) {
+ super(autoHide, modal);
+ }
+
+ @Override
+ public void show() {
+ if (recenter == null) {
+ recenter = Window.addResizeHandler(new ResizeHandler() {
+ @Override
+ public void onResize(final ResizeEvent event) {
+ final int w = event.getWidth();
+ final int h = event.getHeight();
+ AutoCenterDialogBox.this.onResize(w, h);
+ }
+ });
+ }
+ super.show();
+ }
+
+ @Override
+ protected void onUnload() {
+ if (recenter != null) {
+ recenter.removeHandler();
+ recenter = null;
+ }
+ super.onUnload();
+ }
+
+ /**
+ * Invoked when the outer browser window resizes.
+ * <p>
+ * Subclasses may override (but should ensure they still call super.onResize)
+ * to implement custom logic when a window resize occurs.
+ *
+ * @param width new browser window width
+ * @param height new browser window height
+ */
+ protected void onResize(final int width, final int height) {
+ if (isAttached()) {
+ center();
+ }
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
new file mode 100644
index 0000000000..c6ab09a151
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.DialogBox;
+
+/**
+ * A DialogBox that can appear over Flash movies and Java applets.
+ * <p>
+ * Some browsers have issues with placing a &lt;div&gt; (such as that used by
+ * the DialogBox implementation) over top of native UI such as that used by the
+ * Flash plugin. Often the native UI leaks over top of the &lt;div&gt;, which is
+ * not the desired behavior for a dialog box.
+ * <p>
+ * This implementation hides the native resources by setting their display
+ * property to 'none' when the dialog is shown, and restores them back to their
+ * prior setting when the dialog is hidden.
+ * */
+public class PluginSafeDialogBox extends DialogBox {
+ private final PluginSafeDialogBoxImpl impl =
+ GWT.create(PluginSafeDialogBoxImpl.class);
+
+ public PluginSafeDialogBox() {
+ this(false);
+ }
+
+ public PluginSafeDialogBox(final boolean autoHide) {
+ this(autoHide, true);
+ }
+
+ public PluginSafeDialogBox(final boolean autoHide, final boolean modal) {
+ super(autoHide, modal);
+ }
+
+ @Override
+ public void setVisible(final boolean show) {
+ impl.visible(show);
+ super.setVisible(show);
+ }
+
+ @Override
+ public void show() {
+ impl.visible(true);
+ super.show();
+ }
+
+ @Override
+ public void hide(final boolean autoClosed) {
+ impl.visible(false);
+ super.hide(autoClosed);
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
new file mode 100644
index 0000000000..a32fc99fde
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+class PluginSafeDialogBoxImpl {
+ void visible(final boolean dialogVisible) {
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java
new file mode 100644
index 0000000000..e32fe78daa
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.user.client.ui.UIObject;
+
+import java.util.ArrayList;
+
+class PluginSafeDialogBoxImplAutoHide extends PluginSafeDialogBoxImpl {
+ private boolean hidden;
+ private ArrayList<HiddenElement> hiddenElements =
+ new ArrayList<HiddenElement>();
+
+ @Override
+ void visible(final boolean dialogVisible) {
+ if (dialogVisible) {
+ hideAll();
+ } else {
+ showAll();
+ }
+ }
+
+ private void hideAll() {
+ if (!hidden) {
+ hideSet(Document.get().getElementsByTagName("object"));
+ hideSet(Document.get().getElementsByTagName("embed"));
+ hideSet(Document.get().getElementsByTagName("applet"));
+ hidden = true;
+ }
+ }
+
+ private void hideSet(final NodeList<Element> all) {
+ for (int i = 0; i < all.getLength(); i++) {
+ final Element e = all.getItem(i);
+ if (UIObject.isVisible(e)) {
+ hiddenElements.add(new HiddenElement(e));
+ }
+ }
+ }
+
+ private void showAll() {
+ if (hidden) {
+ for (final HiddenElement e : hiddenElements) {
+ e.restore();
+ }
+ hiddenElements.clear();
+ hidden = false;
+ }
+ }
+
+ private static class HiddenElement {
+ private final Element element;
+ private final String visibility;
+
+ HiddenElement(final Element element) {
+ this.element = element;
+ this.visibility = getVisibility(element);
+ setVisibility(element, "hidden");
+ }
+
+ void restore() {
+ setVisibility(element, visibility);
+ }
+
+ private static native String getVisibility(Element elem)
+ /*-{ return elem.style.visibility; }-*/;
+
+ private static native void setVisibility(Element elem, String disp)
+ /*-{ elem.style.visibility = disp; }-*/;
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
new file mode 100644
index 0000000000..7d9c9fc6ac
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+/**
+ * A PopupPanel that can appear over Flash movies and Java applets.
+ * <p>
+ * Some browsers have issues with placing a &lt;div&gt; (such as that used by
+ * the PopupPanel implementation) over top of native UI such as that used by the
+ * Flash plugin. Often the native UI leaks over top of the &lt;div&gt;, which is
+ * not the desired behavior for a dialog box.
+ * <p>
+ * This implementation hides the native resources by setting their display
+ * property to 'none' when the dialog is shown, and restores them back to their
+ * prior setting when the dialog is hidden.
+ * */
+public class PluginSafePopupPanel extends PopupPanel {
+ private final PluginSafeDialogBoxImpl impl =
+ GWT.create(PluginSafeDialogBoxImpl.class);
+
+ public PluginSafePopupPanel() {
+ this(false);
+ }
+
+ public PluginSafePopupPanel(final boolean autoHide) {
+ this(autoHide, true);
+ }
+
+ public PluginSafePopupPanel(final boolean autoHide, final boolean modal) {
+ super(autoHide, modal);
+ }
+
+ @Override
+ public void setVisible(final boolean show) {
+ impl.visible(show);
+ super.setVisible(show);
+ }
+
+ @Override
+ public void show() {
+ impl.visible(true);
+ super.show();
+ }
+
+ @Override
+ public void hide(final boolean autoClosed) {
+ impl.visible(false);
+ super.hide(autoClosed);
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
new file mode 100644
index 0000000000..02ba9aeac5
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.Window;
+
+/**
+ * User agent feature tests we don't create permutations for.
+ * <p>
+ * Some features aren't worth creating full permutations in GWT for, as each new
+ * boolean permutation (only two settings) doubles the compile time required. If
+ * the setting only affects a couple of lines of JavaScript code, the slightly
+ * larger cache files for user agents that lack the functionality requested is
+ * trivial compared to the time developers lose building their application.
+ */
+public class UserAgent {
+ /** Does the browser have ShockwaveFlash plugin enabled? */
+ public static final boolean hasFlash = hasFlash();
+
+ private static native boolean hasFlash()
+ /*-{
+ if (navigator.plugins && navigator.plugins.length) {
+ if (navigator.plugins['Shockwave Flash']) return true;
+ if (navigator.plugins['Shockwave Flash 2.0']) return true;
+
+ } else if (navigator.mimeTypes && navigator.mimeTypes.length) {
+ var mimeType = navigator.mimeTypes['application/x-shockwave-flash'];
+ if (mimeType && mimeType.enabledPlugin) return true;
+
+ } else {
+ try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.7'); return true; } catch (e) {}
+ try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6'); return true; } catch (e) {}
+ try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); return true; } catch (e) {}
+ }
+ return false;
+ }-*/;
+
+ /**
+ * Test for and disallow running this application in an &lt;iframe&gt;.
+ * <p>
+ * If the application is running within an iframe this method requests a
+ * browser generated redirect to pop the application out of the iframe into
+ * the top level window, and then aborts execution by throwing an exception.
+ * This is call should be placed early within the module's onLoad() method,
+ * before any real UI can be initialized that an attacking site could try to
+ * snip out and present in a confusing context.
+ * <p>
+ * If the break out works, execution will restart automatically in a proper
+ * top level window, where the script has full control over the display. If
+ * the break out fails, execution will abort and stop immediately, preventing
+ * UI widgets from being created, leaving the user with an empty frame.
+ */
+ public static void assertNotInIFrame() {
+ if (GWT.isScript() && amInsideIFrame()) {
+ bustOutOfIFrame(Window.Location.getHref());
+ throw new RuntimeException();
+ }
+ }
+
+ private static native boolean amInsideIFrame()
+ /*-{ return top.location != $wnd.location; }-*/;
+
+ private static native void bustOutOfIFrame(String newloc)
+ /*-{ top.location.href = newloc }-*/;
+
+ private UserAgent() {
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java
new file mode 100644
index 0000000000..35ecb12a9d
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/View.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Widget;
+
+/**
+ * Widget to display within a {@link ViewSite}.
+ *<p>
+ * Implementations must override <code>protected void onLoad()</code> and
+ * arrange for {@link #display()} to be invoked once the DOM within the view is
+ * consistent for presentation to the user. Typically this means that the
+ * subclass can start RPCs within <code>onLoad()</code> and then invoke
+ * <code>display()</code> from within the AsyncCallback's
+ * <code>onSuccess(Object)</code> method.
+ */
+public abstract class View extends Composite {
+ ViewSite<? extends View> site;
+
+ @Override
+ protected void onUnload() {
+ site = null;
+ super.onUnload();
+ }
+
+ /** true if this is the current view of its parent view site */
+ public final boolean isCurrentView() {
+ Widget p = getParent();
+ while (p != null) {
+ if (p instanceof ViewSite<?>) {
+ return ((ViewSite<?>) p).getView() == this;
+ }
+ p = p.getParent();
+ }
+ return false;
+ }
+
+ /** Replace the current view in the parent ViewSite with this view. */
+ public final void display() {
+ if (site != null) {
+ site.swap(this);
+ }
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java
new file mode 100644
index 0000000000..30b8408ff8
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+
+/**
+ * Hosts a single {@link View}.
+ * <p>
+ * View instances are attached inside of an invisible DOM node, permitting their
+ * <code>onLoad()</code> method to be invoked and to update the DOM prior to the
+ * elements being made visible in the UI.
+ * <p>
+ * Complaint View instances must invoke {@link View#display()} once the DOM is
+ * ready for presentation.
+ */
+public class ViewSite<V extends View> extends Composite {
+ private final FlowPanel main;
+ private SimplePanel current;
+ private SimplePanel next;
+
+ public ViewSite() {
+ main = new FlowPanel();
+ initWidget(main);
+ }
+
+ /** Get the current view; null if there is no view being displayed. */
+ @SuppressWarnings("unchecked")
+ public V getView() {
+ return current != null ? (V) current.getWidget() : null;
+ }
+
+ /**
+ * Set the next view to display.
+ * <p>
+ * The view will be attached to the DOM tree within a hidden container,
+ * permitting its <code>onLoad()</code> method to execute and update the DOM
+ * without the user seeing the result.
+ *
+ * @param view the next view to display.
+ */
+ public void setView(final V view) {
+ if (next != null) {
+ main.remove(next);
+ }
+ view.site = this;
+ next = new SimplePanel();
+ next.setVisible(false);
+ main.add(next);
+ next.add(view);
+ }
+
+ /**
+ * Invoked after the view becomes the current view and has been made visible.
+ *
+ * @param view the view being displayed.
+ */
+ protected void onShowView(final V view) {
+ }
+
+ @SuppressWarnings("unchecked")
+ final void swap(final View v) {
+ if (next != null && next.getWidget() == v) {
+ if (current != null) {
+ main.remove(current);
+ }
+ current = next;
+ next = null;
+ current.setVisible(true);
+ onShowView((V) v);
+ }
+ }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
new file mode 100644
index 0000000000..97f816fda8
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import static com.google.gwtexpui.safehtml.client.LinkFindReplace.hasValidScheme;
+
+import junit.framework.TestCase;
+
+public class LinkFindReplaceTest extends TestCase {
+ public void testNoEscaping() {
+ String find = "find";
+ String link = "link";
+ LinkFindReplace a = new LinkFindReplace(find, link);
+ assertEquals(find, a.pattern().getSource());
+ assertEquals("<a href=\"link\">find</a>", a.replace(find));
+ assertEquals("find = " + find + ", link = " + link, a.toString());
+ }
+
+ public void testBackreference() {
+ assertEquals("<a href=\"/bug?id=123\">issue 123</a>",
+ new LinkFindReplace("(bug|issue)\\s*([0-9]+)", "/bug?id=$2")
+ .replace("issue 123"));
+ }
+
+ public void testHasValidScheme() {
+ assertTrue(hasValidScheme("/absolute/path"));
+ assertTrue(hasValidScheme("relative/path"));
+ assertTrue(hasValidScheme("http://url/"));
+ assertTrue(hasValidScheme("HTTP://url/"));
+ assertTrue(hasValidScheme("https://url/"));
+ assertTrue(hasValidScheme("mailto://url/"));
+ assertFalse(hasValidScheme("ftp://url/"));
+ assertFalse(hasValidScheme("data:evil"));
+ assertFalse(hasValidScheme("javascript:alert(1)"));
+ }
+
+ public void testInvalidSchemeInReplace() {
+ try {
+ new LinkFindReplace("find", "javascript:alert(1)").replace("find");
+ fail("Expected IllegalStateException");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ public void testInvalidSchemeWithBackreference() {
+ try {
+ new LinkFindReplace(".*(script:[^;]*)", "java$1")
+ .replace("Look at this script: alert(1);");
+ fail("Expected IllegalStateException");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ public void testReplaceEscaping() {
+ assertEquals("<a href=\"a&quot;&amp;&#39;&lt;&gt;b\">find</a>",
+ new LinkFindReplace("find", "a\"&'<>b").replace("find"));
+ }
+
+ public void testHtmlInFind() {
+ String rawFind = "<b>&quot;bold&quot;</b>";
+ LinkFindReplace a = new LinkFindReplace(rawFind, "/bold");
+ assertEquals(rawFind, a.pattern().getSource());
+ assertEquals("<a href=\"/bold\">" + rawFind + "</a>", a.replace(rawFind));
+ }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
new file mode 100644
index 0000000000..9c450bd7a5
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class RawFindReplaceTest extends TestCase {
+ public void testFindReplace() {
+ final String find = "find";
+ final String replace = "replace";
+ final RawFindReplace a = new RawFindReplace(find, replace);
+ assertEquals(find, a.pattern().getSource());
+ assertEquals(replace, a.replace(find));
+ assertEquals("find = " + find + ", replace = " + replace, a.toString());
+ }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
new file mode 100644
index 0000000000..a6b0012f20
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
@@ -0,0 +1,265 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtmlBuilderTest extends TestCase {
+ public void testEmpty() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertTrue(b.isEmpty());
+ assertFalse(b.hasContent());
+ assertEquals("", b.asString());
+
+ b.append("a");
+ assertTrue(b.hasContent());
+ assertEquals("a", b.asString());
+ }
+
+ public void testToSafeHtml() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ b.append(1);
+
+ final SafeHtml h = b.toSafeHtml();
+ assertNotNull(h);
+ assertNotSame(h, b);
+ assertFalse(h instanceof SafeHtmlBuilder);
+ assertEquals("1", h.asString());
+ }
+
+ public void testAppend_boolean() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append(true));
+ assertSame(b, b.append(false));
+ assertEquals("truefalse", b.asString());
+ }
+
+ public void testAppend_char() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append('a'));
+ assertSame(b, b.append('b'));
+ assertEquals("ab", b.asString());
+ }
+
+ public void testAppend_int() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append(4));
+ assertSame(b, b.append(2));
+ assertSame(b, b.append(-100));
+ assertEquals("42-100", b.asString());
+ }
+
+ public void testAppend_long() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append(4L));
+ assertSame(b, b.append(2L));
+ assertEquals("42", b.asString());
+ }
+
+ public void testAppend_float() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append(0.0f));
+ assertEquals("0.0", b.asString());
+ }
+
+ public void testAppend_double() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append(0.0));
+ assertEquals("0.0", b.asString());
+ }
+
+ public void testAppend_String() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append((String) null));
+ assertEquals("", b.asString());
+ assertSame(b, b.append("foo"));
+ assertSame(b, b.append("bar"));
+ assertEquals("foobar", b.asString());
+ }
+
+ public void testAppend_StringBuilder() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append((StringBuilder) null));
+ assertEquals("", b.asString());
+ assertSame(b, b.append(new StringBuilder("foo")));
+ assertSame(b, b.append(new StringBuilder("bar")));
+ assertEquals("foobar", b.asString());
+ }
+
+ public void testAppend_StringBuffer() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append((StringBuffer) null));
+ assertEquals("", b.asString());
+ assertSame(b, b.append(new StringBuffer("foo")));
+ assertSame(b, b.append(new StringBuffer("bar")));
+ assertEquals("foobar", b.asString());
+ }
+
+ public void testAppend_Object() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append((Object) null));
+ assertEquals("", b.asString());
+ assertSame(b, b.append(new Object() {
+ @Override
+ public String toString() {
+ return "foobar";
+ }
+ }));
+ assertEquals("foobar", b.asString());
+ }
+
+ public void testAppend_CharSequence() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append((CharSequence) null));
+ assertEquals("", b.asString());
+ assertSame(b, b.append((CharSequence) "foo"));
+ assertSame(b, b.append((CharSequence) "bar"));
+ assertEquals("foobar", b.asString());
+ }
+
+ public void testAppend_SafeHtml() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.append((SafeHtml) null));
+ assertEquals("", b.asString());
+ assertSame(b, b.append(new SafeHtmlString("foo")));
+ assertSame(b, b.append(new SafeHtmlBuilder().append("bar")));
+ assertEquals("foobar", b.asString());
+ }
+
+ public void testHtmlSpecialCharacters() {
+ assertEquals("&amp;", escape("&"));
+ assertEquals("&lt;", escape("<"));
+ assertEquals("&gt;", escape(">"));
+ assertEquals("&quot;", escape("\""));
+ assertEquals("&#39;", escape("'"));
+
+ assertEquals("&amp;", escape('&'));
+ assertEquals("&lt;", escape('<'));
+ assertEquals("&gt;", escape('>'));
+ assertEquals("&quot;", escape('"'));
+ assertEquals("&#39;", escape('\''));
+
+ assertEquals("&lt;b&gt;", escape("<b>"));
+ assertEquals("&amp;lt;b&amp;gt;", escape("&lt;b&gt;"));
+ }
+
+ public void testEntityNbsp() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.nbsp());
+ assertEquals("&nbsp;", b.asString());
+ }
+
+ public void testTagBr() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.br());
+ assertEquals("<br />", b.asString());
+ }
+
+ public void testTagTableTrTd() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.openElement("table"));
+ assertSame(b, b.openTr());
+ assertSame(b, b.openTd());
+ assertSame(b, b.append("d<a>ta"));
+ assertSame(b, b.closeTd());
+ assertSame(b, b.closeTr());
+ assertSame(b, b.closeElement("table"));
+ assertEquals("<table><tr><td>d&lt;a&gt;ta</td></tr></table>", b.asString());
+ }
+
+ public void testTagDiv() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.openDiv());
+ assertSame(b, b.append("d<a>ta"));
+ assertSame(b, b.closeDiv());
+ assertEquals("<div>d&lt;a&gt;ta</div>", b.asString());
+ }
+
+ public void testTagAnchor() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.openAnchor());
+
+ assertEquals("", b.getAttribute("href"));
+ assertSame(b, b.setAttribute("href", "http://here"));
+ assertEquals("http://here", b.getAttribute("href"));
+ assertSame(b, b.setAttribute("href", "d<a>ta"));
+ assertEquals("d<a>ta", b.getAttribute("href"));
+
+ assertEquals("", b.getAttribute("target"));
+ assertSame(b, b.setAttribute("target", null));
+ assertEquals("", b.getAttribute("target"));
+
+ assertSame(b, b.append("go"));
+ assertSame(b, b.closeAnchor());
+ assertEquals("<a href=\"d&lt;a&gt;ta\">go</a>", b.asString());
+ }
+
+ public void testTagHeightWidth() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.openElement("img"));
+ assertSame(b, b.setHeight(100));
+ assertSame(b, b.setWidth(42));
+ assertSame(b, b.closeSelf());
+ assertEquals("<img height=\"100\" width=\"42\" />", b.asString());
+ }
+
+ public void testStyleName() {
+ final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ assertSame(b, b.openSpan());
+ assertSame(b, b.setStyleName("foo"));
+ assertSame(b, b.addStyleName("bar"));
+ assertSame(b, b.append("d<a>ta"));
+ assertSame(b, b.closeSpan());
+ assertEquals("<span class=\"foo bar\">d&lt;a&gt;ta</span>", b.asString());
+ }
+
+ public void testRejectJavaScript_AnchorHref() {
+ final String href = "javascript:window.close();";
+ try {
+ new SafeHtmlBuilder().openAnchor().setAttribute("href", href);
+ fail("accepted javascript in a href");
+ } catch (RuntimeException e) {
+ assertEquals("javascript unsafe in href: " + href, e.getMessage());
+ }
+ }
+
+ public void testRejectJavaScript_ImgSrc() {
+ final String href = "javascript:window.close();";
+ try {
+ new SafeHtmlBuilder().openElement("img").setAttribute("src", href);
+ fail("accepted javascript in img src");
+ } catch (RuntimeException e) {
+ assertEquals("javascript unsafe in href: " + href, e.getMessage());
+ }
+ }
+
+ public void testRejectJavaScript_FormAction() {
+ final String href = "javascript:window.close();";
+ try {
+ new SafeHtmlBuilder().openElement("form").setAttribute("action", href);
+ fail("accepted javascript in form action");
+ } catch (RuntimeException e) {
+ assertEquals("javascript unsafe in href: " + href, e.getMessage());
+ }
+ }
+
+ private static String escape(final char c) {
+ return new SafeHtmlBuilder().append(c).asString();
+ }
+
+ private static String escape(final String c) {
+ return new SafeHtmlBuilder().append(c).asString();
+ }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
new file mode 100644
index 0000000000..a9d945072d
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_LinkifyTest extends TestCase {
+ public void testLinkify_SimpleHttp1() {
+ final SafeHtml o = html("A http://go.here/ B");
+ final SafeHtml n = o.linkify();
+ assertNotSame(o, n);
+ assertEquals("A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B", n.asString());
+ }
+
+ public void testLinkify_SimpleHttps2() {
+ final SafeHtml o = html("A https://go.here/ B");
+ final SafeHtml n = o.linkify();
+ assertNotSame(o, n);
+ assertEquals("A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B", n.asString());
+ }
+
+ public void testLinkify_Parens1() {
+ final SafeHtml o = html("A (http://go.here/) B");
+ final SafeHtml n = o.linkify();
+ assertNotSame(o, n);
+ assertEquals("A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B", n.asString());
+ }
+
+ public void testLinkify_Parens() {
+ final SafeHtml o = html("A http://go.here/#m() B");
+ final SafeHtml n = o.linkify();
+ assertNotSame(o, n);
+ assertEquals("A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B", n.asString());
+ }
+
+ public void testLinkify_AngleBrackets1() {
+ final SafeHtml o = html("A <http://go.here/> B");
+ final SafeHtml n = o.linkify();
+ assertNotSame(o, n);
+ assertEquals("A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B", n.asString());
+ }
+
+ private static SafeHtml html(String text) {
+ return new SafeHtmlBuilder().append(text).toSafeHtml();
+ }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
new file mode 100644
index 0000000000..d7a3aaf278
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class SafeHtml_ReplaceTest extends TestCase {
+ public void testReplaceEmpty() {
+ SafeHtml o = html("A\nissue42\nB");
+ assertSame(o, o.replaceAll(null));
+ assertSame(o, o.replaceAll(Collections.<FindReplace> emptyList()));
+ }
+
+ public void testReplaceOneLink() {
+ SafeHtml o = html("A\nissue 42\nB");
+ SafeHtml n = o.replaceAll(repls(
+ new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
+ assertNotSame(o, n);
+ assertEquals("A\n<a href=\"?42\">issue 42</a>\nB", n.asString());
+ }
+
+ public void testReplaceNoLeadingOrTrailingText() {
+ SafeHtml o = html("issue 42");
+ SafeHtml n = o.replaceAll(repls(
+ new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
+ assertNotSame(o, n);
+ assertEquals("<a href=\"?42\">issue 42</a>", n.asString());
+ }
+
+ public void testReplaceTwoLinks() {
+ SafeHtml o = html("A\nissue 42\nissue 9918\nB");
+ SafeHtml n = o.replaceAll(repls(
+ new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
+ assertNotSame(o, n);
+ assertEquals("A\n"
+ + "<a href=\"?42\">issue 42</a>\n"
+ + "<a href=\"?9918\">issue 9918</a>\n"
+ + "B"
+ , n.asString());
+ }
+
+ public void testReplaceInOrder() {
+ SafeHtml o = html("A\nissue 42\nReally GWTEXPUI-9918 is better\nB");
+ SafeHtml n = o.replaceAll(repls(
+ new RawFindReplace("(GWTEXPUI-(\\d+))",
+ "<a href=\"gwtexpui-bug?$2\">$1</a>"),
+ new RawFindReplace("(issue\\s+(\\d+))",
+ "<a href=\"generic-bug?$2\">$1</a>")));
+ assertNotSame(o, n);
+ assertEquals("A\n"
+ + "<a href=\"generic-bug?42\">issue 42</a>\n"
+ + "Really <a href=\"gwtexpui-bug?9918\">GWTEXPUI-9918</a> is better\n"
+ + "B"
+ , n.asString());
+ }
+
+ public void testReplaceOverlappingAfterFirstChar() {
+ SafeHtml o = html("abcd");
+ RawFindReplace ab = new RawFindReplace("ab", "AB");
+ RawFindReplace bc = new RawFindReplace("bc", "23");
+ RawFindReplace cd = new RawFindReplace("cd", "YZ");
+
+ assertEquals("ABcd", o.replaceAll(repls(ab, bc)).asString());
+ assertEquals("ABcd", o.replaceAll(repls(bc, ab)).asString());
+ assertEquals("ABYZ", o.replaceAll(repls(ab, bc, cd)).asString());
+ }
+
+ public void testReplaceOverlappingAtFirstCharLongestMatch() {
+ SafeHtml o = html("abcd");
+ RawFindReplace ab = new RawFindReplace("ab", "AB");
+ RawFindReplace abc = new RawFindReplace("[^d][^d][^d]", "234");
+
+ assertEquals("ABcd", o.replaceAll(repls(ab, abc)).asString());
+ assertEquals("234d", o.replaceAll(repls(abc, ab)).asString());
+ }
+
+ public void testReplaceOverlappingAtFirstCharFirstMatch() {
+ SafeHtml o = html("abcd");
+ RawFindReplace ab1 = new RawFindReplace("ab", "AB");
+ RawFindReplace ab2 = new RawFindReplace("[^cd][^cd]", "12");
+
+ assertEquals("ABcd", o.replaceAll(repls(ab1, ab2)).asString());
+ assertEquals("12cd", o.replaceAll(repls(ab2, ab1)).asString());
+ }
+
+ public void testFailedSanitization() {
+ SafeHtml o = html("abcd");
+ LinkFindReplace evil = new LinkFindReplace("(b)", "javascript:alert('$1')");
+ LinkFindReplace ok = new LinkFindReplace("(b)", "/$1");
+ assertEquals("abcd", o.replaceAll(repls(evil)).asString());
+ String linked = "a<a href=\"/b\">b</a>cd";
+ assertEquals(linked, o.replaceAll(repls(ok)).asString());
+ assertEquals(linked, o.replaceAll(repls(evil, ok)).asString());
+ }
+
+ private static SafeHtml html(String text) {
+ return new SafeHtmlBuilder().append(text).toSafeHtml();
+ }
+
+ private static List<FindReplace> repls(FindReplace... repls) {
+ return Arrays.asList(repls);
+ }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
new file mode 100644
index 0000000000..250a1b52f8
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "<p>AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_WikifyListTest extends TestCase {
+ private static final String BEGIN_LIST = "<ul class=\"wikiList\">";
+ private static final String END_LIST = "</ul>";
+
+ private static String item(String raw) {
+ return "<li>" + raw + "</li>";
+ }
+
+ public void testBulletList1() {
+ final SafeHtml o = html("A\n\n* line 1\n* 2nd line");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A</p>"//
+ + BEGIN_LIST //
+ + item("line 1") //
+ + item("2nd line") //
+ + END_LIST //
+ , n.asString());
+ }
+
+ public void testBulletList2() {
+ final SafeHtml o = html("A\n\n* line 1\n* 2nd line\n\nB");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A</p>"//
+ + BEGIN_LIST //
+ + item("line 1") //
+ + item("2nd line") //
+ + END_LIST //
+ + "<p>B</p>" //
+ , n.asString());
+ }
+
+ public void testBulletList3() {
+ final SafeHtml o = html("* line 1\n* 2nd line\n\nB");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals(BEGIN_LIST //
+ + item("line 1") //
+ + item("2nd line") //
+ + END_LIST //
+ + "<p>B</p>" //
+ , n.asString());
+ }
+
+ public void testBulletList4() {
+ final SafeHtml o = html("To see this bug, you have to:\n" //
+ + "* Be on IMAP or EAS (not on POP)\n"//
+ + "* Be very unlucky\n");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>To see this bug, you have to:</p>" //
+ + BEGIN_LIST //
+ + item("Be on IMAP or EAS (not on POP)") //
+ + item("Be very unlucky") //
+ + END_LIST //
+ , n.asString());
+ }
+
+ public void testBulletList5() {
+ final SafeHtml o = html("To see this bug,\n" //
+ + "you have to:\n" //
+ + "* Be on IMAP or EAS (not on POP)\n"//
+ + "* Be very unlucky\n");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>To see this bug, you have to:</p>" //
+ + BEGIN_LIST //
+ + item("Be on IMAP or EAS (not on POP)") //
+ + item("Be very unlucky") //
+ + END_LIST //
+ , n.asString());
+ }
+
+ public void testDashList1() {
+ final SafeHtml o = html("A\n\n- line 1\n- 2nd line");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A</p>"//
+ + BEGIN_LIST //
+ + item("line 1") //
+ + item("2nd line") //
+ + END_LIST //
+ , n.asString());
+ }
+
+ public void testDashList2() {
+ final SafeHtml o = html("A\n\n- line 1\n- 2nd line\n\nB");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A</p>"//
+ + BEGIN_LIST //
+ + item("line 1") //
+ + item("2nd line") //
+ + END_LIST //
+ + "<p>B</p>" //
+ , n.asString());
+ }
+
+ public void testDashList3() {
+ final SafeHtml o = html("- line 1\n- 2nd line\n\nB");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals(BEGIN_LIST //
+ + item("line 1") //
+ + item("2nd line") //
+ + END_LIST //
+ + "<p>B</p>" //
+ , n.asString());
+ }
+
+ private static SafeHtml html(String text) {
+ return new SafeHtmlBuilder().append(text).toSafeHtml();
+ }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
new file mode 100644
index 0000000000..cbb315bb99
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "<p>AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_WikifyPreformatTest extends TestCase {
+ private static final String B = "<span class=\"wikiPreFormat\">";
+ private static final String E = "</span><br />";
+
+ private static String pre(String raw) {
+ return B + raw + E;
+ }
+
+ public void testPreformat1() {
+ final SafeHtml o = html("A\n\n This is pre\n formatted");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A</p>"//
+ + "<p>" //
+ + pre(" This is pre") //
+ + pre(" formatted") //
+ + "</p>" //
+ , n.asString());
+ }
+
+ public void testPreformat2() {
+ final SafeHtml o = html("A\n\n This is pre\n formatted\n\nbut this is not");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A</p>" //
+ + "<p>" //
+ + pre(" This is pre") //
+ + pre(" formatted") //
+ + "</p>" //
+ + "<p>but this is not</p>" //
+ , n.asString());
+ }
+
+ public void testPreformat3() {
+ final SafeHtml o = html("A\n\n Q\n <R>\n S\n\nB");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A</p>" //
+ + "<p>" //
+ + pre(" Q") //
+ + pre(" &lt;R&gt;") //
+ + pre(" S") //
+ + "</p>" //
+ + "<p>B</p>" //
+ , n.asString());
+ }
+
+ public void testPreformat4() {
+ final SafeHtml o = html(" Q\n <R>\n S\n\nB");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>" //
+ + pre(" Q") //
+ + pre(" &lt;R&gt;") //
+ + pre(" S") //
+ + "</p>" //
+ + "<p>B</p>" //
+ , n.asString());
+ }
+
+ private static SafeHtml html(String text) {
+ return new SafeHtmlBuilder().append(text).toSafeHtml();
+ }
+}
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
new file mode 100644
index 0000000000..c983703740
--- /dev/null
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "<p>AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.safehtml.client;
+
+import junit.framework.TestCase;
+
+public class SafeHtml_WikifyTest extends TestCase {
+ public void testWikify_OneLine1() {
+ final SafeHtml o = html("A B");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A B</p>", n.asString());
+ }
+
+ public void testWikify_OneLine2() {
+ final SafeHtml o = html("A B\n");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A B\n</p>", n.asString());
+ }
+
+ public void testWikify_OneParagraph1() {
+ final SafeHtml o = html("A\nB");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A\nB</p>", n.asString());
+ }
+
+ public void testWikify_OneParagraph2() {
+ final SafeHtml o = html("A\nB\n");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A\nB\n</p>", n.asString());
+ }
+
+ public void testWikify_TwoParagraphs() {
+ final SafeHtml o = html("A\nB\n\nC\nD");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A\nB</p><p>C\nD</p>", n.asString());
+ }
+
+ public void testLinkify_SimpleHttp1() {
+ final SafeHtml o = html("A http://go.here/ B");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B</p>", n.asString());
+ }
+
+ public void testLinkify_SimpleHttps2() {
+ final SafeHtml o = html("A https://go.here/ B");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B</p>", n.asString());
+ }
+
+ public void testLinkify_Parens1() {
+ final SafeHtml o = html("A (http://go.here/) B");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B</p>", n.asString());
+ }
+
+ public void testLinkify_Parens() {
+ final SafeHtml o = html("A http://go.here/#m() B");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B</p>", n.asString());
+ }
+
+ public void testLinkify_AngleBrackets1() {
+ final SafeHtml o = html("A <http://go.here/> B");
+ final SafeHtml n = o.wikify();
+ assertNotSame(o, n);
+ assertEquals("<p>A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B</p>", n.asString());
+ }
+
+ private static SafeHtml html(String text) {
+ return new SafeHtmlBuilder().append(text).toSafeHtml();
+ }
+}
diff --git a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
index e9441bb123..f9fe34593f 100644
--- a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index 43b26c1e3f..7d09989647 100644
--- a/gerrit-gwtui/pom.xml
+++ b/gerrit-gwtui/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-gwtui</artifactId>
@@ -40,12 +40,14 @@ limitations under the License.
</dependency>
<dependency>
- <groupId>gwtexpui</groupId>
- <artifactId>gwtexpui</artifactId>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-gwtexpui</artifactId>
+ <version>${project.version}</version>
</dependency>
<dependency>
- <groupId>gwtexpui</groupId>
- <artifactId>gwtexpui</artifactId>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-gwtexpui</artifactId>
+ <version>${project.version}</version>
<classifier>sources</classifier>
<type>jar</type>
</dependency>
@@ -149,53 +151,6 @@ limitations under the License.
</dependency>
</dependencies>
- <profiles>
- <profile>
- <id>all</id>
- <activation>
- <activeByDefault>true</activeByDefault>
- </activation>
- <properties>
- <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUI</GerritGwtUI.browserType>
- <GerritGwtUI.draftCompile>false</GerritGwtUI.draftCompile>
- </properties>
- </profile>
- <profile>
- <id>safari</id>
- <properties>
- <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
- <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
- </properties>
- </profile>
- <profile>
- <id>chrome</id>
- <properties>
- <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
- <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
- </properties>
- </profile>
- <profile>
- <id>webkit</id>
- <properties>
- <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
- <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
- </properties>
- </profile>
- <profile>
- <id>gecko1_8</id>
- <properties>
- <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIgecko1_8</GerritGwtUI.browserType>
- <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
- </properties>
- </profile>
- <profile>
- <id>firefox</id>
- <properties>
- <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIgecko1_8</GerritGwtUI.browserType>
- <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
- </properties>
- </profile>
- </profiles>
<build>
<plugins>
@@ -206,12 +161,11 @@ limitations under the License.
<execution>
<id>optimized</id>
<configuration>
- <module>${GerritGwtUI.browserType}</module>
+ <module>com.google.gerrit.GerritGwtUI</module>
<extraJvmArgs>-Xmx512m</extraJvmArgs>
<compileReport>${gwt.compileReport}</compileReport>
<disableClassMetadata>true</disableClassMetadata>
<disableCastChecking>true</disableCastChecking>
- <draftCompile>${GerritGwtUI.draftCompile}</draftCompile>
</configuration>
<goals>
<goal>compile</goal>
@@ -221,11 +175,10 @@ limitations under the License.
<id>debug</id>
<configuration>
<style>PRETTY</style>
- <module>${GerritGwtUI.browserType}</module>
+ <module>com.google.gerrit.GerritGwtUI</module>
<extraJvmArgs>-Xmx512m</extraJvmArgs>
<disableClassMetadata>true</disableClassMetadata>
<disableRunAsync>true</disableRunAsync>
- <draftCompile>true</draftCompile>
<webappDirectory>${project.build.directory}/${project.build.finalName}_dbg</webappDirectory>
</configuration>
<goals>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
index f2774c97fc..1dce6dfe5e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
@@ -21,4 +21,13 @@
<when-property-is name="user.agent" value="ie8"/>
</any>
</replace-with>
+
+ <replace-with class="com.google.gerrit.client.Themer.ThemerIE">
+ <when-type-is class="com.google.gerrit.client.Themer" />
+ <any>
+ <when-property-is name="user.agent" value="ie6"/>
+ <when-property-is name="user.agent" value="ie8"/>
+ <when-property-is name="user.agent" value="ie9"/>
+ </any>
+ </replace-with>
</module>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index 00597238b3..5dcccb04b5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -14,16 +14,26 @@
package com.google.gerrit.client;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gwt.event.dom.client.ErrorEvent;
-import com.google.gwt.event.dom.client.ErrorHandler;
+import com.google.gwt.event.dom.client.LoadEvent;
+import com.google.gwt.event.dom.client.LoadHandler;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.dom.client.MouseOverHandler;
+import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.UIObject;
public class AvatarImage extends Image {
+ public AvatarImage() {
+ }
+
/** A default sized avatar image. */
- public AvatarImage(Account.Id account) {
+ public AvatarImage(AccountInfo account) {
this(account, 0);
}
@@ -35,30 +45,66 @@ public class AvatarImage extends Image {
* on the avatar provider. A size <= 0 indicates to let the provider
* decide a default size.
*/
- public AvatarImage(Account.Id account, int size) {
- super(url(account, size));
+ public AvatarImage(AccountInfo account, int size) {
+ this(account, size, true);
+ }
+
+ /**
+ * An avatar image for the given account using the requested size.
+ *
+ * @param account The account in which we are interested
+ * @param size A requested size. Note that the size can be ignored depending
+ * on the avatar provider. A size <= 0 indicates to let the provider
+ * decide a default size.
+ * @param addPopup show avatar popup with user info on hovering over the
+ * avatar image
+ */
+ public AvatarImage(AccountInfo account, int size, boolean addPopup) {
+ setAccount(account, size, addPopup);
+ }
+
+ public void setAccount(AccountInfo account, int size, boolean addPopup) {
+ setUrl(isGerritServer(account) ? getGerritServerAvatarUrl() :
+ url(account.email(), size));
+ setVisible(false);
if (size > 0) {
// If the provider does not resize the image, force it in the browser.
setSize(size + "px", size + "px");
}
- addErrorHandler(new ErrorHandler() {
+ addLoadHandler(new LoadHandler() {
@Override
- public void onError(ErrorEvent event) {
- // We got a 404, don't bother showing the image. Either the user doesn't
- // have an avatar or there is no avatar provider plugin installed.
- setVisible(false);
+ public void onLoad(LoadEvent event) {
+ setVisible(true);
}
});
+
+ if (addPopup) {
+ PopupHandler popupHandler = new PopupHandler(account, this);
+ addMouseOverHandler(popupHandler);
+ addMouseOutHandler(popupHandler);
+ }
}
- private static String url(Account.Id id, int size) {
+ private static boolean isGerritServer(AccountInfo account) {
+ return account._account_id() == 0
+ && Util.C.messageNoAuthor().equals(account.name());
+ }
+
+ private static String getGerritServerAvatarUrl() {
+ return Gerrit.RESOURCES.gerritAvatar().getSafeUri().asString();
+ }
+
+ private static String url(String email, int size) {
+ if (email == null) {
+ return "";
+ }
String u;
- if (Gerrit.isSignedIn() && id.equals(Gerrit.getUserAccount().getId())) {
+ if (Gerrit.isSignedIn() && email.equals(Gerrit.getUserAccount().getPreferredEmail())) {
u = "self";
} else {
- u = id.toString();
+ u = email;
}
RestApi api = new RestApi("/accounts/").id(u).view("avatar");
if (size > 0) {
@@ -66,4 +112,87 @@ public class AvatarImage extends Image {
}
return api.url();
}
+
+ private class PopupHandler implements MouseOverHandler, MouseOutHandler {
+ private final AccountInfo account;
+ private final UIObject target;
+
+ private UserPopupPanel popup;
+ private Timer showTimer;
+ private Timer hideTimer;
+
+ public PopupHandler(AccountInfo account, UIObject target) {
+ this.account = account;
+ this.target = target;
+ }
+
+ private UserPopupPanel createPopupPanel(AccountInfo account) {
+ UserPopupPanel popup = new UserPopupPanel(account, false, false);
+ popup.addDomHandler(new MouseOverHandler() {
+ @Override
+ public void onMouseOver(MouseOverEvent event) {
+ scheduleShow();
+ }
+ }, MouseOverEvent.getType());
+ popup.addDomHandler(new MouseOutHandler() {
+ @Override
+ public void onMouseOut(MouseOutEvent event) {
+ scheduleHide();
+ }
+ }, MouseOutEvent.getType());
+ return popup;
+ }
+
+ @Override
+ public void onMouseOver(MouseOverEvent event) {
+ scheduleShow();
+ }
+
+ @Override
+ public void onMouseOut(MouseOutEvent event) {
+ scheduleHide();
+ }
+
+ private void scheduleShow() {
+ if (hideTimer != null) {
+ hideTimer.cancel();
+ hideTimer = null;
+ }
+ if ((popup != null && popup.isShowing() && popup.isVisible())
+ || showTimer != null) {
+ return;
+ }
+ showTimer = new Timer() {
+ @Override
+ public void run() {
+ if (popup == null) {
+ popup = createPopupPanel(account);
+ }
+ if (!popup.isShowing() || !popup.isVisible()) {
+ popup.showRelativeTo(target);
+ }
+
+ }
+ };
+ showTimer.schedule(600);
+ }
+
+ private void scheduleHide() {
+ if (showTimer != null) {
+ showTimer.cancel();
+ showTimer = null;
+ }
+ if (popup == null || !popup.isShowing() || !popup.isVisible()
+ || hideTimer != null) {
+ return;
+ }
+ hideTimer = new Timer() {
+ @Override
+ public void run() {
+ popup.hide();
+ }
+ };
+ hideTimer.schedule(50);
+ }
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index 5448dc0d53..2e94db78ce 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -119,6 +119,11 @@ public class FormatUtil {
}
}
+ /** Format a date using git log's relative date format. */
+ public static String relativeFormat(Date dt) {
+ return RelativeDateFormatter.format(dt);
+ }
+
/**
* Formats an account as a name and an email address.
* <p>
@@ -173,7 +178,7 @@ public class FormatUtil {
return nameEmail(ai);
}
- private static AccountInfo asInfo(Account acct) {
+ public static AccountInfo asInfo(Account acct) {
if (acct == null) {
return AccountInfo.create(0, null, null);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index d67a4ca02d..0b1af89406 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.admin.ProjectScreen;
import com.google.gerrit.client.changes.ChangeConstants;
import com.google.gerrit.client.changes.ChangeListScreen;
@@ -95,6 +96,7 @@ public class Gerrit implements EntryPoint {
GWT.create(GerritResources.class);
public static final SystemInfoService SYSTEM_SVC;
public static final EventBus EVENT_BUS = GWT.create(SimpleEventBus.class);
+ public static Themer THEMER = GWT.create(Themer.class);
private static String myHost;
private static GerritConfig myConfig;
@@ -248,6 +250,11 @@ public class Gerrit implements EntryPoint {
return myAccount;
}
+ /** @return the currently signed in user's account data; empty account data if no account */
+ public static AccountInfo getUserAccountInfo() {
+ return FormatUtil.asInfo(myAccount);
+ }
+
/** @return access token to prove user identity during REST API calls. */
public static String getXGerritAuth() {
return xGerritAuth;
@@ -552,9 +559,17 @@ public class Gerrit implements EntryPoint {
if (signInAnchor != null) {
signInAnchor.setHref(loginRedirect(token));
}
+
+ saveDefaultTheme();
loadPlugins(hpd, token);
}
+ private void saveDefaultTheme() {
+ THEMER.init(Document.get().getElementById("gerrit_sitecss"),
+ Document.get().getElementById("gerrit_header"),
+ Document.get().getElementById("gerrit_footer"));
+ }
+
private void loadPlugins(HostPageData hpd, final String token) {
if (hpd.plugins != null) {
for (final String url : hpd.plugins) {
@@ -754,9 +769,9 @@ public class Gerrit implements EntryPoint {
}
private static void whoAmI(boolean canLogOut) {
- Account account = getUserAccount();
- final CurrentUserPopupPanel userPopup =
- new CurrentUserPopupPanel(account, canLogOut);
+ AccountInfo account = getUserAccountInfo();
+ final UserPopupPanel userPopup =
+ new UserPopupPanel(account, canLogOut, true);
final FlowPanel userSummaryPanel = new FlowPanel();
class PopupHandler implements KeyDownHandler, ClickHandler {
private void showHidePopup() {
@@ -783,7 +798,7 @@ public class Gerrit implements EntryPoint {
final PopupHandler popupHandler = new PopupHandler();
final InlineLabel l = new InlineLabel(FormatUtil.name(account));
l.setStyleName(RESOURCES.css().menuBarUserName());
- final AvatarImage avatar = new AvatarImage(account.getId(), 26);
+ final AvatarImage avatar = new AvatarImage(account, 26, false);
avatar.setStyleName(RESOURCES.css().menuBarUserNameAvatar());
userSummaryPanel.setStyleName(RESOURCES.css().menuBarUserNamePanel());
userSummaryPanel.add(l);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 489ff0092d..a33556ee5a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -21,6 +21,7 @@ public interface GerritCss extends CssResource {
String accountContactPrivacyDetails();
String accountDashboard();
String accountInfoBlock();
+ String accountLinkPanel();
String accountName();
String accountPassword();
String accountUsername();
@@ -34,6 +35,7 @@ public interface GerritCss extends CssResource {
String approvalhint();
String approvalrole();
String approvalscore();
+ String avatarInfoPanel();
String blockHeader();
String bottomheader();
String cAPPROVAL();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index fc7ea53cbf..098cc77d2f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
@@ -51,4 +51,10 @@ public interface GerritResources extends ClientBundle {
@Source("addFileComment.png")
public ImageResource addFileComment();
+
+ @Source("diffy.png")
+ public ImageResource gerritAvatar();
+
+ @Source("draftComments.png")
+ public ImageResource draftComments();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
index 5f62a5260e..ec97e58524 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
@@ -35,6 +35,16 @@ public class GitwebLink {
type = link.type;
}
+ /**
+ * Can we link to a patch set if it's a draft
+ *
+ * @param ps Patch set to check draft status
+ * @return true if it's not a draft, or we can link to drafts
+ */
+ public boolean canLink(final PatchSet ps) {
+ return !ps.isDraft() || type.getLinkDrafts();
+ }
+
public String getLinkName() {
return "(" + type.getLinkName() + ")";
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
new file mode 100644
index 0000000000..3298a06ca8
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client;
+
+import com.google.gerrit.client.changes.Util;
+
+import java.util.Date;
+
+/**
+ * Formatter to format timestamps relative to the current time using time units
+ * in the format defined by {@code git log --relative-date}.
+ */
+public class RelativeDateFormatter {
+ final static long SECOND_IN_MILLIS = 1000;
+
+ final static long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
+
+ final static long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
+
+ final static long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
+
+ final static long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
+
+ final static long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
+
+ final static long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
+
+ /**
+ * @param when {@link Date} to format
+ * @return age of given {@link Date} compared to now formatted in the same
+ * relative format as returned by {@code git log --relative-date}
+ */
+ @SuppressWarnings("boxing")
+ public static String format(Date when) {
+ long ageMillis = (new Date()).getTime() - when.getTime();
+
+ // shouldn't happen in a perfect world
+ if (ageMillis < 0) return Util.C.inTheFuture();
+
+ // seconds
+ if (ageMillis < upperLimit(MINUTE_IN_MILLIS)) {
+ return Util.M.secondsAgo(round(ageMillis, SECOND_IN_MILLIS));
+ }
+
+ // minutes
+ if (ageMillis < upperLimit(HOUR_IN_MILLIS)) {
+ return Util.M.minutesAgo(round(ageMillis, MINUTE_IN_MILLIS));
+ }
+
+ // hours
+ if (ageMillis < upperLimit(DAY_IN_MILLIS)) {
+ return Util.M.hoursAgo(round(ageMillis, HOUR_IN_MILLIS));
+ }
+
+ // up to 14 days use days
+ if (ageMillis < 14 * DAY_IN_MILLIS) {
+ return Util.M.daysAgo(round(ageMillis, DAY_IN_MILLIS));
+ }
+
+ // up to 10 weeks use weeks
+ if (ageMillis < 10 * WEEK_IN_MILLIS) {
+ return Util.M.weeksAgo(round(ageMillis, WEEK_IN_MILLIS));
+ }
+
+ // months
+ if (ageMillis < YEAR_IN_MILLIS) {
+ return Util.M.monthsAgo(round(ageMillis, MONTH_IN_MILLIS));
+ }
+
+ // up to 5 years use "year, months" rounded to months
+ if (ageMillis < 5 * YEAR_IN_MILLIS) {
+ long years = ageMillis / YEAR_IN_MILLIS;
+ String yearLabel = (years > 1) ? Util.C.years() : Util.C.year();
+ long months = round(ageMillis % YEAR_IN_MILLIS, MONTH_IN_MILLIS);
+ String monthLabel =
+ (months > 1) ? Util.C.months() : (months == 1 ? Util.C.month() : "");
+ if (months == 0) {
+ return Util.M.years0MonthsAgo(years, yearLabel);
+ } else {
+ return Util.M.yearsMonthsAgo(years, yearLabel, months, monthLabel);
+ }
+ }
+
+ // years
+ return Util.M.yearsAgo(round(ageMillis, YEAR_IN_MILLIS));
+ }
+
+ private static long upperLimit(long unit) {
+ long limit = unit + unit / 2;
+ return limit;
+ }
+
+ private static long round(long n, long unit) {
+ long rounded = (n + unit / 2) / unit;
+ return rounded;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
new file mode 100644
index 0000000000..a532209fbf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.client;
+
+import com.google.gerrit.client.projects.ThemeInfo;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.StyleElement;
+
+public class Themer {
+ public static class ThemerIE extends Themer {
+ protected ThemerIE() {
+ }
+
+ @Override
+ protected String getCssText(StyleElement el) {
+ return el.getCssText();
+ }
+
+ @Override
+ protected void setCssText(StyleElement el, String css) {
+ el.setCssText(css);
+ }
+ }
+
+ protected StyleElement cssElement;
+ protected Element headerElement;
+ protected Element footerElement;
+ protected String cssText;
+ protected String headerHtml;
+ protected String footerHtml;
+
+ protected Themer() {
+ }
+
+ public void set(ThemeInfo theme) {
+ if (theme != null) {
+ set(theme.css() != null ? theme.css() : cssText,
+ theme.header() != null ? theme.header() : headerHtml,
+ theme.footer() != null ? theme.footer() : footerHtml);
+ } else {
+ set(cssText, headerHtml, footerHtml);
+ }
+ }
+
+ public void clear() {
+ set(null);
+ }
+
+ void init(Element css, Element header, Element footer) {
+ cssElement = StyleElement.as(css);
+ headerElement = header;
+ footerElement = footer;
+
+ cssText = getCssText(this.cssElement);
+ headerHtml = header.getInnerHTML();
+ footerHtml = footer.getInnerHTML();
+ }
+
+ protected String getCssText(StyleElement el) {
+ return el.getInnerHTML();
+ }
+
+ protected void setCssText(StyleElement el, String css) {
+ el.setInnerHTML(css);
+ }
+
+ private void set(String css, String header, String footer) {
+ setCssText(cssElement, css);
+ headerElement.setInnerHTML(header);
+ footerElement.setInnerHTML(footer);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index 7983f9c9b2..01811a6013 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -14,8 +14,8 @@
package com.google.gerrit.client;
+import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gwt.core.client.GWT;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
@@ -24,8 +24,8 @@ import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.user.client.PluginSafePopupPanel;
-public class CurrentUserPopupPanel extends PluginSafePopupPanel {
- interface Binder extends UiBinder<Widget, CurrentUserPopupPanel> {
+public class UserPopupPanel extends PluginSafePopupPanel {
+ interface Binder extends UiBinder<Widget, UserPopupPanel> {
}
private static final Binder binder = GWT.create(Binder.class);
@@ -41,9 +41,10 @@ public class CurrentUserPopupPanel extends PluginSafePopupPanel {
@UiField
Anchor settings;
- public CurrentUserPopupPanel(Account account, boolean canLogOut) {
+ public UserPopupPanel(AccountInfo account, boolean canLogOut,
+ boolean showSettingsLink) {
super(/* auto hide */true, /* modal */false);
- avatar = new AvatarImage(account.getId(), 100);
+ avatar = new AvatarImage(account, 100, false);
setWidget(binder.createAndBindUi(this));
// We must show and then hide this popup so that it is part of the DOM.
// Otherwise the image does not get any events. Calling hide() would
@@ -51,17 +52,21 @@ public class CurrentUserPopupPanel extends PluginSafePopupPanel {
show();
setVisible(false);
setStyleName(Gerrit.RESOURCES.css().userInfoPopup());
- if (account.getFullName() != null) {
- userName.setText(account.getFullName());
+ if (account.name() != null) {
+ userName.setText(account.name());
}
- if (account.getPreferredEmail() != null) {
- userEmail.setText(account.getPreferredEmail());
+ if (account.email() != null) {
+ userEmail.setText(account.email());
}
if (canLogOut) {
logout.setHref(Gerrit.selfRedirect("/logout"));
} else {
logout.setVisible(false);
}
- settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+ if (showSettingsLink) {
+ settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+ } else {
+ settings.setVisible(false);
+ }
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
index 0db578808c..0db578808c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index fa2c5fdaa7..917c078d60 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -19,11 +19,13 @@ import com.google.gwt.i18n.client.Constants;
public interface AccountConstants extends Constants {
String settingsHeading();
+ String changeAvatar();
String fullName();
String preferredEmail();
String registeredOn();
String accountId();
+ String commentVisibilityLabel();
String maximumPageSizeFieldLabel();
String dateFormatLabel();
String contextWholeFile();
@@ -33,6 +35,7 @@ public interface AccountConstants extends Constants {
String reversePatchSetOrder();
String showUsernameInReviewCategory();
String buttonSaveChanges();
+ String showRelativeDateInChangeTable();
String tabAccountSummary();
String tabPreferences();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index fd54363387..7175b6a09b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -1,5 +1,6 @@
settingsHeading = Settings
+changeAvatar = Change Avatar
fullName = Full Name
preferredEmail = Email Address
registeredOn = Registered
@@ -9,11 +10,12 @@ useFlashClipboard = Use Flash Clipboard Widget
copySelfOnEmails = CC Me On Comments I Write
reversePatchSetOrder = Display Patch Sets In Reverse Order
showUsernameInReviewCategory = Display Person Name In Review Category
-defaultContextFieldLabel = Default Context:
maximumPageSizeFieldLabel = Maximum Page Size:
+commentVisibilityLabel = Comment Visibility:
dateFormatLabel = Date/Time Format:
contextWholeFile = Whole File
buttonSaveChanges = Save Changes
+showRelativeDateInChangeTable = Show Relative Dates in Changes Table
tabAccountSummary = Profile
tabPreferences = Preferences
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index c17a0aac2e..639a1cf79c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -23,6 +23,7 @@ import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.OnEditEnabler;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.i18n.client.DateTimeFormat;
@@ -42,9 +43,11 @@ public class MyPreferencesScreen extends SettingsScreen {
private CheckBox copySelfOnEmails;
private CheckBox reversePatchSetOrder;
private CheckBox showUsernameInReviewCategory;
+ private CheckBox relativeDateInChangeTable;
private ListBox maximumPageSize;
private ListBox dateFormat;
private ListBox timeFormat;
+ private ListBox commentVisibilityStrategy;
private Button save;
@Override
@@ -61,6 +64,24 @@ public class MyPreferencesScreen extends SettingsScreen {
maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v));
}
+ commentVisibilityStrategy = new ListBox();
+ commentVisibilityStrategy.addItem(
+ com.google.gerrit.client.changes.Util.C.messageCollapseAll(),
+ AccountGeneralPreferences.CommentVisibilityStrategy.COLLAPSE_ALL.name()
+ );
+ commentVisibilityStrategy.addItem(
+ com.google.gerrit.client.changes.Util.C.messageExpandMostRecent(),
+ AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT.name()
+ );
+ commentVisibilityStrategy.addItem(
+ com.google.gerrit.client.changes.Util.C.messageExpandRecent(),
+ AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT.name()
+ );
+ commentVisibilityStrategy.addItem(
+ com.google.gerrit.client.changes.Util.C.messageExpandAll(),
+ AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_ALL.name()
+ );
+
Date now = new Date();
dateFormat = new ListBox();
for (AccountGeneralPreferences.DateFormat fmt : AccountGeneralPreferences.DateFormat
@@ -94,7 +115,10 @@ public class MyPreferencesScreen extends SettingsScreen {
dateTimePanel.add(dateFormat);
dateTimePanel.add(timeFormat);
}
- final Grid formGrid = new Grid(7, 2);
+
+ relativeDateInChangeTable = new CheckBox(Util.C.showRelativeDateInChangeTable());
+
+ final Grid formGrid = new Grid(9, 2);
int row = 0;
formGrid.setText(row, labelIdx, "");
@@ -125,6 +149,14 @@ public class MyPreferencesScreen extends SettingsScreen {
formGrid.setWidget(row, fieldIdx, dateTimePanel);
row++;
+ formGrid.setText(row, labelIdx, "");
+ formGrid.setWidget(row, fieldIdx, relativeDateInChangeTable);
+ row++;
+
+ formGrid.setText(row, labelIdx, Util.C.commentVisibilityLabel());
+ formGrid.setWidget(row, fieldIdx, commentVisibilityStrategy);
+ row++;
+
add(formGrid);
save = new Button(Util.C.buttonSaveChanges());
@@ -146,6 +178,8 @@ public class MyPreferencesScreen extends SettingsScreen {
e.listenTo(maximumPageSize);
e.listenTo(dateFormat);
e.listenTo(timeFormat);
+ e.listenTo(relativeDateInChangeTable);
+ e.listenTo(commentVisibilityStrategy);
}
@Override
@@ -167,6 +201,8 @@ public class MyPreferencesScreen extends SettingsScreen {
maximumPageSize.setEnabled(on);
dateFormat.setEnabled(on);
timeFormat.setEnabled(on);
+ relativeDateInChangeTable.setEnabled(on);
+ commentVisibilityStrategy.setEnabled(on);
}
private void display(final AccountGeneralPreferences p) {
@@ -180,6 +216,10 @@ public class MyPreferencesScreen extends SettingsScreen {
p.getDateFormat());
setListBox(timeFormat, AccountGeneralPreferences.TimeFormat.HHMM_12, //
p.getTimeFormat());
+ relativeDateInChangeTable.setValue(p.isRelativeDateInChangeTable());
+ setListBox(commentVisibilityStrategy,
+ AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT,
+ p.getCommentVisibilityStrategy());
}
private void setListBox(final ListBox f, final short defaultValue,
@@ -243,6 +283,10 @@ public class MyPreferencesScreen extends SettingsScreen {
p.setTimeFormat(getListBox(timeFormat,
AccountGeneralPreferences.TimeFormat.HHMM_12,
AccountGeneralPreferences.TimeFormat.values()));
+ p.setRelativeDateInChangeTable(relativeDateInChangeTable.getValue());
+ p.setCommentVisibilityStrategy(getListBox(commentVisibilityStrategy,
+ CommentVisibilityStrategy.EXPAND_MOST_RECENT,
+ CommentVisibilityStrategy.values()));
enable(false);
save.setEnabled(false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
index ae04e0aec7..01d6e3c9e5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
@@ -16,13 +16,23 @@ package com.google.gerrit.client.account;
import static com.google.gerrit.client.FormatUtil.mediumFormat;
+import com.google.gerrit.client.AvatarImage;
+import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gwt.i18n.client.LocaleInfo;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
public class MyProfileScreen extends SettingsScreen {
+ private AvatarImage avatar;
+ private Anchor changeAvatar;
private int labelIdx, fieldIdx;
private Grid info;
@@ -30,6 +40,18 @@ public class MyProfileScreen extends SettingsScreen {
protected void onInitUI() {
super.onInitUI();
+ HorizontalPanel h = new HorizontalPanel();
+ add(h);
+
+ VerticalPanel v = new VerticalPanel();
+ v.addStyleName(Gerrit.RESOURCES.css().avatarInfoPanel());
+ h.add(v);
+ avatar = new AvatarImage();
+ v.add(avatar);
+ changeAvatar = new Anchor(Util.C.changeAvatar(), "", "_blank");
+ changeAvatar.setVisible(false);
+ v.add(changeAvatar);
+
if (LocaleInfo.getCurrentLocale().isRTL()) {
labelIdx = 1;
fieldIdx = 0;
@@ -41,7 +63,7 @@ public class MyProfileScreen extends SettingsScreen {
info = new Grid((Gerrit.getConfig().siteHasUsernames() ? 1 : 0) + 4, 2);
info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
- add(info);
+ h.add(info);
int row = 0;
if (Gerrit.getConfig().siteHasUsernames()) {
@@ -72,6 +94,20 @@ public class MyProfileScreen extends SettingsScreen {
}
void display(final Account account) {
+ avatar.setAccount(FormatUtil.asInfo(account), 93, false);
+ new RestApi("/accounts/").id("self").view("avatar.change.url")
+ .get(new AsyncCallback<NativeString>() {
+ @Override
+ public void onSuccess(NativeString changeUrl) {
+ changeAvatar.setHref(changeUrl.asString());
+ changeAvatar.setVisible(true);
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ });
+
int row = 0;
if (Gerrit.getConfig().siteHasUsernames()) {
info.setWidget(row++, fieldIdx, new UsernameField());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index c276a89e3e..51ff978fa4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -23,7 +23,7 @@ import com.google.gerrit.client.groups.GroupInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
import com.google.gerrit.client.ui.AddMemberBox;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.Hyperlink;
@@ -318,7 +318,7 @@ public class AccountGroupMembersScreen extends AccountGroupScreen {
CheckBox checkBox = new CheckBox();
table.setWidget(row, 1, checkBox);
checkBox.setEnabled(enabled);
- table.setWidget(row, 2, new AccountLink(i));
+ table.setWidget(row, 2, new AccountLinkPanel(i));
table.setText(row, 3, i.email());
final FlexCellFormatter fmt = table.getFlexCellFormatter();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 1637919e0d..ce277809d9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -158,6 +158,7 @@ capabilityNames = \
queryLimit, \
runGC, \
startReplication, \
+ streamEvents, \
viewCaches, \
viewConnections, \
viewQueue
@@ -173,6 +174,7 @@ priority = Priority
queryLimit = Query Limit
runGC = Run Garbage Collection
startReplication = Start Replication
+streamEvents = Stream Events
viewCaches = View Caches
viewConnections = View Connections
viewQueue = View Queue
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index deb867ca43..08877d9044 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -27,7 +27,7 @@ import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
import com.google.gerrit.client.ui.AddMemberBox;
import com.google.gerrit.client.ui.ReviewerSuggestOracle;
import com.google.gerrit.common.data.ApprovalDetail;
@@ -329,7 +329,7 @@ public class ApprovalTable extends Composite {
final CellFormatter fmt = table.getCellFormatter();
int col = 0;
- table.setWidget(row, col++, new AccountLink(account));
+ table.setWidget(row, col++, new AccountLinkPanel(account));
rows.put(account._account_id(), row);
if (ad.canRemove()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 500f06e3c9..968c726514 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -113,6 +113,7 @@ public interface ChangeConstants extends Constants {
String includedInTableTag();
String messageNoAuthor();
+ String messageExpandMostRecent();
String messageExpandRecent();
String messageExpandAll();
String messageCollapseAll();
@@ -122,6 +123,7 @@ public interface ChangeConstants extends Constants {
String patchSetInfoCommitter();
String patchSetInfoDownload();
String patchSetInfoParents();
+ String patchSetWithDraftCommentsToolTip();
String initialCommit();
String buttonRebaseChange();
@@ -171,4 +173,10 @@ public interface ChangeConstants extends Constants {
String diffAllSideBySide();
String diffAllUnified();
+
+ String inTheFuture();
+ String month();
+ String months();
+ String year();
+ String years();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 06bd64dde5..4c123784fb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -90,6 +90,7 @@ includedInTableBranch = Branch Name
includedInTableTag = Tag Name
messageNoAuthor = Gerrit Code Review
+messageExpandMostRecent = Expand Most Recent
messageExpandRecent = Expand Recent
messageExpandAll = Expand All
messageCollapseAll = Collapse All
@@ -99,6 +100,7 @@ patchSetInfoAuthor = Author
patchSetInfoCommitter = Committer
patchSetInfoDownload = Download
patchSetInfoParents = Parent(s)
+patchSetWithDraftCommentsToolTip = Draft comment(s) inside
initialCommit = Initial Commit
buttonAbandonChangeBegin = Abandon Change
@@ -152,3 +154,9 @@ buttonClose = Close
diffAllSideBySide = All Side-by-Side
diffAllUnified = All Unified
+
+inTheFuture = in the future
+month = month
+months = months
+years = years
+year = year
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
index 8395907a3e..2fcb7e87dc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.changes;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.common.data.AccountInfoCache;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.SubmitTypeRecord;
@@ -37,10 +38,11 @@ public class ChangeDescriptionBlock extends Composite {
}
public void display(ChangeDetail chg, Boolean starred, Boolean canEditCommitMessage,
- PatchSetInfo info,
- final AccountInfoCache acc, SubmitTypeRecord submitTypeRecord) {
+ PatchSetInfo info, AccountInfoCache acc,
+ SubmitTypeRecord submitTypeRecord,
+ CommentLinkProcessor commentLinkProcessor) {
infoBlock.display(chg, acc, submitTypeRecord);
messageBlock.display(chg.getChange().currentPatchSetId(), starred,
- canEditCommitMessage, info.getMessage());
+ canEditCommitMessage, info.getMessage(), commentLinkProcessor);
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
index afc17f7c60..b4ae2f3d7b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
@@ -18,7 +18,7 @@ import static com.google.gerrit.client.FormatUtil.mediumFormat;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.CommentedActionDialog;
import com.google.gerrit.client.ui.InlineHyperlink;
@@ -105,7 +105,7 @@ public class ChangeInfoBlock extends Composite {
changeIdLabel.setPreviewText(chg.getKey().get());
table.setWidget(R_CHANGE_ID, 1, changeIdLabel);
- table.setWidget(R_OWNER, 1, AccountLink.link(acc, chg.getOwner()));
+ table.setWidget(R_OWNER, 1, AccountLinkPanel.link(acc, chg.getOwner()));
final FlowPanel p = new FlowPanel();
p.add(new ProjectSearchLink(chg.getProject()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index ba46702ebf..098fe0782d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -56,4 +56,15 @@ public interface ChangeMessages extends Messages {
String groupIsNotAllowed(String group);
String groupHasTooManyMembers(String group);
String groupManyMembersConfirmation(String group, int memberCount);
+
+ String secondsAgo(long seconds);
+ String minutesAgo(long minutes);
+ String hoursAgo(long hours);
+ String daysAgo(long days);
+ String weeksAgo(long weeks);
+ String monthsAgo(long months);
+ String yearsAgo(long years);
+ String years0MonthsAgo(long years, String yearLabel);
+ String yearsMonthsAgo(long years, String yearLabel, long months,
+ String monthLabel);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index caaf3bf24e..e02c27c755 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -39,3 +39,13 @@ groupIsEmpty = The group {0} does not have any members to add as reviewers.
groupIsNotAllowed = The group {0} cannot be added as reviewer.
groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers.
groupManyMembersConfirmation = The group {0} has {1} members. Do you want to add them all as reviewers?
+
+secondsAgo = {0} seconds ago
+minutesAgo = {0} minutes ago
+hoursAgo = {0} hours ago
+daysAgo = {0} days ago
+weeksAgo = {0} weeks ago
+monthsAgo = {0} months ago
+years0MonthsAgo = {0} {1} ago
+yearsMonthsAgo = {0} {1}, {2} {3} ago
+yearsAgo = {0} years ago
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index 91c885691a..c69f915ea3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -18,7 +18,10 @@ import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.projects.ConfigInfoCache;
+import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.CommentPanel;
import com.google.gerrit.client.ui.ComplexDisclosurePanel;
import com.google.gerrit.client.ui.ExpandAllCommand;
@@ -28,6 +31,7 @@ import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.data.AccountInfoCache;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.ChangeInfo;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -78,6 +82,7 @@ public class ChangeScreen extends Screen
private PatchSetsBlock patchSetsBlock;
private Panel comments;
+ private CommentLinkProcessor commentLinkProcessor;
private KeyCommandSet keysNavigation;
private KeyCommandSet keysAction;
@@ -264,10 +269,26 @@ public class ChangeScreen extends Screen
@Override
public void onValueChange(final ValueChangeEvent<ChangeDetail> event) {
if (isAttached() && isLastValueChangeHandler()) {
- // Until this screen is fully migrated to the new API, this call must be
- // sequential, because we can't start an async get at the source of every
- // call that might trigger a value change.
- ChangeApi.detail(event.getValue().getChange().getId().get(),
+ // Until this screen is fully migrated to the new API, these calls must
+ // happen sequentially after the ChangeDetail lookup, because we can't
+ // start an async get at the source of every call that might trigger a
+ // value change.
+ CallbackGroup cbs = new CallbackGroup();
+ ConfigInfoCache.get(
+ event.getValue().getChange().getProject(),
+ cbs.add(new GerritCallback<ConfigInfoCache.Entry>() {
+ @Override
+ public void onSuccess(ConfigInfoCache.Entry result) {
+ commentLinkProcessor = result.getCommentLinkProcessor();
+ setTheme(result.getTheme());
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ // Handled by last callback's onFailure.
+ }
+ }));
+ ChangeApi.detail(event.getValue().getChange().getId().get(), cbs.add(
new GerritCallback<com.google.gerrit.client.changes.ChangeInfo>() {
@Override
public void onSuccess(
@@ -275,7 +296,7 @@ public class ChangeScreen extends Screen
changeInfo = result;
display(event.getValue());
}
- });
+ }));
}
}
@@ -305,7 +326,8 @@ public class ChangeScreen extends Screen
detail.isStarred(),
detail.canEditCommitMessage(),
detail.getCurrentPatchSetDetail().getInfo(),
- detail.getAccounts(), detail.getSubmitTypeRecord());
+ detail.getAccounts(), detail.getSubmitTypeRecord(),
+ commentLinkProcessor);
dependsOn.display(detail.getDependsOn());
neededBy.display(detail.getNeededBy());
approvals.display(changeInfo);
@@ -399,6 +421,13 @@ public class ChangeScreen extends Screen
final long AGE = 7 * 24 * 60 * 60 * 1000L;
final Timestamp aged = new Timestamp(System.currentTimeMillis() - AGE);
+ CommentVisibilityStrategy commentVisibilityStrategy =
+ CommentVisibilityStrategy.EXPAND_MOST_RECENT;
+ if (Gerrit.isSignedIn()) {
+ commentVisibilityStrategy = Gerrit.getUserAccount()
+ .getGeneralPreferences().getCommentVisibilityStrategy();
+ }
+
for (int i = 0; i < msgList.size(); i++) {
final ChangeMessage msg = msgList.get(i);
@@ -417,14 +446,29 @@ public class ChangeScreen extends Screen
isRecent = msg.getWrittenOn().after(aged);
}
- final CommentPanel cp =
- new CommentPanel(author, msg.getWrittenOn(), msg.getMessage());
+ final CommentPanel cp = new CommentPanel(author, msg.getWrittenOn(),
+ msg.getMessage(), commentLinkProcessor);
cp.setRecent(isRecent);
cp.addStyleName(Gerrit.RESOURCES.css().commentPanelBorder());
if (i == msgList.size() - 1) {
cp.addStyleName(Gerrit.RESOURCES.css().commentPanelLast());
- cp.setOpen(true);
}
+ boolean isOpen = false;
+ switch (commentVisibilityStrategy) {
+ case COLLAPSE_ALL:
+ break;
+ case EXPAND_RECENT:
+ isOpen = isRecent;
+ break;
+ case EXPAND_ALL:
+ isOpen = true;
+ break;
+ case EXPAND_MOST_RECENT:
+ default:
+ isOpen = i == msgList.size() - 1;
+ break;
+ }
+ cp.setOpen(isOpen);
comments.add(cp);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 97a5a09dd5..3a65d20bb9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -17,7 +17,7 @@ package com.google.gerrit.client.changes;
import static com.google.gerrit.client.FormatUtil.shortFormat;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.ChangeLink;
import com.google.gerrit.client.ui.NavigationTable;
@@ -182,8 +182,8 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
}
}
- private AccountLink link(final Account.Id id) {
- return AccountLink.link(accountCache, id);
+ private AccountLinkPanel link(final Account.Id id) {
+ return AccountLinkPanel.link(accountCache, id);
}
public void addSection(final Section s) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
index 03cc11d5de..4694272f07 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -14,11 +14,12 @@
package com.google.gerrit.client.changes;
+import static com.google.gerrit.client.FormatUtil.relativeFormat;
import static com.google.gerrit.client.FormatUtil.shortFormat;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.ChangeLink;
import com.google.gerrit.client.ui.NavigationTable;
@@ -197,7 +198,7 @@ public class ChangeTable2 extends NavigationTable<ChangeInfo> {
table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
if (c.owner() != null) {
- table.setWidget(row, C_OWNER, new AccountLink(c.owner(), status));
+ table.setWidget(row, C_OWNER, new AccountLinkPanel(c.owner(), status));
} else {
table.setText(row, C_OWNER, "");
}
@@ -206,7 +207,13 @@ public class ChangeTable2 extends NavigationTable<ChangeInfo> {
row, C_PROJECT, new ProjectLink(c.project_name_key(), c.status()));
table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c
.status(), c.branch(), c.topic()));
- table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
+ if (Gerrit.isSignedIn()
+ && Gerrit.getUserAccount().getGeneralPreferences()
+ .isRelativeDateInChangeTable()) {
+ table.setText(row, C_LAST_UPDATE, relativeFormat(c.updated()));
+ } else {
+ table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
+ }
boolean displayName = Gerrit.isSignedIn() && Gerrit.getUserAccount()
.getGeneralPreferences().isShowUsernameInReviewCategory();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
index ea184dfbc0..198480ec6f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
@@ -68,8 +68,9 @@ public class CommitMessageBlock extends Composite {
initWidget(uiBinder.createAndBindUi(this));
}
- public void display(final String commitMessage) {
- display(null, null, false, commitMessage);
+ public void display(String commitMessage,
+ CommentLinkProcessor commentLinkProcessor) {
+ display(null, null, false, commitMessage, commentLinkProcessor);
}
private abstract class CommitMessageEditDialog extends CommentedActionDialog<ChangeDetail> {
@@ -103,7 +104,8 @@ public class CommitMessageBlock extends Composite {
}
public void display(final PatchSet.Id patchSetId,
- Boolean starred, Boolean canEditCommitMessage, final String commitMessage) {
+ Boolean starred, Boolean canEditCommitMessage, final String commitMessage,
+ CommentLinkProcessor commentLinkProcessor) {
starPanel.clear();
if (patchSetId != null && starred != null && Gerrit.isSignedIn()) {
Change.Id changeId = patchSetId.getParentKey();
@@ -170,7 +172,7 @@ public class CommitMessageBlock extends Composite {
// Linkify commit summary
SafeHtml commitSummaryLinkified = new SafeHtmlBuilder().append(commitSummary);
commitSummaryLinkified = commitSummaryLinkified.linkify();
- commitSummaryLinkified = CommentLinkProcessor.apply(commitSummaryLinkified);
+ commitSummaryLinkified = commentLinkProcessor.apply(commitSummaryLinkified);
commitSummaryPre.setInnerHTML(commitSummaryLinkified.asString());
// Hide commit body if there is no body
@@ -180,7 +182,7 @@ public class CommitMessageBlock extends Composite {
// Linkify commit body
SafeHtml commitBodyLinkified = new SafeHtmlBuilder().append(commitBody);
commitBodyLinkified = commitBodyLinkified.linkify();
- commitBodyLinkified = CommentLinkProcessor.apply(commitBodyLinkified);
+ commitBodyLinkified = commentLinkProcessor.apply(commitBodyLinkified);
commitBodyLinkified = commitBodyLinkified.replaceAll("\n\n", "<p></p>");
commitBodyLinkified = commitBodyLinkified.replaceAll("\n", "<br />");
commitBodyPre.setInnerHTML(commitBodyLinkified.asString());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index 76f77a2b07..ca326cd819 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -21,7 +21,7 @@ import com.google.gerrit.client.GitwebLink;
import com.google.gerrit.client.download.DownloadPanel;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountLink;
+import com.google.gerrit.client.ui.AccountLinkPanel;
import com.google.gerrit.client.ui.CommentedActionDialog;
import com.google.gerrit.client.ui.ComplexDisclosurePanel;
import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
@@ -45,6 +45,7 @@ import com.google.gwt.user.client.ui.DisclosurePanel;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Panel;
@@ -78,7 +79,8 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
* Creates a closed complex disclosure panel for a patch set.
* The patch set details are loaded when the complex disclosure panel is opened.
*/
- public PatchSetComplexDisclosurePanel(final PatchSet ps, boolean isOpen) {
+ public PatchSetComplexDisclosurePanel(final PatchSet ps, boolean isOpen,
+ boolean hasDraftComments) {
super(Util.M.patchSetHeader(ps.getPatchSetId()), isOpen);
detailCache = ChangeCache.get(ps.getId().getParentKey()).getChangeDetailCache();
changeDetail = detailCache.get();
@@ -87,11 +89,17 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
body = new FlowPanel();
setContent(body);
+ if (hasDraftComments) {
+ final Image draftComments = new Image(Gerrit.RESOURCES.draftComments());
+ draftComments.setTitle(Util.C.patchSetWithDraftCommentsToolTip());
+ getHeader().add(draftComments);
+ }
+
final GitwebLink gw = Gerrit.getGitwebLink();
final InlineLabel revtxt = new InlineLabel(ps.getRevision().get() + " ");
revtxt.addStyleName(Gerrit.RESOURCES.css().patchSetRevision());
getHeader().add(revtxt);
- if (gw != null) {
+ if (gw != null && gw.canLink(ps)) {
final Anchor revlink =
new Anchor(gw.getLinkName(), false, gw.toRevision(changeDetail.getChange()
.getProject(), ps));
@@ -110,6 +118,7 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
} else {
addOpenHandler(this);
}
+
}
public void setDiffBaseId(PatchSet.Id diffBaseId) {
@@ -250,7 +259,7 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
fp.setStyleName(Gerrit.RESOURCES.css().patchSetUserIdentity());
if (who.getName() != null) {
if (who.getAccount() != null) {
- fp.add(new AccountLink(who));
+ fp.add(new AccountLinkPanel(who));
} else {
final InlineLabel lbl = new InlineLabel(who.getName());
lbl.setStyleName(Gerrit.RESOURCES.css().accountName());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
index 5a6e427dfd..480518541f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
@@ -88,7 +88,8 @@ public class PatchSetsBlock extends Composite {
for (final PatchSet ps : patchSets) {
final PatchSetComplexDisclosurePanel p =
- new PatchSetComplexDisclosurePanel(ps, ps == currps);
+ new PatchSetComplexDisclosurePanel(ps, ps == currps,
+ detail.hasDraftComments(ps.getId()));
if (diffBaseId != null) {
p.setDiffBaseId(diffBaseId);
if (ps == currps) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
index 50fc892f74..6c14740a46 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
@@ -48,7 +48,9 @@ import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class PatchTable extends Composite {
public interface PatchValidator {
@@ -80,6 +82,7 @@ public class PatchTable extends Composite {
private String savePointerId;
private PatchSet.Id base;
private List<Patch> patchList;
+ private Map<Patch.Key, Integer> patchMap;
private ListenableAccountDiffPreference listenablePrefs;
private List<ClickHandler> clickHandlers;
@@ -97,18 +100,25 @@ public class PatchTable extends Composite {
}
public int indexOf(Patch.Key patch) {
- for (int i = 0; i < patchList.size(); i++) {
- if (patchList.get(i).getKey().equals(patch)) {
- return i;
+ Integer i = patchMap().get(patch);
+ return i != null ? i : -1;
+ }
+
+ private Map<Key, Integer> patchMap() {
+ if (patchMap == null) {
+ patchMap = new HashMap<Patch.Key, Integer>();
+ for (int i = 0; i < patchList.size(); i++) {
+ patchMap.put(patchList.get(i).getKey(), i);
}
}
- return -1;
+ return patchMap;
}
public void display(PatchSet.Id base, PatchSetDetail detail) {
this.base = base;
this.detail = detail;
this.patchList = detail.getPatches();
+ this.patchMap = null;
myTable = null;
final DisplayCommand cmd = new DisplayCommand(patchList, base);
@@ -328,36 +338,33 @@ public class PatchTable extends Composite {
}
void updateReviewedStatus(final Patch.Key patchKey, boolean reviewed) {
- final int row = findRow(patchKey);
- if (0 <= row) {
- final Patch patch = getRowItem(row);
- if (patch != null) {
- patch.setReviewedByCurrentUser(reviewed);
-
+ int idx = patchMap().get(patchKey);
+ if (0 <= idx) {
+ Patch patch = patchList.get(idx);
+ if (patch.isReviewedByCurrentUser() != reviewed) {
+ int row = idx + 1;
int col = C_SIDEBYSIDE + 2;
if (patch.getPatchType() == Patch.PatchType.BINARY) {
col = C_SIDEBYSIDE + 3;
}
-
if (reviewed) {
table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
} else {
table.clearCell(row, col);
}
+ patch.setReviewedByCurrentUser(reviewed);
}
}
}
void notifyDraftDelta(final Patch.Key key, final int delta) {
- final int row = findRow(key);
- if (0 <= row) {
- final Patch p = getRowItem(row);
- if (p != null) {
- p.setDraftCount(p.getDraftCount() + delta);
- final SafeHtmlBuilder m = new SafeHtmlBuilder();
- appendCommentCount(m, p);
- SafeHtml.set(table, row, C_DRAFT, m);
- }
+ int idx = patchMap().get(key);
+ if (0 <= idx) {
+ Patch p = patchList.get(idx);
+ p.setDraftCount(p.getDraftCount() + delta);
+ SafeHtmlBuilder m = new SafeHtmlBuilder();
+ appendCommentCount(m, p);
+ SafeHtml.set(table, idx + 1, C_DRAFT, m);
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index 7f61977c38..4e710db053 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
@@ -20,6 +20,7 @@ import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
import com.google.gerrit.client.patches.AbstractPatchContentTable;
import com.google.gerrit.client.patches.CommentEditorContainer;
import com.google.gerrit.client.patches.CommentEditorPanel;
+import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
@@ -79,6 +80,7 @@ public class PublishCommentScreen extends AccountScreen implements
private boolean saveStateOnUnload = true;
private List<CommentEditorPanel> commentEditors;
private ChangeInfo change;
+ private CommentLinkProcessor commentLinkProcessor;
public PublishCommentScreen(final PatchSet.Id psi) {
patchSetId = psi;
@@ -149,8 +151,8 @@ public class PublishCommentScreen extends AccountScreen implements
super.onLoad();
CallbackGroup cbs = new CallbackGroup();
- ChangeApi.revision(patchSetId).view("review").get(cbs.add(
- new AsyncCallback<ChangeInfo>() {
+ ChangeApi.revision(patchSetId).view("review")
+ .get(cbs.add(new AsyncCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo result) {
result.init();
@@ -167,7 +169,7 @@ public class PublishCommentScreen extends AccountScreen implements
@Override
protected void preDisplay(final PatchSetPublishDetail result) {
send.setEnabled(true);
- display(result);
+ PublishCommentScreen.this.preDisplay(result, this);
}
@Override
@@ -177,6 +179,24 @@ public class PublishCommentScreen extends AccountScreen implements
}));
}
+ private void preDisplay(final PatchSetPublishDetail pubDetail,
+ final ScreenLoadCallback<PatchSetPublishDetail> origCb) {
+ ConfigInfoCache.get(pubDetail.getChange().getProject(),
+ new AsyncCallback<ConfigInfoCache.Entry>() {
+ @Override
+ public void onSuccess(ConfigInfoCache.Entry result) {
+ commentLinkProcessor = result.getCommentLinkProcessor();
+ setTheme(result.getTheme());
+ display(pubDetail);
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ origCb.onFailure(caught);
+ }
+ });
+ }
+
@Override
protected void onUnload() {
super.onUnload();
@@ -282,7 +302,7 @@ public class PublishCommentScreen extends AccountScreen implements
for (String value : values) {
ValueRadioButton b = new ValueRadioButton(label, value);
SafeHtml buf = new SafeHtmlBuilder().append(b.format());
- buf = CommentLinkProcessor.apply(buf);
+ buf = commentLinkProcessor.apply(buf);
SafeHtml.set(b, buf);
if (lastState != null && patchSetId.equals(lastState.patchSetId)
@@ -305,7 +325,7 @@ public class PublishCommentScreen extends AccountScreen implements
setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(),
patchSetId.get()));
descBlock.display(changeDetail, null, false, r.getPatchSetInfo(), r.getAccounts(),
- r.getSubmitTypeRecord());
+ r.getSubmitTypeRecord(), commentLinkProcessor);
if (r.getChange().getStatus().isOpen()) {
initApprovals(approvalPanel);
@@ -341,11 +361,14 @@ public class PublishCommentScreen extends AccountScreen implements
priorFile = fn;
}
- final CommentEditorPanel editor = new CommentEditorPanel(c);
+ final CommentEditorPanel editor =
+ new CommentEditorPanel(c, commentLinkProcessor);
if (c.getLine() == AbstractPatchContentTable.R_HEAD) {
- editor.setAuthorNameText(Util.C.fileCommentHeader());
+ editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
+ Util.C.fileCommentHeader());
} else {
- editor.setAuthorNameText(Util.M.lineHeader(c.getLine()));
+ editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
+ Util.M.lineHeader(c.getLine()));
}
editor.setOpen(true);
commentEditors.add(editor);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png
new file mode 100644
index 0000000000..4be4541a05
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png
new file mode 100644
index 0000000000..31c770fdb7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 94254431d9..5f16af6c03 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -20,7 +20,7 @@
@def black #000000;
@def white #ffffff;
-@def norm-font Arial Unicode MS, Arial, sans-serif;
+@def norm-font sans-serif;
@def mono-font monospace;
@eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
@@ -86,6 +86,21 @@ a:hover {
text-decoration: underline;
}
+.accountLinkPanel {
+ display: inline;
+}
+
+.accountLinkPanel img {
+ margin-right: 0.2em;
+ position: relative;
+ top: 2px;
+}
+
+.accountLinkPanel a {
+ position: relative;
+ top: -1px;
+}
+
.accountName {
white-space: nowrap;
}
@@ -922,7 +937,6 @@ a:hover {
}
.sideBySideTableBinaryHeader {
- border-right: thin solid #b0bdcc;
border-left: thin solid #b0bdcc;
width: 100%;
color: grey;
@@ -967,6 +981,13 @@ a:hover {
float: left;
}
+.avatarInfoPanel {
+ margin-right: 10px;
+}
+.avatarInfoPanel td {
+ text-align: center;
+}
+
.infoBlock {
border-collapse: collapse;
border-spacing: 0;
@@ -1160,7 +1181,7 @@ a:hover.downloadLink {
margin-right: 5em;
font-weight: bold;
font-size: medium;
- font-family: Arial Unicode;
+ font-family: norm-font;
}
/** Patch History Table **/
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
index 3b4abe5888..f52999020a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
@@ -26,7 +26,7 @@ public class GroupInfo extends JavaScriptObject {
}
public final AccountGroup.UUID getGroupUUID() {
- return new AccountGroup.UUID(URL.decodePathSegment(id()));
+ return new AccountGroup.UUID(URL.decodeQueryString(id()));
}
public final native String id() /*-{ return this.id; }-*/;
@@ -46,13 +46,13 @@ public class GroupInfo extends JavaScriptObject {
public final AccountGroup.UUID getOwnerUUID() {
String owner = owner_id();
if (owner != null) {
- return new AccountGroup.UUID(URL.decodePathSegment(owner));
+ return new AccountGroup.UUID(URL.decodeQueryString(owner));
}
return null;
}
public final void setOwnerUUID(AccountGroup.UUID uuid) {
- owner_id(URL.encodePathSegment(uuid.get()));
+ owner_id(URL.encodeQueryString(uuid.get()));
}
protected GroupInfo() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index 8de3251bdf..a0789ec72a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -21,6 +21,7 @@ import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.changes.PatchTable;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.CommentPanel;
import com.google.gerrit.client.ui.NavigationTable;
import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
@@ -29,13 +30,14 @@ import com.google.gerrit.common.data.CommentDetail;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchSetDetail;
import com.google.gerrit.prettify.client.ClientSideFormatter;
-import com.google.gerrit.prettify.common.PrettyFormatter;
+import com.google.gerrit.prettify.client.PrettyFormatter;
+import com.google.gerrit.prettify.client.SparseHtmlFile;
import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
@@ -60,6 +62,7 @@ import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.KeyCommand;
import com.google.gwtexpui.globalkey.client.KeyCommandSet;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtorm.client.KeyUtil;
import org.eclipse.jgit.diff.Edit;
@@ -86,6 +89,7 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object>
private HandlerRegistration regComment;
private final KeyCommandSet keysOpenByEnter;
private HandlerRegistration regOpenByEnter;
+ private CommentLinkProcessor commentLinkProcessor;
boolean isDisplayBinary;
protected AbstractPatchContentTable() {
@@ -241,6 +245,10 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object>
render(s, d);
}
+ void setCommentLinkProcessor(CommentLinkProcessor commentLinkProcessor) {
+ this.commentLinkProcessor = commentLinkProcessor;
+ }
+
protected boolean hasDifferences(PatchScript script) {
return hasEdits(script) || hasMeta(script);
}
@@ -306,6 +314,23 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object>
return f;
}
+ protected String getUrlA() {
+ final String rawBase = GWT.getHostPageBaseURL() + "cat/";
+ final String url;
+ if (idSideA == null) {
+ url = rawBase + KeyUtil.encode(patchKey.toString()) + "^1";
+ } else {
+ Patch.Key k = new Patch.Key(idSideA, patchKey.get());
+ url = rawBase + KeyUtil.encode(k.toString()) + "^0";
+ }
+ return url;
+ }
+
+ protected String getUrlB() {
+ final String rawBase = GWT.getHostPageBaseURL() + "cat/";
+ return rawBase + KeyUtil.encode(patchKey.toString()) + "^0";
+ }
+
protected abstract void render(PatchScript script, final PatchSetDetail detail);
protected abstract void onInsertComment(PatchLine pl);
@@ -553,7 +578,8 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object>
return null;
}
- final CommentEditorPanel ed = new CommentEditorPanel(newComment);
+ final CommentEditorPanel ed =
+ new CommentEditorPanel(newComment, commentLinkProcessor);
ed.addFocusHandler(this);
ed.addBlurHandler(this);
boolean isCommentRow = false;
@@ -690,7 +716,8 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object>
protected void bindComment(final int row, final int col,
final PatchLineComment line, final boolean isLast, boolean expandComment) {
if (line.getStatus() == PatchLineComment.Status.DRAFT) {
- final CommentEditorPanel plc = new CommentEditorPanel(line);
+ final CommentEditorPanel plc =
+ new CommentEditorPanel(line, commentLinkProcessor);
plc.addFocusHandler(this);
plc.addBlurHandler(this);
table.setWidget(row, col, plc);
@@ -864,7 +891,7 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object>
final Button replyDone;
PublishedCommentPanel(final AccountInfo author, final PatchLineComment c) {
- super(author, c.getWrittenOn(), c.getMessage());
+ super(author, c.getWrittenOn(), c.getMessage(), commentLinkProcessor);
this.comment = c;
reply = new Button(PatchUtil.C.buttonReply());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index b609f15bf3..9ed3f1802e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -16,20 +16,20 @@ package com.google.gerrit.client.patches;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.CommentPanel;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.DoubleClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.user.client.Timer;
-import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
import java.sql.Timestamp;
@@ -59,11 +59,13 @@ public class CommentEditorPanel extends CommentPanel implements ClickHandler,
private final Button discard;
private final Timer expandTimer;
- public CommentEditorPanel(final PatchLineComment plc) {
+ public CommentEditorPanel(final PatchLineComment plc,
+ final CommentLinkProcessor commentLinkProcessor) {
+ super(commentLinkProcessor);
comment = plc;
addStyleName(Gerrit.RESOURCES.css().commentEditorPanel());
- setAuthorNameText(PatchUtil.C.draft());
+ setAuthorNameText(Gerrit.getUserAccountInfo(), PatchUtil.C.draft());
setMessageText(plc.getMessage());
addDoubleClickHandler(this);
@@ -81,18 +83,6 @@ public class CommentEditorPanel extends CommentPanel implements ClickHandler,
text.addKeyDownHandler(new KeyDownHandler() {
@Override
public void onKeyDown(final KeyDownEvent event) {
- if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE
- && !event.isAnyModifierKeyDown()) {
- event.preventDefault();
-
- if (isNew()) {
- onDiscard();
- } else {
- render();
- }
- return;
- }
-
if ((event.isControlKeyDown() || event.isMetaKeyDown())
&& !event.isAltKeyDown() && !event.isShiftKeyDown()) {
switch (event.getNativeKeyCode()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index d914283c0f..200562a791 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -21,14 +21,17 @@ import com.google.gerrit.client.RpcStatus;
import com.google.gerrit.client.changes.CommitMessageBlock;
import com.google.gerrit.client.changes.PatchTable;
import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.projects.ConfigInfoCache;
+import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchSetDetail;
import com.google.gerrit.prettify.client.ClientSideFormatter;
-import com.google.gerrit.prettify.common.PrettyFactory;
+import com.google.gerrit.prettify.client.PrettyFactory;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -38,6 +41,7 @@ import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.KeyCommand;
@@ -97,6 +101,7 @@ public abstract class PatchScreen extends Screen implements
protected PatchSet.Id idSideB;
protected PatchScriptSettingsPanel settingsPanel;
protected TopView topView;
+ protected CommentLinkProcessor commentLinkProcessor;
private ReviewedPanels reviewedPanels;
private HistoryTable historyTable;
@@ -364,23 +369,40 @@ public abstract class PatchScreen extends Screen implements
if (isFirst && fileList != null && fileList.isLoaded()) {
fileList.movePointerTo(patchKey);
}
- PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB, //
- settingsPanel.getValue(), new ScreenLoadCallback<PatchScript>(this) {
+
+ CallbackGroup cb = new CallbackGroup();
+ ConfigInfoCache.get(patchSetDetail.getProject(),
+ cb.add(new AsyncCallback<ConfigInfoCache.Entry>() {
@Override
- protected void preDisplay(final PatchScript result) {
- if (rpcSequence == rpcseq) {
- onResult(result, isFirst);
- }
+ public void onSuccess(ConfigInfoCache.Entry result) {
+ commentLinkProcessor = result.getCommentLinkProcessor();
+ contentTable.setCommentLinkProcessor(commentLinkProcessor);
+ setTheme(result.getTheme());
}
@Override
- public void onFailure(final Throwable caught) {
- if (rpcSequence == rpcseq) {
- settingsPanel.setEnabled(true);
- super.onFailure(caught);
- }
+ public void onFailure(Throwable caught) {
+ // Handled by ScreenLoadCallback.onFailure.
}
- });
+ }));
+ PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB,
+ settingsPanel.getValue(), cb.addGwtjsonrpc(
+ new ScreenLoadCallback<PatchScript>(this) {
+ @Override
+ protected void preDisplay(final PatchScript result) {
+ if (rpcSequence == rpcseq) {
+ onResult(result, isFirst);
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable caught) {
+ if (rpcSequence == rpcseq) {
+ settingsPanel.setEnabled(true);
+ super.onFailure(caught);
+ }
+ }
+ }));
}
private void onResult(final PatchScript script, final boolean isFirst) {
@@ -396,7 +418,8 @@ public abstract class PatchScreen extends Screen implements
if (idSideB.equals(patchSetDetail.getPatchSet().getId())) {
commitMessageBlock.setVisible(true);
- commitMessageBlock.display(patchSetDetail.getInfo().getMessage());
+ commitMessageBlock.display(patchSetDetail.getInfo().getMessage(),
+ commentLinkProcessor);
} else {
commitMessageBlock.setVisible(false);
Util.DETAIL_SVC.patchSetDetail(idSideB,
@@ -404,7 +427,8 @@ public abstract class PatchScreen extends Screen implements
@Override
public void onSuccess(PatchSetDetail result) {
commitMessageBlock.setVisible(true);
- commitMessageBlock.display(result.getInfo().getMessage());
+ commitMessageBlock.display(result.getInfo().getMessage(),
+ commentLinkProcessor);
}
});
}
@@ -431,6 +455,7 @@ public abstract class PatchScreen extends Screen implements
contentTable.removeFromParent();
contentTable = new UnifiedDiffTable();
contentTable.fileList = fileList;
+ contentTable.setCommentLinkProcessor(commentLinkProcessor);
contentPanel.add(contentTable);
setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
index 15ab951e9e..4bdd615662 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
@@ -22,10 +22,11 @@ import static com.google.gerrit.client.patches.PatchLine.Type.REPLACE;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.common.data.CommentDetail;
import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.common.data.PatchScript.FileMode;
import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.prettify.client.SparseHtmlFile;
import com.google.gerrit.prettify.common.EditList;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
@@ -186,11 +187,16 @@ public class SideBySideTable extends AbstractPatchContentTable {
lines.add(new SkippedLine(lastA, lastB, b.size() - lastB));
}
}
- }else{
+ } else {
// Display the patch header for binary
for (final String line : script.getPatchHeader()) {
appendFileHeader(nc, line);
}
+ // If there is a safe picture involved, we show it
+ if (script.getDisplayMethodA() == DisplayMethod.IMG
+ || script.getDisplayMethodB() == DisplayMethod.IMG) {
+ appendImageLine(script, nc);
+ }
}
if (!hasDifferences(script)) {
appendNoDifferences(nc);
@@ -210,6 +216,42 @@ public class SideBySideTable extends AbstractPatchContentTable {
}
}
+ private SafeHtml createImage(String url) {
+ SafeHtmlBuilder m = new SafeHtmlBuilder();
+ m.openElement("img");
+ m.setAttribute("src", url);
+ m.closeElement("img");
+ return m.toSafeHtml();
+ }
+
+ private void appendImageLine(final PatchScript script,
+ final SafeHtmlBuilder m) {
+ m.openTr();
+ m.setAttribute("valign", "center");
+ m.setAttribute("align", "center");
+
+ m.openTd();
+ m.setStyleName(Gerrit.RESOURCES.css().iconCell());
+ m.closeTd();
+
+ appendLineNumber(m, false);
+ if (script.getDisplayMethodA() == DisplayMethod.IMG) {
+ final String url = getUrlA();
+ appendLineText(m, DELETE, createImage(url), false, true);
+ } else {
+ appendLineNone(m, DELETE);
+ }
+ if (script.getDisplayMethodB() == DisplayMethod.IMG) {
+ final String url = getUrlB();
+ appendLineText(m, INSERT, createImage(url), false, true);
+ } else {
+ appendLineNone(m, INSERT);
+ }
+
+ appendLineNumber(m, true);
+ m.closeTr();
+ }
+
private void populateTableHeader(final PatchScript script,
final PatchSetDetail detail) {
initHeaders(script, detail);
@@ -400,10 +442,7 @@ public class SideBySideTable extends AbstractPatchContentTable {
m.addStyleName(Gerrit.RESOURCES.css().iconCell());
m.closeTd();
- m.openTd();
- m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
- m.nbsp();
- m.closeTd();
+ appendLineNumber(m, false);
m.openTd();
m.setStyleName(Gerrit.RESOURCES.css().sideBySideTableBinaryHeader());
@@ -411,9 +450,7 @@ public class SideBySideTable extends AbstractPatchContentTable {
m.append(line);
m.closeTd();
- m.openTd();
- m.nbsp();
- m.closeTd();
+ appendLineNumber(m, true);
m.closeTr();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
index 82df54a9b2..cacedfd106 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
@@ -23,20 +23,17 @@ import com.google.gerrit.common.data.CommentDetail;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.prettify.client.SparseHtmlFile;
import com.google.gerrit.prettify.common.EditList;
import com.google.gerrit.prettify.common.EditList.Hunk;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
-import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
-import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.UIObject;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtorm.client.KeyUtil;
import java.util.ArrayList;
import java.util.Collections;
@@ -226,94 +223,18 @@ public class UnifiedDiffTable extends AbstractPatchContentTable {
appendFileHeader(nc, line);
}
final ArrayList<PatchLine> lines = new ArrayList<PatchLine>();
- if (!isDisplayBinary) {
- final SparseHtmlFile a = getSparseHtmlFileA(script);
- final SparseHtmlFile b = getSparseHtmlFileB(script);
+
+ if (hasDifferences(script)) {
if (script.getDisplayMethodA() == DisplayMethod.IMG
|| script.getDisplayMethodB() == DisplayMethod.IMG) {
- final String rawBase = GWT.getHostPageBaseURL() + "cat/";
-
- nc.openTr();
- nc.setAttribute("valign", "center");
- nc.setAttribute("align", "center");
-
- nc.openTd();
- nc.nbsp();
- nc.closeTd();
-
- nc.openTd();
- nc.nbsp();
- nc.closeTd();
-
- nc.openTd();
- nc.nbsp();
- nc.closeTd();
-
- nc.openTd();
- if (script.getDisplayMethodA() == DisplayMethod.IMG) {
- if (idSideA == null) {
- appendImgTag(nc, rawBase + KeyUtil.encode(patchKey.toString()) + "^1");
- } else {
- Patch.Key k = new Patch.Key(idSideA, patchKey.get());
- appendImgTag(nc, rawBase + KeyUtil.encode(k.toString()) + "^0");
- }
- }
- if (script.getDisplayMethodB() == DisplayMethod.IMG) {
- appendImgTag(nc, rawBase + KeyUtil.encode(patchKey.toString()) + "^0");
- }
- nc.closeTd();
-
- nc.closeTr();
+ appendImageDifferences(script, nc);
+ } else if (!isDisplayBinary) {
+ appendTextDifferences(script, nc, lines);
}
-
- if (hasDifferences(script)) {
- final boolean syntaxHighlighting =
- script.getDiffPrefs().isSyntaxHighlighting();
- for (final EditList.Hunk hunk : script.getHunks()) {
- appendHunkHeader(nc, hunk);
- while (hunk.next()) {
- if (hunk.isContextLine()) {
- openLine(nc);
- appendLineNumberForSideA(nc, hunk.getCurA());
- appendLineNumberForSideB(nc, hunk.getCurB());
- appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
- closeLine(nc);
- hunk.incBoth();
- lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
-
- } else if (hunk.isDeletedA()) {
- openLine(nc);
- appendLineNumberForSideA(nc, hunk.getCurA());
- padLineNumberForSideB(nc);
- appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
- closeLine(nc);
- hunk.incA();
- lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
- if (a.size() == hunk.getCurA()
- && script.getA().isMissingNewlineAtEnd()) {
- appendNoLF(nc);
- }
-
- } else if (hunk.isInsertedB()) {
- openLine(nc);
- padLineNumberForSideA(nc);
- appendLineNumberForSideB(nc, hunk.getCurB());
- appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
- closeLine(nc);
- hunk.incB();
- lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
- if (b.size() == hunk.getCurB()
- && script.getB().isMissingNewlineAtEnd()) {
- appendNoLF(nc);
- }
- }
- }
- }
- }
- }
- if (!hasDifferences(script)) {
+ } else {
appendNoDifferences(nc);
}
+
resetHtml(nc);
populateTableHeader(script, detail);
if (hasDifferences(script)) {
@@ -347,6 +268,94 @@ public class UnifiedDiffTable extends AbstractPatchContentTable {
}
}
+ private void appendImageLine(final SafeHtmlBuilder nc, final String url,
+ final boolean syntaxHighlighting, final boolean isInsert) {
+ nc.openTr();
+ nc.setAttribute("valign", "center");
+ nc.setAttribute("align", "center");
+
+ nc.openTd();
+ nc.setStyleName(Gerrit.RESOURCES.css().iconCell());
+ nc.closeTd();
+
+ padLineNumberForSideA(nc);
+ padLineNumberForSideB(nc);
+
+ nc.openTd();
+ nc.setStyleName(Gerrit.RESOURCES.css().fileLine());
+ if (isInsert) {
+ setStyleInsert(nc, syntaxHighlighting);
+ } else {
+ setStyleDelete(nc, syntaxHighlighting);
+ }
+ appendImgTag(nc, url);
+ nc.closeTd();
+
+ nc.closeTr();
+ }
+
+ private void appendImageDifferences(final PatchScript script,
+ final SafeHtmlBuilder nc) {
+ final boolean syntaxHighlighting =
+ script.getDiffPrefs().isSyntaxHighlighting();
+ if (script.getDisplayMethodA() == DisplayMethod.IMG) {
+ final String url = getUrlA();
+ appendImageLine(nc, url, syntaxHighlighting, false);
+ }
+ if (script.getDisplayMethodB() == DisplayMethod.IMG) {
+ final String url = getUrlB();
+ appendImageLine(nc, url, syntaxHighlighting, true);
+ }
+ }
+
+ private void appendTextDifferences(final PatchScript script,
+ final SafeHtmlBuilder nc, final ArrayList<PatchLine> lines) {
+ final SparseHtmlFile a = getSparseHtmlFileA(script);
+ final SparseHtmlFile b = getSparseHtmlFileB(script);
+ final boolean syntaxHighlighting =
+ script.getDiffPrefs().isSyntaxHighlighting();
+ for (final EditList.Hunk hunk : script.getHunks()) {
+ appendHunkHeader(nc, hunk);
+ while (hunk.next()) {
+ if (hunk.isContextLine()) {
+ openLine(nc);
+ appendLineNumberForSideA(nc, hunk.getCurA());
+ appendLineNumberForSideB(nc, hunk.getCurB());
+ appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
+ closeLine(nc);
+ hunk.incBoth();
+ lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
+
+ } else if (hunk.isDeletedA()) {
+ openLine(nc);
+ appendLineNumberForSideA(nc, hunk.getCurA());
+ padLineNumberForSideB(nc);
+ appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
+ closeLine(nc);
+ hunk.incA();
+ lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
+ if (a.size() == hunk.getCurA()
+ && script.getA().isMissingNewlineAtEnd()) {
+ appendNoLF(nc);
+ }
+
+ } else if (hunk.isInsertedB()) {
+ openLine(nc);
+ padLineNumberForSideA(nc);
+ appendLineNumberForSideB(nc, hunk.getCurB());
+ appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
+ closeLine(nc);
+ hunk.incB();
+ lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
+ if (b.size() == hunk.getCurB()
+ && script.getB().isMissingNewlineAtEnd()) {
+ appendNoLF(nc);
+ }
+ }
+ }
+ }
+ }
+
@Override
public void display(final CommentDetail cd, boolean expandComments) {
if (cd.isEmpty()) {
@@ -519,6 +528,22 @@ public class UnifiedDiffTable extends AbstractPatchContentTable {
}
}
+ private void setStyleDelete(final SafeHtmlBuilder m,
+ boolean syntaxHighlighting) {
+ m.addStyleName(Gerrit.RESOURCES.css().diffTextDELETE());
+ if (syntaxHighlighting) {
+ m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE());
+ }
+ }
+
+ private void setStyleInsert(final SafeHtmlBuilder m,
+ boolean syntaxHighlighting) {
+ m.addStyleName(Gerrit.RESOURCES.css().diffTextINSERT());
+ if (syntaxHighlighting) {
+ m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT());
+ }
+ }
+
private void appendLineText(final SafeHtmlBuilder m,
boolean syntaxHighlighting, final PatchLine.Type type,
final SparseHtmlFile src, final int i) {
@@ -533,18 +558,12 @@ public class UnifiedDiffTable extends AbstractPatchContentTable {
m.append(text);
break;
case DELETE:
- m.addStyleName(Gerrit.RESOURCES.css().diffTextDELETE());
- if (syntaxHighlighting) {
- m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE());
- }
+ setStyleDelete(m, syntaxHighlighting);
m.append("-");
m.append(text);
break;
case INSERT:
- m.addStyleName(Gerrit.RESOURCES.css().diffTextINSERT());
- if (syntaxHighlighting) {
- m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT());
- }
+ setStyleInsert(m, syntaxHighlighting);
m.append("+");
m.append(text);
break;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
new file mode 100644
index 0000000000..522d348b89
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.projects;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwtexpui.safehtml.client.FindReplace;
+import com.google.gwtexpui.safehtml.client.LinkFindReplace;
+import com.google.gwtexpui.safehtml.client.RawFindReplace;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ConfigInfo extends JavaScriptObject {
+ public final native JavaScriptObject has_require_change_id()
+ /*-{ return this.hasOwnProperty('require_change_id'); }-*/;
+ public final native boolean require_change_id()
+ /*-{ return this.require_change_id; }-*/;
+
+ public final native JavaScriptObject has_use_content_merge()
+ /*-{ return this.hasOwnProperty('use_content_merge'); }-*/;
+ public final native boolean use_content_merge()
+ /*-{ return this.use_content_merge; }-*/;
+
+ public final native JavaScriptObject has_use_contributor_agreements()
+ /*-{ return this.hasOwnProperty('use_contributor_agreements'); }-*/;
+ public final native boolean use_contributor_agreements()
+ /*-{ return this.use_contributor_agreements; }-*/;
+
+ public final native JavaScriptObject has_use_signed_off_by()
+ /*-{ return this.hasOwnProperty('use_signed_off_by'); }-*/;
+ public final native boolean use_signed_off_by()
+ /*-{ return this.use_signed_off_by; }-*/;
+
+ private final native NativeMap<CommentLinkInfo> commentlinks0()
+ /*-{ return this.commentlinks; }-*/;
+ final List<FindReplace> commentlinks() {
+ JsArray<CommentLinkInfo> cls = commentlinks0().values();
+ List<FindReplace> commentLinks = new ArrayList<FindReplace>(cls.length());
+ for (int i = 0; i < cls.length(); i++) {
+ CommentLinkInfo cl = cls.get(i);
+ if (!cl.enabled()) {
+ continue;
+ }
+ if (cl.link() != null) {
+ commentLinks.add(new LinkFindReplace(cl.match(), cl.link()));
+ } else {
+ commentLinks.add(new RawFindReplace(cl.match(), cl.html()));
+ }
+ }
+ return commentLinks;
+ }
+
+ final native ThemeInfo theme() /*-{ return this.theme; }-*/;
+
+ protected ConfigInfo() {
+ }
+
+ static class CommentLinkInfo extends JavaScriptObject {
+ final native String match() /*-{ return this.match; }-*/;
+ final native String link() /*-{ return this.link; }-*/;
+ final native String html() /*-{ return this.html; }-*/;
+ final native boolean enabled() /*-{
+ return !this.hasOwnProperty('enabled') || this.enabled;
+ }-*/;
+
+ protected CommentLinkInfo() {
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
new file mode 100644
index 0000000000..16406f47bb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.projects;
+
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Cache of {@link ConfigInfo} objects by project name. */
+public class ConfigInfoCache {
+ private static final int LIMIT = 25;
+ private static final ConfigInfoCache instance =
+ GWT.create(ConfigInfoCache.class);
+
+ public static class Entry {
+ private final ConfigInfo info;
+ private CommentLinkProcessor commentLinkProcessor;
+
+ private Entry(ConfigInfo info) {
+ this.info = info;
+ }
+
+ public CommentLinkProcessor getCommentLinkProcessor() {
+ if (commentLinkProcessor == null) {
+ commentLinkProcessor = new CommentLinkProcessor(info.commentlinks());
+ }
+ return commentLinkProcessor;
+ }
+
+ public ThemeInfo getTheme() {
+ return info.theme();
+ }
+ }
+
+ public static void get(Project.NameKey name, AsyncCallback<Entry> cb) {
+ instance.getImpl(name, cb);
+ }
+
+ private final LinkedHashMap<String, Entry> cache;
+
+ protected ConfigInfoCache() {
+ cache = new LinkedHashMap<String, Entry>(LIMIT) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected boolean removeEldestEntry(
+ Map.Entry<String, ConfigInfoCache.Entry> e) {
+ return size() > LIMIT;
+ }
+ };
+ }
+
+ private void getImpl(final Project.NameKey name,
+ final AsyncCallback<Entry> cb) {
+ Entry e = cache.get(name.get());
+ if (e != null) {
+ cb.onSuccess(e);
+ return;
+ }
+ ProjectApi.config(name).get(new AsyncCallback<ConfigInfo>() {
+ @Override
+ public void onSuccess(ConfigInfo result) {
+ Entry e = new Entry(result);
+ cache.put(name.get(), e);
+ cb.onSuccess(e);
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ cb.onFailure(caught);
+ }
+ });
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index a6676dc5e2..be133c5748 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -15,6 +15,7 @@ package com.google.gerrit.client.projects;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -32,6 +33,10 @@ public class ProjectApi {
.put(input, asyncCallback);
}
+ static RestApi config(Project.NameKey name) {
+ return new RestApi("/projects/").id(name.get()).view("config");
+ }
+
private static class ProjectInput extends JavaScriptObject {
static ProjectInput create() {
return (ProjectInput) createObject();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
new file mode 100644
index 0000000000..67b6077276
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.client.projects;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ThemeInfo extends JavaScriptObject {
+ public final native String css() /*-{ return this.css; }-*/;
+ public final native String header() /*-{ return this.header; }-*/;
+ public final native String footer() /*-{ return this.footer; }-*/;
+
+ protected ThemeInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
index fd52777112..ef5217619f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.ui;
+import com.google.gerrit.client.AvatarImage;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.AccountInfo;
@@ -22,33 +23,46 @@ import com.google.gerrit.common.data.AccountInfoCache;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gwt.user.client.ui.FlowPanel;
/** Link to any user's account dashboard. */
-public class AccountLink extends InlineHyperlink {
+public class AccountLinkPanel extends FlowPanel {
/** Create a link after locating account details from an active cache. */
- public static AccountLink link(AccountInfoCache cache, Account.Id id) {
+ public static AccountLinkPanel link(AccountInfoCache cache, Account.Id id) {
com.google.gerrit.common.data.AccountInfo ai = cache.get(id);
- return ai != null ? new AccountLink(ai) : null;
+ return ai != null ? new AccountLinkPanel(ai) : null;
}
- public AccountLink(com.google.gerrit.common.data.AccountInfo ai) {
+ public AccountLinkPanel(com.google.gerrit.common.data.AccountInfo ai) {
this(FormatUtil.asInfo(ai));
}
- public AccountLink(UserIdentity ident) {
+ public AccountLinkPanel(UserIdentity ident) {
this(AccountInfo.create(
ident.getAccount().get(),
ident.getName(),
ident.getEmail()));
}
- public AccountLink(AccountInfo info) {
+ public AccountLinkPanel(AccountInfo info) {
this(info, Change.Status.NEW);
}
- public AccountLink(AccountInfo info, Change.Status status) {
- super(FormatUtil.name(info), PageLinks.toAccountQuery(owner(info), status));
- setTitle(FormatUtil.nameEmail(info));
+ public AccountLinkPanel(AccountInfo info, Change.Status status) {
+ addStyleName(Gerrit.RESOURCES.css().accountLinkPanel());
+
+ InlineHyperlink l =
+ new InlineHyperlink(FormatUtil.name(info), PageLinks.toAccountQuery(
+ owner(info), status)) {
+ @Override
+ public void go() {
+ Gerrit.display(getTargetHistoryToken());
+ }
+ };
+ l.setTitle(FormatUtil.nameEmail(info));
+
+ add(new AvatarImage(info, 16));
+ add(l);
}
private static String owner(AccountInfo ai) {
@@ -62,9 +76,4 @@ public class AccountLink extends InlineHyperlink {
return "";
}
}
-
- @Override
- public void go() {
- Gerrit.display(getTargetHistoryToken());
- }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
index a3c7a3c6fd..10cd1f0fa5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
@@ -15,9 +15,9 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.Gerrit;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtexpui.safehtml.client.RegexFindReplace;
+import com.google.gwtexpui.safehtml.client.FindReplace;
import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
import java.util.ArrayList;
@@ -25,17 +25,23 @@ import java.util.Collections;
import java.util.List;
public class CommentLinkProcessor {
- public static SafeHtml apply(SafeHtml buf) {
- try {
- return buf.replaceAll(Gerrit.getConfig().getCommentLinks());
+ private List<FindReplace> commentLinks;
+
+ public CommentLinkProcessor(List<FindReplace> commentLinks) {
+ this.commentLinks = commentLinks;
+ }
+ public SafeHtml apply(SafeHtml buf) {
+ try {
+ return buf.replaceAll(commentLinks);
} catch (RuntimeException err) {
// One or more of the patterns isn't valid on this browser.
// Try to filter the list down and remove the invalid ones.
- List<RegexFindReplace> safe = new ArrayList<RegexFindReplace>();
+ List<FindReplace> safe = new ArrayList<FindReplace>(commentLinks.size());
+
List<PatternError> bad = new ArrayList<PatternError>();
- for (RegexFindReplace r : Gerrit.getConfig().getCommentLinks()) {
+ for (FindReplace r : commentLinks) {
try {
buf.replaceAll(Collections.singletonList(r));
safe.add(r);
@@ -50,7 +56,7 @@ public class CommentLinkProcessor {
for (PatternError e : bad) {
msg.append("\n");
msg.append("\"");
- msg.append(e.pattern.find());
+ msg.append(e.pattern.pattern().getSource());
msg.append("\": ");
msg.append(e.errorMessage);
}
@@ -67,28 +73,25 @@ public class CommentLinkProcessor {
}
try {
- Gerrit.getConfig().setCommentLinks(safe);
+ commentLinks = safe;
return buf.replaceAll(safe);
} catch (RuntimeException err2) {
// To heck with it. The patterns passed individually above but
- // failed as a group? Just drop them all and render without.
+ // failed as a group? Just render without.
//
- Gerrit.getConfig().setCommentLinks(null);
+ commentLinks = null;
return buf;
}
}
}
private static class PatternError {
- RegexFindReplace pattern;
+ FindReplace pattern;
String errorMessage;
- PatternError(RegexFindReplace r, String w) {
+ PatternError(FindReplace r, String w) {
pattern = r;
errorMessage = w;
}
}
-
- private CommentLinkProcessor() {
- }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index 8f8c7eac18..05c6b5a09d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.ui;
+import com.google.gerrit.client.AvatarImage;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.AccountInfo;
@@ -53,22 +54,25 @@ public class CommentPanel extends Composite implements HasDoubleClickHandlers,
private final InlineLabel messageSummary;
private final FlowPanel content;
private final DoubleClickHTML messageText;
+ private CommentLinkProcessor commentLinkProcessor;
private FlowPanel buttons;
private boolean recent;
- public CommentPanel(final AccountInfo author, final Date when, String message) {
- this();
+ public CommentPanel(final AccountInfo author, final Date when, String message,
+ CommentLinkProcessor commentLinkProcessor) {
+ this(commentLinkProcessor);
setMessageText(message);
- setAuthorNameText(FormatUtil.name(author));
+ setAuthorNameText(author, FormatUtil.name(author));
setDateText(FormatUtil.shortFormatDayTime(when));
final CellFormatter fmt = header.getCellFormatter();
- fmt.getElement(0, 0).setTitle(FormatUtil.nameEmail(author));
- fmt.getElement(0, 2).setTitle(FormatUtil.mediumFormat(when));
+ fmt.getElement(0, 1).setTitle(FormatUtil.nameEmail(author));
+ fmt.getElement(0, 3).setTitle(FormatUtil.mediumFormat(when));
}
- protected CommentPanel() {
+ protected CommentPanel(CommentLinkProcessor commentLinkProcessor) {
+ this.commentLinkProcessor = commentLinkProcessor;
final FlowPanel body = new FlowPanel();
initWidget(body);
setStyleName(Gerrit.RESOURCES.css().commentPanel());
@@ -84,14 +88,14 @@ public class CommentPanel extends Composite implements HasDoubleClickHandlers,
setOpen(!isOpen());
}
});
- header.setText(0, 0, "");
- header.setWidget(0, 1, messageSummary);
- header.setText(0, 2, "");
+ header.setText(0, 1, "");
+ header.setWidget(0, 2, messageSummary);
+ header.setText(0, 3, "");
final CellFormatter fmt = header.getCellFormatter();
- fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().commentPanelAuthorCell());
- fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().commentPanelSummaryCell());
- fmt.setStyleName(0, 2, Gerrit.RESOURCES.css().commentPanelDateCell());
- fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
+ fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().commentPanelAuthorCell());
+ fmt.setStyleName(0, 2, Gerrit.RESOURCES.css().commentPanelSummaryCell());
+ fmt.setStyleName(0, 3, Gerrit.RESOURCES.css().commentPanelDateCell());
+ fmt.setHorizontalAlignment(0, 3, HasHorizontalAlignment.ALIGN_RIGHT);
body.add(header);
content = new FlowPanel();
@@ -118,16 +122,17 @@ public class CommentPanel extends Composite implements HasDoubleClickHandlers,
messageSummary.setText(summarize(message));
SafeHtml buf = new SafeHtmlBuilder().append(message).wikify();
- buf = CommentLinkProcessor.apply(buf);
+ buf = commentLinkProcessor.apply(buf);
SafeHtml.set(messageText, buf);
}
- public void setAuthorNameText(final String nameText) {
- header.setText(0, 0, nameText);
+ public void setAuthorNameText(final AccountInfo author, final String nameText) {
+ header.setWidget(0, 0, new AvatarImage(author, 26));
+ header.setText(0, 1, nameText);
}
protected void setDateText(final String dateText) {
- header.setText(0, 2, dateText);
+ header.setText(0, 3, dateText);
}
protected void setMessageTextVisible(final boolean show) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
index e7c2d84744..a26db05e5b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
@@ -15,6 +15,7 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.ThemeInfo;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
@@ -41,6 +42,9 @@ public abstract class Screen extends View {
private String windowTitle;
private Widget titleWidget;
+ private ThemeInfo theme;
+ private boolean setTheme;
+
protected Screen() {
initWidget(new FlowPanel());
setStyleName(Gerrit.RESOURCES.css().screen());
@@ -54,6 +58,14 @@ public abstract class Screen extends View {
}
}
+ @Override
+ protected void onUnload() {
+ super.onUnload();
+ if (setTheme) {
+ Gerrit.THEMER.clear();
+ }
+ }
+
public void registerKeys() {
}
@@ -124,6 +136,10 @@ public abstract class Screen extends View {
body.add(w);
}
+ protected void setTheme(final ThemeInfo t) {
+ theme = t;
+ }
+
/** Get the history token for this screen. */
public String getToken() {
return token;
@@ -167,5 +183,12 @@ public abstract class Screen extends View {
Gerrit.EVENT_BUS.fireEvent(new ScreenLoadEvent(this));
Gerrit.setQueryString(null);
registerKeys();
+
+ if (theme != null) {
+ Gerrit.THEMER.set(theme);
+ setTheme = true;
+ } else {
+ Gerrit.THEMER.clear();
+ }
}
}
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
new file mode 100644
index 0000000000..5be029cadd
--- /dev/null
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client;
+
+import static org.junit.Assert.assertEquals;
+import static com.google.gerrit.client.RelativeDateFormatter.YEAR_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.SECOND_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.MINUTE_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.HOUR_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.DAY_IN_MILLIS;
+
+import java.util.Date;
+
+import org.eclipse.jgit.util.RelativeDateFormatter;
+import org.junit.Test;
+
+public class RelativeDateFormatterTest {
+
+ private static void assertFormat(long ageFromNow, long timeUnit,
+ String expectedFormat) {
+ Date d = new Date(System.currentTimeMillis() - ageFromNow * timeUnit);
+ String s = RelativeDateFormatter.format(d);
+ assertEquals(expectedFormat, s);
+ }
+
+ @Test
+ public void testFuture() {
+ assertFormat(-100, YEAR_IN_MILLIS, "in the future");
+ assertFormat(-1, SECOND_IN_MILLIS, "in the future");
+ }
+
+ @Test
+ public void testFormatSeconds() {
+ assertFormat(1, SECOND_IN_MILLIS, "1 seconds ago");
+ assertFormat(89, SECOND_IN_MILLIS, "89 seconds ago");
+ }
+
+ @Test
+ public void testFormatMinutes() {
+ assertFormat(90, SECOND_IN_MILLIS, "2 minutes ago");
+ assertFormat(3, MINUTE_IN_MILLIS, "3 minutes ago");
+ assertFormat(60, MINUTE_IN_MILLIS, "60 minutes ago");
+ assertFormat(89, MINUTE_IN_MILLIS, "89 minutes ago");
+ }
+
+ @Test
+ public void testFormatHours() {
+ assertFormat(90, MINUTE_IN_MILLIS, "2 hours ago");
+ assertFormat(149, MINUTE_IN_MILLIS, "2 hours ago");
+ assertFormat(35, HOUR_IN_MILLIS, "35 hours ago");
+ }
+
+ @Test
+ public void testFormatDays() {
+ assertFormat(36, HOUR_IN_MILLIS, "2 days ago");
+ assertFormat(13, DAY_IN_MILLIS, "13 days ago");
+ }
+
+ @Test
+ public void testFormatWeeks() {
+ assertFormat(14, DAY_IN_MILLIS, "2 weeks ago");
+ assertFormat(69, DAY_IN_MILLIS, "10 weeks ago");
+ }
+
+ @Test
+ public void testFormatMonths() {
+ assertFormat(70, DAY_IN_MILLIS, "2 months ago");
+ assertFormat(75, DAY_IN_MILLIS, "3 months ago");
+ assertFormat(364, DAY_IN_MILLIS, "12 months ago");
+ }
+
+ @Test
+ public void testFormatYearsMonths() {
+ assertFormat(366, DAY_IN_MILLIS, "1 year ago");
+ assertFormat(380, DAY_IN_MILLIS, "1 year, 1 month ago");
+ assertFormat(410, DAY_IN_MILLIS, "1 year, 2 months ago");
+ assertFormat(2, YEAR_IN_MILLIS, "2 years ago");
+ assertFormat(1824, DAY_IN_MILLIS, "4 years, 12 months ago");
+ }
+
+ @Test
+ public void testFormatYears() {
+ assertFormat(5, YEAR_IN_MILLIS, "5 years ago");
+ assertFormat(60, YEAR_IN_MILLIS, "60 years ago");
+ }
+}
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index 6e26569261..d2189a2878 100644
--- a/gerrit-httpd/pom.xml
+++ b/gerrit-httpd/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-httpd</artifactId>
@@ -66,12 +66,6 @@ limitations under the License.
<dependency>
<groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-launcher</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
<artifactId>gerrit-server</artifactId>
<version>${project.version}</version>
</dependency>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index aa56ae9b55..7241624e41 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -26,7 +26,6 @@ import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.contact.ContactStore;
import com.google.gerrit.server.mail.EmailSender;
import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gwtexpui.safehtml.client.RegexFindReplace;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
@@ -34,12 +33,8 @@ import com.google.inject.ProvisionException;
import org.eclipse.jgit.lib.Config;
import java.net.MalformedURLException;
-import java.util.ArrayList;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
import javax.servlet.ServletContext;
@@ -148,31 +143,6 @@ class GerritConfigProvider implements Provider<GerritConfig> {
config.setSshdAddress(sshInfo.getHostKeys().get(0).getHost());
}
- List<RegexFindReplace> links = new ArrayList<RegexFindReplace>();
- for (String name : cfg.getSubsections("commentlink")) {
- String match = cfg.getString("commentlink", name, "match");
-
- // Unfortunately this validation isn't entirely complete. Clients
- // can have exceptions trying to evaluate the pattern if they don't
- // support a token used, even if the server does support the token.
- //
- // At the minimum, we can trap problems related to unmatched groups.
- try {
- Pattern.compile(match);
- } catch (PatternSyntaxException e) {
- throw new ProvisionException("Invalid pattern \"" + match
- + "\" in commentlink." + name + ".match: " + e.getMessage());
- }
-
- String link = cfg.getString("commentlink", name, "link");
- String html = cfg.getString("commentlink", name, "html");
- if (html == null || html.isEmpty()) {
- html = "<a href=\"" + link + "\">$&</a>";
- }
- links.add(new RegexFindReplace(match, html));
- }
- config.setCommentLinks(links);
-
return config;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
index 7de4bc3757..22d756895f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
@@ -55,6 +55,7 @@ public class GitWebConfig {
type.setProject(cfg.getString("gitweb", null, "project"));
type.setRevision(cfg.getString("gitweb", null, "revision"));
type.setFileHistory(cfg.getString("gitweb", null, "filehistory"));
+ type.setLinkDrafts(cfg.getBoolean("gitweb", null, "linkdrafts", true));
String pathSeparator = cfg.getString("gitweb", null, "pathSeparator");
if (pathSeparator != null) {
if (pathSeparator.length() == 1) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index b93df4334c..bd25fafc31 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -81,7 +81,6 @@ class UrlModule extends ServletModule {
serve("/signout").with(HttpLogoutServlet.class);
serve("/ssh_info").with(SshInfoServlet.class);
serve("/static/*").with(StaticServlet.class);
- serve("/tools/*").with(ToolServlet.class);
serve("/Main.class").with(notFound());
serve("/com/google/gerrit/launcher/*").with(notFound());
@@ -100,6 +99,7 @@ class UrlModule extends ServletModule {
serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
filter("/a/*").through(RequireIdentifiedUserFilter.class);
+ serveRegex("^/(?:a/)?tools/(.*)$").with(ToolServlet.class);
serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class);
serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.class);
serveRegex("^/(?:a/)?groups/(.*)?$").with(GroupsRestApiServlet.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
index ba7a5b866f..3be0cd310b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
@@ -32,7 +32,6 @@ package com.google.gerrit.httpd.gitweb;
import com.google.gerrit.common.data.GerritConfig;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.httpd.GitWebConfig;
-import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -140,7 +139,10 @@ class GitWebServlet extends HttpServlet {
private void makeSiteConfig(final SitePaths site,
final GerritConfig gerritConfig) throws IOException {
- final File myconf = GerritLauncher.createTempFile("gitweb_config", ".perl");
+ if (!site.tmp_dir.exists()) {
+ site.tmp_dir.mkdirs();
+ }
+ File myconf = File.createTempFile("gitweb_config", ".perl", site.tmp_dir);
// To make our configuration file only readable or writable by us;
// this reduces the chances of someone tampering with the file.
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 6cdd9bd3f8..ea2168a8a4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -352,11 +352,9 @@ public class HostPageServlet extends HttpServlet {
String css = HtmlDomUtil.readFile(src.getParentFile(), src.getName());
if (css == null) {
- banner.getParentNode().removeChild(banner);
return info;
}
- banner.removeAttribute("id");
banner.appendChild(hostDoc.createCDATASection("\n" + css + "\n"));
return info;
}
@@ -375,7 +373,6 @@ public class HostPageServlet extends HttpServlet {
Document html = HtmlDomUtil.parseFile(src);
if (html == null) {
- banner.getParentNode().removeChild(banner);
return info;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index a2aa191369..1040da31ee 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -68,6 +68,7 @@ import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OptionUtil;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gson.ExclusionStrategy;
@@ -587,9 +588,7 @@ public class RestApiServlet extends HttpServlet {
Multimap<String, String> config) {
final Set<String> want = Sets.newHashSet();
for (String p : config.get("fields")) {
- Iterables.addAll(want, Splitter.on(',')
- .omitEmptyStrings().trimResults()
- .split(p));
+ Iterables.addAll(want, OptionUtil.splitOptionValue(p));
}
if (!want.isEmpty()) {
gb.addSerializationExclusionStrategy(new ExclusionStrategy() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index 559c270270..22546a7955 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -201,8 +201,9 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
private List<GroupReference> suggestAccountGroup(
@Nullable final ProjectControl projectControl, final String query, final int limit) {
- final int n = limit <= 0 ? 10 : Math.min(limit, 10);
- return Lists.newArrayList(Iterables.limit(groupBackend.suggest(query), n));
+ return Lists.newArrayList(Iterables.limit(
+ groupBackend.suggest(query, projectControl),
+ limit <= 0 ? 10 : Math.min(limit, 10)));
}
@Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index 120b9af847..557e017cac 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -177,13 +177,25 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
private void loadPatchSets() throws OrmException {
ResultSet<PatchSet> source = db.patchSets().byChange(changeId);
List<PatchSet> patches = new ArrayList<PatchSet>();
+ Set<PatchSet.Id> patchesWithDraftComments = new HashSet<PatchSet.Id>();
+ final CurrentUser user = control.getCurrentUser();
+ final Account.Id me =
+ user instanceof IdentifiedUser ? ((IdentifiedUser) user).getAccountId()
+ : null;
for (PatchSet ps : source) {
+ final PatchSet.Id psId = ps.getId();
if (control.isPatchVisible(ps, db)) {
patches.add(ps);
+ if (me != null
+ && db.patchComments().draftByPatchSetAuthor(psId, me)
+ .iterator().hasNext()) {
+ patchesWithDraftComments.add(psId);
+ }
}
- patchsetsById.put(ps.getId(), ps);
+ patchsetsById.put(psId, ps);
}
detail.setPatchSets(patches);
+ detail.setPatchSetsWithDraftComments(patchesWithDraftComments);
}
private void loadMessages() throws OrmException {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
index 5b064c8576..ba50417846 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
@@ -22,15 +22,14 @@ import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mail.CommitMessageEditedSender;
import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.mail.CommitMessageEditedSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
@@ -76,7 +75,6 @@ class EditCommitMessageHandler extends Handler<ChangeDetail> {
private final PatchSetInfoFactory patchSetInfoFactory;
private final PersonIdent myIdent;
- private final ApprovalsUtil approvalsUtil;
private final TrackingFooters trackingFooters;
@Inject
@@ -91,7 +89,7 @@ class EditCommitMessageHandler extends Handler<ChangeDetail> {
final PatchSetInfoFactory patchSetInfoFactory,
final GitReferenceUpdated gitRefUpdated,
@GerritPersonIdent final PersonIdent myIdent,
- final ApprovalsUtil approvalsUtil, TrackingFooters trackingFooters) {
+ TrackingFooters trackingFooters) {
this.changeControlFactory = changeControlFactory;
this.db = db;
this.currentUser = currentUser;
@@ -107,7 +105,6 @@ class EditCommitMessageHandler extends Handler<ChangeDetail> {
this.patchSetInfoFactory = patchSetInfoFactory;
this.gitRefUpdated = gitRefUpdated;
this.myIdent = myIdent;
- this.approvalsUtil = approvalsUtil;
this.trackingFooters = trackingFooters;
}
@@ -136,7 +133,7 @@ class EditCommitMessageHandler extends Handler<ChangeDetail> {
ChangeUtil.editCommitMessage(patchSetId, control.getRefControl(), commitValidators, currentUser, message, db,
commitMessageEditedSenderFactory, hooks, git, patchSetInfoFactory, gitRefUpdated, myIdent,
- approvalsUtil, trackingFooters);
+ trackingFooters);
return changeDetailFactory.create(changeId).call();
} finally {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index 95a8e264b8..8e81dd377d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
@@ -106,7 +106,7 @@ class PatchSetDetailFactory extends Handler<PatchSetDetail> {
throw new NoSuchEntityException();
}
}
-
+ projectKey = control.getProject().getNameKey();
final PatchList list;
try {
@@ -114,8 +114,6 @@ class PatchSetDetailFactory extends Handler<PatchSetDetail> {
oldId = toObjectId(psIdBase);
newId = toObjectId(psIdNew);
- projectKey = control.getProject().getNameKey();
-
list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
} else { // OK, means use base to compare
list = patchListCache.get(control.getChange(), patchSet);
@@ -139,6 +137,7 @@ class PatchSetDetailFactory extends Handler<PatchSetDetail> {
detail = new PatchSetDetail();
detail.setPatchSet(patchSet);
+ detail.setProject(projectKey);
detail.setInfo(infoFactory.get(db, psIdNew));
detail.setPatches(patches);
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
index 907414f9b7..ce100a582f 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
@@ -31,7 +31,7 @@
})();
</script>
<script id="gerrit_hostpagedata"></script>
- <style id="gerrit_sitecss" type="text/css"></style>
+ <style id="gerrit_sitecss" type="text/css"></style>
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 5b0c110f9c..396c09cbaa 100644
--- a/gerrit-launcher/pom.xml
+++ b/gerrit-launcher/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-launcher</artifactId>
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index cf67261d0f..25c9401a9f 100644
--- a/gerrit-main/pom.xml
+++ b/gerrit-main/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-main</artifactId>
diff --git a/gerrit-openid/pom.xml b/gerrit-openid/pom.xml
index 7bd8fbf9c3..a81cf17f82 100644
--- a/gerrit-openid/pom.xml
+++ b/gerrit-openid/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-openid</artifactId>
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index b390af40aa..77187213ad 100644
--- a/gerrit-patch-commonsnet/pom.xml
+++ b/gerrit-patch-commonsnet/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-patch-commonsnet</artifactId>
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index 2274493b0d..222d6229ee 100644
--- a/gerrit-patch-jgit/pom.xml
+++ b/gerrit-patch-jgit/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-patch-jgit</artifactId>
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index ca1cf10d61..c7121c7a68 100644
--- a/gerrit-pgm/pom.xml
+++ b/gerrit-pgm/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-pgm</artifactId>
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
index 0336ddaf8d..aa413d649c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
@@ -33,7 +33,6 @@ import com.google.inject.name.Names;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Set;
-import java.util.TreeSet;
/** Initialize the {@code database} configuration section. */
@Singleton
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
index f750748aa5..e28af7c057 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
@@ -57,7 +57,6 @@ public final class IoUtil {
if (!(cl instanceof URLClassLoader)) {
throw noAddURL("Not loaded by URLClassLoader", null);
}
- @SuppressWarnings("resource")
URLClassLoader urlClassLoader = (URLClassLoader) cl;
Method addURL;
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 3190379316..569ed50ab9 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-plugin-api</artifactId>
@@ -72,8 +72,8 @@ limitations under the License.
<createSourcesJar>true</createSourcesJar>
<artifactSet>
<excludes>
- <exclude>gwtexpui:gwtexpui</exclude>
<exclude>gwtjsonrpc:gwtjsonrpc</exclude>
+ <exclude>com.google.gerrit:gerrit-gwtexpui</exclude>
<exclude>com.google.gerrit:gerrit-prettify</exclude>
<exclude>com.google.gerrit:gerrit-patch-commonsnet</exclude>
<exclude>com.google.gerrit:gerrit-patch-jgit</exclude>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index 34ddd5057a..9b954f9fc9 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -21,7 +21,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-plugin-archetype</artifactId>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index d8e91299dd..66f5a8f2ef 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -21,7 +21,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-plugin-gwt-archetype</artifactId>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
index a88059d3b0..73bf5c6e9c 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
@@ -6,7 +6,7 @@
*/
body, table td, select {
- font-family: Arial Unicode MS, Arial, sans-serif;
+ font-family: sans-serif;
font-size: small;
}
pre {
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 5b9583195b..5195204d4e 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-plugin-gwtui</artifactId>
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index a77626de44..5b12f7d0df 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -21,7 +21,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-plugin-js-archetype</artifactId>
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index b150ac00fb..fa4ae55590 100644
--- a/gerrit-prettify/pom.xml
+++ b/gerrit-prettify/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-prettify</artifactId>
@@ -34,8 +34,9 @@ limitations under the License.
<dependencies>
<dependency>
- <groupId>gwtexpui</groupId>
- <artifactId>gwtexpui</artifactId>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-gwtexpui</artifactId>
+ <version>${project.version}</version>
</dependency>
<dependency>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
index 8d5ddb9000..8e7c699159 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
@@ -14,8 +14,6 @@
package com.google.gerrit.prettify.client;
-import com.google.gerrit.prettify.common.PrettyFactory;
-import com.google.gerrit.prettify.common.PrettyFormatter;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.ui.RootPanel;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java
index df603058bb..c191fa5692 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
import com.google.gwt.core.client.GWT;
import com.google.gwt.i18n.client.Constants;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.properties
index 97ab0cfc4b..97ab0cfc4b 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettifyConstants.properties
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFactory.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
index 364789f34f..f68b629b9d 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFactory.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFactory.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
/** Creates a new PrettyFormatter instance for one formatting run. */
public interface PrettyFactory {
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
index 5786e9587a..a84af5ee02 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
@@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
+import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseHtmlFile.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
index ebe0855b03..0c2af36033 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseHtmlFile.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.prettify.common;
+package com.google.gerrit.prettify.client;
import com.google.gwtexpui.safehtml.client.SafeHtml;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
index 609f091c9f..aa08af075b 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -14,6 +14,7 @@
package com.google.gerrit.prettify.common;
+
import org.eclipse.jgit.diff.Edit;
import java.util.ArrayList;
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index 0482a4a2c7..d69c5c58b2 100644
--- a/gerrit-reviewdb/pom.xml
+++ b/gerrit-reviewdb/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-reviewdb</artifactId>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index 6f121ee03a..ad0f130859 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -65,6 +65,13 @@ public final class AccountGeneralPreferences {
}
}
+ public static enum CommentVisibilityStrategy {
+ COLLAPSE_ALL,
+ EXPAND_MOST_RECENT,
+ EXPAND_RECENT,
+ EXPAND_ALL;
+ }
+
public static enum TimeFormat {
/** 12-hour clock: 1:15 am, 2:13 pm */
HHMM_12("h:mm a"),
@@ -123,6 +130,12 @@ public final class AccountGeneralPreferences {
@Column(id = 11)
protected boolean showUsernameInReviewCategory;
+ @Column(id = 12)
+ protected boolean relativeDateInChangeTable;
+
+ @Column(id = 13, length = 20, notNull = false)
+ protected String commentVisibilityStrategy;
+
public AccountGeneralPreferences() {
}
@@ -226,6 +239,26 @@ public final class AccountGeneralPreferences {
timeFormat = fmt.name();
}
+ public boolean isRelativeDateInChangeTable() {
+ return relativeDateInChangeTable;
+ }
+
+ public void setRelativeDateInChangeTable(final boolean relativeDateInChangeTable) {
+ this.relativeDateInChangeTable = relativeDateInChangeTable;
+ }
+
+ public CommentVisibilityStrategy getCommentVisibilityStrategy() {
+ if (commentVisibilityStrategy == null) {
+ return CommentVisibilityStrategy.EXPAND_MOST_RECENT;
+ }
+ return CommentVisibilityStrategy.valueOf(commentVisibilityStrategy);
+ }
+
+ public void setCommentVisibilityStrategy(
+ CommentVisibilityStrategy strategy) {
+ commentVisibilityStrategy = strategy.name();
+ }
+
public void resetToDefaults() {
maximumPageSize = DEFAULT_PAGESIZE;
showSiteHeader = true;
@@ -237,5 +270,6 @@ public final class AccountGeneralPreferences {
downloadCommand = null;
dateFormat = null;
timeFormat = null;
+ relativeDateInChangeTable = false;
}
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index c070e3edfd..d2434964a9 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -113,6 +113,8 @@ public final class Project {
protected String localDefaultDashboardId;
+ protected String themeName;
+
protected Project() {
}
@@ -206,6 +208,14 @@ public final class Project {
this.localDefaultDashboardId = localDefaultDashboardId;
}
+ public String getThemeName() {
+ return themeName;
+ }
+
+ public void setThemeName(final String themeName) {
+ this.themeName = themeName;
+ }
+
public void copySettingsFrom(final Project update) {
description = update.description;
useContributorAgreements = update.useContributorAgreements;
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index a8930d5b5a..68b1625c89 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-server</artifactId>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 4116633f5c..2d546014e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -32,7 +32,7 @@ import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.events.ApprovalAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
import com.google.gerrit.server.events.ChangeAbandonedEvent;
import com.google.gerrit.server.events.ChangeEvent;
import com.google.gerrit.server.events.ChangeMergedEvent;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java
deleted file mode 100644
index 6e635cb2c2..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License
-
-package com.google.gerrit.common;
-
-import java.util.Collection;
-
-/** Utilities for manipulating Collections . */
-public class CollectionsUtil {
- /**
- * Checks if any of the elements in the first collection can be found in the
- * second collection.
- *
- * @param findAnyOfThese which elements to look for.
- * @param inThisCollection where to look for them.
- * @param <E> type of the elements in question.
- * @return {@code true} if any of the elements in {@code findAnyOfThese} can
- * be found in {@code inThisCollection}, {@code false} otherwise.
- */
- public static <E> boolean isAnyIncludedIn(Collection<E> findAnyOfThese,
- Collection<E> inThisCollection) {
- for (E findThisItem : findAnyOfThese) {
- if (inThisCollection.contains(findThisItem)) {
- return true;
- }
- }
- return false;
- }
-
- private CollectionsUtil() {
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 0ce6892b03..3fd24f1988 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -20,7 +20,6 @@ import com.google.common.collect.Sets;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.Id;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -30,7 +29,6 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -66,39 +64,43 @@ public class ApprovalsUtil {
}
/**
- * Moves the PatchSetApprovals to the specified PatchSet on the change from
- * the prior PatchSet, while keeping the vetos.
+ * Copy min/max scores from one patch set to another.
*
- * @param db database connection to use for updates.
- * @param dest PatchSet to copy to
* @throws OrmException
- * @return List<PatchSetApproval> The previous approvals
*/
- public List<PatchSetApproval> copyVetosToPatchSet(ReviewDb db,
- LabelTypes labelTypes, PatchSet.Id dest) throws OrmException {
- PatchSet.Id source;
- if (dest.get() > 1) {
- source = new PatchSet.Id(dest.getParentKey(), dest.get() - 1);
- } else {
- throw new OrmException("Previous patch set could not be found");
- }
+ public static void copyLabels(ReviewDb db, LabelTypes labelTypes,
+ PatchSet.Id source, PatchSet.Id dest) throws OrmException {
+ Iterable<PatchSetApproval> sourceApprovals =
+ db.patchSetApprovals().byPatchSet(source);
+ copyLabels(db, labelTypes, sourceApprovals, source, dest);
+ }
- List<PatchSetApproval> patchSetApprovals =
- db.patchSetApprovals().byChange(dest.getParentKey()).toList();
- for (PatchSetApproval a : patchSetApprovals) {
- LabelType type = labelTypes.byLabel(a.getLabelId());
- if (type != null && a.getPatchSetId().equals(source) &&
- type.isCopyMinScore() &&
- type.isMaxNegative(a)) {
- db.patchSetApprovals().insert(
- Collections.singleton(new PatchSetApproval(dest, a)));
+ /**
+ * Copy a set's min/max scores from one patch set to another.
+ *
+ * @throws OrmException
+ */
+ public static void copyLabels(ReviewDb db, LabelTypes labelTypes,
+ Iterable<PatchSetApproval> sourceApprovals, PatchSet.Id source,
+ PatchSet.Id dest) throws OrmException {
+ List<PatchSetApproval> copied = Lists.newArrayList();
+ for (PatchSetApproval a : sourceApprovals) {
+ if (source.equals(a.getPatchSetId())) {
+ LabelType type = labelTypes.byLabel(a.getLabelId());
+ if (type == null) {
+ continue;
+ } else if (type.isCopyMinScore() && type.isMaxNegative(a)) {
+ copied.add(new PatchSetApproval(dest, a));
+ } else if (type.isCopyMaxScore() && type.isMaxPositive(a)) {
+ copied.add(new PatchSetApproval(dest, a));
+ }
}
}
- return patchSetApprovals;
+ db.patchSetApprovals().insert(copied);
}
public void addReviewers(ReviewDb db, LabelTypes labelTypes, Change change,
- PatchSet ps, PatchSetInfo info, Set<Id> wantReviewers,
+ PatchSet ps, PatchSetInfo info, Set<Account.Id> wantReviewers,
Set<Account.Id> existingReviewers) throws OrmException {
List<LabelType> allTypes = labelTypes.getLabelTypes();
if (allTypes.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 9569a97737..8f7301401a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -15,7 +15,9 @@
package com.google.gerrit.server;
import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -24,6 +26,7 @@ import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.client.TrackingId;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.config.TrackingFooter;
import com.google.gerrit.server.config.TrackingFooters;
@@ -193,9 +196,9 @@ public class ChangeUtil {
IdentifiedUser user, CommitValidators commitValidators, String message,
ReviewDb db, RevertedSender.Factory revertedSenderFactory,
ChangeHooks hooks, Repository git,
- PatchSetInfoFactory patchSetInfoFactory,
- GitReferenceUpdated gitRefUpdated, PersonIdent myIdent,
- String canonicalWebUrl) throws NoSuchChangeException, EmailException,
+ PatchSetInfoFactory patchSetInfoFactory, PersonIdent myIdent,
+ ChangeInserter changeInserter)
+ throws NoSuchChangeException, EmailException,
OrmException, MissingObjectException, IncorrectObjectTypeException,
IOException, InvalidChangeOperationException {
final Change.Id changeId = patchSetId.getParentKey();
@@ -272,9 +275,11 @@ public class ChangeUtil {
throw new InvalidChangeOperationException(e.getMessage());
}
- change.setCurrentPatchSet(patchSetInfoFactory.get(revertCommit, ps.getId()));
+ PatchSetInfo info = patchSetInfoFactory.get(revertCommit, ps.getId());
+ change.setCurrentPatchSet(info);
ChangeUtil.updated(change);
+
final RefUpdate ru = git.updateRef(ps.getRefName());
ru.setExpectedOldObjectId(ObjectId.zeroId());
ru.setNewObjectId(revertCommit);
@@ -284,17 +289,6 @@ public class ChangeUtil {
"Failed to create ref %s in %s: %s", ps.getRefName(),
change.getDest().getParentKey().get(), ru.getResult()));
}
- gitRefUpdated.fire(change.getProject(), ru);
-
- db.changes().beginTransaction(change.getId());
- try {
- insertAncestors(db, ps.getId(), revertCommit);
- db.patchSets().insert(Collections.singleton(ps));
- db.changes().insert(Collections.singleton(change));
- db.commit();
- } finally {
- db.rollback();
- }
final ChangeMessage cmsg =
new ChangeMessage(new ChangeMessage.Key(changeId,
@@ -303,17 +297,17 @@ public class ChangeUtil {
new StringBuilder("Patch Set " + patchSetId.get() + ": Reverted");
msgBuf.append("\n\n");
msgBuf.append("This patchset was reverted in change: " + change.getKey().get());
-
cmsg.setMessage(msgBuf.toString());
- db.changeMessages().insert(Collections.singleton(cmsg));
+
+ LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
+ changeInserter.insertChange(db, change, cmsg, ps, revertCommit,
+ labelTypes, info, Collections.<Account.Id> emptySet());
final RevertedSender cm = revertedSenderFactory.create(change);
cm.setFrom(user.getAccountId());
cm.setChangeMessage(cmsg);
cm.send();
- hooks.doPatchsetCreatedHook(change, ps, db);
-
return change.getId();
} finally {
revWalk.release();
@@ -327,7 +321,7 @@ public class ChangeUtil {
final ChangeHooks hooks, Repository git,
final PatchSetInfoFactory patchSetInfoFactory,
final GitReferenceUpdated gitRefUpdated, PersonIdent myIdent,
- final ApprovalsUtil approvalsUtil, final TrackingFooters trackingFooters)
+ final TrackingFooters trackingFooters)
throws NoSuchChangeException, EmailException, OrmException,
MissingObjectException, IncorrectObjectTypeException, IOException,
InvalidChangeOperationException, PatchSetInfoNotAvailableException {
@@ -444,8 +438,9 @@ public class ChangeUtil {
"Change %s was modified", change.getId()));
}
- approvalsUtil.copyVetosToPatchSet(db,
+ ApprovalsUtil.copyLabels(db,
refControl.getProjectControl().getLabelTypes(),
+ originalPS.getId(),
change.currentPatchSetId());
final List<FooterLine> footerLines = newCommit.getFooterLines();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
index 792c1e71ac..b271d6a07e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
@@ -60,6 +60,38 @@ public class MimeUtilFileTypeRegistry implements FileTypeRegistry {
mimeUtil.registerMimeDetector(name);
}
+
+ /**
+ * Get specificity of mime types with generic types forced to low values
+ *
+ * "application/octet-stream" is forced to -1.
+ * "text/plain" is forced to 0.
+ * All other mime types return the specificity reported by mimeType itself.
+ *
+ * @param mimeType The mimeType to get the corrected specificity for.
+ * @return The corrected specificity.
+ */
+ private int getCorrectedMimeSpecificity(MimeType mimeType) {
+ // Although the documentation of MimeType's getSpecificity claims that for
+ // example "application/octet-stream" always has a specificity of 0, it
+ // effectively returns 1 for us. This causes problems when trying to get
+ // the correct mime type via sorting. For example in
+ // [application/octet-stream, image/x-icon] both mime types come with
+ // specificity 1 for us. Hence, getMimeType below may end up using
+ // application/octet-stream instead of the more specific image/x-icon.
+ // Therefore, we have to force the specificity of generic types below the
+ // default of 1.
+ //
+ final String mimeTypeStr = mimeType.toString();
+ if (mimeTypeStr.equals("application/octet-stream")) {
+ return -1;
+ }
+ if (mimeTypeStr.equals("text/plain")) {
+ return 0;
+ }
+ return mimeType.getSpecificity();
+ }
+
@SuppressWarnings("unchecked")
public MimeType getMimeType(final String path, final byte[] content) {
Set<MimeType> mimeTypes = new HashSet<MimeType>();
@@ -84,7 +116,7 @@ public class MimeUtilFileTypeRegistry implements FileTypeRegistry {
Collections.sort(types, new Comparator<MimeType>() {
@Override
public int compare(MimeType a, MimeType b) {
- return b.getSpecificity() - a.getSpecificity();
+ return getCorrectedMimeSpecificity(b) - getCorrectedMimeSpecificity(a);
}
});
return types.get(0);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java
new file mode 100644
index 0000000000..24d10f7181
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Function;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+
+/** Utilities for option parsing. */
+public class OptionUtil {
+ private static final Splitter COMMA_OR_SPACE =
+ Splitter.on(CharMatcher.anyOf(", ")).omitEmptyStrings().trimResults();
+
+ private static final Function<String, String> TO_LOWER_CASE =
+ new Function<String, String>() {
+ @Override
+ public String apply(String input) {
+ return input.toLowerCase();
+ }
+ };
+
+ public static Iterable<String> splitOptionValue(String value) {
+ return Iterables.transform(COMMA_OR_SPACE.split(value), TO_LOWER_CASE);
+ }
+
+ private OptionUtil() {
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index e469c34155..18274469d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -56,7 +56,7 @@ public class AccountManager {
private final IdentifiedUser.GenericFactory userFactory;
private final ChangeUserName.Factory changeUserNameFactory;
private final ProjectCache projectCache;
- private final AtomicBoolean firstAccount;
+ private final AtomicBoolean awaitsFirstAccountCheck;
@Inject
AccountManager(final SchemaFactory<ReviewDb> schema,
@@ -73,14 +73,7 @@ public class AccountManager {
this.userFactory = userFactory;
this.changeUserNameFactory = changeUserNameFactory;
this.projectCache = projectCache;
-
- firstAccount = new AtomicBoolean();
- final ReviewDb db = schema.open();
- try {
- firstAccount.set(db.accounts().anyAccounts().toList().isEmpty());
- } finally {
- db.close();
- }
+ this.awaitsFirstAccountCheck = new AtomicBoolean(true);
}
/**
@@ -274,10 +267,20 @@ public class AccountManager {
account.setFullName(who.getDisplayName());
account.setPreferredEmail(extId.getEmailAddress());
- db.accounts().insert(Collections.singleton(account));
- db.accountExternalIds().insert(Collections.singleton(extId));
+ final boolean isFirstAccount = awaitsFirstAccountCheck.getAndSet(false)
+ && db.accounts().anyAccounts().toList().isEmpty();
+
+ try {
+ db.accounts().insert(Collections.singleton(account));
+ db.accountExternalIds().insert(Collections.singleton(extId));
+ } finally {
+ // If adding the account failed, it may be that it actually was the
+ // first account. So we reset the 'check for first account'-guard, as
+ // otherwise the first account would not get administration permissions.
+ awaitsFirstAccountCheck.set(isFirstAccount);
+ }
- if (firstAccount.get() && firstAccount.compareAndSet(true, false)) {
+ if (isFirstAccount) {
// This is the first user account on our site. Assume this user
// is going to be the site's administrator and just make them that
// to bootstrap the authentication database.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 942b0d738f..d2014ecb89 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -142,6 +142,12 @@ public class CapabilityControl {
|| canAdministrateServer();
}
+ /** @return true if the user can stream Gerrit events. */
+ public boolean canStreamEvents() {
+ return canPerform(GlobalCapability.STREAM_EVENTS)
+ || canAdministrateServer();
+ }
+
/** @return true if the user can run the Git garbage collection. */
public boolean canRunGC() {
return canPerform(GlobalCapability.RUN_GC)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java
new file mode 100644
index 0000000000..ec538bc4fd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatarChangeUrl.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.avatar.AvatarProvider;
+import com.google.inject.Inject;
+
+public class GetAvatarChangeUrl implements RestReadView<AccountResource> {
+ private final DynamicItem<AvatarProvider> avatarProvider;
+
+ @Inject
+ GetAvatarChangeUrl(DynamicItem<AvatarProvider> avatarProvider) {
+ this.avatarProvider = avatarProvider;
+ }
+
+ @Override
+ public String apply(AccountResource rsrc)
+ throws ResourceNotFoundException {
+ AvatarProvider impl = avatarProvider.get();
+ if (impl == null) {
+ throw new ResourceNotFoundException();
+ }
+
+ String url = impl.getChangeAvatarUrl(rsrc.getUser());
+ if (Strings.isNullOrEmpty(url)) {
+ throw new ResourceNotFoundException();
+ } else {
+ return url;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 81221aa50d..54f1980718 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -24,12 +24,11 @@ import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
+import static com.google.gerrit.common.data.GlobalCapability.STREAM_EVENTS;
import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
-import com.google.common.base.Function;
-import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@@ -40,6 +39,7 @@ import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OptionUtil;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.account.AccountResource.Capability;
import com.google.gerrit.server.git.QueueProvider;
@@ -63,14 +63,7 @@ class GetCapabilities implements RestReadView<AccountResource> {
if (query == null) {
query = Sets.newHashSet();
}
- Iterables.addAll(query, Iterables.transform(
- Splitter.onPattern("[, ]").omitEmptyStrings().trimResults().split(name),
- new Function<String, String>() {
- @Override
- public String apply(String input) {
- return input.toLowerCase();
- }
- }));
+ Iterables.addAll(query, OptionUtil.splitOptionValue(name));
}
private Set<String> query;
@@ -112,6 +105,7 @@ class GetCapabilities implements RestReadView<AccountResource> {
have.put(VIEW_QUEUE, cc.canViewQueue());
have.put(RUN_GC, cc.canRunGC());
have.put(START_REPLICATION, cc.canStartReplication());
+ have.put(STREAM_EVENTS, cc.canStreamEvents());
have.put(ACCESS_DATABASE, cc.canAccessDatabase());
QueueProvider.QueueType queue = cc.getQueueType();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
new file mode 100644
index 0000000000..8e65a23ff0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GetDiffPreferences implements RestReadView<AccountResource> {
+
+ private final Provider<CurrentUser> self;
+
+ @Inject
+ GetDiffPreferences(Provider<CurrentUser> self) {
+ this.self = self;
+ }
+
+ @Override
+ public DiffPreferencesInfo apply(AccountResource rsrc) throws AuthException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("restricted to administrator");
+ }
+ return DiffPreferencesInfo.parse(rsrc.getUser().getAccountDiffPreference());
+ }
+
+ static class DiffPreferencesInfo {
+ static DiffPreferencesInfo parse(AccountDiffPreference p) {
+ DiffPreferencesInfo info = new DiffPreferencesInfo();
+ info.context = p.getContext();
+ info.expandAllComments = p.isExpandAllComments() ? true : null;
+ info.ignoreWhitespace = p.getIgnoreWhitespace();
+ info.intralineDifference = p.isIntralineDifference() ? true : null;
+ info.lineLength = p.getLineLength();
+ info.manualReview = p.isManualReview() ? true : null;
+ info.retainHeader = p.isRetainHeader() ? true : null;
+ info.showLineEndings = p.isShowLineEndings() ? true : null;
+ info.showTabs = p.isShowTabs() ? true : null;
+ info.showWhitespaceErrors = p.isShowWhitespaceErrors() ? true : null;
+ info.skipDeleted = p.isSkipDeleted() ? true : null;
+ info.skipUncommented = p.isSkipUncommented() ? true : null;
+ info.syntaxHighlighting = p.isSyntaxHighlighting() ? true : null;
+ info.tabSize = p.getTabSize();
+ return info;
+ }
+
+ short context;
+ Boolean expandAllComments;
+ Whitespace ignoreWhitespace;
+ Boolean intralineDifference;
+ int lineLength;
+ Boolean manualReview;
+ Boolean retainHeader;
+ Boolean showLineEndings;
+ Boolean showTabs;
+ Boolean showWhitespaceErrors;
+ Boolean skipDeleted;
+ Boolean skipUncommented;
+ Boolean syntaxHighlighting;
+ int tabSize;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
index b4e770fdbe..34db9672c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
@@ -19,6 +19,7 @@ import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
import java.util.Collection;
@@ -44,7 +45,9 @@ public interface GroupBackend {
GroupDescription.Basic get(AccountGroup.UUID uuid);
/** @return suggestions for the group name sorted by name. */
- Collection<GroupReference> suggest(String name);
+ Collection<GroupReference> suggest(
+ String name,
+ @Nullable ProjectControl project);
/** @return the group membership checker for the backend. */
GroupMembership membershipsOf(IdentifiedUser user);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
index cdbb0e49bc..f7e06344d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
@@ -16,6 +16,8 @@ package com.google.gerrit.server.account;
import com.google.common.collect.Iterables;
import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectControl;
import java.util.Collection;
import java.util.Comparator;
@@ -36,7 +38,7 @@ public class GroupBackends {
};
/**
- * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+ * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
* the best suggestion, or null if one does not exist.
*
* @param groupBackend the group backend
@@ -44,9 +46,23 @@ public class GroupBackends {
* @return the best single GroupReference suggestion
*/
@Nullable
- public static GroupReference findBestSuggestion(
- GroupBackend groupBackend, String name) {
- Collection<GroupReference> refs = groupBackend.suggest(name);
+ public static GroupReference findBestSuggestion(GroupBackend groupBackend,
+ String name) {
+ return findBestSuggestion(groupBackend, name, null);
+ }
+ /**
+ * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
+ * the best suggestion, or null if one does not exist.
+ *
+ * @param groupBackend the group backend
+ * @param name the name for which to suggest groups
+ * @param project the project for which to suggest groups
+ * @return the best single GroupReference suggestion
+ */
+ @Nullable
+ public static GroupReference findBestSuggestion(GroupBackend groupBackend,
+ String name, @Nullable ProjectControl project) {
+ Collection<GroupReference> refs = groupBackend.suggest(name, project);
if (refs.size() == 1) {
return Iterables.getOnlyElement(refs);
}
@@ -60,7 +76,7 @@ public class GroupBackends {
}
/**
- * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+ * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
* the exact suggestion, or null if one does not exist.
*
* @param groupBackend the group backend
@@ -70,7 +86,22 @@ public class GroupBackends {
@Nullable
public static GroupReference findExactSuggestion(
GroupBackend groupBackend, String name) {
- Collection<GroupReference> refs = groupBackend.suggest(name);
+ return findExactSuggestion(groupBackend, name, null);
+ }
+
+ /**
+ * Runs {@link GroupBackend#suggest(String, Project)} and filters the result to return
+ * the exact suggestion, or null if one does not exist.
+ *
+ * @param groupBackend the group backend
+ * @param name the name for which to suggest groups
+ * @param project the project for which to suggest groups
+ * @return the exact single GroupReference suggestion
+ */
+ @Nullable
+ public static GroupReference findExactSuggestion(
+ GroupBackend groupBackend, String name, ProjectControl project) {
+ Collection<GroupReference> refs = groupBackend.suggest(name, project);
for (GroupReference ref : refs) {
if (isExactSuggestion(ref, name)) {
return ref;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 2a8e7c92a3..2e01f26aec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -127,10 +127,15 @@ public class GroupControl {
/** Can this user see this group exists? */
public boolean isVisible() {
AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
+ /* Check for canAdministrateServer may seem redundant, but allows
+ * for visibility of all groups that are not an internal group to
+ * server administrators.
+ */
return (accountGroup != null && accountGroup.isVisibleToAll())
|| user instanceof InternalUser
|| user.getEffectiveGroups().contains(group.getGroupUUID())
- || isOwner();
+ || isOwner()
+ || user.getCapabilities().canAdministrateServer();
}
public boolean isOwner() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
index d06db4d336..a70f9429a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -23,6 +23,7 @@ import com.google.gerrit.common.data.GroupDescriptions;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -71,7 +72,8 @@ public class InternalGroupBackend implements GroupBackend {
}
@Override
- public Collection<GroupReference> suggest(final String name) {
+ public Collection<GroupReference> suggest(final String name,
+ final ProjectControl project) {
Iterable<AccountGroup> filtered = Iterables.filter(groupCache.all(),
new Predicate<AccountGroup>() {
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index ac045f75e9..57a4a22b97 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -31,8 +31,11 @@ public class Module extends RestApiModule {
get(ACCOUNT_KIND).to(GetAccount.class);
get(ACCOUNT_KIND, "avatar").to(GetAvatar.class);
+ get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class);
child(ACCOUNT_KIND, "capabilities").to(Capabilities.class);
get(ACCOUNT_KIND, "groups").to(GetGroups.class);
+ get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
+ put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
new file mode 100644
index 0000000000..db9bc2d71b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.GetDiffPreferences.DiffPreferencesInfo;
+import com.google.gerrit.server.account.SetDiffPreferences.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class SetDiffPreferences implements RestModifyView<AccountResource, Input> {
+ static class Input {
+ Short context;
+ Boolean expandAllComments;
+ Whitespace ignoreWhitespace;
+ Boolean intralineDifference;
+ Integer lineLength;
+ Boolean manualReview;
+ Boolean retainHeader;
+ Boolean showLineEndings;
+ Boolean showTabs;
+ Boolean showWhitespaceErrors;
+ Boolean skipDeleted;
+ Boolean skipUncommented;
+ Boolean syntaxHighlighting;
+ Integer tabSize;
+ }
+
+ private final Provider<CurrentUser> self;
+ private final ReviewDb db;
+
+ @Inject
+ SetDiffPreferences(Provider<CurrentUser> self, ReviewDb db) {
+ this.self = self;
+ this.db = db;
+ }
+
+ @Override
+ public DiffPreferencesInfo apply(AccountResource rsrc, Input input)
+ throws AuthException, OrmException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("restricted to administrator");
+ }
+ if (input == null) {
+ input = new Input();
+ }
+
+ Account.Id accountId = rsrc.getUser().getAccountId();
+ AccountDiffPreference p;
+
+ db.accounts().beginTransaction(accountId);
+ try {
+ p = db.accountDiffPreferences().get(accountId);
+ if (p == null) {
+ p = new AccountDiffPreference(accountId);
+ }
+
+ if (input.context != null) {
+ p.setContext(input.context);
+ }
+ if (input.ignoreWhitespace != null) {
+ p.setIgnoreWhitespace(input.ignoreWhitespace);
+ }
+ if (input.expandAllComments != null) {
+ p.setExpandAllComments(input.expandAllComments);
+ }
+ if (input.intralineDifference != null) {
+ p.setIntralineDifference(input.intralineDifference);
+ }
+ if (input.lineLength != null) {
+ p.setLineLength(input.lineLength);
+ }
+ if (input.manualReview != null) {
+ p.setManualReview(input.manualReview);
+ }
+ if (input.retainHeader != null) {
+ p.setRetainHeader(input.retainHeader);
+ }
+ if (input.showLineEndings != null) {
+ p.setShowLineEndings(input.showLineEndings);
+ }
+ if (input.showTabs != null) {
+ p.setShowTabs(input.showTabs);
+ }
+ if (input.showWhitespaceErrors != null) {
+ p.setShowWhitespaceErrors(input.showWhitespaceErrors);
+ }
+ if (input.skipDeleted != null) {
+ p.setSkipDeleted(input.skipDeleted);
+ }
+ if (input.skipUncommented != null) {
+ p.setSkipUncommented(input.skipUncommented);
+ }
+ if (input.syntaxHighlighting != null) {
+ p.setSyntaxHighlighting(input.syntaxHighlighting);
+ }
+ if (input.tabSize != null) {
+ p.setTabSize(input.tabSize);
+ }
+
+ db.accountDiffPreferences().upsert(Collections.singleton(p));
+ db.commit();
+ } finally {
+ db.rollback();
+ }
+ return DiffPreferencesInfo.parse(p);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index d9c9257296..046dfa5a59 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -26,6 +26,7 @@ import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -82,10 +83,10 @@ public class UniversalGroupBackend implements GroupBackend {
}
@Override
- public Collection<GroupReference> suggest(String name) {
+ public Collection<GroupReference> suggest(String name, ProjectControl project) {
Set<GroupReference> groups = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
for (GroupBackend g : backends) {
- groups.addAll(g.suggest(name));
+ groups.addAll(g.suggest(name, project));
}
return groups;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 2cf372b493..97a03098bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -33,6 +33,7 @@ import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.account.ListGroupMembership;
import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
@@ -164,7 +165,7 @@ public class LdapGroupBackend implements GroupBackend {
}
@Override
- public Collection<GroupReference> suggest(String name) {
+ public Collection<GroupReference> suggest(String name, ProjectControl project) {
AccountGroup.UUID uuid = new AccountGroup.UUID(name);
if (isLdapUUID(uuid)) {
GroupDescription.Basic g = get(uuid);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index fc1102e16f..ac47cb556c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -239,7 +239,7 @@ class LdapRealm implements Realm {
}
}
} catch (NamingException e) {
- log.error("Cannot query LDAP to autenticate user", e);
+ log.error("Cannot query LDAP to authenticate user", e);
throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
} catch (LoginException e) {
log.error("Cannot authenticate server via JAAS", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index e0c78ea7f4..5c965e22d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -18,6 +18,7 @@ import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -29,11 +30,9 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
import java.util.Collections;
-import java.util.List;
import java.util.Set;
public class ChangeInserter {
@@ -53,17 +52,27 @@ public class ChangeInserter {
}
public void insertChange(ReviewDb db, Change change, PatchSet ps,
- RevCommit commit, LabelTypes labelTypes, List<FooterLine> footerLines,
- PatchSetInfo info, Set<Account.Id> reviewers) throws OrmException {
+ RevCommit commit, LabelTypes labelTypes, PatchSetInfo info,
+ Set<Account.Id> reviewers) throws OrmException {
+ insertChange(db, change, null, ps, commit, labelTypes, info, reviewers);
+ }
+
+ public void insertChange(ReviewDb db, Change change,
+ ChangeMessage changeMessage, PatchSet ps, RevCommit commit,
+ LabelTypes labelTypes, PatchSetInfo info, Set<Account.Id> reviewers)
+ throws OrmException {
db.changes().beginTransaction(change.getId());
try {
ChangeUtil.insertAncestors(db, ps.getId(), commit);
db.patchSets().insert(Collections.singleton(ps));
db.changes().insert(Collections.singleton(change));
- ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+ ChangeUtil.updateTrackingIds(db, change, trackingFooters, commit.getFooterLines());
approvalsUtil.addReviewers(db, labelTypes, change, ps, info, reviewers,
Collections.<Account.Id> emptySet());
+ if (changeMessage != null) {
+ db.changeMessages().insert(Collections.singleton(changeMessage));
+ }
db.commit();
} finally {
db.rollback();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 3592bbf6f4..80cdca6702 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -23,6 +23,7 @@ import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_REVISIO
import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_ACCOUNTS;
import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.common.changes.ListChangesOption.LABELS;
+import static com.google.gerrit.common.changes.ListChangesOption.MESSAGES;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
@@ -259,6 +260,9 @@ public class ChangeJson {
}
out.removable_reviewers = removableReviewers(cd, out.labels.values());
}
+ if (options.contains(MESSAGES)) {
+ out.messages = messages(cd);
+ }
out.finish();
if (has(ALL_REVISIONS) || has(CURRENT_REVISION) || limited != null) {
@@ -618,6 +622,38 @@ public class ChangeJson {
return permitted.asMap();
}
+ private Collection<ChangeMessageInfo> messages(ChangeData cd)
+ throws OrmException {
+ List<ChangeMessage> messages =
+ db.get().changeMessages().byChange(cd.getId()).toList();
+ if (messages.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ // chronological order
+ Collections.sort(messages, new Comparator<ChangeMessage>() {
+ @Override
+ public int compare(ChangeMessage a, ChangeMessage b) {
+ return a.getWrittenOn().compareTo(b.getWrittenOn());
+ }
+ });
+
+ List<ChangeMessageInfo> result =
+ Lists.newArrayListWithCapacity(messages.size());
+ for (ChangeMessage message : messages) {
+ PatchSet.Id patchNum = message.getPatchSetId();
+
+ ChangeMessageInfo cmi = new ChangeMessageInfo();
+ cmi.id = message.getKey().get();
+ cmi.author = accountLoader.get(message.getAuthor());
+ cmi.date = message.getWrittenOn();
+ cmi.message = message.getMessage();
+ cmi._revisionNumber = patchNum != null ? patchNum.get() : null;
+ result.add(cmi);
+ }
+ return result;
+ }
+
private Collection<AccountInfo> removableReviewers(ChangeData cd,
Collection<LabelInfo> labels) throws OrmException {
ChangeControl ctl = control(cd);
@@ -846,6 +882,7 @@ public class ChangeJson {
Map<String, LabelInfo> labels;
Map<String, Collection<String>> permitted_labels;
Collection<AccountInfo> removable_reviewers;
+ Collection<ChangeMessageInfo> messages;
String current_revision;
Map<String, RevisionInfo> revisions;
@@ -931,4 +968,12 @@ public class ChangeJson {
super(id);
}
}
+
+ static class ChangeMessageInfo {
+ String id;
+ AccountInfo author;
+ Timestamp date;
+ String message;
+ Integer _revisionNumber;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
new file mode 100644
index 0000000000..79c442c1eb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+
+import org.eclipse.jgit.lib.Constants;
+
+public class ChangeTriplet {
+
+ private final Change.Key changeKey;
+ private final Project.NameKey projectNameKey;
+ private final Branch.NameKey branchNameKey;
+
+ public ChangeTriplet(final String triplet) throws ParseException {
+ int t2 = triplet.lastIndexOf('~');
+ int t1 = triplet.lastIndexOf('~', t2 - 1);
+ if (t1 < 0 || t2 < 0) {
+ throw new ParseException();
+ }
+
+ String project = Url.decode(triplet.substring(0, t1));
+ String branch = Url.decode(triplet.substring(t1 + 1, t2));
+ String changeId = Url.decode(triplet.substring(t2 + 1));
+
+ if (!branch.startsWith(Constants.R_REFS)) {
+ branch = Constants.R_HEADS + branch;
+ }
+
+ changeKey = new Change.Key(changeId);
+ projectNameKey = new Project.NameKey(project);
+ branchNameKey = new Branch.NameKey(projectNameKey, branch);
+ }
+
+ public Change.Key getChangeKey() {
+ return changeKey;
+ }
+
+ public Branch.NameKey getBranchNameKey() {
+ return branchNameKey;
+ }
+
+ public static String format(final Change change) {
+ return change.getProject().get() + "~"
+ + change.getDest().getShortName() + "~"
+ + change.getKey().get();
+ }
+
+ public static class ParseException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ ParseException() {
+ super();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index 0100ebcf18..45328b18fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -21,10 +21,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestCollection;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -33,8 +30,6 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import org.eclipse.jgit.lib.Constants;
-
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.List;
@@ -72,8 +67,7 @@ public class ChangesCollection implements
public ChangeResource parse(TopLevelResource root, IdString id)
throws ResourceNotFoundException, OrmException,
UnsupportedEncodingException {
- ParsedId p = new ParsedId(id.encoded());
- List<Change> changes = findChanges(p);
+ List<Change> changes = findChanges(id.encoded());
if (changes.size() != 1) {
throw new ResourceNotFoundException(id);
}
@@ -87,59 +81,36 @@ public class ChangesCollection implements
return new ChangeResource(control);
}
- private List<Change> findChanges(ParsedId k) throws OrmException {
- if (k.legacyId != null) {
- Change c = db.get().changes().get(k.legacyId);
+ private List<Change> findChanges(String id)
+ throws OrmException, ResourceNotFoundException {
+ // Try legacy id
+ if (id.matches("^[1-9][0-9]*$")) {
+ Change c = db.get().changes().get(Change.Id.parse(id));
if (c != null) {
return ImmutableList.of(c);
}
return Collections.emptyList();
- } else if (k.project == null && k.branch == null && k.changeId != null) {
- Change.Key id = new Change.Key(k.changeId);
- if (id.get().length() == 41) {
- return db.get().changes().byKey(id).toList();
- } else {
- return db.get().changes().byKeyRange(id, id.max()).toList();
- }
}
- return db.get().changes().byBranchKey(
- k.branch(),
- new Change.Key(k.changeId)).toList();
- }
-
- private static class ParsedId {
- Change.Id legacyId;
- String project;
- String branch;
- String changeId;
-
- ParsedId(String id) throws ResourceNotFoundException {
- if (id.matches("^[1-9][0-9]*$")) {
- legacyId = Change.Id.parse(id);
- return;
- }
-
- int t2 = id.lastIndexOf('~');
- int t1 = id.lastIndexOf('~', t2 - 1);
- if (t1 < 0 || t2 < 0) {
- if (!id.matches("^I[0-9a-z]{4,40}$")) {
- throw new ResourceNotFoundException(id);
- }
- changeId = id;
- return;
- }
-
- project = Url.decode(id.substring(0, t1));
- branch = Url.decode(id.substring(t1 + 1, t2));
- changeId = Url.decode(id.substring(t2 + 1));
- if (!branch.startsWith(Constants.R_REFS)) {
- branch = Constants.R_HEADS + branch;
+ // Try isolated changeId
+ if (!id.contains("~")) {
+ Change.Key key = new Change.Key(id);
+ if (key.get().length() == 41) {
+ return db.get().changes().byKey(key).toList();
+ } else {
+ return db.get().changes().byKeyRange(key, key.max()).toList();
}
}
- Branch.NameKey branch() {
- return new Branch.NameKey(new Project.NameKey(project), branch);
+ // Try change triplet
+ ChangeTriplet triplet;
+ try {
+ triplet = new ChangeTriplet(id);
+ } catch (ChangeTriplet.ParseException e) {
+ throw new ResourceNotFoundException(id);
}
+ return db.get().changes().byBranchKey(
+ triplet.getBranchNameKey(),
+ triplet.getChangeKey()).toList();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
new file mode 100644
index 0000000000..798b9c6c52
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.change;
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.server.account.AccountInfo;
+
+import java.sql.Timestamp;
+
+public class CommentInfo {
+ static enum Side {
+ PARENT, REVISION;
+ }
+
+ final String kind = "gerritcodereview#comment";
+ String id;
+ String path;
+ Side side;
+ Integer line;
+ String inReplyTo;
+ String message;
+ Timestamp updated;
+ AccountInfo author;
+
+ CommentInfo(PatchLineComment c, AccountInfo.Loader accountLoader) {
+ id = Url.encode(c.getKey().get());
+ path = c.getKey().getParentKey().getFileName();
+ if (c.getSide() == 0) {
+ side = Side.PARENT;
+ }
+ if (c.getLine() > 0) {
+ line = c.getLine();
+ }
+ inReplyTo = Url.encode(c.getParentUuid());
+ message = Strings.emptyToNull(c.getMessage());
+ updated = c.getWrittenOn();
+ if (accountLoader != null) {
+ author = accountLoader.get(c.getAuthor());
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
new file mode 100644
index 0000000000..ec47d019a3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.inject.TypeLiteral;
+
+public class CommentResource implements RestResource {
+ public static final TypeLiteral<RestView<CommentResource>> COMMENT_KIND =
+ new TypeLiteral<RestView<CommentResource>>() {};
+
+ private final RevisionResource rev;
+ private final PatchLineComment comment;
+
+ CommentResource(RevisionResource rev, PatchLineComment c) {
+ this.rev = rev;
+ this.comment = c;
+ }
+
+ public PatchSet getPatchSet() {
+ return rev.getPatchSet();
+ }
+
+ PatchLineComment getComment() {
+ return comment;
+ }
+
+ String getId() {
+ return comment.getKey().get();
+ }
+
+ Account.Id getAuthorId() {
+ return comment.getAuthor();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
new file mode 100644
index 0000000000..91cfbf8224
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class Comments implements ChildCollection<RevisionResource, CommentResource> {
+ private final DynamicMap<RestView<CommentResource>> views;
+ private final Provider<ListComments> list;
+ private final Provider<ReviewDb> dbProvider;
+
+ @Inject
+ Comments(DynamicMap<RestView<CommentResource>> views,
+ Provider<ListComments> list,
+ Provider<ReviewDb> dbProvider) {
+ this.views = views;
+ this.list = list;
+ this.dbProvider = dbProvider;
+ }
+
+ @Override
+ public DynamicMap<RestView<CommentResource>> views() {
+ return views;
+ }
+
+ @Override
+ public RestView<RevisionResource> list() {
+ return list.get();
+ }
+
+ @Override
+ public CommentResource parse(RevisionResource rev, IdString id)
+ throws ResourceNotFoundException, OrmException {
+ String uuid = id.get();
+ for (PatchLineComment c : dbProvider.get().patchComments()
+ .publishedByPatchSet(rev.getPatchSet().getId())) {
+ if (uuid.equals(c.getKey().get())) {
+ return new CommentResource(rev, c);
+ }
+ }
+ throw new ResourceNotFoundException(id);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
index 348be979fa..5157af4a83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
@@ -42,7 +42,7 @@ class CreateDraft implements RestModifyView<RevisionResource, Input> {
}
@Override
- public Response<GetDraft.Comment> apply(RevisionResource rsrc, Input in)
+ public Response<CommentInfo> apply(RevisionResource rsrc, Input in)
throws AuthException, BadRequestException, ResourceConflictException, OrmException {
if (Strings.isNullOrEmpty(in.path)) {
throw new BadRequestException("path must be non-empty");
@@ -60,9 +60,9 @@ class CreateDraft implements RestModifyView<RevisionResource, Input> {
rsrc.getAccountId(),
Url.decode(in.inReplyTo));
c.setStatus(Status.DRAFT);
- c.setSide(in.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
+ c.setSide(in.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
c.setMessage(in.message.trim());
db.get().patchComments().insert(Collections.singleton(c));
- return Response.created(new GetDraft.Comment(c));
+ return Response.created(new CommentInfo(c, null));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
new file mode 100644
index 0000000000..68b043599b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+class GetComment implements RestReadView<CommentResource> {
+
+ private final AccountInfo.Loader.Factory accountLoaderFactory;
+
+ @Inject
+ GetComment(AccountInfo.Loader.Factory accountLoaderFactory) {
+ this.accountLoaderFactory = accountLoaderFactory;
+ }
+
+ @Override
+ public Object apply(CommentResource rsrc) throws OrmException {
+ AccountInfo.Loader accountLoader = accountLoaderFactory.create(true);
+ CommentInfo ci = new CommentInfo(rsrc.getComment(), accountLoader);
+ accountLoader.fill();
+ return ci;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
index ae0d9e9281..b3cd813532 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
@@ -27,7 +27,8 @@ public class GetDetail implements RestReadView<ChangeResource> {
this.json = json
.addOption(ListChangesOption.LABELS)
.addOption(ListChangesOption.DETAILED_LABELS)
- .addOption(ListChangesOption.DETAILED_ACCOUNTS);
+ .addOption(ListChangesOption.DETAILED_ACCOUNTS)
+ .addOption(ListChangesOption.MESSAGES);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
index 596659d698..6b36048604 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
@@ -14,49 +14,15 @@
package com.google.gerrit.server.change;
-import com.google.common.base.Strings;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-
-import java.sql.Timestamp;
class GetDraft implements RestReadView<DraftResource> {
@Override
public Object apply(DraftResource rsrc) throws AuthException,
BadRequestException, ResourceConflictException, Exception {
- return new Comment(rsrc.getComment());
- }
-
- static enum Side {
- PARENT, REVISION;
- }
-
- static class Comment {
- final String kind = "gerritcodereview#comment";
- String id;
- String path;
- Side side;
- Integer line;
- String inReplyTo;
- String message;
- Timestamp updated;
-
- Comment(PatchLineComment c) {
- id = Url.encode(c.getKey().get());
- path = c.getKey().getParentKey().getFileName();
- if (c.getSide() == 0) {
- side = Side.PARENT;
- }
- if (c.getLine() > 0) {
- line = c.getLine();
- }
- inReplyTo = Url.encode(c.getParentUuid());
- message = Strings.emptyToNull(c.getMessage());
- updated = c.getWrittenOn();
- }
+ return new CommentInfo(rsrc.getComment(), null);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
index 997f5e78bb..08186fec8b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
@@ -24,7 +24,8 @@ public class GetReview implements RestReadView<RevisionResource> {
@Inject
GetReview(ChangeJson json) {
- this.json = json.addOption(ListChangesOption.DETAILED_LABELS)
+ this.json = json
+ .addOption(ListChangesOption.DETAILED_LABELS)
.addOption(ListChangesOption.DETAILED_ACCOUNTS);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
new file mode 100644
index 0000000000..23a71e80e5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class ListComments extends ListDrafts {
+ @Inject
+ ListComments(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf) {
+ super(db, alf);
+ }
+
+ @Override
+ protected boolean includeAuthorInfo() {
+ return true;
+ }
+
+ protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
+ throws OrmException {
+ return db.get().patchComments()
+ .publishedByPatchSet(rsrc.getPatchSet().getId());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
index 564363e02e..47863acb2e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
@@ -24,8 +24,10 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.GetDraft.Comment;
-import com.google.gerrit.server.change.GetDraft.Side;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.change.CommentInfo;
+import com.google.gerrit.server.change.CommentInfo.Side;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -35,23 +37,36 @@ import java.util.List;
import java.util.Map;
class ListDrafts implements RestReadView<RevisionResource> {
- private final Provider<ReviewDb> db;
+ protected final Provider<ReviewDb> db;
+ private final AccountInfo.Loader.Factory accountLoaderFactory;
@Inject
- ListDrafts(Provider<ReviewDb> db) {
+ ListDrafts(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf) {
this.db = db;
+ this.accountLoaderFactory = alf;
+ }
+
+ protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
+ throws OrmException {
+ return db.get().patchComments()
+ .draftByPatchSetAuthor(
+ rsrc.getPatchSet().getId(),
+ rsrc.getAccountId());
+ }
+
+ protected boolean includeAuthorInfo() {
+ return false;
}
@Override
public Object apply(RevisionResource rsrc) throws AuthException,
BadRequestException, ResourceConflictException, Exception {
- Map<String, List<Comment>> out = Maps.newTreeMap();
- for (PatchLineComment c : db.get().patchComments()
- .draftByPatchSetAuthor(
- rsrc.getPatchSet().getId(),
- rsrc.getAccountId())) {
- Comment o = new Comment(c);
- List<Comment> list = out.get(o.path);
+ Map<String, List<CommentInfo>> out = Maps.newTreeMap();
+ AccountInfo.Loader accountLoader =
+ includeAuthorInfo() ? accountLoaderFactory.create(true) : null;
+ for (PatchLineComment c : listComments(rsrc)) {
+ CommentInfo o = new CommentInfo(c, accountLoader);
+ List<CommentInfo> list = out.get(o.path);
if (list == null) {
list = Lists.newArrayList();
out.put(o.path, list);
@@ -59,10 +74,10 @@ class ListDrafts implements RestReadView<RevisionResource> {
o.path = null;
list.add(o);
}
- for (List<Comment> list : out.values()) {
- Collections.sort(list, new Comparator<Comment>() {
+ for (List<CommentInfo> list : out.values()) {
+ Collections.sort(list, new Comparator<CommentInfo>() {
@Override
- public int compare(Comment a, Comment b) {
+ public int compare(CommentInfo a, CommentInfo b) {
int c = firstNonNull(a.side, Side.REVISION).ordinal()
- firstNonNull(b.side, Side.REVISION).ordinal();
if (c == 0) {
@@ -75,6 +90,9 @@ class ListDrafts implements RestReadView<RevisionResource> {
}
});
}
+ if (accountLoader != null) {
+ accountLoader.fill();
+ }
return out;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 7e175a742f..690faf3147 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.change;
import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
+import static com.google.gerrit.server.change.CommentResource.COMMENT_KIND;
import static com.google.gerrit.server.change.DraftResource.DRAFT_KIND;
import static com.google.gerrit.server.change.PatchResource.PATCH_KIND;
import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
@@ -33,9 +34,11 @@ public class Module extends RestApiModule {
bind(Revisions.class);
bind(Reviewers.class);
bind(Drafts.class);
+ bind(Comments.class);
bind(Patches.class);
DynamicMap.mapOf(binder(), CHANGE_KIND);
+ DynamicMap.mapOf(binder(), COMMENT_KIND);
DynamicMap.mapOf(binder(), DRAFT_KIND);
DynamicMap.mapOf(binder(), PATCH_KIND);
DynamicMap.mapOf(binder(), REVIEWER_KIND);
@@ -70,6 +73,9 @@ public class Module extends RestApiModule {
put(DRAFT_KIND).to(PutDraft.class);
delete(DRAFT_KIND).to(DeleteDraft.class);
+ child(REVISION_KIND, "comments").to(Comments.class);
+ get(COMMENT_KIND).to(GetComment.class);
+
child(REVISION_KIND, "files").to(Patches.class);
put(PATCH_KIND, "reviewed").to(PutReviewed.class);
delete(PATCH_KIND, "reviewed").to(DeleteReviewed.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 66b939b168..9c14bcbbae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -92,7 +92,7 @@ public class PostReview implements RestModifyView<RevisionResource, Input> {
static class Comment {
String id;
- GetDraft.Side side;
+ CommentInfo.Side side;
int line;
String inReplyTo;
String message;
@@ -132,6 +132,7 @@ public class PostReview implements RestModifyView<RevisionResource, Input> {
checkComments(input.comments);
}
if (input.notify == null) {
+ log.warn("notify = null; assuming notify = NONE");
input.notify = NotifyHandling.NONE;
}
@@ -288,7 +289,7 @@ public class PostReview implements RestModifyView<RevisionResource, Input> {
}
e.setStatus(PatchLineComment.Status.PUBLISHED);
e.setWrittenOn(timestamp);
- e.setSide(c.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
+ e.setSide(c.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
e.setMessage(c.message);
(create ? ins : upd).add(e);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
index d5eaa9bad2..befb8d70d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
@@ -23,7 +23,7 @@ import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.GetDraft.Side;
+import com.google.gerrit.server.change.CommentInfo.Side;
import com.google.gerrit.server.change.PutDraft.Input;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -85,12 +85,12 @@ class PutDraft implements RestModifyView<DraftResource, Input> {
} else {
db.get().patchComments().update(Collections.singleton(update(c, in)));
}
- return new GetDraft.Comment(c);
+ return new CommentInfo(c, null);
}
private PatchLineComment update(PatchLineComment e, Input in) {
if (in.side != null) {
- e.setSide(in.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
+ e.setSide(in.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
}
if (in.line != null) {
e.setLine(in.line);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index c268530933..154bd64e83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -27,8 +27,6 @@ import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.Revert.Input;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.mail.RevertedSender;
@@ -42,8 +40,6 @@ import com.google.inject.Provider;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
-import javax.annotation.Nullable;
-
public class Revert implements RestModifyView<ChangeResource, Input> {
private final ChangeHooks hooks;
private final RevertedSender.Factory revertedSenderFactory;
@@ -53,8 +49,7 @@ public class Revert implements RestModifyView<ChangeResource, Input> {
private final GitRepositoryManager gitManager;
private final PersonIdent myIdent;
private final PatchSetInfoFactory patchSetInfoFactory;
- private final GitReferenceUpdated gitRefUpdated;
- private final String canonicalWebUrl;
+ private final ChangeInserter changeInserter;
public static class Input {
public String message;
@@ -68,9 +63,8 @@ public class Revert implements RestModifyView<ChangeResource, Input> {
ChangeJson json,
GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
- final GitReferenceUpdated gitRefUpdated,
@GerritPersonIdent final PersonIdent myIdent,
- @CanonicalWebUrl @Nullable final String canonicalWebUrl) {
+ final ChangeInserter changeInserter) {
this.hooks = hooks;
this.revertedSenderFactory = revertedSenderFactory;
this.commitValidatorsFactory = commitValidatorsFactory;
@@ -78,9 +72,8 @@ public class Revert implements RestModifyView<ChangeResource, Input> {
this.json = json;
this.gitManager = gitManager;
this.myIdent = myIdent;
- this.gitRefUpdated = gitRefUpdated;
+ this.changeInserter = changeInserter;
this.patchSetInfoFactory = patchSetInfoFactory;
- this.canonicalWebUrl = canonicalWebUrl;
}
@Override
@@ -104,7 +97,7 @@ public class Revert implements RestModifyView<ChangeResource, Input> {
commitValidators,
Strings.emptyToNull(input.message), dbProvider.get(),
revertedSenderFactory, hooks, git, patchSetInfoFactory,
- gitRefUpdated, myIdent, canonicalWebUrl);
+ myIdent, changeInserter);
return json.format(revertedChangeId);
} catch (InvalidChangeOperationException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index c63bf5d3a7..2b51b0a802 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -106,6 +106,10 @@ public class Submit implements RestModifyView<RevisionResource, Input> {
checkSubmitRule(rsrc);
change = submit(rsrc, caller);
+ if (change == null) {
+ throw new ResourceConflictException("change is "
+ + status(dbProvider.get().changes().get(rsrc.getChange().getId())));
+ }
if (input.waitForMerge) {
mergeQueue.merge(change.getDest());
@@ -123,21 +127,7 @@ public class Submit implements RestModifyView<RevisionResource, Input> {
case MERGED:
return new Output(Status.MERGED, change);
case NEW:
- // If the merge was attempted and it failed the system usually
- // writes a comment as a ChangeMessage and sets status to NEW.
- // Find the relevant message and report that as the conflict.
- final Timestamp before = rsrc.getChange().getLastUpdatedOn();
- ChangeMessage msg = Iterables.getFirst(Iterables.filter(
- Lists.reverse(dbProvider.get().changeMessages()
- .byChange(change.getId())
- .toList()),
- new Predicate<ChangeMessage>() {
- @Override
- public boolean apply(ChangeMessage input) {
- return input.getAuthor() == null
- && input.getWrittenOn().getTime() >= before.getTime();
- }
- }), null);
+ ChangeMessage msg = getConflictMessage(rsrc);
if (msg != null) {
throw new ResourceConflictException(msg.getMessage());
}
@@ -146,8 +136,30 @@ public class Submit implements RestModifyView<RevisionResource, Input> {
}
}
- private Change submit(RevisionResource rsrc, IdentifiedUser caller)
- throws OrmException, ResourceConflictException {
+ /**
+ * If the merge was attempted and it failed the system usually writes a
+ * comment as a ChangeMessage and sets status to NEW. Find the relevant
+ * message and return it.
+ */
+ public ChangeMessage getConflictMessage(RevisionResource rsrc)
+ throws OrmException {
+ final Timestamp before = rsrc.getChange().getLastUpdatedOn();
+ ChangeMessage msg = Iterables.getFirst(Iterables.filter(
+ Lists.reverse(dbProvider.get().changeMessages()
+ .byChange(rsrc.getChange().getId())
+ .toList()),
+ new Predicate<ChangeMessage>() {
+ @Override
+ public boolean apply(ChangeMessage input) {
+ return input.getAuthor() == null
+ && input.getWrittenOn().getTime() >= before.getTime();
+ }
+ }), null);
+ return msg;
+ }
+
+ public Change submit(RevisionResource rsrc, IdentifiedUser caller)
+ throws OrmException {
final Timestamp timestamp = new Timestamp(System.currentTimeMillis());
Change change = rsrc.getChange();
ReviewDb db = dbProvider.get();
@@ -169,8 +181,7 @@ public class Submit implements RestModifyView<RevisionResource, Input> {
}
});
if (change == null) {
- throw new ResourceConflictException("change is "
- + status(db.changes().get(rsrc.getChange().getId())));
+ return null;
}
db.commit();
} finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index 89c507bdee..d7bf5a36b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -16,7 +16,6 @@ package com.google.gerrit.server.changedetail;
import com.google.common.collect.Sets;
import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
@@ -73,7 +72,6 @@ public class RebaseChange {
private final GitReferenceUpdated gitRefUpdated;
private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
private final ChangeHookRunner hooks;
- private final ApprovalsUtil approvalsUtil;
private final MergeUtil.Factory mergeUtilFactory;
private final ProjectCache projectCache;
@@ -84,7 +82,7 @@ public class RebaseChange {
final GitRepositoryManager gitManager,
final GitReferenceUpdated gitRefUpdated,
final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
- final ChangeHookRunner hooks, final ApprovalsUtil approvalsUtil,
+ final ChangeHookRunner hooks,
final MergeUtil.Factory mergeUtilFactory,
final ProjectCache projectCache) {
this.changeControlFactory = changeControlFactory;
@@ -95,7 +93,6 @@ public class RebaseChange {
this.gitRefUpdated = gitRefUpdated;
this.rebasedPatchSetSenderFactory = rebasedPatchSetSenderFactory;
this.hooks = hooks;
- this.approvalsUtil = approvalsUtil;
this.mergeUtilFactory = mergeUtilFactory;
this.projectCache = projectCache;
}
@@ -384,10 +381,8 @@ public class RebaseChange {
"Change %s was modified", change.getId()));
}
- final LabelTypes labelTypes =
- projectCache.get(change.getProject()).getLabelTypes();
- approvalsUtil.copyVetosToPatchSet(db, labelTypes,
- change.currentPatchSetId());
+ ApprovalsUtil.copyLabels(db, projectCache.get(change.getProject())
+ .getLabelTypes(), patchSetId, change.currentPatchSetId());
final ChangeMessage cmsg =
new ChangeMessage(new ChangeMessage.Key(change.getId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 1da611c8e8..62c6863743 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -28,6 +28,7 @@ import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.rules.PrologModule;
import com.google.gerrit.rules.RulesCache;
import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.FileTypeRegistry;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
@@ -64,6 +65,7 @@ import com.google.gerrit.server.auth.UniversalAuthBackend;
import com.google.gerrit.server.auth.ldap.LdapModule;
import com.google.gerrit.server.avatar.AvatarProvider;
import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.ChangeCache;
@@ -93,6 +95,8 @@ import com.google.gerrit.server.patch.PatchListCacheImpl;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.AccessControlModule;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.CommentLinkInfo;
+import com.google.gerrit.server.project.CommentLinkProvider;
import com.google.gerrit.server.project.PerformCreateProject;
import com.google.gerrit.server.project.PermissionCollection;
import com.google.gerrit.server.project.ProjectCacheImpl;
@@ -112,6 +116,8 @@ import com.google.inject.TypeLiteral;
import org.apache.velocity.runtime.RuntimeInstance;
import org.eclipse.jgit.lib.Config;
+import java.util.List;
+
/** Starts global state with standard dependencies. */
public class GerritGlobalModule extends FactoryModule {
@@ -211,6 +217,8 @@ public class GerritGlobalModule extends FactoryModule {
bind(EventFactory.class);
bind(TransferConfig.class);
+ bind(ApprovalsUtil.class);
+ bind(ChangeInserter.class);
bind(ChangeMergeQueue.class).in(SINGLETON);
bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
factory(ReloadSubmitQueueOp.Factory.class);
@@ -251,5 +259,8 @@ public class GerritGlobalModule extends FactoryModule {
bind(AccountManager.class);
bind(ChangeUserName.CurrentUser.class);
factory(ChangeUserName.Factory.class);
+
+ bind(new TypeLiteral<List<CommentLinkInfo>>() {})
+ .toProvider(CommentLinkProvider.class).in(SINGLETON);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index ec14883033..af14015a14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -16,10 +16,8 @@ package com.google.gerrit.server.config;
import static com.google.inject.Scopes.SINGLETON;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
import com.google.gerrit.server.changedetail.PublishDraft;
import com.google.gerrit.server.git.BanCommit;
@@ -39,8 +37,6 @@ public class GerritRequestModule extends FactoryModule {
bind(RequestCleanup.class).in(RequestScoped.class);
bind(RequestScopedReviewDbProvider.class);
bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
- bind(ApprovalsUtil.class);
- bind(ChangeInserter.class);
bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
bind(ChangeControl.Factory.class).in(SINGLETON);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 8d76e90397..2116c0c348 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -24,6 +24,10 @@ import java.io.IOException;
/** Important paths within a {@link SitePath}. */
@Singleton
public final class SitePaths {
+ public static final String CSS_FILENAME = "GerritSite.css";
+ public static final String HEADER_FILENAME = "GerritSiteHeader.html";
+ public static final String FOOTER_FILENAME = "GerritSiteFooter.html";
+
public final File site_path;
public final File bin_dir;
public final File etc_dir;
@@ -35,6 +39,7 @@ public final class SitePaths {
public final File mail_dir;
public final File hooks_dir;
public final File static_dir;
+ public final File themes_dir;
public final File gerrit_sh;
public final File gerrit_war;
@@ -71,6 +76,7 @@ public final class SitePaths {
mail_dir = new File(etc_dir, "mail");
hooks_dir = new File(site_path, "hooks");
static_dir = new File(site_path, "static");
+ themes_dir = new File(site_path, "themes");
gerrit_sh = new File(bin_dir, "gerrit.sh");
gerrit_war = new File(bin_dir, "gerrit.war");
@@ -85,9 +91,9 @@ public final class SitePaths {
ssh_dsa = new File(etc_dir, "ssh_host_dsa_key");
peer_keys = new File(etc_dir, "peer_keys");
- site_css = new File(etc_dir, "GerritSite.css");
- site_header = new File(etc_dir, "GerritSiteHeader.html");
- site_footer = new File(etc_dir, "GerritSiteFooter.html");
+ site_css = new File(etc_dir, CSS_FILENAME);
+ site_header = new File(etc_dir, HEADER_FILENAME);
+ site_footer = new File(etc_dir, FOOTER_FILENAME);
site_gitweb = new File(etc_dir, "gitweb_config.perl");
if (site_path.exists()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/AccountAttribute.java
index 2d88b834d0..e5627c261e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/AccountAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
public class AccountAttribute {
public String name;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/ApprovalAttribute.java
index baa660c798..3059be3760 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ApprovalAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ApprovalAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
public class ApprovalAttribute {
public String type;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
index 5150b48e2f..73398290a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
import com.google.gerrit.reviewdb.client.Change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java
index 47fbdac1c2..4c796f2788 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
public class DependencyAttribute {
public String id;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/MessageAttribute.java
index 71b38b5279..f18bebad41 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/MessageAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
public class MessageAttribute {
public Long timestamp;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchAttribute.java
index 82f44a1c30..12ac30a8ce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
index 1123e5f63b..79d82e3802 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
import java.util.List;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetCommentAttribute.java
index e0c8c13626..7610068a44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetCommentAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
public class PatchSetCommentAttribute {
public String file;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java
index ecf2b9a597..989706534a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
-public class QueryStats {
+public class QueryStatsAttribute {
public final String type = "stats";
public int rowCount;
public long runTimeMilliseconds;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/RefUpdateAttribute.java
index e4d715a3f0..b3808d9b15 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/RefUpdateAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
public class RefUpdateAttribute {
public String oldRev;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
index 99d0350842..4c774c27db 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
public class SubmitLabelAttribute {
public String label;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
index 04b76e1287..1ce2ce6fab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
import java.util.List;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/TrackingIdAttribute.java
index 7d55dd2da8..473ea43f1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/TrackingIdAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/TrackingIdAttribute.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.events;
+package com.google.gerrit.server.data;
public class TrackingIdAttribute {
public String system;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
index baaf30cb3e..b0eb9c6b4f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
@@ -14,6 +14,10 @@
package com.google.gerrit.server.events;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
public class ChangeAbandonedEvent extends ChangeEvent {
public final String type = "change-abandoned";
public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
index 0d5fc31a3e..38996a5e05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
@@ -14,6 +14,10 @@
package com.google.gerrit.server.events;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
public class ChangeMergedEvent extends ChangeEvent {
public final String type = "change-merged";
public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
index 717e23cc05..e761190d6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
@@ -14,6 +14,10 @@
package com.google.gerrit.server.events;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
public class ChangeRestoredEvent extends ChangeEvent {
public final String type = "change-restored";
public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
index f00caaf950..52d7409819 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
@@ -14,6 +14,11 @@
package com.google.gerrit.server.events;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
public class CommentAddedEvent extends ChangeEvent {
public final String type = "comment-added";
public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
index c90ac90de1..7fd033a15b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
@@ -14,6 +14,10 @@
package com.google.gerrit.server.events;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
public class DraftPublishedEvent extends ChangeEvent {
public final String type = "draft-published";
public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index d556e7337b..63bfa710ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -33,6 +33,18 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ApprovalAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.DependencyAttribute;
+import com.google.gerrit.server.data.MessageAttribute;
+import com.google.gerrit.server.data.PatchAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.data.PatchSetCommentAttribute;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gerrit.server.data.SubmitLabelAttribute;
+import com.google.gerrit.server.data.SubmitRecordAttribute;
+import com.google.gerrit.server.data.TrackingIdAttribute;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
index e6ff525562..599fe60ea2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
@@ -14,6 +14,10 @@
package com.google.gerrit.server.events;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
public class MergeFailedEvent extends ChangeEvent {
public final String type = "merge-failed";
public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
index 15e39784bb..fbaf4ef1ce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
@@ -14,6 +14,10 @@
package com.google.gerrit.server.events;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
public class PatchSetCreatedEvent extends ChangeEvent {
public final String type = "patchset-created";
public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
index f90bc81018..944c9ad65e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
@@ -14,6 +14,9 @@
package com.google.gerrit.server.events;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+
public class RefUpdatedEvent extends ChangeEvent {
public final String type = "ref-updated";
public AccountAttribute submitter;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
index a881d8d33e..e00cc60417 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
@@ -14,6 +14,10 @@
package com.google.gerrit.server.events;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+
public class ReviewerAddedEvent extends ChangeEvent {
public final String type = "reviewer-added";
public ChangeAttribute change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
index 69dcb156db..8595472517 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.git;
-enum CommitMergeStatus {
+public enum CommitMergeStatus {
/** */
CLEAN_MERGE("Change has been successfully merged into the git repository."),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index dace0ae90b..9715d9c47f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -27,7 +27,6 @@ import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.Capable;
-import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
@@ -160,8 +159,7 @@ public class MergeOp {
final ProjectCache pc, final LabelNormalizer fs,
final GitReferenceUpdated gru, final MergedSender.Factory msf,
final MergeFailSender.Factory mfsf,
- final LabelTypes labelTypes, final PatchSetInfoFactory psif,
- final IdentifiedUser.GenericFactory iuf,
+ final PatchSetInfoFactory psif, final IdentifiedUser.GenericFactory iuf,
final ChangeControl.GenericFactory changeControlFactory,
final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
final ChangeHooks hooks, final AccountCache accountCache,
@@ -600,6 +598,11 @@ public class MergeOp {
try {
if (rw.isMergedInto(commit, branchTip)) {
commit.statusCode = CommitMergeStatus.ALREADY_MERGED;
+ try {
+ setMergedPatchSet(chg.getId(), ps.getId());
+ } catch (OrmException e) {
+ log.error("Cannot mark change " + chg.getId() + " merged", e);
+ }
continue;
}
} catch (IOException err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 2e14dfb40c..bfaa97ea80 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -14,12 +14,14 @@
package com.google.gerrit.server.git;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.common.data.Permission.isPermission;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
@@ -45,6 +47,7 @@ import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.project.CommentLinkInfo;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
@@ -64,8 +67,16 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
public class ProjectConfig extends VersionedMetaData {
+ public static final String COMMENTLINK = "commentlink";
+ private static final String KEY_MATCH = "match";
+ private static final String KEY_HTML = "html";
+ private static final String KEY_LINK = "link";
+ private static final String KEY_ENABLED = "enabled";
+
private static final String PROJECT_CONFIG = "project.config";
private static final String GROUP_LIST = "groups";
@@ -112,6 +123,7 @@ public class ProjectConfig extends VersionedMetaData {
private static final String KEY_ABBREVIATION = "abbreviation";
private static final String KEY_FUNCTION = "function";
private static final String KEY_COPY_MIN_SCORE = "copyMinScore";
+ private static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
private static final String KEY_VALUE = "value";
private static final String KEY_CAN_OVERRIDE = "canOverride";
private static final Set<String> LABEL_FUNCTIONS = ImmutableSet.of(
@@ -130,6 +142,7 @@ public class ProjectConfig extends VersionedMetaData {
private Map<String, ContributorAgreement> contributorAgreements;
private Map<String, NotifyConfig> notifySections;
private Map<String, LabelType> labelSections;
+ private List<CommentLinkInfo> commentLinkSections;
private List<ValidationError> validationErrors;
private ObjectId rulesId;
@@ -147,6 +160,42 @@ public class ProjectConfig extends VersionedMetaData {
return r;
}
+ public static CommentLinkInfo buildCommentLink(Config cfg, String name,
+ boolean allowRaw) throws IllegalArgumentException {
+ String match = cfg.getString(COMMENTLINK, name, KEY_MATCH);
+ if (match != null) {
+ // Unfortunately this validation isn't entirely complete. Clients
+ // can have exceptions trying to evaluate the pattern if they don't
+ // support a token used, even if the server does support the token.
+ //
+ // At the minimum, we can trap problems related to unmatched groups.
+ Pattern.compile(match);
+ }
+
+ String link = cfg.getString(COMMENTLINK, name, KEY_LINK);
+ String html = cfg.getString(COMMENTLINK, name, KEY_HTML);
+ boolean hasHtml = !Strings.isNullOrEmpty(html);
+
+ String rawEnabled = cfg.getString(COMMENTLINK, name, KEY_ENABLED);
+ Boolean enabled;
+ if (rawEnabled != null) {
+ enabled = cfg.getBoolean(COMMENTLINK, name, KEY_ENABLED, true);
+ } else {
+ enabled = null;
+ }
+ checkArgument(allowRaw || !hasHtml, "Raw html replacement not allowed");
+
+ if (Strings.isNullOrEmpty(match) && Strings.isNullOrEmpty(link) && !hasHtml
+ && enabled != null) {
+ if (enabled) {
+ return new CommentLinkInfo.Enabled(name);
+ } else {
+ return new CommentLinkInfo.Disabled(name);
+ }
+ }
+ return new CommentLinkInfo(name, match, link, html, enabled);
+ }
+
public ProjectConfig(Project.NameKey projectName) {
this.projectName = projectName;
}
@@ -232,6 +281,10 @@ public class ProjectConfig extends VersionedMetaData {
return labelSections;
}
+ public Collection<CommentLinkInfo> getCommentLinkSections() {
+ return commentLinkSections;
+ }
+
public GroupReference resolve(AccountGroup group) {
return resolve(GroupReference.forGroup(group));
}
@@ -332,6 +385,7 @@ public class ProjectConfig extends VersionedMetaData {
loadAccessSections(rc, groupsByName);
loadNotifySections(rc, groupsByName);
loadLabelSections(rc);
+ loadCommentLinkSections(rc);
}
private void loadAccountsSection(
@@ -582,12 +636,33 @@ public class ProjectConfig extends VersionedMetaData {
}
label.setCopyMinScore(
rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, false));
+ label.setCopyMaxScore(
+ rc.getBoolean(LABEL, name, KEY_COPY_MAX_SCORE, false));
label.setCanOverride(
rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, true));
labelSections.put(name, label);
}
}
+ private void loadCommentLinkSections(Config rc) {
+ Set<String> subsections = rc.getSubsections(COMMENTLINK);
+ commentLinkSections = Lists.newArrayListWithCapacity(subsections.size());
+ for (String name : subsections) {
+ try {
+ commentLinkSections.add(buildCommentLink(rc, name, false));
+ } catch (PatternSyntaxException e) {
+ error(new ValidationError(PROJECT_CONFIG, String.format(
+ "Invalid pattern \"%s\" in commentlink.%s.match: %s",
+ rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())));
+ } catch (IllegalArgumentException e) {
+ error(new ValidationError(PROJECT_CONFIG, String.format(
+ "Error in pattern \"%s\" in commentlink.%s.match: %s",
+ rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())));
+ }
+ }
+ commentLinkSections = ImmutableList.copyOf(commentLinkSections);
+ }
+
private Map<String, GroupReference> readGroupList() throws IOException {
groupsByUUID = new HashMap<AccountGroup.UUID, GroupReference>();
Map<String, GroupReference> groupsByName =
@@ -849,6 +924,11 @@ public class ProjectConfig extends VersionedMetaData {
} else {
rc.unset(LABEL, name, KEY_COPY_MIN_SCORE);
}
+ if (label.isCopyMaxScore()) {
+ rc.setBoolean(LABEL, name, KEY_COPY_MAX_SCORE, true);
+ } else {
+ rc.unset(LABEL, name, KEY_COPY_MAX_SCORE);
+ }
if (!label.canOverride()) {
rc.setBoolean(LABEL, name, KEY_CAN_OVERRIDE, false);
} else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index bcc30144fc..3e28b07d87 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -18,7 +18,6 @@ import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
-
import static org.eclipse.jgit.lib.Constants.R_HEADS;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
@@ -30,12 +29,14 @@ import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.CheckedFuture;
@@ -63,6 +64,9 @@ import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Submit;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.TrackingFooters;
@@ -91,6 +95,7 @@ import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -130,6 +135,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -278,10 +284,13 @@ public class ReceiveCommits {
new HashMap<RevCommit, ReplaceRequest>();
private final Set<RevCommit> validCommits = new HashSet<RevCommit>();
+ private ListMultimap<Change.Id, Ref> refsByChange;
private SetMultimap<ObjectId, Ref> refsById;
private Map<String, Ref> allRefs;
private final SubmoduleOp.Factory subOpFactory;
+ private final Provider<Submit> submitProvider;
+ private final MergeQueue mergeQueue;
private final List<CommitValidationMessage> messages = new ArrayList<CommitValidationMessage>();
private ListMultimap<Error, String> errors = LinkedListMultimap.create();
@@ -321,7 +330,9 @@ public class ReceiveCommits {
ReceiveConfig config,
@Assisted final ProjectControl projectControl,
@Assisted final Repository repo,
- final SubmoduleOp.Factory subOpFactory) throws IOException {
+ final SubmoduleOp.Factory subOpFactory,
+ final Provider<Submit> submitProvider,
+ final MergeQueue mergeQueue) throws IOException {
this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
this.db = db;
this.schemaFactory = schemaFactory;
@@ -356,6 +367,8 @@ public class ReceiveCommits {
this.rejectCommits = loadRejectCommitsMap();
this.subOpFactory = subOpFactory;
+ this.submitProvider = submitProvider;
+ this.mergeQueue = mergeQueue;
this.messageSender = new ReceivePackMessageSender();
@@ -998,6 +1011,10 @@ public class ReceiveCommits {
RefControl ctl;
Set<Account.Id> reviewer = Sets.newLinkedHashSet();
Set<Account.Id> cc = Sets.newLinkedHashSet();
+ RevCommit baseCommit;
+
+ @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
+ ObjectId base;
@Option(name = "--topic", metaVar = "NAME", usage = "attach topic to changes")
String topic;
@@ -1005,6 +1022,9 @@ public class ReceiveCommits {
@Option(name = "--draft", usage = "mark new/updated changes as draft")
boolean draft;
+ @Option(name = "--submit", usage = "immediately submit the change")
+ boolean submit;
+
@Option(name = "-r", metaVar = "EMAIL", usage = "add reviewer to changes")
void reviewer(Account.Id id) {
reviewer.add(id);
@@ -1029,6 +1049,10 @@ public class ReceiveCommits {
return draft;
}
+ boolean isSubmit() {
+ return submit;
+ }
+
MailRecipients getMailRecipients() {
return new MailRecipients(reviewer, cc);
}
@@ -1125,13 +1149,41 @@ public class ReceiveCommits {
return;
}
+ if (magicBranch.isDraft() && magicBranch.isSubmit()) {
+ reject(cmd, "cannot submit draft");
+ return;
+ }
+
+ if (magicBranch.isSubmit() && !projectControl.controlForRef(
+ MagicBranch.NEW_CHANGE + ref).canSubmit()) {
+ reject(cmd, "submit not allowed");
+ }
+
+ RevWalk walk = rp.getRevWalk();
+ if (magicBranch.base != null) {
+ try {
+ magicBranch.baseCommit = walk.parseCommit(magicBranch.base);
+ } catch (IncorrectObjectTypeException notCommit) {
+ reject(cmd, "base must be a commit");
+ return;
+ } catch (MissingObjectException e) {
+ reject(cmd, "base not found");
+ return;
+ } catch (IOException e) {
+ log.warn(String.format(
+ "Project %s cannot read %s",
+ project.getName(), magicBranch.base.name()), e);
+ reject(cmd, "internal server error");
+ return;
+ }
+ }
+
// Validate that the new commits are connected with the target
// branch. If they aren't, we want to abort. We do this check by
// looking to see if we can compute a merge base between the new
// commits and the target branch head.
//
try {
- final RevWalk walk = rp.getRevWalk();
final RevCommit tip = walk.parseCommit(magicBranch.cmd.getNewId());
Ref targetRef = rp.getAdvertisedRefs().get(magicBranch.ctl.getRefName());
if (targetRef == null || targetRef.getObjectId() == null) {
@@ -1261,10 +1313,14 @@ public class ReceiveCommits {
try {
Set<ObjectId> existing = Sets.newHashSet();
walk.markStart(walk.parseCommit(magicBranch.cmd.getNewId()));
- markHeadsAsUninteresting(
- walk,
- existing,
- magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+ if (magicBranch.baseCommit != null) {
+ walk.markUninteresting(magicBranch.baseCommit);
+ } else {
+ markHeadsAsUninteresting(
+ walk,
+ existing,
+ magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+ }
List<ChangeLookup> pending = Lists.newArrayList();
final Set<Change.Key> newChangeIds = new HashSet<Change.Key>();
@@ -1471,8 +1527,8 @@ public class ReceiveCommits {
recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
recipients.remove(me);
- changeInserter.insertChange(db, change, ps, commit, labelTypes,
- footerLines, info, recipients.getReviewers());
+ changeInserter.insertChange(db, change, ps, commit, labelTypes, info,
+ recipients.getReviewers());
created = true;
@@ -1498,16 +1554,56 @@ public class ReceiveCommits {
return "send-email newchange";
}
}));
+
+ if (magicBranch != null && magicBranch.isSubmit()) {
+ submit(projectControl.controlFor(change), ps);
+ }
+ }
+ }
+
+ private void submit(ChangeControl changeCtl, PatchSet ps) throws OrmException {
+ Submit submit = submitProvider.get();
+ RevisionResource rsrc = new RevisionResource(new ChangeResource(changeCtl), ps);
+ Change c = submit.submit(rsrc, currentUser);
+ if (c == null) {
+ addError("Submitting change " + changeCtl.getChange().getChangeId()
+ + " failed.");
+ } else {
+ addMessage("");
+ mergeQueue.merge(c.getDest());
+ c = db.changes().get(c.getId());
+ switch (c.getStatus()) {
+ case SUBMITTED:
+ addMessage("Change " + c.getChangeId() + " submitted.");
+ break;
+ case MERGED:
+ addMessage("Change " + c.getChangeId() + " merged.");
+ break;
+ case NEW:
+ ChangeMessage msg = submit.getConflictMessage(rsrc);
+ if (msg != null) {
+ addMessage("Change " + c.getChangeId() + ": " + msg.getMessage());
+ break;
+ }
+ default:
+ addMessage("change " + c.getChangeId() + " is "
+ + c.getStatus().name().toLowerCase());
+ }
}
}
private void preparePatchSetsForReplace() {
try {
readChangesForReplace();
- readPatchSetsForReplace();
- for (ReplaceRequest req : replaceByChange.values()) {
+ for (Iterator<ReplaceRequest> itr = replaceByChange.values().iterator();
+ itr.hasNext();) {
+ ReplaceRequest req = itr.next();
if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
req.validate(false);
+ if (req.skip && req.cmd == null) {
+ itr.remove();
+ replaceByCommit.remove(req.newCommit);
+ }
}
}
} catch (OrmException err) {
@@ -1559,17 +1655,6 @@ public class ReceiveCommits {
}
}
- private void readPatchSetsForReplace() throws OrmException {
- Map<Change.Id, ResultSet<PatchSet>> results = Maps.newHashMap();
- for (ReplaceRequest request : replaceByChange.values()) {
- Change.Id id = request.ontoChange;
- results.put(id, db.patchSets().byChange(id));
- }
- for (ReplaceRequest req : replaceByChange.values()) {
- req.patchSets = results.get(req.ontoChange).toList();
- }
- }
-
private class ReplaceRequest {
final Change.Id ontoChange;
final RevCommit newCommit;
@@ -1577,12 +1662,13 @@ public class ReceiveCommits {
final boolean checkMergedInto;
Change change;
ChangeControl changeCtl;
- List<PatchSet> patchSets;
+ BiMap<RevCommit, PatchSet.Id> revisions;
PatchSet newPatchSet;
ReceiveCommand cmd;
PatchSetInfo info;
ChangeMessage msg;
String mergedIntoRef;
+ boolean skip;
private PatchSet.Id priorPatchSet;
ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
@@ -1591,20 +1677,39 @@ public class ReceiveCommits {
this.newCommit = newCommit;
this.inputCommand = cmd;
this.checkMergedInto = checkMergedInto;
+
+ revisions = HashBiMap.create();
+ for (Ref ref : refs(toChange)) {
+ try {
+ revisions.forcePut(
+ rp.getRevWalk().parseCommit(ref.getObjectId()),
+ PatchSet.Id.fromRef(ref.getName()));
+ } catch (IOException err) {
+ log.warn(String.format(
+ "Project %s contains invalid change ref %s",
+ project.getName(), ref.getName()), err);
+ }
+ }
}
boolean validate(boolean autoClose) throws IOException {
if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) {
return false;
+ } else if (change == null) {
+ reject(inputCommand, "change " + ontoChange + " not found");
+ return false;
}
- if (change == null || patchSets == null) {
- reject(inputCommand, "change " + ontoChange + " not found");
+ priorPatchSet = change.currentPatchSetId();
+ if (!revisions.containsValue(priorPatchSet)) {
+ reject(inputCommand, "change " + ontoChange + " missing revisions");
return false;
}
- if (change.getStatus().isClosed()) {
- reject(inputCommand, "change " + ontoChange + " closed");
+ RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
+ if (newCommit == priorCommit) {
+ // Ignore requests to make the change its current state.
+ skip = true;
return false;
}
@@ -1612,88 +1717,59 @@ public class ReceiveCommits {
if (!changeCtl.canAddPatchSet()) {
reject(inputCommand, "cannot replace " + ontoChange);
return false;
+ } else if (change.getStatus().isClosed()) {
+ reject(inputCommand, "change " + ontoChange + " closed");
+ return false;
+ } else if (revisions.containsKey(newCommit)) {
+ reject(inputCommand, "commit already exists");
+ return false;
}
- rp.getRevWalk().parseBody(newCommit);
+ for (RevCommit prior : revisions.keySet()) {
+ // Don't allow a change to directly depend upon itself. This is a
+ // very common error due to users making a new commit rather than
+ // amending when trying to address review comments.
+ if (rp.getRevWalk().isMergedInto(prior, newCommit)) {
+ reject(inputCommand, "squash commits first");
+ return false;
+ }
+ }
+ rp.getRevWalk().parseBody(newCommit);
if (!validCommit(changeCtl.getRefControl(), inputCommand, newCommit)) {
return false;
}
- priorPatchSet = change.currentPatchSetId();
- for (final PatchSet ps : patchSets) {
- if (ps.getRevision() == null) {
- log.warn("Patch set " + ps.getId() + " has no revision");
- reject(inputCommand, "change state corrupt");
- return false;
- }
+ // Don't allow the same tree if the commit message is unmodified
+ // or no parents were updated (rebase), else warn that only part
+ // of the commit was modified.
+ if (newCommit.getTree() == priorCommit.getTree()) {
+ rp.getRevWalk().parseBody(priorCommit);
+ final boolean messageEq =
+ eq(newCommit.getFullMessage(), priorCommit.getFullMessage());
+ final boolean parentsEq = parentsEqual(newCommit, priorCommit);
+ final boolean authorEq = authorEqual(newCommit, priorCommit);
- final String revIdStr = ps.getRevision().get();
- final ObjectId commitId;
- try {
- commitId = ObjectId.fromString(revIdStr);
- } catch (IllegalArgumentException e) {
- log.warn("Invalid revision in " + ps.getId() + ": " + revIdStr);
- reject(inputCommand, "change state corrupt");
+ if (messageEq && parentsEq && authorEq && !autoClose) {
+ reject(inputCommand, "no changes made");
return false;
- }
-
- try {
- final RevCommit prior = rp.getRevWalk().parseCommit(commitId);
-
- // Don't allow the same commit to appear twice on the same change
- //
- if (newCommit == prior) {
- reject(inputCommand, "commit already exists");
- return false;
+ } else {
+ ObjectReader reader = rp.getRevWalk().getObjectReader();
+ StringBuilder msg = new StringBuilder();
+ msg.append("(W) ");
+ msg.append(reader.abbreviate(newCommit).name());
+ msg.append(":");
+ msg.append(" no files changed");
+ if (!authorEq) {
+ msg.append(", author changed");
}
-
- // Don't allow a change to directly depend upon itself. This is a
- // very common error due to users making a new commit rather than
- // amending when trying to address review comments.
- //
- if (rp.getRevWalk().isMergedInto(prior, newCommit)) {
- reject(inputCommand, "squash commits first");
- return false;
+ if (!messageEq) {
+ msg.append(", message updated");
}
-
- // Don't allow the same tree if the commit message is unmodified
- // or no parents were updated (rebase), else warn that only part
- // of the commit was modified.
- //
- if (priorPatchSet.equals(ps.getId()) && newCommit.getTree() == prior.getTree()) {
- rp.getRevWalk().parseBody(prior);
- final boolean messageEq =
- eq(newCommit.getFullMessage(), prior.getFullMessage());
- final boolean parentsEq = parentsEqual(newCommit, prior);
- final boolean authorEq = authorEqual(newCommit, prior);
-
- if (messageEq && parentsEq && authorEq && !autoClose) {
- reject(inputCommand, "no changes made");
- return false;
- } else {
- ObjectReader reader = rp.getRevWalk().getObjectReader();
- StringBuilder msg = new StringBuilder();
- msg.append("(W) ");
- msg.append(reader.abbreviate(newCommit).name());
- msg.append(":");
- msg.append(" no files changed");
- if (!authorEq) {
- msg.append(", author changed");
- }
- if (!messageEq) {
- msg.append(", message updated");
- }
- if (!parentsEq) {
- msg.append(", was rebased");
- }
- addMessage(msg.toString());
- }
+ if (!parentsEq) {
+ msg.append(", was rebased");
}
- } catch (IOException e) {
- log.error("Change " + change.getId() + " missing " + revIdStr, e);
- reject(inputCommand, "change state corrupt");
- return false;
+ addMessage(msg.toString());
}
}
@@ -1770,10 +1846,12 @@ public class ReceiveCommits {
mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
}
- List<PatchSetApproval> patchSetApprovals =
- approvalsUtil.copyVetosToPatchSet(db, labelTypes, newPatchSet.getId());
- final MailRecipients oldRecipients =
- getRecipientsFromApprovals(patchSetApprovals);
+ List<PatchSetApproval> oldChangeApprovals =
+ db.patchSetApprovals().byChange(change.getId()).toList();
+ final MailRecipients oldRecipients = getRecipientsFromApprovals(
+ oldChangeApprovals);
+ ApprovalsUtil.copyLabels(db, labelTypes, oldChangeApprovals,
+ priorPatchSet, newPatchSet.getId());
approvalsUtil.addReviewers(db, labelTypes, change, newPatchSet, info,
recipients.getReviewers(), oldRecipients.getAll());
recipients.add(oldRecipients);
@@ -1879,10 +1957,30 @@ public class ReceiveCommits {
return "send-email newpatchset";
}
}));
+
+ if (magicBranch != null && magicBranch.isSubmit()) {
+ submit(changeCtl, newPatchSet);
+ }
+
return newPatchSet.getId();
}
}
+ private List<Ref> refs(Change.Id changeId) {
+ if (refsByChange == null) {
+ int estRefsPerChange = 4;
+ refsByChange = ArrayListMultimap.create(
+ allRefs.size() / estRefsPerChange,
+ estRefsPerChange);
+ for (Ref ref : allRefs.values()) {
+ if (ref.getObjectId() != null && PatchSet.isRef(ref.getName())) {
+ refsByChange.put(Change.Id.fromRef(ref.getName()), ref);
+ }
+ }
+ }
+ return refsByChange.get(changeId);
+ }
+
static boolean parentsEqual(RevCommit a, RevCommit b) {
if (a.getParentCount() != b.getParentCount()) {
return false;
@@ -2038,7 +2136,6 @@ public class ReceiveCommits {
if (onto != null) {
final ReplaceRequest req = new ReplaceRequest(onto, c, cmd, false);
req.change = db.changes().get(onto);
- req.patchSets = db.patchSets().byChange(onto).toList();
toClose.add(req);
break;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java
deleted file mode 100644
index 71dbf87ba1..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- * Formatters for code review note headers.
- * <p>
- * This class provides a builder like interface for building the content of a
- * code review note. After instantiation, call as many as necessary
- * <code>append...(...)</code> methods and, at the end, call the
- * {@link #toString()} method to get the built note content.
- */
-class ReviewNoteHeaderFormatter {
-
- private final DateFormat rfc2822DateFormatter;
- private final String anonymousCowardName;
- private final StringBuilder sb = new StringBuilder();
-
- ReviewNoteHeaderFormatter(TimeZone tz, String anonymousCowardName) {
- rfc2822DateFormatter =
- new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
- rfc2822DateFormatter.setCalendar(Calendar.getInstance(tz, Locale.US));
- this.anonymousCowardName = anonymousCowardName;
- }
-
- void appendChangeId(Change.Key changeKey) {
- sb.append("Change-Id: ").append(changeKey.get()).append("\n");
- }
-
- void appendApproval(LabelType type, short value, Account user) {
- sb.append(type.getName());
- sb.append(value < 0 ? "-" : "+").append(Math.abs(value)).append(": ");
- appendUserData(user);
- sb.append("\n");
- }
-
- private void appendUserData(Account user) {
- boolean needSpace = false;
- boolean wroteData = false;
-
- if (user.getFullName() != null && ! user.getFullName().isEmpty()) {
- sb.append(user.getFullName());
- needSpace = true;
- wroteData = true;
- }
-
- if (user.getPreferredEmail() != null && ! user.getPreferredEmail().isEmpty()) {
- if (needSpace) {
- sb.append(" ");
- }
- sb.append("<").append(user.getPreferredEmail()).append(">");
- wroteData = true;
- }
-
- if (!wroteData) {
- sb.append(anonymousCowardName).append(" #").append(user.getId());
- }
- }
-
- void appendProject(String projectName) {
- sb.append("Project: ").append(projectName).append("\n");
- }
-
- void appendBranch(Branch.NameKey branch) {
- sb.append("Branch: ").append(branch.get()).append("\n");
- }
-
- void appendSubmittedBy(Account user) {
- sb.append("Submitted-by: ");
- appendUserData(user);
- sb.append("\n");
- }
-
- void appendSubmittedAt(Date date) {
- sb.append("Submitted-at: ").append(rfc2822DateFormatter.format(date))
- .append("\n");
- }
-
- void appendReviewedOn(String canonicalWebUrl, Change.Id changeId) {
- sb.append("Reviewed-on: ").append(canonicalWebUrl).append(changeId.get())
- .append("\n");
- }
-
- @Override
- public String toString() {
- return sb.toString();
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index 885f1aa46f..39d0ff8b94 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -329,6 +329,11 @@ public abstract class ChangeEmail extends NotificationEmail {
add(RecipientType.CC, ap.getAccountId());
}
} catch (OrmException err) {
+ if (includeZero) {
+ log.warn("Cannot CC users that commented on updated change", err);
+ } else {
+ log.warn("Cannot CC users that reviewed updated change", err);
+ }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 997bc03da6..ce50002197 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -268,7 +268,7 @@ public abstract class OutgoingEmail {
protected boolean shouldSendMessage() {
if (body.length() == 0) {
// If we have no message body, don't send.
- //
+ log.warn("Skipping delivery of email with no body");
return false;
}
@@ -276,7 +276,7 @@ public abstract class OutgoingEmail {
// If we have nobody to send this message to, then all of our
// selection filters previously for this type of message were
// unable to match a destination. Don't bother sending it.
- //
+ log.info("Skipping delivery of email with no recipients");
return false;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
new file mode 100644
index 0000000000..4035c7e47b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Strings;
+
+/** Info about a single commentlink section in a config. */
+public class CommentLinkInfo {
+ public static class Enabled extends CommentLinkInfo {
+ public Enabled(String name) {
+ super(name, true);
+ }
+
+ @Override
+ boolean isOverrideOnly() {
+ return true;
+ }
+ }
+
+ public static class Disabled extends CommentLinkInfo {
+ public Disabled(String name) {
+ super(name, false);
+ }
+
+ @Override
+ boolean isOverrideOnly() {
+ return true;
+ }
+ }
+
+ public final String match;
+ public final String link;
+ public final String html;
+ public final Boolean enabled; // null means true
+
+ public transient final String name;
+
+ public CommentLinkInfo(String name, String match, String link, String html,
+ Boolean enabled) {
+ checkArgument(name != null, "invalid commentlink.name");
+ checkArgument(!Strings.isNullOrEmpty(match),
+ "invalid commentlink.%s.match", name);
+ link = Strings.emptyToNull(link);
+ html = Strings.emptyToNull(html);
+ checkArgument(
+ (link != null && html == null) || (link == null && html != null),
+ "commentlink.%s must have either link or html", name);
+ this.name = name;
+ this.match = match;
+ this.link = link;
+ this.html = html;
+ this.enabled = enabled;
+ }
+
+ private CommentLinkInfo(CommentLinkInfo src, boolean enabled) {
+ this.name = src.name;
+ this.match = src.match;
+ this.link = src.link;
+ this.html = src.html;
+ this.enabled = enabled;
+ }
+
+ private CommentLinkInfo(String name, boolean enabled) {
+ this.name = name;
+ this.match = null;
+ this.link = null;
+ this.html = null;
+ this.enabled = enabled;
+ }
+
+ boolean isOverrideOnly() {
+ return false;
+ }
+
+ CommentLinkInfo inherit(CommentLinkInfo src) {
+ return new CommentLinkInfo(src, enabled);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
new file mode 100644
index 0000000000..114ab90a91
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.List;
+import java.util.Set;
+
+public class CommentLinkProvider implements Provider<List<CommentLinkInfo>> {
+ private final Config cfg;
+
+ @Inject
+ CommentLinkProvider(@GerritServerConfig Config cfg) {
+ this.cfg = cfg;
+ }
+
+ @Override
+ public List<CommentLinkInfo> get() {
+ Set<String> subsections = cfg.getSubsections(ProjectConfig.COMMENTLINK);
+ List<CommentLinkInfo> cls =
+ Lists.newArrayListWithCapacity(subsections.size());
+ for (String name : subsections) {
+ CommentLinkInfo cl = ProjectConfig.buildCommentLink(cfg, name, true);
+ if (cl.isOverrideOnly()) {
+ throw new ProvisionException(
+ "commentlink " + name + " empty except for \"enabled\"");
+ }
+ cls.add(cl);
+ }
+ return ImmutableList.copyOf(cls);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
new file mode 100644
index 0000000000..cd5e5d7ebe
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+import java.util.Map;
+
+public class GetConfig implements RestReadView<ProjectResource> {
+
+ @Override
+ public ConfigInfo apply(ProjectResource resource) {
+ ConfigInfo result = new ConfigInfo();
+ RefControl refConfig = resource.getControl()
+ .controlForRef(GitRepositoryManager.REF_CONFIG);
+ ProjectState state = resource.getControl().getProjectState();
+ if (refConfig.isVisible()) {
+ InheritedBooleanInfo useContributorAgreements = new InheritedBooleanInfo();
+ InheritedBooleanInfo useSignedOffBy = new InheritedBooleanInfo();
+ InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
+ InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
+
+ useContributorAgreements.value = state.isUseContributorAgreements();
+ useSignedOffBy.value = state.isUseSignedOffBy();
+ useContentMerge.value = state.isUseContentMerge();
+ requireChangeId.value = state.isRequireChangeID();
+
+ Project p = state.getProject();
+ useContributorAgreements.configuredValue = p.getUseContributorAgreements();
+ useSignedOffBy.configuredValue = p.getUseSignedOffBy();
+ useContentMerge.configuredValue = p.getUseContentMerge();
+ requireChangeId.configuredValue = p.getRequireChangeID();
+
+ ProjectState parentState = Iterables.getFirst(state.parents(), null);
+ if (parentState != null) {
+ useContributorAgreements.inheritedValue = parentState.isUseContributorAgreements();
+ useSignedOffBy.inheritedValue = parentState.isUseSignedOffBy();
+ useContentMerge.inheritedValue = parentState.isUseContentMerge();
+ requireChangeId.inheritedValue = parentState.isRequireChangeID();
+ }
+
+ result.useContributorAgreements = useContributorAgreements;
+ result.useSignedOffBy = useSignedOffBy;
+ result.useContentMerge = useContentMerge;
+ result.requireChangeId = requireChangeId;
+ }
+
+ // commentlinks are visible to anyone, as they are used for linkification
+ // on the client side.
+ result.commentlinks = Maps.newLinkedHashMap();
+ for (CommentLinkInfo cl : state.getCommentLinks()) {
+ result.commentlinks.put(cl.name, cl);
+ }
+
+ // Themes are visible to anyone, as they are rendered client-side.
+ result.theme = state.getTheme();
+ return result;
+ }
+
+ public static class ConfigInfo {
+ public final String kind = "gerritcodereview#project_config";
+
+ public InheritedBooleanInfo useContributorAgreements;
+ public InheritedBooleanInfo useContentMerge;
+ public InheritedBooleanInfo useSignedOffBy;
+ public InheritedBooleanInfo requireChangeId;
+
+ public Map<String, CommentLinkInfo> commentlinks;
+ public ThemeInfo theme;
+ }
+
+ public static class InheritedBooleanInfo {
+ public Boolean value;
+ public InheritableBoolean configuredValue;
+ public Boolean inheritedValue;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 94e316201c..1c61d96879 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -51,5 +51,7 @@ public class Module extends RestApiModule {
put(DASHBOARD_KIND).to(SetDashboard.class);
delete(DASHBOARD_KIND).to(DeleteDashboard.class);
install(new FactoryModuleBuilder().build(CreateProject.Factory.class));
+
+ get(PROJECT_KIND, "config").to(GetConfig.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 6150a3f7bf..6f80841a9a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -14,11 +14,14 @@
package com.google.gerrit.server.project;
+import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.common.io.Files;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.LabelType;
@@ -34,6 +37,7 @@ import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.CapabilityCollection;
import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.inject.Inject;
@@ -44,7 +48,10 @@ import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -58,17 +65,22 @@ import java.util.Set;
/** Cached information on a project. */
public class ProjectState {
+ private static final Logger log =
+ LoggerFactory.getLogger(ProjectState.class);
+
public interface Factory {
ProjectState create(ProjectConfig config);
}
private final boolean isAllProjects;
+ private final SitePaths sitePaths;
private final AllProjectsName allProjectsName;
private final ProjectCache projectCache;
private final ProjectControl.AssistedFactory projectControlFactory;
private final PrologEnvironment.Factory envFactory;
private final GitRepositoryManager gitMgr;
private final RulesCache rulesCache;
+ private final List<CommentLinkInfo> commentLinks;
private final ProjectConfig config;
private final Set<AccountGroup.UUID> localOwners;
@@ -82,18 +94,24 @@ public class ProjectState {
/** Local access sections, wrapped in SectionMatchers for faster evaluation. */
private volatile List<SectionMatcher> localAccessSections;
+ /** Theme information loaded from site_path/themes. */
+ private volatile ThemeInfo theme;
+
/** If this is all projects, the capabilities used by the server. */
private final CapabilityCollection capabilities;
@Inject
public ProjectState(
+ final SitePaths sitePaths,
final ProjectCache projectCache,
final AllProjectsName allProjectsName,
final ProjectControl.AssistedFactory projectControlFactory,
final PrologEnvironment.Factory envFactory,
final GitRepositoryManager gitMgr,
final RulesCache rulesCache,
+ final List<CommentLinkInfo> commentLinks,
@Assisted final ProjectConfig config) {
+ this.sitePaths = sitePaths;
this.projectCache = projectCache;
this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
this.allProjectsName = allProjectsName;
@@ -101,6 +119,7 @@ public class ProjectState {
this.envFactory = envFactory;
this.gitMgr = gitMgr;
this.rulesCache = rulesCache;
+ this.commentLinks = commentLinks;
this.config = config;
this.capabilities = isAllProjects
? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
@@ -293,6 +312,16 @@ public class ProjectState {
}
/**
+ * @return an iterable that walks in-order from All-Projects through the
+ * project hierarchy to this project.
+ */
+ public Iterable<ProjectState> treeInOrder() {
+ List<ProjectState> projects = Lists.newArrayList(tree());
+ Collections.reverse(projects);
+ return projects;
+ }
+
+ /**
* @return an iterable that walks through the parents of this project. Starts
* from the immediate parent of this project and progresses up the
* hierarchy to All-Projects.
@@ -343,9 +372,7 @@ public class ProjectState {
public LabelTypes getLabelTypes() {
Map<String, LabelType> types = Maps.newLinkedHashMap();
- List<ProjectState> projects = Lists.newArrayList(tree());
- Collections.reverse(projects);
- for (ProjectState s : projects) {
+ for (ProjectState s : treeInOrder()) {
for (LabelType type : s.getConfig().getLabelSections().values()) {
String lower = type.getName().toLowerCase();
LabelType old = types.get(lower);
@@ -363,6 +390,69 @@ public class ProjectState {
return new LabelTypes(Collections.unmodifiableList(all));
}
+ public List<CommentLinkInfo> getCommentLinks() {
+ Map<String, CommentLinkInfo> cls = Maps.newLinkedHashMap();
+ for (CommentLinkInfo cl : commentLinks) {
+ cls.put(cl.name.toLowerCase(), cl);
+ }
+ for (ProjectState s : treeInOrder()) {
+ for (CommentLinkInfo cl : s.getConfig().getCommentLinkSections()) {
+ String name = cl.name.toLowerCase();
+ if (cl.isOverrideOnly()) {
+ CommentLinkInfo parent = cls.get(name);
+ if (parent == null) {
+ continue; // Ignore invalid overrides.
+ }
+ cls.put(name, cl.inherit(parent));
+ } else {
+ cls.put(name, cl);
+ }
+ }
+ }
+ return ImmutableList.copyOf(cls.values());
+ }
+
+ public ThemeInfo getTheme() {
+ ThemeInfo theme = this.theme;
+ if (theme == null) {
+ synchronized (this) {
+ theme = this.theme;
+ if (theme == null) {
+ theme = loadTheme();
+ this.theme = theme;
+ }
+ }
+ }
+ if (theme == ThemeInfo.INHERIT) {
+ ProjectState parent = Iterables.getFirst(parents(), null);
+ return parent != null ? parent.getTheme() : null;
+ }
+ return theme;
+ }
+
+ private ThemeInfo loadTheme() {
+ String name = getConfig().getProject().getName();
+ File dir = new File(sitePaths.themes_dir, name);
+ if (!dir.exists()) {
+ return ThemeInfo.INHERIT;
+ } else if (!dir.isDirectory()) {
+ log.warn("Bad theme for {}: not a directory", name);
+ return ThemeInfo.INHERIT;
+ }
+ try {
+ return new ThemeInfo(readFile(new File(dir, SitePaths.CSS_FILENAME)),
+ readFile(new File(dir, SitePaths.HEADER_FILENAME)),
+ readFile(new File(dir, SitePaths.FOOTER_FILENAME)));
+ } catch (IOException e) {
+ log.error("Error reading theme for " + name, e);
+ return ThemeInfo.INHERIT;
+ }
+ }
+
+ private String readFile(File f) throws IOException {
+ return f.exists() ? Files.toString(f, Charsets.UTF_8) : null;
+ }
+
private boolean getInheritableBoolean(Function<Project, InheritableBoolean> func) {
for (ProjectState s : tree()) {
switch (func.apply(s.getProject())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java
new file mode 100644
index 0000000000..8362b572e4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.project;
+
+public class ThemeInfo {
+ static final ThemeInfo INHERIT = new ThemeInfo(null, null, null);
+
+ public final String css;
+ public final String header;
+ public final String footer;
+
+ ThemeInfo(String css, String header, String footer) {
+ this.css = css;
+ this.header = header;
+ this.footer = footer;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 7bbb073a6c..e74172e209 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -367,7 +367,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
// If its not an account, maybe its a group?
//
- Collection<GroupReference> suggestions = args.groupBackend.suggest(who);
+ Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
if (!suggestions.isEmpty()) {
HashSet<AccountGroup.UUID> ids = new HashSet<AccountGroup.UUID>();
for (GroupReference ref : suggestions) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index f016c37637..0ea280dfce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -14,21 +14,12 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.MessageRevFilter;
@@ -42,78 +33,31 @@ import java.io.IOException;
* Predicate to match changes that contains specified text in commit messages
* body.
*/
-public class MessagePredicate extends OperatorPredicate<ChangeData> {
+public class MessagePredicate extends RevWalkPredicate {
private static final Logger log =
LoggerFactory.getLogger(MessagePredicate.class);
- private final Provider<ReviewDb> db;
- private final GitRepositoryManager repoManager;
private final RevFilter rFilter;
public MessagePredicate(Provider<ReviewDb> db,
GitRepositoryManager repoManager, String text) {
- super(ChangeQueryBuilder.FIELD_MESSAGE, text);
- this.db = db;
- this.repoManager = repoManager;
+ super(db, repoManager, ChangeQueryBuilder.FIELD_MESSAGE, text);
this.rFilter = MessageRevFilter.create(text);
}
@Override
- public boolean match(ChangeData object) throws OrmException {
- final PatchSet patchSet = object.currentPatchSet(db);
-
- if (patchSet == null) {
- return false;
- }
-
- final RevId revision = patchSet.getRevision();
-
- if (revision == null) {
- return false;
- }
-
- final AnyObjectId objectId = ObjectId.fromString(revision.get());
-
- if (objectId == null) {
- return false;
- }
-
- final Change change = object.change(db);
-
- if (change == null) {
- return false;
- }
-
- final Project.NameKey projectName = change.getProject();
-
- if (projectName == null) {
- return false;
- }
-
+ public boolean match(Repository repo, RevWalk rw, Arguments args) {
try {
- final Repository repo = repoManager.openRepository(projectName);
- try {
- final RevWalk rw = new RevWalk(repo);
- try {
- return rFilter.include(rw, rw.parseCommit(objectId));
- } finally {
- rw.release();
- }
- } finally {
- repo.close();
- }
- } catch (RepositoryNotFoundException e) {
- log.error("Repository \"" + projectName.get() + "\" unknown.", e);
+ return rFilter.include(rw, rw.parseCommit(args.objectId));
} catch (MissingObjectException e) {
- log.error(projectName.get() + "\" commit does not exist.", e);
+ log.error(args.projectName.get() + "\" commit does not exist.", e);
} catch (IncorrectObjectTypeException e) {
- log.error(projectName.get() + "\" revision is not a commit.", e);
+ log.error(args.projectName.get() + "\" revision is not a commit.", e);
} catch (IOException e) {
- log.error("Could not search for commit message in \"" + projectName.get()
- + "\" repository.", e);
+ log.error("Could not search for commit message in \"" +
+ args.projectName.get() + "\" repository.", e);
}
-
return false;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index fc35df3404..06d84c1a7e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -21,10 +21,10 @@ import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.events.ChangeAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.data.QueryStatsAttribute;
import com.google.gerrit.server.events.EventFactory;
-import com.google.gerrit.server.events.PatchSetAttribute;
-import com.google.gerrit.server.events.QueryStats;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -267,7 +267,7 @@ public class QueryProcessor {
}
try {
- final QueryStats stats = new QueryStats();
+ final QueryStatsAttribute stats = new QueryStatsAttribute();
stats.runTimeMilliseconds = System.currentTimeMillis();
List<ChangeData> results = queryChanges(queryString);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
new file mode 100644
index 0000000000..58c0feee9b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * Predicate which creates Repository, RevWalk objects and properly
+ * closes them. Git based operators should extend this predicate.
+ *
+ */
+public abstract class RevWalkPredicate extends OperatorPredicate<ChangeData> {
+ private static final Logger log =
+ LoggerFactory.getLogger(RevWalkPredicate.class);
+
+ public static class Arguments {
+ public final PatchSet patchSet;
+ public final RevId revision;
+ public final AnyObjectId objectId;
+ public final Change change;
+ public final Project.NameKey projectName;
+
+ public Arguments(PatchSet patchSet,
+ RevId revision,
+ AnyObjectId objectId,
+ Change change,
+ Project.NameKey projectName) {
+ this.patchSet = patchSet;
+ this.revision = revision;
+ this.objectId = objectId;
+ this.change = change;
+ this.projectName = projectName;
+ }
+ }
+
+ public final Provider<ReviewDb> db;
+ public final GitRepositoryManager repoManager;
+
+ public RevWalkPredicate(Provider<ReviewDb> db,
+ GitRepositoryManager repoManager, String operator, String ref) {
+ super(operator, ref);
+ this.db = db;
+ this.repoManager = repoManager;
+ }
+
+ @Override
+ public boolean match(ChangeData object) throws OrmException {
+ final PatchSet patchSet = object.currentPatchSet(db);
+ if (patchSet == null) {
+ return false;
+ }
+
+ final RevId revision = patchSet.getRevision();
+ if (revision == null) {
+ return false;
+ }
+
+ final AnyObjectId objectId = ObjectId.fromString(revision.get());
+ if (objectId == null) {
+ return false;
+ }
+
+ Change change = object.change(db);
+ if (change == null) {
+ return false;
+ }
+
+ final Project.NameKey projectName = change.getProject();
+ if (projectName == null) {
+ return false;
+ }
+
+ Arguments args = new Arguments(patchSet, revision, objectId, change, projectName);
+
+ try {
+ final Repository repo = repoManager.openRepository(projectName);
+ try {
+ final RevWalk rw = new RevWalk(repo);
+ try {
+ return match(repo, rw, args);
+ } finally {
+ rw.release();
+ }
+ } finally {
+ repo.close();
+ }
+ } catch (RepositoryNotFoundException e) {
+ log.error("Repository \"" + projectName.get() + "\" unknown.", e);
+ } catch (IOException e) {
+ log.error(projectName.get() + " cannot be read as a repository", e);
+ }
+ return false;
+ }
+
+ public abstract boolean match(Repository repo, RevWalk rw, Arguments args);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
new file mode 100644
index 0000000000..93238a8dfb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -0,0 +1,224 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+/** Creates the {@code All-Projects} repository and initial ACLs. */
+public class AllProjectsCreator {
+ private final GitRepositoryManager mgr;
+ private final AllProjectsName allProjectsName;
+ private final PersonIdent serverUser;
+
+ private GroupReference admin;
+ private GroupReference batch;
+ private GroupReference anonymous;
+ private GroupReference registered;
+ private GroupReference owners;
+
+ @Inject
+ AllProjectsCreator(
+ GitRepositoryManager mgr,
+ AllProjectsName allProjectsName,
+ @GerritPersonIdent PersonIdent serverUser) {
+ this.mgr = mgr;
+ this.allProjectsName = allProjectsName;
+ this.serverUser = serverUser;
+
+ this.anonymous = new GroupReference(
+ AccountGroup.ANONYMOUS_USERS,
+ "Anonymous Users");
+ this.registered = new GroupReference(
+ AccountGroup.REGISTERED_USERS,
+ "Registered Users");
+ this.owners = new GroupReference(
+ AccountGroup.PROJECT_OWNERS,
+ "Project Owners");
+ }
+
+ public AllProjectsCreator setAdministrators(GroupReference admin) {
+ this.admin = admin;
+ return this;
+ }
+
+ public AllProjectsCreator setBatchUsers(GroupReference batch) {
+ this.batch = batch;
+ return this;
+ }
+
+ public void create() throws IOException, ConfigInvalidException {
+ Repository git = null;
+ try {
+ git = mgr.openRepository(allProjectsName);
+ initAllProjects(git);
+ } catch (RepositoryNotFoundException notFound) {
+ // A repository may be missing if this project existed only to store
+ // inheritable permissions. For example 'All-Projects'.
+ try {
+ git = mgr.createRepository(allProjectsName);
+ initAllProjects(git);
+
+ RefUpdate u = git.updateRef(Constants.HEAD);
+ u.link(GitRepositoryManager.REF_CONFIG);
+ } catch (RepositoryNotFoundException err) {
+ String name = allProjectsName.get();
+ throw new IOException("Cannot create repository " + name, err);
+ }
+ } finally {
+ if (git != null) {
+ git.close();
+ }
+ }
+ }
+
+ private void initAllProjects(Repository git)
+ throws IOException, ConfigInvalidException {
+ MetaDataUpdate md = new MetaDataUpdate(
+ GitReferenceUpdated.DISABLED,
+ allProjectsName,
+ git);
+ md.getCommitBuilder().setAuthor(serverUser);
+ md.getCommitBuilder().setCommitter(serverUser);
+ md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
+
+ ProjectConfig config = ProjectConfig.read(md);
+ Project p = config.getProject();
+ p.setDescription("Access inherited by all other projects.");
+ p.setRequireChangeID(InheritableBoolean.TRUE);
+ p.setUseContentMerge(InheritableBoolean.TRUE);
+ p.setUseContributorAgreements(InheritableBoolean.FALSE);
+ p.setUseSignedOffBy(InheritableBoolean.FALSE);
+
+ AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
+ AccessSection all = config.getAccessSection(AccessSection.ALL, true);
+ AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
+ AccessSection tags = config.getAccessSection("refs/tags/*", true);
+ AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
+ AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
+
+ grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
+ grant(config, all, Permission.READ, admin, anonymous);
+
+ if (batch != null) {
+ Permission priority = cap.getPermission(GlobalCapability.PRIORITY, true);
+ PermissionRule r = rule(config, batch);
+ r.setAction(Action.BATCH);
+ priority.add(r);
+
+ Permission stream = cap.getPermission(GlobalCapability.STREAM_EVENTS, true);
+ stream.add(rule(config, batch));
+ }
+
+ LabelType cr = initCodeReviewLabel(config);
+ grant(config, heads, cr, -1, 1, registered);
+ grant(config, heads, cr, -2, 2, admin, owners);
+ grant(config, heads, Permission.CREATE, admin, owners);
+ grant(config, heads, Permission.PUSH, admin, owners);
+ grant(config, heads, Permission.SUBMIT, admin, owners);
+ grant(config, heads, Permission.FORGE_AUTHOR, registered);
+ grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
+ grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners);
+
+ grant(config, tags, Permission.PUSH_TAG, admin, owners);
+ grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners);
+
+ grant(config, magic, Permission.PUSH, registered);
+ grant(config, magic, Permission.PUSH_MERGE, registered);
+
+ meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
+ grant(config, meta, Permission.READ, admin, owners);
+ grant(config, meta, cr, -2, 2, admin, owners);
+ grant(config, meta, Permission.PUSH, admin, owners);
+ grant(config, meta, Permission.SUBMIT, admin, owners);
+
+ config.commit(md);
+ }
+
+ private void grant(ProjectConfig config, AccessSection section,
+ String permission, GroupReference... groupList) {
+ grant(config, section, permission, false, groupList);
+ }
+
+ private void grant(ProjectConfig config, AccessSection section,
+ String permission, boolean force, GroupReference... groupList) {
+ Permission p = section.getPermission(permission, true);
+ for (GroupReference group : groupList) {
+ if (group != null) {
+ PermissionRule r = rule(config, group);
+ r.setForce(force);
+ p.add(r);
+ }
+ }
+ }
+
+ private void grant(ProjectConfig config,
+ AccessSection section, LabelType type,
+ int min, int max, GroupReference... groupList) {
+ String name = Permission.LABEL + type.getName();
+ Permission p = section.getPermission(name, true);
+ for (GroupReference group : groupList) {
+ if (group != null) {
+ PermissionRule r = rule(config, group);
+ r.setRange(min, max);
+ p.add(r);
+ }
+ }
+ }
+
+ private PermissionRule rule(ProjectConfig config, GroupReference group) {
+ return new PermissionRule(config.resolve(group));
+ }
+
+ public static LabelType initCodeReviewLabel(ProjectConfig c) {
+ LabelType type = new LabelType("Code-Review", ImmutableList.of(
+ new LabelValue((short) 2, "Looks good to me, approved"),
+ new LabelValue((short) 1, "Looks good to me, but someone else must approve"),
+ new LabelValue((short) 0, "No score"),
+ new LabelValue((short) -1, "I would prefer that you didn't submit this"),
+ new LabelValue((short) -2, "Do not submit")));
+ type.setAbbreviation("CR");
+ type.setCopyMinScore(true);
+ c.getLabelSections().put(type.getName(), type);
+ return type;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index 4286ba0531..0e89a757be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -14,41 +14,23 @@
package com.google.gerrit.server.schema;
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Version;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
import com.google.gerrit.reviewdb.client.SystemConfig;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.GroupUUID;
-import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.jdbc.JdbcExecutor;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
import java.io.File;
import java.io.IOException;
@@ -59,8 +41,7 @@ public class SchemaCreator {
private final @SitePath
File site_path;
- private final GitRepositoryManager mgr;
- private final AllProjectsName allProjectsName;
+ private final AllProjectsCreator allProjectsCreator;
private final PersonIdent serverUser;
private final DataSourceType dataSourceType;
@@ -70,26 +51,24 @@ public class SchemaCreator {
private AccountGroup anonymous;
private AccountGroup registered;
private AccountGroup owners;
+ private AccountGroup batch;
@Inject
public SchemaCreator(SitePaths site,
@Current SchemaVersion version,
- GitRepositoryManager mgr,
- AllProjectsName allProjectsName,
+ AllProjectsCreator ap,
@GerritPersonIdent PersonIdent au,
DataSourceType dst) {
- this(site.site_path, version, mgr, allProjectsName, au, dst);
+ this(site.site_path, version, ap, au, dst);
}
public SchemaCreator(@SitePath File site,
@Current SchemaVersion version,
- GitRepositoryManager gitMgr,
- AllProjectsName ap,
+ AllProjectsCreator ap,
@GerritPersonIdent PersonIdent au,
DataSourceType dst) {
site_path = site;
- mgr = gitMgr;
- allProjectsName = ap;
+ allProjectsCreator = ap;
serverUser = au;
dataSourceType = dst;
versionNbr = version.getVersionNbr();
@@ -110,7 +89,10 @@ public class SchemaCreator {
db.schemaVersion().insert(Collections.singleton(sVer));
initSystemConfig(db);
- initAllProjects();
+ allProjectsCreator
+ .setAdministrators(GroupReference.forGroup(admin))
+ .setBatchUsers(GroupReference.forGroup(batch))
+ .create();
dataSourceType.getIndexScript().run(db);
}
@@ -151,13 +133,13 @@ public class SchemaCreator {
c.accountGroupNames().insert(
Collections.singleton(new AccountGroupName(registered)));
- final AccountGroup batchUsers = newGroup(c, "Non-Interactive Users", null);
- batchUsers.setDescription("Users who perform batch actions on Gerrit");
- batchUsers.setOwnerGroupUUID(admin.getGroupUUID());
- batchUsers.setType(AccountGroup.Type.INTERNAL);
- c.accountGroups().insert(Collections.singleton(batchUsers));
+ batch = newGroup(c, "Non-Interactive Users", null);
+ batch.setDescription("Users who perform batch actions on Gerrit");
+ batch.setOwnerGroupUUID(admin.getGroupUUID());
+ batch.setType(AccountGroup.Type.INTERNAL);
+ c.accountGroups().insert(Collections.singleton(batch));
c.accountGroupNames().insert(
- Collections.singleton(new AccountGroupName(batchUsers)));
+ Collections.singleton(new AccountGroupName(batch)));
owners = newGroup(c, "Project Owners", AccountGroup.PROJECT_OWNERS);
owners.setDescription("Any owner of the project");
@@ -176,128 +158,4 @@ public class SchemaCreator {
c.systemConfig().insert(Collections.singleton(s));
return s;
}
-
- private void initAllProjects() throws IOException, ConfigInvalidException {
- Repository git = null;
- try {
- git = mgr.openRepository(allProjectsName);
- initAllProjects(git);
- } catch (RepositoryNotFoundException notFound) {
- // A repository may be missing if this project existed only to store
- // inheritable permissions. For example 'All-Projects'.
- try {
- git = mgr.createRepository(allProjectsName);
- initAllProjects(git);
- final RefUpdate u = git.updateRef(Constants.HEAD);
- u.link(GitRepositoryManager.REF_CONFIG);
- } catch (RepositoryNotFoundException err) {
- final String name = allProjectsName.get();
- throw new IOException("Cannot create repository " + name, err);
- }
- } finally {
- if (git != null) {
- git.close();
- }
- }
- }
-
- private void initAllProjects(Repository git) throws IOException,
- ConfigInvalidException {
- MetaDataUpdate md =
- new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git);
- md.getCommitBuilder().setAuthor(serverUser);
- md.getCommitBuilder().setCommitter(serverUser);
-
- ProjectConfig config = ProjectConfig.read(md);
- Project p = config.getProject();
- p.setDescription("Access inherited by all other projects.");
- p.setRequireChangeID(InheritableBoolean.TRUE);
- p.setUseContentMerge(InheritableBoolean.TRUE);
- p.setUseContributorAgreements(InheritableBoolean.FALSE);
- p.setUseSignedOffBy(InheritableBoolean.FALSE);
-
- AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
- AccessSection all = config.getAccessSection(AccessSection.ALL, true);
- AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
- AccessSection tags = config.getAccessSection("refs/tags/*", true);
- AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
- AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
-
- grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
- grant(config, all, Permission.READ, admin, anonymous);
-
- LabelType cr = initCodeReviewLabel(config);
- grant(config, heads, cr, -1, 1, registered);
- grant(config, heads, cr, -2, 2, admin, owners);
- grant(config, heads, Permission.CREATE, admin, owners);
- grant(config, heads, Permission.PUSH, admin, owners);
- grant(config, heads, Permission.SUBMIT, admin, owners);
- grant(config, heads, Permission.FORGE_AUTHOR, registered);
- grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
- grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners);
-
- grant(config, tags, Permission.PUSH_TAG, admin, owners);
- grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners);
-
- grant(config, magic, Permission.PUSH, registered);
- grant(config, magic, Permission.PUSH_MERGE, registered);
-
- meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
- grant(config, meta, Permission.READ, admin, owners);
- grant(config, meta, cr, -2, 2, admin, owners);
- grant(config, meta, Permission.PUSH, admin, owners);
- grant(config, meta, Permission.SUBMIT, admin, owners);
-
- md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
- config.commit(md);
- }
-
- private PermissionRule grant(ProjectConfig config, AccessSection section,
- String permission, AccountGroup group1, AccountGroup... groupList) {
- return grant(config, section, permission, false, group1, groupList);
- }
-
- private PermissionRule grant(ProjectConfig config, AccessSection section,
- String permission, boolean force, AccountGroup group1,
- AccountGroup... groupList) {
- Permission p = section.getPermission(permission, true);
- PermissionRule rule = rule(config, group1);
- rule.setForce(force);
- p.add(rule);
- for (AccountGroup group : groupList) {
- rule = rule(config, group);
- rule.setForce(force);
- p.add(rule);
- }
- return rule;
- }
-
- private void grant(ProjectConfig config,
- AccessSection section, LabelType type,
- int min, int max, AccountGroup... groupList) {
- String name = Permission.LABEL + type.getName();
- Permission p = section.getPermission(name, true);
- for (AccountGroup group : groupList) {
- PermissionRule r = rule(config, group);
- r.setRange(min, max);
- p.add(r);
- }
- }
-
- private PermissionRule rule(ProjectConfig config, AccountGroup group) {
- return new PermissionRule(config.resolve(group));
- }
-
- public static LabelType initCodeReviewLabel(ProjectConfig c) {
- LabelType type = new LabelType("Code-Review", ImmutableList.of(
- new LabelValue((short) 2, "Looks good to me, approved"),
- new LabelValue((short) 1, "Looks good to me, but someone else must approve"),
- new LabelValue((short) 0, "No score"),
- new LabelValue((short) -1, "I would prefer that you didn't submit this"),
- new LabelValue((short) -2, "Do not submit")));
- type.setAbbreviation("CR");
- type.setCopyMinScore(true);
- c.getLabelSections().put(type.getName(), type);
- return type;
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 4de38887f2..53930ac7ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
- public static final Class<Schema_77> C = Schema_77.class;
+ public static final Class<Schema_79> C = Schema_79.class;
public static class Module extends AbstractModule {
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java
new file mode 100644
index 0000000000..18ae8b4ff6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_78.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_78 extends SchemaVersion {
+
+ @Inject
+ Schema_78(Provider<Schema_77> prior) {
+ super(prior);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_79.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_79.java
new file mode 100644
index 0000000000..c5087eab3e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_79.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_79 extends SchemaVersion {
+
+ @Inject
+ Schema_79(Provider<Schema_78> prior) {
+ super(prior);
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index 98b0b4a448..9b122ebf59 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -80,7 +80,7 @@ public class GerritCommonTest extends PrologTestCase {
for (LabelType label : labelTypes.getLabelTypes()) {
config.getLabelSections().put(label.getName(), label);
}
- allProjects = new ProjectState(this, allProjectsName, null,
+ allProjects = new ProjectState(null, this, allProjectsName, null, null,
null, null, null, config);
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java
new file mode 100644
index 0000000000..14e0ebf242
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static org.easymock.EasyMock.anyBoolean;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
+import com.google.gerrit.reviewdb.server.PatchSetAccess;
+import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.change.ChangeJson.ChangeMessageInfo;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Module;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.eclipse.jgit.lib.Config;
+
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+public class ChangeJsonTest extends TestCase {
+
+ public void testFormatChangeMessages() throws OrmException {
+
+ // create mocks
+ final CurrentUser currentUser = createMock(CurrentUser.class);
+ final GitRepositoryManager grm = createMock(GitRepositoryManager.class);
+ final AccountByEmailCache abec = createMock(AccountByEmailCache.class);
+ final AccountCache ac = createMock(AccountCache.class);
+ final AccountInfo.Loader.Factory alf =
+ createMock(AccountInfo.Loader.Factory.class);
+ final CapabilityControl.Factory ccf =
+ createMock(CapabilityControl.Factory.class);
+ final GroupBackend gb = createMock(GroupBackend.class);
+ final Realm r = createMock(Realm.class);
+ final PatchListCache plc = createMock(PatchListCache.class);
+ final ProjectCache pc = createMock(ProjectCache.class);
+ final Config config = new Config(); // unable to mock
+ final ReviewDb rdb = createMock(ReviewDb.class);
+ final ChangeAccess ca = createMock(ChangeAccess.class);
+ final PatchSetAccess psa = createMock(PatchSetAccess.class);
+ final PatchSetApprovalAccess psaa =
+ createMock(PatchSetApprovalAccess.class);
+ final ChangeMessageAccess cma = createMock(ChangeMessageAccess.class);
+ AccountInfo.Loader accountLoader = createMock(AccountInfo.Loader.class);
+
+ // create ChangeJson instance
+ Module mod = new Module() {
+ @Override
+ public void configure(Binder binder) {
+ binder.bind(CurrentUser.class).toInstance(currentUser);
+ binder.bind(GitRepositoryManager.class).toInstance(grm);
+ binder.bind(AccountByEmailCache.class).toInstance(abec);
+ binder.bind(AccountCache.class).toInstance(ac);
+ binder.bind(AccountInfo.Loader.Factory.class).toInstance(alf);
+ binder.bind(CapabilityControl.Factory.class).toInstance(ccf);
+ binder.bind(GroupBackend.class).toInstance(gb);
+ binder.bind(Realm.class).toInstance(r);
+ binder.bind(PatchListCache.class).toInstance(plc);
+ binder.bind(ProjectCache.class).toInstance(pc);
+ binder.bind(ReviewDb.class).toInstance(rdb);
+ binder.bind(Config.class).annotatedWith(GerritServerConfig.class)
+ .toInstance(config);
+ binder.bind(String.class).annotatedWith(CanonicalWebUrl.class)
+ .toInstance("");
+ binder.bind(String.class).annotatedWith(AnonymousCowardName.class)
+ .toInstance("");
+ }
+ };
+ ChangeJson json = Guice.createInjector(mod).getInstance(ChangeJson.class);
+
+ // define mock behavior for tests
+ expect(alf.create(anyBoolean())).andReturn(accountLoader).anyTimes();
+
+ Project.NameKey proj = new Project.NameKey("ProjectNameKey");
+ Branch.NameKey forBranch = new Branch.NameKey(proj, "BranchNameKey");
+
+ Change.Key changeKey123 = new Change.Key("ChangeKey123");
+ Change.Id changeId123 = new Change.Id(123);
+ Change change123 = new Change(changeKey123, changeId123, null, forBranch);
+
+ Change.Key changeKey234 = new Change.Key("ChangeKey234");
+ Change.Id changeId234 = new Change.Id(234);
+ Change change234 = new Change(changeKey234, changeId234, null, forBranch);
+
+ expect(ca.get(Sets.newHashSet(changeId123)))
+ .andAnswer(results(Change.class, change123)).anyTimes();
+ expect(ca.get(changeId123)).andReturn(change123).anyTimes();
+ expect(ca.get(Sets.newHashSet(changeId234)))
+ .andAnswer(results(Change.class, change234));
+ expect(ca.get(changeId234)).andReturn(change234);
+ expect(rdb.changes()).andReturn(ca).anyTimes();
+
+ expect(psa.get(EasyMock.<Iterable<PatchSet.Id>>anyObject()))
+ .andAnswer(results(PatchSet.class)).anyTimes();
+ expect(rdb.patchSets()).andReturn(psa).anyTimes();
+
+ expect(psaa.byPatchSet(anyObject(PatchSet.Id.class)))
+ .andAnswer(results(PatchSetApproval.class)).anyTimes();
+ expect(rdb.patchSetApprovals()).andReturn(psaa).anyTimes();
+
+ expect(currentUser.getStarredChanges())
+ .andReturn(Collections.<Change.Id>emptySet()).anyTimes();
+
+ long timeBase = System.currentTimeMillis();
+ ChangeMessage changeMessage1 =changeMessage(
+ changeId123, "cm1", 111, timeBase, 1111, "first message");
+ ChangeMessage changeMessage2 = changeMessage(
+ changeId123, "cm2", 222, timeBase + 1000, 1111, "second message");
+ expect(cma.byChange(changeId123))
+ .andAnswer(results(ChangeMessage.class, changeMessage2, changeMessage1))
+ .anyTimes();
+ expect(cma.byChange(changeId234)).andAnswer(results(ChangeMessage.class));
+ expect(rdb.changeMessages()).andReturn(cma).anyTimes();
+
+ expect(accountLoader.get(anyObject(Account.Id.class)))
+ .andAnswer(accountForId()).anyTimes();
+ accountLoader.fill();
+ expectLastCall().anyTimes();
+
+ replay(rdb, ca, psa, psaa, alf, currentUser, cma, accountLoader);
+
+ // test 1: messages not returned by default
+ ChangeInfo ci = json.format(new ChangeData(changeId123));
+ assertNull(ci.messages);
+
+ json.addOption(ListChangesOption.MESSAGES);
+
+ // test 2: two change messages, in chronological order
+ ci = json.format(new ChangeData(changeId123));
+ assertNotNull(ci.messages);
+ assertEquals(2, ci.messages.size());
+ Iterator<ChangeMessageInfo> cmis = ci.messages.iterator();
+ assertEquals(changeMessage1, cmis.next());
+ assertEquals(changeMessage2, cmis.next());
+
+ // test 3: no change messages
+ ci = json.format(new ChangeData(changeId234));
+ assertNotNull(ci.messages);
+ assertEquals(0, ci.messages.size());
+ }
+
+ private static IAnswer<AccountInfo> accountForId() {
+ return new IAnswer<AccountInfo>() {
+ @Override
+ public AccountInfo answer() throws Throwable {
+ Account.Id id = (Account.Id) EasyMock.getCurrentArguments()[0];
+ AccountInfo ai = new AccountInfo(id);
+ return ai;
+ }};
+ }
+
+ private static <T> IAnswer<ResultSet<T>> results(Class<T> type, T... items) {
+ final List<T> list = Lists.newArrayList(items);
+ return new IAnswer<ResultSet<T>>() {
+ @Override
+ public ResultSet<T> answer() throws Throwable {
+ return new ListResultSet<T>(list);
+ }};
+ }
+
+ private static void assertEquals(ChangeMessage cm, ChangeMessageInfo cmi) {
+ assertEquals(cm.getPatchSetId().get(), (int) cmi._revisionNumber);
+ assertEquals(cm.getMessage(), cmi.message);
+ assertEquals(cm.getKey().get(), cmi.id);
+ assertEquals(cm.getWrittenOn(), cmi.date);
+ assertNotNull(cmi.author);
+ assertEquals(cm.getAuthor(), cmi.author._id);
+ }
+
+ private static ChangeMessage changeMessage(Change.Id changeId,
+ String uuid, int accountId, long time, int psId, String message) {
+ ChangeMessage.Key key = new ChangeMessage.Key(changeId, uuid);
+ Account.Id author = new Account.Id(accountId);
+ Timestamp updated = new Timestamp(time);
+ PatchSet.Id ps = new PatchSet.Id(changeId, psId);
+ ChangeMessage changeMessage = new ChangeMessage(key, author, updated, ps);
+ changeMessage.setMessage(message);
+ return changeMessage;
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
new file mode 100644
index 0000000000..21d1ce4331
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
@@ -0,0 +1,224 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.change.CommentInfo.Side;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.TypeLiteral;
+
+import junit.framework.TestCase;
+
+import org.easymock.IAnswer;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class CommentsTest extends TestCase {
+
+ private Injector injector;
+ private RevisionResource revRes1;
+ private RevisionResource revRes2;
+ private PatchLineComment plc1;
+ private PatchLineComment plc2;
+ private PatchLineComment plc3;
+
+ @Override
+ protected void setUp() throws Exception {
+ @SuppressWarnings("unchecked")
+ final DynamicMap<RestView<CommentResource>> views =
+ createMock(DynamicMap.class);
+ final TypeLiteral<DynamicMap<RestView<CommentResource>>> viewsType =
+ new TypeLiteral<DynamicMap<RestView<CommentResource>>>() {};
+ final AccountInfo.Loader.Factory alf =
+ createMock(AccountInfo.Loader.Factory.class);
+ final ReviewDb db = createMock(ReviewDb.class);
+
+ AbstractModule mod = new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(viewsType).toInstance(views);
+ bind(AccountInfo.Loader.Factory.class).toInstance(alf);
+ bind(ReviewDb.class).toInstance(db);
+ }};
+
+ Account.Id account1 = new Account.Id(1);
+ Account.Id account2 = new Account.Id(2);
+ AccountInfo.Loader accountLoader = createMock(AccountInfo.Loader.class);
+ accountLoader.fill();
+ expectLastCall().anyTimes();
+ expect(accountLoader.get(account1))
+ .andReturn(new AccountInfo(account1)).anyTimes();
+ expect(accountLoader.get(account2))
+ .andReturn(new AccountInfo(account2)).anyTimes();
+ expect(alf.create(true)).andReturn(accountLoader).anyTimes();
+ replay(accountLoader, alf);
+
+ revRes1 = createMock(RevisionResource.class);
+ revRes2 = createMock(RevisionResource.class);
+
+ PatchLineCommentAccess plca = createMock(PatchLineCommentAccess.class);
+ expect(db.patchComments()).andReturn(plca).anyTimes();
+
+ Change.Id changeId = new Change.Id(123);
+ PatchSet.Id psId1 = new PatchSet.Id(changeId, 1);
+ PatchSet ps1 = new PatchSet(psId1);
+ expect(revRes1.getPatchSet()).andReturn(ps1).anyTimes();
+ PatchSet.Id psId2 = new PatchSet.Id(changeId, 2);
+ PatchSet ps2 = new PatchSet(psId2);
+ expect(revRes2.getPatchSet()).andReturn(ps2);
+
+ long timeBase = System.currentTimeMillis();
+ plc1 = newPatchLineComment(psId1, "Comment1", null,
+ "FileOne.txt", Side.REVISION, 1, account1, timeBase,
+ "First Comment");
+ plc2 = newPatchLineComment(psId1, "Comment2", "Comment1",
+ "FileOne.txt", Side.REVISION, 1, account2, timeBase + 1000,
+ "Reply to First Comment");
+ plc3 = newPatchLineComment(psId1, "Comment3", "Comment1",
+ "FileOne.txt", Side.PARENT, 1, account1, timeBase + 2000,
+ "First Parent Comment");
+
+ expect(plca.publishedByPatchSet(psId1))
+ .andAnswer(results(plc1, plc2, plc3)).anyTimes();
+ expect(plca.publishedByPatchSet(psId2))
+ .andAnswer(results()).anyTimes();
+
+ replay(db, revRes1, revRes2, plca);
+ injector = Guice.createInjector(mod);
+ }
+
+ public void testListComments() throws Exception {
+ // test ListComments for patch set 1
+ assertListComments(injector, revRes1, ImmutableMap.of(
+ "FileOne.txt", Lists.newArrayList(plc3, plc1, plc2)));
+
+ // test ListComments for patch set 2
+ assertListComments(injector, revRes2,
+ Collections.<String, ArrayList<PatchLineComment>>emptyMap());
+ }
+
+ public void testGetComment() throws Exception {
+ // test GetComment for existing comment
+ assertGetComment(injector, revRes1, plc1, plc1.getKey().get());
+
+ // test GetComment for non-existent comment
+ assertGetComment(injector, revRes1, null, "BadComment");
+ }
+
+ private static IAnswer<ResultSet<PatchLineComment>> results(
+ final PatchLineComment... comments) {
+ return new IAnswer<ResultSet<PatchLineComment>>() {
+ @Override
+ public ResultSet<PatchLineComment> answer() throws Throwable {
+ return new ListResultSet<PatchLineComment>(Lists.newArrayList(comments));
+ }};
+ }
+
+ private static void assertGetComment(Injector inj, RevisionResource res,
+ PatchLineComment expected, String uuid) throws Exception {
+ GetComment getComment = inj.getInstance(GetComment.class);
+ Comments comments = inj.getInstance(Comments.class);
+ try {
+ CommentResource commentRes = comments.parse(res, IdString.fromUrl(uuid));
+ if (expected == null) {
+ fail("Expected no comment");
+ }
+ CommentInfo actual = (CommentInfo) getComment.apply(commentRes);
+ assertComment(expected, actual);
+ } catch (ResourceNotFoundException e) {
+ if (expected != null) {
+ fail("Expected to find comment");
+ }
+ }
+ }
+
+ private static void assertListComments(Injector inj, RevisionResource res,
+ Map<String, ArrayList<PatchLineComment>> expected) throws Exception {
+ Comments comments = inj.getInstance(Comments.class);
+ RestReadView<RevisionResource> listView =
+ (RestReadView<RevisionResource>) comments.list();
+ @SuppressWarnings("unchecked")
+ Map<String, List<CommentInfo>> actual =
+ (Map<String, List<CommentInfo>>) listView.apply(res);
+ assertNotNull(actual);
+ assertEquals(expected.size(), actual.size());
+ assertEquals(expected.keySet(), actual.keySet());
+ for (String filename : expected.keySet()) {
+ List<PatchLineComment> expectedComments = expected.get(filename);
+ List<CommentInfo> actualComments = actual.get(filename);
+ assertNotNull(actualComments);
+ assertEquals(expectedComments.size(), actualComments.size());
+ for (int i = 0; i < expectedComments.size(); i++) {
+ assertComment(expectedComments.get(i), actualComments.get(i));
+ }
+ }
+ }
+
+ private static void assertComment(PatchLineComment plc, CommentInfo ci) {
+ assertEquals(plc.getKey().get(), ci.id);
+ assertEquals(plc.getParentUuid(), ci.inReplyTo);
+ assertEquals("gerritcodereview#comment", ci.kind);
+ assertEquals(plc.getMessage(), ci.message);
+ assertNotNull(ci.author);
+ assertEquals(plc.getAuthor(), ci.author._id);
+ assertEquals(plc.getLine(), (int) ci.line);
+ assertEquals(plc.getSide() == 0 ? Side.PARENT : Side.REVISION,
+ Objects.firstNonNull(ci.side, Side.REVISION));
+ assertEquals(plc.getWrittenOn(), ci.updated);
+ }
+
+ private static PatchLineComment newPatchLineComment(PatchSet.Id psId,
+ String uuid, String inReplyToUuid, String filename, Side side, int line,
+ Account.Id authorId, long millis, String message) {
+ Patch.Key p = new Patch.Key(psId, filename);
+ PatchLineComment.Key id = new PatchLineComment.Key(p, uuid);
+ PatchLineComment plc =
+ new PatchLineComment(id, line, authorId, inReplyToUuid);
+ plc.setMessage(message);
+ plc.setSide(side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
+ plc.setStatus(Status.PUBLISHED);
+ plc.setWrittenOn(new Timestamp(millis));
+ return plc;
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 51ea5a2725..f1bb7de558 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -540,11 +540,11 @@ public class RefControlTest extends TestCase {
ProjectControl.AssistedFactory projectControlFactory = null;
RulesCache rulesCache = null;
all.put(local.getProject().getNameKey(), new ProjectState(
- projectCache, allProjectsName, projectControlFactory,
- envFactory, mgr, rulesCache, local));
+ null, projectCache, allProjectsName, projectControlFactory,
+ envFactory, mgr, rulesCache, null, local));
all.put(parent.getProject().getNameKey(), new ProjectState(
- projectCache, allProjectsName, projectControlFactory,
- envFactory, mgr, rulesCache, parent));
+ null, projectCache, allProjectsName, projectControlFactory,
+ envFactory, mgr, rulesCache, null, parent));
return all.get(local.getProject().getNameKey());
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
index d9b237b38e..b517de7633 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
@@ -52,16 +52,22 @@ package com.google.gerrit.server.tools.hooks;
import static org.junit.Assert.fail;
+import com.google.common.io.ByteStreams;
+
import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
import org.eclipse.jgit.lib.Repository;
+import org.junit.After;
import org.junit.Before;
import java.io.File;
-import java.net.URISyntaxException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
import java.net.URL;
public abstract class HookTestCase extends LocalDiskRepositoryTestCase {
protected Repository repository;
+ private File hooksh;
@Override
@Before
@@ -70,22 +76,48 @@ public abstract class HookTestCase extends LocalDiskRepositoryTestCase {
repository = createWorkRepository();
}
- protected File getHook(final String name) {
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ if (hooksh != null) {
+ if (!hooksh.delete()) {
+ hooksh.deleteOnExit();
+ }
+ hooksh = null;
+ }
+ }
+
+ protected File getHook(final String name) throws IOException {
final String scproot = "com/google/gerrit/server/tools/root";
final String path = scproot + "/hooks/" + name;
- final URL url = cl().getResource(path);
+ URL url = cl().getResource(path);
if (url == null) {
fail("Cannot locate " + path + " in CLASSPATH");
}
File hook;
- try {
- hook = new File(url.toURI());
- } catch (URISyntaxException e) {
+ if ("file".equals(url.getProtocol())) {
hook = new File(url.getPath());
- }
- if (!hook.isFile()) {
- fail("Cannot locate " + path + " in CLASSPATH");
+ if (!hook.isFile()) {
+ fail("Cannot locate " + path + " in CLASSPATH");
+ }
+ } else if ("jar".equals(url.getProtocol())) {
+ hooksh = File.createTempFile("hook_", ".sh");
+ InputStream in = url.openStream();
+ try {
+ FileOutputStream out = new FileOutputStream(hooksh);
+ try {
+ ByteStreams.copy(in, out);
+ } finally {
+ out.close();
+ }
+ } finally {
+ in.close();
+ }
+ hook = hooksh;
+ } else {
+ fail("Cannot invoke " + url);
+ hook = null;
}
// The hook was copied out of our source control system into the
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index f026920f8c..75a43c0159 100644
--- a/gerrit-sshd/pom.xml
+++ b/gerrit-sshd/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-sshd</artifactId>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 83bc8a5327..8dc3f2c3e9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -15,18 +15,14 @@
package com.google.gerrit.sshd;
import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.sshd.SshScope.Context;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.commons.codec.binary.Base64;
-import org.apache.mina.core.future.IoFuture;
-import org.apache.mina.core.future.IoFutureListener;
import org.apache.sshd.common.KeyPairProvider;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.util.Buffer;
@@ -104,7 +100,7 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
if (myHostKeys.contains(suppliedKey)
|| getPeerKeys().contains(suppliedKey)) {
PeerDaemonUser user = peerFactory.create(sd.getRemoteAddress());
- return success(username, session, sd, user);
+ return SshUtil.success(username, session, sshScope, sshLog, sd, user);
} else {
sd.authenticationError(username, "no-matching-key");
@@ -144,12 +140,14 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
}
}
- if (!createUser(sd, key).getAccount().isActive()) {
+ if (!SshUtil.createUser(sd, userFactory, key.getAccount())
+ .getAccount().isActive()) {
sd.authenticationError(username, "inactive-account");
return false;
}
- return success(username, session, sd, createUser(sd, key));
+ return SshUtil.success(username, session, sshScope, sshLog, sd,
+ SshUtil.createUser(sd, userFactory, key.getAccount()));
}
private Set<PublicKey> getPeerKeys() {
@@ -161,46 +159,6 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
return p.keys;
}
- private boolean success(final String username, final ServerSession session,
- final SshSession sd, final CurrentUser user) {
- if (sd.getCurrentUser() == null) {
- sd.authenticationSuccess(username, user);
-
- // If this is the first time we've authenticated this
- // session, record a login event in the log and add
- // a close listener to record a logout event.
- //
- Context ctx = sshScope.newContext(null, sd, null);
- Context old = sshScope.set(ctx);
- try {
- sshLog.onLogin();
- } finally {
- sshScope.set(old);
- }
-
- session.getIoSession().getCloseFuture().addListener(
- new IoFutureListener<IoFuture>() {
- @Override
- public void operationComplete(IoFuture future) {
- final Context ctx = sshScope.newContext(null, sd, null);
- final Context old = sshScope.set(ctx);
- try {
- sshLog.onLogout();
- } finally {
- sshScope.set(old);
- }
- }
- });
- }
-
- return true;
- }
-
- private IdentifiedUser createUser(final SshSession sd,
- final SshKeyCacheEntry key) {
- return userFactory.create(sd.getRemoteAddress(), key.getAccount());
- }
-
private SshKeyCacheEntry find(final Iterable<SshKeyCacheEntry> keyList,
final PublicKey suppliedKey) {
for (final SshKeyCacheEntry k : keyList) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java
new file mode 100644
index 0000000000..17db6b92fa
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2013 Goldman Sachs
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.sshd.server.auth.gss.GSSAuthenticator;
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * Authenticates users with kerberos (gssapi-with-mic).
+ */
+@Singleton
+class GerritGSSAuthenticator extends GSSAuthenticator {
+ private final AccountCache accounts;
+ private final SshScope sshScope;
+ private final SshLog sshLog;
+ private final GenericFactory userFactory;
+
+ @Inject
+ GerritGSSAuthenticator(final AccountCache accounts, final SshScope sshScope,
+ final SshLog sshLog, final IdentifiedUser.GenericFactory userFactory) {
+ this.accounts = accounts;
+ this.sshScope = sshScope;
+ this.sshLog = sshLog;
+ this.userFactory = userFactory;
+ }
+
+ @Override
+ public boolean validateIdentity(final ServerSession session,
+ final String identity) {
+ final SshSession sd = session.getAttribute(SshSession.KEY);
+ int at = identity.indexOf('@');
+ String username;
+ if (at == -1) {
+ username = identity;
+ } else {
+ username = identity.substring(0, at);
+ }
+ AccountState state = accounts.getByUsername(username);
+ Account account = state == null ? null : state.getAccount();
+ boolean active = account != null && account.isActive();
+ if (active) {
+ return SshUtil.success(username, session, sshScope, sshLog, sd,
+ SshUtil.createUser(sd, userFactory, account.getId()));
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index c43de60d7f..8519e946f9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.server.ssh.SshAddressesModule.IANA_SSH_PORT;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.Version;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.ConfigUtil;
@@ -75,6 +76,8 @@ import org.apache.sshd.server.PublickeyAuthenticator;
import org.apache.sshd.server.SshFile;
import org.apache.sshd.server.UserAuth;
import org.apache.sshd.server.auth.UserAuthPublicKey;
+import org.apache.sshd.server.auth.gss.GSSAuthenticator;
+import org.apache.sshd.server.auth.gss.UserAuthGSS;
import org.apache.sshd.server.channel.ChannelDirectTcpip;
import org.apache.sshd.server.channel.ChannelSession;
import org.apache.sshd.server.kex.DHG1;
@@ -85,9 +88,12 @@ import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
import java.io.IOException;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
+import java.net.UnknownHostException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.PublicKey;
@@ -129,6 +135,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
@Inject
SshDaemon(final CommandFactory commandFactory, final NoShell noShell,
final PublickeyAuthenticator userAuth,
+ final GerritGSSAuthenticator kerberosAuth,
final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
@GerritServerConfig final Config cfg, final SshLog sshLog,
@SshListenAddresses final List<SocketAddress> listen,
@@ -171,6 +178,11 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
String.valueOf(maxConnectionsPerUser));
}
+ final String kerberosKeytab = cfg.getString(
+ "sshd", null, "kerberosKeytab");
+ final String kerberosPrincipal = cfg.getString(
+ "sshd", null, "kerberosPrincipal");
+
if (SecurityUtils.isBouncyCastleRegistered()) {
initProviderBouncyCastle();
} else {
@@ -184,7 +196,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
initFileSystemFactory();
initSubsystems();
initCompression();
- initUserAuth(userAuth);
+ initUserAuth(userAuth, kerberosAuth, kerberosKeytab, kerberosPrincipal);
setKeyPairProvider(hostKeyProvider);
setCommandFactory(commandFactory);
setShellFactory(noShell);
@@ -469,10 +481,36 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
setSubsystemFactories(Collections.<NamedFactory<Command>> emptyList());
}
- @SuppressWarnings("unchecked")
- private void initUserAuth(final PublickeyAuthenticator pubkey) {
- setUserAuthFactories(Arrays
- .<NamedFactory<UserAuth>> asList(new UserAuthPublicKey.Factory()));
+ private void initUserAuth(final PublickeyAuthenticator pubkey,
+ final GSSAuthenticator kerberosAuthenticator,
+ String kerberosKeytab, String kerberosPrincipal) {
+ List<NamedFactory<UserAuth>> authFactories = Lists.newArrayList();
+ if (kerberosKeytab != null) {
+ authFactories.add(new UserAuthGSS.Factory());
+ log.info("Enabling kerberos with keytab " + kerberosKeytab);
+ if (!new File(kerberosKeytab).canRead()) {
+ log.error("Keytab " + kerberosKeytab +
+ " does not exist or is not readable; further errors are possible");
+ }
+ kerberosAuthenticator.setKeytabFile(kerberosKeytab);
+ if (kerberosPrincipal == null) {
+ try {
+ kerberosPrincipal = "host/" +
+ InetAddress.getLocalHost().getCanonicalHostName();
+ } catch(UnknownHostException e) {
+ kerberosPrincipal = "host/localhost";
+ }
+ }
+ log.info("Using kerberos principal " + kerberosPrincipal);
+ if (!kerberosPrincipal.startsWith("host/")) {
+ log.warn("Host principal does not start with host/ " +
+ "which most SSH clients will supply automatically");
+ }
+ kerberosAuthenticator.setServicePrincipalName(kerberosPrincipal);
+ setGSSAuthenticator(kerberosAuthenticator);
+ }
+ authFactories.add(new UserAuthPublicKey.Factory());
+ setUserAuthFactories(authFactories);
setPublickeyAuthenticator(pubkey);
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 6fad42bcda..2e42c23e7a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -42,6 +42,7 @@ import com.google.inject.servlet.RequestScoped;
import org.apache.sshd.common.KeyPairProvider;
import org.apache.sshd.server.CommandFactory;
import org.apache.sshd.server.PublickeyAuthenticator;
+import org.apache.sshd.server.auth.gss.GSSAuthenticator;
import org.eclipse.jgit.lib.Config;
import java.net.SocketAddress;
@@ -84,6 +85,7 @@ public class SshModule extends FactoryModule {
.toProvider(StreamCommandExecutorProvider.class).in(SINGLETON);
bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
+ bind(GSSAuthenticator.class).to(GerritGSSAuthenticator.class);
bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
index da245a34ac..6a4d995be9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
@@ -14,12 +14,19 @@
package com.google.gerrit.sshd;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.sshd.SshScope.Context;
import org.apache.commons.codec.binary.Base64;
+import org.apache.mina.core.future.IoFuture;
+import org.apache.mina.core.future.IoFutureListener;
import org.apache.sshd.common.KeyPairProvider;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.util.Buffer;
+import org.apache.sshd.server.session.ServerSession;
import org.eclipse.jgit.lib.Constants;
import java.io.BufferedReader;
@@ -112,4 +119,46 @@ public class SshUtil {
return keyStr;
}
}
+
+ public static boolean success(final String username, final ServerSession session,
+ final SshScope sshScope, final SshLog sshLog,
+ final SshSession sd, final CurrentUser user) {
+ if (sd.getCurrentUser() == null) {
+ sd.authenticationSuccess(username, user);
+
+ // If this is the first time we've authenticated this
+ // session, record a login event in the log and add
+ // a close listener to record a logout event.
+ //
+ Context ctx = sshScope.newContext(null, sd, null);
+ Context old = sshScope.set(ctx);
+ try {
+ sshLog.onLogin();
+ } finally {
+ sshScope.set(old);
+ }
+
+ session.getIoSession().getCloseFuture().addListener(
+ new IoFutureListener<IoFuture>() {
+ @Override
+ public void operationComplete(IoFuture future) {
+ final Context ctx = sshScope.newContext(null, sd, null);
+ final Context old = sshScope.set(ctx);
+ try {
+ sshLog.onLogout();
+ } finally {
+ sshScope.set(old);
+ }
+ }
+ });
+ }
+
+ return true;
+ }
+
+ public static IdentifiedUser createUser(final SshSession sd,
+ final IdentifiedUser.GenericFactory userFactory,
+ final Account.Id account) {
+ return userFactory.create(sd.getRemoteAddress(), account);
+ }
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index a3e9d6ef23..c938891543 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -136,6 +136,13 @@ final class SetAccountCommand extends BaseCommand {
boolean accountUpdated = false;
boolean sshKeysUpdated = false;
+ ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
+ for (AccountExternalId extId : ids) {
+ if (extId.isScheme(AccountExternalId.SCHEME_USERNAME)) {
+ account.setUserName(extId.getSchemeRest());
+ }
+ }
+
for (String email : addEmails) {
link(id, email);
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index 1b81a47862..99d4baaee6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -16,6 +16,8 @@ package com.google.gerrit.sshd.commands;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.events.ChangeEvent;
import com.google.gerrit.server.git.WorkQueue;
@@ -33,6 +35,7 @@ import java.io.PrintWriter;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
+@RequiresCapability(GlobalCapability.STREAM_EVENTS)
@CommandMetaData(name = "stream-events", descr = "Monitor events occurring in real time")
final class StreamEvents extends BaseCommand {
/** Maximum number of events that may be queued up for each connection. */
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index 6acf27467d..f2325a490d 100644
--- a/gerrit-util-cli/pom.xml
+++ b/gerrit-util-cli/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-util-cli</artifactId>
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 04744aa4d5..1e941e0d88 100644
--- a/gerrit-util-ssl/pom.xml
+++ b/gerrit-util-ssl/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 690860d5f9..0855b7dd35 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
</parent>
<artifactId>gerrit-war</artifactId>
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
-Subproject a2aaf1a0151959e69e8f578d2f60f6a608052e9
+Subproject 74df0dc1e7704224645718c130548fecc7c5549
diff --git a/plugins/replication b/plugins/replication
-Subproject e7b4bce71dabfe1cc2115b00791607e68d61acb
+Subproject 26b0185a6e70c5ccf8b3444fea0014168067f0e
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
-Subproject c868c1fcc9fa5d1216ecc2cee24b913df750ccd
+Subproject 1490f20e92303da4998335a0198c75c2e5d3872
diff --git a/pom.xml b/pom.xml
index df0096ded9..f7798bd79c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@ limitations under the License.
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
<packaging>pom</packaging>
- <version>2.6</version>
+ <version>2.7-SNAPSHOT</version>
<name>Gerrit Code Review - Parent</name>
<url>http://code.google.com/p/gerrit/</url>
@@ -49,7 +49,6 @@ limitations under the License.
<jgitVersion>2.3.1.201302201838-r.209-g18030f9</jgitVersion>
<gwtormVersion>1.6</gwtormVersion>
<gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
- <gwtexpuiVersion>1.3.2</gwtexpuiVersion>
<gwtVersion>2.5.0</gwtVersion>
<bouncyCastleVersion>140</bouncyCastleVersion>
<slf4jVersion>1.6.1</slf4jVersion>
@@ -77,6 +76,7 @@ limitations under the License.
<module>gerrit-common</module>
<module>gerrit-cache-h2</module>
<module>gerrit-httpd</module>
+ <module>gerrit-gwtexpui</module>
<module>gerrit-gwtui</module>
<module>gerrit-launcher</module>
<module>gerrit-main</module>
@@ -525,18 +525,6 @@ limitations under the License.
</dependency>
<dependency>
- <groupId>gwtexpui</groupId>
- <artifactId>gwtexpui</artifactId>
- <version>${gwtexpuiVersion}</version>
- </dependency>
- <dependency>
- <groupId>gwtexpui</groupId>
- <artifactId>gwtexpui</artifactId>
- <version>${gwtexpuiVersion}</version>
- <classifier>sources</classifier>
- </dependency>
-
- <dependency>
<groupId>org.openid4java</groupId>
<artifactId>openid4java</artifactId>
<version>0.9.8</version>
@@ -910,20 +898,5 @@ limitations under the License.
<id>jgit-repository</id>
<url>http://download.eclipse.org/jgit/maven</url>
</repository>
-
- <repository>
- <id>java.net-repository</id>
- <url>http://download.java.net/maven/2/</url>
- </repository>
-
- <repository>
- <id>clojars-repo</id>
- <url>http://clojars.org/repo</url>
- </repository>
-
- <repository>
- <id>scala-tools</id>
- <url>http://scala-tools.org/repo-releases</url>
- </repository>
</repositories>
</project>
diff --git a/tools/gwtui_dbg.launch b/tools/gwtui_dbg.launch
index f007da40fa..8a873be52a 100644
--- a/tools/gwtui_dbg.launch
+++ b/tools/gwtui_dbg.launch
@@ -25,7 +25,7 @@
<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-common/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-gwtui/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER&quot; path=&quot;3&quot; type=&quot;4&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtexpui/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-gwtexpui/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtjsonrpc/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtorm/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-gwtui/target/classes&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
diff --git a/tools/release.sh b/tools/release.sh
index 88e4a00763..b8f3156060 100755
--- a/tools/release.sh
+++ b/tools/release.sh
@@ -15,7 +15,7 @@ do
;;
--no-tests|--without-tests)
flags="$flags -Dgerrit.acceptance-tests.skip=true"
- flags="$flags -Dmaven.tests.skip=true"
+ flags="$flags -Dmaven.test.skip=true"
shift
;;
*)